首页 > 编程语言 >Java SPI META-INF/services 详解

Java SPI META-INF/services 详解

时间:2023-06-07 22:59:00浏览次数:54  
标签:Java 实现 扩展 接口 SPI META return public

目录

什么是SPI

  SPI(Service Provider Interface)是 JDK 提供的一套用来被第三方实现或者扩展的接口,它可以用来启用框架扩展和替换组件。 SPI的作用就是为这些被扩展的API寻找服务实现。

SPI和API的使用场景

  API (Application Programming Interface)在大多数情况下,都是实现方设计接口并完成对接口的实现,调用方仅仅依赖接口调用,且无权选择不同实现。 从使用人员上来说,API 直接被应用开发人员使用。

  SPI (Service Provider Interface)则是调用方来制定接口规范,提供给外部来实现,调用方在调用时则选择自己需要的外部实现。 从使用人员上来说,SPI 被框架扩展人员使用。

SPI的简单实现

下面简单实现一个 JDK 的 SPI 的简单demo

1、工程结构如下

image

2、代码

【UserDao】

package org.ailun;
/**
 * 接口
 */
public interface UserDao {
    String getUser();
}

【UserDaoImplA】

package org.ailun;
/**
 * 实现A
 */
public class UserDaoImplA implements UserDao {
    @Override
    public String getUser() {
        return "UserDaoImplA";
    }
}

【UserDaoImplB】

package org.ailun;
/**
 * 实现B
 */
public class UserDaoImplB implements UserDao {
    @Override
    public String getUser() {
        return "UserDaoImplB";
    }
}

【SpiTest】

package org.ailun;
import java.util.ServiceLoader;
/**
 * 测试入口
 */
public class SpiTest {
    public static void main(String[] args) {
        System.out.println("Hello world!");
        ServiceLoader<UserDao> userDao = ServiceLoader.load(UserDao.class);
        for (UserDao dao : userDao) {
            System.out.println("######################### \t[ " + dao.getUser() + " ]\t #########################");
        }
    }
}

3、资源目录

  然后需要在resources目录下新建META-INF/services目录,并且在这个目录下新建一个与上述接口类全限定名一致 文件名 的 文件,在这个文件中写入接口的实现类的全限定名

【org.ailun.UserDao】

org.ailun.UserDaoImplA
org.ailun.UserDaoImplB

4、启动测试类输出如下内容

Hello world!
######################### 	[ UserDaoImplA ]	 #########################
######################### 	[ UserDaoImplB ]	 #########################

  这样一个简单的SPI的demo就完成了。可以看到其中最为核心的就是通过ServiceLoader这个类来加载具体的实现类的。

SPI原理解析

  通过上面简单的demo,可以看到最关键的实现就是ServiceLoader类中LazyIterator的这个内部类,可以看下这个类的加载相关的核心源码,如下:

// Private inner class implementing fully-lazy provider lookup
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;
    }

    // 加载定义在 resources 目录下新建 META-INF/services 目录下的SPI配置文件内容
    private boolean hasNextService() {
        if (nextName != null) {
            return true;
        }
        if (configs == null) {
            try {
                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;
    }

    // 根据 hasNextService 方法加载的SPI配置的接口实现类的全类名 的内容 按行加载接口的实现类并实例化
    private S 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");
        }
        if (!service.isAssignableFrom(c)) {
            fail(service,
                    "Provider " + cn + " not a subtype");
        }
        try {
            S p = service.cast(c.newInstance());
            providers.put(cn, p);
            return p;
        } catch (Throwable x) {
            fail(service,
                    "Provider " + cn + " could not be instantiated",
                    x);
        }
        throw new Error();          // This cannot happen
    }

	// 提供迭代的入口,从这里看
    public boolean hasNext() {
        if (acc == null) {
            return hasNextService();
        } else {
            PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
                public Boolean run() {
                    return hasNextService();
                }
            };
            return AccessController.doPrivileged(action, acc);
        }
    }

    // 最终返回接口实现类的实例
    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);
        }
    }

    public void remove() {
        throw new UnsupportedOperationException();
    }

}

  上面的代码只贴出了部分关键的实现,有兴趣的读者可以自己去研究,下面贴出比较直观的spi加载的主要流程供参考:

image

Dubbo SPI

  Dubbo作为一个高度可扩展的rpc框架,也依赖于 JAVA 的 SPI ,并且 Dubbo对 JAVA 原生的 SPI 机制作出了一定的扩展,使得其功能更加强大。

  首先,从上面的JAVA SPI 的原理中可以了解到,JAVA 的 SPI 机制有着如下的弊端:

  • 只能遍历所有的实现,并全部实例化。
  • 配置文件中只是简单的列出了所有的扩展实现,而没有给他们命名。导致在程序中很难去准确的引用它们。
  • 扩展如果依赖其他的扩展,做不到自动注入和装配。
  • 扩展很难和其他的框架集成,比如扩展里面依赖了一个Spring bean,原生的Java SPI 不支持。

Dubbo的SPI 有如下几个概念:

(1)扩展点:一个接口。

(2)扩展:扩展(接口)的实现。

(3)扩展自适应实例:其实就是一个Extension的代理,它实现了扩展点接口。在调用扩展点的接口方法时,会根据实际的参数来决定要使用哪个扩展。dubbo会根据接口中的参数,自动地决定选择哪个实现。

(4)@SPI:该注解作用于扩展点的接口上,表明该接口是一个扩展点。

(5)@Adaptive:@Adaptive注解用在扩展接口的方法上。表示该方法是一个自适应方法。Dubbo在为扩展点生成自适应实例时,如果方法有@Adaptive注解,会为该方法生成对应的代码。

  Dubbo 的 SPI 也会从某些固定的路径下去加载配置文件,并且配置的格式与JAVA 原生的不一样,类似于property文件的格式:

image

  下面将基于Dubbo去实现一个简单的扩展实现。首先,要实现LoadBalance这个接口,当然这个接口是被注解标注的可以扩展的:

@SPI("random")
public interface LoadBalance {
    @Adaptive({"loadbalance"})
    <T> Invoker<T> select(List<Invoker<T>> a, URL b, Invocation c) throws RpcException;
}

【实现LoadBalance】

public class myLoadBalance implements LoadBalance {
    @Override
    public <T> Invoker<T> select(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException {
        System.out.println("my demo loadBalance is used, hahahahh");
        // 选择第一个
        return invokers.get(0);
    }
}

  然后,需要在Dubbo SPI的扫描目录下,添加配置文件,注意配置文件的名称要和扩展点的接口名称对应起来:例如META-INF/dubbo/com.alibaba.dubbo.rpc.cluster.LoadBalance

img

还需要在dubbo的spring配置中显式的声明,使用上面自己实现的负载均衡策略:

<dubbo:reference id="helloService" interface="com.dubbo.spi.demo.api.IHelloService" loadbalance="demo" />

  至此,Dubbo 的SPI的demo完成。

Dubbo SPI的原理和 JDK 的实现稍有不同,大概流程如下图,具体的实现读者可以自己了解下源码。
主要看

类:ExtensionLoader
方法:public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type)public T getExtension(String name)

img

总结

关于SPI的详解到此就结束了,总结下SPI能带来的好处:

  • 不需要改动源码就可以实现扩展,解耦。
  • 实现扩展对原来的代码几乎没有侵入性。
  • 只需要添加配置就可以实现扩展,符合开闭原则。

参考:https://blog.csdn.net/cocoa_geforce/article/details/117369236

标签:Java,实现,扩展,接口,SPI,META,return,public
From: https://www.cnblogs.com/hhddd-1024/p/17464806.html

相关文章

  • Java注释
    Java注释单行注释://多行注释:/**/注意:多行注释不能嵌套使用文档注释(Java特有):/***//**@author@version*/......
  • JAVA 基础面试题(框架)
    一、mybatis    首先,mybatis是一个对象关系映射(orm)框架,是为了解决面向对象与关系数据库的存在互不匹配的现象。也就是说mybatis的关注点在于对象与数据库之间的映射,mybatis会把从数据库中拿到的松散数据进行封装,使开发者直接拿到一个对象。mybatis其实就是对jdbc操作数据库......
  • Java基本概念介绍
    Java基本概念介绍Java是一种面向对象的编程语言,由JamesGosling等人在1995年开发而成。Java是跨平台的,这意味着可以在不同的操作系统上运行相同的程序。Java具有许多优点,包括安全性、可移植性、高效性和可扩展性。Java的基本结构Java程序在类中编写。每个Java程序至少需要一个类,并......
  • JAVA的springboot+vue企业客户信息反馈平台,附源码+数据库+文档+PPT
    1、项目介绍企业客户信息反馈平台能够通过互联网得到广泛的、全面的宣传,让尽可能多的用户了解和熟知企业客户信息反馈平台的便捷高效,不仅为客户提供了服务,而且也推广了自己,让更多的客户了解自己。对于企业客户信息反馈而言,若拥有自己的平台,通过平台得到更好的管理,同时提升了形象......
  • JVM内存结构&Java内存模型&Java对象模型
    1.JVM内存结构Java代码是运行在虚拟机上的,而虚拟机在执行Java程序的过程中会把管理的内存划分为若干个不同的数据区域。其中有些区域是随着虚拟机进程的启动而存在,而有些区域则依赖用户线程的启动和结束而建立和销毁。而这些区域会被划分为五个区域,具体的如下:方法区:方法区与是......
  • Java设计模式-适配器模式
    适配器模式(AdapterPattern)是一种常见的设计模式,它主要用于在不改变现有系统结构的情况下,将一个类的接口转换成客户端所期望的另一个接口。在本文中,我们将介绍适配器模式的基本概念、实现方法以及优缺点,并探讨适配器模式在Java编程语言中的具体应用。简介适配器模式是一种结构型......
  • JDK没有JAVAX.ANNOTATION.JAR包解决方案,无法使用@RESOURCE解决方案
    高版本JDK无法使用@Resource注解解决方案1.普通项目下载javax.annotation-api-1.3.2.jar,并在lib目录中引入即可2.Maven项目Maven项目:在pom.xml中进行配置<dependency><groupId>javax.annotation</groupId><artifactId>jsr250-api</artifactId><ver......
  • Java EE考点复习
    JavaEE一、JavaEE概述javaEE结构的软件系统分层从逻辑上分:表示层、业务层、数据持久层分布式系统开发的优点基于面向对象设计思想的多层结构超强的移植性与复用性侧重于Web应用模式的设计,支持分布式开发集成了众多的信息技术,称为一个功能强大的开发平台相对独立的......
  • 关于Java中多线程
    基本概念什么是进程-->是操作系统资源分配和调度的最小(基本)单位(操作系统分配给当前进程一个内存区域供其使用)什么是线程-->是程序运行的基本单位(等待操作系统分配时间片让CPU执行该内存区域中的代码)进程和线程的关系-->一个进程可以存在多个线程线程是由进程创建的(寄......
  • java 接口
    为什么使用接口接口可以实现java中的“多继承”什么是接口接口的关键字是interface接口中的所有方法都是用抽象abstract修饰的;没有方法体;那个类要用这个接口就在那个类中写接口中的方法接口不能实例化,接口就是抽象的概念实现类中必须实现接口中的所有方法实现的关键字imple......