1. 程式人生 > >由淺入深--探究Tomcat9.0之--入門篇2

由淺入深--探究Tomcat9.0之--入門篇2

再次強調一下,Tomcat系列全部文章基於9.0.12版本。

                           入門篇2 Tomcat的啟動(一)

 

   所有的java專案,程式啟動入口其實都是main方法,tomcat也不例外,Tomcat的啟動入口在Bootstrap.java中的main方法中。

那麼,我們的啟動流程就由此開始啦。debug模式,走起~

  其實啟動過程大體分為兩步,第一部是init,第二步是start,各位看官先留一個印象,抓住主幹。

  一: init()方法的輪廓

  1.首先在main方法中用了單例的懶漢模式來例項化物件,這裡面daemon物件用了volatile關鍵字實現了雙重檢查鎖,保證了執行緒安全。第一個關鍵點Bootstrap類中的 bootstrap.init(),那麼tomcat究竟在初始化什麼東西呢?

如果不存在,setContextClassLoader方法在做什麼呢?這裡先留一個懸念,大家往下看。

  2.init方法的關鍵部分給各位觀眾老爺標識了出來。並且介紹了大致的作用。

    public void init() throws Exception {
        //初始化類載入器 commonLoader catalinaLoader sharedLoader
        initClassLoaders();
        //設定當前上下文的類載入器
        Thread.currentThread().setContextClassLoader(catalinaLoader);
        //為catalinaLoader載入器設定安全保護,也就是限定可以載入哪些類
        SecurityClassLoad.securityClassLoad(catalinaLoader);
        if (log.isDebugEnabled())
            log.debug("Loading startup class");
        //根據catalinaLoader類載入器取得class物件
        Class<?> startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");
        //根據catalinaLoader類獲取例項物件
        Object startupInstance = startupClass.getConstructor().newInstance();
        // Set the shared extensions class loader
        if (log.isDebugEnabled())
            log.debug("Setting startup class properties");
        String methodName = "setParentClassLoader";
        Class<?> paramTypes[] = new Class[1];
        paramTypes[0] = Class.forName("java.lang.ClassLoader");
        Object paramValues[] = new Object[1];
        paramValues[0] = sharedLoader;
        Method method =
            startupInstance.getClass().getMethod(methodName, paramTypes);
        //將當前的webapp類載入器的classLoader設定為sharedClassLoader
        method.invoke(startupInstance, paramValues);
        //傳遞使用catalinaLoader例項化的物件的引用
        catalinaDaemon = startupInstance;
    }

   光看這些作用其實並沒有什麼用,想要真正弄懂,獲得提升需要深入到具體的程式碼段。下面我們一步一步具體的來看大佬們是如何實現這些功能的。

二:深入底層

  1..initClassLoaders()這個方法初始化了三個類載入器,為什麼要初始化類載入器呢?第一,我們部署專案的時候,在一個專案裡面,一個雙親委派模型基本實現了對jar包管理的需求,可是在一個tomcat下面可以放很多的war包,每個專案裡面依賴的jar包互不干擾,然後tomcat還有自己依賴的jar包,這些jar包要被所有專案共享。第二,asp,php這些都實現了熱部署,我們jsp當然不能示弱,這個時候tomcat就必須實現自己的classLoader。下面放一張tocmat的類載入器實現圖。

Tomcat類載入器原型圖示題

                            圖片轉自 https://blog.csdn.net/fuzhongmin05/article/details/57404890

 

  Common ClassLoader可以載入tomcat自己的jar和所有web應用的jar。

  Catalina ClassLoader可以載入tomcat自己的核心jar包。

  Shared ClassLoader可以載入所有web應用共享的jar包。

  Webapp ClassLoader可以載入每一個web應用自己的jar包。

 原有的概執行模式沒有變,也是雙親委派模型的執行方式,先去父ClassLoader去找,如果沒找到,就從上往下逐一尋找。我們現在所寫的web應用幾乎都是基於spring的,依賴的jar包都非常多,如果每一個專案的spring的jar包都放置在各自的war包中,那麼記憶體會爆炸式的增長,這裡我們就可以同一專案的依賴版本,然後將通用的依賴全部放置在shared 或者common類載入器載入,會節約很大的記憶體。

  其實我當時看到這裡的時候,我想到了三個問題。不知道各位看官有沒有想到,看原始碼的時候一定不要被繞暈了,看的同時要有自己的理解,如果是自己實現的話是什麼樣的,然後再看看大佬們是怎麼實現的,只有這樣才會真正的理解原始碼,充實提升自己。

  Question 1:大佬們是如何自定義類載入器,然後初始化的。為什麼自定義的類載入器就能載入指定位置的jar包呢?

  Question 2:既然已經初始化完類載入器了,為什麼還要在當前執行緒中加入catalinaLoader呢?

  Question 3:為什麼要用反射的方式執行Catalina的setParentClassLoader()方法呢,這裡直接new一個Catalina物件不行嗎,豈不是方便快捷?

  下面針對這三個問題進行解答,這三個問題搞清楚了,init方法也差不多就清楚了。

  Answer 1:我們繼續跟進initClassLoaders方法一探究竟。

 private void initClassLoaders() {
        try {
            commonLoader = createClassLoader("common", null);
            if( commonLoader == null ) {
                //如果沒有建立成功,則用appClassLoader
                commonLoader=this.getClass().getClassLoader();
            }
            catalinaLoader = createClassLoader("server", commonLoader);
            sharedLoader = createClassLoader("shared", commonLoader);
        } catch (Throwable t) {
            handleThrowable(t);
            log.error("Class loader creation threw exception", t);
            System.exit(1);
        }
    }

   createClassLoader方法第一個引數是name,第二個引數是父載入器。還不是核心程式碼,那我們繼續深入到createClassLoader     方法。

private ClassLoader createClassLoader(String name, ClassLoader parent)
        throws Exception {

        String value = CatalinaProperties.getProperty(name + ".loader");
        if ((value == null) || (value.equals("")))
            return parent;

        value = replace(value);

        List<Repository> repositories = new ArrayList<>();

        String[] repositoryPaths = getPaths(value);

        for (String repository : repositoryPaths) {
            // Check for a JAR URL repository 檢查當前的url是不是網路路徑,如果是的話放到 repositories裡面
            try {
                @SuppressWarnings("unused")
                URL url = new URL(repository);
                repositories.add(new Repository(repository, RepositoryType.URL));
                continue;
            } catch (MalformedURLException e) {
                // Ignore
            }

            // Local repository
            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));
            }
        }

        return ClassLoaderFactory.createClassLoader(repositories, parent);
    }

  我們可以去全文搜尋catalina.loader這個,然後你會在catalina.properties這個檔案裡面看到這麼一行

common.loader="${catalina.base}/lib","${catalina.base}/lib/*.jar","${catalina.home}/lib","${catalina.home}/lib/*.jar"

  這就是我們得到的value的值。 然後是replace方法,這個方法比較大,比較多。不適合把程式碼直接複製進來。我就說一下核心流程,大家對照著看一下應該也懂的七七八八。首先new一個stringBuilder物件,然後建立兩個索引,一個是pos_start,一個是pos_end,然後用indexOf方法擷取"${"和"}"出現的位置,然後對value字串進行擷取每一個括號裡面的內容。然後根據內容獲取相應的replacement比如,

else if (Globals.CATALINA_HOME_PROP.equals(propName)) {
                    replacement = getCatalinaHome();
}

 

 public static final String CATALINA_HOME_PROP = "catalina.home";

  Tomcat有一個Global的全域性變數的類,擷取後的結果符合相應的條件,就執行getCatalinaHome方法,大家注意,這個方法比較有趣。這個方法返回的是檔案的路徑,也就是catalinaHomeFile的getPath方法。大家看一下這個程式碼

 private static final File catalinaHomeFile;
    //這個正則的作用是擷取""之間的內容,並且中間沒有逗號分割的部分。在getPaths方法中會使用這個。
    private static final Pattern PATH_PATTERN = Pattern.compile("(\".*?\")|(([^,])*)");

    static {
        // Will always be non-null
        String userDir = System.getProperty("user.dir");

        // Home first
        String home = System.getProperty(Globals.CATALINA_HOME_PROP);
        File homeFile = null;

        if (home != null) {
            File f = new File(home);
            try {
                homeFile = f.getCanonicalFile();
            } catch (IOException ioe) {
                homeFile = f.getAbsoluteFile();
            }
        }

   這裡用靜態程式碼塊初始化這個這個檔案,檔案的路徑是system.getProperty方法,這個方法大家平時用的少。大家安裝tomcat的時候還記得要設定一個環境變數叫catalinaHome嗎,這個方法就是用來獲取這個環境變數的值的。這一下是不是解答了好多童鞋的疑惑,為什麼要配置環境變數,哈哈哈。 此時我們得到的value其實是這麼一大串東西

"E:\java\sourceCode\apache-tomcat-9.0.12-src\catalina-home/lib","E:\java\sourceCode\apache-tomcat-9.0.12-src\catalina-home/lib/*.jar","E:\java\sourceCode\apache-tomcat-9.0.12-src\catalina-home/lib","E:\java\sourceCode\apache-tomcat-9.0.12-src\catalina-home/lib/*.jar"

用getPaths方法將這些路徑分割Akira,接下來就是遍歷每一個倉庫路徑,先判斷這個倉庫路徑是不是網路路徑,檔案路徑和檔案路徑的種類繫結。有可能是url,glob,jar,dir型別。然後用工廠模式ClassLoaderFactory.createClassLoader方法建立類載入器,這個方法內容比較多,但是邏輯其實不復雜,無非就是讀取每個倉庫下面對應的檔案,如果repository是.jar結尾的,那麼就載入該目錄下所有。jar結尾的檔案。這裡面還涉及一些對檔名的轉換,所以我們的jar包名稱儘量用英語。最後建立的核心程式碼是在return裡面

        return AccessController.doPrivileged(
                new PrivilegedAction<URLClassLoader>() {
                    @Override
                    public URLClassLoader run() {
                        if (parent == null)
                            return new URLClassLoader(array);
                        else
                            return new URLClassLoader(array, parent);
                    }
                });

AccessController.doPrivileged方法是jdk自帶的建立受保護的類載入器的方法,new UrlClassLoader是建立載入器的方法。到這裡第一個問題就清晰啦~。嘿嘿嘿,不知道各位看官有沒有跟上。應該比較通俗易懂了吧。

  Answer 2: 第二個問題其實一個場景大家就明白了,比如我們如果將spring的jar包都放置在共享的目錄中了,現在我們每一個專案需要配置資料來源,spring啟動的時候會去讀取xml檔案或者properties內容然後初始化連線池。這時候就違背了雙親委派機制。父級類載入器需要讀取孩子載入器的內容。怎麼解決呢?大佬們想到了一招,就是將類載入器物件放到當前執行緒中,需要的時候直接從執行緒中獲取。喜歡看原始碼的童鞋不難發現,spring原始碼中也大量使用了這種方式。這樣既不打破雙親委派模型,又實現了功能。不得不說大佬還是大佬啊.....

  Answer 3:第三個問題其實作者已經給出瞭解釋,就在bootstrap啟動類的最上面有註釋。

/**
 * Bootstrap loader for Catalina.  This application constructs a class loader
 * for use in loading the Catalina internal classes (by accumulating all of the
 * JAR files found in the "server" directory under "catalina.home"), and
 * starts the regular execution of the container.  The purpose of this
 * roundabout approach is to keep the Catalina internal classes (and any
 * other classes they depend on, such as an XML parser) out of the system
 * class path and therefore not visible to application level classes.
 */

  注意第四行,這種保持Catalina的內部類的迂迴的方式,目的是為了讓Catalina類(和其他Catalina依賴的類,例如xml解析類)對其他類路徑和對其他應用程式類不可見。 這麼解釋我相信大多數看官還是一頭霧水,哈哈哈~ 

  我是這麼理解的,為了保證Bootstrap類的應用隔離,需要在初始化的時候不依賴任何其他的類,也就是定義屬性的時候除了jdk原生的類之外不能有 new Class()這種方式出現。所以在定義屬性的時候只能用Object型別定義。如果直接用new的方式的話必須在定義的時候指定型別。這樣保證了隔離性,出色的完成了解耦功能,這是個人見解,觀眾大佬們要是感覺不對還希望能指出來共同進步哈,別藏著掖著~

這麼講解會不會太細緻了,然後大家看的不耐煩了呢,請各位給點意見哦。


下一篇介紹start方法,也就是真正開始初始化各個元件的過程。