模仿JDK实现SPI服务
此文章不是介绍SPI服务的概念,是基于对SPI有一定了解,想深入研究其实现原理的文章。本文介绍了SPI实现的整个过程,并结合配置文件的配置模拟实际业务场景中服务的调用。话不多说,直接开码。
1、准备环境
1.1 安装lombok插件,pom文件导入以下依赖
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.8</version>
<scope>provided</scope>
</dependency>
<!--日志-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.28</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.28</version>
</dependency>
<!--yaml读取-->
<dependency>
<groupId>org.jyaml</groupId>
<artifactId>jyaml</artifactId>
<version>1.3</version>
</dependency>
1.2 resources目录结构如下
- 日志配置log4j.properties
# 日记级别(单个级别) 文件/控制台
log4j.rootLogger=debug, stdout,file
# Redirect log messages to console
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target=System.out
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n
# Rirect log messages to a log file
log4j.appender.file=org.apache.log4j.RollingFileAppender
log4j.appender.file.File=test.log
log4j.appender.file.MaxFileSize=5MB
log4j.appender.file.MaxBackupIndex=10
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n
- SPI接口文件com.freesky.spi.myspi.IHello
com.freesky.spi.myspi.HelloImpl1
com.freesky.spi.myspi.HelloImpl2
- 配置文件myapplication.yml
hello:
# 指定实现
impl: com.freesky.spi.myspi.HelloImpl2
2、准备服务
2.1 创建服务接口
public interface IHello {
void hello();
}
2.2 准备两个实现类
public class HelloImpl1 implements IHello {
@Override
public void hello() {
System.out.println("妖怪!出来,我是你孙爷爷!");
}
}
public class HelloImpl2 implements IHello {
@Override
public void hello() {
System.out.println("大师兄不好了,师傅和二师兄被妖怪抓走了");
}
}
2.2 yaml读取工具类
import org.ho.yaml.Yaml;
import java.io.InputStream;
import java.util.Map;
import java.util.Objects;
/**
* @author 陈玉林
* @projectName java-basic
* @className YamlUtil
* @description TODO
* @date 2019/12/12 21:27
*/
public class YamlUtil {
private YamlUtil() {}
private static final String YAML_SPLIT_SYMBOL = "\\.";
@SuppressWarnings("unchecked")
public static Map<String, String> getYamlMap(String ymalResourceName) {
InputStream resourceAsStream = YamlUtil.class.getClassLoader().getResourceAsStream(ymalResourceName);
return (Map<String, String>) Yaml.load(resourceAsStream);
}
public static String getYamlValue(Map map, String property) {
Objects.requireNonNull(property, "属性不可为空");
String[] propertyKeys = property.split(YAML_SPLIT_SYMBOL);
for (String next : propertyKeys) {
Object o = map.get(next);
if (o instanceof Map) {
map = (Map) o;
} else {
return String.valueOf(map.get(next));
}
}
return "";
}
}
2.3 实现服务加载器MyServiceLoader
import lombok.extern.slf4j.Slf4j;
import java.io.*;
import java.net.URL;
import java.util.*;
@Slf4j
public class MyServiceLoader<S> {
/**
* 配置文件的路径
*/
private static final String PREFIX = "META-INF/myservices/";
/**
* 被加载的服务类或接口
*/
private final Class<S> service;
/**
* 缓存已加载的服务类集合
*/
private LinkedHashMap<String, S> providers = new LinkedHashMap<>();
/**
* 类加载器
*/
private final ClassLoader loader;
private MyServiceLoader(Class<S> svc, ClassLoader cl) {
service = Objects.requireNonNull(svc, "服务接口不可为空");
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
reload();
}
private void reload() {
providers.clear();
String fullName = PREFIX + service.getName();
try {
Enumeration<URL> resources = loader.getResources(fullName);
while (resources.hasMoreElements()) {
URL url = resources.nextElement();
String filename = url.getFile();
List<String> implNames = readLines(new File(filename));
Set<String> noRepeatImplNames = getNoRepeatImplNames(implNames);
log.info("实现类名称{}", Arrays.toString(noRepeatImplNames.toArray()));
if (!noRepeatImplNames.isEmpty()) {
noRepeatImplNames.forEach(name -> {
S result = getInstance(name);
if (Objects.nonNull(result)) {
providers.put(name, result);
}
});
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* @description 服务文件内容读取
* @status 使用中
* @param
* @author 陈玉林
* @date 2019/12/13 20:34
*/
@SuppressWarnings("unchecked")
private List<String> readLines(File file) throws IOException {
if (file.exists()) {
if (file.isDirectory()) {
throw new IOException("文件 '" + file + "'是文件夹");
} else if (!file.canRead()) {
throw new IOException("文件 '" + file + "' 不可读");
} else {
FileInputStream input = new FileInputStream(file);
InputStreamReader reader = new InputStreamReader(input);
BufferedReader bufferedReader = new BufferedReader(reader);
List<String> list = new ArrayList();
for(String line = bufferedReader.readLine(); line != null; line = bufferedReader.readLine()) {
list.add(line);
}
return list;
}
} else {
throw new FileNotFoundException("文件"+file+"不存在");
}
}
/**
* @description 反射获取实现类对象
* @status 使用中
* @param
* @author 陈玉林
* @date 2019/12/13 20:34
*/
private S getInstance(String name) {
try {
Class<?> aClass = Class.forName(name, false, loader);
Object instance = aClass.newInstance();
return service.cast(instance);
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
e.printStackTrace();
}
return null;
}
/**
* @param
* @description 去重
* @status 使用中
* @author 陈玉林
* @date 2019/12/13 17:28
*/
private Set<String> getNoRepeatImplNames(List<String> implNames) {
return new HashSet<>(implNames);
}
private List<S> getServices() {
List<S> list = new ArrayList<>();
Set<Map.Entry<String, S>> entries = providers.entrySet();
entries.forEach(i -> {
list.add(i.getValue());
});
return list;
}
public static <S> List<S> load(Class<S> s, ClassLoader cl) {
return new MyServiceLoader<>(s, cl).getServices();
}
}
2.4 测试
import com.freesky.common.YamlUtil;
import java.util.List;
public class Main {
public static void main(String[] args) {
final String targetImpl = YamlUtil.getYamlValue(YamlUtil.getYamlMap("myapplication.yml"), "hello.impl");
List<IHello> services = MyServiceLoader.load(IHello.class, MyServiceLoader.class.getClassLoader());
services.forEach(item -> {
//模拟调用场景,进行配置文件配置实现类的服务调用
if (item.getClass().getName().equals(targetImpl)) {
item.hello();
}
});
}
}
输出:
2019-12-13 20:42:43 INFO MyServiceLoader:44 - 实现类名称[com.freesky.spi.myspi.HelloImpl2, com.freesky.spi.myspi.HelloImpl1]
大师兄不好了,师傅和二师兄被妖怪抓走了
修改yml配置文件为以下内容
hello:
# 指定实现
impl: com.freesky.spi.myspi.HelloImpl1
输出:
2019-12-13 20:42:18 INFO MyServiceLoader:44 - 实现类名称[com.freesky.spi.myspi.HelloImpl2, com.freesky.spi.myspi.HelloImpl1]
妖怪!出来,我是你孙爷爷!
只有把命运掌握在自己手中,从今天起开始努力,即使暂时看不到希望,也要相信自己。因为比你牛几倍的人,依然在努力。