从Servlet 3.0 开始Tomcat已经支持注解式的配置。了解下,在注解的配置方式下,Web是怎样启动起来的。
通过注解配置一个Web应用
下面是一个通过注解实现一个简单的Web应用
public class SpringWebInitializer extend AbstractAnnotationConfigDispatcherServletInitializer { //这里可以配置servlet,filter,listener,context param,attribute等 }
类图如下:
上面四个类,各有各的职责,相对于些.xml
文件,你只需要实现这些抽象类的抽象方法即可。详细解释这里省略。
我们现在关注最顶层的那个接口 WebApplicaitonInitializer
。经过查看其源码。
public interface WebApplicationInitializer { /** * Configure the given {@link ServletContext} with any servlets, filters, listeners * context-params and attributes necessary for initializing this web application. See * examples {@linkplain WebApplicationInitializer above}. * @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; }
SpringServletContainerInitializer
起作用的。源码如下:@HandlesTypes(WebApplicationInitializer.class) public class SpringServletContainerInitializer implements ServletContainerInitializer { @Override public void onStartup(Set<Class<?>> webAppInitializerClasses, ServletContext servletContext) throws ServletException { .......去除无用代码....... for (WebApplicationInitializer initializer : initializers) { // 通知启动WebApplicationInitializer 的实现 initializer.onStartup(servletContext); } } }
Tomcat:
在Tomcat应用服务器启动以后。它会通过反射,利用ClassLoader来加载Web-App文件夹下面的Web应用的jar包。
我们知道,每个Web应用都有一个自己唯一的一个ServletContext
,在Tomcat里面,SerlvetContext
的实现类是ApplicationContext
。看名字也可以知道这代表着一个Application应用的上下文环境。我们现在只关心这个类就可以了。这个类里面有个类型为StandardContext,这是Context
标准实现。
说到这个类,然后我们就需要谈起一个接口ServletContextListener
,这个接口用来提供一个观察者模式,简单的讲就是用来监控ServletContext的启动,销毁生命周期的。
看一下Tomcat所有容器的抽象类里面的ContainerMBean
类里面的addChild
方法,这个StandardContext
x其实就是一个ServletContext
,即为一个应用容器。
//为容器添加子容器 public void addChild(String type, String name) throws MBeanException{ Container contained = (Container) newInstance(type); contained.setName(name); if(contained instanceof StandardHost){ HostConfig config = new HostConfig(); contained.addLifecycleListener(config); } else if(contained instanceof StandardContext){ //添加的容器是一个Tomcat子容器的话,就分配其一个ContextCofig ContextConfig config = new ContextConfig(); //将ContextConfig添加到Container的监听者行列中 contained.addLifecycleListener(config); } boolean oldValue= true; ContainerBase container = doGetManagedResource(); try { oldValue = container.getStartChildren(); container.setStartChildren(false); container.addChild(contained); /* 开始一个初始化,会通知所有正在监听Container的观察者 对于ContextConfig来说,现在应该做的是加载配置等 */ contained.init(); } catch (LifecycleException e){ throw new MBeanException(e); } finally { if(container != null) { container.setStartChildren(oldValue); } } }
Tomcat 在为Host容器添加Context子容器时,会为其分配一个CntextConfig
类。当你看到这个类名应该就会想到,这应该是和Web配置加载有关的一个类。
@Override public void lifecycleEvent(LifecycleEvent event) { ......省略无用代码...... // Process the event that has occurred if (event.getType().equals(Lifecycle.CONFIGURE_START_EVENT)) { //当监听的容器发送,CONFIGURE_START_EVENT时,配置开始 configureStart(); } .....省略无用代码....... }
再来看看configureStart()
方法
protected synchronized void configureStart() { ....省略无用代码····· webConfig(); if (!context.getIgnoreAnnotations()) { applicationAnnotationsConfig(); } ·····省略无用代码····· }
代码中可以看到,Tomcat先是从调用WebConfig()
函数.这个函数的主要的动作就是读取web.xml
配置文件,
- 函数webConfig()的执行动作的流程
- 读取web-fragment.xml和各个jar模块
- 排序所有读取到的fragments
- 查找所有的ServletContainerInitializer(SCIs)
- 处理WEB-INF/Classes文件夹下面的
- 处理所有的注解配置类和,并缓存
- 将所有的web-fragment.xml合并
- 转换所有的JSP代码成Java代码
- 将Web.xml配置转变成代码式的配置
- 查找静态资源默认文件夹
WEB-INF/classes/META-INF/resources
- 将所有的实现ServletContainerInitializers的类添加到StandardContext的initializers集合中
我们在这里重点关注 第三步和第十三步,webConfig()中会调用processServletContainerInitializers()
这个方法即为加载所有的经过 @HandlesTypes注解的类。
protected void webConfig() { .... // Step 3. 查找ServletContainerInitializers if (ok) { processServletContainerInitializers(); } ...... // Step 11. ServletContainerInitializer 交给StandardContext去处理!在这里即为我们的应用所在的容器 if (ok) { for (Map.Entry<ServletContainerInitializer, Set<Class<?>>> entry : initializerClassMap.entrySet()) { if (entry.getValue().isEmpty()) { context.addServletContainerInitializer( entry.getKey(), null); } else { context.addServletContainerInitializer( entry.getKey(), entry.getValue()); } } } }
我们去StandardContext里面去看一下它是怎么处理这个集合属性的。在StandardContext
类里面的startInternel()
方法,就是启动这个
@Override protected synchronized void startInternal() throws LifecycleException { .... // Call ServletContainerInitializers,启动这些 for (Map.Entry<ServletContainerInitializer, Set<Class<?>>> entry : initializers.entrySet()) { try { //启动所有的ServletContainerInitializer entry.getKey().onStartup(entry.getValue(), getServletContext()); } catch (ServletException e) { log.error(sm.getString("standardContext.sciFail"), e); ok = false; break; } } ... }
即: 通过内部启动时,它会通知所有正在监听的 ServletContainerInitializers,这样,即完成了Web应用的加载和初始化的配置!
总结
- Tomcat的Host容器在添加子容器时,会通过解析.xml并通过classloader加载 @HandlesTypes注解的类
- 读取@HandlesTypes注解value值。并放入ServletContainerInitializers 对应的Set集合中
- 在ApplicationContext 内部启动时会通知 ServletContainerInitializers 的onStart方法()。这个onStart方法的第一个参数就是@HandlesTypes注解的value 值指定的Class集合
- 在Spring 应用中,对ServletContainerInitializers的实现就是SpringServletContainerInitializer,注解指定的类就是WebApplicationInitializer.
@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {
@Override
public void onStartup(Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)
throws ServletException {
.......篇幅有限,去除无用代码.......
for (WebApplicationInitializer initializer : initializers) { // 通知启动WebApplicationInitializer 的实现
initializer.onStartup(servletContext);
}
}
}
参考:https://docs.spring.io/spring/docs/current/spring-framework-reference/web.html#mvc