我們都知道JDBC規(guī)范定義在java的核心包rt.jar中,rt.jar是由啟動類加載器去加載的,jdbc的驅動管理器是由具體的數(shù)據(jù)庫廠商提供的,比如mysql和oracle的jar包。實際開發(fā)中一般是把驅動包放到classpath中,但是classpath路徑是由系統(tǒng)類加載器去加載的,正常來說啟動類加載器是無法加載classpath中的類文件的,那么jdbc是如何獲取到驅動的呢?想要弄清楚這個問題,需要先了解java類加載的機制。
一 類加載器的介紹
java代碼通過javac編譯成class文件,而類加載器就是把class文件裝進虛擬機。java中類加載器分為四類,分別是啟動類加載器、擴展類加載器、系統(tǒng)類加載器和自定義類加載器。為什么需要這么多類加載器呢?因為java虛擬機啟動的時候,并不會一次性加載所有的class文件,而是根據(jù)需要去動態(tài)加載,避免很大程度的內存浪費。
-
啟動類加載器
顧名思義,是在虛擬機啟動時加載,是由C++編寫,主要掃描rt.jar這個包,但并不僅僅是這個包,可以通過系統(tǒng)參數(shù)System.getProperty("sun.boot.class.path")獲取掃描的路徑??梢钥吹剑瑔宇惣虞d器加載的是jre和jre/lib目錄下的核心庫。//啟動類加載器加載的路徑 String bootstrapPaths = System.getProperty("sun.boot.class.path"); Arrays.stream(bootstrapPaths.split(":")).forEach(path -> System.out.println(path)); //打印結果如下 /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/resources.jar /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/rt.jar /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/sunrsasign.jar /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/jsse.jar /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/jce.jar /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/charsets.jar /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/jfr.jar /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/classes -
擴展類加載器
通過System.getProperty("java.ext.dirs")可以看到它主要負責加載Java的擴展類庫,默認加載JAVA_HOME/jre/lib/ext/目錄下的所有jar包或者由java.ext.dirs系統(tǒng)屬性指定的jar包。//擴展類加載器加載的路徑: String extPaths = System.getProperty("java.ext.dirs"); Arrays.stream(extPaths.split(":")).forEach(path -> System.out.println(path)); //打印結果如下 /Users/liyefei6/Library/Java/Extensions /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/ext /Library/Java/Extensions /Network/Library/Java/Extensions /System/Library/Java/Extensions /usr/lib/java -
系統(tǒng)類加載器
負責在JVM啟動時,加載來自在命令java中的classpath或者java.class.path系統(tǒng)屬性或者CLASSPATH操作系統(tǒng)屬性所指定的JAR類包和類路徑.//系統(tǒng)類加載器加載的路徑: String appPaths = System.getProperty("java.class.path"); Arrays.stream(appPaths.split(":")).forEach(path -> System.out.println(path)); //打印結果如下 系統(tǒng)類加載器加載的路徑: /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/charsets.jar /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/deploy.jar /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/ext/cldrdata.jar /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/ext/dnsns.jar /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/ext/jaccess.jar /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/ext/jfxrt.jar /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/ext/localedata.jar /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/ext/nashorn.jar /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/ext/sunec.jar /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/ext/sunjce_provider.jar /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/ext/sunpkcs11.jar /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/ext/zipfs.jar /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/javaws.jar /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/jce.jar /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/jfr.jar /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/jfxswt.jar /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/jsse.jar /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/management-agent.jar /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/plugin.jar /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/resources.jar /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/rt.jar /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/lib/ant-javafx.jar /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/lib/dt.jar /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/lib/javafx-mx.jar /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/lib/jconsole.jar /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/lib/packager.jar /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/lib/sa-jdi.jar /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/lib/tools.jar /Users/liyefei6/code/demo-parent/demo-classloader/target/classes /Users/liyefei6/.m2/repository/org/apache/commons/commons-lang3/3.11/commons-lang3-3.11.jar ......public class Test { public static void main(String[] args) { //下面代碼輸出結果為:sun.misc.Launcher$AppClassLoader@18b4aac2 System.out.println(Test.class.getClassLoader()); } } //通過上面代碼打印的結果驗證了Test類使用的類加載器為AppClassLoader,即系統(tǒng)類加載器。 -
自定義類加載器
如果系統(tǒng)提供的類加載器滿足不了需求,比如本地磁盤或者網絡下載的.class文件,那么就可以自定義一個類加載器。下面是官方doc上給出的實現(xiàn)自定義加載器的例子:* class NetworkClassLoader extends ClassLoader { * public Class findClass(String name) { * byte[] b = loadClassData(name); * return defineClass(name, b, 0, b.length); * } * * private byte[] loadClassData(String name) { * // load the class data from the connection * } * }從代碼示例中可以看出,實現(xiàn)自定義類加載器只需要三步:寫一個loadClassData把class文件變?yōu)閎yte數(shù)組、重寫findClass方法、調用defineClass方法。下面是自定義類加載器的實現(xiàn):
//自定義類加載器 public class MyClassLoader extends ClassLoader { private String loadPath; public MyClassLoader(String loadPath) { super(); this.loadPath = loadPath; } @Override public Class findClass(String name) { byte[] b = loadClassData(name); return defineClass(name, b, 0, b.length); } private byte[] loadClassData(String name) { String fullPath = loadPath + name.replaceAll("\\.", "/") + ".class"; File file = new File(fullPath); if (file.exists()) { FileInputStream in = null; ByteArrayOutputStream out = null; try { in = new FileInputStream(file); out = new ByteArrayOutputStream(); byte[] buffer = new byte[1024]; int size = 0; while ((size = in.read(buffer)) != -1) { out.write(buffer, 0, size); } } catch (IOException e) { e.printStackTrace(); } finally { try { in.close(); } catch (IOException e) { e.printStackTrace(); } } return out.toByteArray(); } else { return null; } } }//測試類 public class Test { public static void main(String[] args) throws Exception { //使用默認的類加載器 System.out.println(Test.class.getClassLoader()); //使用自定義的類加載器 MyClassLoader myClassLoader = new MyClassLoader("/Users/liyefei6/classes/"); Class<?> person1Class = myClassLoader.loadClass("com.demo.classloader.Person1"); System.out.println(person1Class.getClassLoader()); } } //打印結果: sun.misc.Launcher$AppClassLoader@18b4aac2//使用的是系統(tǒng)類加載器AppClassLoader com.demo.classloader.MyClassLoader@1fb3ebeb//使用的是自定義類加載器MyClassLoader從系統(tǒng)提供的類加載器掃描的路徑分析到/Users/liyefei6/classes/路徑是不會被掃描到的,就不會被類加載器所加載,所以打印出來的結果是自定義類加載器,而Test類是在classpath下,是會被系統(tǒng)類加載器所加載到的,所以打印的結果是AppClassLoader。有意思的是Person1類中有靜態(tài)代碼塊但并未執(zhí)行,java中嚴格規(guī)定了只有以下五種情況會觸發(fā)類的初始化:
- 遇到new、getstatic、putstatic、invokestatic這四條字節(jié)碼指令
- 使用Java.lang.refect包的方法對類進行反射調用
- 當初始化一個類的時候,如果發(fā)現(xiàn)其父類還沒有進行初始化,則需要先觸發(fā)其父類的初始化
- 當虛擬機啟動時,用戶需要指定一個要執(zhí)行的主類,虛擬機會先執(zhí)行該主類
- JDK1.5中方法句柄所對應的類沒有進行過初始化,則需要先觸發(fā)其初始化
簡單總結就是類加載不一定會初始化,初始化一定需要類加載,Person1類如下所示:
public class Person1 { private String name; static { System.out.println("靜態(tài)代碼塊執(zhí)行 -> Person1"); } public Person1() { System.out.println("構造方法執(zhí)行 -> Person1"); } public String getName() { return name; } public void setName(String name) { this.name = name; } }
二 類加載器執(zhí)行過程
雙親委派模型是一種很好的設計思想,它不僅可以避免重復加載,還可以避免核心類被篡改。比如自己重寫個java.lang.Object并放到Classpath中,如果沒有雙親委派的話直接自己執(zhí)行了,那不安全。雙親委派可以保證這個類只能被頂層Bootstrap Classloader類加載器加載,從而確保只有JVM中有且僅有一份正常的java核心類。如果有多個的話,那么就亂套了。比如相同的類instance of可能返回false,因為可能父類不是同一個類加載器加載的Object。借鑒比較經典的兩幅圖來介紹類加載的過程。

如果一個類加載器收到了類加載的請求,他首先會從自己緩存里查找是否之前加載過這個class,如果加載過直接返回,如果沒加載過的話他不會自己親自去加載,他會把這個請求委派給父類加載器去完成,每一層都是如此,類似遞歸,一直遞歸到頂層父類。也就是Bootstrap ClassLoader,只要加載完成就會返回結果,如果頂層父類加載器無法加載此class,則會返回去交給子類加載器去嘗試加載,若最底層的子類加載器也沒找到,則會拋出ClassNotFoundException。
但是在有些場景下需要打破雙親委派的思想,比如Tomcat、熱部署等等。
Tomcat為什么要破壞雙親委派模型?
因為一個Tomcat可以部署N個web應用,但是每個web應用都需要有自己的classloader,才能互不干擾。比如web1里面有com.test.A.class,web2里面也有com.test.A.class,如果只有一套classloader,出現(xiàn)了兩個重復的類路徑,會相互影響和沖突,所以tomcat打破了,他是線程級別的,不同web應用是不同的classloader。
熱部署打破雙親委派是因為jvm會通過默認的loadClass()方法先找緩存,如果加載過就不會再次加載,你改了class字節(jié)碼也不會熱加載,所以自定義ClassLoader,去掉找緩存那部分,直接就去加載,也就是每次都重新加載。
三 SPI介紹
「SPI」 全稱為 (Service Provider Interface) ,是JDK內置的一種服務提供發(fā)現(xiàn)機制。 目前有不少框架用它來做服務的擴展實現(xiàn), 簡單來說,它就是一種動態(tài)替換發(fā)現(xiàn)的機制。
按照雙親委派模型的思想,JDBC規(guī)范中定義的類是由啟動類加載器去加載的,它是是無法使用mysql或者oracle驅動包里由系統(tǒng)類加載器加載出來的類的。為了解決這個問題,又引入了SPI的規(guī)范,使啟動類加載器可以調用系統(tǒng)類加載器加載出的類。
下面舉例說明SPI的使用,首先創(chuàng)建一個僅包含擴展接口的maven工程spi-interface,然后創(chuàng)建一個接口SPIService。
//SPIService.java
package com.liyefei.service;
public interface SPIService {
void println(String str);
}
<!-- pom.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<groupId>com.liyefei.interface</groupId>
<artifactId>spi-interface</artifactId>
<version>1.0.0</version>
</project>
接下來創(chuàng)建一個maven工程spi-impl作為擴展的實現(xiàn),工程的資源文件下需要創(chuàng)建META-INF/services目錄,目錄中需要創(chuàng)建一個用擴展接口全路徑名稱的文件,文件內容為接口實現(xiàn)類的全路徑名,代碼如下:
//OneServiceImpl.java
package com.liyefei.one;
import com.liyefei.service.SPIService;
public class OneServiceImpl implements SPIService {
@Override
public void println(String str) {
System.out.println("impl service:" + str);
}
}
<!-- pom.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<groupId>com.liyefei.impl</groupId>
<artifactId>spi-impl</artifactId>
<version>1.0.0</version>
<dependencies>
<dependency>
<groupId>com.liyefei.interface</groupId>
<artifactId>spi-interface</artifactId>
<version>1.0.0</version>
</dependency>
</dependencies>
</project>
文件目錄如下:
├── spi-impl
│ ├──src
│ │ ├──main
│ │ ├──java
│ │ └──OneServiceImpl
│ │ ├──resources
│ │ ├──META-INF
│ │ ├──services
│ │ ├──com.liyefei.service
└──pom.xml
最后創(chuàng)建一個spi-test的maven工程,用于測試,工程依賴擴展的實現(xiàn),代碼如下:
//Test.java
package com.liyefei.test;
import com.liyefei.service.SPIService;
import java.util.Iterator;
import java.util.ServiceLoader;
public class Test {
public static void main(String[] args) {
ServiceLoader<SPIService> spiServices = ServiceLoader.load(SPIService.class);
Iterator<SPIService> iterator = spiServices.iterator();
while (iterator.hasNext()) {
iterator.next().println("run");
}
}
}
//打印結果:impl service:run
<!-- pom.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<dependencies>
<dependency>
<groupId>com.liyefei.impl</groupId>
<artifactId>spi-impl</artifactId>
<version>1.0.0</version>
</dependency>
</dependencies>
</project>
從打印結果可以看出,接口SPIService的實現(xiàn)類ONEServiceImpl成功執(zhí)行,想要深究原因只能查看源碼,進入load方法發(fā)現(xiàn)使用了上下文類加載器,源碼如下:
//ServiceLoader.class
public static <S> ServiceLoader<S> load(Class<S> service) {
//上下文類加載器
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
通過源碼一路跟進可以看到,fullName就是由接口擴展實現(xiàn)工程中資源文件夾下"META-INF/services/" + 接口全路徑名稱組成,最后通過Class.forName使用上下文類加載器反射出所需要的類。
//ServiceLoader.class
private static final String PREFIX = "META-INF/services/";
String fullName = PREFIX + service.getName();
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
configs = loader.getResources(fullName);
//ServiceLoader.class
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
Class<?> c = null;
try {
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service, "Provider " + cn + " not found");
}
那么上下文類加載器中存放的是那個類加載器呢?通過類加載器的主要類Launcher.class源碼中可以看到線程上下文類加載器中默認存放的類加載器為系統(tǒng)類加載器。通過Thread.currentThread().setContextClassLoader(this.loader)這行代碼打破了雙親委派模型。
//Launcher.class
public class Launcher {
private static URLStreamHandlerFactory factory = new Launcher.Factory();
private static Launcher launcher = new Launcher();
private static String bootClassPath = System.getProperty("sun.boot.class.path");
private ClassLoader loader;
private static URLStreamHandler fileHandler;
public static Launcher getLauncher() {
return launcher;
}
public Launcher() {
Launcher.ExtClassLoader var1;
try {
//擴展類加載器
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}
try {
//系統(tǒng)類加載器
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
//此處即為jvm啟動時設置的上下文類加載器,即系統(tǒng)類加載器
Thread.currentThread().setContextClassLoader(this.loader);
.....
SPI打破了雙親委派模型,具體的底層實現(xiàn)可以分離出來,將每組實現(xiàn)打包成不同的jar,在具體使用時根據(jù)需要使用不同的jar即可,方便了應用的擴展,JDBC規(guī)范中加載各個供應商的驅動就是通過這種方式去實現(xiàn)的。

四 總結與展望
工作和生活中只要肯思考,就會有收獲。通過一個類加載器可以挖掘出很多的知識點,慢慢就會明白很多應用的實現(xiàn)原理,想要弄清楚原理必須要通過源碼去發(fā)掘、去實踐,也希望自己可以通過多發(fā)掘別人的源碼、學習別人的設計思想,有朝一日可以在技術的道路上越走越遠。