作者:小傅哥
博客:https://bugstack.cn
沉淀、分享、成长,让自己和他人都能有所收获!??
你这代码写的,咋这么轴呢!
说到轴,让我想起初中上学时老师说的话:“你那脑瓜子,咋跟手焖子似的!” 东北话手焖子就是那种冬天戴的大棉手套,棉手套里的棉花都被压的又沉又硬的了,所以来比喻脑瓜子笨。
而写轴代码的大部分都是刚毕业没多久,或者刚开始工作的码农,毕竟经验不足经历不多,写出一些不太好维护的代码也情有可原。而那些绝对多数锻炼出来的老码农,其实代码的稳定程度、设计经验、缜密逻辑,都是相对来说要好很多的。当然一部分老码农,只是老了而已,代码还是那个代码!
所以企业招聘些年轻人,需要年轻的思想。但没必要嚯嚯只是头发没多少的老码农,否则谁来给你平稳落地你那些天马行空的想法呢!难道体验、稳定、流畅,不应该是更值得追求的,非得喜欢全是愣头青似的代码,写出几百个bug,造成大量资损和客诉,让老板觉得很爽?
上一章节,小傅哥带着大家细化的 XML 语句构建器,解耦在解析 XML 中的所需要处理的 Mapper 信息,包括;SQL、入参、出参、类型,并对这些信息进行记录到 ParameterMapping 参数映射处理类中。那么这个一章节我们将结合这部分参数的提取,对执行的 SQL 进行参数的自动化设置,而不是像我们之前那样把参数写成固定的,如图 10-1 所示

?号 需要被替换的地方,目前是通过硬编码的方式进行处理的。而这就是本章节需要解决的问题,如果只是硬编码完成参数设置,那么对于所有那些不同类型的参数就没法进行操作了。这里可以思考??下,参数的处理也就是通常我们使用 JDBC 直接操作数据库时,所使用 ps.setXxx(i, parameter); 设置的各类参数。那么在自动化解析 XML 中 SQL 拆分出所有的参数类型后,则应该根据不同的参数进行不同的类型设置,也就;Long 调用 ps.setLong、String 调用 ps.setString 所以这里需要使用策略模式,在解析 SQL 时按照不同的执行策略,封装进去类型处理器(也就是是实现 TypeHandler

Long\String\Object\...),所以这里最重要的体现则是策略模式的使用。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 所示

核心处理主要分为三块;类型处理、参数设置、参数使用;
这里我们要先解决一个小问题,不知道读者在我们所实现的源码中,是否注意到这样一个参数的传递,如图 10-4

User queryUserInfoById(Long id);
源码详见: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; } } }}在 Mybatis 的源码包中,有一个 type 包,这个包下所提供的就是一套参数的处理策略集合。它通过定义类型处理器接口、由抽象模板实现并定义标准流程,定提取抽象方法交给子类实现,这些子类就是各个类型处理器的具体实现。
源码详见:cn.bugstack.mybatis.type.TypeHandler
public interface TypeHandler<T> { /** * 设置参数 */ void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;}源码详见: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;}源码详见: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); }}类型处理器注册机 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()); } //...} 相对于前面章节所完成的内容,这个章节需要对 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();}参数构建完成后,就可以在 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); } } }}创建一个数据库名称为 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', '小傅哥'); <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<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>源码详见: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();}@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":"小傅哥"}@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":"小傅哥"}