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[] 数组,下面读取流时就调用这个数组就行)
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[] 数组,下面读取流时就调用这个数组就行)