SPI(Service Provider Loader)
介绍
面向对象编程中一般推荐面向接口编程,接口总是要有实现类的。一旦代码里面涉及到了实现类,如果需要替换一种实现类,就要更改代码。
SPI 就是解决这种问题的一种方法,为接口寻找服务实现的机制,将指定实现类的控制权转移到了程序外面。
通俗来说,就是将指定实现类的工作,从代码里面的 Interface i = new InterfaceImpl1(); 这个工作。
换成了Interface i = findImpl(Interface.class);,将 Interface 的实现类是 InterfaceImpl1 写在配置文件里。
这时候就会有两个问题需要搞清楚:
- findImpl 这个方法在 Java SPI 里对应的是哪个方法?
- 这个配置文件在哪?里面的格式应该是什么样的?
让我们来一一寻找这些问题的答案:
-
findImpl 这个方法在 Java SPI 里对应的是哪个方法?
ServiceLoader.load(Interface.class);这个代码是写在接口的工程里的
-
这个配置文件在哪?里面的格式应该是什么样的?
配置文件规定放在 classes 目录下的 META-INF/services/接口的全路径名 这个文本文件里
文本的文件名是接口的全路径名
文本内容是实现类的全路径名
配置文件放在实现类的工程里,相当于毛遂自荐:“我是xxx的实现类”,这样以后要更换实现类的时候,只需要更换实现类的依赖。
应用实践
SPI 机制在很多框架都使用到了,比如:
- Spring 框架
- 数据库加载驱动
- 日志接口
只有这样文字描述的话对初学者还是可能有点模糊,下面我们来实现个简单的日志接口例子,相信从这些代码里读者能体会到 SPI 机制的作用。
在这个日志例子里,我们的日志接口框架名字就叫 oklf4j(OK Logging Facade for Java,是不是跟 slf4j 很像啊哈哈哈哈),有三个 maven 工程:
- oklf4j-api,日志接口类
- nicelog-oklf4j-impl,日志实现类,简单称为 nicelog 好了
- oklf4j-test,就是个测试类,引入上面两个依赖
代码示例我都打包放在github上了,建议下载到本地,看着代码实际运行下会比较清晰
下面给大家介绍一下这三个工程里面都有哪些代码:
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
效果如图:
代码编写完后,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("参数错误"));
}
}
此时会有错误提示:
这是因为我们引入了接口,没有引入日志实现类,现在我们引入日志实现类
<dependency>
<groupId>pers.nicelog</groupId>
<artifactId>nicelog-oklf4j-impl</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
再运行一下测试类,就能看到这样的结果:
相信通过上面的代码流程,读者能够对开头这段对 SPI 的介绍描述有个更具体全面的理解。
SPI 就是解决这种问题的一种方法,为接口寻找服务实现的机制,将指定实现类的控制权转移到了程序外面。
通俗来说,就是将指定实现类的工作,从代码里面的
Interface i = new InterfaceImpl1();这个工作。换成了
Interface i = findImpl(Interface.class);,将Interface 的实现类是 InterfaceImpl1写在配置文件里。
如果有需要,可以自己动手仿造上面 nicelog-oklf4j-impl 工程创建一个新的日志实现类实践一下,印象会更深刻。