? ? ? ? SPI 即 Service Provider Interface ,可以理解為專門提供給服務提供者或者擴展框架功能的開發者去使用的一個接口。SPI 將服務接口和具體的服務實現分離開來,將服務調用方和服務實現者解耦,能夠提升程序的擴展性、可維護性。修改或者替換服務實現并不需要修改調用方。很多框架都使用了 Java 的 SPI 機制,比如:Spring 框架、數據庫加載驅動、日志接口、以及 Dubbo 的擴展實現等等。參考Dubbo的SPI機制,來實現本RPC框架的SPI部分。
? ? ? ? 舉個例子,client端在與server端進行通信時,需要對消息進行序列化。序列化時可以使用序列化算法有很多,包括Hessian、Kryo、ProtoStuff。系統的需求是根據消息中的序列化算法名稱來調用相關序列化算法對應的類中的方法來進行序列化與反序列化,加之為了便于擴展,需要使用SPI來進行解耦。
? ? ? ? SPI的使用方式如下:
Serializer serializer = ExtensionLoader.getExtensionLoader(Serializer.class).getExtension(codecName);
? ? ? ? codecName是序列化算法名稱,需要根據該名稱加載出對應的類。
private final Class<?> type;private ExtensionLoader(Class<?> type) {this.type = type;}// 每個SPI接口都有自身的ExtensionLoaderpublic static <S> ExtensionLoader<S> getExtensionLoader(Class<S> type) {if (type == null) {throw new IllegalArgumentException("Extension type should not be null.");}if (!type.isInterface()) {throw new IllegalArgumentException("Extension type must be an interface.");}if (type.getAnnotation(SPI.class) == null) {throw new IllegalArgumentException("Extension type must be annotated by @SPI");}// firstly get from cache, if not hit, create oneExtensionLoader<S> extensionLoader = (ExtensionLoader<S>) EXTENSION_LOADERS.get(type);if (extensionLoader == null) {EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<S>(type));extensionLoader = (ExtensionLoader<S>) EXTENSION_LOADERS.get(type);}return extensionLoader;}
? ? ? ? 每個SPI接口都有自身的ExtensionLoader,調用getExtensionLoader時,首先會進行一系列的合法檢查操作,之后會嘗試獲取該接口的ExtensionLoader,先嘗試本地緩存CHM中獲取,獲取不到的話再創建Loader對象。
? ? ? ? 之后通過getExtension獲取實例,實例也進行了本地緩存,緩存中沒有的話再創建實例。
private final Map<String, Holder<Object>> cachedInstances = new ConcurrentHashMap<>();public T getExtension(String name) {if (StringUtil.isBlank(name)) {throw new IllegalArgumentException("Extension name should not be null or empty.");}// firstly get from cache, if not hit, create one// 緩存holderHolder<Object> holder = cachedInstances.get(name);if (holder == null) {cachedInstances.putIfAbsent(name, new Holder<>());holder = cachedInstances.get(name);}// create a singleton if no instance exists// holder為空,雙重檢查鎖創建示例Object instance = holder.get();if (instance == null) {synchronized (holder) {instance = holder.get();if (instance == null) {instance = createExtension(name);holder.set(instance);}}}return (T) instance;}
? ? ? ? ? 獲取到類的Class對象后可以通過反射的方式創建此對象。?
// 緩存 private static final Map<Class<?>, Object> EXTENSION_INSTANCES = new ConcurrentHashMap<>();private T createExtension(String name) {// load all extension classes of type T from file and get specific one by name// SPI接口對應的實現類,其標識名與class文件的映射,根據標識名獲取classClass<?> clazz = getExtensionClasses().get(name);if (clazz == null) {throw new RuntimeException("No such extension of name " + name);}T instance = (T) EXTENSION_INSTANCES.get(clazz);if (instance == null) {try {// 緩存中不存在,則創建實例EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());instance = (T) EXTENSION_INSTANCES.get(clazz);} catch (Exception e) {log.error(e.getMessage());}}return instance;}
? ? ? ? ?關鍵是獲取Class對象的過程?,即getExtensionCalsses方法:
// 該SPI接口所有實現類的標識與其Class對象的緩存private final Holder<Map<String, Class<?>>> cachedClasses = new Holder<>(); private static final String SERVICE_DIRECTORY = "META-INF/extensions/"; private Map<String, Class<?>> getExtensionClasses() {// get the loaded extension class from the cache// 根據Interface實現類的類名獲取對應類的緩存Map<String, Class<?>> classes = cachedClasses.get();// double checkif (classes == null) {synchronized (cachedClasses) {classes = cachedClasses.get();if (classes == null) {classes = new HashMap<>();// load all extensions from our extensions directoryloadDirectory(classes);// 將Map集合存儲在Holder中進行緩存cachedClasses.set(classes);}}}return classes;}private void loadDirectory(Map<String, Class<?>> extensionClasses) {// 固定路徑下的文件,SPI接口的類名作為文件名,在此文件中規定需要加載的實現類String fileName = ExtensionLoader.SERVICE_DIRECTORY + type.getName();try {Enumeration<URL> urls;// 系統類加載器,它能夠加載用戶類路徑(ClassPath)上的類和資源。對于SPI機制尤為重要,因為SPI的實現類通常是由應用程序提供并放置在應用程序的類路徑下的ClassLoader classLoader = ExtensionLoader.class.getClassLoader();// 獲取當前類加載器加載的URL資源,文件名確定一般urls是唯一的urls = classLoader.getResources(fileName);if (urls != null) {while (urls.hasMoreElements()) {URL resourceUrl = urls.nextElement();// 使用classLoader加載資源,資源目標在resourceUrl下,加載后的class存儲在extensionClasses Map集合當中loadResource(extensionClasses, classLoader, resourceUrl);}}} catch (IOException e) {log.error(e.getMessage());}}private void loadResource(Map<String, Class<?>> extensionClasses, ClassLoader classLoader, URL resourceUrl) {try (BufferedReader reader = new BufferedReader(new InputStreamReader(resourceUrl.openStream(), UTF_8))) {String line;// read every line// #是注釋,截取注釋之前的部分while ((line = reader.readLine()) != null) {// get index of commentfinal int ci = line.indexOf('#');if (ci >= 0) {// string after # is comment so we ignore itline = line.substring(0, ci);}line = line.trim();if (line.length() > 0) {try {final int ei = line.indexOf('=');// 標識與類名String name = line.substring(0, ei).trim();String clazzName = line.substring(ei + 1).trim();// our SPI use key-value pair so both of them must not be emptyif (name.length() > 0 && clazzName.length() > 0) {// 加載類Class<?> clazz = classLoader.loadClass(clazzName);// 在map中保存extensionClasses.put(name, clazz);}} catch (ClassNotFoundException e) {log.error(e.getMessage());}}}} catch (IOException e) {log.error(e.getMessage());}}
kyro=github.javaguide.serialize.kyro.KryoSerializer
protostuff=github.javaguide.serialize.protostuff.ProtostuffSerializer
hessian=github.javaguide.serialize.hessian.HessianSerializer
? ? ? ?逐層的方法調用,實現了加載META-INF/extensions/路徑下對應SPI配置文件從而加載Class對象并獲取實例的過程,重要部分可參考注釋。
? ? ? ?需要注意META-INF/extensions/下的文件名需要與代碼里一致,代碼里指定的文件名是SPI接口類的全類名。文件里的內容也需要按照(實現類標識=實現類全類名)來編寫,這樣才能與代碼一致,程序才可以正確解析文件,并使用類加載器加載對應的Class。最后按照<實現類標識,實現類Class對象>進行緩存。
? ? ? ?由于(類標識,Class對象)、(Class對象,對象實例)、(類標識,對象實例)這三個緩存的存在,后續可以直接傳入標識獲取到對應類的實例,也優化了RPC框架的性能。
?