欢迎关注公众号:【冬瓜白】
问题初现:手机号查询突然失效
前天测试同学突然找上门来:"你们项目有个问题卡了好几天,用手机号查不到订单了!"我心想这不应该啊,手机号查询可是基础功能。但仔细一看,这问题居然还真阻塞测试好几天了,看来不是简单的SQL写错那么简单。
我们的系统对手机号这类隐私数据有特殊处理:
- 数据库存两个字段:
tel
(不可逆加密)和tel_c
(可逆加密) - 查询时通过MyBatis拦截器自动处理加解密
排查过程
刚开始我怀疑是SQL写错了,但很快排除了这个可能。于是把目光转向了加解密拦截器——这个平时默默工作的"幕后英雄"。
这个拦截器的核心逻辑其实很清晰:
- 解析SQL参数,判断哪些字段需要加密
- 对需要加密的字段进行处理
- 执行SQL并处理返回结果
问题很可能出在第一步的参数解析上。这里用到了MyBatis的boundSql.getParameterObject()
方法,它根据参数类型返回不同结果:
- 单个参数:直接返回
- 多个参数:返回
MapperMethod.ParamMap
- 对象参数:返回对象本身
对比修改前后的Mapper接口:
修改前:
List<SrvInfo> searchSrvList(
@Param("searchVO") SrvSearchVo searchVo,
@Param("start") int start,
@Param("pageSize") int pageSize,
@Param("maxNum") Integer maxNum);
修改后:
List<SrvInfo> searchSrvList(
@Param("searchVO") SrvSearchVo searchVo,
@Param("start") int start,
@Param("pageSize") int pageSize,
@Param("maxNum") Integer maxNum,
@Param("isOversea") boolean isOversea); // 新增参数
就是这新增的isOversea
参数搞的鬼!拦截器里用了个"偷懒"的实现:
Object obj = ((MapperMethod.ParamMap) parameterObj).values().iterator().next();
直接取了ParamMap的第一个值来判断是否需要加密。
HashMap 的顺序不可靠
问题在于MapperMethod.ParamMap
继承自HashMap
,而HashMap的迭代顺序是不保证的!新增一个参数可能导致原有参数的顺序改变。
写个简单测试就一目了然:
public static void main(String[] args) {
System.out.println("修改前的 MapperMethod.ParamMap:");
Map<String, Object> map2 = new HashMap<>();
map2.put("searchVO", "searchVO");
map2.put("start", "start");
map2.put("pageSize", "pageSize");
map2.put("param3", "param3");
map2.put("maxNum", "maxNum");
map2.put("param4", "param4");
map2.put("param1", "param1");
map2.put("param2", "param2");
// 使用迭代器遍历 map2
Iterator<Map.Entry<String, Object>> iterator2 = map2.entrySet().iterator();
while (iterator2.hasNext()) {
Map.Entry<String, Object> entry = iterator2.next();
System.out.println(entry.getKey() + " -> " + entry.getValue());
}
System.out.println("================");
System.out.println("修改后的 MapperMethod.ParamMap:");
Map<String, Object> map1 = new HashMap<>();
// 将 Key 和 Value 设置为相同的值
map1.put("param5", "param5");
map1.put("isOversea", "isOversea");
map1.put("searchVO", "searchVO");
map1.put("start", "start");
map1.put("pageSize", "pageSize");
map1.put("param3", "param3");
map1.put("maxNum", "maxNum");
map1.put("param4", "param4");
map1.put("param1", "param1");
map1.put("param2", "param2");
Iterator<Map.Entry<String, Object>> iterator = map1.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, Object> entry = iterator.next();
System.out.println(entry.getKey() + " -> " + entry.getValue());
}
}
输出:
修改前的 MapperMethod.ParamMap:
searchVO -> searchVO
start -> start
pageSize -> pageSize
param3 -> param3
maxNum -> maxNum
param4 -> param4
param1 -> param1
param2 -> param2
================
修改后的 MapperMethod.ParamMap:
param5 -> param5
isOversea -> isOversea
searchVO -> searchVO
start -> start
pageSize -> pageSize
param3 -> param3
maxNum -> maxNum
param4 -> param4
param1 -> param1
param2 -> param2
解决方案
原来的实现太依赖HashMap的顺序了,这明显是个坑。改进方案也很简单,不取巧,直接全量遍历 MapperMethod.ParamMap
即可。
总结
不要依赖HashMap的顺序:这是老生常谈,但实际开发中还是容易被忽略而踩坑。很多工具用起来简单,但想用好必须了解其底层机制。