After a hundred million years, I’m back

How to load a jar class dynamically

If you want to see the Wiki directly, you can click here

demand

First of all, why is there a need

When I was working on a business platform related to the Internet of Things, I required the platform to be able to access various devices

However, the access modes and fields of devices of different types and manufacturers and protocols are different

There will be lights, cameras, screens, radios, all kinds of things

The lights will have switch functions, the camera will have preview playback functions, the screen will have video playback functions, and the radio will have audio playback and volume adjustment functions

Some devices connect directly via TCP or MQTT, some connect to vendor platforms via HTTP or SDK, and some connect via third-party iot platforms such as OneNet or OceanConnect

Even if the same type of equipment, such as camera, there will be hikang camera and DAhua camera and so on

So in the beginning, docking one device at a time is like writing an IF branch

I felt that this was definitely not a long-term solution, so I considered making an optimization of this content

So I came up with the idea of using dynamic properties plus plugins (can solve some pain points, but gain and loss, that’s another story).

Dynamic properties are left unexpanded, and pluginization is achieved by dynamically loading classes in jars

Train of thought

So how do you do this plug-in

Let’s start by enumerating the similarities and differences between these devices

The same

  • Is the equipment
  • Both require action (control, query, etc.)

The difference between

  • Properties of different
  • Different interconnection modes (different operation modes)

Then we just abstract the similarities to solve the differences

The different attributes can be solved by dynamic attributes

This problem can define an operation interface depending on how it operates

public interface DeviceOperation {

    /** ** Device operation **@param* device equipment@paramOpType Indicates the operation type *@paramOpValue Indicates the operation value *@returnResult */
    OperationResult operate(Device device, String opType, Object opValue);
}
Copy the code

So when we need to connect to the Hikcamera, we can implement a HikvisionCameraOperation and make a JAR (plug-in package), and then let our business service dynamically load this class and instantiate, we can realize the operation of the Hikcamera

The benefits of this implementation are:

  • The code for device operations is not coupled to the business code and can be fixed separatelybugAn updated version

The disadvantages of this implementation are:

  • Because plug-ins are implemented in different projects, debugging becomes more cumbersome at development time

The sample

Based on the above ideas, we first implemented HikvisionCameraOperation in our plug-in project, and then added a configuration file plugin.properties. Set device.type=HikvisionCamera to HikvisionCamera and package it as hikVision-camera.jar

Next we inject a DeviceOperation service instance DeviceOperationService into the business service and add a cache Map of the device type and DeviceOperation implementation class.

When we load hikVision-camera. jar, we cache the extracted device.type and HikvisionCameraOperation instances

When we call the operation function of the Hikon camera, we first obtain the instance of the corresponding implementation class HikvisionCameraOperation from the cache according to the type of the device, and then call the Operate method to operate the camera

So how do we implement dynamically loading classes now

So I implemented a library myself

So let’s do a simple way of writing it

@Slf4j
@Service
public class DeviceOperationService {

    /** * Cache device type and corresponding operation object */
    private final Map<String, DeviceOperation> operationMap = new ConcurrentHashMap<>();

    /** * Plugins extract configuration */
    private final JarPluginConcept concept = new JarPluginConcept.Builder()
            // Call back to the method labeled @onpluginextract
            .extractTo(this)
            .build();

    /** * Plugins match callback **@paramOperation Matched DeviceOperation instance *@paramDeviceType deviceType defined in the configuration file */
    @OnPluginExtract
    public void onPluginExtract(DeviceOperation operation, @PluginProperties("device.type") String deviceType) {
        operationMap.put(deviceType, operation);
    }

    /** * Load the JAR plug-in **@paramFilePath jar filePath */
    public void load(String filePath) {
        concept.load(filePath);
    }
Copy the code

This is how to extract a plug-in

Start by defining a JarPluginConcept, which does configuration such as filters (by package name, class name, etc.) or extractors (extract classes, instances, configuration files, etc.)

Then define a method annotated @onPluginExtract with the parameters you need (class, instance, a property in a configuration file, etc.) and bind it via extractTo

The final call to JarPluginConcept#load passes in the jar’s file path, which triggers a callback to place the device type and corresponding implementation class into the cache

In this way, we can get the corresponding implementation class from the cache by device type to implement the specific function invocation

@Slf4j
@Service
public class DeviceOperationService {

    /** * Cache device type and corresponding operation object */
    private final Map<String, DeviceOperation> operationMap = new ConcurrentHashMap<>();

    /** * Operates a device **@paramDevice Device object *@paramOpType Indicates the operation type *@paramOpValue Indicates the operation value *@returnResult */
    public OperationResult operate(Device device, String opType, Object opValue) {
        // Get the device type
        String deviceType = device.getDeviceType();
        // Get the operation implementation class based on the device type
        DeviceOperation operation = operationMap.get(deviceType);
        if (operation == null) {
            throw new DeviceOperationNotFoundException(deviceType + " not found");
        }
        returnoperation.operate(device, opType, opValue); }}Copy the code

design

Before we get to the whole idea, let’s talk about some of the details I came up with in advance

Because based on the previous version of the library (previously implemented a similar function of the library) I found a lot of places are not working, so I decided to use this library to optimize them

  • Type inference

Previously implemented libraries simply specified a Class parameter and then matched it

When it became clear that there was a need to read the configuration file again, an if branch was added to read the configuration file

So when I implemented this library, I was wondering if I could derive it from user-defined types

For example, method arguments of type Class

or Class
leads to a class or subclass of DeviceOperation. List
extends DeviceOperation> is a list of instances of DeviceOperation’s implementation classes. Properties might be a configuration file with the.properties suffix, etc

I then define an interface to support other types of extensions, so that even if I don’t have the corresponding implementation in my library, users can also customize to solve the problem of some unsupported types

  • The dynamic analysis

The libraries implemented before simply load all the.class files as classes

But if I just want to get all the class names or the configuration files in them, then the class loading step is completely unnecessary

So I was wondering can we just parse whatever we need to extract, if we just want to extract the classes and parse the classes but not the configuration files, if we just want to extract the configuration files and parse the configuration files but not the classes

So I divided the JAR parsing into many steps, extracting the file path name, converting the class name, loading the class, instantiating the object, extracting the.properties file name, loading the configuration file as Properties, and so on

Different parsers then rely on other parsers as pre-parsers

For example, our method argument is Class
, so we need to “load class (parser)”, which in turn depends on “Transform class Name (parser)”, which in turn depends on “Extract file path name (parser)” and so on, layer by layer

This can be approximated as dependency passing in Gradle or Maven

The advantage of this is that you don’t have to do any extra parsing, and the user doesn’t have to manually add a bunch of unknown parsers

  • Plug-ins depend on othersjar

Previously implemented libraries cannot rely on other jars, and if they must, they need to be added to the business service for them to work

So IT occurred to me that as long as the dependent JAR is also loaded as a plug-in, it can be loaded into the corresponding class

For example, if Netty is needed for device interconnection, the Netty package can be used as a basic plug-in, and other plug-ins are built on the basis of this plug-in

The framework

Next, I will talk about the design idea of this library from the general framework

First of all, SPI has its own function in Java, which can also achieve a certain degree of plug-in

So what’s the difference between the two

Spi is designed around the concept of class loading, which is unique to Java (in a narrow sense). The library is based on the concept of plug-ins. Dynamically loading classes is just a way of implementing the concept of plug-ins for Java. The concept of plug-ins can also be applied to other development languages

abstract

The plug-in

From the concept of “plug-in”, it is obvious that we need a Plugin interface, then jar files can implement JarPlugin, and Excel can implement ExcelPlugin

There is then a management class PluginConcept to load the corresponding plug-in

public interface PluginConcept {

    /** * Load the plug-in **@paramO Plug-in source *@returnPlug-in {@link Plugin}
     */
    Plugin load(Object o);
}
Copy the code

As an example of loading an external JAR, we can pass in the file path and return a JarPlugin

Plug-in factory

We can pass in a File path, or we can pass in a File object, should we enumerate them one by one?

Obviously not, we can specify a plug-in factory to match the input object

/** * Plugin factory */
public interface PluginFactory {

    /** * Whether plug-in creation is supported **@paramO Plug-in source *@param concept {@link PluginConcept}
     * @returnReturn true if supported, false */ otherwise
    boolean support(Object o, PluginConcept concept);

    /** * create plugin {@link Plugin}
     *
     * @paramO Plug-in source *@param concept {@link PluginConcept}
     * @returnPlug-in {@link Plugin}
     */
    Plugin create(Object o, PluginConcept concept);
}
Copy the code

In this way, we can implement a JarPathPluginFactory for the JAR File path, a JarFilePluginFactory for the File object, and a corresponding factory if other types need to be adapted

Plug-in context

As mentioned earlier, we split the whole parsing logic into many steps, so the content of each step has to be cached somewhere. It is not possible to re-parse the previous step each time

The PluginContext class is defined to cache everything in the entire parsing process

PluginContextFactory is also provided so that users can easily extend the parser if they need to reference other objects

For example, when you need a Bean in the Spring container, you can customize the context factory to create a context that holds the ApplicationContext

Plug-in filter

When we want to extract a class from a JAR, we must first load the class

There may be only a few classes that fit the criteria, and it’s not necessary to load all of them

You can reduce the scope of parsing by defining PluginFilter to filter the content parsed at each step

For example, when we add a package name filter, only the classes under the corresponding package will be loaded, which is suitable for scenarios where there are many classes but only a few core classes need to be extracted

Plug adapter

When we are done parsed, we can get what we need from the context based on the method parameter types

Match the content in the context by defining the PluginMatcher

For example, the argument type is Class

Plug-in converter

The next step is to see if the content obtained from the context needs to be converted, but not if it is a class

But for example, for the content of a configuration file, the content we get in the context might be a Properties object and the method parameter type is LinkedHashMap

, in which case direct assignment would be problematic
,>

PluginConvertor is defined to facilitate conversions between different types

Plug-in formatter

Once we’ve figured out the element types, we need to determine if the container types match

For example, the Class data we get from the context is Map

> (where key is the file path and name), and the type of the method parameter is defined as List

> or Class
[], need to be formatted according to the specified container type
>
,>

PluginFormatter is defined to accommodate different container types

Plug-in events

Events are absolutely essential, loading, unloading, parsing, matching, converting, formatting, and so on can all be done for event publishing

Both the event itself and the logic on the process can be easily extended

Plug-in auto loading

Basically, the content design is pretty much done, but it’s a little cumbersome to manually call methods every time

So I wondered if I could listen to a directory path that would load automatically when files were added, reload automatically when files were modified, and unload automatically when files were deleted

Automatic plug-in loading is supported by defining PluginAutoLoader

The end of the

That’s all there is to it, but the library is a lot deeper into generics

If you are interested, you can hold a forum, there are more detailed instructions, and other libraries will be updated gradually


Other articles

Spring Boot: an annotation implementation download interface

JDK dynamic proxy

[Java] An asynchronous callback to a synchronous return