一、從java類加載機制說起
類的加載指的是將類的.class文件中的二進制數據讀入到內存中,將其放在運行時數據區的方法區內,然后在堆區創建一個這個類的Java.lang.Class對象,用來封裝類在方法區類的對象。java中的類加載器負載加載來自文件系統、網絡或者其他來源的類文件。jvm的類加載器默認使用的是雙親委派模式。三種默認的類加載器Bootstrap ClassLoader、Extension ClassLoader和System ClassLoader(Application ClassLoader)每一個中類加載器都確定了從哪一些位置加載文件。於此同時我們也可以通過繼承java.lang.classloader實現自己的類加載器。
Bootstrap ClassLoader:負責加載JDK自帶的rt.jar包中的類文件,是所有類加載的父類。
Extension ClassLoader:負責加載java的擴展類庫從jre/lib/ect目錄或者java.ext.dirs系統屬性指定的目錄下加載類,是System ClassLoader的父類加載器。
System ClassLoader(APP ClassLoader):負責從classpath環境變量中加載類文件。
1、雙親委派模型
原理:當一個類加載器收到類加載任務時,會先交給自己的父加載器去完成,因此最終加載任務都會傳遞到最頂層的BootstrapClassLoader,只有當父加載器無法完成加載任務時,才會嘗試自己來加載。
具體:根據雙親委派模式,在加載類文件的時候,子類加載器首先將加載請求委托給它的父加載器,父加載器會檢測自己是否已經加載過類,如果已經加載則加載過程結束,如果沒有加載的話則請求繼續向上傳遞直Bootstrap ClassLoader。如果請求向上委托過程中,如果始終沒有檢測到該類已經加載,則Bootstrap ClassLoader開始嘗試從其對應路勁中加載該類文件,如果失敗則由子類加載器繼續嘗試加載,直至發起加載請求的子加載器為止。每個類加載器只能加載其對應的目錄中的class文件。
采用雙親委派模式可以保證類型加載的安全性,不管是哪個加載器加載這個類,最終都是委托給頂層的BootstrapClassLoader來加載的,只有父類無法加載自己猜嘗試加載,這樣就可以保證任何的類加載器最終得到的都是同樣一個Object對象。
protected Class<?> loadClass(String name, boolean resolve) { synchronized (getClassLoadingLock(name)) { // 首先,檢查該類是否已經被加載,如果從JVM緩存中找到該類,則直接返回
Class<?> c = findLoadedClass(name); if (c == null) { try { // 遵循雙親委派的模型,首先會通過遞歸從父加載器開始找, // 直到父類加載器是BootstrapClassLoader為止
if (parent != null) { c = parent.loadClass(name, false); } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) {} if (c == null) { // 如果還找不到,嘗試通過findClass方法去尋找 // findClass是留給開發者自己實現的,也就是說 // 自定義類加載器時,重寫此方法即可
c = findClass(name); } } if (resolve) { resolveClass(c); } return c; } }
2.雙親委派模型缺陷
在雙親委派模型中,子類加載器可以使用父類加載器已經加載的類,而父類加載器無法使用子類加載器已經加載的。這就導致了雙親委派模型並不能解決所有的類加載器問題。
案例:Java 提供了很多服務提供者接口(Service Provider Interface,SPI),允許第三方為這些接口提供實現。常見的 SPI 有 JDBC、JNDI、JAXP 等,這些SPI的接口由核心類庫提供,卻由第三方實現,這樣就存在一個問題:SPI 的接口是 Java 核心庫的一部分,是由BootstrapClassLoader加載的;SPI實現的Java類一般是由AppClassLoader來加載的。BootstrapClassLoader是無法找到 SPI 的實現類的,因為它只加載Java的核心庫。它也不能代理給AppClassLoader,因為它是最頂層的類加載器。也就是說,雙親委派模型並不能解決這個問題
3.使用線程上下文類加載器(ContextClassLoader)加載
如果不做任何的設置,Java應用的線程的上下文類加載器默認就是AppClassLoader。在核心類庫使用SPI接口時,傳遞的類加載器使用線程上下文類加載器,就可以成功的加載到SPI實現的類。線程上下文類加載器在很多SPI的實現中都會用到。
通常我們可以通過Thread.currentThread().getClassLoader()和Thread.currentThread().getContextClassLoader()獲取線程上下文類加載器。
4、使用類加載器加載資源文件,比如jar包
類加載器除了加載class外,還有一個非常重要功能,就是加載資源,它可以從jar包中讀取任何資源文件,比如,ClassLoader.getResources(String name)方法就是用於讀取jar包中的資源文件
//獲取資源的方法
public Enumeration<URL> getResources(String name) throws IOException { Enumeration<URL>[] tmp = (Enumeration<URL>[]) new Enumeration<?>[2]; if (parent != null) { tmp[0] = parent.getResources(name); } else { tmp[0] = getBootstrapResources(name); } tmp[1] = findResources(name); return new CompoundEnumeration<>(tmp); }
它的邏輯其實跟類加載的邏輯是一樣的,首先判斷父類加載器是否為空,不為空則委托父類加載器執行資源查找任務,直到BootstrapClassLoader,最后才輪到自己查找。而不同的類加載器負責掃描不同路徑下的jar包,就如同加載class一樣,最后會掃描所有的jar包,找到符合條件的資源文件。
// 使用線程上下文類加載器加載資源
public static void main(String[] args) throws Exception{ // Array.class的完整路徑
String name = "java/sql/Array.class"; Enumeration<URL> urls = Thread.currentThread().getContextClassLoader().getResources(name); while (urls.hasMoreElements()) { URL url = urls.nextElement(); System.out.println(url.toString()); } }
二、spring中SPI機制實現
1.SPI機制
(1)SPI思想
SPI的全名為Service Provider Interface.這個是針對廠商或者插件的。
SPI的思想:系統里抽象的各個模塊,往往有很多不同的實現方案,比如日志模塊的方案,xml解析模塊、jdbc模塊的方案等。面向的對象的設計里,我們一般推薦模塊之間基於接口編程,模塊之間不對實現類進行硬編碼。一旦代碼里涉及具體的實現類,就違反了可拔插的原則,如果需要替換一種實現,就需要修改代碼。為了實現在模塊裝配的時候能不在程序里動態指明,這就需要一種服務發現機制。java spi就是提供這樣的一個機制:為某個接口尋找服務實現的機制
(2)SPI約定
當服務的提供者,提供了服務接口的一種實現之后,在jar包的META-INF/services/目錄里同時創建一個以服務接口命名的文件。該文件里就是實現該服務接口的具體實現類。而當外部程序裝配這個模塊的時候,就能通過該jar包META-INF/services/里的配置文件找到具體的實現類名,並裝載實例化,完成模塊的注入。通過這個約定,就不需要把服務放在代碼中了,通過模塊被裝配的時候就可以發現服務類了。
2、SPI使用案例
common-logging apache最早提供的日志的門面接口。只有接口,沒有實現。具體方案由各提供商實現, 發現日志提供商是通過掃描 META-INF/services/org.apache.commons.logging.LogFactory配置文件,通過讀取該文件的內容找到日志提工商實現類。只要我們的日志實現里包含了這個文件,並在文件里制定 LogFactory工廠接口的實現類即可。
3、springboot中的類SPI擴展機制
在springboot的自動裝配過程中,最終會加載META-INF/spring.factories文件,而加載的過程是由SpringFactoriesLoader加載的。從CLASSPATH下的每個Jar包中搜尋所有META-INF/spring.factories配置文件,然后將解析properties文件,找到指定名稱的配置后返回。需要注意的是,其實這里不僅僅是會去ClassPath路徑下查找,會掃描所有路徑下的Jar包,只不過這個文件只會在Classpath下的jar包中。
public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories"; // spring.factories文件的格式為:key=value1,value2,value3 // 從所有的jar包中找到META-INF/spring.factories文件 // 然后從文件中解析出key=factoryClass類名稱的所有value值
public static List<String> loadFactoryNames(Class<?> factoryClass, ClassLoader classLoader) { String factoryClassName = factoryClass.getName(); // 取得資源文件的URL
Enumeration<URL> urls = (classLoader != null ? classLoader.getResources(FACTORIES_RESOURCE_LOCATION) : ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION)); List<String> result = new ArrayList<String>(); // 遍歷所有的URL
while (urls.hasMoreElements()) { URL url = urls.nextElement(); // 根據資源文件URL解析properties文件,得到對應的一組@Configuration類
Properties properties = PropertiesLoaderUtils.loadProperties(new UrlResource(url)); String factoryClassNames = properties.getProperty(factoryClassName); // 組裝數據,並返回
result.addAll(Arrays.asList(StringUtils.commaDelimitedListToStringArray(factoryClassNames))); } return result; }
總結:SPI的好處是避免寫死,調用者可以根據自己的需求調用不同的實現類 。其中SpringBoot start組件也是SPI實現的一種,原理其實是類加載相關的知識點。其中SpringBoot組件中的SPI主要是配置項這一塊,具體可以看下AutoConfigurationImportSelector這個實現類下面的源碼,源碼如下所示:SpringBoot的加載主要使用的是SpringFactoriesLoader這個加載器。在SpringBoot中,AutoConfigurationImportSelector這個類很重要
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) { String factoryTypeName = factoryType.getName(); // 返回的是一個一個的配置文件,such as (org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchAutoConfiguration) return loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList()); } private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) { // 先從緩存里面取數據 MultiValueMap<String, String> result = cache.get(classLoader); if (result != null) { return result; } try { Enumeration<URL> urls = (classLoader != null ? classLoader.getResources(FACTORIES_RESOURCE_LOCATION) : ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION)); result = new LinkedMultiValueMap<>(); while (urls.hasMoreElements()) { URL url = urls.nextElement(); UrlResource resource = new UrlResource(url); // 根據資源路徑獲取properties配置文件 Properties properties = PropertiesLoaderUtils.loadProperties(resource); for (Map.Entry<?, ?> entry : properties.entrySet()) { String factoryTypeName = ((String) entry.getKey()).trim(); for (String factoryImplementationName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) { result.add(factoryTypeName, factoryImplementationName.trim()); } } } // 放到緩存里面,防止重復加載 cache.put(classLoader, result); return result; } catch (IOException ex) { throw new IllegalArgumentException("Unable to load factories from location [" + FACTORIES_RESOURCE_LOCATION + "]", ex); } }
下面的代碼就是把一個一個的配置類進行實例化成對象,其中也是使用了反射的原理。代碼如下所示:
@SuppressWarnings("unchecked") private static <T> T instantiateFactory(String factoryImplementationName, Class<T> factoryType, ClassLoader classLoader) { try { Class<?> factoryImplementationClass = ClassUtils.forName(factoryImplementationName, classLoader); if (!factoryType.isAssignableFrom(factoryImplementationClass)) { throw new IllegalArgumentException( "Class [" + factoryImplementationName + "] is not assignable to factory type [" + factoryType.getName() + "]"); } return (T) ReflectionUtils.accessibleConstructor(factoryImplementationClass).newInstance(); } catch (Throwable ex) { throw new IllegalArgumentException( "Unable to instantiate factory class [" + factoryImplementationName + "] for factory type [" + factoryType.getName() + "]", ex); } }