OUZHANBO

对于我这种菜鸡来说,毕业等于失业

0%

Javaweb从web.xml 到 springboot

Javaweb 从 web.xml 到 springboot

前言

这篇文章依然是参考了这位大佬的文章,讲得已经非常细了,真的非常建议去看下大佬的文章。

几乎所有人都是从 servlet,jsp,filter 开始编写自己的第一个 hello world 工程。那时,还离不开 web.xml 的配置,在 xml 文件中编写繁琐的 servlet 和 filter 的配置。随着 spring 的普及,配置逐渐演变成了两种方式—java configuration 和 xml 配置共存。现如今,springboot 的普及,java configuration 成了主流,xml 配置似乎已经“灭绝”了。不知道你有没有好奇过,这中间都发生了哪些改变,web.xml 中的配置项又是被什么替代项取代了?

servlet3.0 以前的时代(使用 web.xml 配置)

项目的结构和部分代码

图片丢失

1
2
3
4
5
6
7
8
9
public class HelloWorldServlet extends HttpServlet {

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/plain");
PrintWriter out = resp.getWriter();
out.println("hello world");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class HelloWorldFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {

}

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("触发 hello world 过滤器...");
filterChain.doFilter(servletRequest,servletResponse);
}

@Override
public void destroy() {

}
}

在 servlet3.0 之前需要将需要上面的 HelloWorldServlet 和 HelloWorldFilter 配置在WEB-INF/web.xml文件里面才会生效

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?xml version="1.0" encoding="UTF-8"?>
<web-app >
<display-name>Archetype Created Web Application</display-name>


<servlet>
<servlet-name>HelloWorldServlet</servlet-name>
<servlet-class>top.ouzhanbo.webxml.servlet.HelloWorldServlet</servlet-class>
</servlet>

<servlet-mapping>
<servlet-name>HelloWorldServlet</servlet-name>
<url-pattern>/hello</url-pattern>
</servlet-mapping>

<filter>
<filter-name>HelloWorldFilter</filter-name>
<filter-class>top.ouzhanbo.webxml.filter.HelloWorldFilter</filter-class>
</filter>

<filter-mapping>
<filter-name>HelloWorldFilter</filter-name>
<url-pattern>/hello</url-pattern>
</filter-mapping>
</web-app>

servlet3.0 新特性

Servlet 3.0 作为 Java EE 6 规范体系中一员,随着 Java EE 6 规范一起发布。该版本在前一版本(Servlet 2.5)的基础上提供了若干新特性用于简化 Web 应用的开发和部署。其中一项新特性便是提供了无 xml 配置的特性。

servlet3.0 首先提供了 @WebServlet,@WebFilter 等注解,这样便有了抛弃 web.xml 的第一个途径,凭借注解声明 servlet 和 filter 来做到这一点。

除了这种方式,servlet3.0 规范还提供了更强大的功能,可以在运行时动态注册 servlet ,filter,listener。以 servlet 为例,过滤器与监听器与之类似。ServletContext 为动态配置 Servlet 增加了如下方法:

  • ServletRegistration.Dynamic addServlet(String servletName,Class<? extends Servlet> servletClass)
  • ServletRegistration.Dynamic addServlet(String servletName, Servlet servlet)
  • ServletRegistration.Dynamic addServlet(String servletName, String className)
  • T createServlet(Class clazz)
  • ServletRegistration getServletRegistration(String servletName)
  • Map getServletRegistrations()

其中前三个方法的作用是相同的,只是参数类型不同而已;通过 createServlet()方法创建的 Servlet,通常需要做一些自定义的配置,然后使用 addServlet() 方法来将其动态注册为一个可以用于服务的 Servlet。两个 getServletRegistration() 方法主要用于动态为 Servlet 增加映射信息,这等价于在 web.xml 中使用 标签为存在的 Servlet 增加映射信息。上面的 ServletRegistration.Dynamic 看源码发现其实他是继承了 ServletRegistration(FilterRegistration 也是类似)

1
2
3
4
5
6
7
8
9
10
public interface ServletRegistration extends Registration {

public static interface Dynamic extends ServletRegistration, Registration.Dynamic {
public void setLoadOnStartup(int loadOnStartup);
public Set<String> setServletSecurity(ServletSecurityElement constraint);
public void setMultipartConfig(MultipartConfigElement multipartConfig);
public void setRunAsRole(String roleName);
}

}

以上 ServletContext 新增的方法要么是在 ServletContextListener 的 contexInitialized 方法中调用,要么是在 ServletContainerInitializer 的 onStartup() 方法中调用。

ServletContainerInitializer 也是 Servlet 3.0 新增的一个接口,容器在启动时使用 JAR 服务 API(JAR Service API) 来发现 ServletContainerInitializer 的实现类,并且容器将 WEB-INF/lib 目录下 JAR 包中的类都交给该类的 onStartup()方法处理,我们通常需要在该实现类上使用 @HandlesTypes 注解来指定希望被处理的类,过滤掉不希望给 onStartup() 处理的类。

servlet3.0+的项目结构和部分代码

图片丢失

我并未对 HelloWorldServlet 和 HelloWorldFilter 做任何改动,而是新增了一个 CustomServletContainerInitializer , 它实现了 javax.servlet.ServletContainerInitializer 接口,用来在 web 容器启动时加载指定的 servlet 和 filter,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class CustomServletContainerInitializer implements ServletContainerInitializer {

private final static String JAR_HELLO_URL = "/hello";

@Override
public void onStartup(Set<Class<?>> c, ServletContext servletContext) throws ServletException{

System.out.println("创建 helloWorldServlet...");

ServletRegistration.Dynamic servlet = servletContext.addServlet(
HelloWorldServlet.class.getSimpleName(),
HelloWorldServlet.class);
servlet.addMapping(JAR_HELLO_URL);

System.out.println("创建 helloWorldFilter...");

FilterRegistration.Dynamic filter = servletContext.addFilter(
HelloWorldFilter.class.getSimpleName(), HelloWorldFilter.class);

EnumSet<DispatcherType> dispatcherTypes = EnumSet.allOf(DispatcherType.class);
dispatcherTypes.add(DispatcherType.REQUEST);
dispatcherTypes.add(DispatcherType.FORWARD);

filter.addMappingForUrlPatterns(dispatcherTypes, true, JAR_HELLO_URL);

}
}

ServletContext 我们称之为 servlet 上下文,它维护了整个 web 容器中注册的 servlet,filter,listener,以 servlet 为例,可以使用 servletContext.addServlet 等方法来添加 servlet。在上面没用到方法入参中Set<Class<?>> c和注解@HandlesTypes,这里说下这两个东西的作用,@HandlesTypes注解上可以指定需要处理的类,onStartup 方法被调用时将@HandlesTypes中指定类的子类(一定记住是子类,该类的本身不算)放入Set<Class<?>> c这个集合中,如果没有使用@HandlesTypes注解指定处理的类,那Set<Class<?>> c就为null

图片丢失

图片丢失

图片丢失

这么声明一个 ServletContainerInitializer 的实现类,web 容器并不会识别它,所以,需要借助 SPI 机制(如果不懂 SPI 的可以看看这篇文章)来指定该初始化类,这一步骤是通过在项目路径下创建 META-INF/services/javax.servlet.ServletContainerInitializer 来做到的,它只包含一行内容:

1
top.ouzhanbo.websci.CustomServletContainerInitializer

使用 ServletContainerInitializer 和 SPI 机制,我们的 web 应用便可以彻底摆脱 web.xml 了。

在 ContextConfig 类中可以看到通过 SPI 加载 ServletContainerInitializer 的实现类的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
List<ServletContainerInitializer> detectedScis;
try {
WebappServiceLoader<ServletContainerInitializer> loader = new WebappServiceLoader<>(context);
//SPI加载ServletContainerInitializer的实现类
detectedScis = loader.load(ServletContainerInitializer.class);
} catch (IOException e) {
log.error(sm.getString(
"contextConfig.servletContainerInitializerFail",
context.getName()),
e);
ok = false;
return;
}

在 StandardContext 类中可以看到 ServletContainerInitializer 的 onStartup 方法被调用的代码

1
2
3
4
5
6
7
8
9
10
11
12
// Call ServletContainerInitializers
for (Map.Entry<ServletContainerInitializer, Set<Class<?>>> entry :
initializers.entrySet()) {
try {
entry.getKey().onStartup(entry.getValue(),
getServletContext());
} catch (ServletException e) {
log.error(sm.getString("standardContext.sciFail"), e);
ok = false;
break;
}
}

Spring 是如何支持 servlet3.0 的?

在我最初接触 spring 的时候虽然已经有 servlet3.0 但是那时候都是在网上找一些视频来学习的,基本都是学到能用就觉得可以了,当时学的 springmvc+spring+mybatis 里面的 spirngmvc 和 spring 的配置都是用 xml 的,但是当时其实不是很懂 web.xml 中配的那个 listener 和 servlet 标签里面的那个初始化参数标签 init-param 里面那个东西有什么用,后来 springboot 出来后基本就用 springboot 了基本没在用过 ssm 那一套了,所以 spring 搭配 servlet3.0 那套无 xml 配置的方式我完全没用过,看了这位大佬的文章加上百度再搜索了一波资料加上自己调试,现在大概弄懂了用 xml 和用 javaConfig 这两种方式浅显的原理。

使用 xml 配置的方式

先看看项目的结构和部分的代码

图片丢失

代码和配置如下

web.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
id="WebApp_ID" version="3.0">
<display-name>Archetype Created Web Application</display-name>


<context-param>
<!-- servlet容器初始化时的参数,可以通过servletContext.getInitParameter("contextConfigLocation")获取 -->
<param-name>contextConfigLocation</param-name>
<param-value>classpath*:/application.xml</param-value>
</context-param>

<listener>
<!-- 配置servlet容器的监听器 -->
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>


<servlet>
<servlet-name>dispatcherServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!-- servlet的初始化时的参数,可以在DispatcherServlet里通过getInitParameter("contextConfigLocation")获取 -->
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath*:/application-mvc.xml</param-value>
</init-param>
<!-- servlet的加载顺序,如果是为负数就是在第一次请求该servlet时候才会加载,当值为0或者大于0时,表示容器在应用启动时就加载并初始化这个servlet,正数的值越小,该servlet的优先级越高,应用启动时就越先加载 -->
<load-on-startup>1</load-on-startup>
</servlet>

<servlet-mapping>
<servlet-name>dispatcherServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>

Spring 的配置 application.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.1.xsd">

<!-- spring容器扫描 -->
<context:component-scan base-package="top.ouzhanbo.webspring">
<!-- 不扫描spring mvc的controller -->
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>

</beans>

SpringMvc 的配置 application-mvc.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.1.xsd">

<!--spring mvc扫描controller不扫描service-->
<context:component-scan base-package="top.ouzhanbo.webspring">
<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Service"/>
</context:component-scan>
</beans>

MyController 的代码

1
2
3
4
5
6
7
8
9
10
11
12
@Controller
public class MyController{

@Resource
ApplicationContext applicationContext;

@RequestMapping(value="/hello")
@ResponseBody
public String hello(){
return "hello";
}
}

我们在 web.xml 上面配置的监听器 ContextLoaderListener 继承 ContextLoader 类并实现了 ServletContextListener 接口,实现了 ServletContextListener 接口的监听器会监听 servelet 容器,在 servelet 容器启动和关闭时会触发该监听器

ServletContextListener

1
2
3
4
5
6
7
8
public interface ServletContextListener extends EventListener {

//servlet容器启动时调用
default public void contextInitialized(ServletContextEvent sce) {}

//servlet容器销毁时调用
default public void contextDestroyed(ServletContextEvent sce) {}
}

ContextLoaderListener

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
ContextLoaderListener extends ContextLoader implements ServletContextListener {
public ContextLoaderListener() {
}

public ContextLoaderListener(WebApplicationContext context) {
super(context);
}


@Override
public void contextInitialized(ServletContextEvent event) {
//创建并且初始化Spring容器
initWebApplicationContext(event.getServletContext());
}

@Override
public void contextDestroyed(ServletContextEvent event) {
closeWebApplicationContext(event.getServletContext());
ContextCleanupListener.cleanupAttributes(event.getServletContext());
}

}

查看 Tomcat 源码,在 StandardContext 里面的 listenerStart 方法里面有段代码就是获取 ServletContextListener 接口的实现类并且调用它的 contextInitialized 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
Object instances[] = getApplicationLifecycleListeners();
if (instances == null || instances.length == 0) {
return ok;
}

ServletContextEvent event = new ServletContextEvent(getServletContext());
ServletContextEvent tldEvent = null;
if (noPluggabilityListeners.size() > 0) {
noPluggabilityServletContext = new NoPluggabilityServletContext(getServletContext());
tldEvent = new ServletContextEvent(noPluggabilityServletContext);
}
for (Object instance : instances) {
if (!(instance instanceof ServletContextListener)) {
continue;
}
//获取实现了ServletContextListener的监听器
ServletContextListener listener = (ServletContextListener) instance;
try {
fireContainerEvent("beforeContextInitialized", listener);
if (noPluggabilityListeners.contains(listener)) {
listener.contextInitialized(tldEvent);
} else {
//调用contextInitialized方法
listener.contextInitialized(event);
}
fireContainerEvent("afterContextInitialized", listener);
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
fireContainerEvent("afterContextInitialized", listener);
getLogger().error(sm.getString("standardContext.listenerStart",
instance.getClass().getName()), t);
ok = false;
}
}

之后就是调用 ContextLoader 的 initWebApplicationContext 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
//先判断是不是已经创建过了spring的根容器(大部分被spring管理的类都放在这里面,比如被@Service @@Repository,@Component注解标注的类)
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 {
// Store context in local instance variable, to guarantee that
// it is available on ServletContext shutdown.
if (this.context == null) {
//如果没有在context-param里面指定ContextLoader里面的contextClass的值的话,默认使用的时org.springframework.web.context.support.XmlWebApplicationContext这个类的创建的容器(感兴趣的话可以点进去看下就知道了)
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);
}
//最终会到这里,看方法名配置并刷新web应用容器
configureAndRefreshWebApplicationContext(cwac, servletContext);
}
}
//将spring容器保存到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;
}
}

configureAndRefreshWebApplicationContext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class ContextLoader {
public static final String CONFIG_LOCATION_PARAM = "contextConfigLocation";

protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc) {
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
String idParam = sc.getInitParameter(CONTEXT_ID_PARAM);
if (idParam != null) {
wac.setId(idParam);
}
else {
// Generate default id...
wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +
ObjectUtils.getDisplayString(sc.getContextPath()));
}
}

wac.setServletContext(sc);
//这里的CONFIG_LOCATION_PARAM就是contextConfigLocation,这里就是获取到在web.xml中context-param配置contextConfigLocation的值classpath*:/application.xml
String configLocationParam = sc.getInitParameter(CONFIG_LOCATION_PARAM);
if (configLocationParam != null) {
//将spring容器的配置文件application.xml的放入到容器中,后面调用wac.refresh()会用到
wac.setConfigLocation(configLocationParam);
}

// 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(sc, null);
}

customizeContext(sc, wac);
wac.refresh();
}

在 refresh 方法里面的层层深入后 XmlWebApplicationContext 这个类的 loadBeanDefinitions 方法里面看到之前保存的 spring 容器的配置文件路径

图片丢失

之后就是 spring 的加载配置文件的源码了,这里就不展开了(我也没怎么看过),讲那么多其实最关键的就是知道 Spring 容器是通过在 Servlet 容器初始化的时候调用监听器 ContextLoaderListener 的 contextInitialized 方法加载到 Servlet 容器中的。

那么 springmvc 的容器是在哪里加载进来的呢?其实不难猜到就在 DispatcherServlet 初始化的时候加载进来的,在 StandardWrapper 里的 loadServlet 方法里通过反射的方式创建了 servlet 实例并且将其返回给了上层的 load 方法,并且在上层的 load 方法中通过调用 initServlet 方法最终调用到 servlet 的 init 方法。

图片丢失

图片丢失

图片丢失

如果 servlet 是 FrameworkServlet 类或者其子类(DispatcherServlet 继承 FrameworkServlet)的实例那么层层深入后最终会调用到 FrameworkServlet 的 initWebApplicationContext 方法

initWebApplicationContext 方法的的代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
protected WebApplicationContext initWebApplicationContext() {
//这里代码如果深入的话会发现这里的rootContext就是我们上面保存到servlet容器里面的spring容器
WebApplicationContext rootContext =
WebApplicationContextUtils.getWebApplicationContext(getServletContext());
WebApplicationContext wac = null;

if (this.webApplicationContext != null) {
// A context instance was injected at construction time -> use it
wac = this.webApplicationContext;
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);
}
configureAndRefreshWebApplicationContext(cwac);
}
}
}
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();
}
if (wac == null) {
//会调用到这里
// No context instance is defined for this servlet -> create a local one
wac = createWebApplicationContext(rootContext);
}

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);
}
}

if (this.publishContext) {
// Publish the context as a servlet context attribute.
//attraName的值由FrameworkServlet.class.getName() + ".CONTEXT." + getServletName()组合成
String attrName = getServletContextAttributeName();
//将springmvc容器以attraName的名字保存在servlet容器里
getServletContext().setAttribute(attrName, wac);
}

return wac;
}

initWebApplicationContext 会调用到 createWebApplicationContext,最会调用到 createWebApplicationContext(@Nullable ApplicationContext parent) 方法

createWebApplicationContext(@Nullable ApplicationContext parent) 方法的代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
protected WebApplicationContext createWebApplicationContext(@Nullable ApplicationContext parent) {
//可以在web.xml里面servlet标签里面的init-param标签里设置contextClass的值,默认值是XmlWebApplicationContext.class
Class<?> contextClass = getContextClass();
if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
throw new ApplicationContextException(
"Fatal initialization error in servlet with name '" + getServletName() +
"': custom WebApplicationContext class [" + contextClass.getName() +
"] is not of type ConfigurableWebApplicationContext");
}
ConfigurableWebApplicationContext wac =
(ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);

wac.setEnvironment(getEnvironment());
wac.setParent(parent);
//可以在web.xml里面servlet标签里面的init-param标签里设置contextConfigLocation的值,这里获取到值是classpath*:/application-mvc.xml
String configLocation = getContextConfigLocation();
if (configLocation != null) {
wac.setConfigLocation(configLocation);
}
configureAndRefreshWebApplicationContext(wac);

return wac;
}

上面 contextClass 和 contextConfigLocation 的值都是在 HttpServletBean(FrameworkServlet 的父类)的 init 方法中被赋予

图片丢失

最终将 springmvc 的容器保存在 servlet 容器中,这样 springmvc 就成功的加载进来了

完全无 xml 的配置方式

项目的结构和部分的代码

图片丢失

MyConfig 配置类的效果和上面用 xml 的方式时的 application.xml 类似,主要的针对 Spring 容器的,上面的@ComponentScan 注解的内容是告诉 Spring 容器扫描并且加载 top.ouzhanbo.webspring 下的所有被@Service 注解标注的类,useDefaultFilters = false 是为了防止 spring 默认会自动发现被 @Component、@Repository、@Service 和 @Controller 标注的类,并注册进容器中这行为。

1
2
3
4
5
@Configuration
//告诉Spring容器扫描并且加载top.ouzhanbo.webspring下的所有被@Service注解标注的类
@ComponentScan(value = "top.ouzhanbo.webspring",includeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION, classes = {Service.class})},useDefaultFilters = false)
public class MyConfig {
}

MyMvcConfig 配置类和上面的 MyConfig 配置类差不多,只是这个是针对 SpringMvc 容器的

1
2
3
4
5
@Configuration
//告诉SpringMvc容器扫描并且加载top.ouzhanbo.webspring下的所有被@Controller注解标注的类
@ComponentScan(value = "top.ouzhanbo.webspring",includeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION, classes = {Controller.class})},useDefaultFilters = false)
public class MyMvcConfig {
}

MyController 的代码很简单,就是返回一个 hello 字符串

1
2
3
4
5
6
7
8
9
10
@Controller
public class MyController{

@RequestMapping(value="/hello")
@ResponseBody
public String hello(){
return "hello";
}

}

WebInitializer 这个类这里先不介绍后面再说

前面介绍的 servlet3.0 下是如何通过 SPI 的方式注册 Servlet、Filter、Listener 的,其实在无 xml 配置的情况下 Spring 也是通过这种方式来注册 Servlet、Filter、Listener,找到 ServletContainerInitializer 的实现类 SpringServletContainerInitializer

图片丢失

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {

@Override
public void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)
throws ServletException {

List<WebApplicationInitializer> initializers = Collections.emptyList();

if (webAppInitializerClasses != null) {
initializers = new ArrayList<>(webAppInitializerClasses.size());
for (Class<?> waiClass : webAppInitializerClasses) {
// Be defensive: Some servlet containers provide us with invalid classes,
// no matter what @HandlesTypes says...
if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) &&
WebApplicationInitializer.class.isAssignableFrom(waiClass)) {
try {
//获取到所有WebApplicationInitializer类的子类(除了接口和抽象类)并且通过反射创建该类的实例
initializers.add((WebApplicationInitializer)
ReflectionUtils.accessibleConstructor(waiClass).newInstance());
}
catch (Throwable ex) {
throw new ServletException("Failed to instantiate WebApplicationInitializer class", ex);
}
}
}
}

if (initializers.isEmpty()) {
servletContext.log("No Spring WebApplicationInitializer types detected on classpath");
return;
}

servletContext.log(initializers.size() + " Spring WebApplicationInitializers detected on classpath");
AnnotationAwareOrderComparator.sort(initializers);
for (WebApplicationInitializer initializer : initializers) {
//调用WebApplicationInitializer的onStartup方法
initializer.onStartup(servletContext);
}
}

}

SpringServletContainerInitializer 的 onStartup 方法的作用就是找到所有 WebApplicationInitializer 类的子类(除了接口和抽象类)并且调用它们的 onStartup 方法(套娃吖),而我们上面的 WebInitializer 就是 WebApplicationInitializer 的子类

WebInitializer 的继承关系

图片丢失

WebInitializer 的代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class WebInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {


@Override
protected Class<?>[] getRootConfigClasses() {
//返回Spring容器对应的配置类
return new Class[]{MyConfig.class};
}

@Override
protected Class<?>[] getServletConfigClasses() {
//返回SpringMvc容器对应的配置类
return new Class[]{MyMvcConfig.class};
}

@Override
protected String[] getServletMappings() {
//返回DispatcherServlet的拦截路径,在ServletRegistration.Dynamic的addMapping方法里面用到
return new String[]{"/"};
}
}

WebInitializer 的 onStartup 的方法在它的父类 AbstractDispatcherServletInitializer 中实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
public abstract class AbstractDispatcherServletInitializer extends AbstractContextLoaderInitializer {

public static final String DEFAULT_SERVLET_NAME = "dispatcher";


//WebInitializer的onStartup的方法在它的父类AbstractDispatcherServletInitializer中实现
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
//调用父类的AbstractContextLoaderInitializer的onStartup方法
super.onStartup(servletContext);
//主要的作用是创建SpringMvc容器并且将DispatcherServlet注册到Servlet容器中
registerDispatcherServlet(servletContext);
}


protected void registerDispatcherServlet(ServletContext servletContext) {
//默认的名字是dispatcher
String servletName = getServletName();
Assert.hasLength(servletName, "getServletName() must not return null or empty");

//createServletApplicationContext这个方法在其子类AbstractAnnotationConfigDispatcherServletInitializer中实现,起作用是创建并且返回spirngMvc容器
WebApplicationContext servletAppContext = createServletApplicationContext();
Assert.notNull(servletAppContext, "createServletApplicationContext() must not return null");
//createDispatcherServlet创建并且返回DispatcherServlet实例
FrameworkServlet dispatcherServlet = createDispatcherServlet(servletAppContext);
Assert.notNull(dispatcherServlet, "createDispatcherServlet(WebApplicationContext) must not return null");
dispatcherServlet.setContextInitializers(getServletApplicationContextInitializers());

//将DispatcherServlet注册到Servlet容器中
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.");
}

//这里的代码的作用和在web.xml配置的<load-on-startup>1</load-on-startup>一样
registration.setLoadOnStartup(1);
//这里getServletMappings方法调用的就是是我在WebInitializer实现的getServletMappings方法
registration.addMapping(getServletMappings());
registration.setAsyncSupported(isAsyncSupported());

Filter[] filters = getServletFilters();
if (!ObjectUtils.isEmpty(filters)) {
for (Filter filter : filters) {
//将针对DispatcherServlet的Filter注册到Servlet容器中
registerServletFilter(servletContext, filter);
}
}
//用户可以重写这个方法对DispatcherServlet做一些配置,例如我想设置一下DispatcherServlet的加载顺序就重写该方法在里面加一个语句registration.setLoadOnStartup(2);
customizeRegistration(registration);
}


protected String getServletName() {
return DEFAULT_SERVLET_NAME;
}

protected abstract WebApplicationContext createServletApplicationContext();

//创建并且返回DispatcherServlet实例
protected FrameworkServlet createDispatcherServlet(WebApplicationContext servletAppContext) {
return new DispatcherServlet(servletAppContext);
}

@Nullable
protected ApplicationContextInitializer<?>[] getServletApplicationContextInitializers() {
return null;
}


protected abstract String[] getServletMappings();

@Nullable
protected Filter[] getServletFilters() {
return null;
}


protected FilterRegistration.Dynamic registerServletFilter(ServletContext servletContext, Filter filter) {
String filterName = Conventions.getVariableName(filter);
Dynamic registration = servletContext.addFilter(filterName, filter);

if (registration == null) {
int counter = 0;
while (registration == null) {
if (counter == 100) {
throw new IllegalStateException("Failed to register filter with name '" + filterName + "'. " +
"Check if there is another filter registered under the same name.");
}
registration = servletContext.addFilter(filterName + "#" + counter, filter);
counter++;
}
}

registration.setAsyncSupported(isAsyncSupported());
registration.addMappingForServletNames(getDispatcherTypes(), false, getServletName());
return registration;
}

private EnumSet<DispatcherType> getDispatcherTypes() {
return (isAsyncSupported() ?
EnumSet.of(DispatcherType.REQUEST, DispatcherType.FORWARD, DispatcherType.INCLUDE, DispatcherType.ASYNC) :
EnumSet.of(DispatcherType.REQUEST, DispatcherType.FORWARD, DispatcherType.INCLUDE));
}

protected boolean isAsyncSupported() {
return true;
}
//用户可以重写这个方法对DispatcherServlet做一些配置
protected void customizeRegistration(ServletRegistration.Dynamic registration) {
}

}

AbstractDispatcherServletInitializer 的父类 AbstractContextLoaderInitializer 的代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public abstract class AbstractContextLoaderInitializer implements WebApplicationInitializer {

protected final Log logger = LogFactory.getLog(getClass());


//在子类AbstractDispatcherServletInitializer中通过super.onStartup(servletContext)调用该方法
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
//创建Spring容器和Servlet容器的监听器,并且将监听器注册到Servlet容器中
registerContextLoaderListener(servletContext);
}


protected void registerContextLoaderListener(ServletContext servletContext) {
//createRootApplicationContext这个方法在其子类AbstractAnnotationConfigDispatcherServletInitializer中实现,这个方法创建并且返回一个spring容器
WebApplicationContext rootAppContext = createRootApplicationContext();
if (rootAppContext != null) {
//创建Servlet容器的监听器(ContextLoaderListener实现了ServletContextListener),并且将Spring容器的实例作为参数传入
ContextLoaderListener listener = new ContextLoaderListener(rootAppContext);
listener.setContextInitializers(getRootApplicationContextInitializers());
//将监听器注册到servlet容器中
servletContext.addListener(listener);
}
else {
logger.debug("No ContextLoaderListener registered, as " +
"createRootApplicationContext() did not return an application context");
}
}


@Nullable
protected abstract WebApplicationContext createRootApplicationContext();


@Nullable
protected ApplicationContextInitializer<?>[] getRootApplicationContextInitializers() {
return null;
}

}

AbstractDispatcherServletInitializer 的 createServletApplicationContext 方法和 AbstractContextLoaderInitializer 的 createRootApplicationContext 方法都是在子类 AbstractAnnotationConfigDispatcherServletInitializer 中实现的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public abstract class AbstractAnnotationConfigDispatcherServletInitializer
extends AbstractDispatcherServletInitializer {


@Override
@Nullable
protected WebApplicationContext createRootApplicationContext() {
//getRootConfigClasses这个方法在我实现的子类WebInitializer中有实现了,返回的是Spring容器的配置类,在这个案例就是MyConfig.class
Class<?>[] configClasses = getRootConfigClasses();
if (!ObjectUtils.isEmpty(configClasses)) {
//创建Spring容器实例
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
//将配置类注册到容器中
context.register(configClasses);
return context;
}
else {
return null;
}
}

@Override
protected WebApplicationContext createServletApplicationContext() {
//创建SpringMvc容器
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
//getServletConfigClasses这个方法在我实现的子类WebInitializer中有实现了,返回的是SpringMvc容器的配置类,在这个案例就是MyMvcConfig.class
Class<?>[] configClasses = getServletConfigClasses();
if (!ObjectUtils.isEmpty(configClasses)) {
context.register(configClasses);
}
return context;
}

@Nullable
protected abstract Class<?>[] getRootConfigClasses();


@Nullable
protected abstract Class<?>[] getServletConfigClasses();

}

看完上面代码和注释可以看出 WebInitializer 的 onStartup 方法其实主要做了几件事:

  1. 创建 Spring 容器,获取 Spring 容器对应的配置类并且把配置类注册到 Spring 容器,将 Spring 容器作为参数传入创建 Servlet 容器的监听器(ContextLoaderListener 实现了 ServletContextListener),将监听器注册到 servlet 容器中
  2. 创建 SpringMvc 容器,获取 SpringMvc 容器对应的配置类并且把配置类注册到 SpringMvc 容器,将 SpringMvc 容器作为参数传入创建 DispatcherServlet 实例,并且将 DispatcherServlet 实例注册 Servlet 容器中

之后的流程就和上面使用 xml 的方式差不多,在 Servelt 容器监听器 ContextLoaderListener 里的 contextInitialized 方法里面对 Spring 容器进初始化等一些操作,并且将 Spring 容器保存到 servlet 容器中,SpringMvc 容器也是在 Servlet 的 init 方法里面对 SpringMvc 容器进行初始化等一些操作,最终将 SpringMvc 容保存到 servlet 容器中。这里只是简单的描述一下,具体的流程可以按照上面 xml 里面分析的流程去调试代码,这样会比较清晰而且会有不错的收获。

SpringBoot 是如何加载 Servlet 的

SpringBoot 加载 servlet 和 filter 的方式和前面说的使用 web,xml 和使用 SPI 来加载 ServletContainerInitializer 实现类的方法都不同。下面先来看看在 SpringBoot 中 servlet 和 filter 的几种注册方式

注册方式一:servlet3.0 注解 +@ServletComponentScan

SpringBoot 兼容 servlet3.0,可以使用一系列以 Web*开头的注解:@Servlet、@Filter、@WebListener

使用 Servlet3.0 提供的@Servlet 和@WebFilter 注解+@ServletComponentScan 注解

MyFilter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//里面的initParams的参数类似于在web.xml中filter标签里面的init-param标签里设置的一些初始化参数,这些参数可以通过getInitParameter方法获取
@WebFilter(value = "/myServlet",initParams = {@WebInitParam(name = "filterName",value = "myFilter"),@WebInitParam(name = "msg",value = "被我过滤了")})
public class MyFilter extends HttpFilter {
@Override
protected void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
response.setContentType("text/html;charset=utf-8");
String msg = getInitParameter("filterName") +
":" +
getInitParameter("msg");
response.getOutputStream().write(msg.getBytes(StandardCharsets.UTF_8));
response.getOutputStream().write("<br>".getBytes(StandardCharsets.UTF_8));
chain.doFilter(request,response);
}
}

MyServlet

1
2
3
4
5
6
7
@WebServlet(value = "/myServlet")
public class MyServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getOutputStream().write("myServlet:哈喽啊!!!".getBytes(StandardCharsets.UTF_8));
}
}

这是几种方式中最为简洁的方式,如果真的有特殊需求,需要在 springboot 下注册 servlet,filter,可以采用这样的方式,比较直观。

注册方式二:RegistrationBean

MyConfig

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Configuration
public class MyConfig {

@Bean
public ServletRegistrationBean<HelloWorldServlet> helloWorldServlet() {
ServletRegistrationBean<HelloWorldServlet> helloWorldServlet = new ServletRegistrationBean<>();
helloWorldServlet.addUrlMappings("/hello");
helloWorldServlet.setServlet(new HelloWorldServlet());
return helloWorldServlet;
}

@Bean
public FilterRegistrationBean<HelloWorldFilter> helloWorldFilter() {
FilterRegistrationBean<HelloWorldFilter> helloWorldFilter = new FilterRegistrationBean<>();
helloWorldFilter.addUrlPatterns("/hello/*");
helloWorldFilter.setFilter(new HelloWorldFilter());
return helloWorldFilter;
}

}

HelloWorldFilter

1
2
3
4
5
6
7
public class HelloWorldFilter extends HttpFilter {
@Override
protected void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
System.out.println("helloWorldFilter:被我拦截了");
chain.doFilter(request,response);
}
}

HelloWorldServlet

1
2
3
4
5
6
7
public class HelloWorldServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html;charset=utf-8");
resp.getOutputStream().write("helloWorldServlet:被我的处理了".getBytes(StandardCharsets.UTF_8));
}
}

ServletRegistrationBean 和 FilterRegistrationBean 都继承自 RegistrationBean ,RegistrationBean 是 springboot 中广泛应用的一个注册类,负责把 servlet,filter,listener 给容器化,使他们被 spring 托管,并且完成自身对 web 容器的注册。这种注册方式也值得推崇。

图片丢失

RegistrationBean 这个类的几个子类 ServletRegistrationBean、FilterRegistrationBean、ServletListenerRegistrationBean 的作用分别是帮助容器注册 servlet、filter、listener,DelegatingFilterProxyRegistrationBean 这个是用在 SpringSecurity 里的,这笔记力线不管,之后学习 SpringSecurity 的时候才会用到。在这里还需要留意 RegistrationBean 这个类实现了 ServletContextInitializer 接口(注意他和上面说的 ServletContainerInitializer 不是同一个东西),这个接口是一个核心接口,后面还会提到,这里先混个眼熟。

SpringBoot 内置 tomcat 容器加载 servlet 源码分析

图片丢失

SpringBoot 使用内置 tomcat 的情况下没有使用前面说到的 SpringServletContainerInitializer,而是使用 ServletContainerInitializer 的另一个实现类 TomcatStarter,但是这个 TomcatStarter 的调用并依赖不依赖于 servlet3.0 规范和 SPI,它走的是另外一套新的逻辑,在 Spring github 的 issue 中作者说了:https://github.com/spring-projects/spring-boot/issues/321

This was actually an intentional design decision. The search algorithm used by the containers was problematic. It also causes problems when you want to develop an executable WAR as you often want a javax.servlet.ServletContainerInitializer for the WAR that is not executed when you run java -jar.

See the org.springframework.boot.context.embedded.ServletContextInitializer for an option that works with Spring Beans.

springboot 这么做是有意而为之。springboot 考虑到了如下的问题,我们在使用 springboot 时,开发阶段一般都是使用内嵌 tomcat 容器,但部署时却存在两种选择:一种是打成 jar 包,使用 java -jar 的方式运行;另一种是打成 war 包,交给外置容器去运行。前者就会导致容器搜索算法出现问题,因为这是 jar 包的运行策略,不会按照 servlet3.0 的策略去加载 ServletContainerInitializer!最后作者还提供了一个替代选项:ServletContextInitializer,前文还提到 RegistrationBean 实现了 ServletContextInitializer 接口。

在 TomcatStarter 中的 ServletContextInitializer[] initializers 是 springboot 注册 servlet,filter,listener 的关键,先看看 TomcatStarter 的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class TomcatStarter implements ServletContainerInitializer {

private static final Log logger = LogFactory.getLog(TomcatStarter.class);

//存放实现了ServletContextInitializer接口的类的实例,例如ServletRegistrationBean、FilterRegistrationBean、ServletListenerRegistrationBean
private final ServletContextInitializer[] initializers;

private volatile Exception startUpException;

TomcatStarter(ServletContextInitializer[] initializers) {
this.initializers = initializers;
}


@Override
public void onStartup(Set<Class<?>> classes, ServletContext servletContext) throws ServletException {
try {
//这里获取到的ServletContextInitializer的实例并不是我们上面说的RegistrationBean的子类实例
for (ServletContextInitializer initializer : this.initializers) {
initializer.onStartup(servletContext);
}
}
catch (Exception ex) {
this.startUpException = ex;
// Prevent Tomcat from logging and re-throwing when we know we can
// deal with it in the main thread, but log for information here.
if (logger.isErrorEnabled()) {
logger.error("Error starting Tomcat context. Exception: " + ex.getClass().getName() + ". Message: "
+ ex.getMessage());
}
}
}

}

我们可以打断点调试一下,会发现在 TomcatStarter 里面的 ServletContextInitializer[] initializers 里存放的并不是我们上面说的 RegistrationBean 的子类的实例,而是三个用 lambda 表达式实现的匿名内部类的实例,其中在 ServletWebServerApplicationContext 中实现的匿名内部类是是获取 RegistrationBean 子类的实例的核心

图片丢失

想知道这个 ServletWebServerApplicationContext 的实例是在哪被放入 TomcatStarter 中的就要从 main 方法开始一路深入,到 ServletWebServerApplicationContext.createWebServer 前的调用逻辑大概是下面这样,具体自己调试看下比较清楚。

SpringApplication.run => SpringApplication.refreshContext => SpringApplication.refresh => AbstractApplicationContext.refresh

=> ServletWebServerApplicationContext.onRefresh => ServletWebServerApplicationContext.createWebServer

ServletWebServerApplicationContext 中 createWebServer 的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private void createWebServer() {
WebServer webServer = this.webServer;
ServletContext servletContext = getServletContext();
if (webServer == null && servletContext == null) {
ServletWebServerFactory factory = getWebServerFactory();
//getWebServer方法和getSelfInitializer方法是两个关键的的方法,我们后面一个个看
this.webServer = factory.getWebServer(getSelfInitializer());
getBeanFactory().registerSingleton("webServerGracefulShutdown",
new WebServerGracefulShutdownLifecycle(this.webServer));
getBeanFactory().registerSingleton("webServerStartStop",
new WebServerStartStopLifecycle(this, this.webServer));
}
else if (servletContext != null) {
try {
getSelfInitializer().onStartup(servletContext);
}
catch (ServletException ex) {
throw new ApplicationContextException("Cannot initialize servlet context", ex);
}
}
initPropertySources();
}

getWebServer 方法的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public WebServer getWebServer(ServletContextInitializer... initializers) {
if (this.disableMBeanRegistry) {
Registry.disableRegistry();
}
//创建tomcat
Tomcat tomcat = new Tomcat();
File baseDir = (this.baseDirectory != null) ? this.baseDirectory : createTempDir("tomcat");
tomcat.setBaseDir(baseDir.getAbsolutePath());
Connector connector = new Connector(this.protocol);
connector.setThrowOnFailure(true);
tomcat.getService().addConnector(connector);
customizeConnector(connector);
tomcat.setConnector(connector);
tomcat.getHost().setAutoDeploy(false);
configureEngine(tomcat.getEngine());
for (Connector additionalConnector : this.additionalTomcatConnectors) {
tomcat.getService().addConnector(additionalConnector);
}
//在这个方法里面调用了configureContext方法,就是configureContext这个方法创建了TomcatStarter并且将前面说的那几个通过lambda表达式实现的匿名内部类的实例赛道TomcatStarter中的
prepareContext(tomcat.getHost(), initializers);
return getTomcatWebServer(tomcat);
}

prepareContext 的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
protected void prepareContext(Host host, ServletContextInitializer[] initializers) {
File documentRoot = getValidDocumentRoot();
TomcatEmbeddedContext context = new TomcatEmbeddedContext();
if (documentRoot != null) {
context.setResources(new LoaderHidingResourceRoot(context));
}
context.setName(getContextPath());
context.setDisplayName(getDisplayName());
context.setPath(getContextPath());
File docBase = (documentRoot != null) ? documentRoot : createTempDir("tomcat-docbase");
context.setDocBase(docBase.getAbsolutePath());
context.addLifecycleListener(new FixContextListener());
context.setParentClassLoader((this.resourceLoader != null) ? this.resourceLoader.getClassLoader()
: ClassUtils.getDefaultClassLoader());
resetDefaultLocaleMapping(context);
addLocaleMappings(context);
try {
context.setCreateUploadTargets(true);
}
catch (NoSuchMethodError ex) {
// Tomcat is < 8.5.39. Continue.
}
configureTldPatterns(context);
WebappLoader loader = new WebappLoader();
loader.setLoaderClass(TomcatEmbeddedWebappClassLoader.class.getName());
loader.setDelegate(true);
context.setLoader(loader);
if (isRegisterDefaultServlet()) {
addDefaultServlet(context);
}
if (shouldRegisterJspServlet()) {
addJspServlet(context);
addJasperInitializer(context);
}
context.addLifecycleListener(new StaticResourceConfigurer(context));
//这个方法可以自己看下,可以看到除了我们关键的ServletWebServerApplicationContext中匿名实现的ServletContextInitializer类的实例外,剩下的两个ServletContextInitializer实例就是在这里放入数组
ServletContextInitializer[] initializersToUse = mergeInitializers(initializers);
host.addChild(context);
//在这个方法里创建TomcatStarter并且将上面initializersToUse数组中的ServletContextInitializer实例放入TomcatStarter中的ervletContextInitializer[] initializers
configureContext(context, initializersToUse);
postProcessContext(context);
}

configureContext 的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
protected void configureContext(Context context, ServletContextInitializer[] initializers) {
//创建TomcatStarter,并且将ServletContextInitializer实例放入其中
TomcatStarter starter = new TomcatStarter(initializers);
if (context instanceof TomcatEmbeddedContext) {
TomcatEmbeddedContext embeddedContext = (TomcatEmbeddedContext) context;
embeddedContext.setStarter(starter);
embeddedContext.setFailCtxIfServletStartFails(true);
}
//将TomcatStarter通过addServletContainerInitializer方法放入servlet容器中,之后实现了ServletContainerInitializer的TomcatStarter的onStartup方法被调用会在StandardContext类中被调用(上面贴过源码,忘了可以回去看看)
context.addServletContainerInitializer(starter, NO_CLASSES);
for (LifecycleListener lifecycleListener : this.contextLifecycleListeners) {
context.addLifecycleListener(lifecycleListener);
}
for (Valve valve : this.contextValves) {
context.getPipeline().addValve(valve);
}
for (ErrorPage errorPage : getErrorPages()) {
org.apache.tomcat.util.descriptor.web.ErrorPage tomcatErrorPage = new org.apache.tomcat.util.descriptor.web.ErrorPage();
tomcatErrorPage.setLocation(errorPage.getPath());
tomcatErrorPage.setErrorCode(errorPage.getStatusCode());
tomcatErrorPage.setExceptionType(errorPage.getExceptionName());
context.addErrorPage(tomcatErrorPage);
}
for (MimeMappings.Mapping mapping : getMimeMappings()) {
context.addMimeMapping(mapping.getExtension(), mapping.getMimeType());
}
configureSession(context);
new DisableReferenceClearingContextCustomizer().customize(context);
for (TomcatContextCustomizer customizer : this.tomcatContextCustomizers) {
customizer.customize(context);
}
}

再看下 getSelfInitializer 方法的代码

1
2
3
4
private org.springframework.boot.web.servlet.ServletContextInitializer getSelfInitializer() {
//这里用了java8的方法引用的写法是对lambda表达式的简写,等价于(servletContext)->this.selfInitialize(servletContext)
return this::selfInitialize;
}

selfInitialize 方法的代码如下:

1
2
3
4
5
6
7
8
9
private void selfInitialize(ServletContext servletContext) throws ServletException {
prepareWebApplicationContext(servletContext);
registerApplicationScope(servletContext);
WebApplicationContextUtils.registerEnvironmentBeans(getBeanFactory(), servletContext);
for (ServletContextInitializer beans : getServletContextInitializerBeans()) {
//这里的beans有我们关心的RegistrationBean的实例
beans.onStartup(servletContext);
}
}

getServletContextInitializerBeans 方法如下:

1
2
3
4
protected Collection<ServletContextInitializer> getServletContextInitializerBeans() {
//ServletContextInitializerBeans继承了Collection,所以它实现了iterator()方法可以被遍历
return new ServletContextInitializerBeans(getBeanFactory());
}

ServletContextInitializerBeans 的 iterator()方法

1
2
3
4
public Iterator<ServletContextInitializer> iterator() {
//sortedList的声明List<ServletContextInitializer> sortedList
return this.sortedList.iterator();
}

ServletContextInitializerBeans 的构造方法代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public ServletContextInitializerBeans(ListableBeanFactory beanFactory,
Class<? extends ServletContextInitializer>... initializerTypes) {
this.initializers = new LinkedMultiValueMap<>();
this.initializerTypes = (initializerTypes.length != 0) ? Arrays.asList(initializerTypes)
: Collections.singletonList(ServletContextInitializer.class);
//将ServletContextInitializer的实例添加到MultiValueMap<Class<?>, ServletContextInitializer> initializers中
addServletContextInitializerBeans(beanFactory);
addAdaptableBeans(beanFactory);
//下面的逻辑最终会将initializers里面的ServletContextInitializer的实例保存到this.sortedList中
List<ServletContextInitializer> sortedInitializers = this.initializers.values().stream()
.flatMap((value) -> value.stream().sorted(AnnotationAwareOrderComparator.INSTANCE))
.collect(Collectors.toList());
this.sortedList = Collections.unmodifiableList(sortedInitializers);
logMappings(this.initializers);
}

addServletContextInitializerBeans 方法的代码:

1
2
3
4
5
6
7
8
9
10
private void addServletContextInitializerBeans(ListableBeanFactory beanFactory) {
for (Class<? extends ServletContextInitializder> initializerType : this.initializerTypes) {
//通过beanFactory获取在已经注册到Spring容器中的ServletContextInitializer实例,在MyConfig中注册的RegistrationBean在这里就可以获取到
for (Entry<String, ? extends ServletContextInitializer> initializerBean : getOrderedBeansOfType(beanFactory,
initializerType)) {
//将ServletContextInitializer的实例添加到MultiValueMap<Class<?>, ServletContextInitializer> initializers中
addServletContextInitializerBean(initializerBean.getKey(), initializerBean.getValue(), beanFactory);
}
}
}

addServletContextInitializerBean(String beanName, ServletContextInitializer initializer,ListableBeanFactory beanFactory)的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private void addServletContextInitializerBean(String beanName, ServletContextInitializer initializer,
ListableBeanFactory beanFactory) {
if (initializer instanceof ServletRegistrationBean) {
Servlet source = ((ServletRegistrationBean<?>) initializer).getServlet();
//在这个方法里才真正将ServletContextInitializer的实例添加到MultiValueMap<Class<?>, ServletContextInitializer> initializers中
addServletContextInitializerBean(Servlet.class, beanName, initializer, beanFactory, source);
}
else if (initializer instanceof FilterRegistrationBean) {
Filter source = ((FilterRegistrationBean<?>) initializer).getFilter();
addServletContextInitializerBean(Filter.class, beanName, initializer, beanFactory, source);
}
else if (initializer instanceof DelegatingFilterProxyRegistrationBean) {
String source = ((DelegatingFilterProxyRegistrationBean) initializer).getTargetBeanName();
addServletContextInitializerBean(Filter.class, beanName, initializer, beanFactory, source);
}
else if (initializer instanceof ServletListenerRegistrationBean) {
EventListener source = ((ServletListenerRegistrationBean<?>) initializer).getListener();
addServletContextInitializerBean(EventListener.class, beanName, initializer, beanFactory, source);
}
else {
addServletContextInitializerBean(ServletContextInitializer.class, beanName, initializer, beanFactory,
initializer);
}
}

addServletContextInitializerBean(Class<?> type, String beanName, ServletContextInitializer initializer,ListableBeanFactory beanFactory, Object source)的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private void addServletContextInitializerBean(Class<?> type, String beanName, ServletContextInitializer initializer,
ListableBeanFactory beanFactory, Object source) {
//将ServletContextInitializer的实例添加到MultiValueMap<Class<?>, ServletContextInitializer> initializers中
this.initializers.add(type, initializer);
if (source != null) {
// Mark the underlying source as seen in case it wraps an existing bean
this.seen.add(source);
}
if (logger.isTraceEnabled()) {
String resourceDescription = getResourceDescription(beanName, beanFactory);
int order = getOrder(initializer);
logger.trace("Added existing " + type.getSimpleName() + " initializer bean '" + beanName + "'; order="
+ order + ", resource=" + resourceDescription);
}
}

所以最后在 ServletWebServerApplicationContext 中的 selfInitialize 方法里面通过 getServletContextInitializerBeans 获取到的是 ServletContextInitializerBeans 中保存了我们注册到 Spring 容器的 RegistrationBean 的实例的 sortedList,然后遍历 sortedList 从而调用这些 RegistrationBean 的实例的 onStartup 方法。(如果上面的流程解析看不懂,可以配合源码自己走一遍会更加清晰)

看下 RegistrationBean 的 onStartup 方法

1
2
3
4
5
6
7
8
9
public final void onStartup(ServletContext servletContext) throws ServletException {
String description = getDescription();
if (!isEnabled()) {
logger.info(StringUtils.capitalize(description) + " was not registered (disabled)");
return;
}
//调用子类的register方法
register(description, servletContext);
}

DynamicRegistrationBean(FilterRegistrationBean 和 ServletRegistrationBean 的父类)实现了 register 方法

1
2
3
4
5
6
7
8
9
protected final void register(String description, ServletContext servletContext) {
//调用子类的addRegistration方法
D registration = addRegistration(description, servletContext);
if (registration == null) {
logger.info(StringUtils.capitalize(description) + " was not registered (possibly already registered?)");
return;
}
configure(registration);
}

ServletRegistrationBean 的 addRegistration 方法

1
2
3
4
5
6
@Override
protected ServletRegistration.Dynamic addRegistration(String description, ServletContext servletContext) {
String name = getServletName();
//这里的代码就很熟悉了,这里调用的就是servlet3.0提供的动态注册servlet的方法
return servletContext.addServlet(name, this.servlet);
}

看到这就可以大概知道 SpringBoot 是如何加载 servlet 的了,filter 和 listener 也是如此。

注册方式三:继承 ServletContextInitializer 实现 onStartup 方法

看完上面的分析我们还可以用第三种注册 servlet 的方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Component
public class CustomServletContextInitializer implements ServletContextInitializer {

private final static String JAR_HELLO_URL = "/custom";
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
System.out.println("创建 customServlet...");

ServletRegistration.Dynamic servlet = servletContext.addServlet(
CustomServlet.class.getSimpleName(),
CustomServlet.class);
servlet.addMapping(JAR_HELLO_URL);

System.out.println("创建 customFilter...");

FilterRegistration.Dynamic filter = servletContext.addFilter(
CustomFilter.class.getSimpleName(), CustomFilter.class);

EnumSet<DispatcherType> dispatcherTypes = EnumSet.allOf(DispatcherType.class);
dispatcherTypes.add(DispatcherType.REQUEST);
dispatcherTypes.add(DispatcherType.FORWARD);

filter.addMappingForUrlPatterns(dispatcherTypes, true, JAR_HELLO_URL);
}
}

最后这里提一下上面 ServletWebServerApplicationContext 中 createWebServer 的代码中的 getWebServerFactory()方法,它的作用是从 spring 容器中获取一个类型为 ServletWebServerFactory(TomcatServletWebServerFactory 是其子类)的工厂实例,而这个实例是通过 SpringBoot 的自动装配完成的(SpringBoot 的自动装配可以了解一下)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

protected ServletWebServerFactory getWebServerFactory() {
// Use bean names so that we don't consider the hierarchy
String[] beanNames = getBeanFactory().getBeanNamesForType(ServletWebServerFactory.class);
if (beanNames.length == 0) {
throw new ApplicationContextException("Unable to start ServletWebServerApplicationContext due to missing "
+ "ServletWebServerFactory bean.");
}
if (beanNames.length > 1) {
throw new ApplicationContextException("Unable to start ServletWebServerApplicationContext due to multiple "
+ "ServletWebServerFactory beans : " + StringUtils.arrayToCommaDelimitedString(beanNames));
}
//从spring容器中获取一个类型为ServletWebServerFactory的工厂实例并且返回
return getBeanFactory().getBean(beanNames[0], ServletWebServerFactory.class);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration(proxyBeanMethods = false)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@ConditionalOnClass(ServletRequest.class)
@ConditionalOnWebApplication(type = Type.SERVLET)
@EnableConfigurationProperties(ServerProperties.class)
@Import({ ServletWebServerFactoryAutoConfiguration.BeanPostProcessorsRegistrar.class,
//留意这个
ServletWebServerFactoryConfiguration.EmbeddedTomcat.class,
ServletWebServerFactoryConfiguration.EmbeddedJetty.class,
ServletWebServerFactoryConfiguration.EmbeddedUndertow.class })
public class ServletWebServerFactoryAutoConfiguration {
.....
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Configuration(proxyBeanMethods = false)
class ServletWebServerFactoryConfiguration {

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ Servlet.class, Tomcat.class, UpgradeProtocol.class })
@ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT)
static class EmbeddedTomcat {

@Bean
TomcatServletWebServerFactory tomcatServletWebServerFactory(
ObjectProvider<TomcatConnectorCustomizer> connectorCustomizers,
ObjectProvider<TomcatContextCustomizer> contextCustomizers,
ObjectProvider<TomcatProtocolHandlerCustomizer<?>> protocolHandlerCustomizers) {
TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory();
factory.getTomcatConnectorCustomizers()
.addAll(connectorCustomizers.orderedStream().collect(Collectors.toList()));
factory.getTomcatContextCustomizers()
.addAll(contextCustomizers.orderedStream().collect(Collectors.toList()));
factory.getTomcatProtocolHandlerCustomizers()
.addAll(protocolHandlerCustomizers.orderedStream().collect(Collectors.toList()));
return factory;
}

}
}

总结

存在 web.xml 配置的 java web 项目,servlet3.0 的 java web 项目,springboot 内嵌容器的 java web 项目加载 servlet,filter,listener 的流程都是有所差异的,理解清楚这其中的原来,其实并不容易,至少得搞懂 servlet3.0 的规范,springboot 内嵌容器的加载流程等等前置逻辑。

最后还是要感谢这位大佬的文章,我做的只是在他的基础上针对我自己再补充的笔记,文笔没有大佬的简洁,所以如果觉得说得太啰嗦看不懂建议去看这个大佬的文章,你会发觉你发现了新大陆。