雙親委派模型以及SpringFactoriesLoader詳解(最全最簡單的介紹)

文章目錄

前言

前面我們介紹了JavaConfig和常用的Annotation,這一篇文章我們來聊聊SpringFactoriesLoader,在講SpringFactoriesLoader之前我會先說到JVM的類加載器以及雙親委派模型。閑話少敘,直入主題。

類加載的過程

大致的步驟分為如下幾步:

  1. 加載:使用類加載器從不同的地方加載二進(jìn)制流到方法區(qū)
  2. 校驗(yàn):為了確保Class文件的字節(jié)流中包含的信息符合《Java虛擬機(jī)規(guī)范》的全部約束要求。
  3. 準(zhǔn)備:在方法區(qū)為靜態(tài)變量分配內(nèi)存,并初始化默認(rèn)值
  4. 解析:將符號引用替換成直接引用,(符號引用以一組符號來描述所引用的目標(biāo),符號可以是任何形式的字面量,只要使用時(shí)能夠無歧義的定位到目標(biāo)即可。直接引用可以是 1. 直接指向目標(biāo)的指針、類變量、類方法;2、相對偏移量;3、一個(gè)能間接定位到目標(biāo)的句柄。)
  5. 初始化:根據(jù)靜態(tài)變量的賦值語法和靜態(tài)代碼塊語法,生成一個(gè)初始化方法并執(zhí)行。

類加載器

JVM一共有三種類加載器,分別是:

  1. 啟動類加載器(BootstrapClassLoader)加載Java核心類庫(%java.home%lib下面的核心類庫 或 -Xbootclasspath選項(xiàng)指定的jar包);
  2. 擴(kuò)展類加載器(ExtClassLoader)加載擴(kuò)展類庫(%java.home%/lib/ext或者由系統(tǒng)變量-Djava.ext.dir指定位置中的類庫 );
  3. 應(yīng)用類加載器(AppClassLoader)加載應(yīng)用的類路徑(用戶類路徑(java -classpath或-Djava.class.path變量所指的目錄)下的類庫。
    類的繼承關(guān)系如下圖所示:
    在這里插入圖片描述
    JVM通過雙親委派模型進(jìn)行類的加載,我們可以通過繼承java.lang.classLoader實(shí)現(xiàn)自己的類加載器。

何為雙親委派模型

當(dāng)一個(gè)類加載器收到類加載任務(wù)時(shí),會先交給自己的父加載器去完成,因此最終的加載任務(wù)都會傳遞到最頂層的BootstrapClassLoader(啟動類加載器),只有當(dāng)父加載器無法完成加載任務(wù)時(shí),才會嘗試自己來加載。事實(shí)上,大多數(shù)情況下,越基礎(chǔ)的類由越上層的加載器進(jìn)行加載。
其加載流程圖如下:
在這里插入圖片描述














下面就是ClassLoader類的loadClass方法

ClassLoader類的loadClass方法

    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
			//首先,檢查該類是否已經(jīng)被加載,如果從JVM緩存中找到該類,則直接返回。
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
					//遵循雙親委派的模型,首先通過遞歸從父加載器開始找
					//直到父類加載器是BootstrapClassLoader為止
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) { }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
					//如果還找不到,則嘗試通過findClass方法去尋找
					//findClass是留給開發(fā)者自己實(shí)現(xiàn)的,也就是說自定義類加載器時(shí),
					//重寫此方法即可。
                    c = findClass(name);
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

采用雙親委派的一個(gè)好處主要有如下兩點(diǎn):

  1. 防止類被重復(fù)加載
    Java類隨著它的類加載器一起具備了一種帶有優(yōu)先級的層次關(guān)系,通過這種層次關(guān)系可以避免類被重復(fù)加載, 當(dāng)父類已經(jīng)加載了該類時(shí),子類就不會再加載一次。保證了使用不同類加載器最終得到的是同一個(gè)對象。
  2. 保證核心庫的類型安全
    Java核心api中定義的類不會被隨意替換,假設(shè)通過網(wǎng)絡(luò)傳遞一個(gè)名為java.lang.Integer的類,通過雙親委托模式傳遞到啟動類加載器,而啟動類加載器在核心Java API發(fā)現(xiàn)這個(gè)名字的類,發(fā)現(xiàn)該類已被加載,并不會重新加載網(wǎng)絡(luò)傳遞的過來的java.lang.Integer,而直接返回已加載過的Integer.class,這樣便可以防止核心API庫被隨意篡改。

雙親委派模型存在的問題

使用雙親委派模型也存在一些問題,例如:Java提供了很多服務(wù)提供者接口(ServiceProvinderInterface,SPI),允許第三方為這些接口提供實(shí)現(xiàn),常見的SPI有JDBC,JNDI等,這些SPI的接口由核心類庫提供,卻由第三方實(shí)現(xiàn),這樣就存在了一個(gè)問題:SPI的接口是Java核心庫的一部分,是由BootStrapClassLoader加載的;SPI實(shí)現(xiàn)的Java類一般是由AppClassLoader來加載的。BootStrapClassLoader是無法找到SPI的實(shí)現(xiàn)類的。因?yàn)樗患虞dJava的核心庫,它不能代理給AppClassLoader,因?yàn)樗亲铐攲拥念惣虞d器,也就是說,雙親委派模型并不能解決這個(gè)問題。那么如何解決這個(gè)問題呢?

解決辦法

線程上下文加載器(ContextClassLoader)正好解決了這個(gè)問題。從名稱上看,可能會誤解為它是一種新的類加載器,實(shí)際上,它僅僅是Thread類的一個(gè)變量而已,可以通過setContextClassLoader(ClassLoadercl) 和getContextClassLoader()來設(shè)置和獲取該對象,如果不做任何的設(shè)置。Java應(yīng)用的線程上下文類加載器默認(rèn)就是AppClassLoader。在核心類庫使用SPI接口時(shí),傳遞的類加載器使用線程上下文類加載器。就可以成功的加載到SPI實(shí)現(xiàn)的類。線程上下文類加載器在很多SPI的實(shí)現(xiàn)中都會用到。

以JDBC驅(qū)動管理為例

mysql-connector-java-6.0.6.jar 下的META-INF/services目錄下有一個(gè)以 接口全限定名 (java.sql.Driver)為命名的文件,內(nèi)容為實(shí)現(xiàn)類的全限定名。
在這里插入圖片描述
主程序通過java.util.ServiceLoader動態(tài)裝載實(shí)現(xiàn)模塊,它通過掃描META-INF/services目錄下的配置文件找到實(shí)現(xiàn)類的全限定名,把類加載到JVM中。需要注意的是SPI的實(shí)現(xiàn)類必須攜帶一個(gè)不帶參數(shù)的構(gòu)造方法,用于反射生成實(shí)例。如下:

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    //
    // Register ourselves with the DriverManager
    //
    static {
        try {
            java.sql.DriverManager.registerDriver(new Driver());
        } catch (SQLException E) {
            throw new RuntimeException("Can't register driver!");
        }
    }

    /**
     * Construct a new driver and register it with DriverManager
     *  
     */
    public Driver() throws SQLException {
        // Required for Class.forName().newInstance()
    }
}

ServiceLoader 類裝載實(shí)現(xiàn)模塊的代碼如下:

public final class ServiceLoader<S>
    implements Iterable<S>
{
	    private static final String PREFIX = "META-INF/services/";
		    // The class loader used to locate, load, and instantiate providers
    private final ClassLoader loader;
		 /**
     * Creates a new service loader for the given service type, using the
     * current thread's {@linkplain java.lang.Thread#getContextClassLoader
     * context class loader}.
     * 
     * */
	    public static <S> ServiceLoader<S> load(Class<S> service) {
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }
}

加載資源

類加載器除了加載Class外,還有一個(gè)非常重要的功能,就是加載資源,
它可以從jar包中讀取任何資源文件,比如:ClassLoader.getResource(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);
    }

它的邏輯其實(shí)跟類加載的邏輯是一樣的。首先判斷父類加載器是否為空,如果不為空則委托父類加載器執(zhí)行資源查找任務(wù),直到到達(dá)BootstrapClassLoader,只有當(dāng)父類加載器找不到時(shí),最后才輪到自己查找。而不同的類加載器負(fù)責(zé)掃描不同路徑下的jar包。就如同加載class一樣,最后會掃描所有的jar包,找到符合條件的資源文件。findResources(name)方法會遍歷其負(fù)責(zé)加載的所有jar包。找到j(luò)ar包中名稱為name的資源文件,這里的資源可以是任何文件,甚至是.class文件。比如下面的實(shí)例:用于查找String.class文件。

    //尋找String.class文件
    public static void main(String[] args) throws IOException {
        String name = "java/lang/String.class";
        Enumeration<URL> urls = Thread.currentThread().getContextClassLoader().getResources(name);
        while (urls.hasMoreElements()) {
            URL url = urls.nextElement();
            System.out.println(url.toString());
        }
    }

運(yùn)行得到如下結(jié)果:

$JAVA_HOME/jre/lib/rt.jar!/java/lang/String.class

SpringFactoriesLoader詳解

說完了類加載器,以及雙親委派模型還有資源文件的查找,下面就開始介紹我們本篇文章的真正主角,SpringFactoriesLoader 它本質(zhì)上屬于Spring框架私有的一種擴(kuò)展方案,類似于SPI,Spring Boot在Spring基礎(chǔ)上的很多核心的功能都是基于此。根據(jù)資源文件的URL,就可以構(gòu)造相應(yīng)的文件來讀取資源內(nèi)容。

	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();
				//根據(jù)資源文件URL解析properties文件
				Properties properties = PropertiesLoaderUtils.loadProperties(new UrlResource(url));
				String factoryClassNames = properties.getProperty(factoryClassName);	
					//組裝并返回	
result.addAll(Arrays.asList(StringUtils.commaDelimitedListToStringArray(factoryClassNames)));
			}
			return result;
		}
	}

有了前面關(guān)于ClassLoader的知識鋪墊,再來看上面的代碼就簡單了。首先從classpath下每個(gè)jar包下搜尋文件名是META-INF/spring.factories的配置文件,然后將解析properties文件,找到指定名稱的配置后返回,需要注意的是,這里不僅僅是在classpath路徑下查找,會掃描所有路徑下的jar包,只不過這個(gè)文件只會在classpath下的jar包中。簡單看下spring.factories吧。

// 來? org.springframework.boot.autoconfigure下的META-INF/spring.factories
//EnableAutoConfiguration后文會講到,它用于開啟Spring Boot自動配置功能
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\

執(zhí)行loadFactoryNames((EnableAutoConfiguration.class,classLoader)后,得到對應(yīng)的一組@Configuration類,我們就可以通過反射實(shí)例化這些類然后注入到IOC容器中,最后容器里就有了一系列標(biāo)注了@Cofiguration的JavaConfig形式的配置類。

總結(jié)

本文首先介紹了JVM中的三種類加載器,分別是啟動類加載器,擴(kuò)展類加載器,以及應(yīng)用類加載器。然后說到了雙親委派模型以及它的缺點(diǎn)。根據(jù)它的缺點(diǎn)引出了線程上下文加載器(ContextClassLoader) 以及他在SPI的實(shí)現(xiàn)上的運(yùn)用。最后就是詳細(xì)介紹了SpringFactoriesLoader的實(shí)現(xiàn)原理。





作者:碼農(nóng)飛哥

微信公眾號:碼農(nóng)飛哥