使用策略模式,调用参数处理器

一、前言

你这代码写的,咋这么轴呢!

说到轴,让我想起初中上学时老师说的话:“你那脑瓜子,咋跟手焖子似的!”

东北话手焖子就是那种冬天戴的大棉手套,棉手套里的棉花都被压的又沉又硬的了,所以来比喻脑瓜子笨。

而写轴代码的大部分都是刚毕业没多久,或者刚开始工作的码农,毕竟经验不足经历不多,写出一些不太好维护的代码也情有可原。而那些绝对多数锻炼出来的老码农,其实代码的稳定程度、设计经验、缜密逻辑,都是相对来说要好很多的。当然一部分老码农,只是老了而已,代码还是那个代码!

所以企业招聘些年轻人,需要年轻的思想。但没必要嚯嚯只是头发没多少的老码农,否则谁来给你平稳落地你那些天马行空的想法呢!难道体验、稳定、流畅,不应该是更值得追求的,非得喜欢全是愣头青似的代码,写出几百个bug,造成大量资损和客诉,让老板觉得很爽?

二、目标

上一章节,小傅哥带着大家细化的 XML 语句构建器,解耦在解析 XML 中的所需要处理的 Mapper 信息,包括;SQL、入参、出参、类型,并对这些信息进行记录到 ParameterMapping 参数映射处理类中。那么这个一章节我们将结合这部分参数的提取,对执行的 SQL 进行参数的自动化设置,而不是像我们之前那样把参数写成固定的,如图 10-1 所示:

使用策略模式,调用参数处理器插图亿华云

图 10-1 硬编码参数设置

在流程上,通过 DefaultSqlSession#selectOne 方法调用执行器,并通过预处理语句处理器 PreparedStatementHandler 执行参数设置和结果查询。

那么这个流程中我们所处理的参数信息,也就是每个 SQL 执行时,那些?号需要被替换的地方,目前是通过硬编码的方式进行处理的。而这就是本章节需要解决的问题,如果只是硬编码完成参数设置,那么对于所有那些不同类型的参数就没法进行操作了。

所以本章节需要结合结合上一章节所完成的语句构建器对 SQL 参数信息的拆解,本章将会按照这些参数的解析,处理这里硬编码为自动化类型设置。针对不同类型的参数设置,这部分使用了什么设计模式呢?

三、设计

这里可以思考下,参数的处理也就是通常我们使用 JDBC 直接操作数据库时,所使用 ps.setXxx(i, parameter);

设置的各类参数。那么在自动化解析 XML 中 SQL 拆分出所有的参数类型后,则应该根据不同的参数进行不同的类型设置,也就;Long 调用 ps.setLong、String 调用 ps.setString 所以这里需要使用策略模式,在解析 SQL 时按照不同的执行策略,封装进去类型处理器(也就是是实现 TypeHandler接口的过程)。整体设计如图 10-2 所示:

使用策略模式,调用参数处理器插图1亿华云

图 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

使用策略模式,处理参数处理器核心类关系,如图 10-3 所示:

使用策略模式,调用参数处理器插图2亿华云

图 10-3 使用策略模式,处理参数处理器核心类关系

核心处理主要分为三块;类型处理、参数设置、参数使用;

以定义 TypeHandler 类型处理器策略接口,实现不同的处理策略,包括;Long、String、Integer 等。这里我们先只实现2种类型,读者在学习过程中,可以按照这个结构来添加其他类型。类型策略处理器实现完成后,需要注册到处理器注册机中,后续其他模块参数的设置还是使用都是从 Configuration 中获取到 TypeHandlerRegistry 进行使用。那么有了这样的策略处理器以后,在进行操作解析 SQL 的时候,就可以按照不同的类型把对应的策略处理器设置到BoundSql#parameterMappings 参数里,后续使用也是从这里进行获取。

2. 入参数校准

这里我们要先解决一个小问题,不知道读者在我们所实现的源码中,是否注意到这样一个参数的传递,如图 10-4:

使用策略模式,调用参数处理器插图3亿华云

图 10-4 参数设置时入参获取

这里的参数传递后,需要获取第0个参数,而且是硬编码固定的。这是为什么呢?这个第0个参数是哪来的,我们接口里面调用的方法,参数不是一个吗?就像:User queryUserInfoById(Long id);其实这个参数来自于映射器代理类 MapperProxy#invoke 中,因为 invoke 反射调用的方法,入参中是 Object[] args,所以这个参数被传递到后续的参数设置中。而我们的 DAO 测试类是一个已知的固定参数,所以后面硬编码了获取了第0个参数。

使用策略模式,调用参数处理器插图4亿华云

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 param = new ParamMap();

int i = 0;

for (Map.Entry 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 {

/**

* 设置参数

*/

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 implements TypeHandler {

@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 {

@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{

@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

THE END
Copyright © 2024 亿华云