作者:小傅哥
博客:https://bugstack.cn
积淀、分享、成长,让本人和别人都能有所播种!
一、前言
你这代码写的,咋这么轴呢!
说到轴,让我想起初中上学时老师说的话:“你那脑瓜子,咋跟手焖子似的!” 西南话手焖子就是那种冬天戴的大棉手套,棉手套里的棉花都被压的又沉又硬的了,所以来比喻脑瓜子笨。
而写轴代码的大部分都是刚毕业没多久,或者刚开始工作的码农,毕竟经验不足经验不多,写出一些不太好保护的代码也情有可原。而那些绝对多数锤炼进去的老码农,其实代码的稳固水平、设计教训、周密逻辑,都是相对来说要好很多的。当然一部分老码农,只是老了而已,代码还是那个代码!
所以企业招聘些年轻人,须要年老的思维。但没必要嚯嚯只是头发没多少的老码农,否则谁来给你安稳落地你那些天马行空的想法呢!难道体验、稳固、晦涩,不应该是更值得谋求的,非得喜爱全是愣头青似的代码,写出几百个bug,造成大量资损和客诉,让老板感觉很爽?
二、指标
上一章节,小傅哥带着大家细化的 XML 语句构建器,解耦在解析 XML 中的所须要解决的 Mapper 信息,包含;SQL、入参、出参、类型,并对这些信息进行记录到 ParameterMapping 参数映射解决类中。那么这个一章节咱们将联合这部分参数的提取,对执行的 SQL 进行参数的自动化设置,而不是像咱们之前那样把参数写成固定的,如图 10-1 所示
- 在流程上,通过 DefaultSqlSession#selectOne 办法调用执行器,并通过预处理语句处理器 PreparedStatementHandler 执行参数设置和后果查问。
- 那么这个流程中咱们所解决的参数信息,也就是每个 SQL 执行时,那些
?号
须要被替换的中央,目前是通过硬编码的形式进行解决的。而这就是本章节须要解决的问题,如果只是硬编码实现参数设置,那么对于所有那些不同类型的参数就没法进行操作了。 - 所以本章节须要联合联合上一章节所实现的语句构建器对 SQL 参数信息的拆解,本章将会依照这些参数的解析,解决这里硬编码为自动化类型设置。针对不同类型的参数设置,这部分应用了什么设计模式呢?
三、设计
这里能够思考下,参数的解决也就是通常咱们应用 JDBC 间接操作数据库时,所应用 ps.setXxx(i, parameter);
设置的各类参数。那么在自动化解析 XML 中 SQL 拆分出所有的参数类型后,则应该依据不同的参数进行不同的类型设置,也就;Long 调用 ps.setLong
、String 调用 ps.setString
所以这里须要应用策略模式,在解析 SQL 时依照不同的执行策略,封装进去类型处理器(也就是是实现 TypeHandler<T> 接口的过程)。整体设计如图 10-2 所示
- 其实对于参数的解决,因为有很多的类型(
Long\String\Object\...
),所以这里最重要的体现则是策略模式的应用。 - 这里包含了构建参数时依据类型,抉择对应的策略类型处理器,填充到参数映射汇合中。另外一方面是参数的应用,也就是在执行 DefaultSqlSession#selectOne 的链路中,包含了参数的设置,依照参数的不同类型,获取出对应的处理器,以及入参值。留神:因为入参值可能是一个对象中的属性,所以这里咱们用到了后面章节实现的反射类工具 MetaObject 进行值的获取,防止因为动静的对象,没法硬编码获取属性值。
四、实现
1. 工程构造
mybatis-step-09└── src ├── main │ └── java │ └── cn.bugstack.mybatis │ ├── binding │ │ ├── MapperMethod.java │ │ ├── MapperProxy.java │ │ ├── MapperProxyFactory.java │ │ └── MapperRegistry.java │ ├── builder │ │ ├── xml │ │ │ ├── XMLConfigBuilder.java │ │ │ ├── XMLMapperBuilder.java │ │ │ └── XMLStatementBuilder.java │ │ ├── BaseBuilder.java │ │ ├── ParameterExpression.java │ │ ├── SqlSourceBuilder.java │ │ └── StaticSqlSource.java │ ├── datasource │ ├── executor │ │ ├── resultset │ │ │ └── ParameterHandler.java │ │ ├── resultset │ │ │ ├── DefaultResultSetHandler.java │ │ │ └── ResultSetHandler.java │ │ ├── statement │ │ │ ├── BaseStatementHandler.java │ │ │ ├── PreparedStatementHandler.java │ │ │ ├── SimpleStatementHandler.java │ │ │ └── StatementHandler.java │ │ ├── BaseExecutor.java │ │ ├── Executor.java │ │ └── SimpleExecutor.java │ ├── io │ ├── mapping │ │ ├── BoundSql.java │ │ ├── Environment.java │ │ ├── MappedStatement.java │ │ ├── ParameterMapping.java │ │ ├── SqlCommandType.java │ │ └── SqlSource.java │ ├── parsing │ ├── reflection │ ├── scripting │ │ ├── defaults │ │ │ └── DefaultParameterHandler.java │ │ ├── xmltags │ │ │ ├── DynamicContext.java │ │ │ ├── MixedSqlNode.java │ │ │ ├── SqlNode.java │ │ │ ├── StaticTextSqlNode.java │ │ │ ├── XMLLanguageDriver.java │ │ │ └── XMLScriptBuilder.java │ │ ├── LanguageDriver.java │ │ └── LanguageDriverRegistry.java │ ├── session │ │ ├── defaults │ │ │ ├── DefaultSqlSession.java │ │ │ └── DefaultSqlSessionFactory.java │ │ ├── Configuration.java │ │ ├── ResultHandler.java │ │ ├── SqlSession.java │ │ ├── SqlSessionFactory.java │ │ ├── SqlSessionFactoryBuilder.java │ │ └── TransactionIsolationLevel.java │ ├── transaction │ └── type │ ├── BaseTypeHandler.java │ ├── JdbcType.java │ ├── LongTypeHandler.java │ ├── StringTypeHandler.java │ ├── TypeAliasRegistry.java │ ├── TypeHandler.java │ └── TypeHandlerRegistry.java └── test ├── java │ └── cn.bugstack.mybatis.test.dao │ ├── dao │ │ └── IUserDao.java │ ├── po │ │ └── User.java │ └── ApiTest.java └── resources ├── mapper │ └──User_Mapper.xml └── mybatis-config-datasource.xml
工程源码:https://github.com/fuzhengwei/small-mybatis
应用策略模式,解决参数处理器外围类关系,如图 10-3 所示
外围解决次要分为三块;类型解决、参数设置、参数应用;
- 以定义 TypeHandler 类型处理器策略接口,实现不同的解决策略,包含;Long、String、Integer 等。这里咱们先只实现2种类型,读者在学习过程中,能够依照这个构造来增加其余类型。
- 类型策略处理器实现实现后,须要注册到处理器注册机中,后续其余模块参数的设置还是应用都是从 Configuration 中获取到 TypeHandlerRegistry 进行应用。
- 那么有了这样的策略处理器当前,在进行操作解析 SQL 的时候,就能够依照不同的类型把对应的策略处理器设置到 BoundSql#parameterMappings 参数里,后续应用也是从这里进行获取。
2. 入参数校准
这里咱们要先解决一个小问题,不晓得读者在咱们所实现的源码中,是否留神到这样一个参数的传递,如图 10-4
- 这里的参数传递后,须要获取第0个参数,而且是硬编码固定的。这是为什么呢?这个第0个参数是哪来的,咱们接口外面调用的办法,参数不是一个吗?就像:
User queryUserInfoById(Long id);
其实这个参数来自于映射器代理类 MapperProxy#invoke 中,因为 invoke 反射调用的办法,入参中是 Object[] args,所以这个参数被传递到后续的参数设置中。而咱们的 DAO 测试类是一个已知的固定参数,所以前面硬编码了获取了第0个参数。
- JDK 反射调用办法操作固定办法入参
- 那么联合这样的问题,咱们则须要依据办法的信息,给办法做签名操作,以便于转换入参信息为办法的信息。比方数组转换为对应的对象。
源码详见:cn.bugstack.mybatis.binding.MapperMethod
public class MapperMethod { public Object execute(SqlSession sqlSession, Object[] args) { Object result = null; switch (command.getType()) { case SELECT: Object param = method.convertArgsToSqlCommandParam(args); result = sqlSession.selectOne(command.getName(), param); break; default: throw new RuntimeException("Unknown execution method for: " + command.getName()); } return result; } /** * 办法签名 */ public static class MethodSignature { public Object convertArgsToSqlCommandParam(Object[] args) { final int paramCount = params.size(); if (args == null || paramCount == 0) { // 如果没参数 return null; } else if (paramCount == 1) { return args[params.keySet().iterator().next().intValue()]; } else { // 否则,返回一个ParamMap,批改参数名,参数名就是其地位 final Map<String, Object> param = new ParamMap<Object>(); int i = 0; for (Map.Entry<Integer, String> entry : params.entrySet()) { // 1.先加一个#{0},#{1},#{2}...参数 param.put(entry.getValue(), args[entry.getKey().intValue()]); // ... } return param; } } }}
- 在映射器办法中 MapperMethod#execute 将原来的间接将参数 args 传递给 SqlSession#selectOne 办法,调整为转换后再传递对象。
- 其实这里的转换操作就是来自于 Method#getParameterTypes 对参数的获取和解决,与 args 进行比对。如果是单个参数,则间接返回参数 Tree 树结构下的对应节点值。非单个类型,则须要进行循环解决,这样转换后的参数能力被间接应用。
3. 参数策略处理器
在 Mybatis 的源码包中,有一个 type 包,这个包下所提供的就是一套参数的解决策略汇合。它通过定义类型处理器接口、由形象模板实现并定义规范流程,定提取形象办法交给子类实现,这些子类就是各个类型处理器的具体实现。
3.1 策略接口
源码详见:cn.bugstack.mybatis.type.TypeHandler
public interface TypeHandler<T> { /** * 设置参数 */ void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;}
- 首先定义一个类型处理器的接口,这和咱们在日常的业务开发中是相似的,就像如果是发货商品,则定义一个对立标准接口,之后依据这个接口实现出不同的发货策略。
- 这里设置参数也是一样,所有不同类型的参数,都能够被提取进去这些规范的参数字段和异样,后续的子类依照这个规范实现即可。Mybatis 源码中有30+个类型解决
3.2 模板模式
源码详见:cn.bugstack.mybatis.type.BaseTypeHandler
public abstract class BaseTypeHandler<T> implements TypeHandler<T> { @Override public void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException { // 定义形象办法,由子类实现不同类型的属性设置 setNonNullParameter(ps, i, parameter, jdbcType); } protected abstract void setNonNullParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;}
- 通过形象基类的流程模板定义,便于一些参数的判断和解决。不过目前咱们还不须要那么多的流程校验,所以这里只是定义和调用了一个最根本的形象办法 setNonNullParameter。
- 不过有一个这样的构造,能够让大家更加分明整个 Mybatis 源码的框架,便于后续浏览或者扩大此局部源码的时候,有一个框架结构的认知。
3.3 子类实现
源码详见:cn.bugstack.mybatis.type.*
/** * @description Long类型处理器 */public class LongTypeHandler extends BaseTypeHandler<Long> { @Override protected void setNonNullParameter(PreparedStatement ps, int i, Long parameter, JdbcType jdbcType) throws SQLException { ps.setLong(i, parameter); }}/** * @description String类型处理器 */public class StringTypeHandler extends BaseTypeHandler<String>{ @Override protected void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException { ps.setString(i, parameter); }}
- 这里的接口实现举了个例子,别离是;LongTypeHandler、StringTypeHandler,在 Mybatis 源码中还有很多其余类型,这里咱们临时不须要实现那么多,只有分明这个处理过程和编码方式即可。大家在学习中,能够尝试再增加几个其余类型,用于学习验证
3.4 类型注册机
类型处理器注册机 TypeHandlerRegistry 是咱们后面章节实现的,这里只须要在这个类构造下,注册新的类型就能够了。
源码详见:cn.bugstack.mybatis.type.TypeHandlerRegistry
public final class TypeHandlerRegistry { private final Map<JdbcType, TypeHandler<?>> JDBC_TYPE_HANDLER_MAP = new EnumMap<>(JdbcType.class); private final Map<Type, Map<JdbcType, TypeHandler<?>>> TYPE_HANDLER_MAP = new HashMap<>(); private final Map<Class<?>, TypeHandler<?>> ALL_TYPE_HANDLERS_MAP = new HashMap<>(); public TypeHandlerRegistry() { register(Long.class, new LongTypeHandler()); register(long.class, new LongTypeHandler()); register(String.class, new StringTypeHandler()); register(String.class, JdbcType.CHAR, new StringTypeHandler()); register(String.class, JdbcType.VARCHAR, new StringTypeHandler()); } //...}
- 这里在构造函数中,新减少了 LongTypeHandler、StringTypeHandler 两种类型的注册器。
- 同时能够留神到,无论是对象类型,还是根本类型,都是一个类型处理器。只不过在注册的时候多注册了一个。这种操作形式和咱们平时的业务开发中,也是一样的。一种是多注册,另外一种是判断解决。
4. 参数构建
绝对于后面章节所实现的内容,这个章节须要对 SqlSourceBuilder 源码构建器中,创立参数映射 ParameterMapping 须要增加参数处理器的内容。因为只有这样能力不便的从参数映射中获取到对应类型的处理器进行应用。
那么就须要欠缺 ParameterMapping 增加 TypeHandler 属性信息,以及在 ParameterMappingTokenHandler#buildParameterMapping 解决参数映射时,构建出参数的映射。这一部分是在上一章节的实现过程中,细化的欠缺局部,如图 10-6
那么联合上一章节,这里咱们开始扩大出类型的设置。同时留神 MetaClass 反射工具类的应用
源码详见:cn.bugstack.mybatis.builder.SqlSourceBuilder
// 构建参数映射private ParameterMapping buildParameterMapping(String content) { // 先解析参数映射,就是转化成一个 HashMap | #{favouriteSection,jdbcType=VARCHAR} Map<String, String> propertiesMap = new ParameterExpression(content); String property = propertiesMap.get("property"); Class<?> propertyType; if (typeHandlerRegistry.hasTypeHandler(parameterType)) { propertyType = parameterType; } else if (property != null) { MetaClass metaClass = MetaClass.forClass(parameterType); if (metaClass.hasGetter(property)) { propertyType = metaClass.getGetterType(property); } else { propertyType = Object.class; } } else { propertyType = Object.class; } logger.info("构建参数映射 property:{} propertyType:{}", property, propertyType); ParameterMapping.Builder builder = new ParameterMapping.Builder(configuration, property, propertyType); return builder.build();}
- 这一部分就是对参数的细化解决,构建出参数的映射关系,首先是 if 判断对应的参数类型是否在 TypeHandlerRegistry 注册器中,如果不在则拆解对象,按属性进行获取 propertyType 的操作。
- 这一块也用到了 MetaClass 反射工具类的应用,它的存在能够让咱们更加不便的解决,否则还须要要再写反射类进行获取对象属性操作。
5. 参数应用
参数构建实现后,就能够在 DefaultSqlSession#selectOne 调用时设置参数应用了。那么这里的链路关系;Executor#query - > SimpleExecutor#doQuery -> StatementHandler#parameterize -> PreparedStatementHandler#parameterize -> ParameterHandler#setParameters
到了 ParameterHandler#setParameters 就能够看到了依据参数的不同处理器循环设置参数。
源码详见:cn.bugstack.mybatis.scripting.defaults.DefaultParameterHandler
public class DefaultParameterHandler implements ParameterHandler { @Override public void setParameters(PreparedStatement ps) throws SQLException { List<ParameterMapping> parameterMappings = boundSql.getParameterMappings(); if (null != parameterMappings) { for (int i = 0; i < parameterMappings.size(); i++) { ParameterMapping parameterMapping = parameterMappings.get(i); String propertyName = parameterMapping.getProperty(); Object value; if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) { value = parameterObject; } else { // 通过 MetaObject.getValue 反射获得值设进去 MetaObject metaObject = configuration.newMetaObject(parameterObject); value = metaObject.getValue(propertyName); } JdbcType jdbcType = parameterMapping.getJdbcType(); // 设置参数 logger.info("依据每个ParameterMapping中的TypeHandler设置对应的参数信息 value:{}", JSON.toJSONString(value)); TypeHandler typeHandler = parameterMapping.getTypeHandler(); typeHandler.setParameter(ps, i + 1, value, jdbcType); } } }}
- 每一个循环的参数设置,都是从 BoundSql 中获取 ParameterMapping 汇合进行循环操作,而这个汇合参数就是咱们后面 ParameterMappingTokenHandler#buildParameterMapping 构建参数映射时解决的。
- 设置参数时依据参数的 parameterObject 入参的信息,判断是否根本类型,如果不是则从对象中进行拆解获取(也就是一个对象A中包含属性b),解决实现后就能够精确拿到对应的入参值了。因为在映射器办法 MapperMethod 中曾经解决了一遍办法签名,所以这里的入参就更方便使用了
- 根本信息获取实现后,则依据参数类型获取到对应的 TypeHandler 类型处理器,也就是找到 LongTypeHandler、StringTypeHandler 等,确定找到当前,则能够进行对应的参数设置了 typeHandler.setParameter(ps, i + 1, value, jdbcType) 通过这样的形式把咱们之前硬编码的操作进行解耦。
五、测试
1. 当时筹备
1.1 创立库表
创立一个数据库名称为 mybatis 并在库中创立表 user 以及增加测试数据,如下:
CREATE TABLE USER ( id bigint NOT NULL AUTO_INCREMENT COMMENT '自增ID', userId VARCHAR(9) COMMENT '用户ID', userHead VARCHAR(16) COMMENT '用户头像', createTime TIMESTAMP NULL COMMENT '创立工夫', updateTime TIMESTAMP NULL COMMENT '更新工夫', userName VARCHAR(64), PRIMARY KEY (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; insert into user (id, userId, userHead, createTime, updateTime, userName) values (1, '10001', '1_04', '2022-04-13 00:00:00', '2022-04-13 00:00:00', '小傅哥');
1.2 配置数据源
<environments default="development"> <environment id="development"> <transactionManager type="JDBC"/> <dataSource type="POOLED"> <property name="driver" value="com.mysql.jdbc.Driver"/> <property name="url" value="jdbc:mysql://127.0.0.1:3306/mybatis?useUnicode=true"/> <property name="username" value="root"/> <property name="password" value="123456"/> </dataSource> </environment></environments>
- 通过
mybatis-config-datasource.xml
配置数据源信息,包含:driver、url、username、password - 在这里 dataSource 能够按需配置成 DRUID、UNPOOLED 和 POOLED 进行测试验证。
1.3 配置Mapper
<select id="queryUserInfoById" parameterType="java.lang.Long" resultType="cn.bugstack.mybatis.test.po.User"> SELECT id, userId, userName, userHead FROM user where id = #{id}</select><select id="queryUserInfo" parameterType="cn.bugstack.mybatis.test.po.User" resultType="cn.bugstack.mybatis.test.po.User"> SELECT id, userId, userName, userHead FROM user where id = #{id} and userId = #{userId}</select>
- 这部分临时不须要调整,目前还只是一个入参的类型的参数,后续咱们全副欠缺这部分内容当前,则再提供更多的其余参数进行验证。
2. 单元测试
源码详见:cn.bugstack.mybatis.test.ApiTest
@Beforepublic void init() throws IOException { // 1. 从SqlSessionFactory中获取SqlSession SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsReader("mybatis-config-datasource.xml")); sqlSession = sqlSessionFactory.openSession();}
- 因为接下来咱们须要验证两种不同入参的单元测试,别离来测试根本类型参数和对象类型参数。
2.1 根本类型参数
@Testpublic void test_queryUserInfoById() { // 1. 获取映射器对象 IUserDao userDao = sqlSession.getMapper(IUserDao.class); // 2. 测试验证:基本参数 User user = userDao.queryUserInfoById(1L); logger.info("测试后果:{}", JSON.toJSONString(user));}
07:40:08.531 [main] INFO c.b.mybatis.builder.SqlSourceBuilder - 构建参数映射 property:id propertyType:class java.lang.Long07:40:08.598 [main] INFO c.b.m.s.defaults.DefaultSqlSession - 执行查问 statement:cn.bugstack.mybatis.test.dao.IUserDao.queryUserInfoById parameter:107:40:08.875 [main] INFO c.b.m.d.pooled.PooledDataSource - Created connection 183284570.07:40:08.894 [main] INFO c.b.m.s.d.DefaultParameterHandler - 依据每个ParameterMapping中的TypeHandler设置对应的参数信息 value:107:40:08.961 [main] INFO cn.bugstack.mybatis.test.ApiTest - 测试后果:{"id":1,"userHead":"1_04","userId":"10001","userName":"小傅哥"}
- 测试过程中能够在 DefaultParameterHandler#setParameters 中打断点,验证办法参数以及取得到的类型处理器,这里测试验证通过,能够满足根本类型对象的入参信息。
2.2 对象类型参数
@Testpublic void test_queryUserInfo() { // 1. 获取映射器对象 IUserDao userDao = sqlSession.getMapper(IUserDao.class); // 2. 测试验证:对象参数 User user = userDao.queryUserInfo(new User(1L, "10001")); logger.info("测试后果:{}", JSON.toJSONString(user));}
07:41:11.025 [main] INFO c.b.mybatis.builder.SqlSourceBuilder - 构建参数映射 property:userId propertyType:class java.lang.String07:41:11.232 [main] INFO c.b.m.s.defaults.DefaultSqlSession - 执行查问 statement:cn.bugstack.mybatis.test.dao.IUserDao.queryUserInfo parameter:{"id":1,"userId":"10001"}07:41:11.638 [main] INFO c.b.m.d.pooled.PooledDataSource - Created connection 402405659.07:41:11.661 [main] INFO c.b.m.s.d.DefaultParameterHandler - 依据每个ParameterMapping中的TypeHandler设置对应的参数信息 value:107:43:28.516 [main] INFO c.b.m.s.d.DefaultParameterHandler - 依据每个ParameterMapping中的TypeHandler设置对应的参数信息 value:"10001"07:43:30.820 [main] INFO cn.bugstack.mybatis.test.ApiTest - 测试后果:{"id":1,"userHead":"1_04","userId":"10001","userName":"小傅哥"}
- 此案例次要验证对象参数 User 中蕴含两个属性时,查看咱们的代码处理过程,验证是否能够正确获取到两个类型处理器,别离设置参数的过程。
- 从测试后果中,能够看到测试通过,并打印了相干参数的构建和应用。
六、总结
- 到本章节,咱们算是把一个 ORM 框架的根本流程串联起来了,不要硬编码也能实现简略 SQL 的解决。读者搭档能够仔细阅读下以后框架中,所蕴含的分包构造。比方:构建、绑定、映射、反射、执行、类型、事务、数据源等等,尝试画一画他们的链接关系,这样会让你更加清晰当初的代码解耦构造。
- 此章节中比拟重要的体现是对于参数类型的策略化设计,通过策略解耦,模板定义流程,让咱们整个参数设置变得更加清晰,也就不须要硬编码了。
- 除此之外也有一些细节的性能点,如;MapperMethod 中增加办法签名、类型处理器创立和应用时候,都应用了 MetaObject 这样的反射器工具类进行解决。这些细节的性能点,读者须要在学习的过程中,进行调试验证能力更好的排汇此类编码设计的技巧和教训。