浅析Java SPI 与 dubbo SPI

作者:茕祇 时间:2021-10-23 16:54:12 

Java原生SPI

面向接口编程+策略模式

实现

建立接口

Robot


public interface Robot {
   /**
    * 测试方法1
    */
   void sayHello();
}

多个实现类实现接口

RobotA


public class RobotA implements Robot {
   public RobotA() {
       System.out.println("Happy RobotA is loaded");
   }
   @Override
   public void sayHello() {
       System.out.println("i am a very very happy Robot ");
   }
   public void sayBye(){}
}

RobotB


public class RobotB implements Robot {
   public RobotB() {
       System.out.println("SB RobotB is loaded");
   }
   @Override
   public void sayHello() {
       System.out.println("i am a da sha bi ");
   }
   public void sayBye(){}
}

配置实现类与接口

META-INF/services目录下建立一个以接口全限定名为名字的文件,里面的内容是实现类的全限定名

原理

通过ServiceLoader与配置文件中的全限定名加载所有实现类,根据迭代器获取具体的某一个类

我们通过对下面一段代码的分析来说明


ServiceLoader<Robot> serviceLoader=ServiceLoader.load(Robot.class);
serviceLoader.forEach(Robot::sayHello);

load(Robot.class)这个方法的目的只是为了设置类加载器为线程上下文加载器,我们当然可以不这么做,直接调用load(Class service,ClassLoader loader)方法


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

这个load方法其实也没有做什么实质的事,仅仅是实例化了一个ServiceLoad对象返回罢了


public static <S> ServiceLoader<S> load(Class<S> service,
                                       ClassLoader loader)
{
   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();
}
public void reload() {
   //这里的provider是一个对于已实例化对象的缓存,为Map类型
       providers.clear();
       lookupIterator = new LazyIterator(service, loader);
   }

没有,这里仅仅只是检验了参数和权限这样一些准备操作.然后实例化了一个LazyIterator

这是LazyIterator的构造函数


private LazyIterator(Class<S> service, ClassLoader loader) {
   this.service = service;
   this.loader = loader;
}

然后....,没了,ServiceLoader<Robot> serviceLoader=ServiceLoader.load(Robot.class);执行完毕了,到这里,并没有实例化我们所需要的Robot对象,而仅仅只是返回了一个ServiceLoader对象

这时候如果我们去看serviceLoader的对象方法是这样的

浅析Java SPI 与 dubbo SPI

有用的只有这三个方法,reload上面已经提到过,只是重新实例化一个对象而已.

而另外两个iterator()是个迭代器,foreach也只是用于迭代的语法糖罢了.如果我们debug的话,会发现foreach的核心依旧会变成iterator(),好了,接下来重点看iterator()


public Iterator<S> iterator() {
   return new Iterator<S>() {

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();
       }

};

这个方法实际上是返回了一个Iterator对象.而通过这个Iterator,我们可以遍历获取我们所需要的Robot对象.

我们来看其用于获取对象的next方法


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

这个方法是先在缓存里找,缓存里找不到,就需要用最开始的实例化的lookupIterator

再来看看它的next方法


public S next() {
   if (acc == null) {
       return nextService();
   } else {
       PrivilegedAction<S> action = new PrivilegedAction<S>() {
           public S run() { return nextService(); }
       };
       return AccessController.doPrivileged(action, acc);
   }
}

这方法的核心是nextService,我们继续看实现,这个方法比较长,我贴一部分核心


if (!hasNextService())
   throw new NoSuchElementException();
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
   c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
   fail(service,
        "Provider " + cn + " not found");
}

hasNextService()判断是否还可以继续迭代,通过class.forName反射获取实例,最后再加入到provider缓存中.于是基本逻辑就完成了.那nextName哪来的.是在hasNextService()中获取的.

依旧只有核心代码


//获取文件
String fullName = PREFIX + service.getName();
if (loader == null)
   configs = ClassLoader.getSystemResources(fullName);
else
   configs = loader.getResources(fullName);
//解析文件配置
while ((pending == null) || !pending.hasNext()) {
               if (!configs.hasMoreElements()) {
                   return false;
               }
               pending = parse(service, configs.nextElement());
           }
           nextName = pending.next();

根据前缀(即META-INF/services)和接口的全限定名去找到对应的配置文件.然后加载里面的配置,获取具体实现类的名字.

Dubbo增强SPI

实现

建立接口

与原生SPI不同,dubbo需要加入@SPI注解

Robot


@SPI
public interface Robot {
   /**
    * 测试方法1
    */
   void sayHello();
}

多个实现类实现接口

RobotA


public class RobotA implements Robot {
   public RobotA() {
       System.out.println("Happy RobotA is loaded");
   }
   @Override
   public void sayHello() {
       System.out.println("i am a very very happy Robot ");
   }
   public void sayBye(){}
}

RobotB


public class RobotB implements Robot {
   public RobotB() {
       System.out.println("SB RobotB is loaded");
   }
   @Override
   public void sayHello() {
       System.out.println("i am a da sha bi ");
   }
   public void sayBye(){}
}

配置实现类与接口

META-INF/dubbo目录下建立一个以接口全限定名为名字的文件,里面的内容是自定义名字与类的全限定名的键值对,举个例子


robotA = cn.testlove.double_dubbo.inter.impl.RobotA
robotB=cn.testlove.double_dubbo.inter.impl.RobotB

原理

我们通过对下列代码的调用来进行分析


ExtensionLoader<Robot> extensionLoader= ExtensionLoader.getExtensionLoader(Robot.class);
Robot robotB = extensionLoader.getExtension("robotB");

第一句代码没什么好说的,只是获取一个RobotExtensionLoader对象并且缓存在Map中,下次如果是同样的接口可以直接从map中获取


ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
if (loader == null) {
   EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type));
   loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
}

再来看第二句代码


//从缓存中找
final Holder<Object> holder = getOrCreateHolder(name);
Object instance = holder.get();
//双重检查
if (instance == null) {
   synchronized (holder) {
       instance = holder.get();
       if (instance == null) {
           instance = createExtension(name);
           holder.set(instance);
       }
   }
}

首先从缓存里找,找不到再创建一个新的对象。

再看createExtension(name)方法


Class<?> clazz = getExtensionClasses().get(name);

T instance = (T) EXTENSION_INSTANCES.get(clazz);
if (instance == null) {
   EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());
   instance = (T) EXTENSION_INSTANCES.get(clazz);
}
injectExtension(instance);
Set<Class<?>> wrapperClasses = cachedWrapperClasses;
if (CollectionUtils.isNotEmpty(wrapperClasses)) {
   for (Class<?> wrapperClass : wrapperClasses) {
       instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
   }
}
initExtension(instance);
return instance;

注意对于Class<?> clazz = getExtensionClasses().get(name);这一句的理解,这一句是获取配置文件中所有类的Class实例,而不是获取所有扩展类的实例。

接下来的流程其实也就简单了从EXTENSION_INSTANCES缓存中获取instance实例,如果没有,就借助Class对象实例化一个,再放入缓存中

接着用这个instance去实例化一个包装类然后返回.自此,一个我们需要的对象产生了.

最后我们看看getExtensionClasses()这个方法


Map<String, Class<?>> classes = cachedClasses.get();
if (classes == null) {
   synchronized (cachedClasses) {
       classes = cachedClasses.get();
       if (classes == null) {
           classes = loadExtensionClasses();
           cachedClasses.set(classes);
       }
   }
}
return classes;

这里的classes就是用来存各个扩展类Class的Map缓存,如果不存在的话,会调用loadExtensionClasses();去加载,剩下的就是找到对应路径下的配置文件,获取全限定名了

上文我在分析Dubbo SPI时,多次提到Map,缓存二词,我们可以具体有以下这些.其实看名字就大概知道作用了


private final ConcurrentMap<Class<?>, String> cachedNames = new ConcurrentHashMap<>();
private final Holder<Map<String, Class<?>>> cachedClasses = new Holder<>()    
private final Map<String, Object> cachedActivates = new ConcurrentHashMap<>();
private final ConcurrentMap<String, Holder<Object>> cachedInstances = new ConcurrentHashMap<>();
private final Holder<Object> cachedAdaptiveInstance = new Holder<>();
private volatile Class<?> cachedAdaptiveClass = null;
private String cachedDefaultName;

对比原生的java SPI,dubbo的无疑更灵活,可以按需去加载某个类,也可以很便捷的通过自定义的名字去获取类.而且Dubbo还支持setter注入.这点以后再讲.

最后提一个问题,java原生的SPI只有在用iterator遍历到的时候才会实例化对象,那能不能在遇到自己想要的实现对象时就停止遍历,避免不必要的资源消耗呢?

补充:下面看下Dubbo SPI 和 Java SPI 区别?

JDK SPI

JDK 标准的 SPI 会一次性加载所有的扩展实现,如果有的扩展吃实话很耗时,但

也没用上,很浪费资源。

所以只希望加载某个的实现,就不现实了

DUBBO SPI

1,对 Dubbo 进行扩展,不需要改动 Dubbo 的源码

2,延迟加载,可以一次只加载自己想要加载的扩展实现。

3,增加了对扩展点 IOC 和 AOP 的支持,一个扩展点可以直接 setter 注入其它扩展点。

3,Dubbo 的扩展机制能很好的支持第三方 IoC 容器,默认支持 Spring Bean。

来源:https://www.cnblogs.com/GodTestLove/p/14824152.html

标签:Java,SPI,dubbo,SPI
0
投稿

猜你喜欢

  • eclipse中自动生成javadoc文档的方法

    2022-04-08 17:54:12
  • C# 通过同步和异步实现优化做早餐的时间

    2022-10-27 23:09:27
  • Spring StopWatch使用实例详解

    2022-03-31 02:46:57
  • 最近较流行的效果 Android自定义View实现倾斜列表/图片

    2021-09-06 03:56:51
  • Java反射根据不同方法名动态调用不同的方法(实例)

    2022-05-08 00:51:47
  • Spingboot JPA CriteriaBuilder 如何获取指定字段

    2022-07-20 15:40:22
  • Android编程实现XML解析与保存的三种方法详解

    2022-04-15 00:39:01
  • Idea中SpringBoot多模块项目的建立实现

    2023-11-08 07:52:34
  • Android利用RecyclerView实现列表倒计时效果

    2023-09-04 05:29:27
  • Java多线程Future松获取异步任务结果轻松实现

    2022-11-22 15:49:38
  • Java8中的 Lambda表达式教程

    2023-10-13 01:32:29
  • C#微信公众号开发之接收事件推送与消息排重的方法

    2022-01-31 08:44:46
  • Java8之lambda最佳实践_动力节点Java学院整理

    2023-11-28 00:07:28
  • MyBatis映射文件resultMap元素中使用多个association的方法

    2023-11-29 06:53:51
  • 用Java实现简单ATM机功能

    2023-05-10 13:36:03
  • C#计算器编写代码

    2022-12-28 02:57:25
  • Java通过HttpClient进行HTTP请求的代码详解

    2022-06-04 09:50:20
  • Java的动态分派和静态分派的实现

    2023-10-09 12:58:37
  • 使用@Value值注入及配置文件组件扫描

    2023-12-01 21:24:12
  • C++ deque与vector对比的优缺点

    2023-08-28 13:19:16
  • asp之家 软件编程 m.aspxhome.com