长文干货|手写自定义持久层框架!

数据库 Oracle
通过手写自定义框架,我们解决JDBC操作数据库带来的一些问题,例如频繁创建释放数据库连接,硬编码,手动封装返回结果等问题.

 为何要手写自定义持久层框架?

1.JDBC 编码的弊端

  • 会造成硬编码问题(无法灵活切换数据库驱动) 频繁创建和释放数据库连接造成系统资源浪费 影响系统性能
  • sql 语句存在硬编码,造成代码不易维护,实际应用中 sql 变化可能较大,变动 sql 需要改 Java 代码
  • 使用 preparedStatement 向占有位符号传参数存在硬编码, 因 sql 语句的 where 条件不确定甚至没有where条件,修改 sql 还要修改代码 系统不易维护
  • 对结果集解析也存在硬编码, sql变化导致解析代码变化

2.更有助于读 mybatis 持久层框架源码

JDBC代码

  1. public class jdbcConnection { 
  2.     private static Connection connection = null
  3.     private static PreparedStatement preparedStatement = null
  4.     private static ResultSet resultSet = null
  5.  
  6.     public static void main(String[] args) { 
  7.         try { 
  8.             // 加载数据库驱动 
  9.             Class.forName("com.mysql.jdbc.Driver"); 
  10.             // 通过驱动管理类获取数据库连接 
  11.             connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/huodd""root""1234"); 
  12.             // 定义sql语句 ? 表示占位符 
  13.             String sql = "select id,username from user where id = ?"
  14.             // 获取预处理对象 statement 
  15.             PreparedStatement preparedStatement = (PreparedStatement) connection.prepareStatement(sql); 
  16.             // 设置参数 第一个参数为 sql 语句中参数的序号(从1开始) 第二个参数为 设置的参数值 
  17.             preparedStatement.setInt(1, 1); 
  18.             // 向数据库发出sql执行查询 查询出结果集 
  19.             resultSet = preparedStatement.executeQuery(); 
  20.             // 遍历查询结果集 
  21.             while (resultSet.next()) { 
  22.                 int id = resultSet.getInt("id"); 
  23.                 String username = resultSet.getString("username"); 
  24.                 // 封装对象 
  25.                 User user = new User(); 
  26.                 user.setId(id); 
  27.                 user.setUsername(username); 
  28.                 System.out.println(user); 
  29.             } 
  30.         } catch (Exception ex) { 
  31.             ex.printStackTrace(); 
  32.         } finally { 
  33.             try { 
  34.                 // 释放资源 
  35.                 if (resultSet != null) { 
  36.                     resultSet.close(); 
  37.                 } 
  38.                 if (preparedStatement != null) { 
  39.                     preparedStatement.close(); 
  40.                 } 
  41.                 if (connection != null) { 
  42.                     connection.close(); 
  43.                 } 
  44.             } catch (Exception ex) { 
  45.                 ex.printStackTrace(); 
  46.             } 
  47.         } 
  48.     } 

解决问题的思路

  1. 数据库频繁创建连接、释放资源 -> 连接池
  2. sql语句及参数硬编码 -> 配置文件
  3. 手动解析封装结果集 -> 反射、内省

编码前思路整理

1.创建、读取配置文件

  • sqlMapConfig.xml 存放数据库配置信息
  • userMapper.xml :存放sql配置信息
  • 根据配置文件的路径,加载配置文件成字节输入流,存储在内存中Resources#getResourceAsStream(String path)
  • 创建两个JavaBean存储配置文件解析出来的内容
  1. Configuration :核心配置类 ,存放 sqlMapConfig.xml解析出来的内容
  2. MappedStatement:映射配置类:存放mapper.xml解析出来的内容

2.解析配置文件(使用dom4j)

  • 创建类:SqlSessionFactoryBuilder#build(InputStream in) -> 设计模式之构建者模式
  • 使用dom4j解析配置文件,将解析出来的内容封装到容器对象(JavaBean)中

3.创建 SqlSessionFactory 接口及实现类DefaultSqlSessionFactory

  • SqlSessionFactory对象,生产sqlSession会话对象 -> 设计模式之工厂模式

4.创建 SqlSession接口及实现类DefaultSqlSession

  • 定义对数据库的CRUD操作
  1. selectList()
  2. selectOne()
  3. update()
  4. delete()

5.创建Executor接口及实现类SimpleExecutor实现类

  • query(Configuration configuration, MappedStatement mapStatement, Object... orgs) 执行的就是JDBC代码

6.测试代码

用到的设计模式

  • 构建者模式
  • 工厂模式
  • 代理模式

进入编码

1.创建、读取配置文件

sqlMapConfig.xml 存放数据库配置信息

  1. <configuration> 
  2.     <dataSource> 
  3.         <!-- 引入数据库连接信息 --> 
  4.         <property name="driverClass" value="com.mysql.jdbc.Driver"></property> 
  5.         <property name="jdbcUrl" value="jdbc:mysql:///huodd"></property> 
  6.         <property name="user" value="root"></property> 
  7.         <property name="password" value="1234"></property> 
  8.     </dataSource> 
  9.  
  10.     <!-- 引入sql配置文件 --> 
  11.     <mapper resource="userMapper.xml"></mapper> 
  12.  
  13. </configuration> 

userMapper.xml 存放sql配置信息

  1. <mapper namespace="user"
  2.  
  3.     <!-- sql 的唯一标识: namespace.id 组成 => statementId 如 当前的为 user.selectList --> 
  4.     <select id="selectList" resultType="com.huodd.pojo.User" paramterType="com.huodd.pojo.User"
  5.         select * from user 
  6.     </select
  7.  
  8.     <select id="selectOne" paramterType="com.huodd.pojo.User" resultType="com.huodd.pojo.User"
  9.         select * from user where id = #{id} and username =#{username} 
  10.     </select
  11.  
  12. </mapper> 

User.java

  1. public class User { 
  2.     private Integer id; 
  3.     private String username; 
  4.  
  5.     ... 省略getter setter 方法 
  6.     ... 省略 toString 方法 

pom.xml 中引入依赖

  1. <dependency> 
  2.     <groupId>mysql</groupId> 
  3.     <artifactId>mysql-connector-java</artifactId> 
  4.     <version>5.1.17</version> 
  5. </dependency> 
  6. <dependency> 
  7.     <groupId>c3p0</groupId> 
  8.     <artifactId>c3p0</artifactId> 
  9.     <version>0.9.1.2</version> 
  10. </dependency> 
  11. <dependency> 
  12.     <groupId>log4j</groupId> 
  13.     <artifactId>log4j</artifactId> 
  14.     <version>1.2.12</version> 
  15. </dependency> 
  16. <dependency> 
  17.     <groupId>junit</groupId> 
  18.     <artifactId>junit</artifactId> 
  19.     <version>4.10</version> 
  20. </dependency> 
  21. <dependency> 
  22.     <groupId>dom4j</groupId> 
  23.     <artifactId>dom4j</artifactId> 
  24.     <version>1.6.1</version> 
  25. </dependency> 
  26. <dependency> 
  27.     <groupId>jaxen</groupId> 
  28.     <artifactId>jaxen</artifactId> 
  29.     <version>1.1.6</version> 
  30. </dependency> 

创建两个JavaBean对象 用于存储解析的配置文件的内容(Configuration.java、MappedStatement.java)

  1. public class Configuration { 
  2.  
  3.     // 数据源 
  4.     private DataSource dataSource; 
  5.     //map集合 key:statementId value:MappedStatement 
  6.     private Map<String,MappedStatement> mappedStatementMap = new HashMap<>(); 
  7.  
  8.     ... 省略getter setter 方法 

  1. public class MappedStatement { 
  2.  
  3.     // id 
  4.     private String id; 
  5.     // sql 语句 
  6.     private String sql; 
  7.     // 参数值类型 
  8.     private Class<?> paramterType; 
  9.     // 返回值类型 
  10.     private Class<?> resultType; 
  11.  
  12.    ... 省略getter setter 方法 

创建Resources工具类 并编写静态方法getResourceAsSteam(String path)

  1. public class Resources { 
  2.  
  3.     /** 
  4.      * 根据配置文件的路径 将配置文件加载成字节输入流 存储在内存中 
  5.      * @param path 
  6.      * @return InputStream 
  7.      */ 
  8.     public static InputStream getResourceAsStream(String path) { 
  9.         InputStream resourceAsStream = Resources.class.getClassLoader().getResourceAsStream(path); 
  10.         return resourceAsStream; 
  11.     } 

2.解析配置文件(使用dom4j)

创建 SqlSessionFactoryBuilder类 并添加 build 方法

  1. public class SqlSessionFactoryBuilder { 
  2.  
  3.     public SqlSessionFactory build (InputStream in) throws DocumentException, PropertyVetoException, ClassNotFoundException { 
  4.         // 1. 使用 dom4j 解析配置文件 将解析出来的内容封装到Configuration中 
  5.         XMLConfigerBuilder xmlConfigerBuilder = new XMLConfigerBuilder(); 
  6.  
  7.         // configuration 是已经封装好了sql信息和数据库信息的对象 
  8.         Configuration configuration = xmlConfigerBuilder.parseConfig(in); 
  9.  
  10.         // 2. 创建 SqlSessionFactory 对象  工厂类 主要是生产sqlSession会话对象 
  11.         DefaultSqlSessionFactory defaultSqlSessionFactory = new DefaultSqlSessionFactory(configuration); 
  12.  
  13.         return defaultSqlSessionFactory; 
  14.     } 

  1. public class XMLConfigerBuilder { 
  2.  
  3.     private Configuration configuration; 
  4.  
  5.     public XMLConfigerBuilder() { 
  6.         this.configuration = new Configuration(); 
  7.     } 
  8.  
  9.     /** 
  10.      * 该方法 使用dom4j对配置文件进行解析 封装Configuration 
  11.      * @param in 
  12.      * @return 
  13.      */ 
  14.      public Configuration parseConfig (InputStream in) throws DocumentException, PropertyVetoException, ClassNotFoundException { 
  15.          Document document = new SAXReader().read(in); 
  16.          // <configuation> 
  17.          Element rootElement = document.getRootElement(); 
  18.          List<Element> propertyElements = rootElement.selectNodes("//property"); 
  19.          Properties properties = new Properties(); 
  20.          for (Element propertyElement : propertyElements) { 
  21.              properties.setProperty(propertyElement.attributeValue("name"), propertyElement.attributeValue("value")); 
  22.          } 
  23.          // 连接池 
  24.          ComboPooledDataSource comboPooledDataSource = new ComboPooledDataSource(); 
  25.          comboPooledDataSource.setDriverClass(properties.getProperty("driverClass")); 
  26.          comboPooledDataSource.setJdbcUrl(properties.getProperty("jdbcUrl")); 
  27.          comboPooledDataSource.setUser(properties.getProperty("user")); 
  28.          comboPooledDataSource.setPassword(properties.getProperty("password")); 
  29.  
  30.          // 填充 configuration 
  31.          configuration.setDataSource(comboPooledDataSource); 
  32.  
  33.          // mapper 部分  拿到路径 -> 字节输入流 -> dom4j进行解析 
  34.          List<Element> mapperElements = rootElement.selectNodes("//mapper"); 
  35.          XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(configuration); 
  36.          for (Element mapperElement : mapperElements) { 
  37.              String mapperPath = mapperElement.attributeValue("resource"); 
  38.              InputStream resourceAsStream = Resources.getResourceAsStream(mapperPath); 
  39.              xmlMapperBuilder.parse(resourceAsStream); 
  40.          } 
  41.  
  42.          return configuration; 
  43.      } 

  1. public class XMLMapperBuilder { 
  2.  
  3.     private Configuration configuration; 
  4.  
  5.     public XMLMapperBuilder(Configuration configuration) { 
  6.         this.configuration = configuration; 
  7.     } 
  8.  
  9.     public void parse(InputStream inputStream) throws DocumentException, ClassNotFoundException { 
  10.         Document document = new SAXReader().read(inputStream); 
  11.         // <mapper> 
  12.         Element rootElement = document.getRootElement(); 
  13.         String namespace = rootElement.attributeValue("namespace"); 
  14.         List<Element> select = rootElement.selectNodes("//select"); 
  15.         for (Element element : select) { 
  16.             // 获取 id 的值 
  17.             String id = element.attributeValue("id"); 
  18.             String paramterType = element.attributeValue("paramterType"); 
  19.             String resultType = element.attributeValue("resultType"); 
  20.             // 输入参数 class 
  21.             Class<?> paramterTypeClass = getClassType(paramterType); 
  22.             // 返回结果 class 
  23.             Class<?> resultTypeClass = getClassType(resultType); 
  24.             // sql 语句 
  25.             String sqlStr = element.getTextTrim(); 
  26.  
  27.             // 封装 mappedStatement 
  28.             MappedStatement mappedStatement = new MappedStatement(); 
  29.             mappedStatement.setId(id); 
  30.             mappedStatement.setParamterType(paramterTypeClass); 
  31.             mappedStatement.setResultType(resultTypeClass); 
  32.             mappedStatement.setSql(sqlStr); 
  33.  
  34.             // statementId 
  35.             String key = namespace + "." + id; 
  36.             // 填充 configuration 
  37.             configuration.getMappedStatementMap().put(key, mappedStatement); 
  38.         } 
  39.  
  40.     } 
  41.  
  42.     private Class<?> getClassType(String paramterType) throws ClassNotFoundException { 
  43.         Class<?> aClass = Class.forName(paramterType); 
  44.         return aClass; 
  45.     } 

3.创建 SqlSessionFactory 接口及实现类DefaultSqlSessionFactory

  1. public interface SqlSessionFactory { 
  2.     SqlSession openSession(); 

  1. public class DefaultSqlSessionFactory implements SqlSessionFactory { 
  2.  
  3.     private Configuration configuration; 
  4.  
  5.     public DefaultSqlSessionFactory(Configuration configuration) { 
  6.         this.configuration = configuration; 
  7.     } 
  8.  
  9.     @Override 
  10.     public SqlSession openSession() { 
  11.         return new DefaultSqlSession(configuration); 
  12.     } 

4. 创建 SqlSession接口及实现类DefaultSqlSession

  1. public interface SqlSession { 
  2.  
  3.     <E> List<E> selectList(String statementId, Object... param) throws Exception; 
  4.  
  5.     <T> T selectOne(String statementId, Object... params) throws Exception; 
  6.  
  7.     void close() throws SQLException; 
  8.     

  1. public class DefaultSqlSession implements SqlSession { 
  2.  
  3.     private Configuration configuration; 
  4.  
  5.     // 处理器对象 
  6.     private Executor simpleExcutor = new SimpleExecutor(); 
  7.  
  8.     public DefaultSqlSession(Configuration configuration) { 
  9.         this.configuration = configuration; 
  10.     } 
  11.  
  12.     @Override 
  13.     public <E> List<E> selectList(String statementId, Object... param) throws Exception { 
  14.         // 完成对 simpleExcutor里的query方法的调用 
  15.         MappedStatement mappedStatement = configuration.getMappedStatementMap().get(statementId); 
  16.         List<E> list = simpleExcutor.query(configuration, mappedStatement, param); 
  17.         return list; 
  18.     } 
  19.  
  20.     @Override 
  21.     public <T> T selectOne(String statementId, Object... params) throws Exception { 
  22.         List<Object> objects = selectList(statementId, params); 
  23.         if (objects.size() == 1) { 
  24.             return (T) objects.get(0); 
  25.         } else { 
  26.             throw new RuntimeException("返回结果过多"); 
  27.         } 
  28.     } 
  29.  
  30.     @Override 
  31.     public void close() throws SQLException { 
  32.         simpleExcutor.close(); 
  33.     } 
  34.  

5.创建Executor接口及实现类SimpleExecutor实现类

  1. public interface Executor { 
  2.  
  3.     <E> List<E> query(Configuration configuration, MappedStatement mappedStatement, Object... param) throws Exception; 
  4.  
  5.     void close() throws SQLException; 

  1. public class SimpleExecutor implements Executor { 
  2.  
  3.  
  4.     private Connection connection = null
  5.  
  6.     @Override 
  7.     public <E> List<E> query(Configuration configuration, MappedStatement mappedStatement, Object... param) throws Exception { 
  8.         // 注册驱动 获取连接 
  9.         connection = configuration.getDataSource().getConnection(); 
  10.  
  11.         // select * from user where id = #{id} and username = #{username} 
  12.         String sql = mappedStatement.getSql(); 
  13.  
  14.         // 对 sql 进行处理 
  15.         BoundSql boundSql = getBoundSql(sql); 
  16.  
  17.         // select * from where id = ? and username = ? 
  18.         String finalSql = boundSql.getSqlText(); 
  19.  
  20.         // 获取传入参数类对象 
  21.         Class<?> paramterTypeClass = mappedStatement.getParamterType(); 
  22.  
  23.         // 获取预处理 preparedStatement 对象 
  24.         PreparedStatement preparedStatement = connection.prepareStatement(finalSql); 
  25.  
  26.         // 设置参数 
  27.         List<ParameterMapping> parameterMappingList = boundSql.getParameterMappingList(); 
  28.         for (int i = 0; i < parameterMappingList.size(); i++) { 
  29.             ParameterMapping parameterMapping = parameterMappingList.get(i); 
  30.             String name = parameterMapping.getContent(); 
  31.  
  32.             // 反射  获取某一个属性对象 
  33.             Field declaredField = paramterTypeClass.getDeclaredField(name); 
  34.             // 设置暴力访问 
  35.             declaredField.setAccessible(true); 
  36.  
  37.             // 参数传递的值 
  38.             Object o = declaredField.get(param[0]); 
  39.             // 给占位符赋值 
  40.             preparedStatement.setObject(i + 1, o); 
  41.  
  42.         } 
  43.  
  44.         // 执行sql 
  45.         ResultSet resultSet = preparedStatement.executeQuery(); 
  46.  
  47.         // 封装返回结果集 
  48.         // 获取返回参数类对象 
  49.         Class<?> resultTypeClass = mappedStatement.getResultType(); 
  50.         ArrayList<E> results = new ArrayList<>(); 
  51.         while (resultSet.next()) { 
  52.             // 取出 resultSet的元数据 
  53.             ResultSetMetaData metaData = resultSet.getMetaData(); 
  54.             E o = (E) resultTypeClass.newInstance(); 
  55.             int columnCount = metaData.getColumnCount(); 
  56.             for (int i = 1; i <= columnCount; i++) { 
  57.                 // 属性名/字段名 
  58.                 String columnName = metaData.getColumnName(i); 
  59.                 // 属性值/字段值 
  60.                 Object value = resultSet.getObject(columnName); 
  61.  
  62.                 // 使用反射或者内省 根据数据库表和实体的对应关系 完成封装 
  63.                 // 创建属性描述器 为属性生成读写方法 
  64.                 PropertyDescriptor propertyDescriptor = new PropertyDescriptor(columnName, resultTypeClass); 
  65.                 // 获取写方法 
  66.                 Method writeMethod = propertyDescriptor.getWriteMethod(); 
  67.                 // 向类中写入值 
  68.                 writeMethod.invoke(o, value); 
  69.             } 
  70.             results.add(o); 
  71.         } 
  72.         return results; 
  73.     } 
  74.  
  75.     /** 
  76.      * 转换sql语句 完成对 #{} 的解析工作 
  77.      * 1. 将 #{} 使用?进行代替 
  78.      * 2. 解析出 #{} 里面的值进行存储 
  79.      * 
  80.      * @param sql 转换前的原sql 
  81.      * @return 
  82.      */ 
  83.     private BoundSql getBoundSql(String sql) { 
  84.         // 标记处理类: 主要是配合通用解析器 GenericTokenParser 类完成对配置文件等的解析工作 其中TokenHandler 主要完成处理 
  85.         ParameterMappingTokenHandler parameterMappingTokenHandler = new ParameterMappingTokenHandler(); 
  86.  
  87.         // GenericTokenParser: 通用的标记解析器 完成了代码片段中的占位符的解析 然后根据给定的标记处理器( TokenHandler ) 来进行表达式的处理 
  88.  
  89.         // 三个参数: 分别为 openToken (开始标记)、 closeToken (结束标记)、 handler (标记处理器) 
  90.         GenericTokenParser genericTokenParse = new GenericTokenParser("#{""}", parameterMappingTokenHandler); 
  91.         // 解析出来的sql 
  92.         String parseSql = genericTokenParse.parse(sql); 
  93.         // #{} 里面解析出来的参数名称 
  94.         List<ParameterMapping> parameterMappings = parameterMappingTokenHandler.getParameterMappings(); 
  95.  
  96.         BoundSql boundSql = new BoundSql(parseSql, parameterMappings); 
  97.  
  98.         return boundSql; 
  99.  
  100.     } 
  101.  
  102.     @Override 
  103.     public void close() throws SQLException { 
  104.         connection.close(); 
  105.     } 

  1. public class BoundSql { 
  2.     // 解析过后的 sql 语句 
  3.     private String sqlText; 
  4.  
  5.     // 解析出来的参数 
  6.     private List<ParameterMapping> parameterMappingList = new ArrayList<>(); 
  7.      
  8.     // 有参构造方便创建时赋值 
  9.     public BoundSql(String sqlText, List<ParameterMapping> parameterMappingList) { 
  10.         this.sqlText = sqlText; 
  11.         this.parameterMappingList = parameterMappingList; 
  12.     } 
  13.  
  14.    ... 省略getter setter 方法 
  15.  

6.测试代码

  1. public class IPersistenceTest { 
  2.  
  3.     @Test 
  4.     public void test () throws Exception { 
  5.  
  6.         InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml"); 
  7.         SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream); 
  8.         SqlSession sqlSession = sessionFactory.openSession(); 
  9.  
  10.         User user = new User(); 
  11.         user.setId(1); 
  12.         user.setUsername("bd2star"); 
  13.         User res = sqlSession.selectOne("user.selectOne"user); 
  14.         System.out.println(res); 
  15.          
  16.         // 关闭资源 
  17.        sqlSession.close() 
  18.     } 

运行结果如下

  1. User{id=1, username='bd2star'

测试通过 调整代码

创建 接口 Dao及实现类

  1. public interface IUserDao { 
  2.  
  3.     // 查询所有用户 
  4.     public List<User> selectList() throws Exception; 
  5.  
  6.  
  7.     // 根据条件进行用户查询 
  8.     public User selectOne(User user) throws Exception; 

  1. public class UserDaoImpl implements IUserDao { 
  2.     @Override 
  3.     public List<User> findAll() throws Exception { 
  4.         InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml"); 
  5.         SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream); 
  6.         SqlSession sqlSession = sessionFactory.openSession(); 
  7.         List<User> res = sqlSession.selectList("user.selectList"); 
  8.         sqlSession.close(); 
  9.         return res; 
  10.     } 
  11.  
  12.     @Override 
  13.     public User findByCondition(User user) throws Exception { 
  14.         InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml"); 
  15.         SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream); 
  16.         SqlSession sqlSession = sessionFactory.openSession(); 
  17.         User res = sqlSession.selectOne("user.selectOne"user); 
  18.         sqlSession.close(); 
  19.         return res; 
  20.  
  21.     } 

调整测试方法

  1. public class IPersistenceTest { 
  2.  
  3.     @Test 
  4.     public void test () throws Exception { 
  5.         User user = new User(); 
  6.         user.setId(1); 
  7.         user.setUsername("bd2star"); 
  8.         IUserDao userDao = new UserDaoImpl(); 
  9.         User res = userDao.findByCondition(user); 
  10.         System.out.println(res); 
  11.     } 

运行结果如下

  1. User{id=1, username='bd2star'

测试通过

7.补充

huodd.sql

  1. --新建数据库 
  2. CREATE DATABASE huodd; 
  3. --使用数据库 
  4. use huodd; 
  5. --创建表 
  6. CREATE TABLE `user`  ( 
  7.   `id` int(11) NOT NULL
  8.   `username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL
  9.   PRIMARY KEY (`id`) USING BTREE 
  10. ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Compact; 
  11. -- 插入测试数据 
  12. INSERT INTO `userVALUES (1, 'bd2star'); 
  13. INSERT INTO `userVALUES (2, 'bd3star'); 

用到的工具类

GenericTokenParser.java

  1. public class GenericTokenParser { 
  2.  
  3.   private final String openToken; //开始标记 
  4.   private final String closeToken; //结束标记 
  5.   private final TokenHandler handler; //标记处理器 
  6.  
  7.   public GenericTokenParser(String openToken, String closeToken, TokenHandler handler) { 
  8.     this.openToken = openToken; 
  9.     this.closeToken = closeToken; 
  10.     this.handler = handler; 
  11.   } 
  12.  
  13.   /** 
  14.    * 解析${}和#{} 
  15.    * @param text 
  16.    * @return 
  17.    * 该方法主要实现了配置文件、脚本等片段中占位符的解析、处理工作,并返回最终需要的数据。 
  18.    * 其中,解析工作由该方法完成,处理工作是由处理器handler的handleToken()方法来实现 
  19.    */ 
  20.   public String parse(String text) { 
  21.     // 验证参数问题,如果是null,就返回空字符串。 
  22.     if (text == null || text.isEmpty()) { 
  23.       return ""
  24.     } 
  25.  
  26.     // 下面继续验证是否包含开始标签,如果不包含,默认不是占位符,直接原样返回即可,否则继续执行。 
  27.     int start = text.indexOf(openToken, 0); 
  28.     if (start == -1) { 
  29.       return text; 
  30.     } 
  31.  
  32.    // 把text转成字符数组src,并且定义默认偏移量offset=0、存储最终需要返回字符串的变量builder, 
  33.     // text变量中占位符对应的变量名expression。判断start是否大于-1(即text中是否存在openToken),如果存在就执行下面代码 
  34.     char[] src = text.toCharArray(); 
  35.     int offset = 0; 
  36.     final StringBuilder builder = new StringBuilder(); 
  37.     StringBuilder expression = null
  38.     while (start > -1) { 
  39.      // 判断如果开始标记前如果有转义字符,就不作为openToken进行处理,否则继续处理 
  40.       if (start > 0 && src[start - 1] == '\\') { 
  41.         builder.append(src, offset, start - offset - 1).append(openToken); 
  42.         offset = start + openToken.length(); 
  43.       } else { 
  44.         //重置expression变量,避免空指针或者老数据干扰。 
  45.         if (expression == null) { 
  46.           expression = new StringBuilder(); 
  47.         } else { 
  48.           expression.setLength(0); 
  49.         } 
  50.         builder.append(src, offset, start - offset); 
  51.         offset = start + openToken.length(); 
  52.         int end = text.indexOf(closeToken, offset); 
  53.         while (end > -1) {////存在结束标记时 
  54.           if (end > offset && src[end - 1] == '\\') {//如果结束标记前面有转义字符时 
  55.             // this close token is escaped. remove the backslash and continue
  56.             expression.append(src, offset, end - offset - 1).append(closeToken); 
  57.             offset = end + closeToken.length(); 
  58.             end = text.indexOf(closeToken, offset); 
  59.           } else {//不存在转义字符,即需要作为参数进行处理 
  60.             expression.append(src, offset, end - offset); 
  61.             offset = end + closeToken.length(); 
  62.             break; 
  63.           } 
  64.         } 
  65.         if (end == -1) { 
  66.           // close token was not found. 
  67.           builder.append(src, start, src.length - start); 
  68.           offset = src.length; 
  69.         } else { 
  70.           //首先根据参数的key(即expression)进行参数处理,返回?作为占位符 
  71.           builder.append(handler.handleToken(expression.toString())); 
  72.           offset = end + closeToken.length(); 
  73.         } 
  74.       } 
  75.       start = text.indexOf(openToken, offset); 
  76.     } 
  77.     if (offset < src.length) { 
  78.       builder.append(src, offset, src.length - offset); 
  79.     } 
  80.     return builder.toString(); 
  81.   } 

ParameterMapping.java

  1. public class ParameterMapping { 
  2.  
  3.     private String content; 
  4.  
  5.     public ParameterMapping(String content) { 
  6.         this.content = content; 
  7.     } 
  8.  
  9.     ... 省略getter setter 方法 

ParameterMappingTokenHandler.java

  1. public class ParameterMappingTokenHandler implements TokenHandler { 
  2.    private List<ParameterMapping> parameterMappings = new ArrayList<ParameterMapping>(); 
  3.  
  4.    // context是参数名称 #{id} #{username} 
  5.  
  6.    public String handleToken(String content) { 
  7.       parameterMappings.add(buildParameterMapping(content)); 
  8.       return "?"
  9.    } 
  10.  
  11.    private ParameterMapping buildParameterMapping(String content) { 
  12.       ParameterMapping parameterMapping = new ParameterMapping(content); 
  13.       return parameterMapping; 
  14.    } 
  15.  
  16.    public List<ParameterMapping> getParameterMappings() { 
  17.       return parameterMappings; 
  18.    } 
  19.  
  20.    public void setParameterMappings(List<ParameterMapping> parameterMappings) { 
  21.       this.parameterMappings = parameterMappings; 
  22.    } 
  23.  

TokenHandler.java

  1. public interface TokenHandler { 
  2.   String handleToken(String content); 

继续优化自定义框架

通过上述自定义框架,我们解决了JDBC操作数据库带来的一些问题,例如频繁创建释放数据库连接,硬编码,手动封装返回结果等问题

但从测试类可以发现新的问题

  • dao 的实现类存在重复代码 整个操作的过程模板重复 (如创建 SqlSession 调用 SqlSession方法 关闭 SqlSession)
  • dao 的实现类中存在硬编码,如调用 sqlSession 方法时 参数 statementId 的硬编码

解决方案

  • 通过代码模式来创建接口的代理对象

1.添加getMapper方法

删除dao的实现类 UserDaoImpl.java 我们通过代码来实现原来由实现类执行的逻辑

在 SqlSession 中添加 getMapper 方法

  1. public interface SqlSession { 
  2.    <T> T getMapper(Class<?> mapperClass); 

2. 实现类实现方法

DefaultSqlSession 类中实现 getMapper 方法

  1. @Override 
  2. public <T> T getMapper(Class<?> mapperClass) { 
  3.     // 使用 JDK 动态代理 来为 Dao 接口生成代理对象 并返回 
  4.     Object proxyInstance = Proxy.newProxyInstance(DefaultSqlSession.class.getClassLoader(), new Class[]{mapperClass}, new InvocationHandler() { 
  5.         /** 
  6.          * 
  7.          * @param proxy 当前代理对象的引用 
  8.          * @param method 当前被调用方法的引用 
  9.          * @param args 传递的参数 
  10.          * @return 
  11.          * @throws Throwable 
  12.          */ 
  13.         @Override 
  14.         public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { 
  15.             // 底层都还是去执行 JDBC 代码  -> 根据不同情况 调用 selectList() 或者 selectOne() 
  16.             // 准备参数  1. statmentId sql语句的唯一标识  namespace.id = 接口全限定名.方法名 
  17.             //          2. params -> args 
  18.              
  19.             // 拿到的是方法名 findAll 
  20.             String methodName = method.getName(); 
  21.             // 拿到该类的全限定类名 com.huodd.dao.IUserDao 
  22.             String className = method.getDeclaringClass().getName(); 
  23.  
  24.             String statmentId = className + "." + methodName; 
  25.  
  26.             // 获取被调用方法的返回值类型 
  27.             Type genericReturnType = method.getGenericReturnType(); 
  28.             // 判断是否进行了 泛型类型参数化 
  29.             if (genericReturnType instanceof ParameterizedType) { 
  30.                 List<Object> list = selectList(statmentId, args); 
  31.                 return list; 
  32.             } 
  33.             return selectOne(statmentId, args); 
  34.         } 
  35.     }); 
  36.  
  37.     return (T) proxyInstance; 

3.调整mapper.xml配置文件

这里要注意两点

namespace 与 dao 接口的全限定类名保持一致

id 与 dao 接口中定义的方法名保持一致

  1. <mapper namespace="com.huodd.dao.IUserDao"
  2.  
  3.     <!-- sql 的唯一标识: namespace.id 组成 => statementId 如 当前的为 Userselect.List --> 
  4.     <select id="findAll" resultType="com.huodd.pojo.User" paramterType="com.huodd.pojo.User"
  5.         select * from user 
  6.     </select
  7.  
  8.     <select id="findByCondition" paramterType="com.huodd.pojo.User" resultType="com.huodd.pojo.User"
  9.         select * from user where id = #{id} and username =#{username} 
  10.     </select
  11.  
  12. </mapper> 

4. 进入测试

  1. public class IPersistenceTest { 
  2.  
  3.     @Test 
  4.     public void test () throws Exception { 
  5.  
  6.         InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml"); 
  7.         SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream); 
  8.         SqlSession sqlSession = sessionFactory.openSession(); 
  9.         User user = new User(); 
  10.         user.setId(1); 
  11.         user.setUsername("bd2star"); 
  12.   // 此时返回的 userDao 就是代理对象 所以它的类型就是 Proxy 
  13.         IUserDao userDao = sqlSession.getMapper(IUserDao.class); 
  14.         // userDao 是代理对象  调用了接口中的 findAll()  代理对象调用接口中任意方法 都会执行 invoke() 
  15.         List<User> users = userDao.findAll(); 
  16.         System.out.println(users); 
  17.         User res = userDao.findByCondition(user); 
  18.         System.out.println(res); 
  19.  
  20.     } 

运行结果如下

  1. [User{id=1, username='bd2star'}, User{id=2, username='bd3star'}] 
  2. User{id=1, username='bd2star'

目录结构调整

将代码分为两个模块

  • 提供端(自定义持久层框架-本质就是对JDBC代码的封装)
  • 使用端 (引用持久层框架的jar )
  • 包含数据库配置信息
  • 包含sql配置信息
  • 包含sql语句
  • 参数类型
  • 返回值类型

项目目录结构最终为

提供端


使用端


源码地址 https://gitee.com/bx2star/mybatis-learning.git

 

责任编辑:姜华 来源: PoXing
相关推荐

2023-02-27 09:38:36

Springbootstarter

2023-01-03 09:35:34

SpringbootStarter

2009-06-17 16:00:03

Hibernate自定

2009-09-29 10:37:29

Hibernate持久

2009-06-25 14:53:35

自定义UI组件JSF框架

2009-07-07 14:32:47

JDK日志Formatter

2024-11-18 09:18:21

Gin框架验证器

2015-02-12 15:33:43

微信SDK

2009-07-07 14:00:25

JDK日志Handler

2015-02-12 15:38:26

微信SDK

2024-04-09 08:41:41

JVM类加载Java

2020-05-21 18:52:06

PHP框架CodeigniterWeb开发

2016-11-16 21:55:55

源码分析自定义view androi

2016-12-26 15:25:59

Android自定义View

2011-06-23 10:49:13

Qt 自定义信号

2009-07-06 16:59:26

JSP自定义标签

2013-06-27 11:10:01

iOS开发自定义UISlider

2010-09-14 16:47:23

SQL自定义函数

2013-04-19 10:14:24

2021-12-28 15:38:46

Traefik中间件插件
点赞
收藏

51CTO技术栈公众号