Skip to content

Concept Plugin

Linyuzai edited this page Jul 15, 2024 · 32 revisions

概述

目前主要用于动态加载外部jar中的Class

当我们遇到一些插件化的需求时可能就会想到通过如动态加载类的方式来实现

java中自带有spi,不过功能有限,是以类加载作为基础概念

而本库是以插件作为基础概念,类加载作为一种插件的具体实现方式

插件可以是一个jar文件,一段java代码,一个Excel文件...

由于jar文件相对java来说可能更适合作为插件的载体

所以具体实现了jar文件作为插件

示例说明

假设有一个jar包,我们想要提取其中CustomPlugin.class这个类或者子类

首先创建一个JarPluginConcept并添加一个类提取器ClassExtractor,指定提取CustomPlugin.class或是其子类

public class ConceptPluginSample {

    /**
     * 插件提取配置
     */
    private final JarPluginConcept concept = new JarPluginConcept.Builder()
            //添加类提取器
            .addExtractor(new ClassExtractor<Class<? extends CustomPlugin>>() {

                @Override
                public void onExtract(Class<? extends CustomPlugin> plugin) {
                    //回调
                }
            })
            .build();
}

然后调用load方法传入文件地址就会回调jar中匹配到的类,如果没有匹配到则不会触发回调

//传入文件路径
concept.load(filePath);

当然如果存在多个符合条件的Class可以直接指定集合类型

public class ConceptPluginSample {

    /**
     * 插件提取配置
     */
    private final JarPluginConcept concept = new JarPluginConcept.Builder()
            //添加类提取器
            .addExtractor(new ClassExtractor<List<Class<? extends CustomPlugin>>>() {

                @Override
                public void onExtract(List<Class<? extends CustomPlugin>> plugin) {
                    //回调
                }
            })
            .build();
}

集成

implementation 'com.github.linyuzai:concept-plugin-jar:1.0.1'
<dependency>
  <groupId>com.github.linyuzai</groupId>
  <artifactId>concept-plugin-jar</artifactId>
  <version>1.0.1</version>
</dependency>

插件动态匹配

动态匹配可以支持任意类型与数量的插件匹配

public class ConceptPluginSample {

    /**
     * 插件提取配置
     */
    private final JarPluginConcept concept = new JarPluginConcept.Builder()
            //将插件提取到...
            .extractTo(this)
            .build();

    @OnPluginExtract
    public void onPluginExtract(Class<? extends CustomPlugin> pluginClass, Properties properties) {
        //任意一个参数匹配上都会触发回调
    }

    /**
     * 加载一个 jar 插件
     *
     * @param filePath jar 文件路径
     */
    public void load(String filePath) {
        concept.load(filePath);
    }
}

当我们既要获得某些指定的类又想要同时获得配置文件(假设包里定义了一个properties文件)

可以直接定义一个方法,设置参数为我们需要提取的类和配置文件,再在方法上标注@OnPluginExtract

然后使用extractTo将定义了上述方法的对象传入就行了

注解支持

动态匹配还提供了更精准化的注解配置

注解 说明
@PluginPath 路径匹配
@PluginName 名称匹配
@PluginProperties properties文件匹配
@PluginPackage 包名匹配
@PluginClassName 类名匹配
@PluginClass 类匹配
@PluginAnnotation 类上注解匹配

其中@PluginProperties可以单独指定key

  • @PluginProperties("concept-plugin.a")可以直接得到对应的String值(只能是String没有做类型转换)
  • @PluginProperties("concept-plugin.map.**")可以获得concept-plugin.map为前缀的Map<String, String>

当存在多个能匹配上的对象(类,配置文件等等)时,可以通过上述注解保证唯一或是使用集合类

由于匹配字符串使用的都是Spring中的AntPathMatcher,所有注解都支持通配符,如@PluginPackage("com.github.linyuzai.concept.**.plugin")

插件自动加载

支持通过监听本地文件目录变化来自动加载插件

默认使用WatchService来监听文件目录,提供了JarNotifier来自动触发加载卸载

@Slf4j
public class ConceptPluginSample {

    //自动加载器
    private final PluginAutoLoader loader = new WatchServicePluginAutoLoader.Builder()
            .locations(new PluginLocation.Builder()
                    //监听目录
                    .path("/Users/concept/plugin")
                    //所有jar
                    .filter(it -> it.endsWith(".jar"))
                    .build())
            //指定线程池
            .executor(Executors.newSingleThreadExecutor())
            //增删改时触发自动加载,自动重新加载,自动卸载
            .onNotify(new JarNotifier(concept))
            //异常回调
            .onError(e -> log.error("Plugin auto load error", e))
            .build();

    /**
     * 开始监听
     */
    @PostConstruct
    private void start() {
        loader.start();
    }

    /**
     * 结束监听
     */
    @PreDestroy
    private void stop() {
        loader.stop();
    }
}

插件加载流程

  • 通过插件工厂PluginFactory生成一个插件PluginJarPlugin支持解析文件路径,文件对象和URL对象)
  • 准备插件Plugin#prepare()(通过JarURLConnection建立和jar的连接)
  • 通过插件上下文工厂PluginContextFactory生成一个插件上下文PluginContext(上下文用于缓存解析过程中的中间数据)
  • 初始化上下文PluginContext#initialize()
  • 调用插件解析链解析插件PluginResolver#resolve()(解析jar内容)
    • 通过插件匹配器进行匹配PluginMatcher#match()(匹配内容,如匹配Classproperties
    • 通过插件转换器进行转换PluginConvertor#convert()(转换内容,如配置文件转成json格式的字符串)
    • 通过插件格式器格式化PluginFormatter#format()(格式化,如使用List接收时格式化成对应类型)
  • 提取插件(回调对应的PluginExtractor#extract()或是动态匹配的方法)
  • 销毁上下文PluginContext#destroy
  • 释放插件资源Plugn#release()(断开和jar的连接)

插件

作为插件的统一抽象Plugin

针对jar实现了JarPlugin

插件工厂

插件工厂PluginFactory用于将各种对象适配成插件对象

可以通过JarPluginConcept.Builder#addFactory添加自定义插件工厂

工厂 说明
JarPathPluginFactory 支持文件路径
JarFilePluginFactory 支持File对象
JarURLPluginFactory 支持jar协议的URL(jar:file:/xxx!/)

插件上下文

插件上下文PluginContext用于缓存插件加载期间的中间数据

插件上下文工厂

插件上下文工厂PluginContextFactory用来创建插件上下文

可以通过JarPluginConcept.Builder#contextFactory添加自定义上下文工厂

插件提取器

插件提取器PluginExtractor用于回调提取到的插件

通过JarPluginConcept.Builder#addExtractor添加

提取器 说明 数据结构
ClassExtractor 支持提取Class Map<String, Class<CustomPlugin>>
List<Class<CustomPlugin>>
Set<Class<CustomPlugin>>
Collection<Class<CustomPlugin>>
Class<CustomPlugin>[]
Class<CustomPlugin>
InstanceExtractor 支持提取实例,支持能够使用无参构造器实例化的类 Map<String, CustomPlugin>
List<CustomPlugin>
Set<CustomPlugin>
Collection<CustomPlugin>
CustomPlugin
CustomPlugin[]
PropertiesExtractor 支持提取后缀为.properties的文件 Map<String, Properties>
List<Properties>
Set<Properties>
Collection<Properties>
Properties[]
Properties
Map<String, Map<String, String>>
List<Map<String, String>>
Set<Map<String, String>>
Collection<Map<String, String>>
Map<String, String>[]
Map<String, String>
ContentExtractor 支持提取任意的文件内容(jar中会排除.class.properties Map<String, byte[]>
List<byte[]>
Set<byte[]>
Collection<byte[]>
byte[][]
byte[]
Map<String, InputStream>
List<InputStream>
Set<InputStream>
Collection<InputStream>
InputStream[]
InputStream
Map<String, String>
List<String>
Set<String>
Collection<String>
String[]
String
PluginObjectExtractor 可以获得类加载器,URL等数据 Plugin
JarPlugin
PluginContextExtractor 插件加载时的中间数据等 PluginContext
DynamicExtractor
JarDynamicExtractor
动态插件加载

当使用Map时,对应的key将返回文件的路径和名称,如com/github/linyuzai/concept/sample/plugin/CustomPluginImpl.class

支持泛型写法

  • List<Class<? extends CustomPlugin>>
  • Set<? extends CustomPlugin>
  • ...

插件过滤器

插件过滤器PluginFilter用于过滤插件,减少解析的内容

通过JarPluginConcept.Builder#addFilter添加

过滤器 说明
ClassFilter 通过类过滤
ClassNameFilter 通过全限定类名过滤
PackageFilter 通过包名过滤
AnnotationFilter 通过类上标注的注解过滤
ModifierFilter 通过访问修饰符过滤
PathFilter 通过路径过滤
NameFilter 通过名称过滤

其中ModifierFilter用法

//是接口或是抽象类
new ModifierFilter(Modifier::isInterface, Modifier::isAbstract);

可以通过PluginFilter#negate()进行取反

//不是接口并且不是抽象类
new ModifierFilter(Modifier::isInterface, Modifier::isAbstract).negate();

插件解析器

插件解析器PluginResolver用于解析插件内容

通过JarPluginConcept.Builder#addResolver添加

解析器 说明
JarEntryResolver 用于获得JarEntry集合
JarPathNameResolver 用于获得路径名称集合
JarClassNameResolver 将路径名称解析为全限定类名
JarClassResolver 根据全限定类名加载类
JarInstanceResolver 通过类实例化成对象
PropertiesNameResolver 获得.properties后缀的路径名称
JarPropertiesResolver .properties后缀的路径名称加载到Properties
JarByteArrayResolver 读取路径名称对应的内容加载到内存

动态解析

根据添加的插件提取器动态添加插件处理器

比如,当我们只添加了类提取器,那么properties相关的解析器就不会被添加,对应的解析逻辑也不会执行

可以近似的理解为GradleMaven的依赖传递

插件匹配器

插件匹配器PluginMatcher用于在插件解析出来的内容中根据插件提取器中定义的类型来匹配对应的内容

匹配器 说明
ClassMatcher 用于匹配类
InstanceMatcher 用于匹配实例
PropertiesMatcher 用于匹配.properties文件对象,如果PropertiesMap<String, String>
ContentMatcher 用于匹配文件内容对象,如byte[]InputStreamString
PluginObjectMatcher 用于匹配插件对象
PluginContextMatcher 用于匹配上下文

插件转换器

插件转换器PluginConvertor用于将插件匹配器匹配到的内容根据插件提取器中定义的类型来进行转换

如将我们获取的Properties对象转换为Map<String, String>

转换器 说明
ByteArrayToInputStreamMapConvertor 用于将Map<?, byte[]>转换为Map<Object, InputStream>
ByteArrayToStringMapConvertor 用于将Map<?, byte[]>转换为Map<Object, String>
PropertiesToMapMapConvertor 用于将Map<?, Properties>转换为Map<Object, Map<String, String>>

插件格式器

插件格式器PluginFormatter用于将插件转换器转换后的内容根据插件提取器中定义的类型来格式化

如将我们解析之后的Map<String, Class<?>>格式化为List<Class<?>>

格式器 说明
MapToMapFormatter 用于将Map<?, ?>转换为Map<Object, Object>
MapToListFormatter 用于将Map<?, ?>转换为List<Object>
MapToSetFormatter 用于将Map<?, ?>转换为Set<Object>
MapToArrayFormatter 用于将Map<?, ?>转换为E[],对应类型的数组
MapToObjectFormatter 用于将Map<?, ?>转换为Object,获取唯一一个元素

插件事件

在插件加载的过程中会发布一系列的事件PluginEvent

通过JarPluginConcept.Builder#addEventListener添加监听

事件 说明
PluginCreatedEvent 插件创建事件
PluginPreparedEvent 插件准备事件
PluginReleasedEvent 插件资源释放事件
PluginLoadedEvent 插件加载事件
PluginUnloadedEvent 插件卸载事件
PluginResolvedEvent 插件解析事件
PluginFilteredEvent 插件过滤事件
PluginMatchedEvent 插件匹配事件
PluginConvertedEvent 插件转换事件
PluginFormattedEvent 插件格式化事件
PluginExtractedEvent 插件提取事件
PluginAutoLoadEvent 插件自动加载(监听文件新增)
PluginAutoReloadEvent 插件自动重新加载事件(监听文件修改)
PluginAutoUnloadEvent 插件自动卸载事件(监听文件删除)

事件发布者

事件发布者PluginEventPublisher用于发布事件

可以通过JarPluginConcept.Builder#eventPublisher自定义

插件加载日志

基于事件实现的简单的日志输出类PluginLoadLogger

@Slf4j
public class ConceptPluginSample {

    private final JarPluginConcept concept = new JarPluginConcept.Builder()
            .extractTo(this)
            //插件加载日志
            .addEventListener(new PluginLoadLogger(log::info))
            .build();

    //省略其他代码。。。
}

插件类加载器

jar中的类通过JarPluginClassLoader加载

findClass方法无法加载到对应的类时,会遍历其他的插件类加载器尝试加载

插件类加载器工厂

插件类加载器工厂PluginClassLoaderFactory用于提供插件类加载器

可以通过JarPluginConcept.Builder#pluginClassLoaderFactory自定义

插件需要依赖其他jar时的注意事项

当我们的A.jar需要依赖B.jar时,将两个jar都进行一次加载即可

但是需要注意,要在A.jar中的插件触发B.jar中类的类加载之前加载B.jar

简单来说就是先加载B.jar再加载A.jar

或是等所有的jar都加载完成后,再调用插件中的方法

如果已经发生无法加载B.jar中的类的情况,可以重新加载一遍A.jar并替换之前实例化的插件即可重新触发类加载

版本

列表

1.0.1
  • AbstractPluginConcept添加extractTo方法
  • 修复Windows环境下JarPathNameResolver出现的PatternSyntaxException的问题