美文网首页
07.Tomcat源码分析——类加载体系

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

作者: OpenCoder | 来源:发表于2021-08-10 14:33 被阅读0次

    由于在生产环境中,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 "$PRG"`/"$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 "$PRGDIR"/"$EXECUTABLE" ]; then
        echo "Cannot find $PRGDIR/$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 "$PRGDIR"/"$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\"" $LOGGING_MANAGER $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\"" $LOGGING_MANAGER $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 $! > "$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();
    image
    • Thread.currentThread().setContextClassLoader(this.catalinaLoader);

        catalinaLoader会被设置为Tomcat主线程的线程上下文类加载器,并且使用catalinaLoader加载Tomcat容器自身容器下的class
      
    • SecurityClassLoad.securityClassLoad(this.catalinaLoader);

    image

    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以及其子类,通过查看发现其子类有如下:

    image

    而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方法被谁调用了就知道附的啥值了:

    image

    可以看到这个设置方法被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刚开始的赋值匹配上了:

    image

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

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

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

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

    ①从本地缓存中查找:

    image
        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类加载器加载:

    image

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

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

    image

    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的清单文件中引用。

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

    image

    ⑤最后由父类加载器加载

    image

    总结:

    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 正确加载的,你知道是怎么做的吗?

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

    相关文章

      网友评论

          本文标题:07.Tomcat源码分析——类加载体系

          本文链接:https://www.haomeiwen.com/subject/lblzvltx.html