Servlet输入流只能读取一次的解决方案
Servlet输入流只能读取一次的解决方案
需求描述
需求一:
拦截器中获取参数进行鉴权或合法性校验
需求二:
异常处理器中获取原始参数便于分析问题
需求三:
Controller中注入ServletRequest进行使用
网络搜索到的有问题的解决方案
方案一
思路
模仿org.springframework.web.filter.CommonsRequestLoggingFilter方案,使用ContentCachingRequestWrapper缓存流中数据,并在后续需要时从缓存中再次获取
例如:
存在的问题
此种实现不能在拦截器中获取到json类型数据
根本原因:
ContentCachingRequestWrapper的缓存机制是只有触发了流的读取后才会缓存数据,否则缓存没有数据。拦截器在参数绑定之前此时还未触发输入流的读取,因此缓存中无数据
方案二
思路
模仿AbstractRequestLoggingFilter和ContentCachingRequestWrapper自己实现HttpServletRequestWrapper和Filter
例如:
解决HttpServletRequest的输入流只能读取一次的问题
实现思路:
在构造HttpServletRequestWrapper时提前读取输入流中数据缓存起来供后续使用
存在的问题
对于application/json类型的请求可以到达要求,但是对于multipart/form-data和application/x-www-form-urlencoded类型的请求,后续再也无法获取到参数,导致参数绑定失败
根本原因在于
此种实现会提前消费wrapper包装的底层输入流,而对于两种form类型的数据是由容器解析被包装的底层流数据后设置到request的parameter和part中的。
最根本的原因:wrapper模式的局限性
- HttpServletRequestWrapper是一种包装模式,其他方法使用被包装对象的方法实现功能而非重写后的方法
- 仅重写了getInputStream和getReader方法,仅影响到通过这两个方法获取数据的场景,其他获取参数的方法如getParameter()获取参数时使用的getInputStream()仍为被包装的底层流自身的而非我们定义的wrapper实现的可重复使用的流
改进的解决方案
实现方案
只能包装json类型的请求,不包装其他类型,其他类型的参数直接通过getParameterMap()获取即可,无需从流中获取
参数获取注意事项
对于application/json类型的请求,可以直接从wrapper的缓存中获取,也可以从流中再次获取
对于其他类型直接通过getParameterMap()或getParts()获取,由于底层流已消费无法通过流再次获取
参考实现
public class ReusedRequestWrapperFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
ServletRequest requestWrapper = null;
if (servletRequest instanceof HttpServletRequest request) {
//只能包装json类型,其他类型数据(如两种form类型)不能包装否则会导致提前读取request造成form数据无法读取(原因是wrapper模式是包装而非继承不能运行时多态,底层parameter和part等参数还是从底层流中获取)
if (RequestUtils.isJsonRequest(request) && !(request instanceof JsonRequestWrapper)) {
requestWrapper = new JsonRequestWrapper(request);
}
}
if (requestWrapper == null) {
filterChain.doFilter(servletRequest, servletResponse);
} else {
filterChain.doFilter(requestWrapper, servletResponse);
}
}
@Override
public void destroy() {
}
}
/**
* 支持重复读取application/json类型的请求数据
* 不能包装其他类型,否则会导致底层流中数据不可再读,造成参数或文件等数据丢失
*/
public class JsonRequestWrapper extends HttpServletRequestWrapper {
private final byte[] body;
public JsonRequestWrapper(HttpServletRequest request) throws IOException {
super(request);
body = StreamUtils.copyToByteArray(request.getInputStream());
}
@Override
public ServletInputStream getInputStream() throws IOException {
ByteArrayInputStream bais = new ByteArrayInputStream(body);
return new ServletInputStream() {
@Override
public boolean isFinished() {
return bais.available() <= 0;
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setReadListener(ReadListener listener) {
}
@Override
public int read() throws IOException {
return bais.read();
}
};
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(getInputStream()));
}
public byte[] getBodyAsByteArray() {
return body;
}
}
辅助获取参数的类
public abstract class RequestUtils {
private static final ObjectMapper objectMapper = new ObjectMapper();
public static String getParameterMapAsString(ServletRequest request) {
try {
return objectMapper.writeValueAsString(request.getParameterMap());
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
public static String getBodyAsString(ServletRequest request) {
if (request instanceof JsonRequestWrapper wrapper) {
return new String(wrapper.getBodyAsByteArray(), StandardCharsets.UTF_8);
}
return "[No Wrapper]";
}
public static String getInputAsString(ServletRequest request) {
if (isJsonRequest(request)) {
return getBodyAsString(request);
} else {
return getParameterMapAsString(request);
}
}
public static boolean isJsonRequest(ServletRequest request) {
String contentType = request.getContentType();
return contentType != null && contentType.contains(MediaType.APPLICATION_JSON_VALUE);
}
}
使用
拦截器中
@Slf4j
public class AuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info("input:{}", RequestUtils.getInputAsString(request));
return true;
}
}
Controller中
log.info("request:{}", StreamUtils.copyToString(request.getInputStream(), StandardCharsets.UTF_8));
log.info("request:{}",RequestUtils.getBodyAsString(request));
全局异常处理器中:
@ExceptionHandler(value = Exception.class)
@ResponseBody
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ApiResult<?> handle(Exception e, ServletRequest request) {
log.error("input:{}", RequestUtils.getInputAsString(request));
log.error(e.getMessage(), e);
return ApiResult.error(e.getMessage());
}