文章目录
- 前言
- 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 { @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时是否会加载。
-
在新项目中加载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.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);
}
说明一下,加载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种引入的包,使用第二种方式。