[Java]外部jarへのクラスのシンプルでエレガントなロード|プラグイン

たくさんの鳩の後、私は戻ってきました

jarしたがって、今日は主にクラスを動的にロードする方法について話します

Wikiを直接見たい学生は、ここをクリックしてください。

必要

まず、なぜこれが必要なのかについて話しましょう

IoT関連のビジネスプラットフォームで作業していたとき、このプラットフォームがさまざまなデバイスにアクセスできるようにする必要がありました

ただし、さまざまなタイプ、さまざまなメーカー、さまざまなプロトコルのデバイスのアクセス方法は完全に異なり、フィールドも異なります。

たとえば、ライト、カメラ、スクリーン、ラジオなどのさまざまなデバイスがあります。

ライトにはオン/オフなどの機能があり、カメラにはプレビュー再生などの機能があり、画面にはビデオの再生などの機能があり、ラジオにはオーディオの再生や音量の調整などの機能があります。

一部のデバイスは接続されているTCPMQTT直接接続されており、一部のデバイスはメーカーのプラットフォームに接続HTTPまたは接続されており、一部はこれらのサードパーティのIoTプラットフォームに接続または接続されています。SDKOneNetOceanConnect

カメラなど同じ種類の機器でも、HikvisionカメラやDahuaカメラなどがあります。

したがって、最初は、デバイスが接続されるたびに、ifブランチを作成するのと同じです。

これは間違いなく長期的な解決策ではないと思うので、このコンテンツを最適化することを検討してください

そこで、動的属性を使用してプラグインを追加することを考えました(これにより、いくつかの問題点を解決できますが、後の話である利益と損失があります)

動的プロパティは最初に展開されず、プラグインは動的にロードされたjarクラスを介して実現されます

アイデア

では、このプラグインを実行するにはどうすればよいですか?

まず、これらのデバイス間の類似点と相違点をリストしましょう

同じ点

  • すべての機器
  • すべて操作が必要です(制御、クエリなど)

違い

  • さまざまなプロパティ
  • さまざまなドッキング方法(さまざまな操作方法)

次に、相違点を解決するために類似点を抽象化する必要があります。

その中で、さまざまな属性は動的属性によって解決できます

そして、操作モードが異なります、この問題は操作インターフェースを定義することができます

public interface DeviceOperation {

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

次に、Hikvisionカメラを接続する必要がある場合、1つを実装HikvisionCameraOperationして1つjar(プラグインパッケージ)を作成し、ビジネスサービスにこのクラスを動的にロードしてインスタンス化させ、Hikvisionカメラを操作できます。

これを行う利点は次のとおりです。

  • 设备操作的代码不会和业务代码耦合,可以单独修复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<?>[],就需要根据指定的容器类型进行格式化

プラグインフォーマッタを定義することによりPluginFormatter、さまざまなコンテナタイプに適応します

プラグインイベント

イベントは間違いなく不可欠です。ロード、アンロード、解析、マッチング、変換、フォーマットなどは、イベントの公開に使用できます。

イベント自体とプロセスのロジックを拡張すると非常に便利です

プラグインは自動的にロードされます

基本的なコンテンツのデザインはほぼ同じですが、毎回手動でメソッドを呼び出すのは少し面倒ですか?

そこで、特定のディレクトリパスを監視し、ファイルが追加されたときに自動的にロードし、ファイルが変更されたときに自動的にリロードし、ファイルが削除されたときに自動的にアンロードできるかどうか疑問に思いました

定義によりPluginAutoLoaderプラグインの自動読み込みをサポート

終了

主な内容は非常に多く、このライブラリの実装にはジェネリックに関する深い知識がたくさんあります。

興味のある方は、詳細な手順についてご参加ください。他のライブラリは、今後ゆっくりと更新されます。


他の記事

[SpringBoot]アノテーションはダウンロードインターフェースを実装します

[お持ちください]JDK動的プロキシ

[Java]非同期コールバックが同期リターンに変換されます

おすすめ

転載: juejin.im/post/7086261711568633886