Java SPI组件机制

SPI机制

为什么使用spring.factories,不直接用扫描:为了扩展,解耦,能引用外部包,也就是组件化设计,这里就要引申出Java的SPI机制。

什么是 SPI 机制?
SPI 的全名为 Service Provider Interface. 这个是针对厂商或者插件的。在java.util.ServiceLoader的文档里有比较详细的介绍。
简单的总结下 java SPI 机制的思想。我们系统里抽象的各个模块,往往有很多不同的实现方案,比如日志模块的方案,xml解析模块、jdbc模块的方案等。面向的对象的设计里,我们一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。为了实现在模块装配的时候能不在程序里动态指明,这就需要一种服务发现机制。
java SPI 就是提供这样的一个机制:为某个接口寻找服务实现的机制。有点类似IOC的思想,就是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要。

在这里插入图片描述

ServiceLoader加载实现的过程:
比如Sentinel加载日志扩展的ServiceLoader

private static void resolveLoggers() {
    // NOTE: Here we cannot use {@code SpiLoader} directly because it depends on the RecordLog.
    ServiceLoader<Logger> loggerLoader = ServiceLoader.load(Logger.class);

    for (Logger logger : loggerLoader) {
        LogTarget annotation = logger.getClass().getAnnotation(LogTarget.class);
        if (annotation == null) {
            continue;
        }
        String name = annotation.value();
        // Load first encountered logger if multiple loggers are associated with the same name.
        if (StringUtil.isNotBlank(name) && !LOGGER_MAP.containsKey(name)) {
            LOGGER_MAP.put(name, logger);
            System.out.println("Sentinel Logger SPI loaded for <" + name + ">: "
                + logger.getClass().getCanonicalName());
        }
    }
}

API (Application Programming Interface)在大多数情况下,都是实现方制定接口并完成对接口的实现,调用方仅仅依赖接口调用,且无权选择不同实现。从使用人员上来说,API 直接被应用开发人员使用。
SPI (Service Provider Interface)是调用方来制定接口规范,决定使用哪个实现,调用方在调用时选择自己需要的外部实现。 从使用人员上来说,SPI 被框架扩展人员使用。
(其实在平台组件和业务组件现实场景中,往往SPI和API都是一个团队,也可以拆开SPI由平台来处理,调用方和被调用方应用由业务团队负责)

SPI就是面向接口编程、通过配置文件初始化和加载,实现组件发现和注册方式,实现模块之间解耦。

使用场景:
当业务系统需要调用的逻辑比较复杂的时候,仅仅通过API接口满足不了要求,那么就需要接口提供方封装SPI,将复杂内容封装到impl的jar包中提供给调用方使用。
(实际使用中根据具体情况决定)

下面是一些场景的举例:

JDBC场景

java统一定义了java.sql.Driver接口,但是并没有具体的实现,实现方式交给不同的服务厂商。

比如在MySQL的jar包mysql-connector-java.jar中,可以找到META-INF/services目录,该目录下会有一个名字为java.sql.Driver的文件,文件内容是com.mysql.cj.jdbc.Driver,这里面的内容就是针对Java中定义的接口的实现。
在这里插入图片描述

在这里插入图片描述

PostgreSQL的jar包PostgreSQL.jar中,也可以找到同样的配置文件,文件内容是org.postgresql.Driver,这是PostgreSQL对Java的java.sql.Driver的实现。
在这里插入图片描述
这样做的好处:
在没有使用SPI前,我们要显示的使用Class.forName初始化驱动,用了SPI之后就不需要显示的执行Class.forName,直接引用相关的包即可。

Spring Boot 中的 SPI 机制

Spring 中是一种类似与 Java SPI 的加载机制。它在 resources/META-INF/spring.factories 文件中配置接口的实现类名称,然后在程序中读取这些配置文件并实例化。
这种自定义的SPI机制是 Spring Boot Starter 实现的基础。
具体实现原理可以百度Spring Boot自动装配源码解析相关文章。
下面这张图是Spring Boot自动装备原理流程:
在这里插入图片描述

Spring MVC的SPI机制

tomcat启动后,可以根据servlet3.0的规范,通过SPI机制,将实现了ServletContainerInitializer 接口的类全部进行加载
由ContextConfig初始化加载:org.apache.catalina.startup.ContextConfig#processServletContainerInitializers

protected void processServletContainerInitializers() {

        List<ServletContainerInitializer> detectedScis;
        try {
            WebappServiceLoader<ServletContainerInitializer> loader = new WebappServiceLoader<>(context);
            detectedScis = loader.load(ServletContainerInitializer.class);
        } catch (IOException e) {
            log.error(sm.getString(
                    "contextConfig.servletContainerInitializerFail",
                    context.getName()),
                e);
            ok = false;
            return;
        }
      ......

通过tomcat的org.apache.catalina.startup.WebappServiceLoader实现SPI加载。
spring-web[version].jar中对ServletContainerInitializer的定义
在这里插入图片描述

(相当于Tomcat提供了初始化的一套SPI标准,Spring基于这个标准实现)
实现后续的初始化动作,后续初始化的动作这里省略…

特别注意:

注意:这种是基于war方式启动的才使用了SPI机制,如果是直接应用内嵌Tomcat启动方式,则是另外一套实现逻辑。

代码注释中可以看到:
 * Note that a WebApplicationInitializer is only needed if you are building a war file and
 * deploying it. If you prefer to run an embedded web server then you won't need this at
 * all.

一般基于SpringBoot内嵌Tomcat的启动方式是这样的:
在这里插入图片描述

核心方法是:org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext#createWebServer
这里会获取ServletWebServerFactory,由这个方法返回决定使用哪个内置容器进行初始化:

protected ServletWebServerFactory getWebServerFactory() {
	// Use bean names so that we don't consider the hierarchy
	// 用来从classpath中查找具体哪个容器类(tomcat, jetty,netty或undertow)
	String[] beanNames = getBeanFactory().getBeanNamesForType(ServletWebServerFactory.class);
	if (beanNames.length == 0) {
		throw new ApplicationContextException("Unable to start ServletWebServerApplicationContext due to missing "
				+ "ServletWebServerFactory bean.");
	}
	if (beanNames.length > 1) {
		throw new ApplicationContextException("Unable to start ServletWebServerApplicationContext due to multiple "
				+ "ServletWebServerFactory beans : " + StringUtils.arrayToCommaDelimitedString(beanNames));
	}
	return getBeanFactory().getBean(beanNames[0], ServletWebServerFactory.class);
}

关于ServletWebServerFactory对象如何定义的?
Spring Boot启动类加上@SpringBootApplication或@EnableAutoConfiguration注解后,会导入一个AutoConfigurationImportSelector的类(前面讲到),而这个类会去读取spring-boot-autoconfigure包里spring.factories下key为EnableAutoConfiguration对应的全限定名的值,其中包含了ServletWebServerFactoryAutoConfiguration这个类。
在这里插入图片描述

最终引用这个类实现不同容器的装载:
org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryConfiguration
具体参考代码实现

SLFJ 日志门面 场景

SLF4J加载不同提供商的日志实现类,比如log4j、log4j2、logback…
SLF4J的门面处理,SLF4J统一了所有日志的入口和实现,让不同日志采用统一的输出方式。
在这里插入图片描述

具体如何实现:
spring默认使用的是jcl输出日志,为了适配,slf4j采用门面模式进行适配:
针对不同日志接口一般的适配过程:
在这里插入图片描述

SLF4J1.7的版本采用的静态绑定,直接读取org.slf4j.impl.StaticLoggerBinder实现类
SLF4J需要1.8+以上版本才使用了SPI机制,通过SLF4JServiceProvider SPI方式实现对日志实现类的扩展
(SLF4J1.8以上版本一直是alpha状态,目前最新的版本都是1.7.XX,可能是考虑到改动了SPI机制,导致下游日志都得适配,所以一直没推广起来)

log4j-to-slf4j的SPI:log4j适配slf4j接口是通过SPI方式,实现了log4j的provider实现适配:public class SLF4JProvider extends Provider {
在这里插入图片描述

修改log4j的provider使用SLF4J的接口进行适配。

jul-to-slf4j:jul适配到slf4j是通过SpringBoot的自动配置实现。
org.springframework.boot.context.logging.LoggingapplicationListener -> initialize
创建LogbackLoggingSystem对象,并初始化,调用configureJdkLoggingBridgeHandler,注入SLF4JBridgeHandler实现替代jul日志

public void beforeInitialize() {
	super.beforeInitialize();
	configureJdkLoggingBridgeHandler();
}
private void configureJdkLoggingBridgeHandler() {
	try {
		if (isBridgeJulIntoSlf4j()) {
			removeJdkLoggingBridgeHandler();
			SLF4JBridgeHandler.install();
		}
	}
	catch (Throwable ex) {
		// Ignore. No java.util.logging bridge is installed.
	}
}

在这里插入图片描述

Sentinel中应用
  • 加载日志实现
  • 加载限流槽点的扩展实现。

加载日志使用的是Java自带的ServiceLoader。
加载槽点使用的是Sentinel封装的com.alibaba.csp.sentinel.spi.SpiLoader,类似ServlceLoader的实现。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-d7JQTfJr-1660094547831)(vx_images/511222115235573.png =800x)]

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值