提出问题:
前面写了一篇自定义注解的文章,没看过的跳转观看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。
探究问题:
- 首先,来看看为什么流只能获取一次。从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;
}
}
当然,你也可以自己实现一套可以重复读写的流。 只能读取一次的问题已解决,让我们进入下一步探索。
- 我们先来看看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;
}
- 此处不再赘述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一次,那我们能不能把它存起来重复使用呢?能!!搞起!!!
- 上面提到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()));
}
}
- 创建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
问题解决,撒花!!
提问总结:
- 思考思路很重要,提出(发现)问题 -> 验证 -> 定位问题 -> 解决问题 -> 总结反思。
- 该问题的本质是InputSteam只能获取一次,从自定义注解延伸过来,逐步探索。 问题不难,但过程很有趣。
- 虽然是一种解决方案,但是并不推荐在实际项目中使用。参数不多用@PostRequestParam 就够了,参数较多就使用@RequestBody。
- 本文只是提供一种探索方式,希望对大家有些许启发。