SpringBoot如何使用RequestBodyAdvice进行统一参数处理

时间:2022-03-20 08:49:09

SpringBoot RequestBodyAdvice参数处理

在实际项目中 , 往往需要对请求参数做一些统一的操作 , 例如参数的过滤 , 字符的编码 , 第三方的解密等等 , Spring提供了RequestBodyAdvice一个全局的解决方案 , 免去了我们在Controller处理的繁琐 .

RequestBodyAdvice仅对使用了@RqestBody注解的生效 , 因为它原理上还是AOP , 所以GET方法是不会操作的.

  1. package com.xbz.common.web;
  2. import org.springframework.core.MethodParameter;
  3. import org.springframework.http.HttpHeaders;
  4. import org.springframework.http.HttpInputMessage;
  5. import org.springframework.http.converter.HttpMessageConverter;
  6. import org.springframework.web.bind.annotation.ControllerAdvice;
  7. import org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdvice;
  8. import java.io.IOException;
  9. import java.io.InputStream;
  10. import java.lang.reflect.Type;
  11.  
  12. /**
  13. * @title 全局请求参数处理类
  14. * @author Xingbz
  15. * @createDate 2019-8-2
  16. */
  17. @ControllerAdvice(basePackages = "com.xbz.controller")//此处设置需要当前Advice执行的域 , 省略默认全局生效
  18. public class GlobalRequestBodyAdvice implements RequestBodyAdvice {
  19.  
  20. /** 此处如果返回false , 则不执行当前Advice的业务 */
  21. @Override
  22. public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
  23. // return methodParameter.getMethod().isAnnotationPresent(XXApiReq.class);
  24. return false;
  25. }
  26.  
  27. /**
  28. * @title 读取参数前执行
  29. * @description 在此做些编码 / 解密 / 封装参数为对象的操作
  30. *
  31. * */
  32. @Override
  33. public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
  34. return new XHttpInputMessage(inputMessage, "UTF-8");
  35. }
  36.  
  37. /**
  38. * @title 读取参数后执行
  39. * @author Xingbz
  40. */
  41. @Override
  42. public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
  43. return inputMessage;
  44. }
  45.  
  46. /**
  47. * @title 无请求时的处理
  48. */
  49. @Override
  50. public Object handleEmptyBody(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
  51. return body;
  52. }
  53. }
  54.  
  55. //这里实现了HttpInputMessage 封装一个自己的HttpInputMessage
  56. class XHttpInputMessage implements HttpInputMessage {
  57. private HttpHeaders headers;
  58. private InputStream body;
  59.  
  60. public XHttpInputMessage(HttpInputMessage httpInputMessage, String encode) throws IOException {
  61. this.headers = httpInputMessage.getHeaders();
  62. this.body = encode(httpInputMessage.getBody(), encode);
  63. }
  64.  
  65. private InputStream encode(InputStream body, String encode) {
  66. //省略对流进行编码的操作
  67. return body;
  68. }
  69.  
  70. @Override
  71. public InputStream getBody() {
  72. return body;
  73. }
  74.  
  75. @Override
  76. public HttpHeaders getHeaders() {
  77. return null;
  78. }
  79. }

Spring默认提供了接口的抽象实现类RequestBodyAdviceAdapter , 我们可以继承这个类按需实现 , 让代码更简洁一点

  1. package org.springframework.web.servlet.mvc.method.annotation;
  2. import java.io.IOException;
  3. import java.lang.reflect.Type;
  4. import org.springframework.core.MethodParameter;
  5. import org.springframework.http.HttpInputMessage;
  6. import org.springframework.http.converter.HttpMessageConverter;
  7. import org.springframework.lang.Nullable;
  8. public abstract class RequestBodyAdviceAdapter implements RequestBodyAdvice {
  9.  
  10. @Override
  11. public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter,
  12. Type targetType, Class<? extends HttpMessageConverter<?>> converterType)
  13. throws IOException {
  14. return inputMessage;
  15. }
  16.  
  17. @Override
  18. public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter,
  19. Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
  20.  
  21. return body;
  22. }
  23.  
  24. @Override
  25. @Nullable
  26. public Object handleEmptyBody(@Nullable Object body, HttpInputMessage inputMessage,
  27. MethodParameter parameter, Type targetType,
  28. Class<? extends HttpMessageConverter<?>> converterType) {
  29. return body;
  30. }
  31. }

Springboot 对RequestBody的值进行统一修改的几种方式

背景

最近在项目中遇到需要统一对Request请求中的某一个自定义对象的属性进行统一修改的需求。

考虑了几种实现方式,现在记录一下。由于原项目过于复杂,自己写几个demo进行记录。

解决方式

SpringBoot如何使用RequestBodyAdvice进行统一参数处理

方式一:利用filter进行处理

大坑:

​ 如果你想要改变加了RequestBody注解的数据,无论如何你都要通过getInputStream()方法来获取流来拿到对应的参数,然后更改。在不经过拿取流的情况下,spring的RequestBody注解也是通过getInputStream()方法来获取流来映射为request对象。

但是如果你想要的统一的进行修改,也必须经过getInputStream()来首先拿到stream然后才能进行修改。但此时stream被消费之后,就会关闭。

然后你的controller中的参数就拿不到对象,报错如下。

I/O error while reading input message; nested exception is java.io.IOException: Stream closed

可以通过创建并使用自定义的的HttpServletRequestWrapper来避免这种情况。

步骤一:编写自定义HttpServletRequestWrapper

  1. package com.example.testlhf.filter;
  2. import com.alibaba.fastjson.JSON;
  3. import com.alibaba.fastjson.JSONObject;
  4. import com.example.testlhf.entity.Student;
  5. import lombok.extern.slf4j.Slf4j;
  6. import javax.servlet.ReadListener;
  7. import javax.servlet.ServletInputStream;
  8. import javax.servlet.ServletRequest;
  9. import javax.servlet.http.HttpServletRequest;
  10. import javax.servlet.http.HttpServletRequestWrapper;
  11. import java.io.BufferedReader;
  12. import java.io.ByteArrayInputStream;
  13. import java.io.IOException;
  14. import java.io.InputStream;
  15. import java.io.InputStreamReader;
  16. import java.nio.charset.Charset;
  17. /**
  18. * @Description TODO
  19. * @Author yyf
  20. * @Date 2020/10/29 12:48
  21. * @Version 1.0
  22. **/
  23. @Slf4j
  24. public class ChangeStudentNameRequestWrapper extends HttpServletRequestWrapper {
  25. /**
  26. * 存储body数据的容器
  27. */
  28. private byte[] body;
  29. public ChangeStudentNameRequestWrapper(HttpServletRequest request) throws IOException {
  30. super(request);
  31. //接下来的request使用这个
  32. String bodyStr = getBodyString(request);
  33. body = bodyStr.getBytes(Charset.defaultCharset());
  34. }
  35. /**
  36. * 获取请求Body
  37. *
  38. * @param request request
  39. * @return String
  40. */
  41. public String getBodyString(final ServletRequest request) {
  42. try {
  43. return inputStream2String(request.getInputStream());
  44. } catch (IOException e) {
  45. log.error("", e);
  46. throw new RuntimeException(e);
  47. }
  48. }
  49. /**
  50. * 获取请求Body
  51. *
  52. * @return String
  53. */
  54. public String getBodyString() {
  55. final InputStream inputStream = new ByteArrayInputStream(body);
  56. return inputStream2String(inputStream);
  57. }
  58. /**
  59. * 将inputStream里的数据读取出来并转换成字符串
  60. *
  61. * @param inputStream inputStream
  62. * @return String
  63. */
  64. private String inputStream2String(InputStream inputStream) {
  65. StringBuilder sb = new StringBuilder();
  66. BufferedReader reader = null;
  67. try {
  68. reader = new BufferedReader(new InputStreamReader(inputStream, Charset.defaultCharset()));
  69. String line;
  70. while ((line = reader.readLine()) != null) {
  71. sb.append(line);
  72. }
  73. } catch (IOException e) {
  74. log.error("", e);
  75. throw new RuntimeException(e);
  76. } finally {
  77. if (reader != null) {
  78. try {
  79. reader.close();
  80. } catch (IOException e) {
  81. log.error("", e);
  82. }
  83. }
  84. }
  85. JSONObject jsonObject = JSONObject.parseObject(sb.toString());
  86. if (jsonObject != null && jsonObject.get("student") != null) {
  87. Student student = JSON.toJavaObject((JSON) jsonObject.get("student"), Student.class);
  88. log.info("修改之前的学生名称为:" + student.getName());
  89. student.setName("amd");
  90. jsonObject.put("student", student);
  91. return jsonObject.toJSONString();
  92. }
  93. return sb.toString();
  94. }
  95. @Override
  96. public BufferedReader getReader() throws IOException {
  97. return new BufferedReader(new InputStreamReader(getInputStream()));
  98. }
  99. @Override
  100. public ServletInputStream getInputStream() throws IOException {
  101. final ByteArrayInputStream inputStream = new ByteArrayInputStream(body);
  102. return new ServletInputStream() {
  103. @Override
  104. public int read() throws IOException {
  105. return inputStream.read();
  106. }
  107. @Override
  108. public boolean isFinished() {
  109. return false;
  110. }
  111. @Override
  112. public boolean isReady() {
  113. return false;
  114. }
  115. @Override
  116. public void setReadListener(ReadListener readListener) {
  117. }
  118. };
  119. }
  120. }

步骤二:使用自定义的HttpServletRequestWrapper取代原有的

使用自定义的request取代原有的传递给过滤器链。

  1. package com.example.testlhf.filter;
  2. import lombok.extern.slf4j.Slf4j;
  3. import javax.servlet.Filter;
  4. import javax.servlet.FilterChain;
  5. import javax.servlet.FilterConfig;
  6. import javax.servlet.ServletException;
  7. import javax.servlet.ServletRequest;
  8. import javax.servlet.ServletResponse;
  9. import javax.servlet.http.HttpServletRequest;
  10. import java.io.IOException;
  11. /**
  12. * @Description TODO
  13. * @Author yyf
  14. * @Date 2020/10/29 13:20
  15. * @Version 1.0
  16. **/
  17. @Slf4j
  18. public class ReplaceStreamFilter implements Filter {
  19. @Override
  20. public void init(FilterConfig filterConfig) throws ServletException {
  21. log.info("StreamFilter初始化...");
  22. }
  23. @Override
  24. public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
  25. ServletRequest requestWrapper = null;
  26. //获取请求中的流,将取出来的字符串,再次转换成流,然后把它放入到新request对象中,
  27. if (request instanceof HttpServletRequest) {
  28. requestWrapper = new ChangeStudentNameRequestWrapper((HttpServletRequest) request);
  29. }
  30. // 在chain.doFiler方法中传递新的request对象
  31. if (requestWrapper == null) {
  32. chain.doFilter(request, response);
  33. } else {
  34. chain.doFilter(requestWrapper, response);
  35. }
  36. }
  37. @Override
  38. public void destroy() {
  39. log.info("StreamFilter销毁...");
  40. }
  41. }

步骤三:将过滤器注册进spring容器

  1. package com.example.testlhf.filter;
  2. import org.springframework.boot.web.servlet.FilterRegistrationBean;
  3. import org.springframework.context.annotation.Bean;
  4. import org.springframework.context.annotation.Configuration;
  5. import javax.servlet.Filter;
  6. /**
  7. * @Description TODO
  8. * @Author yyf
  9. * @Date 2020/10/29 14:20
  10. * @Version 1.0
  11. **/
  12. @Configuration
  13. public class MyFilterConfig {
  14. /**
  15. * 注册过滤器
  16. *
  17. * @return FilterRegistrationBean
  18. */
  19. @Bean
  20. public FilterRegistrationBean someFilterRegistration() {
  21. FilterRegistrationBean<Filter> registration = new FilterRegistrationBean<>();
  22. registration.setFilter(replaceStreamFilter());
  23. registration.addUrlPatterns("/*");
  24. registration.setName("replaceStreamFilter");
  25. return registration;
  26. }
  27. /**
  28. * 实例化StreamFilter
  29. *
  30. * @return Filter
  31. */
  32. @Bean(name = "replaceStreamFilter")
  33. public Filter replaceStreamFilter() {
  34. return new ReplaceStreamFilter();
  35. }
  36. }

看下效果:

SpringBoot如何使用RequestBodyAdvice进行统一参数处理

到此使用过滤器对post请求中的参数的修改已经完毕。

方式二:使用拦截器进行处理

当我自以为可以使用拦截器前置通知进行处理时才发现,事情并不简单。

步骤一:自定义一个拦截器

如下图实现一个拦截器,preHandle中有HttpServletRequest request参数,虽然可以通过它的流获取到body中数据,但是如果将body中数据进行修改的话,其并不能传递给controller。因为request只有两个set方法。如果将要统一修改的值摄入Attribute,则还仍需从controller中拿到

SpringBoot如何使用RequestBodyAdvice进行统一参数处理

步骤二:在controller中获取值

SpringBoot如何使用RequestBodyAdvice进行统一参数处理

虽然用这种方式可以在request中添加统一的参数,也可以从每一个controller中获取值,但仍需要对每一个controller进行代码修改,显然这种方式并不是我们需要的。

方式三:使用切面处理

步骤一:引入aspect所需要使用的maven依赖

  1. <dependency>
  2. <groupId>org.springframework</groupId>
  3. <artifactId>spring-aspects</artifactId>
  4. <version>5.1.3.RELEASE</version>
  5. </dependency>

步骤二:编写自定义的前置通知以及表达

  1. @Component
  2. @Aspect
  3. public class ChangeStudentNameAdvice {
  4. @Before("execution(* com.example.testlhf.service.impl.*.*(..))&&args(addStudentRequset)")
  5. public void aroundPoints(AddStudentRequset addStudentRequset) {
  6. addStudentRequset.getStudent().setName("amd");
  7. }
  8. }

注意此处的形参需要和args括号内的字符串保持一致,否则报错。

注意此处的形参需要和args括号内的字符串保持一致,否则报错。

步骤三:开启注解@EnableAspectJAutoProxy

SpringBoot如何使用RequestBodyAdvice进行统一参数处理

总结:

首先说下filter和interceptor的区别:两者之间的所依赖的环境不一致,filter作为javaWeb三大组件之一,其作用为:拦截请求,以及过滤相应。其依赖于servlet容器。但interceptor依赖于web框架,例如springmvc框架。最常见的面向切面编程AOP所使用的动态代理模式,即是使用拦截器在service方法执行前或者执行后进行一些操作。他们都可以适用于如下的场景:权限检查,日志记录,事务管理等等。当然包括,对所有的请求某些参数进行统一的修改。

比较三种方式,方式一和方式二所谓的拦截基本都是基于对http请求的拦截,filter执行在interceptor之前。虽然filter和interceptor都有类似链这种概念,但filter可以将request请求修改之后传递给后面的filter,就像电路中的串联,而interceptor的链是独立的,修改其中一个request并不会影响其他的interceptor,类似并联,不能做到只修改一处其他不用修改的方式。

简单来说方式一和方式二针对进入controller进行拦截,而后做一些操作。方式三使用的拦截的理念是针对业务方法的,在执行业务方法的前面对参数进行修改,和spring中对事务控制的实现方式类似。

思考:

虽然第一,第三种方式都可以在技术上实现针对某些方法进行统一的参数修改。但是如果将项目当做一个工程来思考的话,不同于日志打印或者事务控制这种非业务逻辑的处理,这种统一修改某些参数来完成一些操作,已严重入侵了业务逻辑。

真正的解决方式要么在请求的源头就做好参数设置,要么通过配置文件在需要使用的地方来进行某些参数的赋值。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持我们。

原文链接:https://blog.csdn.net/xingbaozhen1210/article/details/98189562