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 separately
bug
An 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 others
jar
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