快收工时,
测试给提了一个Bug:
看到反馈,有点狐疑:这个地方最近没有啥改动,不可能不正常吧
任何问题的出现,都是有原因的。人家已经提了,总得有点回应吧。
虽然将信将疑,按流程,那先复现一下!
真的是这样
,只能看到一页。 平时测了很多单子,绝对不可能只有一页的。。。
有问题了耶
抓紧排查一下
根据接口找到代码,然后查看下最近的代码改动
只是在方法上加了一个注解。
分页的代码还在呢:
代码语言:javascript复制PageHelper.startPage(reqDTO.getPageNum(), reqDTO.getPageSize());
除了这个,最近一周这个方法没有别的改动了。
新加的这个注解,看着也没啥问题呀,其它项目也是这样的且已经上线,没有发现有啥问题。
问题现在就摆在这:的确是加了这个敏感数据解密注解后,分页就不正确。
对齐下颗粒度
先简单介绍下目前这个加解密组件的原理:
应用中需要引入敏感数据加解密组件,这个组件的作用:
1、负责把请求中的明文,转换为密文,然后用密文去数据库中进行查询。
2、负责把返回值中的密文,转换为明文,返回给用户。
那么,在应用中的哪个地方使用这个组件呢?
目前的软件系统中大多采用分层架构,譬如大名鼎鼎的MVC
MVC架构
在分层架构中,"越底层越稳定",即越靠近底层的层,其变化的频率和幅度通常越小。
这样一分析是不是就敏感数据加解密组件用在哪一层是不是就清晰了:DAO【数据访问层】。
需求就是这样一个敏感数据加解密的需求,问题是为什么这个接口报错了呢?
疑点
要说和之前上线的几个项目有什么不一样,就是多了个
代码语言:javascript复制PageHelper.startPage(pageNum, pageSize);
这种情况下写个测试代码Debug一下。
果然是引入解密注解的原因。因为加解密组件在处理List<E>返回值时会引入一个新的List<E>:
为什么没有报错?
因为Page<E>是ArrayList<E>的子类。 根据里氏替换原则,当基类对象出现的地方,子类对象应该能够无缝替换它,而不会引起任何错误或异常。
且,代码中使用PageInfo.of来处理返回的Page<E>对象。
只是分页相关的数据丢掉了。。。
真相大白!!!
怎么改?
- 直接改分页插件中对List的处理。依赖这个组件的项目多,改动影响的地方多。真要改,需要各种场景测一下,耗时多,周期长。好像等不起。。。
- 使用PagetHelper的另一种写法,也在Java8中推荐的写法。
protected Page<AdminOrderListVO> pages(AdminOrderListReqDTO reqDTO) {
Page<AdminOrderListVO> doSelectPage = PageHelper.startPage(reqDTO.getPageNum(), reqDTO.getPageSize(),true).doSelectPage(() -> {
orderInquiryRepository.listOrderForAdminV2(reqDTO);
});
return doSelectPage;
}
这种写法为什么没问题?
因为没有使用加密组件处理过的返回值。
经过PageHelper分页组件处理后,查到的List数据会被统一处理为Page<Object>,不管是哪种用法:
com.github.pagehelper.dialect.AbstractHelperDialect#afterPage
com.github.pagehelper.PageInterceptor#intercept
之前的为什么有问题?
因为使用了业务代码中使用加解密组件处理过的返回值。 这个返回值是个ArrayList而不是期望的Page。
是不是之前的pagehelper用法有问题?
不是的。pagehelper在5.1.10才开始支持Lambda,PageHelper.startPage在老版本中,只有这一种用法:
代码语言:javascript复制//获取第1页,10条内容,默认查询总数count
PageHelper.startPage(1, 10);
//紧跟着的第一个select方法会被分页
List<User> list = userMapper.selectIf(1);
assertEquals(2, list.get(0).getId());
assertEquals(10, list.size());
//分页时,实际返回的结果list类型是Page<E>,如果想取出分页信息,需要强制转换为Page<E>
assertEquals(182, ((Page) list).getTotal());
用List承接查到的Page,然后在需求的地方强制转型。
小结
本文深入剖析了一个关于分页失效的Bug案例,揭示了加解密组件如何在不经意间干扰了分页逻辑。通过详细的排查过程,我们发现敏感数据加解密组件在处理分页数据时,因其对返回值的转换操作,导致分页信息丢失,从而引发分页失效的问题。文章不仅探讨了加解密组件的原理和使用场景,还提供了针对性的解决方案,包括调整分页插件的处理逻辑或采用更合适的分页写法。此次经历提醒我们,在引入新组件时需全面考虑其对现有功能的影响,确保系统的稳定性和兼容性。
REFERNCE
SpringBoot 实现数据加密脱敏(注解 反射 AOP)
Springboot整合Hutool自定义注解实现数据脱敏
SpringBoot 采用 JsonSerializer 和 Aop 实现可控制的数据脱敏
数据脱敏实现:"想在哪脱就在哪脱,想脱谁就脱谁! ! !"
Spring Boot如何优雅实现数据加密存储、模糊匹配和脱敏
聊聊数据脱敏的 6 种方案
MyBatis 插件 注解 轻松实现数据脱敏
一个注解让 Spring Boot 项目接口返回数据脱敏
太强了!一个注解解决数据脱敏问题
数据脱敏的 3 种常见方案,好用到爆!
MyBatis拦截器优雅实现数据脱敏
关于ORM框架选型的一些思考
新项目建议使用MyBatis-Plus。 https://baomidou.com/introduce/
虽然MyBatis-Plus有这样那样的问题,但目前仍然在持续更新,且提升开发效率真的是杠杠的。
PageHelper在一些老项目中仍有使用,下面再简单介绍下。
Mybatis-PageHelper的使用方法
引入分页插件依赖:
代码语言:javascript复制<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>最新版本</version>
</dependency>
PageHelper的原理
PageHelper 方法使用了静态的 ThreadLocal 参数,分页参数和线程是绑定的。
只要你可以保证在 PageHelper 方法调用后紧跟 MyBatis 查询方法,这就是安全的。因为 PageHelper 在 finally 代码段中自动清除了 ThreadLocal 存储的对象。
PageHelper.startPage 静态方法调用
除了 PageHelper.startPage 方法外,还提供了类似用法的 PageHelper.offsetPage 方法。
在你需要进行分页的 MyBatis 查询方法前调用 PageHelper.startPage 静态方法即可,紧跟在这个方法后的第一个MyBatis 查询方法会被进行分页。
代码语言:javascript复制//获取第1页,10条内容,默认查询总数count
PageHelper.startPage(1, 10);
//紧跟着的第一个select方法会被分页
List<User> list = userMapper.selectIf(1);
assertEquals(2, list.get(0).getId());
assertEquals(10, list.size());
//分页时,实际返回的结果list类型是Page<E>,如果想取出分页信息,需要强制转换为Page<E>
assertEquals(182, ((Page) list).getTotal());
分页插件Mybatis-PageHelper支持以下几种调用方式:
代码语言:javascript复制//第一种,RowBounds方式的调用
List<User> list = sqlSession.selectList("x.y.selectIf", null, new RowBounds(0, 10));
//第二种,Mapper接口方式的调用,推荐这种使用方式。
PageHelper.startPage(1, 10);
List<User> list = userMapper.selectIf(1);
//第三种,Mapper接口方式的调用,推荐这种使用方式。
PageHelper.offsetPage(1, 10);
List<User> list = userMapper.selectIf(1);
//第四种,参数方法调用
//存在以下 Mapper 接口方法,你不需要在 xml 处理后两个参数
public interface CountryMapper {
List<User> selectByPageNumSize(
@Param("user") User user,
@Param("pageNum") int pageNum,
@Param("pageSize") int pageSize);
}
//配置supportMethodsArguments=true
//在代码中直接调用:
List<User> list = userMapper.selectByPageNumSize(user, 1, 10);
//第五种,参数对象
//如果 pageNum 和 pageSize 存在于 User 对象中,只要参数有值,也会被分页
//有如下 User 对象
public class User {
//其他fields
//下面两个参数名和 params 配置的名字一致
private Integer pageNum;
private Integer pageSize;
}
//存在以下 Mapper 接口方法,你不需要在 xml 处理后两个参数
public interface CountryMapper {
List<User> selectByPageNumSize(User user);
}
//当 user 中的 pageNum!= null && pageSize!= null 时,会自动分页
List<User> list = userMapper.selectByPageNumSize(user);
//第六种,ISelect 接口方式
//jdk6,7用法,创建接口
Page<User> page = PageHelper.startPage(1, 10).doSelectPage(new ISelect() {
@Override
public void doSelect() {
userMapper.selectGroupBy();
}
});
//jdk8 lambda用法
Page<User> page = PageHelper.startPage(1, 10).doSelectPage(()-> userMapper.selectGroupBy());
//也可以直接返回PageInfo,注意doSelectPageInfo方法和doSelectPage
pageInfo = PageHelper.startPage(1, 10).doSelectPageInfo(new ISelect() {
@Override
public void doSelect() {
userMapper.selectGroupBy();
}
});
//对应的lambda用法
pageInfo = PageHelper.startPage(1, 10).doSelectPageInfo(() -> userMapper.selectGroupBy());
//count查询,返回一个查询语句的count数
long total = PageHelper.count(new ISelect() {
@Override
public void doSelect() {
userMapper.selectLike(user);
}
});
//lambda
total=PageHelper.count(()->userMapper.selectLike(user));
https://github.com/pagehelper/Mybatis-PageHelper/blob/master/wikis/zh/HowToUse.md