Dynamic Configuration of Java Plugins

Peter Harrison - Aug 16 '21 - - Dev Community

When writing Java applications you should write them in a way where you can extend the capability of the application without modification of the core application. One mechanism to achieve this is plugins.

Inside the application the user might need to configure the plugin, and therefore will need a configuration UI to do so. Rather than developing a separate UI component for each new plugin there is a simple way to support new plugins with dynamically created configuration UI.

The interface for the plugins can be simple, including a object that contains the configuration.

public interface Plugin {
    public Map<String,Object> process( Action action, Map<String,Object> context ) throws Exception;
}

public class Action {
    private Map<String,Object> config;
    public void setConfig(Map<String,Object> config) {
        this.config = config;
    }
    public Object getConfigField( String field ) {
        return this.config.get(field);
    }
    ...
}
Enter fullscreen mode Exit fullscreen mode

Within implementations of a plugin the user can simple call action.getConfigField() to get configuration details.

Why are we not setting fields on the Plugin? The process method can be called with different actions, each with its own configuration, and so we don't want to configure the singleton itself. The action containing the configuration is generic and not coupled with the plugin implementations. The plugin instances are singletons and will have runtime fields which don't change between calls, while the individual calls to the process method potentially have unique action configurations.

In order to expose the configuration schema to the outside world we can introduce annotations on the plugins so that we can expose the details required in a REST API. To achieve this you can create custom runtime annotations to annotate each plugin.

@Retention(RetentionPolicy.RUNTIME)
public @interface ConfigurationDetail {
    public String name();
    public String description();
    public String documentUrl();
}

@Retention(RetentionPolicy.RUNTIME)
@Repeatable(ConfigurationFields.class)
public @interface ConfigurationField {
    public String field();
    public String name();
    public String description() default "";
    public boolean required() default false;
}

@Retention(RetentionPolicy.RUNTIME)
@Repeatable(ConfigurationFields.class)
public @interface ConfigurationField {
    public String field();
    public String name();
    public String description() default "";
    public boolean required() default false;
    public String board() default "";
}
Enter fullscreen mode Exit fullscreen mode

You can now use these annotations on each plugin you write.

@Component("freemarker")
@ConfigurationDetail( name="Freemarker Templating", 
description = "Creates a resulting document from a Template using FreeMarker")
@ConfigurationField(field="resource",name="Resource of the template", type=ConfigurationFieldType.RESOURCE)
@ConfigurationField(field="response",name="Field to place resulting completed document", type=ConfigurationFieldType.RESOURCE)
public class FreemarkerPlugin implements Plugin {
...
Enter fullscreen mode Exit fullscreen mode

From there it is a matter of writing a static method to collect all the annotations at runtime, which can then be exposed inside a REST API.

for( Entry<String,?> pluginEntry : plugins.entrySet()) {
    PluginMetadata pluginMetadata = new PluginMetadata();
    pluginMetadata.setId(pluginEntry.getKey());     
    Class<?> clazz = pluginEntry.getValue().getClass();
    if(clazz.isAnnotationPresent(ConfigurationField.class)) {
        ConfigurationField annotation = 
        clazz.getAnnotation(ConfigurationField.class);
        List<PluginField> configFieldList = new ArrayList<PluginField>();
        configFieldList.add(configurationFieldToMap(annotation));
    pluginMetadata.setFields(configFieldList);
...
Enter fullscreen mode Exit fullscreen mode

The REST API exposes the details from the annotations which can then be used to generate a dynamic form to enter the configuration required for the plugins.

This is an example of pushing business logic of an application into configuration. Rather than having applications coded against the domain the users can plumb together different plugins. And the plugins themselves can be written in a way decoupled from the core code.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .