代码编织梦想

前言

上一篇文章中我们分析了 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, "/");
            }
        }
    }
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qq_33291299/article/details/129632515

tomcat8源码分析系列-启动分析(四) webapp_黄小厮的博客-爱代码爱编程

前言 上一篇文章中我们分析了 Service、Engine、Host、Pipeline、Valve 组件的启动逻辑,在 HostConfig 中会实例化 StandardContext,并启动 Context 容器,完成

tomcat源码分析---启动过程_hixiaoxiaoniao的博客-爱代码爱编程

   Tomcat启动是由Bootstrap开始的,大家都知道Tomcat最顶层的容器是Server,而Server又是是由Catalina调用的,理论上可以不用Bootstrap,只用Catalina就可以启动tomat了,使用Bootstrap的理由是。设计者希望有多种方式启动Tomcat,Catalina是一种固定的启动方式,而调用它的方式可能有

tomcat源码分析 -- tomcat整体架构_lppl010_的博客-爱代码爱编程

https://blog.csdn.net/w1992wishes/article/details/79242797 本章结构如下: 前言Tomcat顶层结构ServerServiceConnectorContainerTomncat启动流程 一、前言 一般而言,对于一个复杂的系统,直接扎进去看源码会是很难受的,会浪费大量的时间和脑细胞,却得不到理想的

tomcat源码系列---启动分析之catalina启动_lhrimperial的博客-爱代码爱编程

tomcat源码系列—启动分析之Catalina启动 tomcat的初始化过程,是由Bootstrap反射调用Catalina的load方法完成tomcat的初始化,包括server.xml的解析、实例化各大组件、初始化组

tomcat源码系列---启动分析之webapp_lhrimperial的博客-爱代码爱编程

tomcat源码系列—启动分析(四)webapp 前言 上一篇文章中我们分析了 Service、Engine、Host、Pipeline、Valve 组件的启动逻辑,在 HostConfig 中会实例化 Standard

tomcat源码解析---web项目在tomcat中的启动过程分析_笨笨小孩945的博客-爱代码爱编程

平常开发中只需要把开发好的war包上传到服务器,启动服务器,web项目就跟着启动运行了,这是为什么?服务器都做了哪些事情?下面我们通过跟踪调试tomcat源码,分析一下web项目的启动过程。 源码下载地址: http://m

java双亲委派模型_Tomcat源码分析 - 遵循双亲委派模型与打破双亲委派共存,Tomcat类加载器源码分析...-爱代码爱编程

背景 我们项目在类加载时一般都遵循双亲委派模型,但在Tomcat项目源码中却打破了双亲委派机制的模型,在每个Webapp应用启动加载时创建独立WebappClassLoader类加载器,这样做是出于哪些重要的考虑?于此同时Tomcat也创建了多种类型的全局类加载器,并且它们负责加载特定的配置路径,它为什么要这样设计,是出于什么考虑?今天我们通过源码就

三. Tomcat源码分析-类加载和类加载器-爱代码爱编程

三. Tomcat源码分析-类加载和类加载器 1. 使用maven构建tomcat源码 1. 下载源码 https://tomcat.apache.org/download-80.cgi 2. 新建catalina-home目录 解压、新建catalina-home目录,同时将目录中的conf和webapps文件夹复制到catalina-home

tomcat源码分析-类加载器-爱代码爱编程

类加载器 在分析 tomcat 类加载之前,我们简单的回顾下 java 体系的类加载器 启动类加载器(Bootstrap ClassLoader):加载对象是java的核心类库,把一些的 java 类加载到 jvm 中,它并不是我们熟悉的 ClassLoader,而是 jvm 层面由 C/C++ 实现的类加载器,负责加载 $JAVA_HOME/jre/

tomcat源码分析-session源码解析-爱代码爱编程

tomcat session 设计分析 tomcat session 组件图如下所示,其中 Context 对应一个 webapp 应用,每个 webapp 有多个 HttpSessionListener, 并且每个应用的 session 是独立管理的,而 session 的创建、销毁由 Manager 组件完成,它内部维护了 N 个 Session 实

javaweb——servlet方法介绍和体系结构-爱代码爱编程

Servlet方法 getServletInfo()方法  用于获取信息,通常返回null或者空字符串 @Override public String getServletInfo() { return ""; }  getServletConfig()方法 用于获取Servlet配置对象 在init

tomcat spring web项目源代码加密-爱代码爱编程

为了防止产品代码泄漏或授权等被破解,想到对源码加密,说是对源码加密,实际是需要对class文件进行加密。如果对class文件加密了,那类加载器如何能解析呢?本文讲解的就是SpringWeb项目加密后如何能在tomcat下面启

javaweb——servlet执行流程和生命周期-爱代码爱编程

Servlet执行流程 在JavaWeb——Servlet简介以及入门案例_北岭山脚鼠鼠的博客-CSDN博客这个入门案例里面,实现了通过http://localhost:8080/web-demo/demo1该路径实现了对web项目的访问 url分为了三部分,http://localhost:8080/ 实现了连接Tomcat服务器,web-dem

springmvc-请求与响应-爱代码爱编程

SpringMVC-请求与响应 4,请求与响应 SpringMVC是web层的框架,主要的作用是接收请求、接收数据、响应结果 请求映射路径请求参数日期类型参数传递响应json数据 4.1 设置请求映射路径 4.1.

tomcat源码分析-爱代码爱编程

Server初始化 StandardServer是由Catalina进行init初始化的,调用的是LifecycleBase父类的init方法,而StandardServer继承至LifecycleMBeanBase,重写了initInternal方法。关于这块的知识,请参考上一篇Lifecycle的博客 StandardServer初始化的时序图如下