1. ParameterHandler在MyBatis中的核心作用
ParameterHandler是MyBatis四大核心接口之一(另外三个是Executor、StatementHandler和ResultSetHandler),专门负责处理SQL语句的参数绑定。当你在Mapper接口中定义了一个方法,比如User selectById(@Param("id") Integer id),最终执行时,ParameterHandler就会把方法参数id的值设置到PreparedStatement中。
这个接口最常用的场景是处理预编译SQL语句中的占位符(?)。假设你写了这样的SQL:
xml复制<select id="selectById" resultType="User">
SELECT * FROM user WHERE id = #{id}
</select>
MyBatis会将其转换为SELECT * FROM user WHERE id = ?,然后由ParameterHandler把实际参数值设置到这个问号位置。
2. ParameterHandler的默认实现与工作原理
2.1 DefaultParameterHandler源码解析
MyBatis默认的ParameterHandler实现是org.apache.ibatis.scripting.defaults.DefaultParameterHandler。它的核心方法是setParameters:
java复制public void setParameters(PreparedStatement ps) {
ParameterMapping[] parameterMappings = boundSql.getParameterMappings();
if (parameterMappings != null) {
for (int i = 0; i < parameterMappings.length; i++) {
ParameterMapping parameterMapping = parameterMappings[i];
Object value;
String propertyName = parameterMapping.getProperty();
if (boundSql.hasAdditionalParameter(propertyName)) {
value = boundSql.getAdditionalParameter(propertyName);
} else {
value = parameterObject;
// 处理嵌套属性
MetaObject metaObject = configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
TypeHandler typeHandler = parameterMapping.getTypeHandler();
typeHandler.setParameter(ps, i + 1, value, parameterMapping.getJdbcType());
}
}
}
这段代码展示了参数处理的完整流程:
- 遍历所有参数映射(ParameterMapping)
- 通过属性名从参数对象中获取值
- 使用对应的TypeHandler将值设置到PreparedStatement中
2.2 参数绑定的三种典型场景
- 简单类型参数:
java复制User selectById(Integer id);
这种情况下,parameterObject就是id的值本身,propertyName为null,直接使用。
- Map参数:
java复制User selectByMap(Map<String, Object> params);
从Map中根据key获取value,key就是propertyName。
- POJO对象参数:
java复制User selectByUser(User user);
通过反射获取对象属性值,比如user.id对应propertyName="id"。
3. 自定义ParameterHandler拦截器的实战应用
3.1 拦截器实现步骤
创建一个ParameterHandler拦截器需要以下步骤:
- 定义Interceptor实现类:
java复制@Intercepts({
@Signature(type = ParameterHandler.class, method = "setParameters", args = {PreparedStatement.class})
})
public class ParameterInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
ParameterHandler parameterHandler = (ParameterHandler) invocation.getTarget();
// 获取原始参数
Field parameterObjectField = parameterHandler.getClass().getDeclaredField("parameterObject");
parameterObjectField.setAccessible(true);
Object parameterObject = parameterObjectField.get(parameterHandler);
// 参数处理逻辑
if (parameterObject instanceof Map) {
// 处理Map参数
} else if (parameterObject != null) {
// 处理其他类型参数
}
// 继续执行原流程
return invocation.proceed();
}
}
- 注册拦截器:
xml复制<plugins>
<plugin interceptor="com.example.ParameterInterceptor"/>
</plugins>
3.2 典型应用场景
- SQL注入防护:
java复制// 在拦截器中检查参数值
if (parameterObject instanceof String) {
String value = (String) parameterObject;
if (containsSqlInjection(value)) {
throw new IllegalArgumentException("检测到SQL注入风险");
}
}
- 敏感数据脱敏:
java复制if (parameterObject instanceof User) {
User user = (User) parameterObject;
if (user.getPassword() != null) {
user.setPassword(encrypt(user.getPassword()));
}
}
- 参数统一处理:
java复制// 统一处理日期格式
if (parameterObject instanceof Date) {
Date date = (Date) parameterObject;
parameterObject = new SimpleDateFormat("yyyy-MM-dd").format(date);
}
4. ParameterHandler拦截器的进阶技巧与坑点
4.1 获取BoundSql的两种方式
- 通过反射获取:
java复制Field boundSqlField = parameterHandler.getClass().getDeclaredField("boundSql");
boundSqlField.setAccessible(true);
BoundSql boundSql = (BoundSql) boundSqlField.get(parameterHandler);
- 通过MappedStatement获取(需要先获取Configuration):
java复制Field configurationField = parameterHandler.getClass().getDeclaredField("configuration");
configurationField.setAccessible(true);
Configuration configuration = (Configuration) configurationField.get(parameterHandler);
MappedStatement mappedStatement = configuration.getMappedStatement(statementId);
BoundSql boundSql = mappedStatement.getBoundSql(parameterObject);
4.2 常见问题排查
- 拦截器不生效:
- 检查@Intercepts注解配置是否正确
- 确认拦截器是否在mybatis-config.xml中正确配置
- 确保拦截器在插件链中的顺序正确
- 参数处理导致类型转换异常:
- 修改参数后需要确保类型与TypeHandler匹配
- 对于日期等特殊类型,建议保持原类型,在SQL中使用函数处理
- 性能问题:
- 避免在拦截器中做复杂计算
- 对于高频调用的简单SQL,考虑添加条件判断跳过不必要的处理
4.3 与其它拦截器的执行顺序
MyBatis拦截器的执行顺序由配置顺序决定,但ParameterHandler拦截器通常会在StatementHandler之后执行。如果需要确保执行顺序,可以在配置时明确指定:
xml复制<plugins>
<!-- 先执行的拦截器 -->
<plugin interceptor="com.example.FirstInterceptor"/>
<!-- 后执行的拦截器 -->
<plugin interceptor="com.example.ParameterInterceptor"/>
</plugins>
5. 实际案例:动态表名参数处理
假设我们需要实现一个动态表名的查询,表名通过参数指定但需要防止SQL注入:
- 定义Mapper接口:
java复制List<User> selectFromTable(@Param("tableName") String tableName, @Param("id") Integer id);
- 实现拦截器:
java复制@Override
public Object intercept(Invocation invocation) throws Throwable {
ParameterHandler parameterHandler = (ParameterHandler) invocation.getTarget();
Object parameterObject = getFieldValue(parameterHandler, "parameterObject");
if (parameterObject instanceof Map) {
Map<?, ?> paramMap = (Map<?, ?>) parameterObject;
String tableName = (String) paramMap.get("tableName");
if (!isValidTableName(tableName)) {
throw new IllegalArgumentException("非法表名");
}
// 修改SQL
BoundSql boundSql = getFieldValue(parameterHandler, "boundSql");
String newSql = boundSql.getSql().replace("${tableName}", tableName);
setFieldValue(boundSql, "sql", newSql);
}
return invocation.proceed();
}
private boolean isValidTableName(String name) {
return name.matches("[a-zA-Z_][a-zA-Z0-9_]*");
}
- 使用方式:
java复制// 会自动替换${tableName}为安全的表名
List<User> users = userMapper.selectFromTable("user_2023", 1);
这个案例展示了如何安全地处理动态表名场景,既保持了灵活性又防止了SQL注入风险。在实际项目中,类似的思路还可以应用于动态字段名、动态条件等场景。
