SpringMVC源码分析(一)启动流程分析

a、SpringMVC 在启动过程中主要做了什么事情?

SpringMVC在启动过程中是什么时候解析web.xml文件的,又是什么时候初始化9大内置对象的?

java 复制代码
<?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和配置。它通常包含以下内容:
  1. 数据源和事务管理器:用于处理数据库操作和事务管理。
  2. 持久化层(DAO)和业务层(Service)的bean:用于处理数据访问和业务逻辑。
  3. 安全配置:用于配置安全相关的bean,如认证和授权配置。
  4. 缓存配置:用于配置缓存相关的bean,如缓存管理器和缓存注解处理器。
  5. 其他共享bean:例如,公共工具类、全局异常处理器等。

子容器(Servlet WebApplicationContext)

主要负责加载与Web应用程序相关的bean和配置。它通常包含以下内容:
  1. 控制器(Controller)和视图解析器(ViewResolver):用于处理请求和生成响应。
  2. Web相关的bean:例如,处理文件上传的MultipartResolver、处理静态资源的ResourceHandler等。
  3. Web安全配置:用于配置Web安全相关的bean,如Spring Security配置。
  4. 其他与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)

java 复制代码
public class ContextLoaderListener extends ContextLoader implements ServletContextListener {
 public ContextLoaderListener(WebApplicationContext context) {
  super(context);
 }
 @Override
 public void contextInitialized(ServletContextEvent event) {
  initWebApplicationContext(event.getServletContext());
 }
 @Override
 public void contextDestroyed(ServletContextEvent event) {
  closeWebApplicationContext(event.getServletContext());
  ContextCleanupListener.cleanupAttributes(event.getServletContext());
 }
} 

调用contextInitialized()

创建WebApplicationContext,设置必要的参数,如配置文件位置contextConfigLocation。

调用refresh()。在这个过程中,XmlWebApplicationContext 会解析 XML 配置文件,加载 bean 定义,并创建并初始化所有的 bean。将这个容器当作父容器存到servletContext里边。

java 复制代码
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); // null
					cwac.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

java 复制代码
public void onStartup(ServletContext servletContext) throws ServletException {
	super.onStartup(servletContext); // 创建ContextLoaderListener
	registerDispatcherServlet(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");

	// 创建DispatcherServlet
	FrameworkServlet dispatcherServlet = createDispatcherServlet(servletAppContext);
	Assert.notNull(dispatcherServlet, "createDispatcherServlet(WebApplicationContext) must not return null");
	dispatcherServlet.setContextInitializers(getServletApplicationContextInitializers()); // null

	ServletRegistration.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

java 复制代码
@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里边的值赋值给servlet
         bw.setPropertyValues(pvs, true);
   }
   // 初始化Servlet,创建Spring容器
   initServletBean();
}

2. FrameworkServlet#initServletBean()

java 复制代码
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()

java 复制代码
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()

java 复制代码
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 information
		if (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 #refresh
	ConfigurableEnvironment env = wac.getEnvironment();
	if (env instanceof ConfigurableWebEnvironment) {
		((ConfigurableWebEnvironment) env).initPropertySources(getServletContext(), getServletConfig());
	}

	postProcessWebApplicationContext(wac); // NOP
	applyInitializers(wac);
	// invoke refresh method
	wac.refresh();
}

在子容器初始化之前添加了一个监听容器刷新的事件ContextRefreshListener,当容器刷新完成后将会调用ContextRefreshListener.onApplicationEvent()方法。发布容器刷新事件ContextRefreshedEvent,最终会再刷新事件处理器中调用FrameworkServlet.onApplicationEvent()。

5. FrameworkServlet.onApplicationEvent()

java 复制代码
private class ContextRefreshListener implements ApplicationListener<ContextRefreshedEvent> {

	@Override
	public void onApplicationEvent(ContextRefreshedEvent event) {
		FrameworkServlet.this.onApplicationEvent(event);
	}
}

6. FrameworkServlet#onApplicationEvent()

java 复制代码
public void onApplicationEvent(ContextRefreshedEvent event) {
	this.refreshEventReceived = true;
	synchronized (this.onRefreshMonitor) {
		onRefresh(event.getApplicationContext());
	}
}

7. DispatcherServlet#onRefresh()

java 复制代码
protected void onRefresh(ApplicationContext context) {
	initStrategies(context);
}

protected void initStrategies(ApplicationContext context) {
    // 初始化用于处理文件上传的MultipartResolver,即:从IOC中获取名称为"multipartResolver"的bean
    initMultipartResolver(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中默认的组件

html 复制代码
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

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、一种是我们自定义上传文件解析器

java 复制代码
<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去配置文件里边取值,然后加载到子容器中。

java 复制代码
// 通过PropertiesLoaderUtils工具类加载DispatcherServlet.properties
//DEFAULT_STRATEGIES_PATH = "DispatcherServlet.properties";
ClassPathResource resource = new ClassPathResource(DEFAULT_STRATEGIES_PATH, DispatcherServlet.class);
//spi
defaultStrategies = PropertiesLoaderUtils.loadProperties(resource);
html 复制代码
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的实现:

  1. AcceptHeaderLocaleResolver: 根据HTTP的Accept-Language头来解析地区信息。
  2. FixedLocaleResolver: 提供固定的地区信息,无论请求是什么。
  3. CookieLocaleResolver: 将地区信息保存在浏览器的Cookie中。
  4. SessionLocaleResolver: 将地区信息保存在HTTP Session中。
  5. 如果我们没有自己配置LocaleResolver的Bean,Spring
    MVC将默认使用AcceptHeaderLocaleResolver。
html 复制代码
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的顺序。

java 复制代码
public class CustomHandlerMapping extends AbstractHandlerMapping {
 //保存映射关系
    Map<String, Object> pathHandlers = new HashMap<>();

    public CustomHandlerMapping() {
        // 添加一些自定义的映射
        // 此处仅作示例,一般我们不会在构造函数中添加映射
        // 更常见的是在配置类中或者通过其他方式动态添加映射
        this.pathHandlers.put("/custom", new CustomHandler());
    }

    @Override
    protected 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集合中。

适配逻辑:

java 复制代码
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对象。

java 复制代码
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方法,并直接返回其返回值。

java 复制代码
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对象。

java 复制代码
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,那么它会使用一组默认的处理器异常解析器。

4.6.1 ExceptionHandlerExceptionResolver

用于处理通过@ExceptionHandler注解处理的方法抛出的异常。

4.6.2 ResponseStatusExceptionResolver

用于处理通过@ResponseStatus注解处理的异常

4.6.3 DefaultHandlerExceptionResolver

用于处理Spring MVC自己抛出的一些特定的异常

4.6.4 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.7 初始化视图解析器initViewResolvers()

默认是寻找所有类型为ViewResolver的bean,将他们作为试图解析器,默认的试图解析器是InternalResourceViewResolver。它们是用于将控制器返回的逻辑视图名解析为具体的视图对象,这个对象可以是一个 JSP、一个 HTML 页面、一个 PDF 视图、一个 Thymeleaf 模板等。

4.8 初始化重定向initFlashMapManager()

默认的是SessionFlashMapManager。

FlashMap是Spring MVC提供的一种临时存储数据的方式,它用于存储一次请求的数据,并在下一次请求中使用。FlashMap通常用于重定向请求时传递数据,比如,当你在处理POST请求后重定向到一个GET请求时,你可能希望重定向的GET请求能够访问POST请求处理的一些结果数据,这就可以使用FlashMap来实现。例如可以这样存储Flash属性:

java 复制代码
@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属性:

java 复制代码
@RequestMapping(method = RequestMethod.GET)
public String displayData(Model model) {
    // 如果存在Flash属性,它们会自动添加到Model中
    if (model.containsAttribute("message")) {
        System.out.println("Message-FlashMap: " + model.getAttribute("message"));
    }
    
    // 处理GET请求...
}
相关推荐
成富9 分钟前
文本转SQL(Text-to-SQL),场景介绍与 Spring AI 实现
数据库·人工智能·sql·spring·oracle
Re.不晚16 分钟前
Java入门15——抽象类
java·开发语言·学习·算法·intellij-idea
雷神乐乐22 分钟前
Maven学习——创建Maven的Java和Web工程,并运行在Tomcat上
java·maven
码农派大星。26 分钟前
Spring Boot 配置文件
java·spring boot·后端
顾北川_野33 分钟前
Android 手机设备的OEM-unlock解锁 和 adb push文件
android·java
江深竹静,一苇以航35 分钟前
springboot3项目整合Mybatis-plus启动项目报错:Invalid bean definition with name ‘xxxMapper‘
java·spring boot
confiself1 小时前
大模型系列——LLAMA-O1 复刻代码解读
java·开发语言
Wlq04151 小时前
J2EE平台
java·java-ee
XiaoLeisj1 小时前
【JavaEE初阶 — 多线程】Thread类的方法&线程生命周期
java·开发语言·java-ee
鹿屿二向箔1 小时前
基于SSM(Spring + Spring MVC + MyBatis)框架的汽车租赁共享平台系统
spring·mvc·mybatis