零、引言
本文只是博主在看书与阅读源码时的一些小笔记,整理的可能有点乱,后续有更加深入,且更加内聚的文章~
一、动态代理MapperProxy对象的创建
我们知道,mybatis通过 mapper interface 即可实现对对应mapper.xml 文件的调用,它其实是使用了动态代理,mybatis在启动时,会为每个接口创建一个对应的 MapperProxy对象,我们在调用接口中的方法时,实际上是调用了这个 MapperProxy。
MapperProxy创建流程:
二、动态代理MapperProxy对象在执行sql时的策略分发
2.1 MapperProxy进行MapperMethod构建
MapperProxy 是这个接口的实现,那么它的实际执行方法,也就是invoke方法,则是委托了 MapperMethod 去执行的。
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else if (isDefaultMethod(method)) {
return invokeDefaultMethod(proxy, method, args);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
final MapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(sqlSession, args);
}
MapperProxy 内部执行具体方法时流程示意:
MapperMethod是真正执行动态代理对象逻辑的实现类,通过调用它的 execute,采取命令模式进行逻辑分发,与 sqlSession 共同参与业务的执行,下面是 execute 方法的部分代码,清晰明了。
SqlCommand command
对象持有了当前方法的全限定名,并且标明了方法的 type(增删改查)MethodSignature method
对象则持有方法参数与返回值的信息,比如是返回Map 还是 游标,还是 Many,以及具体返回类型,如 List,另外,它还用作从动态代理获得的参数,使用paramNameResolver进行解析(如下面的 convertArgsToSqlCommandParam 方法)。
Object result;
switch (command.getType()) {
case INSERT: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.insert(command.getName(), param));
break;
}
case UPDATE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.update(command.getName(), param));
break;
}
case DELETE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.delete(command.getName(), param));
break;
}
2.2 MapperMethod对参数进行解析并调用sqlSession的具体执行方法
确定了具体的命令后,紧接着进行参数解析。这里比较有意思,pojo,map,或者 @Param注解的参数都可以,如果没有@Param注解的参数,则直接使用 #{1} 或者 #{param1} 去引用。
具体代码可以参考 org.apache.ibatis.reflection.ParamNameResolver#getNamedParams
,这块内容具体会在下文 additionalParameter 参数的构建与参数的设置进行比较详细的说明。
参数解析完毕后,调用 sqlSession的 #selectList 方法。
实际上到这里,整个调用逻辑已经很清晰了,我们在进行业务开发时,调用的mapper接口中的方法,实际上调用的是其代理实现,也就是 MapperProxy。在MapperProxy中,为每个方法都生成一个MethodProxy,让 MethodProxy 来进行具体业务的执行(也就是调用sqlSession),这一大层的封装实际上就是为了更好的去调用 sqlSession
三、SqlSession概览
从上面的一大圈子中,最后绕到了 SqlSession 这个“对象”(实际上是一个接口),sqlSession可以算是mybatis中最核心的部分,我们以最简单的 DefaultSqlSession 作为逻辑的切入点:
SqlSession下有四大对象:
- Executor 执行器,它负责调度,相当于中央控制器
- StatementHandler 是使用数据库statement(PreparedStatement)执行操作,是四大对象核心
- ParameterHandler 用于SQL对参数的处理
- ResultHandler 对数据集进行封装
3.1、Executor
Executor是通过 SqlSessionFactory ,具体是通过 Configuration 来创建的,Configuration 不仅仅是配置,它也是很多实例的创建者,mybatis 额外放出的插件支持,实际上就是在 Configuration 创建时注入的(类似拦截器):
可以看到,默认创建的实际上是 SimpleExecutor,默认的 Executor,如果开启了缓存,则会在 Executor 外面再代理一层 CachingExecutor
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,将拦截器,注册到插件上。mybatis的许多非侵入的插件开发都是基于 InterceptorChain 来完成的,可以把它类比为 spring 的拦截器
小插曲:在SqlSession调用Executor执行sql前,Configuration会根据 Methrod 的全限定名来获取到 MappedStatement 对象,这个对象是根据我们所写的 xml 文件来解析生成的。
MappedStatement 几乎包含了所有sql运行时需要的东西,包括:
被映射的 statement ,粗看里面包含
- mapper xml 资源
- 参数
- 请求类型(SELECT UPDATE)
- 排序
- 分页 - resultmap(映射关系)
就拿刚才的列表查询来举例,其中核心的执行方法为 doQuery
(org.apache.ibatis.executor.SimpleExecutor#doQuery)
doQuery
的入参方法很明了,一个前面提到的MappedStatement
对象parameter
参数(example),rowBounds
分页相关,resultHandler
结果处理器,boundSql
执行语句,包括方法的入参
@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);
}
}
在这里,引入了我们 sqlSession 里面另外一个重要的对象,也就是上面提到的 StatementHandler。
StatementHandler的 prepare 方法预编译和基础设置,parameterize 方法 来设置参数并执行,可见 StatementHandler的重要性。
3.2、StatementHandler
3.2.1 简单说说 StatementHandler
statementHandler 翻译成数据库会话器,在执行之前生成的那个是 RoutingStatementHandler ,它其实也是一个代理模式(或者说装饰模式),我们真正的业务是由CallableStatementHandler(用于存储过程),PrepaerStatementHandler(预编译sql),SimpleStatementHandler 来执行的(非预编译sql)。
StatementHandler 主要的三个方法为:
- prepare
- parameterize
- query
显而易见, BaseStatementHandler 则是数据库会话器的模板方法,这里不多赘述。
上面说到,RoutingStatementHandler是一个静态代理模式,它的构造函数却有一点特别,主要是里面还使用了策略模式,或者说命令模式来创建内置被代理的对象:
public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
switch (ms.getStatementType()) {
case STATEMENT:
delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
break;
case PREPARED:
delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
break;
case CALLABLE:
delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
break;
default:
throw new ExecutorException("Unknown statement type: " + ms.getStatementType());
}
}
3.2.2 StatementHandler 的 prepare 与 parameterize
3.2.2.1 prepare
目光先来到 BaseStatementHandler,它其实主要就一个方法:prepare。
prepare 中,instantiateStatement 是子类需要去实现的方法,比如对于PreparedStatementHandler来说,它是对 SQL 进行预编译,下面我们还能看到设置了超时,获取最大行数等。
@Override
public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException {
ErrorContext.instance().sql(boundSql.getSql());
Statement statement = null;
try {
statement = instantiateStatement(connection);
setStatementTimeout(statement, transactionTimeout);
setFetchSize(statement);
return statement;
} catch (SQLException e) {
closeStatement(statement);
throw e;
} catch (Exception e) {
closeStatement(statement);
throw new ExecutorException("Error preparing statement. Cause: " + e, e);
}
}
instantiateStatement 方法则是需要交给子类去实现,根据书中所说,这块是对sql进行预编译。这方面的源码跟着比较难受,以PrepaerStatementHandler实现的instantiateStatement为例,原本的connection对象,proxyConnectionImpl被日志输出静态代理封装了一层,又被Driud再次代理了一次,没有去细嚼这块的具体实现。
3.2.2.2 parameterize
BaseStatementHandler 没有对 parameterize 的实现,还是拿PreparedStatementHandler的实现来看,实际上就是简单调用了一下 parameterHandler 的一个方法,进行参数设置。
作为sqlSession主要组件的 parameterHandler,后面再细说
@Override
public void parameterize(Statement statement) throws SQLException {
parameterHandler.setParameters((PreparedStatement) statement);
}
3.2.2.3 query
同样,query也需要子类去实现,拿PreparedStatementHandler的实现来看,实际上也是简单调用了一下 resultSetHandler 的一个方法,进行查询。
作为sqlSession主要组件的 resultSetHandler,后面再细说
@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
ps.execute();
return resultSetHandler.handleResultSets(ps);
}
3.3、ParameterHandler
@Override
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) {// OUT 模式涉及到存储过程,这里不谈
Object value;
String propertyName = parameterMapping.getProperty();
if (boundSql.hasAdditionalParameter(propertyName)) { // 判断该参数是否是 additionalParameter
value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {// 参数是否为空
value = null;
} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {// 是否是一些可以直接处理的基础类型
value = parameterObject;
} else {
MetaObject metaObject = configuration.newMetaObject(parameterObject);// 否则通过将parameterObject解析为MetaObject来取值
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 e) {
throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
} catch (SQLException e) {
throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
}
}
}
}
}
可以看到代码虽然多,但实际上很简单,拿到所有入参进行遍历,可以清晰分为:
- 1、首先判断 ParameterMode 的可以忽略不看,它涉及到存储过程这个姥爷比较凉
- 2、判断该参数是否是 additionalParameter
- 3、判断是不是没有参数
- 4、该参数是否是可以被typeHandler直接处理的简单类型,是的话就直接转换
- 5、将parameterObject解析为metaObject来取值
3.3.1 additionalParameter 参数的构建与参数的设置
那么 AdditionParameter是什么呢?我们追踪到创建并且对它进行赋值的地方:
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
for (Map.Entry<String, Object> entry : context.getBindings().entrySet()) {
boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());
}
return boundSql;
可以看到,实际上它是基于 context.getBindings()
赋值的,context
对象中的 biddings
又是什么?这个问题先放后,首先, parameterObject 是怎么来的?实际上文章开头已经说的很清楚,是从动态代理进来的那个参数,前面的 2.2 章节有简单提到,是通过 MapperMethod的 MethodSignature 去进行转换的。
3.3.1.1 parameterObject的构建
转换的代码位于,org.apache.ibatis.reflection.ParamNameResolver#getNamedParams
,我们简单看下 parameterObject 是怎么构建的:
代码首行出现了一个变量 names, names 这个列表实际上很简单,就是使用反射拿到方法本身,如果你在方法参数上用了 Param 注解,就以你的命名为准,如果你没命名,且开启了 useActualParamName
配置,则通过反射去拿参数名字,再拿不到,参数名就会指定成0,1,2,3(参数顺序)。(代码参考org.apache.ibatis.reflection.ParamNameResolver#ParamNameResolver
)
回到我们的 getNamedParams
,它实际上和 names的设定 没有区别,但是它做了一层兜底,它将参数按照顺序,命名成了 param1,param2,param3(注意没有param0)..... 其实我没太看明白为什么要这么做,个人猜测是为了做向前兼容。
/**
* <p>
* A single non-special parameter is returned without a name.
* Multiple parameters are named using the naming rule.
* In addition to the default names, this method also adds the generic names (param1, param2,
* ...).
* </p>
*/
public Object getNamedParams(Object[] args) {
final int paramCount = names.size();
if (args == null || paramCount == 0) {
return null;
} else if (!hasParamAnnotation && paramCount == 1) {
return args[names.firstKey()];
} else {
final Map<String, Object> param = new ParamMap<>();
int i = 0;
for (Map.Entry<Integer, String> entry : names.entrySet()) {
param.put(entry.getValue(), args[entry.getKey()]);
// add generic param names (param1, param2, ...)
final String genericParamName = GENERIC_NAME_PREFIX + String.valueOf(i + 1);
// ensure not to overwrite parameter named with @Param
if (!names.containsValue(genericParamName)) {// 兜底机制!如果参数名字不是 param+参数顺序,则默认给一个
param.put(genericParamName, args[entry.getKey()]);
}
i++;
}
return param;
}
}
3.3.1.2 DynamicContext 对象的构建
public static final String PARAMETER_OBJECT_KEY = "_parameter";
public static final String DATABASE_ID_KEY = "_databaseId";
public DynamicContext(Configuration configuration, Object parameterObject) {
if (parameterObject != null && !(parameterObject instanceof Map)) {
MetaObject metaObject = configuration.newMetaObject(parameterObject);
bindings = new ContextMap(metaObject);
} else {
bindings = new ContextMap(null);
}
bindings.put(PARAMETER_OBJECT_KEY, parameterObject);
bindings.put(DATABASE_ID_KEY, configuration.getDatabaseId());
}
DynamicContext 其构造方法实际上十分简单,我们可以看到,如果说 parameterObject 不是 map 类型(比如说使用实体对象直接传入方法,且在xml中指定parameterType),那么将会走上面的分支,否则走下面的,分支过后则是简单增加两个 k-v 键值对(_parameter
-> 参数列表
,_databaseId
-> 比如mysql
)。
那么问题又回来到了最开始说的 boundSql.hasAdditionalParameter(propertyName)
,什么情况下才会走这个分支?实际上这和 OGNL
相关,如果说我们使用了类似这样的语法,则会进入到此分支。
<select id="selectBlogsLike" resultType="Blog">
<bind name="pattern" value="'%' + _parameter.getTitle() + '%'" />
SELECT * FROM BLOG
WHERE title LIKE #{pattern}
</select>
这实际上给了我们一些低侵入修改mybatis的入口,我们可以在 bidding 中放入更多的参数,再结合 OGNL 表达式,实现更强大的动态 Sql (但是感觉还是不如在java代码里去控制实在 = =)
3.3.2 可被typeHandler直接处理的简单类型(仅针对单参数)
类型的注册代码位于 org.apache.ibatis.type.TypeHandlerRegistry
,比较简单,这里不过多赘述。
EXTRA:可以自定义 typeHandler 将值进行转换,最简单的,比如枚举,如果说我们数据库存的是类似 0,1,2 那么就可以通过自定义 typeHandler 将枚举根据自己定义的规则进行转换
3.3.3 将parameterObject解析为metaObject来取值
实际上大部分的参数解析都走了这里的逻辑,MetaObject 实际上可以简单理解为就是个强大的反射托管类,它将传入的东西进行了复杂的反射解析,这里不细说,它说简单也简单,但是说复杂,也确实很复杂。
这是其中最简单的一个实现,其实就是拿到了实体类的 get 方法,进行反射调用。如果有get方法,就直接拿到我们的get方法,如果没有get方法,则拿到字段(Field)属性本身的获取方法。
private Object getBeanProperty(PropertyTokenizer prop, Object object) {
try {
Invoker method = metaClass.getGetInvoker(prop.getName());
try {
return method.invoke(object, NO_ARGUMENTS);
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
} catch (RuntimeException e) {
throw e;
} catch (Throwable t) {
throw new ReflectionException("Could not get property '" + prop.getName() + "' from " + object.getClass() + ". Cause: " + t.toString(), t);
}
}
通过 metaObject 拿到值后,最后还是通过 TypeHandlerRegistry 来将值置换为具体的类型,我们就拿 Integer 类型来举例,如果说 value 是一个 Integer,会拿到我们的 IntegerTypeHandler
,最终调用到我们PreparedStatement 的 setInt 方法,如参为下标 + 参数,比如说第三个参数,值为 100,则 ps.set(3,100)
@Override
public void setNonNullParameter(PreparedStatement ps, int i, Integer parameter, JdbcType jdbcType)
throws SQLException {
ps.setInt(i, parameter);
}
3.4、StatementHandler 的 query
通过 StatementHandler
和 ParameterHandler
对sql以及参数的预处理后,我们拿到了一个 Statement
对象,这个对象最初是在 instantiateStatement
由三个模板子类创建的,然后经过 ParameterHandler 对其进行参数设置。
最终调用 StatementHandler 的 query 方法。
@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);
}
}
我们还是以 PreparedStatementHandler 为例,我们发现外层代码很简洁,但实际上内部的调用过程十分复杂,特别是经过Droid等连接池代理过后... 但是我们只需要关心我们的结果(封装在 statement 对象中),最后是如何通过 ResultHandler 解析成我们需要的格式的即可。
@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
ps.execute();
return resultSetHandler.handleResultSets(ps);
}
额外:为什么各种handler都要Configuration来创建?
其实是为了注入拦截器(也就是mybatis插件)
四、调用过程概览
参考:
- https://blog.csdn.net/thebigdipperbdx/article/details/83041682 (解答了博主对于additionParameter的疑惑)
- 深入浅出MyBatis技术原理与实战 —— 杨开振