Skip to content

Commit 47d1dad

Browse files
committed
WIP: new plugin system first iteration
1 parent 0b54e90 commit 47d1dad

File tree

5 files changed

+244
-30
lines changed

5 files changed

+244
-30
lines changed

server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/configuration/PluginConfiguration.java

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,13 @@
2020
@Configuration
2121
public class PluginConfiguration
2222
{
23-
// private final ApplicationContext applicationContext;
24-
// private final PluginLoader pluginLoader;
25-
//
26-
// public LowcoderPluginManager lowcoderPluginManager()
27-
// {
28-
// return new LowcoderPluginManager(applicationContext, pluginLoader);
29-
// }
23+
private final ApplicationContext applicationContext;
24+
private final PluginLoader pluginLoader;
25+
26+
public LowcoderPluginManager lowcoderPluginManager()
27+
{
28+
return new LowcoderPluginManager(applicationContext, pluginLoader);
29+
}
3030

3131
@SuppressWarnings("unchecked")
3232
@Bean
Lines changed: 193 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,217 @@
11
package org.lowcoder.api.framework.plugin;
22

3+
import static org.springframework.web.reactive.function.server.RequestPredicates.DELETE;
4+
import static org.springframework.web.reactive.function.server.RequestPredicates.GET;
5+
import static org.springframework.web.reactive.function.server.RequestPredicates.OPTIONS;
6+
import static org.springframework.web.reactive.function.server.RequestPredicates.PATCH;
7+
import static org.springframework.web.reactive.function.server.RequestPredicates.POST;
8+
import static org.springframework.web.reactive.function.server.RequestPredicates.PUT;
9+
import static org.springframework.web.reactive.function.server.RouterFunctions.route;
10+
11+
import java.lang.reflect.InvocationTargetException;
12+
import java.lang.reflect.Method;
13+
import java.util.ArrayList;
14+
import java.util.Comparator;
15+
import java.util.LinkedHashMap;
16+
import java.util.List;
317
import java.util.Map;
418

19+
import org.apache.commons.collections4.CollectionUtils;
20+
import org.apache.commons.lang3.StringUtils;
21+
import org.lowcoder.plugin.EndpointExtension;
522
import org.lowcoder.plugin.LowcoderPlugin;
6-
import org.lowcoder.sdk.config.CommonConfig;
7-
import org.springframework.boot.system.ApplicationHome;
8-
import org.springframework.context.ConfigurableApplicationContext;
23+
import org.lowcoder.plugin.PluginEndpoint;
24+
import org.lowcoder.sdk.exception.BaseException;
25+
import org.springframework.context.ApplicationContext;
26+
import org.springframework.core.ResolvableType;
927
import org.springframework.stereotype.Component;
28+
import org.springframework.web.reactive.function.server.RequestPredicate;
29+
import org.springframework.web.reactive.function.server.RouterFunction;
30+
import org.springframework.web.reactive.function.server.ServerRequest;
31+
import org.springframework.web.reactive.function.server.ServerResponse;
1032

1133
import jakarta.annotation.PostConstruct;
34+
import jakarta.annotation.PreDestroy;
1235
import lombok.RequiredArgsConstructor;
1336
import lombok.extern.slf4j.Slf4j;
37+
import reactor.core.publisher.Mono;
1438

1539
@RequiredArgsConstructor
1640
@Component
1741
@Slf4j
1842
public class LowcoderPluginManager
1943
{
20-
private final ConfigurableApplicationContext applicationContext;
21-
private final CommonConfig common;
22-
private final ApplicationHome applicationHome;
23-
24-
private Map<String, LowcoderPlugin> plugins;
25-
44+
private final ApplicationContext applicationContext;
45+
private final PluginLoader pluginLoader;
2646

47+
private Map<String, LowcoderPlugin> plugins = new LinkedHashMap<>();
48+
private List<RouterFunction<ServerResponse>> routes = new ArrayList<>();
49+
2750
@PostConstruct
2851
private void loadPlugins()
2952
{
53+
registerPlugins();
54+
List<LowcoderPlugin> sorted = new ArrayList<>(plugins.values());
55+
sorted.sort(Comparator.comparing(LowcoderPlugin::loadOrder));
56+
57+
for (LowcoderPlugin plugin : sorted)
58+
{
59+
if (plugin.load(applicationContext))
60+
{
61+
log.info("Plugin [{}] loaded successfully.", plugin.pluginId());
62+
registerEndpoints(plugin);
63+
}
64+
}
65+
}
66+
67+
@PreDestroy
68+
public void unloadPlugins()
69+
{
70+
for (LowcoderPlugin plugin : plugins.values())
71+
{
72+
try
73+
{
74+
plugin.unload();
75+
}
76+
catch(Throwable cause)
77+
{
78+
log.warn("Error unloading plugin: {}!", plugin.pluginId(), cause);
79+
}
80+
}
81+
}
82+
83+
public List<RouterFunction<ServerResponse>> getEndpoints()
84+
{
85+
return this.routes;
86+
}
87+
88+
public List<PluginInfo> getLoadedPluginsInfo()
89+
{
90+
List<PluginInfo> infos = new ArrayList<>();
91+
for (LowcoderPlugin plugin : plugins.values())
92+
{
93+
infos.add(new PluginInfo(plugin.pluginId(), plugin.description(), plugin.pluginInfo()));
94+
}
95+
return infos;
96+
}
97+
98+
private void registerPlugins()
99+
{
100+
List<LowcoderPlugin> loaded = pluginLoader.loadPlugins();
101+
if (CollectionUtils.isNotEmpty(loaded))
102+
{
103+
for (LowcoderPlugin plugin : loaded)
104+
{
105+
if (!plugins.containsKey(plugin.pluginId()))
106+
{
107+
log.info("Registered plugin: {} ({})", plugin.pluginId(), plugin.getClass().getName());
108+
plugins.put(plugin.pluginId(), plugin);
109+
}
110+
else
111+
{
112+
log.warn("Plugin {} already registered (from: {}), skipping {}.", plugin.pluginId(),
113+
plugins.get(plugin.pluginId()).getClass().getName(),
114+
plugin.getClass().getName());
115+
}
116+
}
117+
}
118+
}
119+
120+
121+
private void registerEndpoints(LowcoderPlugin plugin)
122+
{
123+
if (CollectionUtils.isNotEmpty(plugin.endpoints()))
124+
{
125+
for (PluginEndpoint endpoint : plugin.endpoints())
126+
{
127+
Method[] handlers = endpoint.getClass().getDeclaredMethods();
128+
if (handlers != null && handlers.length > 0)
129+
{
130+
for (Method handler : handlers)
131+
{
132+
registerEndpointHandler(plugin, endpoint, handler);
133+
}
134+
}
135+
}
136+
}
137+
}
138+
139+
@SuppressWarnings("unchecked")
140+
private void registerEndpointHandler(LowcoderPlugin plugin, PluginEndpoint endpoint, Method handler)
141+
{
142+
if (handler.isAnnotationPresent(EndpointExtension.class))
143+
{
144+
if (checkHandlerMethod(handler))
145+
{
146+
147+
EndpointExtension endpointMeta = handler.getAnnotation(EndpointExtension.class);
148+
routes.add(route(createRequestPredicate(plugin, endpointMeta), req -> {
149+
Mono<ServerResponse> result = null;
150+
try
151+
{
152+
result = (Mono<ServerResponse>)handler.invoke(endpoint, req);
153+
}
154+
catch (IllegalAccessException | InvocationTargetException cause)
155+
{
156+
throw new BaseException("Error running handler for [ " + endpointMeta.method() + ": " + endpointMeta.uri() + "] !");
157+
}
158+
return result;
159+
})
160+
);
161+
log.info("Registered plugin endpoint: {} -> {} -> {}: {}", plugin.pluginId(), endpoint.getClass().getSimpleName(), endpointMeta.method(), endpointMeta.uri());
162+
}
163+
else
164+
{
165+
log.error("Cannot register plugin endpoint: {} -> {} -> {}! Handler method must be defined as: public Mono<ServerResponse> {}(ServerRequest request)", plugin.pluginId(), endpoint.getClass().getSimpleName(), handler.getName(), handler.getName());
166+
}
167+
}
168+
}
169+
170+
171+
private boolean checkHandlerMethod(Method method)
172+
{
173+
ResolvableType returnType = ResolvableType.forMethodReturnType(method);
30174

175+
return (returnType.isAssignableFrom(Mono.class)
176+
&& returnType.getGenerics().length == 1
177+
&& returnType.getGeneric(0).isAssignableFrom(ServerResponse.class)
178+
&& method.getParameterCount() == 1
179+
&& method.getParameterTypes()[0].isAssignableFrom(ServerRequest.class)
180+
);
31181
}
32182

183+
private RequestPredicate createRequestPredicate(LowcoderPlugin plugin, EndpointExtension endpoint)
184+
{
185+
String basePath = "/plugins/" + plugin.pluginId();
186+
187+
switch(endpoint.method())
188+
{
189+
case GET:
190+
return GET(pluginEndpointUri(basePath, endpoint.uri()));
191+
case POST:
192+
return POST(pluginEndpointUri(basePath, endpoint.uri()));
193+
case PUT:
194+
return PUT(pluginEndpointUri(basePath, endpoint.uri()));
195+
case PATCH:
196+
return PATCH(pluginEndpointUri(basePath, endpoint.uri()));
197+
case DELETE:
198+
return DELETE(pluginEndpointUri(basePath, endpoint.uri()));
199+
case OPTIONS:
200+
return OPTIONS(pluginEndpointUri(basePath, endpoint.uri()));
201+
}
202+
return null;
203+
}
204+
205+
private String pluginEndpointUri(String basePath, String uri)
206+
{
207+
return StringUtils.join(basePath, StringUtils.prependIfMissing(uri, "/"));
208+
}
209+
210+
211+
private record PluginInfo(
212+
String id,
213+
String description,
214+
Object info
215+
) {}
216+
33217
}

server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/PathBasedPluginLoader.java

Lines changed: 42 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,24 @@
11
package org.lowcoder.api.framework.plugin;
22

33
import java.io.IOException;
4+
import java.net.URL;
5+
import java.net.URLClassLoader;
46
import java.nio.file.Files;
57
import java.nio.file.Path;
68
import java.util.ArrayList;
7-
import java.util.Iterator;
89
import java.util.List;
9-
import java.util.ServiceLoader;
10+
import java.util.Set;
1011

1112
import org.apache.commons.collections4.CollectionUtils;
1213
import org.apache.commons.lang3.StringUtils;
13-
import org.lowcoder.plugin.api.LowcoderPlugin;
14+
import org.lowcoder.plugin.LowcoderPlugin;
1415
import org.lowcoder.sdk.config.CommonConfig;
16+
import org.reflections.Reflections;
17+
import org.reflections.scanners.SubTypesScanner;
18+
import org.reflections.scanners.TypeAnnotationsScanner;
19+
import org.reflections.util.ClasspathHelper;
20+
import org.reflections.util.ConfigurationBuilder;
21+
import org.springframework.beans.factory.BeanClassLoaderAware;
1522
import org.springframework.boot.system.ApplicationHome;
1623
import org.springframework.stereotype.Component;
1724

@@ -21,10 +28,12 @@
2128
@Slf4j
2229
@RequiredArgsConstructor
2330
@Component
24-
public class PathBasedPluginLoader implements PluginLoader
31+
public class PathBasedPluginLoader implements PluginLoader, BeanClassLoaderAware
2532
{
2633
private final CommonConfig common;
2734
private final ApplicationHome applicationHome;
35+
36+
private ClassLoader beanClassLoader;
2837

2938
@Override
3039
public List<LowcoderPlugin> loadPlugins()
@@ -49,6 +58,7 @@ public List<LowcoderPlugin> loadPlugins()
4958
{
5059
for (LowcoderPlugin plugin : loadedPlugins)
5160
{
61+
log.debug(" - loaded plugin: {} :: {}", plugin.pluginId(), plugin.description());
5262
plugins.add(plugin);
5363
}
5464
}
@@ -94,26 +104,38 @@ protected List<String> findPluginCandidates(Path pluginsDir)
94104
return pluginCandidates;
95105
}
96106

97-
protected List<LowcoderPlugin> loadPluginCandidates(String pluginJar)
107+
protected List<LowcoderPlugin> loadPluginCandidates(String pluginsDir)
98108
{
99109
List<LowcoderPlugin> pluginCandidates = new ArrayList<>();
100110

101-
PluginJarClassLoader pluginClassLoader = null;
111+
URLClassLoader testClassLoader = null;
112+
102113
try
103114
{
104-
pluginClassLoader = new PluginJarClassLoader(getClass().getClassLoader(), Path.of(pluginJar));
115+
testClassLoader = URLClassLoader.newInstance(new URL[] {
116+
Path.of(pluginsDir).toUri().toURL()
117+
}, beanClassLoader);
118+
119+
Reflections reflections = new Reflections(new ConfigurationBuilder()
120+
.addClassLoader(testClassLoader)
121+
.addUrls(ClasspathHelper.forClassLoader(testClassLoader))
122+
.setScanners(new SubTypesScanner(false), new TypeAnnotationsScanner())
123+
);
105124

106-
107-
ServiceLoader<LowcoderPlugin> pluginServices = ServiceLoader.load(LowcoderPlugin.class, pluginClassLoader);
108-
if (pluginServices != null )
125+
Set<Class<? extends LowcoderPlugin>> found = reflections.getSubTypesOf(LowcoderPlugin.class);
126+
for (Class<? extends LowcoderPlugin> pluginClass : found)
109127
{
110-
Iterator<LowcoderPlugin> pluginIterator = pluginServices.iterator();
111-
while(pluginIterator.hasNext())
128+
log.debug(" - found plugin: {}", pluginClass.getName());
129+
try
112130
{
113-
LowcoderPlugin plugin = pluginIterator.next();
131+
LowcoderPlugin plugin = pluginClass.getConstructor().newInstance();
114132
log.debug(" - loaded plugin: {} - {}", plugin.pluginId(), plugin.description());
115133
pluginCandidates.add(plugin);
116134
}
135+
catch(Throwable loadFail)
136+
{
137+
log.error(" - error loading plugin: {}!", pluginClass.getName(), loadFail);
138+
}
117139
}
118140
}
119141
catch(Throwable cause)
@@ -138,4 +160,11 @@ private Path getAbsoluteNormalizedPath(String path)
138160

139161
return null;
140162
}
163+
164+
165+
@Override
166+
public void setBeanClassLoader(ClassLoader classLoader)
167+
{
168+
this.beanClassLoader = classLoader;
169+
}
141170
}

server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/PluginLoader.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import java.util.List;
44

5-
import org.lowcoder.plugin.api.LowcoderPlugin;
5+
import org.lowcoder.plugin.LowcoderPlugin;
66

77
public interface PluginLoader
88
{

server/api-service/lowcoder-server/src/main/resources/selfhost/ce/application.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ common:
5252
max-query-timeout: ${LOWCODER_MAX_QUERY_TIMEOUT:120}
5353
plugin-dirs:
5454
- plugins
55+
- /tmp/plugins
5556

5657
material:
5758
mongodb-grid-fs:

0 commit comments

Comments
 (0)