一、前言
類載入器,其實是很複雜一個東西,想等到我完全什麼都弄明白了再寫出來,估計不太現實。。。現在只能是知道多少寫多少吧。
首先,我提一個問題:在我們自己的servlet中(比如ssm中,controller的程式碼),可以訪問 tomcat 安裝目錄下 lib 中的類嗎?(servlet-api.jar包中的不算)
好好思考一下再回答。如果你說不可以,那可能接下來會有點尷尬。。。
二、測試
1、tomcat 類載入器結構複習
我們們看圖說話,應用程式類載入器,主要載入classpath路徑下的類,在tomcat 的啟動指令碼里,最終會設定為 bin 目錄下的bootstrap.jar 和tomcat-juli.jar:
common類載入器主要用於載入 tomcat 中介軟體自身、webapp 都可以訪問的類;
catalina 類載入器,主要用於載入 tomcat 自身的類, webapp 不能訪問;
共享類(shared)類載入器, 主要是用於載入 webapp 共享的類,比如大家都用 spring 開發,該類載入器的初衷就是載入 共用的 spring 相關的jar包。
這三者的載入路徑,可以檢視 Tomcat (我這邊是Tomcat 8)安裝目錄下,conf / catalina.properties:
1 # 2 # 3 # List of comma-separated paths defining the contents of the "common" 4 # classloader. Prefixes should be used to define what is the repository type. 5 # Path may be relative to the CATALINA_HOME or CATALINA_BASE path or absolute. 6 # If left as blank,the JVM system loader will be used as Catalina's "common" 7 # loader. 8 # Examples: 9 # "foo": Add this folder as a class repository 10 # "foo/*.jar": Add all the JARs of the specified folder as class 11 # repositories 12 # "foo/bar.jar": Add bar.jar as a class repository 13 # 14 # Note: Values are enclosed in double quotes ("...") in case either the 15 # ${catalina.base} path or the ${catalina.home} path contains a comma. 16 # Because double quotes are used for quoting, the double quote character 17 # may not appear in a path. 18 common.loader="${catalina.base}/lib","${catalina.base}/lib/*.jar","${catalina.home}/lib","${catalina.home}/lib/*.jar" 19 20 # 21 # List of comma-separated paths defining the contents of the "server" 22 # classloader. Prefixes should be used to define what is the repository type. 23 # Path may be relative to the CATALINA_HOME or CATALINA_BASE path or absolute. 24 # If left as blank, the "common" loader will be used as Catalina's "server" 25 # loader. 26 # Examples: 27 # "foo": Add this folder as a class repository 28 # "foo/*.jar": Add all the JARs of the specified folder as class 29 # repositories 30 # "foo/bar.jar": Add bar.jar as a class repository 31 # 32 # Note: Values may be enclosed in double quotes ("...") in case either the 33 # ${catalina.base} path or the ${catalina.home} path contains a comma. 34 # Because double quotes are used for quoting, the double quote character 35 # may not appear in a path. 36 server.loader= 37 38 # 39 # List of comma-separated paths defining the contents of the "shared" 40 # classloader. Prefixes should be used to define what is the repository type. 41 # Path may be relative to the CATALINA_BASE path or absolute. If left as blank, 42 # the "common" loader will be used as Catalina's "shared" loader. 43 # Examples: 44 # "foo": Add this folder as a class repository 45 # "foo/*.jar": Add all the JARs of the specified folder as class 46 # repositories 47 # "foo/bar.jar": Add bar.jar as a class repository 48 # Please note that for single jars, e.g. bar.jar, you need the URL form 49 # starting with file:. 50 # 51 # Note: Values may be enclosed in double quotes ("...") in case either the 52 # ${catalina.base} path or the ${catalina.home} path contains a comma. 53 # Because double quotes are used for quoting, the double quote character 54 # may not appear in a path. 55 shared.loader=
但是,應該是從 tomcat 7開始, common.loader 和 shared.loader 已經預設置空了。 為什麼留空的原因,這裡先不詳細講述。(因為我也不完全懂啊,哈哈哈)
Webapp 類載入器就不用說了, 主要是載入自身目錄下的 WEB-INF/classes、 WEB-INF/lib 中的類。
對這部分感興趣的,可以再看看我的另一篇文章:實戰分析Tomcat的類載入器結構(使用Eclipse MAT驗證)
我們再回頭看看,文章開頭的圖裡,清晰地展示了: webapp的類載入器的parent,即為 common 類載入器。 那麼,只要我們在 業務程式碼裡進行如下呼叫,應該就獲取到了 common 類載入器,於是就可以愉快地載入 Tomcat 安裝目錄下的 lib目錄的jar了:
ClassLoader classLoader = this.getClass().getClassLoader(); ClassLoader directparent = classLoader.getParent();
2、驗證程式
我這邊建了個簡單的web程式,只有一個servlet。
MyServlet .java:
1 import javax.servlet.*; 2 import java.io.IOException; 3 import java.lang.reflect.InvocationTargetException; 4 import java.lang.reflect.Method; 5 import java.net.URL; 6 import java.net.URLClassLoader; 7 15 public class MyServlet implements Servlet { 16 @Override 17 public void init(ServletConfig config) throws ServletException { 18 19 } 20 21 @Override 22 public ServletConfig getServletConfig() { 23 return null; 24 } 25 26 27 @Override 28 public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException { 29 ClassLoader classLoader = this.getClass().getClassLoader(); 30 System.out.println("當前類載入器(webapp載入器):" + classLoader); 31 printPath(classLoader); 32 33 ClassLoader directparent = classLoader.getParent(); 34 System.out.println("父載入器(tomcat 自身的載入器):" + directparent); 35 printPath(directparent); 36 37 // 從父載入器開始迴圈,應該會按順序取到:應用類載入器--ext類載入器--bootstrap載入器 38 classLoader = directparent; 39 while (classLoader != null){ 40 ClassLoader parent = classLoader.getParent(); 41 System.out.println("當前類載入器為:" + parent); 42 printPath(parent); 43 classLoader = parent; 44 } 45 46 if (directparent != null) { 47 try { 48 Class<?> loadClass = directparent.loadClass("org.apache.catalina.core.StandardEngine"); 49 Object instance = loadClass.newInstance(); 50 Method[] methods = loadClass.getMethods(); 51 System.out.println("以下為StandardEngine的所有方法................."); 52 for (Method method : methods) { 53 System.out.println(method); 54 } 55 56 System.out.println("反射呼叫方法測試............................"); 57 Method getDefaultHostMethod = loadClass.getMethod("getDefaultHost"); 58 Object result = getDefaultHostMethod.invoke(instance); 59 System.out.println("before:" + result); 60 61 Method setDefaultHostMethod = loadClass.getMethod("setDefaultHost", String.class); 62 setDefaultHostMethod.invoke(instance,"hahaha..."); 63 64 Object afterResult = getDefaultHostMethod.invoke(instance); 65 System.out.println("after:" + afterResult); 66 67 } catch (ClassNotFoundException | IllegalAccessException | InstantiationException | InvocationTargetException | NoSuchMethodException e) { 68 e.printStackTrace(); 69 } 70 } 71 } 72 73 private void printPath(ClassLoader directparent) { 74 if (directparent instanceof URLClassLoader){ 75 URLClassLoader urlClassLoader = (URLClassLoader) directparent; 76 URL[] urLs = urlClassLoader.getURLs(); 77 for (URL urL : urLs) { 78 System.out.println(urL); 79 } 80 } 81 } 82 83 @Override 84 public String getServletInfo() { 85 return null; 86 } 87 88 @Override 89 public void destroy() { 90 91 } 92 }
加入到 web.xml中:
<servlet> <servlet-name>MyServlet</servlet-name> <servlet-class>MyServlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>MyServlet</servlet-name> <url-pattern>/*</url-pattern> </servlet-mapping>
pom.xml:
1 <?xml version="1.0" encoding="UTF-8"?> 2 3 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 4 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 5 <modelVersion>4.0.0</modelVersion> 6 7 <groupId>com.ckl</groupId> 8 <artifactId>tomcatclassloader</artifactId> 9 <version>1.0-SNAPSHOT</version> 10 <packaging>war</packaging> 11 12 <name>tomcatclassloader Maven Webapp</name> 13 <!-- FIXME change it to the project's website --> 14 <url>http://www.example.com</url> 15 16 <properties> 17 <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> 18 <maven.compiler.source>1.7</maven.compiler.source> 19 <maven.compiler.target>1.7</maven.compiler.target> 20 </properties> 21 22 <dependencies> 23 24 <dependency> 25 <groupId>javax.servlet</groupId> 26 <artifactId>javax.servlet-api</artifactId> 27 <version>3.1.0</version> 28 <scope>provided</scope> 29 </dependency> 30 31 </dependencies> 32 33 <build> 34 <finalName>tomcatclassloader</finalName> 35 <pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) --> 36 <plugins> 37 <plugin> 38 <artifactId>maven-clean-plugin</artifactId> 39 <version>3.1.0</version> 40 </plugin> 41 <!-- see http://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_war_packaging --> 42 <plugin> 43 <artifactId>maven-resources-plugin</artifactId> 44 <version>3.0.2</version> 45 </plugin> 46 <plugin> 47 <artifactId>maven-compiler-plugin</artifactId> 48 <version>3.8.0</version> 49 </plugin> 50 <plugin> 51 <artifactId>maven-surefire-plugin</artifactId> 52 <version>2.22.1</version> 53 </plugin> 54 <plugin> 55 <artifactId>maven-war-plugin</artifactId> 56 <version>3.2.2</version> 57 </plugin> 58 <plugin> 59 <artifactId>maven-install-plugin</artifactId> 60 <version>2.5.2</version> 61 </plugin> 62 <plugin> 63 <artifactId>maven-deploy-plugin</artifactId> 64 <version>2.8.2</version> 65 </plugin> 66 </plugins> 67 </pluginManagement> 68 </build> 69 </project>
執行結果如下:
當前類載入器(webapp載入器):ParallelWebappClassLoader context: tomcatclassloader delegate: false ----------> Parent Classloader: java.net.URLClassLoader@1372ed45 file:/F:/ownprojects/tomcatclassloader/target/tomcatclassloader/WEB-INF/classes/ 父載入器(tomcat 自身的載入器):java.net.URLClassLoader@1372ed45 file:/D:/soft/apache-tomcat-8.5.23/lib/ file:/D:/soft/apache-tomcat-8.5.23/lib/annotations-api.jar file:/D:/soft/apache-tomcat-8.5.23/lib/catalina-ant.jar file:/D:/soft/apache-tomcat-8.5.23/lib/catalina-ha.jar file:/D:/soft/apache-tomcat-8.5.23/lib/catalina-storeconfig.jar file:/D:/soft/apache-tomcat-8.5.23/lib/catalina-tribes.jar file:/D:/soft/apache-tomcat-8.5.23/lib/catalina.jar file:/D:/soft/apache-tomcat-8.5.23/lib/ecj-4.6.3.jar file:/D:/soft/apache-tomcat-8.5.23/lib/el-api.jar file:/D:/soft/apache-tomcat-8.5.23/lib/jasper-el.jar file:/D:/soft/apache-tomcat-8.5.23/lib/jasper.jar file:/D:/soft/apache-tomcat-8.5.23/lib/jaspic-api.jar file:/D:/soft/apache-tomcat-8.5.23/lib/jsp-api.jar file:/D:/soft/apache-tomcat-8.5.23/lib/servlet-api.jar file:/D:/soft/apache-tomcat-8.5.23/lib/tomcat-api.jar file:/D:/soft/apache-tomcat-8.5.23/lib/tomcat-coyote.jar 。。。此處省略部分。。。 當前類載入器為:sun.misc.Launcher$AppClassLoader@18b4aac2 file:/D:/soft/apache-tomcat-8.5.23/bin/bootstrap.jar file:/D:/soft/apache-tomcat-8.5.23/bin/tomcat-juli.jar 當前類載入器為:sun.misc.Launcher$ExtClassLoader@43d7741f file:/C:/Program%20Files/Java/jdk1.8.0_172/jre/lib/ext/access-bridge-64.jar file:/C:/Program%20Files/Java/jdk1.8.0_172/jre/lib/ext/cldrdata.jar 。。。此處省略部分。。。 當前類載入器為:null 以下為StandardEngine的所有方法................. public void org.apache.catalina.core.StandardEngine.setParent(org.apache.catalina.Container) public org.apache.catalina.Service org.apache.catalina.core.StandardEngine.getService() 。。。此處省略部分。。。 反射呼叫方法測試............................ before:null after:hahaha...
由上可知,我們訪問tomcat 自身的類,比如 org.apache.catalina.core.StandardEngine,是完全沒問題的。
二、可怕的引數傳遞實驗
1、實驗思路
不太好描述,直接碼,我們首先定義一個測試類,
# TestSample.java public class TestSample { public void printClassLoader(TestSample testSample) { System.out.println(testSample.getClass().getClassLoader()); } }
這個類,足夠簡單,裡面僅一個方法,方法接收一個自己型別的引數,方法體是列印出引數的類載入器。
在測試類中,直接 new 一個該類的物件A,然後呼叫其 printClassLoader,將物件A自己傳入,預設的列印結果是:
TestSample loader = new TestSample(); loader.printClassLoader(loader);
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$AppClassLoader 這個類,就是我們的應用類載入器,一般程式裡,沒有顯示定義過類載入器的話,classpath下的類都由該類載入。
我們要做的試驗有兩個:
1、如果傳入的引數物件,由另外一個類載入器載入的,能呼叫成功嗎,如果成功,結果是什麼?
2、如果由兩個相同類載入器的不同例項,來載入 TestSample ,然後反射獲取物件,那麼其中一個能作為另一個物件的 printClassLoader 的引數嗎?
開始之前,先準備好我們自定義的類載入器,
1 import java.io.ByteArrayOutputStream; 2 import java.io.FileInputStream; 3 import java.io.UnsupportedEncodingException; 4 5 /** 6 * desc: 7 * 8 * @author : caokunliang 9 * creat_date: 2019/6/13 0013 10 * creat_time: 10:19 11 **/ 12 public class MyClassLoader extends ClassLoader { 13 private String classPath; 14 private String className; 15 16 17 public MyClassLoader(String classPath, String className) { 18 this.classPath = classPath; 19 this.className = className; 20 } 21 22 @Override 23 protected Class<?> findClass(String name) throws ClassNotFoundException { 24 byte[] data = getData(); 25 try { 26 String string = new String(data, "utf-8"); 27 System.out.println(string); 28 } catch (UnsupportedEncodingException e) { 29 e.printStackTrace(); 30 } 31 32 return defineClass(className,data,0,data.length); 33 } 34 35 private byte[] getData(){ 36 String path = classPath; 37 38 try { 39 FileInputStream inputStream = new FileInputStream(path); 40 ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); 41 byte[] bytes = new byte[2048]; 42 int num = 0; 43 while ((num = inputStream.read(bytes)) != -1){ 44 byteArrayOutputStream.write(bytes, 0,num); 45 } 46 47 return byteArrayOutputStream.toByteArray(); 48 } catch (Exception e) { 49 e.printStackTrace(); 50 } 51 52 return null; 53 } 54 }
使用方法就像下面這樣:
MyClassLoader classLoader = new MyClassLoader("F:\\\\ownprojects\\\\test\\\\out\\\\TestSample.class", className); Class<?> loadClass = classLoader.findClass(className);
2、實驗1:應用預設載入器 && 自定義載入器
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; /** * desc: * * @author : caokunliang * creat_date: 2019/6/14 0014 * creat_time: 17:04 **/ public class MainTest {
public static void testMyClassLoaderAndAppClassloader()throws Exception{ // TestSample類由sun.misc.Launcher$AppClassLoader 載入,那麼 printClassLoader 需要的引數型別應該也是 Launcher$AppClassLoader載入的TestSample型別 // 而這裡的 sample 正好滿足,所以可以成功 TestSample sample = new TestSample(); sample.printClassLoader(sample); String className = "TestSample"; MyClassLoader classLoader = new MyClassLoader("F:\\\\ownprojects\\\\test\\\\out\\\\TestSample.class", className); Class<?> loadClass = classLoader.findClass(className); Object instance = loadClass.newInstance(); // 檢視是否能賦值 System.out.println(sample.getClass().isAssignableFrom(loadClass) ); // error: 這裡會報錯哦 TestSample instance1 = (TestSample) instance; sample.printClassLoader(instance1); } public static void main(String[] args) throws Exception { testMyClassLoaderAndAppClassloader(); } }
執行結果如下,在上圖示紅行,會報錯,錯誤為轉型錯誤:
[Loaded TestSample from __JVM_DefineClass__]
Exception in thread "main" java.lang.ClassCastException: TestSample cannot be cast to TestSample
at MainTest.testMyClassLoaderAndAppClassloader(MainTest.java:25)
at MainTest.main(MainTest.java:48)
這裡可以看出來,不同類載入器載入的類,即使是同一個類,也是不相容的。因為這個例子中,一個是由Launcher$AppClassLoader載入,一個是自定義載入器載入的。
下面,我們將進一步驗證這個結論。
3、實驗2:自定義載入器 && 自定義載入器 (不同例項)
實驗 3-1:
public static void testMyClassLoaderAndAnotherMyClassLoader() throws Exception { String className = "TestSample"; MyClassLoader classLoader = new MyClassLoader("F:\\\\ownprojects\\\\test\\\\out\\\\TestSample.class", className); Class<?> loadClass = classLoader.findClass(className); Object instance = loadClass.newInstance(); MyClassLoader classLoader1 = new MyClassLoader("F:\\\\ownprojects\\\\test\\\\out\\\\TestSample.class", className); Class<?> loadClass1 = classLoader1.findClass(className); Object instance1 = loadClass1.newInstance(); Method method = instance.getClass().getMethod("printClassLoader", new Class[]{TestSample.class}); method.invoke(instance,instance); }
上圖紅色處,會報錯,報錯如下,原因是TestSample.class 預設在classpath下,由應用類載入器載入,而 instance 是由 classLoader 載入的,引數型別因此不匹配:
Exception in thread "main" java.lang.NoSuchMethodException: TestSample.printClassLoader(TestSample) at java.lang.Class.getMethod(Class.java:1786) at MainTest.testMyClassLoaderAndAnotherMyClassLoader(MainTest.java:43) at MainTest.main(MainTest.java:49)
實驗 3-2:
(改動僅標紅處)
public static void testMyClassLoaderAndAnotherMyClassLoader() throws Exception { String className = "TestSample"; MyClassLoader classLoader = new MyClassLoader("F:\\\\ownprojects\\\\test\\\\out\\\\TestSample.class", className); Class<?> loadClass = classLoader.findClass(className); Object instance = loadClass.newInstance(); MyClassLoader classLoader1 = new MyClassLoader("F:\\\\ownprojects\\\\test\\\\out\\\\TestSample.class", className); Class<?> loadClass1 = classLoader1.findClass(className); Object instance1 = loadClass1.newInstance(); Method method = instance.getClass().getMethod("printClassLoader", new Class[]{loadClass}); method.invoke(instance,instance); }
可以正常執行,結果為:
[Loaded TestSample from __JVM_DefineClass__]
MyClassLoader@41a4555e
實驗3-3:
public static void testMyClassLoaderAndAnotherMyClassLoader() throws Exception { String className = "TestSample"; MyClassLoader classLoader = new MyClassLoader("F:\\\\ownprojects\\\\test\\\\out\\\\TestSample.class", className); Class<?> loadClass = classLoader.findClass(className); Object instance = loadClass.newInstance(); MyClassLoader classLoader1 = new MyClassLoader("F:\\\\ownprojects\\\\test\\\\out\\\\TestSample.class", className); Class<?> loadClass1 = classLoader1.findClass(className); Object instance1 = loadClass1.newInstance(); Method method = instance.getClass().getMethod("printClassLoader", new Class[]{loadClass1}); method.invoke(instance,instance); }
報錯,錯誤和實驗3-1差不多:
Exception in thread "main" java.lang.NoSuchMethodException: TestSample.printClassLoader(TestSample) at java.lang.Class.getMethod(Class.java:1786) at MainTest.testMyClassLoaderAndAnotherMyClassLoader(MainTest.java:43) at MainTest.main(MainTest.java:49)
實驗3-4:
public static void testMyClassLoaderAndAnotherMyClassLoader() throws Exception { String className = "TestSample"; MyClassLoader classLoader = new MyClassLoader("F:\\\\ownprojects\\\\test\\\\out\\\\TestSample.class", className); Class<?> loadClass = classLoader.findClass(className); Object instance = loadClass.newInstance(); MyClassLoader classLoader1 = new MyClassLoader("F:\\\\ownprojects\\\\test\\\\out\\\\TestSample.class", className); Class<?> loadClass1 = classLoader1.findClass(className); Object instance1 = loadClass1.newInstance(); Method method = instance.getClass().getMethod("printClassLoader", new Class[]{loadClass}); method.invoke(instance,instance1); }
此時報錯和前面不同:
Exception in thread "main" java.lang.IllegalArgumentException: argument type mismatch at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at MainTest.testMyClassLoaderAndAnotherMyClassLoader(MainTest.java:44) at MainTest.main(MainTest.java:49)
好了,做了這麼多實驗,想必大概都瞭解了吧,引數型別不只是完全限定類名要一致,而且還需要類載入器一致才行。
簡單的引數傳遞,實際上隱藏瞭如此之多的東西。引數要傳對,看來不能拼人品啊,還是得靠知識。
三、關於Tomcat 中類載入器的思考
不知道看完了上面的實驗,大家有沒有想到一個問題,在我們的servlet 開發中,servlet-api.jar 包預設是由 tomcat 提供的,意思也就是,servlet-api.jar中的類應該都是由 tomcat 的common 類載入器載入的。(這個早已驗證,可翻我之前的部落格)
servlet-api.jar包中,有很多類,大家肯定用過 javax.servlet.Filter#doFilter :
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException;
我們思考一個問題,假如 我們在我們 web-inf/lib下,自己放上一個 servlet-api.jar,那麼載入 web-inf/lib 的自然就是 webappClassloader,那麼載入我們的filter的,也就是 webappClassloader。那麼我們的filter的引數,預設就應該只接受 webappclassloader 載入的 ServletRequest 、ServletResponse 類。
但是,很顯然,因為 Tomcat 的lib下面也有 servlet-api.jar,給我們的filter 傳遞的 reqeust引數,應該是由其 自己的common 類載入器載入的,問題來了,這樣還能呼叫成功我們的 filter 方法嗎?按理說,不可能,應該會報一個引數型別不匹配的錯誤才對,因為上一章的實驗結果就擺在那裡。
那就再測試一次吧,事實勝於雄辯,首先,我們將複用第一章的例子的servlet,唯一要改的,只是pom.xml(註釋了provided那行):
1 <dependency> 2 <groupId>javax.servlet</groupId> 3 <artifactId>javax.servlet-api</artifactId> 4 <version>3.1.0</version> 5 <!--<scope>provided</scope>--> 6 </dependency>
maven打包部署到tomcat,我們啟動Tomcat時,可以在catalina.sh/bat 中加一個引數:-XX:+TraceClassLoading,啟動後,訪問我們的 MyServlet,並沒有什麼異常(大家可以試試)。
然後我看了下,servletRequest等class,到底從哪載入的,下圖可以看出來,都是來自 tomcat 自身的 servlet-api.jar包:
而我們的 web-inf下的 servlet-api 包,完全就是個悲劇,被忽略了啊。。。慘。。。(我要你有何用??)
而且,另外一個層面來說,執行完全沒報錯,說明 webapp 中 載入servlet-api.jar包的classloader 和 tomcat 載入 servlet-api.jar包的classloader 為同一個,不然早就報錯了。那麼意思就是說, webapp 中載入 servlet-api.jar ,其實用的 tomcat 的common 類載入器去載入。(我真的柯南附體了。。。) 反證法也可以說明這一點,因為我們在 webapp的lib 下,是可以不放 servlet-api.jar包的,jar包只在 tomcat 有,而 webapp 的類載入器又不能去載入 tomcat 的東西,所以,只能說: webapp 類載入器委託了 tomcat 幫他載入。
我們可以看看 webappclassloader 的實現,我本地原始碼版本是 tomcat 7的,不過無所謂,都差不多:
org.apache.catalina.loader.WebappClassLoaderBase#loadClass(java.lang.String, boolean):
1 synchronized (getClassLoadingLockInternal(name)) { 2 if (log.isDebugEnabled()) 3 log.debug("loadClass(" + name + ", " + resolve + ")"); 4 Class<?> clazz = null; 5 6 7 // (0) Check our previously loaded local class cache // 檢查本載入器是否載入過了,本地有個map 8 clazz = findLoadedClass0(name); 9 if (clazz != null) { 10 if (log.isDebugEnabled()) 11 log.debug(" Returning class from cache"); 12 if (resolve) 13 resolveClass(clazz); 14 return (clazz); 15 } 16 17 // (0.1) Check our previously loaded class cache // 呼叫了本載入器的本地方法,檢視是否載入過了 18 clazz = findLoadedClass(name); 19 if (clazz != null) { 20 if (log.isDebugEnabled()) 21 log.debug(" Returning class from cache"); 22 if (resolve) 23 resolveClass(clazz); 24 return (clazz); 25 } 26 27 // (0.2) Try loading the class with the system class loader, to prevent // 先交給 擴充套件類載入器,免得把 jre/ext下面的類自己載入了出大事 28 // the webapp from overriding J2SE classes 29 try { 30 clazz = j2seClassLoader.loadClass(name); 31 if (clazz != null) { 32 if (resolve) 33 resolveClass(clazz); 34 return (clazz); 35 } 36 } catch (ClassNotFoundException e) { 37 // Ignore 38 } 39 40 // (0.5) Permission to access this class when using a SecurityManager 這個不管,我們這邊是null 41 if (securityManager != null) { 42 int i = name.lastIndexOf('.'); 43 if (i >= 0) { 44 try { 45 securityManager.checkPackageAccess(name.substring(0,i)); 46 } catch (SecurityException se) { 47 String error = "Security Violation, attempt to use " + 48 "Restricted Class: " + name; 49 log.info(error, se); 50 throw new ClassNotFoundException(error, se); 51 } 52 } 53 } 54 55 boolean delegateLoad = delegate || filter(name); //預設為false,可以配置,如果為true,表示應該交給 tomcat 的common類載入器先載入 56 57 // (1) Delegate to our parent if requested 58 if (delegateLoad) { 59 if (log.isDebugEnabled()) 60 log.debug(" Delegating to parent classloader1 " + parent); 61 try { 62 clazz = Class.forName(name, false, parent); 63 if (clazz != null) { 64 if (log.isDebugEnabled()) 65 log.debug(" Loading class from parent"); 66 if (resolve) 67 resolveClass(clazz); 68 return (clazz); 69 } 70 } catch (ClassNotFoundException e) { 71 // Ignore 72 } 73 } 74 75 // (2) Search local repositories // 如果 tomcat 的common類載入器 載入失敗,則有自己載入 76 if (log.isDebugEnabled()) 77 log.debug(" Searching local repositories"); 78 try { 79 clazz = findClass(name); 80 if (clazz != null) { 81 if (log.isDebugEnabled()) 82 log.debug(" Loading class from local repository"); 83 if (resolve) 84 resolveClass(clazz); 85 return (clazz); 86 } 87 } catch (ClassNotFoundException e) { 88 // Ignore 89 } 90 91 // (3) Delegate to parent unconditionally // 如果自己載入失敗了,別說了,都甩給 tomcat 的common類載入器吧 92 if (!delegateLoad) { 93 if (log.isDebugEnabled()) 94 log.debug(" Delegating to parent classloader at end: " + parent); 95 try { 96 clazz = Class.forName(name, false, parent); 97 if (clazz != null) { 98 if (log.isDebugEnabled()) 99 log.debug(" Loading class from parent"); 100 if (resolve) 101 resolveClass(clazz); 102 return (clazz); 103 } 104 } catch (ClassNotFoundException e) { 105 // Ignore 106 } 107 }
簡單歸納下:
1、webappclassloader 載入時,先看本載入器的快取,看看是否載入過了,載入過了直接返回,否則進入2;
2、先給 jdk 的jre/ext 類載入器載入, jre/ext 如果載入不了,會丟給 Bootstrap 載入器,如果載入到了,則返回,否則進入3;
3、判斷delegate 屬性,如果為true,則進入3.1,為false,則進入 3.2
3.1 丟給tomcat 的common 類載入器,載入成功則返回,否則本載入器真正嘗試載入,成功則返回,否則拋異常:載入失敗。
3.2 先讓自己類載入器嘗試,成功則返回,否則丟給 tomcat 載入,成功則返回,否則拋異常:載入失敗。
四、總結
物件,由類生成,類,由類載入器載入而來。 物件的方法引數的型別,也和類載入器息息相關, 這個引數是 類載入器 A 載入的class B型別,你必須也傳一個這樣的給我,我才認啊。
舉個例子,假設你先後有過兩個女朋友,前女友給你送了個iphone 8,現女友也送了你一個iphone 8, 這兩個iphone 8 都是同一個地方買的,那這兩個iPhone 8 能一樣嗎?要不問問你現女友去?
所以說啊,java這東西,他麼的易學難精。。。繼續努力吧。 下篇可以寫寫熱部署、OSGI的問題,(半桶水,我自己也要去研究下,哈哈)。。