07.Tomcat源码分析——类加载体系

简介: 由于在生产环境中,Tomcat一般部署在Linux系统下,所以本文将以 startup.sh shell脚本为准,对Tomcat的启动进行分析。

我们启动Tomcat的命令如下:

sh startup.sh

startup.sh的脚本代码如下:

# Better OS/400 detection: see Bugzilla 31132
os400=false
case "`uname`" in
OS400*) os400=true;;
esac

# resolve links - $0 may be a softlink
PRG="$0"

while [ -h "$PRG" ] ; do
  ls=`ls -ld "$PRG"`
  link=`expr "$ls" : '.*-> \(.*\)$'`
  if expr "$link" : '/.*' > /dev/null; then
    PRG="$link"
  else
    PRG=`dirname "{
   
   mathJaxContainer[1]}link"
  fi
done

PRGDIR=`dirname "$PRG"`
EXECUTABLE=catalina.sh

# Check that target executable exists
if $os400; then
  # -x will Only work on the os400 if the files are:
  # 1. owned by the user
  # 2. owned by the PRIMARY group of the user
  # this will not work if the user belongs in secondary groups
  eval
else
  if [ ! -x "{
   
   mathJaxContainer[2]}EXECUTABLE" ]; then
    echo "Cannot find {
   
   mathJaxContainer[3]}EXECUTABLE"
    echo "The file is absent or does not have execute permission"
    echo "This file is needed to run this program"
    exit 1
  fi
fi

exec "{
   
   mathJaxContainer[4]}EXECUTABLE" start "$@"

重点看最后一行代码:exec "$PRGDIR"/"$EXECUTABLE" start "$@"

这里有两个变量:

  • PRGDIR:当前shell脚本所在的路径;
  • EXECUTABLE:脚本catalina.sh。

通过以上变量的解释,我们知道了其实执行startup.sh就是在执行catalina.sh,并且传递了参数start。

catalina.sh中接收到start参数后的执行的脚本分支见代码如下:

elif [ "$1" = "start" ] ; then

# 此处省略参数校验的脚本

  shift
  touch "$CATALINA_OUT"
  if [ "$1" = "-security" ] ; then
    if [ $have_tty -eq 1 ]; then
      echo "Using Security Manager"
    fi
    shift
    eval "\"$_RUNJAVA\"" "\"$LOGGING_CONFIG\"" {
   
   mathJaxContainer[7]}JAVA_OPTS $CATALINA_OPTS \
      -Djava.endorsed.dirs="\"$JAVA_ENDORSED_DIRS\"" -classpath "\"$CLASSPATH\"" \
      -Djava.security.manager \
      -Djava.security.policy=="\"$CATALINA_BASE/conf/catalina.policy\"" \
      -Dcatalina.base="\"$CATALINA_BASE\"" \
      -Dcatalina.home="\"$CATALINA_HOME\"" \
      -Djava.io.tmpdir="\"$CATALINA_TMPDIR\"" \
      org.apache.catalina.startup.Bootstrap "$@" start \
      >> "$CATALINA_OUT" 2>&1 "&"

  else
    eval "\"$_RUNJAVA\"" "\"$LOGGING_CONFIG\"" {
   
   mathJaxContainer[10]}JAVA_OPTS $CATALINA_OPTS \
      -Djava.endorsed.dirs="\"$JAVA_ENDORSED_DIRS\"" -classpath "\"$CLASSPATH\"" \
      -Dcatalina.base="\"$CATALINA_BASE\"" \
      -Dcatalina.home="\"$CATALINA_HOME\"" \
      -Djava.io.tmpdir="\"$CATALINA_TMPDIR\"" \
      org.apache.catalina.startup.Bootstrap "$@" start \
      >> "$CATALINA_OUT" 2>&1 "&"

  fi

  if [ ! -z "$CATALINA_PID" ]; then
    echo {
   
   mathJaxContainer[12]}CATALINA_PID"
  fi

  echo "Tomcat started."

从以上代码片段可以看出,最终使用java命令执行了org.apache.catalina.startup.Bootstrap类中的main方法,参数也是start。Bootstrap的main方法的实现见代码如下:

public static void main(String[] args) {
   
   
        synchronized(daemonLock) {
   
   
            if (daemon == null) {
   
   
                Bootstrap bootstrap = new Bootstrap();

                try {
   
   
                    bootstrap.init();
                } catch (Throwable var5) {
   
   
                    handleThrowable(var5);
                    var5.printStackTrace();
                    return;
                }

                daemon = bootstrap;
            } else {
   
   
                Thread.currentThread().setContextClassLoader(daemon.catalinaLoader);
            }
        }

        try {
   
   
            String command = "start";
            if (args.length > 0) {
   
   
                command = args[args.length - 1];
            }

            if (command.equals("startd")) {
   
   
                args[args.length - 1] = "start";
                daemon.load(args);
                daemon.start();
            } else if (command.equals("stopd")) {
   
   
                args[args.length - 1] = "stop";
                daemon.stop();
            } else if (command.equals("start")) {
   
   
                daemon.setAwait(true);
                daemon.load(args);
                daemon.start();
                if (null == daemon.getServer()) {
   
   
                    System.exit(1);
                }
            } else if (command.equals("stop")) {
   
   
                daemon.stopServer(args);
            } else if (command.equals("configtest")) {
   
   
                daemon.load(args);
                if (null == daemon.getServer()) {
   
   
                    System.exit(1);
                }

                System.exit(0);
            } else {
   
   
                log.warn("Bootstrap: command \"" + command + "\" does not exist.");
            }
        } catch (Throwable var7) {
   
   
            Throwable t = var7;
            if (var7 instanceof InvocationTargetException && var7.getCause() != null) {
   
   
                t = var7.getCause();
            }

            handleThrowable(t);
            t.printStackTrace();
            System.exit(1);
        }

    }

可以很清晰的看到,一上来就调用了 bootstrap.init();方法进行初始化的操作,代码如下:

    public void init() throws Exception {
   
   
        this.initClassLoaders();
        Thread.currentThread().setContextClassLoader(this.catalinaLoader);
        SecurityClassLoad.securityClassLoad(this.catalinaLoader);
        if (log.isDebugEnabled()) {
   
   
            log.debug("Loading startup class");
        }

        Class<?> startupClass = this.catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");
        Object startupInstance = startupClass.getConstructor().newInstance();
        if (log.isDebugEnabled()) {
   
   
            log.debug("Setting startup class properties");
        }

        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;
    }

而init方法一上来调用了这三行关键代码:

  • this.initClassLoaders();

  • Thread.currentThread().setContextClassLoader(this.catalinaLoader);

​ catalinaLoader会被设置为Tomcat主线程的线程上下文类加载器,并且使用catalinaLoader加载Tomcat容器自身容器下的class

  • SecurityClassLoad.securityClassLoad(this.catalinaLoader);


securityClassLoad方法主要加载Tomcat容器所需的class,包括:

  • Tomcat核心class,即org.apache.catalina.core路径下的class;
  • org.apache.catalina.loader.WebappClassLoader$PrivilegedFindResourceByName;
  • Tomcat有关session的class,即org.apache.catalina.session路径下的class;
  • Tomcat工具类的class,即org.apache.catalina.util路径下的class;
  • javax.servlet.http.Cookie;
  • Tomcat处理请求的class,即org.apache.catalina.connector路径下的class;
  • Tomcat其它工具类的class,也是org.apache.catalina.util路径下的class;

ok,Common/Catalina/Shared ClassLoader 已经创建好了,那么肯定是要被使用的,是在哪里使用的呢?它们之间同Webapp ClassLoader又是怎么联系起来的?我们继续看init方法:

    public void init() throws Exception {
   
   
        this.initClassLoaders();
        Thread.currentThread().setContextClassLoader(this.catalinaLoader);
        SecurityClassLoad.securityClassLoad(this.catalinaLoader);
        if (log.isDebugEnabled()) {
   
   
            log.debug("Loading startup class");
        }
        //1.先加载Catalina类
        Class<?> startupClass = this.catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");
        //2.通过反射实例化对象
        Object startupInstance = startupClass.getConstructor().newInstance();
        if (log.isDebugEnabled()) {
   
   
            log.debug("Setting startup class properties");
        }
        //3.catalina的setParentClassLoader方法
        String methodName = "setParentClassLoader";
        Class<?>[] paramTypes = new Class[]{
   
   Class.forName("java.lang.ClassLoader")};
        Object[] paramValues = new Object[]{
   
   this.sharedLoader};\
        //4.将catalina设置为sharedLoader的上级加载器
        Method method = startupInstance.getClass().getMethod(methodName, paramTypes);
        method.invoke(startupInstance, paramValues);
        this.catalinaDaemon = startupInstance;
    }

在我们上面的securityClassLoad()方法中会执行 loadCorePackage方法,该方法源码如下:

    private static final void loadCorePackage(ClassLoader loader) throws Exception {
   
   
        String basePackage = "org.apache.catalina.core.";
        loader.loadClass("org.apache.catalina.core.AccessLogAdapter");
        loader.loadClass("org.apache.catalina.core.ApplicationContextFacade$PrivilegedExecuteMethod");
        loader.loadClass("org.apache.catalina.core.ApplicationDispatcher$PrivilegedForward");
        loader.loadClass("org.apache.catalina.core.ApplicationDispatcher$PrivilegedInclude");
        loader.loadClass("org.apache.catalina.core.ApplicationPushBuilder");
        loader.loadClass("org.apache.catalina.core.AsyncContextImpl");
        loader.loadClass("org.apache.catalina.core.AsyncContextImpl$AsyncRunnable");
        loader.loadClass("org.apache.catalina.core.AsyncContextImpl$DebugException");
        loader.loadClass("org.apache.catalina.core.AsyncListenerWrapper");
        loader.loadClass("org.apache.catalina.core.ContainerBase$PrivilegedAddChild");
        loader.loadClass("org.apache.catalina.core.DefaultInstanceManager$AnnotationCacheEntry");
        loader.loadClass("org.apache.catalina.core.DefaultInstanceManager$AnnotationCacheEntryType");
        loader.loadClass("org.apache.catalina.core.DefaultInstanceManager$PrivilegedGetField");
        loader.loadClass("org.apache.catalina.core.DefaultInstanceManager$PrivilegedGetMethod");
        loader.loadClass("org.apache.catalina.core.DefaultInstanceManager$PrivilegedLoadClass");
        loader.loadClass("org.apache.catalina.core.ApplicationHttpRequest$AttributeNamesEnumerator");
    }

其中 loader.loadClass("org.apache.catalina.core.ContainerBase$PrivilegedAddChild"); 会加载到 ContainerBase以及其子类,通过查看发现其子类有如下:


而StandardContext类就是一个核心的子类,因为在其 startInternal()方法中:

    /**
     * Start this component and implement the requirements
     * of {@link LifecycleBase#startInternal()}.
     *
     * @exception LifecycleException if this component detects a fatal error
     *  that prevents this component from being used
     */
    @Override
    protected synchronized void startInternal() throws LifecycleException {
   
   

        // 省略前边的代码 

        if (getLoader() == null) {
   
   
            WebappLoader webappLoader = new WebappLoader(getParentClassLoader());
            webappLoader.setDelegate(getDelegate());
            setLoader(webappLoader);
        }
       // 省略中间的代码 
       // Start our subordinate components, if any
       if ((loader != null) && (loader instanceof Lifecycle))
            ((Lifecycle) loader).start(); 
       // 省略后边的代码 
    }

我们发现了关键的 WebappLoader 的创建,并且将WebappLoader设置为了当前的类加载器。

同时创建WebappLoader 的时候,传递了一个参数 getParentClassLoader() ,我们可以看下 WebappLoader的构造函数:

    public WebappLoader(ClassLoader parent) {
   
   
        this.classLoader = null;
        this.context = null;
        this.delegate = false;
        this.loaderClass = ParallelWebappClassLoader.class.getName();
        this.parentClassLoader = null;
        this.reloadable = false;
        this.support = new PropertyChangeSupport(this);
        this.classpath = null;
        this.parentClassLoader = parent;
    }

ok,最后一行代码给webappLoader设置了对应的父级类加载器,getParentClassLoader() 这个方法会获取父容器parentClassLoader的属性,也就是找到ContainerBase中的setParentClassLoader方法被谁调用了就知道附的啥值了:


可以看到这个设置方法被CopyParentClassLoaderRule的begin方法中赋值了:

    public void begin(String namespace, String name, Attributes attributes)
        throws Exception {
   
   

        if (digester.getLogger().isDebugEnabled())
            digester.getLogger().debug("Copying parent class loader");
        Container child = (Container) digester.peek(0);
        Object parent = digester.peek(1);
        Method method =
            parent.getClass().getMethod("getParentClassLoader", new Class[0]);
        ClassLoader classLoader =
            (ClassLoader) method.invoke(parent, new Object[0]);
        child.setParentClassLoader(classLoader);

    }

这里的classLoader其实就是通过反射取出来的 SharedLoader,这里刚好就与我们的BootStrap中init刚开始的赋值匹配上了:

因此,我们也能彻底知道,WebAppLoader的父级类加载器就是我们的ShareClassLoader了。

代码阅读到这里,已经基本清楚了Tomcat中ClassLoader的总体结构,总结如下:

Tomcat存在common、catalina、shared三个公共的classloader,默认情况下,这三个classloader其实是同一个,都是common classloader,而针对每个webapp,也就是context(对应代码中的StandardContext类),都有自己的WebappClassLoader实例来加载每个应用自己的类,该类加载实例的parent即是Shared ClassLoader。

OK,在理解了整个类加载器结构后,我们再来看 WebAppLoader 是如何加载我们的类的,重点查看 loadClass 方法,由于代码过长,直接截取核心代码:

①从本地缓存中查找:

    protected Class<?> findLoadedClass0(String name) {
   
   
        String path = this.binaryNameToPath(name, true);
        ResourceEntry entry = (ResourceEntry)this.resourceEntries.get(path);
        return entry != null ? entry.loadedClass : null;
    }

②缓存中没有,则从JVM的引导类Bootstrap类加载器加载:

该类加载器包含 Java 虚拟机提供的基本运行时类,以及系统扩展目录 ( $JAVA_HOME/jre/lib/ext) 中存在的 JAR 文件中的任何类。

③再从系统类加载器进行查找:

System这个类加载器通常从CLASSPATH环境变量的内容中初始化。所有这些类对 Tomcat 内部类和 Web 应用程序都是可见的。但是,标准的 Tomcat 启动脚本($CATALINA_HOME/bin/catalina.sh%CATALINA_HOME%\bin\catalina.bat)完全忽略了CLASSPATH环境变量本身的内容,而是从以下存储库构建 System 类加载器:

  • $CATALINA_HOME/bin/bootstrap.jar — 包含用于初始化 Tomcat 服务器的 main() 方法,以及它所依赖的类加载器实现类。

  • $CATALINA_BASE/bin/tomcat-juli.jar*或 *$CATALINA_HOME/bin/tomcat-juli.jar — 日志实现类。其中包括java.util.loggingAPI 的增强类 ,称为 Tomcat JULI,以及 Tomcat 内部使用的 Apache Commons Logging 库的包重命名副本。有关更多详细信息,请参阅日志记录文档

    如果tomcat-juli.jar存在于 $CATALINA_BASE/bin 中*,则使用它代替*$CATALINA_HOME/bin 中的那个 。它在某些日志记录配置中很有用

  • $CATALINA_HOME/bin/commons-daemon.jar — 来自Apache Commons Daemon项目的类。这个 JAR 文件不存在于CLASSPATH构建者 catalina.bat| .sh脚本,但从bootstrap.jar的清单文件中引用。

④由当前类加载器进行加载:

⑤最后由父类加载器加载

总结:

Web应用类加载器默认的加载顺序是:

(1).先从缓存中加载;
(2).如果没有,则从JVM的Bootstrap类加载器查找;
(3).如果没有,则从当前类加载器加载查找(按照WEB-INF/classes、WEB-INF/lib的顺序);
(4).如果没有,则从父类加载器加载,父类加载器采用默认的委派模式

tomcat提供了delegate属性用于控制是否启用java委派模式,默认false(不启用),当设置为true时,tomcat将使用java的默认委派模式,这时加载顺序如下:

(1).先从缓存中加载;
(2).如果没有,则从JVM的Bootstrap类加载器加载;
(3).如果没有,则从父类加载器加载,加载顺序是AppClassLoader、Common、Shared。
(4).如果没有,则从当前类加载器加载(按照WEB-INF/classes、WEB-INF/lib的顺序);

思考题:

我们在使用 JDBC 时,都需要加载 Driver 驱动,不知道你注意到没有,不写

Class.forName("com.mysql.jdbc.Driver")

也是可以让 com.mysql.jdbc.Driver 正确加载的,你知道是怎么做的吗?

这个问题大家可以下来自己尝试阅读源码得到解决。

目录
相关文章
|
5月前
|
XML Java 应用服务中间件
SpringBoot配置外部Tomcat项目启动流程源码分析(长文)
SpringBoot配置外部Tomcat项目启动流程源码分析(长文)
53 0
|
11月前
|
前端开发 Java 应用服务中间件
TOMCAT 源码分析 -- 一次请求
TOMCAT 源码分析 -- 一次请求
68 0
|
11月前
|
Java 应用服务中间件
TOMCAT 源码分析 -- 构建环境
TOMCAT 源码分析 -- 构建环境
77 0
|
11月前
|
监控 前端开发 Java
TOMCAT 源码分析 -- 启动(下)
TOMCAT 源码分析 -- 启动
71 0
|
11月前
|
XML 前端开发 Java
TOMCAT 源码分析 -- 启动(上)
TOMCAT 源码分析 -- 启动
107 0
|
11月前
|
Java 应用服务中间件 容器
Tomcat源码分析之中文乱码(一)
Tomcat源码分析之中文乱码(一)
137 0
|
11月前
|
算法 安全 应用服务中间件
Tomcat源码分析之 doGet方法(四)
Tomcat源码分析之 doGet方法(四)
33 0
|
11月前
|
设计模式 应用服务中间件 容器
Tomcat源码分析之 doGet方法(三)
Tomcat源码分析之 doGet方法(三)
42 0
|
11月前
|
应用服务中间件 索引
Tomcat源码分析之 doGet方法(二)
Tomcat源码分析之 doGet方法(二)
58 0
|
11月前
|
算法 应用服务中间件
Tomcat源码分析之 doGet方法(一)
Tomcat源码分析之 doGet方法(一)
62 0