上一篇文章中我们分析了 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 这一块,后续会专门写一篇博客进行分析
首先,我们来看下StandardContext重要的几个属性,包括了我们熟悉的 ServletContext、servlet容器相关的Listener(比如 SessionListener 和 ContextListener)、FilterConfig
protected ApplicationContext context:即ServletContext上下文private InstanceManager instanceManager:根据 class 实例化对象,比如 Listener、Filter、Servlet 实例对象private List
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
下面,将分析下几个重要的步骤
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 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 {@Overridepublic void onStartup(Set> 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 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,即 Servletfor (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 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, "/");}}}