一、构建 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>