说说SpringMVC从http流到Controller接口参数的转换过程
一,前言
谈起springMVC框架接口请求过程大部分人可能会这样回答:负责将请求分发给对应的handler,然后handler会去调用实际的接口。核心功能是这样的,但是这样的回答未免有些草率。面试过很多人,大家彷佛约定好了的一般,给的都是这样”泛泛”的标准答案。最近开发遇到了这样的两个场景:
- 1>,上游的回调接口要求接受类型为application/x-www-form-urlencode,请求方式post,接受消息为xml文本。
- 2>,对接系统动态生成文件(文件实时变更,采用chunk编码),导致业务系统无法预览文件(浏览器会直接下载),采用中转接口对文件流进行转发。
针对上述需求,如何开发rest风格的接口解决呢?
二、request的生命周期
我们知道,当一个请求到达后端web应用(mvc架构的应用)监听的端口, 率先被拦截器拦截到,然后转交到对应的接口。我们知道底层的数据必定是数据流形式的,那么他是怎么把流转成接口需要的参数,从而发起调用的呢?此时我们便需要去研究DispathServlet的处理逻辑了。
2.1 DispatchServlet具备的职能
- handler 容器
- handler 前、后置处理器
- 请求转发(交由HandlerApdater.handler()执行)
- 响应结果转发
具体入口代码如下(DipatchServlet.doDispatch):
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
ModelAndView mv = null;
Exception dispatchException = null;
try {
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);
// 找到与请求匹配的handler
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}
// 找到与请求匹配的HandlerAdpater
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// ... 省略部分代码
// handler 前置处理器
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
// handler 调用: 会实际调用到我们的controller接口
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
applyDefaultViewName(processedRequest, mv);
// handler 后置处理
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
dispatchException = ex;
}
catch (Throwable err) {
dispatchException = new NestedServletException("Handler dispatch failed", err);
}
// 返回结果分发
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception ex) {
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch (Throwable err) {
triggerAfterCompletion(processedRequest, response, mappedHandler,
new NestedServletException("Handler processing failed", err));
}
finally {
// 省略部分代码
}
}
这个接口就是我们寻常所说的handler的转发逻辑。但是我们也知道了实际上去调用我们controller接口的是HandlerAdapter
2.2 HandlerAdapter具备的职能
从上述我们知道了请求的转发过程,现在我们要弄清楚handler怎么调用到我们的controller接口的(以RequestMappingHandlerAdapter为例)。
- argumentResolvers 参数解析器,提供了supportsParameter()、resolveArgument()两个方法来告诉容器是否能解析该参数以及怎么解析
- returnValueHandlers 返回值解析器,
- modelAndViewResolvers 模型视图解析器
- messageConverters 消息转换器,
跟踪源码发现(RequestMappingHandlerAdapter.invokeHandlerMethod()),他调用Controller接口发生再ServletInvocableHandlerMethod.invokeAndHandle()方法。看一下主体逻辑:
public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {
// 调用controller接口
Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
// ... 省略部分代码
try {
// 处理返回结果
this.returnValueHandlers.handleReturnValue(
returnValue, getReturnValueType(returnValue), mavContainer, webRequest);
}
catch (Exception ex) {
if (logger.isTraceEnabled()) {
logger.trace(formatErrorForReturnValue(returnValue), ex);
}
throw ex;
}
}
调用controller接口的方法跟踪源码会发现,主要是通过request寻找到正确的参数解析器,然后去解析参数,这里我们以@RequestBody标注的参数为例,看其是如何解析的:
(RequestResponseBodyMethodProcessor.readWithMessageConverters())
protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter parameter,
Type targetType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {
MediaType contentType;
boolean noContentType = false;
//... 省略部分代码
EmptyBodyCheckingHttpInputMessage message;
try {
message = new 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))) {
if (message.hasBody()) {
HttpInputMessage msgToUse =
getAdvice().beforeBodyRead(message, parameter, targetType, converterType);
body = (genericConverter != null ? genericConverter.read(targetType, contextClass, msgToUse) :
((HttpMessageConverter<T>) converter).read(targetClass, msgToUse));
body = getAdvice().afterBodyRead(body, msgToUse, parameter, targetType, converterType);
}
else {
body = getAdvice().handleEmptyBody(null, message, parameter, targetType, converterType);
}
break;
}
}
}
catch (IOException ex) {
throw new HttpMessageNotReadableException("I/O error while reading input message", ex, inputMessage);
}
// ... 省略部分代码
return body;
}
可以看到其实就是简单的找到适配的MessageConvert,调用其read方法即可。把参数解析出来之后,发起对controller接口调用。至此从发起请求到落到controller接口的过程就是这样子的。
2.3 总结从容器接受到请求到交付到controller接口的过程。
上图较为完整的描述了从http报文字节流到controller接口java对象的过程,返回的处理是类型的流程不在赘述。
三、总结
有章节二知道了生命周期,我们知道严格意义上,对于问题一,我们只需要定义一个HandlerMethodArgumentResolver去专门解析类似参数(实际上我们用@RequestBody修饰的参数,那么只需要定义一个MessageConvert即可),然后注入到容器即可。针对问题二,其实只要不要覆盖原生的MessageConverts对于文件流的输出本身SpringMVC就是支持的,但是因为我们通常注入MessageConvert是通过WebMvcConfigurerAdapter实现会导致默认的转换器丢失需要特别注意。