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

网友投稿 694 2023-05-24

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

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

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

1.JDBC 编码的弊端

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

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

JDBC代码

解决问题的思路

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

编码前思路整理

1.创建、读取配置文件

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

Configuration :核心配置类 ,存放 sqlMapConfig.xml解析出来的内容MappedStatement:映射配置类:存放mapper.xml解析出来的内容

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

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

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

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

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

定义对数据库的CRUD操作

selectList()selectOne()update()delete()

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

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

6.测试代码

用到的设计模式

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

进入编码

1.创建、读取配置文件

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

userMapper.xml 存放sql配置信息

User.java

public class User {     private Integer id;     private String username;      ... 省略getter setter 方法     ... 省略 toString 方法 }

pom.xml 中引入依赖

     mysql     mysql-connector-java     5.1.17       c3p0     c3p0     0.9.1.2       log4j     log4j     1.2.12       junit     junit     4.10       dom4j     dom4j     1.6.1       jaxen     jaxen     1.1.6 

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

public class Configuration {      // 数据源     private DataSource dataSource;     //map集合 key:statementId value:MappedStatement     private Map mappedStatementMap = new HashMap<>();      ... 省略getter setter 方法 }

public class MappedStatement {      // id     private String id;     // sql 语句     private String sql;     // 参数值类型     private Class paramterType;     // 返回值类型     private Class resultType;     ... 省略getter setter 方法 }

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

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

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

创建 SqlSessionFactoryBuilder类 并添加 build 方法

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

public class XMLMapperBuilder {      private Configuration configuration;      public XMLMapperBuilder(Configuration configuration) {         this.configuration = configuration;     }      public void parse(InputStream inputStream) throws DocumentException, ClassNotFoundException {         Document document = new SAXReader().read(inputStream);         //          Element rootElement = document.getRootElement();         String namespace = rootElement.attributeValue("namespace");         List select = rootElement.selectNodes("//select");         for (Element element : select) {             // 获取 id 的值             String id = element.attributeValue("id");             String paramterType = element.attributeValue("paramterType");             String resultType = element.attributeValue("resultType");             // 输入参数 class             Class paramterTypeClass = getClassType(paramterType);             // 返回结果 class             Class resultTypeClass = getClassType(resultType);             // sql 语句             String sqlStr = element.getTextTrim();              // 封装 mappedStatement             MappedStatement mappedStatement = new MappedStatement();             mappedStatement.setId(id);             mappedStatement.setParamterType(paramterTypeClass);             mappedStatement.setResultType(resultTypeClass);             mappedStatement.setSql(sqlStr);              // statementId             String key = namespace + "." + id;             // 填充 configuration             configuration.getMappedStatementMap().put(key, mappedStatement);         }      }      private Class getClassType(String paramterType) throws ClassNotFoundException {         Class aClass = Class.forName(paramterType);         return aClass;     } }

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

public interface SqlSessionFactory {     SqlSession openSession(); }

public class DefaultSqlSessionFactory implements SqlSessionFactory {      private Configuration configuration;      public DefaultSqlSessionFactory(Configuration configuration) {         this.configuration = configuration;     }      @Override     public SqlSession openSession() {         return new DefaultSqlSession(configuration);     } }

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

public interface SqlSession {       List selectList(String statementId, Object... param) throws Exception;       T selectOne(String statementId, Object... params) throws Exception;      void close() throws SQLException;     }

public class DefaultSqlSession implements SqlSession {      private Configuration configuration;      // 处理器对象     private Executor simpleExcutor = new SimpleExecutor();      public DefaultSqlSession(Configuration configuration) {         this.configuration = configuration;     }      @Override     public  List selectList(String statementId, Object... param) throws Exception {         // 完成对 simpleExcutor里的query方法的调用         MappedStatement mappedStatement = configuration.getMappedStatementMap().get(statementId);         List list = simpleExcutor.query(configuration, mappedStatement, param);         return list;     }      @Override     public  T selectOne(String statementId, Object... params) throws Exception {         List objects = selectList(statementId, params);         if (objects.size() == 1) {             return (T) objects.get(0);         } else {             throw new RuntimeException("返回结果过多");         }     }      @Override     public void close() throws SQLException {         simpleExcutor.close();     }  }

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

public interface Executor {       List query(Configuration configuration, MappedStatement mappedStatement, Object... param) throws Exception;      void close() throws SQLException; }

public class SimpleExecutor implements Executor {       private Connection connection = null;      @Override     public  List query(Configuration configuration, MappedStatement mappedStatement, Object... param) throws Exception {         // 注册驱动 获取连接         connection = configuration.getDataSource().getConnection();          // select * from user where id = #{id} and username = #{username}         String sql = mappedStatement.getSql();          // 对 sql 进行处理         BoundSql boundSql = getBoundSql(sql);          // select * from where id = ? and username = ?         String finalSql = boundSql.getSqlText();          // 获取传入参数类对象         Class paramterTypeClass = mappedStatement.getParamterType();          // 获取预处理 preparedStatement 对象         PreparedStatement preparedStatement = connection.prepareStatement(finalSql);          // 设置参数         List parameterMappingList = boundSql.getParameterMappingList();         for (int i = 0; i < parameterMappingList.size(); i++) {             ParameterMapping parameterMapping = parameterMappingList.get(i);             String name = parameterMapping.getContent();              // 反射  获取某一个属性对象             Field declaredField = paramterTypeClass.getDeclaredField(name);             // 设置暴力访问             declaredField.setAccessible(true);              // 参数传递的值             Object o = declaredField.get(param[0]);             // 给占位符赋值             preparedStatement.setObject(i + 1, o);          }          // 执行sql         ResultSet resultSet = preparedStatement.executeQuery();          // 封装返回结果集         // 获取返回参数类对象         Class resultTypeClass = mappedStatement.getResultType();         ArrayList results = new ArrayList<>();         while (resultSet.next()) {             // 取出 resultSet的元数据             ResultSetMetaData metaData = resultSet.getMetaData();             E o = (E) resultTypeClass.newInstance();             int columnCount = metaData.getColumnCount();             for (int i = 1; i <= columnCount; 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);             }             results.add(o);         }         return results;     }      /**      * 转换sql语句 完成对 #{} 的解析工作      * 1. 将 #{} 使用?进行代替      * 2. 解析出 #{} 里面的值进行存储      *      * @param sql 转换前的原sql      * @return      */     private BoundSql getBoundSql(String sql) {         // 标记处理类: 主要是配合通用解析器 GenericTokenParser 类完成对配置文件等的解析工作 其中TokenHandler 主要完成处理         ParameterMappingTokenHandler parameterMappingTokenHandler = new ParameterMappingTokenHandler();          // GenericTokenParser: 通用的标记解析器 完成了代码片段中的占位符的解析 然后根据给定的标记处理器( TokenHandler ) 来进行表达式的处理          // 三个参数: 分别为 openToken (开始标记)、 closeToken (结束标记)、 handler (标记处理器)         GenericTokenParser genericTokenParse = new GenericTokenParser("#{", "}", parameterMappingTokenHandler);         // 解析出来的sql         String parseSql = genericTokenParse.parse(sql);         // #{} 里面解析出来的参数名称         List parameterMappings = parameterMappingTokenHandler.getParameterMappings();          BoundSql boundSql = new BoundSql(parseSql, parameterMappings);          return boundSql;      }      @Override     public void close() throws SQLException {         connection.close();     } }

public class BoundSql {     // 解析过后的 sql 语句     private String sqlText;      // 解析出来的参数     private List parameterMappingList = new ArrayList<>();          // 有参构造方便创建时赋值     public BoundSql(String sqlText, List parameterMappingList) {         this.sqlText = sqlText;         this.parameterMappingList = parameterMappingList;     }     ... 省略getter setter 方法  }

6.测试代码

public class IPersistenceTest {      @Test     public void test () throws Exception {          InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");         SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);         SqlSession sqlSession = sessionFactory.openSession();          User user = new User();         user.setId(1);         user.setUsername("bd2star");         User res = sqlSession.selectOne("user.selectOne", user);         System.out.println(res);                  // 关闭资源        sqlSession.close()     } }

运行结果如下

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

测试通过 调整代码

创建 接口 Dao及实现类

public interface IUserDao {      // 查询所有用户     public List selectList() throws Exception;       // 根据条件进行用户查询     public User selectOne(User user) throws Exception; }

public class UserDaoImpl implements IUserDao {     @Override     public List findAll() throws Exception {         InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");         SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);         SqlSession sqlSession = sessionFactory.openSession();         List res = sqlSession.selectList("user.selectList");         sqlSession.close();         return res;     }      @Override     public User findByCondition(User user) throws Exception {         InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");         SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);         SqlSession sqlSession = sessionFactory.openSession();         User res = sqlSession.selectOne("user.selectOne", user);         sqlSession.close();         return res;      } }

调整测试方法

public class IPersistenceTest {      @Test     public void test () throws Exception {         User user = new User();         user.setId(1);         user.setUsername("bd2star");         IUserDao userDao = new UserDaoImpl();         User res = userDao.findByCondition(user);         System.out.println(res);     } }

运行结果如下

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

测试通过

7.补充

huodd.sql

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

用到的工具类

GenericTokenParser.java

public class GenericTokenParser {    private final String openToken; //开始标记   private final String closeToken; //结束标记   private final TokenHandler handler; //标记处理器    public GenericTokenParser(String openToken, String closeToken, TokenHandler handler) {     this.openToken = openToken;     this.closeToken = closeToken;     this.handler = handler;   }    /**    * 解析${}和#{}    * @param text    * @return    * 该方法主要实现了配置文件、脚本等片段中占位符的解析、处理工作,并返回最终需要的数据。    * 其中,解析工作由该方法完成,处理工作是由处理器handler的handleToken()方法来实现    */   public String parse(String text) {     // 验证参数问题,如果是null,就返回空字符串。     if (text == null || text.isEmpty()) {       return "";     }      // 下面继续验证是否包含开始标签,如果不包含,默认不是占位符,直接原样返回即可,否则继续执行。     int start = text.indexOf(openToken, 0);     if (start == -1) {       return text;     }     // 把text转成字符数组src,并且定义默认偏移量offset=0、存储最终需要返回字符串的变量builder,     // text变量中占位符对应的变量名expression。判断start是否大于-1(即text中是否存在openToken),如果存在就执行下面代码     char[] src = text.toCharArray();     int offset = 0;     final StringBuilder builder = new StringBuilder();     StringBuilder expression = null;     while (start > -1) {      // 判断如果开始标记前如果有转义字符,就不作为openToken进行处理,否则继续处理       if (start > 0 && src[start - 1] == '\\') {         builder.append(src, offset, start - offset - 1).append(openToken);         offset = start + openToken.length();       } else {         //重置expression变量,避免空指针或者老数据干扰。         if (expression == null) {           expression = new StringBuilder();         } else {           expression.setLength(0);         }         builder.append(src, offset, start - offset);         offset = start + openToken.length();         int end = text.indexOf(closeToken, offset);         while (end > -1) {////存在结束标记时           if (end > offset && src[end - 1] == '\\') {//如果结束标记前面有转义字符时             // this close token is escaped. remove the backslash and continue.             expression.append(src, offset, end - offset - 1).append(closeToken);             offset = end + closeToken.length();             end = text.indexOf(closeToken, offset);           } else {//不存在转义字符,即需要作为参数进行处理             expression.append(src, offset, end - offset);             offset = end + closeToken.length();             break;           }         }         if (end == -1) {           // close token was not found.           builder.append(src, start, src.length - start);           offset = src.length;         } else {           //首先根据参数的key(即expression)进行参数处理,返回?作为占位符           builder.append(handler.handleToken(expression.toString()));           offset = end + closeToken.length();         }       }       start = text.indexOf(openToken, offset);     }     if (offset < src.length) {       builder.append(src, offset, src.length - offset);     }     return builder.toString();   } }

ParameterMapping.java

public class ParameterMapping {      private String content;      public ParameterMapping(String content) {         this.content = content;     }      ... 省略getter setter 方法 }

ParameterMappingTokenHandler.java

public class ParameterMappingTokenHandler implements TokenHandler {    private List parameterMappings = new ArrayList();     // context是参数名称 #{id} #{username}     public String handleToken(String content) {       parameterMappings.add(buildParameterMapping(content));       return "?";    }     private ParameterMapping buildParameterMapping(String content) {       ParameterMapping parameterMapping = new ParameterMapping(content);       return parameterMapping;    }     public List getParameterMappings() {       return parameterMappings;    }     public void setParameterMappings(List parameterMappings) {       this.parameterMappings = parameterMappings;    }  }

TokenHandler.java

public interface TokenHandler {   String handleToken(String content); }

继续优化自定义框架

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

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

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

解决方案

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

1.添加getMapper方法

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

在 SqlSession 中添加 getMapper 方法

public interface SqlSession {     T getMapper(Class mapperClass); }

2. 实现类实现方法

DefaultSqlSession 类中实现 getMapper 方法

3.调整mapper.xml配置文件

这里要注意两点

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

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

4. 进入测试

public class IPersistenceTest {      @Test     public void test () throws Exception {          InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");         SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);         SqlSession sqlSession = sessionFactory.openSession();         User user = new User();         user.setId(1);         user.setUsername("bd2star");   // 此时返回的 userDao 就是代理对象 所以它的类型就是 Proxy         IUserDao userDao = sqlSession.getMapper(IUserDao.class);         // userDao 是代理对象  调用了接口中的 findAll()  代理对象调用接口中任意方法 都会执行 invoke()         List users = userDao.findAll();         System.out.println(users);         User res = userDao.findByCondition(user);         System.out.println(res);      } }

运行结果如下

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

目录结构调整

将代码分为两个模块

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

项目目录结构最终为

提供端

使用端

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:Redis 核心篇:唯快不破的秘密
下一篇:上了热门才发现!Airbnb又一开源力作
相关文章