文章目录

  • 前言
  • ClassLoader
  • JAVA SPI机制
  • Spring SPI机制
    • 示例
    • 原理
  • 如何加载jar包里的class

前言

Java的SPI机制与Spring中的SPI机制是如何实现的?

ClassLoader

这里涉及到了class Loader的机制,有些复杂,jdk中提供默认3个class Loader:

  • Bootstrap ClassLoader:加载jdk核心类库;加载%JAVA_HOME\lib%下的jar;
  • ExtClassLoader:加载jdk扩展类库;加载%JAVA_HOME\lib\ext%下的jar;
  • AppClassLoader:加载classpath下的class,以及关联到maven仓库里的jar;

AppClassLoaderExtClassLoader父类都是URLClassLoader,我们自定义也是继承URLClassLoader进行扩展的;

所以,当我们使用类加载器加载资源时,它会找上面这些路径,而AppClassLoader是找当前执行程序的classpath,也就是我们target/classes目录,如果有是maven引用了其他依赖包,那么也会将maven地址下的依赖包的路径加到AppClassLoaderURL里,如果是多模块的项目,还会把引用的其他模块下target/classes的目录也加进来。

JAVA SPI机制

Java中提供的SPI机制是通过读取META-INF/services/目录下的接口文件,从而加载到实现类。

其规则如下:

  1. 规定号开放api
  2. 实现提供方需要依赖开发接口完成实现,例如msyql
  3. 实现提供方,resource下提供META-INF/services/接口全名文件,内容为实现类

例如下面这个:

重现建一个项目app用来测试

  1. 定义接口plugin-api打成jar

    /** * @author ALI * @since 2023/6/30 */public interface Plugin {Object run(Object data);}
  2. 定义实现,然后打成jar

    /** * @author ALI * @since 2023/6/30 */public class PluginImpl implements Plugin {@Overridepublic Object run(Object data) {Motest motest = new Motest();System.out.println(motest.getName());System.out.println(data);return null;}}/** * @author ALI * @since 2023/6/30 */public class Motest {private String name;public Motest() {name = "sss";}public String getName() {return name;}}

    这里我还定义了一个其他的类,用来测试再load class时是否会加载。

  3. 在新项目中加载jar中的资源,引入plugin-api

     /** * 使用jar的classLoader */private static void load2() throws Exception{String jarPath = "E:/workspace/git/test-plugin/app/target/classes/plugin-impl-1.0-SNAPSHOT.jar";URLClassLoader jarUrlClassLoader = new URLClassLoader(new URL[]{new URL("file:" + jarPath)});// ServerLoader搜索ServiceLoader<Plugin> load = ServiceLoader.load(Plugin.class, jarUrlClassLoader);Iterator<Plugin> iterator = load.iterator();while (iterator.hasNext()) {// 实例化对象:这里会进行加载(Class.forName),然后反射实例化Plugin next = iterator.next();next.run("sdsdsdsds");}}

    这里使用ServiceLoader时传入了jarClassLoader,开篇已经解释过了:因为类加载器的原因,不会加载我们自定义的jar包,所以手动创建类加载器。

    结果已经很显而易见,已经成功加载了,这种方式的划,会加载jar包里实现了接口的所有实现类,这个方式使用也是很方便的。

  4. 使用URLClassLoader加载class

Spring SPI机制

在Spring中,它的SPI机制,和JAVA 中的类似,需要这样的条件:

  1. 定义接口模块包,用于开发给第三方实现;

  2. 第三方要有resources\META-INF\spring.factories文件,其内容是键值对方式,key为接口类,value就是我们的实现类;

而Spring执行就是获取到文件里的value,然后反射实例化。

示例

  1. 定义接口模块

  1. 定义第三方实现组件,并配置spring.factoryies

  2. 项目中引入接口模块组件,和实现组件

    结果:

原理

loadFactories两个参数

Class factoryType:用于反射实例化;

ClassLoader classLoader:用于加载资源,所有这里可以直接使用URLClassLoader指定jar的类加载,如果不指定,就是它自己本身的类加载;

public static <T> List<T> loadFactories(Class<T> factoryType, @Nullable ClassLoader classLoader) {Assert.notNull(factoryType, "'factoryType' must not be null");ClassLoader classLoaderToUse = classLoader;if (classLoaderToUse == null) {// 如果为空,它用自己的加载器classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();}// 这里就是加载spring.factories文件里的value值// 找出所有的实现类的类路径List<String> factoryImplementationNames = loadFactoryNames(factoryType, classLoaderToUse);if (logger.isTraceEnabled()) {logger.trace("Loaded [" + factoryType.getName() + "] names: " + factoryImplementationNames);}List<T> result = new ArrayList<>(factoryImplementationNames.size());// 遍历找出来的类,然后通过反射实例化for (String factoryImplementationName : factoryImplementationNames) {result.add(instantiateFactory(factoryImplementationName, factoryType, classLoaderToUse));}// 排序AnnotationAwareOrderComparator.sort(result);return result;}

这里看一下

public static List<String> loadFactoryNames(Class<" />> factoryType, @Nullable ClassLoader classLoader) {// 将接口类转化成类路径,如com.liry.pluginapi.PluginString factoryTypeName = factoryType.getName();// 先获取到spring.factories里的键值对(map),然后再getreturn loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList());}
private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {// 缓存;程序运行中需要多次获取MultiValueMap<String, String> result = cache.get(classLoader);if (result != null) {return result;}try {// 通过类加载获取所有资源地址urlEnumeration<URL> urls = (classLoader != null ?classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));result = new LinkedMultiValueMap<>();// 遍历while (urls.hasMoreElements()) {URL url = urls.nextElement();UrlResource resource = new UrlResource(url);// 通过PropertiesLoaderUtils工具获取spring.factories里的键值对Properties properties = PropertiesLoaderUtils.loadProperties(resource);for (Map.Entry<?, ?> entry : properties.entrySet()) {String factoryTypeName = ((String) entry.getKey()).trim();// 将value通过逗号分隔成数组,然后再全部添加到结果集中for (String factoryImplementationName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {result.add(factoryTypeName, factoryImplementationName.trim());}}}// 加入缓存cache.put(classLoader, result);return result;}catch (IOException ex) {throw new IllegalArgumentException("Unable to load factories from location [" +FACTORIES_RESOURCE_LOCATION + "]", ex);}}

注意:MultiValueMap这个map相同的key不会覆盖value,而是组成链表,如下,一个key可以有多个value,逗号分隔

public void add(K key, @Nullable V value) {List<V> values = this.targetMap.computeIfAbsent(key, k -> new LinkedList<>());values.add(value);}

如何加载jar包里的class

假设需要获取一个jar包里的class该如何?

如下4个步骤即可:

public static void main(String[] args) throws Exception {String packageName = "com.liry.springplugin";// 1. 转换为 com/liry/springpluginString packagePath = ClassUtils.convertClassNameToResourcePath(packageName);// 2. 通过类加载器加载jar包URL//ClassLoader classLoader = Test.class.getClassLoader();ClassLoader classLoader = new URLClassLoader(new URL[]{new URL("file:E:\\workspace\\git\\test-plugin\\spring-plugin\\target\\spring-plugin-1.0-SNAPSHOT.jar")});URL resources = classLoader.getResource(packagePath);// 3. 打开资源通道JarFile jarFile = null;URLConnection urlConnection = resources.openConnection();if (urlConnection instanceof java.net.JarURLConnection) {java.net.JarURLConnection jarURLConnection = (java.net.JarURLConnection) urlConnection;jarFile = jarURLConnection.getJarFile();}// 定义一个结果集List<String> resultClasses = new ArrayList<>();// 4. 遍历资源文件Enumeration<JarEntry> entries = jarFile.entries();while (entries.hasMoreElements()) {JarEntry entry = entries.nextElement();// 文件全路径String path = entry.getName();// 判断是否在指定包路径下,jar包里有多层目录、MF文件、class文件等多种文件信息if (path.startsWith(packagePath)) {// 使用spring的路径匹配器匹配class文件if (path.endsWith(".class")) {resultClasses.add(path);}}}resultClasses.forEach(System.out::println);}

说明一下,加载jar包的问题;

上面给出了两种方式

第一种:使用类加载加载

ClassLoader classLoader = Test.class.getClassLoader();

第二种:使用URLClassLoader加载

ClassLoader classLoader = new URLClassLoader(new URL[]{new URL("file:E:\\workspace\\git\\test-plugin\\spring-plugin\\target\\spring-plugin-1.0-SNAPSHOT.jar")});

这两种方式不同之处在于,查找jar的路径,第一种方式因为我测试项目使用的maven,在pom.xml里引入了spring-plugin-1.0-SNAPSHOT的包,所以才能通过类加载器直接进行加载,这是因为使用maven,maven引用的依赖路径会被加入到AppClassLoader种,然后使用Test.class.getClassLoader()去加载class时,会委派给AppClassLoader进行加载,才会加载到。

所以,如果不是在maven种引入的包,使用第二种方式。