[Java] Simple and elegant loading of classes in external jars | plug-in

After so many pigeons, I'm back

So today is mainly to talk about how to dynamically load a jarclass in

Students who want to see the Wiki directly can click here

need

First let's talk about why there is a need for this

When I was working on an IoT-related business platform, I required this platform to be able to access various devices

However, the access methods of devices of different types and different manufacturers and different protocols are completely different, and the fields are also different.

For example, there will be various devices such as lights, cameras, screens, radios, etc.

The light will have functions such as on/off, the camera will have functions such as preview playback, the screen will have functions such as playing videos, and the radio will have functions such as playing audio and adjusting the volume.

Some devices are connected TCPor MQTTdirectly connected, some devices are connected HTTPor connected to the SDKmanufacturer's platform, and some are connected to OneNetor through OceanConnectthese third-party IoT platforms.

Even if it is the same type of equipment, such as cameras, there will be Hikvision cameras and Dahua cameras, etc.

So at the beginning, each time a device is connected, it is equivalent to writing a ifbranch

I think this is definitely not a long-term solution, so consider optimizing this content

So I thought of using dynamic attributes to add plug-ins (it can solve some pain points, but there are gains and losses, which is a later story)

Dynamic properties are not expanded first, and plug-in is achieved through dynamically loaded jarclasses

ideas

So how to do this plug-in?

First, let's list the similarities and differences between these devices

Same point

  • all equipment
  • All require operations (control, query, etc.)

difference

  • different properties
  • Different docking methods (different operation methods)

Then we just need to abstract the similarities to resolve the differences.

Among them, the different attributes can be solved by dynamic attributes

And the operation mode is different, this problem can define an operation interface

public interface DeviceOperation {

    /**
     * 设备操作
     *
     * @param device  设备
     * @param opType  操作类型
     * @param opValue 操作值
     * @return 操作结果
     */
    OperationResult operate(Device device, String opType, Object opValue);
}
复制代码

Then when we need to connect the Hikvision camera, we can implement one HikvisionCameraOperationand make one jar(plug-in package), and then let our business service dynamically load this class and instantiate it, and then we can operate the Hikvision camera.

The benefits of doing this are:

  • 设备操作的代码不会和业务代码耦合,可以单独修复bug更新版本

这样实现的坏处是:

  • 由于插件的实现在不同的项目中,开发时调试起来会更加麻烦

示例

基于上述的思路,我们先在我们的插件项目中实现海康摄像头的具体操作类HikvisionCameraOperation,然后添加一个配置文件plugin.properties,设置一个属性device.type=HikvisionCamera即设备类型为海康摄像头,最后打包成hikvision-camera.jar

接着我们在业务服务中注入一个设备操作服务实例DeviceOperationService,添加一个设备类型和设备操作实现类的缓存Map<String, DeviceOperation>

当我们加载hikvision-camera.jar时,将提取到的device.typeHikvisionCameraOperation实例缓存起来

等到我们调用海康摄像头的操作功能时,先根据设备的设备类型从缓存中获得对应实现类HikvisionCameraOperation的实例,然后调用operate方法就能操作摄像头了

那么我们现在要怎么实现动态加载类呢

于是乎我自己实现了一个库

先上一个简单的写法

@Slf4j
@Service
public class DeviceOperationService {

    /**
     * 缓存设备类型和对应的操作对象
     */
    private final Map<String, DeviceOperation> operationMap = new ConcurrentHashMap<>();

    /**
     * 插件提取配置
     */
    private final JarPluginConcept concept = new JarPluginConcept.Builder()
            //回调到标注了@OnPluginExtract的方法
            .extractTo(this)
            .build();

    /**
     * 插件匹配回调
     *
     * @param operation  匹配到的 DeviceOperation 实例
     * @param deviceType 配置文件中定义的设备类型
     */
    @OnPluginExtract
    public void onPluginExtract(DeviceOperation operation, @PluginProperties("device.type") String deviceType) {
        operationMap.put(deviceType, operation);
    }

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

上面就是提取插件的写法

首先定义一个JarPluginConcept,主要是做一些配置,如过滤器(按包名,类名等等),或者是提取器(如提取类,实例,配置文件等等)

接着定义一个方法并标注@OnPluginExtract,参数就是你需要的内容(可以是类,实例,或者配置文件中的某个属性等等),通过extractTo进行绑定

最后调用JarPluginConcept#load传入jar的文件路径后,就会触发回调把设备类型和对应的实现类放入缓存

这样我们就能通过设备类型从缓存中获得对应的实现类,实现特定功能的调用

@Slf4j
@Service
public class DeviceOperationService {

    /**
     * 缓存设备类型和对应的操作对象
     */
    private final Map<String, DeviceOperation> operationMap = new ConcurrentHashMap<>();

    /**
     * 操作一个设备
     *
     * @param device  设备对象
     * @param opType  操作类型
     * @param opValue 操作值
     * @return 操作结果
     */
    public OperationResult operate(Device device, String opType, Object opValue) {
        //获得设备类型
        String deviceType = device.getDeviceType();
        //根据设备类型获得操作实现类
        DeviceOperation operation = operationMap.get(deviceType);
        if (operation == null) {
            throw new DeviceOperationNotFoundException(deviceType + " not found");
        }
        return operation.operate(device, opType, opValue);
    }
}
复制代码

设计

在说整个设计思路之前,先说说我提前想到的一些细节想法

因为基于上一个版本的库(之前实现过一个类似功能的库)我发现有很多地方不好用,就想着借着这个库都优化掉

  • 类型推导

之前实现的库都是直接指定一个Class参数,然后去匹配

后来发现又有读取配置文件的需求,就硬生生加了一个读取配置文件的if分支

所以在实现这个库的时候我就想着,能不能根据使用者定义的类型来推导

比如方法参数的类型是Class<DeviceOperation>Class<? extends DeviceOperation>就能推导出是DeviceOperation的类或子类,List<? extends DeviceOperation>就是DeviceOperation的实现类的实例列表,Properties就可能是.properties后缀的配置文件等等

然后再定义一个接口,支持其他类型的扩展,这样就算我的库里没对应的实现,使用者也可以通过自定义来解决一些不支持的类型的问题

  • 动态解析

之前实现的库直接会把所有的.class文件加载成类

但如果我现在只想得到所有的类名或者是里面的配置文件,那么类加载这个步骤就完全没必要了

所以我就在想能不能需要提取什么就解析什么,如果我们只要提取类,就解析类但不解析配置文件,如果只要配置文件,就解析配置文件但不解析类

于是我把jar的解析分成了很多步,提取文件路径名称,转化类名,加载类,实例化对象,提取.properties文件名,加载配置文件为Properties等等

然后不同的解析器会依赖其他的解析器作为前置解析器

比如我们的方法参数是Class<? extends DeviceOperation>,所以我们需要“加载类(解析器)”,而“加载类(解析器)”又需要依赖“转化类名(解析器)”,“转化类名(解析器)”又需要“提取文件路径名称(解析器)”等等,一层一层往上依赖

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

这样做的好处就是不会有一些额外的解析逻辑做无用功,使用者也不需要手动添加一堆不知道什么功能的解析器

  • 插件依赖其他的jar

之前实现的库没办法依赖其他的jar,如果必须依赖,那么就需要在业务服务中添加依赖才能正常使用

所以我想到只要把依赖的jar也当作插件加载进来,不就可以加载到对应的类了么

比如有些设备对接需要用到Netty,那么就可以把Netty的包作为一个基础插件,其他的插件都在Netty这个插件的基础上构建

框架

接下来就从总体框架讲讲这个库的设计思路

首先java中其实自带了spi的功能,也能够实现一定程度上的插件化

那么这两者有什么区别呢

spi的设计思想是基于类加载这个java的独有体系(狭义上讲),而这个库是以“插件”这个概念为基础,动态加载类只是针对java在插件化这个概念上的一种具体实现方式,你完全可以把一个Excel作为一个插件来解析,而“插件”这个概念也可以应用于其他的开发语言

抽象

插件

从“插件”这个概念来说,显而易见,我们需要一个Plugin接口,然后jar文件可以实现JarPluginExcel可以实现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<?>[],就需要根据指定的容器类型进行格式化

PluginFormatterAdapt to different container types by defining plugin formatters

Plugin events

Events are definitely essential. Loading, unloading, parsing, matching, conversion, formatting, etc., can be used for event publishing

It is very convenient to extend the logic of the event itself and the process

Plugins are automatically loaded

The basic content design is almost the same, but is it a little troublesome to manually call the method every time?

So I wondered if I could monitor a certain directory path, automatically load when a file is added, automatically reload when a file is modified, and automatically unload when a file is deleted

PluginAutoLoaderSupport autoloading plugins by definition

Finish

The main content is so much. The implementation of this library has a lot of in-depth knowledge of generics.

If you are interested, you can join us for more detailed instructions, and other libraries will be updated slowly in the future.


other articles

[Spring Boot] An annotation implements the download interface

[Bring it to you] JDK dynamic proxy

[Java] Asynchronous callback is converted to synchronous return

Guess you like

Origin juejin.im/post/7086261711568633886
Recommended