欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Spring MVC 基于 RequestBodyAdvice 和 ResponseBodyAdvice 全局处理输入输出 博客分类: Spring spring 

程序员文章站 2024-03-22 08:14:46
...
使用场景:

1.需要对项目中的所有输入进行前后空格的过滤
2.替换一些特殊字符的输入
3.解密一些关键性字段
4.注入一些参数在请求方法的时候
5.返回参数统一处理,如果后台返回空,统一返回成功信息
6.身份证等特殊字符统一做 * 号处理等

Code:
主要就是用到了 RequestBodyAdvice 和 ResponseBodyAdvice 两个接口和一个注解@ControllerAdvice

RequestBodyAdvice:在 sping 4.2 新加入的一个接口,它可以使用在 @RequestBody 修改的参数之前进行参数的处理,比如进行参数的解密。

ResponseBodyAdvice:在 spring 4.1 新加入的一个接口,在消息体被HttpMessageConverter写入之前允许 Controller 中 @ResponseBody 修饰的方法调整响应中的内容,比如进行相应的加密。

SpringBoot 中可以利用@RequestBody这样的注解完成请求内容体与对象的转换。而RequestBodyAdvice 则可用于在请求内容对象转换的前后时刻进行拦截处理,其定义了几个方法:
supports:判断是否支持handleEmptyBody,当请求体为空时调用
beforeBodyRead:在请求体未读取(转换)时调用
afterBodyRead:在请求体完成读取后调用


SpringMVC对出参和入参有非常友好的拓展支持,方便你对数据的输入和输出有更大的执行权,我们如何通过SpringMVC定义的结果做一系列处理呢?

RequestBodyAdvice:
入参,针对所有以@RequestBody的参数做处理
ResponseBodyAdvice:
出参,针对所有以@ResponseBody的参数做处理

@ControllerAdvice
public class LogResponseBodyAdvice implements ResponseBodyAdvice {
/**
*
* @param returnType
* @param converterType
* @return
*/
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true;
}

@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
// 做任何事情 body 就是返回的结果对象,没有处理之前
return body;
}
}

注意事项: 自定义的处理对象类上必须得加上@ControllerAdvice注解!

为什么?
源码中RequestMappingHandlerAdapter类在执行initControllerAdviceCache()做初始化的时候会执行一个:

List<ControllerAdviceBean> beans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
AnnotationAwareOrderComparator.sort(beans);

而ControllerAdviceBean.findAnnotatedBeans方法会查找类上有ControllerAdvice注解的类才会加入到处理当中:

public static List<ControllerAdviceBean> findAnnotatedBeans(ApplicationContext applicationContext) {
List<ControllerAdviceBean> beans = new ArrayList<ControllerAdviceBean>();
for (String name : BeanFactoryUtils.beanNamesForTypeIncludingAncestors(applicationContext, Object.class)) {
if (applicationContext.findAnnotationOnBean(name, ControllerAdvice.class) != null) {
beans.add(new ControllerAdviceBean(name, applicationContext));
}
}
return beans;
}


所以大家可以根据自己的需要,定义结果的入参和出参结果做一些特殊处理。


请求参数去空格:

/**
* 去掉前后空格和特殊字符
*/
@Slf4j
@ControllerAdvice
public class CustomRequestBodyAdvice implements RequestBodyAdvice {
    @Override
    public boolean supports(MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
        return true;
    }

    @Override
    public Object handleEmptyBody(Object body, HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
        return body;
    }

    @Override
    public HttpInputMessage beforeBodyRead(HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) throws IOException {
        return new CustomHttpInputMessage(httpInputMessage);
    }

    @Override
    public Object afterBodyRead(Object body, HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
        return body;
    }

    class CustomHttpInputMessage implements HttpInputMessage{
        private HttpInputMessage origin;

        public CustomHttpInputMessage(HttpInputMessage httpInputMessage) {
            this.origin = httpInputMessage;
        }

        @Override
        public InputStream getBody() throws IOException {
            HttpHeaders headers = origin.getHeaders();
            InputStream body = origin.getBody();

            // 空参,get 请求,流为空,非 application/json 请求,不处理参数
            MediaType contentType = headers.getContentType();
            if(contentType == null){return body;}
            if(!contentType.isCompatibleWith(MediaType.APPLICATION_JSON)){return body;}
            if(body == null){return body;}
            String params = IOUtils.toString(body, "utf-8");
            if(StringUtils.isBlank(params)){return body;}

            // 正式过滤 json 参数
            Object parse = JSON.parse(params);
            if (parse instanceof JSONArray) {
                JSONArray jsonArray = (JSONArray) parse;
                trimJsonArray(jsonArray);
            } else if (parse instanceof JSONObject) {
                trimJsonObject((JSONObject) parse);
            } else {
                log.error("参数不支持去空格:" + parse+ " contentType:"+contentType);
            }
            return IOUtils.toInputStream(JSON.toJSONString(parse, SerializerFeature.WriteMapNullValue), "UTF-8");
        }

        private void trimJsonObject(JSONObject jsonObject) {
            Iterator<Map.Entry<String, Object>> iterator = jsonObject.entrySet().iterator();
            while (iterator.hasNext()) {
                Map.Entry<String, Object> next = iterator.next();
                String key = next.getKey();
                Object value = next.getValue();
                if (value instanceof JSONArray) {
                    trimJsonArray((JSONArray) value);
                }else if(value instanceof JSONObject){
                    trimJsonObject((JSONObject) value);
                }else if(value instanceof  String){
                    String trimValue = StringUtils.trim(ObjectUtils.toString(value));
                    next.setValue(filterDangerString(trimValue));
                }
            }
        }

        private void trimJsonArray(JSONArray jsonArray) {
            for (int i = 0; i < jsonArray.size(); i++) {
                Object object = jsonArray.get(i);
                if(object instanceof JSONObject){
                    JSONObject jsonObject = jsonArray.getJSONObject(i);
                    trimJsonObject(jsonObject);
                }else if(object instanceof  String){
                    String trimValue = StringUtils.trim(ObjectUtils.toString(object));
                    jsonArray.set(i,trimValue);
                }

            }
        }

        @Override
        public HttpHeaders getHeaders() {
            return origin.getHeaders();
        }

        private String filterDangerString(String value) {
            if(StringUtils.isBlank(value))return value;

            value = value.replaceAll(";", ";");
            value = value.replaceAll("'", "‘");
            value = value.replaceAll("<", "《");
            value = value.replaceAll(">", "》");
            value = value.replaceAll("\\(", "(");
            value = value.replaceAll("\\)", ")");
            value = value.replaceAll("\\?", "?");
            return value;
        }

    }
}


各种拦截器接口的执行顺序:

Filter ---> HandlerInterceptor ---> RequestBodyAdvice ---> @Aspect.Pointcut ---> Controller.body


@RestControllerAdvice和@ControllerAdvice 在具体使用上:

1)注解有@ControllerAdvice的类, 需要在具体方法上同时添加@ExceptionHandler和@ResponseBody注解;

2)注解有@RestControllerAdvice的类,只需要在具体方法上添加@ExceptionHandler注解。


注意:在 Spring MVC 中,ServletRequest中getReader()和getInputStream()只能调用一次(Java中input流只能读取一次,主要原因是通标记的方法来判断流是否读取完毕,读取位 -1就是流读取完毕)。而又由于@RequestBody注解获取输出参数的方式也是根据流的方式获取的。所以如果我们前面使用流获取后,后面的@RequestBody就获取不到对应的输入流了。

解决思路就是:先读取流,然后在将流写回去(将流赋值给一个 byte[] 数组,下面读取流时就调用这个数组就行)








相关标签: spring