Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >嗨,CRUD BOY们,是时候掌握Spring MVC的处理流程了

嗨,CRUD BOY们,是时候掌握Spring MVC的处理流程了

作者头像
程序猿杜小头
发布于 2022-12-01 13:40:17
发布于 2022-12-01 13:40:17
45000
代码可运行
举报
文章被收录于专栏:程序猿杜小头程序猿杜小头
运行总次数:0
代码可运行

Running with Spring Boot v2.5.4, Java 11.0.12

Spring MVC是一款构建于Servlet API之上、基于同步阻塞I/O模型的主流Java Web开发框架,这种I/O模型意味着一个Http请求对应一个线程,即每一个Http请求都是在各自线程上下文中完成处理的;此外,Spring 5.0提供了一款基于异步非阻塞I/O模型的Java Web开发框架,即Spring WebFlux;大家不用纠结Spring官方会不会在将来的某个时间点将Spring MVC置为废弃(deprecated)态,至少目前来看,Spring MVC依然是流行的,在Spring官网关于Reactive的介绍中有一张图相当精致,与大家分享:


笔者自2017年7月毕业后一直服役于某央企一子公司,第一年主要参与集团公司委派的课题性项目,基本没写啥代码;自18年国庆至今,虽然辗转于多个项目组,但角色一直没变,那就是CRUD BOY,还特么挺稳的,哈哈。即使菜如CRUD BOY,也没有理由不掌握Spring MVC的相关知识,为什么这么忽悠呢?以Tomcat为例,它是目前应用最为广泛的Servlet容器,当Tomcat接收到一个Http请求后,底层复杂的Socket解析工作由它代劳了,Http请求解析完成后,它直接将HttpServletRequestHttpServletResponse(这时候还是一个空的对象)对象一并传给Servlet处理,大家只需要面向Servlet编程即可;在Spring MVC框架问世后,Servlet开始退居幕后,Java程序猿的工作变得更加轻松而聚焦了,压根不再需要和HttpServletRequest、HttpServletResponse打交道,会复制粘贴@RestController就行;前辈们做了这么多苦活、累活,如果连Spring MVC内部处理流程还一无所知,实在有点说不过去了···

1. 写在前面

一个Http请求在Spring应用中的执行链路比较长,涉及逻辑也比较多,下面通过阿里的Arthas工具来探测这条执行链路。

1.1 编写Controller并启动应用

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@RestController
@RequestMapping(path = "/crimson_typhoon")
public class CrimsonTyphoonController {
    @PostMapping(path = "/v1/fire")
    public Map<String, Object> v1Fire(@RequestBody @Valid UserDto userDto, 
                                      @RequestParam("dryRun") Boolean dryRun) {
        return ImmutableMap.of("status", "success", "code", 200);
    }
}

@Getter
@Setter
@NoArgsConstructor
public class UserDto {
    @NotBlank
    private String name;

    @NotNull
    private int age;
}

1.2 启动Arthas

执行如下命令以监听并在控制台打印CrimsonTyphoonControllerv1Fire()方法的执行路径。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
stack com.example.crimson_typhoon.controller.CrimsonTyphoonController v1Fire

1.3 发起Http请求

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
curl --location --request POST 'http://localhost:8080/crimson_typhoon/v1/fire?dryRun=true' \
--header 'Content-Type: application/json' \
--data-raw '{
    "name": "crimson_typhoon",
    "age": 18
}'

1.4 执行链路

如果有大佬是从上往下看这串执行链路信息的,这边建议您出门左转,谢谢!

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
ts=2021-11-09 21:22:37;thread_name=http-nio-8080-exec-2;id=37;is_daemon=true;priority=5;TCCL=org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedWebappClassLoader@149debbb
    @com.example.crimson_typhoon.controller.CrimsonTyphoonController.v1Fire()
        at jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(NativeMethodAccessorImpl.java:-2)
        at jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:566)
        at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:197)
        at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:141)
        at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:106)
        at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:895)
        at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:808)
        at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
        at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1064)
        at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:963)
        at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006)
        at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:909)
        at javax.servlet.http.HttpServlet.service(HttpServlet.java:681)
        at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883)
        at javax.servlet.http.HttpServlet.service(HttpServlet.java:764)
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:227)
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
        at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
        at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)
        at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
        at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)
        at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
        at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)
        at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
        at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:197)
        at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97)
        at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:542)
        at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:135)
        at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)
        at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78)
        at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:357)
        at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:382)
        at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65)
        at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:893)
        at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1726)
        at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
        at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191)
        at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)
        at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
        at java.lang.Thread.run(Thread.java:834)

大家应该对上面这坨信息很熟悉才对,因为在排查问题时经常可以在日志中看到;上述执行链路交代了两件事:

  1. 当一个Http请求到达后,Tomcat从其线程池中捞出一个线程来处理该Http请求的,记得《Go语言学习笔记》这本书中曾经提到:一切Go程序都是基于Channel的,Java又何尝不是呢?一切Java程序都是基于Thread的;
  2. 在Http请求进入Spring MVC之前,先要依次过一遍ApplicationFilterChain中的Filter,默认有4个:OrderedCharacterEncodingFilterOrderedFormContentFilterOrderedRequestContextFilterWsFilter

Filter来源于Servlet规范,并不是Spring中的术语。跟Filter紧密联系的还有FilterConfigFilterChain,它们的主要内容如下图所示:

Filter既可以过滤HttpServletRequest,也可以过滤HttpServletResponse;FilterConfig由Servlet容器组装,以初始化Filter;FilterChain同样由Servlet容器组装,为开发人员提供Filter链的执行视图,FilterChain是责任链模式的一个典型应用;在Filter链的结尾,将会调用真正的资源,比如在ApplicationFilterChain中,Filter链执行完毕后,将Http请求委派给DispatcherServlet处理,而DispatcherServlet恰恰就是Spring MVC的门户,如下所示:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public final class ApplicationFilterChain implements FilterChain {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response) {
        if (pos < n) {
            ApplicationFilterConfig filterConfig = filters[pos++];
            Filter filter = filterConfig.getFilter();
            filter.doFilter(request, response, this);
            return;
        }
        // 过滤器链中的所有过滤器执行完毕后,
        // 将HTTP请求委派给DispatcherServlet处理;
        servlet.service(request, response);
    }
}

关于Filter还有一个比较重要的知识点,那就是Filter的执行流程,类似一种U型链路,如下图所示:

2. Spring MVC处理流程

如果单单看Arthas针对v1Fire()方法输出的执行路径,会造成大家误认为Spring MVC处理流程很简短的假象,事实上,当我们一步一步DEBUG的时候,才知道这特么完全是无底洞啊!刚刚提到DispatcherServlet是Spring MVC的门户,那自然要从它开始了,在介绍DispatcherServlet之前,先来看看它的继承关系:

Front Controller设计模式中,通常由一个核心Controller负责将Http请求路由到其他Controller中处理,Spring MVC实现了这一模式,这个核心Controller就是DispatcherServlet。DispatcherServlet就像一个包工头,只揽活却不干活,当接收到Http请求后,它会将该请求委派给HandlerMappingHandlerAdapterHandlerExceptionResolverViewResolver等小弟处理。doDispatch()方法是其核心逻辑所在,为了更直观地展现Spring MVC整体流程骨架,笔者剔除了一些不相干的逻辑,如下所示:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class DispatcherServlet extends FrameworkServlet {
    protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
        HandlerExecutionChain mappedHandler = null;
        try {
            ModelAndView mv = null;
            Exception dispatchException = null;
            try {
                // Determine handler for the current request.
                mappedHandler = getHandler(request);

                // Determine handler adapter for the current request.
                HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

                // Invoke all HandlerInterceptor preHandle() in HandlerExecutionChain.
                if (!mappedHandler.applyPreHandle(request, response)) {
                    return;
                }

                // Actually invoke the handler.
                mv = ha.handle(request, response, mappedHandler.getHandler());

                // Invoke all HandlerInterceptor postHandle() in HandlerExecutionChain.
                mappedHandler.applyPostHandle(request, response, mv);
            } catch (Exception ex) {
                dispatchException = ex;
            }
            // Handle the result of handler invocation, which is either a ModelAndView 
            // or an Exception to be resolved to a ModelAndView.
            processDispatchResult(request, response, mappedHandler, mv, dispatchException);
        } catch (Exception ex) {
            // Invoke all HandlerInterceptor afterCompletion() in HandlerExecutionChain.
            triggerAfterCompletion(request, response, mappedHandler, ex);
        }
    }
}

Spring MVC的内部逻辑与HandlerMapping、HandlerAdapter息息相关,下面将从两个章节对它们进行分析。

2.1 HandlerMapping

HandlerMapping用于将Http请求映射到对应的HandlerExecutionChain

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public interface HandlerMapping {
    /**
     * @param request current HTTP request
     * @return a HandlerExecutionChain instance containing handler object and
     * any interceptors
     */
    HandlerExecutionChain getHandler(HttpServletRequest request);
}

HandlerExecutionChain内部维护了handlerinterceptorList这俩个成员变量。其中,handler一般指的是由@Controller标注的控制器,interceptorList指的是HandlerInterceptor列表。HandlerExecutionChain的精简版源码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class HandlerExecutionChain {
    private final Object handler;
    private final List<HandlerInterceptor> interceptorList = new ArrayList<>();

    public HandlerExecutionChain(Object handler, List<HandlerInterceptor> interceptorList) {}

    // Apply preHandle methods of registered interceptors.
    public boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) {}

    // Apply postHandle methods of registered interceptors.
    public void applyPostHandle(HttpServletRequest request, HttpServletResponse response, ModelAndView mv) {}

    // Trigger afterCompletion callbacks on the mapped HandlerInterceptors.
    public void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response, Exception ex) {}
}

紧接着,我们再来看一下HandlerInterceptor。顾名思义,这个拦截器可以在handler执行前后进行拦截操作,具体通过下面三个方法:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public interface HandlerInterceptor {
    // Interception point before the execution of a handler.
    default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        return true;
    }

    // Interception point after successful execution of a handler.
    default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
                            ModelAndView modelAndView) {
    }

    // Callback after completion of request processing.
    default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, 
                                 Exception ex) {
    }
}

HandlerInterceptor的执行流程也很重要,尤其是postHandle()afterCompletion()这俩方法,具体参见下图:


那HandlerMapping需要开发人员自定义吗?一般是不需要这么做的。默认地,Spring MVC会提供一些不同映射规则的HandlerMapping,这些HandlerMapping会通过initHandlerMappings()方法提前填充到DispatcherServlet中的handlerMappings成员变量中:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
private void initHandlerMappings(ApplicationContext context) {
    Map<String, HandlerMapping> matchingBeans =
            BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class);
    if (!matchingBeans.isEmpty()) {
        this.handlerMappings = new ArrayList<>(matchingBeans.values());
        AnnotationAwareOrderComparator.sort(this.handlerMappings);
    }
}

handlerMappings中各元素如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
0 = RequestMappingHandlerMapping
1 = BeanNameUrlHandlerMapping
2 = RouterFunctionMapping
3 = SimpleUrlHandlerMapping
4 = WelcomePageHandlerMapping

RequestMappingHandlerMapping无疑是最重要的一个HandlerMapping,从其名称大概能猜测到其映射规则一定与@RequestMapping注解有关,刚好CrimsonTyphoonController也由@RequestMapping标注。下面就来聊聊它是如何根据Http请求获取到相匹配的HandlerExecutionChain的。抽象类AbstractHandlerMethodMapping是RequestMappingHandlerMapping的父类,它实现了InitializingBean接口,那应该会覆盖afterPropertiesSet()方法,否则不是闲得无聊吗?从其源码来看果然如此:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@Override
public void afterPropertiesSet() {
    initHandlerMethods();
}

initHandlerMethods()方法主要用于初始化AbstractHandlerMethodMapping中MappingRegistry类型的成员变量。具体地,通过遍历所有Bean,判断当前Bean是否由@Controller@RequestMapping注解标注;若是,则填充MappingRegistry类中HashMap<RequestMappingInfo, MappingRegistration>类型的成员变量,即registry;HashMap中key/value信息如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
key   = RequestMappingInfo{POST /crimson_typhoon/v1/fire}
value = MappingRegistration{
            HandlerMethod{
                public java.util.Map com.example.crimson_typhoon.controller.CrimsonTyphoonController.v1Fire(
                    com.example.crimson_typhoon.dto.UserDto,
                    java.lang.Boolean
                )
            }
        }

有了上面这层映射关系,根据Http请求获取对应的HandlerMethod就是轻而易举的事了(在HandlerMethod中,有两个比较重要的成员变量,分别是Object类型的bean变量和java.lang.reflect.Method类型的method变量);最后,将HandlerMethod与HandlerInterceptor封装到HandlerExecutionChain实例中去就结束了。

handler其实是被封装在HandlerMethod实例中的。

2.2 HandlerAdapter

上一小节主要介绍如何根据Http请求获取相匹配的HandlerExecutionChain实例;在本小节中,将重点关注HandlerExecutionChain中HandlerMethod的执行流程。既然HandlerMethod中的method变量是java.lang.reflect.Method类型的,那这架势肯定是奔着反射去的了,回顾下java.lang.reflect.Method的核心API

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class Method {
    public Object invoke(Object obj, Object... args) {
        // 实现细节忽略
    }
}

很显然,method实例要想顺利调用其invoke()方法,现在还差一个方法参数,那就从参数解析开始分析吧。


顾名思义,HandlerAdapter是一个适配器,它为Spring MVC带来了拓展性,可以适配任意类型的handler,绝不仅仅是那些由@Controller注解标注的handler。与HandlerMapping类似,开发人员若无特殊需求,一般也无需自定义HandlerAdapter。默认地,Spring MVC会提供一些可以适配不同handler的HandlerAdapter,比如:适配org.springframework.web.servlet.mvc.Controller接口的SimpleControllerHandlerAdapter,适配javax.servlet.Servlet接口的SimpleServletHandlerAdapter等;此外,这些HandlerAdapter是通过initHandlerAdapters()方法提前填充到DispatcherServlet中这一handlerAdapters成员变量中的:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
private void initHandlerAdapters(ApplicationContext context) {
    Map<String, HandlerAdapter> matchingBeans =
            BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerAdapter.class);
    if (!matchingBeans.isEmpty()) {
        this.handlerAdapters = new ArrayList<>(matchingBeans.values());
        AnnotationAwareOrderComparator.sort(this.handlerAdapters);
    }
}

handlerAdapters中各元素如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
0 = RequestMappingHandlerAdapter
1 = HandlerFunctionAdapter
2 = HttpRequestHandlerAdapter
3 = SimpleControllerHandlerAdapter

RequestMappingHandlerAdapter既然能排在第一把交椅,那足以说明它的重要性,它主要用于适配HandlerMethod类型的handler,如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class RequestMappingHandlerAdapter {
    /**
     * This implementation expects the handler to be an HandlerMethod.
     */
    @Override
    public final boolean supports(Object handler) {
        return handler instanceof HandlerMethod;
    }
}

DispatcherServlet发现RequestMappingHandlerAdapter的supports()方法返回true,就不再继续寻找适配器了,因为最匹配的适配器已经找到。

此外,RequestMappingHandlerAdapter同样实现了InitializingBean接口,用于初始化argumentResolversreturnValueHandlers这俩成员变量,如下所示:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@Override
public void afterPropertiesSet() {
    if (this.argumentResolvers == null) {
        List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers();
        this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
    }
    if (this.returnValueHandlers == null) {
        List<HandlerMethodReturnValueHandler> handlers = getDefaultReturnValueHandlers();
        this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers);
    }
}

argumentResolvers中各元素如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
 0 = RequestParamMethodArgumentResolver
 1 = RequestParamMapMethodArgumentResolver
 2 = PathVariableMethodArgumentResolver
 3 = PathVariableMapMethodArgumentResolver
 4 = MatrixVariableMethodArgumentResolver
 5 = MatrixVariableMapMethodArgumentResolver
 6 = ServletModelAttributeMethodProcessor
 7 = RequestResponseBodyMethodProcessor
 8 = RequestPartMethodArgumentResolver
 9 = RequestHeaderMethodArgumentResolver
10 = RequestHeaderMapMethodArgumentResolver
11 = ServletCookieValueMethodArgumentResolver
12 = ExpressionValueMethodArgumentResolver
13 = SessionAttributeMethodArgumentResolver
14 = RequestAttributeMethodArgumentResolver
15 = ServletRequestMethodArgumentResolver
16 = ServletResponseMethodArgumentResolver
17 = HttpEntityMethodProcessor
18 = RedirectAttributesMethodArgumentResolver
19 = ModelMethodProcessor
20 = MapMethodProcessor
21 = ErrorsMethodArgumentResolver
22 = SessionStatusMethodArgumentResolver
23 = UriComponentsBuilderMethodArgumentResolver
24 = PrincipalMethodArgumentResolver
25 = RequestParamMethodArgumentResolver
26 = ServletModelAttributeMethodProcessor

argumentResolversHandlerMethodArgumentResolverComposite类型的,后者是一个复合类,持有多个HandlerMethodArgumentResolver类型的方法参数解析器。当需要获取方法参数解析器时,HandlerMethodArgumentResolverComposite会遍历其所持有的所有参数解析器,若HandlerMethodArgumentResolver的supportsParameter()方法返回true,这意味着找到了最匹配的方法参数解析器,不再继续查找。在本文中,Http请求携带了两种参数,具体如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
------------------------------
{
    "name": "crimson_typhoon",
    "age": 28
}
------------------------------
dryRun=true
------------------------------

对于Http请求体中的JSON串,交由argumentResolvers中的RequestResponseBodyMethodProcessor负责解析;它有两个作用,一是将Http请求体中的JSON串封装到由@RequestBody标注的参数实例中,即UserDto,二是将由@ResponseBody标注的handler中方法的返回值写入到Http响应体中,参数解析相关源码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class RequestResponseBodyMethodProcessor {
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.hasParameterAnnotation(RequestBody.class);
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
                                  NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());
        String name = Conventions.getVariableNameForParameter(parameter);

        if (binderFactory != null) {
            WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name);
            if (arg != null) {
                validateIfApplicable(binder, parameter);
                if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
                    throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());
                }
            }
        }
        return adaptArgumentIfNecessary(arg, parameter);
    }
}

RequestResponseBodyMethodProcessor持有的messageConverters成员变量中内嵌了近10个HttpMessageConverter类型的转换器,因此具体的参数解析工作将由其中一个转换器负责,至于究竟是哪一个转换器,这要看canRead()方法是否能返回true了,若返回值为true,则会通过该转换器的read()方法解析出最终的参数;本文所提及的JSON串最终是由MappingJackson2CborHttpMessageConverter负责填充到UserDto实例中去的(由于@Valid注解的存在,还会紧接着进行Bean Validation操作)。此外,HttpMessageConverter具备双向转换能力,其源码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public interface HttpMessageConverter<T> {
    // Indicates whether the given class can be read by this converter.
    boolean canRead(Class<?> clazz, @Nullable MediaType mediaType);

    // Indicates whether the given class can be written by this converter.
    boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType);

    // Read an object of the given type from the given input message, and returns it.
    T read(Class<? extends T> clazz, HttpInputMessage inputMessage);

    // Write an given object to the given output message.
    void write(T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage);
}

对于Http请求URL中的参数,交由argumentResolvers中的RequestParamMethodArgumentResolver负责,具体借助org.springframework.core.convert.support.Converter接口的实现类StringToBooleanConverter将字符串类型值转换为布尔值。

argumentResolvers中各元素如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
 0 = ModelAndViewMethodReturnValueHandler
 1 = ModelMethodProcessor
 2 = ViewMethodReturnValueHandler
 3 = ResponseBodyEmitterReturnValueHandler
 4 = StreamingResponseBodyReturnValueHandler
 5 = HttpEntityMethodProcessor
 6 = HttpHeadersReturnValueHandler
 7 = CallableMethodReturnValueHandler
 8 = DeferredResultMethodReturnValueHandler
 9 = AsyncTaskMethodReturnValueHandler
10 = ServletModelAttributeMethodProcessor
11 = RequestResponseBodyMethodProcessor
12 = ViewNameMethodReturnValueHandler
13 = MapMethodProcessor
14 = ServletModelAttributeMethodProcessor

returnValueHandlersHandlerMethodReturnValueHandlerComposite类型的,后者也是一个复合类,持有多个HandlerMethodReturnValueHandler类型的方法返回值解析器。由于CrimsonTyphoonController由@RestController标注,而@RestController注解接口又由@ResponseBody标注,因此RequestResponseBodyMethodProcessor最终从这些候选解析器中脱颖而出,很熟悉对不对?废话不多说,这一解析器依然会委派MappingJackson2CborHttpMessageConverter将v1Fire()方法的返回值转换为JSON串,然后写入到Http响应体中去。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class RequestResponseBodyMethodProcessor {
    @Override
    public boolean supportsReturnType(MethodParameter returnType) {
        return (AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), ResponseBody.class) ||
                returnType.hasMethodAnnotation(ResponseBody.class));
    }

    @Override
    public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
                                  ModelAndViewContainer mavContainer, NativeWebRequest webRequest) {
        mavContainer.setRequestHandled(true);
        ServletServerHttpRequest inputMessage = createInputMessage(webRequest);
        ServletServerHttpResponse outputMessage = createOutputMessage(webRequest);

        writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);
    }
}

目前,前后端分离已成为业界开发与部署的标准模式,压根不会将html、jsp等页面嵌入在后端应用中,因此视图渲染就不再赘述了。

3. 总结

事实上,本文也只是粗略分析了Spring MVC的处理流程,还有一些重要的细节没有覆盖,比如:统一异常处理,限于篇幅,后续再介绍它的原理与最佳实践方案吧。

参考文档

  1. https://spring.io/reactive
  2. https://docs.spring.io/spring-framework/docs/current/reference/html/
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-11-10,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 程序猿杜小头 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
原理解读:Spring MVC统一异常处理
当前,Spring统一异常处理机制是Java开发人员普遍使用的一种技术,在业务校验失败的时候,直接抛出业务异常即可,这明显简化了业务异常的治理流程与复杂度。值得一提的是,统一异常处理机制并不是Spring Boot提供的,而是Spring MVC,前者只是为Spring MVC自动配置了刚好够用的若干组件而已,具体配置了哪些组件,感兴趣的读者可以到spring-boot-autoconfigure模块中找到答案。
程序猿杜小头
2022/12/01
1.3K1
原理解读:Spring MVC统一异常处理
阿里问题定位神器 Arthas 的骚操作,定位线上BUG,超给力!
公司有个渠道系统,专门对接三方渠道使用,没有什么业务逻辑,主要是转换报文和参数校验之类的工作,起着一个承上启下的作用。
程序员小强
2020/03/23
1.3K0
阿里问题定位神器 Arthas 的骚操作,定位线上BUG,超给力!
IDEA 新建 Spring MVC 工程项目与 SpringMVC 运行流程
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/ajianyingxiaoqinghan/article/details/80727200
剑影啸清寒
2019/05/29
3.8K0
Spring Cloud Config Server迁移节点或容器化带来的问题
为了说明下面的内容,我们可以先尝试重现一下问题:在一个测试环境中,将Spring Cloud Config的配置中心迁移到另外一个节点上,即配置中心的IP地址发生了变化。在完成迁移之后,我们会发现该环境下各个微服务应用的健康状态会变得时好时坏,并且在日志中会出现类似下面的报错:
程序猿DD
2018/07/31
1.3K0
Spring Cloud Config Server迁移节点或容器化带来的问题
springboot之mvc原理(一)-请求处理
springboot出现以后,我们搭建spring应用的复杂度大大降低,仅仅需要简单的注解和若干配置类就能构建简单的应用,这些都依赖于springboot默认集成了一整套的spring核心组件,比如在新版本的springboot的中,web和aop能力是完全不用配置和注解开始就能直接使用,这也是springboot设计和存在的初衷,尽可能大的程度上降低spring应用搭建和配置成本,将研发人员的主要精力尽可能投入在业务开发中。
叔牙
2020/11/19
2.9K0
springboot之mvc原理(一)-请求处理
聊聊springboot tomcat的maxHttpFormPostSize
本文主要研究一下spring boot tomcat的maxHttpFormPostSize参数
code4it
2023/08/17
6740
URL 解析与鉴权中的陷阱 —— Spring 篇
在上一篇文章中介绍了 Java Web 应用中 URL 解析与鉴权认证中的常见陷阱,但主要针对 Servlet 容器进行分析。前文末尾也提到了,当前 Web 应用多是使用 Spring 全家桶进行开发,其路由匹配方式又与传统 Web 容器有所差异,因此本文就对其进行分析。
evilpan
2023/10/17
1.4K0
URL 解析与鉴权中的陷阱 —— Spring 篇
Spring MVC介绍(二)之 Spring MVC 执行流程解析
此时启动容器,访问 http://localhost:8080/spring_mvc/beanName ,可以看到显示的是我们control返回的数据。
zoro
2019/04/11
5140
Spring MVC介绍(二)之 Spring MVC 执行流程解析
.MysqlDataTruncation: Data truncation: Data too long for column 'content' at row 1
版权声明:本文为博主原创文章,欢迎转载。 https://blog.csdn.net/chengyuqiang/article/details/89037174
程裕强
2019/07/02
1.6K0
.MysqlDataTruncation: Data truncation: Data too long for column 'content' at row 1
springboot2.0配置多数据源出错HikariPool-1 - jdbcUrl is required with driverClassName.
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
suveng
2019/09/18
3.3K0
spring(错误一) spring mvc上传多张图片,报错:java.lang.NoSuchMethodException解决方案
出现这个错误的原因是 在方法里面 MultipartFile[] files没有加入@RequestParam;
Java架构师必看
2021/05/31
4990
chaos-monkey-spring-boot小试牛刀
chaos-monkey-spring-boot是专门为Spring Boot打造的Chaos Monkey
code4it
2018/09/17
1K0
还在使用kill -9 pid结束spring boot项目吗?那你已经落伍了!
kill可将指定的信息送至程序。预设的信息为SIGTERM(15),可将指定程序终止。若仍无法终止该程序,可使用SIGKILL(9)信息尝试强制删除程序。程序或工作的编号可利用ps指令或jobs指令查看(这段话来自菜鸟教程)。
肉眼品世界
2021/01/25
9050
还在使用kill -9 pid结束spring boot项目吗?那你已经落伍了!
springBoot系列教程07:异常捕获
发生异常是很正常的事,异常种类也是千奇百怪,发生异常并不可怕,只要正确的处理,并正确的返回错误信息并无大碍,如果不进行捕获或者处理,分分钟服务器宕机是很正常的事
肖哥哥
2018/08/02
8870
springBoot系列教程07:异常捕获
初学Spring Cloud踩坑之org.springframework.web.client.HttpClientErrorException: 400 null
初学Spring Cloud踩坑之org.springframework.web.client.HttpClientErrorException: 400 null
Java架构师必看
2021/05/14
1.7K0
聊聊springcloud的serviceRegistryEndpoint
本文主要研究一下springcloud的serviceRegistryEndpoint
code4it
2018/09/17
1.4K0
springcloud踩坑
直到我看见了 服务提供方的报错 一下子我就明白了 我数据库服务没开!!!!!!!!!!! 这里只是想分享以下踩坑经验 希望大家不要像我一样粗心
暴躁的程序猿
2022/03/24
3550
feign.FeignException$MethodNotAllowed: status 405 reading xxx#yyy(Integer)
使用feign 调用异常 feign.FeignException$MethodNotAllowed: status 405 reading ConsumerService#findById(Integer)
时间静止不是简史
2020/07/27
2.2K0
feign.FeignException$MethodNotAllowed: status 405 reading xxx#yyy(Integer)
Swagger-ui在文件上传时报错net::ERR_CONNECTION_RESET
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
yingzi_code
2019/08/30
1.9K0
Spring不能将包含key值为null的map集合转换成JSON
# Spring不能将包含key值为null的map集合转换成JSON Null key for a Map not allowed in JSON Spring不能将包含key值为null 的 map集合转换成 JSON # 1. 问题描述 编写代码进行测试的时候,控制台报出如下错误 org.springframework.http.converter.HttpMessageNotWritableException: Could not write JSON: Null key for a Map not
taixingyiji
2022/07/25
2.6K0
推荐阅读
相关推荐
原理解读:Spring MVC统一异常处理
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
查看详情【社区公告】 技术创作特训营有奖征文