【新姿势体验版】重复读取&修改Request请求体(Java)

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内嵌容器,目测版本变更影响较小,可作为一种新的解决思路。(注:个人当前没有此需求,未在生产环境中使用。若使用,请自验。)

  • 17
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值