写在前面
上一篇我们讲完了 Executor,知道它负责"调度"。但 Executor 自己不直接跟数据库打交道——它把脏活累活外包给了 StatementHandler。StatementHandler 才是那个真正跟 JDBC Statement 打交道的"包工头"。
这篇文章我们拆解两个问题:
StatementHandler 是怎么决定用 Statement、PreparedStatement 还是 CallableStatement 的? 你传的 Java 对象参数,是怎么变成 SQL 里的 ?占位符的?
一、StatementHandler 家族:四个兄弟,分工明确
StatementHandler 的类图很有意思——一个接口,五个实现:
StatementHandler (接口)
├── RoutingStatementHandler —— 门面,负责"派活"
├── BaseStatementHandler —— 抽象基类,封装公共逻辑
├── SimpleStatementHandler —— 处理 Statement(静态 SQL)
├── PreparedStatementHandler —— 处理 PreparedStatement(预编译,默认)
└── CallableStatementHandler —— 处理 CallableStatement(存储过程)
1.1 RoutingStatementHandler:就是个派单的
publicclassRoutingStatementHandlerimplementsStatementHandler{
privatefinal StatementHandler delegate;
publicRoutingStatementHandler(Executor executor,
MappedStatement ms, Object parameter,
RowBounds rowBounds, ResultHandler resultHandler,
BoundSql boundSql){
switch (ms.getStatementType()) {
case STATEMENT:
delegate = new SimpleStatementHandler(...);
break;
case PREPARED:
delegate = new PreparedStatementHandler(...);
break;
case CALLABLE:
delegate = new CallableStatementHandler(...);
break;
default:
thrownew ExecutorException(
"Unknown statement type: " + ms.getStatementType());
}
}
@Override
public Statement prepare(Connection connection,
Integer transactionTimeout)throws SQLException {
return delegate.prepare(connection, transactionTimeout);
}
@Override
publicvoidparameterize(Statement statement)
throws SQLException {
delegate.parameterize(statement);
}
// ... 其他方法全部委托给 delegate
}
RoutingStatementHandler 自己啥也不干——就是个转发器。它根据 MappedStatement.getStatementType() 的值(默认是 PREPARED)决定创建哪个具体的 StatementHandler。
这就像一个快递分拣中心:包裹进来,根据地址判断是同城、跨省还是国际件,然后交给对应的派送团队。分拣中心自己不送货,但它决定了包裹归谁送。
你可能想问:为什么非要多这一层?直接 new 不就行了?
答案是:为了插件。
Configuration.newStatementHandler()创建的是RoutingStatementHandler,然后插件拦截的是这个对象。如果没有这一层,插件就得自己判断拦截哪个具体实现。多一层代理,插件就只需要关心StatementHandler接口,不用管底层是谁。
1.2 BaseStatementHandler:公共代码的仓库
publicabstractclassBaseStatementHandler
implementsStatementHandler{
protectedfinal Configuration configuration;
protectedfinal Executor executor;
protectedfinal MappedStatement mappedStatement;
protectedfinal RowBounds rowBounds;
protectedfinal BoundSql boundSql;
protectedfinal ParameterHandler parameterHandler;
protectedfinal ResultSetHandler resultSetHandler;
protectedBaseStatementHandler(...){
// 初始化各种字段
this.parameterHandler = configuration.newParameterHandler(
mappedStatement, parameterObject, boundSql);
this.resultSetHandler = configuration.newResultSetHandler(
executor, mappedStatement, rowBounds,
parameterHandler, resultHandler, boundSql);
}
@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);
thrownew ExecutorException(
"Error preparing statement. Cause: " + e, e);
}
}
protectedabstract Statement instantiateStatement(
Connection connection)throws SQLException;
}
BaseStatementHandler 在构造方法里做了两件重要的事:
创建 ParameterHandler——负责参数绑定创建 ResultSetHandler——负责结果映射(下一篇讲)
prepare() 方法定义了准备 Statement 的标准流程:
instantiateStatement()—— 子类实现,创建具体的 StatementsetStatementTimeout()—— 设置超时setFetchSize()—— 设置 fetch size
然后 prepare() 返回创建好的 Statement,交给 Executor 去调用 parameterize()。
1.3 PreparedStatementHandler:主角登场
MyBatis 默认用的就是 PreparedStatementHandler,预编译 + 参数绑定,防注入还高效。
publicclassPreparedStatementHandler
extendsBaseStatementHandler{
publicPreparedStatementHandler(Executor executor,
MappedStatement mappedStatement, Object parameter,
RowBounds rowBounds, ResultHandler resultHandler,
BoundSql boundSql){
super(executor, mappedStatement, parameter,
rowBounds, resultHandler, boundSql);
}
@Override
protected Statement instantiateStatement(
Connection connection)throws SQLException {
String sql = boundSql.getSql();
if (mappedStatement.getKeyGenerator()
instanceof Jdbc3KeyGenerator) {
String[] keyColumnNames =
mappedStatement.getKeyColumns();
if (keyColumnNames == null) {
return connection.prepareStatement(sql,
PreparedStatement.RETURN_GENERATED_KEYS);
} else {
return connection.prepareStatement(sql,
keyColumnNames);
}
} elseif (mappedStatement.getResultSetType()
!= null) {
return connection.prepareStatement(sql,
mappedStatement.getResultSetType().getValue(),
ResultSet.CONCUR_READ_ONLY);
} else {
return connection.prepareStatement(sql);
}
}
@Override
publicvoidparameterize(Statement statement)
throws SQLException {
parameterHandler.setParameters(
(PreparedStatement) statement);
}
@Override
publicintupdate(Statement statement)throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
ps.execute();
int rows = ps.getUpdateCount();
Object parameterObject = boundSql.getParameterObject();
KeyGenerator keyGenerator =
mappedStatement.getKeyGenerator();
keyGenerator.processAfter(executor, mappedStatement,
ps, parameterObject);
return rows;
}
@Override
public <E> List<E> query(Statement statement,
ResultHandler resultHandler)throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
ps.execute();
return resultSetHandler.handleResultSets(ps);
}
}
instantiateStatement() 根据配置决定怎么创建 PreparedStatement:
如果用 Jdbc3KeyGenerator(自增主键回填),传入RETURN_GENERATED_KEYS如果指定了 resultSetType,创建支持滚动/只读的结果集否则走最简单的 prepareStatement(sql)
parameterize() 就一行——把活交给 ParameterHandler。这就像一个厨师把调料的配比工作交给副手,自己专心炒菜。
update() 和 query() 是最终执行 SQL 的地方,调用了 PreparedStatement.execute(),然后分别处理更新计数或结果集。
1.4 SimpleStatementHandler:简单粗暴
publicclassSimpleStatementHandler
extendsBaseStatementHandler{
@Override
protected Statement instantiateStatement(
Connection connection)throws SQLException {
if (mappedStatement.getResultSetType() != null) {
return connection.createStatement(
mappedStatement.getResultSetType().getValue(),
ResultSet.CONCUR_READ_ONLY);
} else {
return connection.createStatement();
}
}
@Override
publicvoidparameterize(Statement statement){
// Nope
}
@Override
public <E> List<E> query(Statement statement,
ResultHandler resultHandler)throws SQLException {
String sql = boundSql.getSql();
statement.execute(sql);
return resultSetHandler.handleResultSets(statement);
}
}
SimpleStatementHandler 用 Statement 而不是 PreparedStatement,直接拼接 SQL。parameterize() 是空的——因为 Statement 不支持参数化,参数已经在生成 BoundSql 的时候拼接进去了。
这就像一个快餐店:不做预订,来了就做,做完就送。优点是快(不用预编译),缺点是容易出问题(SQL 注入风险)。除非你知道自己在做什么,否则别用。
什么时候会用
SimpleStatementHandler?当你在 Mapper XML 里显式指定statementType="STATEMENT"的时候。但说实话,99% 的场景你都应该用默认的PREPARED。
二、ParameterHandler:把 Java 对象塞进 SQL
ParameterHandler 的接口极简:
publicinterfaceParameterHandler{
Object getParameterObject();
voidsetParameters(PreparedStatement ps)
throws SQLException;
}
就一个方法 setParameters,负责把 Java 参数设置到 PreparedStatement 里。
2.1 DefaultParameterHandler 的实现
publicclassDefaultParameterHandler
implementsParameterHandler{
privatefinal TypeHandlerRegistry typeHandlerRegistry;
privatefinal MappedStatement mappedStatement;
privatefinal Object parameterObject;
privatefinal BoundSql boundSql;
@Override
publicvoidsetParameters(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)) {
value = boundSql
.getAdditionalParameter(propertyName);
} elseif (parameterObject == null) {
value = null;
} elseif (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 e) {
thrownew TypeException(
"Could not set parameters for mapping: "
+ parameterMapping + ". Cause: " + e, e);
} catch (SQLException e) {
thrownew TypeException(
"Could not set parameters for mapping: "
+ parameterMapping + ". Cause: " + e, e);
}
}
}
}
}
}
这段代码的逻辑清晰但细节多,我们拆开看:
参数值从哪里取?
setParameters 遍历 boundSql.getParameterMappings(),每个 ParameterMapping 对应一个 #{xxx} 占位符。获取参数值的优先级:
boundSql.hasAdditionalParameter(propertyName)—— 动态 SQL 产生的额外参数(比如<foreach>生成的__frch_item_0)parameterObject == null—— 参数对象本身就是 nulltypeHandlerRegistry.hasTypeHandler(parameterObject.getClass())—— 参数对象本身是基本类型(Integer、String 等),直接用通过 MetaObject 反射取值 —— 参数对象是 POJO,通过 metaObject.getValue(propertyName)取属性
TypeHandler:类型转换的翻译官
拿到参数值后,通过 typeHandler.setParameter(ps, i + 1, value, jdbcType) 设置到 PreparedStatement。TypeHandler 是 Java 类型和 JDBC 类型之间的"翻译官"。
举个例子:StringTypeHandler
publicclassStringTypeHandler
extendsBaseTypeHandler<String> {
@Override
publicvoidsetNonNullParameter(
PreparedStatement ps, int i,
String parameter, JdbcType jdbcType)
throws SQLException {
ps.setString(i, parameter);
}
@Override
public String getNullableResult(
ResultSet rs, String columnName)
throws SQLException {
return rs.getString(columnName);
}
}
MyBatis 内置了一堆 TypeHandler:
IntegerTypeHandler→PreparedStatement.setInt()StringTypeHandler→PreparedStatement.setString()DateTypeHandler→PreparedStatement.setTimestamp()BlobTypeHandler→PreparedStatement.setBlob()
如果你的参数类型不在内置列表里,可以自定义 TypeHandler,然后在 mybatis-config.xml 里注册。
null 值处理
如果参数值为 null,且没有指定 jdbcType,MyBatis 会调用 configuration.getJdbcTypeForNull() 获取默认值(通常是 OTHER,但不同数据库表现不同)。这也是为什么官方文档建议你在 #{xxx} 里写 jdbcType——不然 null 值可能会导致数据库驱动摸不着头脑。
比如 #{createTime, jdbcType=TIMESTAMP},明确告诉 MyBatis:这列是时间戳,null 也按 TIMESTAMP 处理。
三、从 XML 到 BoundSql:参数映射的前置工作
ParameterHandler 依赖 BoundSql,BoundSql 又依赖 ParameterMapping。它们是怎么产生的?
当你在 Mapper XML 里写:
<selectid="findById"resultType="User">
SELECT * FROM user WHERE id = #{id}
</select>
XMLStatementBuilder 解析这个标签时,会把 #{id} 提取为 ParameterMapping,然后生成 DynamicSqlSource 或 RawSqlSource(取决于是否含动态 SQL)。
BoundSql 的最终形态:
sql:SELECT * FROM user WHERE id = ?(占位符替换为?)parameterMappings:一个列表,包含ParameterMapping{property='id', jdbcType=null, typeHandler=IntegerTypeHandler}parameterObject:你传入的 Java 对象
ParameterHandler 拿到 BoundSql 后,按 parameterMappings 的顺序逐个填值。
四、小结:SQL 执行的准备阶段
Executor.doQuery()
│
▼
Configuration.newStatementHandler()
│
├── new RoutingStatementHandler() —— 根据 StatementType 路由
│ └── 创建具体 StatementHandler(默认 PreparedStatementHandler)
│
└── interceptorChain.pluginAll() —— 植入插件代理
│
▼
BaseStatementHandler.prepare()
│
├── instantiateStatement() —— 子类实现,创建 Statement/PreparedStatement
├── setStatementTimeout() —— 设置超时
└── setFetchSize() —— 设置 fetch size
│
▼
StatementHandler.parameterize()
│
▼
ParameterHandler.setParameters()
│
├── 遍历 ParameterMapping 列表
├── 从 parameterObject / MetaObject / additionalParameter 取值
├── 通过 TypeHandler 类型转换
└── PreparedStatement.setXxx() 设置参数
│
▼
PreparedStatement.execute() —— 真正发送 SQL 到数据库
五、几个容易踩的坑
为什么我的参数传进去了,SQL 里却变成了 null?
检查 parameterObject 的类型和 ParameterMapping 的 property 是否匹配。如果参数是 POJO,MyBatis 通过 MetaObject 反射取属性。如果属性名写错了(比如大小写不匹配),MetaObject.getValue() 会返回 null,不会报错。
为什么同样的代码,换个数据库就报类型转换错误?
TypeHandler 是通用的,但 JdbcType 的处理可能因数据库而异。比如 Oracle 对 null 值的处理比 MySQL 更严格。建议显式指定 jdbcType:
#{createTime, jdbcType=TIMESTAMP}
为什么批量插入时,用 BATCH Executor 比 foreach 快?
foreach 是在 MyBatis 层拼接 SQL,生成一条超长的 INSERT INTO ... VALUES (...), (...), (...),本质还是一条 SQL。BatchExecutor 是利用 JDBC 的 addBatch(),把多条 SQL 攒起来一次性发过去,减少了网络往返。
下篇预告
下一篇讲 ResultSetHandler——数据库返回的行记录,是怎么变成你写的 Java 对象的。那才是 ORM 最魔幻的地方。
本系列文章基于 MyBatis 3.5.x 源码,写作时对照源码逐行验证。如果发现有问题的地方,欢迎指正。
夜雨聆风