共计 19531 个字符,预计需要花费 49 分钟才能阅读完成。
版权声明:本文为博主原创文章,转载请附上原文出处链接和声明。
原文链接:http://uhfun.cn/tech/2020/05/22/ 傻傻分不清 -ServletContainerInitializer-SpringServletContainerInitializer-WebApplicationInitializer-SpringBootServletInitializer-ServletContextInitializer 都是些啥.html
这几个类乍一看有点像,仔细一看还是有点像,特别是 ServletContainerInitializer
和ServletContextInitializer
,眼神不好的可能还真看不出来。那它们之间到底有什么区别呢?它们之间有什么联系吗?
全限定名
首先我们先来看一下它们的全限定名
-
ServletContainerInitializer(
javax.servlet.ServletContainerInitializer
) -
SpringServletContainerInitializer(
org.springframework.web.SpringServletContainerInitializer
) -
WebApplicationInitializer(
org.springframework.web.WebApplicationInitializer
) -
SpringBootServletInitializer(
org.springframework.boot.web.servlet.support.SpringBootServletInitializer
) -
ServletContextInitializer(
org.springframework.boot.web.servlet.ServletContextInitializer
)
ServletContainerInitializer
ServletContainerInitializer 是什么
因为这个不是 spring 家族的,我们就先来讲一下这个类
ServletContainerInitializer
是 servlet3.0 规范中引入的接口,能够让 web 应用程序在 servlet 容器启动后做一些自定义的操作。
ServletContainerInitializer 基于服务提供者接口(SPI)概念,因此你需要在你的 jar
包目录下添加 META-INF/services/javax.servlet.ServletContainerInitializer
文件,内容就是 ServletContainerInitializer
实现类的全限定名。
例如在
org.springframework:spring-web
的META-INF/services
中存在javax.servlet.ServletContainerInitializer
文件,它的内容就是 springMVC 提供的实现类org.springframework.web.SpringServletContainerInitializer
ServletContainerInitializer 怎么用
ServletContainerInitializer` 只有一个方法
package javax.servlet;
...
public interface ServletContainerInitializer {public void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException;
}
ServletContainerInitializer#onStartup
方法由 Servlet 容器调用 (必须至少支持 Servlet 3.0 版本)。我们在这个方法中通过编程的方式去注册Servlet
Filter
Listenner
等组件,代替web.xml
。
可以配合 @HandleTypes
注解,通过指定 Class,容器会把所有的指定类的子类作为方法 onStartup
的参数Set<Class<?>> c
传递进来
例如
SpringServletContainerInitializer
传递了 spring 自定义的WebApplicationInitializer
package org.springframework.web; ... @HandlesTypes(WebApplicationInitializer.class) public class SpringServletContainerInitializer implements ServletContainerInitializer {...}
SpringServletContainerInitializer 和 WebApplicationInitializer
我们就接着前面的例子,来讲 SpringServletContainerInitializer
,那为什么和WebApplicationInitializer
一起讲呢?
SpringServletContainerInitializer 是什么
根据名字不难看出,它比 ServletContainerInitializer
多了 Spring
的前缀,顾名思义 它是 Spring 提供的ServletContainerInitializer
的实现类
WebApplicationInitializer 是什么
WebApplicationInitializer
是 Spring 提供的接口,和 ServletContainerInitializer
没有直接关系,但是和它有间接关系。WebApplicationInitializer
在 SpringServletContainerInitializer
中实例化后被调用。
SpringServletContainerInitializer 和 WebApplicationInitializer 的关系
它们之间的关系可以理解为,SpringServletContainerInitializer
实现了 servlet 容器提供的接口带了个头,接下来的事可以交由 spring 自己定义的WebApplicationInitializer
SpringServletContainerInitializer
源码如下,可以看到,SpringServletContainerInitializer
将传入的 webAppInitializerClasses
通过反射实例化,然后根据 @Order
注解排序后,依次调用
@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {
@Override
public void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)
throws ServletException {List<WebApplicationInitializer> initializers = new LinkedList<>();
if (webAppInitializerClasses != null) {for (Class<?> waiClass : webAppInitializerClasses) {if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) &&
WebApplicationInitializer.class.isAssignableFrom(waiClass)) {
try {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) {initializer.onStartup(servletContext);
}
}
}
SpringServletContainerInitializer 和 WebApplicationInitializer 怎么用
我们可以使用 WebApplicationInitializer
接口来代替 web.xml
配置
继承 AbstractAnnotationConfigDispatcherServletInitializer
例如我们自定义一个类,继承 Spring 为我们提供的 AbstractAnnotationConfigDispatcherServletInitializer
, 不需要添加 @Configuration 等注解,因为 Servlet 容器会自动将我们自定义的MyCustomWebApplicationInitializer
class 传入SpringServletContainerInitializer#onStartup
,而SpringServletContainerInitializer
会为我们实例化这个类并调用它。
public class MyCustomWebApplicationInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
/**
* 为 {@linkPlan#createRootApplicationContext() 根应用程序上下文}
* 指定 {@code@Configuration} 和 / 或 {@code@Component} 类
* @ 返回根应用上下文的配置,如果不需要创建和注册根上下文,则返回{@code null
*/
@Nullable
@Override
protected Class<?>[] getRootConfigClasses() {return null;}
/**
* 为 {@linkPlan#createServletApplicationContext()Servlet 应用程序上下文} 指定
* {@code@Configuration}和 / 或 {@code@Component} 类
* @返回 Servlet 应用程序上下文的配置,或者如果所有配置都通过根配置类指定,则为{@code null}
*/
@Nullable
@Override
protected Class<?>[] getServletConfigClasses() {return new Class<?>[]{WebConfig.class};
}
/**
* 指定 {@code DispatcherServlet} 的 Servlet 映射
* 例如 {@code“/”}、{@code“/app”} 等
*/
@Override
protected String[] getServletMappings() {return new String[]{"/"};
}
/**
* 返回注册 {@link DispatcherServlet} 的名称
* 默认为{@link#Default_Servlet_Name} (public static final String DEFAULT_SERVLET_NAME = "dispatcher";)
*/
@Override
protected String getServletName() {return "myCustomServletName";}
}
自己实现 WebApplicationInitializer
同样,我们可以实现一个简单版的WebApplicationInitializer
,效果也是一样的。
public class SimpleWebApplicationInitializer implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext servletContext) {AnnotationConfigWebApplicationContext webContext = new AnnotationConfigWebApplicationContext();
servletContext.addListener(new ContextLoaderListener(webContext));
webContext.register(WebConfig.class);
ServletRegistration.Dynamic registration = servletContext.addServlet("myCustomServletName", new DispatcherServlet(webContext));
registration.setLoadOnStartup(1);
registration.addMapping("/");
}
}
SpringBootServletInitializer
SpringBootServletInitializer 是什么
同样从名字中我们可以大致猜到,它是 SpringBoot
提供的,SpringBoot
替我们实现了一些功能
它是一个抽象类,实现了 WebApplicationInitializer
接口,注意不是 Servlet3.0 规范提供的那个接口,是 Spring
提供的自家初始化接口,你问我它是做什么的?那我们不如来看看源码是怎么说的
这是一个固执己见的
WebApplicationInitializer
让我们可以使用传统的 WAR 包的方式部署运行SpringApplication
,可以将servlet
、filter
和ServletContextInitializer
从应用程序上下文绑定到服务器。如果要配置应用程序,要么覆盖
configure(SpringApplicationBuilder)
方法 (调用SpringApplicationBuilder#Sources(Class.)
),要么使初始化式本身成为@configuration
。如果将SpringBootServletInitializer
与其他WebApplicationInitializer
结合使用,则可能还需要添加@Ordered
注解来配置特定的启动顺序。请注意,只有在构建和部署 WAR 文件时才需要 WebApplicationInitializer。如果您更喜欢运行嵌入式 Web 服务器,那么您根本不需要这个。
/**
* An opinionated {@link WebApplicationInitializer} to run a {@link SpringApplication}
* from a traditional WAR deployment. Binds {@link Servlet}, {@link Filter} and
* {@link ServletContextInitializer} beans from the application context to the server.
* <p>
* To configure the application either override the
* {@link #configure(SpringApplicationBuilder)} method (calling
* {@link SpringApplicationBuilder#sources(Class...)}) or make the initializer itself a
* {@code @Configuration}. If you are using {@link SpringBootServletInitializer} in
* combination with other {@link WebApplicationInitializer WebApplicationInitializers} you
* might also want to add an {@code @Ordered} annotation to configure a specific startup
* order.
* <p>
* Note that a WebApplicationInitializer is only needed if you are building a war file and
* deploying it. If you prefer to run an embedded web server then you won't need this at
* all.
*
* @author Dave Syer
* @author Phillip Webb
* @author Andy Wilkinson
* @since 2.0.0
* @see #configure(SpringApplicationBuilder)
*/
public abstract class SpringBootServletInitializer implements WebApplicationInitializer {
这些话简单来讲什么意思呢?
它是说,如果你需要使用外部容器,打 war 包然后部署,那么你需要继承这个类然后重写 configure
方法,像这个样子
@SpringBootApplication
public class DemoSpringmvcApplication extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {return builder.sources(DemoSpringmvcApplication.class);
}
}
如果你觉得内嵌的 tomcat 挺好用的,那么你就当什么事情都没有发生
SpringBootServletInitializer 怎么运作的
首先你要知道,它是一个 WebApplicationInitializer
,它的启动靠的是 SpringServletContainerInitializer
,
而 SpringServletContainerInitializer
靠的是 Servlet 容器。所以它的启动靠的就是外部容器。
因此当外部容器启动时,会调用SpringBootServletInitializer#onStartup
那我们来看看它的 onStartup
方法里做了什么,它首先调用 createRootApplicationContext
来创建 web 应用上下文
@Override
public void onStartup(ServletContext servletContext) throws ServletException {this.logger = LogFactory.getLog(getClass());
WebApplicationContext rootAppContext = createRootApplicationContext(servletContext);
if (rootAppContext != null) {servletContext.addListener(new ContextLoaderListener(rootAppContext) {
@Override
public void contextInitialized(ServletContextEvent event) {// no-op because the application context is already initialized}
});
}
else {this.logger.debug("No ContextLoaderListener registered, as createRootApplicationContext() did not"
+ "return an application context");
}
}
再来看 createRootApplicationContext
方法
protected WebApplicationContext createRootApplicationContext(ServletContext servletContext) {SpringApplicationBuilder builder = createSpringApplicationBuilder();
builder.main(getClass());
ApplicationContext parent = getExistingRootWebApplicationContext(servletContext);
if (parent != null) {this.logger.info("Root context already created (using as parent).");
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, null);
builder.initializers(new ParentContextApplicationContextInitializer(parent));
}
builder.initializers(new ServletContextApplicationContextInitializer(servletContext));
builder.contextClass(AnnotationConfigServletWebServerApplicationContext.class);
builder = configure(builder);
builder.listeners(new WebEnvironmentPropertySourceInitializer(servletContext));
SpringApplication application = builder.build();
if (application.getAllSources().isEmpty()
&& MergedAnnotations.from(getClass(), SearchStrategy.TYPE_HIERARCHY).isPresent(Configuration.class)) {application.addPrimarySources(Collections.singleton(getClass()));
}
Assert.state(!application.getAllSources().isEmpty(),
"No SpringApplication sources have been defined. Either override the"
+ "configure method or add an @Configuration annotation");
// Ensure error pages are registered
if (this.registerErrorPageFilter) {application.addPrimarySources(Collections.singleton(ErrorPageFilterConfiguration.class));
}
return run(application);
}
- 如果有父级应用上下文,设置一个初始化器
ParentContextApplicationContextInitializer
,这个初始化器会在ConfigurableApplicationContext#refresh()
刷新之前设置父级应用上下文并添加事件监听 - 设置一个 servletContext 初始化的初始化器,同样会在
ConfigurableApplicationContext#refresh()
刷新之前将servletContext
添加到应用上下文中 - 设置上下文类型,AnnotationConfigServletWebServerApplicationContext
-
向此应用程序添加更多源 (配置类
@Configuration
和组件@Component
)。例如上面启动类重写的
configure
,就是将我们的DemoSpringmvcApplication
作为配置类添加到了源中,好让启动后能够读到这些配置,因为这个DemoSpringmvcApplication
拥有@SpringBootApplication
注解。protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {return builder.sources(DemoSpringmvcApplication.class); }
查看源码可知,
@SpringBootApplication
注解就是一个功能更加强大的@Configuration
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @SpringBootConfiguration @EnableAutoConfiguration @ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class), @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) }) public @interface SpringBootApplication {...}
- 添加
WebEnvironmentPropertySourceInitializer
监听,监听的事件是ApplicationEnvironmentPreparedEvent
(当 SpringApplication 正在启动并且环境首次可供检查和修改时发布的事件) - 构建 SpringApplication,如果
configure
没有配置源,会检查自己是否是配置类,如果是就加到primarySources
中,意味着其实如果SpringBootServletInitializer
的子类就是配置类,默认会添加到 source 中 - 确保注册了错误页面
- 运行
ServletContextInitializer
什么是 ServletContextInitializer
它的方法和 WebApplicationInitializer
一模一样,但是它是 SpringBoot
提供的
我们看看源码怎么描述的
用于以编程方式配置 Servlet 3.0+{@link ServletContext Context}的接口。与 WebApplicationInitializer 不同的是实现此接口 (且不实现 WebApplicationInitializer) 的类 不会 被 SpringServletContainerInitializer 检测到,因此 Servlet 容器不会自动引导
此接口的设计方式类似于 ServletContainerInitializer,但其生命周期由 Spring 管理,而不是 Servlet 容器。
有关配置示例,请参阅 WebApplicationInitializer
/**
* Interface used to configure a Servlet 3.0+ {@link ServletContext context}
* programmatically. Unlike {@link WebApplicationInitializer}, classes that implement this
* interface (and do not implement {@link WebApplicationInitializer}) will <b>not</b> be
* detected by {@link SpringServletContainerInitializer} and hence will not be
* automatically bootstrapped by the Servlet container.
* <p>
* This interface is designed to act in a similar way to
* {@link ServletContainerInitializer}, but have a lifecycle that's managed by Spring and
* not the Servlet container.
* <p>
* For configuration examples see {@link WebApplicationInitializer}.
*
* @author Phillip Webb
* @since 1.4.0
* @see WebApplicationInitializer
*/
@FunctionalInterface
public interface ServletContextInitializer {
/**
* Configure the given {@link ServletContext} with any servlets, filters, listeners
* context-params and attributes necessary for initialization.
* @param servletContext the {@code ServletContext} to initialize
* @throws ServletException if any call against the given {@code ServletContext}
* throws a {@code ServletException}
*/
void onStartup(ServletContext servletContext) throws ServletException;
}
为什么另外设计了这个接口?
不是可以直接实现一个 ServletContainerInitializer
吗?为什么要另外用这个接口去实现相同的功能。
因为设计者,就是不让内嵌容器支持ServletContainerInitializer
,可以在这个 issue 中找到些答案
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.
Do you have a specific case where this is causing problems or was it more of an observation?
这实际上是一个有意的设计决定。容器使用的搜索算法存在问题。当您想要开发一个可执行的 WAR 时,这也会导致问题,因为您通常需要一个 javax.servlet.ServletContainerInitializer 用于在运行 java-jar 时不执行的 WAR。
有关使用 SpringBean 的选项,请参阅 org.springframework.boot.context.embedded.ServletContextInitializer。
我的理解是,当你的项目在开发时,为了开发方便,你可能使用的是内嵌的容器。而当部署到生成环境可能用的是 war 包的方式,部署到外部的容器中。这样你的代码必须同时兼容这两种方式。例如这样
@SpringBootApplication
public class DemoSpringmvcApplication extends SpringBootServletInitializer {
// 使用内嵌容器时,main 方法入口在这里,启动初始化的某个时间段我也启动了我的内嵌容器
// 使用外部容器时,忽略我的存在
public static void main(String[] args) {SpringApplication.run(DemoSpringmvcApplication.class, args);
}
// 使用内嵌容器时,我不会被调用。// 外部容器时,外部容器检测到 SpringServletContainerInitializer,然后又检测到继承自 WebApplicationInitializer 的我,然后我被调用了,初始化也开始了
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {return builder.sources();
}
}
如果内嵌容器支持ServletContainerInitializer
,那这份代码运行就会有意向不到的问题,当然会有什么问题,我也不清楚?
ServletContextInitializer 怎么被调用的
链路很长,我就找 ServletContextInitializer
被调用的关键代码
首先,在 ServletWebServerApplicationContext#onRefresh
里会调用 createWebServer,顾名思义,创建 web 容器
@Override
protected void onRefresh() {super.onRefresh();
try {createWebServer();
}
catch (Throwable ex) {throw new ApplicationContextException("Unable to start web server", ex);
}
}
可以看到,在 createWebServer
根据 webServer == null && servletContext == null
说明我在创建应用上下文之前并没有外部容器启动,那么就创建一个内嵌容器,factory.getWebServer(getSelfInitializer())
,传入了 servletContextInitializers,
private void createWebServer() {
WebServer webServer = this.webServer;
ServletContext servletContext = getServletContext();
if (webServer == null && servletContext == null) {ServletWebServerFactory factory = getWebServerFactory();
this.webServer = factory.getWebServer(getSelfInitializer());
}
else if (servletContext != null) {
try {getSelfInitializer().onStartup(servletContext);
}
catch (ServletException ex) {throw new ApplicationContextException("Cannot initialize servlet context", ex);
}
}
initPropertySources();}
方法调用 createWebServer
-> getWebServer
-> prepareContext
-> configureContext
然后就来到了configureContext
,有一步很关键,context.addServletContainerInitializer(starter, NO_CLASSES)
protected void configureContext(Context context, ServletContextInitializer[] initializers) {TomcatStarter starter = new TomcatStarter(initializers);
if (context instanceof TomcatEmbeddedContext) {TomcatEmbeddedContext embeddedContext = (TomcatEmbeddedContext) context;
embeddedContext.setStarter(starter);
embeddedContext.setFailCtxIfServletStartFails(true);
}
context.addServletContainerInitializer(starter, NO_CLASSES);
...
}
我们看一下这个方法,它往里面放的不是别人,正是大名鼎鼎的ServletContainerInitializer
/**
* Add a ServletContainerInitializer instance to this web application.
*
* @param sci The instance to add
* @param classes The classes in which the initializer expressed an
* interest
*/
public void addServletContainerInitializer(ServletContainerInitializer sci, Set<Class<?>> classes);
那我们看看这个 TomcatStarter
是何方神圣,仔细一看,里面就是依次调用传入的 ServletContextInitializers
class TomcatStarter implements ServletContainerInitializer {private static final Log logger = LogFactory.getLog(TomcatStarter.class);
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 {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());
}
}
}
Exception getStartUpException() {return this.startUpException;}
}
看到这里,我们发现了内嵌的 tomcat 不会以 spi 方式加载 ServletContainerInitializer
,而是用TomcatStarter
的 onStartup,间接启动 ServletContextInitializers
,来达到ServletContainerInitializer
的效果。
是不是有点像 SpringServletContainerInitializer
老大哥带 WebApplicationInitializer
的套路
所以 ServletContextInitializer
源码里让你参考 @see WebApplicationInitializer
也不是没道理
*
* @author Phillip Webb
* @since 1.4.0
* @see WebApplicationInitializer
*/
@FunctionalInterface
public interface ServletContextInitializer {
ServletContextInitializer 应用
那 ServletContextInitializer 有现成的应用吗?
有!DispatcherServletRegistrationBean
,它是什么?在检测到 DispatcherServletAutoConfiguration
配置看,生效后 DispatcherServletRegistrationBean
就交由 spring 的 IOC 容器管理了,因此 ServletContextInitializer
能够被拿到
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass(DispatcherServlet.class)
@AutoConfigureAfter(ServletWebServerFactoryAutoConfiguration.class)
public class DispatcherServletAutoConfiguration {
...
@Configuration(proxyBeanMethods = false)
@Conditional(DispatcherServletRegistrationCondition.class)
@ConditionalOnClass(ServletRegistration.class)
@EnableConfigurationProperties(WebMvcProperties.class)
@Import(DispatcherServletConfiguration.class)
protected static class DispatcherServletRegistrationConfiguration {@Bean(name = DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME)
@ConditionalOnBean(value = DispatcherServlet.class, name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
public DispatcherServletRegistrationBean dispatcherServletRegistration(DispatcherServlet dispatcherServlet,
WebMvcProperties webMvcProperties, ObjectProvider<MultipartConfigElement> multipartConfig) {
DispatcherServletRegistrationBean registration = new DispatcherServletRegistrationBean(dispatcherServlet,
webMvcProperties.getServlet().getPath());
registration.setName(DEFAULT_DISPATCHER_SERVLET_BEAN_NAME);
registration.setLoadOnStartup(webMvcProperties.getServlet().getLoadOnStartup());
multipartConfig.ifAvailable(registration::setMultipartConfig);
return registration;
}
}
}
它是这么拿的
- 在前面讲的
org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext#createWebServer
中调用 factory.getWebServer(getSelfInitializer()) -
然后调用 org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext#getServletContextInitializerBeans
protected Collection<ServletContextInitializer> getServletContextInitializerBeans() {return new ServletContextInitializerBeans(getBeanFactory()); }
-
ServletContextInitializerBeans
这个类是一个封装了ServletContextInitializer
的集合类,通过它的构造方法我们也能发现,它传入了beanFactory
,不用看源码也能大概猜到它是从 Spring 容器中去找这些类的 bean 实例public ServletContextInitializerBeans(ListableBeanFactory beanFactory, Class<? extends ServletContextInitializer>... initializerTypes){}
总结
说了那么多,这些类的出发点都是往 ServletContext
容器中注册 Servlet
,Filter
或者EventListener
只是生命周期由不同的容器托管,在不同的地方调用,但是最终的结果都是一样的