resultsethandler 在 sql 执行完毕、jdbc resultset 可读后,将密文行数据安全解密并映射为 java 对象,不参与参数写入、sql 构建或事务;需结合 @encryptfield 注解与类型校验精准识别字段,手动接管映射流程,妥善处理空值、异常与性能问题,确保解密结果无缝进入后续业务环节。

ResultSetHandler 拦截时机和作用边界
MyBatis 的 ResultSetHandler 是查询结果填充阶段的最终处理者,它在 SQL 执行完成、JDBC ResultSet 可读之后,将每一行数据映射为 Java 对象(如 User)——这正是解密操作最安全、最可控的位置:原始密文已从数据库取出,但尚未被业务代码感知。
注意它不处理参数写入,也不参与 SQL 构建或事务控制;如果你在 ParameterHandler 或 Executor 层做了加密,那 ResultSetHandler 就只负责“逆向还原”,不能替代写入侧逻辑。
常见错误现象包括:
- 解密后字段未正确赋值到对象属性(比如用了
field.setAccessible(true)但没 catchIllegalAccessException) - 对 null 密文字段直接调用解密方法,抛出
NullPointerException - 使用了非 public 字段但未显式设置
setAccessible(true),导致反射失败
如何精准识别需要解密的字段
靠字段名硬匹配(如判断是否为 "phone")不可靠,容易误伤或漏解。推荐结合注解 + 类型双重校验:
- 定义一个运行时注解
@EncryptField,标注在实体类字段上(如private String phone;) - 在
intercept()方法中,先通过resultObject.getClass()获取类型,再反射扫描所有Field,检查是否含该注解 - 进一步校验字段类型是否为
String或你封装的Encrypt类(避免对Long id也尝试解密) - 若启用多策略(如 AES/SM4 混用),可从注解中读取
strategy()属性传给解密工具类
不要在每次拦截都重复反射扫描——把 Class → List<field></field> 映射缓存到 ConcurrentHashMap 中,首次加载后复用。
解密逻辑必须绕开 MyBatis 默认映射流程
MyBatis 默认会把数据库列值(如 phone_enc)直接 set 到同名列对应的属性(phone)。如果你的数据库字段名是 phone_enc,但 Java 属性叫 phone,就不能依赖 ResultMap 自动绑定,而要手动接管。
实操建议:
- 在 XML 中明确指定列别名,让密文列映射到带标记的属性:
<result column="phone_enc" property="phone"></result> - 在
intercept()中拿到resultObject后,遍历已缓存的@EncryptField字段,用ResultSet.getString("phone_enc")取值,解密后再field.set(resultObject, decryptedValue) - 如果数据库列名和属性名不一致(如 DB 列是
mobile_cipher,Java 属性是phone),需在注解中额外声明列名:@EncryptField(column = "mobile_cipher") - 避免修改
ResultSet本身(如调用updateString),它可能已被关闭或只读
空值、异常与性能的现实处理
生产环境里,密文字段可能为空、乱码、或被人工篡改过,解密失败不能让整个查询崩掉。
- 对
null或空字符串密文,直接跳过解密,保留原值(field.set(..., null)) - 捕获解密异常(如
InvalidCipherTextException),记录 warn 日志并设为null或兜底值,不 throw - 解密操作本身有 CPU 开销,若单次查询返回上千条记录,建议异步批量解密或加开关控制(如配置项
mybatis.db-safe.decrypt-enabled=true) - 不要在
ResultSetHandler中调用远程服务或 DB 查询做密钥管理——密钥应预加载进内存,用ThreadLocal或 Spring@Value注入
真正难的不是写完解密逻辑,而是让解密后的值能无缝进入后续所有环节:JSON 序列化、日志打印、DTO 转换——这意味着解密必须发生在 MyBatis 完成对象构建之后、但业务代码第一次访问该字段之前。这个窗口很窄,稍有延迟或错位,就会出现“查出来还是密文”的诡异问题。










