Java SPI(Service Provider Interface)是 Java 标准库提供的一种服务提供者框架,它允许开发人员定义一组服务接口,然后通过 SPI 机制让第三方服务提供者来实现这些接口。这种机制允许应用程序在不修改代码的情况下动态地加载和使用第三方实现。Spring Boot 利用了 Java SPI 机制来实现自动配置功能,使得开发人员可以更加方便地集成第三方库和框架。

Java SPI 的实现原理

Java SPI 的核心是提供了一种在运行时发现和加载服务实现的机制。在具体实现上,服务提供者在自己的 JAR 包中提供一个特定的配置文件,该文件列出了该服务的实现类。Java 库会自动加载这些服务提供者,并将它们实例化为服务接口的具体实现。

Java SPI 的实现步骤如下:

1.假设我们正在开发一个日志框架,需要支持多种日志记录器,我们可以定义一个 cc.oolo.logging.Logger 接口如下所示:

package cc.oolo.logging;

public interface Logger {
    void debug(String message);

    void info(String message);

    void warn(String message);

    void error(String message, Throwable throwable);
}

2.然后我们可以定义两个实现该接口的日志记录器类 cc.oolo.logging.ConsoleLoggercc.oolo.logging.FileLogger,分别记录日志到控制台和文件中,如下所示:

package cc.oolo.logging;

public class ConsoleLogger implements Logger {
    @Override
    public void debug(String message) {
        System.out.println("ConsoleLogger [DEBUG] " + message);
    }

    @Override
    public void info(String message) {
        System.out.println("ConsoleLogger [INFO] " + message);
    }

    @Override
    public void warn(String message) {
        System.out.println("ConsoleLogger [WARN] " + message);
    }

    @Override
    public void error(String message, Throwable throwable) {
        System.out.println("ConsoleLogger [ERROR] " + message);
    }
}
package cc.oolo.logging;

public class FileLogger implements Logger {


    @Override
    public void debug(String message) {
        System.out.println("FileLogger [DEBUG] " + message);
    }

    @Override
    public void info(String message) {
        System.out.println("FileLogger [INFO] " + message);
    }

    @Override
    public void warn(String message) {
        System.out.println("FileLogger [WARN] " + message);
    }

    @Override
    public void error(String message, Throwable throwable) {
        System.out.println("FileLogger [ERROR] " + message);
    }

}

src/main/resources 目录下创建 META-INF/services 文件夹,并在该文件夹下创建一个名为 cc.oolo.logging.Logger 的文件,文件内容为服务提供者实现类的全限定名,如下所示:

cc.oolo.logging.ConsoleLogger
cc.oolo.logging.FileLogger

我们可以编写一个简单的测试程序 cc.oolo.App,通过 SPI 机制加载服务提供者实现类并调用其方法,如下所示:

package cc.oolo;

import cc.oolo.logging.Logger;

import java.util.ServiceLoader;

public class App {
    public static void main(String[] args) {
        ServiceLoader<Logger> loader = ServiceLoader.load(Logger.class);
        String message = "Hello, World!";
        try {
            int a = 10 / 0; // 抛出异常
        } catch (Exception e) {
            for (Logger logger : loader) {
                logger.error(message, e);
            }
        }
    }
}

运行结果:

ConsoleLogger [ERROR] Hello, World!
FileLogger [ERROR] Hello, World!

进程已结束,退出代码0

如果想要切换使用 FileLogger 实现,只需要修改该文件的内容为:

cc.oolo.logging.FileLogger

如果您想要切换使用 ConsoleLogger 实现,只需要修改该文件的内容为:

cc.oolo.logging.ConsoleLogger

然后重新编译并运行程序即可。

Springboot 自动配置

Spring Boot自动配置是一种约定优于配置的方式,它可以根据应用程序的依赖和配置自动配置Spring应用程序上下文中的bean。这意味着开发者不需要手动编写大量的配置代码,而是可以通过引入合适的依赖来快速搭建一个可工作的应用程序。

Spring Boot自动配置的原理

Spring Boot自动配置的原理是通过Spring的条件化配置机制实现的。条件化配置机制是Spring框架中的一个重要特性,它基于一组条件来决定是否应该创建或跳过某个bean定义。Spring Boot利用条件化配置机制来判断哪些bean需要被创建,哪些需要被跳过。

Spring Boot自动配置有三个关键组件:

  • 自动配置类:定义了需要自动配置的bean,以及这些bean的创建条件。
  • 自动配置属性:定义了需要自动配置的bean的属性。
  • 自动配置条件:定义了哪些条件下需要自动配置某个bean。

当一个应用程序启动时,Spring Boot会自动扫描classpath下的所有自动配置类,并根据条件化配置机制决定哪些bean需要被创建。

以Redisson举例,我们在SpringBoot中使用Redisson的时候,一般会引入一个包

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>${version}</version>
</dependency>

spring.factories文件中定义了一组键值对,其中键表示自动配置类或者配置类的全限定名,值则表示该类所对应的配置类或者实现类的全限定名。当Spring Boot启动时,会自动扫描spring.factories文件,并根据其中的配置信息来自动配置所需要的类或者进行相应的配置。

下图所示:org.redisson.spring.starter.RedissonAutoConfiguration就是需要被SpringBoot 加载的配置类

在我们使用redisson的时候,一般都是先注入RedissonClient,然后调用RedissonClient的方法

    @Autowired
    private RedissonClient redissonClient;

这是因为在RedissonAutoConfiguration.class类中,通过@Bean的方式将RedissonClient配置到了Spring的IOC容器中,@ConditionalOnMissingBean注解的用途为,当IOC容器中不存在RedissonClient的Bean的时候,才会执行方法中的逻辑

    @Bean(
        destroyMethod = "shutdown"
    )
    @ConditionalOnMissingBean({RedissonClient.class})
    public RedissonClient redisson() throws IOException {
        Config config = null;
        Method clusterMethod = ReflectionUtils.findMethod(RedisProperties.class, "getCluster");
        Method timeoutMethod = ReflectionUtils.findMethod(RedisProperties.class, "getTimeout");
        Object timeoutValue = ReflectionUtils.invokeMethod(timeoutMethod, this.redisProperties);
        int timeout;
        Method nodesMethod;
        if (null == timeoutValue) {
            timeout = 10000;
        } else if (!(timeoutValue instanceof Integer)) {
            nodesMethod = ReflectionUtils.findMethod(timeoutValue.getClass(), "toMillis");
            timeout = ((Long)ReflectionUtils.invokeMethod(nodesMethod, timeoutValue)).intValue();
        } else {
            timeout = (Integer)timeoutValue;
        }

        if (this.redissonProperties.getConfig() != null) {
            try {
                config = Config.fromYAML(this.redissonProperties.getConfig());
            } catch (IOException var13) {
                try {
                    config = Config.fromJSON(this.redissonProperties.getConfig());
                } catch (IOException var12) {
                    throw new IllegalArgumentException("Can't parse config", var12);
                }
            }
        } else if (this.redissonProperties.getFile() != null) {
            try {
                InputStream is = this.getConfigStream();
                config = Config.fromYAML(is);
            } catch (IOException var11) {
                try {
                    InputStream is = this.getConfigStream();
                    config = Config.fromJSON(is);
                } catch (IOException var10) {
                    throw new IllegalArgumentException("Can't parse config", var10);
                }
            }
        } else if (this.redisProperties.getSentinel() != null) {
            nodesMethod = ReflectionUtils.findMethod(RedisProperties.Sentinel.class, "getNodes");
            Object nodesValue = ReflectionUtils.invokeMethod(nodesMethod, this.redisProperties.getSentinel());
            String[] nodes;
            if (nodesValue instanceof String) {
                nodes = this.convert(Arrays.asList(((String)nodesValue).split(",")));
            } else {
                nodes = this.convert((List)nodesValue);
            }

            config = new Config();
            ((SentinelServersConfig)config.useSentinelServers().setMasterName(this.redisProperties.getSentinel().getMaster()).addSentinelAddress(nodes).setDatabase(this.redisProperties.getDatabase()).setConnectTimeout(timeout)).setPassword(this.redisProperties.getPassword());
        } else {
            Method method;
            if (clusterMethod != null && ReflectionUtils.invokeMethod(clusterMethod, this.redisProperties) != null) {
                Object clusterObject = ReflectionUtils.invokeMethod(clusterMethod, this.redisProperties);
                method = ReflectionUtils.findMethod(clusterObject.getClass(), "getNodes");
                List<String> nodesObject = (List)ReflectionUtils.invokeMethod(method, clusterObject);
                String[] nodes = this.convert(nodesObject);
                config = new Config();
                ((ClusterServersConfig)config.useClusterServers().addNodeAddress(nodes).setConnectTimeout(timeout)).setPassword(this.redisProperties.getPassword());
            } else {
                config = new Config();
                String prefix = "redis://";
                method = ReflectionUtils.findMethod(RedisProperties.class, "isSsl");
                if (method != null && (Boolean)ReflectionUtils.invokeMethod(method, this.redisProperties)) {
                    prefix = "rediss://";
                }

                ((SingleServerConfig)config.useSingleServer().setAddress(prefix + this.redisProperties.getHost() + ":" + this.redisProperties.getPort()).setConnectTimeout(timeout)).setDatabase(this.redisProperties.getDatabase()).setPassword(this.redisProperties.getPassword());
            }
        }

        if (this.redissonAutoConfigurationCustomizers != null) {
            Iterator var19 = this.redissonAutoConfigurationCustomizers.iterator();

            while(var19.hasNext()) {
                RedissonAutoConfigurationCustomizer customizer = (RedissonAutoConfigurationCustomizer)var19.next();
                customizer.customize(config);
            }
        }

        return Redisson.create(config);
    }

制作starter

以一个加密工具类为例:

创建一个新的工程

引入加密工具类

      <dependency>
          <groupId>commons-codec</groupId>
          <artifactId>commons-codec</artifactId>
      </dependency>

注释pom.xml中Springboot的打包工具

首先创建一个接口Digest

package cc.oolo.digest;

public interface Digest {
    public String digest(String raw);
}

创建Md5DigestShaDigest分别代表md5加密和sha256加密

package cc.oolo.digest.impl;

import cc.oolo.digest.Digest;
import org.apache.commons.codec.digest.DigestUtils;

public class Md5Digest implements Digest {
    @Override
    public String digest(String raw) {
        System.out.println("使用md5");
        return DigestUtils.md5Hex(raw);
    }
}
package cc.oolo.digest.impl;

import cc.oolo.digest.Digest;
import org.apache.commons.codec.digest.DigestUtils;

public class ShaDigest implements Digest {
    @Override
    public String digest(String raw) {
        System.out.println("使用sha256");
        return DigestUtils.sha256Hex(raw);
    }
}

创建conf文件夹,并创建一个Config类,使用@ConditionalOnProperty注解,根据yml配置文件中的digest.type判断,digest.type=md5,那么就加载md5Digest,如果digest.type=sha,那么就加载shaDigest

package cc.oolo.digest.conf;

import cc.oolo.digest.Digest;
import cc.oolo.digest.impl.Md5Digest;
import cc.oolo.digest.impl.ShaDigest;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class Config {

    @Bean
    @ConditionalOnProperty(prefix = "digest",name = "type",havingValue = "md5")
    public Digest md5Digest(){
        System.out.println("Loading Md5Digest Object");
        return new Md5Digest();
    }

    @Bean
    @ConditionalOnProperty(prefix = "digest",name = "type",havingValue = "sha")
    public Digest shaDigest(){
        System.out.println("Loading ShaDigest Object");
        return new ShaDigest();
    }
}

然后在resource.META-INFO文件夹下创建一个spring.factories文件,写入以下配置

org.springframework.boot.autoconfigure.EnableAutoConfiguration=cc.oolo.digest.conf.Config

最后执行mvn install,将项目本身编译并打包到本地仓库

切换到另外一个项目,引入我们刚刚发布的包

        <dependency>
            <groupId>cc.oolo</groupId>
            <artifactId>digest-spring-boot-starter</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>

然后在apllication.yml中添加配置

digest:
  type: sha

测试:

    @Resource
    private Digest digest;

    @Test
    public void digestTest(){
        System.out.println(digest.digest("测试"));
    }

将配置文件中的sha修改为md5,再次进行测试

可以看到,我们可以通过配置文件来控制spring加载的bean。

总结

Java SPI 是一种服务发现机制,它允许第三方库在不需要依赖特定的实现类的情况下,动态地加载和使用某个接口的实现类。Java SPI 的实现步骤包括创建接口、创建实现类、创建配置文件和加载实现类等。

Spring Boot 自动配置是 Spring Boot 框架提供的一种自动化配置机制,它通过分析应用程序的类路径和已有的 Bean 定义,自动地配置 Spring 应用程序的环境。Spring Boot 自动配置的实现步骤包括创建自动配置类、提供默认 Bean 的配置和加载自动配置类等。

Java SPI 和 Spring Boot 自动配置都是非常有用的技术,它们可以帮助开发人员实现插件化和自动化配置的功能,提高开发效率和应用程序的灵活性。但是它们也存在一些缺点,例如 Java SPI 无法传递参数和处理依赖关系,而 Spring Boot 自动配置可能会出现配置冲突或者不符合预期的配置等问题。

因此,在使用 Java SPI 和 Spring Boot 自动配置时,需要根据实际情况进行权衡和选择,以达到最好的应用效果。

最后修改:2023 年 05 月 18 日
如果觉得我的文章对你有用,请随意赞赏