文章目录
前情提要
在项目中需要使用过滤器 在请求调用 Controller 方法前修改请求参数和在结果返回之前修改返回结果。
在 Controller 中定义如下接口:
java
@PostMapping("/hello")
public JSONObject hello(@RequestBody Map<String, Object> params) {
return JSONObject.parseObject(JSON.toJSONString(params));
}
定义的过滤器如下:
java
public class ServNoFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
// 获取请求体内容
String requestBody = getRequestBody(httpServletRequest);
// 业务处理
......
// 放行
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
private String getRequestBody(HttpServletRequest request) throws IOException {
BufferedReader reader = new BufferedReader(request.getReader());
StringBuilder sb = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
return sb.toString();
}
}
此时启动项目,访问接口,则会在控制台打印如下异常信息:Request processing failed; nested exception is java.lang.IllegalStateException: getReader() has already been called for this request
。
表示在过滤器中已经通过 request.getReader()
方法将请求流读取。
如果在过滤器中将 getReader()
换成 getInputStream()
就会报请求体为空 异常:org.springframework.http.converter.HttpMessageNotReadableException: Required request body is missing
。
这是因为在 Servlet 中,请求对象的输入流只能被读取一次。而在第一次读取请求体时,Servlet 容器会将请求体保存在内存中,并将其解析成相应的请求参数和请求头信息。如果在后续的处理中再次读取请求体,就可能会导致数据错误或异常。
解决方案
自定义 HttpServletRequest 包装类 RequestWrapper
在 Servlet 中,原始的 HttpServletRequest
对象中的请求流(即请求体)只能读取一次。这是因为 HTTP 协议是基于流的协议,服务器在读取请求流时会将其消耗掉,一旦读取完毕,就无法再次读取
当 Servlet 容器读取完请求流后,会将请求的内容解析并储存在相应的属性中,如请求参数、请求头信息等。在后续的处理过程中,Servlet 可以从这些属性中获取请求内容,而不必再次读取请求流。
因此,我们需要自定义 RequestWrapper
将请求流保存下来,并提供方法来多次读取请求体的内容。
自定义 HttpServletRequest
包装类 RequestWrapper
如下:
java
/**
* HttpServletRequest 包装类,允许在 Servlet 中多次读取请求体内容
* 重写了 getInputStream()方法和 getReader() 方法,返回可以多次读取的流。
*/
public class RequestWrapper extends HttpServletRequestWrapper {
private final byte[] body;
/**
* 构造 RequestWrapper 对象
*
* @param request 原始 HttpServletRequest 对象
* @param context 请求体内容
*/
public RequestWrapper(HttpServletRequest request, String context) {
super(request);
this.body = context.getBytes(StandardCharsets.UTF_8);
}
/**
* 重写 getInputStream 方法,返回经过包装后的 ServletInputStream 对象
*
* @return 经过包装后的 ServletInputStream 对象
*/
@Override
public ServletInputStream getInputStream() {
return new ServletInputStreamWrapper(new ByteArrayInputStream(body));
}
/**
* 重写 getReader 方法,返回经过包装后的 BufferedReader 对象
*
* @return 经过包装后的 BufferedReader 对象
*/
@Override
public BufferedReader getReader() {
return new BufferedReader(new InputStreamReader(getInputStream(), StandardCharsets.UTF_8));
}
/**
* 私有内部类,用于包装 ServletInputStream 对象
*/
private static class ServletInputStreamWrapper extends ServletInputStream {
private final ByteArrayInputStream inputStream;
/**
* 构造函数,传入待包装的 ByteArrayInputStream 对象
*
* @param inputStream 待包装的 ByteArrayInputStream 对象
*/
public ServletInputStreamWrapper(ByteArrayInputStream inputStream) {
this.inputStream = inputStream;
}
/**
* 重写 read 方法,读取流中的下一个字节
*
* @return 读取到的下一个字节,如果已达到流的末尾,则返回-1
*/
@Override
public int read() {
return inputStream.read();
}
/**
* 覆盖 isFinished 方法,指示流是否已完成读取数据
*
* @return 始终返回 false,表示流未完成读取数据
*/
@Override
public boolean isFinished() {
return false;
}
/**
* 重写 isReady 方法,指示流是否准备好进行读取操作
*
* @return 始终返回 false,表示流未准备好进行读取操作
*/
@Override
public boolean isReady() {
return false;
}
/**
* 重写 setReadListener 方法,设置读取监听器
*
* @param readListener 读取监听器
*/
@Override
public void setReadListener(ReadListener readListener) {
}
}
}
自定义 HttpServletResponse 包装类 ResponseWrapper
与请求流(即请求体)一样,原始的 HttpServletResponse
对象中的响应流(即响应体)只能写入一次。当服务器在向客户端发送响应时,会将响应流写入到网络传输通道中,一旦写入完毕,就无法再次修改或写入。
因此我们需要通过自定义 ResponseWrapper
包装原始的 HttpServletResponse
对象并重写其输出流或者输出写方法,从而实现对响应流的修改和控制。
自定义 HttpServletResponse
包装类 ResponseWrapper
如下:
java
/**
* HttpServletResponse 包装类对,提供对响应数据的处理和操作。
*/
public class ResponseWrapper extends HttpServletResponseWrapper {
private final ByteArrayOutputStream outputStream;
private ServletOutputStream servletOutputStream;
private PrintWriter writer;
/**
* 构造函数,传入原始的 HttpServletResponse 对象
*
* @param response 原始的 HttpServletResponse 对象
*/
public ResponseWrapper(HttpServletResponse response) {
super(response);
this.outputStream = new ByteArrayOutputStream();
}
/**
* 重写 getOutputStream 方法,返回经过包装后的 ServletOutputStream 对象
*
* @return 经过包装后的 ServletOutputStream 对象
*/
@Override
public ServletOutputStream getOutputStream() {
if (servletOutputStream == null) {
servletOutputStream = new ServletOutputStreamWrapper(outputStream);
}
return servletOutputStream;
}
/**
* 重写 getWriter 方法,返回经过包装后的 PrintWriter 对象
*
* @return 经过包装后的 PrintWriter 对象
*/
@Override
public PrintWriter getWriter() {
if (writer == null) {
writer = new PrintWriter(getOutputStream());
}
return writer;
}
/**
* 获取响应数据,并指定字符集
*
* @param charsetName 字符集名称
* @return 响应数据字符串
*/
public String getResponseData(String charsetName) {
Charset charset = Charset.forName(charsetName);
byte[] bytes = outputStream.toByteArray();
return new String(bytes, charset);
}
/**
* 设置响应数据,并指定字符集
*
* @param responseData 响应数据字符串
* @param charsetName 字符集名称
*/
public void setResponseData(String responseData, String charsetName) {
Charset charset = Charset.forName(charsetName);
byte[] bytes = responseData.getBytes(charset);
outputStream.reset();
try {
outputStream.write(bytes);
} catch (IOException e) {
// 处理异常
}
setCharacterEncoding(charsetName);
}
/**
* 私有内部类,用于包装 ServletOutputStream 对象
*/
private static class ServletOutputStreamWrapper extends ServletOutputStream {
private final ByteArrayOutputStream outputStream;
/**
* 构造函数,传入待包装的 ByteArrayOutputStream 对象
*
* @param outputStream 待包装的 ByteArrayOutputStream 对象
*/
public ServletOutputStreamWrapper(ByteArrayOutputStream outputStream) {
this.outputStream = outputStream;
}
/**
* 重写 write 方法,将指定字节写入输出流
*
* @param b 字节
*/
@Override
public void write(int b) {
outputStream.write(b);
}
/**
* 重写 isReady 方法,指示输出流是否准备好接收写入操作
*
* @return 始终返回 false,表示输出流未准备好接收写入操作
*/
@Override
public boolean isReady() {
return false;
}
/**
* 重写 setWriteListener 方法,设置写入监听器
*
* @param writeListener 写入监听器
*/
@Override
public void setWriteListener(WriteListener writeListener) {
}
}
}
自定义过滤器 MiddlewareFilter
我们的需求是:在请求到达服务器之前,对请求参数进行修改;在响应返回之前,对响应结果进行处理。
对于这样的需求,我们可以通过自定义过滤器来实现。大致实现思路如下:
-
修改请求参数(请求体),我们可以:
- 获取请求体内容。
- 修改请求体内容。
- 将修改后的请求对象替换原来的请求对象,以便后续获取修改后的参数。
-
修改响应结果(响应体),我们可以:
- 获取响应数据。
- 对响应数据进行处理。
- 将修改后的数据作为最终结果返回。
同时为了确保每个请求在请求时只会被过滤一次,我们可以继承 OncePerRequestFilter
来定义自己的过滤器。
最终,自定义过滤器如下:
java
public class MiddlewareFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
// 1. 从 HttpServletRequest 对象中获取请求体内容
String requestBody = getRequestBody(httpServletRequest);
// 2. 解析请求体内容为JSON对象
JSONObject jsonBody = JSONObject.parseObject(requestBody);
// 3. 修改请求体内容
jsonBody.put("paramKey","paramValue");
// 4. 包装 HttpServletRequest 对象为自定义的 RequestWrapper 对象,以便后续的处理
RequestWrapper requestWrapper = new RequestWrapper(httpServletRequest, jsonBody.toJSONString());
// 5. 包装 HttpServletResponse 对象为自定义的 ResponseWrapper 对象,以便后续的处理
ResponseWrapper responseWrapper = new ResponseWrapper(httpServletResponse);
// 6. 调用下一个过滤器或 Servlet
filterChain.doFilter(requestWrapper, responseWrapper);
// 7. 获取响应数据
String responseData = responseWrapper.getResponseData(StandardCharsets.UTF_8.name());
// 8. 解析响应数据为JSON对象
JSONObject jsonData = JSONObject.parseObject(responseData);
// 9. 在这里可以对响应数据进行处理
jsonData.put("responseKey", "responseValue");
// 10. 将修改后的 JSON 对象转换为字符串
responseData = jsonData.toJSONString();
// 11. 将修改后的 JSON 对象设置为最终的响应数据
responseWrapper.setResponseData(responseData, StandardCharsets.UTF_8.name());
// 12. 将响应数据写入原始的响应对象,解决响应数据无法被多个过滤器处理问题
OutputStream outputStream = httpServletResponse.getOutputStream();
outputStream.write(responseData.getBytes(StandardCharsets.UTF_8));
outputStream.flush();
}
/**
* 获取请求体内容。
*
* @param request HttpServletRequest对象
* @return 请求体内容
* @throws IOException 如果读取请求体内容时发生I/O异常
*/
private String getRequestBody(HttpServletRequest request) throws IOException {
BufferedReader reader = new BufferedReader(new InputStreamReader(request.getInputStream()));
StringBuilder sb = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
return sb.toString();
}
}
配置过滤器
注解
通过 Java Servlet 3.0 规范中引入的 @WebFilter
注解配置过滤器。
@WebFilter
注解可以应用在实现了 Filter
接口或继承自 OncePerRequestFilter
的类上,标识该类为过滤器,并指定过滤器的相关配置,包括拦截的 URL 路径、执行顺序以及初始化参数等。
我们可以在 MiddlewareFilter
过滤器上使用 @WebFilter
注解注册该过滤器并指定执行该过滤器执行的顺序和拦截的 URL:
java
@WebFilter(value = "1000", urlPatterns = "/hello")
public class MiddlewareFilter extends OncePerRequestFilter {
......
}
value
:设置过滤器的执行顺序,数字越小,优先级越高。urlPatterns
:指定要拦截的 URL 路径,允许指定多个 URL 路径urlPatterns = {"/hello","/hello1"}
。
还需要再启动类上使用@ServletComponentScan
注解扫描和注册带有 @WebServlet
、@WebFilter
和 @WebListener
注解的组件:
java
@ServletComponentScan
@SpringBootApplication
public class Demo1Application {
public static void main(String[] args) {
SpringApplication.run(Demo1Application.class, args);
}
}
配置类
除了注解的形式配置过滤器,我们还可以通过配置类的形式进行配置。
创建 FilterConfig
类用于配置需要注册的过滤器,同时在类上添加 @Configuration
注解,标识该类为配置类,在项目启动时 Spring 会自动扫描该类中的 Bean 定义,并将其加载到容器中:
java
@Configuration
public class FilterConfig {
@Bean
public FilterRegistrationBean<MiddlewareFilter> middlewareFilter() {
FilterRegistrationBean<MiddlewareFilter> registration = new FilterRegistrationBean<>();
registration.setFilter(new MiddlewareFilter()); // 设置过滤器实例
registration.addUrlPatterns("/hello"); // 拦截的 URL 路径
registration.setOrder(1000); // 设置过滤器执行顺序(数字越小,越先执行)
return registration;
}
}
在类中我们定义了名为 middlewareFilter
的方法,用于注册我们自定义的 MiddlewareFilter
过滤器。
在 方法中,创建了一个 FilterRegistrationBean
对象用于注册和配置过滤器,并设置 MiddlewareFilter
对象作为过滤器实例,指定了过滤器要拦截的 URL 路径,滤器执行顺序。
最后将 FilterRegistrationBean
对象返回,以便 Spring 自动进行注册和管理。
编写 Controller 测试
创建两个接口,同样的逻辑,接收一个请求体参数 params
,再将接收的参数以 JSON 格式返回:
java
@RestController
public class BasicController {
/**
* 处理 /hello 请求的方法
* @param params 请求体参数,以键值对的形式传递
* @return 经过转换后的 JSONObject 对象
*/
@PostMapping("/hello")
public JSONObject hello(@RequestBody Map<String, Object> params) {
return JSONObject.parseObject(JSON.toJSONString(params));
}
@PostMapping("/hello1")
public JSONObject hello1(@RequestBody Map<String,Object> params) {
return JSONObject.parseObject(JSON.toJSONString(params));
}
}
启动项目,在 ApiFox 中分别以同样的请求参数发送 POST
请求调用 /hello
、/hello1
接口:
-
请求参数:
json{ "name": "hello", "age": 20 }
-
/hello
接口返回结果:json{ "paramKey": "paramValue", "responseKey": "responseValue", "name": "hello", "age": 20 }
-
/hello1
接口返回结果:json{ "name": "hello", "age": 20 }
复制多个 MiddlewareFilter
过滤器模拟多层过滤器修改请求体参数和返回结果,测试结果如下:
json
{
"paramKey": "paramValue", //过滤器1
"responseKey2": "responseValue2", //过滤器2
"responseKey": "responseValue", //过滤器2
"paramKey2": "paramValue2", //过滤器1
"name": "hello",
"age": 20
}