likes
comments
collection
share

RequestBodyAdviceAdapter 在重构中的实战应用与注意事项

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

前言

哈喽,大家好,今天和大家分享在重构过程中,使用RequestBodyAdviceAdapter来解决实际业务场景的一些心得,本章主要包括以下几个问题:

  1. 在什么场景下使用 RequestBodyAdviceAdapter
  2. 适用的场景
  3. 使用方式
  4. 使用中可能遇到的一些坑
1.在什么场景下使用

在重构过程中遇到这样一种场景,用户访问管理平台的菜单列表,需要根据当前登录用户,查询到该用户所属机构的数据,比如用户张三属于A机构,那张三的列表中只能显示A机构的数据,为了防止请求数据被恶意篡改,前端是没有将机构编码传入到后台接口中,需要后台开发人员从Shiro中取出该用户的机构信息,并将机构编码作为查询条件执行后续的sql语句。

代码如下:

@PostMapping("list")
public Result seletList(@RequestBody QueryList vo) {
        JwtObject jwtObject = (JwtObject) SecurityUtils.getSubject().getPrincipal();
        String hospitalId = jwtObject.getHospitalId();
        vo.setHospitalId(hospitalId);
        return Result.success(service.list(vo));
}

上面这种方式可以解决问题,但是我相信应该没有人会这么写,毕竟几百个方法,总不能每个方法都写一遍吧。

除此之外,我们可以使用RequestBodyAdviceAdapter 来解决,它是 Spring MVC 中的一个组件,主要是用于拦截和处理 被@RequestBody 注解修饰的方法,就比如上面这个post方法 。

2.RequestBodyAdviceAdapter 是什么

其实这个知识点大家基本都是知道的,但是这里还是要简单的介绍一下,RequestBodyAdviceAdapter 是一个抽象类, 它的主要作用是在请求体数据被实际读取并映射到相应对象之前(以上述的seletList方法为例,就是在进入这个方法之前,还没有对vo进行赋值),对请求体数据进行预处理或增强 。

简单的理解:就是在进入selectList方法之前,对前端或者第三方传过来的参数进行预处理,处理完成后才会正式进入接口中执行业务逻辑。我之前我也是经常用这个类来完成一些场景,比如:

  • 请求体数据校验:我们可以拦截请求体数据,校验请求体是否为空,是否存在指定字段等。
  • 请求体数据解密/加密:这个场景我也经常用到,之前提供接口给第三方调用时,我在这里做请求参数的数据解密。
  • 请求体日志记录:记录请求体日志,方便后续排查问题。
  • 请求入参预处理:这个就是本章中的举例场景,对所有的入参增加机构编码

3.代码示例

  • 用法一:创建子类,继承RequestBodyAdviceAdapter

下面这种方式方式可以实现对所有被@RequestBody修饰的方法进行处理,增加orgCode变量


@Slf4j
@ControllerAdvice
public class ChildAdviceAdapter extends RequestBodyAdviceAdapter {


@Override
public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
        return true;
    }

    @Override
public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
        String httpBody = resolve(inputMessage,resolveHospitalId);
            if (httpBody == null) {
                log.error("参数处理失败");
                throw new BusinessException("参数处理失败");
            }
        //返回处理后的消息体
        return new CustomHttpMessage(new ByteArrayInputStream(httpBody.getBytes()),inputMessage.getHeaders());
    }

    /**
     *处理入参
     * @param inputMessage 消息体
     * @return 明文
     */
    private String resolve(HttpInputMessage inputMessage,ResolveHospitalId resolveHospitalId) throws IOException {
        InputStream encryptStream = inputMessage.getBody();
        String encryptBody = StreamUtils.copyToString(encryptStream, Charset.defaultCharset());

        JwtObject jwtObject = (JwtObject) SecurityUtils.getSubject().getPrincipal();
        String orgCode = jwtObject.getOrgCode();
        
        JSONObject json = JSON.parseObject(encryptBody);
        json.put("orgCode",hospitalId);
        return json.toJSONString();
    }

}
  • 用法二:配合注解使用

如果有些方法不想走这个逻辑,可以通过增加自定义注解的方式进行排除,用法也很简单

第一步,定义一个注解

@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface OrgAnnotation {
    String value() default "";
}

第二步,增加方法判断是否存在注解

    /**
     * 是否存在注解
     *
     * @param methodParameter methodParameter
     * @return true/false
     */
    private boolean supportRequest(MethodParameter methodParameter) {
        Annotation methodAnnotation = methodParameter.getMethodAnnotation(OrgAnnotation.class);
        if(methodAnnotation != null){
           //如果业务需要,可以在注解上定义参数,处理更多的场景
            String type = methodParameter.getMethod().getAnnotation(ResolveHospitalId.class).value();
            if("3".equals(businessType)){
                return false;
            }
        }
        return true;
    }

@Override
public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
        return supportRequest(methodParameter);
    }

 @Override
public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
.....省略
}

使用注解

@OrgAnnotation("3")
@PostMapping("list")
public Result seletList(@RequestBody QueryList vo) {
        JwtObject jwtObject = (JwtObject) SecurityUtils.getSubject().getPrincipal();
        String hospitalId = jwtObject.getHospitalId();
        vo.setHospitalId(hospitalId);
        return Result.success(service.list(vo));
}

4.一些注意点

  • 仅适用于POST、PUT、PATCH等请求方法:也就是前面说的只能处理带有请求体的请求方法
  • 请求体数据只能读取一次: 如果在 RequestBodyAdviceAdapter 中删除掉了请求体中的数据,那么后续的业务逻辑就无法再获取请求体数据了,因此在使用时需要注意不要消费掉原始的请求体数据。
  • 处理顺序的问题: 如果配置了多个 RequestBodyAdviceAdapter 实例,这些实例的处理顺序是无法保证的
  • 线程安全问题: 可能会被多个线程同时调用,因此需要注意线程安全问题,防止出现并发问题。

最后

RequestBodyAdviceAdapter 的使用是比较简单的,之所以我还是写了这一篇文章,是因为我觉得如果使用得当,用起来还是蛮方便顺手的。本章对自定义注解的使用没有做过多的介绍,下一篇我会分享在重构过程中,使用自定义注解+Aop的一些使用场景。