文章目录
- 前言
- 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;
AppClassLoader
和ExtClassLoader
父类都是URLClassLoader
,我们自定义也是继承URLClassLoader
进行扩展的;
所以,当我们使用类加载器加载资源时,它会找上面这些路径,而AppClassLoader
是找当前执行程序的classpath
,也就是我们target/classes
目录,如果有是maven引用了其他依赖包,那么也会将maven地址下的依赖包的路径加到AppClassLoader
的URL
里,如果是多模块的项目,还会把引用的其他模块下target/classes
的目录也加进来。
JAVA SPI机制
Java中提供的SPI机制是通过读取META-INF/services/
目录下的接口文件,从而加载到实现类。
其规则如下:
- 规定号开放api
- 实现提供方需要依赖开发接口完成实现,例如msyql
- 实现提供方,resource下提供
META-INF/services/接口全名
文件,内容为实现类
例如下面这个:
重现建一个项目app
用来测试
-
定义接口
plugin-api
打成jar
包/*** @author ALI* @since 2023/6/30*/ public interface Plugin {Object run(Object data); }
-
定义实现,然后打成
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时是否会加载。
-
在新项目中加载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包里实现了接口的所有实现类,这个方式使用也是很方便的。
-
使用
URLClassLoader
加载class
Spring SPI机制
在Spring中,它的SPI机制,和JAVA 中的类似,需要这样的条件:
-
定义接口模块包,用于开发给第三方实现;
-
第三方要有
resources\META-INF\spring.factories
文件,其内容是键值对方式,key为接口类,value就是我们的实现类;
而Spring执行就是获取到文件里的value,然后反射实例化。
示例
- 定义接口模块
-
定义第三方实现组件,并配置spring.factoryies
-
项目中引入接口模块组件,和实现组件
结果:
原理
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种引入的包,使用第二种方式。