likes
comments
collection
share

动态修改SpringBoot的Request参数(自定义注解+切面)

作者站长头像
站长
· 阅读数 18

背景

笔者最近接到一个需求,需要将系统中所有的列表(分页接口)增加一个排序的功能,系统是基于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<T>有统一入口,方便管理

思路

按照以上前提,很简单就有一个思路产生,直接修改URLParams不就可以了吗?是的,但是SpringMVC框架的原生的Request对象,有一个特点,只能被读一次,且无法修改。

这个问题引发了进一步的思考,有没有方式解决这个问题呢,看一些文章,很自然的发现相关类HttpServletRequestWrapper,继承该类可以通过组合的方式,对Request进行增强

其实这个增强很简单,即在调用getParameter(String name)之前,先调用一个前置的Map<String,String[]>中的值,如果Map为空,或者取值为空,就继续调用getParameter(String name)完成原有逻辑,将核心代码给出如下:

public class SortRewriteBodyWrapper extends HttpServletRequestWrapper {

    private Map<String, String[]> 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<Student> 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被复用了且两个接口的排序字段不同怎么处理等等,如果大家关于代码和缺陷有问题和想法,欢迎在评论区留言,最后,感谢浏览,感恩