|
| 1 | +# Lowcoder plugin system (WIP) |
| 2 | + |
| 3 | +This is an ongoing effort to refactor current plugin system based on pf4j library. |
| 4 | + |
| 5 | +## Reasoning |
| 6 | + |
| 7 | +1. create a cleaner and simpler plugin system with clearly defined purpose(s) (new endpoints, new datasource types, etc..) |
| 8 | +2. lowcoder does not need live plugin loading/reloading/unloading/updates, therefore the main feature of pf4j is rendered useless, in fact it adds a lot of complexity due to classloaders used for managing plugins (especially in spring/boot applications) |
| 9 | +3. simpler and easier plugin detection - just a jar with a class implementing a common interface (be it a simple pojo project or a complex spring/boot implementation) |
| 10 | + |
| 11 | +## How it works |
| 12 | + |
| 13 | +The main entrypoint for plugin system is in **lowcoder-server** module with class **org.lowcoder.api.framework.configuration.PluginConfiguration** |
| 14 | +It creates: |
| 15 | +- LowcoderPluginManager bean which is responsible for plugin lifecycle management |
| 16 | +- Adds plugin defined endpoints to lowcoder by creating **pluginEndpoints** bean |
| 17 | +- TODO: Adds plugin defined datasources to lowcoder by creating **pluginDatasources** bean |
| 18 | + |
| 19 | +### lowcoder-plugin-api library |
| 20 | + |
| 21 | +This library contains APIs for plugin implementations. |
| 22 | +It is used by both, lowcoder API server as well as all plugins. |
| 23 | + |
| 24 | +### PluginLoader |
| 25 | + |
| 26 | +The sole purpose of a PluginLoader is to find plugin candidates and load them into VM. |
| 27 | +There is currently one implementation that based on paths - **PathBasedPluginLoader**, it: |
| 28 | +- looks in folders and subfolders defined in **application.yaml** - entries can point to a folder or specific jar file. If a relative path is supplied, the location of lowcoder API server application jar is used as parent folder (when run in non-packaged state, eg. in IDE, it uses the folder where ServerApplication.class is generated) |
| 29 | + |
| 30 | +```yaml |
| 31 | +common: |
| 32 | + plugin-dirs: |
| 33 | + - plugins |
| 34 | + - /some/custom/path/myGreatPlugin.jar |
| 35 | +``` |
| 36 | +- finds all **jar**(s) and inspects them for classes implementing **LowcoderPlugin** interface |
| 37 | +- instantiates all LowcoderPlugin implementations |
| 38 | +
|
| 39 | +### LowcoderPluginManager |
| 40 | +
|
| 41 | +The main job of plugin manager is to: |
| 42 | +- register plugins found and instantiated by **PluginLoader** |
| 43 | +- start registered plugins by calling **LowcoderPlugin.load()** method |
| 44 | +- create and register **RouterFunction**(s) for all loaded plugin endpoints |
| 45 | +- TODO: create and register datasources for all loaded plugin datasources |
| 46 | +
|
| 47 | +## Plugin project structure |
| 48 | +
|
| 49 | +Plugin jar can be structured in any way you like. It can be a plain java project, but also a spring/boot based project or based on any other framework. |
| 50 | +
|
| 51 | +It is composed from several parts: |
| 52 | +- class(es) implementing **LowcoderPlugin** interface |
| 53 | +- class(es) implementing **LowcoderEndpoint** interface, containing endpoint handler functions marked with **@EndpointExtension** annotation. These functions must obey following format: |
| 54 | +
|
| 55 | +```java |
| 56 | + @EndpointExtension(uri = <endpoint uri>, method = <HTTP method>) |
| 57 | + public Mono<ServerResponse> <handler name>(ServerRequest request) |
| 58 | + { |
| 59 | + ... your endpoint logic implementation |
| 60 | + } |
| 61 | + |
| 62 | + for example: |
| 63 | + |
| 64 | + @EndpointExtension(uri = "/hello-world", method = Method.GET) |
| 65 | + public Mono<ServerResponse> helloWorld(ServerRequest request) |
| 66 | + { |
| 67 | + return ServerResponse.ok().body(Mono.just(Hello.builder().message("Hello world!").build()), Hello.class); |
| 68 | + } |
| 69 | +``` |
| 70 | +- TODO: class(es) impelemting **LowcoderDatasource** interface |
| 71 | + |
| 72 | +### LowcoderPlugin implementations |
| 73 | + |
| 74 | +Methods of interest: |
| 75 | +- **pluginId()** - unique plugin ID - if a plugin with such ID is already loaded, subsequent plugins whith this ID will be ignored |
| 76 | +- **description()** - short plugin description |
| 77 | +- **load(ApplicationContext parentContext)** - is called during plugin startup - this is the place where you should completely initialize your plugin. If initialization fails, return false |
| 78 | +- **unload()** - is called during lowcoder API server shutdown - this is the place where you should release all resources |
| 79 | +- **endpoints()** - needs to contain all initialized **PluginEndpoints** you want to expose, for example: |
| 80 | + |
| 81 | +```java |
| 82 | + @Override |
| 83 | + public List<PluginEndpoint> endpoints() |
| 84 | + { |
| 85 | + List<PluginEndpoint> endpoints = new ArrayList<>(); |
| 86 | + |
| 87 | + endpoints.add(new HelloWorldEndpoint()); |
| 88 | + |
| 89 | + return endpoints; |
| 90 | + } |
| 91 | +``` |
| 92 | +- **pluginInfo()** - should return a record object with additional information about your plugin. It is serialized to JSON as part of the **/plugins** listing (see **"info"** object in this example): |
| 93 | + |
| 94 | +```json |
| 95 | +[ |
| 96 | + { |
| 97 | + "id": "example-plugin", |
| 98 | + "description": "Example plugin for lowcoder platform", |
| 99 | + "info": {} |
| 100 | + }, |
| 101 | + { |
| 102 | + "id": "enterprise", |
| 103 | + "description": "Lowcoder enterprise plugin", |
| 104 | + "info": { |
| 105 | + "enabledFeatures": [ |
| 106 | + "endpointApiUsage" |
| 107 | + ] |
| 108 | + } |
| 109 | + } |
| 110 | +] |
| 111 | +``` |
| 112 | + |
| 113 | +## TODOs |
| 114 | + |
| 115 | +1. Implement endpoint security - currently all plugin endpoints are public (probably by adding **security** attribute to **@EndpointExtension** and enforcing it) |
| 116 | + |
| 117 | + |
| 118 | +## QUESTIONS / CONSIDERATIONS |
| 119 | + |
| 120 | +1. currently the plugin endpoints are prefixed with **/plugin/{pluginId}/** - this is hardcoded, do we want to make it configurable? |
| 121 | + |
| 122 | + |
0 commit comments