美文网首页面试精选Java技术升华方案
如果你不看这一篇,那么SPI你永远一知半解!

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

作者: 小胖学编程 | 来源:发表于2021-08-11 21:51 被阅读0次
  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值的不同,选择合适的子类完成业务逻辑。

相关文章

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

    网上回答:SPI是什么? 思考:类加载器与SPI2.1 类加载器知识2.2 SPI与线程上下文加载器2.3 需要S...

  • 如果你只在乎…

    如果你只在乎美貌,那么你永远在叹息美貌的存否; 如果你只在乎金钱,那么你永远在惋惜金钱的来去; 如果你只在乎珍馐,...

  • 正因为有无常

    如果生命是有常的,那么你肯定就不想活了。 比如,你现在正在疾病中,那么你将永远病着。 如果你现在失业,你将永远找不...

  • 《心智力:如何从破产者变成亿万富翁》: Never give u

    如果你不能飞行,那么可以奔跑。如果你不能奔跑,那么可以行走。如果你不能行走,那么可以爬行。但无论做什么,你永远必须...

  • 世界上最大的谎言是:你不行

    很多事情,如果你当时没有去做,那么可能你永远也不会去做了;很多人,如果你当时没有同他交往,那么可能你即将永远错过他...

  • 《不由分说爱上这世界》—黄佟佟(摘抄)

    1 永远活得兴致勃勃。 如果你有力量,那么我愿你永远睿智犀利,世事洞若观火;如果你有爱,那么我愿意你自带那爱的剧本...

  • Python基础14

    第八章 函数 8.1 函数是什么 这一小节可以不看 从这一篇教程开始,我会让你明白Python没有你想象中的那么简...

  • 手账控选择本皮,就看这3点

    上一篇,我们提过了如何安排纸质笔记和电子笔记。如果你选纸质笔记,不看重外皮,这篇可以不看了。如果你和我一样,曾经纠...

  • 等你到35岁~念南康

    这一年你已经35岁了,我才认识你。 虽然你永远停在28岁,但是如果你的灵魂从未离去的话,那么你可以走了。江水那...

  • 认清自我

    如果不能克服自我迷恋,那么“你永远不会得到满足,你永远不会自由。” ...

网友评论

    本文标题:如果你不看这一篇,那么SPI你永远一知半解!

    本文链接:https://www.haomeiwen.com/subject/hhbcbltx.html