類加載機制的源碼解讀
前言
繼前文深入剖析雙親委派機制之后,本文將引直接走進具體的代碼實現,一探其真正的實現思路。
源碼閱讀
Tomcat 啟動的起點在于 Bootstrap 類的 main()方法。在 main()方法執(zhí)行之前,其靜態(tài)代碼塊(static{})會率先被執(zhí)行。因此,我們將首先深入探討靜態(tài)代碼塊的運行機制,然后再分析 main()方法的執(zhí)行流程。
Bootstrap.static{}
static {
// 獲取用戶目錄
String userDir = System.getProperty("user.dir");
// 優(yōu)先從環(huán)境變量獲取 CATALINA_HOME
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();
}
}
// 若環(huán)境變量中未獲取到,則嘗試從 bootstrap.jar 所在目錄的上一級目錄獲取
if (homeFile == null) {
File bootstrapJar = new File(userDir, "bootstrap.jar");
if (bootstrapJar.exists()) {
File f = new File(userDir, "..");
try {
homeFile = f.getCanonicalFile();
} catch (IOException ioe) {
homeFile = f.getAbsoluteFile();
}
}
}
// 若以上兩種方式均未獲取到,則使用用戶目錄作為 CATALINA_HOME
if (homeFile == null) {
File f = new File(userDir);
try {
homeFile = f.getCanonicalFile();
} catch (IOException ioe) {
homeFile = f.getAbsoluteFile();
}
}
// 設置 CATALINA_HOME 屬性
catalinaHomeFile = homeFile;
System.setProperty(Globals.CATALINA_HOME_PROP, catalinaHomeFile.getPath());
// 獲取 CATALINA_BASE,若未設置,則使用 CATALINA_HOME 作為 CATALINA_BASE
String base = System.getProperty(Globals.CATALINA_BASE_PROP);
if (base == null) {
catalinaBaseFile = catalinaHomeFile;
} else {
File baseFile = new File(base);
try {
baseFile = baseFile.getCanonicalFile();
} catch (IOException ioe) {
baseFile = baseFile.getAbsoluteFile();
}
catalinaBaseFile = baseFile;
}
// 設置 CATALINA_BASE 屬性
System.setProperty(Globals.CATALINA_BASE_PROP, catalinaBaseFile.getPath());
}
在啟動的初始階段,Tomcat 需要確定自身的根目錄(catalina.home)和工作目錄(catalina.base)。 這段代碼描述了尋找這兩個關鍵路徑的邏輯:
- 探尋根目錄: 首先,代碼會嘗試從系統(tǒng)環(huán)境變量中獲取 catalina.home 的值。 若未找到,則會進一步探尋 bootstrap.jar 所在目錄的上一級目錄,并將該目錄作為 catalina.home。 最終,若仍無法確定 catalina.home,則將當前用戶目錄(user.dir)作為默認值。
- 定位工作目錄: 隨后,代碼會嘗試從系統(tǒng)環(huán)境變量中獲取 catalina.base 的值。 若未找到,則將 catalina.home 的值作為 catalina.base。
最后,代碼會將最終確定的 catalina.home 和 catalina.base 路徑信息設置為系統(tǒng)屬性,以便在后續(xù)的啟動流程中使用。
這段代碼如同為 Tomcat 構建一座穩(wěn)固的基石,它負責加載并設置 catalina.home 和 catalina.base 相關的信息,為后續(xù)的啟動流程奠定基礎。
main()
Tomcat 的 main 方法可概括為兩個主要階段:**初始化 (**init) 和 加載與啟動 (load+start);。
public static void main(String args[]) {
// 初始化階段 main方法第一次執(zhí)行的時候,daemon肯定為null,所以直接new了一個Bootstrap對象,然后執(zhí)行其init()方法
if (daemon == null) {
Bootstrap bootstrap = new Bootstrap();
try {
bootstrap.init();
} catch (Throwable t) {
handleThrowable(t);
t.printStackTrace();
return;
}
//daemon守護對象設置為bootstrap
daemon = bootstrap;
} else {
// 當作為服務運行時,對stop的調用將在新線程上進行,
// 因此確保使用正確的類加載器以防止一系列類未找到異常。
Thread.currentThread().setContextClassLoader(daemon.catalinaLoader);
}
// 加載與啟動階段
// 執(zhí)行守護對象的load方法和start方法
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 t) {
// 展開異常以獲得更清晰的錯誤報告
if (t instanceof InvocationTargetException &&
t.getCause() != null) {
t = t.getCause();
}
handleThrowable(t);
t.printStackTrace();
System.exit(1);
}
}
我們點到init()里面去看看~
public void init() throws Exception {
// 非常關鍵的地方,初始化類加載器s,后面我們會詳細具體地分析這個方法
initClassLoaders();
// 設置上下文類加載器為catalinaLoader,這個類加載器負責加載Tomcat專用的類
Thread.currentThread().setContextClassLoader(catalinaLoader);
// 暫時略過,后面會講
SecurityClassLoad.securityClassLoad(catalinaLoader);
// 使用catalinaLoader加載我們的Catalina類
// Load our startup class and call its process() method
if (log.isDebugEnabled())
log.debug("Loading startup class");
Class<?> startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");
Object startupInstance = startupClass.getConstructor().newInstance();
// 設置Catalina類的parentClassLoader屬性為sharedLoader
// 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);
method.invoke(startupInstance, paramValues);
// catalina守護對象為剛才使用catalinaLoader加載類、并初始化出來的Catalina對象
catalinaDaemon = startupInstance;
}
initClassLoaders 方法是 Tomcat 類加載機制的核心,它負責初始化 Tomcat 的各個類加載器,構建起 Tomcat 獨特的類加載體系。通過分析這個方法,我們可以清晰地驗證上一節(jié)提到的 Tomcat 類加載圖。
private void initClassLoaders() {
try {
// 創(chuàng)建commonLoader,如果未創(chuàng)建成果的話,則使用應用程序類加載器作為commonLoader
commonLoader = createClassLoader("common", null);
if( commonLoader == null ) {
// no config file, default to this loader - we might be in a 'single' env.
commonLoader=this.getClass().getClassLoader();
}
// 創(chuàng)建catalinaLoader,父類加載器為commonLoader
catalinaLoader = createClassLoader("server", commonLoader);
// 創(chuàng)建sharedLoader,父類加載器為commonLoader
sharedLoader = createClassLoader("shared", commonLoader);
} catch (Throwable t) {
// 如果創(chuàng)建的過程中出現異常了,日志記錄完成之后直接系統(tǒng)退出
handleThrowable(t);
log.error("Class loader creation threw exception", t);
System.exit(1);
}
}
createClassLoader 方法是 Tomcat 類加載器創(chuàng)建的底層方法,它負責根據配置文件 catalina.properties 中的配置信息來創(chuàng)建具體的類加載器實例。 CatalinaProperties.getProperty("xxx") 方法用于從 conf/catalina.properties 文件中獲取指定屬性的值,為 createClassLoader 方法提供必要的配置信息。
private ClassLoader createClassLoader(String name, ClassLoader parent)
throws Exception {
// 獲取類加載器待加載的位置,如果為空,則不需要加載特定的位置,使用父類加載返回回去。
String value = CatalinaProperties.getProperty(name + ".loader");
if ((value == null) || (value.equals("")))
return parent;
// 替換屬性變量,比如:${catalina.base}、${catalina.home}
value = replace(value);
List<Repository> repositories = new ArrayList<>();
// 解析屬性路徑變量為倉庫路徑數組
String[] repositoryPaths = getPaths(value);
// 對每個倉庫路徑進行repositories設置。我們可以把repositories看成一個個待加載的位置對象,可以是一個classes目錄,一個jar文件目錄等等
for (String repository : repositoryPaths) {
// Check for a JAR URL repository
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));
}
}
// 使用類加載器工廠創(chuàng)建一個類加載器
return ClassLoaderFactory.createClassLoader(repositories, parent);
}
我們來分析一下ClassLoaderFactory.createClassLoader--類加載器工廠創(chuàng)建類加載器。
public static ClassLoader createClassLoader(List<Repository> repositories,
final ClassLoader parent)
throws Exception {
if (log.isDebugEnabled())
log.debug("Creating new class loader");
// Construct the "class path" for this class loader
Set<URL> set = new LinkedHashSet<>();
// 遍歷repositories,對每個repository進行類型判斷,并生成URL,每個URL我們都要校驗其有效性,有效的URL我們會放到URL集合中
if (repositories != null) {
for (Repository repository : repositories) {
if (repository.getType() == RepositoryType.URL) {
URL url = buildClassLoaderUrl(repository.getLocation());
if (log.isDebugEnabled())
log.debug(" Including URL " + url);
set.add(url);
} else if (repository.getType() == RepositoryType.DIR) {
File directory = new File(repository.getLocation());
directory = directory.getCanonicalFile();
if (!validateFile(directory, RepositoryType.DIR)) {
continue;
}
URL url = buildClassLoaderUrl(directory);
if (log.isDebugEnabled())
log.debug(" Including directory " + url);
set.add(url);
} else if (repository.getType() == RepositoryType.JAR) {
File file=new File(repository.getLocation());
file = file.getCanonicalFile();
if (!validateFile(file, RepositoryType.JAR)) {
continue;
}
URL url = buildClassLoaderUrl(file);
if (log.isDebugEnabled())
log.debug(" Including jar file " + url);
set.add(url);
} else if (repository.getType() == RepositoryType.GLOB) {
File directory=new File(repository.getLocation());
directory = directory.getCanonicalFile();
if (!validateFile(directory, RepositoryType.GLOB)) {
continue;
}
if (log.isDebugEnabled())
log.debug(" Including directory glob "
+ directory.getAbsolutePath());
String filenames[] = directory.list();
if (filenames == null) {
continue;
}
for (int j = 0; j < filenames.length; j++) {
String filename = filenames[j].toLowerCase(Locale.ENGLISH);
if (!filename.endsWith(".jar"))
continue;
File file = new File(directory, filenames[j]);
file = file.getCanonicalFile();
if (!validateFile(file, RepositoryType.JAR)) {
continue;
}
if (log.isDebugEnabled())
log.debug(" Including glob jar file "
+ file.getAbsolutePath());
URL url = buildClassLoaderUrl(file);
set.add(url);
}
}
}
}
// Construct the class loader itself
final URL[] array = set.toArray(new URL[set.size()]);
if (log.isDebugEnabled())
for (int i = 0; i < array.length; i++) {
log.debug(" location " + i + " is " + array[i]);
}
// 從這兒看,最終所有的類加載器都是URLClassLoader的對象~~
return AccessController.doPrivileged(
new PrivilegedAction<URLClassLoader>() {
@Override
public URLClassLoader run() {
if (parent == null)
return new URLClassLoader(array);
else
return new URLClassLoader(array, parent);
}
});
}
對 initClassLoaders 分析完,Tomcat 還會進行一項至關重要的安全措施,即 SecurityClassLoad.securityClassLoad 方法。 讓我們深入探究這個方法,看看它在 Tomcat 的安全體系中扮演著怎樣的角色。
public static void securityClassLoad(ClassLoader loader) throws Exception {
securityClassLoad(loader, true);
}
static void securityClassLoad(ClassLoader loader, boolean requireSecurityManager) throws Exception {
if (requireSecurityManager && System.getSecurityManager() == null) {
return;
}
loadCorePackage(loader);
loadCoyotePackage(loader);
loadLoaderPackage(loader);
loadRealmPackage(loader);
loadServletsPackage(loader);
loadSessionPackage(loader);
loadUtilPackage(loader);
loadValvesPackage(loader);
loadJavaxPackage(loader);
loadConnectorPackage(loader);
loadTomcatPackage(loader);
}
private static final void loadCorePackage(ClassLoader loader) throws Exception {
final String basePackage = "org.apache.catalina.core.";
loader.loadClass(basePackage + "AccessLogAdapter");
loader.loadClass(basePackage + "ApplicationContextFacade$PrivilegedExecuteMethod");
loader.loadClass(basePackage + "ApplicationDispatcher$PrivilegedForward");
loader.loadClass(basePackage + "ApplicationDispatcher$PrivilegedInclude");
loader.loadClass(basePackage + "ApplicationPushBuilder");
loader.loadClass(basePackage + "AsyncContextImpl");
loader.loadClass(basePackage + "AsyncContextImpl$AsyncRunnable");
loader.loadClass(basePackage + "AsyncContextImpl$DebugException");
loader.loadClass(basePackage + "AsyncListenerWrapper");
loader.loadClass(basePackage + "ContainerBase$PrivilegedAddChild");
loadAnonymousInnerClasses(loader, basePackage + "DefaultInstanceManager");
loader.loadClass(basePackage + "DefaultInstanceManager$AnnotationCacheEntry");
loader.loadClass(basePackage + "DefaultInstanceManager$AnnotationCacheEntryType");
loader.loadClass(basePackage + "ApplicationHttpRequest$AttributeNamesEnumerator");
}
這段代碼使用 catalinaLoader 加載了 Tomcat 源代碼中各個專用類,這些類主要分布在以下幾個包中:
- org.apache.catalina.core.*
- org.apache.coyote.*
- org.apache.catalina.loader.*
- org.apache.catalina.realm.*
- org.apache.catalina.servlets.*
- org.apache.catalina.session.*
- org.apache.catalina.util.*
- org.apache.catalina.valves.*
- javax.servlet.http.Cookie
- org.apache.catalina.connector.*
- org.apache.tomcat.*
至此,我們已經逐一分析了 init 方法中的關鍵方法,包括 initClassLoaders、SecurityClassLoad.securityClassLoad 等,了解了 Tomcat 初始化階段的各個步驟。
WebApp 類加載器
在深入探究 Tomcat 啟動流程的過程中,我們發(fā)現似乎遺漏了一個關鍵角色——WebApp 類加載器。
WebApp 類加載器是每個 Web 應用獨有的,而每個 Web 應用本質上就是一個 Context。 因此,我們理所當然地將目光投向了 Context 的實現類。 Tomcat 中,StandardContext 是 Context 的默認實現,而 WebApp 類加載器正是誕生于 StandardContext 類的 startInternal() 方法中。
protected synchronized void startInternal() throws LifecycleException {
if (getLoader() == null) {
WebappLoader webappLoader = new WebappLoader(getParentClassLoader());
webappLoader.setDelegate(getDelegate());
setLoader(webappLoader);
}
}
這段代碼的邏輯非常簡潔,它在 WebApp 類加載器不存在的情況下,會創(chuàng)建一個新的 WebApp 類加載器,并將其設置為當前 Context 的加載器(setLoader)。
到這里,我們已經完成了對 Tomcat 啟動過程和類加載機制的全面解析,從 Bootstrap 類開始,一步步深入,最終揭開了 WebApp 類加載器的面紗。
通過這個分析過程,我們不僅了解了 Tomcat 啟動和類加載的具體步驟,更深刻地理解了 Tomcat 采用這種獨特的多層級類加載機制的深層原因,以及這種設計帶來的種種優(yōu)勢。