Java SPI 机制初探|得物技术

一,概述

什么是 SPI

SPI 即 Service Provider Interface ,也就是“服务提供者的接口”。

SPI 将服务接口和具体的服务实现分离开来,将服务调用方和服务实现者解耦,能够提升程序的扩展性、可维护性。同时,修改或者替换服务的实现不需要修改调用方。

Java 中有许多地方都使用到了 SPI 机制,比如数据库加载驱动 JDBC、Spring、以及Dubbo 的扩展实现等。

对比 API 有什么区别

API:接口实现方同时负责接口定义和接口实现,接口控制权在服务提供方。

SPI:服务调用方负责接口定义,不同的接口实现方根据接口定义可以有不同的实现,能够在运行时动态的加载不用实现类,接口控制权在服务调用方。

SPI 有什么用

解耦

在框架开发中,通常需要依赖一些可插拔的功能,但不希望实现具体的适配(能够保持灵活性)。SPI 机制通过定义接口和动态加载实现 ,可以让框架与服务实现解耦。

※ 场景

  • 一个数据库连接池库需要支持多个数据库实现(如 MySQL、PostgreSQL),可以通过 SPI 机制动态加载这些数据库驱动。

日志框架(比如 SLF4J)的具体实现,可以通过 SPI 机制加载不同的日志库(如 Log4j、Logback)。

可扩展

SPI 机制提供了动态发现和加载服务的能力,可以让应用程序非常方便地实现扩展,而不需要修改现有代码。

※ 场景

一个文件处理系统需要支持不同的文件格式(如 JSON 或 XML)。通过 SPI 机制可以动态发现不同的文件解析器插件,无需提前硬编码支持的格式。

动态加载

SPI 可以用来实现插件化架构,通过动态加载具体的服务实现增减模块,而无需重新发布整个系统。

※ 场景

  • Web 服务器(如 Tomcat)可以通过 SPI 机制动态加载不同的 HTTP 处理器或过滤器。

  • 数据分析系统可以通过 SPI 机制动态加载新的分析算法。

SPI 工作机制

二,ServiceLoader

ServiceLoader 是 JDK 中提供的服务加载类,位于 java.util 包下,final 修饰不可被继承,是实现 SPI 机制的核心。

源码解析

public final class ServiceLoader<S>    implements Iterable<S>{    // 默认加载路径前缀    private static final String PREFIX = "META-INF/services/";       // The class or interface representing the service being loaded    // 被加载的实例或接口    private final Class<S> service;        // The class loader used to locate, load, and instantiate providers    // 类加载器    private final ClassLoader loader;       // The access control context taken when the ServiceLoader is created    private final AccessControlContext acc;        // Cached providers, in instantiation order    // 本地缓存,key: 类名 value;类    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();      // The current lazy-lookup iterator    // 迭代器    private LazyIterator lookupIterator;}

※ load 方法

// 暴露给外部使用的加载方法public static <S> ServiceLoader<S> load(Class<S> service,                                        ClassLoader loader){    return new ServiceLoader<>(service, loader);}public static <S> ServiceLoader<S> load(Class<S> service) {    ClassLoader cl = Thread.currentThread().getContextClassLoader();    return ServiceLoader.load(service, cl);}
// 构造方法私有化private ServiceLoader(Class<S> svc, ClassLoader cl) {    service = Objects.requireNonNull(svc, "Service interface cannot be null");    // 如果指定了claseLoader,则使用该classLoader,如果没有指定,则使用默认classLoader    loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;    acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;    reload();}

※ reload 方法

public void reload() {    // 清空缓存,将linkedHashMap的头和尾置为null    providers.clear();    lookupIterator = new LazyIterator(service, loader);}

ServiceLoader 实现了 Iterable 接口的方法后,拥有了迭代的能力,在这个 iterator 方法被调用时,首先会在 ServiceLoader 的 Provider 缓存中进行查找,如果缓存中没有命中,则在 LazyIterator 中进行查找。

public Iterator<S> iterator() {    return new Iterator<S>() {        // 本地缓存providers        Iterator<Map.Entry<String,S>> knownProviders            = providers.entrySet().iterator();               public boolean hasNext() {            // 优先查本地缓存            if (knownProviders.hasNext())                return true;            // 没有则在LazyInterator中进行查找            return lookupIterator.hasNext();        }              public S next() {            if (knownProviders.hasNext())                return knownProviders.next().getValue();            return lookupIterator.next();        }             public void remove() {            throw new UnsupportedOperationException();        }        };}

LazyIterator 使 ServiceLoader 拥有了懒加载的能力,只有调用 iterator 方法或遍历的时候才会去加载对应的实现类,核心代码如下:

// 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;    }        private boolean hasNextService() {        if (nextName != null) {            return true;        }        if (configs == null) {            try {            // 配置文件路径,默认 /META-INF/services/ 开头                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;    }        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();    }
}

小结

ServiceLoader 本质上就是读取约定目录(/META-INF/services/ )下对应接口全限定命名的文件,然后通过反射全量加载文件中定义的所有接口实现类,从而将接口与实现进行解耦。

※ 优点

解耦 :接口与实现分离,无需在代码中硬编码实现类。

扩展性 :新增实现只需添加配置,无需修改已有代码。

※ 缺点

线程不安全 : ServiceLoader 非线程安全,不能保证单例。

性能开销 :每次迭代都重新加载文件(可通过缓存解决),并且会全量配置文件中指定的所有实现类。

无健壮性 :配置错误(如类未找到)会抛出异常而非优雅降级。

三,SPI 实际应用场景

JDBC

JDK 中定义了 Driver 接口,用于连接数据库。

package java.sql;
import java.util.logging.Logger;
public interface Driver {       Connection connect(String url, java.util.Properties info)        throws SQLException;       boolean acceptsURL(String url) throws SQLException;    DriverPropertyInfo[] getPropertyInfo(String url, java.util.Properties info)                         throws SQLException;    int getMajorVersion();    int getMinorVersion();    boolean jdbcCompliant();    public Logger getParentLogger() throws SQLFeatureNotSupportedException;}

不同的数据库厂商实现这个接口,从而实现与数据库的连接,以我们最熟悉的 MySQL 为例,获取数据库连接的示例代码如下:

// 获取数据库连接DriverManager connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "123");// 创建statementStatement statement = connection.createStatement();// 执行sqlResultSet resultSet = statement.executeQuery("select * from student");

其中 DriverManager 加载时会执行静态代码块去加载 driver,部分核心代码如下:

static {    loadInitialDrivers();    println("JDBC DriverManager initialized");}
private static void loadInitialDrivers() {    String drivers;    try {        drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {            public String run() {                return System.getProperty("jdbc.drivers");            }        });    } catch (Exception ex) {        drivers = null;    }        // 如果驱动程序被打包为服务提供者(Service Provider),则加载它。    // 通过类加载器获取所有驱动程序    AccessController.doPrivileged(new PrivilegedAction<Void>() {        public Void run() {            // 这里就使用ServiceLoader去加载接口实现类            // 以mysql-connector-java为例,加载的是 com.mysql.jdbc.Driver 和 com.mysql.fabric.jdbc.FabricMySQLDriver            ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);            Iterator<Driver> driversIterator = loadedDrivers.iterator();            try{                while(driversIterator.hasNext()) {                    driversIterator.next();                }            } catch(Throwable t) {            // Do nothing            }            return null;        }    });        println("DriverManager.initialize: jdbc.drivers = " + drivers);        if (drivers == null || drivers.equals("")) {        return;    }    String[] driversList = drivers.split(":");    println("number of Drivers:" + driversList.length);    for (String aDriver : driversList) {        try {            println("DriverManager.Initialize: loading " + aDriver);            // 加载数据库驱动Driver, 以mysql-connector-java为例,这里加载的是 com.mysql.jdbc.Driver 和 com.mysql.fabric.jdbc.FabricMySQLDrive            // 并注册到registeredDrivers中            Class.forName(aDriver, true,                    ClassLoader.getSystemClassLoader());        } catch (Exception ex) {            println("DriverManager.Initialize: load failed: " + ex);        }    }}

随后在 getConnection 方法中遍历已经加载的 driver,执行 connect 方法进行连接。

private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();
// 遍历已经注册的driver,调用connect方法进行连接for(DriverInfo aDriver : registeredDrivers) {     if(isDriverAllowed(aDriver.driver, callerCL)) {        try {            println("    trying " + aDriver.driver.getClass().getName());            Connection con = aDriver.driver.connect(url, info);            if (con != null) {                // Success!                println("getConnection returning " + aDriver.driver.getClass().getName());                return (con);            }        } catch (SQLException ex) {            if (reason == null) {                reason = ex;            }        }     } else {        println("    skipping: " + aDriver.getClass().getName());    }
}

public class Driver extends NonRegisteringDriver implements java.sql.Driver {    public Driver() throws SQLException {    }       static {        try {            DriverManager.registerDriver(new Driver());        } catch (SQLException var1) {            throw new RuntimeException("Can't register driver!");        }    }}

Spring

Spring 中没有直接使用 Java SPI 机制,不过 Spring 的 spring.factories 机制类似于 SPI 机制并拥有更强大的扩展机制,通过读取 META-INF/spring.factories 文件实现自动装配、上下文初始化等功能。

// SpringFactoriesLoader 源码核心逻辑public final class SpringFactoriesLoader {    public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
    public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {        // 从所有jar包的spring.factories文件加载配置        Enumeration<URL> urls = (classLoader != null ?                 classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :                 ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
        List<String> result = new ArrayList<>();        while (urls.hasMoreElements()) {            Properties properties = PropertiesLoaderUtils.loadProperties(                new UrlResource(urls.nextElement()));            String factoryClassNames = properties.getProperty(factoryType.getName());            // 支持逗号分隔的多个实现            result.addAll(Arrays.asList(StringUtils.commaDelimitedListToStringArray(factoryClassNames)));        }        return result;    }
    public static <T> List<T> loadFactories(Class<T> factoryType, @Nullable ClassLoader classLoader) {        // 1. 加载所有实现类名        List<String> names = loadFactoryNames(factoryType, classLoader);        // 2. 实例化所有实现        List<T> instances = new ArrayList<>(names.size());        for (String name : names) {            Class<?> instanceClass = ClassUtils.forName(name, classLoader);            instances.add((T) ReflectionUtils.accessibleConstructor(instanceClass).newInstance());        }        // 3. 按@Order排序        AnnotationAwareOrderComparator.sort(instances);        return instances;    }
    private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {    MultiValueMap<String, String> result = (MultiValueMap)cache.get(classLoader);    if (result != null) {        return result;    } else {        try {            Enumeration<URL> urls = classLoader != null ? classLoader.getResources("META-INF/spring.factories") : ClassLoader.getSystemResources("META-INF/spring.factories");            MultiValueMap<String, String> result = new LinkedMultiValueMap();               while(urls.hasMoreElements()) {                URL url = (URL)urls.nextElement();                UrlResource resource = new UrlResource(url);                Properties properties = PropertiesLoaderUtils.loadProperties(resource);                Iterator var6 = properties.entrySet().iterator();                      while(var6.hasNext()) {                    Map.Entry<?, ?> entry = (Map.Entry)var6.next();                    String factoryClassName = ((String)entry.getKey()).trim();                    String[] var9 = StringUtils.commaDelimitedListToStringArray((String)entry.getValue());                    int var10 = var9.length;                     for(int var11 = 0; var11 < var10; ++var11) {                        String factoryName = var9[var11];                        result.add(factoryClassName, factoryName.trim());                    }                }            }            // 读取META-INF/spring.factories文件夹下文件,解析文件内容并缓存到Map中            cache.put(classLoader, result);            return result;        } catch (IOException var13) {            IOException ex = var13;            throw new IllegalArgumentException("Unable to load factories from location [META-INF/spring.factories]", ex);        }    }}}

spring.factories 文件的格式为 xxx=xxxx,=号前面的 key 为接口全限定名,=号后面的 value 为接口实现类全限定名 ,多个实现类之间用逗号分割,下图中指定的 key 为 EnableAutoConfiguration,这样 spring 就会为=号后面的类注册为 bean。spring 自动装配原理这里暂不展开。

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\org.apache.hadoop.hbase.client.MonitorAlicloudHBaseAutoConfiguration

Dubbo

Dubbo 的可扩展机制也使用到了 SPI,不过不是原生的 SPI,而是经过优化的。

由上文 ServiceLoader 源码解析可知,SPI 会读取配置文件,遍历所有类并实例化,如果某些类并不会使用,此时还是会加载进来,造成了资源的浪费;Java SPI 配置文件中只是简单列出了所有扩展实现,没有给他们命名,无法在程序中准确引用;无法做到自动注入和装配等等…… Dubbo 扩展点机制优化了上述问题,提供了动态扩展的能力。

Demo

定义一个接口,并使用 @SPI 注解标注,这表明这个接口是一个扩展点,可被 Dubbo 的 ExtensionLoader 加载。

@SPIpublic interface DemoSpi {    void say();}

接口实现:

public class DemoSpiImpl implements DemoSpi {    public void say() {    }}

将实现类放在特定目录下,Dubbo 在加载扩展类的时候,会从 META-INF/services/ META-INF/dubbo/ META-INF/dubbo/internal/ 这几个目录下读取。这里在 META-INF/dubbo 目录下新建一个以 DemoSpi 接口名为文件名的文件,内容如下:

demoSpiImpl = com.xxx.xxx.DemoSpiImpl(为 DemoSpi 接口实现类的全类名)

接口实现类的全类名)使用如下:

public class DubboSPITest {     @Test        public void sayHello() throws Exception {        ExtensionLoader<DemoSpi> extensionLoader =             ExtensionLoader.getExtensionLoader(DemoSpi.class);        DemoSpi dmeoSpi = extensionLoader.getExtension("demoSpiImpl");        dmeoSpi.sayHello();    }}

源码解析

由上面的例子可以看出,Dubbo 主要通过 ExtensionLoader 加载配置文件中指定的实现类,整体流程上和 ServiceLoader 加载流程类似,同时做了相关的优化扩展。

※ duboo 加载扩展主要步骤

  • 读取并解析配置文件

  • 缓存所有扩展实现

  • 基于用户执行的扩展名,实例化对应的扩展实现

  • 进行扩展实例化属性的 IOC 注入及实例化扩展的包装类,实现 AOP 特性

SPI 加载固定扩展类的入口是 ExtensionLoader 的 getExtension 方法,该方法主要调用 createExtession 方法获取扩展实例:

private T createExtension(String name, boolean wrap) {    // 从配置文件中加载所有的拓展类,可得到“配置项名称”到“配置类”的映射关系表        Class<?> clazz = getExtensionClasses().get(name);    // 如果没有该接口的扩展,或者该接口的实现类不允许重复但实际上重复了,直接抛出异常        if (clazz == null || unacceptableExceptions.contains(name)) {        throw findException(name);    }    try {        T instance = (T) EXTENSION_INSTANCES.get(clazz);        // 这段代码保证了扩展类只会被构造一次,也就是单例的.                if (instance == null) {            EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.getDeclaredConstructor().newInstance());            instance = (T) EXTENSION_INSTANCES.get(clazz);        }        // 向实例中注入依赖                injectExtension(instance);        // 如果启用包装的话,则自动为进行包装.                // 比如我基于 Protocol 定义了 DubboProtocol 的扩展,但实际上在 Dubbo 中不是直接使用的 DubboProtocol, 而是其包装类  ProtocolListenerWrapper                if (wrap) {            List<Class<?>> wrapperClassesList = new ArrayList<>();                   if (cachedWrapperClasses != null) {                wrapperClassesList.addAll(cachedWrapperClasses);                            wrapperClassesList.sort(WrapperComparator.COMPARATOR);                Collections.reverse(wrapperClassesList);            }
            // 循环创建 Wrapper 实例                        if (CollectionUtils.isNotEmpty(wrapperClassesList)) {                for (Class<?> wrapperClass : wrapperClassesList) {                    Wrapper wrapper = wrapperClass.getAnnotation(Wrapper.class);                    if (wrapper == null                            || (ArrayUtils.contains(wrapper.matches(), name) && !ArrayUtils.contains(wrapper.mismatches(), name))) {                        // 将当前 instance 作为参数传给 Wrapper 的构造方法,并通过反射创建 Wrapper 实例。                        // 然后向 Wrapper 实例中注入依赖,最后将 Wrapper 实例再次赋值给 instance 变量                                                instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));                    }                }            }        }        // 初始化                initExtension(instance);        return instance;    } catch (Throwable t) {        throw new IllegalStateException("Extension instance (name: " + name + ", class: " +                type + ") couldn't be instantiated: " + t.getMessage(), t);    }}

※ 上述代码全流程

  • 通过 getExtensionClasses 获取所有的拓展类

  • 通过反射创建拓展对象

  • 向拓展对象中注入依赖

  • 将拓展对象包裹在相应的 Wrapper 对象中

  • 初始化扩展对象

获取所有拓展类 getExtensionClasses 和 JDK SPI 类似,也是先从缓存中拿,拿不到加锁再次查缓存,还拿不到则通过 loadExtensionClasses 加载拓展类。

private Map<String, Class<?>> getExtensionClasses() {    Map<String, Class<?>> classes = (Map)this.cachedClasses.get();    if (classes == null) {        synchronized(this.cachedClasses) {            classes = (Map)this.cachedClasses.get();            if (classes == null) {                classes = this.loadExtensionClasses();                this.cachedClasses.set(classes);            }        }    }     return classes;}

loadExtensionClasses 总共做了两件事情,一是解析 @SPI 注解,二是调用 loadDirectory 方法加载指定文件夹配置文件。

private Map<String, Class<?>> loadExtensionClasses() {    // 缓存默认的 SPI 扩展名        cacheDefaultExtensionName();     Map<String, Class<?>> extensionClasses = new HashMap<>();
    // 基于策略来加载指定文件夹下的文件        // 分别读取 META-INF/services/ META-INF/dubbo/ META-INF/dubbo/internal/ 这几个目录下的配置文件        for (LoadingStrategy strategy : strategies) {        loadDirectory(extensionClasses, strategy.directory(), type.getName(), strategy.preferExtensionClassLoader(), strategy.overridden(), strategy.excludedPackages());        loadDirectory(extensionClasses, strategy.directory(), type.getName().replace("org.apache", "com.alibaba"), strategy.preferExtensionClassLoader(), strategy.overridden(), strategy.excludedPackages());    }     return extensionClasses;}

其中 loadDirectory 方法则是拿到指定文件夹下文件的全路径名,调用 loadResource 去加载资源。

private void loadResource(Map<String, Class<?>> extensionClasses, ClassLoader classLoader, java.net.URL resourceURL, boolean overridden, String... excludedPackages) {    try {        BufferedReader reader = new BufferedReader(new InputStreamReader(resourceURL.openStream(), StandardCharsets.UTF_8));        Throwable var7 = null;               try {            String line;            try {            // 读取配置文件中每一行                while((line = reader.readLine()) != null) {                    // 只读取#前面的内容,#后面的为注释                    int ci = line.indexOf(35);                    if (ci >= 0) {                        line = line.substring(0, ci);                    }                                line = line.trim();                    if (line.length() > 0) {                        try {                            String name = null;                            int i = line.indexOf(61);                            if (i > 0) {                            // 分别读取=前的作为name,=后面的为具体的实现类全路径名                                name = line.substring(0, i).trim();                                line = line.substring(i + 1).trim();                            }                                              if (line.length() > 0 && !this.isExcluded(line, excludedPackages)) {                                this.loadClass(extensionClasses, resourceURL, Class.forName(line, true, classLoader), name, overridden);                            }                        } catch (Throwable var21) {                            Throwable t = var21;                            IllegalStateException e = new IllegalStateException("Failed to load extension class (interface: " + this.type + ", class line: " + line + ") in " + resourceURL + ", cause: " + t.getMessage(), t);                            this.exceptions.put(line, e);                        }                    }                }            } catch (Throwable var22) {                var7 = var22;                throw var22;            }        } finally {            //close ...                }    } catch (Throwable var24) {        Throwable t = var24;        logger.error("Exception occurred when loading extension class (interface: " + this.type + ", class file: " + resourceURL + ") in " + resourceURL, t);    }
}

loadResource 则主要读取配置文件中每一行,分为 key 和 value 两部分,key 为=前面的实现类别名,value 为=后面的实现类实例,加载实例的过程为 loadClass 方法,如下:

private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name,                       boolean overridden) throws NoSuchMethodException {    if (!type.isAssignableFrom(clazz)) {        throw new IllegalStateException("Error occurred when loading extension class (interface: " +                type + ", class line: " + clazz.getName() + "), class "                + clazz.getName() + " is not subtype of interface.");    }    // 检测目标类上是否有 Adaptive 注解        if (clazz.isAnnotationPresent(Adaptive.class)) {        cacheAdaptiveClass(clazz, overridden);    } else if (isWrapperClass(clazz)) {        // 缓存包装类                cacheWrapperClass(clazz);    } else {        // 进入到这里,表明只是该类只是一个普通的拓展类                // 检测 clazz 是否有默认的构造方法,如果没有,则抛出异常                clazz.getConstructor();        if (StringUtils.isEmpty(name)) {            // 如果 name 为空,则尝试从 Extension 注解中获取 name,或使用小写的类名作为 name            name = findAnnotationName(clazz);            if (name.length() == 0) {                throw new IllegalStateException("No such extension name for the class " + clazz.getName() + " in the config " + resourceURL);            }        }              String[] names = NAME_SEPARATOR.split(name);        if (ArrayUtils.isNotEmpty(names)) {            // 如果类上有 Activate 注解,则使用 names 数组的第一个元素作为键,                        // 存储 name 到 Activate 注解对象的映射关系                        cacheActivateClass(clazz, names[0]);            for (String n : names) {                // 存储 Class 到名称的映射关系                                cacheName(clazz, n);                // 存储 name 到 Class 的映射关系.                                // 如果存在同一个扩展名对应多个实现类,基于 override 参数是否允许覆盖,如果不允许,则抛出异常.                                saveInExtensionClass(extensionClasses, clazz, n, overridden);            }        }    }}

Dubbo SPI 应用场景

※ 协议扩展

# META-INF/dubbo/org.apache.dubbo.rpc.Protocoldubbo=org.apache.dubbo.rpc.protocol.dubbo.DubboProtocolhttp=org.apache.dubbo.rpc.protocol.http.HttpProtocolgrpc=org.apache.dubbo.rpc.protocol.grpc.GrpcProtocol

※ 集群容错

@SPI(FailoverCluster.NAME)public interface Cluster {    @Adaptive    <T> Invoker<T> join(Directory<T> directory) throws RpcException;}
// 使用示例<dubbo:reference cluster="failfast"/>

※ 服务治理 filter 过滤器

@SPIpublic interface Filter {    Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException;        public interface Listener {        void onResponse(Result appResponse, Invoker<?> invoker, Invocation invocation);              void onError(Throwable t, Invoker<?> invoker, Invocation invocation);    }}

四,总结

值得一提的是,SPI 在实际使用过程中存在一些问题:

※ 资源浪费预性能问题

由上文源码解析可知,ServiceLoader 在加载时会扫描 META-INF/services 目录并解析文件,会通过反射实例化所有实现类,如果实现类很多,或者初始化很耗时,会造成一定程度上的性能开销和资源浪费,所以 Dubbo 的 ExtensionLoader 在此之上进行了优化,通过缓存以及在配置文件中指定 key 实现只加载部分实现类。

※ 多个实现类加载顺序问题

ServiceLoader 加载服务提供者实现的顺序由 classpath 中 jar 包的顺序决定,如果你的逻辑依赖于获取到的第一个实现,在不同环境下可能会出现加载顺序不一致导致的异常问题,所以尽可能避免依赖加载顺序。

※ 类重复加载问题

如果 classpath 中存在多个 jar 包都申明了同一个接口的相同实现类,或者同一个 jar 包被不同类加载器加载多次,会导致同一个实现类被多次加载和实例化,可能导致单例失效或资源冲突。

总的来说,SPI 机制的应用场景还是很广的,其核心在于通过读取配置文件动态加载接口实现,解耦了接口定义与实现 ,实现了框架可扩展性,在各大框架中都能看到其身影。

往期回顾

1.Java volatile 关键字到底是什么|得物技术

2.社区搜索离线回溯系统设计:架构、挑战与性能优化|得物技术

3.从 Rust 模块化探索到 DLB 2.0 实践|得物技术

4.eBPF 助力 NAS 分钟级别 Pod 实例溯源|得物技术

5.正品库拍照 PWA 应用的实现与性能优化|得物技术

文 / 琉璃

关注得物技术,每周一、三更新技术干货

要是觉得文章对你有帮助的话,欢迎评论转发点赞~

未经得物技术许可严禁转载,否则依法追究法律责任。

相关推荐

从中心调度到自驱协作多 Agent 架构的三次反转|得物技术

本文探讨多Agent系统中的控制权分配问题,提出三大协作架构:Master Agent(中心调度)、任务看板(共享状态)与Advisor(轻量咨询)。通过划清上下文边界与模型边界,强调应以信息相关性而非职业标签划分Agent。三种模式分别将决策权集中于中心、下放至共享状态、再下沉至执行者,形成控制权逐步下沉的演进路径。案例显示,合理设计可提升复杂任务的并行性、弹性与可维护性。

SmartCodeTech的博客 405

Jev 实战:为AI工作流引入轻量决策层 x10倍提速|得物技术

完成这三个场景后,我对适用边界有了更明确的认识:它们都可以先定义答案范围,再根据判断结果驱动下一步操作。接入时,我会优先明确输入、选项、采纳条件和兜底路径,再评估Jev是否适合承担这一环节。结合这些实践,我更容易理解Jev(TypeSafe System One)作为专用结构化决策模型不生成自然语言,而是返回带概率的判断结果。后续接入时,我会继续从脚本中边界明确的判断任务开始,分别验证准确性、耗时和费用,再决定是否扩大使用范围。下面整理了相关能力、限制和接入要点,便于查阅。

SmartCodeTech的博客 343

国产算力·昇腾 910C 大模型推理优化(下):源码修改与算子优化,提速30%|得物技术

本文延续上篇,深入vllm-ascend源码优化:通过多流重叠实现MoE共享专家并行计算,消除冗余事件开销,并解除与FUSED_MC2的互斥限制,实现c=1下TPOT从8.2ms降至8.0ms;优化decode阶段host侧开销,引入状态跟踪跳过冗余拷贝;设计Block Table CPU快路径,规避小batch下NPU kernel启动延迟;针对GDN层融合L2Norm至recurrent kernel,减少算子调用与内存往返。综合优化后推理性能显著提升,形成可复用的昇腾910C大模型推理优化方法论。

SmartCodeTech的博客 238

国产算力·昇腾 910C 大模型推理优化(上):镜像更新与参数调优,提速 40%|得物技术

文中详细给出每一项优化的动机、实现(含具体源码改动路径与关键代码片段)、验证数据与踩坑记录(包括十余项实测无效或负向的尝试),希望为同样在国产算力上做推理优化的团队提供一份可直接复用的工程参考。

SmartCodeTech的博客 217

动态规则的 Agent 实践:用确定性工程驾驭 LLM 的资产智能验收

面对财务验收中规则多变、材料非结构化、核对复杂等痛点,传统规则引擎难以应对。系统采用“规则即配置”思路,由大语言模型(LLM)负责理解合同、扫描件等非结构化材料并执行语义比对,工程系统则保障流程编排与可追溯性。通过异步处理、幂等设计、三类流量隔离及双通道(文本/图像)处理,实现高效稳定运行。结合用户反馈构建自动标准答案,辅以试跑、回测与发布门禁机制,形成闭环质量体系。底层采用确定性工作流架构,确保行为可预期、可测试,真正实现灵活规则的快速迭代与安全上线。

SmartCodeTech的博客 343

发一张图,它居然“跃“出屏幕了?一文读懂得物 3D 空间图片技术实践

3D 空间图片功能的落地,涉及模型生成、内容分发、客户端渲染之间的一系列取舍。本文重点分享如何生产出高质量、小体积的模型,以及如何在手机上流畅运行。

SmartCodeTech的博客 555

别再只卷向量检索了,得物交易搜索如何用“生成式”实现召回范式跃迁?

你搜"af1",它得给你"空军一号"。这一年,得物交易搜索把召回从"做选择题"改成了"写答案"——不再从商品池里"找"最像的,而是让模型自己"生成"你真正想要的。从 LLM 语义索引到 HLLM、语义 ID,搜索是怎么从"检索"走向"生成"的?

SmartCodeTech的博客 593

指标平台:从语义底座到智能消费的实践路径|得物技术

这篇文章想做的,不是在本体化的宏大叙事上再叠加一层理论,而是把我们在真实落地中验证过的路径讲清楚:语义底座怎么从零建立起来、怎么保证质量不失控、怎么支撑 Text2SQL 和 AI Coding 两条消费主线,以及沿着这条路径往前走,下一步该往哪个方向发力。

SmartCodeTech的博客 316

企业级 MultiAgent 落地:Plan 模式与主子 Agent 协作|得物技术

基于 AgentScope Java 的企业级 MultiAgent 平台,为研发、数据分析和知识问答等场景提供统一的模型调用、工具接入、任务编排、流式输出与运行追踪能力。

SmartCodeTech的博客 300

得物小摊 AI Native 演进实录:用 Harness 构建可控 AI 交付

AI Coding 一个人用很顺手,进了团队就漏:指令没人审、经验难沉淀、过程看不见、链路不闭环。作者团队基于 Multica 做了 EP-Harness,把 Agent 当成员管,给它套一层可执行、可观察、可治理的 Harness,并让反馈闭环。成果:可追溯决策、多 Agent 协作,高频异常从每 4 小时 2400 多条降到个位数,对得物技术补上团队级 AI 协作关键一环。

SmartCodeTech的博客 540

企业级 MultiAgent 的记忆系统:短期上下文与四层记忆架构实现|得物技术

在 MultiAgent 平台中,Agent 的"记忆"能力是实现智能对话和个性化服务的核心资产。本文深入剖析 Agent 记忆系统的设计理念与实现细节,重点解析长期记忆架构的写入与加载流程,涵盖 LLM 智能判断、语义检索、Token 预算控制、冲突处理等关键技术,为开发者提供完整的技术实现参考。

SmartCodeTech的博客 502

EP-Harness:从个人 AI Coding 到团队级 Agent 工作流|得物技术

AI Coding 个人用得爽,一到团队就崩。EP-Harness 把 Agent 从聊天工具变成团队协作成员——可管理、可追溯、可闭环。落地后高频异常从 2400+/4h 收敛到个位数。

SmartCodeTech的博客 688

得物知识问答:复合检索 Agent 的系统设计实践

从"搜到什么用什么"到"搜→筛→挖→验"自主决策闭环——我们造了一个有判断力的 RAG Agent,不是套壳。

SmartCodeTech的博客 733

实战从零开始构建一个Coding Agent:Violin |得物技术

理解了 coding agent 的构建原理,也就掌握了理解其他 agent 的一把钥匙。笔者对每天都在使用的 agent 原理很感兴趣,于是决定自己写一个:violin。

SmartCodeTech的博客 926

AI Native 交易核心系统的研发范式|得物技术

如何改造旧研发流程适配 AI 编码?本文提出五道标准化关口治理思路,依托 Claude Code Spec-Driven 插件打通需求澄清、方案设计、TDD 落地、门禁管控与埋点监控,构建带人工确认门控的完整工作流,以五层自底向上架构实现 AI 编码规范化落地。

SmartCodeTech的博客 972

推荐系统体验的数字化突破:得物自动化评测平台的技术实践|AICon 文章整理

「得物推荐 AI Harness 工程化实践系列」的末篇内容,本系列共三篇连载。本篇(末篇)详解得物推荐评测平台的完整技术体系:从主观体验量化到大模型自动化评测流水线,涵盖多维度指标设计、人机协作提示词校验闭环、分布式任务调度与成本精控等核心工程实现,以及单周百万级审计需求下实现 91% 资源成本优化的工业级落地实战细节。

SmartCodeTech的博客 1492

RAG 核心概念与原理:Chunking、Embedding、相似度、HNSW 与多路召回|得物技术

决定 RAG 效果的关键不在大模型,而是文本切块、查询改写、多路召回与精排。合理切块存好知识库内容,向量匹配抓语义、BM25 匹配关键词互补。先优化用户提问,快速召回一批相关文档,再用精排模型精细筛选,整套检索流程配合到位,问答准确度才会明显提升。

SmartCodeTech的博客 1079

从“机械应答“到“服务伙伴“:得物高可控智能客服的 Agent 工程实践|AICon 演讲整理

本次分享围绕以下核心技术展开:客服 Agent 挑战与架构设计(Single-Agent→Multi-Agent→Harness);高可控性数据飞轮(PE 自动化 + DPO 训练);RL 策略训练和人工经验对齐;多轮半双工消息流控制。

SmartCodeTech的博客 744

得物推荐系统诊断 Agent:从 “调接口” 到 “会思考”|AICon 演讲整理

本文是得物技术专家在 AICon 上海演讲整理的技术实录。AICon上海2026_全球人工智能开发与应用大会暨大模型应用生态展_InfoQ技术大会「得物推荐 AI Harness 工程化实践系列」的中篇内容,本系列共三篇连载。本篇(中篇)进一步详解得物推荐复杂业务场景下,AI代码从生成、防护校验到安全上线的完整技术体系、自研AI Harness整体架构框架、全流程安全防护机制、混合智能体核心算法实现,以及工业级工程落地的实战细节。

SmartCodeTech的博客 1361

得物 OceanBase 落地实践

得物选型OceanBase多模数据库应对业务挑战,通过验证其高可用架构、存储压缩技术及混合负载处理能力。测试显示OceanBase在复杂查询场景性能较MySQL提升130-200倍,在高并发TP/AP混合场景表现优异,存储成本降低40%+。通过DBA内部报表业务试点验证其稳定性后,逐步推广至核心业务场景,优化数据库技术栈,实现性能与成本的双重收益。

SmartCodeTech的博客 1380

AI UITester:AI Native 的 UI 自动化测试新范式|得物技术

ai_uitester 代表了 AI Native 的 UI 自动化测试新范式:这不仅是工具的升级,而是测试范式的转变 ------ 从 "代码驱动" 转向 "视觉驱动",从 "人工调试" 转向 "AI 自愈",从 "三端分离" 转向 "统一抽象"。

SmartCodeTech的博客 774

从狂野代码到按目标生产:得物推荐 AI Harness 的工程化实践|AICon 演讲整理

本文是得物技术专家在 AICon 上海演讲整理的技术实录。「得物推荐 AI Harness 工程化实践系列」的开篇内容,本系列共三篇连载。将系统拆解得物推荐复杂业务场景下,AI 代码从生成、防护校验到安全上线的完整技术体系,逐一详解自研 AI Harness 整体架构框架、全流程安全防护机制、混合智能体核心算法实现,以及工业级工程落地的实战细节。本篇(上篇):将整体介绍团队自研的 AI Harness 体系,包括建设思路、全生命周期防护机制、混合智能体架构及落地效果。

SmartCodeTech的博客 1080

从表单到 Agent:得物社区活动搭建的 AI 实践之路

一场营销活动从策划到上线,运营要在三个系统间跳转 10 + 次、填写 40 + 个字段。我们用 AI 重新设计了这条链路 —— 从 “AI 帮你填表单” 到 “两阶段 Agent + 聚合工作台”。这篇文章记录的不是技术细节,而是这条路上的选择和反思。

SmartCodeTech的博客 741

从埋点需求到规则资产:Hermes Agent 重构得物数仓工作流

埋点和指标需求中,消耗数据承接方的不是生成答案,而是补齐证据链、对齐历史口径、识别下游影响,并在生产写入前留下可追溯的确认记录。本文数据承接方指承接业务需求的数据同学或业务合作伙伴;后文讨论内部研发、元数据、查询和门禁能力在流程中的角色。

SmartCodeTech的博客 656

让 Claude Code 拥有自我进化和记忆系统|得物技术

本文记录了为 Claude Code 构建的一套持久化记忆与自我学习系统的设计思路与实现细节。这套系统让 AI 助手能够跨越会话边界记住用户习惯,并在每次对话结束后自动提炼行为规律和系统信息,通过向量检索快速提取关键信息,下一次对话时主动应用,逐步进化为更懂你的编程伙伴。

SmartCodeTech的博客 994

用 LLM Agent 重构告警排查流程|得物技术

告警排查需频繁切换日志、APM、链路追踪等多个平台,耗时 10~30 分钟,且高度依赖个人经验。我们做了 Troubleshooter——用 LLM Agent 自动完成数据采集、根因分析和处置建议生成。上线后中位数排查耗时从 20 分钟降到 4.4 分钟,覆盖 11 个服务和 10+ 种告警类型。

SmartCodeTech的博客 1004

HorizonVault 技术深潜:如何在 HDD 上做出 100GB/s+ 级大吞吐分布式存储|得物技术

HorizonVault是一款面向Kafka远程存储的高吞吐分布式存储引擎,通过分布式并行写入和状态感知调度解决冷数据存储的三大核心问题:吞吐瓶颈、性能抖动和恢复能力。系统采用分层架构,Meta负责全局资源调度,Broker处理请求路由,Store实现本地磁盘管理,形成完整的写入治理链路。通过将HDD磁盘提升为一等资源,实现顺序追加写入、多盘并行和慢盘隔离,在保证高吞吐的同时控制延迟。存储结构采用DiskStore组织磁盘资源,LogSegment顺序写入数据,IndexSegment定位读取,Change

SmartCodeTech的博客 890

Claude Code Harness 工程:数仓侧落地方案|得物技术

Harness 工程通过职责分层实现研发流水线可靠性提升。Claude 负责需求理解与代码生成等语义层工作;hooks 负责规范检查等确定性执行;Subagents 隔离血缘查询、自测等高 token 操作;Memory 承载跨会话状态持久化。四层协同使数仓 AI 开发从"对话式辅助"转变为"规则驱动的自动化流程"。

SmartCodeTech的博客 1092

BP Claw 破解 AI 编码输入难题 ——FlinkSpec 需求智能化实践|得物技术

本文为 FlinkSpec 系列之开篇,亦是这场工程化变革的序章。BP Claw 所立之处,不过链路之源。FlinkSpec 所图,乃以 AI 之力,将实时数仓从需求落地至验收上线的全程工序,熔铸为一套精密自洽、生生不息的智能工程体系。宏图未竟,后续系列将逐章揭幕,敬请期待!

SmartCodeTech的博客 1035

基于 Harness + SDD + 多仓管理模式的 AI 全栈开发实践|得物技术

Harness(约束)思维的本质是:给 AI 一个已有的实现作为参照,让它照着复刻一份,而不是凭空创造。就像给一个新入职的工程师说 "你照着这个模块的风格,写一个类似的",而不是 "你自由发挥"------ 前者往往能更快产出符合团队规范的代码。

SmartCodeTech的博客 1184
上一篇: 得物向量数据库落地实践
下一篇: Valkey 单点性能比肩 Redis 集群了?Valkey8.0 新特性分析|得物技术
得物技术
得物技术 得物技术官方账号 得物技术官方账号
博客等级 码龄6年 2492粉丝 407原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值