目录
- 前言
- 一、场景
- 二、原因分析
- 三、解决
- 四、更多
前言
曾经遇见这么一段代码,能看出来是把request又重新包装了一下,核心信息都不会改变
后面了解到这叫
装饰器模式(Decorator Pattern) :也称为包装模式(Wrapper Pattern) 是指在不改变原有对象的基础之上,将功能附加到对象上,提供了比继承更有弹性的替代方案(扩展原有对象的功能),属于结构型模式。
装饰器模式的核心是功能扩展,使用装饰器模式可以透明且动态地扩展类的功能。
概念是这样,但还是不懂,好好的,你装饰它干啥?
一、场景
最常见的场景:在进入到控制器之前,请求httpRequest在过滤器或拦截器中被处理
这些处理可能是: 读取请求体RequestBody中的某个属性值;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// ....
code = ServletUtils.getRequestBodyValue(request,"code");
// ...
}
public static String getRequestBodyValue(HttpServletRequest request,String key) throws IOException, JSONException {
if (key == null || key.isEmpty()) {
throw new IllegalArgumentException("Key cannot be null or empty.");
}
// 读取请求体
BufferedReader reader = new BufferedReader(new InputStreamReader(request.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
requestBody.append(line);
}
// 解析JSON字符串
JSONObject jsonObject = JSON.parseObject(requestBody.toString());
// 获取参数值
String value = jsonObject.getString(key);
return value;
}
当我这么做的时候,发现在请求进入到Controller后报错了。
运行报错HttpMessageNotReadableException: Required request body is missing:
二、原因分析
过滤器中的 request.getInputStream
读取了请求流的信息,后续的过滤器,或者Controller(@RequestBody)都将得到一个“失效”的Request对象
展开讲讲:
在Java的HttpServletRequest中,请求体(Request Body)是以流的形式提供的,通常通过getInputStream()或getReader()方法访问。这些方法只能被调用一次
(为啥呢?)
请求体是一个输入流(ServletInputStream),流的内容是按顺序读取的。
流一旦被读取后,指针会移动到流的末尾,后续再次读取时将无法重新获取内容
其次,HTTP协议本身设计为单次传输,请求体流的内容在传输完成后不会自动重置。
gan,没看懂,反正就是只能读一次呗
三、解决
出来吧,装饰器–!
核心:继承HttpServletRequestWrapper,应用了装饰器模式对HttpServletRequest进行了增强。
具体表现为:缓存请求体,并且重写getInputStream和getReader方法。
一句话: 后续读取的是缓存的请求体而不是原始流
package com.hong.security.common;
import org.springframework.util.StreamUtils;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
/**
* @author wanghong
* @date 2020/05/11 22:47
**/
public class WrappedRequest extends HttpServletRequestWrapper {
private byte[] requestBody = null;
public WrappedRequest(HttpServletRequest request) {
super(request);
// 缓存请求body
try {
requestBody = StreamUtils.copyToByteArray(request.getInputStream());
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public ServletInputStream getInputStream() throws IOException {
if (requestBody == null) {
requestBody = new byte[0];
}
final ByteArrayInputStream bais = new ByteArrayInputStream(requestBody);
return new ServletInputStream() {
@Override
public int read() throws IOException {
return bais.read();
}
@Override
public boolean isFinished() {
// TODO Auto-generated method stub
return true;
}
@Override
public boolean isReady() {
// TODO Auto-generated method stub
return true;
}
@Override
public void setReadListener(ReadListener listener) {
// TODO Auto-generated method stub
}
};
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(getInputStream()));
}
}
如果在构造MyRequestBodyWrapper之前已经有其他组件读取了请求体,则会导致缓存失败。因此,确保这个包装器是在请求体首次读取之前应用的非常重要
import com.tty.tty_admin.common.entity.MyRequestBodyWrapper;
import org.springframework.stereotype.Component;
import org.springframework.util.ServletUtils;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
@Component // 必须注册到容器中才能生效
public class MyTestFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
Filter.super.init(filterConfig);
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("我的测试");
HttpServletRequest request = (HttpServletRequest) servletRequest;
MyRequestBodyWrapper wrappedRequest = new MyRequestBodyWrapper(request);
// 获取请求体内容(可选)
String code = ServletUtils.getRequestBodyValue(request, "code");
System.out.println(code);
// 传递装饰后的请求对象
filterChain.doFilter(wrappedRequest, servletResponse);
}
@Override
public void destroy() {
Filter.super.destroy();
}
filterChain.doFilter(wrappedRequest, servletResponse);
这样就能保证传入控制器的是requestwrapper,而不是原request!!!!
四、更多
学习若依框架发现,利用Spring Cloud Gateway内置的ServerWebExchangeUtils.cacheRequestBodyAndRequest
方法,减少了手动实现请求包装器的复杂性
CacheRequestFilter,优秀优秀!
package com.ruoyi.gateway.filter;
import java.util.Collections;
import java.util.List;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.OrderedGatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
/**
* 解决流不能重复读取问题
*
* @author ruoyi
*/
@Component
public class CacheRequestFilter extends AbstractGatewayFilterFactory<CacheRequestFilter.Config>
{
public CacheRequestFilter()
{
super(Config.class);
}
@Override
public String name()
{
return "CacheRequestFilter";
}
@Override
public GatewayFilter apply(Config config)
{
CacheRequestGatewayFilter cacheRequestGatewayFilter = new CacheRequestGatewayFilter();
Integer order = config.getOrder();
if (order == null)
{
return cacheRequestGatewayFilter;
}
return new OrderedGatewayFilter(cacheRequestGatewayFilter, order);
}
public static class CacheRequestGatewayFilter implements GatewayFilter
{
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain)
{
// GET DELETE 不过滤,get请求没有请求体,delete请求一般也不会用到
HttpMethod method = exchange.getRequest().getMethod();
if (method == null || method == HttpMethod.GET || method == HttpMethod.DELETE)
{
return chain.filter(exchange);
}
return ServerWebExchangeUtils.cacheRequestBodyAndRequest(exchange, (serverHttpRequest) -> {
if (serverHttpRequest == exchange.getRequest())
{
return chain.filter(exchange);
}
return chain.filter(exchange.mutate().request(serverHttpRequest).build());
});
}
}
@Override
public List<String> shortcutFieldOrder()
{
return Collections.singletonList("order");
}
static class Config
{
private Integer order;
public Integer getOrder()
{
return order;
}
public void setOrder(Integer order)
{
this.order = order;
}
}
}
重点,这一段
return ServerWebExchangeUtils.cacheRequestBodyAndRequest(exchange, (serverHttpRequest) -> {
if (serverHttpRequest == exchange.getRequest())
{
return chain.filter(exchange);
}
return chain.filter(exchange.mutate().request(serverHttpRequest).build());
});
使用装饰器后,控制器中会自动使用装饰后的ServerWebExchange中的request对象,这是因为Spring Cloud Gateway在处理请求时会通过过滤器链来修改和增强ServerWebExchange。具体来说,CacheRequestFilter通过ServerWebExchangeUtils.cacheRequestBodyAndRequest方法缓存了请求体,并替换了原始的ServerWebExchange中的请求对象。