由于做数据脱敏时,其中一个方案为使用Mybatis Interceptor,因此本篇介绍下Mybatis的执行流程,看下有哪些核心的类和方法,以及拦截器是如何切入SQL的执行流程的;
1. Spring-Mybatis的执行流程介绍
1. spring通过sqlSessionFactoryBean创建sqlSessionFactory,在使用sqlSessionFactoryBean时,我们通常会指定configLocation和mapperLocations,来告诉sqlSessionFactoryBean去哪里读取配置文件以及去哪里读取mapper文件。
2. 得到配置文件和mapper文件的位置后,分别调用XmlConfigBuilder.parse()和XmlMapperBuilder.parse()创建Configuration和MappedStatement,Configuration类顾名思义,存放的是Mybatis所有的配置,而MappedStatement类存放的是每条sql语句的封装,MappedStatement以map的形式存放到Configuration对象中,key为对应方法的全路径。
3. spring通过ClassPathMapperScanner扫描所有的Mapper接口,为其创建BeanDefinition对象,但由于他们本质上都是没有被实现的接口,所以spring会将他们的BeanDefinition的beanClass属性修改为MapperFactorybean。
4. MapperFactoryBean也实现了FactoryBean接口,spring在创建Bean时会调用FactoryBean.getObject()方法获取Bean,最终是通过mapperProxyFactory的newInstance方法为mapper接口创建代理,创建代理的方式是JDK,最终生成的代理对象是MapperProxy。
5. 调用mapper的所有接口本质上调用的都是MapperProxy.invoke方法,内部调用sqlSession的insert/update/delete等各种方法。
// MapperMethod.java
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
if (SqlCommandType.INSERT == command.getType()) {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.insert(command.getName(), param));
} else if (SqlCommandType.UPDATE == command.getType()) {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.update(command.getName(), param));
} else if (SqlCommandType.DELETE == command.getType()) {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.delete(command.getName(), param));
} else if (SqlCommandType.SELECT == command.getType()) {
if (method.returnsVoid() && method.hasResultHandler()) {
executeWithResultHandler(sqlSession, args);
result = null;
} else if (method.returnsMany()) {
result = executeForMany(sqlSession, args);
} else if (method.returnsMap()) {
result = executeForMap(sqlSession, args);
} else {
Object param = method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(command.getName(), param);
}
} else if (SqlCommandType.FLUSH == command.getType()) {
result = sqlSession.flushStatements();
} else {
throw new BindingException("Unknown execution method for: " + command.getName());
}
if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
throw new BindingException("Mapper method '" + command.getName()
+ " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
}
return result;
}
6. SqlSession可以理解为一次会话,SqlSession会从Configuration中获取对应的MappedStatement,交给Executor执行。
// DefaultSqlSession.java
@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
// 从configuration对象中使用被调用方法的全路径,获取对应的MappedStatement
MappedStatement ms = configuration.getMappedStatement(statement);
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
7. Executor会先创建StatementHandler,StatementHandler可以理解为是一次语句的执行。
8. 然后Executor会获取连接,具体获取连接的方式取决于Datasource的实现,可以使用连接池等方式获取连接。
9. 之后调用StatementHandler.prepare方法,对应到jdbc执行流程中的Connection.prepareStatement这一步。
10. Executor再调用StatementHandler的parameterize方法,设置参数,对应到jdbc执行流程的StatementHandler.setXXX()设置参数,内部会创建ParameterHandler方法。
// SimpleExecutor.java
@Override
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
// 创建StatementHandler,对应第7步
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
// 获取连接,再调用conncetion.prepareStatement创建prepareStatement,设置参数
stmt = prepareStatement(handler, ms.getStatementLog());
// 执行prepareStatement
return handler.<E>query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
11. 再由ResultSetHandler处理返回结果,处理jdbc的返回值,将其转换为java的对象。
2. 从MapperProxy开始的代码走读
这篇文章纯粹是为了更好的理顺mybatis的执行流程,执行流程具体可参考下面的流程图,mybatis的代码基于3.4.6;
先给结论,将整体流程及涉及的类、类之间的调用关系整理为流程图:
通过上面的图,可以知道:
- 核心流程一是指mapperMethod的执行流程,这个过程主要是指对Mapper接口的方法参数的解析;
- 核心流程二是指executor的执行流程,这个过程主要是指StatementHandler的创建,包含:BoundSql 的创建(返回SQL语句)、parameterHandler(执行SQL前的SQL参数处理器)和ResultSetHandler(SQL结果赋值到ResutSets时的值处理)的创建;
- StatementHandler执行流程,这部分主要执行 SQL 并对结果集进行处理;
- 通过上面的流程,了解了一些关键操作的执行顺序:(1)Mapper接口参数的组装在 exector 之前前,同样在所有的拦截器执行前;(2)BoundSql生成SQL语句的时机在于 statementHandler 的创建,早于parameterHandler和ResultSetHandler的时机;
3. 源码分析
下面从MapperProxyFactory开始走读代码;
(1)Mapper接口代理的工厂——MapperProxyFactory
(2)Mapper接口代理类——MapperProxy
- MapperMethod的对象初始化时,填充SqlSession、Mapper接口、接口内的方法;
- MapperProxy的invoke方法的核心在于创建MapperMethod对象,并通过执行mapperMethod的execute方法继续执行;
(3)Mapper接口方法——MapperMethod
- MapperMethod初始化时,构建SQL命令SqlCommand和方法签名参数MethodSignature;
- MethodSignature的convertArgsToSqlCommandParam完成mybatis从方法参数到SQL命令参数的转换。
- execute方法实际是执行DefaultSqlSession的update/insert/select/delete等操作;
- SQL命令参数的解析convertArgsToSqlCommandParam在真正执行 Executor 方法之前;
(4)Mapper接口方法签名——MethodSignature
- MethodSignature的convertArgsToSqlCommandParam方法,实际上是调用ParamNameResolver的getNameParams方法;
(5)Mapper接口方法参数名解析——ParamNameResolver
Mybatis的参数传递情况分为:一个参数、Map参数、javaBean参数、多个参数、Collection参数、List参数、Array数组参数;这个类负责解析Mapper接口的方法参数;(解析的规则示例详细可参考:Mybatis第三篇:参数解析)
ParamNameResolver的getNameParams方法:
通过上述代码可知:
- MapperMethod执行SQL操作时,通过MethodSignature的convertArgsToSqlCommandParam解析参数,后续以解析后的方法参数来生成SQL语句并执行SQL语句;
- MethodSignature构建被调用的方法的签名信息,即通过ParamNameResolver来构建方法参数的别名信息,包含2步:(1)解析方法参数的别名:负责解析方法的参数和参数注解,构建参数下标及对应的别名的映射关系;(2)构建方法参数对象:结合参数别名映射关系,构建参数别名和参数对象的映射关系;
实际上Mapper方法参数的解析除了根据注解别名来构建参数名和参数对象的映射关系外,针对集合类也做了特殊处理(org.apache.ibatis.session.defaults.DefaultSqlSession#wrapCollection),在后面介绍相关代码;
这里补充参数解析的示例:
场景一:
List<Object> batchSelectByXxx(@Param("xxx") List<String> xxxList)
参数映射:构建以xxx为key,xxxList为value的Map对象。
场景二:
int batchInsert(List<Object> xxxList)
参数映射:构建以list/collection为key,xxxList为value的Map对象
场景三:
int update(Object record)
参数映射:构建以Object为参数对象,没有Map。
场景四:
List<Object> listByParam(@Param("paramA") String strA, @Param("paramB") String strB)
参数映射:构建以paramA为key,strA为value;以paramB为key,strB为value的Map对象。
场景五:
List<Object> listByParam(String strA, String strB)
参数映射,构建以param0为key,strA为value;以param1为key,strB为value的Map对象。
(6)MapperMethod中执行SQL的对象——SqlSession
上面分析完了MapperMethod的invoke方法的第1步:convertArgsToSqlCommandParam方法的实现,现在回到invoke方法的第2步:通过SqlSession的方法来执行SQL;
DefaultSqlSession是SqlSession的默认实现类,以下是SqlSession的select和update方法:
通过上述代码可知:
- DefaultSqlSession的select和update等方法实际上是调用Executor来执行的;
(7)DefaultSqlSession执行前获取的MappedStatement对象
MappedStatement维护了Mapper接口对应的Mapper.xml中的一条<select|update|delete|insert>节点的封装,MappedStatement类的成员变量也对应<select|update|delete|insert>节点上的标签属性,如下:
MappedStatement的核心方法——getBoundSql
(8)DefaultSqlSession通过Executor执行增删改查
执行Executor的方法前先做一步专门针对集合类的参数解析:
集合类的参数解析代码如下:
- DefaultSqlSession#wrapCollection的过程在MethodSignature#convertArgsToSqlCommandParam之后,针对只有一个参数对象且没有@Param标注别名的集合类对象会构建一个Map进行返回。
- 回忆下mybatis的xml定义中经常会遍历列表对象,存在<foreach collection="list">的语句,这里的list就是在这个过程中生成的。
Executor从CachingExecutor到SimpleExecutor的顺序进行执行,核心在SimpleExecutor当中:
- Executor从CachingExecutor到SimpleExecutor的顺序进行执行,核心在SimpleExecutor当中;
- SimpleExecutor#doQuery的核心流程包括:1.创建StatementHandler对象;2.参数初始化prepareStatement;3.执行StatementHandler的 query 方法;
- 创建StatementHandler的过程中完成了 BoundSql 的执行、parameterHandler和 ResultSetHandler 的创建;这一点很重要!!!
- prepareStatement内部会执行parameterHandler的方法;
- 注意点:BoundSql的生成早于prepareStatement方法的执行,因此 mybatis 原有的顺序必然导致parameterHandler的执行结果无法影响 BoundSql 的生成,只能在执行SQL之前,做一些诸如通过parameterHandler影响参数值的事情;
(9)获取StatementHandler——拦截器的织入
可以看到,拦截器就是这里织入的,这也是走读Mybatis执行过程需要了解的一个点,也就是我们可以通过注册拦截器,在真正执行SQL之前,做一些自定义的操作;
(10)再回到handler执行增删改查的位置——根据类型路由RoutingStatementHandler
这里switch进入PreparedStatementHandler:
PreparedStatementHandler的父类——BaseStatementHandler
通过上面的代码可知:
- RoutingStatementHandler内部创建的是PreparedStatementHandler。
- PreparedStatementHandler的创建中包含:boundSql的生成、parameterHandler和resultSetHandler的创建。
PreparedStatementHandler的执行后针对 update 操作后会进行主键的设置(通过KeyGenerator设置);针对 select 操作后执行resultSetHandler的执行动作;
至此Mybatis的执行过程的源码走读已经结束;
参考: