在上一章说过,当StandardContext被添加到StandardHost下,会触发StandardContext的start方法进行启动,首先自然是调用StandardContext的initInternal(),接着是startInternal(),initInternal()也没做多少关键的事,而startInternal()又过于多,300的方法实在很难分析,所以,还是只找几个关键的地方。
一、交给ContextConfig处理
在上一章也说过,StandardContext还要依赖ContextConfig来进行工作,而StandardContext要想调用ContextConfig,需要发出一个事件,遍历所有监听者,把事件传递给他,也就是下面这段。事件名称为CONFIGURE_START_EVENT。
//告诉ContextConfig开始配置启动了
fireLifecycleEvent(Lifecycle.CONFIGURE_START_EVENT, null);
接着是ContextConfig的lifecycleEvent方法,首先判断事件类型,如果是CONFIGURE_START_EVENT,则直接调用configureStart开始加载,configureStart具体实现也在webConfig方法中。
if (event.getType().equals(Lifecycle.CONFIGURE_START_EVENT)) {
/**
* 当StandardContext发出CONFIGURE_START_EVENT事件时,开始配置web项目
*/
configureStart();
} else if (event.getType().equals(Lifecycle.BEFORE_START_EVENT)) {
beforeStart();
}
protected synchronized void configureStart() {
/**
* 具体配置实现
*/
webConfig();
}
............
webConfig方法也就是本文的重中之重,这里面涉及很多复杂的问题。
大概分四步骤,读取解析配置文件,也就是web.xml、找出带有注解的Class、合并、向StandardContext中设置对应属性,包括创建和添加Wrapper。
这里找出注解的意思是找到带有WebServlet、WebFilter、WebListener注解的类,也就是如为什么我们不用在web.xml中配置servlet,而使用@WebServlet即可的原因。
一个项目中有多个Servlet,每个Servlet要被Wrapper所管理,也就是一个Servlet对应一个Wrapper,这些Wrapper还要被StandardContext管理,故要将创建的Wrapper作为子容器添加到StandardContext。
二、读取配置文件
当然读取web.xml也是通过Digester来完成,此时的Digester通过DigesterFactory来创建,web.xml的读取规则在WebRuleSet中,也是非常多。
protected void webConfig() {
/**
* 处理web.xml的配置
*/
if (!webXmlParser.parseWebXml(contextWebXml, webXml, false)) {
ok = false;
}
}
读取后存放在webXml对象中,如内部变量servlets存放着所有配置的Servlet信息,servlets是一个HaspMap,key为<servlet-name>
配置的值,value为ServletDef,ServletDef内部的servletClass变量保存着这个Servlet的完整类名。
还有内部变量servletMappings保存着映射关系,也是一个HashMap,key为url地址,value为servlets变量的key。
三、处理带有注解的Class
if (!webXml.isMetadataComplete() || typeInitializerMap.size() > 0) {
/**
* 处理带有注解的Servlet
*/
processClasses(webXml, orderedFragments);
}
但是你会发现,这一步是有条件的,第一个条件也就是web.xml配置的属性metadata-complete="true|false",如果配置为true,最终取反为false,那就是说必须第二个条件满足才处理注解。metadata-complete属性意思也就是告诉Tomcat是否寻找注解。
而第二个条件虽然判断的只是size>0,但是往这个集合中put的数据的过程还是复杂的,在前一步的processServletContainerInitializers方法中处理,大概就是扫描当前应用每一个jar包里面META-INF/services/javax.servlet.ServletContainerInitializer所指定的实现类,并且这个实现类带有注解HandlesTypes才添加,默认貌似是0个,所以,处不处理注解由metadata-complete说了算。
要完本步任务,还不止一个方法,首先看processClasses,在这个方法中首先列举出/WEB-INF/classes下的所有资源,然后遍历,如果不是META-INF目录,则再交给processAnnotationsWebResource处理。
protected void processClasses(WebXml webXml, Set<WebXml> orderedFragments) {
Map<String, JavaClassCacheEntry> javaClassCache = new HashMap<>();
if (ok) {
/**
* 获取项目/WEB-INF/classes下所有资源
*/
WebResource[] webResources =
context.getResources().listResources("/WEB-INF/classes");
for (WebResource webResource : webResources) {
/**
* 如果为META-INF目录则跳过
*/
if ("META-INF".equals(webResource.getName())) {
continue;
}
processAnnotationsWebResource(webResource, webXml,
webXml.isMetadataComplete(), javaClassCache);
}
}
if (ok) {
processAnnotations(orderedFragments, webXml.isMetadataComplete(), javaClassCache);
}
javaClassCache.clear();
}
在进一层,在此方法中,逻辑也比较简单,就是依次拿出所有class文件,在交给processAnnotationsStream处理。
protected void processAnnotationsWebResource(WebResource webResource,
WebXml fragment, boolean handlesTypesOnly,
Map<String, JavaClassCacheEntry> javaClassCache) {
/**
* 如果还是文件夹,递归遍历
*/
if (webResource.isDirectory()) {
WebResource[] webResources =
webResource.getWebResourceRoot().listResources(
webResource.getWebappPath());
if (webResources.length > 0) {
if (log.isDebugEnabled()) {
log.debug(sm.getString(
"contextConfig.processAnnotationsWebDir.debug",
webResource.getURL()));
}
for (WebResource r : webResources) {
//递归
processAnnotationsWebResource(r, fragment, handlesTypesOnly, javaClassCache);
}
}
/**
* 如果是一个文件,并且文件名以.class结尾
*/
} else if (webResource.isFile() &&
webResource.getName().endsWith(".class")) {
/**
* 拿到他的输入流
*/
try (InputStream is = webResource.getInputStream()) {
/**
* 处理这个Class
*/
processAnnotationsStream(is, fragment, handlesTypesOnly, javaClassCache);
} catch (IOException e) {
log.error(sm.getString("contextConfig.inputStreamWebResource",
webResource.getWebappPath()), e);
} catch (ClassFormatException e) {
log.error(sm.getString("contextConfig.inputStreamWebResource",
webResource.getWebappPath()), e);
}
}
}
processAnnotationsStream中的难点其实是JavaClass对象。也就是如何解析parser.parse(),这个过程没有深入了解。
最终可以根据JavaClass获得其class的父类、接口集合、类名、注解等信息。
但是要判断handlesTypesOnly变量,为false才继续处理,其实在此处有一个有意思的事,在后面会说。
protected void processAnnotationsStream(InputStream is, WebXml fragment,
boolean handlesTypesOnly, Map<String, JavaClassCacheEntry> javaClassCache)
throws ClassFormatException, IOException {
ClassParser parser = new ClassParser(is);
/**
* 解析成JavaClass对象
*/
JavaClass clazz = parser.parse();
checkHandlesTypes(clazz, javaClassCache);
if (handlesTypesOnly) {
return;
}
processClass(fragment, clazz);
}
这是倒数第二步,根据不同的注解处理不同的逻辑,其实都是向WebXml中添加对象,也就是添加和映射Servlet、Filter、Listener。
protected void processClass(WebXml fragment, JavaClass clazz) {
/**
* 获取类中所有注解
*/
AnnotationEntry[] annotationsEntries = clazz.getAnnotationEntries();
if (annotationsEntries != null) {
String className = clazz.getClassName();
/**
* 遍历类中的注解
*/
for (AnnotationEntry ae : annotationsEntries) {
String type = ae.getAnnotationType();
/**
* 如果是@WebServlet注解
*/
if ("Ljavax/servlet/annotation/WebServlet;".equals(type)) {
processAnnotationWebServlet(className, ae, fragment);
/**
* 如果是@WebFilter注解
*/
} else if ("Ljavax/servlet/annotation/WebFilter;".equals(type)) {
processAnnotationWebFilter(className, ae, fragment);
/**
* 如果是@WebListener注解
*/
} else if ("Ljavax/servlet/annotation/WebListener;".equals(type)) {
fragment.addListener(className);
} else {
}
}
}
}
当上面这些结束后,当前的webXml对象中存放着项目所有Servlet、Listener等信息,也就是把web.xml中的配置信息都映射到了WebXml对象中。
四、合并
接下来还要为项目添加一个默认的处理,默认的处理通过getDefaultWebXmlFragment获取,通过webXml.merge(defaults);
合并。但这一步还是要metadata-complete=false的,
你会发现默认有个/地址,如果我们的项目也配置了个/该怎么处理?
其实Tomcat还是会优先我们的。
五、configureContext
这算是最后一步,这部分主要将WebXml对象中的信息转移到StandardContext中。所以,我们会在这个方法中大量看到context.setXXX(webXml.getxxx());
在上面也提到过会把Servlet类放入Wrapper中,让Wrapper管理我们的Servlet,所以,就有了下面一段代码,最终构造好Wrapper将他添加到StandardContext中。
(Wrapper的实现类是StandardWrapper)
for (ServletDef servlet : webxml.getServlets().values()) {
Wrapper wrapper = context.createWrapper();
//wrapper.setXXX .....
//给Wrapper设置Servlet名。
wrapper.setServletClass(servlet.getServletClass());
//添加到Context中
context.addChild(wrapper);
}
六、结尾1
回到StandardContext的startInternal方法,下面还有个关键点,也就是下面这段,findChildren此时返回所有Wrapper。
/**
* 加载并启动项目
*/
if (!loadOnStartup(findChildren())){
log.error(sm.getString("standardContext.servletFail"));
ok = false;
}
loadOnStartup方法找出loadOnStartup大于0的Wrapper,并按照loadOnStartup的大小依次实例化并调用Servlet的init()方法。
这里的loadOnStartup也就是我们配置的<load-on-startup>
。
public boolean loadOnStartup(Container children[]) {
TreeMap<Integer, ArrayList<Wrapper>> map = new TreeMap<>();
/**
* 遍历Wrapper
*/
for (int i = 0; i < children.length; i++) {
Wrapper wrapper = (Wrapper) children[i];
int loadOnStartup = wrapper.getLoadOnStartup();
/**
* 如果loadOnStartup小于0,跳过开始下一个
*/
if (loadOnStartup < 0)
continue;
Integer key = Integer.valueOf(loadOnStartup);
ArrayList<Wrapper> list = map.get(key);
if (list == null) {
list = new ArrayList<>();
map.put(key, list);
}
/**
* 记录下这个loadOnStartup大于0的wrapper
*/
list.add(wrapper);
}
/**
* 遍历所有loadOnStartup值大于0的Wrapper
*/
for (ArrayList<Wrapper> list : map.values()) {
for (Wrapper wrapper : list) {
try {
/**
* 启动
*/
wrapper.load();
} catch (ServletException e) {
getLogger().error(sm.getString("standardContext.loadOnStartup.loadException",
getName(), wrapper.getName()), StandardWrapper.getRootCause(e));
if(getComputedFailCtxIfServletStartFails()) {
return false;
}
}
}
}
return true;
}
Warpper.load先根据Servlet类名实例化,后在initServlet(),initServlet就是调用到我们的init方法。
private synchronized void initServlet(Servlet servlet)
throws ServletException {
// xxxxxxxxxxxxxxxxxxxxxx
servlet.init(facade); //init
// xxxxxxxxxxxxxxxxxxxx
七、结尾2
在上面说过,这段代码还要判断handlesTypesOnly为false才继续解析注解,processAnnotationsStream被两个地方所调用,其中一个地方就是上述在解析项目目录下所有class文件的时候,还有个地方是在解析jar包的时候。
protected void processAnnotationsStream(InputStream is, WebXml fragment,
boolean handlesTypesOnly, Map<String, JavaClassCacheEntry> javaClassCache)
throws ClassFormatException, IOException {
ClassParser parser = new ClassParser(is);
/**
* 解析成JavaClass对象
*/
JavaClass clazz = parser.parse();
checkHandlesTypes(clazz, javaClassCache);
if (handlesTypesOnly) {
return;
}
processClass(fragment, clazz);
}
这又得看 processJarsForWebFragments方法,返回/WEB-INF/lib下的所有jar。
也就是说,我们只要写一个类,继承HttpServlet,并且加入注解@WebServlet,打包成jar,放入WEB-INF/lib下,也是可以正常工作的。
其实加不加载这个jar中Servlet还是由这段控制,如果htOnly运算结果为false,则解析,但是processJarsForWebFragments的返回值还不止是/WEB-INF/lib下的jar,还有TOMCAT_HOME/lib目录下的jar(这里指非Tomcat本身的jar),这是不是说明我们打包后放入这个下面也能解析?
其实不是的,fragment.getWebappJar()的返回值代表是不是项目的一部分,如果不是则返回false,最终取反为true,那么到了processAnnotationsStream中是不会继续解析的。
(fragment.getWebappJar()通过类加载器来判断)
protected void processAnnotations(Set<WebXml> fragments,
boolean handlesTypesOnly, Map<String, JavaClassCacheEntry> javaClassCache) {
for (WebXml fragment : fragments) {
boolean htOnly = handlesTypesOnly || !fragment.getWebappJar() ||
fragment.isMetadataComplete();
...............
processAnnotationsUrl(url, annotations, htOnly, javaClassCache);
...............
}
}