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

posted @ 2022-01-19 20:22  江湖小小白  阅读(745)  评论(0编辑  收藏  举报