mybatis之ParameterHandler
MyBatis ParameterHandler:参数绑定的核心桥梁(从源码到实践)
ParameterHandler 是 MyBatis 中连接 Java 参数与 JDBC 预编译 SQL 的关键组件,负责将 Java 中的参数值正确绑定到 SQL 语句的占位符(?)上。它通过协调 TypeHandler 完成 Java 类型到 JDBC 类型的转换,是 SQL 预编译后执行前的最后一步关键操作。本文结合源码,深入解析 ParameterHandler 的工作机制、参数绑定流程及核心设计。
ParameterHandler 接口:参数处理的规范定义
ParameterHandler 接口仅定义了两个核心方法,清晰界定了参数处理的职责边界:
public interface ParameterHandler {
// 1. 获取原始参数对象(如 JavaBean、Map、基本类型等)
Object getParameterObject();
// 2. 核心方法:将参数绑定到 PreparedStatement 的占位符上
void setParameters(PreparedStatement ps) throws SQLException;
}
- getParameterObject():提供对原始参数的访问,方便其他组件(如插件)获取参数信息;
- setParameters(PreparedStatement):负责将 Java 参数值通过 JDBC API(如
ps.setInt(1, value))绑定到 SQL 占位符,是参数处理的核心逻辑。
DefaultParameterHandler:唯一实现类的核心逻辑
MyBatis 仅提供了一个 ParameterHandler 实现类 ——DefaultParameterHandler,它完整实现了从 Java 参数到 JDBC 占位符的绑定流程。其核心依赖包括:
TypeHandlerRegistry:管理所有类型处理器,负责 Java 类型与 JDBC 类型的转换;MappedStatement:包含 SQL 节点的配置信息(如参数映射、类型别名等);parameterObject:用户传入的原始参数(可能是 JavaBean、Map、数组等);BoundSql:包含解析后的 SQL 语句和参数映射列表(ParameterMapping)。
构造函数:初始化核心依赖
public class DefaultParameterHandler implements ParameterHandler {
private final TypeHandlerRegistry typeHandlerRegistry; // 类型处理器注册表
private final MappedStatement mappedStatement; // SQL配置元信息
private final Object parameterObject; // 原始参数对象
private final BoundSql boundSql; // 封装SQL和参数映射
private final Configuration configuration; // MyBatis全局配置
// 构造函数:初始化所有依赖组件
public DefaultParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
this.mappedStatement = mappedStatement;
this.configuration = mappedStatement.getConfiguration();
this.typeHandlerRegistry = configuration.getTypeHandlerRegistry();
this.parameterObject = parameterObject;
this.boundSql = boundSql;
}
// 获取原始参数对象
public Object getParameterObject() {
return parameterObject;
}
// ... 核心方法 setParameters 见下文
}
构造函数通过注入核心依赖,为参数绑定做好准备。其中 TypeHandlerRegistry 是关键,它存储了 MyBatis 内置及用户自定义的类型处理器,用于参数类型转换。
核心方法:setParameters () 详解
setParameters() 是参数处理的核心,其逻辑可概括为:遍历参数映射列表→获取参数值→匹配类型处理器→绑定到 PreparedStatement。
完整源码与步骤解析
public void setParameters(PreparedStatement ps) {
ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
// 1. 获取SQL中的参数映射列表(每个元素对应一个?占位符)
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
if (parameterMappings != null) {
// 2. 遍历每个参数映射,依次绑定到PreparedStatement
for (int i = 0; i < parameterMappings.size(); i++) {
ParameterMapping parameterMapping = parameterMappings.get(i);
// 3. 过滤存储过程的OUT参数(OUT参数由CallableStatementHandler单独处理)
if (parameterMapping.getMode() != ParameterMode.OUT) {
Object value; // 存储当前参数的值
String propertyName = parameterMapping.getProperty(); // 参数名称(如#{id}中的id)
// 4. 从参数对象中获取参数值
if (boundSql.hasAdditionalParameter(propertyName)) {
// 4.1 优先从额外参数中获取(如动态SQL中通过${}生成的参数)
value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
// 4.2 参数对象为null,直接赋值null
value = null;
} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
// 4.3 参数对象本身是简单类型(如Integer、String),直接作为值
value = parameterObject;
} else {
// 4.4 参数对象是复杂类型(如JavaBean、Map),通过MetaObject获取属性值
MetaObject metaObject = configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
// 5. 获取当前参数的类型处理器(TypeHandler)
TypeHandler typeHandler = parameterMapping.getTypeHandler();
// 6. 获取当前参数对应的JDBC类型(如VARCHAR、INTEGER)
JdbcType jdbcType = parameterMapping.getJdbcType();
// 6.1 若参数值为null且未指定JdbcType,使用全局默认配置(如NULL)
if (value == null && jdbcType == null) {
jdbcType = configuration.getJdbcTypeForNull();
}
try {
// 7. 核心:通过TypeHandler将参数值绑定到PreparedStatement的指定位置(i+1,JDBC索引从1开始)
typeHandler.setParameter(ps, i + 1, value, jdbcType);
} catch (SQLException | TypeException e) {
throw new TypeException("参数绑定失败:" + parameterMapping + ". 原因:" + e, e);
}
}
}
}
}
关键步骤拆解
(1)参数映射列表(ParameterMapping)
ParameterMapping 是 SQL 占位符与 Java 参数的映射关系,包含:
property:参数名称(如#{userName}中的userName);typeHandler:该参数对应的类型处理器;jdbcType:目标 JDBC 类型(如VARCHAR);mode:参数模式(IN/OUT/INOUT,默认为IN)。
这些信息在 MyBatis 解析 Mapper 时生成,存储在 BoundSql 中。
(2)参数值的获取逻辑(优先级)
参数值的获取是核心环节,MyBatis 支持多种参数类型(基本类型、JavaBean、Map 等),获取逻辑按以下优先级:
- 额外参数(AdditionalParameter):动态 SQL 中通过
setAdditionalParameter()添加的临时参数(如<if test="name != null">中生成的参数); - 参数对象为 null:直接返回 null;
- 简单类型参数:若参数是基本类型(如
Integer、String)或有对应的TypeHandler,直接使用参数对象本身; - 复杂类型参数:通过
MetaObject(MyBatis 的反射工具)获取属性值(支持 JavaBean 的 getter 方法、Map 的 key 访问)。
示例:
- 若参数是
User对象({id:1, name:"张三"}),propertyName="name"则通过metaObject.getValue("name")获取"张三"; - 若参数是
Map({id:1, name:"张三"}),propertyName="name"则直接获取map.get("name")。
(3)TypeHandler:类型转换的执行者
TypeHandler 是参数类型转换的核心,负责将 Java 类型转换为 JDBC 类型。MyBatis 内置了大量类型处理器(如 IntegerTypeHandler、StringTypeHandler),也支持自定义。
typeHandler.setParameter(...) 最终调用 JDBC 方法完成绑定,例如:
IntegerTypeHandler调用ps.setInt(index, value);StringTypeHandler调用ps.setString(index, value)。
(4)null 值处理
当参数值为 null 时,需指定 JDBC 类型(否则数据库可能无法识别):
- 优先使用
ParameterMapping中配置的jdbcType(如#{name,jdbcType=VARCHAR}); - 若未配置,使用全局配置
configuration.setJdbcTypeForNull(JdbcType.NULL)(默认为OTHER,部分数据库可能不兼容,建议显式配置)。
TypeHandler:类型转换的关键角色
ParameterHandler 的参数绑定依赖 TypeHandler 完成类型转换,二者是策略模式的典型应用:ParameterHandler 负责流程调度,TypeHandler 负责具体类型的转换逻辑。
TypeHandler 接口定义
public interface TypeHandler<T> {
// 设置参数(核心方法)
void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;
// 从结果集中获取值(用于查询结果映射)
T getResult(ResultSet rs, String columnName) throws SQLException;
T getResult(ResultSet rs, int columnIndex) throws SQLException;
T getResult(CallableStatement cs, int columnIndex) throws SQLException;
}
TypeHandler 不仅用于参数绑定(setParameter),还用于查询结果映射(getResult),是连接 Java 与 JDBC 类型的双向桥梁。
TypeHandlerRegistry:类型处理器的注册表
TypeHandlerRegistry 管理所有类型处理器,提供:
- 内置类型处理器的注册(如
String→VARCHAR、Integer→INTEGER); - 自定义类型处理器的注册(通过
@MappedTypes、@MappedJdbcTypes注解); - 根据 Java 类型和 JDBC 类型匹配合适的
TypeHandler。
在 DefaultParameterHandler 中,通过 typeHandlerRegistry.hasTypeHandler(parameterObject.getClass()) 判断参数是否为简单类型,简化参数值获取逻辑。
实战场景与常见问题
1. 不同参数类型的处理示例
(1)基本类型参数
<select id="selectById" resultType="User">
SELECT * FROM user WHERE id = #{id}
</select>
- 参数:
Integer id = 1; - 处理流程:
parameterObject是Integer(简单类型)→value = parameterObject(即1)→ 匹配IntegerTypeHandler→ 调用ps.setInt(1, 1)。
(2)JavaBean 参数
<insert id="insertUser">
INSERT INTO user(name, age) VALUES(#{name}, #{age})
</insert>
- 参数:
User user = new User("张三", 20); - 处理流程:
parameterObject是User(复杂类型)→ 通过MetaObject获取name和age→ 分别使用StringTypeHandler和IntegerTypeHandler绑定。
(3)Map 参数
<select id="selectByMap" resultType="User">
SELECT * FROM user WHERE name = #{name} AND age = #{age}
</select>
- 参数:
Map<String, Object> params = new HashMap<>(); params.put("name", "张三"); params.put("age", 20); - 处理流程:
parameterObject是Map→ 通过MetaObject直接获取map.get("name")和map.get("age")→ 绑定参数。
(4)多参数(@Param 注解)
User selectByParams( String name, Integer age);
<select id="selectByParams" resultType="User">
SELECT * FROM user WHERE name = #{name} AND age = #{age}
</select>
- 处理流程:MyBatis 自动将多参数封装为
Map(key 为@Param注解值)→ 后续逻辑同 Map 参数。
2. 常见问题与解决方案
(1)参数为 null 时绑定失败
问题:
#{name}对应的参数值为null,且未指定jdbcType,导致数据库无法识别类型;解决方案:显式指定jdbcType:
SELECT * FROM user WHERE name = #{name, jdbcType=VARCHAR}
(2)自定义类型参数绑定
场景:需要将 Java 枚举类型(如
StatusEnum)绑定为数据库INT类型(存储枚举的 code);解决步骤:
自定义TypeHandler:
public class StatusEnumTypeHandler extends BaseTypeHandler<StatusEnum> { public void setParameter(PreparedStatement ps, int i, StatusEnum parameter, JdbcType jdbcType) throws SQLException { ps.setInt(i, parameter.getCode()); // 绑定枚举的code值 } // 其他方法省略(查询时使用) }注册类型处理器(mybatis-config.xml):
<typeHandlers> <typeHandler handler="com.example.StatusEnumTypeHandler" javaType="com.example.StatusEnum"/> </typeHandlers>在 Mapper 中使用:
<update id="updateStatus"> UPDATE user SET status = #{status} WHERE id = #{id} </update>
(3)参数名称与属性不匹配
- 问题:
#{userName}对应的参数对象中属性为name,导致MetaObject获取值失败; - 解决方案:
- 修正参数名称与属性一致;
- 或使用别名:
#{userName, property=name}(指定实际属性名)。
总结
ParameterHandler 作为 MyBatis 参数绑定的核心组件,通过以下机制确保 Java 参数正确映射到 JDBC 预编译 SQL:
- 流程驱动:
setParameters()方法按 “遍历参数映射→获取参数值→类型转换→绑定参数” 的流程执行; - 类型转换:依赖
TypeHandler完成 Java 类型到 JDBC 类型的转换,支持内置和自定义处理器; - 灵活性:支持多种参数类型(基本类型、JavaBean、Map 等),通过
MetaObject统一处理复杂对象的属性访问