关于Post请求自定义注解与RequestBody注解共同使用问题的探讨

提出问题:

前面写了一篇自定义注解的文章,没看过的跳转观看SpringBoot自定义post独立参数注解,支持多参数
有小伙伴问我,@PostRequestParam 和 @RequestBody 可以混用吗?暂且不作解答,跟随我一步一步探索吧。

验证问题:

Request的本质就是流,由于部分内容是严格遵循规则的,所以会帮你提前处理好,比如headers。 而对于Body里的内容变化较多,一般交给你自己处理。我们先做个实验看看。

@PostRequestParam 和 @RequestBody 混用(前后顺序不同,报错方不同,不过异常的本质是一样的)。

    @PostMapping("test")
    public Result test(@PostRequestParam String token,@RequestBody PageInfo pageInfo){
        return null;
    }

请求异常,提示"Required request body is missing"。再来试试多个@RequestBody 同时使用。

    @PostMapping("test")
    public Result test(@RequestBody PageInfo pageInfo , @RequestBody TestInfo TestInfo){
        return null;
    }

请求异常,提示"I/O error while reading input message; nested exception is java.io.IOException: Stream closed"。
目前来看,两者不能混用且@RequestBody也不能多个一起使用。跟着我扒一扒源码,let’s go。

探究问题:

  1. 首先,来看看为什么流只能获取一次。从HttpServletRequest的getInputStream方法入手,该方法返回的是ServletInputStream。
public interface HttpServletRequest extends ServletRequest {
	public ServletInputStream getInputStream() throws IOException;
}

我们继续跟进ServletInputStream。

public abstract class ServletInputStream extends InputStream {
	/**
	 * 重置方法,直接抛出异常,不支持reset。
	 */
	 public synchronized void reset() throws IOException {
        throw new IOException("mark/reset not supported");
    }
    /**
     * 想要执行reset方法,该方法必须返回true。
     */
    public boolean markSupported() {
        return false;
    }
}

从ServletInputStream源码可以看到,其并未重写对应的markSupported 和 reset 方法。 那么,InputStream就只能获取一次。那有没有可以多次获取的流呢?答案有的,来看看ByteArrayInputStream。

public
class ByteArrayInputStream extends InputStream {
	 protected byte buf[];
	 protected int pos;
	 protected int mark = 0;

	public boolean markSupported() {
        return true;
    }
    public void mark(int readAheadLimit) {
        mark = pos;
    }
    public synchronized void reset() {
        pos = mark;
    }
}

当然,你也可以自己实现一套可以重复读写的流。 只能读取一次的问题已解决,让我们进入下一步探索。

  1. 我们先来看看HttpMessageConverter接口,这个接口定义是精髓所在。
public interface HttpMessageConverter<T> {

	/**
	 * 判断当前转换器是否支持当前消息
	 */
	boolean canRead(Class<?> clazz, @Nullable MediaType mediaType);

	/**
	 * 判断当前转换器是否可以将消息转换为目标格式
	 */
	boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType);

	/**
	 * 当前转换器支持的数据类型
	 */
	List<MediaType> getSupportedMediaTypes();

	/**
	 * 读取数据,重点关注HttpInputMessage。
	 */
	T read(Class<? extends T> clazz, HttpInputMessage inputMessage)
			throws IOException, HttpMessageNotReadableException;

	/**
	 * 按照目标格式写给OutputMessage
	 */
	void write(T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage)
			throws IOException, HttpMessageNotWritableException;

}

这个接口很明显,定义了参数处理的全过程。接着来看看HandlerMethodReturnValueHandler,主要定义了返回值处理方法。

public interface HandlerMethodReturnValueHandler {

	/**
	 * 判断是否支持返回类型
	 */
	boolean supportsReturnType(MethodParameter returnType);

	/**
	 * 对执行结果做类型结果转换
	 */
	void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
			ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception;
}
  1. 此处不再赘述SpringMVC相关内容,有兴趣自己研究。 我们重点关注下RequestResponseBodyMethodProcessor这个类,这是实现@RequestBody的核心。`
public class HandlerMethodReturnValueHandler extends AbstractMessageConverterMethodArgumentResolver 
implements HandlerMethodArgumentResolver,HandlerMethodReturnValueHandler{
	//代码省略
}

HandlerMethodArgumentResolver和AbstractMessageConverterMethodArgumentResolver 是不是很熟悉,去翻翻上一篇文章。OK,来关注下重点方法resolveArgument,看看参数解析是怎么做的。

public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
			NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {

		//代码省略
		Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());
		//代码省略
	}

跟随readWithMessageConverters一路走下去,读取了body,同时寻找了合适的消息转换器。

protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter parameter,
                                                   Type targetType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {
//           代码省略
        AbstractMessageConverterMethodArgumentResolver.EmptyBodyCheckingHttpInputMessage message;
        try {
//			这里获取inputStream转换成message。
            message = new AbstractMessageConverterMethodArgumentResolver.EmptyBodyCheckingHttpInputMessage(inputMessage);
//           寻找合适的转换器
            for (HttpMessageConverter<?> converter : this.messageConverters) {
                Class<HttpMessageConverter<?>> converterType = (Class<HttpMessageConverter<?>>) converter.getClass();
                GenericHttpMessageConverter<?> genericConverter =
                        (converter instanceof GenericHttpMessageConverter ? (GenericHttpMessageConverter<?>) converter : null);
                if (genericConverter != null ? genericConverter.canRead(targetType, contextClass, contentType) :
                        (targetClass != null && converter.canRead(targetClass, contentType))) {
//                 代码省略
                    break;
                }
            }
        }
        catch (IOException ex) {
            throw new HttpMessageNotReadableException("I/O error while reading input message", ex, inputMessage);
        }
//        代码省略
        return body;
    }

顺带提一下,上面消息转换器有10种(默认8种),这里使用的是MapppingJackson2HttpMessageConverter。

  • ByteArrayHttpMessageConverter
  • StringHttpMessageConverter
  • StringHttpMessageConverter
  • ResourceHttpMessageConverter
  • ResourceRegionHttpMessageConverter
  • SourceHttpMessageConverter
  • AllEncompassingFormHttpMessageConvertter
  • MapppingJackson2HttpMessageConverter
  • MapppingJackson2HttpMessageConverter
  • Jaxb2RootElementHttpMessageConverter

OK,这里已经找到对应的原因。 有兴趣的小伙伴可以去跟踪一下,可以发现更多的东西。

解决问题:

那问题已经定位,有没有解决方案呢?答案是有!!本质问题是InputStream只能get一次,那我们能不能把它存起来重复使用呢?能!!搞起!!!

  1. 上面提到ByteArrayInputStream,那我们就用它来解决。SpringBoot提供了HttpServletRequest的包装类HttpServletRequestWrapper,我们就从他入手。
/**
 * @Author Doit
 * @Date 2022/9/6 14:22
 * @Desc HttpServletRequestWrappers
 * @Version 1.0
 * @Slogan Just do it.
 */
public class HttpServletRequestWrappers extends HttpServletRequestWrapper {
	//声明载体
    private byte[] body;
   
    public HttpServletRequestWrappers(HttpServletRequest request) throws IOException {
        super(request);
        body = StreamUtils.copyToByteArray(request.getInputStream());
    }
    @Override
    public ServletInputStream getInputStream() {
        final ByteArrayInputStream bis = new ByteArrayInputStream(body);

        return new ServletInputStream() {
            @Override
            public int read() {
                return bis.read();
            }
           
            @Override
            public boolean isFinished() {
                return false;
            }

            @Override
            public boolean isReady() {
                return false;
            }
            
            @Override
            public void setReadListener(ReadListener listener) {

            }
        };
    }

    @Override
    public BufferedReader getReader(){
        return new BufferedReader(new InputStreamReader(this.getInputStream()));
    }
}
  1. 创建Filter拦截所有请求,用包装类HttpServletRequestWrappers替换掉HttpServletRequestWrapper。
/**
 * @Author Doit
 * @Date 2022/9/6 16:29
 * @Desc HttpServletRequestWrappers 替换 ServletRequest
 * @Version 1.0
 * @Slogan Just do it.
 */
@Component
@WebFilter(urlPatterns = "/*")
public class ReplaceStreamFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequestWrappers wrapper = new HttpServletRequestWrappers((HttpServletRequest) request);
        chain.doFilter(wrapper,response);
    }
}

OK,到这里解决方案已完成。Test it !

随意创建两个VO实体类,。

@Data
public class PageInfoVO {
    private int pageNum;
    private int pageSize;
}
@Data
public class TestInfoVO {
    String token;
    String user;
}

编写RestController。

    @PostMapping("testPT")
    public void test(@RequestBody PageInfoVO pageInfo, @RequestBody TestInfoVO testInfo){
        System.out.println("testPT : ".concat(pageInfo.toString()).concat(testInfo.toString()));
    }
    @PostMapping("testPPP")
    public void test(@RequestBody PageInfoVO pageInfo, @PostRequestParam String user, @PostRequestParam String token){
        System.out.println("testPPP : ".concat(pageInfo.toString()).concat(" , token - ").concat(token).concat(", user - ").concat(user));
    }

请求内容如下。

{
    "pageNum":"1000",
    "pageSize":"2000",
    "user":"doit",
    "token":"token"
}

两个controller输出结果为

testPT : PageInfoVO(pageNum=1000, pageSize=2000)TestInfoVO(token=token, user=doit)
testPPP : PageInfoVO(pageNum=1000, pageSize=2000) , token - token, user - doit

问题解决,撒花!!

提问总结:

  1. 思考思路很重要,提出(发现)问题 -> 验证 -> 定位问题 -> 解决问题 -> 总结反思。
  2. 该问题的本质是InputSteam只能获取一次,从自定义注解延伸过来,逐步探索。 问题不难,但过程很有趣。
  3. 虽然是一种解决方案,但是并不推荐在实际项目中使用。参数不多用@PostRequestParam 就够了,参数较多就使用@RequestBody。
  4. 本文只是提供一种探索方式,希望对大家有些许启发。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

码农的散文诗

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值