浅谈SPI

news2024/9/25 11:14:26

目录

  • 前言
  • JDK SPI
    • JDBC SPI
    • ServiceLoader实现原理
    • 小结
  • Spring+SpringBoot SPI
    • 实现原理
    • Debug
    • 小结
  • Dubbo SPI
    • 如何使用
    • 实现原理

前言

SPI,英文全称是Service Provider Interface,直译是“服务提供接口”或“服务提供者接口”,是一种基于ClassLoader来发现并加载服务的机制。

这里的“服务”泛指任何一个可以提供服务的功能、模块、应用或系统,这些“服务”在设计接口或规范体系时,往往会预留一些比较关键的口子或者扩展点,让调用方按照既定的规范去自由发挥实现,而这些所谓的“比较关键的口子或者扩展点”,我们就叫“服务”提供的“接口”。

一个标准的SPI,由3个组件构成,分别是:

  • Service:一个公开的接口或抽象类,定义了一个抽象的功能模块;
  • Service Provider:Service的一个实现类;
  • ServiceLoader:SPI机制中的核心组件,负责在运行时发现并加载Service Provider。

JDK SPI

JDK自带的SPI实现,流程如图所示:
在这里插入图片描述

JDBC SPI

以JDBC举例,来看一下具体如何使用。

JDK在java.sql包下有个名为Driver的接口,定义了若干个方法,而具体实现则由具体的DB厂商来实现,如MySQL、Oracle等。

package java.sql;

public interface Driver {

    java.sql.Connection connect(String url, java.util.Properties info) throws SQLException;

    boolean acceptsURL(String url) throws SQLException;

    java.sql.DriverPropertyInfo[] getPropertyInfo(String url, java.util.Properties info) throws SQLException;

    int getMajorVersion();

    int getMinorVersion();

    boolean jdbcCompliant();

    public java.util.logging.Logger getParentLogger() throws SQLFeatureNotSupportedException;
}

在MySQL和Oracle的jar包中,在META-INF/services目录下有个名为java.sql.Driver的文件,里面的内容分别为com.mysql.cj.jdbc.Driver和oracle.jdbc.OracleDriver,即MySQL和Oracle里实现Driver接口的实现类的全路径。
在这里插入图片描述
在这里插入图片描述

此外,我还自定义了一个SimonLeeSQL工程,来实现Driver接口。

定义AbstractSimonLeeSqlDriver,实现Driver接口:

public abstract class AbstractSimonLeeSqlDriver implements java.sql.Driver {

    @Override
    public java.sql.Connection connect(String url, java.util.Properties info) {
        return null;
    }

    @Override
    public boolean acceptsURL(String url) {
        return false;
    }

    @Override
    public java.sql.DriverPropertyInfo[] getPropertyInfo(String url, java.util.Properties info) {
        return new java.sql.DriverPropertyInfo[0];
    }

    @Override
    public int getMajorVersion() {
        return 0;
    }

    @Override
    public int getMinorVersion() {
        return 0;
    }

    @Override
    public boolean jdbcCompliant() {
        return false;
    }

    @Override
    public java.util.logging.Logger getParentLogger() {
        return null;
    }
}

再定义SimonLeeSqlDriver1与SimonLeeSqlDriver2,继承AbstractSimonLeeSqlDriver:

public class SimonLeeSqlDriver1 extends AbstractSimonLeeSqlDriver {
}
public class SimonLeeSqlDriver2 extends AbstractSimonLeeSqlDriver {
}

最后在resources的META-INF/services目录下定义个名为java.sql.Driver的文件,里面的内容为如下2行:
在这里插入图片描述

将SimonLeeSQL工程mvn install,在A工程的pom.xml里分别引入MySQl、Oracle、SimonLeeSQL的坐标。

<dependency>
	<groupId>mysql</groupId>
	<artifactId>mysql-connector-java</artifactId>
	<version>8.0.33</version>
</dependency>
<dependency>
	<groupId>com.oracle.database.jdbc</groupId>
	<artifactId>ojdbc8</artifactId>
	<version>21.3.0.0</version>
</dependency>
<dependency>
	<groupId>com.simonlee</groupId>
	<artifactId>SimonLeeSQL</artifactId>
	<version>1.0-SNAPSHOT</version>
</dependency>

在A工程里,使用java.util.ServiceLoader.load()和其内部的迭代器加载上述4个Driver接口的实现类。

public static void main(String[] args) {
	java.util.ServiceLoader<java.sql.Driver> drivers = java.util.ServiceLoader.load(java.sql.Driver.class);
	java.util.Iterator<java.sql.Driver> iterator = drivers.iterator();
	while (iterator.hasNext()) {
		java.sql.Driver driver = iterator.next();
		System.out.println(driver);
	}
}

看一下输出结果,能正确拿到4个具体实现类的实例。
在这里插入图片描述

在具体应用中,A工程如果想更换DB驱动,只需要在pom.xml里引入DB厂商提供的Maven坐标即可(前提是遵循了JDBC的SPI玩法),无需改动任何代码,实现了热插拔。

ServiceLoader实现原理

java.util.ServiceLoader实现了Iterable接口,并且在内部定义了LazyIterator迭代器。

private class LazyIterator implements Iterator<S> {

	private Class<S> service;
	private ClassLoader loader;
	private Enumeration<java.net.URL> configs = null;
	private Iterator<String> pending = null;
	private String nextName = null;

	@Override
	public boolean hasNext() {
		if (nextName != null) {
			return true;
		}
		if (configs == null) {
			try {
				// PREFIX = META-INF/services/
				// service.getName() = java.sql.Driver
				// fullName = META-INF/services/java.sql.Driver
				String fullName = PREFIX + service.getName();
				if (loader == null) {
					configs = ClassLoader.getSystemResources(fullName);
				} else {
					// java.lang.ClassLoader.getResources()扫描所有META-INF/services/java.sql.Driver文件
					// loader = Launcher$AppClassLoader
					configs = loader.getResources(fullName);
				}
			} catch (java.io.IOException x) {
				fail(service, "Error locating configuration files", x);
			}
		}
		while ((pending == null) || !pending.hasNext()) {
			if (!configs.hasMoreElements()) {
				return false;
			}
			// configs.nextElement() =
			//  jar:file:/D:/MavenRepo/com/mysql/mysql-connector-j/8.0.33/mysql-connector-j-8.0.33.jar!/META-INF/services/java.sql.Driver
			//  jar:file:/D:/MavenRepo/com/oracle/database/jdbc/ojdbc8/21.3.0.0/ojdbc8-21.3.0.0.jar!/META-INF/services/java.sql.Driver
			//  jar:file:/D:/MavenRepo/com/simonlee/SimonLeeSQL/1.0-SNAPSHOT/SimonLeeSQL-1.0-SNAPSHOT.jar!/META-INF/services/java.sql.Driver
			pending = parse(service, configs.nextElement());
		}
		// nextName =
		//  com.mysql.cj.jdbc.Driver
		//  oracle.jdbc.OracleDriver
		//  com.simonlee.sql.SimonLeeSqlDriver1
		//  com.simonlee.sql.SimonLeeSqlDriver2
		nextName = pending.next();
		return true;
	}

	@Override
	public S next() {
		if (!hasNext()) {
			throw new NoSuchElementException();
		}
		String cn = nextName;
		nextName = null;
		Class<?> c = null;
		try {
			// java.lang.Class.forName(),反射机制来实例化对象
			// c =
			//  class com.mysql.cj.jdbc.Driver
			//  class oracle.jdbc.OracleDriver
			//  class com.simonlee.sql.SimonLeeSqlDriver1
			//  class com.simonlee.sql.SimonLeeSqlDriver2
			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.key = nextName, value = 具体实例
			providers.put(cn, p);
			return p;
		} catch (Throwable x) {
			fail(service, "Provider " + cn + " could not be instantiated", x);
		}
		throw new Error();
	}

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

运行完后,providers的内容如图所示:
在这里插入图片描述

小结

在这里插入图片描述

Spring+SpringBoot SPI

相较于JDK SPI,Spring+SpringBoot的区别是:

  • 配置文件路径为META-INF
  • 配置文件名固定为spring.factories
  • 配置文件内容为key-value,key固定为org.springframework.boot.autoconfigure.EnableAutoConfiguration,value为接口的实现类的全路径

实现原理

通过Spring里的org.springframework.core.io.support.SpringFactoriesLoader.loadFactories()读取所有jar包的META-INF/spring.factories资源文件,并从文件中读取一堆的类似EnableAutoConfiguration标识的类路径,将这些类创建对应的Spring Bean对象注入到容器中,就完成了SpringBoot的自动装配底层核心原理。

Spring里的org.springframework.core.io.support.SpringFactoriesLoader:

public class SpringFactoriesLoader {

    public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";

    static final Map<ClassLoader, Map<String, List<String>>> cache = new org.springframework.util.ConcurrentReferenceHashMap<>();

    public static List<String> loadFactoryNames(Class<?> factoryType, @org.springframework.lang.Nullable ClassLoader classLoader) {
        ClassLoader classLoaderToUse = classLoader;
        if (classLoaderToUse == null) {
            classLoaderToUse = org.springframework.core.io.support.SpringFactoriesLoader.class.getClassLoader();
        }
        String factoryTypeName = factoryType.getName();
        return loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList());
    }

    private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
        Map<String, List<String>> result = cache.get(classLoader);
        if (result != null) {
            return result;
        }

        result = new HashMap<>();
        try {
            // FACTORIES_RESOURCE_LOCATION = META-INF/spring.factories
            // java.lang.ClassLoader.getResources()扫描所有META-INF/spring.factories文件
            Enumeration<java.net.URL> urls = classLoader.getResources(FACTORIES_RESOURCE_LOCATION);
            while (urls.hasMoreElements()) {
                java.net.URL url = urls.nextElement();
                org.springframework.core.io.UrlResource resource = new org.springframework.core.io.UrlResource(url);
                Properties properties = org.springframework.core.io.support.PropertiesLoaderUtils.loadProperties(resource);
                // 遍历每个META-INF/spring.factories文件,拿到里面的key-value对
                for (Map.Entry<?, ?> entry : properties.entrySet()) {
                    String factoryTypeName = ((String) entry.getKey()).trim();
                    // 将value按半角逗号分割,转成数组
                    String[] factoryImplementationNames = StringUtils.commaDelimitedListToStringArray((String) entry.getValue());
                    // 遍历数组,拿到里面的值
                    for (String factoryImplementationName : factoryImplementationNames) {
                        // result.key = org.springframework.boot.autoconfigure.EnableAutoConfiguration(包括但不限于)
                        // result.value = 接口的实现类的全路径
                        result.computeIfAbsent(factoryTypeName, key -> new ArrayList<>()).add(factoryImplementationName.trim());
                    }
                }
            }

            // Replace all lists with unmodifiable lists containing unique elements
            result.replaceAll((factoryType, implementations) -> implementations.stream()
                    .distinct()
                    .collect(java.util.stream.Collectors.collectingAndThen(java.util.stream.Collectors.toList(), Collections::unmodifiableList)));
            // 将result放到cache里,后续调用时可直接拿到
            cache.put(classLoader, result);
        } catch (java.io.IOException ex) {
            throw new IllegalArgumentException("Unable to load factories from location [" + FACTORIES_RESOURCE_LOCATION + "]", ex);
        }
        return result;
    }
}

SpringBoot里的org.springframework.boot.autoconfigure.AutoConfigurationImportSelector:

public class AutoConfigurationImportSelector
        implements org.springframework.context.annotation.DeferredImportSelector,
        org.springframework.beans.factory.BeanClassLoaderAware,
        org.springframework.context.ResourceLoaderAware,
        org.springframework.beans.factory.BeanFactoryAware,
        org.springframework.context.EnvironmentAware,
        org.springframework.core.Ordered {

    protected List<String> getCandidateConfigurations(org.springframework.core.type.AnnotationMetadata metadata,
                                                      org.springframework.core.annotation.AnnotationAttributes attributes) {
        // getSpringFactoriesLoaderFactoryClass() = EnableAutoConfiguration.class
        // 调用Spring里的org.springframework.core.io.support.SpringFactoriesLoader
        // 从cache里获取key = org.springframework.boot.autoconfigure.EnableAutoConfiguration的value,即接口的实现类的全路径
        List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader());
        Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories." +
                " If you are using a custom packaging, make sure that file is correct.");
        return configurations;
    }

    protected Class<?> getSpringFactoriesLoaderFactoryClass() {
        return org.springframework.boot.autoconfigure.EnableAutoConfiguration.class;
    }
}

Debug

对SimonLeeSQL工程进行改造,定义SimonLeeSqlDriver3与SimonLeeSqlDriver4,继承AbstractSimonLeeSqlDriver。

public class SimonLeeSqlDriver3 extends AbstractSimonLeeSqlDriver {
}
public class SimonLeeSqlDriver4 extends AbstractSimonLeeSqlDriver {
}

最后在resources的META-INF目录下定义个名为spring.factories的文件,里面的内容为如下2行:
在这里插入图片描述

将SimonLeeSQL工程重新mvn install,在A工程的pom.xml里新加引入mybatis-spring-boot-autoconfigure的坐标。

<dependency>
	<groupId>org.mybatis.spring.boot</groupId>
	<artifactId>mybatis-spring-boot-autoconfigure</artifactId>
	<version>2.3.2</version>
</dependency>

其中的META-INF/spring.factories文件如下所示:
在这里插入图片描述

启动A工程,Debug结果如下所示:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

可以看到,cache里有key=EnableAutoConfiguration和SimonLeeTestKey的,并且key=EnableAutoConfiguration的value里有MybatisLanguageDriverAutoConfiguration、MybatisAutoConfiguration、SimonLeeSqlDriver3、SimonLeeSqlDriver4,符合预期。

小结

相较于JDK SPI,Spring+SpringBoot做了如下优化:

  • 如果想让一个jar包利用SPI机制支持2个接口com.xxx.Interface1和com.xxx.Interface2,JDK需要在META-INT/services目录下创建2个文件com.xxx.Interface1和com.xxx.Interface2,而Spring+SpringBoot只需在META-INT目录下的spring.factories文件里加入对应的key即可(参考mybatis-spring-boot-autoconfigure)
  • 如果想获取特定的接口的实现类,JDK由于采用List存储,需要自己进行遍历并编写各种比较代码,而Spring+SpringBoot采用Map存储,getKey即可(参考AutoConfigurationImportSelector获取key=EnableAutoConfiguration)。

参考:SpringBoot——SpringBoot SPI原理

Dubbo SPI

如何使用

自定义SimonLeeDubbo接口,并加上@org.apache.dubbo.common.extension.SPI注解。

@org.apache.dubbo.common.extension.SPI
public interface SimonLeeDubbo {
}

自定义2个类SimonLeeDubboImpl1和SimonLeeDubboImpl2,实现SimonLeeDubbo接口

public class SimonLeeDubboImpl1 implements SimonLeeDubbo {
}
public class SimonLeeDubboImpl2 implements SimonLeeDubbo {
}

最后在resources的META-INF/services目录下定义个名为com.simonlee.dubbo.SimonLeeDubbo的文件,里面的内容为如下2行:
在这里插入图片描述
使用org.apache.dubbo.common.extension.ExtensionLoader.getExtension()加载上述2个SimonLeeDubbo接口的实现类。

public static void main(String[] args) {
	org.apache.dubbo.rpc.model.ApplicationModel applicationModel = org.apache.dubbo.rpc.model.ApplicationModel.defaultModel();
	// 通过Protocol获取指定像ServiceLoader一样的加载器
	org.apache.dubbo.common.extension.ExtensionLoader<SimonLeeDubbo> dubbos = applicationModel.getExtensionLoader(SimonLeeDubbo.class);

	// 通过指定的名称从加载器中获取指定的实现类
	SimonLeeDubbo dubbo11 = dubbos.getExtension("simonLeeDubboImpl1");
	System.out.println(dubbo11);

	// 再次通过指定的名称从加载器中获取指定的实现类,看看打印的引用是否创建了新对象
	SimonLeeDubbo dubbo12 = dubbos.getExtension("simonLeeDubboImpl1");
	System.out.println(dubbo12);

	SimonLeeDubbo dubbo2 = dubbos.getExtension("simonLeeDubboImpl2");
	System.out.println(dubbo2);
}

看一下输出结果,2次拿到的simonLeeDubboImpl1是同个引用,说明用到了缓存。
在这里插入图片描述

实现原理

// 获取该接口的所有扩展点集合
private Map<String, Class<?>> getExtensionClasses() {
    // 先从缓存里面取,看看有没有该接口的扩展点集合
    Map<String, Class<?>> classes = cachedClasses.get();
    // 如果没有任何缓存的话
    if (classes == null) {
        // 则采用双检索的形式保证线程安全粒度去读取磁盘文件加载扩展点
        synchronized (cachedClasses) {
            classes = cachedClasses.get();
            if (classes == null) {
                // 真正加载扩展点的逻辑
                classes = loadExtensionClasses();
                // 如果加载到的话,则放入到缓存中,下一次就可以直接从缓存中取了
                cachedClasses.set(classes);
            }
        }
    }
    return classes;
}// 正在加载扩展点的逻辑
private Map<String, Class<?>> loadExtensionClasses() {
    // 此处省略若干行代码...
    Map<String, Class<?>> extensionClasses = new HashMap<>();
    for (LoadingStrategy strategy : strategies) {
        // 按照每种磁盘路径的策略去加载磁盘文件
        loadDirectory(extensionClasses, strategy, type.getName());
        // compatible with old ExtensionFactory
        if (this.type == ExtensionInjector.class) {
            loadDirectory(extensionClasses, strategy, ExtensionFactory.class.getName());
        }
    }
    return extensionClasses;
}// 查看加载磁盘的策略有哪些,发现是一个方法
private static volatile LoadingStrategy[] strategies = loadLoadingStrategies();// 继续查看该方法的逻辑,结果发现该方法加载了 LoadingStrategy 接口的所有实现类
private static LoadingStrategy[] loadLoadingStrategies() {
    // java.util.ServiceLoader.load()
    return stream(load(LoadingStrategy.class).spliterator(), false)
        .sorted()
        .toArray(LoadingStrategy[]::new);
}// LoadingStrategy 实现类之一,重点关注 directory 方法
public class DubboInternalLoadingStrategy implements LoadingStrategy {
    @Override
    public String directory() {
        return "META-INF/dubbo/internal/";
    }
    // 此处省略若干行代码...
}// LoadingStrategy 实现类之一,重点关注 directory 方法
public class DubboLoadingStrategy implements LoadingStrategy {
    @Override
    public String directory() {
        return "META-INF/dubbo/";
    }
    // 此处省略若干行代码...
}// LoadingStrategy 实现类之一,重点关注 directory 方法
public class ServicesLoadingStrategy implements LoadingStrategy {
    @Override
    public String directory() {
        return "META-INF/services/";
    }
    // 此处省略若干行代码...
}

private void loadDirectory(Map<String, Class<?>> extensionClasses, LoadingStrategy strategy, String type) {
	loadDirectory(extensionClasses, strategy.directory(), type, strategy.preferExtensionClassLoader(), strategy.overridden(),
			strategy.includedPackages(), strategy.excludedPackages(), strategy.onlyExtensionClassLoaderPackages());
	String oldType = type.replace("org.apache", "com.alibaba");
	loadDirectory(extensionClasses, strategy.directory(), oldType, strategy.preferExtensionClassLoader(), strategy.overridden(),
		strategy.includedPackagesInCompatibleType(), strategy.excludedPackages(), strategy.onlyExtensionClassLoaderPackages());
}private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir, String type, boolean extensionLoaderClassLoaderFirst,
						   boolean overridden, String[] includedPackages, String[] excludedPackages, String[] onlyExtensionClassLoaderPackages) {
	// dir = META-INF/services/
	// type = com.simonlee.dubbo.SimonLeeDubbo
	// fileName = META-INF/services/com.simonlee.dubbo.SimonLeeDubbo
	String fileName = dir + type;
	try {
		List<ClassLoader> classLoadersToLoad = new LinkedList<>();

		// try to load from ExtensionLoader's ClassLoader first
		if (extensionLoaderClassLoaderFirst) {
			ClassLoader extensionLoaderClassLoader = ExtensionLoader.class.getClassLoader();
			if (ClassLoader.getSystemClassLoader() != extensionLoaderClassLoader) {
				classLoadersToLoad.add(extensionLoaderClassLoader);
			}
		}

		// load from scope model
		Set<ClassLoader> classLoaders = scopeModel.getClassLoaders();

		if (CollectionUtils.isEmpty(classLoaders)) {
			// java.lang.ClassLoader.getSystemResources()
			Enumeration<java.net.URL> resources = ClassLoader.getSystemResources(fileName);
			if (resources != null) {
				while (resources.hasMoreElements()) {
					loadResource(extensionClasses, null, resources.nextElement(), overridden, includedPackages, excludedPackages,
						onlyExtensionClassLoaderPackages);
				}
			}
		} else {
			classLoadersToLoad.addAll(classLoaders);
		}

		Map<ClassLoader, Set<java.net.URL>> resources = ClassLoaderResourceLoader.loadResources(fileName, classLoadersToLoad);
		resources.forEach(((classLoader, urls) -> loadFromClass(
			extensionClasses, overridden, urls, classLoader, includedPackages, excludedPackages, onlyExtensionClassLoaderPackages)));
	} catch (Throwable t) {
		logger.error("Exception occurred when loading extension class (interface: " + type + ", description file: " + fileName + ").", t);
	}
}private void loadFromClass(Map<String, Class<?>> extensionClasses, boolean overridden, Set<java.net.URL> urls, ClassLoader classLoader,
						   String[] includedPackages, String[] excludedPackages, String[] onlyExtensionClassLoaderPackages) {
	if (CollectionUtils.isNotEmpty(urls)) {
		for (java.net.URL url : urls) {
			loadResource(extensionClasses, classLoader, url, overridden, includedPackages, excludedPackages, onlyExtensionClassLoaderPackages);
		}
	}
}private void loadResource(Map<String, Class<?>> extensionClasses, ClassLoader classLoader, java.net.URL resourceURL, boolean overridden,
						  String[] includedPackages, String[] excludedPackages, String[] onlyExtensionClassLoaderPackages) {
	try {
		try (BufferedReader reader = new BufferedReader(new InputStreamReader(resourceURL.openStream(), StandardCharsets.UTF_8))) {
			String line;
			String clazz;
			// 遍历每1行,每1行即为1个SimonLeeDubbo接口的具体实现类
			while ((line = reader.readLine()) != null) {
				final int ci = line.indexOf('#');
				if (ci >= 0) {
					line = line.substring(0, ci);
				}
				line = line.trim();
				if (line.length() > 0) {
					try {
						String name = null;
						int i = line.indexOf('=');
						if (i > 0) {
							name = line.substring(0, i).trim();
							clazz = line.substring(i + 1).trim();
						} else {
							clazz = line;
						}
						if (StringUtils.isNotEmpty(clazz)
							&& !isExcluded(clazz, excludedPackages)
							&& isIncluded(clazz, includedPackages)
							&& !isExcludedByClassLoader(clazz, classLoader, onlyExtensionClassLoaderPackages)) {
							// java.lang.Class.forName(),反射机制来实例化对象
							loadClass(extensionClasses, resourceURL, Class.forName(clazz, true, classLoader), name, overridden);
						}
					} catch (Throwable t) {
						IllegalStateException e = new IllegalStateException("Failed to load extension class (interface: " + type +
							", class line: " + line + ") in " + resourceURL + ", cause: " + t.getMessage(), t);
						exceptions.put(line, e);
					}
				}
			}
		}
	} catch (Throwable t) {
		logger.error("Exception occurred when loading extension class (interface: " + type + ", class file: " + resourceURL + ") in " + resourceURL, t);
	}
}

简单跟踪代码后,发现原来我们可以在三个路径下添加SPI机制的文件,分别为:

  • META-INF/dubbo/internal/
  • META-INF/dubbo/
  • META-INF/services/

真相也清楚了,Dubbo框架会从这3个资源目录下去加载SPI机制的文件,只不过从路径的名称上可以看出,META-INF/dubbo/internal/存放的是Dubbo内置的一些扩展点,META-INF/services/存放的是Dubbo自身的一些业务逻辑所需要的一些扩展点,而META-INF/dubbo/存放的是上层业务系统自身的一些定制Dubbo的相关扩展点。

作者:曼特宁

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1971277.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

YOLOv8目标检测算法改进之融合SCconv的特征提取方法

引言 YOLO目标检测算法历经发展&#xff0c;目前已经成为了目标检测领域的经典算法。当前&#xff0c;YOLO目标检测算法已经更新到YOLOv10&#xff0c;但从大家的反映来看,YOLOv10的效果并不理想&#xff08;该算法的创新点是提升检测速度&#xff0c;并不提升精度&#xff0c…

JVM: 方法调用

文章目录 一、介绍二、方法调用的原理1、静态绑定2、动态绑定&#xff08;1&#xff09;介绍&#xff08;2&#xff09;原理 一、介绍 在JVM中&#xff0c;一共有五个字节码指令可以执行方法调用&#xff1a; invokestatic: 调用静态方法。invokespecial&#xff1a;调用对象…

大模型参与城市规划中的应用

人工智能咨询培训老师叶梓 转载标明出处 传统的城市规划往往依赖于专业规划师的经验和判断&#xff0c;耗时耗力&#xff0c;且难以满足居民多样化的需求。近年来&#xff0c;大模型&#xff08;LLMs&#xff09;的崛起为城市规划领域带来了新的机遇。清华大学电子工程系的Zhil…

微信小程序多端框架实现app内自动升级

多端框架生成的app&#xff0c;如果实现app内自动升级&#xff1f; 一、Android 实现app自动升级&#xff0c;华为应用市场 1、获取 应用市场地址 下载地址 2、在微信开放平台进行配置 应用下载地址&#xff1a;应用市场点击分享&#xff0c;里面有一个复制连接功能 应用市…

XMLDecoder反序列化

XMLDecoder反序列化 基础知识 就简单讲讲吧&#xff0c;就是为了解析xml内容的 一般我们的xml都是标签属性这样的写法 比如person对象以xml的形式存储在文件中 在decode反序列化方法后&#xff0c;控制台成功打印出反序列化的对象。 就是可以根据我们的标签识别是什么成分…

QT多媒体编程(一)——音频编程知识详解及MP3音频播放器Demo

目录 引言 一、QtMultimedia模块简介 主要类和功能 二、QtMultimedia相关类及函数解析 QAudioInput QAudioOutput QAudioFormat QMediaPlayer QMediaPlaylist QCamera 三、音频项目实战Demo UI界面 核心代码 运行结果 四、结论 引言 在数字时代&#xff0c;音频…

ArcGIS for js 分屏(vue项目)

一、引入依赖 import {onMounted, ref} from "vue"; import Map from "arcgis/core/Map"; import MapView from "arcgis/core/views/MapView"; import WebTileLayer from "arcgis/core/layers/WebTileLayer"; 二、页面布局 <tem…

22. Hibernate 性能之缓存

1. 前言 本节和大家一起聊聊性能优化方案之&#xff1a;缓存。通过本节学习&#xff0c;你将了解到&#xff1a; 什么是缓存&#xff0c;缓存的作用&#xff1b;HIbernate 中的缓存级别&#xff1b;如何使用缓存。 2. 缓存 2.1 缓存是什么 现实世界里&#xff0c;缓存是一个…

纪念二2024.07 federated-解决mysql跨库联表问题

若需要创建FEDERATED引擎表&#xff0c;则目标端实例要开启FEDERATED引擎。从MySQL5.5开始FEDERATED引擎默认安装 只是没有启用&#xff0c;进入命令行输入 show engines ; FEDERATED行状态为NO。 mysql安装配置文件 一、连接工具查看是否开启federated show engines 二、m…

VMware Workstation17 安装 CentOS7 教程

今天给伙伴们分享一下VMware Workstation17 安装 CentOS7 教程&#xff0c;希望看了有所收获。 我是公众号「想吃西红柿」「云原生运维实战派」作者&#xff0c;对云原生运维感兴趣&#xff0c;也保持时刻学习&#xff0c;后续会分享工作中用到的运维技术&#xff0c;在运维的路…

JS【详解】内存泄漏(含泄漏场景、避免方案、检测方法),垃圾回收 GC (含引用计数、标记清除、标记整理、分代式垃圾回收)

内存泄漏 在执行一个长期运行的应用程序时&#xff0c;应用程序分配的内存没有被释放&#xff0c;导致可用内存逐渐减少&#xff0c;最终可能导致浏览器崩溃或者应用性能严重下降的情况&#xff0c;即 JS 内存泄漏 可能导致内存泄漏的场景 不断创建全局变量未及时清理的闭包&…

Graylog 收集网络设备日志的详细配置指南

需求:网络日志接入到日志服务中,做日志的备份和查询。 交换机或是其它网络设备日志需要接入到graylog日志服务中进行备份和查询。 软件版本 graylog5.1 架构图 一、添加inputs 接受日志信息 二、编辑inputs 配置 第1个红框 title 代表通道的名称,您可以根据需要自由定义…

【CTF-Crypto】格密码基础(例题较多,非常适合入门!)

格密码相关 文章目录 格密码相关格密码基本概念&#xff08;属于后量子密码&#xff09;基础的格运算&#xff08;行列式运算&#xff09;SVP&#xff08;shortest Vector Problem&#xff09;最短向量问题CVP&#xff08;Closet Vector Problem&#xff09;最近向量问题 做题要…

浏览器用户文件夹详解 - ShortCuts(六)

1. Shortcuts简介 1.1 什么是Shortcuts文件&#xff1f; Shortcuts文件是Chromium浏览器中用于存储用户创建的快捷方式信息的一个重要文件。每当用户在浏览器中创建快捷方式时&#xff0c;这些信息都会被记录在Shortcuts文件中。通过这些记录&#xff0c;用户可以方便地快速访…

《小迪安全》学习笔记02

域名默认存放目录和IP默认存放目录不一样。 IP地址是WWW文件里的&#xff0c;域名访问是WWW里的一个子目录里的&#xff08;比如是blog&#xff09;。 Nmap: Web源码拓展 拿到一个网站的源码&#xff0c;要分析这几个方面↑。 不同类型产生的漏洞类型也不一样 在网站中&…

MSPM0G3507_2024电赛自动行驶小车(H题)_问题与感悟

这次电赛题目选的简单了&#xff0c;还规定不能使用到摄像头&#xff0c;这让我之前学习的Opencv 4与树莓派无用武之地了&#xff0c;但我当时对于三子棋题目饶有兴趣&#xff0c;但架不住队友想稳奖&#xff0c;只能选择这个H题了...... 之后我还想抽空将这个E题三子棋题目做…

快手批量取关

目录 突然发现快手木有批量取关功能&#xff0c;没有功能就创造功能 执行代码中 逐渐变少 后面关注列表没人了&#xff0c;总数还有32&#xff0c;不知道是不是帮测出个bug还是咋的(^_^) 突然发现快手木有批量取关功能&#xff0c;没有功能就创造功能 刚开始1000多人 执行代…

中间件之异步通讯组件rocketmq入门

一、概述 1.1介绍 RocketMQ是阿里巴巴2016年MQ中间件&#xff0c;使用Java语言开发&#xff0c;RocketMQ 是一款开源的分布式消息系统&#xff0c;基于高可用分布式集群技术&#xff0c;提供低延时的、高可靠的消息发布与订阅服务。同时&#xff0c;广泛应用于多个领域&#…

暖水袋 亚马逊日本站认证 PSE认证步骤

暖水袋是用来加热取暖的生活用品&#xff0c;有内置热水来加热的类型和利用微波炉加热后使用的类型等。内置热水的暖水袋有塑料制、橡胶制、陶器制等多种类型&#xff0c;但是利用加热石头而不是利用热水来取暖的产品类型为审查对象外商品。 审查资料 每个 ASIN 的文件&#x…

成为AI产品经理,为何应选择LLMs方向?

前言 随着人工智能&#xff08;AI&#xff09;技术的快速发展&#xff0c;越来越多的人开始考虑如何在这个领域找到自己的位置。对于那些希望成为AI产品经理的人来说&#xff0c;选择LLMs&#xff08;Large Language Models&#xff0c;大型语言模型&#xff09;方向是一个非常…