在以往的编码中,使用过 spring-data-jpa
,也用过 hibernate
和 mybatis
。在简单的数据库操做中,spring-data-jpa
是用起来最爽的,毕竟在 IntelliJ IDEA
中能够得到以下体验:java
瞧瞧,实体类属性推导,查询条件推导。声明完接口就能够用了,一行sql
都不用敲,多爽 : Pmysql
在这里就不讨论这三个框架的优劣了,毕竟就我目前的使用场景而言,也体会不太出来到底谁好用...毕竟复杂的SQL
查询都是要 相似hql
或者XML
的解决方案来作的。spring
本着挖坑学习的精神,今天开始会试着一步一步作出一个本身的数据库帮助库 (不敢叫框架,毕竟#行业标准里太多 feature,实力不够,作不来 ORZ).sql
今天就作个雏形吧,雏形的意思就是:看起来好像完成了一些功能,但只是实验性得编码 : P数据库
这个帮助库就命名为 ice
吧,请原谅 起名字困难症 ORZsegmentfault
这是一个 笔记 类型的文章,全部可能会有一些 啊 写到这里才想起来 这样的状况...缓存
本文只引用
mysql-connecter
和lombok
这两个包。
前者是数据库驱动,因为这是个挖坑性质的东西,因此只针对MYSQL
作功能了;
后者是代码生成框架,挺好用的,强烈推荐mybatis也就是说,
ice
并不使用常见的数据库链接池,好比druid
、cp30
。而是本身实现一个缓存链接获取器
,毕竟挖坑就挖深点嘛哈哈。
本文假定读者具有必定的 Java 能力,好比 反射、代理 这两个点,有兴趣能够看看我以前的文章。app
Configuration
用过前边所说的三个框架的同窗确定配过配置文件对吧,我通常配合 spring-boot
使用 spring-data-jpa
,因此在 application.properties
配置;其余两个框架则是在传统的 SSH
、SSM
环境下配置 application-*.xml
。框架
既然是雏形,那么 ice
前期就直接 code-based configuration
了 (才不是偷懒...)
/** * Created by krun on 2017/9/22. */ @Builder @Data @NoArgsConstructor @AllArgsConstructor public class Configuration { private String driverClass; //驱动类名 private String connectionURL; //链接url private String username; //数据库用户名 private String password; //数据库密码 }
好,配置就搞定啦,毕竟常见的链接参数均可以直接在 connectionURL
中附带嘛。
ConnectionProvider
/** * Created by krun on 2017/9/22. */ public class ConnectionProvider{ /** * 不直接用构造器而是用这种方式获取实例,纯粹是我我的喜爱,感受这样更有 "经过配置获得" 的意思。 */ public static CachedConnection configure (Configuration configuration) { return new CachedConnection(configuration); } private Class driverClass = null; private Configuration configuration; private volatile Connection connection; private CachedConnection (Configuration configuration) { this.configuration = configuration; try { // 加载驱动 this.driverClass = Class.forName(this.configuration.getDriverClass( )); } catch (ClassNotFoundException e) { throw new RuntimeException("没法加载 JDBC 驱动: " + this.configuration.getDriverClass( )); } } // 内部用来获取一个新链接 private synchronized Connection create ( ) { // 检查是否已经加载驱动,没有的话抛出异常。 if (driverClass == null) { throw new RuntimeException("还没有加载 JDBC 驱动."); } else { try { // 获取一个新链接 return DriverManager.getConnection(this.configuration.getConnectionURL( ), this.configuration.getUsername( ), this.configuration.getPassword( )); } catch (SQLException e) { throw new RuntimeException(e); } } } // 暴露给外界获取一个链接,在这里进行 "是否有可用链接" 和 "链接有效性检查" public synchronized Connection provide( ) throws SQLException { if (connection == null) { connection = createConnection( ); } else if (connection.isClosed( )) { connection = createConnection( ); } return connection; } }
Repository
这个彻底是受 spring-data-jpa
的影响,我以为"方法映射数据库操做"的映射方式是最吼的,只是 JPA
的接口更简洁些。
/** * Created by krun on 2017/9/22. */ public interface Repository<E, I extends Serializable> { List<E> findAll(); //获取表内全部元素 E save(E e); //保存元素,当元素存在id时,尝试更新(update);不存在id时,尝试插入(insert) long delete(E e); //删除元素 boolean exist(E e); //判断给定元素是否存在 }
考虑到实现难度,如今不打算作"方法名解析到sql语句"。所以仍是直接引入一个 @Query
注解来设置方法对应的 SQL
操做:
/** * Created by krun on 2017/9/22. */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface Query { // 暂时也不作别名处理了 String value(); }
约定 @Query
注解中的 SQL 语句使用 %s
占位符指明表名(这由 Repository<E, I> 中 E 解析而来),用 ?
占位符指定参数,这是为了方便直接把语句放入PreparedStatement
使用。
那么结合一下,如今的模板应该是这样的:
/** * Created by krun on 2017/9/22. */ public interface Repository<E, I extends Serializable> { @Query("SELECT * FROM %s") List<E> findAll(); ... }
RepositoryFactory
如今用户能够继承 Repository
接口来声明一个指定实体类的 repository
,咱们须要一个工厂类来为这些接口类建立代理对象(Proxy
)以注入咱们的方法拦截器。
/** * Created by krun on 2017/9/22. */ public class RepositoryFactory { //全局工厂的名字 private static final String GLOBAL_FACTORY = "GLOBAL"; //用来保存给定名称和其对应的工厂实例 private static final LinkedHashMap<String, RepositoryFactory> factoryMap; static { factoryMap = new LinkedHashMap<>(); } // 这与以前 Connection.configure 的写法同样,纯粹我的喜爱。 public static RepositoryFactory configure(Configuration configure) { return RepositoryFactory.configure(GLOBAL_FACTORY, configure); } public static RepositoryFactory configure(String name, Configuration configure) { if (RepositoryFactory.factoryMap.get(name) == null) { synchronized ( RepositoryFactory.factoryMap ) { if (RepositoryFactory.factoryMap.get(name) == null) { RepositoryFactory.factoryMap.put(name, new RepositoryFactory(ConnectionProvider.configure(configure))); } else { throw new RuntimeException(name + " 的工厂已经被初始化了,不能再对其进行配置。"); } } } return RepositoryFactory.factoryMap.get(name); } public synchronized static RepositoryFactory get() { return RepositoryFactory.get(GLOBAL_FACTORY); } public synchronized static RepositoryFactory get(String name) { return RepositoryFactory.factoryMap.get(name); } // 每一个工厂类实例都持有一个本身的 链接提供者,由于多数状况下全局只会有一个工厂类实例... @Getter private ConnectionProvider connectionProvider; //用于保存每一个工厂实例所建立的 repository 实例,用以复用,避免重复建立 repository 实例。 private final LinkedHashMap<Class<? extends Repository>, Repository> repositoryMap; private RepositoryFactory(ConnectionProvider connectionProvider) { this.connectionProvider = connectionProvider; this.repositoryMap = new LinkedHashMap<>(); } // 为 Repository 接口建立代理实例,并注入咱们本身的方法拦截器:RepositoryInvocationHandler @SuppressWarnings("unchecked") private <E, I extends Serializable, T extends Repository<E, I>> T getProxy(Class<T> repositoryClass) { return (T) Proxy.newProxyInstance(repositoryClass.getClassLoader(), new Class[] {repositoryClass}, new RepositoryInvocationHandler(this, repositoryClass)); } // 获取给定 repository 类型的代理实例 @SuppressWarnings("unchecked") public <E, I extends Serializable, T extends Repository<E, I>> T getRepository(Class<T> repositoryClass) { T repository; if ((repository = (T) repositoryMap.get(repositoryClass)) == null) { synchronized ( repositoryMap ) { if ((repository = (T) repositoryMap.get(repositoryClass)) == null) { repository = getProxy(repositoryClass); repositoryMap.put(repositoryClass, repository); } } } return repository; } }
RepositoryInvocationHandler
咱们刚才在 RepositoryFactory.getProxy
中建立了一个RepositoryInvocationHandler
实例,并传入了RepositoryFactory
实例以及代理的Repository
类型。
这由于在方法拦截器中,咱们须要获取一些东西:
connection
/** * Created by krun on 2017/9/22. */ public class RepositoryInvocationHandler implements InvocationHandler { private RepositoryFactory factory; //用于保存repository的泛型信息,后面能够比较方便地获取,虽然也能够经过 "method.getDeclaringClass()" 来获取,但总以为麻烦了些。 private Class<? extends Repository> invokeRepositoryClass; public RepositoryInvocationHandler (RepositoryFactory factory, Class<? extends Repository> invokeRepositoryClass) { this.factory = factory; this.invokeRepositoryClass = invokeRepositoryClass; } public Object invoke (Object proxy, Method method, Object[] args) throws Throwable { String methodName = method.getName( ); // 根据方法名选择合适的 handle方法,之后应该是要改为表驱动,否则太多 if-else 了 ORZ // 提及来,表驱动的话,就有合适的地方暴露接口给用户修改方法映射逻辑了。 if (methodName.startsWith("find")) { return handleFind(method, args); } else if (methodName.startsWith("save")) { } else if (methodName.startsWith("delete")) { } else if (methodName.startsWith("exist")) { } return null; } // 经过保存的 invokeRepositoryClass 获取其持有的泛型信息 private String getEntityName () { if (! Repository.class.isAssignableFrom(this.invokeRepositoryClass)) { throw new RuntimeException(String.format("接口 [%s] 并无继承 Repository", this.invokeRepositoryClass.getName( ))); } // 这里没有作太多考虑,暂时没遇到问题而已... ParameterizedType parameterizedType = (ParameterizedType) this.invokeRepositoryClass.getGenericInterfaces()[0]; return ((Class)parameterizedType.getActualTypeArguments()[0]).getSimpleName().toLowerCase(); } @SuppressWarnings("unchecked") private Object handleFind (Method method, Object... args) { // 获取方法上的 @Query 注解 Query query = method.getAnnotation(Query.class); if (query == null) { throw new IllegalArgumentException("也许你忘了为 " + method.getDeclaringClass( ).getSimpleName( ) + "." + method.getName( ) + "() 设置 @Query 注解"); } // java 7的 "try-with-resource" 语法糖,挺方便的,不用操心 connection 关没关了 // 忽然想起来,这样写的话好像... ConnectionProvider 就没用了啊 ... ORZ try (Connection connection = factory.getConnectionProvider().provide()) { PreparedStatement preparedStatement = (PreparedStatement) connection //简单得替换一下表名占位符 .prepareStatement(String.format(query.value(), getEntityName())); // 粗暴得把参数都塞进去... // 之后估计要作个 switch-case 把参数类型检查作一下 for (int i = 1; i <= args.length; i++) { preparedStatement.setObject(i, args[i - 1]); } System.out.println(preparedStatement.asSql()); // 把结果打出来看看 ResultSet resultSet = preparedStatement.executeQuery(); ResultSetMetaData metaData = resultSet.getMetaData(); while (resultSet.next()) { for (int i = 1; i <= metaData.getColumnCount(); i++) { System.out.print(String.valueOf(resultSet.getObject(i)) + "\t"); } System.out.println(); } resultSet.close(); } catch (SQLException e) { throw new RuntimeException(e); } // 一样的简单粗暴,只为了看效果哈哈 try { // 注:这种写法在 "List<Student> findAll()" 这种状况会报错,由于 List 是接口,没法为其建立实例 return method.getReturnType().newInstance(); } catch (InstantiationException | IllegalAccessException e) { e.printStackTrace( ); } return null; } }
/** * Created by krun on 2017/9/22. */ public class App { @Data public static class Student { private String id; private String name; } interface StudentRepository extends Repository<Student, String> { @Query("SELECT * FROM %s WHERE gender = ?") List<Student> findByGender(String gender); @Query("SELECT * FROM %s WHERE id > ?") List<Student> findByIdAfter(String id); @Query("SELECT * FROM %s WHERE name = ?") Student findByName(String name); } public static void main(String[] args ) { RepositoryFactory factory = RepositoryFactory.configure(Configuration.builder() .driverClass("com.mysql.jdbc.Driver") .connectionURL("jdbc:mysql://localhost:3306/hsc") .username("gdpi") .password("gdpi") .build()); StudentRepository studentRepository = factory.getRepository(StudentRepository .class); studentRepository .findByName("krun"); } } > SELECT * FROM student WHERE name = 'krun' > 20152200000 计算机技术系 男 2015 软件技术 krun