Original address: Comparison between JDK's own SPI mechanism and Dubbo's SPI mechanism
1. JDK has its own SPI mechanism
(1) Introduction
The full name of SPI is Service Provider Interface, which is a service discovery mechanism. The essence of SPI is to configure the fully qualified name of the interface implementation class in the file, and the service loader reads the configuration file and loads the implementation class. This allows you to dynamically replace implementation classes for interfaces at run time. Because of this feature, we can provide extended functions for our program through SPI mechanism without changing the interface source code.
(2) Writing examples
After creating a maven project, define an interface first
public interface UploadCDN { void upload (String url); }
Next, define two implementation classes
public class QiyiCDN implements UploadCDN { @Override public void upload (String url) { System.out.println( "upload to Qiyi"); } } public class ChinaNetCDN implements UploadCDN { @Override public void upload (String url) { System.out.println( "upload to ChinaNetCDN"); } }
Create a text file under resource/META-INF/services. The file name is the fully qualified name of the interface: com.lzq.spidemo.service.UploadCDN, and write the following contents
com.lzq.spidemo.service.Impl.QiyiCDN com.lzq.spidemo.service.Impl.ChinaNetCDN
Start writing test code
public class Demo { public static void main (String[] args) { //Note: the directory "META-INF/services /" is written in ServiceLoader ServiceLoader<UploadCDN> uploadCDN = ServiceLoader.load(UploadCDN.class); for (UploadCDN u : uploadCDN) { u.upload( "filePath"); } } }
As you can see, all implementation classes will create new instances and call corresponding methods.
(3) ServiceLoader source code
The load function passes the class object of the class loader and interface of the current thread into the ServiceLoader object
public static <S> ServiceLoader<S> load (Class<S> service) { ClassLoader cl = Thread.currentThread().getContextClassLoader(); return ServiceLoader.load(service, cl); }
After the ServiceLoader object is created, an internal iterator object lazyitterer is initialized
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 { //Find the text file of the corresponding interface, and then parse all the implementation classes recorded in it 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; } }
2.Dubbo's SPI mechanism
(1) Introduction
dubbo's spi is extended on the basis of jdk's spi. You can specify or an implementation class
(2) Instance
First define an interface LoadBalance, where @ SPI acts on the interface class to specify the default implementation class ID.
@Adaptive table name this method will be dynamically implemented by the agent.
@SPI("demo") public interface LoadBalance { @Adaptive void Hello (); }
Define two implementation classes
public class DemoLoadbalance implements LoadBalance { @Override public void Hello () { System.out.println( "this is demo balance"); } } public class TestLoadBalance implements LoadBalance { @Override public void Hello () { System.out.println( "this is test balance"); } }
Create a text file under resource/META-INF/services. The file name is the fully qualified name of the interface:
com.lzq.dubbospidemo.service.LoadBalance, and write the following contents. Note that the writing method here is different from that of jdk, and the identification needs to be written in front
demo=com.lzq.dubbospidemo.service. impl.DemoLoadbalance test=com.lzq.dubbospidemo.service. impl.TestLoadBalance
Start test
public static void main (String[] args) { ExtensionLoader<LoadBalance> extensionLoader = ExtensionLoader.getExtensionLoader(LoadBalance.class); LoadBalance demoBalance = extensionLoader.getExtension( "demo"); demoBalance.Hello(); LoadBalance testBalance = extensionLoader.getExtension( "test"); testBalance.Hello(); LoadBalance balance = extensionLoader.getDefaultExtension(); balance.Hello(); }
The test results are shown in the figure
3. Source code analysis
Let's take a look at the process of getting the ExtensionLoader
public static <T> ExtensionLoader<T> getExtensionLoader (Class<T> type) { if (type == null) { //null parameter cannot be passed in throw new IllegalArgumentException( "Extension type == null"); } else if (!type.isInterface()) { //Interface required throw new IllegalArgumentException( "Extension type(" + type + ") is not interface!"); } else if (!withExtensionAnnotation(type)) { //SPI annotation required throw new IllegalArgumentException( "Extension type(" + type + ") is not extension, because WITHOUT @" + SPI.class.getSimpleName() + " Annotation!"); } else { //EXTENSION_LOADERS is a map object that can be accessed concurrently, which is equivalent to a cache //The following steps first try to get the loader object from the cache. If it does not exist in the cache, create a new loader object //Put it into the cache and get it from the cache again ExtensionLoader<T> loader = (ExtensionLoader)EXTENSION_LOADERS.get(type); if (loader == null) { EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader(type)); loader = (ExtensionLoader)EXTENSION_LOADERS.get(type); } return loader; } }
Let's take a look at the process of obtaining the concrete implementation class
public T getExtension (String name) { if (name != null && name.length() != 0) { //If the parameter is true, the default extension is used if ( "true".equals(name)) { return this.getDefaultExtension(); } else { //As above, first check whether the cache has a corresponding instance object. If not, create a new one, and then get it //Here, the Holder object holds a volatile value attribute to ensure visibility to all threads Holder<Object> holder = (Holder) this.cachedInstances.get(name); if (holder == null) { this.cachedInstances.putIfAbsent(name, new Holder()); holder = (Holder) this.cachedInstances.get(name); } Object instance = holder.get(); if (instance == null) { //Personally, I think the holder object here is mainly to reduce the granularity of locks. synchronized(holder) { //Check twice to get, so this is thread safe //This is a bit like the creation of lazy singleton mode instance = holder.get(); if (instance == null) { instance = this.createExtension(name); holder.set(instance); } } } return instance; } } else { throw new IllegalArgumentException( "Extension name == null"); } }
The process of creating an instance is as follows:
private T createExtension (String name) { //Here, we will first find the class object from the cache. If not, we will load all extension classes from the configuration file, and finally get the map of the extension name and the corresponding class. See the loadExtensionClasses function for details Class<?> clazz = (Class) this.getExtensionClasses().get(name); if (clazz == null) { throw this.findException(name); } else { try { T instance = EXTENSION_INSTANCES.get(clazz); if (instance == null) { EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance()); instance = EXTENSION_INSTANCES.get(clazz); } // Inject dependencies into the instance to implement IOC this.injectExtension(instance); Set<Class<?>> wrapperClasses = this.cachedWrapperClasses; Class wrapperClass; if (wrapperClasses != null && !wrapperClasses.isEmpty()) { // Pass the current instance as a parameter to the constructor of the Wrapper, and create the Wrapper instance through reflection. // Then inject dependencies into the Wrapper instance, and finally assign the Wrapper instance to the instance variable again //This is actually the implementation of AOP for( Iterator i$ = wrapperClasses.iterator(); i$.hasNext(); instance = this.injectExtension(wrapperClass.getConstructor( this.type).newInstance(instance))) { // The SPI instance (created according to the specified name) is injected through the construction method with parameters // After the injection is successful and the instance is created, the assembled Wrapper instance will be returned // In this way, when looping to the next Wrapper class, it actually injects the previous Wrapper class instance // This also explains why the post definition is executed first wrapperClass = (Class)i$.next(); } } return instance; } catch (Throwable var7) { throw new IllegalStateException( "Extension instance(name: " + name + ", class: " + this.type + ") could not be instantiated: " + var7.getMessage(), var7); } } } //Parse the SPI annotation and load the extension class from the directory of the configuration file private Map<String, Class<?>> loadExtensionClasses() { SPI defaultAnnotation = (SPI) this.type.getAnnotation(SPI.class); if (defaultAnnotation != null) { String value = defaultAnnotation.value(); if ((value = value.trim()).length() > 0) { String[] names = NAME_SEPARATOR.split(value); if (names.length > 1) { throw new IllegalStateException( "more than 1 default extension name on extension " + this.type.getName() + ": " + Arrays.toString(names)); } if (names.length == 1) { this.cachedDefaultName = names[ 0]; } } } Map<String, Class<?>> extensionClasses = new HashMap(); this.loadDirectory(extensionClasses, "META-INF/dubbo/internal/"); this.loadDirectory(extensionClasses, "META-INF/dubbo/"); this.loadDirectory(extensionClasses, "META-INF/services/"); return extensionClasses; }
The IOC here is implemented based on setter function injection dependency
private T injectExtension (T instance) { try { if ( this.objectFactory != null) { Method[] arr$ = instance.getClass().getMethods(); int len$ = arr$.length; for( int i$ = 0; i$ < len$; ++i$) { Method method = arr$[i$]; if (method.getName().startsWith( "set") && method.getParameterTypes().length == 1 && Modifier.isPublic(method.getModifiers())) { //Get setter method parameter type Class pt = method.getParameterTypes()[ 0]; try { String property = method.getName().length() > 3 ? method.getName().substring( 3, 4).toLowerCase() + method.getName().substring( 4) : ""; // Get dependent objects from ObjectFactory Object object = this.objectFactory.getExtension(pt, property); if (object != null) { // Set dependency by calling setter method through reflection method.invoke(instance, object); } } catch (Exception var9) { logger.error( "fail to inject via method " + method.getName() + " of interface " + this.type.getName() + ": " + var9.getMessage(), var9); } } } } } catch (Exception var10) { logger.error(var10.getMessage(), var10); } return instance; }
4. Talk about the implementation of AOP
reference resources Blog
In the above source code implementation, the processing of wapper class is involved. dubbo implements wapper based on wapper class. It judges whether a class is wapper. In fact, it judges whether the class contains a constructor whose parameter type is SPI interface type
private boolean isWrapperClass (Class<?> clazz) { try { // An attempt was made to get a constructor with a parameter type of SPI interface type clazz.getConstructor( this.type); return true; } catch (NoSuchMethodException var3) { return false; } }
At the same time, in the above loop code, it can be seen that when there are multiple wappers, the instance object injected by each wapper is actually the previous wapper. The following is an example
-
Similarly, create an interface class AopService
@SPI public interface AopService { @Adaptive void service (); }
-
Create two implementation classes
public class CppAopService implements AopService { @Override public void service () { System.out.println( "this is c++ aop service"); } } public class JavaAopService implements AopService { @Override public void service () { System.out.println( "this is java aop service"); } }
-
Create a wapper class to enhance the implementation class (AOP)
public class AopServiceWapper1 implements AopService { private AopService aopService; //You must have this constructor to be judged as a wapper class public AopServiceWapper1 (AopService service) { this.aopService = service; } @Override public void service () { System.out.println( "before wapper1"); aopService.service(); System.out.println( "after wapper1"); } } public class AopServiceWapper2 implements AopService { private AopService aopService; //You must have this constructor to be judged as a wapper class public AopServiceWapper2 (AopService service) { this.aopService = service; } @Override public void service () { System.out.println( "before wapper2"); aopService.service(); System.out.println( "after wapper2"); } }
-
Two wapper s need to be added to the configuration file
wapper1=com.lzq.dubboaopdemo.aopservice.Impl.AopServiceWapper1 wapper2=com.lzq.dubboaopdemo.aopservice.Impl.AopServiceWapper2 java=com.lzq.dubboaopdemo.aopservice.Impl.JavaAopService cpp=com.lzq.dubboaopdemo.aopservice.Impl.JavaAopService
-
Test
public static void main (String[] args) { ExtensionLoader<AopService> loader = ExtensionLoader.getExtensionLoader(AopService.class); AopService service = loader.getExtension( "java"); service.service(); }