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