Java的SPI SPI全称为Service Provider Interface,是jdk内置的一种服务提供发现机制。简单来说,它就是一种动态替换发现的机制。
SPI规定,所有要预先声明的类都应该放在META-INF/services中。配置的文件名是接口/抽象类的全限定名,文件内容是抽象类的子类或接口的实现类的全限定类名,如果有多个,借助换行符,一行一个。
具体使用时,使用jdk内置的ServiceLoader类来加载预先配置好的实现类。
举个例子:
在META-INF/services中声明一个文件名为site.milanchen.demo.SpiDemoInterface的文件,文件内容为:
site.milanchen.demo.SpiDemoInterfaceImpl
在site.milanchen.demo包下新建一个接口,类名必须跟上面配置的文件名一样:SpiDemoInterface。
在接口中声明一个test()方法:
1 2 3 public interface SpiDemoInterface { void test () ; }
接下来再新建一个SpiDemoInterfaceImpl,并实现SpiDemoInterface:
1 2 3 4 5 6 public class SpiDemoInterfaceImpl implements SpiDemoInterface { @Override public void test () { System.out.println("SpiDemoInterfaceImpl#test() run..." ); } }
编写主运行类,测试效果:
1 2 3 4 5 6 public class App { public static void main (String[] args) { ServiceLoader<SpiDemoInterface> loaders = ServiceLoader.load(SpiDemoInterface.class); loaders.foreach(SpiDemoInterface::test); } }
运行结果:
SpiDemoInterfaceImpl#test() run...
Spring的SPI SpringFramework 利用 SpringFactoriesLoader 都是调用 loadFactoryNames 方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public static List<String> loadFactoryNames (Class<?> factoryClass, @Nullable ClassLoader classLoader) { String factoryClassName = factoryClass.getName(); return loadSpringFactories(classLoader).getOrDefault(factoryClassName, Collections.emptyList()); }
使用给定的类加载器从 META-INF/spring.factories 中加载给定类型的工厂实现的全限定类名。
结合之前SpringBoot自动配置提到的spring.factories内容,可以知道它只是key-value的关系。
这么设计的好处:不再局限于接口-实现类的模式,key可以随意定义。 (如上面的 org.springframework.boot.autoconfigure.EnableAutoConfiguration是一个注解)
来看方法实现,第一行代码获取的是要被加载的接口/抽象类的全限定名,下面的 return 分为两部分:loadSpringFactories 和 getOrDefault。getOrDefault方法很明显是Map中的方法,不再解释,主要来详细看loadSpringFactories方法。
loadSpringFactories 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories" ;private static final Map<ClassLoader, MultiValueMap<String, String>> cache = new ConcurrentReferenceHashMap <>();private static Map<String, List<String>> loadSpringFactories (@Nullable ClassLoader classLoader) { MultiValueMap<String, String> result = cache.get(classLoader); if (result != null ) { return result; } try { 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); Properties properties = PropertiesLoaderUtils.loadProperties(resource); for (Map.Entry<?, ?> entry : properties.entrySet()) { String factoryClassName = ((String) entry.getKey()).trim(); for (String factoryName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) { result.add(factoryClassName, factoryName.trim()); } } } cache.put(classLoader, result); return result; } catch (IOException ex) { throw new IllegalArgumentException ("Unable to load factories from location [" + FACTORIES_RESOURCE_LOCATION + "]" , ex); } }
它拿到每一个文件,并用Properties方式加载文件,之后把这个文件中每一组键值对都加载出来,放入MultiValueMap中。
如果一个接口/抽象类有多个对应的目标类,则使用英文逗号隔开。StringUtils.commaDelimitedListToStringArray会将大字符串拆成一个一个的全限定类名。
整理完后,整个result放入cache中。下一次再加载时就无需再次加载spring.factories文件了。
文章作者: 米兰
原始链接: https://blog.milanchen.site/posts/spi.html
版权声明: 转载请声明出处