Tomcat启动类加载机制总结

1 背景知识:

(1) Tomcat和CataLina是什么关系?

 Tomcat的前身为Catalina,Catalina又是一个轻量级的Servlet容器。

Tomcat的前身为Catalina,Catalina又是一个轻量级的Servlet容器。在美国,catalina是一个很美的小岛。所以Tomcat作者的寓意可能是想把Tomcat设计成一个优雅美丽且轻量级的web服务器。Tomcat从4.x版本开始除了作为支持Servlet的容器外,额外加入了很多的功能,比如:jsp、el、naming等等,所以说Tomcat不仅仅是Catalina
 
(2) Servlet与Tomcat的逻辑?
 
所谓Servlet,其实就是Sun为了让Java能实现动态可交互的网页,从而进入Web编程领域而制定的一套标准。

在互联网兴起之初,当时的Sun公司(后面被Oracle收购)已然看到了这次机遇,于是设计出了Applet来对Web应用的支持。不过事实却并不是预期那么得好,Sun悲催地发现Applet并没有给业界带来多大的影响。经过反思,Sun就想既然机遇出现了,市场前景也非常不错,总不能白白放弃了呀,怎么办呢?于是又投入精力去搞一套规范出来,这时Servlet诞生了!

一个Servlet主要做下面三件事情:

  • 创建并填充Request对象,包括:URI、参数、method、请求头信息、请求体信息等
  • 创建Response对象
  • 执行业务逻辑,将结果通过Response的输出流输出到客户端

Servlet没有main方法,所以,如果要执行,则需要在一个容器里面才能执行,这个容器就是为了支持Servlet的功能而存在,Tomcat其实就是一个Servlet容器的实现

(3) Tomcat总体架构

从组件的角度看

  • Server: 表示服务器,它提供了一种优雅的方式来启动和停止整个系统,不必单独启停连接器和容器;它是Tomcat构成的顶级构成元素,所有一切均包含在Server中;

  • Service: 表示服务,Server可以运行多个服务。比如一个Tomcat里面可运行订单服务、支付服务、用户服务等等;Server的实现类StandardServer可以包含一个到多个Services, Service的实现类为StandardService调用了容器(Container)接口,其实是调用了Servlet Engine(引擎),而且StandardService类中也指明了该Service归属的Server;

  • Container: 表示容器,可以看做Servlet容器;引擎(Engine)、主机(Host)、上下文(Context)和Wraper均继承自Container接口,所以它们都是容器。

    • Engine -- 引擎
    • Host -- 主机
    • Context -- 上下文
    • Wrapper -- 包装器
  • Connector: 表示连接器, 它将Service和Container连接起来,首先它需要注册到一个Service,它的作用就是把来自客户端的请求转发到Container(容器),这就是它为什么称作连接器, 它支持的协议如下:

    • 支持AJP协议
    • 支持Http协议
    • 支持Https协议
  • Service内部还有各种支撑组件,下面简单罗列一下这些组件

    • Manager -- 管理器,用于管理会话Session
    • Logger -- 日志器,用于管理日志
    • Loader -- 加载器,和类加载有关,只会开放给Context所使用
    • Pipeline -- 管道组件,配合Valve实现过滤器功能
    • Valve -- 阀门组件,配合Pipeline实现过滤器功能
    • Realm -- 认证授权组件 

从源码的设计角度看

  • Jsper模: 这个子模块负责jsp页面的解析、jsp属性的验证,同时也负责将jsp页面动态转换为java代码并编译成class文件。在Tomcat源代码中,凡是属于org.apache.jasper包及其子包中的源代码都属于这个子模块;

  • Servlet和Jsp模块: 这个子模块的源代码属于javax.servlet包及其子包,如我们非常熟悉的javax.servlet.Servlet接口、javax.servet.http.HttpServlet类及javax.servlet.jsp.HttpJspPage就位于这个子模块中;

  • Catalina模块: 这个子模块包含了所有以org.apache.catalina开头的java源代码。该子模块的任务是规范了Tomcat的总体架构,定义了Server、Service、Host、Connector、Context、Session及Cluster等关键组件及这些组件的实现,这个子模块大量运用了Composite设计模式。同时也规范了Catalina的启动及停止等事件的执行流程。从代码阅读的角度看,这个子模块应该是我们阅读和学习的重点。

  • Connector模块: 如果说上面三个子模块实现了Tomcat应用服务器的话,那么这个子模块就是Web服务器的实现。所谓连接器(Connector)就是一个连接客户和应用服务器的桥梁,它接收用户的请求,并把用户请求包装成标准的Http请求(包含协议名称,请求头Head,请求方法是Get还是Post等等)。同时,这个子模块还按照标准的Http协议,负责给客户端发送响应页面,比如在请求页面未发现时,connector就会给客户端浏览器发送标准的Http 404错误响应页面。

  • Resource模块: 这个子模块包含一些资源文件,如Server.xml及Web.xml配置文件。严格说来,这个子模块不包含java源代码,但是它还是Tomcat编译运行所必需的。

从一个完整请求的角度来看

假设来自客户的请求为:http://localhost:8080/test/index.jsp 请求被发送到本机端口8080,被在那里侦听的Coyote HTTP/1.1 Connector,然后

  • Connector把该请求交给它所在的Service的Engine来处理,并等待Engine的回应
  • Engine获得请求localhost:8080/test/index.jsp,匹配它所有虚拟主机Host
  • Engine匹配到名为localhost的Host(即使匹配不到也把请求交给该Host处理,因为该Host被定义为该Engine的默认主机)
  • localhost Host获得请求/test/index.jsp,匹配它所拥有的所有Context
  • Host匹配到路径为/test的Context(如果匹配不到就把该请求交给路径名为""的Context去处理)
  • path="/test"的Context获得请求/index.jsp,在它的mapping table中寻找对应的servlet
  • Context匹配到URL PATTERN为*.jsp的servlet,对应于JspServlet类,构造HttpServletRequest对象和HttpServletResponse对象,作为参数调用JspServlet的doGet或doPost方法
  • Context把执行完了之后的HttpServletResponse对象返回给Host
  • Host把HttpServletResponse对象返回给Engine
  • Engine把HttpServletResponse对象返回给Connector
  • Connector把HttpServletResponse对象返回给客户browser

2 Tomcat启动过程

直接看时序图:

 

 我们可以看到,Tomcat启动就是从main方法开始。我们下载Tomcat的二进制源码文件,通过ant进行build生成源文件。我们把生成的源文件bootstrap.jar和cataline.jar进行解压可以看到源代码。

 

 

Tomcat的main方法在org.apache.catalina.startup.Bootstrap 里

首先我们来看bootstap的main方法:

 1  public static void main(String[] args) {
 2         synchronized(daemonLock) {
 3             if (daemon == null) {
 4                 Bootstrap bootstrap = new Bootstrap();
 5 
 6                 try {
 7                     bootstrap.init();
 8                 } catch (Throwable var5) {
 9                     handleThrowable(var5);
10                     var5.printStackTrace();
11                     return;
12                 }
13 
14                 daemon = bootstrap;
15             } else {
16                 Thread.currentThread().setContextClassLoader(daemon.catalinaLoader);
17             }
18         }
19 
20         try {
21             String command = "start";
22             if (args.length > 0) {
23                 command = args[args.length - 1];
24             }
25 
26             if (command.equals("startd")) {
27                 args[args.length - 1] = "start";
28                 daemon.load(args);
29                 daemon.start();
30             } else if (command.equals("stopd")) {
31                 args[args.length - 1] = "stop";
32                 daemon.stop();
33             } else if (command.equals("start")) {
34                 daemon.setAwait(true);
35                 daemon.load(args);
36                 daemon.start();
37                 if (null == daemon.getServer()) {
38                     System.exit(1);
39                 }
40             } else if (command.equals("stop")) {
41                 daemon.stopServer(args);
42             } else if (command.equals("configtest")) {
43                 daemon.load(args);
44                 if (null == daemon.getServer()) {
45                     System.exit(1);
46                 }
47 
48                 System.exit(0);
49             } else {
50                 log.warn("Bootstrap: command \"" + command + "\" does not exist.");
51             }
52         } catch (Throwable var7) {
53             Throwable t = var7;
54             if (var7 instanceof InvocationTargetException && var7.getCause() != null) {
55                 t = var7.getCause();
56             }
57 
58             handleThrowable(t);
59             t.printStackTrace();
60             System.exit(1);
61         }

核心流程就是Bootstrap 对象,调用它的 init 方法初始化,然后根据启动参数,分别调用 Bootstrap 对象的不同方法。

那么Init()方法做了什么呢?

public void init() throws Exception {
    //初始化classloader,这就是本文的重点
this.initClassLoaders();
    //设置当前线程的contextClassLoader为catalinaLoader Thread.currentThread().setContextClassLoader(
this.catalinaLoader); SecurityClassLoad.securityClassLoad(this.catalinaLoader); if (log.isDebugEnabled()) { log.debug("Loading startup class"); }     //通过cataLinaLoader加载CataLina,并初始化startupInstance对象 Class<?> startupClass = this.catalinaLoader.loadClass("org.apache.catalina.startup.Catalina"); Object startupInstance = startupClass.getConstructor().newInstance(); if (log.isDebugEnabled()) { log.debug("Setting startup class properties"); }     //通过反射调用setParentClassLoader方法 String methodName = "setParentClassLoader"; Class<?>[] paramTypes = new Class[]{Class.forName("java.lang.ClassLoader")}; Object[] paramValues = new Object[]{this.sharedLoader}; Method method = startupInstance.getClass().getMethod(methodName, paramTypes); method.invoke(startupInstance, paramValues); this.catalinaDaemon = startupInstance; }

我们首先看一下Tomcat到底初始化了哪些classLoader:

private Object catalinaDaemon = null;
    ClassLoader commonLoader = null;
    ClassLoader catalinaLoader = null;
    ClassLoader sharedLoader = null;

    public Bootstrap() {
    }

    private void initClassLoaders() {
        try {
            //初始化commomLoader
            this.commonLoader = this.createClassLoader("common", (ClassLoader)null);
            if (this.commonLoader == null) {
                this.commonLoader = this.getClass().getClassLoader();
            }
            // catalinaLoader初始化, 父classloader是commonLoader
            this.catalinaLoader = this.createClassLoader("server", this.commonLoader);
             // sharedLoader初始化,父classloader是commonLoader
            this.sharedLoader = this.createClassLoader("shared", this.commonLoader);
        } catch (Throwable var2) {
            handleThrowable(var2);
            log.error("Class loader creation threw exception", var2);
            System.exit(1);
        }

    }                            

如何创建classLoader的?

private ClassLoader createClassLoader(String name, ClassLoader parent) throws Exception {
    // 从 catalina.property文件里找 common.loader, shared.loader, server.loader 对应的值 String value
= CatalinaProperties.getProperty(name + ".loader"); if (value != null && !value.equals("")) { value = this.replace(value);
      //构造repositories列表 List
<Repository> repositories = new ArrayList(); String[] repositoryPaths = getPaths(value); String[] var6 = repositoryPaths; int var7 = repositoryPaths.length; for(int var8 = 0; var8 < var7; ++var8) { String repository = var6[var8]; try { new URL(repository); repositories.add(new Repository(repository, RepositoryType.URL)); } catch (MalformedURLException var11) { if (repository.endsWith("*.jar")) { repository = repository.substring(0, repository.length() - "*.jar".length()); repositories.add(new Repository(repository, RepositoryType.GLOB)); } else if (repository.endsWith(".jar")) { repositories.add(new Repository(repository, RepositoryType.JAR)); } else { repositories.add(new Repository(repository, RepositoryType.DIR)); } } }       //再将Repository 列表传入ClassLoaderFactory.createClassLoader 方法 return ClassLoaderFactory.createClassLoader(repositories, parent); } else { return parent; } }

我们再看一下ClassLoaderFactory.createClassLoader 方法做了什么:

public static ClassLoader createClassLoader(List<ClassLoaderFactory.Repository> repositories, final ClassLoader parent) throws Exception {
        if (log.isDebugEnabled()) {
            log.debug("Creating new class loader");
        }

        Set<URL> set = new LinkedHashSet();
        if (repositories != null) {
            Iterator var3 = repositories.iterator();

            label93:
            while(true) {
                while(true) {
                    if (!var3.hasNext()) {
                        break label93;
                    }

                    ClassLoaderFactory.Repository repository = (ClassLoaderFactory.Repository)var3.next();
                    if (repository.getType() == ClassLoaderFactory.RepositoryType.URL) {
                        URL url = buildClassLoaderUrl(repository.getLocation());
                        if (log.isDebugEnabled()) {
                            log.debug("  Including URL " + url);
                        }

                        set.add(url);
                    } else {
                        File directory;
                        URL url;
                        if (repository.getType() == ClassLoaderFactory.RepositoryType.DIR) {
                            directory = new File(repository.getLocation());
                            directory = directory.getCanonicalFile();
                            if (validateFile(directory, ClassLoaderFactory.RepositoryType.DIR)) {
                                url = buildClassLoaderUrl(directory);
                                if (log.isDebugEnabled()) {
                                    log.debug("  Including directory " + url);
                                }

                                set.add(url);
                            }
                        } else if (repository.getType() == ClassLoaderFactory.RepositoryType.JAR) {
                            directory = new File(repository.getLocation());
                            directory = directory.getCanonicalFile();
                            if (validateFile(directory, ClassLoaderFactory.RepositoryType.JAR)) {
                                url = buildClassLoaderUrl(directory);
                                if (log.isDebugEnabled()) {
                                    log.debug("  Including jar file " + url);
                                }

                                set.add(url);
                            }
                        } else if (repository.getType() == ClassLoaderFactory.RepositoryType.GLOB) {
                            directory = new File(repository.getLocation());
                            directory = directory.getCanonicalFile();
                            if (validateFile(directory, ClassLoaderFactory.RepositoryType.GLOB)) {
                                if (log.isDebugEnabled()) {
                                    log.debug("  Including directory glob " + directory.getAbsolutePath());
                                }

                                String[] filenames = directory.list();
                                if (filenames != null) {
                                    String[] var7 = filenames;
                                    int var8 = filenames.length;

                                    for(int var9 = 0; var9 < var8; ++var9) {
                                        String s = var7[var9];
                                        String filename = s.toLowerCase(Locale.ENGLISH);
                                        if (filename.endsWith(".jar")) {
                                            File file = new File(directory, s);
                                            file = file.getCanonicalFile();
                                            if (validateFile(file, ClassLoaderFactory.RepositoryType.JAR)) {
                                                if (log.isDebugEnabled()) {
                                                    log.debug("    Including glob jar file " + file.getAbsolutePath());
                                                }

                                                URL url = buildClassLoaderUrl(file);
                                                set.add(url);
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }

        final URL[] array = (URL[])set.toArray(new URL[0]);
        if (log.isDebugEnabled()) {
            for(int i = 0; i < array.length; ++i) {
                log.debug("  location " + i + " is " + array[i]);
            }
        }

        return (ClassLoader)AccessController.doPrivileged(new PrivilegedAction<URLClassLoader>() {
            public URLClassLoader run() {
                return parent == null ? new URLClassLoader(array) : new URLClassLoader(array, parent);
            }
        });

去掉主要逻辑,此方法的Repository 列表就是这个URLClassLoader 可以加在的类的路径。我们直接看返回值,返回的是

new URLClassLoader(array)
也就是返回相应的类加载器:
public class URLClassLoader extends SecureClassLoader implements Closeable {

...
public URLClassLoader(URL[] urls, ClassLoader parent) {
        super(parent);
        // this is to make the stack depth consistent with 1.1
        SecurityManager security = System.getSecurityManager();
        if (security != null) {
            security.checkCreateClassLoader();
        }
        this.acc = AccessController.getContext();
        ucp = new URLClassPath(urls, acc);
    }

...
}

public URLClassLoader(URL[] urls) {
super();
// this is to make the stack depth consistent with 1.1
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkCreateClassLoader();
}
this.acc = AccessController.getContext();
ucp = new URLClassPath(urls, acc);
}
public class SecureClassLoader extends ClassLoader {...}

此时我们就分析到最底端了。我们回过头看三个类加载器,其实可以在catalina.property文件里找到相应的配置

common.loader="${catalina.base}/lib","${catalina.base}/lib/*.jar","${catalina.home}/lib","${catalina.home}/lib/*.jar"
server.loader=
shared.loader=
其中 shared.loader, server.loader 是没有值的,createClassLoader 方法里如果没有值的话,就返回传入的 parent ClassLoader,也就是说,commonLoader,catalinaLoader,sharedLoader 其实是一个对象。
 
初始化完三个ClassLoader对象后,init() 方法就使用 catalinaClassLoader 加载了org.apache.catalina.startup.Catalina 类,并创建了一个对象,然后通过反射调用这个对象的 setParentClassLoader 方法,传入的参数是 sharedClassLoader。最后吧这个 Catania 对象复制给 catalinaDaemon 属性。
 
3 深入理解Tomcat类加载机制:
(1)什么是类加载器?
Java是一门面向对象的语言,而对象又必然依托于类。类要运行,必须首先被加载到内存,这就需要类加载器。类加载器又有多种,比如加载java自带的核心类,加载java支持的可扩展类,加载我们程序员自己编写的类。那么为什么要设计多个类加载器呢?主要是因为安全问题。如果自己编写的类也用一个加载器进行加载的话,那么它的实现可能有一定的危险性和隐藏的安全性。
假如我们自己编写一个类java.util.Object,它的实现可能有一定的危险性或者隐藏的bug。而我们知道Java自带的核心类里面也有java.util.Object,如果JVM启动的时候先行加载的是我们自己编写的java.util.Object,那么就有可能出现安全问题!
为了保证java最基本的、最核心的功能不会被破坏,java采用双亲委派模型。
 
(2)双亲委派模型:
 
双亲委派模式对类加载器定义了层级,每个类加载器都有一个父类加载器。在一个类需要加载的时候,首先委派给父类加载器来加载,而父类加载器又委派给祖父类加载器来加载,以此类推。如果父类及上面的类加载器都加载不了,那么才会由当前类加载器来加载,并将被加载的类缓存起来。

所以上述类是这么加载的

  • Java自带的核心类 -- 由启动类加载器加载
  • Java支持的可扩展类 -- 由扩展类加载器加载
  • 我们自己编写的类 -- 默认由应用程序类加载器或其子类加载。

但是双亲委派模型也有一定的问题,就是在java核心类中,有SPI(Service Provider Interface)接口,它需要第三方实现类进行支持。如果使用双亲委派模型,那么第三方实现类也需要放在Java核心类里面才可以,不然的话第三方实现类将不能被加载使用。这显然是不合理的。为此有了上下文加载器

public void setContextClassLoader(ClassLoader cl)
public ClassLoader getContextClassLoader()
我们可以通过在SPI类里面调用getContextClassLoader来获取第三方实现类的类加载器。由第三方实现类通过调用setContextClassLoader来传入自己实现的类加载器, 这样就变相地解决了双亲委派模式遇到的问题。
(3)Tomcat的类加载器
Tomcat的类加载器和双亲委派模型有所差别,主要是因为在于一个Tomcat容器允许同时运行多个Web程序,每个Web程序依赖的类又必须是相互隔离的。因此,如果Tomcat使用双亲委派模式来加载类的话,将导致Web程序依赖的类变为共享的。我们看一下tomcat的类加载器:

 

 有点想设计模式里面的装饰器思想,Tomcat类加载器是在原有的基础上增加了功能。

Common类加载器,负责加载Tomcat和Web应用都复用的类
  • Catalina类加载器,负责加载Tomcat专用的类,而这些被加载的类在Web应用中将不可见
  • Shared类加载器,负责加载Tomcat下所有的Web应用程序都复用的类,而这些被加载的类在Tomcat中将不可见
    • WebApp类加载器,负责加载具体的某个Web应用程序所使用到的类,而这些被加载的类在Tomcat和其他的Web应用程序都将不可见
    • Jsp类加载器,每个jsp页面一个类加载器,不同的jsp页面有不同的类加载器,方便实现jsp页面的热插拔

为什么要分别设计类加载器呢?其实就是为了隔离。

我们先来看一个例子

package solutions;
import java.io.File;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
public class ClassLoaderDemo {
    public static void main(String[] args) throws Exception {

        MyClassLoader loader = new MyClassLoader();

        //使用MyClassLoader加载cl.test.A
        Class aClazz = loader.loadClass("solutions.A");
        Object a = aClazz.newInstance();

        Method getB = aClazz.getMethod("getB", null);
        Object b = getB.invoke(a, null);

        System.out.println(a.getClass().getClassLoader());
        System.out.println(b.getClass().getClassLoader());
    }
}

class MyClassLoader extends URLClassLoader{
    //该路径是一个非classpath路径
    public static File file = new File("D:\\myproject\\demo\\testClass");

    //MyClassLoader从上面的非classpath路径加载类
    public MyClassLoader() throws Exception {
        super(new URL[] {file.toURL()}, null, null);
    }
}
package solutions;


public class A {
    B b = new B();
    public B getB() {
        return b;
    }
}

class B {
}

 

 根据输出结果可以知道:

如果一个类由某个类加载器加载,那么因为使用该类(本例中A)而加载的类
(本例中B)也会使用加载同一个类加载器
其实在Tomcat中,基本上一个Web App所有的操作都是由一个Servlet启动的,我们在定义自己的Servlet时可能会使用其他的三方类库,比如MyBatis。结合着上面的例子可以知道,如果我们在加载Servlet时使用的是一个自定义的ClassLoader类实例,那么该Servlet中引用的三方类库,如:MyBatis也会由该ClassLoader加载。显然是不符合Tomcat的要求的。所以怎么解决隔离的要求呢?
我们为每个Web App都专门实例化一个ClassLoader实例,那么这样就做到了不同Web App的隔离。
因为我们知道Java中一个类是由该类名称以及该类的defining loader定义的。

 

 接下来我们来看Tomcat是如何加载web app目录里的web app的。

(4)StandardContext实例化

package org.apache.catalina.startup;
public void start() {
        if (log.isDebugEnabled()) {
            log.debug(sm.getString("hostConfig.start"));
        }

        try {
            ObjectName hostON = this.host.getObjectName();
            this.oname = new ObjectName(hostON.getDomain() + ":type=Deployer,host=" + this.host.getName());
            Registry.getRegistry((Object)null, (Object)null).registerComponent(this, this.oname, this.getClass().getName());
        } catch (Exception var2) {
            log.warn(sm.getString("hostConfig.jmx.register", new Object[]{this.oname}), var2);
        }

        if (!this.host.getAppBaseFile().isDirectory()) {
            log.error(sm.getString("hostConfig.appBase", new Object[]{this.host.getName(), this.host.getAppBaseFile().getPath()}));
            this.host.setDeployOnStartup(false);
            this.host.setAutoDeploy(false);
        }

        if (this.host.getDeployOnStartup()) {
            this.deployApps();
        }

    }

protected void deployApps() {
File appBase = this.host.getAppBaseFile();
File configBase = this.host.getConfigBaseFile();
String[] filteredAppPaths = this.filterAppPaths(appBase.list());
    //Deploy XML descriptors  from configBase
  this.deployDescriptors(configBase, configBase.list());
//Deploy wars
  this.deployWARs(appBase, filteredAppPaths);
//Deploy expanded folders
  //我们主要看如何部署webapps里面的应用
  this.deployDirectories(appBase, filteredAppPaths);
}
 

 

protected void deployDirectories(File appBase, String[] files) {
        if (files != null) {
            ExecutorService es = this.host.getStartStopExecutor();
            List<Future<?>> results = new ArrayList();
            String[] var5 = files;
            int var6 = files.length;
        //对于webapps下面的所有目录,进行依次部署,一般一个目录表示一个单独的web app
            for(int var7 = 0; var7 < var6; ++var7) {
                String file = var5[var7];
                if (!file.equalsIgnoreCase("META-INF") && !file.equalsIgnoreCase("WEB-INF")) {
                    File dir = new File(appBase, file);
                    if (dir.isDirectory()) {
                        ContextName cn = new ContextName(file, false);
                        if (!this.isServiced(cn.getName()) && !this.deploymentExists(cn.getName())) {
                           //并行部署多个web app,DeployDirectory构造函数第一个参数为HostConfig,这里传的是this
                 results.add(es.submit(
new HostConfig.DeployDirectory(this, cn, dir))); } } } } Iterator var12 = results.iterator(); while(var12.hasNext()) { Future result = (Future)var12.next(); try { result.get(); } catch (Exception var11) { log.error(sm.getString("hostConfig.deployDir.threaded.error"), var11); } } } }

我们再来看看DeployDirectory的定义:

private static class DeployDirectory implements Runnable {
        private HostConfig config;
        private ContextName cn;
        private File dir;

        public DeployDirectory(HostConfig config, ContextName cn, File dir) {
            this.config = config;
            this.cn = cn;
            this.dir = dir;
        }

        public void run() {
            //这里调用HostConfig的deployDirectory进行单个web app
            this.config.deployDirectory(this.cn, this.dir);
        }
    }    

 

我们来看deployDirectioy()方法:
protected void deployDirectory(ContextName cn, File dir) {        long startTime = 0L;        if (log.isInfoEnabled()) {    
startTime
= System.currentTimeMillis(); log.info(sm.getString("hostConfig.deployDir", new Object[]{dir.getAbsolutePath()})); } Context context = null; File xml = new File(dir, "META-INF/context.xml"); File xmlCopy = new File(this.host.getConfigBaseFile(), cn.getBaseName() + ".xml"); boolean copyThisXml = this.isCopyXML(); boolean deployThisXML = this.isDeployThisXML(dir, cn); HostConfig.DeployedApplication deployedApp; try { if (deployThisXML && xml.exists()) { synchronized(this.digesterLock) { try {
              //最主要的代码就是这里,这里会创建一个StandardContext类实例,也就是每个web app
              //都会对应一个StandardContext实例 context
= (Context)this.digester.parse(xml); } catch (Exception var26) { log.error(sm.getString("hostConfig.deployDescriptor.error", new Object[]{xml}), var26); context = new FailedContext(); } finally { this.digester.reset(); if (context == null) { context = new FailedContext(); } } } if (!copyThisXml && context instanceof StandardContext) { copyThisXml = ((StandardContext)context).getCopyXML(); } if (copyThisXml) { Files.copy(xml.toPath(), xmlCopy.toPath()); ((Context)context).setConfigFile(xmlCopy.toURI().toURL()); } else { ((Context)context).setConfigFile(xml.toURI().toURL()); } } else if (!deployThisXML && xml.exists()) { log.error(sm.getString("hostConfig.deployDescriptor.blocked", new Object[]{cn.getPath(), xml, xmlCopy})); context = new FailedContext(); } else {
          //contextClass = org.apache.catalina.core.StandardContext
          //同样是实例化一个StandardContext的实例
context
= (Context)Class.forName(this.contextClass).getConstructor().newInstance(); } ... }

也就是说,每个web app都会对应一个StandardContext实例。

接下来我们可以看下StandardContext与类加载机制的StandardContext.startInternal()方法。

 

package org.apache.catalina.core;

protected synchronized void startInternal() throws LifecycleException {
        if (log.isDebugEnabled()) {
            log.debug("Starting " + this.getBaseName());
        }
//1.发布正在启动的JMX通知,这样可以通过NotificationListener来监听Web应用的启动。
        if (this.getObjectName() != null) {
            Notification notification = new Notification("j2ee.state.starting", this.getObjectName(), this.sequenceNumber.getAndIncrement());
            this.broadcaster.sendNotification(notification);
        }

        this.setConfigured(false);
        boolean ok = true;//2.启动当前维护的JNDI资源。(哼,不懂JNDI)
        if (this.namingResources != null) {
            this.namingResources.start();
        }
//3.初始化临时工作目录,即设置的workDir,默认为$CATALINA-BASE/work/<Engine名称>/<Host名称>/<Context名称>this.postWorkDirectory();
//
4.初始化当前Context使用的WebResouceRoot并启动。
//WebResouceRoot维护了Web应用所以的资源集合(Class文件、Jar包以及其他资源文件),主要用于类加载器和按照路径查找资源文件。
if (this.getResources() == null) {
            if (log.isDebugEnabled()) {
                log.debug("Configuring default Resources");
            }

            try {
                this.setResources(new StandardRoot(this));
            } catch (IllegalArgumentException var21) {
                log.error(sm.getString("standardContext.resourcesInit"), var21);
                ok = false;
            }
        }

        if (ok) {
            this.resourcesStart();
        }
//5.创建Web应用类加载器webappLoader,webappLoader继承自LifecycleMBeanBase,
//在其启动后会去创建Web应用类加载器(ParallelWebappClassLoader)。
//每个StandardContext对象都持有一个WebappLoader对象,也就是自己的
//类加载器,所有该StandardContext加载的三方类和其他StandardContext加载的三方类是隔离的
//getParentClassLoader返回的parentClassLoader是其父类加载器
        //是由CopyParentClassLoaderRule.begin中配置的,通过digester
        //注入的,实现share加载
if (this.getLoader() == null) {
WebappLoader webappLoader
= new WebappLoader(); webappLoader.setDelegate(this.getDelegate()); this.setLoader(webappLoader); } //6如果没有设置Cookie处理器,默认为Rfc6265CookieProcessor。 if (this.cookieProcessor == null) { this.cookieProcessor = new Rfc6265CookieProcessor(); } //7设置字符集映射,用于根据Locale获取字符集编码。 this.getCharsetMapper(); boolean dependencyCheck = true; //8web应用的依赖检测 try { dependencyCheck = ExtensionValidator.validateApplication(this.getResources(), this); } catch (IOException var20) { log.error(sm.getString("standardContext.extensionValidationError"), var20); dependencyCheck = false; } if (!dependencyCheck) { ok = false; } //9 NamingContextListener注册 String useNamingProperty = System.getProperty("catalina.useNaming"); if (useNamingProperty != null && useNamingProperty.equals("false")) { this.useNaming = false; } if (ok && this.isUseNaming() && this.getNamingContextListener() == null) { NamingContextListener ncl = new NamingContextListener(); ncl.setName(this.getNamingContextName()); ncl.setExceptionOnFailedWrite(this.getJndiExceptionOnFailedWrite()); this.addLifecycleListener(ncl); this.setNamingContextListener(ncl); } if (log.isDebugEnabled()) { log.debug("Processing standard container startup"); } ClassLoader oldCCL = this.bindThread(); //10启动Web应用类加载器,此时真正创建出ParallelWebappClassLoader实例 try { if (ok) { Loader loader = this.getLoader(); if (loader instanceof Lifecycle) { ((Lifecycle)loader).start(); } this.setClassLoaderProperty("clearReferencesRmiTargets", this.getClearReferencesRmiTargets()); this.setClassLoaderProperty("clearReferencesStopThreads", this.getClearReferencesStopThreads()); this.setClassLoaderProperty("clearReferencesStopTimerThreads", this.getClearReferencesStopTimerThreads()); this.setClassLoaderProperty("clearReferencesHttpClientKeepAliveThread", this.getClearReferencesHttpClientKeepAliveThread()); this.setClassLoaderProperty("clearReferencesObjectStreamClassCaches", this.getClearReferencesObjectStreamClassCaches()); this.setClassLoaderProperty("clearReferencesObjectStreamClassCaches", this.getClearReferencesObjectStreamClassCaches()); this.setClassLoaderProperty("clearReferencesThreadLocals", this.getClearReferencesThreadLocals()); this.unbindThread(oldCCL); oldCCL = this.bindThread(); this.logger = null; this.getLogger();
//11启动安全组件 Realm realm
= this.getRealmInternal(); if (null != realm) { if (realm instanceof Lifecycle) { ((Lifecycle)realm).start(); } CredentialHandler safeHandler = new CredentialHandler() { public boolean matches(String inputCredentials, String storedCredentials) { return StandardContext.this.getRealmInternal().getCredentialHandler().matches(inputCredentials, storedCredentials); } public String mutate(String inputCredentials) { return StandardContext.this.getRealmInternal().getCredentialHandler().mutate(inputCredentials); } }; this.context.setAttribute("org.apache.catalina.CredentialHandler", safeHandler); } //12发布configure_start事件,ContextConfig监听该实践以完成Servlet的创建
this.fireLifecycleEvent("configure_start", (Object)null); 
Container[] var31 = this.findChildren();
int var8 = var31.length;
//13.启动Context子节点Wrapper。
for(int var9 = 0; var9 < var8; ++var9) {
Container child = var31[var9];
if (!child.getState().isAvailable()) {
child.start();
}
}
//14启动Context的pipeline。
if (this.pipeline instanceof Lifecycle) {
((Lifecycle)this.pipeline).start();
}
//15创建会话管理器
Manager contextManager = null;
Manager manager = this.getManager();
if (manager == null) {
if (log.isDebugEnabled()) {
log.debug(sm.getString("standardContext.cluster.noManager", new Object[]{this.getCluster() != null, this.distributable}));
}

if (this.getCluster() != null && this.distributable) {
try {
contextManager = this.getCluster().createManager(this.getName());
} catch (Exception var19) {
log.error(sm.getString("standardContext.cluster.managerError"), var19);
ok = false;
}
} else {
contextManager = new StandardManager();
}
}
//16将Context的Web资源集合添加到ServletContext。
if (contextManager != null) {
if (log.isDebugEnabled()) {
log.debug(sm.getString("standardContext.manager", new Object[]{contextManager.getClass().getName()}));
}

this.setManager((Manager)contextManager);
}

if (manager != null && this.getCluster() != null && this.distributable) {
this.getCluster().registerManager(manager);
}
}

if (!this.getConfigured()) {
log.error(sm.getString("standardContext.configurationFail"));
ok = false;
}
//17创建实例管理器instanceManager,用于创建对象实例,如Servlet、Filter等。
if (ok) {
this.getServletContext().setAttribute("org.apache.catalina.resources", this.getResources());
if (this.getInstanceManager() == null) {
this.setInstanceManager(this.createInstanceManager());
}
//18将Jar包扫描器添加到ServletContext
this.getServletContext().setAttribute(InstanceManager.class.getName(), this.getInstanceManager());
InstanceManagerBindings.bind(this.getLoader().getClassLoader(), this.getInstanceManager());
this.getServletContext().setAttribute(JarScanner.class.getName(), this.getJarScanner());
this.getServletContext().setAttribute("org.apache.catalina.webappVersion", this.getWebappVersion());
}
//19合并参数
this.mergeParameters();
Iterator var27 = this.initializers.entrySet().iterator();

while(var27.hasNext()) {
Entry entry = (Entry)var27.next();
//20启动添加到Context的ServletContainerInitializer。
try {
((ServletContainerInitializer)entry.getKey()).onStartup((Set)entry.getValue(), this.getServletContext());
} catch (ServletException var22) {
log.error(sm.getString("standardContext.sciFail"), var22);
ok = false;
break;
}
}
//21实例化应用类监听器ApplicationListener。
if (ok && !this.listenerStart()) {
log.error(sm.getString("standardContext.listenerFail"));
ok = false;
}

if (ok) {
this.checkConstraintsForUncoveredMethods(this.findConstraints());
}
//22启动会话管理器
try {
Manager manager = this.getManager();
if (manager instanceof Lifecycle) {
((Lifecycle)manager).start();
}
} catch (Exception var18) {
log.error(sm.getString("standardContext.managerFail"), var18);
ok = false;
}
//23实例化FilterConfig、Filter并调用Filter.init()
if (ok && !this.filterStart()) {
log.error(sm.getString("standardContext.filterFail"));
ok = false;
}
//24对于loadOnStartup大于等于0的Wrapper,调用Wrapper.load(),该方法负责实例化Servlet,并调用Servlet.init()进行初始化
if (ok && !this.loadOnStartup(this.findChildren())) {
log.error(sm.getString("standardContext.servletFail"));
ok = false;
}
//25启动后台定时处理程序,只有backgroundProcessorDelay>0才启动,用于监控守护文件的变更。
super.threadStart();
} finally {
this.unbindThread(oldCCL);
}

if (ok) {
if (log.isDebugEnabled()) {
log.debug("Starting completed");
}
} else {
log.error(sm.getString("standardContext.startFailed", new Object[]{this.getName()}));
}
//26发布正在运行的JMX通知。
this.startTime = System.currentTimeMillis();
Notification notification;
if (ok && this.getObjectName() != null) {
notification = new Notification("j2ee.state.running", this.getObjectName(), this.sequenceNumber.getAndIncrement());
this.broadcaster.sendNotification(notification);
}
//27释放资源,如关闭jar文件
this.getResources().gc();
if (!ok) {//28设置Context状态
this.setState(LifecycleState.FAILED);
if (this.getObjectName() != null) {
notification = new Notification("j2ee.object.failed", this.getObjectName(), this.sequenceNumber.getAndIncrement());
this.broadcaster.sendNotification(notification);
}
} else {
this.setState(LifecycleState.STARTING);
}


//3.初始化临时工作目录,即设置的workDir,默认为$CATALINA-BASE/work/<Engine名称>/<Host名称>/<Context名称>
private void postWorkDirectory() {
String workDir = this.getWorkDir();
String engineName;
if (workDir == null || workDir.length() == 0) {
String hostName = null;
engineName = null;
String hostWorkDir = null;
Container parentHost = this.getParent();
if (parentHost != null) {
hostName = parentHost.getName();
if (parentHost instanceof StandardHost) {
hostWorkDir = ((StandardHost)parentHost).getWorkDir();
}

Container parentEngine = parentHost.getParent();
if (parentEngine != null) {
engineName = parentEngine.getName();
}
}

if (hostName == null || hostName.length() < 1) {
hostName = "_";
}

if (engineName == null || engineName.length() < 1) {
engineName = "_";
}

String temp = this.getBaseName();
if (temp.startsWith("/")) {
temp = temp.substring(1);
}

temp = temp.replace('/', '_');
temp = temp.replace('\\', '_');
if (temp.length() < 1) {
temp = "ROOT";
}

if (hostWorkDir != null) {
workDir = hostWorkDir + File.separator + temp;
} else {
workDir = "work" + File.separator + engineName + File.separator + hostName + File.separator + temp;
}

this.setWorkDir(workDir);
}

File dir = new File(workDir);
if (!dir.isAbsolute()) {
engineName = null;

try {
engineName = this.getCatalinaBase().getCanonicalPath();
dir = new File(engineName, workDir);
} catch (IOException var7) {
log.warn(sm.getString("standardContext.workCreateException", new Object[]{workDir, engineName, this.getName()}), var7);
}
}

if (!dir.mkdirs() && !dir.isDirectory()) {
log.warn(sm.getString("standardContext.workCreateFail", new Object[]{dir, this.getName()}));
}

if (this.context == null) {
this.getServletContext();
}

this.context.setAttribute("javax.servlet.context.tempdir", dir);
this.context.setAttributeReadOnly("javax.servlet.context.tempdir");
}
//5同时webappLoader提供了backgroundProcess方法,用于Context后台处理,
//当检测到Web应用的类文件、Jar包发生变化时,重新加载Context。
public void backgroundProcess() {
    if (reloadable && modified()) {
        try {
            Thread.currentThread().setContextClassLoader
                (WebappLoader.class.getClassLoader());
            if (context != null) {
                context.reload();
            }
        } finally {
            if (context != null && context.getLoader() != null) {
                Thread.currentThread().setContextClassLoader
                    (context.getLoader().getClassLoader());
            }
        }
    }
}
//7.设置字符集映射,用于根据Locale获取字符集编码。
public CharsetMapper getCharsetMapper() {

    // Create a mapper the first time it is requested
    if (this.charsetMapper == null) {
        try {
            Class<?> clazz = Class.forName(charsetMapperClass);
            this.charsetMapper = (CharsetMapper) clazz.getConstructor().newInstance();
        } catch (Throwable t) {
            ExceptionUtils.handleThrowable(t);
            this.charsetMapper = new CharsetMapper();
        }
    }

    return this.charsetMapper;

}
//19合并参数
private void mergeParameters() {
Map<String, String> mergedParams = new HashMap();
String[] names = this.findParameters();
String[] var3 = names;
int var4 = names.length;

int var5;
for(var5 = 0; var5 < var4; ++var5) {
String s = var3[var5];
mergedParams.put(s, this.findParameter(s));
}

ApplicationParameter[] params = this.findApplicationParameters();
ApplicationParameter[] var9 = params;
var5 = params.length;

for(int var12 = 0; var12 < var5; ++var12) {
ApplicationParameter param = var9[var12];
if (param.getOverride()) {
if (mergedParams.get(param.getName()) == null) {
mergedParams.put(param.getName(), param.getValue());
}
} else {
mergedParams.put(param.getName(), param.getValue());
}
}

ServletContext sc = this.getServletContext();
Iterator var11 = mergedParams.entrySet().iterator();

while(var11.hasNext()) {
Entry<String, String> entry = (Entry)var11.next();
sc.setInitParameter((String)entry.getKey(), (String)entry.getValue());
}

}
 

(5)servlet加载:

要介绍Servlet加载首先要看下StandardWrapper类,StandardWrapper是Servlet的封装,在web.xml中配置的每个servlet-class都会对应一个StandardWrapperStandardWrapper.servletClass(String类型,还未加载)对应其servlet-class具体配置。

不管是在启动时加载Servlet还是在第一个请求到来时加载Servlet都会调用StandardWrapper.load方法。

在介绍StandardWrapper.load方法之前,我们首先看下InstanceManager,每个StandardContext都会持有一个InstanceManager实例,StandardContext.InstanceManager会在StandardContext.startInternal中实例化,默认的InstanceManager实现是DefaultInstanceManager,DefaultInstanceManager会持有一个ClassLoader实例,该ClassLoader实例其实就是StandardContext持有的WebappLoader.classLoader。

public DefaultInstanceManager(Context context, Map<String, Map<String, String>> injectionMap, org.apache.catalina.Context catalinaContext, ClassLoader containerClassLoader) {
        
//获取standardContext持有的webapploader.classLoader
this.classLoader = catalinaContext.getLoader().getClassLoader(); this.privileged = catalinaContext.getPrivileged(); this.containerClassLoader = containerClassLoader;//是加载StandardContext类的类加载器 this.ignoreAnnotations = catalinaContext.getIgnoreAnnotations(); Log log = catalinaContext.getLogger(); Set<String> classNames = new HashSet(); loadProperties(classNames, "org/apache/catalina/core/RestrictedServlets.properties", "defaultInstanceManager.restrictedServletsResource", log); loadProperties(classNames, "org/apache/catalina/core/RestrictedListeners.properties", "defaultInstanceManager.restrictedListenersResource", log); loadProperties(classNames, "org/apache/catalina/core/RestrictedFilters.properties", "defaultInstanceManager.restrictedFiltersResource", log); this.restrictedClasses = Collections.unmodifiableSet(classNames); this.context = context; this.injectionMap = injectionMap; this.postConstructMethods = catalinaContext.findPostConstructMethods(); this.preDestroyMethods = catalinaContext.findPreDestroyMethods(); }

 

我们看一下StandardWrapper.load方法:

public synchronized void load() throws ServletException {
        this.instance = this.loadServlet();
        if (!this.instanceInitialized) {
            this.initServlet(this.instance);
        }

        if (this.isJspServlet) {
            StringBuilder oname = new StringBuilder(this.getDomain());
            oname.append(":type=JspMonitor");
            oname.append(this.getWebModuleKeyProperties());
            oname.append(",name=");
            oname.append(this.getName());
            oname.append(this.getJ2EEKeyProperties());

            try {
                this.jspMonitorON = new ObjectName(oname.toString());
                Registry.getRegistry((Object)null, (Object)null).registerComponent(this.instance, this.jspMonitorON, (String)null);
            } catch (Exception var3) {
                this.log.warn(sm.getString("standardWrapper.jspMonitorError", new Object[]{this.instance}));
            }
        }

    }

    public synchronized Servlet loadServlet() throws ServletException {
        if (!this.singleThreadModel && this.instance != null) {
            return this.instance;
        } else {
            PrintStream out = System.out;
            if (this.swallowOutput) {
                SystemLogHandler.startCapture();
            }

            boolean var12 = false;

            Servlet servlet;
            try {
                var12 = true;
                long t1 = System.currentTimeMillis();
                if (this.servletClass == null) {
                    this.unavailable((UnavailableException)null);
                    throw new ServletException(sm.getString("standardWrapper.notClass", new Object[]{this.getName()}));
                }
          //获取StandardContext持有的InstanceManager对象实例
                InstanceManager instanceManager = ((StandardContext)this.getParent()).getInstanceManager();
          //加载servlet
                try {
                    servlet = (Servlet)instanceManager.newInstance(this.servletClass);
                } catch (ClassCastException var13) {
                    this.unavailable((UnavailableException)null);
                    throw new ServletException(sm.getString("standardWrapper.notServlet", new Object[]{this.servletClass}), var13);
                } catch (Throwable var14) {
.....}

下面看DefaultInstanceManager.newInstance是如何实例化类的:

public Object newInstance(String className) throws IllegalAccessException, InvocationTargetException, NamingException, InstantiationException, ClassNotFoundException, IllegalArgumentException, NoSuchMethodException, SecurityException {
//首先根据类名加载Class对象
        Class<?> clazz = this.loadClassMaybePrivileged(className, this.classLoader);
        return this.newInstance(clazz.getConstructor().newInstance(), clazz);
    }

protected Class<?> loadClassMaybePrivileged(String className, ClassLoader classLoader) throws ClassNotFoundException {
        Class clazz;
        if (SecurityUtil.isPackageProtectionEnabled()) {
            try {
                clazz = (Class)AccessController.doPrivileged(new DefaultInstanceManager.PrivilegedLoadClass(className, classLoader));
            } catch (PrivilegedActionException var6) {
                Throwable t = var6.getCause();
                if (t instanceof ClassNotFoundException) {
                    throw (ClassNotFoundException)t;
                }

                throw new RuntimeException(t);
            }
        } else {
//实际调用loadClass方法
            clazz = this.loadClass(className, classLoader);
        }

        this.checkAccess(clazz);
        return clazz;
    }


protected Class<?> loadClass(String className, ClassLoader classLoader) throws ClassNotFoundException {
//如果是Tomcat内部类,则只使用containerClassLoader尝试加载
    //containerClassLoader是构造函数中传入的加载StandardContext类的加载器
    //这是和其他StandardContext共用的加载器
        if (className.startsWith("org.apache.catalina")) {
            return this.containerClassLoader.loadClass(className);
        } else {
            try {
//如果不是Tomcat内部类,同样先使用containerClassLoader进行加载
        //所以Servlet中引用的三方类会先使用share版本
                Class<?> clazz = this.containerClassLoader.loadClass(className);
                if (ContainerServlet.class.isAssignableFrom(clazz)) {
                    return clazz;
                }
            } catch (Throwable var4) {
                ExceptionUtils.handleThrowable(var4);
            }
//如果不是上述情景,则使用该StandardContext自己的类加载器进行加载
            return classLoader.loadClass(className);
        }
    }

 

总结:本文主要介绍tomcat整体架构并深入分析了Tomcat类加载机制和类隔离原理,即每个StandardContext都持有一个自己的ClassLoader实例。
 
参考文档:
2 https://www.jianshu.com/p/eba6d4227ddf
3 https://www.jianshu.com/p/bb943f64e4ba
posted @ 2022-04-05 21:58  吾心似秋月666  阅读(405)  评论(0编辑  收藏  举报