专注于 JetBrains IDEA 全家桶,永久激活,教程
持续更新 PyCharm,IDEA,WebStorm,PhpStorm,DataGrip,RubyMine,CLion,AppCode 永久激活教程

SPI机制详解:Java插件扩展原理与ServiceLoader使用实践

什么是SPI机制?

SPI(Service Provider Interface),是JDK内置的一种服务提供发现机制,可以用于启用框架扩展和替换组件

如:java.sql.Driver接口,其他不同厂商可以针对同一接口做出不同的实现

Java中SPI机制的主要思想就是将装配的控制权移到程序之外,在模块化设计中,这个机制尤其重要,其核心思想就是解耦

img_1

SPI简单案例

目录结构:

img_2

第一步:创建一个Search接口

package com.comtom.spi;
import java.util.List;
public interface Search {
public List<String> searchDoc(String keyword);
}

第二步:创建“文件搜索”实现类

package com.comtom.spi;
import java.util.List;
public class FileSearch implements Search{
@Override
public List<String> searchDoc(String keyword) {
    System.out.println("文件搜索"+keyword);
    return null;
}
}

第三步:创建“数据库搜索”实现类

package com.comtom.spi;
import java.util.List;
public class DataBaseSearch implements Search{
@Override
public List<String> searchDoc(String keyword) {
    System.out.println("数据库搜索"+keyword);
    return null;
}
}

第四步:在resource目录下创建目录/META-INF/services,在该目录下创建Search类的全限定名路径文件,如下图所示:

img_3

注:IDEA需要选择文件类型,右键选择Override File Type,弹框Choose File Type,选择textmate

img_4

com.comtom.spi.Search文件中添加FileSearch类的全限定名或DataBaseSearch类的全限定名

com.comtom.spi.FileSearch
com.comtom.spi.DataBaseSearch

开始测试

public class Main {
public static void main(String[] args) {
    ServiceLoader<Search> s = ServiceLoader.load(Search.class);
    Iterator<Search> iterator = s.iterator();
    while (iterator.hasNext()) {
        Search next = iterator.next();
        next.searchDoc("hello world");
    }
}
}

结果:在com.comtom.spi.Search文件添加几个类全限定名,就会有几条结果数据

文件搜索hello world
数据库搜索hello world

SPI机制的应用

JDBC DriverManager

在JDBC4.0之前,连接数据库需要使用Class.forName("com.mysql.jdbc.Driver")先加载数据库相关的驱动,然后再获取连接等操作,在JDBC4.0以后,不需要使用Class.forName("com.mysql.jdbc.Driver")加载驱动,就可以直接获取连接,这种方式就是使用了SPI机制

1、 Java在rt.jar包中定义了java.sql.Driver接口,没有具体的实现,类似于上述案例中的Search接口
2、 在mysql的jar包mysql-connector-java-5.1.49.jar中,有META-INF/services目录,里面的文件定义了驱动实现类,类似于上述案例的FileSearch和DataBaseSearch

img_5

1、 在建立连接时,就会在加载类的时候加载驱动

img_6
2、 loadInitialDrivers()方法就会调用Driver.class对应的权限定名文件,读取对应实现类,类似测试时的操作

img_7

loadInitialDrivers()方法实现步骤如下:

**1、** 从系统变量中获取有关驱动的定义  
**2、** 使用SPI来获取驱动的实现  
**3、** 遍历使用SPI获取到的具体实现,实例化各个实现类  
**4、** 根据第一步获取到的驱动列表来实例化具体实现类  

SPI机制的实现步骤:

1、 ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);封装接口类型和类加载器,并初始化了一个迭代器
2、 Iterator<Driver> driversIterator = loadedDrivers.iterator();获取迭代器
3、 遍历迭代器,调用driversIterator.hasNext()方法时会去resource目录下所有的META-INF/services目录下的java.sql.Driver文件,并找到文件中的实现类的名字
4、 调用driversIterator.next();方法,此时就会根据驱动名字具体实例化各个实现类

Common Logging

common-logging(也称为Jakarta Commons Logging,JCL)是常用的日志库门面

JCL依赖

<!--引入common-logging-->
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.2</version>
</dependency>

日志的实例是通过LogFactory的getLog()方法创建的:

public static getLog(Class clazz) throws LogConfigurationException {
return getFactory().getInstance(clazz);
}

分析getFactory方法做了什么

public static LogFactory getFactory() throws LogConfigurationException {
     //获取一个类加载器
    ClassLoader contextClassLoader = getContextClassLoaderInternal();
    //如果类加载器为空,且经过诊断确认,则输出类加载器为空
    if (contextClassLoader == null && isDiagnosticsEnabled()) {
        logDiagnostic("Context classloader is null.");
    }
    //返回这个类加载器注册过的日志工厂
    LogFactory factory = getCachedFactory(contextClassLoader);
    //如果注册过,直接返回,没有注册过,
    if (factory != null) {
        return factory;
    } else {
        if (isDiagnosticsEnabled()) {
            logDiagnostic("[LOOKUP] LogFactory implementation requested for the first time for context classloader " + objectId(contextClassLoader));
            logHierarchy("[LOOKUP] ", contextClassLoader);
        }
        //加载properties文件
        Properties props = getConfigurationFile(contextClassLoader, "commons-logging.properties");
        ClassLoader baseClassLoader = contextClassLoader;
        String factoryClass;
        if (props != null) {
            //如果commons-logging.properties配置文件存在,且文件中配置了use_tccl参数,参数值为false,则将当前类加载器赋值给获取到的日志类加载器
            factoryClass = props.getProperty("use_tccl");
            if (factoryClass != null && !Boolean.valueOf(factoryClass)) {
                baseClassLoader = thisClassLoader;
            }
        }
        //决定使用哪个factory
        //首先尝试查找vm系统中org.apache.commons.logging.LogFactory,并判断其是否可以指定为factory
        if (isDiagnosticsEnabled()) {
            logDiagnostic("[LOOKUP] Looking for system property [org.apache.commons.logging.LogFactory] to define the LogFactory subclass to use...");
        }
        try {
            factoryClass = getSystemProperty("org.apache.commons.logging.LogFactory", (String)null);
            if (factoryClass != null) {
                if (isDiagnosticsEnabled()) {
                    logDiagnostic("[LOOKUP] Creating an instance of LogFactory class '" + factoryClass + "' as specified by system property " + "org.apache.commons.logging.LogFactory");
                }
                factory = newFactory(factoryClass, baseClassLoader, contextClassLoader);
            } elseif (isDiagnosticsEnabled()) {
                logDiagnostic("[LOOKUP] No system property [org.apache.commons.logging.LogFactory] defined.");
            }
        } catch (SecurityException var9) {
            if (isDiagnosticsEnabled()) {
                logDiagnostic("[LOOKUP] A security exception occurred while trying to create an instance of the custom factory class: [" + trim(var9.getMessage()) + "]. Trying alternative implementations...");
            }
        } catch (RuntimeException var10) {
            if (isDiagnosticsEnabled()) {
                logDiagnostic("[LOOKUP] An exception occurred while trying to create an instance of the custom factory class: [" + trim(var10.getMessage()) + "] as specified by a system property.");
            }
            throw var10;
        }
          //然后开始尝试使用java spi服务发现机制,在META-INF/services下寻找org.apache.commons.logging.LogFactory实现
        String factoryClassName;
        if (factory == null) {
            if (isDiagnosticsEnabled()) {
                logDiagnostic("[LOOKUP] Looking for a resource file of name [META-INF/services/org.apache.commons.logging.LogFactory] to define the LogFactory subclass to use...");
            }
            try {
                InputStream is = getResourceAsStream(contextClassLoader, "META-INF/services/org.apache.commons.logging.LogFactory");
                if (is != null) {
                    BufferedReader rd;
                    try {
                        rd = new BufferedReader(new InputStreamReader(is, "UTF-8"));
                    } catch (UnsupportedEncodingException var7) {
                        rd = new BufferedReader(new InputStreamReader(is));
                    }
                    factoryClassName = rd.readLine();
                    rd.close();
                    if (factoryClassName != null && !"".equals(factoryClassName)) {
                        if (isDiagnosticsEnabled()) {
                            logDiagnostic("[LOOKUP]  Creating an instance of LogFactory class " + factoryClassName + " as specified by file '" + "META-INF/services/org.apache.commons.logging.LogFactory" + "' which was present in the path of the context classloader.");
                        }
                        factory = newFactory(factoryClassName, baseClassLoader, contextClassLoader);
                    }
                } elseif (isDiagnosticsEnabled()) {
                    logDiagnostic("[LOOKUP] No resource file with name 'META-INF/services/org.apache.commons.logging.LogFactory' found.");
                }
            } catch (Exception var8) {
                if (isDiagnosticsEnabled()) {
                    logDiagnostic("[LOOKUP] A security exception occurred while trying to create an instance of the custom factory class: [" + trim(var8.getMessage()) + "]. Trying alternative implementations...");
                }
            }
        }
        //尝试从classpath根目录下的commons-logging.properties中查找org.apache.commons.logging.LogFactory属性指定的factory
        if (factory == null) {
            if (props != null) {
                if (isDiagnosticsEnabled()) {
                    logDiagnostic("[LOOKUP] Looking in properties file for entry with key 'org.apache.commons.logging.LogFactory' to define the LogFactory subclass to use...");
                }
                factoryClass = props.getProperty("org.apache.commons.logging.LogFactory");
                if (factoryClass != null) {
                    if (isDiagnosticsEnabled()) {
                        logDiagnostic("[LOOKUP] Properties file specifies LogFactory subclass '" + factoryClass + "'");
                    }
                    factory = newFactory(factoryClass, baseClassLoader, contextClassLoader);
                } elseif (isDiagnosticsEnabled()) {
                    logDiagnostic("[LOOKUP] Properties file has no entry specifying LogFactory subclass.");
                }
            } elseif (isDiagnosticsEnabled()) {
                logDiagnostic("[LOOKUP] No properties file available to determine LogFactory subclass from..");
            }
        }
        //最后,使用后备factory实现,org.apache.commons.logging.impl.LogFactoryImpl
        if (factory == null) {
            if (isDiagnosticsEnabled()) {
                logDiagnostic("[LOOKUP] Loading the default LogFactory implementation 'org.apache.commons.logging.impl.LogFactoryImpl' via the same classloader that loaded this LogFactory class (ie not looking in the context classloader).");
            }
            factory = newFactory("org.apache.commons.logging.impl.LogFactoryImpl", thisClassLoader, contextClassLoader);
        }

        if (factory != null) {
            cacheFactory(contextClassLoader, factory);
            if (props != null) {
                Enumeration names = props.propertyNames();
                while(names.hasMoreElements()) {
                    String name = (String)names.nextElement();
                    factoryClassName = props.getProperty(name);
                    factory.setAttribute(name, factoryClassName);
                }
            }
        }
        return factory;
    }
}

logFactory()方法实现步骤:

1、 从VM系统属性中查找org.apache.commons.logging.LogFactory
2、 使用SPI服务发现机制,发现org.apache.commons.logging.LogFactory的实现
3、 查找classpath根目录下的commons-logging.properties的org.apache.commons.logging.LogFactory属性是否指定factory实现
4、 使用默认factory实现,org.apache.commons.logging.impl.LogFactoryImpl

总结

LogFactory抽象类中的getLog()方法返回值类型为Log接口,提供了所有日志相关的方法,因此如果日志实现者需要提供一个日志系统,只需要实现该接口,并且使用继承自org.apache.commons.logging.LogFactory的子类创建Log,必然可以实现一个松耦合的日志系统

Spring

在springboot的自动装配过程中,最终会加载META-INF/spring.factories文件,而加载的过程是由SpringFactoriesLoader加载的。从CLASSPATH下的每个Jar包中搜寻所有META-INF/spring.factories配置文件,然后将解析properties文件,找到指定名称的配置后返回。需要注意的是,其实这里不仅仅是会去ClassPath路径下查找,会扫描所有路径下的Jar包,只不过这个文件只会在Classpath下的jar包中。

public staticfinal String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
// spring.factories文件的格式为:key=value1,value2,value3
// 从所有的jar包中找到META-INF/spring.factories文件
// 然后从文件中解析出key=factoryClass类名称的所有value值
public static List<String> loadFactoryNames(Class<?> factoryClass, ClassLoader classLoader) {
String factoryClassName = factoryClass.getName();
// 取得资源文件的URL
Enumeration<URL> urls = (classLoader != null ? classLoader.getResources(FACTORIES_RESOURCE_LOCATION) : ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
List<String> result = new ArrayList<String>();
// 遍历所有的URL
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
// 根据资源文件URL解析properties文件,得到对应的一组@Configuration类
Properties properties = PropertiesLoaderUtils.loadProperties(new UrlResource(url));
String factoryClassNames = properties.getProperty(factoryClassName);
// 组装数据,并返回
result.addAll(Arrays.asList(StringUtils.commaDelimitedListToStringArray(factoryClassNames)));
}
return result;
}

插件

SPI机制使用最多的就是插件开发,SPI机制提供了一个万物皆可插件的思想

Eclipse使用OGSI作为插件系统的基础,动态添加新的插件,停止现有插件,以动态的方式管理组件的声明周期

插件的文件结构

在指定目录下包含以下三个文件:

  • META-INF/MANIFEST.MF: 项目基本配置信息,版本、名称、启动器等
  • build.properties: 项目的编译配置信息,包括,源代码路径、输出路径
  • plugin.xml:插件的操作配置信息,包含弹出菜单及点击菜单后对应的操作执行类等

当eclipse启动时,会遍历plugins文件夹的目录,扫描每个插件的清单文件MANIFEST.MF,并建立一个内部模型来记录它所找到的每一个插件的信息,这就实现了动态添加新的插件,eclipse不需要知道插件如何开发,只需要在启动时根据配置文件解析、加载到系统里就可以了,这就是SPI机制的体现

SPI深入理解

SPI的使用

现实生产中的使用步骤:

1、 由某组织或公司定义标准,就是定义接口,例如java.sql.Driver
2、 由具体的厂商或框架开发者实现接口
3、 在META-INF/services目录下定义一个名字为接口全限定名的文件,文件内容是具体的实现类文件名

实现接口,implements接口
4、 引用jar包来实现具体功能,完成定制业务

SPI和API的区别

SPI:接口位于调用方所在的包中

理解:

1、 由A先定义标准,A并不知道其他人怎么实现
2、 因为有了ServiceLoader,SPI服务提供发现机制,B直接拿A的标准来实现,C也拿A的标准来实现,可能存在多个不同的实现,甚至A都会拿自己的标准来实现
3、 调用方不关心这些不同的调用,直接利用ServiceLoader.load()调用标准接口,这样每换一个厂商,代码也不需要跟着改 松耦合思想

API(应用程序接口):接口位于实现方所在的包中

理解:

1、 由A定义标准,A自己实现
2、 没有其他开发厂商,调用方仅仅依赖并直接使用,无权选择具体不同的实现

SPI的实现原理

//实现Iterable接口,用于遍历所有的服务实现类
publicfinalclass ServiceLoader<S>
implements Iterable<S>
{
//查找配置文件的目录
privatestaticfinal String PREFIX = "META-INF/services/";
//表示被加载的服务的类或接口
privatefinal Class<S> service;
//类加载器,用于定位、加载、实例化服务提供者
privatefinal ClassLoader loader;
//访问控制上下文
privatefinal AccessControlContext acc;
//缓存已经被实例化的服务提供者,按照实例化的顺序存储
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
//迭代器
private LazyIterator lookupIterator;
//重新加载;相当于重新创建ServiceLoader,用于新的服务提供者安装到正在运行的java虚拟机中的情况
public void reload() {
    //清空缓存中所有已经实例化的服务提供者
    providers.clear();
    //新建一个迭代器,该迭代器会重新遍历查找和实例化服务提供者
    lookupIterator = new LazyIterator(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();
}
//解析失败处理的方法
private static void fail(Class<?> service, String msg, Throwable cause)
    throws ServiceConfigurationError
{
    thrownew ServiceConfigurationError(service.getName() + ": " + msg,
                                        cause);
}
private static void fail(Class<?> service, String msg)
    throws ServiceConfigurationError
{
    thrownew ServiceConfigurationError(service.getName() + ": " + msg);
}
private static void fail(Class<?> service, URL u, int line, String msg)
    throws ServiceConfigurationError
{
    fail(service, u + ":" + line + ": " + msg);
}
//解析服务提供者配置文件中的一行
private int parseLine(Class<?> service, URL u, BufferedReader r, int lc,
                      List<String> names)
    throws IOException, ServiceConfigurationError
{
    String ln = r.readLine();
    if (ln == null) {
        return -1;
    }
    //首先去掉注释校验,然后保存
    int ci = ln.indexOf('#');
    if (ci >= 0) ln = ln.substring(0, ci);
    ln = ln.trim();
    int n = ln.length();
    if (n != 0) {
        if ((ln.indexOf(' ') >= 0) || (ln.indexOf('\t') >= 0))
            fail(service, u, lc, "Illegal configuration-file syntax");
        int cp = ln.codePointAt(0);
        if (!Character.isJavaIdentifierStart(cp))
            fail(service, u, lc, "Illegal provider-class name: " + ln);
        for (int i = Character.charCount(cp); i < n; i += Character.charCount(cp)) {
            cp = ln.codePointAt(i);
            if (!Character.isJavaIdentifierPart(cp) && (cp != '.'))
                fail(service, u, lc, "Illegal provider-class name: " + ln);
        }
        //重复的配置项和已经被实例化的配置项不会被保存
        if (!providers.containsKey(ln) && !names.contains(ln))
            names.add(ln);
    }
    //返回下一行行号
    return lc + 1;
}
//解析配置文件,解析指定的url配置文件
private Iterator<String> parse(Class<?> service, URL u)
    throws ServiceConfigurationError
{
    InputStream in = null;
    BufferedReader r = null;
    ArrayList<String> names = new ArrayList<>();
    try {
        in = u.openStream();
        r = new BufferedReader(new InputStreamReader(in, "utf-8"));
        int lc = 1;
        //使用parseLine方法进行解析,未被实例化的服务提供者会被保存到缓存中去
        while ((lc = parseLine(service, u, r, lc, names)) >= 0);
    } catch (IOException x) {
        fail(service, "Error reading configuration file", x);
    } finally {
        try {
            if (r != null) r.close();
            if (in != null) in.close();
        } catch (IOException y) {
            fail(service, "Error closing configuration file", y);
        }
    }
    return names.iterator();
}
//服务提供者查找的迭代器
privateclass LazyIterator
    implements Iterator<S>
{
    Class<S> service;//服务提供者接口
    ClassLoader loader;//类加载器
    Enumeration<URL> configs = null;//保存实现类的url
    Iterator<String> pending = null;//保存实现类的全名
    String nextName = null;//迭代器中下一个实现类的全名
    private LazyIterator(Class<S> service, ClassLoader loader) {
        this.service = service;
        this.loader = loader;
    }
    private boolean hasNextService() {
        if (nextName != null) {
            returntrue;
        }
        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()) {
                returnfalse;
            }
            pending = parse(service, configs.nextElement());
        }
        nextName = pending.next();
        returntrue;
    }
    private S nextService() {
        if (!hasNextService())
            thrownew 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);
        }
        thrownew 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() {
        thrownew UnsupportedOperationException();
    }
}
//获取迭代器
public Iterator<S> iterator() {
    //返回遍历服务提供者的迭代器
    returnnew Iterator<S>() {
        //以懒加载的方式加载可用的服务提供者
        //懒加载的实现是:解析配置文件和实例化服务提供者的工作由迭代器本身完成
        //按照实例化顺序返回已经缓存的服务提供者实例
        Iterator<Map.Entry<String,S>> knownProviders
            = providers.entrySet().iterator();
        public boolean hasNext() {
            if (knownProviders.hasNext())
                returntrue;
            return lookupIterator.hasNext();
        }
        public S next() {
            if (knownProviders.hasNext())
                return knownProviders.next().getValue();
            return lookupIterator.next();
        }
        public void remove() {
            thrownew UnsupportedOperationException();
        }
    };
}
//为指定的服务使用指定的类加载器来创建一个ServiceLoader
publicstatic <S> ServiceLoader<S> load(Class<S> service,
                                        ClassLoader loader)
{
    returnnew ServiceLoader<>(service, loader);
}
//使用线程上下文的类加载器来创建ServiceLoader
publicstatic <S> ServiceLoader<S> load(Class<S> service) {
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}
//使用扩展类加载器为指定的服务创建ServiceLoader
//只能找到并加载已经安装到当前Java虚拟机中的服务提供者,应用程序类路径中的服务提供者将被忽略
publicstatic <S> ServiceLoader<S> loadInstalled(Class<S> service) {
    ClassLoader cl = ClassLoader.getSystemClassLoader();
    ClassLoader prev = null;
    while (cl != null) {
        prev = cl;
        cl = cl.getParent();
    }
    return ServiceLoader.load(service, prev);
}

public String toString() {
    return"java.util.ServiceLoader[" + service.getName() + "]";
}
}

源码说明:

  • ServiceLoader实现了Iterable接口,所以它具有迭代器的属性,这里主要是实现了迭代器的hasNextnext方法,这里的迭代器lookupIterator是懒加载迭代器(LazyIterator)
  • LazyIterator中的hasNext方法,静态变量PREFIX就是"META-INF/services/"目录,这也就是需要在classpath下的"META-INF/services/"目录里创建一个以服务接口命名的文件的原因
  • 通过反射加载类对象(Class.forName()),并用newInstance方法对类进行实例化,然后将实例化后的对象缓存到providers对象中,providersLinkedHashMap<String,S>类型的,S为接口规范类似上述SPI简单案例中的Search接口,然后返回实例对象s(多态,类似于Search s=new FileSearch())

所以这也是为什么ServiceLoader不是实例化以后就去读取配置文件中的具体实现,并进行实例化。 而是等到使用迭代器去遍历的时候,才会加载对应的配置文件去解析,调用hasNext方法的时候会去加载配置文件进行解析,调用next方法的时候进行实例化并缓存

SPI机制的缺陷

  • 不能按需加载,需要遍历所有实现,并全部实例化,然后在循环中才能找到我们需要的实现

如果不想用某些实现类,或者某些类实例化很耗时,这就造成了资源的浪费

  • 获取某个实现类的方式不够灵活,只能通过Iterator迭代器获取,不能根据某个参数来获取对应的实现类
  • 多个并发多线程使用ServiceLoader类的实例是不安全的
未经允许不得转载:搜云库 » SPI机制详解:Java插件扩展原理与ServiceLoader使用实践

JetBrains 全家桶,激活、破解、教程

提供 JetBrains 全家桶激活码、注册码、破解补丁下载及详细激活教程,支持 IntelliJ IDEA、PyCharm、WebStorm 等工具的永久激活。无论是破解教程,还是最新激活码,均可免费获得,帮助开发者解决常见激活问题,确保轻松破解并快速使用 JetBrains 软件。获取免费的破解补丁和激活码,快速解决激活难题,全面覆盖 2024/2025 版本!

联系我们联系我们