線程上下文的類加載器(setContextClassLoader)
-
當前類加載器(Current ClassLoader)
每個類都會使用自己的類加載器(即加載自身的類加載器)來去加載其他類(指的是是所依賴的類),如果classX引用ClassY,那麼ClassX的類加載器就會去加載ClassY(前提是ClassY尚未加載) -
線程上下文類加載器(Context ClassLoader)
線程上下文類加載器是從JDK1.2開始引入的,類Thread中的getContextClassLoader()與setContextClassLoader(ClassLoader cl)分別用來獲取和設置上下文類加載器
線程上下文類加載器的重要性
-
SPI :(Service Provider Interface)服務提供者接口如:jdbc 是用來聲明接口制定一些標準(僅僅通過雙親委託來加載類時 父加載器無法看到子類加載器(命名空間) 但是父加載器加載 如 根加載器加載一些rt.jar包裏的一些內容如jdbc定義的接口,需要調外部別人實現的一些類時就無法訪問到)
-
父類ClassLoader可以使用當前線程Thread.currentThread().getContextClassLoader()所指定的classloader加載對應的類放到內存中,供父類加載器加載的類使用。這就:改變了父classLoader不能使用子ClassLoader或是其他沒有直接父子關係的classLoader加載的類的情況。即改變了雙親委託模型**
-
線程上下文類加載器就是當前線程的Current Classloader。
-
在雙親委託模型下,類加載是由下至上的,即下層的類加載器會委託上層的加載。但是對於SPI來說,有些接口是java核心庫所提供的,而java核心庫是由啓動類加載器來加載的,
而這些接口的實現去來自於不同的jar包(廠商提供)java的啓動類加載器是不會加載其他來源的jar包,這樣傳統的雙親委託模型就無法滿足SPI的要求,
而通過給當前線程設置上下文類加載器,就可以由設置的上下文類加載器來實現對於接口實現類的加載
public class MyTest24 {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getContextClassLoader()); //AppClassLoader
System.out.println(Thread.class.getClassLoader()); //null Thred位於lang包
}
}
如果沒有設置線程上下文類加載器,該線程上下文類加載器默認設置爲系統類加載器
public class MyTest25 implements Runnable {
private Thread thread;
public MyTest25() {
this.thread = new Thread(this);
thread.start();
}
@Override
public void run() {
ClassLoader classLoader = this.thread.getContextClassLoader();
this.thread.setContextClassLoader(classLoader);
System.out.println("Class: " + classLoader.getClass());
System.out.println("parent: " + classLoader.getParent().getClass());
}
public static void main(String[] args) {
new MyTest25();
}
}
/*
Class: class sun.misc.Launcher$AppClassLoader //如果沒有設置線程上下文類加載器 默認設置爲 系統類加載器 具體在getLauncher 講解裏
parent: class sun.misc.Launcher$ExtClassLoader //系統類加載器的父類 只是打破加載規則 並不打破包含規則
*/
線程上下文類加載器的一般使用模式
線程上下文類加載器的一般使用模式(獲取->使用->還原)
//獲取
ClassLoader classLoader = Thread.currentThread.getContextClassLoader();
try{
//設置要使用的類加載器
Thread.currentThread.setContextClassLoader(cls);
//使用
MyMethod();
}finally{
//如果不還原使用的就是設置的類加載器
//還原
Thread.currentThread.setContextClassLoader(classLoader);
}
上例中:
MyMethod裏面則調用了Thread.currentThread().getContextClassLoader(),獲取當前線程的上下文類加載器做某些事情 MyMethod()。
-
如果一個類由類加載器A加載,那麼這個類的依賴類也是由相同的類加載器加載的(如果該依賴類之前沒有被加載過的話) 如:
啓動類加載器掃描不到系統類加載器的內容 上下文類加載器就是爲了解決這個問題 -
ContextClassLoader的作用就是爲了破壞Java的類加載委託機制(如上)
-
當高層提供了統一的接口讓低層去實現,同時又要在高層加載(或實例化)底層的類時,就必須要通過線程上下文類加載器來幫助高層的ClassLoader找到並加載該類(上下文類加載器就是爲了解決父類加載器加載的類無法看到子類加載器加載的類)
我們可以直接使用getClassLoader獲取系統類加載器去加載對應的類爲什麼還要使用上下文類加載器?
1.方便,執行的任何代碼都在線程中 我們可以隨時取出來對應的上下文類加載器使用2.解決 高層加載底層類問題
3.有特定的情況,當前的線程類加載器 不一定是系統類加載器 此時不能加載classpath下的.class文件
import java.sql.Driver;
import java.util.Iterator;
import java.util.ServiceLoader;
public class MyTest26 {
public static void main(String[] args) {
//Driver驅動連接規範的一些信息
//load()等價與load(Class<S> service, ClassLoader loader)
//load(需要加載服務的class,Thread.currentThread().getContextClassLoader())
ServiceLoader<Driver> loader = ServiceLoader.load(Driver.class);
Iterator<Driver> iterator = loader.iterator();
while (iterator.hasNext()){
Driver driver = iterator.next();
System.out.println("driver: "+driver.getClass()+",loader: "+driver.getClass().getClassLoader());
}
System.out.println("當前線程上下文類加載器: "+Thread.currentThread().getContextClassLoader());
System.out.println("ServiceLoader的類加載器:"+loader.getClass().getClassLoader());
}
}
/*
爲什麼僅僅是Driver.class 這個接口就能找到以下兩個Driver實現類
服務提供者將提供者(供應商)的程序配置文件放在資源目錄META-INF/services目錄**下當中。
該文件的名稱(如mysql 加載java.sql.Driver文件 類加載器就可以加載這個文件裏面每一行的二進制名(類名的路徑 此時對應的類就會被加載))
driver: class com.alibaba.druid.proxy.DruidDriver,loader: sun.misc.Launcher$AppClassLoader@18b4aac2 所有maven下載的jar都位於classpath下
driver: class com.alibaba.druid.mock.MockDriver,loader: sun.misc.Launcher$AppClassLoader@18b4aac2
driver: class com.mysql.cj.jdbc.Driver,loader: sun.misc.Launcher$AppClassLoader@18b4aac2
---
當前線程上下文類加載器: sun.misc.Launcher$AppClassLoader@18b4aac2 未設置默認爲系統類加載器
ServiceLoader的類加載器:null --ServiceLoader位於rt.jar包下
*/
ServiceLoader(服務加載器) 源碼分析 (jdk1.6之後)
1.doc官方文檔ServiceLoader(服務加載器)在SPI中的重要作用分析
ServiceLoader就是用於加載提供者在特定位置META-INF/services 的服務對應服務文件名裏的二進制文件名的類
extends Object
implements 12
- ServiceLoader是一個服務提供加載者
-已知服務的接口(抽象類的集合), 用來加載服務特定的實現(如加載JDBC接口實現類)。提供者(廠商)中的類通常實現服務者接口並繼承服務本身中定義的類。服務提供者(如JDBC驅動)可以安裝在Java平臺的實現中以擴展名的形式,比如說以jar文件被放置到任何常用的擴展目錄中。提供程序也可以通過將它們添加到應用程序的類路徑(classpath)或其他特定於平臺的方法 ,讓其變得可用。 - 對於加載目的,服務由單一類型表示,即單個接口或抽象類。(抽象類可以是已使用,但不建議使用。)給定服務的提供者包含一個或多個具體類,用數據擴展這個服務類型和特定於提供程序的代碼。提供程序類是典型的不是整個提供者本身,而是一個包含足夠內容的代理決定供應商是否能夠滿足特定要求的信息請求和代碼一起,可以根據需要創建實際的提供者。提供者類的細節往往是高度特定於服務的;沒有
單個類或接口可能統一它們,所以沒有這樣的類型在這裏定義。這個設施實施的唯一要求是提供者實現類必須有一個無參數的構造函數,這樣它們才能在加載期間實例化。 - 服務提供者(供應商)將提供的的程序配置文件放在驅動包資源目錄META-INF/services目錄下當中。該文件的名稱(如mysql 加載java.sql.Driver文件 類加載器就可以加載這個文件裏面每一行的二進制名(類名類名的路徑 此時對應的類就會被加載))這是一個服務提供者(sun)制定的標準 是完全限定的 href = " . . / lang /服務類型二進制名。該文件包含一個完全限定的二進制名的具體提供程序列表,每行放置一個(類的二進制名)。空格和每個字符周圍的製表符name和空行將被忽略。註釋字符“#”;在每行第一個註釋字符後面的所有字符將被忽略。文件名必須用UTF-8編碼。(這句話的意思 服務提供者必須要一種方式告訴JDK我的提供者(廠商)具體的類是如何定義在 META-INF/services目錄下的,而且文件名是服務類型的名字(給父類使用類二進制名)在這個文件中就可以一行指定一個提供者的類名(實現類) 需要上下文加載器加載的類 #是作爲註釋)
- 如果在多個配置文件中出現了同一個具體提供者類的名字的話,或在相同的配置文件中名字出現了一次以上,重複項將被忽略。配置文件特定的提供者不需要在相同的jar文件或其他發行版中單位作爲提供者本身。提供者必須是可從相同的類加載器,與最初被定位配置文件相同的類加載器加載出來;注意,這個並不是必要的。
- 提供程序是按需定位和實例化的。一個服務加載器維護一個已加載的提供者緩存。每次調用{@link #iterator}方法都會返回一個迭代器,它首先返回緩存中的所有元素實例化順序,然後延遲定位和實例化任何剩餘的提供程序,依次將每個提供程序添加到緩存中。可以清除緩存通過{@link #reload reload}方法。(提供者當中的類都是延遲實例化和加載的 換句話說就是什麼時候使用就什麼時候加載,在serviceLoader中存在緩存,緩存已經加載過的服務提供者提供的類)
緩存存儲位置
他是按照實例化順序添加緩存的
- 服務加載程序總是在調用者的安全上下文中執行。受信任的系統代碼通常應該調用這個類中的方法,並且他們返回的迭代器的方法,從一個特權安全上下文。
- 並不是線程安全的一個類
- 除非另有說明,否則將null參數傳遞給any方法將導致拋出{@link NullPointerException}。
- 假設我們有一個服務類型com.example.CodecSet (提供者類型與sci規範對應) 這是用於表示某些協議的編碼器/解碼器對的集合。在這種情況下,它是一個抽象類,有兩個抽象方法:
public abstract Encoder getEncoder(String encodingName);//獲取解碼器
public abstract Decoder getDecoder(String encodingName);//獲取編碼器
每個方法返回一個適當的對象或null(如果提供程序)不支持給定的編碼。典型的提供者支持不止一個編碼。
如果com.example.impl是com.example.CodecSet(是java定義的一個規範接口的)的具體實現類的話,那麼它的jar文件也包含一個名爲的 META-INF/services/com.example.CodecSet的文件(對應提供者也必須在指定位置有一個同樣的接口名的文件) - 這個文件會包含如下這一行
com.example.impl.StandardCodecs (類似mysql的驅動)
- CodecSet類創建並保存一個服務實例 如上如果是兩個就是加載保存兩個實例
private static ServiceLoader<CodecSet>codecSetLoader= ServiceLoader.load(CodecSet.class);
此時load(CodecSet.class); 加載的就是/com.example.CodecSet的文件中對應的二進制名的類的實例
(個人理解就是你要對接java規範中那個服務CodecSet就是對應的服務實例)
- The type of the service to be loaded by this loader (加載的泛型就是要加載的服務的類型)
ServiceLoader.load(String service)源碼 解析
public static <S> ServiceLoader<S> load(Class<S> service) {
//ServiceLoader在覈心庫中使用根加載器無法加載對應實現類
//所以的獲取當前上下文類加載器(未設置 在launcher中設置爲系統類加載器
//基本類加載都會加載Launcher類)
//核心代碼
ClassLoader cl = Thread.currentThread().getContextClassLoader();
//重載
return ServiceLoader.load(service, cl);
}
public static <S> ServiceLoader<S> load(Class<S> service,
ClassLoader loader)
{
// 創界一個服務加載器 調用其構造方法 傳入需要掃描的規範接口 傳入需要加載的類加載器
return new ServiceLoader<>(service, loader);
}
private ServiceLoader(Class<S> svc, ClassLoader cl) {
service = Objects.requireNonNull(svc, "Service interface cannot be null");
//如果沒有使用系統類加載器有使用自己的
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
//安全問題的判斷
acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
//調用
reload();
}
public void reload() {
//清除已加載的提供者緩存
providers.clear();
//延遲實例化和加載 (什麼時候用什麼時候加載) 調用內部類其構造方法
lookupIterator = new LazyIterator(service, loader);
}
private class LazyIterator
implements Iterator<S>
{
//文件名
Class<S> service;
//加載器
ClassLoader loader;
Enumeration<URL> configs = null;
Iterator<String> pending = null;
String nextName = null;
private LazyIterator(Class<S> service, ClassLoader loader) {
this.service = service;
this.loader = loader;
}
private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (configs == null) {
try {
//拼接對應的加載路徑 PREFIX 在serviceLoader中定義了"META-INF/services/";
//SPI規定了 所以加載的就是fullName = 其目錄下 +服務接口文件名
String fullName = PREFIX + service.getName();
if (loader == null)
//此時加載指定文件裏的類
configs = ClassLoader.getSystemResources(fullName);
else
//沒有加載器系統路徑
configs = loader.getResources(fullName);
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
}
//迭代掃描目錄
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return false;
}
//解析文件
pending = parse(service, configs.nextElement());
}
nextName = pending.next();
return true;
}
private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
//文件中對應的每一行二進制類名
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
//使用loader加載對應類名並不實例化
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service,
"Provider " + cn + " not found");
}
if (!service.isAssignableFrom(c)) {
fail(service,
"Provider " + cn + " not a subtype");
}
try {
//實例化
S p = service.cast(c.newInstance());
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service,
"Provider " + cn + " could not be instantiated",
x);
}
throw new Error(); // This cannot happen
}
//下一行 hasNext next remove 用於迭代
public boolean hasNext() {
if (acc == null) {
return hasNextService();
} else {
PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
public Boolean run() { return hasNextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}
//當前行數據
public S next() {
if (acc == null) {
return nextService();
} else {
PrivilegedAction<S> action = new PrivilegedAction<S>() {
public S run() { return nextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}
public void remove() {
throw new UnsupportedOperationException();
}
}
當設置上下文爲擴展類加載器,此時驅動在當前目錄下 此時就無法找到對應的jar 此時驅動裏的實現類就無法加載
public class MyTest26 {
public static void main(String[] args) {
//設置上下文爲擴展類加載器
Thread.currentThread().setContextClassLoader(MyTest23.class.getClassLoader().getParent());
//Driver驅動連接規範的一些信息
//load()等價與load(Class<S> service, ClassLoader loader)
//load(需要加載服務的class,Thread.currentThread().getContextClassLoader())
ServiceLoader<Driver> loader = ServiceLoader.load(Driver.class);
Iterator<Driver> iterator = loader.iterator();
while (iterator.hasNext()) {
Driver driver = iterator.next();
System.out.println("driver: " + driver.getClass() + ",loader: " + driver.getClass().getClassLoader());
}
System.out.println("當前線程上下文類加載器: " + Thread.currentThread().getContextClassLoader());
System.out.println("ServiceLoader的類加載器:" + loader.getClass().getClassLoader());
}
}
當前線程上下文類加載器: sun.misc.Launcher$ExtClassLoader@1eb44e46
ServiceLoader的類加載器:null
問題分析P31
public class MyTest27 {
public static void main(String[] args) throws Exception {
Class.forName("com.mysql.jdbc.Driver");
DriverManager.getConnection("jdbc:mysql://localhost:3306/mytestdb","username","password");
}
}
注意的點: 此時判斷是否是同一個類加載器加載的(命名空間) 避免找不到對應的類