带你从零了解 Java SPI 机制

92 阅读6分钟

SPI(Service Provider Loader)

介绍

面向对象编程中一般推荐面向接口编程,接口总是要有实现类的。一旦代码里面涉及到了实现类,如果需要替换一种实现类,就要更改代码。

SPI 就是解决这种问题的一种方法,为接口寻找服务实现的机制,将指定实现类的控制权转移到了程序外面

通俗来说,就是将指定实现类的工作,从代码里面的 Interface i = new InterfaceImpl1(); 这个工作。

换成了Interface i = findImpl(Interface.class);,将 Interface 的实现类是 InterfaceImpl1 写在配置文件里。

这时候就会有两个问题需要搞清楚:

  1. findImpl 这个方法在 Java SPI 里对应的是哪个方法?
  2. 这个配置文件在哪?里面的格式应该是什么样的?

让我们来一一寻找这些问题的答案:

  1. findImpl 这个方法在 Java SPI 里对应的是哪个方法?

    ServiceLoader.load(Interface.class);

    这个代码是写在接口的工程里的

  2. 这个配置文件在哪?里面的格式应该是什么样的?

    配置文件规定放在 classes 目录下的 META-INF/services/接口的全路径名 这个文本文件里

    文本的文件名是接口的全路径名

    文本内容是实现类的全路径名

    配置文件放在实现类的工程里,相当于毛遂自荐:“我是xxx的实现类”,这样以后要更换实现类的时候,只需要更换实现类的依赖。

应用实践

SPI 机制在很多框架都使用到了,比如:

  1. Spring 框架
  2. 数据库加载驱动
  3. 日志接口

只有这样文字描述的话对初学者还是可能有点模糊,下面我们来实现个简单的日志接口例子,相信从这些代码里读者能体会到 SPI 机制的作用。

在这个日志例子里,我们的日志接口框架名字就叫 oklf4j(OK Logging Facade for Java,是不是跟 slf4j 很像啊哈哈哈哈),有三个 maven 工程:

  1. oklf4j-api,日志接口类
  2. nicelog-oklf4j-impl,日志实现类,简单称为 nicelog 好了
  3. oklf4j-test,就是个测试类,引入上面两个依赖

代码示例我都打包放在github上了,建议下载到本地,看着代码实际运行下会比较清晰

链接:github.com/hyb0302/blo…

下面给大家介绍一下这三个工程里面都有哪些代码:

oklf4j-api

maven工程坐标:

<dependency>
    <groupId>pers.oklf4j</groupId>
    <artifactId>oklf4j-api</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

Logger 日志接口类,规范有什么日志操作

package pers.oklf4j.api;
​
/**
 * 日志接口
 */
public interface Logger {
​
    /**
     * 信息日志接口
     * @param msg
     */
    void info(String msg);
​
    /**
     * 错误日志接口
     * @param msg
     * @param t
     */
    void error(String msg, Throwable t);
​
}

LoggerPoxy,日志代理类接口,创建这个代理类接口主要是为了在日志的前面打上对应的类名

跟我们在项目中用的 private final Logger logger = LoggerFactory.getLogger(this.getClass()); 相同意思

package pers.oklf4j.proxy;
​
import pers.oklf4j.api.Logger;
​
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
​
/**
 * 日志代理类,主要是为了实现在日志消息前加上类名
 */
public class LoggerPoxy implements Logger {
​
    private final Logger origin;
​
    private final Class<?> clazz;
​
    /**
     * 这里时间格式也可以拓展成从配置文件中取
     */
    private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
​
    public LoggerPoxy(Logger origin, Class<?> clazz) {
        this.origin = origin;
        this.clazz = clazz;
    }
​
    @Override
    public void info(String msg) {
        origin.info(prefix() + msg);
    }
​
    @Override
    public void error(String msg, Throwable t) {
        origin.error(prefix() + msg, t);
    }
​
    private String prefix() {
        return DATE_TIME_FORMATTER.format(LocalDateTime.now()) + "--" + clazz.getName() + ": ";
    }
}
​

还有最后一个类就是 Oklf4jFactory,帮助我们获取 Logger 实现类

SPI 的核心就是这段代码

ServiceLoader<Logger> loggerServiceLoader = ServiceLoader.load(Logger.class);

通俗来说,就是将指定实现类的工作,从代码里面的 Interface i = new InterfaceImpl1(); 这个工作。

换成了Interface i = findImpl(Interface.class);,将 Interface 的实现类是 InterfaceImpl1 写在配置文件里。

package pers.oklf4j.factory;
​
import pers.oklf4j.api.Logger;
import pers.oklf4j.proxy.LoggerPoxy;
​
import java.util.Map;
import java.util.ServiceLoader;
import java.util.concurrent.ConcurrentHashMap;
​
/**
 * 获取日志操作类实例
 */
public class Oklf4jFactory {
​
    private static final Map<Class<?>, LoggerPoxy> LOGGER_CACHE = new ConcurrentHashMap<>();
​
    private static Logger LOGGER;
​
    static  {
        ServiceLoader<Logger> loggerServiceLoader = ServiceLoader.load(Logger.class);
        //只取第一个
        for (Logger logger : loggerServiceLoader) {
            LOGGER = logger;
            break;
        }
    }
​
    /**
     * 获取日志操作对象
     * @param clazz
     * @return
     */
    public static Logger getLogger(Class<?> clazz) {
        if (LOGGER == null) {
            System.err.println("没有指定日志实现类");
        }
        return LOGGER_CACHE.computeIfAbsent(clazz, c -> new LoggerPoxy(LOGGER, c));
    }
​
}
​

代码编写完后,maven 要 install 打包本项目到本地仓库,供后面的工程调用依赖

nicelog-oklf4j-impl

这是日志实现工程,里面引入了对应的日志接口,实现接口记录日志

maven 坐标:

<dependency>
    <groupId>pers.nicelog</groupId>
    <artifactId>nicelog-oklf4j-impl</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

我们首先要引入我们上一步的日志接口的依赖:

<dependency>
    <groupId>pers.oklf4j</groupId>
    <artifactId>oklf4j-api</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

这里只是简单用控制台打印了一下日志

package pers.nicelog.impl;
​
import pers.oklf4j.api.Logger;
​
/**
 * 日志实现类,简单使用控制台打印
 */
public class NiceLoggerImpl implements Logger {
​
    private static final String PREFIX = "[NiceLoggerImpl]";
​
    @Override
    public void info(String s) {
        System.out.println(PREFIX + s);
    }
​
    @Override
    public void error(String s, Throwable throwable) {
        System.err.println(PREFIX + s);
        throwable.printStackTrace();
    }
​
}

到这里我们就得在实现类的工程里指定日志接口的实现类了,这样我们以后工程里的日志实现类要更换的话,只需要更换实现类的依赖就完成了,不需要改动代码。

配置文件要放在哪呢?

配置文件规定放在 classes 目录下的 META-INF/services/接口的全路径名 这个文本文件里

在我们的这个例子里,接口是 Logger,全路径名是 pers.oklf4j.api.Logger

那么目录就应该是 META-INF/services/pers.oklf4j.api.Logger

内容是什么呢?

文本内容是实现类的全路径名

实现类是 NiceLoggerImpl,全路径名是 pers.nicelog.impl.NiceLoggerImpl

那么文本META-INF/services/pers.oklf4j.api.Logger 的内容就是pers.nicelog.impl.NiceLoggerImpl

效果如图:

image.png 代码编写完后,maven 要 install 打包本项目到本地仓库,供后面的工程调用依赖

接下来就是测试类工程

oklf4j-test

引入日志接口依赖:

<dependency>
    <groupId>pers.oklf4j</groupId>
    <artifactId>oklf4j-api</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

这时写下测试代码:

import pers.oklf4j.api.Logger;
import pers.oklf4j.factory.Oklf4jFactory;
​
import java.util.Map;
​
public class Test {
​
​
    public static void main(String[] args) throws InterruptedException {
        Logger testLogger = Oklf4jFactory.getLogger(Test.class);
        testLogger.info("测试信息日志");
        testLogger.error("测试错误日志", new IllegalArgumentException("参数错误"));
​
        Thread.sleep(500L);
        System.out.println();
        Logger mapLogger = Oklf4jFactory.getLogger(Map.class);
        mapLogger.info("测试信息日志");
        mapLogger.error("测试错误日志", new IllegalArgumentException("参数错误"));
    }
}
​

此时会有错误提示:

image.png

这是因为我们引入了接口,没有引入日志实现类,现在我们引入日志实现类

<dependency>
    <groupId>pers.nicelog</groupId>
    <artifactId>nicelog-oklf4j-impl</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

再运行一下测试类,就能看到这样的结果:

image-20230225200049623.png

相信通过上面的代码流程,读者能够对开头这段对 SPI 的介绍描述有个更具体全面的理解。

SPI 就是解决这种问题的一种方法,为接口寻找服务实现的机制,将指定实现类的控制权转移到了程序外面

通俗来说,就是将指定实现类的工作,从代码里面的 Interface i = new InterfaceImpl1(); 这个工作。

换成了Interface i = findImpl(Interface.class);,将 Interface 的实现类是 InterfaceImpl1 写在配置文件里。

如果有需要,可以自己动手仿造上面 nicelog-oklf4j-impl 工程创建一个新的日志实现类实践一下,印象会更深刻。