diff --git a/application/src/main/java/run/halo/app/plugin/ExtensionContextRegistry.java b/application/src/main/java/run/halo/app/plugin/ExtensionContextRegistry.java index 18a17d5d0..419e45d99 100644 --- a/application/src/main/java/run/halo/app/plugin/ExtensionContextRegistry.java +++ b/application/src/main/java/run/halo/app/plugin/ExtensionContextRegistry.java @@ -1,9 +1,11 @@ package run.halo.app.plugin; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import org.springframework.lang.NonNull; /** *

Plugin application context registrar.

@@ -18,7 +20,8 @@ import java.util.concurrent.ConcurrentHashMap; public class ExtensionContextRegistry { private static final ExtensionContextRegistry INSTANCE = new ExtensionContextRegistry(); - private final Map registry = new ConcurrentHashMap<>(); + private final Map registry = new HashMap<>(); + private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock(); public static ExtensionContextRegistry getInstance() { return INSTANCE; @@ -27,35 +30,101 @@ public class ExtensionContextRegistry { private ExtensionContextRegistry() { } - public void register(String pluginId, PluginApplicationContext context) { - registry.put(pluginId, context); + /** + * Acquire the read lock when using getPluginApplicationContexts and getByPluginId. + */ + public void acquireReadLock() { + this.readWriteLock.readLock().lock(); } - public PluginApplicationContext remove(String pluginId) { - return registry.remove(pluginId); + /** + * Release the read lock after using getPluginApplicationContexts and getByPluginId. + */ + public void releaseReadLock() { + this.readWriteLock.readLock().unlock(); + } + + /** + * Register plugin application context to registry map. + * + * @param pluginId plugin id(name) + * @param context plugin application context + */ + public void register(String pluginId, PluginApplicationContext context) { + this.readWriteLock.writeLock().lock(); + try { + registry.put(pluginId, context); + } finally { + this.readWriteLock.writeLock().unlock(); + } + } + + /** + * Remove plugin application context from registry map. + * + * @param pluginId plugin id + */ + public void remove(String pluginId) { + this.readWriteLock.writeLock().lock(); + try { + PluginApplicationContext removed = registry.remove(pluginId); + if (removed != null) { + removed.close(); + } + } finally { + this.readWriteLock.writeLock().unlock(); + } } /** * Gets plugin application context by plugin id from registry map. + * Note: ensure call {@link #containsContext(String)} after call this method. * * @param pluginId plugin id * @return plugin application context * @throws IllegalArgumentException if plugin id not found in registry */ + @NonNull public PluginApplicationContext getByPluginId(String pluginId) { - PluginApplicationContext context = registry.get(pluginId); - if (context == null) { - throw new IllegalArgumentException( - String.format("The plugin [%s] can not be found.", pluginId)); + this.readWriteLock.readLock().lock(); + try { + PluginApplicationContext context = registry.get(pluginId); + if (context == null) { + throw new IllegalArgumentException( + String.format("The plugin [%s] can not be found.", pluginId)); + } + return context; + } finally { + this.readWriteLock.readLock().unlock(); } - return context; } + /** + * Check whether the registry contains the plugin application context by plugin id. + * + * @param pluginId plugin id + * @return true if contains, otherwise false + */ public boolean containsContext(String pluginId) { - return registry.containsKey(pluginId); + this.readWriteLock.readLock().lock(); + try { + return registry.containsKey(pluginId); + } finally { + this.readWriteLock.readLock().unlock(); + } } + /** + * Gets all plugin application contexts from registry map. + * + * @return plugin application contexts + */ public List getPluginApplicationContexts() { - return new ArrayList<>(registry.values()); + this.readWriteLock.readLock().lock(); + try { + return new ArrayList<>(registry.values()); + } finally { + this.readWriteLock.readLock().unlock(); + } } } diff --git a/application/src/main/java/run/halo/app/plugin/PluginApplicationEventBridgeDispatcher.java b/application/src/main/java/run/halo/app/plugin/PluginApplicationEventBridgeDispatcher.java index 8de3e96eb..75f22908a 100644 --- a/application/src/main/java/run/halo/app/plugin/PluginApplicationEventBridgeDispatcher.java +++ b/application/src/main/java/run/halo/app/plugin/PluginApplicationEventBridgeDispatcher.java @@ -27,12 +27,17 @@ public class PluginApplicationEventBridgeDispatcher if (!isSharedEventAnnotationPresent(event.getClass())) { return; } - List pluginApplicationContexts = - ExtensionContextRegistry.getInstance().getPluginApplicationContexts(); - for (PluginApplicationContext pluginApplicationContext : pluginApplicationContexts) { - log.debug("Bridging broadcast event [{}] to plugin [{}]", event, - pluginApplicationContext.getPluginId()); - pluginApplicationContext.publishEvent(event); + ExtensionContextRegistry.getInstance().acquireReadLock(); + try { + List pluginApplicationContexts = + ExtensionContextRegistry.getInstance().getPluginApplicationContexts(); + for (PluginApplicationContext pluginApplicationContext : pluginApplicationContexts) { + log.debug("Bridging broadcast event [{}] to plugin [{}]", event, + pluginApplicationContext.getPluginId()); + pluginApplicationContext.publishEvent(event); + } + } finally { + ExtensionContextRegistry.getInstance().releaseReadLock(); } } diff --git a/application/src/main/java/run/halo/app/plugin/PluginApplicationInitializer.java b/application/src/main/java/run/halo/app/plugin/PluginApplicationInitializer.java index c17ae31f3..d437b6198 100644 --- a/application/src/main/java/run/halo/app/plugin/PluginApplicationInitializer.java +++ b/application/src/main/java/run/halo/app/plugin/PluginApplicationInitializer.java @@ -153,10 +153,7 @@ public class PluginApplicationInitializer { public void contextDestroyed(String pluginId) { Assert.notNull(pluginId, "pluginId must not be null"); - PluginApplicationContext removed = contextRegistry.remove(pluginId); - if (removed != null) { - removed.close(); - } + contextRegistry.remove(pluginId); } private Set> findCandidateComponents(String pluginId) { diff --git a/application/src/main/java/run/halo/app/plugin/PluginCompositeRouterFunction.java b/application/src/main/java/run/halo/app/plugin/PluginCompositeRouterFunction.java index c5a609e6d..dedab890f 100644 --- a/application/src/main/java/run/halo/app/plugin/PluginCompositeRouterFunction.java +++ b/application/src/main/java/run/halo/app/plugin/PluginCompositeRouterFunction.java @@ -4,6 +4,7 @@ import static run.halo.app.plugin.ExtensionContextRegistry.getInstance; import java.util.ArrayList; import java.util.List; +import org.springframework.context.support.AbstractApplicationContext; import org.springframework.lang.NonNull; import org.springframework.stereotype.Component; import org.springframework.web.reactive.function.server.HandlerFunction; @@ -48,27 +49,35 @@ public class PluginCompositeRouterFunction implements RouterFunction> routerFunctions() { - var rawRouterFunctions = getInstance().getPluginApplicationContexts() - .stream() - .flatMap(applicationContext -> applicationContext - .getBeanProvider(RouterFunction.class) - .orderedStream()) - .map(router -> (RouterFunction) router) - .toList(); - var reverseProxies = reverseProxyRouterFunctionFactory.getRouterFunctions(); + getInstance().acquireReadLock(); + try { + List contexts = getInstance().getPluginApplicationContexts() + .stream() + .filter(AbstractApplicationContext::isActive) + .toList(); + var rawRouterFunctions = contexts + .stream() + .flatMap(applicationContext -> applicationContext + .getBeanProvider(RouterFunction.class) + .orderedStream()) + .map(router -> (RouterFunction) router) + .toList(); + var reverseProxies = reverseProxyRouterFunctionFactory.getRouterFunctions(); - var endpointBuilder = new CustomEndpointsBuilder(); - getInstance().getPluginApplicationContexts() - .forEach(context -> context.getBeanProvider(CustomEndpoint.class) + var endpointBuilder = new CustomEndpointsBuilder(); + contexts.forEach(context -> context.getBeanProvider(CustomEndpoint.class) .orderedStream() .forEach(endpointBuilder::add)); - var customEndpoint = endpointBuilder.build(); + var customEndpoint = endpointBuilder.build(); - List> routerFunctions = - new ArrayList<>(rawRouterFunctions.size() + reverseProxies.size() + 1); - routerFunctions.addAll(rawRouterFunctions); - routerFunctions.addAll(reverseProxies); - routerFunctions.add(customEndpoint); - return routerFunctions; + List> routerFunctions = + new ArrayList<>(rawRouterFunctions.size() + reverseProxies.size() + 1); + routerFunctions.addAll(rawRouterFunctions); + routerFunctions.addAll(reverseProxies); + routerFunctions.add(customEndpoint); + return routerFunctions; + } finally { + getInstance().releaseReadLock(); + } } } diff --git a/application/src/test/java/run/halo/app/plugin/PluginCompositeRouterFunctionTest.java b/application/src/test/java/run/halo/app/plugin/PluginCompositeRouterFunctionTest.java index 7a75fd836..88649147e 100644 --- a/application/src/test/java/run/halo/app/plugin/PluginCompositeRouterFunctionTest.java +++ b/application/src/test/java/run/halo/app/plugin/PluginCompositeRouterFunctionTest.java @@ -55,6 +55,7 @@ class PluginCompositeRouterFunctionTest { @SuppressWarnings("unchecked") void setUp() { var fakeContext = mock(PluginApplicationContext.class); + when(fakeContext.isActive()).thenReturn(true); ExtensionContextRegistry.getInstance().register("fake-plugin", fakeContext); when(rawRouterFunctionsProvider.orderedStream()).thenReturn(Stream.empty());