网站建设的系统分析,上海 网站开发 兼职,农产品现货交易平台,金华金义东轨道建设网站Mybatis系列原理剖析之#xff1a;项目实战#xff1a;自定义持久层框架
持久层是JAVA EE三层体系架构中#xff0c;与数据库进行交互的一层#xff0c;持久层往往被称为dao层。需要说明的是#xff0c;持久层的技术选型有很多#xff0c;绝不仅仅只有mybatis一种。像早…Mybatis系列原理剖析之项目实战自定义持久层框架
持久层是JAVA EE三层体系架构中与数据库进行交互的一层持久层往往被称为dao层。需要说明的是持久层的技术选型有很多绝不仅仅只有mybatis一种。像早期可能会直接使用jdbc来与数据库进行交互那么这里就需要思考一个问题既然已经有jdbc实现与数据库的交互为什么还需要使用mybatis这类持久层框架呢 虽然jdbc提供了与数据库交互的基本功能但它需要手动编写大量的SQL语句和处理代码使代码显得冗长和难以维护。而MyBatis可以通过配置文件和注解来映射Java对象和SQL语句使得开发者可以更加专注于业务逻辑的实现而不必过多关注SQL语句的编写和维护。此外MyBatis还提供了缓存功能、动态SQL语句、多数据源支持等特性使得开发者可以更加灵活地处理数据访问地需求。因此使用Mybatis等持久层框架可以提高开发效率和代码可维护性 在自定义持久层框架开始之前我们首先回顾一下jdbc实现与数据库交互时的配置代码并以此分析存在的问题。然后设计方案解决这些问题进而自定义持久层框架。
JDBC配置回顾
public static void main(String[] args){Connection connection null;PreparedStatement preparedStatement null;ResultSet resultSet null;try{// 加载数据库驱动Class.forName(com.mysql.jdbc.Driver);// 通过驱动管理类获取数据库链接connection DriverManager.getConnection(jdbc:mysql://localhost:3306/mybatis?characterEncoding utf - 8 , root , root );// 定义sql语句表示占位符String sql select * from user where username ?;// 获取预处理statementpreparedStatement connection.prepareStatement(sql);// 设置参数第一个参数为sql语句中参数的序号(从1开始)第二个参数为设置的参数值preparedStatement.setString(1, tom);// 向数据库发出sql执行查询查询出结果集resultSet preparedStatement.executeQuery();// 遍历查询结果集while(resultSet.next()){int id resultSet.getInt(id);String username resultSet.getString(username);// 封装Useruser.setId(id);user.setUsername(username);}System.out.println(user);}catch(Exception e){e.printStackTrace();}finally{// 释放资源if(resultSet ! null){try{resultSet.close();}catch(SQLException e){e.printStackTrace();}}if(preparedStatement ! null){try{preparedStatement.close();}catch(SQLException e){e.printStackTrace();}}}} 上面是一段非常常见的采用jdbc的方式连接mysql数据库的配置。简单分析下上述代码中存在的一些问题
JDBC问题分析 硬编码问题 直观能够看到的首先就是一个硬编码问题在上述加载数据库驱动和链接时我们都显式的将配置写在了代码中。如果后续配置发生了变化比如将驱动由com.mysql.jdbc.Driver升级为com.mysql.cj.jdbc.Driver或者驱动由Mysql变为Oracle。此时都需要对原始代码进行变更然后重新编译源文件再次重新打包部署 数据库连接频繁创建/释放 上述查询数据库的代码可能会被调用多次而上面的代码每次被调用都会尝试创建一个新的数据连接并在使用完成后释放。而我们知道数据库链接是一个非常宝贵的资源在获取数据库连接时底层需要首先建立TCP连接完成三次握手这一过程是比较消耗资源影响性能的。 SQL语句与查询/操作耦合 可以看到sql语句与数据库查询、结果集操作耦合在一起同时也存在硬编码问题。而且这样也导致sql语句散落在各个业务代码操作中非常不便于后续的管理和代码优化。 手动封装返回结果集较为繁琐 上面代码对结果集进行遍历并将数据库中的每个列赋值给实体类的相应属性。此时如果实力类的个数较多会存在手动封装较为繁琐的问题这些都是可以优化的点。
针对上述的几个问题我们来设计一下相应的解决方案
针对硬编码问题相应的解决方案比较普遍的就是采用配置文件的方式。将数据库连接等配置信息写在配置文件中然后在代码里读取配置文件。针对数据库连接频繁创建/释放问题可以采用连接池的方式来进行解决。目前市面上常见的连接池有很多诸如C3P0、DBCP、Druid等。针对SQL语句与查询/操作耦合在一起的问题同样也可以采用配置文件的方式来解决尝试将sql语句单独存放在某个配置文件中从而实现对SQL的统一集中管理和硬编码问题。针对手动封装返回结果集的问题可以采用反射的方式进行解决。直接将查询结果与相应的实体类利用反射进行映射从而节省了手动封装过程的繁琐。
自定义持久层框架设计思路
在设计之前需要明确的是我们当前自定义的持久层框架本质仍然是对JDBC代码的封装只不过在封装的过程中要把JDBC中存在的问题进行规避和解决。
整体的设计思路包括两部分 使用端 使用端指上层的一些项目会来使用我们设计的持久层框架。使用时需要引入框架的jar包。 由于不同项目的数据库连接信息、sql语句等都是不相同的因此使用端需要提供包括数据库配置sql配置sql语句参数类型返回值类型等信息并将它们写在配置文件中 1sqlMapConfig.xml存放数据库配置信息。 2Mapper.xml存放sql配置信息。 自定义持久层框架本身 本身也是一个工程本质是对JDBC代码进行了封装因此需要读取项目提供的上述两个配置文件并解析出配置信息来构建JDBC连接。 加载配置文件。 功能根据配置文件的路径加载配置文件成字节输入流并以流的方式加载到内存中。实现创建Resources类定义方法getResourceAsStream(String path) 创建两个JavaBean容器对象存放的就是对配置文件解析出来的内容 Configuration核心配置类存放sqlMapConfig.xml解析出来的内容MappedStatement映射配置类: 存放mapper.xml解析出来的内容 解析配置文件dom4j 创建类SqlSessionFactoryBuilder 方法build(InputStream in) 使用dom4j解析配置文件将解析出来的内容封装到容器对象中 创建SqlSessionFactory对象生成SqlSession会话对象包含了增删改查等一系列操作工厂模式 创建SqlSessionFactory接口及实现类DefaultSqlSessionFactory openSession生成sqlSession对象 创建SqlSession接口及实现类DefaultSession 定义对数据库的crud操作selectList()、selectOne()、update()、delete() 创建Executor接口及实现类SimpleExecutor实现类用来封装CRUD操作。 query(Configuration, MapedStatement, Object… params)执行的就是JDBC代码。可变长参数params即sql执行时所需要的占位符参数因为无法确认参数个数所以采用可变参的形式。
总结一下上面的设计思路由于我们自定义的持久层框架本质上是对JDBC代码进行了封装所以它底层执行的还是JDBC代码。JDBC代码想要执行两部分信息必不可少数据库配置信息、sql配置信息而这两部分信息此时已经由使用端采用配置文件的方式进行提供。因此持久层框架需要完成的功能实际上就是采用dom4j技术对上述的配置文件进行解析并把解析出来的内容封装到两个JavaBeanConfiguration、MappedStatement中。这两个参数经过层层传递传递到SimpleExecutor的query方法中最终在query方法中执行JDBC代码操作。
自定义持久层框架代码实现
使用端代码实现
根据上面的分析使用端需要提供数据库连接配置、sql查询语句等信息并使用配置文件的方式。因此接下来我们就在使用端创建的Maven项目的resources目录下创建两个配置文件sqlMapConfig.xml和UserMapper.xml前者保存mysql数据库的配置信息后者保存sql查询语句的相关信息。
sqlMapConfig.xml
!-- 配置mysql的连接信息--configurationdatasourceproperties namedriverClass valuecom.mysql.jdbc.Driver/propertiesproperties namejdbcUrl valuejdbc:mysql://127.0.0.1:3306/CustomPersistent/propertiesproperties nameusername valueroot/propertiesproperties namepassword valueroot/properties/datasource!-- 存放mapper.xml的全路径--mapper resourceUserMapper.xml/mapper
/configuration上述配置文件利用datasource标签标识数据库连接信息并将配置使用properties来指定。最后利用mapper标签指明mapper.xml文件所在的位置方便后续文件的读取。
UserMapper.xml
mapper nameuser!-- sql的唯一标识由namespace.id来组成 statementId--!-- resultType需要保存全限定类名后续持久层框架才能借助反射来自动封装结果集--select idselectList resultTypecom.lagou.pojo.Userselect * from user/select!-- 使用#{}替换? 作为新的框架进行识别的占位符与parameterType对象的属性相对应--select idselectOne resultTypecom.lagou.pojo.User parameterTypecom.lagou.pojo.Userselect * from user where id #{id} and username #{username}/select
/mapper不同业务/数据表 相应的sql语句应该用不同的Mapper来保存这里假设查询用户表相关数据则定义UserMapper.xml的配置文件。对于CRUD的不同查询方式使用标签select 、insert等来区分。为了能够唯一标识一条sql语句采用namespace.id的方式将每个namespace.id的值封装成一条statementId。同时设置了一些属性值来提供sql语句查询时的额外信息比如用resultType来标识结果集对对应的实体类用paramType标识sql查询时需要的参数对应的实体类。
上述我们简单的在使用端定义好了我们最初设计思路中的两个配置文件由于我们底层的持久层框架还没定义好所以无法演示使用的代码因此接下来我们先介绍自定义持久层框架的相关代码。
自定义持久层框架代码实现
同样的按照上面的设计思路来一步步的实现。
创建Resources类
public class Resources {// 根据配置文件的路径将配置文件加载成字节输入流存储在内存中public static InputStream getResourceAsStream(String path){InputStream inputStream Resources.class.getClassLoader().getResourceAsStream(path);return inputStream;}
}有了上面这个类之后我们就可以在之前的使用端代码里编写测试代码尝试来读取两个配置文件。
public class CustomPersistenceTest {public static void main(String[] args) throws IOException {InputStream resourceAsStream Resources.getResourceAsStream(sqlMapConfig.xml);byte[] bytes new byte[1024];int length;StringBuilder builder new StringBuilder();while((length resourceAsStream.read(bytes)) ! -1){String str new String(bytes, 0, length);builder.append(str);}System.out.println(resourceAsStream);System.out.println(builder.toString());}
}打印结果
java.io.BufferedInputStream6d6f6e28
!-- 配置mysql的连接信息--configurationdatasourceproperties namedriverClass valuecom.mysql.jdbc.Driver/propertiesproperties namejdbcUrl valuejdbc:mysql://127.0.0.1:3306/CustomPersistent/propertiesproperties nameusername valueroot/propertiesproperties namepassword valueroot/properties/datasource!-- 存放mapper.xml的全路径--mapper resourceUserMapper.xml/mapper
/configuration可以看到定义的getResourceAsStream方法成功以流的方式读取到了定义的sqlConfigMapper.xml的配置信息接下来就是利用dom4j来对其进行解析了。在此之前先定义两个JavaBean容器对象来存储解析好的配置类。
容器对象定义
MappedStatement核心配置类存放mapper.xml解析出来的内容
public class MappedStatement {//id标识private String id;//返回值类型private String resultType;//参数值类型private String paramterType;//sql语句private String sql;public String getId() {return id;}public void setId(String id) {this.id id;}public String getResultType() {return resultType;}public void setResultType(String resultType) {this.resultType resultType;}public String getParamterType() {return paramterType;}public void setParamterType(String paramterType) {this.paramterType paramterType;}public String getSql() {return sql;}public void setSql(String sql) {this.sql sql;}
}MappedStatement类封装了上面介绍的在使用端的配置文件中包含的一些配置参数比如idresultTypepa rameterType。
Configuration核心配置类存放sqlMapConfig.xml解析出来的内容
public class Configuration {// 数据库配置信息private DataSource dataSource;/*** sql配置信息** key: StatementId* value: 封装好的MappedStatement对象*/MapString, MappedStatement mappedStatementMap new HashMap();public DataSource getDataSource() {return dataSource;}public void setDataSource(DataSource dataSource) {this.dataSource dataSource;}public MapString, MappedStatement getMappedStatementMap() {return mappedStatementMap;}public void setMappedStatementMap(MapString, MappedStatement mappedStatementMap) {this.mappedStatementMap mappedStatementMap;}
}dataSource对象封装了在使用端sqlMapConfig.xml中的Datasource属性同时有一个MapString, MappedStatement的对象用于封装上面的MappedStatement对象这样的好处在于将每个MappedStatement对象用唯一id映射保存在内存中便于后续框架根据使用端传入的id来快速获取相应sql配置来执行。
使用dom4j解析配置文件
下面给出使用dom4j来解析配置文件的核心流程代码
public class XMLConfigBuilder {private Configuration configuration;public XMLConfigBuilder() {this.configuration new Configuration();}/*** 该方法就是使用dom4j对配置文件进行解析封装Configuration*/public Configuration parseConfig(InputStream inputStream) throws DocumentException, PropertyVetoException {Document document new SAXReader().read(inputStream);//configurationElement rootElement document.getRootElement();ListElement list rootElement.selectNodes(//property);Properties properties new Properties();for (Element element : list) {String name element.attributeValue(name);String value element.attributeValue(value);properties.setProperty(name,value);}// 使用c3p0连接池作为数据库连接对象减少数据库的频繁连接/释放ComboPooledDataSource comboPooledDataSource new ComboPooledDataSource();comboPooledDataSource.setDriverClass(properties.getProperty(driverClass));comboPooledDataSource.setJdbcUrl(properties.getProperty(jdbcUrl));comboPooledDataSource.setUser(properties.getProperty(username));comboPooledDataSource.setPassword(properties.getProperty(password));configuration.setDataSource(comboPooledDataSource);//mapper.xml解析: 拿到路径--字节输入流---dom4j进行解析ListElement mapperList rootElement.selectNodes(//mapper);for (Element element : mapperList) {String mapperPath element.attributeValue(resource);InputStream resourceAsSteam Resources.getResourceAsSteam(mapperPath);XMLMapperBuilder xmlMapperBuilder new XMLMapperBuilder(configuration);xmlMapperBuilder.parse(resourceAsSteam);}return configuration;}}上述代码使用dom4j来对一个xml文件进行解析。
首先利用new SAXReader().read(inputStream)方法将字节输入流解析成一个Document对象然后就可以根据标签来对xml文件进行遍历和检索。首先调用document.getRootElement();获取到根标签然后找到property标签进行遍历从中获取到配置文件中对于数据库连接的配置并以此来生成一个C3P0的连接池对象赋值给datasource。
在解析完数据库连接配置后接着根据配置的maaper路径再次调用Resources.getResourceAsSteam方法获取到mapper.xml的sql配置信息使用XMLMapperBuilder对象的parse方法封装到configuration对象中。
XMLMapperBuilder类代码如下所示
public class XMLMapperBuilder {private Configuration configuration;public XMLMapperBuilder(Configuration configuration) {this.configuration configuration;}public void parse(InputStream inputStream) throws DocumentException {// 将xml文件解析成Documen对象Document document new SAXReader().read(inputStream);Element rootElement document.getRootElement();// 获取根标签的namespace属性String namespace rootElement.attributeValue(namespace);// 遍历所有的select标签ListElement list rootElement.selectNodes(//select);for (Element element : list) {// 获取select标签中的各个属性String id element.attributeValue(id);String resultType element.attributeValue(resultType);String paramterType element.attributeValue(paramterType);String sqlText element.getTextTrim();MappedStatement mappedStatement new MappedStatement();mappedStatement.setId(id);mappedStatement.setResultType(resultType);mappedStatement.setParamterType(paramterType);mappedStatement.setSql(sqlText);String key namespace.id;configuration.getMappedStatementMap().put(key,mappedStatement);}}
}至此我们完成了对使用端传入的配置文件的解析并封装到了Configuration对象中。有了该对象后后面我们着手考虑的就是根据获取到的Datasource连接池创建数据库连接然后执行使用端传入的指定SQL语句。
生成接口代理类对象 我们思考另一个问题使用端该如何与框架交互来传入指定要执行的SQL语句和参数 上面介绍过mapper.xml配置文件中namespace标签以及每个select标签的参数类型、返回值类型**采用全限定类名的原因在于框架可以借助反射来匹配dao层的接口类与定义的mapper.xmlsql配置文件以及与相应实体类属性进行绑定。**同时Configuration核心配置类中封装的MapString, MappedStatement属性其键值就是由mapper标签的namespace属性与select标签的id属性拼接而成的。
由于dao层是接口类没有具体的实现逻辑。因为为了在方法执行时实现具体的处理逻辑我们就可以借助于代理类来实现。利用反射根据使用端传入的dao层接口类拼接上该类相应的调用方法以此值为键(statementId)来从Configuration配置类的mappedStatementMap属性中获取相应的sql语句。 Overridepublic T T getMapper(Class? mapperClass) {// 使用JDK动态代理来为Dao接口生成代理对象并返回Object proxyInstance Proxy.newProxyInstance(DefaultSqlSession.class.getClassLoader(), new Class[]{mapperClass}, new InvocationHandler() {Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {// 底层都还是去执行JDBC代码 //根据不同情况来调用selctList或者selectOne// 准备参数 1statmentid :sql语句的唯一标识namespace.id 接口全限定名.方法名// 方法名findAllString methodName method.getName();String className method.getDeclaringClass().getName();String statementId className.methodName;// 准备参数2params:args// 获取被调用方法的返回值类型Type genericReturnType method.getGenericReturnType();// 判断是否进行了 泛型类型参数化if(genericReturnType instanceof ParameterizedType){ListObject objects selectList(statementId, args);return objects;}return selectOne(statementId,args);}});return (T) proxyInstance;}上面代码使用JDK动态代理来为Dao层接口生成代理对象这样后续在使用端调用dao层接口时就会进入到代理类的InvocationHandler参数的invoke方法中执行实现dao层接口与mapper.xml中的sql绑定。
接下来我们将该方法封装到sqlSession方法中借助sqlSession接口类来实现获取dao层接口的代理类实例。
创建sqlSession封装CRUD操作
在上面封装好了配置文件信息后接下来我们需要思考的就是该如何运行配置文件中的Sql了。这里我们再做一层封装将可能执行的CRUD操作封装在sqlSession中。这里基于开发的依赖倒置原则我们还是先将CRUD操作封装在sqlSession接口中再创建一个DefaultSqlSession的实现类来编写具体的处理逻辑
public interface SqlSession {//查询所有public E ListE selectList(String statementid,Object... params) throws Exception;//根据条件查询单个public T T selectOne(String statementid,Object... params) throws Exception;//为Dao接口生成代理实现类public T T getMapper(Class? mapperClass);}这里新添加了一个getMapper方法目的是为了创建dao层接口的代理类。
SqlSession对象是一个轻量级的、非线程安全的对象它和数据库连接相关联一般需要在每个数据库操作中创建一个新的SqlSession对象。 因此我们利用工厂类设计模式定义一个SqlSessionFactory类来生产sqlSession对象
public interface SqlSessionFactory {public SqlSession openSession();}创建DefaultSqlSession实现类类生成代理对象
sqlSession接口中定义了操作数据库的多种操作CRUD等。接下来我们定义一个DefaultSqlSession类来具体实现上述功能根据dao层调用的不同接口执行相应的SQL处理逻辑。
public class DefaultSqlSession implements SqlSession {private Configuration configuration;public DefaultSqlSession(Configuration configuration) {this.configuration configuration;}Overridepublic E ListE selectList(String statementid, Object... params) throws Exception {//将要去完成对simpleExecutor里的query方法的调用simpleExecutor simpleExecutor new simpleExecutor();MappedStatement mappedStatement configuration.getMappedStatementMap().get(statementid);// 把具体的sql执行封装到SimpleExecutor中ListObject list simpleExecutor.query(configuration, mappedStatement, params);return (ListE) list;}Overridepublic T T selectOne(String statementid, Object... params) throws Exception {ListObject objects selectList(statementid, params);if(objects.size()1){return (T) objects.get(0);}else {throw new RuntimeException(查询结果为空或者返回结果过多);}}Overridepublic T T getMapper(Class? mapperClass) {...}}在创建了DefaultSqlSession类后我们采用工厂类的设计模式定义一个DefaultSqlSessionFactory工厂类来生产sqlSession对象。在DefaultSqlSessionFactory中包含了数据库连接配置此外还有一些缓存、事务等一系列配置信息并根据这些配置信息创建SqlSession对象。
SqlSessionFactory工厂类的作用是封装了SqlSession对象的创建过程并且对SqlSession对象进行了统一的管理使得我们可以更加方便地获取SqlSession对象并进行数据库操作。同时SqlSessionFactory也保证了SqlSession对象的线程安全性和可重用性从而提高了系统的性能和可维护性。
这样我们就可以通过DefaultSqlSessionFactory获取SqlSession对象然后使用SqlSession对象进行数据库操作。
public class DefaultSqlSessionFactory implements SqlSessionFactory {private Configuration configuration;public DefaultSqlSessionFactory(Configuration configuration) {this.configuration configuration;}Overridepublic SqlSession openSession() {return new DefaultSqlSession(configuration);}
}封装SqlSessionFactoryBuilder
最后我们将上面的dom4j解析配置文件以及sqlSessionFactory的创建逻辑统一封装在SqlSessionFactoryBuilder类中从而便于使用端更快捷地获取到sqlSessionFactory对象来生产sqlSession对象。SqlSessionFactoryBuilder类的存在主要是为了解耦SqlSessionFactory对象的创建过程和应用程序的代码。
SqlSessionFactory对象的创建过程通常需要读取配置文件、解析配置信息、创建数据源对象等一系列操作这些操作通常比较复杂而且需要依赖于具体的持久层框架。为了避免将这些复杂的操作和具体的持久层框架耦合在一起我们通常会将SqlSessionFactory对象的创建过程封装在SqlSessionFactoryBuilder类中并将SqlSessionFactoryBuilder类作为一个独立的类提供给应用程序使用。这样应用程序就可以通过SqlSessionFactoryBuilder类来获取SqlSessionFactory对象而不需要关心SqlSessionFactory对象的创建细节。
public class SqlSessionFactoryBuilder {public SqlSessionFactory build(InputStream in) throws DocumentException, PropertyVetoException {// 第一使用dom4j解析配置文件将解析出来的内容封装到Configuration中XMLConfigBuilder xmlConfigBuilder new XMLConfigBuilder();Configuration configuration xmlConfigBuilder.parseConfig(in);// 第二创建sqlSessionFactory对象工厂类生产sqlSession:会话对象DefaultSqlSessionFactory defaultSqlSessionFactory new DefaultSqlSessionFactory(configuration);return defaultSqlSessionFactory;}
}至此目前我们的dao层接口代理类和操作数据库的CRUD等功能也已经实现了解决了使用端与持久层框架的交互问题。并且在sqlSession类中封装了CURD操作以及具体的实现逻辑。即现在我们已经能拿到具体的待执行sql只剩下最后的执行步骤了。
使用连接池执行sql
最初提到持久层框架底层sql执行实际上使用的也仍然是JDBC因此我们将JDBC操作仍然封装到类中创建Executor类和simpleExecutor类。
public interface Executor {public E ListE query(Configuration configuration,MappedStatement mappedStatement,Object... params) throws Exception;}public class simpleExecutor implements Executor {Override //userpublic E ListE query(Configuration configuration, MappedStatement mappedStatement, Object... params) throws Exception {// 1. 注册驱动获取连接Connection connection configuration.getDataSource().getConnection();// 2. 获取sql语句 : select * from user where id #{id} and username #{username}//转换sql语句 select * from user where id ? and username ? 转换的过程中还需要对#{}里面的值进行解析存储String sql mappedStatement.getSql();BoundSql boundSql getBoundSql(sql);// 3.获取预处理对象preparedStatementPreparedStatement preparedStatement connection.prepareStatement(boundSql.getSqlText());// 4. 设置参数//获取到了参数的全路径String paramterType mappedStatement.getParamterType();Class? paramtertypeClass getClassType(paramterType);ListParameterMapping parameterMappingList boundSql.getParameterMappingList();for (int i 0; i parameterMappingList.size(); i) {ParameterMapping parameterMapping parameterMappingList.get(i);String content parameterMapping.getContent();//反射Field declaredField paramtertypeClass.getDeclaredField(content);//暴力访问declaredField.setAccessible(true);Object o declaredField.get(params[0]);preparedStatement.setObject(i1,o);}// 5. 执行sqlResultSet resultSet preparedStatement.executeQuery();String resultType mappedStatement.getResultType();Class? resultTypeClass getClassType(resultType);ArrayListObject objects new ArrayList();// 6. 封装返回结果集while (resultSet.next()){Object o resultTypeClass.newInstance();//元数据ResultSetMetaData metaData resultSet.getMetaData();for (int i 1; i metaData.getColumnCount(); i) {// 字段名String columnName metaData.getColumnName(i);// 字段的值Object value resultSet.getObject(columnName);//使用反射或者内省根据数据库表和实体的对应关系完成封装PropertyDescriptor propertyDescriptor new PropertyDescriptor(columnName, resultTypeClass);Method writeMethod propertyDescriptor.getWriteMethod();writeMethod.invoke(o,value);}objects.add(o);}return (ListE) objects;}private Class? getClassType(String paramterType) throws ClassNotFoundException {if(paramterType!null){Class? aClass Class.forName(paramterType);return aClass;}return null;}/*** 完成对#{}的解析工作1.将#{}使用进行代替2.解析出#{}里面的值进行存储* param sql* return*/private BoundSql getBoundSql(String sql) {//标记处理类配置标记解析器来完成对占位符的解析处理工作ParameterMappingTokenHandler parameterMappingTokenHandler new ParameterMappingTokenHandler();GenericTokenParser genericTokenParser new GenericTokenParser(#{, }, parameterMappingTokenHandler);//解析出来的sqlString parseSql genericTokenParser.parse(sql);//#{}里面解析出来的参数名称ListParameterMapping parameterMappings parameterMappingTokenHandler.getParameterMappings();BoundSql boundSql new BoundSql(parseSql,parameterMappings);return boundSql;}}
上面的代理就是采用JDBC方式访问数据库的尝试书写逻辑了。
对于传入的MappedStatement对象获取其sql值并对其解析将#{}使用进行代替同时解析出#{}里面的值进行存储。目的是为了后续根据原先#{}中指定的属性来从传入的参数属性中获取值进行替换。解析完sql后利用反射根据参数的全限定类名获取到相应的Class类遍历第一步中的所有占位符参数利用反射获取到传入的实体类中相应属性的值并传入preparedStatement对象中。利用preparedStatement.executeQuery()执行sql并利用反射获取结果封装实体类。对结果集进行遍历获取每一个列的值封装到结果实体类对象的相应属性中最后返回结果实体类对象的List集合。
上面的代码频繁用了Java反射技术来根据传入的参数类路径和结果类路径获取sql占位符的参数值以及封装结果集。免去了我们手动封装结果集的繁琐实现了动态地sql参数绑定。
总结
至此我们已经完成了自定义持久层框架中的解析xml配置文件 - 封装实体类 - 生成接口代理类对象 - 实现sqlSession封装CRUD操作 - 封装 JDBC执行sql。实现了我们最初在设计自定义持久层框架中的思路流程。再次归纳总结一下我们的设计流程和思路
创建Resources工具类根据配置文件的路径将配置文件加载成字节输入流存储在内存中创建XMLConfigBuilder和XMLMapperBuilder类利用dom4j技术对字节输入流根据标签层层进行解析取出数据库配置和sql配置信息将取出的数据库配置和sql配置信息封装到Configuration配置类中创建dao层接口的代理类从而实现dao层接口方法与mapper.xml中指定配置SQL的映射实现使用端dao层接口调用能够执行mapper.xml中相应sql的处理逻辑。封装sqlSession接口定义一系列数据库操作方法进行诸如CRUD操作创建sqlSessionFactory封装我们之前创建的数据库连接配置等信息用来生产sqlSession对象进而可以使用SqlSession对象进行数据库操作。创建sqlSession接口和sqlSessionFactory接口的实现类DefaultSqlSession和DefaultSqlSessionFactory。DefaultSqlSession用来实现实现一系列数据库操作方法即根据dao层接口的不同方法执行相应的处理逻辑。DefaultSqlSessionFactory用来实现根据数据库连接等配置信息生产sqlSession对象。使用建造者设计模式将上面的dom4j解析配置文件逻辑以及sqlSessionFactory的创建逻辑统一封装在SqlSessionFactoryBuilder类中从而实现根据数据库配置等信息创建sqlSessionFactory的操作。SqlSessionFactoryBuilder类的存在主要是为了解耦SqlSessionFactory对象的创建过程和应用程序的代码。最后封装JDBC执行类替换mapper.xml中的sql占位符。并根据传入参数对象的属性值赋值以及返回参数类型封装结果集。
使用端测试
最后我们编写使用端来引入我们自定义的持久层框架并根据该持久层框架编写测试类进行测试。
创建IPersistence_testMAVEN模块在pom.xml文件中引入: !--引入自定义持久层框架的依赖--dependenciesdependencygroupIdcom.lagou/groupIdartifactIdIPersistence/artifactIdversion1.0-SNAPSHOT/version/dependency/dependencies接着定义测试类
public class IPersistenceTest {Testpublic void test() throws Exception {InputStream resourceAsSteam Resources.getResourceAsSteam(sqlMapConfig.xml);SqlSessionFactory sqlSessionFactory new SqlSessionFactoryBuilder().build(resourceAsSteam);SqlSession sqlSession sqlSessionFactory.openSession();//调用User user new User();user.setId(1);user.setUsername(张三);/* User user2 sqlSession.selectOne(user.selectOne, user);System.out.println(user2);*//* ListUser users sqlSession.selectList(user.selectList);for (User user1 : users) {System.out.println(user1);}*/IUserDao userDao sqlSession.getMapper(IUserDao.class);ListUser all userDao.findAll();for (User user1 : all) {System.out.println(user1);}}}输出结果
User{id1, usernamelisi}
User{id2, usernamezhangsan}当我们在使用端的dao层接口调用finalAll方法后持久层框架执行了UserMapper.xml文件中定义的sql方法和参数并将返回值封装在了User对象中返回。验证了我们自定义持久层框架的设计思路和实现是正确的。
总体来说上述自定义持久层框架的设计没有特别复杂但是仍然包含了持久层框架中比较核心的几个模块和技术。上面的讲述过程可能比较繁琐很多东西没有讲得很清有兴趣的可以跟着代码写一遍从而加深自己的理解。
完整的项目代码我放在下面的github上了有兴趣的大家可以download下来学习: https://github.com/TAM-Lab/BlogCodeRepository/tree/main/mybatis