动态修改SpringBoot的Request参数(自定义注解+切面)
背景
笔者最近接到一个需求,需要将系统中所有的列表(分页接口)增加一个排序的功能,系统是基于MybatisPlus实现的,分页方面并没有太多问题,但是对于排序的字段会有明显的几个问题如下:
- 列表中部分字段是字典转换的,与原值字段不一致
比如状态StatusDesc
为中文禁用
,真实排序字段为Status
- 多表关联查询,必须指定字段的表来源,否则查询会报错
select t1.* from student t1 join class t2 on t1.class_id=t2.id order name
name
字段必须指定是学生表还是班级表的,不然无法进行查询
温馨提示:代码中大量使用了Hutool
工具类,请替换为自己项目中的工具类,抱歉:)
解法
笔者的解法有几个前提,先放出来,免得大家直接复制代码发现无法使用,但是整体的思路是一致的,就算情况不一致,也可以借鉴修改,实现相关功能
前提
- 列表分页都是
GET
请求,参数由URLParams
传递 - 生成
MybatisPlus
分页信息Page
有统一入口,方便管理
思路
按照以上前提,很简单就有一个思路产生,直接修改URLParams
不就可以了吗?是的,但是SpringMVC框架的原生的Request
对象,有一个特点,只能被读一次,且无法修改。
这个问题引发了进一步的思考,有没有方式解决这个问题呢,看一些文章,很自然的发现相关类HttpServletRequestWrapper
,继承该类可以通过组合
的方式,对Request
进行增强
其实这个增强很简单,即在调用getParameter(String name)
之前,先调用一个前置的Map
中的值,如果Map
为空,或者取值为空,就继续调用getParameter(String name)
完成原有逻辑,将核心代码给出如下:
public class SortRewriteBodyWrapper extends HttpServletRequestWrapper { private Map params; public SortRewriteBodyWrapper(HttpServletRequest request) { super(request); } @Override public String getParameter(String name) { String result; if (CollUtil.isEmpty(params)) { return super.getParameter(name); } Object v = params.get(name); if (v != null) { String[] strArr = (String[]) v; if (strArr.length > 0) { result = strArr[0]; } else { result = null; } return result; } return super.getParameter(name); } public void addParam(String key, String value) { if (CollUtil.isEmpty(params)) { params = new HashMap(); } params.put(key, new String[]{value}); } }
集成
有了核心实现以后,就是要将该Wrapper
集成进入系统之中,笔者是通过自定义Filter
的方式集成的,主要是因为:
- 集成简单,方便
- 可以通过
Filter Flag
控制影响范围 - 可以通过设置
Filter
的顺序不影响其他功能
Filter部分
public class SortRewriteParamFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { filterChain.doFilter(new SortRewriteBodyWrapper(request), response); } @Override protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException { String pageNum = ServletUtils.getParameter("pageNum"); return StrUtil.isEmpty(pageNum); } }
至于Filter集成到MvcConfig
的部分这里笔者就略过了,不同版本的SpringBoot
的配置不尽相同,大家去搜索一下,就能找到
自定义注解
这里解释一下,为什么会有自定义注解,因为每一个列表的字段都不相同,同时可能在返回给前端的过程中,还进行了向Vo
或者Domain
的转换,需要通过注解进行灵活动态的控制,方便配置排序的别名
SortRewrite
@Target({ElementType.PARAMETER, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface SortRewrite { Class rewriter(); String defaultSort() default "createTime"; }
用途: Controller,标识列表中的排序值的转换列表
属性说明:
- rewriter() Vo的类
- defaultSort() 默认排序的字段
示例:
@GetMapping("/list") @SortRewrite(rewriter = Student.class) public Page list() { return null; }
SortAlias
@Target({ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface SortAlias { String value(); }
用途: Vo字段注解,用于标识排序的别名
属性说明:
- value() 别名的名称
- defaultSort() 默认排序的字段
示例:
public class Student { @SortAlias("t1.name") private String name; }
切面
@Order(0) @Aspect @Component @Slf4j public class SortRewriteAspect { @Pointcut("@annotation(SortRewrite)") public void pointCut() { } @Before("pointCut()") public void beforePage(JoinPoint joinPoint) { HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())) .getRequest(); String orderCol = request.getParameter("orderByColumn"); boolean isWrapper = request instanceof HttpServletRequestWrapper; //循环获取最原始的request,直到找到SortRewriteBodyWrapper while (isWrapper) { if (request instanceof SortRewriteBodyWrapper) { break; } request = (HttpServletRequest) ((HttpServletRequestWrapper) request).getRequest(); isWrapper = request instanceof HttpServletRequestWrapper; } if (request instanceof SortRewriteBodyWrapper) { SortRewriteBodyWrapper reqWrap = (SortRewriteBodyWrapper) request; // 如果有排序字段,则进行重写 // 获取重写类 SortRewrite sortWrite = getSortWrite(joinPoint); if (StrUtil.isNotEmpty(orderCol)) { Class rewriter = sortWrite.rewriter(); // 重写排序字段 Field orderField = ReflectUtil.getField(rewriter, orderCol); if (orderField != null) { SortAlias sortAlias = orderField.getAnnotation(SortAlias.class); // 如果有别名,则进行重写 if (sortAlias != null) { String alias = sortAlias.value(); if (StrUtil.isNotEmpty(alias)) { // 重写排序字段 reqWrap.addParam("orderByColumn", alias); } } } } else { String sort = sortWrite.defaultSort(); reqWrap.addParam("orderByColumn", sort); reqWrap.addParam("isAsc", "desc"); } } } private static SortRewrite getSortWrite(JoinPoint joinPoint) { try { Class targetCls = joinPoint.getTarget().getClass(); MethodSignature ms = (MethodSignature) joinPoint.getSignature(); Method targetMethod = targetCls.getDeclaredMethod(ms.getName(), ms.getParameterTypes()); SortRewrite sortRewrite = targetMethod.getAnnotation(SortRewrite.class); return sortRewrite; } catch (Exception e) { throw new RuntimeException(e); } } }
切面逻辑概括:
- 循环直到获取到
SortRewriteBodyWrapper
类型的Request
- 反射获取
SortRewrite
注解中的类 - 反射获取
SortAlias
中的别名 - 将真实的排序字段加到前置的
Map
中 - 如果不存在排序字段,设置默认字段
缺陷
这个版本的代码是笔者初步的一个设计,肯定会有一些疏漏和欠考虑的地方,比如如果Vo
被复用了且两个接口的排序字段不同怎么处理等等,如果大家关于代码和缺陷有问题和想法,欢迎在评论区留言,最后,感谢浏览,感恩