07.Tomcat源碼分析——類加載體系

由于在生產(chǎn)環(huán)境中,Tomcat一般部署在Linux系統(tǒng)下,所以本文將以 startup.sh shell腳本為準(zhǔn),對Tomcat的啟動(dòng)進(jìn)行分析。

我們啟動(dòng)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 "$@"

重點(diǎn)看最后一行代碼:exec "PRGDIR"/"EXECUTABLE" start "$@"

這里有兩個(gè)變量:

  • PRGDIR:當(dāng)前shell腳本所在的路徑;
  • EXECUTABLE:腳本catalina.sh。

通過以上變量的解釋,我們知道了其實(shí)執(zhí)行startup.sh就是在執(zhí)行catalina.sh,并且傳遞了參數(shù)start。

catalina.sh中接收到start參數(shù)后的執(zhí)行的腳本分支見代碼如下:

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

# 此處省略參數(shù)校驗(yàn)的腳本

  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命令執(zhí)行了org.apache.catalina.startup.Bootstrap類中的main方法,參數(shù)也是start。Bootstrap的main方法的實(shí)現(xiàn)見代碼如下:

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

    }

可以很清晰的看到,一上來就調(diào)用了 bootstrap.init();方法進(jìn)行初始化的操作,代碼如下:

    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方法一上來調(diào)用了這三行關(guān)鍵代碼:

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

      catalinaLoader會(huì)被設(shè)置為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有關(guān)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 已經(jīng)創(chuàng)建好了,那么肯定是要被使用的,是在哪里使用的呢?它們之間同Webapp ClassLoader又是怎么聯(lián)系起來的?我們繼續(xù)看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.通過反射實(shí)例化對象
        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設(shè)置為sharedLoader的上級加載器
        Method method = startupInstance.getClass().getMethod(methodName, paramTypes);
        method.invoke(startupInstance, paramValues);
        this.catalinaDaemon = startupInstance;
    }

在我們上面的securityClassLoad()方法中會(huì)執(zhí)行 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"); 會(huì)加載到 ContainerBase以及其子類,通過查看發(fā)現(xiàn)其子類有如下:

image

而StandardContext類就是一個(gè)核心的子類,因?yàn)樵谄?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(); 
       // 省略后邊的代碼 
    }

我們發(fā)現(xiàn)了關(guān)鍵的 WebappLoader 的創(chuàng)建,并且將WebappLoader設(shè)置為了當(dāng)前的類加載器。

同時(shí)創(chuàng)建WebappLoader 的時(shí)候,傳遞了一個(gè)參數(shù) getParentClassLoader() ,我們可以看下 WebappLoader的構(gòu)造函數(shù):

    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設(shè)置了對應(yīng)的父級類加載器,getParentClassLoader() 這個(gè)方法會(huì)獲取父容器parentClassLoader的屬性,也就是找到ContainerBase中的setParentClassLoader方法被誰調(diào)用了就知道附的啥值了:

image

可以看到這個(gè)設(shè)置方法被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其實(shí)就是通過反射取出來的 SharedLoader,這里剛好就與我們的BootStrap中init剛開始的賦值匹配上了:

image

因此,我們也能徹底知道,WebAppLoader的父級類加載器就是我們的ShareClassLoader了。

代碼閱讀到這里,已經(jīng)基本清楚了Tomcat中ClassLoader的總體結(jié)構(gòu),總結(jié)如下:

Tomcat存在common、catalina、shared三個(gè)公共的classloader,默認(rèn)情況下,這三個(gè)classloader其實(shí)是同一個(gè),都是common classloader,而針對每個(gè)webapp,也就是context(對應(yīng)代碼中的StandardContext類),都有自己的WebappClassLoader實(shí)例來加載每個(gè)應(yīng)用自己的類,該類加載實(shí)例的parent即是Shared ClassLoader。

OK,在理解了整個(gè)類加載器結(jié)構(gòu)后,我們再來看 WebAppLoader 是如何加載我們的類的,重點(diǎn)查看 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的引導(dǎo)類Bootstrap類加載器加載:

image

該類加載器包含 Java 虛擬機(jī)提供的基本運(yùn)行時(shí)類,以及系統(tǒng)擴(kuò)展目錄 ( $JAVA_HOME/jre/lib/ext) 中存在的 JAR 文件中的任何類。

③再從系統(tǒng)類加載器進(jìn)行查找:

image

System這個(gè)類加載器通常從CLASSPATH環(huán)境變量的內(nèi)容中初始化。所有這些類對 Tomcat 內(nèi)部類和 Web 應(yīng)用程序都是可見的。但是,標(biāo)準(zhǔn)的 Tomcat 啟動(dòng)腳本($CATALINA_HOME/bin/catalina.sh%CATALINA_HOME%\bin\catalina.bat)完全忽略了CLASSPATH環(huán)境變量本身的內(nèi)容,而是從以下存儲(chǔ)庫構(gòu)建 System 類加載器:

  • $CATALINA_HOME/bin/bootstrap.jar — 包含用于初始化 Tomcat 服務(wù)器的 main() 方法,以及它所依賴的類加載器實(shí)現(xiàn)類。

  • CATALINA_BASE/bin/tomcat-juli.jar*或 *CATALINA_HOME/bin/tomcat-juli.jar — 日志實(shí)現(xiàn)類。其中包括java.util.loggingAPI 的增強(qiáng)類 ,稱為 Tomcat JULI,以及 Tomcat 內(nèi)部使用的 Apache Commons Logging 庫的包重命名副本。有關(guān)更多詳細(xì)信息,請參閱日志記錄文檔。

    如果tomcat-juli.jar存在于 CATALINA_BASE/bin 中*,則使用它代替*CATALINA_HOME/bin 中的那個(gè) 。它在某些日志記錄配置中很有用

  • $CATALINA_HOME/bin/commons-daemon.jar — 來自Apache Commons Daemon項(xiàng)目的類。這個(gè) JAR 文件不存在于CLASSPATH構(gòu)建者 catalina.bat| .sh腳本,但從bootstrap.jar的清單文件中引用。

④由當(dāng)前類加載器進(jìn)行加載:

image

⑤最后由父類加載器加載

image

總結(jié):

Web應(yīng)用類加載器默認(rèn)的加載順序是:

(1).先從緩存中加載;
(2).如果沒有,則從JVM的Bootstrap類加載器查找;
(3).如果沒有,則從當(dāng)前類加載器加載查找(按照WEB-INF/classes、WEB-INF/lib的順序);
(4).如果沒有,則從父類加載器加載,父類加載器采用默認(rèn)的委派模式

tomcat提供了delegate屬性用于控制是否啟用java委派模式,默認(rèn)false(不啟用),當(dāng)設(shè)置為true時(shí),tomcat將使用java的默認(rèn)委派模式,這時(shí)加載順序如下:

(1).先從緩存中加載;
(2).如果沒有,則從JVM的Bootstrap類加載器加載;
(3).如果沒有,則從父類加載器加載,加載順序是AppClassLoader、Common、Shared。
(4).如果沒有,則從當(dāng)前類加載器加載(按照WEB-INF/classes、WEB-INF/lib的順序);

思考題:

我們在使用 JDBC 時(shí),都需要加載 Driver 驅(qū)動(dòng),不知道你注意到?jīng)]有,不寫

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

也是可以讓 com.mysql.jdbc.Driver 正確加載的,你知道是怎么做的嗎?

這個(gè)問題大家可以下來自己嘗試閱讀源碼得到解決。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容