SpringMVC-ContextLoaderListener
前言
距离上一篇博客已经一个多月了 感觉都好久了!今天决定还是写篇博客 让自己坚持下,不然再这么下去就荒废了 哈哈!
为什么要写博客呢?
其实还是源于我在知乎上面的 看到的一个回答。
问题是这样的:生活中最浪费时间的事情有哪些?我看到下面的一个贴子回答是:
学而不思,犹豫不决,看到这个 我想到 哇偶 总结的好到位!
好了 废话不多说,为什么要写Spring MVC 呢 当然是因为我博主我最近在看Spring MVC 的源码了! 为什么开篇想讲 2个容器呢?随着Spring Boot 的越来越流行 我们之前配置XML 的方式也逐渐 被舍弃。我们刚使用Spring MVC的时候 一定也知道怎么去标准的配置 比如ContextLoaderListener DispatcherServlet等等 那为什么要有这些配置,这些配置又去怎么运行的呢?
今天 我就从自己的理解 去分析下 , 理解不当的地方 请各位见谅,留言指出 多谢!
PS: 这篇文章 有点跑题 重点讲了一个程序 tomcat 是怎么去加载web配置 又是怎么样将我们的ContextLoaderListener监听类加入到监听列表中的 最后串联了下整个Servlet 容器怎么去启动一个应用程序的~
下面一片文章 我会重点去看下ContextLoaderListener 里面怎么去创建WebApplication容器的,这里面我也走了一点弯路 一开始以为 我们的ContextLoaderListene 是通过ApplicationContext中的addListener添加的 最终 我一步步发现 我想错了!
标准配置
来 首先回顾下 我们的标准配置
<!--Spring 配置-->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!--Spring Root 根容器的配置-->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>WEB-INF/configs/spring/applicationContext.xml</param-value>
</context-param>
<!--Spring MVC 配置-->
<servlet>
<servlet-name>mvc-dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!--Servlet 容器的配置-->
<!--这边可以不写 默认读取/WEB-INF/{servletName}-dispatcher-servlet.xml -->
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/configs/spring/mvc-dispatcher-servlet.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>mvc-dispatcher</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
从上面的我的注释中 也可以看看出 一个是Spring 的 一个是Spring MVC 的
Spring Root容器
Spring Root容器 一般都是配置了非Controller的Bean 比如 Dao,Service 等等Bean都会在这个容器里面。
我们看到 这个配置contextparam 中的节点 设置了ApplicationContext的config路径 这个后面在源码中 我会讲到,先过~~
ContextLoaderListener 是被怎么执行的
listener
我们看到listener 这个节点配置了一个ContextLoaderListener类,那我们来详细聊下 这个类 是做什么的
首先 我们知道 服务在启动的时候 会给我们应用提供了一个容器的上下文环境ServletContext 这个上下文环境 就是我们应用程序的宿主环境
这边简答的说下ServletContenxt: 当我们的Servlet容器 也就是我们经常用的Tomcat或者Jetty 等 在web应用启动的时候 会创建一个Servlet对象,这个对象可以被 web应用下面的所有的Servlet所访问。
也就是说一个web应用只有一个ServletContent.记得这点很重要,到后面的代码中 我们就能知道 我们的WebApplicationContext 其实就是ServletContent的一个属性Attribute。
我们可以从Spring 项目的的一个ContextLoaderTests 单元测试类中 可以看出
@Test
public void testContextLoaderListenerWithDefaultContext() {
MockServletContext sc = new MockServletContext("");
sc.addInitParameter(ContextLoader.CONFIG_LOCATION_PARAM,
"/org/springframework/web/context/WEB-INF/applicationContext.xml " +
"/org/springframework/web/context/WEB-INF/context-addition.xml");
ServletContextListener listener = new ContextLoaderListener();
ServletContextEvent event = new ServletContextEvent(sc);
listener.contextInitialized(event);
WebApplicationContext context = (WebApplicationContext) sc.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);
...忽略
}
仔细看下 WebApplicationContext的获取方式 当然这个我后面也会在ContextLoaderListener的源码中讲到
这边配置的节点 就是把ContextLoaderListener 加入到 启动的监听列表里面 当程序启动后 会用ServletContextEvent作为参数 去初始化listener 上面的单元测试代码 也写很清楚了
ContextLoaderListener
<!--此类位于Spring-mvc项目中 org/springframework/web/context/ContextLoaderListener.java-->
public class ContextLoaderListener extends ContextLoader implements ServletContextListener{
public ContextLoaderListener() {
}
public ContextLoaderListener(WebApplicationContext context) {
super(context);
}
/**
* Initialize the root web application context.
*/
@Override
public void contextInitialized(ServletContextEvent event) {
initWebApplicationContext(event.getServletContext());
}
/**
* Close the root web application context.
*/
@Override
public void contextDestroyed(ServletContextEvent event) {
closeWebApplicationContext(event.getServletContext());
ContextCleanupListener.cleanupAttributes(event.getServletContext());
}
}
ContextLoaderListener 继承了ContextLoader类 实现了ServletContextListener接口
其中contextInitialized,contextDestroyed 方法都是ServletContextListener接口里面的方法
ServletContextListener
那我们看下ServletContextListener 这个类
public interface ServletContextListener extends EventListener {
public void contextInitialized(ServletContextEvent sce);
public void contextDestroyed(ServletContextEvent sce);
}
这个接口也是很简单 2个方法 而且接口继承了 一个空的EventListener接口 这样的写法 是为了标识某一类的接口,程序有的地方 判断的时候 用到这个。这样的写法 源码中很多地方用到过。
这边这样写是为了 程序在启动的时候 从xml 读取到Listener节点的配置的类 只有实现了ServletContextListener接口的类 才能加入到监听列表中
那又是怎么加入 怎么去调用的呢?
带着 这个问题 我们一步步去看看
我在上面 提到过 Servlet容器会在每个web程序启动的时候会分配一个ServletContext的上下文,那下面我们看下这个ServletContext 是什么
ServletContext
首先我们看下 这个类 位于什么地方 javax.servlet-api-3.0.1-sources.jar!\javax\servlet\ServletContext.java
这个类是位于javax.servlet-api 这个包里面的 我们都知道 这个包 定义了servlet的标准 web容器都是根据这些标准去实现的
public interface ServletContext {
<!--Adds the listener with the given class name to this ServletContext.-->
public void addListener(String className);
<!--adds the given listener to this ServletContext.-->
public void addListener(Class <? extends EventListener> listenerClass);
<!--Adds a listener of the given class type to this ServletContext.-->
public <T extends EventListener> void addListener(T t);
}
这个是我截取了 我关心的方法
看到这个方法的时候 我们就能明白 为什么 上面的ServletContextListener 要继承了一个空的EventListener接口了吧!
容器在启动的时候 就是把web.xml 中配置的监听类 加入到 分配到上下文环境ServletContext中的
那我怀着好奇心 继续寻找ServletContext的实现类
ApplicationContext
此类位于tomcat-embed-core-8.5.45-sources.jar!\org\apache\catalina\core\ApplicationContext.java
这个就是在tomcat中的了 tomcat本质就是一个Servlet容器 所以一定是要按照javax.servlet-api里面的定义的标准接口去实现的,话不多说 我们去看下代码是怎么写的,我截取了部分有关我讲的代码 有兴趣的对照着源码 看看
private final StandardContext context;
public ApplicationContext(StandardContext context) {
super();
this.context = context;
this.service = ((Engine) context.getParent().getParent()).getService();
this.sessionCookieConfig = new ApplicationSessionCookieConfig(context);
// Populate session tracking modes
populateSessionTrackingModes();
}
public <T extends EventListener> void addListener(T t) {
if (!context.getState().equals(LifecycleState.STARTING_PREP)) {
throw new IllegalStateException(
sm.getString("applicationContext.addListener.ise",
getContextPath()));
}
boolean match = false;
if (t instanceof ServletContextAttributeListener ||
t instanceof ServletRequestListener ||
t instanceof ServletRequestAttributeListener ||
t instanceof HttpSessionIdListener ||
t instanceof HttpSessionAttributeListener) {
context.addApplicationEventListener(t);
match = true;
}
if (t instanceof HttpSessionListener ||
(t instanceof ServletContextListener && newServletContextListenerAllowed)) {
// Add listener directly to the list of instances rather than to
// the list of class names.
context.addApplicationLifecycleListener(t);
match = true;
}
if (match) return;
}
我们看到 ApplicationContext 初始化的是传入了一个StandardContext对象 而且最终addListener 也是调用的StandardContext类中的addApplicationEventListener方法 看了下上面的代码 监听事件 还做了区分 放入了2个集合中
StandardContext
那我们就来看下StandardContext 是怎么做的
public class StandardContext extends ContainerBase
implements Context, NotificationEmitter {
//这边是一个对象锁 后面添加applicationListeners的时候会用到
private final Object applicationListenersLock = new Object();
<!--
这个是一个存放监听启动类的名称的数组 这边其实才是 启动的时候 我们存放xmL 里面的监听类的数组 一开始 我搞错了 后面我会仔细描述下
-->
private String applicationListeners[] = new String[0];
<!--这边2个数组 就是 添加监听对象的时候存放的数组 但是为什么要一个用集合 一个用数组 好奇怪 哈哈 -->
private List<Object> applicationEventListenersList = new CopyOnWriteArrayList<>();
public void addApplicationEventListener(Object listener) {
applicationEventListenersList.add(listener);
}
private Object applicationLifecycleListenersObjects[] = new Object[0];
public void addApplicationLifecycleListener(Object listener) {
int len = applicationLifecycleListenersObjects.length;
Object[] newListeners = Arrays.copyOf(
applicationLifecycleListenersObjects, len + 1);
newListeners[len] = listener;
applicationLifecycleListenersObjects = newListeners;
}
<!--
仔细看下 这个方法时一个重写方法 也就是看到这个方法 我才知道 我一开始 想的错了
我以为容器是调用了 上面的2个方法 也就是我们一路跟随的addListener方法
将我们XML里面的监听类 加入监听列表的
然而并不是这样的-->
@Override
public void addApplicationListener(String listener) {
synchronized (applicationListenersLock) {
String results[] = new String[applicationListeners.length + 1];
for (int i = 0; i < applicationListeners.length; i++) {
if (listener.equals(applicationListeners[i])) {
log.info(sm.getString("standardContext.duplicateListener",listener));
return;
}
results[i] = applicationListeners[i];
}
results[applicationListeners.length] = listener;
applicationListeners = results;
}
fireContainerEvent("addApplicationListener", listener);
}
/**
* Configure the set of instantiated application event listeners
* for this Context.
* @return <code>true</code> if all listeners wre
* initialized successfully, or <code>false</code> otherwise.
*/
public boolean listenerStart() {
// Instantiate the required listeners
String listeners[] = findApplicationListeners();//就是返回 applicationListeners[]数组
Object results[] = new Object[listeners.length];//这边是存储 我们监听类的是实例化后的对象
boolean ok = true;
for (int i = 0; i < results.length; i++) {
String listener = listeners[i];
results[i] = getInstanceManager().newInstance(listener);//实例化 我们的监听类
}
// 这个是吧 实例化的类 拆分成了2个监听数组对象 不知道为啥 ServletContextListener 是单独的一个
// 这个也对应了 applicationContext中的方法 也是按照这样的类型 拆分了存储的
ArrayList<Object> eventListeners = new ArrayList<>();
ArrayList<Object> lifecycleListeners = new ArrayList<>();
for (int i = 0; i < results.length; i++) {
if ((results[i] instanceof ServletContextAttributeListener)
|| (results[i] instanceof ServletRequestAttributeListener)
|| (results[i] instanceof ServletRequestListener)
|| (results[i] instanceof HttpSessionIdListener)
|| (results[i] instanceof HttpSessionAttributeListener)) {
eventListeners.add(results[i]);
}
if ((results[i] instanceof ServletContextListener)
|| (results[i] instanceof HttpSessionListener)) {
lifecycleListeners.add(results[i]);
}
}
for (Object eventListener: getApplicationEventListeners()) {
eventListeners.add(eventListener);
}
setApplicationEventListeners(eventListeners.toArray());
for (Object lifecycleListener: getApplicationLifecycleListeners()) {
lifecycleListeners.add(lifecycleListener);
<!--这边 通过applicationContext 添加的ServletContextListener 这个会被添加的2个监听集合中-->
if (lifecycleListener instanceof ServletContextListener) {
noPluggabilityListeners.add(lifecycleListener);
}
}
setApplicationLifecycleListeners(lifecycleListeners.toArray());
Object instances[] = getApplicationLifecycleListeners();
if (instances == null || instances.length == 0) {
return ok;
}
ServletContextEvent event = new ServletContextEvent(getServletContext());
ServletContextEvent tldEvent = null;
<!--这个就是 刚才上面说的 noPluggabilityListeners 的意思是不可插拔的监听列表 这个就是非WebXML 中配置的 ServletContextListener监听类-->
if (noPluggabilityListeners.size() > 0) {
noPluggabilityServletContext = new NoPluggabilityServletContext(getServletContext());
tldEvent = new ServletContextEvent(noPluggabilityServletContext);
}
for (int i = 0; i < instances.length; i++) {
if (!(instances[i] instanceof ServletContextListener))
continue;
ServletContextListener listener = (ServletContextListener) instances[i];
fireContainerEvent("beforeContextInitialized", listener);
if (noPluggabilityListeners.contains(listener)) {
listener.contextInitialized(tldEvent);
} else {
<!--这边就是 调用我们的ContextLoaderListener#contextInitialized的地方-->
listener.contextInitialized(event);
}
fireContainerEvent("afterContextInitialized", listener);
}
return ok;
}
}
看到这边 我们才知道 ContextLoaderListener#contextInitialized 是怎么被调用起来的 listenerStart的方法 就是处理监听类的地方
最后 关注下 addApplicationListener 这个类方法 我的注释 注释上也说了 看到了这个方法 我才明白 我前面想的是错的,
存放我们的XML的监听类的地方 其实是applicationListeners[] 数组
那既然addApplicationListener是重写的方法 他的父类是Context类
最后我有查找了下addApplicationListener方法时 是在哪些地方使用的 这样我们就知道我们的XML中配置的监听类是什么加进入 刚才上面的数组的
最后我在 tomcat-embed-core-8.5.45-sources.jar!\org\apache\catalina\startup\ContextConfig.java 类中找到了这个方法的使用 看名字要也能知道 这个是一个Context的配置类
这也能符合我们的想法 监听类就是在配置Context的是加入的 那我们看下是否 值这样的
ContextConfig
configureContext
public class ContextConfig implements LifecycleListener{
protected Context context = null;
<!--这个方法在LifecycleBase.java#fireLifecycleEvent被调用-->
@Override
public void lifecycleEvent(LifecycleEvent event) {
context = (Context) event.getLifecycle();
if (event.getType().equals(Lifecycle.CONFIGURE_START_EVENT)) {
configureStart();//开始配置
} else if (event.getType().equals(Lifecycle.BEFORE_START_EVENT)) {
beforeStart();
} else if (event.getType().equals(Lifecycle.AFTER_START_EVENT)) {
// Restore docBase for management tools
if (originalDocBase != null) {
context.setDocBase(originalDocBase);
}
} else if (event.getType().equals(Lifecycle.CONFIGURE_STOP_EVENT)) {
configureStop();
} else if (event.getType().equals(Lifecycle.AFTER_INIT_EVENT)) {
init();
} else if (event.getType().equals(Lifecycle.AFTER_DESTROY_EVENT)) {
destroy();
}
}
/**
* Process a "contextConfig" event for this Context.
*/
protected synchronized void configureStart() {
webConfig();//开始webConfig配置
if (!context.getIgnoreAnnotations()) {
applicationAnnotationsConfig();
}
if (ok) {
validateSecurityRoles();
}
// Configure an authenticator if we need one
if (ok) {
authenticatorConfig();
}
}
<!--根据webxml的信息 去配置Context-->
protected void webConfig() {
WebXml webXml = createWebXml();//创建WebXml对象
// Parse context level web.xml
InputSource contextWebXml = getContextWebXmlSource();//获取webXML的地址
if (!webXmlParser.parseWebXml(contextWebXml, webXml, false)) {
ok = false;
}
ServletContext sContext = context.getServletContext();
// Step 9. Apply merged web.xml to Context
if (ok) {
configureContext(webXml);//这边是拿到了webXml 开始配置
}
}
private void configureContext(WebXml webxml) {
for (FilterDef filter : webxml.getFilters().values()) {
if (filter.getAsyncSupported() == null) {
filter.setAsyncSupported("false");
}
context.addFilterDef(filter);
}
for (FilterMap filterMap : webxml.getFilterMappings()) {
context.addFilterMap(filterMap);
}
for (String listener : webxml.getListeners()) {
context.addApplicationListener(listener);//这边就是 添加我们监听类的地方
}
}
}
仔细看下这个ContextConfig这个类 从中我们可以看到 Context的创建也是从这个类中完成的 这个记住下
在继续深入下
到了这边 其实 已经讲的差不多了 但是 小伙伴们 一定想知道Tomcat 究竟是怎么去调用到 上面的执行方法的
我们知道listenerStart 中处理了监听事件 那这个又是怎么去运行的呢 和Tomcat 又有什么关系呢,由于篇幅问题 我就不一一列出代码了 大概的描述下
- tomcat-embed-core-8.5.45-sources.jar!\org\apache\catalina\startup\Tomcat.java 中有一个Start()方法
protected Server server;
public void start() throws LifecycleException {
getServer();
getConnector();
server.start();
}
public interface Server extends Lifecycle
这其中 server.start();方法是Lifecycle接口中
2. tomcat-embed-core-8.5.45-sources.jar!\org\apache\catalina\util\LifecycleBase.java
<!--LifecycleBase这个类实现了Lifecycle接口 -->
public abstract class LifecycleBase implements Lifecycle
{
public final synchronized void start() throws LifecycleException{
startInternal();//这边执行startInternal方法
}
protected abstract void startInternal() throws LifecycleException;
}
<!--LifecycleMBeanBase类 又继承了LifecycleBase类-->
public abstract class LifecycleMBeanBase extends LifecycleBase
<!--ContainerBase 又继承了LifecycleMBeanBase法-->
public abstract class ContainerBase extends LifecycleMBeanBase implements Container
- StandardContext 类 我们应该很熟悉了吧
<!--StandardContext 又继承了ContainerBase类 并且重写startInternal方法 -->
public class StandardContext extends ContainerBase implements Context, NotificationEmitter
{
protected synchronized void startInternal() throws LifecycleException {
<!--这个方法真的很长 要好好去找listenerStart-->
// Configure and call application event listeners
if (ok) {
if (!listenerStart()) {
log.error(sm.getString("standardContext.listenerFail"));
ok = false;
}
}
}
}
看到这个listenerStart 应该清楚了吧 ~结合我上面讲的 就能串联上了~
到此 我们应该知道了 从Tomcat 到ContextLoaderListener.java 是怎么运行起来的吧
最后还是给大家画个小图吧 简单明了 方便记忆 画图工具用的markdown 自带的 有点low 见谅哈
妈呀 这个图 花的好费劲呀 就这样吧~
总结一下
通过以上的分析 我们知道 ContextLoaderListener 是怎么被加入监听列表 contextInitialized 方法时怎么执行的
写在这边 我感觉我要分篇文章去写了 发现写的有的太细了~
但是我相信 通过这篇文章 你能知道 Servlet容器是怎么处理一个web应用的
最后还是 希望大家多多点赞 后面感觉还要写几篇
下面一篇文章 我就去分析下 ContextLoaderListener 中webApplication的创建
哇偶 断断续续 写了一天~ 真费劲呀 下次要快点了