一、构建 SqlSessionFactory 的过程
SqlSessionFactory
是 MyBatis
核心类之一,其重要功能是创建 MyBatis 的核心接口 SqlSession
。MyBatis 通过 SqlSessionFactoryBuilder
构建 SqlSessionFactory
,构建分为两步:
- 通过
org.apache.ibatis.builder.xml.XMLConfigBuilder
解析 XML 配置文件,读取配置参数并存入org.apache.ibatis.session.Configuration
中 - 使用
Configuration
构建SqlSessionFactory
,MyBatis 提供了SqlSessionFactory
的默认实现类org.apache.ibatis.session.defaults.DefaultSqlSessionFactory
这种创建方式是一种 Builder 模式。对于复杂对象而言,直接使用构造函数构建会导致大量的逻辑放在构造函数中,使得代码看起来很繁杂。使用一个参数类总领全局,然后按步骤构建可以降低构建的复杂性
1.1 构建 Configuration
Configuration
的作用如下:
- 读入配置文件,包括基础配置文件和映射器文件
- 初始化基础配置,比如 MyBatis 的别名,一些重要的类对象(插件、映射器、
ObjectFactory
和TypeHandler
) - 提供
Configuration
单例,为后续创建SqlSessionFactory
服务提供配置参数
1.2 映射器内部组成
插件需要频繁访问映射器内部组成,所以有必要单独研究一下映射器的内部组成。一般而言,映射器由 3 部分组成:
-
MappedStatement
:保存映射器的一个节点,包括配置的 sql、sql id、resultMap、resultType 等重要配置内容 -
SqlSource
:提供BoundSql
的地方,是MappedStatement
的一个属性 -
BoundSql
:建立 SQL 和相关参数的地方,有三个常用属性,sql、parameterObject 和 parameterMappings
一般而言,我们主要对参数和 SQL 进行修改,这部分主要反映在 BoundSql
类上,在插件中可以通过 BoundSql
拿到当前运行 SQL 和参数及参数规则,并做出适当修改,满足我们的需求
BoundSql
主要有三个属性:sql
、rameterObject
和 parameterMappings
-
parameterObject
:参数对象本身,可以传递简单对象、POJO
、Map
或者@Param
注解的参数,其传递规则如下:- 传递简单对象会将其变为简单对象的包装类传递,如
int
变为Integer
传递 - 传递
POJO
或Map
,parameterObject 就是传入的POJO
或Map
不变 - 当传递多个参数,如果没有
@Param
注解,那么 MyBatis 会将 parameterObject 变为一个Map<String, Object>
对象,类似于这样的形式:{"param1":p1, "param2":p2}
,所以我们可以使用 #{param1} 引用一个参数 - 如果使用
@Param
注解,那么 MyBatis 会将 parameterObject 变为一个Map<String, Object>
对象。只是将 Map 的键值置换为@Param
注解的键值。例如@Param("key1") String p1, @Param("key2") String p2
,parameterObject 的形式为{"key1":p1, "key2":p2}
- 传递简单对象会将其变为简单对象的包装类传递,如
-
parameterMappings
:是一个List
,每个元素都是ParameterMapping
对象。这个对象会描述我们的参数,参数包括属性、名称、表达式、JavaType、typeHandler 信息,一般不会去改变它。通过 parameterMappings 可以实现参数和 SQL 的结合,以便PreParedStatement
通过 parameterMappings 找到 parameterObject 对象的属性并设置参数 -
sql
:即我们书写在映射器里面的一条 SQL,可以通过插件进行改写
1.3 构建 SqlSessionFactory
有了 Configuration
便可以快速构建 SqlSessionFactory
二、SqlSession 运行过程
2.1 映射器动态代理
todo
2.2 SqlSession 四大对象
映射器其实就是一个动态代理对象,最终会进入 MapperMethod
的 execute
方法。execute
经过简单判断就调用 SqlSession 的删除、更新、插入、选择等方法。这些方法是通过 Executor
、StatementHandler
、ParameterHandler
和 ResultHandler
来完成数据库操作和结果返回的
-
Executor
:执行器,负责调度StatementHandler
、ParameterHandler
和ResultHandler
执行 SQL并返回结果 -
StatementHandler
:使用数据库的 Statement(PrepareStatement
)执行操作,是四大对象的核心 -
ParameterHandler
:用于 SQL 对参数的处理 -
ResultHandler
:进行最后结果数据集(ResultSet
)的封装返回处理
2.2.1 Executor
Executor
负责执行 Java 和 数据库交互,在 MyBatis 中存在三种执行器,可以在配置文件的 setting 元素的 defaultExecutorType
指定
public enum ExecutorType {
SIMPLE, REUSE, BATCH
}
- SIMPLE:简单执行器,它是默认执行器
- REUSE:重用预处理语句的执行器
- BATCH:重用语句和批量更新,针对批量专用的执行器
构造过程如下:
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor executor;
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
} else {
executor = new SimpleExecutor(this, transaction);
}
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
执行过程中,interceptorChain.pluginAll(executor)
就是 MyBatis 插件执行的地方,这行代码会为 executor
构建一层层代理对象,在调度真实的 Executor
方法之前执行配置插件的代码可以修改。SimpleExecutor
的查询处理过程如下所示:
public class SimpleExecutor extends BaseExecutor {
@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 handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
stmt = prepareStatement(handler, ms.getStatementLog());
return handler.query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
Connection connection = getConnection(statementLog);
stmt = handler.prepare(connection, transaction.getTimeout());
handler.parameterize(stmt);
return stmt;
}
如代码所示,SimpleExecutor
的查询过程一共有 4 步:
- 通过
configuration
创建StatementHandler
- 调用
StatementHandler#prepare
进行预编译和基础设置 - 调用
StatementHandler#parameterize
设置参数 - 调用
StatementHandler#query
执行查询并通过ResultHandler
组装查询结果并返回
2.2.2 StatementHandler
StatementHandler
,顾名思义就是处理数据库会话的,其创建过程如下:
public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
return statementHandler;
}
实际创建的是 RoutingStatementHandler
对象,它实现了 StatementHandler
接口。和 Executor
一样用代理对象做了一层层封装
statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
但 RoutingStatementHandler
对象也不是实际处理查询任务的对象,它通过 适配模式 找到对应的 StatementHandler
来执行具体的数据库操作,MyBatis 也有三种 StatementHandler:SimpleStatementHandler
、PreparedStatementHandler
以及 CallableStatementHandler
我们以最常用的 PreparedStatementHandler
为例讲解 StatementHandler
的三个主要方法:prepare、parameterize 和 query 是如何执行的
首先 Executor
调用 StatementHandler#prepare
进行 SQL 预编译
// BaseStatementHandler
@Override
public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException {
ErrorContext.instance().sql(boundSql.getSql());
Statement statement = instantiateStatement(connection);
setStatementTimeout(statement, transactionTimeout);
setFetchSize(statement);
return statement;
}
instantiateStatement
对 SQL 进行预编译,做了一些基础设置(超时、获取最大行数等),然后 Executor
调用 StatementHandler#parameterize
设置参数
// PreparedStatementHandler
@Override
public void parameterize(Statement statement) throws SQLException {
parameterHandler.setParameters((PreparedStatement) statement);
}
可以看到这步操作是调用 ParameterHandler
完成的,2.2.3 中将详细描述参数设置过程。最后 Executor
调用 StatementHandler#query
执行查询
// PreparedStatementHandler
@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
ps.execute();
return resultSetHandler.handleResultSets(ps);
}
StatementHandler#query
执行 PreparedStatement#execute
执行 SQL,并将结果通过 ResultSetHandler
处理返回,详见 2.2.4
综上所述,我们就知道 MyBatis 处理一条 SQL 的执行过程了,整体过程如下图所示(以 SimpleExecutor
的查询为例):
下面我们再具体看看 ParameterHandler
和 ResultHandler
是如何设置参数并将结果封装返回的
2.2.3 ParameterHandler
ParameterHandler
是参数处理器,主要完成对预编译参数的设置。我们先看一下其接口定义:
public interface ParameterHandler {
Object getParameterObject();
void setParameters(PreparedStatement ps) throws SQLException;
}
ParameterHandler#getParameterObject
返回参数对象,ParameterHandler#setParameters
设置预编译 SQL 语句的参数
MyBatis 提供了 默认实现类 DefaultParameterHandler
,我们主要看一下 setParameters
方法的实现
public void setParameters(PreparedStatement ps) {
ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
if (parameterMappings != null) {
for (int i = 0; i < parameterMappings.size(); i++) {
ParameterMapping parameterMapping = parameterMappings.get(i);
if (parameterMapping.getMode() != ParameterMode.OUT) {
Object value;
String propertyName = parameterMapping.getProperty();
if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
value = null;
} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
value = parameterObject;
} else {
MetaObject metaObject = configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
TypeHandler typeHandler = parameterMapping.getTypeHandler();
JdbcType jdbcType = parameterMapping.getJdbcType();
if (value == null && jdbcType == null) {
jdbcType = configuration.getJdbcTypeForNull();
}
try {
typeHandler.setParameter(ps, i + 1, value, jdbcType);
} catch (TypeException | SQLException e) {
throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
}
}
}
}
}
2.2.4 ResultHandler
ResultHandler 负责处理 SQL 执行结果集,接口定义如下:
public interface ResultSetHandler {
<E> List<E> handleResultSets(Statement stmt) throws SQLException;
<E> Cursor<E> handleCursorResultSets(Statement stmt) throws SQLException;
void handleOutputParameters(CallableStatement cs) throws SQLException;
}
其中,handleResultSets
负责封装结果集。MyBatis 提供了实现类 DefaultResultSetHandler
,它涉及到使用 JAVASSIST 或 CGLIB 作为延迟加载,然后通过 TypeHandler
和 ObjectFactory
进行组装再返回结果
2.3 SqlSession 运行总结
SqlSession
内部运行图如下所示
SqlSession
通过 Executor
创建 StatementHandler
来执行 SQL 请求的。StatementHandler
要经过三个步骤:
- prepare 预编译 SQL
- parameterize 设置参数
- query/update 执行 SQL
其中 parameterize 调用 ParameterHandler
设置参数,参数类型根据类型处理器 TypeHandler
处理。query/update 方法通过 ResultHandler
进行结果封装,如果是 update 语句则返回整数,否则通过 TypeHandler
处理结果类型,然后用 ObjectFactory
提供的规则封装对象,返回给调用者
三、插件
3.1 插件接口
在 MyBatis 中使用插件必须实现 Interceptor
接口
public interface Interceptor {
Object intercept(Invocation invocation) throws Throwable;
default Object plugin(Object target) {
return Plugin.wrap(target, this);
}
default void setProperties(Properties properties) {
// NOP
}
}
-
intercept
:直接覆盖你要拦截对象的原有方法,它是插件的核心方法。通过参数 invocation 可以反射调用原对象的方法 -
plugin
:target
是被拦截对象,它的作用是给被拦截对象生成一个代理对象并返回。为了方便生成代理对象,MyBatis 使用 org.apache.ibatis.plugin.Plugin#wrap 方法生成代理对象 -
setProperties
:允许在 plugin 元素中配置所需参数,方法在插件初始化的时候就被调用一次,然后将插件对象存入到配置中,以便后续取出
可以看到,MyBatis 使用 模板模式 进行插件开发
3.2 插件初始化
插件初始化在 MyBatis 初始化的时候完成,关键代码如下:
// XMLConfigBuilder.class
private void pluginElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
String interceptor = child.getStringAttribute("interceptor");
Properties properties = child.getChildrenAsProperties();
// resolveClass -> (Class<T>) Resources.classForName(string);
Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance();
interceptorInstance.setProperties(properties);
configuration.addInterceptor(interceptorInstance);
}
}
}
// configuration.class
public void addInterceptor(Interceptor interceptor) {
interceptorChain.addInterceptor(interceptor);
}
// InterceptorChain.class
public void addInterceptor(Interceptor interceptor) {
interceptors.add(interceptor);
}
MyBatis 在上下文初始化过程中,就开始读入插件节点和我们配置的参数,然后通过反射生成 Interceptor
实例,并调用 setProperties
方法设置我们配置的参数,最后将 interceptorInstance
保存到配置对象 configuration
中
所以插件的初始化是 MyBatis 初始化的时候完成的,这样有助于提高性能
3.3 插件的代理和反射设计
插件使用的是 责任链模式,责任链处理的对象是 MyBatis 四大对象中的一个,在 InterceptorChain
中的每个 Interceptor 都有机会处理目标对象
1 在 SqlSession
四大对象初始化时,调用 InterceptorChain#pluginAll
为其嵌套生成代理对象
// InterceptorChain.class
private final List<Interceptor> interceptors = new ArrayList<>();
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
2 InterceptorChain#pluginAll
为目标类生成嵌套代理对象,即有多个拦截器,就为目标类嵌套生成代理类
// Interceptor.class
default Object plugin(Object target) {
return Plugin.wrap(target, this);
}
// Plugin.class
public static Object wrap(Object target, Interceptor interceptor) {
// 获取 Interceptor 上 @Intercepts 注解设置的签名方法
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
Class<?> type = target.getClass();
// 获取 @Signature 注解 type 属性对应的所有签名方法
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
if (interfaces.length > 0) {
return Proxy.newProxyInstance(type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap));
}
return target;
}
3 在调用四大对象的方法时,会代用 InvocationHandler#invoke
,由于 Plugin
实现了 InvocationHandler
接口,故实际调用 Plugin#invoke
方法
// Plugin.class
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
// 获取 @Intercepts 注解的所有拦截方法
Set<Method> methods = signatureMap.get(method.getDeclaringClass());
/*
如果该方法是 @Intercepts 注解设置的拦截方法,则调用 Interceptor.intercept 进行拦截处理,否则直接反射调用 method.invoke(target, args)
*/
if (methods != null && methods.contains(method)) {
return interceptor.intercept(new Invocation(target, method, args));
}
return method.invoke(target, args);
} catch (Exception e) {
throw ExceptionUtil.unwrapThrowable(e);
}
}
如果该方法是 @Intercepts
注解设置的拦截方法,则调用 Interceptor.intercept
进行拦截处理,否则直接反射调用 method.invoke(target, args)
。因此在 Interceptor#intercept
中可以自定义实际的拦截逻辑
Invocation
封装了目标类 target
、要代理的方法 method
以及 method
的参数 args
,可通过 Invocation#proceed
完成方法的反射调用。具体代码如下
public class Invocation {
private final Object target;
private final Method method;
private final Object[] args;
public Invocation(Object target, Method method, Object[] args) {
this.target = target;
this.method = method;
this.args = args;
}
public Object proceed() throws InvocationTargetException, IllegalAccessException {
return method.invoke(target, args);
}
// ignore get and set
}
3.4 常用工具类——MetaObject
MetaObject
可有效读取或设置类属性,其关键的方法有三个:
-
MetaObject SystemMetaObject#forObject(Object object)
用于包装对象,获取目标对象 object 的 MetaObject
-
Object MetaObject#getValue(String name)
获取对象属性值,支持 OGNL
-
void MetaObject#setValue(String name, Object value)
设置对象属性值,支持 OGNL
对象导航图语言(Object Graph Navigation Language),简称 OGNL,是一种表达式语言
3.5 插件开发示例
QueryTrancePlugin
插件用于在 SQL 中追加调用链路和应用名称
/**
* QueryTrancePlugin
*
* @author lzn
* @version 1.0
* @date 2020/9/23 10:02
*/
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
public class QueryTrancePlugin implements Interceptor {
private static final String SPLIT = "@@@";
private static final String SQL_POS = "delegate.boundSql.sql";
private static final String SELECT = "select";
private static final String APPLICATION_NAME = "applicationName";
private static final String CLASS_REGEX = "classRegex";
/**
* 应用名称
*/
private String applicationName;
/**
* 类方法正则表达式
*/
private String classRegex;
@Override
public Object intercept(Invocation invocation) throws Throwable {
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
MetaObject metaStatementHandler = SystemMetaObject.forObject(statementHandler);
// 分离代理对象链(由于目标类可能被多个插件拦截,从而形成多层代理,可循环分理出最原始的目标类)
while (metaStatementHandler.hasGetter("h")) {
Object object = metaStatementHandler.getValue("h");
metaStatementHandler = SystemMetaObject.forObject(object);
}
String sql = (String) metaStatementHandler.getValue(SQL_POS);
if (sql != null && sql.toLowerCase().trim().indexOf(SELECT) == 0) {
StringBuilder builder = new StringBuilder(sql);
// 获取调用链路
StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace();
builder.append(SPLIT).append(this.applicationName);
builder.append(SPLIT).append(WebToolUtils.getLocalIP());
for (StackTraceElement stackTraceElement : stackTraceElements) {
if (stackTraceElement.getClassName().matches(this.classRegex)) {
builder.append(SPLIT).append(stackTraceElement.getClassName()).append("#").append(stackTraceElement.getMethodName());
break;
}
}
metaStatementHandler.setValue(SQL_POS, builder.toString());
}
return invocation.proceed();
}
@Override
public void setProperties(Properties properties) {
this.applicationName = properties.getProperty(APPLICATION_NAME);
this.classRegex = properties.getProperty(CLASS_REGEX);
}
}
在应用的配置文件中,插件配置如下
<configuration>
<plugins>
<plugin interceptor="*.QueryTrancePlugin">
<property name="applicationName" value="application-demo"/>
<property name="classRegex" value="*.service.*"/>
</plugin>
</plugins>
</configuration>