如果你不看这一篇,那么SPI你永远一知半解!

  1. 网上回答:SPI是什么?
  2. 思考:类加载器与SPI
    2.1 类加载器知识
    2.2 SPI与线程上下文加载器
    2.3 需要SPI进行类加载吗?
  3. 为什么要引入SPI
    3.1 案例实现
  4. 业务项目有机会使用SPI吗
    4.1 业务项目常用—spring方式

1. 网上回答:SPI是什么?

一般是先说定义:SPI ,全称为 Service Provider Interface,是一种服务发现机制。它通过在ClassPath路径下的META-INF/services文件夹查找文件,自动加载文件里所定义的类。

然后讲JDBC:毕竟JDBC是每一个JAVA程序员的hello world

但是,并没有深入思考,SPI使用的场景!!!

2. 思考:类加载器与SPI

SPI引入会打破JVM类加载器的双亲委派模型,JVM会引入线程上下文加载器Thread Context ClassLoader。这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置。

2.1 类加载器知识

类加载阶段,通过类加载器,将class文件读取到内存中,并在内存中生成class对象。

类的生命周期.png

而常见的类加载器:

  • 启动类加载器(BootStrap):加载的是<JAVA_HOME>/lib中的class文件,也就是JDK的依赖。
  • 拓展类加载器(Extension):加载的是<JAVA_HOME>/lib/ext目录下或者由系统变量-Djava.ext.dir指定位路径中的类库。
  • 应用程序加载器(AppClassLoader):程序默认加载器,加载用户类路径上指定的类库。

2.2 SPI与线程上下文加载器

SPI接口一般有两种情况,一种是JDK中声明的SPI接口,一种是框架使用的SPI例如dubbo。这两种情况使用的加载器是不同的。

JDK声明的SPI接口:SPI的实现类一般是由应用加载器Application ClassLoader加载的,而JDK提供的SPI接口是Bootstrap ClassLoader加载:也就导致SPI接口无法找到对应的实现类。根本原因:并不是同一个类加载器进行加载的。为了解决这种情况,JVM设计出线程上下文加载器,来打破双亲委派模型。即父类使用子类的类加载器来进行类加载。从而保证父子类由一个类加载器进行加载,

框架的SPI接口:无论是父类还是子类,均使用的是Application ClassLoader加载,不会打破双亲委派模型,子类将委托父类的类加载器完成类的加载,从而保证了父子类由一个类加载器进行加载。

2.3 需要SPI进行类加载吗?

针对JDK的SPI接口,需要由线程上下文类加载器来完成父子类的加载,保证SPI接口和实现类由一个类加载器完成加载。

3. 为什么要引入SPI

场景:如何寻找一个接口的所有实现类。

  • Spring环境:可以依赖注入一个集合,那么会扫描Spring容器中该接口的所有时间类。
  • 侵入代码:可以维护一个枚举类或者一个map来存储接口所有的实现类。

但是对于框架来说,即不能和Spring强耦合,也无法侵入代码未卜先知声明所有的子类。所以就有了SPI:Service Provider Interface,是一种服务发现机制。

SPI目的:在框架中找到某个接口的实现所有实现类,存储到List中。或者更新颖的玩法,在子类上声明注解,通过SPI找到所有实现类,然后得到实现类上的注解参数,将其组合为一个Map。当不同的请求到达时,可以动态的选择不同的子类来进行处理。

由上可知,SPI的目的是通过读取规定配置信息,通过反射的方式创建接口实现类。

3.1 案例实现

源码位置—限流组件:sentinel1.8.1源码

读取META-INF/services/的配置信息,即SPI实现类的全类名。通过类加载器完成类加载过程,通过反射完成对象的创建:
源码位置:com.alibaba.csp.sentinel.spi.SpiLoader

public final class SpiLoader<S> {

    // Default path for the folder of Provider configuration file
    private static final String SPI_FILE_PREFIX = "META-INF/services/";

    // Cache the SpiLoader instances, key: classname of Service, value: SpiLoader instance
    private static final ConcurrentHashMap<String, SpiLoader> SPI_LOADER_MAP = new ConcurrentHashMap<>();

    // Cache the classes of Provider
    private final List<Class<? extends S>> classList = Collections.synchronizedList(new ArrayList<Class<? extends S>>());

    // Cache the sorted classes of Provider
    private final List<Class<? extends S>> sortedClassList = Collections.synchronizedList(new ArrayList<Class<? extends S>>());

    /**
     * Cache the classes of Provider, key: aliasName, value: class of Provider.
     * Note: aliasName is the value of {@link Spi} when the Provider class has {@link Spi} annotation and value is not empty,
     * otherwise use classname of the Provider.
     */
    private final ConcurrentHashMap<String, Class<? extends S>> classMap = new ConcurrentHashMap<>();

    // Cache the singleton instance of Provider, key: classname of Provider, value: Provider instance
    private final ConcurrentHashMap<String, S> singletonMap = new ConcurrentHashMap<>();

    // Whether this SpiLoader has been loaded, that is, loaded the Provider configuration file
    private final AtomicBoolean loaded = new AtomicBoolean(false);

    // Default provider class
    private Class<? extends S> defaultClass = null;

    // The Service class, must be interface or abstract class
    private Class<S> service;

    /**
     * Create SpiLoader instance via Service class
     * Cached by className, and load from cache first
     * 创建SpiLoader实例通过SPI接口
     */
    public static <T> SpiLoader<T> of(Class<T> service) {
        AssertUtil.notNull(service, "SPI class cannot be null");
        AssertUtil.isTrue(service.isInterface() || Modifier.isAbstract(service.getModifiers()),
                "SPI class[" + service.getName() + "] must be interface or abstract class");

        String className = service.getName();
        SpiLoader<T> spiLoader = SPI_LOADER_MAP.get(className);
        if (spiLoader == null) {
            synchronized (SpiLoader.class) {
                spiLoader = SPI_LOADER_MAP.get(className);
                if (spiLoader == null) {
                    SPI_LOADER_MAP.putIfAbsent(className, new SpiLoader<>(service));
                    spiLoader = SPI_LOADER_MAP.get(className);
                }
            }
        }

        return spiLoader;
    }

   /**
     * 
     *  加载配置文件中提供的类
     */
    public void load() {
        if (!loaded.compareAndSet(false, true)) {
            return;
        }
       // 组装配置文件地址 
        String fullFileName = SPI_FILE_PREFIX + service.getName();
        ClassLoader classLoader;
        if (SentinelConfig.shouldUseContextClassloader()) {
            classLoader = Thread.currentThread().getContextClassLoader();
        } else {
            //获取到父类的类加载器
            classLoader = service.getClassLoader();
        }
        if (classLoader == null) {
            classLoader = ClassLoader.getSystemClassLoader();
        }
        Enumeration<URL> urls = null;
        try {
           //类加载器读取资源信息
            urls = classLoader.getResources(fullFileName);
        } catch (IOException e) {
            fail("Error locating SPI configuration file, filename=" + fullFileName + ", classloader=" + classLoader, e);
        }

        if (urls == null || !urls.hasMoreElements()) {
            RecordLog.warn("No SPI configuration file, filename=" + fullFileName + ", classloader=" + classLoader);
            return;
        }

        while (urls.hasMoreElements()) {
            URL url = urls.nextElement();

            InputStream in = null;
            BufferedReader br = null;
            try {
                in = url.openStream();
                br = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8));
                String line;
                while ((line = br.readLine()) != null) {
                    if (StringUtil.isBlank(line)) {
                        // Skip blank line
                        continue;
                    }

                    line = line.trim();
                    int commentIndex = line.indexOf("#");
                    if (commentIndex == 0) {
                        // Skip comment line
                        continue;
                    }

                    if (commentIndex > 0) {
                        line = line.substring(0, commentIndex);
                    }
                    line = line.trim();

                    Class<S> clazz = null;
                    try {
                        //通过反射创建子类对象
                        clazz = (Class<S>) Class.forName(line, false, classLoader);
                    } catch (ClassNotFoundException e) {
                        fail("class " + line + " not found", e);
                    }

                    if (!service.isAssignableFrom(clazz)) {
                        fail("class " + clazz.getName() + "is not subtype of " + service.getName() + ",SPI configuration file=" + fullFileName);
                    }

                    classList.add(clazz);
                    //读取子类上的配置
                    Spi spi = clazz.getAnnotation(Spi.class);
                    String aliasName = spi == null || "".equals(spi.value()) ? clazz.getName() : spi.value();
                    if (classMap.containsKey(aliasName)) {
                        Class<? extends S> existClass = classMap.get(aliasName);
                        fail("Found repeat alias name for " + clazz.getName() + " and "
                                + existClass.getName() + ",SPI configuration file=" + fullFileName);
                    }
                    classMap.put(aliasName, clazz);

                    if (spi != null && spi.isDefault()) {
                        if (defaultClass != null) {
                            fail("Found more than one default Provider, SPI configuration file=" + fullFileName);
                        }
                        defaultClass = clazz;
                    }

                    RecordLog.info("[SpiLoader] Found SPI implementation for SPI {}, provider={}, aliasName={}"
                            + ", isSingleton={}, isDefault={}, order={}",
                        service.getName(), line, aliasName
                            , spi == null ? true : spi.isSingleton()
                            , spi == null ? false : spi.isDefault()
                            , spi == null ? 0 : spi.order());
                }
            } catch (IOException e) {
                fail("error reading SPI configuration file", e);
            } finally {
                closeResources(in, br);
            }
        }
        //子类排序
        sortedClassList.addAll(classList);
        Collections.sort(sortedClassList, new Comparator<Class<? extends S>>() {
            @Override
            public int compare(Class<? extends S> o1, Class<? extends S> o2) {
                Spi spi1 = o1.getAnnotation(Spi.class);
                int order1 = spi1 == null ? 0 : spi1.order();

                Spi spi2 = o2.getAnnotation(Spi.class);
                int order2 = spi2 == null ? 0 : spi2.order();

                return Integer.compare(order1, order2);
            }
        });
    }

4. 业务项目有机会使用SPI吗

因为现在Spring一统天下,只针对业务系统来说,Spring的bean单例池+依赖注入集合对象的方式可以找到某个接口的在Spring容器中的所有实现子类。故SPI这种服务发现的方式其实用到的机会不是很多。

但是脱离Spring的一些框架,使用SPI方法找到接口对应的所有子类的方式是比较常见的。例如dubbo、sentinel、seata等框架。了解SPI的对阅读源码或者自己造轮子是非常有帮助的。

4.1 业务项目常用—spring方式

项目常用的实现方式:实现服务的发现。

@Service
@Slf4j
public class CommonInfoServiceImpl implements InfoService{

    private Map<TypeEnum, ContentItemService> serviceMap = new HashMap<>();

    /**
     * 构造器注入,注入Spring容器中ContentItemService接口所有的子类Bean。并将其放入到Map缓存中。
     * key:{@link TypeEnum},value:子类Bean。
     */
    @Autowired
    public CommonOcrItemInfoServiceImpl(List<ContentItemService> contentItemList) {
        for (ContentItemService contentItemService : contentItemList) {
            List<TypeEnum> typeEnums = contentItemService.getTypeEnum();
            for (TypeEnum typeEnum : typeEnums) {
                serviceMap.put(sourceTypeEnum, contentItemService);
            }
        }
    }
}

接口类:

public interface ContentItemService {
    //业务逻辑的实现类
    List<ItemInfoResp> listItemId(List<ItemInfoRequestVO> vo);
    //子类对应的枚举对象
    List<TypeEnum> getTypeEnum();
}

项目启动后,会加载Spring容器中的子类对象,组合成Map,根据参数的TypeEnum值的不同,选择合适的子类完成业务逻辑。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,607评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,047评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,496评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,405评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,400评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,479评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,883评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,535评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,743评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,544评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,612评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,309评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,881评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,891评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,136评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,783评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,316评论 2 342

推荐阅读更多精彩内容