• Spring MVC @EnableWebMvc 流程


    接上篇:https://www.cnblogs.com/jhxxb/p/13598074.html

    @EnableWebMvc

    使用 @EnableWebMvc 和不使用它有一个非常非常重要的区别:

    使用 @EnableWebMvc 原来是依托于这个 WebMvcConfigurationSupport 配置类向容器中注入了对应的 Bean,所以它们都是交给了 Spring 管理的(所以可以 @Autowired 它们)

    但是,但是,但是(重说三),若是走了 Spring 它自己去读取配置文件走默认值,它的 Bean 是没有交给 Spring 管理的,没有交给 Spring 管理的

    它是这样创建的:context.getAutowireCapableBeanFactory().createBean(clazz),它创建出来的 Bean 都不会交给 Spring 管理

    注意 CreateBean 和 CrateBean 的不同:https://blog.csdn.net/f641385712/article/details/88651128

    源码

    从 initStrategies 方法开始

    public class DispatcherServlet extends FrameworkServlet {
        /**
         * HttpServletBean 直接继承自 java 的 HttpServlet,其作用是将 Servlet 中配置的参数设置到相应的 Bean 属性上
         * FrameworkServlet 直接继承自 HttpServletBean,初始化了 WebApplicationContext
         * DispatcherServlet 直接继承自 FrameworkServlet,初始化了自身的 9 个组件
         */
        @Override
        protected void onRefresh(ApplicationContext context) {
            initStrategies(context);
        }
    
        /**
         * 子类若有需要,可以复写此方法,去初始化自己的其余组件(比如要和它集成等等)
         */
        protected void initStrategies(ApplicationContext context) {
            initMultipartResolver(context);
            initLocaleResolver(context);
            initThemeResolver(context);
    
            // 下面是复数,有 s
            initHandlerMappings(context);
            initHandlerAdapters(context);
            initHandlerExceptionResolvers(context);
            initRequestToViewNameTranslator(context);
            initViewResolvers(context);
            initFlashMapManager(context);
        }

    九大组件初始化

    文件上传

    public class DispatcherServlet extends FrameworkServlet {
        private void initMultipartResolver(ApplicationContext context) {
            try {
                // 若我们向容器里配置了此 Bean 就有,否则默认是不支持文件上传的
                // 备注:注意配置这些配型 Bean 的名称,都是有固定值的,必须保证一样,否则配置将不生效。下同
                this.multipartResolver = context.getBean(MULTIPART_RESOLVER_BEAN_NAME, MultipartResolver.class);
                if (logger.isTraceEnabled()) {
                    logger.trace("Detected " + this.multipartResolver);
                } else if (logger.isDebugEnabled()) {
                    logger.debug("Detected " + this.multipartResolver.getClass().getSimpleName());
                }
            } catch (NoSuchBeanDefinitionException ex) {
                // Default is no multipart resolver.
                this.multipartResolver = null;
                if (logger.isTraceEnabled()) {
                    logger.trace("No MultipartResolver '" + MULTIPART_RESOLVER_BEAN_NAME + "' declared");
                }
            }
        }

    看 MultipartResolver 接口

    public interface MultipartResolver {
        /**
         * 当收到请求时 DispatcherServlet#checkMultipart() 方法会调用 MultipartResolver#isMultipart() 方法判断请求中是否包含文件。
         * 如果请求数据中包含文件,则调用 MultipartResolver#resolveMultipart() 方法对请求的数据进行解析。
         * 然后将文件数据解析成 MultipartFile 并封装在 MultipartHttpServletRequest(继承了 HttpServletRequest) 对象中,最后传递给 Controller
         */
        boolean isMultipart(HttpServletRequest request);
        MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) throws MultipartException;
        void cleanupMultipart(MultipartHttpServletRequest request);
    }

    CommonsMultipartResolver 使用 commons Fileupload 来处理 multipart 请求,所以在使用时,必须要引入相应的 jar 包

    StandardServletMultipartResolver 是基于 Servlet 3.0来处理 multipart 请求的(基于request.getParts()方法),使用支持 Servlet 3.0 的容器

    不一样的是,配置 StandardServletMultipartResolver 这个 Bean 的时候,它的初始化参数都在 web.xml 的 <multipart-config> 里面配置

    <servlet>
        <servlet-name>dispatcherServlet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:springmvc.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
        <multipart-config>
            <location>D:/</location>
            <!--上传文件最大 2M-->
            <max-file-size>2097152</max-file-size>
            <!--整个请求上传文件最大 4M-->
            <max-request-size>4194304</max-request-size>
        </multipart-config>
    </servlet>
    <servlet-mapping>
        <servlet-name>dispatcherServlet</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

    LocaleResolver

    public class DispatcherServlet extends FrameworkServlet {
        private void initLocaleResolver(ApplicationContext context) {
            try {
                // 若自己没有配置 LocaleResolver,会调用 getDefaultStrategy 去获取默认的处理器:
                // 默认处理器在 DispatcherServlet.properties 这个文件里配置了
                this.localeResolver = context.getBean(LOCALE_RESOLVER_BEAN_NAME, LocaleResolver.class);
                if (logger.isTraceEnabled()) {
                    logger.trace("Detected " + this.localeResolver);
                } else if (logger.isDebugEnabled()) {
                    logger.debug("Detected " + this.localeResolver.getClass().getSimpleName());
                }
            } catch (NoSuchBeanDefinitionException ex) {
                // We need to use the default.
                this.localeResolver = getDefaultStrategy(context, LocaleResolver.class);
                if (logger.isTraceEnabled()) {
                    logger.trace("No LocaleResolver '" + LOCALE_RESOLVER_BEAN_NAME + "': using default [" + this.localeResolver.getClass().getSimpleName() + "]");
                }
            }
        }

    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

    看 LocaleResolver 接口

    public interface LocaleResolver {
        /**
         * 根据 request 对象根据指定的方式获取一个 Locale,如果没有获取到,则使用用户指定的默认 Locale
         */
        Locale resolveLocale(HttpServletRequest request);
    
        /**
         * 用于实现 Locale 的切换。比如 SessionLocaleResolver 获取 Locale 的方式是从 session 中读取,但如果用
         * 户想要切换其展示的样式(由英文切换为中文),那么这里的 setLocale() 方法就提供了这样一种可能
         */
        void setLocale(HttpServletRequest request, @Nullable HttpServletResponse response, @Nullable Locale locale);
    }

    对于 LocaleResolver,其主要作用在于根据不同的用户区域展示不同的视图,而用户的区域也称为 Locale,该信息是可以由前端直接获取的。通过这种方式,可以实现一种国际化的目的,比如中国一个视图,外国一个视图。

    解析视图需要两个参数:一是视图名,另一个是 Locale。视图名是处理器返回的,Locale 是从哪里来的?这就是 LocaleResolver 要做的事

    • FixedLocaleResolver:在声明该 resolver 时,需要指定一个默认的 Locale,在进行 Locale 获取时,始终返回该 Locale,并且调用其 setLocale() 方法也无法改变其 Locale。
    • CookieLocaleResolver:读取 Locale 的方式是在 session 中通过 Cookie 来获取其指定的 Locale,如果修改了 Cookie 的值,页面视图也会同步切换。
    • SessionLocaleResolver:会将 Locale 信息存储在 session 中,如果用户想要修改 Locale 信息,只要修改 session 中对应属性的值即可。
    • AcceptHeaderLocaleResolver:其会通过用户请求中名称为 Accept-Language 的 header 来获取 Locale 信息,如果想要修改展示的视图,只需要修改该 header 信息即可。

    对于 Locale 的切换,Spring 是通过拦截器来实现的,其提供了一个 LocaleChangeInterceptor,若要生效,这个 Bean 需要自己配置

    ThemeResolver 主题

    public class DispatcherServlet extends FrameworkServlet {
        private void initThemeResolver(ApplicationContext context) {
            try {
                this.themeResolver = context.getBean(THEME_RESOLVER_BEAN_NAME, ThemeResolver.class);
                if (logger.isTraceEnabled()) {
                    logger.trace("Detected " + this.themeResolver);
                } else if (logger.isDebugEnabled()) {
                    logger.debug("Detected " + this.themeResolver.getClass().getSimpleName());
                }
            } catch (NoSuchBeanDefinitionException ex) {
                // We need to use the default.
                this.themeResolver = getDefaultStrategy(context, ThemeResolver.class);
                if (logger.isTraceEnabled()) {
                    logger.trace("No ThemeResolver '" + THEME_RESOLVER_BEAN_NAME + "': using default [" + this.themeResolver.getClass().getSimpleName() + "]");
                }
            }
        }

    ThemeResolver 接口

    public interface ThemeResolver {
        String resolveThemeName(HttpServletRequest request);
        void setThemeName(HttpServletRequest request, @Nullable HttpServletResponse response, @Nullable String themeName);
    }

     

    主题就是系统的整体样式或风格,可通过 Spring MVC 框架提供的主题(theme)设置应用的整体样式风格,提高用户体验。Spring MVC 的主题就是一些静态资源的集合,即包括样式及图片,用来控制应用的视觉风格。

    SpringMVC 中一个主题对应一个 properties 文件,里面存放着跟当前主题相关的所有资源、如图片、css 样式等。

    主题使用得太少了,特别现在前后端分离了。

    HandlerMapping

    public class DispatcherServlet extends FrameworkServlet {
        // DispatcherServlet 初始化 HandlerMappings
        // Spring 中的 DispatcherServlet 允许有多个
        // 默认情况下 @RequestMapping 和 BeanNameUrl 的方式都是被支持的
        private void initHandlerMappings(ApplicationContext context) {
            this.handlerMappings = null;
    
            // detectAllHandlerMappings 该属性默认为 true,表示会去容器内找所有的 HandlerMapping 类型的定义信息
            // 若想改为 false,可以调用它的 setDetectAllHandlerMappings() 自行设置(绝大部分情况没必要)
            if (this.detectAllHandlerMappings) {
                // 这里注意:若你没有标注注解`@EnableWebMvc`,那么这里找的结果是空的
                // 若你标注了此注解,这个注解就会默认向容器内注入两个 HandlerMapping:RequestMappingHandlerMapping 和 BeanNameUrlHandlerMapping
                Map<String, HandlerMapping> matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);
                if (!matchingBeans.isEmpty()) {
                    this.handlerMappings = new ArrayList<>(matchingBeans.values());
                    // 多个的话还需要进行一次排序
                    AnnotationAwareOrderComparator.sort(this.handlerMappings);
                }
            } else {
                // 不全部查找,那就只找一个名字为`handlerMapping`的 HandlerMapping 实现精准控制
                // 绝大多数情况下,我们并不需要这么做
                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 的兜底策略,它会向容器注册两个:RequestMappingHandlerMapping 和 BeanNameUrlHandlerMapping
            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");
                }
            }
        }

    用来查找 Handler,在 SpringMVC 中会有很多请求,每个请求都需要一个 Handler 处理,具体接收到一个请求之后使用哪个 Handler 进行处理?这就是 HandlerMapping 需要做的事

    作用是根据当前请求的找到对应的 Handler,并将 Handler(执行程序)与一堆 HandlerInterceptor(拦截器,也是它来处理的)封装到 HandlerExecutionChain 对象中。返回给中央调度器

    HandlerMapping 可以有多个,开启了 @EnableMvc 注解后,就不读取 DispatcherServlet.properties 中的默认值了

    HandlerAdapter

    public interface HandlerAdapter {
        /**
         * 当前 HandlerAdapter 是否支持这个 Handler
         */
        boolean supports(Object handler);
    
        /**
         * 调用 handle 处理这个请求,然后返回 ModelAndView 对象
         */
        @Nullable
        ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception;
        long getLastModified(HttpServletRequest request, Object handler);
    }

    因为 SpringMVC 中的 Handler 可以是任意的形式,只要能处理请求就 ok,但是 Servlet 需要的处理方法的结构却是固定的,都是以 request 和 response 为参数的方法。如何让固定的 Servlet 处理方法调用灵活的 Handler 来进行处理?这就是 HandlerAdapter 要做的事情。

    Handler 是用来干活的工具,HandlerMapping 用于根据需要干的活找到相应的工具,HandlerAdapter 是使用工具干活的人

    HandlerAdapter 可以有多个

    HandlerExceptionResolver

    其它组件都是用来干活的。在干活的过程中难免会出现问题,出问题后怎么办呢?这就需要有一个专门的角色对异常情况进行处理,在 SpringMVC 中就是 HandlerExceptionResolver。具体来说,此组件的作用是根据异常设置 ModelAndView,之后再交给 render 方法进行渲染。

    以前我们可以用 web.xml 的 <error-page> 标签来捕获状态码 500、400 的异常,但是这个已经 out 了,现在全局的异常都可以交给 HandlerExceptionResolver 去捕获处理

    这个接口捕获的是所有异常,而 Spring 官方推荐的是使用 @ExceptionHandler 注解去捕获固定的异常

    这个类建议交给 Spring 子容器管理(可以多实现),因为它就像一个特殊的 Controller

    RequestToViewNameTranslator

    Spring MVC 是通过 ViewName 来找到对应的视图的,而此接口的作用就是从 request 中获取 viewName。

    public interface RequestToViewNameTranslator {
        @Nullable
        String getViewName(HttpServletRequest request) throws Exception;
    }

    只有一个默认实现

    public class DefaultRequestToViewNameTranslator implements RequestToViewNameTranslator {
        private static final String SLASH = "/";
    
        private String prefix = "";
    
        private String suffix = "";
    
        private String separator = SLASH;
    
        private boolean stripLeadingSlash = true;
    
        private boolean stripTrailingSlash = true;
    
        private boolean stripExtension = true;
    
        private UrlPathHelper urlPathHelper = new UrlPathHelper();
    
        @Override
        public String getViewName(HttpServletRequest request) {
            // 调用 UrlPathHelper 的 getLookupPathForRequest 方法获取一个 looup 路径
            String lookupPath = this.urlPathHelper.getLookupPathForRequest(request, HandlerMapping.LOOKUP_PATH);
            return (this.prefix + transformPath(lookupPath) + this.suffix);
        }
    
        @Nullable
        protected String transformPath(String lookupPath) { // 对获取的路径字符串再做个简单处理
            String path = lookupPath;
            if (this.stripLeadingSlash && path.startsWith(SLASH)) {
                path = path.substring(1);
            }
            if (this.stripTrailingSlash && path.endsWith(SLASH)) {
                path = path.substring(0, path.length() - 1);
            }
            if (this.stripExtension) {
                path = StringUtils.stripFilenameExtension(path);
            }
            if (!SLASH.equals(this.separator)) {
                path = StringUtils.replace(path, SLASH, this.separator);
            }
            return path;
        }

    ViewResolver

    public class DispatcherServlet extends FrameworkServlet {
        private void initViewResolvers(ApplicationContext context) {
            this.viewResolvers = null;
    
            // detectAllViewResolvers 默认为 true,会去容器里找到所有的视图解析器的 Bean。我们可以通过 init-param 配置为 false,来关闭这个(不建议)
            // 另外,需要注意的是,我们发现虽然我们没有自己注册 Bean 进去,但是在 matchingBeans 这一步时,已经有值了,怎么回事呢?
            // 继续扣源码发现:当有 @EnableWebMvc 这个注解时,会导入 DelegatingWebMvcConfiguration,而它是 WebMvcConfigurationSupport 的子类,
            // 而 WebMvcConfigurationSupport 它默认配置注册了很多东西到 MVC 的配置中,所以我们才会发现 matchingBeans 有值了。
            // 去掉 @EnableWebMvc 后,从容器里就拿不出 Bean 了,只能读取配置文件里的默认值了
            if (this.detectAllViewResolvers) {
                // 如果detectAllViewResolvers为true,那么就会去容器里找所有的(包含所有祖先上下文)容器里的所有的此接口下的此类的bean,最后都放进去(可以有多个嘛)
                Map<String, ViewResolver> matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(context, ViewResolver.class, true, false);
                if (!matchingBeans.isEmpty()) {
                    this.viewResolvers = new ArrayList<>(matchingBeans.values());
                    // 保持排序性
                    AnnotationAwareOrderComparator.sort(this.viewResolvers);
                }
            } else {
                try {
                    ViewResolver vr = context.getBean(VIEW_RESOLVER_BEAN_NAME, ViewResolver.class);
                    this.viewResolvers = Collections.singletonList(vr); // 使用 singletonList 是为了性能考虑,节约内存
                } catch (NoSuchBeanDefinitionException ex) {
                    // Ignore, we'll add a default ViewResolver later.
                }
            }
    
            // 若还为 null,就采用默认配置的视图解析器 InternalResourceViewResolver
            if (this.viewResolvers == null) {
                this.viewResolvers = getDefaultStrategies(context, ViewResolver.class);
                if (logger.isTraceEnabled()) {
                    logger.trace("No ViewResolvers declared for servlet '" + getServletName() + "': using default strategies from DispatcherServlet.properties");
                }
            }
        }

    用来将 String 类型的视图名和 Locale 解析为 View 类型的视图。View 是用来渲染页面的,也就是将程序返回的参数填入模板里,生成 html(也可能是其它类型)文件。这里就有两个关键问题:使用哪个模板?用什么技术(规则)填入参数?这其实是 ViewResolver 主要要做的工作,ViewResolver 需要找到渲染所用的模板和所用的技术(也就是视图的类型)进行渲染,具体的渲染过程则交由不同的视图自己完成。

    public interface ViewResolver {
        @Nullable
        View resolveViewName(String viewName, Locale locale) throws Exception;
    }

    • AbstractCachingViewResolver:基于缓存的抽象视图解析器
    • UrlBasedViewResolver:实现了缓存 提供了prefix、suffix 拼接的 url 视图解析器
    • InternalResourceViewResolver:基于url 的内部资源视图解析器
    • XmlViewResolver:基于 xml 的缓存视图解析器
    • BeanNameViewResolver:beanName 来自容器,并且不支持缓存
    • ResourceBundleViewResolver:这个有点复杂
    • FreeMarkerViewResolver:基于 url,但会解析成特定的 view,实现类也非常的多,在 Spring MVC 里是一个非常重要的概念(比如什么时候返回页面,什么时候返回 JSON)
    • ViewResolverComposite 简单来说就是使用简单的 List 来保存你配置使用的视图解析器

    ViewResolvers 可以有多个

    FlashMapManager

    用来管理 FlashMap 的,FlashMap 主要用在 redirect 中传递参数。

    抽象类采用模板模式定义整个流程,具体实现类用 SessionFlashMapManager 通过模板方法提供了具体操作 FlashMap 的功能。

    功能说明:

    • 实际的 Session 中保存的 FlashMap 是 List 类型,也就是说一个 Session 可以保存多个 FlashMap,一个 FlashMap 保存着一套 Redirect 转发所传递的参数
    • FlashMap 继承自 HashMap,除了用于 HashMap 的功能和设置有效期,还可以保存 Redirect 后的目标路径和通过 url 传递的参数,这两项内容主要用来从 Session 保存的多个 FlashMap 中查找当前的 FalshMap

    https://blog.csdn.net/f641385712/article/details/87934909

    https://www.bilibili.com/video/BV19K4y1L7MT

  • 相关阅读:
    PHP 产生唯一码的方法分析
    Nginx 缓存cache的5种方案
    Nginx 常见应用技术指南
    BigPipe 技术细节分析
    Nginx 配置负载均衡
    linux下调整java版本
    跨域cookie在IE与firefox下的不同
    css2.1中 firefox 与IE 对margintop的不同解释
    ADOQuery代替ClientDataSet做3Tier系统
    查询数据库中的表建个进度条
  • 原文地址:https://www.cnblogs.com/jhxxb/p/14150050.html
Copyright © 2020-2023  润新知