tomcat源码分析-启动分析(四) webapp-爱代码爱编程
前言
上一篇文章中我们分析了 Service、Engine、Host、Pipeline、Valve 组件的启动逻辑,在 HostConfig 中会实例化 StandardContext,并启动 Context 容器,完成 webapp 应用程序的启动,这一块是最贴近我们开发的应用程序。在这一篇文章中,我们将要分析 tomcat 是如何解析并初始化应用程序定义的 Servlet、Filter、Listener 等
首先我们思考几个问题:
1、 tomcat 如何支持 servlet3.0 的注解编程,比如对 javax.servlet.annotation.WebListener 注解的支持?
如果 tomcat 利用 ClassLoader 加载 webapp 下面所有的 class,从而分析 Class 对象的注解,这样子肯定会导致很多问题,比如 MetaSpace 出现内存溢出,而且加载了很多不想干的类,我们知道 jvm 卸载 class 的条件非常苛刻,这显然是不可取的。因此,tomcat 开发了字节码解析的工具类,位于
org.apache.tomcat.util.bcel
,bcel 即 :Byte Code Engineering Library,专门用于解析 class 字节码,而不是像我们前面猜测的那样,把类加载到 jvm 中
1、 假如 webapp 目录有多个应用,使用的开源框架的 jar 版本不尽一致,tomcat 是怎样避免出现类冲突?
不同的 webapp 使用不同的 ClassLoader 实例加载 class,因此 webapp 内部加载的 class 是不同的,自然不会出现类冲突,当然这里要排除 ClassLoader 的 parent 能够加载的 class。关于 ClassLoader 这一块,后续会专门写一篇博客进行分析
1、Context 容器
首先,我们来看下StandardContext重要的几个属性,包括了我们熟悉的 ServletContext、servlet容器相关的Listener(比如 SessionListener 和 ContextListener)、FilterConfig
protected ApplicationContext context:即ServletContext上下文
private InstanceManager instanceManager:根据 class 实例化对象,比如 Listener、Filter、Servlet 实例对象
private List<Object> applicationEventListenersList:SessionListener、ContextListner 等集合
private HashMap<String, ApplicationFilterConfig> filterConfigs:filer 名字与 FilterConfig 的映射关系
private Loader loader:用于加载class等资源
private final ReadWriteLock loaderLock:用于对loader的读写操作
protected Manager manager:Session管理器
private final ReadWriteLock managerLock:用于对manager的读写操作
private HashMap<String, String> servletMappings:url与Servlet名字的映射关系
private HashMap<Integer, ErrorPage> statusPages:错误码与错误页的映射
private JarScanner jarScanner:用于扫描jar包资源
StandardContext 和其他 Container 一样,也是重写了 startInternal 方法。由于涉及到 webapp 的启动流程,需要很多准备工作,比如使用 WebResourceRoot 加载资源文件、利用 Loader 加载 class、使用 JarScanner 扫描 jar 包,等等。因此StandardContext 的启动逻辑比较复杂,这里描述下几个重要的步骤:
1、 创建工作目录,比如$CATALINA_HOME\work\Catalina\localhost\examples;实例化 ContextServlet,应用程序拿到的是 ApplicationContext的外观模式
2、 实例化 WebResourceRoot,默认实现类是 StandardRoot,用于读取 webapp 的文件资源
3、 实例化 Loader 对象,Loader 是 tomcat 对于 ClassLoader 的封装,用于支持在运行期间热加载 class
4、 发出 CONFIGURE_START_EVENT 事件,ContextConfig 会处理该事件,主要目的是从 webapp 中读取 servlet 相关的 Listener、Servlet、Filter 等
5、 实例化 Sesssion 管理器,默认使用 StandardManager
6、 调用 listenerStart,实例化 servlet 相关的各种 Listener,并且调用
ServletContextListener
7、 处理 Filter
8、 加载 Servlet
下面,将分析下几个重要的步骤
1.1 触发 CONFIGURE_START_EVENT 事件
ContextConfig 它是一个 LifycycleListener,它在 Context 启动过程中是承担了一个非常重要的角色。StandardContext 会发出 CONFIGURE_START_EVENT 事件,而 ContextConfig 会处理该事件,主要目的是通过 web.xml 或者 Servlet3.0 的注解配置,读取 Servlet 相关的配置信息,比如 Filter、Servlet、Listener 等,其核心逻辑在 ContextConfig#webConfig()
方法中实现。下面,我们对 ContextConfig 进行详细分析
1、 首先,是通过 WebXmlParser 对 web.xml 进行解析,如果存在 web.xml 文件,则会把文件中定义的 Servlet、Filter、Listener 注册到 WebXml 实例中
WebXmlParser webXmlParser = new WebXmlParser(context.getXmlNamespaceAware(),
context.getXmlValidation(), context.getXmlBlockExternal());
Set<WebXml> defaults = new HashSet<>();
defaults.add(getDefaultWebXmlFragment(webXmlParser));
// 创建 WebXml实例,并解析 web.xml 文件
WebXml webXml = createWebXml();
InputSource contextWebXml = getContextWebXmlSource();
if (!webXmlParser.parseWebXml(contextWebXml, webXml, false)) {
ok = false;
1、 接下来,会处理 javax.servlet.ServletContainerInitializer,把对象实例保存到 ContextConfig 的 Map 中,待 Wrapper 子容器添加到 StandardContext 子容器中之后,再把 ServletContainerInitializer 加入 ServletContext 中。ServletContainerInitializer 是 servlet3.0 提供的一个 SPI,可以通过 HandlesTypes 筛选出相关的 servlet 类,并可以对 ServletContext 进行额外处理,下面是一个自定义的 ServletContainerInitializer,实现了 ServletContainerInitializer 接口,和 jdk 提供的其它 SPI 一样,需要在 META-INF/services/javax.servlet.ServletContainerInitializer 文件中指定该类名 net.dwade.tomcat.CustomServletContainerInitializer
@HandlesTypes( Filter.class )
public class CustomServletContainerInitializer implements ServletContainerInitializer {
@Override
public void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException {
for ( Class<?> type : c ) {
System.out.println( type.getName() );
}
}
1、 如果没有 web.xml 文件,tomcat 会先扫描 WEB-INF/classes 目录下面的 class 文件,然后扫描 WEB-INF/lib 目录下面的 jar 包,解析字节码读取 servlet 相关的注解配置类,这里不得不吐槽下 serlvet3.0 注解,对 servlet 注解的处理相当重量级。tomcat 不会预先把该 class 加载到 jvm 中,而是通过解析字节码文件,获取对应类的一些信息,比如注解、实现的接口等,核心代码如下所示:
protected void processAnnotationsStream(InputStream is, WebXml fragment,
boolean handlesTypesOnly, Map<String,JavaClassCacheEntry> javaClassCache)
throws ClassFormatException, IOException {
// is 即 class 字节码文件的 IO 流
ClassParser parser = new ClassParser(is);
// 使用 JavaClass 封装 class 相关的信息
JavaClass clazz = parser.parse();
checkHandlesTypes(clazz, javaClassCache);
if (handlesTypesOnly) {
return;
}
AnnotationEntry[] annotationsEntries = clazz.getAnnotationEntries();
if (annotationsEntries != null) {
String className = clazz.getClassName();
for (AnnotationEntry ae : annotationsEntries) {
String type = ae.getAnnotationType();
if ("Ljavax/servlet/annotation/WebServlet;".equals(type)) {
processAnnotationWebServlet(className, ae, fragment);
}else if ("Ljavax/servlet/annotation/WebFilter;".equals(type)) {
processAnnotationWebFilter(className, ae, fragment);
}else if ("Ljavax/servlet/annotation/WebListener;".equals(type)) {
fragment.addListener(className);
} else {
// Unknown annotation - ignore
}
}
}
tomcat 使用自己的工具类 ClassParser 通过对字节码文件进行解析,获取其注解,并把 WebServlet、WebFilter、WebListener 注解的类添加到 WebXml 实例中,统一由它对 ServletContext 进行参数配置。tomcat 对字节码的处理是由org.apache.tomcat.util.bcel
包完成的,bcel 即 Byte Code Engineering Library,其实现比较繁锁,需要对字节码结构有一定的了解,感兴趣的童鞋可以研究下底层实现。
1、 配置信息读取完毕之后,会把 WebXml 装载的配置赋值给 ServletContext,在这个时候,ContextConfig 会往 StardardContext 容器中添加子容器(即 Wrapper 容器),部分代码如下所示:
private void configureContext(WebXml webxml) {
// 设置 Filter 定义
for (FilterDef filter : webxml.getFilters().values()) {
if (filter.getAsyncSupported() == null) {
filter.setAsyncSupported("false");
}
context.addFilterDef(filter);
}
// 设置 FilterMapping,即 Filter 的 URL 映射
for (FilterMap filterMap : webxml.getFilterMappings()) {
context.addFilterMap(filterMap);
}
// 往 Context 中添加子容器 Wrapper,即 Servlet
for (ServletDef servlet : webxml.getServlets().values()) {
Wrapper wrapper = context.createWrapper();
// 省略若干代码。。。
wrapper.setOverridable(servlet.isOverridable());
context.addChild(wrapper);
}
// ......
1、 tomcat 还会加载 WEB-INF/classes/META-INF/resources/、WEB-INF/lib/xxx.jar/META-INF/resources/ 的静态资源,这一块的作用暂时不清楚,关键代码如下所示:
// fragments 包括了 WEB-INF/classes、WEB-INF/lib/xxx.jar
protected void processResourceJARs(Set<WebXml> fragments) {
for (WebXml fragment : fragments) {
URL url = fragment.getURL();
if ("jar".equals(url.getProtocol()) || url.toString().endsWith(".jar")) {
try (Jar jar = JarFactory.newInstance(url)) {
jar.nextEntry();
String entryName = jar.getEntryName();
while (entryName != null) {
if (entryName.startsWith("META-INF/resources/")) {
context.getResources().createWebResourceSet(
WebResourceRoot.ResourceSetType.RESOURCE_JAR,
"/", url, "/META-INF/resources");
break;
}
jar.nextEntry();
entryName = jar.getEntryName();
}
}
} else if ("file".equals(url.getProtocol())) {
File file = new File(url.toURI());
File resources = new File(file, "META-INF/resources/");
if (resources.isDirectory()) {
context.getResources().createWebResourceSet(
WebResourceRoot.ResourceSetType.RESOURCE_JAR,
"/", resources.getAbsolutePath(), null, "/");
}
}
}