SpringMVC源码分析(一)启动流程分析
a、SpringMVC 在启动过程中主要做了什么事情?
SpringMVC在启动过程中是什么时候解析web.xml文件的,又是什么时候初始化9大内置对象的?
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version="4.0"><!--Spring配置文件--><context-param><param-name>contextConfigLocation</param-name><param-value>classpath:applicationContext.xml</param-value></context-param><!--Spring监听器--><listener><listener-class> org.springframework.web.context.ContextLoaderListener </listener-class></listener><!--SpringMVC前端控制器--><servlet><servlet-name> SpringMVC </servlet-name><servlet-class> org.springframework.web.servlet.DispatcherServlet </servlet-class><!-- 配置springMVC需要加载的配置文件--><init-param><param-name>contextConfigLocation</param-name><param-value>classpath:spring/springMVC.xml</param-value></init-param><load-on-startup>1</load-on-startup><async-supported>true</async-supported></servlet><servlet-mapping><servlet-name>SpringMVC</servlet-name><url-pattern>/</url-pattern></servlet-mapping></web-app>
b、父子容器的概念
在Spring MVC中,有两个容器:父容器(Root WebApplicationContext)和子容器(Servlet WebApplicationContext)。
父容器(Root WebApplicationContext)
主要负责加载应用程序中的共享bean和配置。它通常包含以下内容:
- 数据源和事务管理器:用于处理数据库操作和事务管理。
- 持久化层(DAO)和业务层(Service)的bean:用于处理数据访问和业务逻辑。
- 安全配置:用于配置安全相关的bean,如认证和授权配置。
- 缓存配置:用于配置缓存相关的bean,如缓存管理器和缓存注解处理器。
- 其他共享bean:例如,公共工具类、全局异常处理器等。
子容器(Servlet WebApplicationContext)
主要负责加载与Web应用程序相关的bean和配置。它通常包含以下内容:
- 控制器(Controller)和视图解析器(ViewResolver):用于处理请求和生成响应。
- Web相关的bean:例如,处理文件上传的MultipartResolver、处理静态资源的ResourceHandler等。
- Web安全配置:用于配置Web安全相关的bean,如Spring Security配置。
- 其他与Web应用程序相关的bean:例如,国际化资源处理器、消息转换器等。
Spring本身的ApplicationContext属于父容器(Root WebApplicationContext),它通常包含整个应用程序范围内的bean和配置。这包括父容器中的共享bean,以及子容器中的bean。在Spring MVC中,通常使用ContextLoaderListener来加载父容器的ApplicationContext。
总结起来,父容器(Root WebApplicationContext)包含应用程序范围内的共享bean和配置,子容器(Servlet WebApplicationContext)包含与Web应用程序相关的bean和配置,而Spring本身的ApplicationContext属于父容器,包含整个应用程序的bean和配置。
Spring 父子容器是指 Spring 容器的层次结构,其中一个容器作为另一个容器的父容器。父容器可以提供资源和 bean 给子容器,子容器可以覆盖或扩展父容器的 bean 定义。
这种父子关系在 Spring Web 应用中非常常见。通常情况下,在 Web 应用中会有两个 Spring 容器:Root WebApplicationContext 和 Servlet WebApplicationContext。
c、启动过程中两个关键类
ContextLoaderListener和DispatcherServlet
Tomcat启动时候会先创建servlet容器,然后解析web.xml。
1、首先加载conf/web.xml文件,然后加载web应用程序中的WEB-INF/web.xml文件。
2、按照以下顺序读取和处理web.xml中的元素:context-param -> listener -> filter -> servlet。
context-param元素用于向ServletContext提供键值对,即应用程序上下文信息,可以写在任意位置。
servlet元素用于定义servlet的名字和类,servlet-mapping元素用于指定访问servlet的URL,servlet-mapping必须出现在servlet之后,servlet的初始化顺序按照load-on-startup元素指定的值,如果值为正数或零,则按照从小到大的顺序初始化,如果值为负数或未定义,则在第一次请求时初始化。
先解析context-param,然后创建ServletContext对象,并将context-param转换为键值对交给ServletContext,然后再解析listener-class,并创建监听器实例。如果监听器类实现了ServletContextListener接口,那么它的contextInitialized(ServletContextEvent sce)方法和contextDestroyed(ServletContextEvent sce)方法会在ServletContext对象创建和销毁时被调用,这两个方法的参数sce可以通过getServletContext()方法获取ServletContext对象。
1、ContextLoaderListener
ContextLoaderListener实现了Servlet规范中的javax.servlet.ServletContextListener,WEB容器在启动过程中即ServletContext对象创建时会回调ServletContextListener.contextInitialized(), 同理在销毁的时候会回调contextDestroyed(ServletContextEvent sce)
public class ContextLoaderListener extends ContextLoader implements ServletContextListener {public ContextLoaderListener(WebApplicationContext context) {super(context);}@Overridepublic void contextInitialized(ServletContextEvent event) {initWebApplicationContext(event.getServletContext());}@Overridepublic void contextDestroyed(ServletContextEvent event) {closeWebApplicationContext(event.getServletContext());ContextCleanupListener.cleanupAttributes(event.getServletContext());}
}
调用contextInitialized()
创建WebApplicationContext,设置必要的参数,如配置文件位置contextConfigLocation。
调用refresh()。在这个过程中,XmlWebApplicationContext 会解析 XML 配置文件,加载 bean 定义,并创建并初始化所有的 bean。将这个容器当作父容器存到servletContext里边。
public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {/*** 首先通过WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE这个String类型的静态变量获取一个父容器* 父容器作为全局变量存储在application对象中,如果存在则有且只能有一个* 如果在初始化父容器时发现已经存在则直接抛出异常* 这个值在下面的代码中会放入application中*/if (servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE) != null) {throw new IllegalStateException("Cannot initialize context because there is already a root application context present - " +"check whether you have multiple ContextLoader* definitions in your web.xml!");}servletContext.log("Initializing Spring root WebApplicationContext");Log logger = LogFactory.getLog(ContextLoader.class);if (logger.isInfoEnabled()) {logger.info("Root WebApplicationContext: initialization started");}long startTime = System.currentTimeMillis();try {// context代表父容器,已经有值了// xml会在这里创建if (this.context == null) {this.context = createWebApplicationContext(servletContext);}if (this.context instanceof ConfigurableWebApplicationContext) {ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context;if (!cwac.isActive()) {if (cwac.getParent() == null) {ApplicationContext parent = loadParentContext(servletContext); // nullcwac.setParent(parent);}// 创建spring容器,调用refresh()方法完成父容器的初始化configureAndRefreshWebApplicationContext(cwac, servletContext);}}// 将父容器放入到了servletContext中//在servlet域中设置根容器(在子容器就可以直接拿到了)servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);ClassLoader ccl = Thread.currentThread().getContextClassLoader();if (ccl == ContextLoader.class.getClassLoader()) {currentContext = this.context;}else if (ccl != null) {currentContextPerThread.put(ccl, this.context);}if (logger.isInfoEnabled()) {long elapsedTime = System.currentTimeMillis() - startTime;logger.info("Root WebApplicationContext initialized in " + elapsedTime + " ms");}return this.context;}catch (RuntimeException | Error ex) {logger.error("Context initialization failed", ex);servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, ex);throw ex;}
}
2、DispatcherServlet
当创建完成父容器之后,就开始创建子容器。解析servlet标签,即DispatcherServlet。
1、在web.xml文件中配置dispatcherServlet的servlet-name,servlet-class,init-param和load-on-startup等信息。
2、会根据load-on-startup的值,按照从小到大的顺序初始化servlet实例,并调用它们的init()方法
先看一下DispatcherServlet的继承关系:
org.springframework.web.servlet.support.AbstractDispatcherServletInitializer#onStartup
public void onStartup(ServletContext servletContext) throws ServletException {super.onStartup(servletContext); // 创建ContextLoaderListenerregisterDispatcherServlet(servletContext); // 创建DispatcherServlet
}protected void registerDispatcherServlet(ServletContext servletContext) {String servletName = getServletName();Assert.hasLength(servletName, "getServletName() must not return null or empty");WebApplicationContext servletAppContext = createServletApplicationContext(); // 子容器Assert.notNull(servletAppContext, "createServletApplicationContext() must not return null");// 创建DispatcherServletFrameworkServlet dispatcherServlet = createDispatcherServlet(servletAppContext);Assert.notNull(dispatcherServlet, "createDispatcherServlet(WebApplicationContext) must not return null");dispatcherServlet.setContextInitializers(getServletApplicationContextInitializers()); // nullServletRegistration.Dynamic registration = servletContext.addServlet(servletName, dispatcherServlet);if (registration == null) {throw new IllegalStateException("Failed to register servlet with name '" + servletName + "'. " +"Check if there is another servlet registered under the same name.");}registration.setLoadOnStartup(1);registration.addMapping(getServletMappings()); // 处理哪些映射registration.setAsyncSupported(isAsyncSupported()); // 默认为true,支持异步Filter[] filters = getServletFilters(); // 过滤器if (!ObjectUtils.isEmpty(filters)) {for (Filter filter : filters) {// 添加filter到servlet容器中registerServletFilter(servletContext, filter);}}customizeRegistration(registration);
}protected FrameworkServlet createDispatcherServlet(WebApplicationContext servletAppContext) {return new DispatcherServlet(servletAppContext);
}
1. HttpServletBean#init()
WEB容器启动后调用Servlet的init()方法进行初始化,此方法的实现实在父类HttpServletBean中:
org.springframework.web.servlet.HttpServletBean#init
@Override
public final void init() throws ServletException {// 解析 init-param 并封装只 pvs 中(xml)PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);if (!pvs.isEmpty()) {// 将当前的这个 Servlet 类转化为一个 BeanWrapper,从而能够以 Spring 的方法来对 init-param 的值进行注入BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext());bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, getEnvironment()));initBeanWrapper(bw);// 修改servlet状态,并将pvs里边的值赋值给servletbw.setPropertyValues(pvs, true);}// 初始化Servlet,创建Spring容器initServletBean();
}
2. FrameworkServlet#initServletBean()
protected final void initServletBean() throws ServletException {getServletContext().log("Initializing Spring " + getClass().getSimpleName() + " '" + getServletName() + "'");if (logger.isInfoEnabled()) {logger.info("Initializing Servlet '" + getServletName() + "'");}long startTime = System.currentTimeMillis();try {// 对子容器进行初始化this.webApplicationContext = initWebApplicationContext();initFrameworkServlet(); // NOP}catch (ServletException | RuntimeException ex) {logger.error("Context initialization failed", ex);throw ex;}if (logger.isDebugEnabled()) {String value = this.enableLoggingRequestDetails ?"shown which may lead to unsafe logging of potentially sensitive data" :"masked to prevent unsafe logging of potentially sensitive data";logger.debug("enableLoggingRequestDetails='" + this.enableLoggingRequestDetails +"': request parameters and headers will be " + value);}if (logger.isInfoEnabled()) {logger.info("Completed initialization in " + (System.currentTimeMillis() - startTime) + " ms");}
}
3. FrameworkServlet#initWebApplicationContext()
protected WebApplicationContext initWebApplicationContext() {// 从ServletContext中获取父容器WebApplicationContext rootContext =WebApplicationContextUtils.getWebApplicationContext(getServletContext());WebApplicationContext wac = null;if (this.webApplicationContext != null) {wac = this.webApplicationContext; // 子容器if (wac instanceof ConfigurableWebApplicationContext) {ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;if (!cwac.isActive()) {if (cwac.getParent() == null) {cwac.setParent(rootContext); // 父子间建立关系}// 调用refresh()方法对子容器进行初始化configureAndRefreshWebApplicationContext(cwac);}}}if (wac == null) { // 不会进入wac = findWebApplicationContext();}if (wac == null) { // 不会进入wac = createWebApplicationContext(rootContext);}if (!this.refreshEventReceived) { // refreshEventReceived=true不会进入synchronized (this.onRefreshMonitor) {onRefresh(wac);}}if (this.publishContext) {String attrName = getServletContextAttributeName();// 将子容器也放入到ServletContext中getServletContext().setAttribute(attrName, wac);}return wac;
}
4. FrameworkServlet#configureAndRefreshWebApplicationContext()
protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac) {if (ObjectUtils.identityToString(wac).equals(wac.getId())) {// The application context id is still set to its original default value// -> assign a more useful id based on available informationif (this.contextId != null) {wac.setId(this.contextId);}else {// Generate default id...wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +ObjectUtils.getDisplayString(getServletContext().getContextPath()) + '/' + getServletName());}}wac.setServletContext(getServletContext());wac.setServletConfig(getServletConfig());wac.setNamespace(getNamespace());// 添加了一个监听子容器刷新的事件wac.addApplicationListener(new SourceFilteringListener(wac, new ContextRefreshListener()));// The wac environment's #initPropertySources will be called in any case when the context// is refreshed; do it eagerly here to ensure servlet property sources are in place for// use in any post-processing or initialization that occurs below prior to #refreshConfigurableEnvironment env = wac.getEnvironment();if (env instanceof ConfigurableWebEnvironment) {((ConfigurableWebEnvironment) env).initPropertySources(getServletContext(), getServletConfig());}postProcessWebApplicationContext(wac); // NOPapplyInitializers(wac);// invoke refresh methodwac.refresh();
}
在子容器初始化之前添加了一个监听容器刷新的事件ContextRefreshListener,当容器刷新完成后将会调用ContextRefreshListener.onApplicationEvent()方法。发布容器刷新事件ContextRefreshedEvent,最终会再刷新事件处理器中调用FrameworkServlet.onApplicationEvent()。
5. FrameworkServlet.onApplicationEvent()
private class ContextRefreshListener implements ApplicationListener<ContextRefreshedEvent> {@Overridepublic void onApplicationEvent(ContextRefreshedEvent event) {FrameworkServlet.this.onApplicationEvent(event);}
}
6. FrameworkServlet#onApplicationEvent()
public void onApplicationEvent(ContextRefreshedEvent event) {this.refreshEventReceived = true;synchronized (this.onRefreshMonitor) {onRefresh(event.getApplicationContext());}
}
7. DispatcherServlet#onRefresh()
protected void onRefresh(ApplicationContext context) {initStrategies(context);
}protected void initStrategies(ApplicationContext context) {// 初始化用于处理文件上传的MultipartResolver,即:从IOC中获取名称为“multipartResolver”的beaninitMultipartResolver(context); // 初始化用于国际化的LocaleResolver(从IOC中获取名称为“localeResolver”的bean)initLocaleResolver(context);// 初始化用于主题风格的ThemeResolver(从IOC中获取名称为“themeResolver”的bean)initThemeResolver(context);// 初始化用于处理Request请求及流转的HandlerMapping,根据变量”detectAllHandlerMappings“,有如下3种获取方式:// 方式1:获取IOC中所有类型为HandlerMapping的bean集合// 方式2:获取名称为“handlerMapping”的bean// 方式3:获取DispatcherServlet.properties文件中配置的HandlerMapping的bean列表initHandlerMappings(context);// 初始化用于进行请求处理的HandlerAdapter,根据变量”detectAllHandlerAdapters“,有如下3种获取方式:// 方式1:获取IOC中所有类型为HandlerAdapter的bean集合// 方式2:获取名称为“handlerAdapter”的bean// 方式3:获取DispatcherServlet.properties文件中配置的HandlerAdapter的bean列表initHandlerAdapters(context);// 初始化用于对异常的类型进行处理并生成ModelAndView的HandlerExceptionResolver,根据变量”detectAllHandlerExceptionResolvers“,有如下3种获取方式:// 方式1:获取IOC中所有类型为HandlerExceptionResolver的bean集合// 方式2:获取名称为“handlerExceptionResolver”的bean// 方式3:获取DispatcherServlet.properties文件中配置的HandlerExceptionResolver的bean列表initHandlerExceptionResolvers(context);// 初始化用于获取“逻辑视图名称”的RequestToViewNameTranslator(从IOC中获取名称为“viewNameTranslator”的bean)initRequestToViewNameTranslator(context);// 初始化用于创建View对象的ViewResolver,根据变量”viewResolver“,有如下3种获取方式:// 方式1:获取IOC中所有类型为ViewResolver的bean集合// 方式2:获取名称为“viewResolver”的bean// 方式3:获取DispatcherServlet.properties文件中配置的ViewResolver的bean列表initViewResolvers(context);// 初始化用于管理FlashMap的FlashMapManager(从IOC中获取名称为“flashMapManager”的bean)initFlashMapManager(context);
}
8、总结
如果在web.xml中配置了org.springframework.web.context.ContextLoaderListener,那么Tomcat在启动的时候会先加载父容器,并将其放到ServletContext中;
然后会加载DispatcherServlet,因为DispatcherServlet实质是一个Servlet,所以会先执行它的init方法。这个init方法在HttpServletBean这个类中实现,其主要工作是做一些初始化工作,将我们在web.xml中配置的参数书设置到Servlet中,然后再触发FrameworkServlet的initServletBean()方法;
FrameworkServlet主要作用是初始化Spring子上下文,设置其父上下文,并将其放入ServletContext中;
FrameworkServlet在调用initServletBean()的过程中同时会触发DispatcherServlet的onRefresh()方法,这个方法会初始化Spring MVC的各个功能组件。比如异常处理器、视图处理器、请求映射处理等。
3、DispatcherServlet.properties
DispatcherServlet中默认的组件
org.springframework.web.servlet.LocaleResolver=org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolverorg.springframework.web.servlet.ThemeResolver=org.springframework.web.servlet.theme.FixedThemeResolverorg.springframework.web.servlet.HandlerMapping=org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping,\org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping,\org.springframework.web.servlet.function.support.RouterFunctionMappingorg.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.HandlerFunctionAdapterorg.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.DefaultHandlerExceptionResolverorg.springframework.web.servlet.RequestToViewNameTranslator=org.springframework.web.servlet.view.DefaultRequestToViewNameTranslatororg.springframework.web.servlet.ViewResolver=org.springframework.web.servlet.view.InternalResourceViewResolverorg.springframework.web.servlet.FlashMapManager=org.springframework.web.servlet.support.SessionFlashMapManager
4、SpringMVC中的9大内置组件初始化
4.1 初始化文件解析器initMultipartResolver()
初始化multipart解析器的, MultipartResolver是一个接口,在Web开发中,multipart解析器主要用于处理HTTP请求中的multipart/form-data类型的数据,这种数据类型通常用于文件上传。
SpringMVC提供了两种multipart解析器:
1、CommonsMultipartResolver:基于Apache Commons FileUpload库实现的multipart解析器。
2、StandardServletMultipartResolver:基于Servlet 3.0规范实现的multipart解析器。
在Spring MVC中,如果你定义了一个名为"multipartResolver"的bean,那么Spring MVC就会使用你定义的这个bean作为multipart解析器。否则,Spring MVC就不会处理multipart/form-data类型的请求。
LOCALE_RESOLVER_BEAN_NAME = “localeResolver”;
1、一种是我们自定义上传文件解析器
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"><!-- 设置最大上传文件为5MB --><property name="maxUploadSize" value="5242880"/>
</bean>或者--------------@Bean
public CommonsMultipartResolver multipartResolver() {CommonsMultipartResolver multipartResolver = new CommonsMultipartResolver();multipartResolver.setMaxUploadSize(5242880);return multipartResolver;
}
2、默认文件上传解析器
如果我们没有自定义上传文件的bean,那么就会利用spi去配置文件里边取值,然后加载到子容器中。
// 通过PropertiesLoaderUtils工具类加载DispatcherServlet.properties
//DEFAULT_STRATEGIES_PATH = "DispatcherServlet.properties";
ClassPathResource resource = new ClassPathResource(DEFAULT_STRATEGIES_PATH, DispatcherServlet.class);
//spi
defaultStrategies = PropertiesLoaderUtils.loadProperties(resource);
org.springframework.web.servlet.LocaleResolver=org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver
4.2 初始化国际化应用initLocaleResolver()
初始化 DispatcherServlet 中的 LocaleResolver。在Spring MVC中,LocaleResolver是一个接口,它定义了如何解析客户端的地区信息。地区信息通常被用于国际化应用,可以根据不同的地区显示不同的信息。
该方法的主要逻辑如下:
首先,尝试从Spring上下文中获取名为localeResolver的bean,如果获取到了,就使用该bean作为地区解析器。
如果没有获取到,那么就会使用getDefaultStrategy方法来获取一个默认的地区解析器。具体的实现类是在DispatcherServlet.properties中配置的和上边的一样也是利用SPI技术。
在Spring MVC中,有几个预定义的LocaleResolver的实现:
- AcceptHeaderLocaleResolver: 根据HTTP的Accept-Language头来解析地区信息。
- FixedLocaleResolver: 提供固定的地区信息,无论请求是什么。
- CookieLocaleResolver: 将地区信息保存在浏览器的Cookie中。
- SessionLocaleResolver: 将地区信息保存在HTTP Session中。
- 如果我们没有自己配置LocaleResolver的Bean,Spring
MVC将默认使用AcceptHeaderLocaleResolver。
private void initLocaleResolver(ApplicationContext context) {try {this.localeResolver = context.getBean(LOCALE_RESOLVER_BEAN_NAME, LocaleResolver.class);catch (NoSuchBeanDefinitionException ex) {this.localeResolver = getDefaultStrategy(context, LocaleResolver.class);}
}
4.3 初始化主题initThemeResolver()
初始化 ThemeResolver 的方法。ThemeResolver 是一个接口,定义了如何解析应用程序的主题的规则。主题是Spring MVC中用于改变视图层外观的一种机制,例如颜色、CSS样式等。在Spring MVC中,可以为每个用户设置不同的主题,或者为所有用户设置一个统一的主题。
具体查找和上边一样,查找的是themeResolverbean,默认实现的是FixedThemeResolver,保存到themeResolver 。
4.4 初始化HandlerMapping()
初始化处理器映射(Handler Mapping),这里也是和上边一样如果我们自己定义HandlerMapping,就用我们自己定义的,我们可以定义多个,如果没有自定义用默认的,默认有三个实现。
4.4.1 自定义实现HandlerMapping
实现HandlerMapping接口或者继承AbstractHandlerMapping。创建一个CustomHandlerMapping类,它继承自AbstractHandlerMapping。
然后,在Spring MVC配置中注册这个自定义的HandlerMapping。当一个请求的URL路径为"/custom"时,CustomHandlerMapping就会返回CustomHandler作为处理器。
自定义的HandlerMapping与Spring MVC默认的HandlerMapping(比如RequestMappingHandlerMapping)是可以共存的(需要手动注入了,因为有默认的了,就不加载默认的了)。当一个请求到来时,Spring MVC会按照HandlerMapping的顺序来查找处理器。你可以通过实现Ordered接口或者使用@Order注解来调整HandlerMapping的顺序。
public class CustomHandlerMapping extends AbstractHandlerMapping {//保存映射关系Map<String, Object> pathHandlers = new HashMap<>();public CustomHandlerMapping() {// 添加一些自定义的映射// 此处仅作示例,一般我们不会在构造函数中添加映射// 更常见的是在配置类中或者通过其他方式动态添加映射this.pathHandlers.put("/custom", new CustomHandler());}@Overrideprotected Object getHandlerInternal(HttpServletRequest request) throws Exception {// 根据请求的URL路径查找对应的处理器String path = request.getServletPath();return this.pathHandlers.get(path);}
}class CustomHandler {// 这里可以添加自定义的处理器方法
}
4.4.2 默认实现BeanNameUrlHandlerMapping
BeanNameUrlHandlerMapping:这个处理器映射使用 Spring bean 的名称作为 URL
来映射请求。例如,一个名为 “/test” 的 bean 将会处理来自 “/test” URL
的请求。这种方式在实际开发中用的比较少,因为它不够灵活且易于混淆。
BeanNameUrlHandlerMapping的寻找流程:
1、找出Spring容器中所有的beanName
2、判断beanName是不是以“/”开头
3、如果是,则把它当作一个Handler,并把beanName作为key,bean对象作为value存入handlerMap中,handlerMap就是一个Map
4.4.3 默认实现SimpleUrlHandlerMapping
SimpleUrlHandlerMapping:这个处理器映射允许我们显式地指定 URL与处理器之间的映射关系。然后将handlerMapping都保存到handlerMappings集合中。
4.4.4 默认实现 RequestMappingHandlerMapping
RequestMappingHandlerMapping:这个是最常用的处理器映射。它会扫描 Spring容器中的所有控制器(Controller),找出带有 @RequestMapping 注解的方法,然后根据注解的参数(例如,HTTP方法、URL 等)来建立请求与方法之间的映射。在处理 HTTP 请求时,它会找到与请求参数匹配的那个方法来处理请求。
RequestMappingHandlerMapping查找handler流程
默认得处理器映射(Handler Mapping)是spring创建的,是经过了bean得生命周期得,也就是通过getBean() -> createBean()那一套流程得,RequestMappingHandlerMapping类图:
RequestMappingHandlerMapping实现了InitializingBean,在初始化时候会执行afterPropertiesSet方法然后调用initHandlerMethods方法,这个方法会去解析所有的handler。
在 Spring MVC 中,RequestMappingHandlerMapping 是负责处理标有 @RequestMapping 注解的 Controller 的。它的工作流程如下:
1、当 Spring 容器启动的时候,RequestMappingHandlerMapping 会进行初始化,在初始化的过程中,afterPropertiesSet 方法会被调用。这个方法会进一步调用 initHandlerMethods 方法。
2、在 initHandlerMethods 方法中,会获取Spring 容器中所有的Bean。对于获取到的每一个 Bean,RequestMappingHandlerMapping 都会检查它是否标有 @Controller 或者 @RequestMapping 注解。只有标有这些注解的 Bean 才会被认为是一个有效的 Controller。
3、如果一个 Bean 被认为是一个有效的 Controller,那么 RequestMappingHandlerMapping 会进一步检查这个 Controller 中的所有方法。对于每一个方法,如果它上面有 @RequestMapping 注解,那么 RequestMappingHandlerMapping 会根据这个注解以及它所在的 Controller 的信息,创建一个 RequestMappingInfo 对象。
4、RequestMappingInfo 对象包含了请求的 URL、请求的方法、请求的参数等信息。这个对象会被用来与进来的 HTTP 请求进行匹配,以决定哪一个方法应该被用来处理这个请求。
5、最后将保存k-v,将path-List保存到pathLookup map中,为什么是list,因为相同的路径有get、post区分。将 RequestMappingInfo-headlMethod保存到registry map中。
创建handlerMethod
当解析到带有@RequestMapping注解的方法时,会创建一个HandlerMethod对象,该对象包含了方法的相关信息,如所属的类、方法名、参数列表等
构建RequestMappingInfo:
代表了一个请求路径和请求方法的映射关系
4.5 初始化handler适配器initHandlerAdapters()
处理器适配器(HandlerAdapter)负责调用处理器(Handler)的处理方法,最后的方法都封装成了handler, 不同的handler有不同的handlerAdapter来负责调用。
首先从应用程序上下文中获取所有类型为HandlerAdapter的bean。如果找不到任何HandlerAdapter的bean,那么它会使用一组默认的处理器适配器。这组默认的处理器适配器包括RequestMappingHandlerAdapter、HttpRequestHandlerAdapter和SimpleControllerHandlerAdapter、HandlerFunctionAdapter,并将其保存到handlerAdapters集合中。
适配逻辑:
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");
}
4.5.1 自定义HandlerAdapter
当我们自定义了handlerMapping就需要自定义HandlerAdapter来对他的handler进行匹配,当然,也可以利用默认的。
4.5.2 HttpRequestHandlerAdapter
HttpRequestHandlerAdapter:这个类是用于处理实现了HttpRequestHandler接口的类,这个接口只有一个方法handleRequest,用于直接处理请求和响应。它会将HttpServletRequest和HttpServletResponse对象作为参数传递给handleRequest方法,并将返回值封装成一个ModelAndView对象。
public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler)throws Exception {((HttpRequestHandler) handler).handleRequest(request, response);return null;
}
4.5.3 SimpleControllerHandlerAdapter
SimpleControllerHandlerAdapter:这个类是用于处理实现了Controller接口的类,这个接口也只有一个方法handleRequest,用于返回一个ModelAndView对象。它会将HttpServletRequest和HttpServletResponse对象作为参数传递给handleRequest方法,并直接返回其返回值。
public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler)throws Exception {return ((Controller) handler).handleRequest(request, response);
}
4.5.4 HandlerFunctionAdapter
HandlerFunctionAdapter:这个类是用于处理实现了HandlerFunction接口的类,这个接口是一个函数式接口,用于定义一个函数,接受一个ServerRequest对象并返回一个Mono对象。它会将HttpServletRequest和HttpServletResponse对象转换成ServerRequest和ServerResponse对象,并调用apply方法来执行函数,并将返回值转换成一个ModelAndView对象。
HandlerFunction<?> handlerFunction = (HandlerFunction<?>) handler;
serverResponse = handlerFunction.handle(serverRequest);
上面这几个接收的直接就是Requeset对象,不用SpringMVC做额外的解析,所以比较简单,比较复杂的是RequestMappingHandlerAdapter,它执行的是加了@RequestMapping的方法,而这种方法的写法可以是多种多样,SpringMVC需要根据方法的定义去解析Request对象,从请求中获取出对应的数据然后传递给方法,并执行。
4.5.5 RequestMappingHandlerAdapter
RequestMappingHandlerAdapter:这个类是用于处理带有@RequestMapping注解的方法。它会根据请求的参数,头部,属性等来解析方法的参数,并调用反射来执行方法。
此处比较复杂,后续单独说明
4.6 初始化异常initHandlerExceptionResolvers()
负责将在处理器处理过程中抛出的异常转换为对应的模型-视图结果。
在initHandlerExceptionResolvers()方法中,获取所有类型为HandlerExceptionResolver的Bean。如果找不到任何HandlerExceptionResolver的Bean,那么它会使用一组默认的处理器异常解析器。
这里的默认是会取读取DispatcherServlet.properties
private void initHandlerExceptionResolvers(ApplicationContext context) {this.handlerExceptionResolvers = null;if (this.detectAllHandlerExceptionResolvers) {// Find all HandlerExceptionResolvers in the ApplicationContext, including ancestor contexts.Map<String, HandlerExceptionResolver> matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerExceptionResolver.class, true, false);if (!matchingBeans.isEmpty()) {this.handlerExceptionResolvers = new ArrayList<>(matchingBeans.values());// We keep HandlerExceptionResolvers in sorted order.AnnotationAwareOrderComparator.sort(this.handlerExceptionResolvers);}}else {try {HandlerExceptionResolver her =context.getBean(HANDLER_EXCEPTION_RESOLVER_BEAN_NAME, HandlerExceptionResolver.class);this.handlerExceptionResolvers = Collections.singletonList(her);}catch (NoSuchBeanDefinitionException ex) {// Ignore, no HandlerExceptionResolver is fine too.}}// Ensure we have at least some HandlerExceptionResolvers, by registering// default HandlerExceptionResolvers if no other resolvers are found.if (this.handlerExceptionResolvers == null) {this.handlerExceptionResolvers = getDefaultStrategies(context, HandlerExceptionResolver.class);if (logger.isTraceEnabled()) {logger.trace("No HandlerExceptionResolvers declared in servlet '" + getServletName() +"': using default strategies from DispatcherServlet.properties");}}}
4.6.0 DispatcherServlet.properties
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
4.6.1 ExceptionHandlerExceptionResolver
用于处理通过@ExceptionHandler注解处理的方法抛出的异常。
4.6.2 ResponseStatusExceptionResolver
用于处理通过@ResponseStatus注解处理的异常
4.6.3 DefaultHandlerExceptionResolver
用于处理Spring MVC自己抛出的一些特定的异常
4.7 initRequestToViewNameTranslator
initRequestToViewNameTranslator方法用于初始化RequestToViewNameTranslator的,RequestToViewNameTranslator是一个接口,用于根据请求解析出一个默认的视图名称,当ModelAndView对象不为null,但是没有指定视图时,就会使用这个接口来获取视图名称。
寻找bean名字为viewNameTranslator试图解析器,没有的话用DefaultRequestToViewNameTranslator,并存放到viewNameTranslator。
工作原理是:它会去掉请求URL的前缀和后缀,然后将剩下的部分作为视图名称。例如,如果我们设置prefix为"/app/“,suffix为”.html",stripExtension为false,那么请求的URL"/app/home.html"就会被解析为"home.html"。如果我们设置stripExtension为true,那么就会被解析为"home"。
4.8 初始化视图解析器initViewResolvers()
默认是寻找所有类型为ViewResolver的bean,将他们作为试图解析器,默认的试图解析器是InternalResourceViewResolver。它们是用于将控制器返回的逻辑视图名解析为具体的视图对象,这个对象可以是一个 JSP、一个 HTML 页面、一个 PDF 视图、一个 Thymeleaf 模板等。
4.9 初始化重定向initFlashMapManager()
默认的是SessionFlashMapManager。
FlashMap是Spring MVC提供的一种临时存储数据的方式,它用于存储一次请求的数据,并在下一次请求中使用。FlashMap通常用于重定向请求时传递数据,比如,当你在处理POST请求后重定向到一个GET请求时,你可能希望重定向的GET请求能够访问POST请求处理的一些结果数据,这就可以使用FlashMap来实现。例如可以这样存储Flash属性:
@RequestMapping(method = RequestMethod.POST)
public String handlePostData(@ModelAttribute("data") Data data, RedirectAttributes redirectAttrs) {// 处理POST请求...// 将一些数据存储在FlashMap中,以便在重定向后的GET请求中使用redirectAttrs.addFlashAttribute("message", "123");// 重定向到GET请求return "redirect:/get-data";
}
然后在重定向后的GET请求中提取Flash属性:
@RequestMapping(method = RequestMethod.GET)
public String displayData(Model model) {// 如果存在Flash属性,它们会自动添加到Model中if (model.containsAttribute("message")) {System.out.println("Message-FlashMap: " + model.getAttribute("message"));}// 处理GET请求...
}
相关文章:

SpringMVC源码分析(一)启动流程分析
a、SpringMVC 在启动过程中主要做了什么事情? SpringMVC在启动过程中是什么时候解析web.xml文件的,又是什么时候初始化9大内置对象的? <?xml version"1.0" encoding"UTF-8"?> <web-app xmlns"http://xml…...

ARM 10.12
设置按键中断,按键1按下,LED亮,再按一次,灭 按键2按下,蜂鸣器响。再按一次,不响 按键3按下,风扇转,再按一次,风扇停 src/key.c #include"key.h"//按键3的配…...

vue-rouer 路由
安装/配置: //进入项目目录:(在搭建项目的时候安装了) cnpm install vue-router --save旧版路由 需要自己配置 //项目中载入,一般在main.js中载入:import VueRouter from vue-routerVue.use(VueRouter)let router new VueRouter({}) //其中配置路径和地址//在Vue中引入:n…...
元数据的前世今生
什么是元数据 元数据(Metadata)是描述数据的数据。它是一组信息,用于描述数据的特征、属性、结构和内容,以便更好地管理、理解、组织和使用数据。让人们能够清楚拥有什么数据、代表什么、源自何处、如何在系统中移动,以及哪些人可以使用源数据,如何使用。 元数据通常包…...

Python实现简易过滤删除数字的方法
嗨喽~大家好呀,这里是魔王呐 ❤ ~! python更多源码/资料/解答/教程等 点击此处跳转文末名片免费获取 如果想从一个含有数字,汉字,字母的列表中滤除仅含有数字的字符, 当然可以采取正则表达式来完成,但是有点太麻烦了…...

软件测试定位bug方法+定位案例(详解)
1、问题bug定位技巧 首先,作为开发也好,测试也好,定位问题有一个总的思路,而这个思路是和数据的走向一致的。 大致是这样: 用户层面问题 -> Web页面/软件界面 -> 中间件 -> 后端服务 -> 代码 -> 数据…...

【算法练习Day21】组合剪枝
📝个人主页:Sherry的成长之路 🏠学习社区:Sherry的成长之路(个人社区) 📖专栏链接:练题 🎯长路漫漫浩浩,万事皆有期待 文章目录 组合剪枝总结: …...
NPM相关命令
临时使用 npm --registry https://registry.npm.taobao.org install 包名2.永久设置为淘宝镜像 npm config set registry https://registry.npm.taobao.org3.换回国外官方源 npm config set registry https://registry.npmjs.org4.查看使用的源地址 npm config get registr…...

Kubernetes 集群部署 Prometheus 和 Grafana
Kubernetes 集群部署 Prometheus 和 Grafana 文章目录 Kubernetes 集群部署 Prometheus 和 Grafana一.部署 node-exporter1.node-exporter 安装2.部署 node-exporter 二.部署Prometheus1.Prometheus 安装和配置(1)创建 sa 账号,对 sa 做 rbac…...

【算法-动态规划】零钱兑换 II-力扣 518
💝💝💝欢迎来到我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。 推荐:kuan 的首页,持续学…...

Hadoop3教程(六):HDFS中的DataNode
文章目录 (63)DataNode工作机制(64)数据完整性(65)掉线时限参数设置参考文献 (63)DataNode工作机制 DataNode内部存储了一个又一个Block,每个block由数据和数据元数据组…...

Macos音乐制作:Ableton Live 11 Suite for Mac中文版
Ableton Live 11是一款数字音频工作站软件,用于音乐制作、录音、混音和现场演出。它由Ableton公司开发,是一款极其流行的音乐制作软件之一。 以下是Ableton Live 11的一些主要特点和功能: Comping功能:Live 11增加了Comping功能…...

ThinkPHP5小语种学习平台
有需要请加文章底部Q哦 可远程调试 ThinkPHP5小语种学习平台 一 介绍 此小语种学习平台基于ThinkPHP5框架开发,数据库mysql,前端bootstrap。平台角色分为学生,教师和管理员三种。学生注册登录后可观看学习视频,收藏视频…...

升级包版本之后Reflections反射包在springboot jar环境下扫描不到class排查过程记录
📢📢📢📣📣📣 哈喽!大家好,我是「奇点」,江湖人称 singularity。刚工作几年,想和大家一同进步🤝🤝 一位上进心十足的【Java ToB端大厂…...

Excel 函数大全应用,包含各类常用函数
Excel 函数大全应用,各类函数应用与案例实操。 AIGC ChatGPT 职场案例 AI 绘画 与 短视频制作, Power BI 商业智能 68集, 数据库Mysql8.0 54集 数据库Oracle21C 142集, Office 2021实战, Python 数据分析࿰…...

深入浅出的介绍一下虚拟机VMware Workstation——part3(VMware快照)
虚拟机VMware使用 前言快照的原理快照的使用 前言 可以先查看之前的2篇博文,学习基础的虚拟机使用 深入浅出的介绍一下虚拟机VMware Workstation——part1 深入浅出的介绍一下虚拟机VMware Workstation——part2(详细安装与使用) 由于我们使用虚拟机的初衷就是用来…...

《Python基础教程》专栏总结篇
大家好,我是爱编程的喵喵。双985硕士毕业,现担任全栈工程师一职,热衷于将数据思维应用到工作与生活中。从事机器学习以及相关的前后端开发工作。曾在阿里云、科大讯飞、CCF等比赛获得多次Top名次。现为CSDN博客专家、人工智能领域优质创作者。…...

JavaScript 事件
HTML 事件是发生在 HTML 元素上的事情。 当在 HTML 页面中使用 JavaScript 时, JavaScript 可以触发这些事件。 HTML 事件 HTML 事件可以是浏览器行为,也可以是用户行为。 以下是 HTML 事件的实例: HTML 页面完成加载HTML input 字段改变…...

轻松学会这招,给大量视频批量添加滚动字幕不求人
想要给大量视频批量添加滚动字幕不求人吗?下面就教你一个简单的方法。首先你需要下载并安装一款名为“固乔剪辑助手”的软件,这是一款非常专业的视频剪辑软件,它可以帮助你快速地给大量视频添加滚动字幕。 打开固乔剪辑助手软件后,…...

哪个文字转语音配音软件最好用?
现在TTS技术不断发展,文字转语音技术已经越来越成熟,声音听着拟人度非常高,现在好用的软件也不在少数。很多手机里面都有自带的朗读功能,如果觉得声音不够,也可以自己下载软件使用。给大家分享一下我一直使用的一款文字…...

接口测试中缓存处理策略
在接口测试中,缓存处理策略是一个关键环节,直接影响测试结果的准确性和可靠性。合理的缓存处理策略能够确保测试环境的一致性,避免因缓存数据导致的测试偏差。以下是接口测试中常见的缓存处理策略及其详细说明: 一、缓存处理的核…...
OpenLayers 可视化之热力图
注:当前使用的是 ol 5.3.0 版本,天地图使用的key请到天地图官网申请,并替换为自己的key 热力图(Heatmap)又叫热点图,是一种通过特殊高亮显示事物密度分布、变化趋势的数据可视化技术。采用颜色的深浅来显示…...

树莓派超全系列教程文档--(61)树莓派摄像头高级使用方法
树莓派摄像头高级使用方法 配置通过调谐文件来调整相机行为 使用多个摄像头安装 libcam 和 rpicam-apps依赖关系开发包 文章来源: http://raspberry.dns8844.cn/documentation 原文网址 配置 大多数用例自动工作,无需更改相机配置。但是,一…...

微软PowerBI考试 PL300-选择 Power BI 模型框架【附练习数据】
微软PowerBI考试 PL300-选择 Power BI 模型框架 20 多年来,Microsoft 持续对企业商业智能 (BI) 进行大量投资。 Azure Analysis Services (AAS) 和 SQL Server Analysis Services (SSAS) 基于无数企业使用的成熟的 BI 数据建模技术。 同样的技术也是 Power BI 数据…...
多场景 OkHttpClient 管理器 - Android 网络通信解决方案
下面是一个完整的 Android 实现,展示如何创建和管理多个 OkHttpClient 实例,分别用于长连接、普通 HTTP 请求和文件下载场景。 <?xml version"1.0" encoding"utf-8"?> <LinearLayout xmlns:android"http://schemas…...

UDP(Echoserver)
网络命令 Ping 命令 检测网络是否连通 使用方法: ping -c 次数 网址ping -c 3 www.baidu.comnetstat 命令 netstat 是一个用来查看网络状态的重要工具. 语法:netstat [选项] 功能:查看网络状态 常用选项: n 拒绝显示别名&#…...
Python如何给视频添加音频和字幕
在Python中,给视频添加音频和字幕可以使用电影文件处理库MoviePy和字幕处理库Subtitles。下面将详细介绍如何使用这些库来实现视频的音频和字幕添加,包括必要的代码示例和详细解释。 环境准备 在开始之前,需要安装以下Python库:…...

Linux --进程控制
本文从以下五个方面来初步认识进程控制: 目录 进程创建 进程终止 进程等待 进程替换 模拟实现一个微型shell 进程创建 在Linux系统中我们可以在一个进程使用系统调用fork()来创建子进程,创建出来的进程就是子进程,原来的进程为父进程。…...
Web 架构之 CDN 加速原理与落地实践
文章目录 一、思维导图二、正文内容(一)CDN 基础概念1. 定义2. 组成部分 (二)CDN 加速原理1. 请求路由2. 内容缓存3. 内容更新 (三)CDN 落地实践1. 选择 CDN 服务商2. 配置 CDN3. 集成到 Web 架构 …...
OD 算法题 B卷【正整数到Excel编号之间的转换】
文章目录 正整数到Excel编号之间的转换 正整数到Excel编号之间的转换 excel的列编号是这样的:a b c … z aa ab ac… az ba bb bc…yz za zb zc …zz aaa aab aac…; 分别代表以下的编号1 2 3 … 26 27 28 29… 52 53 54 55… 676 677 678 679 … 702 703 704 705;…...