ServletRequest + Tomcat + 反射 + ByteBuffer
背景: 很多WEB项目需要前置解析请求体内容场景,如部分请求体中存在鉴权或特殊适配字段。正常通过Request对象获取InputStream对象后,如果通过IOUtil等工具将内容读取出来,则控制器方法解析失败,报请求体为空等错误,这是因为底层设计流只能读取一次。网上普遍解决办法是通过继承HttpServletRequestWrapper包装类,重写相关读取方法,以内部变量方式保存消息体内容,具体代码简单搜索即可获得。个人觉得此类做法代码略多,工作之余,通过源码走读发现可通过反射方式实现同样效果,记录下来分享给大家,如有雷同,敬请忽略。
看完有收获,小手点一点,分享来一波,不能让前面的包装类这一种方式用到JAVA灭绝哇,能看到就不是一般的缘分。注:重复读最好还是官方支持,是需求太奇特,还是官方太官方。
实现细节
1、Springboot项目,application/json请求,@RequestBody + @Valid 接收
2、位置:Filter
3、原理:反射
4、版本:tomcat-embed-core:9.0.65
注: 仅限Tomcat内嵌容器(其它容器请求实现类不同,可参考可讨论)
// 具体方法内容
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
try {
// 此处要根据URL、METHOD、注解等各类方式设置白名单,不建议全局处理
CoyoteInputStream inputStream = (CoyoteInputStream) servletRequest.getInputStream();
Field ibField = ReflectionUtils.findField(CoyoteInputStream.class, "ib");
if (ibField != null) {
ReflectionUtils.makeAccessible(ibField);
InputBuffer inputBuffer = (InputBuffer) ReflectionUtils.getField(ibField, inputStream);
String body = IOUtils.toString(inputStream, StandardCharsets.UTF_8);
// byte[] newBody = "{\"source\":\"\",\"entry\":\"\"}".getBytes(StandardCharsets.UTF_8);
// inputBuffer.setByteBuffer(ByteBuffer.wrap(newBody));
// 此处代码必须存在,否则控制器端无法绑定方法参数。如有内容修改,请参考以上注释代码,重新set即可。
inputBuffer.setByteBuffer(ByteBuffer.wrap(body.getBytes(StandardCharsets.UTF_8)));
// 此处重复读&改消息体已经结束,惊不惊喜?意不意外?以前用到的时候,为啥没人写过这种方式呢?
// 如果对参数具体类型及要求不要,或者参数及其简单,直接使用Map等类直接反序列化即可
//下述主要处理特殊场景,及对servlet处理流程的理解有很好的帮助(白瞎了自己那么多年经验,整体吹皮毛)
Class<?> paramClass = getParamClassFromRequest(servletRequest);
if (paramClass != null) {
Object bodyObject = JSONObject.parseObject(body, paramClass);
// 枚举处理
if (bodyObject instanceof RequestInfo) {
// 不要问这个类从哪里来,私人定制
RequestInfo requestInfo = (RequestInfo) bodyObject;
System.out.println(requestInfo.getEntry());
System.out.println(requestInfo.getSource());
}
}
}
} catch (Exception e) {
log.error(e.getMessage());
}
filterChain.doFilter(servletRequest, servletResponse);
}
// import内容,省的走歪路,也不影响正文阅读
import com.alibaba.fastjson.JSONObject;
import org.apache.catalina.connector.CoyoteInputStream;
import org.apache.catalina.connector.InputBuffer;
import org.apache.commons.io.IOUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.http.server.RequestPath;
import org.springframework.util.ReflectionUtils;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerExecutionChain;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import org.springframework.web.util.ServletRequestPathUtils;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Field;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
代码比较简单,稍微熟悉JAVA的基本都能看明白,此处不再具体描述。代码中注释部分为修改请求体部分,此处可根据业务需求具体分析处理。此外,getParamClassFromRequest方法是为获取调用方法参数类型,考虑到其他同事有遇到这个项目需求,此处一并分享给大家。(如果仅需要重复读改,直接return;如果无聊,可浅尝Spring Servlet精华)
1、注入HandlerMapping对象(查看DispatcherServlet代码看解析请求流程)
@Autowired
@Qualifier("requestMappingHandlerMapping")
private RequestMappingHandlerMapping handlerMapping;
2、getParamClassFromRequest方法代码
private Class<?> getParamClassFromRequest(ServletRequest servletRequest) {
HttpServletRequest request = (HttpServletRequest) servletRequest;
RequestPath requestPath = ServletRequestPathUtils.parseAndCache(request);
// 为什么要设置这个属性?因为直接用报错了。我们用的只是皮毛,感谢Spring让我入行。
request.setAttribute(ServletRequestPathUtils.class.getName() + ".PATH", requestPath);
try {
HandlerExecutionChain handler = handlerMapping.getHandler(request);
if (handler != null) {
Class<?>[] parameterTypes = ((HandlerMethod) handler.getHandler()).getMethod().getParameterTypes();
for (Class<?> parameter : parameterTypes) {
// 此处根据自身需求修改,默认都是@RequestBody修饰参数,都到这一步了,很多方法都可判断
if (!ServletRequest.class.isAssignableFrom(parameter)) {
return parameter;
}
}
}
} catch (Exception e) {
}
return null;
}
注: 此方法仅限获取第一个非Request对象,仅供参考
输入输出:
日志:
可以看到使用这种方式,完全不再需要自己去实现自定义类及其内部类那一套流程,过程极其简单,可读性也更好,只是限制了Tomcat内嵌容器,目测版本变更影响较小,可作为一种新的解决思路。(注:个人当前没有此需求,未在生产环境中使用。若使用,请自验。)