Java SPI机制
java的spi就是一种服务提供发现机制,在一方制定好接口规范后,通过spi的机制可以它的子实现类进行服务发现,以及加载它的子实现类,通过这种机制,让我们在引入第三方库时,不用讲第三方库中的类硬编码到我们的代码中,而是通过java spi的机制来动态加载这些类
1、使用方法
- 在classpath:resource文件夹下创建一个META-INF文件夹
- 再在META-INF文件夹下创建一个services文件夹
- 然后在services文件夹创建对应的文件,文件名-接口的全限定名,文件内容就是需要加载的子实现类的全限定名
2、加载方式
- 通过ServiceLoader这个类的
load()
方法,传入对应的class
对象进行加载,但是这里不会真正的加载,只是讲我们的文件中的类全限定名,存放到providerNames
集合中 - 他内部是一个迭代器设计模式的实现,只有当我们进行迭代时,调用
next()
方法时,会调用provides的next()
方法迭代providerNames
集合对象,获取对象内的每个全限定名,然后调用get
方法获取对应的实例对象,这一步才会创建对象,而且使用ServiceLoader
迭代的时候,会将所有的实现类都加载并实例化,以便在迭代过程中提供这些实现的实例。- 优点:
- 核心思想:
- 解耦,使得第三方服务模块的装配控制的逻辑与调用者的业务代码分离。可以根据实际业务情况进行使用或扩展。
- 核心思想:
- 缺点:
- 1、获取接口的实现类的方式不灵活
serviceLoader 只能通过 Iterator 形式遍历获取,不能根据参数获取指定的某个实现类。 - 2、资源浪费
serviceLoader 只能通过遍历的方式将接口的实现类全部获取、加载并实例化一遍。如果不想用某些实现类,它也被加载并实例化,造成浪费。
- 1、获取接口的实现类的方式不灵活
- 优点:
- 其次他也打破了在加载我们自定义类的类加载的一个双亲委派机制,它会获取我们当前的上下文的加载器(appClassLoader),如果为空也会获取systemClassLoader进行加载,而不是层层递归,委托向上进行加载。
AppClassLoader
和SystemClassLoader
在层级上是一样的。Bootstrap ClassLoader
负责从 rt.jar 加载标准 JDK 类文件,它是 Java 中所有类加载器的父类。 Bootstrap 类加载器没有任何父类。Extension ClassLoader
将类加载请求委托(delegate)给其父 Bootstrap,如果不成功,则从jre/lib/ext
目录或 java.ext.dirs 系统属性指向的任何其他目录加载类系统或应用程序类加载器
,它负责从 CLASSPATH 环境变量、-classpath 或 -cp 命令行选项、JAR 内 list 文件的类路径属性加载应用程序特定类。- 应用程序类加载器是
Extension ClassLoader
的子类,由sun.misc.Launcher$AppClassLoader
类实现。 - 除了
Bootstrap 类加载器
,它主要是用C 语言实现的,所有Java 类加载器都是使用java.lang.ClassLoader
实现的。
- 应用程序类加载器是
3、测试用例
public class SpiSolution {public static void main(String[] args) {ServiceLoader<Object> load = ServiceLoader.load(Object.class);Iterator<Object> iterator = load.iterator();while (iterator.hasNext()) {Object next = iterator.next();}}
}
4、load方法
默认去找上下文类加载器:
@CallerSensitive
public static <S> ServiceLoader<S> load(Class<S> service) {ClassLoader cl = Thread.currentThread().getContextClassLoader();return new ServiceLoader<>(Reflection.getCallerClass(), service, cl);
}
没有就去获取系统类加载器:
new ServiceLoader<>(Reflection.getCallerClass(), service, cl):
private ServiceLoader(Class<?> caller, Class<S> svc, ClassLoader cl) {Objects.requireNonNull(svc);if (VM.isBooted()) {checkCaller(caller, svc);if (cl == null) {cl = ClassLoader.getSystemClassLoader();}} else {// if we get here then it means that ServiceLoader is being used// before the VM initialization has completed. At this point then// only code in the java.base should be executing.Module callerModule = caller.getModule();Module base = Object.class.getModule();Module svcModule = svc.getModule();if (callerModule != base || svcModule != base) {fail(svc, "not accessible to " + callerModule + " during VM init");}// restricted to boot loader during startupcl = null;}this.service = svc;this.serviceName = svc.getName();this.layer = null;this.loader = cl;this.acc = (System.getSecurityManager() != null)? AccessController.getContext(): null;
}
5、创建调用iterator()对象
5.1 创建LookupIterator()
会创建一个newLookupIterator()
和一个迭代器对象new Iterator<S>()
public Iterator<S> iterator() {// create lookup iterator if neededif (lookupIterator1 == null) {lookupIterator1 = newLookupIterator();}return new Iterator<S>() {// record reload countfinal int expectedReloadCount = ServiceLoader.this.reloadCount;// index into the cached providers listint index;/*** Throws ConcurrentModificationException if the list of cached* providers has been cleared by reload.*/private void checkReloadCount() {if (ServiceLoader.this.reloadCount != expectedReloadCount)throw new ConcurrentModificationException();}@Overridepublic boolean hasNext() {checkReloadCount();if (index < instantiatedProviders.size())return true;return lookupIterator1.hasNext();}@Overridepublic S next() {checkReloadCount();S next;if (index < instantiatedProviders.size()) {next = instantiatedProviders.get(index);} else {next = lookupIterator1.next().get();instantiatedProviders.add(next);}index++;return next;}};
5.2 创建LookupIterator()
LookupIterator:
- 会创建懒加载服务迭代器:
Iterator<Provider<S>> first = new ModuleServicesLookupIterator<>();
Iterator<Provider<S>> second = new LazyClassPathLookupIterator<>();
private Iterator<Provider<S>> newLookupIterator() {assert layer == null || loader == null;if (layer != null) {return new LayerLookupIterator<>();} else {Iterator<Provider<S>> first = new ModuleServicesLookupIterator<>();Iterator<Provider<S>> second = new LazyClassPathLookupIterator<>();return new Iterator<Provider<S>>() {@Overridepublic boolean hasNext() {return (first.hasNext() || second.hasNext());}@Overridepublic Provider<S> next() {if (first.hasNext()) {return first.next();} else if (second.hasNext()) {return second.next();} else {throw new NoSuchElementException();}}};}}
5.3 LazyClassPathLookupIterator()
LazyClassPathLookupIterator:去读取
6、迭代过程中加载
迭代过程中才会加载:
- 迭代过程中获取
next()
,实际获取lookupIterator1.next().get()
去创建实例对象:
public Iterator<S> iterator() {// create lookup iterator if neededif (lookupIterator1 == null) {lookupIterator1 = newLookupIterator();}return new Iterator<S>() {// record reload countfinal int expectedReloadCount = ServiceLoader.this.reloadCount;// index into the cached providers listint index;/*** Throws ConcurrentModificationException if the list of cached* providers has been cleared by reload.*/private void checkReloadCount() {if (ServiceLoader.this.reloadCount != expectedReloadCount)throw new ConcurrentModificationException();}@Overridepublic boolean hasNext() {checkReloadCount();if (index < instantiatedProviders.size())return true;return lookupIterator1.hasNext();}@Overridepublic S next() {checkReloadCount();S next;if (index < instantiatedProviders.size()) {next = instantiatedProviders.get(index);} else {next = lookupIterator1.next().get();instantiatedProviders.add(next);}index++;return next;}};
}
7、源码应用
7.1 JDBC
jdbc去动态拓展,要去使用其它厂商的服务,如oracle、mysql,它只需要制定一个接口的规范,由其它厂商去遵循它的规范,就可以实现动态可插拔。
7.2 SpringMVC
在 Spring MVC 中,Servlet 3.0 的 SPI(Service Provider Interface)机制可以帮助您实现零 XML 配置文件的方式,通过注解和接口的实现来自动初始化 Spring MVC 相关的配置,从而实现无需显式的 XML 配置文件。以下是在 Spring MVC 中通过 Servlet 3.0 的 SPI 实现零 XML 配置的详细步骤:
- 在spring-web的包下,有个
META-INF/services/javax.servlet.ServletContainerInitiaLizer
,内容是实现类org.springframework.web.SpringServletContainerInitializer
,其为servlet为我们提供的一个接口,并会在tomcat,jetty
等web容器启动时调用onStartUp()方法,使用@HandlesTypes(WebApplicationInitializer.class)
,将所有的实现类扫描到webAppInitializerClasses
集合中作为参数onStartup()
方法中。 - 创建一个
new ArrayList()
集合,将所有符合条件的类,比如实现了WebApplicationInitializer.class
接口的类,然后通过反射创建对象,添加到集合中,然后再统一迭代ArrayList集合中的类,调用每个对象的onStartup()
进行初始化
@HandlesTypes({WebApplicationInitializer.class})
public class SpringServletContainerInitializer implements ServletContainerInitializer {public SpringServletContainerInitializer() {}public void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext) throws ServletException {List<WebApplicationInitializer> initializers = Collections.emptyList();Iterator var4;if (webAppInitializerClasses != null) {initializers = new ArrayList(webAppInitializerClasses.size());var4 = webAppInitializerClasses.iterator();while(var4.hasNext()) {Class<?> waiClass = (Class)var4.next();if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) && WebApplicationInitializer.class.isAssignableFrom(waiClass)) {try {((List)initializers).add((WebApplicationInitializer)ReflectionUtils.accessibleConstructor(waiClass, new Class[0]).newInstance());} catch (Throwable var7) {throw new ServletException("Failed to instantiate WebApplicationInitializer class", var7);}}}}if (((List)initializers).isEmpty()) {servletContext.log("No Spring WebApplicationInitializer types detected on classpath");} else {servletContext.log(((List)initializers).size() + " Spring WebApplicationInitializers detected on classpath");AnnotationAwareOrderComparator.sort((List)initializers);var4 = ((List)initializers).iterator();while(var4.hasNext()) {WebApplicationInitializer initializer = (WebApplicationInitializer)var4.next();initializer.onStartup(servletContext);}}}
}
1. 创建扩展接口和实现类:
首先,定义一个扩展接口,例如 WebApplicationInitializer
:
public interface WebApplicationInitializer {void onStartup(ServletContext servletContext) throws ServletException;
}
然后,创建实现了 WebApplicationInitializer
接口的类,这些类将负责初始化 Spring MVC 配置:
public class MyWebAppInitializer implements WebApplicationInitializer {@Overridepublic void onStartup(ServletContext servletContext) throws ServletException {// 在这里初始化 Spring MVC 配置}
}
2. 使用 @HandlesTypes 注解:
在 MyWebAppInitializer
类上使用 @HandlesTypes
注解,以便在应用启动时将所有实现了 WebApplicationInitializer
接口的类传递给容器:
@HandlesTypes(WebApplicationInitializer.class)
public class MyWebAppInitializer implements WebApplicationInitializer {@Overridepublic void onStartup(ServletContext servletContext) throws ServletException {// 在这里初始化 Spring MVC 配置}
}
3. 初始化 Spring MVC 配置:
在 onStartup
方法内,您可以使用 Spring 的注解来初始化 Spring MVC 配置,例如注册 DispatcherServlet
、扫描控制器、视图解析器等:
public class MyWebAppInitializer implements WebApplicationInitializer {@Overridepublic void onStartup(ServletContext servletContext) throws ServletException {AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();context.register(AppConfig.class);DispatcherServlet dispatcherServlet = new DispatcherServlet(context);ServletRegistration.Dynamic registration = servletContext.addServlet("dispatcher", dispatcherServlet);registration.setLoadOnStartup(1);registration.addMapping("/");}
}
4. 部署到 Servlet 3.0 容器:
将您的应用部署到支持 Servlet 3.0 的容器,例如 Tomcat 7+。
5. 实现零 XML 配置:
通过上述步骤,您的 Spring MVC 应用现在可以实现零 XML 配置。MyWebAppInitializer
类的实现会在应用启动时被容器检测到并执行,从而初始化 Spring MVC 配置,而无需显式的 XML 配置文件。
通过使用 Servlet 3.0 的 SPI 机制和 WebApplicationInitializer
接口,您可以在 Spring MVC 中实现零 XML 配置,更加便捷地进行应用初始化和配置。
7.3 Springboot的自动装配
Springboot starter的自动装配
Spring SPI与 JDK SPI 类似, 相对于 Java 的 SPI 的主要在于:
-
Spring SPI 指定配置文件为 classpath 下的
META-INF/spring.factories
,所有的拓展点配置放到一个文件中。
配置文件内容为key-value
类型,key 为接口的全限定名, value 为 实现类的全限定名
,可以为多个。 -
Spring Boot通过
@EnableAutoConfiguration
注解来开启自动配置功能。这个注解实际上包含了两个注解:@Configuration
和@Import
。@Configuration
注解表示该类是一个配置类,用于定义Bean的实例化和装配规则。@Import
注解用于导入其他配置类,从而将它们的配置规则合并到当前配置类中。
-
自动装配其实是通过条件化装配、自动配置类、配置属性绑定来实现的
-
条件化装配:Spring Boot 使用条件化注解(
@Conditional
)来实现自动装配。这些注解基于运行时环境的条件来决定是否需要装配某个组件。 -
自动配置类:Spring Boot 通过在 classpath 下的
META-INF/spring.factories
文件中定义自动配置类,这些自动配置类使用了条件化注解,根据条件来装配相应的组件。 -
配置属性绑定:自动配置类使用了配置属性(
@ConfigurationProperties
)来绑定应用程序的配置到相应的组件中。配置属性可以从 application.properties 或 application.yml 文件中读取。
所以如果我们想要引入一些第三方包,就需要按照下面几步来操作:
-
源码分析:
-
@SpringBootApplication
是一个复合注解,包含多个注解的元注解,相当于同时添加了三个注解的效-
@EnableAutoConfiguration
:启用 SpringBoot 的自动配置机制@Configuration
:允许在上下文中注册额外的 bean 或导入其他配置类@ComponentScan
:扫描被@Component
(@Service
,@Controller
)注解的 bean,注解默认会扫描启动类所在的包下所有的类 ,可以自定义不扫描某些 bean。如下图所示,容器中将排除TypeExcludeFilter
和AutoConfigurationExcludeFilter
。
-
-
@EnableAutoConfiguration
是一个复合注解- 这里面用
@Import
注解导入了一个AutoConfigurationImportSelector
类,这个AutoConfigurationImportSelector
类实现了DeferredImportSelector
接口,重写了selectImports
方法,它会先判断自动配置这个注解是否开启,只有开启了就会调用getAutoConfigurationEntry
方法,具体方法内会带调用getCandidateConfigurations
方法,在这个方法内就回去调用一个SpringFactoriesLoader
的loadFactoryNames
方法去读取META-INF/spring.factories
内的数据然后,获取所有符合条件的类的全限定类名,存储到List
集合中返回,然后再根据一些条件化配置对集合进行筛选,比如@ConditionalOnClass
:当类路径下有指定类的条件下,@ConditionalOnProperty
:yml配置文件中是否进行了属性配置,去除一些不符合条件的全限定名,满足条件这些类就会被加载到 IoC 容器中。
- 这里面用
总结:
- SpringBoot 启动时,会扫描
META-INF/spring.factories
文件,获取所有自动配置类的全限定名 - 根据项目的依赖关系和配置信息,选择并加载相应的自动配置类.
- 自动配置类使用
@ConditionalOnXXX
注解来进行条件装配,通过判断特定条件是否满足来确定是否进行自动装配
- 自动配置类使用
7.3 dubbo spi
dubbo spi和java spi不是同一种实现方式,因为他有一个很大的改进,java spi迭代的时候,会将所有的实现类都加载并实例化,不能制定就获取其中一个,而dubbo是按需加载,它只是一开始读取到了配置文件后,把这些配置文件内的类进行存储,这个过程叫做一个IOC和aop,当你使用的使用才会进行加载,
8、spi应用
实战中,我自己也写了个rpc,对所有模块进行了一个spi的统一模块管理,也是参考了dubbo,分为两个,一个是系统的spi就读取程序本身所要使用的bean,然进行加载,除此之外还有一个用户的spi,用户spi,就是说用户可以遵循我的规范来玩的化,就可以对我们程序中的一些模块的增加或者增强,进行一些拓展的行为。
9、总结
好处:
- 可拓展,减少硬编码,从而减少一个耦合性
- 此外动态可插拔