SpringBoot中SPI技术动态加载服务源码
什么是SPI
SPI是jdk中的一种服务发现机制,在java中可以用来扩展API和第三方实现。相比于API,可以动态替换发现。
- 我的理解:
SPI是一种动态服务发现方式。如果我们想要调用别人实现的方法,那么肯定是调用别人的实现类来操作。但是java里面是面向接口编程,实现多个类之间的解耦。所以SPI操作就是你可以通过调用接口来调用实现类。那么如何才能知道接口和实现类之间的绑定呢?就是通过配置文件,那么我们怎么加载配置文件,使用Classloader类加载器去加载,这样就可以了。而springboot里面封装的ServiceLoader,本质还是调用ClassLoader类去加载。
SpringBoot中使用SPI技术源码
入口
public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
return new SpringApplication(primarySources).run(args);
}
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
this.resourceLoader = resourceLoader;
Assert.notNull(primarySources, "PrimarySources must not be null");
this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
this.webApplicationType = WebApplicationType.deduceFromClasspath();
// getSpringFactoriesInstances方法获取spring.factories
this.bootstrapRegistryInitializers = new ArrayList<>(
getSpringFactoriesInstances(BootstrapRegistryInitializer.class));
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
this.mainApplicationClass = deduceMainApplicationClass();
}
private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args) {
ClassLoader classLoader = getClassLoader();
// Use names and ensure unique to protect against duplicates
// 加载读取spring.factories文件
Set<String> names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
AnnotationAwareOrderComparator.sort(instances);
return instances;
}
loadSpringFactories方法加载spring.factories文件
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
// 获取类加载器
ClassLoader classLoaderToUse = classLoader;
if (classLoader == null) {
classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
}
// 获取全路径接口的名称
String factoryTypeName = factoryType.getName();
// 根据接口加载接口对应的实现类
return (List)loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList());
}
private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
Map<String, List<String>> result = (Map)cache.get(classLoader);
if (result != null) {
return result;
} else {
HashMap result = new HashMap();
try {
// 加载指定路径的资源
// 如果当前类加载器没找到就委托父类加载器查找
Enumeration urls = classLoader.getResources("META-INF/spring.factories");
// 遍历存在的多个META-INF/spring.factories文件
while(urls.hasMoreElements()) {
// 获取其中一个META-INF/spring.factories文件封装的url
URL url = (URL)urls.nextElement();
UrlResource resource = new UrlResource(url);
// 将url文件的内容转为Properties文件
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
Iterator var6 = properties.entrySet().iterator();
// 遍历Properties文件中内容,转化为Map映射
while(var6.hasNext()) {
Entry<?, ?> entry = (Entry)var6.next();
String factoryTypeName = ((String)entry.getKey()).trim();
String[] factoryImplementationNames = StringUtils.commaDelimitedListToStringArray((String)entry.getValue());
String[] var10 = factoryImplementationNames;
int var11 = factoryImplementationNames.length;
for(int var12 = 0; var12 < var11; ++var12) {
String factoryImplementationName = var10[var12];
((List)result.computeIfAbsent(factoryTypeName, (key) -> {
return new ArrayList();
})).add(factoryImplementationName.trim());
}
}
}
result.replaceAll((factoryType, implementations) -> {
return (List)implementations.stream().distinct().collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList));
});
// 使用哪个类加载器加载了哪些实现类,都缓存到cache中
cache.put(classLoader, result);
return result;
} catch (IOException var14) {
throw new IllegalArgumentException("Unable to load factories from location [META-INF/spring.factories]", var14);
}
}
}
说明:上面的result变量存放了所有的接口和实现类的映射关系。
自定义SPI实现同包调用
思路:
- 创建META-INF/services目录,放在classpath下面(放在resources下面)。
- 在META-INF/services目录下创建一个以"接口全限定名"为命名的文件,内容为实现类的全限定名。
- 主程序通过java.util.ServiceLoader动态状态实现模块,它通过扫描META-INF/services目录下的配置文件找到实现类的全限定名,把类加载到JVM。
目录结构:
com.sunpy.permissionservice.spi.ISpiSunpyService文件:
com.sunpy.permissionservice.spi.SpiSunpyServiceOne
com.sunpy.permissionservice.spi.SpiSunpyServiceTwo
接口和实现类:
public interface ISpiSunpyService {
public void outMsg();
}
public class SpiSunpyServiceOne implements ISpiSunpyService{
@Override
public void outMsg() {
System.out.println("SpiSunpyServiceOne服务信息");
}
}
public class SpiSunpyServiceTwo implements ISpiSunpyService{
@Override
public void outMsg() {
System.out.println("SpiSunpyServiceTwo服务信息");
}
}
测试:
@Test
public void spiTest() {
ServiceLoader<ISpiSunpyService> serviceLoader = ServiceLoader.load(ISpiSunpyService.class);
serviceLoader.forEach(ISpiSunpyService::outMsg);
}
自定义SPI实现引入jar包调用
public class SunpyTest {
public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/services/com.sunpy.spi.ISpiService";
public static Map<String, List<String>> loadClassName() throws IOException {
ClassLoader classLoader = SunpyTest.class.getClassLoader();
Enumeration<URL> urls = classLoader.getResources(FACTORIES_RESOURCE_LOCATION);
Map<String, List<String>> map = new HashMap<>();
List<String> list = new ArrayList<>();
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
URLConnection urlConnection = url.openConnection();
BufferedReader br = new BufferedReader(new InputStreamReader(urlConnection.getInputStream()));
String content = "";
while ((content = br.readLine()) != null) {
list.add(content);
}
map.put(FACTORIES_RESOURCE_LOCATION, list);
}
return map;
}
public static void doMethod() throws IOException, ClassNotFoundException, InstantiationException, IllegalAccessException, InvocationTargetException, NoSuchMethodException {
Map<String, List<String>> map = loadClassName();
List<String> list = map.get(FACTORIES_RESOURCE_LOCATION);
for (String clazzName : list) {
Class<?> clazz = Class.forName(clazzName);
Method method = clazz.getMethod("outMsg");
method.invoke(clazz.newInstance());
}
}
public static void main(String[] args) throws Exception {
doMethod();
}
}
封装加载动态服务的工具类
/**
* 类加载工具类
*
* @author sunpy
* @date 2023-05-20
*/
public class ClassLoaderUtils {
/**
* 获取resource下的properties文件
* @param path
* @return
*/
public static Properties getProperty(String path) {
try {
ClassLoader classLoader = ClassLoaderUtils.class.getClassLoader();
InputStream is = classLoader.getResourceAsStream(path);
Properties props = new Properties();
props.load(is);
return props;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* 根据全路径报名加载jar包中的类
* @param path
* @param <T>
* @return
*/
public static <T> T loadClass(String path) {
try {
ClassLoader loader = ClassLoaderUtils.class.getClassLoader();
Class<?> clazz = loader.loadClass(path);
return (T) clazz.newInstance();
} catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) {
throw new RuntimeException(e);
}
}
/**
* 获取对应classpath根路径下的文件
* @param path
* @return
*/
public static URL getResource(String path) {
return ClassLoaderUtils.class.getResource(path);
}
public static void main(String[] args) {
URL url = ClassLoaderUtils.getResource("/META-INF/maven.com..google.guava.guava/pom.xml");
System.out.println(url);
}
}