聊聊如何实现一个支持键值对的SPI
前言
如果用过JDK提供的SPI机制的朋友,大概就会知道它无法按需加载。之前写过一篇文章聊聊基于jdk实现的spi如何与spring整合实现依赖注入。利用spring的依赖注入来实现spi按需加载,这种方案就是要借用spring。今天我们在聊聊另外一种实现方式,就是我们自己手写一个
实现思路
整体思路和jdk实现spi差不多,如果对jdk实现的spi不了解,可以查看我之前写的文章java之spi机制简介。差别就是我们在配置文件是以key-value的形式存在,形如
springMysql=com.github.lybgeek.dialect.mysql.SpringMysqlDialect
实现逻辑
1、约定好要进行解析的目录,比如META-INF/services/
private static final String SERVICE_DIRECTORY = "META-INF/services/";
2、约定好要解析的文件名命名,比如
com.github.lybgeek.dialect.SpringSqlDialect
3、约定好文件内容格式,比如
springMysql=com.github.lybgeek.dialect.mysql.SpringMysqlDialect
4、获取约定好的目录,解析文件,并将相应内容放入缓存
/**
* Load files under SERVICE_DIRECTORY.
*/
private void loadDirectory(final Map<String, Class<?>> classes) {
String fileName = SERVICE_DIRECTORY + clazz.getName();
try {
ClassLoader classLoader = ExtensionLoader.class.getClassLoader();
Enumeration<URL> urls = classLoader != null ? classLoader.getResources(fileName)
: ClassLoader.getSystemResources(fileName);
if (urls != null) {
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
loadResources(classes, url);
}
}
} catch (IOException t) {
log.error("load extension class error {}", fileName, t);
}
}
private void loadResources(final Map<String, Class<?>> classes, final URL url) throws IOException {
try (InputStream inputStream = url.openStream()) {
Properties properties = new Properties();
properties.load(inputStream);
properties.forEach((k, v) -> {
String name = (String) k;
String classPath = (String) v;
if (StringUtils.isNotBlank(name) && StringUtils.isNotBlank(classPath)) {
try {
loadClass(classes, name, classPath);
} catch (ClassNotFoundException e) {
throw new IllegalStateException("load extension resources error", e);
}
}
});
} catch (IOException e) {
throw new IllegalStateException("load extension resources error", e);
}
}
private void loadClass(final Map<String, Class<?>> classes,
final String name, final String classPath) throws ClassNotFoundException {
Class<?> subClass = Class.forName(classPath);
if (!clazz.isAssignableFrom(subClass)) {
throw new IllegalStateException("load extension resources error," + subClass + " subtype is not of " + clazz);
}
Activate annotation = subClass.getAnnotation(Activate.class);
if (annotation == null) {
throw new IllegalStateException("load extension resources error," + subClass + " with Activate annotation");
}
Class<?> oldClass = classes.get(name);
if (oldClass == null) {
classes.put(name, subClass);
} else if (oldClass != subClass) {
throw new IllegalStateException("load extension resources error,Duplicate class " + clazz.getName() + " name " + name + " on " + oldClass.getName() + " or " + subClass.getName());
}
}
5、根据key,去缓存查找相应的类实例
public T getActivate(final String name) {
if (StringUtils.isBlank(name)) {
throw new NullPointerException("get Activate name is null");
}
Holder<Object> objectHolder = cachedInstances.get(name);
if (objectHolder == null) {
cachedInstances.putIfAbsent(name, new Holder<>());
objectHolder = cachedInstances.get(name);
}
Object value = objectHolder.getValue();
if (value == null) {
synchronized (cachedInstances) {
value = objectHolder.getValue();
if (value == null) {
value = createExtension(name);
objectHolder.setValue(value);
}
}
}
return (T) value;
}
核心代码
@Slf4j
@SuppressWarnings("all")
public final class ExtensionLoader<T> {
private static final String SERVICE_DIRECTORY = "META-INF/services/";
private static final Map<Class<?>, ExtensionLoader<?>> LOADERS = new ConcurrentHashMap<>();
private final Class<T> clazz;
private final Holder<Map<String, Class<?>>> cachedClasses = new Holder<>();
private final Map<String, Holder<Object>> cachedInstances = new ConcurrentHashMap<>();
private final Map<Class<?>, Object> ActivateInstances = new ConcurrentHashMap<>();
private String cachedDefaultName;
/**
* Instantiates a new Extension loader.
*
* @param clazz the clazz.
*/
private ExtensionLoader(final Class<T> clazz) {
this.clazz = clazz;
if (clazz != ExtensionFactory.class) {
ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getExtensionClasses();
}
}
/**
* Gets extension loader.
*
* @param <T> the type parameter
* @param clazz the clazz
* @return the extension loader.
*/
public static <T> ExtensionLoader<T> getExtensionLoader(final Class<T> clazz) {
if (clazz == null) {
throw new NullPointerException("extension clazz is null");
}
if (!clazz.isInterface()) {
throw new IllegalArgumentException("extension clazz (" + clazz + ") is not interface!");
}
if (!clazz.isAnnotationPresent(SPI.class)) {
throw new IllegalArgumentException("extension clazz (" + clazz + ") without @" + SPI.class + " Annotation");
}
ExtensionLoader<T> extensionLoader = (ExtensionLoader<T>) LOADERS.get(clazz);
if (extensionLoader != null) {
return extensionLoader;
}
LOADERS.putIfAbsent(clazz, new ExtensionLoader<>(clazz));
return (ExtensionLoader<T>) LOADERS.get(clazz);
}
/**
* Gets default Activate.
*
* @return the default Activate.
*/
public T getDefaultActivate() {
getExtensionClasses();
if (StringUtils.isBlank(cachedDefaultName)) {
return null;
}
return getActivate(cachedDefaultName);
}
/**
* Gets Activate.
*
* @param name the name
* @return the Activate.
*/
public T getActivate(final String name) {
if (StringUtils.isBlank(name)) {
throw new NullPointerException("get Activate name is null");
}
Holder<Object> objectHolder = cachedInstances.get(name);
if (objectHolder == null) {
cachedInstances.putIfAbsent(name, new Holder<>());
objectHolder = cachedInstances.get(name);
}
Object value = objectHolder.getValue();
if (value == null) {
synchronized (cachedInstances) {
value = objectHolder.getValue();
if (value == null) {
value = createExtension(name);
objectHolder.setValue(value);
}
}
}
return (T) value;
}
public Set<String> getSupportedExtensions() {
Map<String, Class<?>> clazzes = getExtensionClasses();
return Collections.unmodifiableSet(new TreeSet<>(clazzes.keySet()));
}
@SuppressWarnings("unchecked")
private T createExtension(final String name) {
Class<?> aClass = getExtensionClasses().get(name);
if (aClass == null) {
throw new IllegalArgumentException("name is error");
}
Object o = ActivateInstances.get(aClass);
if (o == null) {
try {
ActivateInstances.putIfAbsent(aClass, aClass.newInstance());
o = ActivateInstances.get(aClass);
} catch (InstantiationException | IllegalAccessException e) {
throw new IllegalStateException("Extension instance(name: " + name + ", class: "
+ aClass + ") could not be instantiated: " + e.getMessage(), e);
}
}
return (T) o;
}
/**
* Gets extension classes.
*
* @return the extension classes
*/
public Map<String, Class<?>> getExtensionClasses() {
Map<String, Class<?>> classes = cachedClasses.getValue();
if (classes == null) {
synchronized (cachedClasses) {
classes = cachedClasses.getValue();
if (classes == null) {
classes = loadExtensionClass();
cachedClasses.setValue(classes);
}
}
}
return classes;
}
private Map<String, Class<?>> loadExtensionClass() {
SPI annotation = clazz.getAnnotation(SPI.class);
if (annotation != null) {
String value = annotation.value();
if (StringUtils.isNotBlank(value)) {
cachedDefaultName = value;
}
}
Map<String, Class<?>> classes = new HashMap<>(16);
loadDirectory(classes);
return classes;
}
/**
* Load files under SERVICE_DIRECTORY.
*/
private void loadDirectory(final Map<String, Class<?>> classes) {
String fileName = SERVICE_DIRECTORY + clazz.getName();
try {
ClassLoader classLoader = ExtensionLoader.class.getClassLoader();
Enumeration<URL> urls = classLoader != null ? classLoader.getResources(fileName)
: ClassLoader.getSystemResources(fileName);
if (urls != null) {
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
loadResources(classes, url);
}
}
} catch (IOException t) {
log.error("load extension class error {}", fileName, t);
}
}
private void loadResources(final Map<String, Class<?>> classes, final URL url) throws IOException {
try (InputStream inputStream = url.openStream()) {
Properties properties = new Properties();
properties.load(inputStream);
properties.forEach((k, v) -> {
String name = (String) k;
String classPath = (String) v;
if (StringUtils.isNotBlank(name) && StringUtils.isNotBlank(classPath)) {
try {
loadClass(classes, name, classPath);
} catch (ClassNotFoundException e) {
throw new IllegalStateException("load extension resources error", e);
}
}
});
} catch (IOException e) {
throw new IllegalStateException("load extension resources error", e);
}
}
private void loadClass(final Map<String, Class<?>> classes,
final String name, final String classPath) throws ClassNotFoundException {
Class<?> subClass = Class.forName(classPath);
if (!clazz.isAssignableFrom(subClass)) {
throw new IllegalStateException("load extension resources error," + subClass + " subtype is not of " + clazz);
}
Activate annotation = subClass.getAnnotation(Activate.class);
if (annotation == null) {
throw new IllegalStateException("load extension resources error," + subClass + " with Activate annotation");
}
Class<?> oldClass = classes.get(name);
if (oldClass == null) {
classes.put(name, subClass);
} else if (oldClass != subClass) {
throw new IllegalStateException("load extension resources error,Duplicate class " + clazz.getName() + " name " + name + " on " + oldClass.getName() + " or " + subClass.getName());
}
}
/**
* The type Holder.
*
* @param <T> the type parameter.
*/
public static class Holder<T> {
private volatile T value;
/**
* Gets value.
*
* @return the value
*/
public T getValue() {
return value;
}
/**
* Sets value.
*
* @param value the value
*/
public void setValue(final T value) {
this.value = value;
}
}
}
使用示例
1、定义服务接口
@SPI("mysql")
public interface SqlDialect {
String dialect();
}
2、定义具体实现类
@Activate
public class MysqlDialect implements SqlDialect {
@Override
public String dialect() {
return "mysql";
}
}
@Activate
public class OracleDialect implements SqlDialect {
@Override
public String dialect() {
return "oracle";
}
}
3、src/main/resources/下建立/META-INF/services 目录,新增一个以接口命名的文件
4、接口命名的文件填入如下内容
5、加载服务类
SqlDialect sqlDialect = ExtensionLoader.getExtensionLoader(SqlDialect.class).getActivate("mysql");
6、测试
@Test
public void testSpi(){
SqlDialect sqlDialect = ExtensionLoader.getExtensionLoader(SqlDialect.class).getActivate("mysql");
Assert.assertEquals("mysql",sqlDialect.dialect());
}
总结
如果有用过dubbo的spi的朋友,就会发现上面实现的思路基本上就是dubbo的spi简化版。如果是有了解过shenyu网关的spi机制的朋友,就会发现上面的实现思路和shenyu网关基本上是一样了。
demo链接
关注公众号
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
-
上一篇
从决策者的角度理解 DevOps
在上两篇的文章中,我们分别从【员工】和【Leader】的角度去理解了 DevOps。 从员工的角度理解 DevOps 从 Leader 的角度理解 DevOps 如果是一个决策者,应该如何理解 DevOps? 回顾 角色 理解 员工 根据一个规则,拿着一系列工具,按照某个流程来完成工作。大部分的员工是不会去【感知】或者【质疑】流程或者工具。 Leader 规则的制定者,实施者,需要根据团队情况,指定,改变,评估规矩。Leader 需要根据 CAMS 以及团队实际情况,构建团队文化,自动化,评估体系,共享体系。 Agile,DevOps 只是一种方式 不要有这种误区: 不做 DevOps 没有竞争力,做了 DevOps 就一定有竞争力。 举个例子,如果是一个3~5人的初创团队,DevOps 对于他们,不会有直接的帮助,更应该把重点放在拓展业务,打磨产品。 之前的文章中,我们一直在讨论 Agile,DevOps 这些概念,决策者视角下,这些只是提升【企业竞争力】的一种方式而已。 任何企业都可以有符合自身情况的更好方式,提升【企业竞争力】。 DevOps 能给企业带来什么? 身为决策者,需要...
-
下一篇
工作3年的Java程序员,轻松拿到阿里P6Offer,只因为他搞明白了Redis这几个问题!!
Redis中的多路复用模型 Redis6用到了多线程?那多线程应用在哪些地方,引入多线程后,又改如何保证线程安全性呢? 同时,如何在性能和线程安全性方面做好平衡? 关于Redis的单线程模型 在Redis6.0之前,我们一直说Redis是单线程,所以并不会存在线程安全问题,而这个单线程,实际上就是在做数据IO处理中,是用的主线程来串行执行,如图4-7所示。 Redis基于Reactor模式设计开发了自己的一套高效事件处理模型,这个事件处理模型对应的就是Redis中的文件事件处理器,这个文件事件处理器是单线程运行的,这也是为什么我们一直强调Redis是线程安全的。 既然Redis是基于Reactor模型实现,那它必然用了I/O多路复用机制来监听多个客户端连接,然后把感兴趣的事件(READ/ACCEPT/CLOSE/WRITE)注册到多路复用器中。 文件事件处理器中使用I/O多路复用模型同时监听多个客户端连接,并且根据当前连接执行的任务类型关联不同的事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)来处理这些事件。 这样设计的好处: 文件事件处理器实现了高性能的网络IO通信模型 ...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- CentOS7设置SWAP分区,小内存服务器的救世主
- Crontab安装和使用
- CentOS7,8上快速安装Gitea,搭建Git服务器
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- CentOS8,CentOS7,CentOS6编译安装Redis5.0.7
- SpringBoot2更换Tomcat为Jetty,小型站点的福音
- Docker容器配置,解决镜像无法拉取问题
- CentOS7编译安装Cmake3.16.3,解决mysql等软件编译问题
- Red5直播服务器,属于Java语言的直播服务器
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果




微信收款码
支付宝收款码