Comparison between JDK's own SPI mechanism and Dubbo's SPI mechanism

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

   
  1. public interface UploadCDN {
  2.     void upload (String url);
  3. }

Next, define two implementation classes

   
  1. public class QiyiCDN implements UploadCDN {
  2.     @Override
  3.     public void upload (String url) {
  4.        System.out.println( "upload to Qiyi");
  5.   }
  6. }
  7. public class ChinaNetCDN implements UploadCDN {
  8.     @Override
  9.     public void upload (String url) {
  10.        System.out.println( "upload to ChinaNetCDN");
  11.   }
  12. }

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

   
  1. com.lzq.spidemo.service.Impl.QiyiCDN
  2. com.lzq.spidemo.service.Impl.ChinaNetCDN

Start writing test code

   
  1. public class Demo {
  2.     public static void main (String[] args) {
  3.         //Note: the directory "META-INF/services /" is written in ServiceLoader
  4.        ServiceLoader<UploadCDN> uploadCDN = ServiceLoader.load(UploadCDN.class);
  5.         for (UploadCDN u : uploadCDN) {
  6.            u.upload( "filePath");
  7.       }
  8.   }
  9. }

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

   
  1. public static <S> ServiceLoader<S> load (Class<S> service) {
  2.     ClassLoader cl = Thread.currentThread().getContextClassLoader();
  3.     return ServiceLoader.load(service, cl);
  4. }

After the ServiceLoader object is created, an internal iterator object lazyitterer is initialized

   
  1. private class LazyIterator
  2.         implements Iterator<S>
  3.   {
  4.        Class<S> service;
  5.        ClassLoader loader;
  6.        Enumeration<URL> configs = null;
  7.        Iterator<String> pending = null;
  8.         String nextName = null;
  9.         private LazyIterator (Class<S> service, ClassLoader loader) {
  10.             this.service = service;
  11.             this.loader = loader;
  12.       }
  13.         private boolean hasNextService () {
  14.             if (nextName != null) {
  15.                 return true;
  16.           }
  17.             if (configs == null) {
  18.                 try {
  19.                     //Find the text file of the corresponding interface, and then parse all the implementation classes recorded in it
  20.                     String fullName = PREFIX + service.getName();
  21.                     if (loader == null)
  22.                        configs = ClassLoader.getSystemResources(fullName);
  23.                     else
  24.                         configs = loader.getResources(fullName);
  25.               } catch (IOException x) {
  26.                    fail(service, "Error locating configuration files", x);
  27.               }
  28.           }
  29.             while ((pending == null) || !pending.hasNext()) {
  30.                 if (!configs.hasMoreElements()) {
  31.                     return false;
  32.               }
  33.                pending = parse(service, configs.nextElement());
  34.           }
  35.            nextName = pending.next();
  36.             return true;
  37.       }
  38.      
  39.   }

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.

   
  1. @SPI("demo")
  2. public interface LoadBalance {
  3.     @Adaptive
  4.     void Hello ();
  5. }

Define two implementation classes

   
  1. public class DemoLoadbalance implements LoadBalance {
  2.     @Override
  3.     public void Hello () {
  4.        System.out.println( "this is demo balance");
  5.   }
  6. }
  7. public class TestLoadBalance implements LoadBalance {
  8.     @Override
  9.     public void Hello () {
  10.        System.out.println( "this is test balance");
  11.   }
  12. }

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

   
  1. demo=com.lzq.dubbospidemo.service. impl.DemoLoadbalance
  2. test=com.lzq.dubbospidemo.service. impl.TestLoadBalance

Start test

   
  1. public static void main (String[] args) {
  2. ExtensionLoader<LoadBalance> extensionLoader = ExtensionLoader.getExtensionLoader(LoadBalance.class);
  3. LoadBalance demoBalance = extensionLoader.getExtension( "demo");
  4. demoBalance.Hello();
  5. LoadBalance testBalance = extensionLoader.getExtension( "test");
  6. testBalance.Hello();
  7. LoadBalance balance = extensionLoader.getDefaultExtension();
  8. balance.Hello();
  9. }

The test results are shown in the figure

3. Source code analysis

Let's take a look at the process of getting the ExtensionLoader

   
  1. public static <T> ExtensionLoader<T> getExtensionLoader (Class<T> type) {
  2. if (type == null) {
  3. //null parameter cannot be passed in
  4. throw new IllegalArgumentException( "Extension type == null");
  5. } else if (!type.isInterface()) {
  6. //Interface required
  7. throw new IllegalArgumentException( "Extension type(" + type + ") is not interface!");
  8. } else if (!withExtensionAnnotation(type)) {
  9. //SPI annotation required
  10. throw new IllegalArgumentException( "Extension type(" + type + ") is not extension, because WITHOUT @" + SPI.class.getSimpleName() + " Annotation!");
  11. } else {
  12. //EXTENSION_LOADERS is a map object that can be accessed concurrently, which is equivalent to a cache
  13. //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
  14. //Put it into the cache and get it from the cache again
  15. ExtensionLoader<T> loader = (ExtensionLoader)EXTENSION_LOADERS.get(type);
  16. if (loader == null) {
  17. EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader(type));
  18. loader = (ExtensionLoader)EXTENSION_LOADERS.get(type);
  19. }
  20. return loader;
  21. }
  22. }

Let's take a look at the process of obtaining the concrete implementation class

   
  1. public T getExtension (String name) {
  2. if (name != null && name.length() != 0) {
  3. //If the parameter is true, the default extension is used
  4. if ( "true".equals(name)) {
  5. return this.getDefaultExtension();
  6. } else {
  7. //As above, first check whether the cache has a corresponding instance object. If not, create a new one, and then get it
  8. //Here, the Holder object holds a volatile value attribute to ensure visibility to all threads
  9. Holder<Object> holder = (Holder) this.cachedInstances.get(name);
  10. if (holder == null) {
  11. this.cachedInstances.putIfAbsent(name, new Holder());
  12. holder = (Holder) this.cachedInstances.get(name);
  13. }
  14. Object instance = holder.get();
  15. if (instance == null) {
  16. //Personally, I think the holder object here is mainly to reduce the granularity of locks.
  17. synchronized(holder) {
  18. //Check twice to get, so this is thread safe
  19. //This is a bit like the creation of lazy singleton mode
  20. instance = holder.get();
  21. if (instance == null) {
  22. instance = this.createExtension(name);
  23. holder.set(instance);
  24. }
  25. }
  26. }
  27. return instance;
  28. }
  29. } else {
  30. throw new IllegalArgumentException( "Extension name == null");
  31. }
  32. }

The process of creating an instance is as follows:

   
  1. private T createExtension (String name) {
  2. //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
  3. Class<?> clazz = (Class) this.getExtensionClasses().get(name);
  4. if (clazz == null) {
  5. throw this.findException(name);
  6. } else {
  7. try {
  8. T instance = EXTENSION_INSTANCES.get(clazz);
  9. if (instance == null) {
  10. EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());
  11. instance = EXTENSION_INSTANCES.get(clazz);
  12. }
  13. // Inject dependencies into the instance to implement IOC
  14. this.injectExtension(instance);
  15. Set<Class<?>> wrapperClasses = this.cachedWrapperClasses;
  16. Class wrapperClass;
  17. if (wrapperClasses != null && !wrapperClasses.isEmpty()) {
  18. // Pass the current instance as a parameter to the constructor of the Wrapper, and create the Wrapper instance through reflection.
  19. // Then inject dependencies into the Wrapper instance, and finally assign the Wrapper instance to the instance variable again
  20. //This is actually the implementation of AOP
  21. for( Iterator i$ = wrapperClasses.iterator(); i$.hasNext(); instance = this.injectExtension(wrapperClass.getConstructor( this.type).newInstance(instance))) {
  22. // The SPI instance (created according to the specified name) is injected through the construction method with parameters
  23. // After the injection is successful and the instance is created, the assembled Wrapper instance will be returned
  24. // In this way, when looping to the next Wrapper class, it actually injects the previous Wrapper class instance
  25. // This also explains why the post definition is executed first
  26. wrapperClass = (Class)i$.next();
  27. }
  28. }
  29. return instance;
  30. } catch (Throwable var7) {
  31. throw new IllegalStateException( "Extension instance(name: " + name + ", class: " + this.type + ") could not be instantiated: " + var7.getMessage(), var7);
  32. }
  33. }
  34. }
  35. //Parse the SPI annotation and load the extension class from the directory of the configuration file
  36. private Map<String, Class<?>> loadExtensionClasses() {
  37. SPI defaultAnnotation = (SPI) this.type.getAnnotation(SPI.class);
  38. if (defaultAnnotation != null) {
  39. String value = defaultAnnotation.value();
  40. if ((value = value.trim()).length() > 0) {
  41. String[] names = NAME_SEPARATOR.split(value);
  42. if (names.length > 1) {
  43. throw new IllegalStateException( "more than 1 default extension name on extension " + this.type.getName() + ": " + Arrays.toString(names));
  44. }
  45. if (names.length == 1) {
  46. this.cachedDefaultName = names[ 0];
  47. }
  48. }
  49. }
  50. Map<String, Class<?>> extensionClasses = new HashMap();
  51. this.loadDirectory(extensionClasses, "META-INF/dubbo/internal/");
  52. this.loadDirectory(extensionClasses, "META-INF/dubbo/");
  53. this.loadDirectory(extensionClasses, "META-INF/services/");
  54. return extensionClasses;
  55. }

The IOC here is implemented based on setter function injection dependency

   
  1. private T injectExtension (T instance) {
  2. try {
  3. if ( this.objectFactory != null) {
  4. Method[] arr$ = instance.getClass().getMethods();
  5. int len$ = arr$.length;
  6. for( int i$ = 0; i$ < len$; ++i$) {
  7. Method method = arr$[i$];
  8. if (method.getName().startsWith( "set") && method.getParameterTypes().length == 1 && Modifier.isPublic(method.getModifiers())) {
  9. //Get setter method parameter type
  10. Class pt = method.getParameterTypes()[ 0];
  11. try {
  12. String property = method.getName().length() > 3 ? method.getName().substring( 3, 4).toLowerCase() + method.getName().substring( 4) : "";
  13. // Get dependent objects from ObjectFactory
  14. Object object = this.objectFactory.getExtension(pt, property);
  15. if (object != null) {
  16. // Set dependency by calling setter method through reflection
  17. method.invoke(instance, object);
  18. }
  19. } catch (Exception var9) {
  20. logger.error( "fail to inject via method " + method.getName() + " of interface " + this.type.getName() + ": " + var9.getMessage(), var9);
  21. }
  22. }
  23. }
  24. }
  25. } catch (Exception var10) {
  26. logger.error(var10.getMessage(), var10);
  27. }
  28. return instance;
  29. }

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

   
  1. private boolean isWrapperClass (Class<?> clazz) {
  2. try {
  3. // An attempt was made to get a constructor with a parameter type of SPI interface type
  4. clazz.getConstructor( this.type);
  5. return true;
  6. } catch (NoSuchMethodException var3) {
  7. return false;
  8. }
  9. }

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

   
  1. @SPI
  2. public interface AopService {
  3. @Adaptive
  4. void service ();
  5. }
  • Create two implementation classes

   
  1. public class CppAopService implements AopService {
  2. @Override
  3. public void service () {
  4. System.out.println( "this is c++ aop service");
  5. }
  6. }
  7. public class JavaAopService implements AopService {
  8. @Override
  9. public void service () {
  10. System.out.println( "this is java aop service");
  11. }
  12. }
  • Create a wapper class to enhance the implementation class (AOP)

   
  1. public class AopServiceWapper1 implements AopService {
  2. private AopService aopService;
  3. //You must have this constructor to be judged as a wapper class
  4. public AopServiceWapper1 (AopService service) {
  5. this.aopService = service;
  6. }
  7. @Override
  8. public void service () {
  9. System.out.println( "before wapper1");
  10. aopService.service();
  11. System.out.println( "after wapper1");
  12. }
  13. }
  14. public class AopServiceWapper2 implements AopService {
  15. private AopService aopService;
  16. //You must have this constructor to be judged as a wapper class
  17. public AopServiceWapper2 (AopService service) {
  18. this.aopService = service;
  19. }
  20. @Override
  21. public void service () {
  22. System.out.println( "before wapper2");
  23. aopService.service();
  24. System.out.println( "after wapper2");
  25. }
  26. }
  • Two wapper s need to be added to the configuration file

   
  1. wapper1=com.lzq.dubboaopdemo.aopservice.Impl.AopServiceWapper1
  2. wapper2=com.lzq.dubboaopdemo.aopservice.Impl.AopServiceWapper2
  3. java=com.lzq.dubboaopdemo.aopservice.Impl.JavaAopService
  4. cpp=com.lzq.dubboaopdemo.aopservice.Impl.JavaAopService
  • Test

   
  1. public static void main (String[] args) {
  2. ExtensionLoader<AopService> loader = ExtensionLoader.getExtensionLoader(AopService.class);
  3. AopService service = loader.getExtension( "java");
  4. service.service();
  5. }

Keywords: Java Maven

Added by __greg on Tue, 07 Dec 2021 06:32:05 +0200