Java] 在外部 Jars 中简单而优雅地加载类|插件化-设计在外部 Jars 中简单而优雅地加载类|插件化设计
在说整个设计思路之前,先说说我提前想到的一些细节想法
因为基于上一个版本的库(之前实现过一个类似功能的库)我发现有很多地方不好用,就想着借着这个库都优化掉
- 类型推导
之前实现的库都是直接指定一个Class
参数,然后去匹配
后来发现又有读取配置文件的需求,就硬生生加了一个读取配置文件的if
分支
所以在实现这个库的时候我就想着,能不能根据使用者定义的类型来推导
比如方法参数的类型是Class<DeviceOperation>
或Class<? extends DeviceOperation>
就能推导出是DeviceOperation
的类或子类,List<? extends DeviceOperation>
就是DeviceOperation
的实现类的实例列表,Properties
就可能是.properties
后缀的配置文件等等
然后再定义一个接口,支持其他类型的扩展,这样就算我的库里没对应的实现,使用者也可以通过自定义来解决一些不支持的类型的问题
- 动态解析
之前实现的库直接会把所有的.class
文件加载成类
但如果我现在只想得到所有的类名或者是里面的配置文件,那么类加载这个步骤就完全没必要了
所以我就在想能不能需要提取什么就解析什么,如果我们只要提取类,就解析类但不解析配置文件,如果只要配置文件,就解析配置文件但不解析类
于是我把jar
的解析分成了很多步,提取文件路径名称,转化类名,加载类,实例化对象,提取.properties
文件名,加载配置文件为Properties
等等
然后不同的解析器会依赖其他的解析器作为前置解析器
比如我们的方法参数是Class<? extends DeviceOperation>
,所以我们需要“加载类(解析器)”,而“加载类(解析器)”又需要依赖“转化类名(解析器)”,“转化类名(解析器)”又需要“提取文件路径名称(解析器)”等等,一层一层往上依赖
可以近似理解为Gradle
或Maven
中的依赖传递
这样做的好处就是不会有一些额外的解析逻辑做无用功,使用者也不需要手动添加一堆不知道什么功能的解析器
- 插件依赖其他的
jar
之前实现的库没办法依赖其他的jar
,如果必须依赖,那么就需要在业务服务中添加依赖才能正常使用
所以我想到只要把依赖的jar
也当作插件加载进来,不就可以加载到对应的类了么
比如有些设备对接需要用到Netty
,那么就可以把Netty
的包作为一个基础插件,其他的插件都在Netty
这个插件的基础上构建
框架
接下来就从总体框架讲讲这个库的设计思路
首先java
中其实自带了spi
的功能,也能够实现一定程度上的插件化
那么这两者有什么区别呢
spi
的设计思想是基于类加载这个java
的独有体系(狭义上讲),而这个库是以“插件”这个概念为基础,动态加载类只是针对java
在插件化这个概念上的一种具体实现方式,你完全可以把一个Excel
作为一个插件来解析,而“插件”这个概念也可以应用于其他的开发语言
抽象
插件
从“插件”这个概念来说,显而易见,我们需要一个Plugin
接口,然后jar
文件可以实现JarPlugin
,Excel
可以实现ExcelPlugin
然后有一个管理类PluginConcept
来加载对应的插件
public interface PluginConcept {
/**
* 加载插件
*
* @param o 插件源
* @return 插件 {@link Plugin}
*/
Plugin load(Object o);
}
以加载外部jar
为例,我们可以传入文件路径,然后返回一个JarPlugin
插件工厂
我们可以传入一个文件路径,也可以传入一个File
对象,难道我们要一个一个枚举出来么?
显然不可能,我们可以定一个插件工厂,来匹配输入对象
/**
* 插件工厂
*/
public interface PluginFactory {
/**
* 是否支持插件创建
*
* @param o 插件源
* @param concept {@link PluginConcept}
* @return 如果支持返回 true,否则返回 false
*/
boolean support(Object o, PluginConcept concept);
/**
* 创建插件 {@link Plugin}
*
* @param o 插件源
* @param concept {@link PluginConcept}
* @return 插件 {@link Plugin}
*/
Plugin create(Object o, PluginConcept concept);
}
这样,我们可以为jar
文件路径实现一个JarPathPluginFactory
,为File
对象实现一个JarFilePluginFactory
,如果需要适配其他类型,就实现一个对应的工厂
插件上下文
之前说过我们把整个解析逻辑分成了很多步,那么每一步解析出来的内容肯定要找地方缓存起来,不可能每次重新解析一遍上一个步骤
通过定义上下文类PluginContext
来缓存整个解析流程中的所有内容
当然也提供了对应的工厂PluginContextFactory
,这样的话当使用者自定义解析器时如果需要引用其他对象也能十分方便的扩展
比如当需要用到Spring
容器中的Bean
时,就可以自定义上下文工厂,创建一个持有ApplicationContext
的上下文
插件过滤器
当我们想从jar
中提取类时,必然会先进行类加载
而符合条件的类可能就那么几个,完全没有必要把全部的类都加载一遍
通过定义插件过滤器PluginFilter
来过滤每一步解析的内容,这样就能减小解析的范围
比如当我们添加了一个包名过滤器,这样只有对应包下的类才会进行加载,适合类非常多但是只需要提取几个核心类的场景
插件匹配器
当我们解析完之后,就可以根据方法的参数类型从上下文中获取我们需要的内容了
通过定义插件匹配器PluginMatcher
来匹配上下文中的内容
比如,参数类型为Class<?>
,结合之前提到的类型推导,我们就可以从“加载类(解析器)”的解析结果中获得需要的类
插件转换器
接下来我们就要看从上下文中获得的内容是否需要转换,当然如果是类的话就不用转换了
但是比如像配置文件的内容,我们在上下文中获得的内容可能是Properties
对象,而方法参数类型为LinkedHashMap<String, String>
,这样的话直接赋值就会有问题
通过定义插件转换器PluginConvertor
来做转换,方便不同类型之间的转换
插件格式器
当我们搞定了元素类型之后,还需要判断容器类型是否匹配
比如我们从上下文中获得的类数据是Map<String, Class<?>>
(其中key
为文件路径和名称),而方法参数的类型定义的是List<Class<?>>
或者是Class<?>[]
,就需要根据指定的容器类型进行格式化
通过定义插件格式器PluginFormatter
来适配不同的容器类型
插件事件
事件肯定是必不可少的,加载,卸载,解析,匹配,转换,格式化等等,都可以进行事件发布
事件本身和流程上的逻辑扩展起来都是十分方便
插件自动加载
基本上的内容设计的差不多了,但是每次都要手动调用方法是不是有亿点点小麻烦
于是我就想到能不能监听某个目录路径,当文件新增时自动加载,文件修改时自动重新加载,文件删除时自动卸载
通过定义PluginAutoLoader
来支持自动加载插件