一、关于servlet
- 详解servlet,https://www.runoob.com/servlet/servlet-tutorial.html
- 总览一下:
- servlet与servlet容器
Java Servlet(Java服务器小程序)是一个基于Java技术的Web组件,运行在服务器端,它由Servlet容器所管理,用于生成动态的内容
。 Servlet是平台独立的Java类
,编写一个Servlet,实际上就是按照Servlet规范编写一个Java类。Servlet被编译为平台独立 的字节码,可以被动态地加载到支持Java技术的Web服务器中运行。
- Servlet容器也叫做Servlet引擎,
是Web服务器或应用程序服务器的一部分
,用于在发送的请求和响应之上提供网络服务,解码基于 MIME的请求,格式化基于MIME的响应。Servlet没有main方法,不能独立运行,它必须被部署到Servlet容器中,由容器来实例化和调用 Servlet的方法(如doGet()和doPost()),Servlet容器在Servlet的生命周期内包容和管理Servlet
。在JSP技术 推出后,管理和运行Servlet/JSP的容器也称为Web容器。
(注:常用的MIME类型:text/html,application/pdf,video/quicktime,application /java,image/jpeg,application/jar,application/octet-stream,application/x- zip)
有了servlet之后,用户通过单击某个链接或者直接在浏览器的地址栏中输入URL来访问Servlet
,Web服务器接收到该请求后,并不是将 请求直接交给Servlet,而是交给Servlet容器。Servlet容器实例化Servlet,调用Servlet的一个特定方法对请求进行处理, 并产生一个响应
。这个响应由Servlet容器返回给Web服务器,Web服务器包装这个响应,以HTTP响应的形式发送给Web浏览器。
- servlet容器能提供什么?
我们知道需要由servlet容器来管理和运行servlet,但是为什么要这样做呢?使用servlet容器的原因有:
通信支持:利用容器提供的方法,你能轻松的让servlet与web服务器对话,而不用自己建立serversocket、监听某个端口、创建流等等。
容器知道自己与web服务器之间的协议,所以你的servlet不用担心web服务器(如Apache,jetty)和你自己的web代码之间的API,只需要考 虑如何在servlet中实现业务逻辑(如处理一个订单)。
生命周期管理:servlet容器控制着servlet的生与死,它负责加载类、实例化和初始化servlet,调用servlet方法,以及使servlet实例被垃圾回收,有了servlet容器,你不需要太多的考虑资源管理。
多线程支持:容器会自动为它所接收的每个servlet请求创建一个新的java线程。针对用户的请求,如果servlet已经运行完相应的http服务方法,这个线程就会结束。这并不是说你不需要考虑线程安全性,其实你还会遇到同步问题,不过这样能使你少做很多工作。
声明方式实现安全:利用servlet容器,你
可以使用xml部署描述文件来配置和修改安全性,而不必将其硬编码写到servlet类代码中
。JSP支持:servlet容器负责将jsp代码翻译为真正的java代码。
- Servlet具有以下优点:
- Servlet是单实例多线程的运行方式,每个请求在一个独立的线程中运行,而提供服务的Servlet实例只有一个。
- Servlet具有可升级性,能响应更多的请求,因为Servlet容器使用一个线程而不是操作系统进程,而线程仅占用有限的系统资源。
- Servlet使用标准的API,被更多的Web服务器所支持。
- Servlet使用Java语言编写,因此拥有Java程序语言的所有优点,包括容易开发和平台独立性。
- Servlet可以访问Java平台丰富的类库,使得各种应用的开发更为容易。
- Servlet容器给Servlet提供额外的功能,如错误处理和安全。
其实,servlet就是一种使用http协议在服务器与客户端之间通信的技术。是Socket的一种应用
。
- Tomcat
学习Servlet技术,就需要有一个Servlet运行环境,也就是需要有一个Servlet容器,本文用的是Tomcat。还有其他如jetty
。Tomcat和IIS、Apache等Web服务器一样,具有处理HTML页面的功能,另外它还是一个Servlet和JSP容器,独立的 Servlet容器是Tomcat的默认模式。不过,Tomcat处理静态HTML的能力不如Apache,我们可以将Apache和Tomcat集成在 一起使用,Apache作为HTTP Web服务器,Tomcat作为Web容器。关于apache和tomcat的区别。
Tomcat服务器接受客户请求并做出响应的过程如下:
- 客户端(通常都是浏览器)访问Web服务器,发送HTTP请求。
- Web服务器接收到请求后,传递给Servlet容器。
- Servlet容器加载Servlet,产生Servlet实例后,向其传递表示请求和响应的对象。
- Servlet实例使用请求对象得到客户端的请求信息,然后进行相应的处理。
- Servlet实例将处理结果通过响应对象发送回客户端,
容器负责确保响应正确送出,同时将控制返回给Web服务器。
二、从web.xml说起
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_5_0.xsd"
version="5.0">
<!-- spring配置文件,包括开启bean扫描,aop,事务 -->
<!-- Spring加载的xml文件,不配置默认为applicationContext.xml -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/spring-config.xml</param-value>
</context-param>
<!--ContextLoaderListener用于在启动web容器的时候,去上面的位置 读取配置文件并初始化Spring容器。启动父容器,即IOC容器,管理Dao,Service-->
<!-- 该类作为spring的listener使用,它会在创建时自动查找web.xml配置的applicationContext.xml文件 -->
<listener>
<listener-class>
org.springframework.web.context.ContextLoaderListener
</listener-class>
</listener>
<!--spring mvc配置-->
<!-- 配置Spring MVC的DispatcherServlet,也可以配置为继承了DispatcherServlet的自定义类,这里配置spring mvc的配置(扫描controller) -->
<!--用于启动子容器,也即是springMVC容器-->
<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/spring-mvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>dispatcher</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
- spring父容器的加载
Tomcat启动的时候会依次加载web.xml中配置的Listener、Filter和Servlet。所以根据上面的配置,会首先加载ContextLoaderListener
,这个类继承了ContextLoader,用来初始化Spring根上下文,并将其放入ServletContext中。
实现 javax.servlet.ServletContextListener 接口,继承 ContextLoader 类,实现 Servlet 容器启动和关闭时,分别初始化和销毁 WebApplicationContext 容器
public class ContextLoaderListener extends ContextLoader implements ServletContextListener {
public ContextLoaderListener() {
}
/**
* As of Spring 3.1, supports injecting the root web application context
*/
public ContextLoaderListener(WebApplicationContext context) {
super(context);
}
/**
* Initialize the root web application context.
*/
@Override
public void contextInitialized(ServletContextEvent event) {
// <1> 初始化 Root WebApplicationContext
initWebApplicationContext(event.getServletContext());
}
/**
* Close the root web application context.
*/
@Override
public void contextDestroyed(ServletContextEvent event) {
// <2> 销毁 Root WebApplicationContext
closeWebApplicationContext(event.getServletContext());
ContextCleanupListener.cleanupAttributes(event.getServletContext());
}
}
下面就以这个为入口分析下代码。Tomcat容器首先会调用ContextLoadListener的contextInitialized()
方法,这个方法又调用了父类ContextLoader的initWebApplicationContext()方法
。下面是这个方法的源代码。
public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
//如果ServletContext中已经存在Spring容器则报错
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!");
}
Log logger = LogFactory.getLog(ContextLoader.class);
servletContext.log("Initializing Spring root WebApplicationContext");
if (logger.isInfoEnabled()) {
logger.info("Root WebApplicationContext: initialization started");
}
long startTime = System.currentTimeMillis();
try {
// Store context in local instance variable, to guarantee that
// it is available on ServletContext shutdown.
if (this.context == null) {
//这里创建了webApplicationContext,默认创建的是XmlWebApplicationContext
//如果想要自定义实现类,可以在web.xml的<context-param>中配置contextClass这个参数
//此时的Context还没进行配置,相当于只是个"空壳"
this.context = createWebApplicationContext(servletContext);
}
if (this.context instanceof ConfigurableWebApplicationContext) {
ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context;
if (!cwac.isActive()) {
// The context has not yet been refreshed -> provide services such as
// setting the parent context, setting the application context id, etc
if (cwac.getParent() == null) {
// The context instance was injected without an explicit parent ->
// determine parent for root web application context, if any.
ApplicationContext parent = loadParentContext(servletContext);
cwac.setParent(parent);
}
//读取Spring的配置文件,初始化父上下文环境
configureAndRefreshWebApplicationContext(cwac, servletContext);
}
}
//将根上下文存入ServletContext中。
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.isDebugEnabled()) {
logger.debug("Published root WebApplicationContext as ServletContext attribute with name [" +
WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE + "]");
}
if (logger.isInfoEnabled()) {
long elapsedTime = System.currentTimeMillis() - startTime;
logger.info("Root WebApplicationContext: initialization completed in " + elapsedTime + " ms");
}
return this.context;
}
catch (RuntimeException ex) {
logger.error("Context initialization failed", ex);
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, ex);
throw ex;
}
catch (Error err) {
logger.error("Context initialization failed", err);
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, err);
throw err;
}
}
spring体系下,ApplicationContext的集成体系
执行时序图:
至此,Spring的父(根)上下文已经初始化完毕,并且已经存在ServletContext中。
- springMVC子容器的加载
DispatcherServlet的继承图如下
我们知道,加载web.xml中配置的Listener、Filter后,就会加载Servlet。在我们的web.xml中就是org.springframework.web.servlet.DispatcherServlet
,而通过继承树我们知道,DispatcherServlet也是Servlet的实现,所以加载流程也是Servlet的流程。因为DispatcherServlet继承了FrameworkServlet,FrameworkServlet又继承了HttpServletBean,HttpServletBean又继承HttpServlet并且重写了init方法,所以创建子上下文时的入口就在这个init方法。
- HttpServletBean.init(): 重写 GenericServlet 中的方法,负责将 ServletConfig 设置到当前 Servlet 对象中
public final void init() throws ServletException {
//读取Servlet配置的init-param,创建DispatcherServlet实例
PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
if (!pvs.isEmpty()) {
try {
BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext());
bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, getEnvironment()));
initBeanWrapper(bw);
bw.setPropertyValues(pvs, true);
}
catch (BeansException ex) {
if (logger.isErrorEnabled()) {
logger.error("Failed to set bean properties on servlet '" + getServletName() + "'", ex);
}
throw ex;
}
}
// HttpServletBean的这个方法中没有做任何事情,子类FrameWorkServlet这个类的initServletBean()方法重写了
// 这个类,所以后续工作会在这个方法中执行。
initServletBean();
}
下面是FrameWorkServlet这个类的initServletBean()方法
//FrameWorkServlet.initServletBean()
protected final void initServletBean() throws ServletException {
getServletContext().log("Initializing Spring FrameworkServlet '" + getServletName() + "'");
if (this.logger.isInfoEnabled()) {
this.logger.info("FrameworkServlet '" + getServletName() + "': initialization started");
}
long startTime = System.currentTimeMillis();
try {
//这里是重点,初始化子Spring上下文
this.webApplicationContext = initWebApplicationContext();
initFrameworkServlet();
}
catch (ServletException ex) {
this.logger.error("Context initialization failed", ex);
throw ex;
}
catch (RuntimeException ex) {
this.logger.error("Context initialization failed", ex);
throw ex;
}
if (this.logger.isInfoEnabled()) {
long elapsedTime = System.currentTimeMillis() - startTime;
this.logger.info("FrameworkServlet '" + getServletName() + "': initialization completed in " +
elapsedTime + " ms");
}
}
下面是initWebApplicationContext()方法的具体代码
protected WebApplicationContext initWebApplicationContext() {
// <1> 获得根 WebApplicationContext 对象
WebApplicationContext rootContext = WebApplicationContextUtils.getWebApplicationContext(getServletContext());
// <2> 获得 WebApplicationContext wac 对象
WebApplicationContext wac = null;
// 第一种情况,如果构造方法已经传入 webApplicationContext 属性,则直接使用
if (this.webApplicationContext != null) {
// A context instance was injected at construction time -> use it
wac = this.webApplicationContext;
// 如果是 ConfigurableWebApplicationContext 类型,并且未激活,则进行初始化
if (wac instanceof ConfigurableWebApplicationContext) {
ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;
if (!cwac.isActive()) { // 未激活
// The context has not yet been refreshed -> provide services such as
// setting the parent context, setting the application context id, etc
if (cwac.getParent() == null) {
// The context instance was injected without an explicit parent -> set
// the root application context (if any; may be null) as the parent
cwac.setParent(rootContext);
}
// 配置和初始化 wac
configureAndRefreshWebApplicationContext(cwac);
}
}
}
// 第二种情况,从 ServletContext 获取对应的 WebApplicationContext 对象
if (wac == null) {
// No context instance was injected at construction time -> see if one
// has been registered in the servlet context. If one exists, it is assumed
// that the parent context (if any) has already been set and that the
// user has performed any initialization such as setting the context id
wac = findWebApplicationContext();
}
// 第三种,创建一个 WebApplicationContext 对象
if (wac == null) {
// No context instance is defined for this servlet -> create a local one
wac = createWebApplicationContext(rootContext);
}
// <3> 如果未触发刷新事件,则主动触发刷新事件
if (!this.refreshEventReceived) {
// Either the context is not a ConfigurableApplicationContext with refresh
// support or the context injected at construction time had already been
// refreshed -> trigger initial onRefresh manually here.
synchronized (this.onRefreshMonitor) {
onRefresh(wac);
}
}
// <4> 将 context 设置到 ServletContext 中
if (this.publishContext) {
// 将Spring子上下文存入ServletContext
String attrName = getServletContextAttributeName();
getServletContext().setAttribute(attrName, wac);
}
return wac;
}
最后看下DispatcherServlet中的onRefresh()方法
,这个方法初始化了很多策略:
注意:FrameWorkServlet.onRefresh()只有DispatcherServlet中进行重写
//初始化九大组件
protected void initStrategies(ApplicationContext context) {
initMultipartResolver(context);
initLocaleResolver(context);
initThemeResolver(context);
initHandlerMappings(context);
initHandlerAdapters(context);
initHandlerExceptionResolvers(context);
initRequestToViewNameTranslator(context);
initViewResolvers(context);
initFlashMapManager(context);
}
上面都是通过类似的机制,加载所有实现了该接口的类:
到此为止,SpringMVC的启动过程结束了。这边做下SpringMVC初始化总结:
- HttpServletBean的主要做一些初始化工作,将我们在web.xml中配置的参数设置到Servlet中;
- FrameworkServlet主要作用是
初始化Spring子上下文,设置其父上下文,并将其和ServletContext关联;
- DispatcherServlet:初始化各个功能的实现类。比如异常处理、视图处理、请求映射处理等。
时序图:
三、传统的Spring MVC项目启动流程
- 如果在web.xml中配置了org.springframework.web.context.ContextLoaderListener,
那么Tomcat在启动的时候会先加载父容器,并将其放到ServletContext中
; - 然后会加载DispatcherServlet(这块流程建议从init方法一步步往下看,流程还是很清晰的),因为DispatcherServlet实质是一个Servlet,所以会先执行它的init方法。这个init方法在HttpServletBean这个类中实现,其主要工作是做一些初始化工作,将我们在web.xml中配置的参数书设置到Servlet中,然后再触发FrameworkServlet的initServletBean()方法;
- FrameworkServlet
主要作用是初始化Spring子上下文,设置其父上下文,并将其放入ServletContext中;
- FrameworkServlet在调用initServletBean()的过程中同时会触发DispatcherServlet的onRefresh()方法,这个方法会初始化Spring MVC的各个功能组件。比如异常处理器、视图处理器、请求映射处理等。
四、SpringBoot集成SpringMVC
我们知道,SpringBoot的自动导入机制就是依赖spring framework内部使用的通用的工厂加载机制
。其导入链如下:
@SpringBootApplication->
@EnableAutoConfiguration>
@Import({AutoConfigurationImportSelector.class})
AutoConfigurationImportSelector类下的
List<String>configurations=this.getCandidateConfigurations(annotationMetadata,attributes);
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
getBeanClassLoader());
Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you "
+ "are using a custom packaging, make sure that file is correct.");
return configurations;
}
springFactoriesLoader是spring framework内部使用的通用的工厂加载机制,其可加载并实例化可能出现在classpath上的多个jar包中的META-INF/spring.factories文件中定义的指定类型的工厂,可视为一种类似于SPI的接口。
SpringBoot利用这种SPI接口实现了autoconfiguration机制:委托SpringFactoriesLoader来加载所有配置在META-INF/spring.factories中的org.springframework.boot.autoconfigure.EnableAutoConfiguration对应的值,spring-boot-autoconfiguration jar包中的META-INF/spring.factories中的EnableAutoConfiguration配置了众多供springboot导入的类
。我们先回想一下SpringBoot预先读取的可配置Config类有哪个是与WebMVC相关的?经过比对我们发现在Key=EnableAutoConfiguration,Value=WebMvcAutoConfiguration就是关于SpringBoot自动配置WebMVC的可配置类
,关于SpringBoot自动配置WebMVC的玄机也正是在这个配置类里面。
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration,\
于是,我们必须好好看一下这个可配置类到底做了什么事情,能让SpringBoot启动完成之后也自动初始化好了一个WebMVC的环境。
点开可以看到这个可配置类上有很多注解修饰,这些注解分别限定在某些条件满足或不满足情况下,spring才会初始化并配置这个Config类。关于这些注解和文字解释如下:
//声明为配置类,并且Bean的方法不进行代理
@Configuration(proxyBeanMethods = false)
//判断当前环境是一个SERVLET环境,也就是说是Web环境下这个配置类才生效
@ConditionalOnWebApplication(type = Type.SERVLET)
//判断当前环境是否含有Servlet,DispatcherServlet,WebMvcConfigurer实例(前面SpringMVC章节零XML方式介绍的接口,
//类似引入web.xml的Java实例),这些都是WebMVC必不可少的组件,只有这些都存在这个配置类才生效
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
//判断当前环境是否存在WebMvcConfigurationSupport,如果不存在这个配置类才生效
//为什么这里不存在WebMvcConfigurationSupport时才能让配置类生效呢?因为这个接口是一个自定义配置WebMVC的接口,
//如果实现了这个接口就意味着开发者自己手动进行了webMVC的配置,那么SpringBoot就不再帮你自动配置了,防止了配置冲突。
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
//自动配置顺序:数值越低越优先配置
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
//WebMVC配置的核心注解,主要是对DispatcherServlet进行Java配置,以及对任务执行和校验器进行Java配置。
//当这个配置类生效之后,就会接着进行DispatcherServletAutoConfiguration,TaskExecutionAutoConfiguration,
//ValidationAutoConfiguration的配置,重点看DispatcherServletAutoConfiguration
@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,
ValidationAutoConfiguration.class })
public class WebMvcAutoConfiguration {
.....
}
从上面可以看到,SpringBoot要想自动配置WebMVC环境,那么是需要满足上述注解定义的一些条件:
- 处于Web环境下
- 容器中已经初始化好了WebMVC必须的组件
- 用户没有自己手工配置过WebMVC
这些条件都满足后,SpringBoot就会为我们自动配置一个WebMVC环境,但是这个环境是怎么样的呢(比如采用什么容器,端口号是什么,DispatchServlet怎么配置,Resover怎么配置,Converter怎么配置,等等
)?这个就是最后一个注解@AutoConfigureAfter内声明的来定义了,实际上也就是由DispatcherServletAutoConfiguration.class通过Java零XML配置方式在代码里为我们提前写好定义的环境。
对于核心DispatcherServlet的自动配置,点开这个核心DispatcherServletAutoConfiguration.class
:
可以看到这个DispatcherServlet自动配置类也有很多注解修饰:
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
//如果环境中已经有了用户自己配置的DispatcherServlet,就不再自动配置
@ConditionalOnClass(DispatcherServlet.class)
//在这个DispatcherServlet自动配置之后,通过ServletWebServerFactoryAutoConfiguration对web容器进行配置
@AutoConfigureAfter(ServletWebServerFactoryAutoConfiguration.class)
在这个DispatcherServlet自动配置类中,最核心的就是对DispatcherServlet的配置,可以看到对DispatcherServlet配置的核心代码在静态内部类DispatcherServletConfiguration的dispatcherServlet方法上
,通过@Bean返回给Spring容器:
从这个方法内部我们就看出了,SpringBoot自动配置DispatcherServlet时是直接new的,然后设置各种参数最终通过@Bean交给Spring容器。
在上面的方法执行结束将一个DispatcherServlet交给Spring容器后,接下来会调用静态内部类DispatcherServletRegistrationConfiguration的dispatcherServletRegistration方法,对DispatcherServlet的参数进行配置
:
这里设置DispatcherServlet名字为dispatcherServlet;启动顺序是默认值-1(使用时加载)
;请求接收路径是/;配置完成后交给Spring容器,这就是对DispatcherServlet的Java代码配置了
。
不过这里有一个问题
,就是这里返回给Spring容器的是一个DispatcherServletRegistrationBean,那么Tomcat等Web容器时怎么将这个DispatcherServletRegistrationBean内包含的DispatcherServlet信息解析出来并生效运行呢?这个知识点有必要做一下说明。
首先看这个DispatcherServletRegistrationBean的构造是怎么样的:
这个DispatcherServletRegistrationBean继承了ServletRegistrationBean,这个ServletRegistrationBean是携带了供Tomcat容器解析加载的DispatcherServlet,让我们看下这个ServletRegistrationBean的继承关系图:
就可以知道这个DispatcherServletRegistrationBean实际最终是实现了ServletContextInitializer接口,看到这里如果前面对Servlet的SPI有所了解很快就能反应过来,这里是有一个onStartup方法通过SPI机制让Tomcat启动时能自动调用到onStartup里面来,通过下面调用链最终Spring容器进行了addServlet操作将DispatchServlet添加到Spring容器中并生效
,最后进行config方法对DispatcherServlet进行设置:
但是读了上面的源码又引发了两个很值得深思的问题
:
- 在静态内部类DispatcherServletConfiguration的dispatcherServlet方法上,通过new一个DispatcherServlet之后直接用@Bean返回给Spring容器了。在学习SpringMVC时我们知道DispatcherServlet需要传入一个ApplicationContext上下文,如果没有传递则会去默认配置文件解析出一个上下文。但是这里这个DispatcherServlet却没有关联任何Spring上下文的地方却能使DispatcherServlet起作用,那这个关联操作到底是在哪里进行的呢?
这需要我们看SpringBoot中DispatcherServlet的源码是怎么写的,让我们看下有参的构造是怎么样的,因为无参构造只是一个空方法:
使用IDEAJ的查找工具,看下这个this.webApplicationContext在哪些地方被设置,可以找到是在setApplicationContext方法内进行设置的:
看到这个重写方法以及方法注释,根据Spring的知识我们应该猜想到这个类应该是实现了ApplicationContextAware接口
。ApplicationContextAware接口是做什么的呢?这个接口是当Spring容器初始化结束之后,实现了ApplicationContextAware接口的实现类就会被调用并且执行setApplicationContext方法
。回到这里也就是说当Spring容器初始化完成之后,由于实现了ApplicationContextAware接口,于是会执行setApplicationContext方法,在这个方法中将初始化完成的Spring上下文赋值给this.webApplicationContext变量(这个变量就是DispatchServlet内部的Spring上下文变量),于是就解释了为什么SpringBoot在new一个DispatchServlet时不需要传入Spring上下文的原因
。
- 理解了上面的问题和答案,就引出第二个问题:
前面学习SpringMVC时知道对DispatcherServlet的配置是将Spring容器对象作为参数传给DispatcherServlet;但是SpringBoot自动配置WebMVC时却是将DispatcherServlet对象传给Spring容器;这两种场景的实现是反过来的,为什么会这样设计呢?其实这两种方式的区别就在于它们分别是怎么接在Spring容器的。
回答为什么SpringBoot是将DispatchServlet传给Spring容器的问题,就要结合上面第一个问题的解析。这是因为要调用实现了ApplicationContextAware接口的实现类,则必须保证这个实现类在Spring容器当中才能生效。也就是说SpringBoot中实现了ApplicationContextAware接口的DispatchServlet要想触发接口方法,则必须作为一个Bean存在于Spring容器中,这就是SpringBoot为什么要将DispatchServlet作为Bean传入到Spring容器中的原因了
。
- 上面介绍完SpringBoot对DispatchServlet进行自动配置的细节,那么还为我们做了哪些组件配置呢?这里要回过来看WebMvcAutoConfiguration
在这个自动配置类中,定义了一个静态内部类WebMvcAutoConfigurationAdapter,实现了WebMvcConfigurer接口。
这个就和之前学习SpringMVC应用时介绍的一样,通过实现WebMvcConfigurer接口就可以拥有一个类似web.xml功能,可以对其它组件进行配置操作:
public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer
在这个里面实现了默认的Resolver,MessageConverters,ViewResolver等组件
。如果我们要添加自定义的组件怎么办呢?对于SpringMVC而言我们必须在方法中将这个自定义的组件添加到对应组件集合中,但是SpringBoot对此进行的拓展,使得我们可以直接通过@Bean的方式就可以添加自定义组件(这一点在SpringMVC是做不到自定义添加的):
五、总结一下SpringBoot除了自动配置DispatchServlet之外,还配置了以下
- spring boot会默认注入一个视图解析器:
ContentNegotiatingViewResolver
主要做2个事情:
- 整合所有的视图解析器
- 遍历所有的视图解析器选一个最佳的方案
spring boot 会在当前的spring容器里面找到所有HttpMessageConverter类型的Bean 封装成一个集合
- spring boot会自己注入一个默认的实现 jackson
- 如果用户需要自己配置的话
只需要@Bean
- SpringBoot实现一个字符串到日期的参数转换器:
每当前端要将字符串日期传递到后台之后,要自动转换成日期格式类型时,可以写一个Converter进行转换。
至此,关于SpringBoot自动配置WebMVC的源码原理就分享到这里了。
- 以springmvc自动装配为例看底层源码细节:配置类WebMvcAutoConfiguration.
- 这个配置类有多个注解分别限定在某些条件满足或不满足情况下,spring才会初始化并配置这个Config类,
- 其中注解@AutoConfigureAfter中DispatcherServletAutoConfiguration是自动装配的核心类,
- 这里主要对DispatcherServlet进行配置,
和servlet被tomcat调用onStartup()一样
,- 需要先new出DispatcherServlet并放入Spring容器,然后在tomcat调用前进行addXXX参数的操作,
- 在springboot实现的javaConfig里,
filter,listener,DispatcherServlet,servlet这些servler子类都用XXXRegistrationBean来封装,
- 这些XXXRegistrationBean都实现了RegistrationBean超类(这个超类拥有onStartup方法),于是保证这些Bean都可以被 tomcat识别并执行,在执行各种不同类型的Bean时,会调用不同的register或configure方法进行add各自不同参数的方法,
- 从而实现了各种servlet组件参数的自动配置和加载步骤。
对于web容器的配置是ServletWebServerFactoryAutoConfiguration配置类中进行主要对web容器类型IP端口连接数等参数进行配置。