Java和dubbo中的SPI机制学习

偏执的太偏执、 2022-05-26 07:54 242阅读 0赞

关于java的SPI机制,可以参考: https://docs.oracle.com/javase/tutorial/sound/SPI-intro.html

为了实现在模块装配时的时候不在程序中动态指明,需要提供一种服务发现机制,为某个接口寻找服务实现的机制,就是将装配的控制权转移到程序之外,在模块化设计中这个机制尤其重要。

Java SPI(Service Provider Interface)的具体约定如下:当服务的提供者提供了服务接口的一种实现之后,在jar包的META-INF/services/ 目录中同时创建一个以服务接口命名的文件,该文件中的内容就是实现该服务接口的具体实现类。

Java中提供了一个用于服务实现查找的工具类:java.util.ServiceLoader。

16de7fca-0833-3d75-84b9-1a8a3ef2d197.png

该实例用于演示在maven环境下的service声明定义,居然在idea下无法调试启动…

resources下指定目录 META-INF/services目录(被写死在代码中):

[java] view plain copy

  1. public final class ServiceLoader<S
  2. implements Iterable<S
  3. {
  4. private static final String PREFIX = “META-INF/services/“;

注意需要将服务声明的文件名称定义为: example.spi.service.Service,与接口名称一致,其中的内容包括:

[java] view plain copy

  1. example.spi.service.PrintServiceImpl
  2. example.spi.service.EchoServiceImpl

不同的实现类需要以换行符分隔,在SpiMain的主方法中,使用ServiceLoader进行加载操作:

[java] view plain copy

  1. public static void main(String[] args) {
  2. ServiceLoader serviceLoader = ServiceLoader.load(Service.class);
  3. for (Service service : serviceLoader) {
  4. service.printInfo();
  5. }
  6. }

在ServiceLoader的load方法中,会初始化ServiceLoader.LazyIterator,实现了标准的迭代器接口Iterator(以及hasNext, next方法),其中hasNextService()方法中会从当前的ClassLoader加载 PREFIX(”META-INF/services/“) + service名称(class名称),

[java] view plain copy

  1. private boolean hasNextService() {
  2. if (nextName != null) {
  3. return true;
  4. }
  5. if (configs == null) {
  6. try {
  7. String fullName = PREFIX + service.getName();
  8. if (loader == null)
  9. configs = ClassLoader.getSystemResources(fullName);
  10. else
  11. configs = loader.getResources(fullName);
  12. } catch (IOException x) {
  13. fail(service, “Error locating configuration files”, x);
  14. }
  15. }
  16. while ((pending == null) || !pending.hasNext()) {
  17. if (!configs.hasMoreElements()) {
  18. return false;
  19. }
  20. pending = parse(service, configs.nextElement());
  21. }
  22. nextName = pending.next();
  23. return true;
  24. }

在parse方法中会根据文件进行逐行解析,如果下一步存在对应的实现,该方法返回true,接着就可以访问nextService方法来获得下一个服务实现:

[java] view plain copy

  1. private S nextService() {
  2. if (!hasNextService())
  3. throw new NoSuchElementException();
  4. String cn = nextName;
  5. nextName = null;
  6. Class<?> c = null;
  7. try {
  8. c = Class.forName(cn, false, loader);
  9. } catch (ClassNotFoundException x) {
  10. fail(service,
  11. “Provider “ + cn + “ not found”);
  12. }
  13. if (!service.isAssignableFrom(c)) {
  14. fail(service,
  15. “Provider “ + cn + “ not a subtype”);
  16. }
  17. try {
  18. S p = service.cast(c.newInstance());
  19. providers.put(cn, p);
  20. return p;
  21. } catch (Throwable x) {
  22. fail(service,
  23. “Provider “ + cn + “ could not be instantiated”,
  24. x);
  25. }
  26. throw new Error(); // This cannot happen
  27. }

将nextName(下一行实现的类名称)进行实例化,注意需要实现对应接口并有无参构造函数,并将实现放到providers这个Map中,以便下次直接使用(除非进行reload操作,否则不会更新该表,而reload操作是在ServiceLoader启动时初始化的)。

[java] view plain copy

  1. public void reload() {
  2. providers.clear();
  3. lookupIterator = new LazyIterator(service, loader);
  4. }

虽然ServiceLoader也算是使用了延迟加载,但只能通过遍历所有获取,将接口的实现类全部加载并实例化一遍,而且只能通过Iterator形式获取,不能根据某个参数来获取。

参考: https://my.oschina.net/pingpangkuangmo/blog/508963 来分析dubbo的服务体系。

在dubbo中大量使用了该方式进行服务发现和服务注册,并进行了一定的扩展,实现了 com.alibaba.dubbo.common.extension.ExtensionLoader 类,内部注册服务的目录迁移为 META-INF/dubbo/internal类型:

[java] view plain copy

  1. private static final String DUBBO_DIRECTORY = “META-INF/dubbo/“;
  2. private static final String DUBBO_INTERNAL_DIRECTORY = DUBBO_DIRECTORY + “internal/“;

在ExtensionLoader中,不仅会对其中的服务进行初始化,还可以像IOC一样,对引用的其他服务进行set操作,这部分参考ExtensionLoader.injectExtension(T instance)类,进行注入的类型必须为set开头方法,参数只有一个,方法为public,而且参数必须为接口(只有接口ExtensionFactory才能根据该接口进行查找服务实现类)。

[java] view plain copy

  1. private T injectExtension(T instance) {
  2. try {
  3. if (objectFactory != null) {
  4. for (Method method : instance.getClass().getMethods()) {
  5. if (method.getName().startsWith(“set”)
  6. && method.getParameterTypes().length == 1
  7. && Modifier.isPublic(method.getModifiers())) {
  8. Class<?> pt = method.getParameterTypes()[0];
  9. try {
  10. String property = method.getName().length() > 3 ? method.getName().substring(3, 4).toLowerCase() + method.getName().substring(4) : “”;
  11. Object object = objectFactory.getExtension(pt, property);
  12. if (object != null) {
  13. method.invoke(instance, object);
  14. }
  15. } catch (Exception e) {
  16. logger.error(“fail to inject via method “ + method.getName()
    • “ of interface “ + type.getName() + “: “ + e.getMessage(), e);
  17. }
  18. }
  19. }
  20. }
  21. } catch (Exception e) {
  22. logger.error(e.getMessage(), e);
  23. }
  24. return instance;
  25. }

如果接口的实现由多个,则此时采取的策略是,并不去注入一个具体的实现者,而是注入一个动态生成的实现者,这个动态生成的实现者的逻辑是确定的,能够根据不同的参数来使用不同的实现者实现相应的方法。这个动态生成的实现者的class就是ExtensionLoader的Class<?> cachedAdaptiveClass

在查找SPI Annotation,使用dubbo配置的扩展方式进行注册,例如在获取AdaptiveExtensionFactory时,使用的构造函数用于加载扩展点:

[java] view plain copy

  1. public AdaptiveExtensionFactory() {
  2. ExtensionLoader loader = ExtensionLoader.getExtensionLoader(ExtensionFactory.class);
  3. List list = new ArrayList();
  4. for (String name : loader.getSupportedExtensions()) {
  5. list.add(loader.getExtension(name));
  6. }
  7. factories = Collections.unmodifiableList(list);
  8. }

而getExtension()方法能够查找具体的objectFactory(SPI,Spring)可以从对应的文件声明以及spring容器中查找具体的bean。

对于ExtensionFactory会从三个地方加载extensionClass:

[java] view plain copy

  1. private Map> loadExtensionClasses() {
  2. final SPI defaultAnnotation = type.getAnnotation(SPI.class);
  3. if(defaultAnnotation != null) {
  4. String value = defaultAnnotation.value();
  5. if(value != null && (value = value.trim()).length() > 0) {
  6. String[] names = NAME_SEPARATOR.split(value);
  7. if(names.length > 1) {
  8. throw new IllegalStateException(“more than 1 default extension name on extension “ + type.getName()
    • “: “ + Arrays.toString(names));
  9. }
  10. if(names.length == 1) cachedDefaultName = names[0];
  11. }
  12. }
  13. Map> extensionClasses = new HashMap>();
  14. loadFile(extensionClasses, DUBBO_INTERNAL_DIRECTORY);
  15. loadFile(extensionClasses, DUBBO_DIRECTORY);
  16. loadFile(extensionClasses, SERVICES_DIRECTORY);
  17. return extensionClasses;
  18. }

分别为 /META-INF/dubbo/internal, /META-INF/dubbo/,META-INF/services。

可以看出dubbo的扩展机制虽然与SPI比较类似,但额外增加了其他功能,例如可以根据接口名称来获取服务,服务声明文件支持A=B的方式,此时A为名称B为实现类;支持扩展IOC依赖注入功能,可以为Service之间的依赖关系注入相关的服务并保证单例。

转载于:https://blog.csdn.net/clamaa/article/details/70046590\#

发表评论

表情:
评论列表 (有 0 条评论,242人围观)

还没有评论,来说两句吧...

相关阅读

    相关 jdkdubboSPI机制

    [jdk和dubbo的SPI机制][jdk_dubbo_SPI] 前言:开闭原则一直是软件开发领域中所追求的,开闭原则中的"开"是指对于组件功能的扩展是开放的,是允许对其