自从上次更新已经半年有余,这其中经历了很多事情,等后续年终总结的时候和大家好好聊聊。
老规矩,今天还是更新技术文章,说一个近期遇到的尴尬问题。前端传给后端的JSON数据,如果开发者对此进行了拦截并进行了消费,那么后续在controller中就无法再次获取对应数据。原因在于服务端是通过IO流来解析JSON数据,而流是一种特殊的结构,只要读完就没有了,而在某些场景下往往希望可以多次读取。
举一个非常简单的例子,接口幂等性实现,即同一个接口在规定时间内多次接收到相同参数的请求,那么此时需要拒绝这些相同请求。我们在具体实现的时候,可能会先将请求中的参数提取出来,如果参数是JOSN数据,那么由于流已经读取了,因此后续在接口是无法再次获取JSON数据的。
第一步,新建一个名为many-json
的SpringBoot项目,并在其中新增Web依赖。
第二步,新建一个interceptor包,并在该包内新建一个RequestInterceptor类,这个类需要实现HandlerInterceptor接口并重写其中的preHandle方法:
public class RequestInterceptor implements HandlerInterceptor {
private static final Logger logger = LoggerFactory.getLogger(RequestInterceptor.class);
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String line = request.getReader().readLine();
logger.info("读取的信息为:{}",line);
return true;
}
}
这里我们就简单一些,通过流将请求中的参数打印输出一下,这样流就读完了。
第三步,新建一个config包,并在该包内新建一个MyWebMvcConfig类,这个类需要实现WebMvcConfigurer接口并重写其中的addInterceptors方法:
@Configuration
public class MyWebMvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new RequestInterceptor()).addPathPatterns("/**");
}
}
这其实就是注册拦截器,并设置该拦截器对所有请求都进行拦截。
第四步,新建一个controller包,并在该包内新建一个KenBingsController类,然后提供一个名为test的接口,注意该接口中参数通过JSON格式来传递:
@RestController
public class KenBingsController {
private static final Logger logger = LoggerFactory.getLogger(KenBingsController.class);
@PostMapping("/test")
public String test(@RequestBody String message){
logger.info("用户输入的信息为:{}",message);
return message;
}
}
第五步,启动项目进行测试。可以看到当用户访问/test
接口的时候,该请求被拦截器所拦截,因此preHandle方法将会执行,输入如下信息:
但是由于我们在test方法的参数中使用了@RequestBody
注解,而该注解底层是通过解析IO流来解析JSON数据的,加上我们在拦截器中已经读取了流,因此后续接口中就得不到数据:
可是现在我们希望IO流可以被多次读取,此时该如何操作呢?可以利用装饰者模式对HttpServletRequest进行增强,即拦截HttpServletRequest请求且请求参数为JOSN格式调用新的HttpServletRequest包装类。
第一步,新建一个wrapper包,并在该包内新建一个MyRequestWrapper类,这个类需要继承HttpServletRequestWrapper类并重写其中的getInputStream和getReader方法,同时重载一下父类ServletRequestWrapper中有HttpServletRequest和HttpServletResponse对象的构造方法:
/**
* 自定义请求包装类
*/
public class MyRequestWrapper extends HttpServletRequestWrapper {
private final byte[] bytes;
public MyRequestWrapper(HttpServletRequest request,HttpServletResponse response) throws IOException {
super(request);
request.setCharacterEncoding("UTF-8");
response.setCharacterEncoding("UTF-8");
bytes = request.getReader().readLine().getBytes(StandardCharsets.UTF_8);
}
@Override
public ServletInputStream getInputStream() throws IOException {
ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
return 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 bais.read();
}
@Override
public int available() throws IOException {
return bytes.length;
}
};
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(getInputStream()));
}
}
这其实就是自定义了一个新的HttpServletRequest类,并重载了一个包含HttpServletRequest和HttpServletResponse对象的构造方法,目的就是修改请求和响应的字符编码格式以及从IO流出读取数据,然后存入一个字节数组中,并通过重写getInputStream和getReader方法分别从字节数组中获取数据并构造IO流进行返回,这样就实现了IO流的多次读取。
第二步,新建一个filter包,并在该包内新建一个MyRequestFilter类,这个类需要实现Filter接口并重写其中的doFilter方法:
/**
* 请求拦截器,只有JSON数据才会使用自定义的RequestWrapper
*/
public class MyRequestFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
if(servletRequest instanceof HttpServletRequest){
HttpServletRequest request = (HttpServletRequest) servletRequest;
if(StringUtils.startsWithIgnoreCase(request.getContentType(), MediaType.APPLICATION_JSON_VALUE)){
MyRequestWrapper wrapper = new MyRequestWrapper(request,(HttpServletResponse) servletResponse);
filterChain.doFilter(wrapper,servletResponse);
return;
}
filterChain.doFilter(request,servletResponse);
}
filterChain.doFilter(servletRequest,servletResponse);
}
}
可以看到这里我们重写了doFilter方法,目的就是判断请求的类型,如果请求是HttpServletRequest且请求数据类型为JSON格式才会调用自定义的MyRequestWrapper,即将HttpServletRequest替换为MyRequestWrapper,走IO流可以多次读取的逻辑,之后让过滤器继续往下执行。
请注意,过滤器最好不要使用@Component
注解交由Spring容器来管理,这样会导致每个接口都会被进行过滤,最好是开发者自己手动注册,并且配置过滤的接口。
第三步,在之前定义的MyWebMvcConfig类中将这个自定义的MyRequestFilter过滤器注册进去:
@Configuration
public class MyWebMvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new RequestInterceptor()).addPathPatterns("/**");
}
@Bean
FilterRegistrationBean<MyRequestFilter> myRequestFilterFilterRegistrationBean(){
FilterRegistrationBean<MyRequestFilter> bean = new FilterRegistrationBean<>();
bean.setFilter(new MyRequestFilter());
bean.addUrlPatterns("/*");
return bean;
}
}
第四步,启动项目进行测试。可以发现现在访问/test
接口,Postman会返回正常数据:
查看一下控制台可以看到现在controller中也能获取到JSON数据了:
通过装饰者模式对HttpServletRequest进行增强这一方式可以解决JSON重复读取问题,其本质上是对请求数据格式进行判断。如果是JOSN格式,则自定义HttpServletRequest对象,先将数据从IO流中读取,然后存入一个字节数组中,后续多次读取则是多次读取该字节数组并以IO流形式进行。