乐于分享
好东西不私藏

MyBatis 源码解读(三):StatementHandler 与参数处理——SQL 是怎么被编译和填充的

MyBatis 源码解读(三):StatementHandler 与参数处理——SQL 是怎么被编译和填充的

写在前面

上一篇我们讲完了 Executor,知道它负责"调度"。但 Executor 自己不直接跟数据库打交道——它把脏活累活外包给了 StatementHandler。StatementHandler 才是那个真正跟 JDBC Statement 打交道的"包工头"。

这篇文章我们拆解两个问题:

  1. StatementHandler 是怎么决定用 Statement、PreparedStatement 还是 CallableStatement 的?
  2. 你传的 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 在构造方法里做了两件重要的事:

  1. 创建 ParameterHandler——负责参数绑定
  2. 创建 ResultSetHandler——负责结果映射(下一篇讲)

prepare() 方法定义了准备 Statement 的标准流程:

  1. instantiateStatement() —— 子类实现,创建具体的 Statement
  2. setStatementTimeout() —— 设置超时
  3. 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} 占位符。获取参数值的优先级:

  1. boundSql.hasAdditionalParameter(propertyName) —— 动态 SQL 产生的额外参数(比如 <foreach> 生成的 __frch_item_0
  2. parameterObject == null —— 参数对象本身就是 null
  3. typeHandlerRegistry.hasTypeHandler(parameterObject.getClass()) —— 参数对象本身是基本类型(Integer、String 等),直接用
  4. 通过 MetaObject 反射取值 —— 参数对象是 POJO,通过 metaObject.getValue(propertyName) 取属性

TypeHandler:类型转换的翻译官

拿到参数值后,通过 typeHandler.setParameter(ps, i + 1, value, jdbcType) 设置到 PreparedStatementTypeHandler 是 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 依赖 BoundSqlBoundSql 又依赖 ParameterMapping。它们是怎么产生的?

当你在 Mapper XML 里写:

<selectid="findById"resultType="User">
    SELECT * FROM user WHERE id = #{id}
</select>

XMLStatementBuilder 解析这个标签时,会把 #{id} 提取为 ParameterMapping,然后生成 DynamicSqlSource 或 RawSqlSource(取决于是否含动态 SQL)。

BoundSql 的最终形态:

  • sqlSELECT * 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 源码,写作时对照源码逐行验证。如果发现有问题的地方,欢迎指正。