Skip to content

Concept Plugin

Linyuzai edited this page Apr 11, 2022 · 32 revisions

概述

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

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

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

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

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

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

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

示例说明

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();

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

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

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

当然如果存在多个符合条件的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.0'
<dependency>
  <groupId>com.github.linyuzai</groupId>
  <artifactId>concept-plugin-jar</artifactId>
  <version>1.0.0</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用于将各种对象适配成插件对象

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

插件上下文

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

插件上下文工厂

插件上下文工厂PluginContextFactory默认实现DefaultPluginContextFactory生成DefaultPluginContext

DefaultPluginContext默认使用LinkedHashMap缓存数据

插件提取器

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

提取器 说明 数据结构
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用于过滤插件,减少解析的内容

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

其中ModifierFilter用法

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

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

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

插件解析器

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

解析器 说明
JarEntryResolver 通过类过滤
JarPathNameResolver 通过全限定类名过滤
JarClassNameResolver 通过包名过滤
JarClassResolver 通过访问修饰符过滤
JarInstanceResolver 通过路径过滤
PropertiesNameResolver 通过名称过滤
JarPropertiesResolver 通过名称过滤
JarByteArrayResolver 通过类上标注的注解过滤

动态解析

插件匹配器

插件转换器

插件格式器

插件事件

插件类加载器

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

Clone this wiki locally