由淺入深--探究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的類載入器實現圖。
圖片轉自 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方法,也就是真正開始初始化各個元件的過程。