Mybatis参数读取源码分析
- Mybatis框架最底层需要访问数据库,本质是到数据库去执行sql语句,那么给sql语句传递参数是必不可少的一个环节,sql语句在预编译阶段会用占位符替换参数的位置,在需要执行的时候需要将参数放到对应的位置上,这个将参数替换占位符的工作就是ParameterHandler来完成的,本文我们来看看这个对象处理的相关知识。
一、ParameterHandler
- ParameterHandler在Mybatis四大对象中负责将sql中的占位符替换为真正的参数,它是一个接口,有且只有一个实现类DefaultParameterHandler
public interface ParameterHandler {
Object getParameterObject();
void setParameters(PreparedStatement ps)
throws SQLException;
}
- setParameters是处理参数最核心的方法。
二、DefaultParameterHandler
- DefaultParameterHandler是接口的唯一实现类,代码不多,主要是setParameters方法,通过注释我们可以看到其大体的功能主流程
public class DefaultParameterHandler implements ParameterHandler {
//1.类型处理器注册中心
private final TypeHandlerRegistry typeHandlerRegistry;
//2.MappedStatement是保存sql语句的数据结构
private final MappedStatement mappedStatement;
//3.参数对象
private final Object parameterObject;
//4.BoundSql对象是sql语句和相关信息的封装
private BoundSql boundSql;
//5.全局配置对象
private Configuration configuration;
//构造方法
public DefaultParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
this.mappedStatement = mappedStatement;
this.configuration = mappedStatement.getConfiguration();
this.typeHandlerRegistry = mappedStatement.getConfiguration().getTypeHandlerRegistry();
this.parameterObject = parameterObject;
this.boundSql = boundSql;
}
@Override
public Object getParameterObject() {
return parameterObject;
}
/**
* 将占位符替换为参数值
* */
@Override
public void setParameters(PreparedStatement ps) {
ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
//1.获取sql语句的参数,ParameterMapping里面包含参数的名称类型等详细信息,还包括类型处理器
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
if (parameterMappings != null) {
//2.遍历依次处理
for (int i = 0; i < parameterMappings.size(); i++) {
ParameterMapping parameterMapping = parameterMappings.get(i);
//3.OUT类型参数不处理
if (parameterMapping.getMode() != ParameterMode.OUT) {
Object value;
//4.获取参数名称
String propertyName = parameterMapping.getProperty();
//5.如果propertyName是动态参数,就会从动态参数中取值。(当使用<foreach>的时候,MyBatis会自动生成额外的动态参数)
if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
//6.如果参数是null,不管属性名是什么,都会返回null。
value = null;
} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
//7.判断类型处理器是否有参数类型,如果参数是一个简单类型,或者是一个注册了typeHandler的对象类型,就会直接使用该参数作为返回值,和属性名无关。
value = parameterObject;
} else {
//8.这种情况下是复杂对象或者Map类型,通过反射方便的取值。通过MetaObject操作
MetaObject metaObject = configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
TypeHandler typeHandler = parameterMapping.getTypeHandler();
//9.获取对应的数据库类型
JdbcType jdbcType = parameterMapping.getJdbcType();
//空类型
if (value == null && jdbcType == null) {
jdbcType = configuration.getJdbcTypeForNull();
}
//10.对PreparedStatement的占位符设置值(类型处理器可以给PreparedStatement设值)
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.sql语句中的占位符?都对应了BoundSql#parameterMappings集合中的一个元素,在该对象中记录了对应的参数名称和参数的相关属性。
在SimpleExecutor的doQuery方法中调用prepareStatement方法处理参数占位符,方法里面调用的就是PreparedStatementHandler的parameterize
方法,最终调用的是DefaultParameterHandler#setParameters,在PreparedStatementHandler内部持有ParameterHandler对象。
PS:简单来说就是SimpleExecutor#doQuery方法内部通过配置对象创建StatementHandler -> 调用PreparedStatementHandler#parameterize方
法(内部持有ParameterHandler) ->DefaultParameterHandler#setParameters来完成sql语句执行之前的参数替换占位符
三、流程调试
- 本节我们通过调试来看看前面说的核心流程,看看ParameterHandler参数处理在整个查询的哪一个步骤完成。断点在:SimpleExecutor#doQuery方法的prepareStatement(handler, ms.getStatementLog()),如下,
然后执行一次查询操作:
mapper.findMemberById(1);
3.1 SimpleExecutor#doQuery
- SimpleExecutor#doQuery方法是一次查询的入口,查询处理和返回结果集都在这里。在第2步调用prepareStatement方法获取Statement,里面就包含参数的预处理。(这里还可以参考 14-Mybatis源码和设计模式-5(Executor组件与模板模式,装饰器模式))
/**
* 查询的实现
* */
@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();
//1.创建StatementHandler
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
//2.用StatementHandler对象创建stmt,并使用StatementHandler对占位符进行处理
stmt = prepareStatement(handler, ms.getStatementLog());
//3.通过statementHandler对象调用ResultSetHandler将结果集转化为指定对象返回
return handler.<E>query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
3.2 SimpleExecutor#prepareStatement
- prepareStatement方法是获取Statement,默认是PrepareStatement。(其实是一个增强了的代理对象,具备日志能力的代理对象),其中第三步就是进行参数处理。
/**
* 创建Statement
* */
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
//1.获取connection对象的动态代理,添加日志能力;(这里参考日志模块的代理模式)
Connection connection = getConnection(statementLog);
//2.使用StatementHandler,利用connection创建(prepare)Statement
stmt = handler.prepare(connection, transaction.getTimeout());
//3.使用StatementHandler处理占位符
handler.parameterize(stmt);
return stmt;
}
3.3 PreparedStatementHandler#parameterize
- SimpleExecutor#prepareStatement 的方法首先会走到RoutingStatementHandler#parameterize,但是RoutingStatementHandler其实是一个静态代理,实际上走的是其内部的PreparedStatementHandler(参考:17-Mybatis源码分析(StatementHandler数据库访问))。
后面就到了PreparedStatementHandler#parameterize,方法很简单,直接调用ParameterHandler的setParameters方法,直接走到DefaultParameterHandler#setParameters
@Override
public void parameterize(Statement statement) throws SQLException {
parameterHandler.setParameters((PreparedStatement) statement);
}
3.4 DefaultParameterHandler#setParameters
- 参考前面的注释和下面的调试信息,这里会直接使用typeHandler进行设值操作,调用之前实际上已经通过参数类型获取到了对应的typeHandler。(parameterMapping.getTypeHandler())
3.5 BaseTypeHandler#setParameter
- 这里实际上走的是BaseTypeHandler的子类IntegerTypeHandler,不过setParameter是BaseTypeHandler的一个模板方法,里面的一个流程方法setNonNullParameter会调用子类IntegerTypeHandler的,在IntegerTypeHandler的setNonNullParameter里面直接调用PreparedStatement的setInt方法。到这里其实就走到了PreparedStatement的代理类的invoke方法(我们前面提到过,其实四大对象运行时都是一个代理对象,是一个具备日志能力的代理对象,这里参考:11-Mybatis源码和设计模式-2(日志模块和适配器模式,代理模式)),然后就到了PreparedStatementLogger类。(下面文字写错了,因为参数是1,所以走下面一个逻辑)
//BaseTypeHandler
@Override
public void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException {
//省略...
try {
setNonNullParameter(ps, i, parameter, jdbcType);
//省略...
}
}
//IntegerTypeHandler
@Override
public void setNonNullParameter(PreparedStatement ps, int i, Integer parameter, JdbcType jdbcType)
throws SQLException {
ps.setInt(i, parameter);
}
3.6 PreparedStatementLogger#invoke
- 最后通过method.invoke(statement, params),终于到了真实的PreparedStatement对象,调用的是PreparedStatement的setInt方法。我们发现绕了一圈已经到了JDBC的包下了,是的,Mybatis封装了很多最后底层参数设值还是JDBC那一套。
四、小结
4.1 调试流程图
4.2 小结
- Executor是sql语句的执行器,Executor通过配置对象创建StatementHandler,继而得到了StatementHandler,StatementHandler是整个数据库访问过程的控制关键,它的内部持有ParameterHandler,因此StatementHandler可以通过后者来处理参数。在StatementHandler处理参数的过程中会通过参数类型来找到对应的typeHandler来处理参数,整个过程中Statement对象都作为参数在传递,到了typeHandler他会调用Statement的setInt来设置值,其实整个过程中Statement对象都在传递,Mybatis通过封装,但是还是在使用JDBC的API。
- 关于Mybatis中数据访问的整体流程,可以阅读参考文章[3]
五、参考
- [1] 深入学习MyBatis中的参数(推荐)
- [2] 带注释源码
- [3] [23-Mybatis 核心流程03-数据读取阶段]
Java 面试宝典是大明哥全力打造的 Java 精品面试题,它是一份靠谱、强大、详细、经典的 Java 后端面试宝典。它不仅仅只是一道道面试题,而是一套完整的 Java 知识体系,一套你 Java 知识点的扫盲贴。
它的内容包括:
- 大厂真题:Java 面试宝典里面的题目都是最近几年的高频的大厂面试真题。
- 原创内容:Java 面试宝典内容全部都是大明哥原创,内容全面且通俗易懂,回答部分可以直接作为面试回答内容。
- 持续更新:一次购买,永久有效。大明哥会持续更新 3+ 年,累计更新 1000+,宝典会不断迭代更新,保证最新、最全面。
- 覆盖全面:本宝典累计更新 1000+,从 Java 入门到 Java 架构的高频面试题,实现 360° 全覆盖。
- 不止面试:内容包含面试题解析、内容详解、知识扩展,它不仅仅只是一份面试题,更是一套完整的 Java 知识体系。
- 宝典详情:https://www.yuque.com/chenssy/sike-java/xvlo920axlp7sf4k
- 宝典总览:https://www.yuque.com/chenssy/sike-java/yogsehzntzgp4ly1
- 宝典进展:https://www.yuque.com/chenssy/sike-java/en9ned7loo47z5aw
目前 Java 面试宝典累计更新 400+ 道,总字数 42w+。大明哥还在持续更新中,下图是大明哥在 2024-12 月份的更新情况:
想了解详情的小伙伴,扫描下面二维码加大明哥微信【daming091】咨询
同时,大明哥也整理一套目前市面最常见的热点面试题。微信搜[大明哥聊 Java]或扫描下方二维码关注大明哥的原创公众号[大明哥聊 Java] ,回复【面试题】 即可免费领取。