使用SPI解耦你的實現類


什么是SPI機制

最近我建了另一個文章分類,用於擴展JDK中一些重要但不常用的功能。

SPI,全名Service Provider Interface,是一種服務發現機制。它可以看成是一種針對接口實現類的解耦方案。我們只需要采用配置文件方式配置好接口的實現類,就可以利用SPI機制去加載到它們了,當我們需要修改實現類時,改改配置文件就可以了,而不需要去改代碼。

當然,有的同學可能會問,spring也可以做接口實現類的解耦,是不是SPI就沒用了呢?雖然兩者都可以達到相同的目的,但是不一定所有應用都可以引入spring框架,例如JDBC自動發現驅動並注冊,它就是采用SPI機制,它就不大可能引入spring來解耦接口實現類。另外,druiddubbo等都采用了SPI機制。

怎么使用SPI

需求

利用SPI機制加載用戶服務接口的實現類並測試。

工程環境

JDK:1.8.0_201

maven:3.6.1

IDE:eclipse 4.12

主要步驟

  1. 編寫用戶服務類接口和實現類;
  2. classpath路徑下的META-INF/services文件夾下配置好接口的實現類;
  3. 利用SPI機制加載接口實現類並測試。

創建項目

項目類型Maven Project,打包方式jar

引入依賴

        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>

編寫用戶服務類接口

路徑:cn.zzs.spi

public interface UserService {
	void save();
}

編寫接口實現類

路徑:cn.zzs.spi。這里就簡單實現就好了。

public class UserServiceImpl1 implements UserService {

	@Override
	public void save() {
		System.err.println("執行服務1的save方法");
	}
}
// ------------------------
public class UserServiceImpl2 implements UserService {

	@Override
	public void save() {
		System.err.println("執行服務2的save方法");
	}
}

配置接口文件

resources路徑下創建META-INF/services文件夾,並以UserService的全限定類名為文件名,創建一個文件。如圖所示。

UserService接口實現類配置文件

文件中寫入接口實現類的全限定類名,多個用換行符隔開。

cn.zzs.spi.UserServiceImpl1
cn.zzs.spi.UserServiceImpl2

編寫測試方法

路徑:test下的cn.zzs.spi。如果實際項目中配置了比較多的接口文件,可以考慮抽取工具類。

public class UserServiceTest {

	@Test
	public void test() {
        // 1. 創建一個ServiceLoader對象
        ServiceLoader<UserService> userServiceLoader = ServiceLoader.load(UserService.class);
        // 2. 創建一個迭代器
        Iterator<UserService> userServiceIterator = userServiceLoader.iterator();
        // 3. 加載配置文件並實例化接口實現類
        while(userServiceIterator.hasNext()) {
        	UserService userService = userServiceIterator.next();
        	userService.save();
        	System.out.println("==================");
        }
	}
}

測試結果

執行服務1的save方法
==================
執行服務2的save方法
==================

SPI在JDBC中的應用

本文以mysql 8.0.15版本的驅動來說明。首先,當我們調用Class.forName("com.mysql.cj.jdbc.Driver")時,會去執行這個類的靜態代碼塊,在靜態代碼塊中就會完成驅動注冊。

    static {
        try {
            //靜態代碼塊中注冊當前驅動
            java.sql.DriverManager.registerDriver(new Driver());
        } catch (SQLException E) {
            throw new RuntimeException("Can't register driver!");
        }
    }

JDK6后不再需要Class.forName(driver)也能注冊驅動。因為從JDK6開始,DriverManager增加了以下靜態代碼塊,當類被加載時會執行static代碼塊的loadInitialDrivers方法。

而這個方法會通過查詢系統參數(jdbc.drivers)和SPI機制兩種方式去加載數據庫驅動。

注意:考慮篇幅,以下代碼經過修改,僅保留所需部分。

    static {
        loadInitialDrivers();
    }
    //這個方法通過兩個渠道加載所有數據庫驅動:
    //1. 查詢系統參數jdbc.drivers獲得數據驅動類名
    //2. SPI機制
    private static void loadInitialDrivers() {
        //通過系統參數jdbc.drivers讀取數據庫驅動的全路徑名。該參數可以通過啟動參數來設置,其實引入SPI機制后這一步好像沒什么意義了。
        String drivers;
        try {
            drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
                public String run() {
                    return System.getProperty("jdbc.drivers");
                }
            });
        } catch (Exception ex) {
            drivers = null;
        }
        //使用SPI機制加載驅動
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
                //讀取META-INF/services/java.sql.Driver文件的類全路徑名。
                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();
                //加載並初始化類
                try{
                    while(driversIterator.hasNext()) {
                    	// 這里才會去實例化驅動
                        driversIterator.next();
                    }
                } catch(Throwable t) {
                // Do nothing
                }
                return null;
            }
        });

        if (drivers == null || drivers.equals("")) {
            return;
        }
        //加載jdbc.drivers參數配置的實現類
        String[] driversList = drivers.split(":");
        for (String aDriver : driversList) {
            try {
                Class.forName(aDriver, true,
                        ClassLoader.getSystemClassLoader());
            } catch (Exception ex) {
                println("DriverManager.Initialize: load failed: " + ex);
            }
        }
    }

mysql的驅動包中,我們可以看到SPI的配置文件。

Driver接口實現類配置文件

源碼分析

本文將根據測試例子中方法的調用順序來分析。

	@Test
	public void test() {
        // 1. 創建一個ServiceLoader對象
        ServiceLoader<UserService> userServiceLoader = ServiceLoader.load(UserService.class);
        // 2. 創建一個迭代器
        Iterator<UserService> userServiceIterator = userServiceLoader.iterator();
        // 3. 加載配置文件並實例化接口實現類
        while(userServiceIterator.hasNext()) {
        	UserService userService = userServiceIterator.next();
        	userService.save();
        	System.out.println("==================");
        }
	}

注意:考慮篇幅,以下代碼經過修改,僅保留所需部分。

創建一個ServiceLoader

我們從load(Class service)方法開始分析,可以看到,調用這個方法時還不會去加載配置文件和初始化接口實現類。因為SPI采用延遲加載的方式,只有去調用hasNext()才會去加載配置文件,調用next()才會去實例化對象。

    public static <S> ServiceLoader<S> load(Class<S> service) {
    	// 獲得當前線程上下文的類加載器
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }
    public static <S> ServiceLoader<S> load(Class<S> service,
                                            ClassLoader loader)
    {	
    	// 創建一個ServiceLoader對象
        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();
    }
    // 存放接口實現類對象。形式為全限定類名=實例對象
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
    // 迭代器,有加載和實例化接口實現類的方法
    private LazyIterator lookupIterator;
    public void reload() {
    	// 清空存放的接口實現類對象
        providers.clear();
        // 創建一個LazyIterator
        lookupIterator = new LazyIterator(service, loader);
    }
    // LazyIterator是ServiceLoader的內部類
    private class LazyIterator implements Iterator<S> {
        private LazyIterator(Class<S> service, ClassLoader loader) {
            this.service = service;
            this.loader = loader;
        }
	}

創建一個迭代器

因為SPI機制采用了延遲加載的方式,所以在沒有調用next()之前,providers會是一個空的Map,也就是說以下的knownProviders也會是一個空的迭代器,所以,這個時候都必須去調用lookupIterator的方法,本文討論的正是這種情況。

    public Iterator<S> iterator() {
        return new Iterator<S>() {
			// providers的迭代器,一般為空
            Iterator<Map.Entry<String,S>> knownProviders
                = providers.entrySet().iterator();
			
            public boolean hasNext() {
                if (knownProviders.hasNext())
                    return true;
                return lookupIterator.hasNext();
            }

            public S next() {
                if (knownProviders.hasNext())
                    return knownProviders.next().getValue();
                return lookupIterator.next();
            }

            public void remove() {
                throw new UnsupportedOperationException();
            }

        };
    }

加載配置文件

前面已經提到,當調用hasNext()時才會去加載配置文件。那么,我們直接看LazyIteratorhasNext()方法

	// 接口類型
  	Class<S> service;
	// 類加載器
	ClassLoader loader;
	// 配置文件列表,一般只有一個
	Enumeration<URL> configs = null;
	// 所有實現類全限定類名的迭代器
	Iterator<String> pending = null;
	// 下一個實現類全限定類名
	String nextName = null;
	public boolean hasNext() {
		return hasNextService();
	}
	private boolean hasNextService() {
		// 判斷是否有下一個實現類全限定類名,有的話直接返回true
		// 第一次調用這個方法nextName肯定是null的
		if(nextName != null) {
			return true;
		}
		// 下面就是加載配置文件了
		if(configs == null) {
			// 本文例子中:fullName = META-INF/services/cn.zzs.spi.UserService
			String fullName = PREFIX + service.getName();
			if(loader == null)
				configs = ClassLoader.getSystemResources(fullName);
			else
				configs = loader.getResources(fullName);
		}
		// pending是所有實現類全限定類名的迭代器,此時是空
		while((pending == null) || !pending.hasNext()) {
			// 如果文件中沒有配置實現類,直接返回false
			if(!configs.hasMoreElements()) {
				return false;
			}
			// 解析配置文件,並初始化pending迭代器
			pending = parse(service, configs.nextElement());
		}
		// 將第一個實現類的全限定類名賦值給nextName
		nextName = pending.next();
		return true;
	}

解析的過程就是簡單的IO操作,這里就不再擴展了。

實例化接口實現類

前面已經提到,當調用next()時才會去實例化接口實現類。那么,我們直接看LazyIteratornext()方法。

    public S next() {
    	return nextService();
    }

    private S nextService() {
    	// 判斷是否有下一個接口實現類。因為前面已經有nextName,所以直接返回true
        if (!hasNextService())
            throw new NoSuchElementException();
        // 獲得下一個接口實現類的全限定類名
        String cn = nextName;
        // 將nextName置空,這樣下次調用hasNext()就會重新賦值nextName
        nextName = null;
        Class<?> c = null;
        // 加載接口實現類
        c = Class.forName(cn, false, loader);
        // 判斷是否是指定接口的實現類
        if (!service.isAssignableFrom(c)) {
            fail(service,"Provider " + cn  + " not a subtype");
        }
        // 轉化為指定類型
        S p = service.cast(c.newInstance());
        // 放入providers的Map中
        // 前面提到過,只有調用了next()方法,這個Map才會放入元素
        providers.put(cn, p);
        return p;
    }

以上,SPI的源碼基本分析完。

參考資料

-深入理解SPI機制

相關源碼請移步:https://github.com/ZhangZiSheng001/01-spi-demo

本文為原創文章,轉載請附上原文出處鏈接:https://www.cnblogs.com/ZhangZiSheng001/p/12114744.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM