麒麟v10 上部署 TiDB v5.1.2 生产环境优化实践
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 中引入依赖
创建两个JavaBean对象 用于存储解析的配置文件的内容(Configuration.java、MappedStatement.java)
public class Configuration { // 数据源 private DataSource dataSource; //map集合 key:statementId value:MappedStatement private Map
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); //
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 {
public class DefaultSqlSession implements SqlSession { private Configuration configuration; // 处理器对象 private Executor simpleExcutor = new SimpleExecutor(); public DefaultSqlSession(Configuration configuration) { this.configuration = configuration; } @Override public
5.创建Executor接口及实现类SimpleExecutor实现类
public interface Executor {
public class SimpleExecutor implements Executor { private Connection connection = null; @Override public
public class BoundSql { // 解析过后的 sql 语句 private String sqlText; // 解析出来的参数 private List
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
public class UserDaoImpl implements IUserDao { @Override public List
调整测试方法
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
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 {
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
运行结果如下
[User{id=1, username='bd2star'}, User{id=2, username='bd3star'}] User{id=1, username='bd2star'}
目录结构调整
将代码分为两个模块
提供端(自定义持久层框架-本质就是对JDBC代码的封装)使用端 (引用持久层框架的jar )包含数据库配置信息包含sql配置信息包含sql语句参数类型返回值类型
项目目录结构最终为
提供端
使用端
版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。