问题场景 :
我们一般直接获取request请求里的参数进行数据处理,现在想在过滤器里获取请求参数并打印,方便做日志排查。
但是POST请求中的参数是存储在流中的,只能读一次,无法多次读取。
解决办法
使用ThreadLocal存储请求数据
最简单的办法就是获取到请求数据之后存储在一个地方,这样下次再使用的时候就直接从存储处获取,而不再从request请求流里获取
拦截的filter
@Component
public class MyFilter implements Filter {
public void init(FilterConfig filterConfig) throws ServletException {
}
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse res = (HttpServletResponse) response;
//获取请求参数,并转换成字符串存储在ThreadLocal
byte[] bytes = StreamUtils.copyToByteArray(request.getInputStream());
String params = new String(bytes, req.getCharacterEncoding());
RequestThreadLocalCache.setRequestToThreadLocal(params);
System.out.println("filer-post请求参数:" + params);
chain.doFilter(request,response);
}
public void destroy() {
}
}
简单的ThreadLocal存储工具类
public class RequestThreadLocalCache {
private static ThreadLocal<String> threadLocal = new ThreadLocal<String>();
public static void setRequestToThreadLocal(String str) {
threadLocal.set(str);
}
public static String getRequestFromThreadLocal() {
return threadLocal.get();
}
}
@RestController
public class HelloController {
@RequestMapping("hello")
public String hello(User user){
//从threadlocal获取参数
System.out.println("controller请求参数:" + user);
String paramString = RequestThreadLocalCache.getRequestFromThreadLocal();
Gson gson = new Gson();
User threadLocaleUser = gson.fromJson(paramString, User.class);
System.out.println("controller请求参数(from threadlocal):" + threadLocaleUser);
return "hello? " + threadLocaleUser.getName();
}
}
我们可以看下此时post请求的输出结果:
filter里可以正常拿到参数,但是在controller里无法通过方法参数user拿到参数值,但是可以从ThreadLocal里拿到之前存储的参数
而get请求的输出结果如下:
这也证明了前面说的,只有Post请求的参数是存放在流中的
这个办法的缺点在于框架帮我们做的请求参数对象的封装都不能使用,需要自己重新一一获取参数,比较麻烦,而且这里我仅处理了最简单的json参数,如果参数类型比较复杂,那么处理起来就更麻烦了
封装HttpServletRequestWrapper子类
创建一个HttpServletRequestWrapper的子类,将请求体中的stream复制一份到子类的byte数组里,然后重写getInputStream()和getReader()方法。
这样每次在调用子类的getInputStream()方法实际都是从之前保存的byte数组里获取对应的流信息,在整个请求期间该数组对象一直存在,也就可以实现请求流的重复读取。
HttpServletRequestWrapper的子类
public class RequestWrapper extends HttpServletRequestWrapper {
private byte[] body;
public RequestWrapper(HttpServletRequest request) throws IOException {
super(request);
//获取请求参数
body = StreamUtils.copyToByteArray(request.getInputStream());
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(this.getInputStream()));
}
@Override
public ServletInputStream getInputStream() throws IOException {
final ByteArrayInputStream byteInputStream = new ByteArrayInputStream(this.body);
ServletInputStream servletInputStream = new ServletInputStream() {
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener readListener) {
}
@Override
public int read() throws IOException {
return byteInputStream.read();
}
};
return servletInputStream;
}
}
对应的filter 和controller
@Component
public class MyFilter2 extends OncePerRequestFilter {
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
RequestWrapper requestWrapper = new RequestWrapper(httpServletRequest);
//获取请求参数
byte[] bytes = StreamUtils.copyToByteArray(requestWrapper.getInputStream());
String params = new String(bytes, httpServletRequest.getCharacterEncoding());
System.out.println("MyFilter2: filer-post请求参数:" + params);
filterChain.doFilter(requestWrapper,httpServletResponse);
}
}
@RestController
public class HelloController2 {
@RequestMapping("hello2")
public String hello(@RequestBody User user, HttpServletRequest request) throws IOException {
//从threadlocal获取参数
byte[] bytes = StreamUtils.copyToByteArray(request.getInputStream());
System.out.println("request参数:" + new String(bytes));
System.out.println("controller请求参数:" + user);
return "hello? " + user.getName();
}
}
输出结果为:
注意: 这里Controller的user请求参数需要加上@RequestBody注解,只有加上该注解,才会从request的请求体中获取参数。
所以在方案一中不能加@RequestBody注解,因为request流已经被读取过了再次读取会报错 : 400 Bad Request
本文中我的post请求参数如下
如果我使用application/x-www-form-urlencoded,不使用@RequestBody注解controller方法也可以获取到user对象(不过此时就不是从request的stream流中获取的了)
但是这样处理后我们在controller使用request.getParameter("name");
获取参数还是会失败,
如果我们想通过getParameter()方法获取POST请求里的参数,需要满足如下三个条件:
(Servlet参数可用性,POST请求规范)
- HTTP请求或者是HTTPS请求。
- HTTP的请求方法为POST方式。(不支持PUT)
- 内容类型是application/x-www-form-urlencoded。
该解决方案的优点也很明显,通过封装HttpServletRequestWrapper的子类并重写其getInputStream()和getReader()方法,保证在整个web请求的过程中,参数都可以正常获取,也不影响框架按照既有api封装参数信息和我们原来的参数处理逻辑