Java中的SPI机制与扫描class原理

5 篇文章 0 订阅

前言

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的目录也加进来。

image-20230804144522488

image-20230804144419885

JAVA SPI机制

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

其规则如下:

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

例如下面这个:

image-20230713104226856

重现建一个项目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 {
        @Override
        public 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包,所以手动创建类加载器。

    image-20230713144008469

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

  4. 使用URLClassLoader加载class

Spring SPI机制

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

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

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

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

示例

  1. 定义接口模块

image-20230804152730928

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

    image-20230804152135550

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

    image-20230804152923183

    结果:

    image-20230804153102826

原理

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.Plugin
		String factoryTypeName = factoryType.getName();
        // 先获取到spring.factories里的键值对(map),然后再get
		return 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 {
            // 通过类加载获取所有资源地址url
			Enumeration<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/springplugin
        String 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);
    }

image-20230803174544910

说明一下,加载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种引入的包,使用第二种方式。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值