• SpringMVC


    原理流程图

    https://www.processon.com/view/link/63f1d5cc2f69f86c1f96ee9c

    我们在使用SpringMVC时,传统的方式是通过定义web.xml,比如:

    <web-app>
    
        <servlet>
            <servlet-name>appservlet-name>
            <servlet-class>org.springframework.web.servlet.DispatcherServletservlet-class>
            <init-param>
                <param-name>contextConfigLocationparam-name>
                <param-value>/WEB-INF/spring.xmlparam-value>
            init-param>
            <load-on-startup>1load-on-startup>
        servlet>
    
        <servlet-mapping>
            <servlet-name>appservlet-name>
            <url-pattern>/app/*url-pattern>
        servlet-mapping>
    
    web-app>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    我们只要定义这样的一个web.xml,然后启动Tomcat,那么我们就能正常使用SpringMVC了。

    SpringMVC中,最为核心的就是DispatcherServlet,在启动Tomcat的过程中:

    1. Tomcat会先创建DispatcherServlet对象
    2. 然后调用DispatcherServlet对象的init()

    而在init()方法中,会创建一个Spring容器,并且添加一个ContextRefreshListener监听器,该监听器会监听ContextRefreshedEvent事件(Spring容器启动完成后就会发布这个事件),也就是说Spring容器启动完成后,就会执行ContextRefreshListener中的onApplicationEvent()方法,从而最终会执行DispatcherServlet中的initStrategies(),这个方法中会初始化更多内容:

    protected void initStrategies(ApplicationContext context) {
            initMultipartResolver(context);
            initLocaleResolver(context);
            initThemeResolver(context);
            initHandlerMappings(context);
            initHandlerAdapters(context);
            initHandlerExceptionResolvers(context);
            initRequestToViewNameTranslator(context);
            initViewResolvers(context);
            initFlashMapManager(context);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    其中最为核心的就是HandlerMappingHandlerAdapter

    什么是Handler?

    Handler表示请求处理器,在SpringMVC中有四种Handler:

    1. 实现了Controller接口的Bean对象
    2. 实现了HttpRequestHandler接口的Bean对象
    3. 添加了@RequestMapping注解的方法
    4. 一个HandlerFunction对象

    比如实现了Controller接口的Bean对象:

    @Component("/test")
    public class BeanNameController implements Controller {
    	@Override
    	public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse 				response) throws Exception {
        	System.out.println("xxx");
        	return new ModelAndView();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    实现了HttpRequestHandler接口的Bean对象:

    @Component("/test")
    public class BeanNameController implements HttpRequestHandler {
        @Override
        public void handleRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
                System.out.println("xxx");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    添加了@RequestMapping注解的方法:

    @RequestMapping
    @Component
    public class Controller {
        @Autowired
        private Service service;
    
        @RequestMapping(method = RequestMethod.GET, path = "/test")
        @ResponseBody
        public String test(String username) {
            return "xxx";
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    一个HandlerFunction对象(以下代码中有两个):

    @ComponentScan("com.xxx")
    @Configuration
    public class AppConfig {
        @Bean
        public RouterFunction<ServerResponse> person() {
            return route().GET("/app/person", request -> ServerResponse.status(HttpStatus.OK).body("Hello GET")).POST("/app/person", request -> ServerResponse.status(HttpStatus.OK).body("Hello POST")).build();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    什么是HandlerMapping?

    HandlerMapping负责去寻找Handler,并且保存路径和Handler之间的映射关系。

    因为有不同类型的Handler,所以在SpringMVC中会由不同的HandlerMapping来负责寻找Handler,比如:

    1. BeanNameUrlHandlerMapping:负责Controller接口和HttpRequestHandler接口
    2. RequestMappingHandlerMapping:负责@RequestMapping的方法
    3. RouterFunctionMapping:负责RouterFunction以及其中的HandlerFunction

    BeanNameUrlHandlerMapping的寻找流程:

    1. 找出Spring容器中所有的beanName
    2. 判断beanName是不是以“/”开头
    3. 如果是,则把它当作一个Handler,并把beanName作为key,bean对象作为value存入handlerMap
    4. handlerMap就是一个Map

    RequestMappingHandlerMapping的寻找流程:

    1. 找出Spring容器中所有beanType
    2. 判断beanType是不是有@Controller注解,或者是不是有@RequestMapping注解
    3. 判断成功则继续找beanType中加了@RequestMapping的Method
    4. 并解析@RequestMapping中的内容,比如method、path,封装为一个RequestMappingInfo对象
    5. 最后把RequestMappingInfo对象做为key,Method对象封装为HandlerMethod对象后作为value,存入registry
    6. registry就是一个Map

    RouterFunctionMapping的寻找流程会有些区别,但是大体是差不多的,相当于是一个path对应一个HandlerFunction。

    各个HandlerMapping除开负责寻找Handler并记录映射关系之外,自然还需要根据请求路径找到对应的Handler,在源码中这三个HandlerMapping有一个共同的父类AbstractHandlerMapping

    在这里插入图片描述

    AbstractHandlerMapping实现了HandlerMapping接口,并实现了getHandler(HttpServletRequest request)方法。

    AbstractHandlerMapping会负责调用子类的getHandlerInternal(HttpServletRequest request)方法从而找到请求对应的Handler,然后AbstractHandlerMapping负责将Handler和应用中所配置的HandlerInterceptor整合成为一个HandlerExecutionChain对象。

    所以寻找Handler的源码实现在各个HandlerMapping子类中的getHandlerInternal()中,根据请求路径找到Handler的过程并不复杂,因为路径和Handler的映射关系已经存在Map中了。

    比较困难的点在于,当DispatcherServlet接收到一个请求时,该利用哪个HandlerMapping来寻找Handler呢?看源码:

    protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
        if (this.handlerMappings != null) {
            for (HandlerMapping mapping : this.handlerMappings) {
                HandlerExecutionChain handler = mapping.getHandler(request);
                if (handler != null) {
                    return handler;
                }
            }
        }
        return null;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    很简单,就是遍历,找到就返回,默认顺序为:

    在这里插入图片描述

    所以BeanNameUrlHandlerMapping的优先级最高,比如:

    @Component("/test")
    public class BeanNameController implements Controller {
        @Override
        public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
            System.out.println("Hello xxx");
            return new ModelAndView();
        }
    }
    
    
    @RequestMapping(method = RequestMethod.GET, path = "/test")
    @ResponseBody
    public String test(String username) {
        return "Hi xxx";
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    请求路径都是/test,但是最终是Controller接口的会生效。

    什么是HandlerAdapter?

    找到了Handler之后,接下来就该去执行了,比如执行下面这个test()

    @RequestMapping(method = RequestMethod.GET, path = "/test") 
    @ResponseBody 
    public String test(String username) {     
        return "xxx"; 
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    但是由于有不同种类的Handler,所以执行方式是不一样的,再来总结一下Handler的类型:

    1. 实现了Controller接口的Bean对象,执行的是Bean对象中的handleRequest()
    2. 实现了HttpRequestHandler接口的Bean对象,执行的是Bean对象中的handleRequest()
    3. 添加了@RequestMapping注解的方法,具体为一个HandlerMethod,执行的就是当前加了注解的方法
    4. 一个HandlerFunction对象,执行的是HandlerFunction对象中的handle()

    所以,按逻辑来说,找到Handler之后,我们得判断它的类型,比如代码可能是这样的:

    Object handler = mappedHandler.getHandler(); 
    if(handler instanceof Controller)
    
    {
        ((Controller) handler).handleRequest(request, response);
    } else if(handler instanceof HttpRequestHandler)
    
    {
        ((HttpRequestHandler) handler).handleRequest(request, response);
    } else if(handler instanceof HandlerMethod)
    
    {
        ((HandlerMethod) handler).getMethod().invoke(...);
    } else if(handler instanceof HandlerFunction)
    
    {
        ((HandlerFunction) handler).handle(...);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    但是SpringMVC并不是这么写的,还是采用的适配模式,把不同种类的Handler适配成一个HandlerAdapter,后续再执行HandlerAdapter的handle()方法就能执行不同种类Hanlder对应的方法。

    针对不同的Handler,会有不同的适配器:

    1. HttpRequestHandlerAdapter
    2. SimpleControllerHandlerAdapter
    3. RequestMappingHandlerAdapter
    4. HandlerFunctionAdapter

    适配逻辑为:

    protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {
        if (this.handlerAdapters != null) {
            for (HandlerAdapter adapter : this.handlerAdapters) {
                if (adapter.supports(handler)) {
                    return adapter;
                }
            }
        }
        throw new ServletException("No adapter for handler [" + handler + "]: The DispatcherServlet configuration needs to include a HandlerAdapter that supports this handler");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    传入handler,遍历上面四个Adapter,谁支持就返回谁,比如判断的代码依次为:

    public boolean supports(Object handler) {
        return (handler instanceof HttpRequestHandler);
    }
    
    public boolean supports(Object handler) {
        return (handler instanceof Controller);
    }
    
    public final boolean supports(Object handler) {
        return (handler instanceof HandlerMethod && supportsInternal((HandlerMethod) handler));
    }
    
    public boolean supports(Object handler) {
        return handler instanceof HandlerFunction;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    根据Handler适配出了对应的HandlerAdapter后,就执行具体HandlerAdapter对象的handle()方法了,比如:

    HttpRequestHandlerAdapter的handle():

    public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {     
        ((HttpRequestHandler) handler).handleRequest(request, response);     
        return null; 
    }
    
    • 1
    • 2
    • 3
    • 4

    SimpleControllerHandlerAdapter的handle():

    public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {     
        return ((Controller) handler).handleRequest(request, response); 
    }
    
    • 1
    • 2
    • 3

    HandlerFunctionAdapter的handle():

    HandlerFunction<?> handlerFunction = (HandlerFunction<?>) handler; serverResponse = handlerFunction.handle(serverRequest);
    
    • 1

    因为这三个接收的直接就是Requeset对象,不用SpringMVC做额外的解析,所以比较简单,比较复杂的是RequestMappingHandlerAdapter,它执行的是加了@RequestMapping的方法,而这种方法的写法可以是多种多样,SpringMVC需要根据方法的定义去解析Request对象,从请求中获取出对应的数据然后传递给方法,并执行。

    @RequestMapping方法参数解析

    当SpringMVC接收到请求,并找到了对应的Method之后,就要执行该方法了,不过在执行之前需要根据方法定义的参数信息,从请求中获取出对应的数据,然后将数据传给方法并执行。

    一个HttpServletRequest通常有:

    1. request parameter
    2. request attribute
    3. request session
    4. reqeust header
    5. reqeust body

    比如如下几个方法:

    public String test(String username) {     
        return "xxx"; 
    }
    
    • 1
    • 2
    • 3

    表示要从request parameter中获取key为username的value

    public String test(@RequestParam("uname") String username) {     
        return "xxx"; 
    }
    
    • 1
    • 2
    • 3

    表示要从request parameter中获取key为uname的value

    public String test(@RequestAttribute String username) {     
        return "xxx"; 
    }
    
    • 1
    • 2
    • 3

    表示要从request attribute中获取key为username的value

    public String test(@SessionAttribute String username) {     
        return "xxx"; 
    }
    
    • 1
    • 2
    • 3

    表示要从request session中获取key为username的value

    public String test(@RequestHeader String username) {     
        return "xxx"; 
    }
    
    • 1
    • 2
    • 3

    表示要从request header中获取key为username的value

    public String test(@RequestBody String username) {     
        return "xxx"; 
    }
    
    • 1
    • 2
    • 3

    表示获取整个请求体

    所以,我们发现SpringMVC要去解析方法参数,看该参数到底是要获取请求中的哪些信息。

    而这个过程,源码中是通过HandlerMethodArgumentResolver来实现的,比如:

    1. RequestParamMethodArgumentResolver:负责处理@RequestParam
    2. RequestHeaderMethodArgumentResolver:负责处理@RequestHeader
    3. SessionAttributeMethodArgumentResolver:负责处理@SessionAttribute
    4. RequestAttributeMethodArgumentResolver:负责处理@RequestAttribute
    5. RequestResponseBodyMethodProcessor:负责处理@RequestBody
    6. 还有很多其他的…

    而在判断某个参数该由哪个HandlerMethodArgumentResolver处理时,也是很粗暴:

    private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
        HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter);
        if (result == null) {
            for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) {
                if (resolver.supportsParameter(parameter)) {
                    result = resolver;
                    this.argumentResolverCache.put(parameter, result);
                    break;
                }
            }
        }
        return result;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    就是遍历所有的HandlerMethodArgumentResolver,哪个能支持处理当前这个参数就由哪个处理。

    比如:

    @RequestMapping(method = RequestMethod.GET, path = "/test") 
    @ResponseBody public String test(@RequestParam @SessionAttribute String username) {     	System.out.println(username);
        return "xxx"; 
    }
    
    • 1
    • 2
    • 3
    • 4

    以上代码的username将对应RequestParam中的username,而不是session中的,因为在源码中RequestParamMethodArgumentResolver更靠前。

    当然HandlerMethodArgumentResolver也会负责从request中获取对应的数据,对应的是resolveArgument()方法。

    比如RequestParamMethodArgumentResolver:

    protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception {
        HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class);
        if (servletRequest != null) {
            Object mpArg = MultipartResolutionDelegate.resolveMultipartArgument(name, parameter, servletRequest);
            if (mpArg != MultipartResolutionDelegate.UNRESOLVABLE) {
                return mpArg;
            }
        }
        Object arg = null;
        MultipartRequest multipartRequest = request.getNativeRequest(MultipartRequest.class);
        if (multipartRequest != null) {
            List<MultipartFile> files = multipartRequest.getFiles(name);
            if (!files.isEmpty()) {
                arg = (files.size() == 1 ? files.get(0) : files);
            }
        }
        if (arg == null) {
            String[] paramValues = request.getParameterValues(name);
            if (paramValues != null) {
                arg = (paramValues.length == 1 ? paramValues[0] : paramValues);
            }
        }
        return arg;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    核心是:

    if (arg == null) {     
        String[] paramValues = request.getParameterValues(name);     
        if (paramValues != null) {         
            arg = (paramValues.length == 1 ? paramValues[0] : paramValues);     
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    按同样的思路,可以找到方法中每个参数所要求的值,从而执行方法,得到方法的返回值。

    @RequestMapping方法返回值解析

    而方法返回值,也会分为不同的情况。比如有没有加@ResponseBody注解,如果方法返回一个String:

    1. 加了@ResponseBody注解:表示直接将这个String返回给浏览器
    2. 没有加@ResponseBody注解:表示应该根据这个String找到对应的页面,把页面返回给浏览器

    在SpringMVC中,会利用HandlerMethodReturnValueHandler来处理返回值:

    1. RequestResponseBodyMethodProcessor:处理加了@ResponseBody注解的情况
    2. ViewNameMethodReturnValueHandler:处理没有加@ResponseBody注解并且返回值类型为String的情况
    3. ModelMethodProcessor:处理返回值是Model类型的情况
    4. 还有很多其他的…

    我们这里只讲RequestResponseBodyMethodProcessor,因为它会处理加了@ResponseBody注解的情况,也是目前我们用得最多的情况。

    RequestResponseBodyMethodProcessor相当于会把方法返回的对象直接响应给浏览器,如果返回的是一个字符串,那么好说,直接把字符串响应给浏览器,那如果返回的是一个Map呢?是一个User对象呢?该怎么把这些复杂对象响应给浏览器呢?

    处理这块,SpringMVC会利用HttpMessageConverter来处理,比如默认情况下,SpringMVC会有4个HttpMessageConverter:

    1. ByteArrayHttpMessageConverter:处理返回值为字节数组的情况,把字节数组返回给浏览器
    2. StringHttpMessageConverter:处理返回值为字符串的情况,把字符串按指定的编码序列号后返回给浏览器
    3. SourceHttpMessageConverter:处理返回值为XML对象的情况,比如把DOMSource对象返回给浏览器
    4. AllEncompassingFormHttpMessageConverter:处理返回值为MultiValueMap对象的情况

    StringHttpMessageConverter的源码也比较简单:

    protected void writeInternal(String str, HttpOutputMessage outputMessage) throws IOException {
        HttpHeaders headers = outputMessage.getHeaders();
        if (this.writeAcceptCharset && headers.get(HttpHeaders.ACCEPT_CHARSET) == null) {
            headers.setAcceptCharset(getAcceptedCharsets());
        }
        Charset charset = getContentTypeCharset(headers.getContentType());
        StreamUtils.copy(str, charset, outputMessage.getBody());
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    先看有没有设置Content-Type,如果没有设置则取默认的,默认为ISO-8859-1,所以默认情况下返回中文会乱码,可以通过以下来中方式来解决:

    @ComponentScan("com.xxx")
    @Configuration
    @EnableWebMvc
    public class AppConfig implements WebMvcConfigurer {
        @Override
        public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
            StringHttpMessageConverter messageConverter = new StringHttpMessageConverter();
            messageConverter.setDefaultCharset(StandardCharsets.UTF_8);
            converters.add(messageConverter);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    不过以上四个Converter是不能处理Map对象或User对象的,所以如果返回的是Map或User对象,那么得单独配置一个Converter,比如MappingJackson2HttpMessageConverter,这个Converter比较强大,能把String、Map、User对象等等都能转化成JSON格式。

    @ComponentScan("com.xxx")
    @Configuration
    @EnableWebMvc
    public class AppConfig implements WebMvcConfigurer {
        @Override
        public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
            MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
            messageConverter.setDefaultCharset(StandardCharsets.UTF_8);
            converters.add(messageConverter);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    具体转化的逻辑就是Jackson2的转化逻辑。

    SpringMVC处理请求核心流程图

    https://www.processon.com/view/link/63f4cf1176e6143857799c2a

    SpringMVC父子容器

    我们可以在web.xml文件中这么来定义:

    <web-app>
    
        <listener>
            <listener-class>org.springframework.web.context.ContextLoaderListenerlistener-class>
        listener>
    
        <context-param>
            <param-name>contextConfigLocationparam-name>
            <param-value>/WEB-INF/spring.xmlparam-value>
        context-param>
    
        <servlet>
            <servlet-name>appservlet-name>
            <servlet-class>org.springframework.web.servlet.DispatcherServletservlet-class>
            <init-param>
                <param-name>contextConfigLocationparam-name>
                <param-value>/WEB-INF/spring-mvc.xmlparam-value>
            init-param>
            <load-on-startup>1load-on-startup>
        servlet>
    
        <servlet-mapping>
            <servlet-name>appservlet-name>
            <url-pattern>/app/*url-pattern>
        servlet-mapping>
    
    web-app>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    在这个web.xml文件中,我们定义了一个listener和servlet。

    父容器的创建

    ContextLoaderListener的作用是用来创建一个Spring容器,就是我们说的SpringMVC父子容器中的父容器,执行流程为:

    1. Tomcat启动,解析web.xml时
    2. 发现定义了一个ContextLoaderListener,Tomcat就会执行该listener中的contextInitialized()方法,该方法就会去创建要给Spring容器
    3. 从ServletContext中获取contextClass参数值,该参数表示所要创建的Spring容器的类型,可以在web.xml中通过来进行配置
    4. 如果没有配置该参数,那么则会从ContextLoader.properties文件中读取org.springframework.web.context.WebApplicationContext配置项的值,SpringMVC默认提供了一个ContextLoader.properties文件,内容为org.springframework.web.context.support.XmlWebApplicationContext
    5. 所以XmlWebApplicationContext就是要创建的Spring容器类型
    6. 确定好类型后,就用反射调用无参构造方法创建出来一个XmlWebApplicationContext对象
    7. 然后继续从ServletContext中获取contextConfigLocation参数的值,也就是一个spring配置文件的路径
    8. 把spring配置文件路径设置给Spring容器,然后调用refresh(),从而启动Spring容器,从而解析spring配置文件,从而扫描生成Bean对象等
    9. 这样Spring容器就创建出来了
    10. 有了Spring容器后,就会把XmlWebApplicationContext对象作为attribute设置到ServletContext中去,key为WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE
    11. 把Spring容器存到ServletContext中的原因,是为了给Servlet创建出来的子容器来作为父容器的

    子容器的创建

    Tomcat启动过程中,执行完ContextLoaderListener的contextInitialized()之后,就会创建DispatcherServlet了,web.xml中定义DispatcherServlet时,load-on-startup为1,表示在Tomcat启动过程中要把这个DispatcherServlet创建并初始化出来,而这个过程是比较费时间的,所以要把load-on-startup设置为1,如果不为1,会在servlet接收到请求时才来创建和初始化,这样会导致请求处理比较慢。

    1. Tomcat启动,解析web.xml时
    2. 创建DispatcherServlet对象
    3. 调用DispatcherServlet的init()
    4. 从而调用initServletBean()
    5. 从而调用initWebApplicationContext(),这个方法也会去创建一个Spring容器(就是子容器)
    6. initWebApplicationContext()执行过程中,会先从ServletContext拿出ContextLoaderListener所创建的Spring容器(父容器),记为rootContext
    7. 然后读取contextClass参数值,可以在servlet中的标签来定义想要创建的Spring容器类型,默认为XmlWebApplicationContext
    8. 然后创建一个Spring容器对象,也就是子容器
    9. 将rootContext作为parent设置给子容器(父子关系的绑定)
    10. 然后读取contextConfigLocation参数值,得到所配置的Spring配置文件路径
    11. 然后就是调用Spring容器的refresh()方法
    12. 从而完成了子容器的创建

    SpringMVC初始化

    子容器创建完后,还会调用一个DispatcherServlet的onRefresh()方法,这个方法会从Spring容器中获取一些特殊类型的Bean对象,并设置给DispatcherServlet对象中对应的属性,比如HandlerMapping、HandlerAdapter。

    流程为:

    1. 会先从Spring容器中获取HandlerMapping类型的Bean对象,如果不为空,那么就获取出来的Bean对象赋值给DispatcherServlet的handlerMappings属性
    2. 如果没有获取到,则会从DispatcherServlet.properties文件中读取配置,从而得到SpringMVC默认给我们配置的HandlerMapping

    DispatcherServlet.properties文件内容为:

    # Default implementation classes for DispatcherServlet's strategy interfaces. # Used as fallback when no matching beans are found in the DispatcherServlet context. # Not meant to be customized by application developers.  org.springframework.web.servlet.LocaleResolver=org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver  org.springframework.web.servlet.ThemeResolver=org.springframework.web.servlet.theme.FixedThemeResolver  org.springframework.web.servlet.HandlerMapping=org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping,\ org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping,\ org.springframework.web.servlet.function.support.RouterFunctionMapping  org.springframework.web.servlet.HandlerAdapter=org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter,\ org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter,\ org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter,\ org.springframework.web.servlet.function.support.HandlerFunctionAdapter   org.springframework.web.servlet.HandlerExceptionResolver=org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver,\ org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver,\ org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver  org.springframework.web.servlet.RequestToViewNameTranslator=org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator  org.springframework.web.servlet.ViewResolver=org.springframework.web.servlet.view.InternalResourceViewResolver  org.springframework.web.servlet.FlashMapManager=org.springframework.web.servlet.support.SessionFlashMapManager
    
    • 1

    默认提供了3个HandlerMapping,4个HandlerAdapter,这些概念在后续DispatcherServlet处理请求时都是会用到的。

    值得注意的是,从配置文件读出这些类后,是会利用Spring容器去创建出来对应的Bean对象,而不是一个普通的Java对象,而如果是Bean对象,那么就会触发Bean的初始化逻辑,比如RequestMappingHandlerAdapter,后续在分析请求处理逻辑时,会发现这个类是非常重要的,而它就实现了InitializingBean接口,从而Bean对象在创建时会执行afterPropertiesSet()方法。

    RequestMappingHandlerAdapter初始化

    我们先可以简单理解RequestMappingHandlerAdapter,它的作用就是在收到请求时来调用请求对应的方法的,所以它需要去解析方法参数,方法返回值。

    在RequestMappingHandlerAdapter的afterPropertiesSet()方法中,又会做以下事情(这些事情大家可能现在看不懂,可以后面回头再来看,我先列在这):

    1. 从Spring容器中找到加了@ControllerAdvice的Bean对象
      1. 解析出Bean对象中加了@ModelAttribute注解的Method对象,并存在modelAttributeAdviceCache这个Map中
      2. 解析出Bean对象中加了@InitBinder注解的Method对象,并存在initBinderAdviceCache这个Map中
      3. 如果Bean对象实现了RequestBodyAdvice接口或者ResponseBodyAdvice接口,那么就把这个Bean对象记录在requestResponseBodyAdvice集合中
    1. 从Spring容器中获取用户定义的HandlerMethodArgumentResolver,以及SpringMVC默认提供的,整合为一个HandlerMethodArgumentResolverComposite对象,HandlerMethodArgumentResolver是用来解析方法参数
    2. 从Spring容器中获取用户定义的HandlerMethodReturnValueHandler,以及SpringMVC默认提供的,整合为一个HandlerMethodReturnValueHandlerComposite对象,HandlerMethodReturnValueHandler是用来解析方法返回值

    以上是RequestMappingHandlerAdapter这个Bean的初始化逻辑。

    RequestMappingHandlerMapping初始化

    RequestMappingHandlerMapping的作用是,保存我们定义了哪些@RequestMapping方法及对应的访问路径,而RequestMappingHandlerMapping的初始化就是去找到这些映射关系:

    1. 找出容器中定义的所有的beanName
    2. 根据beanName找出beanType
    3. 判断beanType上是否有@Controller注解或@RequestMapping注解,如果有那么就表示这个Bean对象是一个Handler
    4. 如果是一个Handler,就通过反射找出加了@RequestMapping注解的Method,并解析@RequestMapping注解上定义的参数信息,得到一个对应的RequestMappingInfo对象,然后结合beanType上@RequestMapping注解所定义的path,以及当前Method上@RequestMapping注解所定义的path,进行整合,则得到了当前这个Method所对应的访问路径,并设置到RequestMappingInfo对象中去
    5. 所以,一个RequestMappingInfo对象就对应了一个加了@RequestMapping注解的Method,并且请求返回路径也记录在了RequestMappingInfo对象中
    6. 把当前Handler,也就是beanType中的所有RequestMappingInfo都找到后,就会存到MappingRegistry对象中
    7. 在存到MappingRegistry对象过程中,会像把Handler,也就是beanType,以及Method,生成一个HandlerMethod对象,其实就是表示一个方法
    8. 然后获取RequestMappingInfo对象中的path
    9. 把path和HandlerMethod对象存在一个Map中,属性叫做pathLookup
    10. 这样在处理请求时,就可以同请求路径找到HandlerMethod,然后找到Method,然后执行了

    WebApplicationInitializer的方式

    除开使用web.xml外,我们还可以直接定义一个WebApplicationInitializer来使用SpringMVC,比如:

    public class MyWebApplicationInitializer implements WebApplicationInitializer {
    
        @Override
        public void onStartup(ServletContext servletContext) {
    
            // Load Spring web application configuration
            AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
            context.register(AppConfig.class);
    
            // Create and register the DispatcherServlet
            DispatcherServlet servlet = new DispatcherServlet(context);
            ServletRegistration.Dynamic registration = servletContext.addServlet("app", servlet);
            registration.setLoadOnStartup(1);
            registration.addMapping("/*");
        }
    }
    
    @ComponentScan("com.xxx")
    @Configuration
    public class AppConfig  {
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    这种方法我们也能使用SpringMVC,流程为:

    1. Tomcat启动过程中就会调用到我们所写的onStartup()
    2. 从而创建一个Spring容器
    3. 从而创建一个DispatcherServlet对象并初始化
    4. 而DispatcherServlet初始化所做的事情和上述是一样的

    那为什么Tomcat启动时能调用到MyWebApplicationInitializer中的onStartup()呢?

    这个跟Tomcat的提供的扩展机制有关,在SpringMVC中有这样一个类:

    @HandlesTypes(WebApplicationInitializer.class)
    public class SpringServletContainerInitializer implements ServletContainerInitializer {
    
        @Override
        public void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)
                throws ServletException {
            // ...
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    这个类实现了javax.servlet.ServletContainerInitializer接口,并且在SpringMVC中还有这样一个文件:META-INF/services/Tomcatjavax.servlet.ServletContainerInitializer,文件内容为org.springframework.web.SpringServletContainerInitializer。

    很明显,是SPI,所以Tomcat在启动过程中会找到这个SpringServletContainerInitializer,并执行onStartup(),并且还会找到@HandlesTypes注解中所指定的WebApplicationInitializer接口的实现类,并传递给onStartup()方法,这其中就包括了我们自己定义的MyWebApplicationInitializer。

    在SpringServletContainerInitializer的onStartup()中就会调用MyWebApplicationInitializer的onStartup()方法了:

    @HandlesTypes(WebApplicationInitializer.class)
    public class SpringServletContainerInitializer implements ServletContainerInitializer {
        @Override
        public void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext) throws ServletException {
            List<WebApplicationInitializer> initializers = Collections.emptyList();
            if (webAppInitializerClasses != null) {
                initializers = new ArrayList<>(webAppInitializerClasses.size());
                for (Class<?> waiClass : webAppInitializerClasses) {
                    // 过滤掉接口、抽象类                 
                    if (!waiClass.isInterface() &&
                            !Modifier.isAbstract(waiClass.getModifiers()) && WebApplicationInitializer.class.isAssignableFrom(waiClass)) {
                        try {
                            // 实例化                         
                            initializers.add((WebApplicationInitializer) ReflectionUtils.accessibleConstructor(waiClass).newInstance());
                        } catch (Throwable ex) {
                            throw new ServletException("Failed to instantiate WebApplicationInitializer class", ex);
                        }
                    }
                }
            }
            if (initializers.isEmpty()) {
                servletContext.log("No Spring WebApplicationInitializer types detected on classpath");
                return;
            }
            servletContext.log(initializers.size() + " Spring WebApplicationInitializers detected on classpath");
            AnnotationAwareOrderComparator.sort(initializers);
            // 调用initializer.onStartup()         
            for (WebApplicationInitializer initializer : initializers) {
                initializer.onStartup(servletContext);
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    方法参数解析

    在RequestMappingHandlerAdapter的初始化逻辑中会设置一些默认的HandlerMethodArgumentResolver,他们就是用来解析各种类型的方法参数的。

    比如:

    1. RequestParamMethodArgumentResolver,用来解析加了@RequestParam注解的参数,或者什么都没加的基本类型参数(非基本类型的会被ServletModelAttributeMethodProcessor处理)
    2. PathVariableMethodArgumentResolver,用来解析加了@PathVariable注解的参数
    3. RequestHeaderMethodArgumentResolver,用来解析加了@RequestHeader注解的参数

    比如RequestParamMethodArgumentResolver中是这么处理的:

    protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception {
        HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class);
        // ...      
        if (arg == null) {
            String[] paramValues = request.getParameterValues(name);
            if (paramValues != null) {
                arg = (paramValues.length == 1 ? paramValues[0] : paramValues);
            }
        }
        return arg;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    很简单了,就是把请求中对应的parameterValue拿出来,最为参数值传递给方法。

    其他的类似,都是从请求中获取相对应的信息传递给参数。

    但是需要注意的是,我们从请求中获取的值可能很多时候都是字符串,那如果参数类型不是String,该怎么办呢?这就需要进行类型转换了,比如代码是这么写的:

    @RequestMapping(method = RequestMethod.GET, path = "/test") 
    @ResponseBody public String test(@RequestParam User user) {     	System.out.println(user.getName()); 
    return "hello xxx"; 
    }
    
    • 1
    • 2
    • 3
    • 4

    表示要获取请求中user对应的parameterValue,但是我们发请求时是这么发的:

    http://localhost:8080/tuling-web/app/test?user=xxx

    那么SpringMVC就需要将字符串zhouyu转换成为User对象,这就需要我们自定义类型转换器了,比如:

    public class StringToUserEditor extends PropertyEditorSupport {
        @Override
        public void setAsText(String text) throws IllegalArgumentException {
            User user = new User();
            user.setName(text);
            this.setValue(user);
        }
    }
    
    @InitBinder public void initBinder(WebDataBinder binder) {     binder.registerCustomEditor(User.class, new StringToUserEditor()); }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    Spring默认提供的Converter:org.springframework.core.convert.support.DefaultConversionService#addCollectionConverters

    MultipartFile解析

    文件上传代码如下:

    @RequestMapping(method = RequestMethod.POST, path = "/test") 
    @ResponseBody public String test(MultipartFile file) {     System.out.println(file.getName());     
    return "hello xxx"; }
    
    • 1
    • 2
    • 3

    要理解SpringMVC的文件上传,我们得先回头看看直接基于Servlet的文件上传,代码如下:

    @WebServlet(name = "uploadFileServlet", urlPatterns = "/uploadFile")
    @MultipartConfig
    public class UploadFileServlet extends HttpServlet {
        public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
            Collection<Part> parts = request.getParts();
            for (Part part : parts) {
                //content-disposition对于的内容为:form-data; name="file"; filename="xxx.xlsx"    
                String header = part.getHeader("content-disposition");
                String fileName = getFileName(header);
                if (fileName != null) {
                    part.write("D://upload" + File.separator + fileName);
                } else {
                    System.out.println(part.getName());
                }
            }
            response.setCharacterEncoding("utf-8");
            response.setContentType("text/html;charset=utf-8");
            PrintWriter out = response.getWriter();
            out.println("上传成功");
            out.flush();
            out.close();
        }
    
        public String getFileName(String header) {
            String[] arr = header.split(";");
            if (arr.length < 3) return null;
            String[] arr2 = arr[2].split("=");
            String fileName = arr2[1].substring(arr2[1].lastIndexOf("\\") + 1).replaceAll("\"", "");
            return fileName;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    可以看到第一行代码是:

    Collection parts = request.getParts();

    从request中拿到了一个Part集合,而这个集合中Part可以表示一个文件,也可以表示一个字符串。

    比如发送这么一个请求:

    在这里插入图片描述

    那么这个请求中就会有两个Part,一个Part表示文件,一个Part表示文本。

    有了这个知识点,我们再来看Controller中的代码:

    @RequestMapping(method = RequestMethod.POST, path = "/test") 
    @ResponseBody public String test(MultipartFile file, String test) {     System.out.println(file.getName());                                                       System.out.println(test);     
    return "hello xxx"; }
    
    • 1
    • 2
    • 3

    方法中的两个参数分别表示:

    1. file对应的是文件Part
    2. test对应的就是文本Part

    假如我请求是这么发的呢:

    在这里插入图片描述
    表达里面的test=tuling,请求parameter中的test=zhouyu,那最终test等于哪个呢?

    答案是两个:

    那如果我只想获取表达里的test呢?可以用@RequestPart注解:

    当接收到一个请求后:

    1. SpringMVC利用MultipartResolver来判断当前请求是不是一个multipart/form-data请求
    2. 如果是会把这个请求封装为StandardMultipartHttpServletRequest对象
    3. 并且获取请求中所有的Part,并且遍历每个Part
    4. 判断Part是文件还是文本
    5. 如果是文件,会把Part封装为一个StandardMultipartFile对象(实现了MultipartFile接口),并且会把StandardMultipartFile对象添加到multipartFiles中
    6. 如果是文本,会把Part的名字添加到multipartParameterNames中
    7. 然后在解析某个参数时
    8. 如果参数类型是MultipartFile,会根据参数名字从multipartFiles中获取出StandardMultipartFile对象,最终把这个对象传给方法

    方法返回值解析

    在RequestMappingHandlerAdapter的初始化逻辑中会设置一些默认的HandlerMethodReturnValueHandler,他们就是用来解析各种类型的方法返回值的。

    比如:

    1. ModelAndViewMethodReturnValueHandler,处理的就是返回值类为ModelAndView的情况
    2. RequestResponseBodyMethodProcessor,处理的就是方法上或类上加了@ResponseBody的情况
    3. ViewNameMethodReturnValueHandler,处理的就是返回值为字符串的请求(无@ResponseBody)

    我们重点看RequestResponseBodyMethodProcessor。

    假如代码如下:

    @Controller public class XController {      
        @RequestMapping(method = RequestMethod.GET, path = "/test")     
        @ResponseBody     
        public User test() {         
            User user = new User();         
            user.setName("xxx");         
            return user;     
        }  
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    方法返回的是User对象,那么怎么把这个User对象返回给浏览器来展示呢?那得看当前请求设置的Accept请求头,比如我用Chrome浏览器发送请求,默认给我设置的就是:Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,/;q=0.8,application/signed-exchange;v=b3;q=0.9

    表示当前这个请求接收的内容格式,比如html格式、xml格式、各种图片格式等等。

    如果我们的方法返回的是一个字符串,那么就对应html格式,就没问题,而如果我们不是返回的字符串,那我们就转成字符串,通常就是JSON格式的字符串。

    所以,我们需要将User对象转换成JSON字符串,默认SpringMVC是不能转换的,此时请求会报错:

    在这里插入图片描述

    而要完成这件事情,我们需要添加一个MappingJackson2HttpMessageConverter,通过它就能把User对象或者Map对象等转成一个JSON字符串。

    XML的添加方式:

    mvc:annotation-driven mvc:message-converters

    记得要引入Jackson2的依赖:

    com.fasterxml.jackson.core jackson-databind 2.13.2

    我们看一下MappingJackson2HttpMessageConverter的构造方法:

    public MappingJackson2HttpMessageConverter() {     this(Jackson2ObjectMapperBuilder.json().build()); }   
    public MappingJackson2HttpMessageConverter(ObjectMapper objectMapper) {     super(objectMapper, MediaType.APPLICATION_JSON, new MediaType("application", "*+json")); }
    
    • 1
    • 2

    表示MappingJackson2HttpMessageConverter支持的MediaType为"application/json"、“application/*+json”。

    所以如果我们明确指定方法返回的MediaType为"text/plain",那么MappingJackson2HttpMessageConverter就不能处理了,比如:

    @RequestMapping(method = RequestMethod.GET, path = "/test", produces = "text/plain") @ResponseBody public User test() {     
        User user = new User();     
        user.setName("xxx");     
        return user; }
    
    • 1
    • 2
    • 3
    • 4

    以上代码表示,需要把一个User对象转成一个纯文本字符串,默认是没有这种转换器的。

    一个HttpMessageConverter中有一个canWrite()方法,表示这个HttpMessageConverter能把什么类型转成什么MediaType返回给浏览器。

    比如SpringMVC自带一个StringHttpMessageConverter,它能够把一个String对象返回给浏览器,支持所有的MediaType。

    那为了支持把User对象转成纯文本,我们可以自定义ZhouyuHttpMessageConverter:

    public class XHttpMessageConverter extends AbstractHttpMessageConverter<User> {
        @Override
        public List<MediaType> getSupportedMediaTypes() {
            ArrayList<MediaType> mediaTypes = new ArrayList<>();
            mediaTypes.add(MediaType.ALL);
            return mediaTypes;
        }
    
        @Override
        protected boolean supports(Class clazz) {
            return User.class == clazz;
        }
    
        @Override
        protected User readInternal(Class<? extends User> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
            return null;
        }
    
        @Override
        protected void writeInternal(User user, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
            StreamUtils.copy(user.getName(), Charset.defaultCharset(), outputMessage.getBody());
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    我定义的这个HttpMessageConverter就能够把User对象转成纯文本。

    拦截器解析

    我们可以使用HandlerInterceptor来拦截请求:

    package org.springframework.web.servlet;  
    import javax.servlet.http.HttpServletRequest; 
    import javax.servlet.http.HttpServletResponse;  
    import org.springframework.lang.Nullable; 
    import org.springframework.web.method.HandlerMethod;  
    public interface HandlerInterceptor {      
        default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)  throws Exception {          
            return true;     
        }       
        default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,   @Nullable ModelAndView modelAndView) throws Exception { 
        }           
        default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,  @Nullable Exception ex) throws Exception { 
        }  
    } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    具体执行顺序看下图:

    https://www.processon.com/view/link/63e9f3e6234df52a1e9303fb

    @EnableWebMvc解析

    @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Documented @Import(DelegatingWebMvcConfiguration.class) public @interface EnableWebMvc { }

    导入了一个DelegatingWebMvcConfiguration配置类,这个配置类定义了很多个Bean,比如RequestMappingHandlerMapping,后续在创建RequestMappingHandlerMapping这个Bean对象时,会调用DelegatingWebMvcConfiguration的getInterceptors()方法来获取拦截器:

    @Bean
    @SuppressWarnings("deprecation")
    public RequestMappingHandlerMapping requestMappingHandlerMapping(...) {
        RequestMappingHandlerMapping mapping = createRequestMappingHandlerMapping();
        mapping.setInterceptors(getInterceptors(conversionService, resourceUrlProvider));
        // ...     
        return mapping;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    而在getInterceptors()方法中会调用addInterceptors()方法,从而会调用WebMvcConfigurerComposite的addInterceptors()方法,然后会遍历调用WebMvcConfigurer的addInterceptors()方法来添加拦截器:

    public void addInterceptors(InterceptorRegistry registry) {     
        for (WebMvcConfigurer delegate : this.delegates) {  
            delegate.addInterceptors(registry);     
        } 
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    那么delegates集合中的值是哪来的呢?在DelegatingWebMvcConfiguration中进行了一次set注入:

    @Autowired(required = false)
    public void setConfigurers(List<WebMvcConfigurer> configurers) {
        if (!CollectionUtils.isEmpty(configurers)) {
            this.configurers.addWebMvcConfigurers(configurers);
        }
    }
    
    public void addWebMvcConfigurers(List<WebMvcConfigurer> configurers) {
        if (!CollectionUtils.isEmpty(configurers)) {
            this.delegates.addAll(configurers);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    所以就是把Spring容器中的WebMvcConfigurer的Bean添加到了delegates集合中。

    所以,我们可以配置WebMvcConfigurer类型的Bean,并通过addInterceptors()方法来给SpringMvc添加拦截器。

    同理我们可以利用WebMvcConfigurer中的其他方法来对SpringMvc进行配置,比如

    @ComponentScan("com.xxx")
    @Configuration
    @EnableWebMvc
    public class AppConfig implements WebMvcConfigurer {
        @Override
        public void configurePathMatch(PathMatchConfigurer configurer) {
            configurer.addPathPrefix("/xxx", t -> t.equals(ZhouyuController.class));
        }
    
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    所以@EnableWebMvc的作用是提供了可以让程序员通过定义WebMvcConfigurer类型的Bean来对SpringMVC进行配置的功能。

    另外值得注意的是,如果加了@EnableWebMvc注解,那么Spring容器中会有三个HandlerMapping类型的Bean:

    1. RequestMappingHandlerMapping
    2. BeanNameUrlHandlerMapping
    3. RouterFunctionMapping

    如果没有加@EnableWebMvc注解,那么Spring容器中默认也会有三个HandlerMapping类型的Bean:

    1. BeanNameUrlHandlerMapping
    2. RequestMappingHandlerMapping
    3. RouterFunctionMapping

    就顺序不一样而已,源码中是根据DispatcherServlet.properties文件来配置有哪些HandlerMapping的。

    private void initHandlerMappings(ApplicationContext context) {
        this.handlerMappings = null;
        // 默认为true,获取HandlerMapping类型的Bean         
        if (this.detectAllHandlerMappings) {
            // Find all HandlerMappings in the ApplicationContext, including ancestor contexts.             
            Map<String, HandlerMapping> matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);
            if (!matchingBeans.isEmpty()) {
                this.handlerMappings = new ArrayList<>(matchingBeans.values());
                // We keep HandlerMappings in sorted order.                 
                AnnotationAwareOrderComparator.sort(this.handlerMappings);
            }
        }
        // 获取名字叫handlerMapping的Bean         
        else {
            try {
                HandlerMapping hm = context.getBean(HANDLER_MAPPING_BEAN_NAME, HandlerMapping.class);
                this.handlerMappings = Collections.singletonList(hm);
            } catch (NoSuchBeanDefinitionException ex) {
                // Ignore, we'll add a default HandlerMapping later.         
            }
        }
        // 如果从Spring容器中没有找到HandlerMapping类型的Bean         
        // 就根据DispatcherServlet.properties配置来创建HandlerMapping类型的Bean         
        // 默认就有这么一个文件,会创建出来三个HandlerMapping的Bean        
        if (this.handlerMappings == null) {
            this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class);
            if (logger.isTraceEnabled()) {
                logger.trace("No HandlerMappings declared for servlet '" + getServletName() + "': using default strategies from DispatcherServlet.properties");
            }
        }
        for (HandlerMapping mapping : this.handlerMappings) {
            if (mapping.usesPathPatterns()) {
                this.parseRequestPath = true;
                break;
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    由于加和不加@EnableWebMvc注解之后的HandlerMapping顺序不一样,可能会导致一些问题(工作中很难遇到):

    @Component("/test") public class BeanNameUrlController implements Controller {     		
        @Override     
        public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {         
            System.out.println("BeanNameUrlController");        
            return null;    
        } 
    }
    
    @RestController public class XController {      
        @GetMapping("/test")     
        public String test() {         
            System.out.println("XController");         
            return null;     
        }  
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    这两个Controller访问路径是一样的,但是负责处理的HandlerMapping是不一样的,

    1. BeanNameUrlController对应的是BeanNameUrlHandlerMapping
    2. XController对应的是RequestMappingHandlerMapping

    如果加了@EnableWebMvc注解,顺序为:

    1. RequestMappingHandlerMapping
    2. BeanNameUrlHandlerMapping
    3. RouterFunctionMapping

    会先由RequestMappingHandlerMapping处理/test请求,最终执行的是ZhouyuController中的test

    如果没有加@EnableWebMvc注解,顺序为:

    1. BeanNameUrlHandlerMapping
    2. RequestMappingHandlerMapping
    3. RouterFunctionMapping

    会先由BeanNameUrlHandlerMapping处理/test请求,最终执行的是BeanNameUrlController中的test

    注意,一个HandlerMapping处理完请求后就不会再让其他HandlerMapping来处理请求了。

  • 相关阅读:
    学会使用ECharts
    FPGA之旅设计99例之第十四例-----接收红外遥控数据
    Centos7 系统开通后修改数据盘挂载目录
    Apollo规划代码ros移植-动态障碍物处理(一)
    spring 源码编码若干问题
    Android---touch 事件分发
    2023年R1快开门式压力容器操作证模拟考试题库及R1快开门式压力容器操作理论考试试题
    移动安全实战分享
    阿里云效和阿里在线idea使用
    坚持三月,刷完了阿里P8技术官整理的这3份1000道Java高频面试题笔记,成功上岸阿里P7职位
  • 原文地址:https://blog.csdn.net/xwj1992930/article/details/134393233