Java SPI机制

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 只能通过遍历的方式将接口的实现类全部获取、加载并实例化一遍。如果不想用某些实现类,它也被加载并实例化,造成浪费。
  • 其次他也打破了在加载我们自定义类的类加载的一个双亲委派机制,它会获取我们当前的上下文的加载器(appClassLoader),如果为空也会获取systemClassLoader进行加载,而不是层层递归,委托向上进行加载。
    • AppClassLoaderSystemClassLoader 在层级上是一样的。
    • 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 startup
        cl = 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 needed
    if (lookupIterator1 == null) {
        lookupIterator1 = newLookupIterator();
    }

    return new Iterator<S>() {

        // record reload count
        final int expectedReloadCount = ServiceLoader.this.reloadCount;

        // index into the cached providers list
        int 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();
        }

        @Override
        public boolean hasNext() {
            checkReloadCount();
            if (index < instantiatedProviders.size())
                return true;
            return lookupIterator1.hasNext();
        }

        @Override
        public 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>>() {
                @Override
                public boolean hasNext() {
                    return (first.hasNext() || second.hasNext());
                }
                @Override
                public 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 needed
    if (lookupIterator1 == null) {
        lookupIterator1 = newLookupIterator();
    }

    return new Iterator<S>() {

        // record reload count
        final int expectedReloadCount = ServiceLoader.this.reloadCount;

        // index into the cached providers list
        int 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();
        }

        @Override
        public boolean hasNext() {
            checkReloadCount();
            if (index < instantiatedProviders.size())
                return true;
            return lookupIterator1.hasNext();
        }

        @Override
        public 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 {
    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
        // 在这里初始化 Spring MVC 配置
    }
}

2. 使用 @HandlesTypes 注解:

MyWebAppInitializer 类上使用 @HandlesTypes 注解,以便在应用启动时将所有实现了 WebApplicationInitializer 接口的类传递给容器:

@HandlesTypes(WebApplicationInitializer.class)
public class MyWebAppInitializer implements WebApplicationInitializer {
    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
        // 在这里初始化 Spring MVC 配置
    }
}

3. 初始化 Spring MVC 配置:

onStartup 方法内,您可以使用 Spring 的注解来初始化 Spring MVC 配置,例如注册 DispatcherServlet、扫描控制器、视图解析器等:

public class MyWebAppInitializer implements WebApplicationInitializer {
    @Override
    public 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。如下图所示,容器中将排除TypeExcludeFilterAutoConfigurationExcludeFilter

  • @EnableAutoConfiguration是一个复合注解

    • 这里面用 @Import 注解导入了一个 AutoConfigurationImportSelector 类,这个AutoConfigurationImportSelector类实现了 DeferredImportSelector 接口,重写了selectImports方法,它会先判断自动配置这个注解是否开启,只有开启了就会调用getAutoConfigurationEntry方法,具体方法内会带调用getCandidateConfigurations方法,在这个方法内就回去调用一个SpringFactoriesLoaderloadFactoryNames方法去读取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、总结

好处:

  • 可拓展,减少硬编码,从而减少一个耦合性
  • 此外动态可插拔
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

redvelet

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值