From 9f3973940e67c8e144f911f3088e8d80cea31eca Mon Sep 17 00:00:00 2001 From: John Niang Date: Thu, 25 Jan 2024 05:32:16 +0800 Subject: [PATCH 1/6] Refactor plugin manager Signed-off-by: John Niang --- .../app/PluginApplicationContextFactory.java | 15 + .../reconciler/ReverseProxyReconciler.java | 4 +- .../halo/app/plugin/BasePluginFactory.java | 52 ---- ...efaultPluginApplicationContextFactory.java | 263 +++++++++++++++++ .../DefaultPluginRouterFunctionRegistry.java | 62 ++++ .../app/plugin/ExtensionComponentsFinder.java | 11 +- .../app/plugin/ExtensionContextRegistry.java | 130 --------- .../halo/app/plugin/HaloPluginManager.java | 266 +++++------------- .../app/plugin/PluginApplicationContext.java | 14 +- ...luginApplicationEventBridgeDispatcher.java | 47 ---- .../plugin/PluginApplicationInitializer.java | 264 ----------------- .../app/plugin/PluginAutoConfiguration.java | 87 +++--- .../plugin/PluginBeforeStopSyncListener.java | 11 +- .../plugin/PluginCompositeRouterFunction.java | 70 ----- .../app/plugin/PluginControllerManager.java | 59 ++-- .../plugin/PluginRequestMappingManager.java | 42 --- .../plugin/PluginRouterFunctionRegistry.java | 12 + .../app/plugin/PluginStartedListener.java | 11 +- .../SharedApplicationContextFactory.java | 60 ++++ .../SharedApplicationContextHolder.java | 89 ------ .../app/plugin/SpringComponentsFinder.java | 109 ++----- .../app/plugin/SpringExtensionFactory.java | 57 +--- .../run/halo/app/plugin/SpringPlugin.java | 65 +++++ .../halo/app/plugin/SpringPluginFactory.java | 39 +++ .../halo/app/plugin/SpringPluginManager.java | 12 + .../DefaultExtensionGetter.java | 4 +- .../ReverseProxyRouterFunctionFactory.java | 32 +-- .../ReverseProxyRouterFunctionRegistry.java | 74 ++--- .../app/theme/finders/FinderRegistry.java | 41 ++- .../ReverseProxyReconcilerTest.java | 5 +- ...faultPluginRouterFunctionRegistryTest.java | 34 +++ .../PluginCompositeRouterFunctionTest.java | 96 ------- .../SharedApplicationContextFactoryTest.java | 29 ++ .../SharedApplicationContextHolderTest.java | 37 --- .../plugin/SpringComponentsFinderTest.java | 39 --- ...ReverseProxyRouterFunctionFactoryTest.java | 14 +- ...everseProxyRouterFunctionRegistryTest.java | 50 +--- 37 files changed, 868 insertions(+), 1438 deletions(-) create mode 100644 application/src/main/java/run/halo/app/PluginApplicationContextFactory.java delete mode 100644 application/src/main/java/run/halo/app/plugin/BasePluginFactory.java create mode 100644 application/src/main/java/run/halo/app/plugin/DefaultPluginApplicationContextFactory.java create mode 100644 application/src/main/java/run/halo/app/plugin/DefaultPluginRouterFunctionRegistry.java delete mode 100644 application/src/main/java/run/halo/app/plugin/ExtensionContextRegistry.java delete mode 100644 application/src/main/java/run/halo/app/plugin/PluginApplicationEventBridgeDispatcher.java delete mode 100644 application/src/main/java/run/halo/app/plugin/PluginApplicationInitializer.java delete mode 100644 application/src/main/java/run/halo/app/plugin/PluginCompositeRouterFunction.java delete mode 100644 application/src/main/java/run/halo/app/plugin/PluginRequestMappingManager.java create mode 100644 application/src/main/java/run/halo/app/plugin/PluginRouterFunctionRegistry.java create mode 100644 application/src/main/java/run/halo/app/plugin/SharedApplicationContextFactory.java delete mode 100644 application/src/main/java/run/halo/app/plugin/SharedApplicationContextHolder.java create mode 100644 application/src/main/java/run/halo/app/plugin/SpringPlugin.java create mode 100644 application/src/main/java/run/halo/app/plugin/SpringPluginFactory.java create mode 100644 application/src/main/java/run/halo/app/plugin/SpringPluginManager.java create mode 100644 application/src/test/java/run/halo/app/plugin/DefaultPluginRouterFunctionRegistryTest.java delete mode 100644 application/src/test/java/run/halo/app/plugin/PluginCompositeRouterFunctionTest.java create mode 100644 application/src/test/java/run/halo/app/plugin/SharedApplicationContextFactoryTest.java delete mode 100644 application/src/test/java/run/halo/app/plugin/SharedApplicationContextHolderTest.java diff --git a/application/src/main/java/run/halo/app/PluginApplicationContextFactory.java b/application/src/main/java/run/halo/app/PluginApplicationContextFactory.java new file mode 100644 index 0000000000..cccb9cebbf --- /dev/null +++ b/application/src/main/java/run/halo/app/PluginApplicationContextFactory.java @@ -0,0 +1,15 @@ +package run.halo.app; + +import org.springframework.context.ApplicationContext; + +public interface PluginApplicationContextFactory { + + /** + * Create and refresh application context. + * + * @param pluginId plugin id + * @return refresh application context for the plugin. + */ + ApplicationContext create(String pluginId); + +} diff --git a/application/src/main/java/run/halo/app/core/extension/reconciler/ReverseProxyReconciler.java b/application/src/main/java/run/halo/app/core/extension/reconciler/ReverseProxyReconciler.java index 63f2a2e738..e0a34ca771 100644 --- a/application/src/main/java/run/halo/app/core/extension/reconciler/ReverseProxyReconciler.java +++ b/application/src/main/java/run/halo/app/core/extension/reconciler/ReverseProxyReconciler.java @@ -55,12 +55,12 @@ public Controller setupWith(ControllerBuilder builder) { private void registerReverseProxy(ReverseProxy reverseProxy) { String pluginId = getPluginId(reverseProxy); - routerFunctionRegistry.register(pluginId, reverseProxy).block(); + routerFunctionRegistry.register(pluginId, reverseProxy); } private void cleanUpResources(ReverseProxy reverseProxy) { String pluginId = getPluginId(reverseProxy); - routerFunctionRegistry.remove(pluginId, reverseProxy.getMetadata().getName()).block(); + routerFunctionRegistry.remove(pluginId, reverseProxy.getMetadata().getName()); } private void addFinalizerIfNecessary(ReverseProxy oldReverseProxy) { diff --git a/application/src/main/java/run/halo/app/plugin/BasePluginFactory.java b/application/src/main/java/run/halo/app/plugin/BasePluginFactory.java deleted file mode 100644 index 211cdee0d7..0000000000 --- a/application/src/main/java/run/halo/app/plugin/BasePluginFactory.java +++ /dev/null @@ -1,52 +0,0 @@ -package run.halo.app.plugin; - -import java.util.Optional; -import lombok.extern.slf4j.Slf4j; -import org.pf4j.Plugin; -import org.pf4j.PluginFactory; -import org.pf4j.PluginWrapper; -import org.springframework.beans.factory.NoSuchBeanDefinitionException; -import org.springframework.beans.factory.support.DefaultListableBeanFactory; - -/** - * The default implementation for PluginFactory. - *

Get a {@link BasePlugin} instance from the {@link PluginApplicationContext}.

- * - * @author guqing - * @since 2.0.0 - */ -@Slf4j -public class BasePluginFactory implements PluginFactory { - - @Override - public Plugin create(PluginWrapper pluginWrapper) { - return getPluginContext(pluginWrapper) - .map(context -> { - try { - var basePlugin = context.getBean(BasePlugin.class); - var pluginContext = context.getBean(PluginContext.class); - basePlugin.setContext(pluginContext); - return basePlugin; - } catch (NoSuchBeanDefinitionException e) { - log.info( - "No bean named 'basePlugin' found in the context create default instance"); - DefaultListableBeanFactory beanFactory = - context.getDefaultListableBeanFactory(); - var pluginContext = beanFactory.getBean(PluginContext.class); - BasePlugin pluginInstance = new BasePlugin(pluginContext); - beanFactory.registerSingleton(Plugin.class.getName(), pluginInstance); - return pluginInstance; - } - }) - .orElse(null); - } - - private Optional getPluginContext(PluginWrapper pluginWrapper) { - try { - return Optional.of(ExtensionContextRegistry.getInstance()) - .map(registry -> registry.getByPluginId(pluginWrapper.getPluginId())); - } catch (IllegalArgumentException e) { - return Optional.empty(); - } - } -} diff --git a/application/src/main/java/run/halo/app/plugin/DefaultPluginApplicationContextFactory.java b/application/src/main/java/run/halo/app/plugin/DefaultPluginApplicationContextFactory.java new file mode 100644 index 0000000000..adb512645d --- /dev/null +++ b/application/src/main/java/run/halo/app/plugin/DefaultPluginApplicationContextFactory.java @@ -0,0 +1,263 @@ +package run.halo.app.plugin; + +import static org.springframework.util.ResourceUtils.CLASSPATH_URL_PREFIX; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.stream.Stream; +import lombok.extern.slf4j.Slf4j; +import org.pf4j.PluginRuntimeException; +import org.springframework.boot.env.PropertySourceLoader; +import org.springframework.boot.env.YamlPropertySourceLoader; +import org.springframework.context.ApplicationContext; +import org.springframework.context.event.ContextClosedEvent; +import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.context.event.EventListener; +import org.springframework.core.ResolvableType; +import org.springframework.core.env.PropertySource; +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.stereotype.Controller; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.ServerResponse; +import reactor.core.Exceptions; +import run.halo.app.PluginApplicationContextFactory; +import run.halo.app.extension.ReactiveExtensionClient; +import run.halo.app.infra.properties.HaloProperties; +import run.halo.app.theme.DefaultTemplateNameResolver; +import run.halo.app.theme.ViewNameResolver; +import run.halo.app.theme.finders.FinderRegistry; + +@Slf4j +public class DefaultPluginApplicationContextFactory implements PluginApplicationContextFactory { + + private final SpringPluginManager pluginManager; + + public DefaultPluginApplicationContextFactory(SpringPluginManager pluginManager) { + this.pluginManager = pluginManager; + } + + /** + * Create and refresh application context. + * + * @param pluginId plugin id + * @return refresh application context for the plugin. + */ + @Override + public ApplicationContext create(String pluginId) { + log.debug("Preparing to create application context for plugin {}", pluginId); + var pluginWrapper = pluginManager.getPlugin(pluginId); + + var context = new PluginApplicationContext(pluginId); + context.setParent(pluginManager.getSharedContext()); + + var classLoader = pluginWrapper.getPluginClassLoader(); + var resourceLoader = new DefaultResourceLoader(classLoader); + context.setResourceLoader(resourceLoader); + var mutablePropertySources = context.getEnvironment().getPropertySources(); + + resolvePropertySources(pluginId, resourceLoader) + .forEach(mutablePropertySources::addLast); + + var beanFactory = context.getBeanFactory(); + context.registerBean(AggregatedRouterFunction.class); + beanFactory.registerSingleton("pluginWrapper", pluginWrapper); + + var rootContext = pluginManager.getRootContext(); + rootContext.getBeanProvider(ViewNameResolver.class) + .ifAvailable(viewNameResolver -> { + var templateNameResolver = + new DefaultTemplateNameResolver(viewNameResolver, context); + beanFactory.registerSingleton("templateNameResolver", templateNameResolver); + }); + + rootContext.getBeanProvider(ReactiveExtensionClient.class) + .ifAvailable(client -> { + var reactiveSettingFetcher = new DefaultReactiveSettingFetcher(client, pluginId); + var settingFetcher = new DefaultSettingFetcher(reactiveSettingFetcher); + beanFactory.registerSingleton("reactiveSettingFetcher", reactiveSettingFetcher); + beanFactory.registerSingleton("settingFetcher", settingFetcher); + }); + + var classNames = pluginManager.getExtensionClassNames(pluginId); + classNames.stream() + .map(className -> { + try { + return classLoader.loadClass(className); + } catch (ClassNotFoundException e) { + throw new PluginRuntimeException(String.format(""" + Failed to load class %s for plugin %s.\ + """, className, pluginId), e); + } + }) + .forEach(context::register); + + rootContext.getBeanProvider(PluginRequestMappingHandlerMapping.class) + .ifAvailable(handlerMapping -> { + var handlerMappingManager = + new PluginHandlerMappingManager(pluginId, handlerMapping); + beanFactory.registerSingleton("pluginHandlerMappingManager", handlerMappingManager); + }); + + context.registerBean(PluginControllerManager.class); + + rootContext.getBeanProvider(FinderRegistry.class) + .ifAvailable(finderRegistry -> { + var finderManager = new FinderManager(pluginId, finderRegistry); + beanFactory.registerSingleton("finderManager", finderManager); + }); + + rootContext.getBeanProvider(PluginRouterFunctionRegistry.class) + .ifUnique(registry -> { + var pluginRouterFunctionManager = new PluginRouterFunctionManager(registry); + beanFactory.registerSingleton( + "pluginRouterFunctionManager", + pluginRouterFunctionManager + ); + }); + + log.debug("Created application context for plugin {}", pluginId); + log.debug("Refreshing application context for plugin {}", pluginId); + + context.refresh(); + log.debug("Refreshed application context for plugin {}", pluginId); + return context; + } + + private static class FinderManager { + + private final String pluginId; + + private final FinderRegistry finderRegistry; + + private FinderManager(String pluginId, FinderRegistry finderRegistry) { + this.pluginId = pluginId; + this.finderRegistry = finderRegistry; + } + + @EventListener + public void onApplicationEvent(ContextClosedEvent event) { + this.finderRegistry.unregister(this.pluginId); + } + + @EventListener + public void onApplicationEvent(ContextRefreshedEvent event) { + this.finderRegistry.register(this.pluginId, event.getApplicationContext()); + } + + } + + private static class PluginRouterFunctionManager { + + private final PluginRouterFunctionRegistry routerFunctionRegistry; + + private Collection> routerFunctions; + + private PluginRouterFunctionManager(PluginRouterFunctionRegistry routerFunctionRegistry) { + this.routerFunctionRegistry = routerFunctionRegistry; + } + + @EventListener + public void onApplicationEvent(ContextClosedEvent event) { + if (routerFunctions != null) { + routerFunctionRegistry.unregister(routerFunctions); + } + } + + @EventListener + public void onApplicationEvent(ContextRefreshedEvent event) { + var routerFunctions = event.getApplicationContext() + .>getBeanProvider( + ResolvableType.forClassWithGenerics(RouterFunction.class, ServerResponse.class) + ) + .orderedStream() + .toList(); + routerFunctionRegistry.register(routerFunctions); + this.routerFunctions = routerFunctions; + } + } + + + private static class PluginHandlerMappingManager { + private final String pluginId; + + private final PluginRequestMappingHandlerMapping handlerMapping; + + private PluginHandlerMappingManager(String pluginId, + PluginRequestMappingHandlerMapping handlerMapping) { + this.pluginId = pluginId; + this.handlerMapping = handlerMapping; + } + + @EventListener + public void onApplicationEvent(ContextRefreshedEvent event) { + var context = event.getApplicationContext(); + context.getBeansWithAnnotation(Controller.class) + .values() + .forEach(controller -> + handlerMapping.registerHandlerMethods(this.pluginId, controller) + ); + } + + @EventListener + public void onApplicationEvent(ContextClosedEvent event) { + handlerMapping.unregister(this.pluginId); + } + } + + private List> resolvePropertySources(String pluginId, + ResourceLoader resourceLoader) { + var haloProperties = pluginManager.getRootContext() + .getBeanProvider(HaloProperties.class) + .getIfAvailable(); + if (haloProperties == null) { + return List.of(); + } + + var propertySourceLoader = new YamlPropertySourceLoader(); + var propertySources = new ArrayList>(); + var configsPath = haloProperties.getWorkDir().resolve("plugins").resolve("configs"); + // resolve user defined config + Stream.of( + configsPath.resolve(pluginId + ".yaml"), + configsPath.resolve(pluginId + ".yml") + ) + .map(path -> resourceLoader.getResource(path.toUri().toString())) + .forEach(resource -> { + var sources = + loadPropertySources("user-defined-config", resource, propertySourceLoader); + propertySources.addAll(sources); + }); + + // resolve default config + Stream.of( + CLASSPATH_URL_PREFIX + "/config.yaml", + CLASSPATH_URL_PREFIX + "/config.yaml" + ) + .map(resourceLoader::getResource) + .forEach(resource -> { + var sources = loadPropertySources("default-config", resource, propertySourceLoader); + propertySources.addAll(sources); + }); + return propertySources; + } + + private List> loadPropertySources(String propertySourceName, + Resource resource, + PropertySourceLoader propertySourceLoader) { + if (log.isDebugEnabled()) { + log.debug("Loading property sources from {}", resource); + } + if (!resource.exists()) { + return List.of(); + } + try { + return propertySourceLoader.load(propertySourceName, resource); + } catch (IOException e) { + throw Exceptions.propagate(e); + } + } +} diff --git a/application/src/main/java/run/halo/app/plugin/DefaultPluginRouterFunctionRegistry.java b/application/src/main/java/run/halo/app/plugin/DefaultPluginRouterFunctionRegistry.java new file mode 100644 index 0000000000..602fd56063 --- /dev/null +++ b/application/src/main/java/run/halo/app/plugin/DefaultPluginRouterFunctionRegistry.java @@ -0,0 +1,62 @@ +package run.halo.app.plugin; + +import java.util.Collection; +import java.util.concurrent.CopyOnWriteArraySet; +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.server.HandlerFunction; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.RouterFunctions; +import org.springframework.web.reactive.function.server.ServerRequest; +import org.springframework.web.reactive.function.server.ServerResponse; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +/** + * A composite {@link RouterFunction} implementation for plugin. + * + * @author guqing + * @since 2.0.0 + */ +@Component +public class DefaultPluginRouterFunctionRegistry + implements RouterFunction, PluginRouterFunctionRegistry { + + private final Collection> routerFunctions; + + public DefaultPluginRouterFunctionRegistry() { + this.routerFunctions = new CopyOnWriteArraySet<>(); + } + + @Override + @NonNull + public Mono> route(@NonNull ServerRequest request) { + return Flux.fromIterable(this.routerFunctions) + .concatMap(routerFunction -> routerFunction.route(request)) + .next(); + } + + @Override + public void accept(@NonNull RouterFunctions.Visitor visitor) { + this.routerFunctions.forEach(routerFunction -> routerFunction.accept(visitor)); + } + + @Override + public void register(Collection> routerFunctions) { + this.routerFunctions.addAll(routerFunctions); + } + + @Override + public void unregister(Collection> routerFunctions) { + this.routerFunctions.removeAll(routerFunctions); + } + + /** + * Only for testing. + * + * @return maintained router functions. + */ + Collection> getRouterFunctions() { + return routerFunctions; + } +} diff --git a/application/src/main/java/run/halo/app/plugin/ExtensionComponentsFinder.java b/application/src/main/java/run/halo/app/plugin/ExtensionComponentsFinder.java index a4bc66741a..f76d8722a2 100644 --- a/application/src/main/java/run/halo/app/plugin/ExtensionComponentsFinder.java +++ b/application/src/main/java/run/halo/app/plugin/ExtensionComponentsFinder.java @@ -3,6 +3,7 @@ import java.util.ArrayList; import java.util.List; import org.pf4j.ExtensionPoint; +import org.pf4j.PluginManager; import org.springframework.context.ApplicationContext; import org.springframework.stereotype.Component; @@ -15,12 +16,12 @@ @Component public class ExtensionComponentsFinder { public static final String SYSTEM_PLUGIN_ID = "system"; - private final HaloPluginManager haloPluginManager; + private final PluginManager pluginManager; private final ApplicationContext applicationContext; - public ExtensionComponentsFinder(HaloPluginManager haloPluginManager, + public ExtensionComponentsFinder(PluginManager pluginManager, ApplicationContext applicationContext) { - this.haloPluginManager = haloPluginManager; + this.pluginManager = pluginManager; this.applicationContext = applicationContext; } @@ -33,7 +34,7 @@ public ExtensionComponentsFinder(HaloPluginManager haloPluginManager, */ public List getExtensions(Class type) { assertExtensionPoint(type); - List components = new ArrayList<>(haloPluginManager.getExtensions(type)); + List components = new ArrayList<>(pluginManager.getExtensions(type)); components.addAll(applicationContext.getBeansOfType(type).values()); return List.copyOf(components); } @@ -53,7 +54,7 @@ public List getExtensions(Class type, String pluginId) { components.addAll(applicationContext.getBeansOfType(type).values()); return components; } else { - components.addAll(haloPluginManager.getExtensions(type, pluginId)); + components.addAll(pluginManager.getExtensions(type, pluginId)); } return components; } diff --git a/application/src/main/java/run/halo/app/plugin/ExtensionContextRegistry.java b/application/src/main/java/run/halo/app/plugin/ExtensionContextRegistry.java deleted file mode 100644 index 419e45d99c..0000000000 --- a/application/src/main/java/run/halo/app/plugin/ExtensionContextRegistry.java +++ /dev/null @@ -1,130 +0,0 @@ -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.locks.ReentrantReadWriteLock; -import org.springframework.lang.NonNull; - -/** - *

Plugin application context registrar.

- *

It contains a map, the key is the plugin id and the value is application context of plugin - * .

- *

when the plugin is enabled, an application context will be registered in the map by plugin id. - * it will be deleted according to its id when the plugin is disabled.

- * - * @author guqing - * @since 2021-11-15 - */ -public class ExtensionContextRegistry { - private static final ExtensionContextRegistry INSTANCE = new ExtensionContextRegistry(); - - private final Map registry = new HashMap<>(); - private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock(); - - public static ExtensionContextRegistry getInstance() { - return INSTANCE; - } - - private ExtensionContextRegistry() { - } - - /** - * Acquire the read lock when using getPluginApplicationContexts and getByPluginId. - */ - public void acquireReadLock() { - this.readWriteLock.readLock().lock(); - } - - /** - * 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) { - 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(); - } - } - - /** - * 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) { - 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() { - 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/HaloPluginManager.java b/application/src/main/java/run/halo/app/plugin/HaloPluginManager.java index 6409be4a71..7066a610d6 100644 --- a/application/src/main/java/run/halo/app/plugin/HaloPluginManager.java +++ b/application/src/main/java/run/halo/app/plugin/HaloPluginManager.java @@ -10,24 +10,20 @@ import org.pf4j.DefaultPluginManager; import org.pf4j.ExtensionFactory; import org.pf4j.ExtensionFinder; -import org.pf4j.PluginDependency; import org.pf4j.PluginDescriptor; import org.pf4j.PluginDescriptorFinder; import org.pf4j.PluginFactory; import org.pf4j.PluginRuntimeException; import org.pf4j.PluginState; import org.pf4j.PluginStateEvent; +import org.pf4j.PluginStateListener; import org.pf4j.PluginWrapper; -import org.springframework.beans.BeansException; import org.springframework.beans.factory.DisposableBean; -import org.springframework.beans.factory.InitializingBean; import org.springframework.context.ApplicationContext; -import org.springframework.context.ApplicationContextAware; -import org.springframework.lang.NonNull; +import org.springframework.data.util.Lazy; import run.halo.app.plugin.event.HaloPluginBeforeStopEvent; import run.halo.app.plugin.event.HaloPluginLoadedEvent; import run.halo.app.plugin.event.HaloPluginStartedEvent; -import run.halo.app.plugin.event.HaloPluginStateChangedEvent; import run.halo.app.plugin.event.HaloPluginStoppedEvent; /** @@ -39,22 +35,27 @@ */ @Slf4j public class HaloPluginManager extends DefaultPluginManager - implements ApplicationContextAware, InitializingBean, DisposableBean { + implements DisposableBean, SpringPluginManager { private final Map startingErrors = new HashMap<>(); - private ApplicationContext rootApplicationContext; + private final ApplicationContext rootContext; - private PluginApplicationInitializer pluginApplicationInitializer; + private final Lazy sharedContext; - private PluginRequestMappingManager requestMappingManager; - - public HaloPluginManager() { - super(); + public HaloPluginManager(Path pluginsRoot, ApplicationContext rootContext) { + super(pluginsRoot); + this.rootContext = rootContext; + // We have to initialize share context lazily because the root context has not refreshed + this.sharedContext = Lazy.of(() -> SharedApplicationContextFactory.create(rootContext)); } - public HaloPluginManager(Path pluginsRoot) { - super(pluginsRoot); + @Override + protected void initialize() { + super.initialize(); + + // add additional listener + addPluginStateListener(new PluginStartedEventAdapter()); } @Override @@ -64,31 +65,15 @@ protected ExtensionFactory createExtensionFactory() { @Override protected ExtensionFinder createExtensionFinder() { - return new SpringComponentsFinder(this); - } - - @Override - public final void setApplicationContext(@NonNull ApplicationContext rootApplicationContext) - throws BeansException { - this.rootApplicationContext = rootApplicationContext; - } - - final PluginApplicationContext getPluginApplicationContext(String pluginId) { - return pluginApplicationInitializer.getPluginApplicationContext(pluginId); + var finder = new SpringComponentsFinder(this); + addPluginStateListener(finder); + return finder; } @Override protected PluginFactory createPluginFactory() { - return new BasePluginFactory(); - } - - @Override - public final void afterPropertiesSet() { - this.pluginApplicationInitializer = - new PluginApplicationInitializer(this, rootApplicationContext); - - this.requestMappingManager = - rootApplicationContext.getBean(PluginRequestMappingManager.class); + var contextFactory = new DefaultPluginApplicationContextFactory(this); + return new SpringPluginFactory(contextFactory); } @Override @@ -101,19 +86,12 @@ protected PluginWrapper createPluginWrapper(PluginDescriptor pluginDescriptor, P ClassLoader pluginClassLoader) { // create the plugin wrapper log.debug("Creating wrapper for plugin '{}'", pluginPath); - HaloPluginWrapper pluginWrapper = + var pluginWrapper = new HaloPluginWrapper(this, pluginDescriptor, pluginPath, pluginClassLoader); pluginWrapper.setPluginFactory(getPluginFactory()); return pluginWrapper; } - @Override - protected void firePluginStateEvent(PluginStateEvent event) { - rootApplicationContext.publishEvent( - new HaloPluginStateChangedEvent(this, event.getPlugin(), event.getOldState())); - super.firePluginStateEvent(event); - } - @Override protected PluginState stopPlugin(String pluginId, boolean stopDependents) { checkPluginId(pluginId); @@ -131,7 +109,7 @@ protected PluginState stopPlugin(String pluginId, boolean stopDependents) { return pluginState; } - rootApplicationContext.publishEvent(new HaloPluginBeforeStopEvent(this, pluginWrapper)); + rootContext.publishEvent(new HaloPluginBeforeStopEvent(this, pluginWrapper)); if (stopDependents) { List dependents = dependencyResolver.getDependents(pluginId); @@ -141,153 +119,42 @@ protected PluginState stopPlugin(String pluginId, boolean stopDependents) { dependents.addAll(0, dependencyResolver.getDependents(dependent)); } } - try { - log.info("Stop plugin '{}'", getPluginLabel(pluginDescriptor)); - if (pluginWrapper.getPlugin() != null) { - pluginWrapper.getPlugin().stop(); - } - pluginWrapper.setPluginState(PluginState.STOPPED); - // release plugin resources - releaseAdditionalResources(pluginId); - - startedPlugins.remove(pluginWrapper); - - rootApplicationContext.publishEvent(new HaloPluginStoppedEvent(this, pluginWrapper)); - firePluginStateEvent(new PluginStateEvent(this, pluginWrapper, pluginState)); - } catch (Exception e) { - log.error(e.getMessage(), e); - startingErrors.put(pluginWrapper.getPluginId(), PluginStartingError.of( - pluginWrapper.getPluginId(), e.getMessage(), e.toString())); - } - return pluginWrapper.getPluginState(); - } - @Override - public PluginState stopPlugin(String pluginId) { - return this.stopPlugin(pluginId, true); - } - - @Override - public void startPlugins() { - startingErrors.clear(); - long ts = System.currentTimeMillis(); - - for (PluginWrapper pluginWrapper : resolvedPlugins) { - checkExtensionFinderReady(pluginWrapper); - PluginState pluginState = pluginWrapper.getPluginState(); - if ((PluginState.DISABLED != pluginState) && (PluginState.STARTED != pluginState)) { - try { - log.info("Start plugin '{}'", getPluginLabel(pluginWrapper.getDescriptor())); - // inject bean - pluginApplicationInitializer.onStartUp(pluginWrapper.getPluginId()); - - pluginWrapper.getPlugin().start(); - - requestMappingManager.registerHandlerMappings(pluginWrapper); + log.info("Stop plugin '{}'", getPluginLabel(pluginDescriptor)); + pluginWrapper.getPlugin().stop(); + pluginWrapper.setPluginState(PluginState.STOPPED); + // release plugin resources + releaseAdditionalResources(pluginId); - pluginWrapper.setPluginState(PluginState.STARTED); - pluginWrapper.setFailedException(null); - startedPlugins.add(pluginWrapper); + startedPlugins.remove(pluginWrapper); - rootApplicationContext.publishEvent( - new HaloPluginStartedEvent(this, pluginWrapper)); - } catch (Exception | LinkageError e) { - pluginWrapper.setPluginState(PluginState.FAILED); - pluginWrapper.setFailedException(e); - startingErrors.put(pluginWrapper.getPluginId(), PluginStartingError.of( - pluginWrapper.getPluginId(), e.getMessage(), e.toString())); - releaseAdditionalResources(pluginWrapper.getPluginId()); - log.error("Unable to start plugin '{}'", - getPluginLabel(pluginWrapper.getDescriptor()), e); - } finally { - firePluginStateEvent(new PluginStateEvent(this, pluginWrapper, pluginState)); - } - } - } + rootContext.publishEvent(new HaloPluginStoppedEvent(this, pluginWrapper)); + firePluginStateEvent(new PluginStateEvent(this, pluginWrapper, pluginState)); - log.info("[Halo] {} plugins are started in {}ms. {} failed", - getPlugins(PluginState.STARTED).size(), - System.currentTimeMillis() - ts, startingErrors.size()); + return pluginWrapper.getPluginState(); } @Override - public PluginState startPlugin(String pluginId) { - return doStartPlugin(pluginId); + public void startPlugins() { + throw new UnsupportedOperationException( + "The operation of starting all plugins is not supported." + ); } @Override - public void stopPlugins() { - doStopPlugins(); - } - - private PluginState doStartPlugin(String pluginId) { - checkPluginId(pluginId); - - PluginWrapper pluginWrapper = getPlugin(pluginId); - - checkExtensionFinderReady(pluginWrapper); - - PluginDescriptor pluginDescriptor = pluginWrapper.getDescriptor(); - PluginState pluginState = pluginWrapper.getPluginState(); - if (PluginState.STARTED == pluginState) { - log.debug("Already started plugin '{}'", getPluginLabel(pluginDescriptor)); - return PluginState.STARTED; - } - - if (!resolvedPlugins.contains(pluginWrapper)) { - log.warn("Cannot start an unresolved plugin '{}'", getPluginLabel(pluginDescriptor)); - return pluginState; - } - - if (PluginState.DISABLED == pluginState) { - // automatically enable plugin on manual plugin start - if (!enablePlugin(pluginId)) { - return pluginState; - } - } - - for (PluginDependency dependency : pluginDescriptor.getDependencies()) { - // start dependency only if it marked as required (non-optional) or if it's optional - // and loaded - if (!dependency.isOptional() || plugins.containsKey(dependency.getPluginId())) { - startPlugin(dependency.getPluginId()); - } - } - log.info("Start plugin '{}'", getPluginLabel(pluginDescriptor)); - + public PluginState startPlugin(String pluginId) { try { - // load and inject bean - pluginApplicationInitializer.onStartUp(pluginId); - - // create plugin instance and start it - pluginWrapper.getPlugin().start(); - - requestMappingManager.registerHandlerMappings(pluginWrapper); - - pluginWrapper.setPluginState(PluginState.STARTED); - startedPlugins.add(pluginWrapper); - - rootApplicationContext.publishEvent(new HaloPluginStartedEvent(this, pluginWrapper)); - } catch (Exception e) { - log.error("Unable to start plugin '{}'", - getPluginLabel(pluginWrapper.getDescriptor()), e); - pluginWrapper.setPluginState(PluginState.FAILED); - startingErrors.put(pluginWrapper.getPluginId(), PluginStartingError.of( - pluginWrapper.getPluginId(), e.getMessage(), e.toString())); - releaseAdditionalResources(pluginId); - } finally { - firePluginStateEvent(new PluginStateEvent(this, pluginWrapper, pluginState)); + return super.startPlugin(pluginId); + } catch (Throwable t) { + // TODO Do not release additional resources here. + // releaseAdditionalResources(pluginId); + throw t; } - return pluginWrapper.getPluginState(); } - private void checkExtensionFinderReady(PluginWrapper pluginWrapper) { - if (extensionFinder instanceof SpringComponentsFinder springComponentsFinder) { - springComponentsFinder.readPluginStorageToMemory(pluginWrapper); - return; - } - // should never happen - throw new PluginRuntimeException("Plugin component classes may not loaded yet."); + @Override + public void stopPlugins() { + doStopPlugins(); } private void doStopPlugins() { @@ -300,7 +167,7 @@ private void doStopPlugins() { PluginState pluginState = pluginWrapper.getPluginState(); if (PluginState.STARTED == pluginState) { try { - rootApplicationContext.publishEvent( + rootContext.publishEvent( new HaloPluginBeforeStopEvent(this, pluginWrapper)); log.info("Stop plugin '{}'", getPluginLabel(pluginWrapper.getDescriptor())); if (pluginWrapper.getPlugin() != null) { @@ -310,7 +177,7 @@ private void doStopPlugins() { itr.remove(); releaseAdditionalResources(pluginWrapper.getPluginId()); - rootApplicationContext.publishEvent( + rootContext.publishEvent( new HaloPluginStoppedEvent(this, pluginWrapper)); firePluginStateEvent(new PluginStateEvent(this, pluginWrapper, pluginState)); } catch (PluginRuntimeException e) { @@ -326,32 +193,41 @@ private void doStopPlugins() { * Release plugin holding release on stop. */ public void releaseAdditionalResources(String pluginId) { - removePluginComponentsCache(pluginId); - // release request mapping - requestMappingManager.removeHandlerMappings(pluginId); - try { - pluginApplicationInitializer.contextDestroyed(pluginId); - } catch (Exception e) { - log.warn("Plugin application context close failed. ", e); - } } @Override protected PluginWrapper loadPluginFromPath(Path pluginPath) { PluginWrapper pluginWrapper = super.loadPluginFromPath(pluginPath); - rootApplicationContext.publishEvent(new HaloPluginLoadedEvent(this, pluginWrapper)); + rootContext.publishEvent(new HaloPluginLoadedEvent(this, pluginWrapper)); return pluginWrapper; } - private void removePluginComponentsCache(String pluginId) { - if (extensionFinder instanceof SpringComponentsFinder springComponentsFinder) { - springComponentsFinder.removeComponentsStorage(pluginId); - } - } @Override public void destroy() throws Exception { stopPlugins(); } - // end-region + + @Override + public ApplicationContext getRootContext() { + return rootContext; + } + + @Override + public ApplicationContext getSharedContext() { + return sharedContext.get(); + } + + private class PluginStartedEventAdapter implements PluginStateListener { + + @Override + public void pluginStateChanged(PluginStateEvent event) { + if (!PluginState.STARTED.equals(event.getPluginState())) { + return; + } + // Indicate the state is started. + var pluginWrapper = event.getPlugin(); + rootContext.publishEvent(new HaloPluginStartedEvent(event.getSource(), pluginWrapper)); + } + } } diff --git a/application/src/main/java/run/halo/app/plugin/PluginApplicationContext.java b/application/src/main/java/run/halo/app/plugin/PluginApplicationContext.java index 610e032921..8352cec338 100644 --- a/application/src/main/java/run/halo/app/plugin/PluginApplicationContext.java +++ b/application/src/main/java/run/halo/app/plugin/PluginApplicationContext.java @@ -2,7 +2,7 @@ import java.util.List; import java.util.concurrent.locks.StampedLock; -import org.springframework.context.support.GenericApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.util.Assert; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; @@ -16,18 +16,18 @@ * @author guqing * @since 2.0.0 */ -public class PluginApplicationContext extends GenericApplicationContext { +public class PluginApplicationContext extends AnnotationConfigApplicationContext { private final GvkExtensionMapping gvkExtensionMapping = new GvkExtensionMapping(); - private String pluginId; + private final String pluginId; - public String getPluginId() { - return pluginId; + public PluginApplicationContext(String pluginId) { + this.pluginId = pluginId; } - public void setPluginId(String pluginId) { - this.pluginId = pluginId; + public String getPluginId() { + return pluginId; } /** diff --git a/application/src/main/java/run/halo/app/plugin/PluginApplicationEventBridgeDispatcher.java b/application/src/main/java/run/halo/app/plugin/PluginApplicationEventBridgeDispatcher.java deleted file mode 100644 index 75f22908a4..0000000000 --- a/application/src/main/java/run/halo/app/plugin/PluginApplicationEventBridgeDispatcher.java +++ /dev/null @@ -1,47 +0,0 @@ -package run.halo.app.plugin; - -import java.lang.reflect.AnnotatedElement; -import java.util.List; -import lombok.extern.slf4j.Slf4j; -import org.springframework.context.ApplicationEvent; -import org.springframework.context.ApplicationListener; -import org.springframework.core.annotation.AnnotationUtils; -import org.springframework.stereotype.Component; - -/** - *

The main application bridges the events marked with {@code SharedEvent} annotation to the - * enabled plug-in so that it can be listened by the plugin.

- * - * @author guqing - * @see SharedEvent - * @see PluginApplicationContext - * @since 2.0.0 - */ -@Slf4j -@Component -public class PluginApplicationEventBridgeDispatcher - implements ApplicationListener { - - @Override - public void onApplicationEvent(ApplicationEvent event) { - if (!isSharedEventAnnotationPresent(event.getClass())) { - return; - } - 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(); - } - } - - private boolean isSharedEventAnnotationPresent(AnnotatedElement annotatedElement) { - return AnnotationUtils.findAnnotation(annotatedElement, SharedEvent.class) != null; - } -} diff --git a/application/src/main/java/run/halo/app/plugin/PluginApplicationInitializer.java b/application/src/main/java/run/halo/app/plugin/PluginApplicationInitializer.java deleted file mode 100644 index 7bdaa3699b..0000000000 --- a/application/src/main/java/run/halo/app/plugin/PluginApplicationInitializer.java +++ /dev/null @@ -1,264 +0,0 @@ -package run.halo.app.plugin; - -import static org.springframework.util.ResourceUtils.CLASSPATH_URL_PREFIX; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.stream.Stream; -import lombok.extern.slf4j.Slf4j; -import org.pf4j.PluginRuntimeException; -import org.pf4j.PluginWrapper; -import org.springframework.beans.factory.support.DefaultListableBeanFactory; -import org.springframework.boot.env.PropertySourceLoader; -import org.springframework.boot.env.YamlPropertySourceLoader; -import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.AnnotationConfigUtils; -import org.springframework.core.env.PropertySource; -import org.springframework.core.io.DefaultResourceLoader; -import org.springframework.core.io.Resource; -import org.springframework.core.io.ResourceLoader; -import org.springframework.lang.NonNull; -import org.springframework.util.Assert; -import org.springframework.util.StopWatch; -import reactor.core.Exceptions; -import run.halo.app.extension.ReactiveExtensionClient; -import run.halo.app.infra.properties.HaloProperties; -import run.halo.app.theme.DefaultTemplateNameResolver; -import run.halo.app.theme.DefaultViewNameResolver; - -/** - * Plugin application initializer will create plugin application context by plugin id and - * register beans to plugin application context. - * - * @author guqing - * @since 2021-11-01 - */ -@Slf4j -public class PluginApplicationInitializer { - - protected final HaloPluginManager haloPluginManager; - - private final ExtensionContextRegistry contextRegistry = ExtensionContextRegistry.getInstance(); - private final SharedApplicationContextHolder sharedApplicationContextHolder; - private final ApplicationContext rootApplicationContext; - - private final HaloProperties haloProperties; - - public PluginApplicationInitializer(HaloPluginManager haloPluginManager, - ApplicationContext rootApplicationContext) { - Assert.notNull(haloPluginManager, "The haloPluginManager must not be null"); - Assert.notNull(rootApplicationContext, "The rootApplicationContext must not be null"); - this.haloPluginManager = haloPluginManager; - this.rootApplicationContext = rootApplicationContext; - sharedApplicationContextHolder = rootApplicationContext - .getBean(SharedApplicationContextHolder.class); - haloProperties = rootApplicationContext.getBean(HaloProperties.class); - } - - private PluginApplicationContext createPluginApplicationContext(String pluginId) { - PluginWrapper plugin = haloPluginManager.getPlugin(pluginId); - ClassLoader pluginClassLoader = plugin.getPluginClassLoader(); - - StopWatch stopWatch = new StopWatch("initialize-plugin-context"); - stopWatch.start("Create PluginApplicationContext"); - PluginApplicationContext pluginApplicationContext = new PluginApplicationContext(); - pluginApplicationContext.setClassLoader(pluginClassLoader); - - if (sharedApplicationContextHolder != null) { - pluginApplicationContext.setParent(sharedApplicationContextHolder.getInstance()); - } - - // populate plugin to plugin application context - pluginApplicationContext.setPluginId(pluginId); - stopWatch.stop(); - - stopWatch.start("Create DefaultResourceLoader"); - DefaultResourceLoader defaultResourceLoader = new DefaultResourceLoader(pluginClassLoader); - pluginApplicationContext.setResourceLoader(defaultResourceLoader); - - var mutablePropertySources = pluginApplicationContext.getEnvironment().getPropertySources(); - resolvePropertySources(pluginId, pluginApplicationContext) - .forEach(mutablePropertySources::addLast); - - stopWatch.stop(); - - DefaultListableBeanFactory beanFactory = - (DefaultListableBeanFactory) pluginApplicationContext.getBeanFactory(); - - stopWatch.start("registerAnnotationConfigProcessors"); - AnnotationConfigUtils.registerAnnotationConfigProcessors(beanFactory); - stopWatch.stop(); - - pluginApplicationContext.registerBean(AggregatedRouterFunction.class); - - beanFactory.registerSingleton("pluginContext", createPluginContext(plugin)); - - // TODO deprecated - beanFactory.registerSingleton("pluginWrapper", haloPluginManager.getPlugin(pluginId)); - - beanFactory.registerSingleton("templateNameResolver", - new DefaultTemplateNameResolver( - rootApplicationContext.getBean(DefaultViewNameResolver.class), - pluginApplicationContext)); - populateSettingFetcher(pluginId, beanFactory); - - log.debug("Total millis: {} ms -> {}", stopWatch.getTotalTimeMillis(), - stopWatch.prettyPrint()); - - return pluginApplicationContext; - } - - private void initApplicationContext(String pluginId) { - if (contextRegistry.containsContext(pluginId)) { - log.debug("Plugin application context for [{}] has bean initialized.", pluginId); - return; - } - StopWatch stopWatch = new StopWatch(); - - stopWatch.start("createPluginApplicationContext"); - PluginApplicationContext pluginApplicationContext = - createPluginApplicationContext(pluginId); - stopWatch.stop(); - - stopWatch.start("findCandidateComponents"); - Set> candidateComponents = findCandidateComponents(pluginId); - stopWatch.stop(); - - stopWatch.start("registerBean"); - for (Class component : candidateComponents) { - log.debug("Register a plugin component class [{}] to context", component); - pluginApplicationContext.registerBean(component); - } - stopWatch.stop(); - - stopWatch.start("refresh plugin application context"); - pluginApplicationContext.refresh(); - stopWatch.stop(); - - contextRegistry.register(pluginId, pluginApplicationContext); - - log.debug("initApplicationContext total millis: {} ms -> {}", - stopWatch.getTotalTimeMillis(), stopWatch.prettyPrint()); - } - - PluginContext createPluginContext(PluginWrapper pluginWrapper) { - if (pluginWrapper instanceof HaloPluginWrapper haloPluginWrapper) { - return new PluginContext(haloPluginWrapper.getPluginId(), - pluginWrapper.getDescriptor().getVersion(), - haloPluginWrapper.getRuntimeMode()); - } - throw new PluginRuntimeException("PluginWrapper must be instance of HaloPluginWrapper"); - } - - private void populateSettingFetcher(String pluginName, - DefaultListableBeanFactory listableBeanFactory) { - ReactiveExtensionClient extensionClient = - rootApplicationContext.getBean(ReactiveExtensionClient.class); - ReactiveSettingFetcher reactiveSettingFetcher = - new DefaultReactiveSettingFetcher(extensionClient, pluginName); - listableBeanFactory.registerSingleton("settingFetcher", - new DefaultSettingFetcher(reactiveSettingFetcher)); - listableBeanFactory.registerSingleton("reactiveSettingFetcher", reactiveSettingFetcher); - } - - public void onStartUp(String pluginId) { - initApplicationContext(pluginId); - } - - @NonNull - public PluginApplicationContext getPluginApplicationContext(String pluginId) { - return contextRegistry.getByPluginId(pluginId); - } - - public void contextDestroyed(String pluginId) { - Assert.notNull(pluginId, "pluginId must not be null"); - contextRegistry.remove(pluginId); - } - - private Set> findCandidateComponents(String pluginId) { - StopWatch stopWatch = new StopWatch("findCandidateComponents"); - - stopWatch.start("getExtensionClassNames"); - Set extensionClassNames = haloPluginManager.getExtensionClassNames(pluginId); - if (extensionClassNames == null) { - log.debug("No components class names found for plugin [{}]", pluginId); - extensionClassNames = Set.of(); - } - stopWatch.stop(); - - // add extensions for each started plugin - PluginWrapper plugin = haloPluginManager.getPlugin(pluginId); - log.debug("Registering extensions of the plugin '{}' as beans", pluginId); - Set> candidateComponents = new HashSet<>(); - for (String extensionClassName : extensionClassNames) { - log.debug("Load extension class '{}'", extensionClassName); - try { - stopWatch.start("loadClass"); - Class extensionClass = - plugin.getPluginClassLoader().loadClass(extensionClassName); - stopWatch.stop(); - - candidateComponents.add(extensionClass); - } catch (ClassNotFoundException e) { - log.error(e.getMessage(), e); - } - } - log.debug("total millis: {}ms -> {}", stopWatch.getTotalTimeMillis(), - stopWatch.prettyPrint()); - return candidateComponents; - } - - private List> resolvePropertySources(String pluginId, - ResourceLoader resourceLoader) { - var propertySourceLoader = new YamlPropertySourceLoader(); - var propertySources = new ArrayList>(); - var configsPath = haloProperties.getWorkDir().resolve("plugins").resolve("configs"); - - // resolve user defined config - Stream.of( - configsPath.resolve(pluginId + ".yaml"), - configsPath.resolve(pluginId + ".yml") - ) - .map(path -> resourceLoader.getResource(path.toUri().toString())) - .forEach(resource -> { - var sources = - loadPropertySources("user-defined-config", resource, propertySourceLoader); - propertySources.addAll(sources); - }); - - // resolve default config - Stream.of( - CLASSPATH_URL_PREFIX + "/config.yaml", - CLASSPATH_URL_PREFIX + "/config.yaml" - ) - .map(resourceLoader::getResource) - .forEach(resource -> { - var sources = loadPropertySources("default-config", resource, propertySourceLoader); - propertySources.addAll(sources); - }); - return propertySources; - } - - private List> loadPropertySources(String propertySourceName, - Resource resource, - PropertySourceLoader propertySourceLoader) { - logConfigLocation(resource); - if (resource.exists()) { - try { - return propertySourceLoader.load(propertySourceName, resource); - } catch (IOException e) { - throw Exceptions.propagate(e); - } - } - return List.of(); - } - - private void logConfigLocation(Resource resource) { - if (log.isDebugEnabled()) { - log.debug("Loading property sources from {}", resource); - } - } -} diff --git a/application/src/main/java/run/halo/app/plugin/PluginAutoConfiguration.java b/application/src/main/java/run/halo/app/plugin/PluginAutoConfiguration.java index 6bf8c85a5e..6a53e0507a 100644 --- a/application/src/main/java/run/halo/app/plugin/PluginAutoConfiguration.java +++ b/application/src/main/java/run/halo/app/plugin/PluginAutoConfiguration.java @@ -8,6 +8,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.time.Instant; +import java.util.Objects; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.pf4j.CompoundPluginLoader; @@ -25,6 +26,7 @@ import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.autoconfigure.web.WebProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.reactive.accept.RequestedContentTypeResolver; @@ -70,18 +72,12 @@ public PluginRequestMappingHandlerMapping pluginRequestMappingHandlerMapping() { } @Bean - public PluginRequestMappingManager pluginRequestMappingManager() { - return new PluginRequestMappingManager(pluginRequestMappingHandlerMapping()); - } - - @Bean - public HaloPluginManager pluginManager() { + public PluginManager pluginManager(ApplicationContext context) { // Setup RuntimeMode System.setProperty("pf4j.mode", pluginProperties.getRuntimeMode().toString()); // Setup Plugin folder - String pluginsRoot = - StringUtils.defaultString(pluginProperties.getPluginsRoot(), "plugins"); + String pluginsRoot = Objects.toString(pluginProperties.getPluginsRoot(), "plugins"); System.setProperty("pf4j.pluginsDir", pluginsRoot); String appHome = System.getProperty("app.home"); @@ -90,49 +86,48 @@ public HaloPluginManager pluginManager() { System.setProperty("pf4j.pluginsDir", appHome + File.separator + pluginsRoot); } - HaloPluginManager pluginManager = - new HaloPluginManager(new File(pluginsRoot).toPath()) { - @Override - protected PluginLoader createPluginLoader() { - if (pluginProperties.getCustomPluginLoader() != null) { - Class clazz = pluginProperties.getCustomPluginLoader(); - try { - Constructor constructor = clazz.getConstructor(PluginManager.class); - return (PluginLoader) constructor.newInstance(this); - } catch (Exception ex) { - throw new IllegalArgumentException( - String.format("Create custom PluginLoader %s failed. Make sure" - + "there is a constructor with one argument that accepts " - + "PluginLoader", - clazz.getName())); - } - } else { - return new CompoundPluginLoader() - .add(createDevelopmentPluginLoader(this), this::isDevelopment) - .add(new JarPluginLoader(this)); + var pluginManager = new HaloPluginManager(new File(pluginsRoot).toPath(), context) { + @Override + protected PluginLoader createPluginLoader() { + if (pluginProperties.getCustomPluginLoader() != null) { + Class clazz = pluginProperties.getCustomPluginLoader(); + try { + Constructor constructor = clazz.getConstructor(PluginManager.class); + return (PluginLoader) constructor.newInstance(this); + } catch (Exception ex) { + throw new IllegalArgumentException( + String.format("Create custom PluginLoader %s failed. Make sure" + + "there is a constructor with one argument that accepts " + + "PluginLoader", + clazz.getName())); } + } else { + return new CompoundPluginLoader() + .add(createDevelopmentPluginLoader(this), this::isDevelopment) + .add(new JarPluginLoader(this)); } + } - @Override - protected PluginStatusProvider createPluginStatusProvider() { - if (PropertyPluginStatusProvider.isPropertySet(pluginProperties)) { - return new PropertyPluginStatusProvider(pluginProperties); - } - return super.createPluginStatusProvider(); + @Override + protected PluginStatusProvider createPluginStatusProvider() { + if (PropertyPluginStatusProvider.isPropertySet(pluginProperties)) { + return new PropertyPluginStatusProvider(pluginProperties); } + return super.createPluginStatusProvider(); + } - @Override - protected PluginRepository createPluginRepository() { - var developmentPluginRepository = - new DefaultDevelopmentPluginRepository(getPluginsRoots()); - developmentPluginRepository - .setFixedPaths(pluginProperties.getFixedPluginPath()); - return new CompoundPluginRepository() - .add(developmentPluginRepository, this::isDevelopment) - .add(new JarPluginRepository(getPluginsRoots())) - .add(new DefaultPluginRepository(getPluginsRoots())); - } - }; + @Override + protected PluginRepository createPluginRepository() { + var developmentPluginRepository = + new DefaultDevelopmentPluginRepository(getPluginsRoots()); + developmentPluginRepository + .setFixedPaths(pluginProperties.getFixedPluginPath()); + return new CompoundPluginRepository() + .add(developmentPluginRepository, this::isDevelopment) + .add(new JarPluginRepository(getPluginsRoots())) + .add(new DefaultPluginRepository(getPluginsRoots())); + } + }; pluginManager.setExactVersionAllowed(pluginProperties.isExactVersionAllowed()); // only for development mode diff --git a/application/src/main/java/run/halo/app/plugin/PluginBeforeStopSyncListener.java b/application/src/main/java/run/halo/app/plugin/PluginBeforeStopSyncListener.java index 06065fef20..af1f4bb7bc 100644 --- a/application/src/main/java/run/halo/app/plugin/PluginBeforeStopSyncListener.java +++ b/application/src/main/java/run/halo/app/plugin/PluginBeforeStopSyncListener.java @@ -26,12 +26,15 @@ public PluginBeforeStopSyncListener(ReactiveExtensionClient client) { @EventListener public Mono onApplicationEvent(@NonNull HaloPluginBeforeStopEvent event) { var pluginWrapper = event.getPlugin(); - ExtensionContextRegistry registry = ExtensionContextRegistry.getInstance(); - if (!registry.containsContext(pluginWrapper.getPluginId())) { + var p = pluginWrapper.getPlugin(); + if (!(p instanceof SpringPlugin springPlugin)) { return Mono.empty(); } - var pluginContext = registry.getByPluginId(pluginWrapper.getPluginId()); - return cleanUpPluginExtensionResources(pluginContext); + var applicationContext = springPlugin.getApplicationContext(); + if (!(applicationContext instanceof PluginApplicationContext pluginApplicationContext)) { + return Mono.empty(); + } + return cleanUpPluginExtensionResources(pluginApplicationContext); } private Mono cleanUpPluginExtensionResources(PluginApplicationContext context) { diff --git a/application/src/main/java/run/halo/app/plugin/PluginCompositeRouterFunction.java b/application/src/main/java/run/halo/app/plugin/PluginCompositeRouterFunction.java deleted file mode 100644 index d82b26c670..0000000000 --- a/application/src/main/java/run/halo/app/plugin/PluginCompositeRouterFunction.java +++ /dev/null @@ -1,70 +0,0 @@ -package run.halo.app.plugin; - -import static run.halo.app.plugin.ExtensionContextRegistry.getInstance; - -import com.google.common.collect.Iterables; -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; -import org.springframework.web.reactive.function.server.RouterFunction; -import org.springframework.web.reactive.function.server.RouterFunctions; -import org.springframework.web.reactive.function.server.ServerRequest; -import org.springframework.web.reactive.function.server.ServerResponse; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import run.halo.app.plugin.resources.ReverseProxyRouterFunctionRegistry; - -/** - * A composite {@link RouterFunction} implementation for plugin. - * - * @author guqing - * @since 2.0.0 - */ -@Component -public class PluginCompositeRouterFunction implements RouterFunction { - - private final ReverseProxyRouterFunctionRegistry reverseProxyRouterFunctionFactory; - - public PluginCompositeRouterFunction( - ReverseProxyRouterFunctionRegistry reverseProxyRouterFunctionFactory) { - this.reverseProxyRouterFunctionFactory = reverseProxyRouterFunctionFactory; - } - - @Override - @NonNull - public Mono> route(@NonNull ServerRequest request) { - return Flux.fromIterable(routerFunctions()) - .concatMap(routerFunction -> routerFunction.route(request)) - .next(); - } - - @Override - public void accept(@NonNull RouterFunctions.Visitor visitor) { - routerFunctions().forEach(routerFunction -> routerFunction.accept(visitor)); - } - - @SuppressWarnings("unchecked") - private Iterable> routerFunctions() { - 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(); - - return Iterables.concat(rawRouterFunctions, reverseProxies); - } finally { - getInstance().releaseReadLock(); - } - } -} diff --git a/application/src/main/java/run/halo/app/plugin/PluginControllerManager.java b/application/src/main/java/run/halo/app/plugin/PluginControllerManager.java index c50bef688e..672a568d24 100644 --- a/application/src/main/java/run/halo/app/plugin/PluginControllerManager.java +++ b/application/src/main/java/run/halo/app/plugin/PluginControllerManager.java @@ -1,58 +1,49 @@ package run.halo.app.plugin; -import java.util.Map; +import static org.springframework.core.ResolvableType.forClassWithGenerics; + import java.util.concurrent.ConcurrentHashMap; -import java.util.stream.Stream; +import org.springframework.context.event.ContextClosedEvent; +import org.springframework.context.event.ContextRefreshedEvent; import org.springframework.context.event.EventListener; -import org.springframework.core.ResolvableType; -import org.springframework.stereotype.Component; +import reactor.core.Disposable; import run.halo.app.extension.ExtensionClient; -import run.halo.app.extension.controller.ControllerManager; -import run.halo.app.extension.controller.DefaultControllerManager; +import run.halo.app.extension.controller.Controller; +import run.halo.app.extension.controller.ControllerBuilder; import run.halo.app.extension.controller.Reconciler; -import run.halo.app.extension.controller.Reconciler.Request; -import run.halo.app.plugin.event.HaloPluginBeforeStopEvent; -import run.halo.app.plugin.event.HaloPluginStartedEvent; -@Component public class PluginControllerManager { - private final Map controllerManagerMap; + private final ConcurrentHashMap controllers; private final ExtensionClient client; public PluginControllerManager(ExtensionClient client) { this.client = client; - controllerManagerMap = new ConcurrentHashMap<>(); + controllers = new ConcurrentHashMap<>(); } @EventListener - public void onPluginStarted(HaloPluginStartedEvent event) { - var plugin = event.getPlugin(); - - var controllerManager = controllerManagerMap.computeIfAbsent(plugin.getPluginId(), - id -> new DefaultControllerManager(client)); - - getReconcilers(plugin.getPluginId()) - .forEach(controllerManager::start); + public void onApplicationEvent(ContextRefreshedEvent event) { + event.getApplicationContext() + .>getBeanProvider( + forClassWithGenerics(Reconciler.class, Reconciler.Request.class)) + .orderedStream() + .forEach(this::start); } @EventListener - public void onPluginBeforeStop(HaloPluginBeforeStopEvent event) { - // remove controller manager - var plugin = event.getPlugin(); - var controllerManager = controllerManagerMap.remove(plugin.getPluginId()); - if (controllerManager != null) { - // stop all reconcilers - getReconcilers(plugin.getPluginId()) - .forEach(controllerManager::stop); - } + public void onApplicationEvent(ContextClosedEvent event) throws Exception { + controllers.values() + .forEach(Disposable::dispose); + controllers.clear(); } - private Stream> getReconcilers(String pluginId) { - var context = ExtensionContextRegistry.getInstance().getByPluginId(pluginId); - return context.>getBeanProvider( - ResolvableType.forClassWithGenerics(Reconciler.class, Request.class)) - .orderedStream(); + private void start(Reconciler reconciler) { + var builder = new ControllerBuilder(reconciler, client); + var controller = reconciler.setupWith(builder); + controllers.put(reconciler.getClass().getName(), controller); + controller.start(); } + } diff --git a/application/src/main/java/run/halo/app/plugin/PluginRequestMappingManager.java b/application/src/main/java/run/halo/app/plugin/PluginRequestMappingManager.java deleted file mode 100644 index 8b0bffffb6..0000000000 --- a/application/src/main/java/run/halo/app/plugin/PluginRequestMappingManager.java +++ /dev/null @@ -1,42 +0,0 @@ -package run.halo.app.plugin; - -import java.util.Collection; -import lombok.extern.slf4j.Slf4j; -import org.pf4j.PluginWrapper; -import org.springframework.context.support.GenericApplicationContext; -import org.springframework.stereotype.Controller; -import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerMapping; - -/** - * Plugin mapping manager. - * - * @author guqing - * @see RequestMappingHandlerMapping - */ -@Slf4j -public class PluginRequestMappingManager { - - private final PluginRequestMappingHandlerMapping requestMappingHandlerMapping; - - public PluginRequestMappingManager( - PluginRequestMappingHandlerMapping pluginRequestMappingHandlerMapping) { - this.requestMappingHandlerMapping = pluginRequestMappingHandlerMapping; - } - - public void registerHandlerMappings(PluginWrapper pluginWrapper) { - String pluginId = pluginWrapper.getPluginId(); - getControllerBeans(pluginId) - .forEach(handler -> - requestMappingHandlerMapping.registerHandlerMethods(pluginId, handler)); - } - - public void removeHandlerMappings(String pluginId) { - requestMappingHandlerMapping.unregister(pluginId); - } - - private Collection getControllerBeans(String pluginId) { - GenericApplicationContext pluginContext = - ExtensionContextRegistry.getInstance().getByPluginId(pluginId); - return pluginContext.getBeansWithAnnotation(Controller.class).values(); - } -} diff --git a/application/src/main/java/run/halo/app/plugin/PluginRouterFunctionRegistry.java b/application/src/main/java/run/halo/app/plugin/PluginRouterFunctionRegistry.java new file mode 100644 index 0000000000..070a738b64 --- /dev/null +++ b/application/src/main/java/run/halo/app/plugin/PluginRouterFunctionRegistry.java @@ -0,0 +1,12 @@ +package run.halo.app.plugin; + +import java.util.Collection; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.ServerResponse; + +public interface PluginRouterFunctionRegistry { + void register(Collection> routerFunctions); + + void unregister(Collection> routerFunctions); + +} diff --git a/application/src/main/java/run/halo/app/plugin/PluginStartedListener.java b/application/src/main/java/run/halo/app/plugin/PluginStartedListener.java index 0697027a91..791f04f7d4 100644 --- a/application/src/main/java/run/halo/app/plugin/PluginStartedListener.java +++ b/application/src/main/java/run/halo/app/plugin/PluginStartedListener.java @@ -45,9 +45,14 @@ private Mono createOrUpdate(Unstructured unstructured) { @EventListener public Mono onApplicationEvent(HaloPluginStartedEvent event) { var pluginWrapper = event.getPlugin(); - var pluginApplicationContext = ExtensionContextRegistry.getInstance() - .getByPluginId(pluginWrapper.getPluginId()); - + var p = pluginWrapper.getPlugin(); + if (!(p instanceof SpringPlugin springPlugin)) { + return Mono.empty(); + } + var applicationContext = springPlugin.getApplicationContext(); + if (!(applicationContext instanceof PluginApplicationContext pluginApplicationContext)) { + return Mono.empty(); + } var pluginName = pluginWrapper.getPluginId(); return client.get(Plugin.class, pluginName) diff --git a/application/src/main/java/run/halo/app/plugin/SharedApplicationContextFactory.java b/application/src/main/java/run/halo/app/plugin/SharedApplicationContextFactory.java new file mode 100644 index 0000000000..24ae29e2c8 --- /dev/null +++ b/application/src/main/java/run/halo/app/plugin/SharedApplicationContextFactory.java @@ -0,0 +1,60 @@ +package run.halo.app.plugin; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.security.web.server.context.ServerSecurityContextRepository; +import run.halo.app.core.extension.service.AttachmentService; +import run.halo.app.extension.DefaultSchemeManager; +import run.halo.app.extension.ExtensionClient; +import run.halo.app.extension.ReactiveExtensionClient; +import run.halo.app.infra.BackupRootGetter; +import run.halo.app.infra.ExternalLinkProcessor; +import run.halo.app.infra.ExternalUrlSupplier; +import run.halo.app.notification.NotificationCenter; +import run.halo.app.notification.NotificationReasonEmitter; + +/** + * Utility for creating shared application context. + * + * @author guqing + * @author johnniang + * @since 2.12.0 + */ +public enum SharedApplicationContextFactory { + ; + + public static ApplicationContext create(ApplicationContext rootContext) { + // TODO Optimize creation timing + var sharedContext = new GenericApplicationContext(); + + var beanFactory = sharedContext.getBeanFactory(); + + // register shared object here + var extensionClient = rootContext.getBean(ExtensionClient.class); + var reactiveExtensionClient = rootContext.getBean(ReactiveExtensionClient.class); + beanFactory.registerSingleton("extensionClient", extensionClient); + beanFactory.registerSingleton("reactiveExtensionClient", reactiveExtensionClient); + + DefaultSchemeManager defaultSchemeManager = + rootContext.getBean(DefaultSchemeManager.class); + beanFactory.registerSingleton("schemeManager", defaultSchemeManager); + beanFactory.registerSingleton("externalUrlSupplier", + rootContext.getBean(ExternalUrlSupplier.class)); + beanFactory.registerSingleton("serverSecurityContextRepository", + rootContext.getBean(ServerSecurityContextRepository.class)); + beanFactory.registerSingleton("attachmentService", + rootContext.getBean(AttachmentService.class)); + beanFactory.registerSingleton("backupRootGetter", + rootContext.getBean(BackupRootGetter.class)); + beanFactory.registerSingleton("notificationReasonEmitter", + rootContext.getBean(NotificationReasonEmitter.class)); + beanFactory.registerSingleton("notificationCenter", + rootContext.getBean(NotificationCenter.class)); + beanFactory.registerSingleton("externalLinkProcessor", + rootContext.getBean(ExternalLinkProcessor.class)); + // TODO add more shared instance here + + sharedContext.refresh(); + return sharedContext; + } +} diff --git a/application/src/main/java/run/halo/app/plugin/SharedApplicationContextHolder.java b/application/src/main/java/run/halo/app/plugin/SharedApplicationContextHolder.java deleted file mode 100644 index d0b0baec80..0000000000 --- a/application/src/main/java/run/halo/app/plugin/SharedApplicationContextHolder.java +++ /dev/null @@ -1,89 +0,0 @@ -package run.halo.app.plugin; - -import org.springframework.beans.factory.support.DefaultListableBeanFactory; -import org.springframework.context.ApplicationContext; -import org.springframework.security.web.server.context.ServerSecurityContextRepository; -import org.springframework.stereotype.Component; -import run.halo.app.core.extension.service.AttachmentService; -import run.halo.app.extension.DefaultSchemeManager; -import run.halo.app.extension.ExtensionClient; -import run.halo.app.extension.ReactiveExtensionClient; -import run.halo.app.infra.BackupRootGetter; -import run.halo.app.infra.ExternalLinkProcessor; -import run.halo.app.infra.ExternalUrlSupplier; -import run.halo.app.notification.NotificationCenter; -import run.halo.app.notification.NotificationReasonEmitter; - -/** - *

This {@link SharedApplicationContextHolder} class is used to hold a singleton instance of - * {@link SharedApplicationContext}.

- *

If sharedApplicationContext cache is null when calling the {@link #getInstance()} method, - * then it will call {@link #createSharedApplicationContext()} to create and cache it. Otherwise, - * it will be obtained directly.

- *

It is thread safe.

- * - * @author guqing - * @since 2.0.0 - */ -@Component -public class SharedApplicationContextHolder { - - private final ApplicationContext rootApplicationContext; - private volatile SharedApplicationContext sharedApplicationContext; - - public SharedApplicationContextHolder(ApplicationContext applicationContext) { - this.rootApplicationContext = applicationContext; - } - - /** - * Get singleton instance of {@link SharedApplicationContext}. - * - * @return a singleton instance of {@link SharedApplicationContext}. - */ - public SharedApplicationContext getInstance() { - if (this.sharedApplicationContext == null) { - synchronized (SharedApplicationContextHolder.class) { - if (this.sharedApplicationContext == null) { - this.sharedApplicationContext = createSharedApplicationContext(); - } - } - } - return this.sharedApplicationContext; - } - - SharedApplicationContext createSharedApplicationContext() { - // TODO Optimize creation timing - SharedApplicationContext sharedApplicationContext = new SharedApplicationContext(); - sharedApplicationContext.refresh(); - - DefaultListableBeanFactory beanFactory = - (DefaultListableBeanFactory) sharedApplicationContext.getBeanFactory(); - - // register shared object here - var extensionClient = rootApplicationContext.getBean(ExtensionClient.class); - var reactiveExtensionClient = rootApplicationContext.getBean(ReactiveExtensionClient.class); - beanFactory.registerSingleton("extensionClient", extensionClient); - beanFactory.registerSingleton("reactiveExtensionClient", reactiveExtensionClient); - - DefaultSchemeManager defaultSchemeManager = - rootApplicationContext.getBean(DefaultSchemeManager.class); - beanFactory.registerSingleton("schemeManager", defaultSchemeManager); - beanFactory.registerSingleton("externalUrlSupplier", - rootApplicationContext.getBean(ExternalUrlSupplier.class)); - beanFactory.registerSingleton("serverSecurityContextRepository", - rootApplicationContext.getBean(ServerSecurityContextRepository.class)); - beanFactory.registerSingleton("attachmentService", - rootApplicationContext.getBean(AttachmentService.class)); - beanFactory.registerSingleton("backupRootGetter", - rootApplicationContext.getBean(BackupRootGetter.class)); - beanFactory.registerSingleton("notificationReasonEmitter", - rootApplicationContext.getBean(NotificationReasonEmitter.class)); - beanFactory.registerSingleton("notificationCenter", - rootApplicationContext.getBean(NotificationCenter.class)); - beanFactory.registerSingleton("externalLinkProcessor", - rootApplicationContext.getBean(ExternalLinkProcessor.class)); - // TODO add more shared instance here - - return sharedApplicationContext; - } -} diff --git a/application/src/main/java/run/halo/app/plugin/SpringComponentsFinder.java b/application/src/main/java/run/halo/app/plugin/SpringComponentsFinder.java index 66ba0a0cd4..afd3c0e641 100644 --- a/application/src/main/java/run/halo/app/plugin/SpringComponentsFinder.java +++ b/application/src/main/java/run/halo/app/plugin/SpringComponentsFinder.java @@ -6,7 +6,6 @@ import java.io.Reader; import java.nio.charset.StandardCharsets; import java.util.Collections; -import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; @@ -15,10 +14,7 @@ import java.util.concurrent.locks.StampedLock; import lombok.extern.slf4j.Slf4j; import org.pf4j.AbstractExtensionFinder; -import org.pf4j.PluginDependency; import org.pf4j.PluginManager; -import org.pf4j.PluginState; -import org.pf4j.PluginStateEvent; import org.pf4j.PluginWrapper; import org.pf4j.processor.ExtensionStorage; import org.springframework.util.Assert; @@ -49,31 +45,39 @@ public Map> readClasspathStorages() { @Override public Map> readPluginsStorages() { + // We have to copy the source code from `org.pf4j.LegacyExtensionFinder.readPluginsStorages` + // because we have to adapt to the new extensions resource location + // `META-INF/plugin-components.idx`. log.debug("Reading components storages from plugins"); Map> result = new LinkedHashMap<>(); List plugins = pluginManager.getPlugins(); for (PluginWrapper plugin : plugins) { - readPluginStorageToMemory(plugin); - } - - return result; - } + String pluginId = plugin.getDescriptor().getPluginId(); + log.debug("Reading extensions storage from plugin '{}'", pluginId); + Set bucket = new HashSet<>(); - @Override - public void pluginStateChanged(PluginStateEvent event) { - // see supper class for more details - if (checkForExtensionDependencies == null && PluginState.STARTED.equals( - event.getPluginState())) { - for (PluginDependency dependency : event.getPlugin().getDescriptor() - .getDependencies()) { - if (dependency.isOptional()) { - log.debug("Enable check for extension dependencies via ASM."); - checkForExtensionDependencies = true; - break; + try { + log.debug("Read '{}'", EXTENSIONS_RESOURCE); + ClassLoader pluginClassLoader = plugin.getPluginClassLoader(); + try (var resourceStream = + pluginClassLoader.getResourceAsStream(EXTENSIONS_RESOURCE)) { + if (resourceStream == null) { + log.debug("Cannot find '{}'", EXTENSIONS_RESOURCE); + } else { + collectExtensions(resourceStream, bucket); + } } + + debugExtensions(bucket); + + result.put(pluginId, bucket); + } catch (IOException e) { + log.error("Failed to read components from " + EXTENSIONS_RESOURCE, e); } } + + return result; } private void collectExtensions(InputStream inputStream, Set bucket) throws IOException { @@ -82,34 +86,6 @@ private void collectExtensions(InputStream inputStream, Set bucket) thro } } - protected void readPluginStorageToMemory(PluginWrapper pluginWrapper) { - String pluginId = pluginWrapper.getPluginId(); - if (containsComponentsStorage(pluginId)) { - return; - } - log.debug("Reading components storage from plugin '{}'", pluginId); - Set bucket = new HashSet<>(); - - try { - log.debug("Read '{}'", EXTENSIONS_RESOURCE); - ClassLoader pluginClassLoader = pluginWrapper.getPluginClassLoader(); - try (InputStream resourceStream = pluginClassLoader.getResourceAsStream( - EXTENSIONS_RESOURCE)) { - if (resourceStream == null) { - log.debug("Cannot find '{}'", EXTENSIONS_RESOURCE); - } else { - collectExtensions(resourceStream, bucket); - } - } - - debugExtensions(bucket); - - putComponentsStorage(pluginId, bucket); - } catch (IOException e) { - log.error(e.getMessage(), e); - } - } - protected boolean containsComponentsStorage(String pluginId) { Assert.notNull(pluginId, "The pluginId cannot be null"); long stamp = entryStampedLock.tryOptimisticRead(); @@ -125,42 +101,5 @@ protected boolean containsComponentsStorage(String pluginId) { return contains; } - protected void putComponentsStorage(String pluginId, Set components) { - Assert.notNull(pluginId, "The pluginId cannot be null"); - // When the lock remains in write mode, the read lock cannot be obtained - long stamp = entryStampedLock.writeLock(); - try { - Map> componentNamesMap; - if (super.entries == null) { - componentNamesMap = new HashMap<>(); - } else { - componentNamesMap = new HashMap<>(super.entries); - } - log.debug("Load [{}] component names into storage cache for plugin [{}].", - components.size(), pluginId); - componentNamesMap.put(pluginId, components); - super.entries = componentNamesMap; - } finally { - entryStampedLock.unlockWrite(stamp); - } - } - - protected void removeComponentsStorage(String pluginId) { - Assert.notNull(pluginId, "The pluginId cannot be null"); - long stamp = entryStampedLock.writeLock(); - try { - Map> componentNamesMap; - if (super.entries == null) { - componentNamesMap = new HashMap<>(); - } else { - componentNamesMap = new HashMap<>(super.entries); - } - log.debug("Removing components storage from cache [{}].", pluginId); - componentNamesMap.remove(pluginId); - super.entries = componentNamesMap; - } finally { - entryStampedLock.unlockWrite(stamp); - } - } } diff --git a/application/src/main/java/run/halo/app/plugin/SpringExtensionFactory.java b/application/src/main/java/run/halo/app/plugin/SpringExtensionFactory.java index 3d6ad7f966..dc769d8296 100644 --- a/application/src/main/java/run/halo/app/plugin/SpringExtensionFactory.java +++ b/application/src/main/java/run/halo/app/plugin/SpringExtensionFactory.java @@ -3,7 +3,6 @@ import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.util.Comparator; -import java.util.Objects; import java.util.Optional; import java.util.stream.Stream; import lombok.RequiredArgsConstructor; @@ -11,7 +10,6 @@ import org.pf4j.Extension; import org.pf4j.ExtensionFactory; import org.pf4j.PluginManager; -import org.pf4j.PluginRuntimeException; import org.pf4j.PluginWrapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.config.AutowireCapableBeanFactory; @@ -65,16 +63,9 @@ public class SpringExtensionFactory implements ExtensionFactory { @Override @Nullable public T create(Class extensionClass) { - Optional contextOptional = - getPluginApplicationContextBy(extensionClass); - if (contextOptional.isPresent()) { - // When the plugin starts, the class has been loaded into the plugin application - // context, - // so you only need to get it directly - PluginApplicationContext pluginApplicationContext = contextOptional.get(); - return pluginApplicationContext.getBean(extensionClass); - } - return createWithoutSpring(extensionClass); + return getPluginApplicationContextBy(extensionClass) + .map(context -> context.getBean(extensionClass)) + .orElseGet(() -> createWithoutSpring(extensionClass)); } /** @@ -127,47 +118,13 @@ private Object[] nullParameters(final Constructor constructor) { return new Object[constructor.getParameterCount()]; } - protected Optional getPluginApplicationContextBy( + protected Optional getPluginApplicationContextBy( final Class extensionClass) { return Optional.ofNullable(this.pluginManager.whichPlugin(extensionClass)) .map(PluginWrapper::getPlugin) - .map(plugin -> { - if (plugin instanceof BasePlugin basePlugin) { - return basePlugin; - } - throw new PluginRuntimeException( - "The plugin must be an instance of BasePlugin"); - }) - .map(plugin -> { - var pluginName = plugin.getContext().getName(); - if (this.pluginManager instanceof HaloPluginManager haloPluginManager) { - if (log.isTraceEnabled()) { - log.trace(" Extension class ' " + nameOf(extensionClass) - + "' belongs to a non halo-plugin (or main application)" - + " '" + nameOf(plugin) - + ", but the used Halo plugin-manager is a spring-plugin-manager. " - + "Therefore" - + " the extension class will be autowired by using the managers " - + "application " - + "contexts"); - } - return haloPluginManager.getPluginApplicationContext(pluginName); - } - if (log.isTraceEnabled()) { - log.trace( - " Extension class ' " + nameOf(extensionClass) - + "' belongs to halo-plugin '" - + nameOf(plugin) - + "' and will be autowired by using its application context."); - } - return ExtensionContextRegistry.getInstance().getByPluginId(pluginName); - }); - } - - private String nameOf(final BasePlugin plugin) { - return Objects.nonNull(plugin) - ? plugin.getContext().getName() - : "system"; + .filter(SpringPlugin.class::isInstance) + .map(plugin -> (SpringPlugin) plugin) + .map(SpringPlugin::getApplicationContext); } private String nameOf(final Class clazz) { diff --git a/application/src/main/java/run/halo/app/plugin/SpringPlugin.java b/application/src/main/java/run/halo/app/plugin/SpringPlugin.java new file mode 100644 index 0000000000..9e4db0dca2 --- /dev/null +++ b/application/src/main/java/run/halo/app/plugin/SpringPlugin.java @@ -0,0 +1,65 @@ +package run.halo.app.plugin; + +import org.pf4j.Plugin; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; +import run.halo.app.PluginApplicationContextFactory; + +public class SpringPlugin extends Plugin { + + private ApplicationContext context; + + private Plugin delegate; + + private final PluginApplicationContextFactory contextFactory; + + private final PluginContext pluginContext; + + public SpringPlugin(PluginApplicationContextFactory contextFactory, + PluginContext pluginContext) { + this.contextFactory = contextFactory; + this.pluginContext = pluginContext; + } + + @Override + public void start() { + // initialize context + var pluginId = pluginContext.getName(); + this.context = contextFactory.create(pluginId); + + var pluginOpt = context.getBeanProvider(Plugin.class) + .stream() + .findFirst(); + if (pluginOpt.isPresent()) { + this.delegate = pluginOpt.get(); + if (this.delegate instanceof BasePlugin basePlugin) { + basePlugin.setContext(pluginContext); + } + this.delegate.start(); + } + } + + @Override + public void stop() { + if (this.delegate != null) { + this.delegate.stop(); + } + if (context instanceof ConfigurableApplicationContext configurableContext) { + configurableContext.close(); + } + // reset application context + context = null; + } + + @Override + public void delete() { + if (delegate != null) { + delegate.delete(); + } + } + + public ApplicationContext getApplicationContext() { + return context; + } + +} diff --git a/application/src/main/java/run/halo/app/plugin/SpringPluginFactory.java b/application/src/main/java/run/halo/app/plugin/SpringPluginFactory.java new file mode 100644 index 0000000000..0a942e5eda --- /dev/null +++ b/application/src/main/java/run/halo/app/plugin/SpringPluginFactory.java @@ -0,0 +1,39 @@ +package run.halo.app.plugin; + +import lombok.extern.slf4j.Slf4j; +import org.pf4j.Plugin; +import org.pf4j.PluginFactory; +import org.pf4j.PluginWrapper; +import run.halo.app.PluginApplicationContextFactory; + +/** + * The default implementation for PluginFactory. + *

Get a {@link BasePlugin} instance from the {@link PluginApplicationContext}.

+ * + * @author guqing + * @since 2.0.0 + */ +@Slf4j +public class SpringPluginFactory implements PluginFactory { + + + private final PluginApplicationContextFactory contextFactory; + + public SpringPluginFactory(PluginApplicationContextFactory contextFactory) { + this.contextFactory = contextFactory; + } + + @Override + public Plugin create(PluginWrapper pluginWrapper) { + var pluginContext = new PluginContext( + pluginWrapper.getPluginId(), + pluginWrapper.getDescriptor().getVersion(), + pluginWrapper.getRuntimeMode() + ); + return new SpringPlugin( + contextFactory, + pluginContext + ); + } + +} diff --git a/application/src/main/java/run/halo/app/plugin/SpringPluginManager.java b/application/src/main/java/run/halo/app/plugin/SpringPluginManager.java new file mode 100644 index 0000000000..c730a3e66a --- /dev/null +++ b/application/src/main/java/run/halo/app/plugin/SpringPluginManager.java @@ -0,0 +1,12 @@ +package run.halo.app.plugin; + +import org.pf4j.PluginManager; +import org.springframework.context.ApplicationContext; + +public interface SpringPluginManager extends PluginManager { + + ApplicationContext getRootContext(); + + ApplicationContext getSharedContext(); + +} diff --git a/application/src/main/java/run/halo/app/plugin/extensionpoint/DefaultExtensionGetter.java b/application/src/main/java/run/halo/app/plugin/extensionpoint/DefaultExtensionGetter.java index 2c7c7e98d1..a0fad45575 100644 --- a/application/src/main/java/run/halo/app/plugin/extensionpoint/DefaultExtensionGetter.java +++ b/application/src/main/java/run/halo/app/plugin/extensionpoint/DefaultExtensionGetter.java @@ -6,6 +6,7 @@ import java.util.stream.Stream; import lombok.RequiredArgsConstructor; import org.pf4j.ExtensionPoint; +import org.pf4j.PluginManager; import org.springframework.context.ApplicationContext; import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.lang.NonNull; @@ -15,7 +16,6 @@ import run.halo.app.extension.ReactiveExtensionClient; import run.halo.app.infra.SystemConfigurableEnvironmentFetcher; import run.halo.app.infra.SystemSetting.ExtensionPointEnabled; -import run.halo.app.plugin.HaloPluginManager; @Component @RequiredArgsConstructor @@ -23,7 +23,7 @@ public class DefaultExtensionGetter implements ExtensionGetter { private final SystemConfigurableEnvironmentFetcher systemConfigFetcher; - private final HaloPluginManager pluginManager; + private final PluginManager pluginManager; private final ApplicationContext applicationContext; diff --git a/application/src/main/java/run/halo/app/plugin/resources/ReverseProxyRouterFunctionFactory.java b/application/src/main/java/run/halo/app/plugin/resources/ReverseProxyRouterFunctionFactory.java index 39776f1147..907c8eadf4 100644 --- a/application/src/main/java/run/halo/app/plugin/resources/ReverseProxyRouterFunctionFactory.java +++ b/application/src/main/java/run/halo/app/plugin/resources/ReverseProxyRouterFunctionFactory.java @@ -4,15 +4,18 @@ import static org.springframework.web.reactive.function.server.RequestPredicates.GET; import static org.springframework.web.reactive.function.server.RequestPredicates.accept; +import java.util.List; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; +import org.pf4j.PluginManager; import org.springframework.context.ApplicationContext; import org.springframework.core.io.DefaultResourceLoader; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; import org.springframework.http.server.PathContainer; import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; import org.springframework.stereotype.Component; import org.springframework.util.Assert; import org.springframework.web.reactive.function.server.RouterFunction; @@ -20,15 +23,11 @@ import org.springframework.web.reactive.function.server.ServerRequest; import org.springframework.web.reactive.function.server.ServerResponse; import org.springframework.web.util.pattern.PathPatternParser; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; import run.halo.app.core.extension.ReverseProxy; import run.halo.app.core.extension.ReverseProxy.FileReverseProxyProvider; import run.halo.app.core.extension.ReverseProxy.ReverseProxyRule; import run.halo.app.infra.exception.NotFoundException; import run.halo.app.infra.utils.PathUtils; -import run.halo.app.plugin.ExtensionContextRegistry; -import run.halo.app.plugin.HaloPluginManager; import run.halo.app.plugin.PluginConst; /** @@ -44,7 +43,7 @@ @AllArgsConstructor public class ReverseProxyRouterFunctionFactory { - private final HaloPluginManager haloPluginManager; + private final PluginManager pluginManager; private final ApplicationContext applicationContext; /** @@ -56,18 +55,17 @@ public class ReverseProxyRouterFunctionFactory { * @param pluginName plugin name(nullable if system) * @return A reverse proxy RouterFunction handle(nullable) */ - @NonNull - public Mono> create(ReverseProxy reverseProxy, - String pluginName) { + @Nullable + public RouterFunction create(ReverseProxy reverseProxy, String pluginName) { return createReverseProxyRouterFunction(reverseProxy, nullSafePluginName(pluginName)); } - private Mono> createReverseProxyRouterFunction( + @Nullable + private RouterFunction createReverseProxyRouterFunction( ReverseProxy reverseProxy, @NonNull String pluginName) { Assert.notNull(reverseProxy, "The reverseProxy must not be null."); var rules = getReverseProxyRules(reverseProxy); - - return rules.map(rule -> { + return rules.stream().map(rule -> { String routePath = buildRoutePath(pluginName, rule); log.debug("Plugin [{}] registered reverse proxy route path [{}]", pluginName, routePath); @@ -81,15 +79,15 @@ private Mono> createReverseProxyRouterFunction( return ServerResponse.ok() .bodyValue(resource); }); - }).reduce(RouterFunction::and); + }).reduce(RouterFunction::and).orElse(null); } private String nullSafePluginName(String pluginName) { return pluginName == null ? PluginConst.SYSTEM_PLUGIN_NAME : pluginName; } - private Flux getReverseProxyRules(ReverseProxy reverseProxy) { - return Flux.fromIterable(reverseProxy.getRules()); + private List getReverseProxyRules(ReverseProxy reverseProxy) { + return reverseProxy.getRules(); } public static String buildRoutePath(String pluginId, ReverseProxyRule reverseProxyRule) { @@ -137,15 +135,11 @@ private Resource loadResourceByFileRule(String pluginName, ReverseProxyRule rule } private ResourceLoader getResourceLoader(String pluginName) { - ExtensionContextRegistry registry = ExtensionContextRegistry.getInstance(); - if (registry.containsContext(pluginName)) { - return registry.getByPluginId(pluginName); - } if (PluginConst.SYSTEM_PLUGIN_NAME.equals(pluginName)) { return applicationContext; } DefaultResourceLoader resourceLoader = - BundleResourceUtils.getResourceLoader(haloPluginManager, pluginName); + BundleResourceUtils.getResourceLoader(pluginManager, pluginName); if (resourceLoader == null) { throw new NotFoundException("Plugin [" + pluginName + "] not found."); } diff --git a/application/src/main/java/run/halo/app/plugin/resources/ReverseProxyRouterFunctionRegistry.java b/application/src/main/java/run/halo/app/plugin/resources/ReverseProxyRouterFunctionRegistry.java index 062db66fe2..41d87c7683 100644 --- a/application/src/main/java/run/halo/app/plugin/resources/ReverseProxyRouterFunctionRegistry.java +++ b/application/src/main/java/run/halo/app/plugin/resources/ReverseProxyRouterFunctionRegistry.java @@ -2,7 +2,6 @@ import com.google.common.collect.LinkedHashMultimap; import java.util.HashMap; -import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.locks.StampedLock; @@ -10,8 +9,8 @@ import org.springframework.util.Assert; import org.springframework.web.reactive.function.server.RouterFunction; import org.springframework.web.reactive.function.server.ServerResponse; -import reactor.core.publisher.Mono; import run.halo.app.core.extension.ReverseProxy; +import run.halo.app.plugin.PluginRouterFunctionRegistry; /** * A registry for {@link RouterFunction} of plugin. @@ -21,6 +20,9 @@ */ @Component public class ReverseProxyRouterFunctionRegistry { + + private final PluginRouterFunctionRegistry pluginRouterFunctionRegistry; + private final ReverseProxyRouterFunctionFactory reverseProxyRouterFunctionFactory; private final StampedLock lock = new StampedLock(); private final Map> proxyNameRouterFunctionRegistry = @@ -29,7 +31,9 @@ public class ReverseProxyRouterFunctionRegistry { LinkedHashMultimap.create(); public ReverseProxyRouterFunctionRegistry( + PluginRouterFunctionRegistry pluginRouterFunctionRegistry, ReverseProxyRouterFunctionFactory reverseProxyRouterFunctionFactory) { + this.pluginRouterFunctionRegistry = pluginRouterFunctionRegistry; this.reverseProxyRouterFunctionFactory = reverseProxyRouterFunctionFactory; } @@ -38,20 +42,18 @@ public ReverseProxyRouterFunctionRegistry( * * @param pluginId plugin id * @param reverseProxy reverse proxy - * @return a mono */ - public Mono register(String pluginId, ReverseProxy reverseProxy) { + public void register(String pluginId, ReverseProxy reverseProxy) { Assert.notNull(pluginId, "The plugin id must not be null."); final String proxyName = reverseProxy.getMetadata().getName(); long stamp = lock.writeLock(); try { pluginIdReverseProxyMap.put(pluginId, proxyName); - return reverseProxyRouterFunctionFactory.create(reverseProxy, pluginId) - .map(routerFunction -> { - proxyNameRouterFunctionRegistry.put(proxyName, routerFunction); - return routerFunction; - }) - .then(); + var routerFunction = reverseProxyRouterFunctionFactory.create(reverseProxy, pluginId); + if (routerFunction != null) { + proxyNameRouterFunctionRegistry.put(proxyName, routerFunction); + pluginRouterFunctionRegistry.register(Set.of(routerFunction)); + } } finally { lock.unlockWrite(stamp); } @@ -60,65 +62,25 @@ public Mono register(String pluginId, ReverseProxy reverseProxy) { /** * Only for test. */ - protected int reverseProxySize(String pluginId) { + int reverseProxySize(String pluginId) { Set names = pluginIdReverseProxyMap.get(pluginId); return names.size(); } - /** - * Remove reverse proxy router function by plugin id. - * - * @param pluginId plugin id - */ - public Mono remove(String pluginId) { - Assert.notNull(pluginId, "The plugin id must not be null."); - long stamp = lock.writeLock(); - try { - Set proxyNames = pluginIdReverseProxyMap.removeAll(pluginId); - for (String proxyName : proxyNames) { - proxyNameRouterFunctionRegistry.remove(proxyName); - } - return Mono.empty(); - } finally { - lock.unlockWrite(stamp); - } - } - /** * Remove reverse proxy router function by pluginId and reverse proxy name. */ - public Mono remove(String pluginId, String reverseProxyName) { + public void remove(String pluginId, String reverseProxyName) { long stamp = lock.writeLock(); try { pluginIdReverseProxyMap.remove(pluginId, reverseProxyName); - proxyNameRouterFunctionRegistry.remove(reverseProxyName); - return Mono.empty(); + var removedRouterFunction = proxyNameRouterFunctionRegistry.remove(reverseProxyName); + if (removedRouterFunction != null) { + pluginRouterFunctionRegistry.unregister(Set.of(removedRouterFunction)); + } } finally { lock.unlockWrite(stamp); } } - /** - * Gets reverse proxy {@link RouterFunction} by reverse proxy name. - */ - public RouterFunction getRouterFunction(String proxyName) { - long stamp = lock.readLock(); - try { - return proxyNameRouterFunctionRegistry.get(proxyName); - } finally { - lock.unlockRead(stamp); - } - } - - /** - * Gets all reverse proxy {@link RouterFunction}. - */ - public List> getRouterFunctions() { - long stamp = lock.readLock(); - try { - return List.copyOf(proxyNameRouterFunctionRegistry.values()); - } finally { - lock.unlockRead(stamp); - } - } } diff --git a/application/src/main/java/run/halo/app/theme/finders/FinderRegistry.java b/application/src/main/java/run/halo/app/theme/finders/FinderRegistry.java index 7c0b9c3502..7a368c5d22 100644 --- a/application/src/main/java/run/halo/app/theme/finders/FinderRegistry.java +++ b/application/src/main/java/run/halo/app/theme/finders/FinderRegistry.java @@ -8,8 +8,7 @@ import org.springframework.context.ApplicationContext; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Component; -import run.halo.app.plugin.ExtensionContextRegistry; -import run.halo.app.plugin.PluginApplicationContext; +import run.halo.app.plugin.SpringPlugin; import run.halo.app.plugin.event.HaloPluginStartedEvent; import run.halo.app.plugin.event.HaloPluginStoppedEvent; @@ -92,17 +91,19 @@ public void afterPropertiesSet() throws Exception { */ @EventListener(HaloPluginStartedEvent.class) public void onPluginStarted(HaloPluginStartedEvent event) { - String pluginId = event.getPlugin().getPluginId(); - PluginApplicationContext pluginApplicationContext = ExtensionContextRegistry.getInstance() - .getByPluginId(pluginId); - pluginApplicationContext.getBeansWithAnnotation(Finder.class) - .forEach((beanName, finderObject) -> { - // register finder - String finderName = registerFinder(finderObject); - // add to plugin finder lookup - pluginFindersLookup.computeIfAbsent(pluginId, k -> new ArrayList<>()) - .add(finderName); - }); + var plugin = event.getPlugin().getPlugin(); + var pluginId = event.getPlugin().getPluginId(); + if (plugin instanceof SpringPlugin springPlugin) { + var context = springPlugin.getApplicationContext(); + context.getBeansWithAnnotation(Finder.class) + .forEach((beanName, finderObject) -> { + // register finder + String finderName = registerFinder(finderObject); + // add to plugin finder lookup + pluginFindersLookup.computeIfAbsent(pluginId, k -> new ArrayList<>()) + .add(finderName); + }); + } } /** @@ -120,6 +121,20 @@ public void onPluginStopped(HaloPluginStoppedEvent event) { pluginFindersLookup.get(pluginId).forEach(this::removeFinder); } + public void register(String pluginId, ApplicationContext pluginContext) { + pluginContext.getBeansWithAnnotation(Finder.class) + .forEach((beanName, finder) -> { + var finderName = registerFinder(finder); + pluginFindersLookup.computeIfAbsent(pluginId, igored -> new ArrayList<>()) + .add(finderName); + }); + } + + public void unregister(String pluginId) { + pluginFindersLookup.getOrDefault(pluginId, List.of()) + .forEach(this::removeFinder); + } + /** * Only for test. * diff --git a/application/src/test/java/run/halo/app/core/extension/reconciler/ReverseProxyReconcilerTest.java b/application/src/test/java/run/halo/app/core/extension/reconciler/ReverseProxyReconcilerTest.java index e23319f4c4..0f88e2392d 100644 --- a/application/src/test/java/run/halo/app/core/extension/reconciler/ReverseProxyReconcilerTest.java +++ b/application/src/test/java/run/halo/app/core/extension/reconciler/ReverseProxyReconcilerTest.java @@ -3,6 +3,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -17,7 +18,6 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import reactor.core.publisher.Mono; import run.halo.app.core.extension.ReverseProxy; import run.halo.app.extension.ExtensionClient; import run.halo.app.extension.Metadata; @@ -54,7 +54,7 @@ void reconcileRemoval() { .setLabels(Map.of(PluginConst.PLUGIN_NAME_LABEL_NAME, "fake-plugin")); reverseProxy.setRules(List.of()); - when(routerFunctionRegistry.remove(anyString(), anyString())).thenReturn(Mono.empty()); + doNothing().when(routerFunctionRegistry).remove(anyString(), anyString()); when(client.fetch(ReverseProxy.class, "fake-reverse-proxy")) .thenReturn(Optional.of(reverseProxy)); @@ -62,7 +62,6 @@ void reconcileRemoval() { verify(routerFunctionRegistry, never()).register(anyString(), any(ReverseProxy.class)); - verify(routerFunctionRegistry, never()).remove(eq("fake-plugin")); verify(routerFunctionRegistry, times(1)) .remove(eq("fake-plugin"), eq("fake-reverse-proxy")); } diff --git a/application/src/test/java/run/halo/app/plugin/DefaultPluginRouterFunctionRegistryTest.java b/application/src/test/java/run/halo/app/plugin/DefaultPluginRouterFunctionRegistryTest.java new file mode 100644 index 0000000000..578c524e5f --- /dev/null +++ b/application/src/test/java/run/halo/app/plugin/DefaultPluginRouterFunctionRegistryTest.java @@ -0,0 +1,34 @@ +package run.halo.app.plugin; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; + +import java.util.Set; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.ServerResponse; + +/** + * Tests for {@link DefaultPluginRouterFunctionRegistry}. + * + * @author guqing + * @since 2.0.0 + */ +@ExtendWith(MockitoExtension.class) +class DefaultPluginRouterFunctionRegistryTest { + + @InjectMocks + DefaultPluginRouterFunctionRegistry routerFunctionRegistry; + + @Test + void shouldRegisterRouterFunction() { + RouterFunction routerFunction = mock(InvocationOnMock::getMock); + routerFunctionRegistry.register(Set.of(routerFunction)); + assertEquals(Set.of(routerFunction), routerFunctionRegistry.getRouterFunctions()); + } + +} diff --git a/application/src/test/java/run/halo/app/plugin/PluginCompositeRouterFunctionTest.java b/application/src/test/java/run/halo/app/plugin/PluginCompositeRouterFunctionTest.java deleted file mode 100644 index 3752c5d5d1..0000000000 --- a/application/src/test/java/run/halo/app/plugin/PluginCompositeRouterFunctionTest.java +++ /dev/null @@ -1,96 +0,0 @@ -package run.halo.app.plugin; - -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.util.List; -import java.util.stream.Stream; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.beans.factory.ObjectProvider; -import org.springframework.http.codec.ServerCodecConfigurer; -import org.springframework.mock.http.server.reactive.MockServerHttpRequest; -import org.springframework.mock.web.server.MockServerWebExchange; -import org.springframework.web.reactive.function.server.HandlerFunction; -import org.springframework.web.reactive.function.server.RouterFunction; -import org.springframework.web.reactive.function.server.ServerResponse; -import org.springframework.web.reactive.function.server.support.RouterFunctionMapping; -import org.springframework.web.server.ServerWebExchange; -import reactor.core.publisher.Mono; -import reactor.test.StepVerifier; -import run.halo.app.plugin.resources.ReverseProxyRouterFunctionRegistry; - -/** - * Tests for {@link PluginCompositeRouterFunction}. - * - * @author guqing - * @since 2.0.0 - */ -@ExtendWith(MockitoExtension.class) -class PluginCompositeRouterFunctionTest { - ServerCodecConfigurer codecConfigurer = ServerCodecConfigurer.create(); - - @Mock - ReverseProxyRouterFunctionRegistry reverseProxyRouterFunctionRegistry; - - @Mock - ObjectProvider rawRouterFunctionsProvider; - - @InjectMocks - PluginCompositeRouterFunction compositeRouterFunction; - - HandlerFunction handlerFunction; - - @BeforeEach - @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()); - - when(fakeContext.getBeanProvider(RouterFunction.class)) - .thenReturn(rawRouterFunctionsProvider); - - compositeRouterFunction = - new PluginCompositeRouterFunction(reverseProxyRouterFunctionRegistry); - - handlerFunction = request -> ServerResponse.ok().build(); - RouterFunction routerFunction = request -> Mono.just(handlerFunction); - - when(reverseProxyRouterFunctionRegistry.getRouterFunctions()) - .thenReturn(List.of(routerFunction)); - } - - @AfterEach - void cleanUp() { - ExtensionContextRegistry.getInstance().remove("fake-plugin"); - } - - @Test - void route() { - RouterFunctionMapping mapping = new RouterFunctionMapping(compositeRouterFunction); - mapping.setMessageReaders(this.codecConfigurer.getReaders()); - - Mono result = mapping.getHandler(createExchange("https://example.com/match")); - - StepVerifier.create(result) - .expectNext(handlerFunction) - .expectComplete() - .verify(); - - verify(rawRouterFunctionsProvider).orderedStream(); - } - - private ServerWebExchange createExchange(String urlTemplate) { - return MockServerWebExchange.from(MockServerHttpRequest.get(urlTemplate)); - } - -} diff --git a/application/src/test/java/run/halo/app/plugin/SharedApplicationContextFactoryTest.java b/application/src/test/java/run/halo/app/plugin/SharedApplicationContextFactoryTest.java new file mode 100644 index 0000000000..80518da34e --- /dev/null +++ b/application/src/test/java/run/halo/app/plugin/SharedApplicationContextFactoryTest.java @@ -0,0 +1,29 @@ +package run.halo.app.plugin; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.ApplicationContext; + +/** + * Tests for {@link SharedApplicationContextFactory}. + * + * @author guqing + * @since 2.0.0 + */ +@SpringBootTest +@AutoConfigureTestDatabase +class SharedApplicationContextFactoryTest { + + @Autowired + ApplicationContext applicationContext; + + @Test + void createSharedApplicationContext() { + var sharedContext = SharedApplicationContextFactory.create(applicationContext); + assertNotNull(sharedContext); + } +} \ No newline at end of file diff --git a/application/src/test/java/run/halo/app/plugin/SharedApplicationContextHolderTest.java b/application/src/test/java/run/halo/app/plugin/SharedApplicationContextHolderTest.java deleted file mode 100644 index 0bf05ef13f..0000000000 --- a/application/src/test/java/run/halo/app/plugin/SharedApplicationContextHolderTest.java +++ /dev/null @@ -1,37 +0,0 @@ -package run.halo.app.plugin; - -import static org.assertj.core.api.Assertions.assertThat; - -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; -import org.springframework.boot.test.context.SpringBootTest; - -/** - * Tests for {@link SharedApplicationContextHolder}. - * - * @author guqing - * @since 2.0.0 - */ -@SpringBootTest -@AutoConfigureTestDatabase -class SharedApplicationContextHolderTest { - - @Autowired - SharedApplicationContextHolder sharedApplicationContextHolder; - - @Test - void getInstance() { - SharedApplicationContext instance1 = sharedApplicationContextHolder.getInstance(); - SharedApplicationContext instance2 = sharedApplicationContextHolder.getInstance(); - assertThat(instance1).isNotNull(); - assertThat(instance1).isEqualTo(instance2); - } - - @Test - void createSharedApplicationContext() { - SharedApplicationContext sharedApplicationContext = - sharedApplicationContextHolder.createSharedApplicationContext(); - assertThat(sharedApplicationContext).isNotNull(); - } -} \ No newline at end of file diff --git a/application/src/test/java/run/halo/app/plugin/SpringComponentsFinderTest.java b/application/src/test/java/run/halo/app/plugin/SpringComponentsFinderTest.java index ec9a21d192..a0d5463335 100644 --- a/application/src/test/java/run/halo/app/plugin/SpringComponentsFinderTest.java +++ b/application/src/test/java/run/halo/app/plugin/SpringComponentsFinderTest.java @@ -3,15 +3,9 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; import java.io.File; -import java.io.FileInputStream; import java.io.FileNotFoundException; -import java.util.Set; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -47,28 +41,6 @@ void setUp() throws FileNotFoundException { testFile = ResourceUtils.getFile("classpath:plugin/test-plugin-components.idx"); } - @Test - void readPluginStorageToMemory() throws FileNotFoundException { - boolean contains = springComponentsFinder.containsComponentsStorage("fakePlugin"); - assertThat(contains).isFalse(); - - when(pluginWrapper.getPluginId()).thenReturn("fakePlugin"); - when(pluginWrapper.getPluginClassLoader()).thenReturn(pluginClassLoader); - when(pluginClassLoader.getResourceAsStream(any())) - .thenReturn(new FileInputStream(testFile)); - - springComponentsFinder.readPluginStorageToMemory(pluginWrapper); - - contains = springComponentsFinder.containsComponentsStorage("fakePlugin"); - assertThat(contains).isTrue(); - - verify(pluginClassLoader, times(1)).getResourceAsStream(any()); - - // repeat it - springComponentsFinder.readPluginStorageToMemory(pluginWrapper); - verify(pluginClassLoader, times(1)).getResourceAsStream(any()); - } - @Test void containsPlugin() { boolean exist = springComponentsFinder.containsComponentsStorage("NotExist"); @@ -78,15 +50,4 @@ void containsPlugin() { .hasMessage("The pluginId cannot be null"); } - @Test - void removeComponentsCache() { - springComponentsFinder.putComponentsStorage("fakePlugin", Set.of("A")); - boolean contains = springComponentsFinder.containsComponentsStorage("fakePlugin"); - assertThat(contains).isTrue(); - - springComponentsFinder.removeComponentsStorage("fakePlugin"); - - contains = springComponentsFinder.containsComponentsStorage("fakePlugin"); - assertThat(contains).isFalse(); - } } \ No newline at end of file diff --git a/application/src/test/java/run/halo/app/plugin/resources/ReverseProxyRouterFunctionFactoryTest.java b/application/src/test/java/run/halo/app/plugin/resources/ReverseProxyRouterFunctionFactoryTest.java index 46f080d98e..9cb294dee2 100644 --- a/application/src/test/java/run/halo/app/plugin/resources/ReverseProxyRouterFunctionFactoryTest.java +++ b/application/src/test/java/run/halo/app/plugin/resources/ReverseProxyRouterFunctionFactoryTest.java @@ -1,5 +1,7 @@ package run.halo.app.plugin.resources; +import static org.junit.jupiter.api.Assertions.assertNotNull; + import java.util.List; import java.util.Map; import org.junit.jupiter.api.Test; @@ -7,11 +9,10 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.pf4j.PluginManager; import org.springframework.context.ApplicationContext; -import reactor.test.StepVerifier; import run.halo.app.core.extension.ReverseProxy; import run.halo.app.extension.Metadata; -import run.halo.app.plugin.HaloPluginManager; import run.halo.app.plugin.PluginConst; /** @@ -24,7 +25,7 @@ class ReverseProxyRouterFunctionFactoryTest { @Mock - private HaloPluginManager haloPluginManager; + private PluginManager pluginManager; @Mock private ApplicationContext applicationContext; @@ -34,11 +35,8 @@ class ReverseProxyRouterFunctionFactoryTest { @Test void create() { - var routerFunction = - reverseProxyRouterFunctionFactory.create(mockReverseProxy(), "fakeA"); - StepVerifier.create(routerFunction) - .expectNextCount(1) - .verifyComplete(); + var routerFunction = reverseProxyRouterFunctionFactory.create(mockReverseProxy(), "fakeA"); + assertNotNull(routerFunction); } private ReverseProxy mockReverseProxy() { diff --git a/application/src/test/java/run/halo/app/plugin/resources/ReverseProxyRouterFunctionRegistryTest.java b/application/src/test/java/run/halo/app/plugin/resources/ReverseProxyRouterFunctionRegistryTest.java index 0b688c5702..4d9d007cbe 100644 --- a/application/src/test/java/run/halo/app/plugin/resources/ReverseProxyRouterFunctionRegistryTest.java +++ b/application/src/test/java/run/halo/app/plugin/resources/ReverseProxyRouterFunctionRegistryTest.java @@ -6,8 +6,6 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; @@ -17,11 +15,9 @@ import org.springframework.web.reactive.function.server.RouterFunction; import org.springframework.web.reactive.function.server.ServerResponse; import reactor.core.publisher.Mono; -import reactor.test.StepVerifier; import run.halo.app.core.extension.ReverseProxy; import run.halo.app.extension.Metadata; -import run.halo.app.plugin.ExtensionContextRegistry; -import run.halo.app.plugin.PluginApplicationContext; +import run.halo.app.plugin.PluginRouterFunctionRegistry; /** * Tests for {@link ReverseProxyRouterFunctionRegistry}. @@ -33,61 +29,35 @@ class ReverseProxyRouterFunctionRegistryTest { @InjectMocks - private ReverseProxyRouterFunctionRegistry registry; + ReverseProxyRouterFunctionRegistry registry; @Mock - private ReverseProxyRouterFunctionFactory reverseProxyRouterFunctionFactory; + ReverseProxyRouterFunctionFactory reverseProxyRouterFunctionFactory; - @BeforeEach - void setUp() { - ExtensionContextRegistry instance = ExtensionContextRegistry.getInstance(); - instance.register("fake-plugin", Mockito.mock(PluginApplicationContext.class)); - } - - @AfterEach - void tearDown() { - ExtensionContextRegistry.getInstance().remove("fake-plugin"); - } + @Mock + PluginRouterFunctionRegistry pluginRouterFunctionRegistry; @Test void register() { ReverseProxy mock = getMockReverseProxy(); - registry.register("fake-plugin", mock) - .as(StepVerifier::create) - .verifyComplete(); + registry.register("fake-plugin", mock); assertThat(registry.reverseProxySize("fake-plugin")).isEqualTo(1); // repeat register a same reverse proxy - registry.register("fake-plugin", mock) - .as(StepVerifier::create) - .verifyComplete(); + registry.register("fake-plugin", mock); assertThat(registry.reverseProxySize("fake-plugin")).isEqualTo(1); verify(reverseProxyRouterFunctionFactory, times(2)).create(any(), any()); } - @Test - void remove() { - ReverseProxy mock = getMockReverseProxy(); - registry.register("fake-plugin", mock) - .as(StepVerifier::create) - .verifyComplete(); - - registry.remove("fake-plugin").block(); - - assertThat(registry.reverseProxySize("fake-plugin")).isEqualTo(0); - } - @Test void removeByKeyValue() { ReverseProxy mock = getMockReverseProxy(); - registry.register("fake-plugin", mock) - .as(StepVerifier::create) - .verifyComplete(); + registry.register("fake-plugin", mock); - registry.remove("fake-plugin", "test-reverse-proxy").block(); + registry.remove("fake-plugin", "test-reverse-proxy"); assertThat(registry.reverseProxySize("fake-plugin")).isEqualTo(0); } @@ -100,7 +70,7 @@ private ReverseProxy getMockReverseProxy() { RouterFunction routerFunction = request -> Mono.empty(); when(reverseProxyRouterFunctionFactory.create(any(), any())) - .thenReturn(Mono.just(routerFunction)); + .thenReturn(routerFunction); return mock; } } \ No newline at end of file From 4e077fe8cdefe16f081f55736d832eb033ef6c23 Mon Sep 17 00:00:00 2001 From: John Niang Date: Thu, 25 Jan 2024 14:56:40 +0800 Subject: [PATCH 2/6] Refactor plugin event trigger Signed-off-by: John Niang --- .../reconciler/PluginReconciler.java | 4 +- .../infra/DefaultSystemVersionSupplier.java | 17 +- ...efaultPluginApplicationContextFactory.java | 68 ++++++- .../run/halo/app/plugin/DevPluginLoader.java | 43 ++++ .../halo/app/plugin/HaloPluginManager.java | 186 +++++++----------- .../app/plugin/PluginAutoConfiguration.java | 141 +------------ .../plugin/PluginCreatedEventReconciler.java | 107 ---------- .../plugin/PluginDevelopmentInitializer.java | 11 +- .../run/halo/app/plugin/PluginProperties.java | 5 - .../SharedApplicationContextFactory.java | 1 + .../run/halo/app/plugin/SpringPlugin.java | 11 ++ .../halo/app/plugin/SpringPluginFactory.java | 1 + .../app/plugin/event/PluginCreatedEvent.java | 21 -- .../event/SpringPluginStartedEvent.java | 18 ++ .../event/SpringPluginStartingEvent.java | 18 ++ .../event/SpringPluginStoppedEvent.java | 18 ++ .../event/SpringPluginStoppingEvent.java | 18 ++ .../DefaultSystemVersionSupplierTest.java | 21 +- 18 files changed, 301 insertions(+), 408 deletions(-) create mode 100644 application/src/main/java/run/halo/app/plugin/DevPluginLoader.java delete mode 100644 application/src/main/java/run/halo/app/plugin/PluginCreatedEventReconciler.java delete mode 100644 application/src/main/java/run/halo/app/plugin/event/PluginCreatedEvent.java create mode 100644 application/src/main/java/run/halo/app/plugin/event/SpringPluginStartedEvent.java create mode 100644 application/src/main/java/run/halo/app/plugin/event/SpringPluginStartingEvent.java create mode 100644 application/src/main/java/run/halo/app/plugin/event/SpringPluginStoppedEvent.java create mode 100644 application/src/main/java/run/halo/app/plugin/event/SpringPluginStoppingEvent.java diff --git a/application/src/main/java/run/halo/app/core/extension/reconciler/PluginReconciler.java b/application/src/main/java/run/halo/app/core/extension/reconciler/PluginReconciler.java index a8915b6a92..8f629886c1 100644 --- a/application/src/main/java/run/halo/app/core/extension/reconciler/PluginReconciler.java +++ b/application/src/main/java/run/halo/app/core/extension/reconciler/PluginReconciler.java @@ -262,8 +262,8 @@ private void resolveStaticResources(Plugin plugin) { var p = pluginManager.getPlugin(pluginName); var classLoader = p.getPluginClassLoader(); var resLoader = new DefaultResourceLoader(classLoader); - var entryRes = resLoader.getResource("classpath:/console/main.js"); - var cssRes = resLoader.getResource("classpath:/console/style.css"); + var entryRes = resLoader.getResource("classpath:console/main.js"); + var cssRes = resLoader.getResource("classpath:console/style.css"); if (entryRes.exists()) { var entry = UriComponentsBuilder.newInstance() .pathSegment("plugins", pluginName, "assets", "console", "main.js") diff --git a/application/src/main/java/run/halo/app/infra/DefaultSystemVersionSupplier.java b/application/src/main/java/run/halo/app/infra/DefaultSystemVersionSupplier.java index c29c34ab46..f91b48e732 100644 --- a/application/src/main/java/run/halo/app/infra/DefaultSystemVersionSupplier.java +++ b/application/src/main/java/run/halo/app/infra/DefaultSystemVersionSupplier.java @@ -1,10 +1,9 @@ package run.halo.app.infra; import com.github.zafarkhaja.semver.Version; -import org.apache.commons.lang3.StringUtils; -import org.springframework.beans.factory.annotation.Autowired; +import java.util.Objects; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.info.BuildProperties; -import org.springframework.lang.Nullable; import org.springframework.stereotype.Component; /** @@ -17,21 +16,19 @@ public class DefaultSystemVersionSupplier implements SystemVersionSupplier { private static final String DEFAULT_VERSION = "0.0.0"; - @Nullable - private BuildProperties buildProperties; + private final ObjectProvider buildProperties; - @Autowired(required = false) - public void setBuildProperties(@Nullable BuildProperties buildProperties) { + public DefaultSystemVersionSupplier(ObjectProvider buildProperties) { this.buildProperties = buildProperties; } @Override public Version get() { - if (buildProperties == null) { + var properties = buildProperties.getIfUnique(); + if (properties == null) { return Version.valueOf(DEFAULT_VERSION); } - String projectVersion = - StringUtils.defaultString(buildProperties.getVersion(), DEFAULT_VERSION); + var projectVersion = Objects.toString(properties.getVersion(), DEFAULT_VERSION); return Version.valueOf(projectVersion); } } diff --git a/application/src/main/java/run/halo/app/plugin/DefaultPluginApplicationContextFactory.java b/application/src/main/java/run/halo/app/plugin/DefaultPluginApplicationContextFactory.java index adb512645d..01c6dd6148 100644 --- a/application/src/main/java/run/halo/app/plugin/DefaultPluginApplicationContextFactory.java +++ b/application/src/main/java/run/halo/app/plugin/DefaultPluginApplicationContextFactory.java @@ -12,6 +12,7 @@ import org.springframework.boot.env.PropertySourceLoader; import org.springframework.boot.env.YamlPropertySourceLoader; import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationListener; import org.springframework.context.event.ContextClosedEvent; import org.springframework.context.event.ContextRefreshedEvent; import org.springframework.context.event.EventListener; @@ -27,6 +28,12 @@ import run.halo.app.PluginApplicationContextFactory; import run.halo.app.extension.ReactiveExtensionClient; import run.halo.app.infra.properties.HaloProperties; +import run.halo.app.plugin.event.HaloPluginBeforeStopEvent; +import run.halo.app.plugin.event.HaloPluginStartedEvent; +import run.halo.app.plugin.event.HaloPluginStoppedEvent; +import run.halo.app.plugin.event.SpringPluginStartedEvent; +import run.halo.app.plugin.event.SpringPluginStoppedEvent; +import run.halo.app.plugin.event.SpringPluginStoppingEvent; import run.halo.app.theme.DefaultTemplateNameResolver; import run.halo.app.theme.ViewNameResolver; import run.halo.app.theme.finders.FinderRegistry; @@ -41,7 +48,8 @@ public DefaultPluginApplicationContextFactory(SpringPluginManager pluginManager) } /** - * Create and refresh application context. + * Create and refresh application context. Make sure the plugin has already loaded + * before. * * @param pluginId plugin id * @return refresh application context for the plugin. @@ -52,6 +60,7 @@ public ApplicationContext create(String pluginId) { var pluginWrapper = pluginManager.getPlugin(pluginId); var context = new PluginApplicationContext(pluginId); + context.registerShutdownHook(); context.setParent(pluginManager.getSharedContext()); var classLoader = pluginWrapper.getPluginClassLoader(); @@ -103,6 +112,9 @@ public ApplicationContext create(String pluginId) { }); context.registerBean(PluginControllerManager.class); + beanFactory.registerSingleton("springPluginStoppedEventAdapter", + new SpringPluginStoppedEventAdapter(pluginId)); + beanFactory.registerSingleton("haloPluginEventBridge", new HaloPluginEventBridge()); rootContext.getBeanProvider(FinderRegistry.class) .ifAvailable(finderRegistry -> { @@ -139,7 +151,7 @@ private FinderManager(String pluginId, FinderRegistry finderRegistry) { } @EventListener - public void onApplicationEvent(ContextClosedEvent event) { + public void onApplicationEvent(ContextClosedEvent ignored) { this.finderRegistry.unregister(this.pluginId); } @@ -161,7 +173,7 @@ private PluginRouterFunctionManager(PluginRouterFunctionRegistry routerFunctionR } @EventListener - public void onApplicationEvent(ContextClosedEvent event) { + public void onApplicationEvent(ContextClosedEvent ignored) { if (routerFunctions != null) { routerFunctionRegistry.unregister(routerFunctions); } @@ -203,11 +215,59 @@ public void onApplicationEvent(ContextRefreshedEvent event) { } @EventListener - public void onApplicationEvent(ContextClosedEvent event) { + public void onApplicationEvent(ContextClosedEvent ignored) { handlerMapping.unregister(this.pluginId); } } + private class SpringPluginStoppedEventAdapter + implements ApplicationListener { + + private final String pluginId; + + private SpringPluginStoppedEventAdapter(String pluginId) { + this.pluginId = pluginId; + } + + @Override + public void onApplicationEvent(ContextClosedEvent event) { + var plugin = pluginManager.getPlugin(pluginId).getPlugin(); + if (plugin instanceof SpringPlugin springPlugin) { + event.getApplicationContext() + .publishEvent(new SpringPluginStoppedEvent(this, springPlugin)); + } + } + } + + private class HaloPluginEventBridge { + + @EventListener + public void onApplicationEvent(SpringPluginStartedEvent event) { + var pluginContext = event.getSpringPlugin().getPluginContext(); + var pluginWrapper = pluginManager.getPlugin(pluginContext.getName()); + + pluginManager.getRootContext() + .publishEvent(new HaloPluginStartedEvent(this, pluginWrapper)); + } + + @EventListener + public void onApplicationEvent(SpringPluginStoppingEvent event) { + var pluginContext = event.getSpringPlugin().getPluginContext(); + var pluginWrapper = pluginManager.getPlugin(pluginContext.getName()); + pluginManager.getRootContext() + .publishEvent(new HaloPluginBeforeStopEvent(this, pluginWrapper)); + } + + @EventListener + public void onApplicationEvent(SpringPluginStoppedEvent event) { + var pluginContext = event.getSpringPlugin().getPluginContext(); + var pluginWrapper = pluginManager.getPlugin(pluginContext.getName()); + pluginManager.getRootContext() + .publishEvent(new HaloPluginStoppedEvent(this, pluginWrapper)); + } + + } + private List> resolvePropertySources(String pluginId, ResourceLoader resourceLoader) { var haloProperties = pluginManager.getRootContext() diff --git a/application/src/main/java/run/halo/app/plugin/DevPluginLoader.java b/application/src/main/java/run/halo/app/plugin/DevPluginLoader.java new file mode 100644 index 0000000000..9698b711fe --- /dev/null +++ b/application/src/main/java/run/halo/app/plugin/DevPluginLoader.java @@ -0,0 +1,43 @@ +package run.halo.app.plugin; + +import java.nio.file.Files; +import java.nio.file.Path; +import org.pf4j.DevelopmentPluginLoader; +import org.pf4j.PluginDescriptor; +import org.pf4j.PluginManager; + +public class DevPluginLoader extends DevelopmentPluginLoader { + + private final PluginProperties pluginProperties; + + public DevPluginLoader( + PluginManager pluginManager, + PluginProperties pluginProperties + ) { + super(pluginManager); + this.pluginProperties = pluginProperties; + } + + @Override + public ClassLoader loadPlugin(Path pluginPath, PluginDescriptor pluginDescriptor) { + var classesDirectories = pluginProperties.getClassesDirectories(); + if (classesDirectories != null) { + classesDirectories.forEach( + classesDirectory -> pluginClasspath.addClassesDirectories(classesDirectory) + ); + } + var libDirectories = pluginProperties.getLibDirectories(); + if (libDirectories != null) { + libDirectories.forEach( + libDirectory -> pluginClasspath.addJarsDirectories(libDirectory) + ); + } + return super.loadPlugin(pluginPath, pluginDescriptor); + } + + @Override + public boolean isApplicable(Path pluginPath) { + // Currently we only support a plugin loading from directory in dev mode. + return Files.isDirectory(pluginPath); + } +} diff --git a/application/src/main/java/run/halo/app/plugin/HaloPluginManager.java b/application/src/main/java/run/halo/app/plugin/HaloPluginManager.java index 7066a610d6..8589f099dc 100644 --- a/application/src/main/java/run/halo/app/plugin/HaloPluginManager.java +++ b/application/src/main/java/run/halo/app/plugin/HaloPluginManager.java @@ -1,28 +1,31 @@ package run.halo.app.plugin; import java.nio.file.Path; -import java.util.Collections; -import java.util.HashMap; -import java.util.Iterator; +import java.nio.file.Paths; import java.util.List; -import java.util.Map; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.pf4j.CompoundPluginLoader; +import org.pf4j.CompoundPluginRepository; import org.pf4j.DefaultPluginManager; +import org.pf4j.DefaultPluginRepository; import org.pf4j.ExtensionFactory; import org.pf4j.ExtensionFinder; +import org.pf4j.JarPluginLoader; +import org.pf4j.JarPluginRepository; import org.pf4j.PluginDescriptor; import org.pf4j.PluginDescriptorFinder; import org.pf4j.PluginFactory; -import org.pf4j.PluginRuntimeException; +import org.pf4j.PluginLoader; +import org.pf4j.PluginRepository; import org.pf4j.PluginState; import org.pf4j.PluginStateEvent; import org.pf4j.PluginStateListener; +import org.pf4j.PluginStatusProvider; import org.pf4j.PluginWrapper; -import org.springframework.beans.factory.DisposableBean; import org.springframework.context.ApplicationContext; import org.springframework.data.util.Lazy; -import run.halo.app.plugin.event.HaloPluginBeforeStopEvent; -import run.halo.app.plugin.event.HaloPluginLoadedEvent; +import run.halo.app.infra.SystemVersionSupplier; import run.halo.app.plugin.event.HaloPluginStartedEvent; import run.halo.app.plugin.event.HaloPluginStoppedEvent; @@ -31,31 +34,37 @@ * It provides methods for managing the plugin lifecycle. * * @author guqing + * @author johnniang * @since 2.0.0 */ @Slf4j -public class HaloPluginManager extends DefaultPluginManager - implements DisposableBean, SpringPluginManager { - - private final Map startingErrors = new HashMap<>(); +public class HaloPluginManager extends DefaultPluginManager implements SpringPluginManager { private final ApplicationContext rootContext; private final Lazy sharedContext; - public HaloPluginManager(Path pluginsRoot, ApplicationContext rootContext) { - super(pluginsRoot); + private final PluginProperties pluginProperties; + + public HaloPluginManager(ApplicationContext rootContext, + PluginProperties pluginProperties, + SystemVersionSupplier systemVersionSupplier) { + this.pluginProperties = pluginProperties; this.rootContext = rootContext; // We have to initialize share context lazily because the root context has not refreshed this.sharedContext = Lazy.of(() -> SharedApplicationContextFactory.create(rootContext)); + super.runtimeMode = pluginProperties.getRuntimeMode(); + + setExactVersionAllowed(pluginProperties.isExactVersionAllowed()); + setSystemVersion(systemVersionSupplier.get().getNormalVersion()); + + super.initialize(); } @Override protected void initialize() { - super.initialize(); - - // add additional listener - addPluginStateListener(new PluginStartedEventAdapter()); + // Leave the implementation empty because the super#initialize eagerly initializes + // components before properties set. } @Override @@ -93,119 +102,54 @@ protected PluginWrapper createPluginWrapper(PluginDescriptor pluginDescriptor, P } @Override - protected PluginState stopPlugin(String pluginId, boolean stopDependents) { - checkPluginId(pluginId); - PluginWrapper pluginWrapper = getPlugin(pluginId); - PluginDescriptor pluginDescriptor = pluginWrapper.getDescriptor(); - PluginState pluginState = pluginWrapper.getPluginState(); - if (PluginState.STOPPED == pluginState) { - log.debug("Already stopped plugin '{}'", getPluginLabel(pluginDescriptor)); - return PluginState.STOPPED; - } - - // test for disabled plugin - if (PluginState.DISABLED == pluginState) { - // do nothing - return pluginState; - } - - rootContext.publishEvent(new HaloPluginBeforeStopEvent(this, pluginWrapper)); - - if (stopDependents) { - List dependents = dependencyResolver.getDependents(pluginId); - while (!dependents.isEmpty()) { - String dependent = dependents.remove(0); - stopPlugin(dependent, false); - dependents.addAll(0, dependencyResolver.getDependents(dependent)); - } - } - - log.info("Stop plugin '{}'", getPluginLabel(pluginDescriptor)); - pluginWrapper.getPlugin().stop(); - pluginWrapper.setPluginState(PluginState.STOPPED); - // release plugin resources - releaseAdditionalResources(pluginId); - - startedPlugins.remove(pluginWrapper); - - rootContext.publishEvent(new HaloPluginStoppedEvent(this, pluginWrapper)); - firePluginStateEvent(new PluginStateEvent(this, pluginWrapper, pluginState)); - - return pluginWrapper.getPluginState(); + protected PluginLoader createPluginLoader() { + var compoundLoader = new CompoundPluginLoader(); + compoundLoader.add(new DevPluginLoader(this, this.pluginProperties), this::isDevelopment); + compoundLoader.add(new JarPluginLoader(this)); + return compoundLoader; } @Override - public void startPlugins() { - throw new UnsupportedOperationException( - "The operation of starting all plugins is not supported." - ); + protected PluginStatusProvider createPluginStatusProvider() { + if (PropertyPluginStatusProvider.isPropertySet(pluginProperties)) { + return new PropertyPluginStatusProvider(pluginProperties); + } + return super.createPluginStatusProvider(); } @Override - public PluginState startPlugin(String pluginId) { - try { - return super.startPlugin(pluginId); - } catch (Throwable t) { - // TODO Do not release additional resources here. - // releaseAdditionalResources(pluginId); - throw t; - } + protected PluginRepository createPluginRepository() { + var developmentPluginRepository = + new DefaultDevelopmentPluginRepository(getPluginsRoots()); + developmentPluginRepository + .setFixedPaths(pluginProperties.getFixedPluginPath()); + return new CompoundPluginRepository() + .add(developmentPluginRepository, this::isDevelopment) + .add(new JarPluginRepository(getPluginsRoots())) + .add(new DefaultPluginRepository(getPluginsRoots())); } @Override - public void stopPlugins() { - doStopPlugins(); - } - - private void doStopPlugins() { - startingErrors.clear(); - // stop started plugins in reverse order - Collections.reverse(startedPlugins); - Iterator itr = startedPlugins.iterator(); - while (itr.hasNext()) { - PluginWrapper pluginWrapper = itr.next(); - PluginState pluginState = pluginWrapper.getPluginState(); - if (PluginState.STARTED == pluginState) { - try { - rootContext.publishEvent( - new HaloPluginBeforeStopEvent(this, pluginWrapper)); - log.info("Stop plugin '{}'", getPluginLabel(pluginWrapper.getDescriptor())); - if (pluginWrapper.getPlugin() != null) { - pluginWrapper.getPlugin().stop(); - } - pluginWrapper.setPluginState(PluginState.STOPPED); - itr.remove(); - releaseAdditionalResources(pluginWrapper.getPluginId()); - - rootContext.publishEvent( - new HaloPluginStoppedEvent(this, pluginWrapper)); - firePluginStateEvent(new PluginStateEvent(this, pluginWrapper, pluginState)); - } catch (PluginRuntimeException e) { - log.error(e.getMessage(), e); - startingErrors.put(pluginWrapper.getPluginId(), PluginStartingError.of( - pluginWrapper.getPluginId(), e.getMessage(), e.toString())); - } - } + protected List createPluginsRoot() { + var pluginsRoot = pluginProperties.getPluginsRoot(); + if (StringUtils.isNotBlank(pluginsRoot)) { + return List.of(Paths.get(pluginsRoot)); } - } - - /** - * Release plugin holding release on stop. - */ - public void releaseAdditionalResources(String pluginId) { + return super.createPluginsRoot(); } @Override - protected PluginWrapper loadPluginFromPath(Path pluginPath) { - PluginWrapper pluginWrapper = super.loadPluginFromPath(pluginPath); - rootContext.publishEvent(new HaloPluginLoadedEvent(this, pluginWrapper)); - return pluginWrapper; + public void startPlugins() { + throw new UnsupportedOperationException( + "The operation of starting all plugins is not supported." + ); } - @Override - public void destroy() throws Exception { - stopPlugins(); + public void stopPlugins() { + throw new UnsupportedOperationException( + "The operation of stopping all plugins is not supported." + ); } @Override @@ -218,6 +162,18 @@ public ApplicationContext getSharedContext() { return sharedContext.get(); } + private class PluginStoppedEventAdapter implements PluginStateListener { + + @Override + public void pluginStateChanged(PluginStateEvent event) { + if (!PluginState.STOPPED.equals(event.getPluginState())) { + return; + } + var pluginWrapper = event.getPlugin(); + rootContext.publishEvent(new HaloPluginStoppedEvent(event.getSource(), pluginWrapper)); + } + } + private class PluginStartedEventAdapter implements PluginStateListener { @Override diff --git a/application/src/main/java/run/halo/app/plugin/PluginAutoConfiguration.java b/application/src/main/java/run/halo/app/plugin/PluginAutoConfiguration.java index 6a53e0507a..f94e9e97ef 100644 --- a/application/src/main/java/run/halo/app/plugin/PluginAutoConfiguration.java +++ b/application/src/main/java/run/halo/app/plugin/PluginAutoConfiguration.java @@ -2,27 +2,10 @@ import static run.halo.app.plugin.resources.BundleResourceUtils.getJsBundleResource; -import java.io.File; import java.io.IOException; -import java.lang.reflect.Constructor; -import java.nio.file.Files; -import java.nio.file.Path; import java.time.Instant; -import java.util.Objects; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; -import org.pf4j.CompoundPluginLoader; -import org.pf4j.CompoundPluginRepository; -import org.pf4j.DefaultPluginRepository; -import org.pf4j.DevelopmentPluginLoader; -import org.pf4j.JarPluginLoader; -import org.pf4j.JarPluginRepository; -import org.pf4j.PluginDescriptor; -import org.pf4j.PluginLoader; import org.pf4j.PluginManager; -import org.pf4j.PluginRepository; -import org.pf4j.PluginStatusProvider; -import org.pf4j.RuntimeMode; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.autoconfigure.web.WebProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; @@ -48,23 +31,11 @@ @EnableConfigurationProperties(PluginProperties.class) public class PluginAutoConfiguration { - private final PluginProperties pluginProperties; - - private final SystemVersionSupplier systemVersionSupplier; - - @Qualifier("webFluxContentTypeResolver") - private final RequestedContentTypeResolver requestedContentTypeResolver; - - public PluginAutoConfiguration(PluginProperties pluginProperties, - SystemVersionSupplier systemVersionSupplier, - RequestedContentTypeResolver requestedContentTypeResolver) { - this.pluginProperties = pluginProperties; - this.systemVersionSupplier = systemVersionSupplier; - this.requestedContentTypeResolver = requestedContentTypeResolver; - } - @Bean - public PluginRequestMappingHandlerMapping pluginRequestMappingHandlerMapping() { + public PluginRequestMappingHandlerMapping pluginRequestMappingHandlerMapping( + @Qualifier("webFluxContentTypeResolver") + RequestedContentTypeResolver requestedContentTypeResolver + ) { PluginRequestMappingHandlerMapping mapping = new PluginRequestMappingHandlerMapping(); mapping.setContentTypeResolver(requestedContentTypeResolver); mapping.setOrder(-1); @@ -72,106 +43,14 @@ public PluginRequestMappingHandlerMapping pluginRequestMappingHandlerMapping() { } @Bean - public PluginManager pluginManager(ApplicationContext context) { - // Setup RuntimeMode - System.setProperty("pf4j.mode", pluginProperties.getRuntimeMode().toString()); - - // Setup Plugin folder - String pluginsRoot = Objects.toString(pluginProperties.getPluginsRoot(), "plugins"); - - System.setProperty("pf4j.pluginsDir", pluginsRoot); - String appHome = System.getProperty("app.home"); - if (RuntimeMode.DEPLOYMENT == pluginProperties.getRuntimeMode() - && StringUtils.isNotBlank(appHome)) { - System.setProperty("pf4j.pluginsDir", appHome + File.separator + pluginsRoot); - } - - var pluginManager = new HaloPluginManager(new File(pluginsRoot).toPath(), context) { - @Override - protected PluginLoader createPluginLoader() { - if (pluginProperties.getCustomPluginLoader() != null) { - Class clazz = pluginProperties.getCustomPluginLoader(); - try { - Constructor constructor = clazz.getConstructor(PluginManager.class); - return (PluginLoader) constructor.newInstance(this); - } catch (Exception ex) { - throw new IllegalArgumentException( - String.format("Create custom PluginLoader %s failed. Make sure" - + "there is a constructor with one argument that accepts " - + "PluginLoader", - clazz.getName())); - } - } else { - return new CompoundPluginLoader() - .add(createDevelopmentPluginLoader(this), this::isDevelopment) - .add(new JarPluginLoader(this)); - } - } - - @Override - protected PluginStatusProvider createPluginStatusProvider() { - if (PropertyPluginStatusProvider.isPropertySet(pluginProperties)) { - return new PropertyPluginStatusProvider(pluginProperties); - } - return super.createPluginStatusProvider(); - } - - @Override - protected PluginRepository createPluginRepository() { - var developmentPluginRepository = - new DefaultDevelopmentPluginRepository(getPluginsRoots()); - developmentPluginRepository - .setFixedPaths(pluginProperties.getFixedPluginPath()); - return new CompoundPluginRepository() - .add(developmentPluginRepository, this::isDevelopment) - .add(new JarPluginRepository(getPluginsRoots())) - .add(new DefaultPluginRepository(getPluginsRoots())); - } - }; - - pluginManager.setExactVersionAllowed(pluginProperties.isExactVersionAllowed()); - // only for development mode - if (RuntimeMode.DEPLOYMENT.equals(pluginManager.getRuntimeMode())) { - pluginManager.setSystemVersion(getSystemVersion()); - } - return pluginManager; - } - - DevelopmentPluginLoader createDevelopmentPluginLoader(PluginManager pluginManager) { - return new DevelopmentPluginLoader(pluginManager) { - - @Override - public ClassLoader loadPlugin(Path pluginPath, - PluginDescriptor pluginDescriptor) { - if (pluginProperties.getClassesDirectories() != null) { - for (String classesDirectory : - pluginProperties.getClassesDirectories()) { - pluginClasspath.addClassesDirectories(classesDirectory); - } - } - if (pluginProperties.getLibDirectories() != null) { - for (String libDirectory : - pluginProperties.getLibDirectories()) { - pluginClasspath.addJarsDirectories(libDirectory); - } - } - return super.loadPlugin(pluginPath, pluginDescriptor); - } - - @Override - public boolean isApplicable(Path pluginPath) { - return Files.exists(pluginPath) - && Files.isDirectory(pluginPath); - } - }; - } - - String getSystemVersion() { - return systemVersionSupplier.get().getNormalVersion(); + public PluginManager pluginManager(ApplicationContext context, + SystemVersionSupplier systemVersionSupplier, + PluginProperties pluginProperties) { + return new HaloPluginManager(context, pluginProperties, systemVersionSupplier); } @Bean - public RouterFunction pluginJsBundleRoute(HaloPluginManager haloPluginManager, + public RouterFunction pluginJsBundleRoute(PluginManager pluginManager, WebProperties webProperties) { var cacheProperties = webProperties.getResources().getCache(); return RouterFunctions.route() @@ -179,7 +58,7 @@ public RouterFunction pluginJsBundleRoute(HaloPluginManager halo String pluginName = request.pathVariable("name"); String fileName = request.pathVariable("resource"); - var jsBundle = getJsBundleResource(haloPluginManager, pluginName, fileName); + var jsBundle = getJsBundleResource(pluginManager, pluginName, fileName); if (jsBundle == null || !jsBundle.exists()) { return ServerResponse.notFound().build(); } diff --git a/application/src/main/java/run/halo/app/plugin/PluginCreatedEventReconciler.java b/application/src/main/java/run/halo/app/plugin/PluginCreatedEventReconciler.java deleted file mode 100644 index 711938386f..0000000000 --- a/application/src/main/java/run/halo/app/plugin/PluginCreatedEventReconciler.java +++ /dev/null @@ -1,107 +0,0 @@ -package run.halo.app.plugin; - -import io.micrometer.common.util.StringUtils; -import java.time.Duration; -import java.time.Instant; -import java.util.UUID; -import lombok.extern.slf4j.Slf4j; -import org.springframework.context.SmartLifecycle; -import org.springframework.context.event.EventListener; -import org.springframework.stereotype.Component; -import reactor.core.Exceptions; -import reactor.core.publisher.Mono; -import run.halo.app.core.extension.Plugin; -import run.halo.app.extension.ReactiveExtensionClient; -import run.halo.app.extension.controller.Controller; -import run.halo.app.extension.controller.ControllerBuilder; -import run.halo.app.extension.controller.DefaultController; -import run.halo.app.extension.controller.DefaultQueue; -import run.halo.app.extension.controller.Reconciler; -import run.halo.app.extension.controller.RequestQueue; -import run.halo.app.plugin.event.PluginCreatedEvent; - -/** - * Plugin event reconciler. - * If other plugin events need to be reconciled, consider sharing this reconciler. - * - * @author guqing - * @since 2.2.0 - */ -@Slf4j -@Component -public class PluginCreatedEventReconciler - implements Reconciler, SmartLifecycle { - - private final RequestQueue pluginEventQueue; - - private final ReactiveExtensionClient client; - - private final Controller pluginEventController; - - private boolean running = false; - - public PluginCreatedEventReconciler(ReactiveExtensionClient client) { - this.client = client; - pluginEventQueue = new DefaultQueue<>(Instant::now); - pluginEventController = this.setupWith(null); - } - - @Override - public Result reconcile(PluginCreatedEvent pluginCreatedEvent) { - String pluginName = pluginCreatedEvent.getPluginName(); - try { - ensureConfigMapNameNotEmptyIfSettingIsNotBlank(pluginName); - } catch (InterruptedException e) { - throw Exceptions.propagate(e); - } - return null; - } - - @Override - public Controller setupWith(ControllerBuilder builder) { - return new DefaultController<>( - this.getClass().getName(), - this, - pluginEventQueue, - null, - Duration.ofMillis(100), - Duration.ofSeconds(1000) - ); - } - - @EventListener(PluginCreatedEvent.class) - public void handlePluginCreated(PluginCreatedEvent pluginCreatedEvent) { - pluginEventQueue.addImmediately(pluginCreatedEvent); - } - - void ensureConfigMapNameNotEmptyIfSettingIsNotBlank(String pluginName) - throws InterruptedException { - client.fetch(Plugin.class, pluginName) - .switchIfEmpty(Mono.error(new IllegalStateException("Plugin not found: " + pluginName))) - .filter(plugin -> StringUtils.isNotBlank(plugin.getSpec().getSettingName())) - .filter(plugin -> StringUtils.isBlank(plugin.getSpec().getConfigMapName())) - .doOnNext(plugin -> { - // has settingName value but configMapName not configured - plugin.getSpec().setConfigMapName(UUID.randomUUID().toString()); - }) - .flatMap(client::update) - .block(); - } - - @Override - public void start() { - pluginEventController.start(); - running = true; - } - - @Override - public void stop() { - running = false; - pluginEventController.dispose(); - } - - @Override - public boolean isRunning() { - return running; - } -} diff --git a/application/src/main/java/run/halo/app/plugin/PluginDevelopmentInitializer.java b/application/src/main/java/run/halo/app/plugin/PluginDevelopmentInitializer.java index a7bb190e2b..20aa4da110 100644 --- a/application/src/main/java/run/halo/app/plugin/PluginDevelopmentInitializer.java +++ b/application/src/main/java/run/halo/app/plugin/PluginDevelopmentInitializer.java @@ -5,6 +5,7 @@ import java.nio.file.Path; import java.time.Duration; import lombok.extern.slf4j.Slf4j; +import org.pf4j.PluginManager; import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.context.ApplicationListener; import org.springframework.dao.OptimisticLockingFailureException; @@ -23,22 +24,22 @@ @Component public class PluginDevelopmentInitializer implements ApplicationListener { - private final HaloPluginManager haloPluginManager; + private final PluginManager pluginManager; private final PluginProperties pluginProperties; private final ReactiveExtensionClient extensionClient; - public PluginDevelopmentInitializer(HaloPluginManager haloPluginManager, + public PluginDevelopmentInitializer(PluginManager pluginManager, PluginProperties pluginProperties, ReactiveExtensionClient extensionClient) { - this.haloPluginManager = haloPluginManager; + this.pluginManager = pluginManager; this.pluginProperties = pluginProperties; this.extensionClient = extensionClient; } @Override - public void onApplicationEvent(@NonNull ApplicationReadyEvent event) { - if (!haloPluginManager.isDevelopment()) { + public void onApplicationEvent(@NonNull ApplicationReadyEvent ignored) { + if (!pluginManager.isDevelopment()) { return; } createFixedPluginIfNecessary(); diff --git a/application/src/main/java/run/halo/app/plugin/PluginProperties.java b/application/src/main/java/run/halo/app/plugin/PluginProperties.java index 1e26442f2f..db2844305d 100644 --- a/application/src/main/java/run/halo/app/plugin/PluginProperties.java +++ b/application/src/main/java/run/halo/app/plugin/PluginProperties.java @@ -4,7 +4,6 @@ import java.util.ArrayList; import java.util.List; import lombok.Data; -import org.pf4j.PluginLoader; import org.pf4j.RuntimeMode; import org.springframework.boot.context.properties.ConfigurationProperties; @@ -67,8 +66,4 @@ public class PluginProperties { */ private String pluginsRoot; - /** - * Allows providing custom plugin loaders. - */ - private Class customPluginLoader; } diff --git a/application/src/main/java/run/halo/app/plugin/SharedApplicationContextFactory.java b/application/src/main/java/run/halo/app/plugin/SharedApplicationContextFactory.java index 24ae29e2c8..5beb2e97ef 100644 --- a/application/src/main/java/run/halo/app/plugin/SharedApplicationContextFactory.java +++ b/application/src/main/java/run/halo/app/plugin/SharedApplicationContextFactory.java @@ -26,6 +26,7 @@ public enum SharedApplicationContextFactory { public static ApplicationContext create(ApplicationContext rootContext) { // TODO Optimize creation timing var sharedContext = new GenericApplicationContext(); + sharedContext.registerShutdownHook(); var beanFactory = sharedContext.getBeanFactory(); diff --git a/application/src/main/java/run/halo/app/plugin/SpringPlugin.java b/application/src/main/java/run/halo/app/plugin/SpringPlugin.java index 9e4db0dca2..c80842128f 100644 --- a/application/src/main/java/run/halo/app/plugin/SpringPlugin.java +++ b/application/src/main/java/run/halo/app/plugin/SpringPlugin.java @@ -4,6 +4,9 @@ import org.springframework.context.ApplicationContext; import org.springframework.context.ConfigurableApplicationContext; import run.halo.app.PluginApplicationContextFactory; +import run.halo.app.plugin.event.SpringPluginStartedEvent; +import run.halo.app.plugin.event.SpringPluginStartingEvent; +import run.halo.app.plugin.event.SpringPluginStoppingEvent; public class SpringPlugin extends Plugin { @@ -30,6 +33,7 @@ public void start() { var pluginOpt = context.getBeanProvider(Plugin.class) .stream() .findFirst(); + context.publishEvent(new SpringPluginStartingEvent(this, this)); if (pluginOpt.isPresent()) { this.delegate = pluginOpt.get(); if (this.delegate instanceof BasePlugin basePlugin) { @@ -37,10 +41,14 @@ public void start() { } this.delegate.start(); } + context.publishEvent(new SpringPluginStartedEvent(this, this)); } @Override public void stop() { + if (context != null) { + context.publishEvent(new SpringPluginStoppingEvent(this, this)); + } if (this.delegate != null) { this.delegate.stop(); } @@ -62,4 +70,7 @@ public ApplicationContext getApplicationContext() { return context; } + public PluginContext getPluginContext() { + return pluginContext; + } } diff --git a/application/src/main/java/run/halo/app/plugin/SpringPluginFactory.java b/application/src/main/java/run/halo/app/plugin/SpringPluginFactory.java index 0a942e5eda..da4fa015b2 100644 --- a/application/src/main/java/run/halo/app/plugin/SpringPluginFactory.java +++ b/application/src/main/java/run/halo/app/plugin/SpringPluginFactory.java @@ -11,6 +11,7 @@ *

Get a {@link BasePlugin} instance from the {@link PluginApplicationContext}.

* * @author guqing + * @author johnniang * @since 2.0.0 */ @Slf4j diff --git a/application/src/main/java/run/halo/app/plugin/event/PluginCreatedEvent.java b/application/src/main/java/run/halo/app/plugin/event/PluginCreatedEvent.java deleted file mode 100644 index 8341c755f6..0000000000 --- a/application/src/main/java/run/halo/app/plugin/event/PluginCreatedEvent.java +++ /dev/null @@ -1,21 +0,0 @@ -package run.halo.app.plugin.event; - -import lombok.Getter; -import org.springframework.context.ApplicationEvent; -import run.halo.app.core.extension.Plugin; - -/** - * The {@link Plugin} created event. - * - * @author guqing - * @since 2.0.0 - */ -@Getter -public class PluginCreatedEvent extends ApplicationEvent { - private final String pluginName; - - public PluginCreatedEvent(Object source, String pluginName) { - super(source); - this.pluginName = pluginName; - } -} diff --git a/application/src/main/java/run/halo/app/plugin/event/SpringPluginStartedEvent.java b/application/src/main/java/run/halo/app/plugin/event/SpringPluginStartedEvent.java new file mode 100644 index 0000000000..990603435e --- /dev/null +++ b/application/src/main/java/run/halo/app/plugin/event/SpringPluginStartedEvent.java @@ -0,0 +1,18 @@ +package run.halo.app.plugin.event; + +import org.springframework.context.ApplicationEvent; +import run.halo.app.plugin.SpringPlugin; + +public class SpringPluginStartedEvent extends ApplicationEvent { + + private final SpringPlugin springPlugin; + + public SpringPluginStartedEvent(Object source, SpringPlugin springPlugin) { + super(source); + this.springPlugin = springPlugin; + } + + public SpringPlugin getSpringPlugin() { + return springPlugin; + } +} diff --git a/application/src/main/java/run/halo/app/plugin/event/SpringPluginStartingEvent.java b/application/src/main/java/run/halo/app/plugin/event/SpringPluginStartingEvent.java new file mode 100644 index 0000000000..49f4b737ae --- /dev/null +++ b/application/src/main/java/run/halo/app/plugin/event/SpringPluginStartingEvent.java @@ -0,0 +1,18 @@ +package run.halo.app.plugin.event; + +import org.springframework.context.ApplicationEvent; +import run.halo.app.plugin.SpringPlugin; + +public class SpringPluginStartingEvent extends ApplicationEvent { + + private final SpringPlugin springPlugin; + + public SpringPluginStartingEvent(Object source, SpringPlugin springPlugin) { + super(source); + this.springPlugin = springPlugin; + } + + public SpringPlugin getSpringPlugin() { + return springPlugin; + } +} diff --git a/application/src/main/java/run/halo/app/plugin/event/SpringPluginStoppedEvent.java b/application/src/main/java/run/halo/app/plugin/event/SpringPluginStoppedEvent.java new file mode 100644 index 0000000000..4de3d53b00 --- /dev/null +++ b/application/src/main/java/run/halo/app/plugin/event/SpringPluginStoppedEvent.java @@ -0,0 +1,18 @@ +package run.halo.app.plugin.event; + +import org.springframework.context.ApplicationEvent; +import run.halo.app.plugin.SpringPlugin; + +public class SpringPluginStoppedEvent extends ApplicationEvent { + + private final SpringPlugin springPlugin; + + public SpringPluginStoppedEvent(Object source, SpringPlugin springPlugin) { + super(source); + this.springPlugin = springPlugin; + } + + public SpringPlugin getSpringPlugin() { + return springPlugin; + } +} diff --git a/application/src/main/java/run/halo/app/plugin/event/SpringPluginStoppingEvent.java b/application/src/main/java/run/halo/app/plugin/event/SpringPluginStoppingEvent.java new file mode 100644 index 0000000000..fd43131427 --- /dev/null +++ b/application/src/main/java/run/halo/app/plugin/event/SpringPluginStoppingEvent.java @@ -0,0 +1,18 @@ +package run.halo.app.plugin.event; + +import org.springframework.context.ApplicationEvent; +import run.halo.app.plugin.SpringPlugin; + +public class SpringPluginStoppingEvent extends ApplicationEvent { + + private final SpringPlugin springPlugin; + + public SpringPluginStoppingEvent(Object source, SpringPlugin springPlugin) { + super(source); + this.springPlugin = springPlugin; + } + + public SpringPlugin getSpringPlugin() { + return springPlugin; + } +} diff --git a/application/src/test/java/run/halo/app/infra/DefaultSystemVersionSupplierTest.java b/application/src/test/java/run/halo/app/infra/DefaultSystemVersionSupplierTest.java index b323a8d968..6be7f0a5f1 100644 --- a/application/src/test/java/run/halo/app/infra/DefaultSystemVersionSupplierTest.java +++ b/application/src/test/java/run/halo/app/infra/DefaultSystemVersionSupplierTest.java @@ -1,11 +1,16 @@ package run.halo.app.infra; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; import com.github.zafarkhaja.semver.Version; import java.util.Properties; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.info.BuildProperties; /** @@ -15,14 +20,14 @@ * @since 2.0.0 */ +@ExtendWith(MockitoExtension.class) class DefaultSystemVersionSupplierTest { + @InjectMocks private DefaultSystemVersionSupplier systemVersionSupplier; - @BeforeEach - void setUp() { - systemVersionSupplier = new DefaultSystemVersionSupplier(); - } + @Mock + ObjectProvider buildPropertiesProvider; @Test void getWhenBuildPropertiesNotSet() { @@ -34,7 +39,7 @@ void getWhenBuildPropertiesNotSet() { void getWhenBuildPropertiesButVersionIsNull() { Properties properties = new Properties(); BuildProperties buildProperties = new BuildProperties(properties); - systemVersionSupplier.setBuildProperties(buildProperties); + when(buildPropertiesProvider.getIfUnique()).thenReturn(buildProperties); Version version = systemVersionSupplier.get(); assertThat(version.toString()).isEqualTo("0.0.0"); @@ -45,14 +50,14 @@ void getWhenBuildPropertiesAndVersionNotEmpty() { Properties properties = new Properties(); properties.put("version", "2.0.0"); BuildProperties buildProperties = new BuildProperties(properties); - systemVersionSupplier.setBuildProperties(buildProperties); + when(buildPropertiesProvider.getIfUnique()).thenReturn(buildProperties); Version version = systemVersionSupplier.get(); assertThat(version.toString()).isEqualTo("2.0.0"); properties.put("version", "2.0.0-SNAPSHOT"); buildProperties = new BuildProperties(properties); - systemVersionSupplier.setBuildProperties(buildProperties); + when(buildPropertiesProvider.getIfUnique()).thenReturn(buildProperties); version = systemVersionSupplier.get(); assertThat(version.toString()).isEqualTo("2.0.0-SNAPSHOT"); assertThat(version.getPreReleaseVersion()).isEqualTo("SNAPSHOT"); From cbb9b2821af1fa2f0e0ca33565b6336aebba7f94 Mon Sep 17 00:00:00 2001 From: John Niang Date: Thu, 25 Jan 2024 15:09:18 +0800 Subject: [PATCH 3/6] Wait for extensions deleted Signed-off-by: John Niang --- .../plugin/PluginBeforeStopSyncListener.java | 35 ++++++++++++++++--- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/application/src/main/java/run/halo/app/plugin/PluginBeforeStopSyncListener.java b/application/src/main/java/run/halo/app/plugin/PluginBeforeStopSyncListener.java index af1f4bb7bc..92a5d4c4a1 100644 --- a/application/src/main/java/run/halo/app/plugin/PluginBeforeStopSyncListener.java +++ b/application/src/main/java/run/halo/app/plugin/PluginBeforeStopSyncListener.java @@ -1,10 +1,15 @@ package run.halo.app.plugin; +import java.time.Duration; +import lombok.extern.slf4j.Slf4j; import org.springframework.context.event.EventListener; import org.springframework.lang.NonNull; +import org.springframework.retry.RetryException; import org.springframework.stereotype.Component; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import reactor.util.retry.Retry; +import run.halo.app.extension.GroupVersionKind; import run.halo.app.extension.ReactiveExtensionClient; import run.halo.app.plugin.event.HaloPluginBeforeStopEvent; @@ -14,6 +19,7 @@ * @author guqing * @since 2.0.0 */ +@Slf4j @Component public class PluginBeforeStopSyncListener { @@ -24,17 +30,17 @@ public PluginBeforeStopSyncListener(ReactiveExtensionClient client) { } @EventListener - public Mono onApplicationEvent(@NonNull HaloPluginBeforeStopEvent event) { + public void onApplicationEvent(@NonNull HaloPluginBeforeStopEvent event) { var pluginWrapper = event.getPlugin(); var p = pluginWrapper.getPlugin(); if (!(p instanceof SpringPlugin springPlugin)) { - return Mono.empty(); + return; } var applicationContext = springPlugin.getApplicationContext(); if (!(applicationContext instanceof PluginApplicationContext pluginApplicationContext)) { - return Mono.empty(); + return; } - return cleanUpPluginExtensionResources(pluginApplicationContext); + cleanUpPluginExtensionResources(pluginApplicationContext).block(Duration.ofMinutes(1)); } private Mono cleanUpPluginExtensionResources(PluginApplicationContext context) { @@ -42,7 +48,26 @@ private Mono cleanUpPluginExtensionResources(PluginApplicationContext cont return Flux.fromIterable(gvkExtensionNames.entrySet()) .flatMap(entry -> Flux.fromIterable(entry.getValue()) .flatMap(extensionName -> client.fetch(entry.getKey(), extensionName)) - .flatMap(client::delete)) + .flatMap(client::delete) + .flatMap(e -> waitForDeleted(e.groupVersionKind(), e.getMetadata().getName()))) .then(); } + + private Mono waitForDeleted(GroupVersionKind gvk, String name) { + return client.fetch(gvk, name) + .flatMap(e -> { + if (log.isDebugEnabled()) { + log.debug("Wait for {}/{} deleted", gvk, name); + } + return Mono.error(new RetryException("Wait for extension deleted")); + }) + .retryWhen(Retry.backoff(10, Duration.ofMillis(100)) + .filter(RetryException.class::isInstance)) + .then() + .doOnSuccess(v -> { + if (log.isDebugEnabled()) { + log.debug("{}/{} was deleted successfully.", gvk, name); + } + }); + } } From a3820577e47ce99570a0de37eabb3fac2a214d12 Mon Sep 17 00:00:00 2001 From: John Niang Date: Thu, 25 Jan 2024 18:12:28 +0800 Subject: [PATCH 4/6] Remove unused classes Signed-off-by: John Niang --- ...efaultPluginApplicationContextFactory.java | 49 +++++++++++++------ .../halo/app/plugin/HaloPluginManager.java | 29 ----------- 2 files changed, 33 insertions(+), 45 deletions(-) diff --git a/application/src/main/java/run/halo/app/plugin/DefaultPluginApplicationContextFactory.java b/application/src/main/java/run/halo/app/plugin/DefaultPluginApplicationContextFactory.java index 01c6dd6148..442e82120d 100644 --- a/application/src/main/java/run/halo/app/plugin/DefaultPluginApplicationContextFactory.java +++ b/application/src/main/java/run/halo/app/plugin/DefaultPluginApplicationContextFactory.java @@ -6,6 +6,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.concurrent.TimeUnit; import java.util.stream.Stream; import lombok.extern.slf4j.Slf4j; import org.pf4j.PluginRuntimeException; @@ -22,6 +23,7 @@ import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; import org.springframework.stereotype.Controller; +import org.springframework.util.StopWatch; import org.springframework.web.reactive.function.server.RouterFunction; import org.springframework.web.reactive.function.server.ServerResponse; import reactor.core.Exceptions; @@ -59,6 +61,9 @@ public ApplicationContext create(String pluginId) { log.debug("Preparing to create application context for plugin {}", pluginId); var pluginWrapper = pluginManager.getPlugin(pluginId); + var sw = new StopWatch("CreateApplicationContextFor" + pluginId); + + sw.start("Create"); var context = new PluginApplicationContext(pluginId); context.registerShutdownHook(); context.setParent(pluginManager.getSharedContext()); @@ -66,11 +71,16 @@ public ApplicationContext create(String pluginId) { var classLoader = pluginWrapper.getPluginClassLoader(); var resourceLoader = new DefaultResourceLoader(classLoader); context.setResourceLoader(resourceLoader); + sw.stop(); + + sw.start("LoadPropertySources"); var mutablePropertySources = context.getEnvironment().getPropertySources(); resolvePropertySources(pluginId, resourceLoader) .forEach(mutablePropertySources::addLast); + sw.stop(); + sw.start("RegisterBeans"); var beanFactory = context.getBeanFactory(); context.registerBean(AggregatedRouterFunction.class); beanFactory.registerSingleton("pluginWrapper", pluginWrapper); @@ -84,26 +94,13 @@ public ApplicationContext create(String pluginId) { }); rootContext.getBeanProvider(ReactiveExtensionClient.class) - .ifAvailable(client -> { + .ifUnique(client -> { var reactiveSettingFetcher = new DefaultReactiveSettingFetcher(client, pluginId); var settingFetcher = new DefaultSettingFetcher(reactiveSettingFetcher); beanFactory.registerSingleton("reactiveSettingFetcher", reactiveSettingFetcher); beanFactory.registerSingleton("settingFetcher", settingFetcher); }); - var classNames = pluginManager.getExtensionClassNames(pluginId); - classNames.stream() - .map(className -> { - try { - return classLoader.loadClass(className); - } catch (ClassNotFoundException e) { - throw new PluginRuntimeException(String.format(""" - Failed to load class %s for plugin %s.\ - """, className, pluginId), e); - } - }) - .forEach(context::register); - rootContext.getBeanProvider(PluginRequestMappingHandlerMapping.class) .ifAvailable(handlerMapping -> { var handlerMappingManager = @@ -130,12 +127,32 @@ public ApplicationContext create(String pluginId) { pluginRouterFunctionManager ); }); + sw.stop(); + sw.start("LoadComponents"); + var classNames = pluginManager.getExtensionClassNames(pluginId); + classNames.stream() + .map(className -> { + try { + return classLoader.loadClass(className); + } catch (ClassNotFoundException e) { + throw new PluginRuntimeException(String.format(""" + Failed to load class %s for plugin %s.\ + """, className, pluginId), e); + } + }) + .forEach(clazzName -> context.registerBean(clazzName)); + sw.stop(); log.debug("Created application context for plugin {}", pluginId); - log.debug("Refreshing application context for plugin {}", pluginId); + log.debug("Refreshing application context for plugin {}", pluginId); + sw.start("Refresh"); context.refresh(); + sw.stop(); log.debug("Refreshed application context for plugin {}", pluginId); + if (log.isDebugEnabled()) { + log.debug("\n{}", sw.prettyPrint(TimeUnit.MILLISECONDS)); + } return context; } @@ -295,7 +312,7 @@ private List> resolvePropertySources(String pluginId, // resolve default config Stream.of( CLASSPATH_URL_PREFIX + "/config.yaml", - CLASSPATH_URL_PREFIX + "/config.yaml" + CLASSPATH_URL_PREFIX + "/config.yml" ) .map(resourceLoader::getResource) .forEach(resource -> { diff --git a/application/src/main/java/run/halo/app/plugin/HaloPluginManager.java b/application/src/main/java/run/halo/app/plugin/HaloPluginManager.java index 8589f099dc..6244f0fd22 100644 --- a/application/src/main/java/run/halo/app/plugin/HaloPluginManager.java +++ b/application/src/main/java/run/halo/app/plugin/HaloPluginManager.java @@ -18,16 +18,11 @@ import org.pf4j.PluginFactory; import org.pf4j.PluginLoader; import org.pf4j.PluginRepository; -import org.pf4j.PluginState; -import org.pf4j.PluginStateEvent; -import org.pf4j.PluginStateListener; import org.pf4j.PluginStatusProvider; import org.pf4j.PluginWrapper; import org.springframework.context.ApplicationContext; import org.springframework.data.util.Lazy; import run.halo.app.infra.SystemVersionSupplier; -import run.halo.app.plugin.event.HaloPluginStartedEvent; -import run.halo.app.plugin.event.HaloPluginStoppedEvent; /** * PluginManager to hold the main ApplicationContext. @@ -162,28 +157,4 @@ public ApplicationContext getSharedContext() { return sharedContext.get(); } - private class PluginStoppedEventAdapter implements PluginStateListener { - - @Override - public void pluginStateChanged(PluginStateEvent event) { - if (!PluginState.STOPPED.equals(event.getPluginState())) { - return; - } - var pluginWrapper = event.getPlugin(); - rootContext.publishEvent(new HaloPluginStoppedEvent(event.getSource(), pluginWrapper)); - } - } - - private class PluginStartedEventAdapter implements PluginStateListener { - - @Override - public void pluginStateChanged(PluginStateEvent event) { - if (!PluginState.STARTED.equals(event.getPluginState())) { - return; - } - // Indicate the state is started. - var pluginWrapper = event.getPlugin(); - rootContext.publishEvent(new HaloPluginStartedEvent(event.getSource(), pluginWrapper)); - } - } } From 93c8c8e205ab9f0ddd4ed2f439bdfedadb2e0301 Mon Sep 17 00:00:00 2001 From: John Niang Date: Thu, 25 Jan 2024 23:56:05 +0800 Subject: [PATCH 5/6] Refactor finder registry Signed-off-by: John Niang --- ...efaultPluginApplicationContextFactory.java | 6 + .../theme/finders/DefaultFinderRegistry.java | 110 ++++++++++++++ .../app/theme/finders/FinderRegistry.java | 136 +----------------- .../app/theme/finders/FinderRegistryTest.java | 30 +--- 4 files changed, 126 insertions(+), 156 deletions(-) create mode 100644 application/src/main/java/run/halo/app/theme/finders/DefaultFinderRegistry.java diff --git a/application/src/main/java/run/halo/app/plugin/DefaultPluginApplicationContextFactory.java b/application/src/main/java/run/halo/app/plugin/DefaultPluginApplicationContextFactory.java index 442e82120d..7fb283ebde 100644 --- a/application/src/main/java/run/halo/app/plugin/DefaultPluginApplicationContextFactory.java +++ b/application/src/main/java/run/halo/app/plugin/DefaultPluginApplicationContextFactory.java @@ -10,6 +10,7 @@ import java.util.stream.Stream; import lombok.extern.slf4j.Slf4j; import org.pf4j.PluginRuntimeException; +import org.springframework.beans.factory.support.DefaultBeanNameGenerator; import org.springframework.boot.env.PropertySourceLoader; import org.springframework.boot.env.YamlPropertySourceLoader; import org.springframework.context.ApplicationContext; @@ -65,6 +66,7 @@ public ApplicationContext create(String pluginId) { sw.start("Create"); var context = new PluginApplicationContext(pluginId); + context.setBeanNameGenerator(DefaultBeanNameGenerator.INSTANCE); context.registerShutdownHook(); context.setParent(pluginManager.getSharedContext()); @@ -85,6 +87,10 @@ public ApplicationContext create(String pluginId) { context.registerBean(AggregatedRouterFunction.class); beanFactory.registerSingleton("pluginWrapper", pluginWrapper); + if (pluginWrapper.getPlugin() instanceof SpringPlugin springPlugin) { + beanFactory.registerSingleton("pluginContext", springPlugin.getPluginContext()); + } + var rootContext = pluginManager.getRootContext(); rootContext.getBeanProvider(ViewNameResolver.class) .ifAvailable(viewNameResolver -> { diff --git a/application/src/main/java/run/halo/app/theme/finders/DefaultFinderRegistry.java b/application/src/main/java/run/halo/app/theme/finders/DefaultFinderRegistry.java new file mode 100644 index 0000000000..fd1a9499d4 --- /dev/null +++ b/application/src/main/java/run/halo/app/theme/finders/DefaultFinderRegistry.java @@ -0,0 +1,110 @@ +package run.halo.app.theme.finders; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; + +/** + * Finder registry for class annotated with {@link Finder}. + * + * @author guqing + * @since 2.0.0 + */ +@Component +public class DefaultFinderRegistry implements FinderRegistry, InitializingBean { + private final Map> pluginFindersLookup = new ConcurrentHashMap<>(); + private final Map finders = new ConcurrentHashMap<>(64); + + private final ApplicationContext applicationContext; + + public DefaultFinderRegistry(ApplicationContext applicationContext) { + this.applicationContext = applicationContext; + } + + Object get(String name) { + return finders.get(name); + } + + /** + * Given a name, register a Finder for it. + * + * @param name the canonical name + * @param finder the finder to be registered + * @throws IllegalStateException if the name is already existing + */ + void putFinder(String name, Object finder) { + if (finders.containsKey(name)) { + throw new IllegalStateException( + "Finder with name '" + name + "' is already registered"); + } + finders.put(name, finder); + } + + /** + * Register a finder. + * + * @param finder register a finder that annotated with {@link Finder} + * @return the name of the finder + */ + String putFinder(Object finder) { + var name = getFinderName(finder); + this.putFinder(name, finder); + return name; + } + + private String getFinderName(Object finder) { + var annotation = finder.getClass().getAnnotation(Finder.class); + if (annotation == null) { + // should never happen + throw new IllegalStateException("Finder must be annotated with @Finder"); + } + String name = annotation.value(); + if (name == null) { + name = finder.getClass().getSimpleName(); + } + return name; + } + + public void removeFinder(String name) { + finders.remove(name); + } + + public Map getFinders() { + return Map.copyOf(finders); + } + + @Override + public void afterPropertiesSet() { + // initialize finders from application context + applicationContext.getBeansWithAnnotation(Finder.class) + .forEach((beanName, finder) -> { + var finderName = getFinderName(finder); + this.putFinder(finderName, finder); + }); + } + + @Override + public void register(String pluginId, ApplicationContext pluginContext) { + pluginContext.getBeansWithAnnotation(Finder.class) + .forEach((beanName, finder) -> { + var finderName = getFinderName(finder); + this.putFinder(finderName, finder); + pluginFindersLookup + .computeIfAbsent(pluginId, ignored -> new ArrayList<>()) + .add(finderName); + }); + } + + @Override + public void unregister(String pluginId) { + var finderNames = pluginFindersLookup.remove(pluginId); + if (finderNames != null) { + finderNames.forEach(finders::remove); + } + } + +} diff --git a/application/src/main/java/run/halo/app/theme/finders/FinderRegistry.java b/application/src/main/java/run/halo/app/theme/finders/FinderRegistry.java index 7a368c5d22..7dfe034751 100644 --- a/application/src/main/java/run/halo/app/theme/finders/FinderRegistry.java +++ b/application/src/main/java/run/halo/app/theme/finders/FinderRegistry.java @@ -1,16 +1,7 @@ package run.halo.app.theme.finders; -import java.util.ArrayList; -import java.util.List; import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import org.springframework.beans.factory.InitializingBean; import org.springframework.context.ApplicationContext; -import org.springframework.context.event.EventListener; -import org.springframework.stereotype.Component; -import run.halo.app.plugin.SpringPlugin; -import run.halo.app.plugin.event.HaloPluginStartedEvent; -import run.halo.app.plugin.event.HaloPluginStoppedEvent; /** * Finder registry for class annotated with {@link Finder}. @@ -18,131 +9,12 @@ * @author guqing * @since 2.0.0 */ -@Component -public class FinderRegistry implements InitializingBean { - private final Map> pluginFindersLookup = new ConcurrentHashMap<>(); - private final Map finders = new ConcurrentHashMap<>(64); +public interface FinderRegistry { - private final ApplicationContext applicationContext; + Map getFinders(); - public FinderRegistry(ApplicationContext applicationContext) { - this.applicationContext = applicationContext; - } + void register(String pluginId, ApplicationContext pluginContext); - Object get(String name) { - return finders.get(name); - } + void unregister(String pluginId); - /** - * Given a name, register a Finder for it. - * - * @param name the canonical name - * @param finder the finder to be registered - * @throws IllegalStateException if the name is already existing - */ - public void registerFinder(String name, Object finder) { - if (finders.containsKey(name)) { - throw new IllegalStateException( - "Finder with name '" + name + "' is already registered"); - } - finders.put(name, finder); - } - - /** - * Register a finder. - * - * @param finder register a finder that annotated with {@link Finder} - * @return the name of the finder - */ - public String registerFinder(Object finder) { - Finder annotation = finder.getClass().getAnnotation(Finder.class); - if (annotation == null) { - throw new IllegalStateException("Finder must be annotated with @Finder"); - } - String name = annotation.value(); - if (name == null) { - name = finder.getClass().getSimpleName(); - } - this.registerFinder(name, finder); - return name; - } - - public void removeFinder(String name) { - finders.remove(name); - } - - public Map getFinders() { - return Map.copyOf(finders); - } - - @Override - public void afterPropertiesSet() throws Exception { - // initialize finders from application context - applicationContext.getBeansWithAnnotation(Finder.class) - .forEach((k, v) -> { - registerFinder(v); - }); - } - - /** - * Register finders for a plugin. - * - * @param event plugin started event - */ - @EventListener(HaloPluginStartedEvent.class) - public void onPluginStarted(HaloPluginStartedEvent event) { - var plugin = event.getPlugin().getPlugin(); - var pluginId = event.getPlugin().getPluginId(); - if (plugin instanceof SpringPlugin springPlugin) { - var context = springPlugin.getApplicationContext(); - context.getBeansWithAnnotation(Finder.class) - .forEach((beanName, finderObject) -> { - // register finder - String finderName = registerFinder(finderObject); - // add to plugin finder lookup - pluginFindersLookup.computeIfAbsent(pluginId, k -> new ArrayList<>()) - .add(finderName); - }); - } - } - - /** - * Remove finders registered by the plugin. - * - * @param event plugin stopped event - */ - @EventListener(HaloPluginStoppedEvent.class) - public void onPluginStopped(HaloPluginStoppedEvent event) { - String pluginId = event.getPlugin().getPluginId(); - boolean containsKey = pluginFindersLookup.containsKey(pluginId); - if (!containsKey) { - return; - } - pluginFindersLookup.get(pluginId).forEach(this::removeFinder); - } - - public void register(String pluginId, ApplicationContext pluginContext) { - pluginContext.getBeansWithAnnotation(Finder.class) - .forEach((beanName, finder) -> { - var finderName = registerFinder(finder); - pluginFindersLookup.computeIfAbsent(pluginId, igored -> new ArrayList<>()) - .add(finderName); - }); - } - - public void unregister(String pluginId) { - pluginFindersLookup.getOrDefault(pluginId, List.of()) - .forEach(this::removeFinder); - } - - /** - * Only for test. - * - * @param pluginId plugin id - * @param finderName finder name - */ - void addPluginFinder(String pluginId, String finderName) { - pluginFindersLookup.computeIfAbsent(pluginId, k -> new ArrayList<>()) - .add(finderName); - } } diff --git a/application/src/test/java/run/halo/app/theme/finders/FinderRegistryTest.java b/application/src/test/java/run/halo/app/theme/finders/FinderRegistryTest.java index bcaf9866fc..9dc9e8cbb1 100644 --- a/application/src/test/java/run/halo/app/theme/finders/FinderRegistryTest.java +++ b/application/src/test/java/run/halo/app/theme/finders/FinderRegistryTest.java @@ -2,18 +2,14 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Mockito.when; import java.util.Map; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; -import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; -import org.pf4j.PluginWrapper; import org.springframework.context.ApplicationContext; -import run.halo.app.plugin.event.HaloPluginStoppedEvent; /** * Tests for {@link FinderRegistry}. @@ -24,29 +20,29 @@ @ExtendWith(MockitoExtension.class) class FinderRegistryTest { - private FinderRegistry finderRegistry; + private DefaultFinderRegistry finderRegistry; @Mock private ApplicationContext applicationContext; @BeforeEach void setUp() { - finderRegistry = new FinderRegistry(applicationContext); + finderRegistry = new DefaultFinderRegistry(applicationContext); } @Test void registerFinder() { assertThatThrownBy(() -> { - finderRegistry.registerFinder(new Object()); + finderRegistry.putFinder(new Object()); }).isInstanceOf(IllegalStateException.class) .hasMessage("Finder must be annotated with @Finder"); - String s = finderRegistry.registerFinder(new FakeFinder()); + String s = finderRegistry.putFinder(new FakeFinder()); assertThat(s).isEqualTo("test"); } @Test void removeFinder() { - String s = finderRegistry.registerFinder(new FakeFinder()); + String s = finderRegistry.putFinder(new FakeFinder()); assertThat(s).isEqualTo("test"); Object test = finderRegistry.get("test"); assertThat(test).isNotNull(); @@ -60,25 +56,11 @@ void removeFinder() { void getFinders() { assertThat(finderRegistry.getFinders()).hasSize(0); - finderRegistry.registerFinder(new FakeFinder()); + finderRegistry.putFinder(new FakeFinder()); Map finders = finderRegistry.getFinders(); assertThat(finders).hasSize(1); } - @Test - void onPluginStopped() { - finderRegistry.registerFinder("a", new Object()); - finderRegistry.addPluginFinder("fake", "a"); - - HaloPluginStoppedEvent event = Mockito.mock(HaloPluginStoppedEvent.class); - PluginWrapper pluginWrapper = Mockito.mock(PluginWrapper.class); - when(event.getPlugin()).thenReturn(pluginWrapper); - when(pluginWrapper.getPluginId()).thenReturn("fake"); - - finderRegistry.onPluginStopped(event); - assertThat(finderRegistry.get("a")).isNull(); - } - @Finder("test") static class FakeFinder { From 55d7b544ad1cd8e77f587c0188b56f0ab4f2faf4 Mon Sep 17 00:00:00 2001 From: John Niang Date: Fri, 26 Jan 2024 14:04:39 +0800 Subject: [PATCH 6/6] Support shared event mechanism Signed-off-by: John Niang --- ...efaultPluginApplicationContextFactory.java | 36 ++++++++++++++++++- .../DefaultSharedEventListenerRegistry.java | 34 ++++++++++++++++++ .../PluginApplicationContextFactory.java | 2 +- .../plugin/SharedEventListenerRegistry.java | 12 +++++++ .../run/halo/app/plugin/SpringPlugin.java | 1 - .../halo/app/plugin/SpringPluginFactory.java | 1 - 6 files changed, 82 insertions(+), 4 deletions(-) create mode 100644 application/src/main/java/run/halo/app/plugin/DefaultSharedEventListenerRegistry.java rename application/src/main/java/run/halo/app/{ => plugin}/PluginApplicationContextFactory.java (91%) create mode 100644 application/src/main/java/run/halo/app/plugin/SharedEventListenerRegistry.java diff --git a/application/src/main/java/run/halo/app/plugin/DefaultPluginApplicationContextFactory.java b/application/src/main/java/run/halo/app/plugin/DefaultPluginApplicationContextFactory.java index 7fb283ebde..7142016a6a 100644 --- a/application/src/main/java/run/halo/app/plugin/DefaultPluginApplicationContextFactory.java +++ b/application/src/main/java/run/halo/app/plugin/DefaultPluginApplicationContextFactory.java @@ -14,6 +14,7 @@ import org.springframework.boot.env.PropertySourceLoader; import org.springframework.boot.env.YamlPropertySourceLoader; import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationListener; import org.springframework.context.event.ContextClosedEvent; import org.springframework.context.event.ContextRefreshedEvent; @@ -28,7 +29,6 @@ import org.springframework.web.reactive.function.server.RouterFunction; import org.springframework.web.reactive.function.server.ServerResponse; import reactor.core.Exceptions; -import run.halo.app.PluginApplicationContextFactory; import run.halo.app.extension.ReactiveExtensionClient; import run.halo.app.infra.properties.HaloProperties; import run.halo.app.plugin.event.HaloPluginBeforeStopEvent; @@ -133,6 +133,15 @@ public ApplicationContext create(String pluginId) { pluginRouterFunctionManager ); }); + + rootContext.getBeanProvider(SharedEventListenerRegistry.class) + .ifUnique(listenerRegistry -> { + var shareEventListenerAdapter = new ShareEventListenerAdapter(listenerRegistry); + beanFactory.registerSingleton( + "shareEventListenerAdapter", + shareEventListenerAdapter + ); + }); sw.stop(); sw.start("LoadComponents"); @@ -162,6 +171,31 @@ public ApplicationContext create(String pluginId) { return context; } + private static class ShareEventListenerAdapter { + + private final SharedEventListenerRegistry listenerRegistry; + + private ApplicationListener listener; + + private ShareEventListenerAdapter(SharedEventListenerRegistry listenerRegistry) { + this.listenerRegistry = listenerRegistry; + } + + @EventListener + public void onApplicationEvent(ContextRefreshedEvent event) { + this.listener = sharedEvent -> event.getApplicationContext().publishEvent(sharedEvent); + listenerRegistry.register(this.listener); + } + + @EventListener(ContextClosedEvent.class) + public void onApplicationEvent() { + if (this.listener != null) { + this.listenerRegistry.unregister(this.listener); + } + } + + } + private static class FinderManager { private final String pluginId; diff --git a/application/src/main/java/run/halo/app/plugin/DefaultSharedEventListenerRegistry.java b/application/src/main/java/run/halo/app/plugin/DefaultSharedEventListenerRegistry.java new file mode 100644 index 0000000000..28ab808411 --- /dev/null +++ b/application/src/main/java/run/halo/app/plugin/DefaultSharedEventListenerRegistry.java @@ -0,0 +1,34 @@ +package run.halo.app.plugin; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ApplicationListener; +import org.springframework.stereotype.Component; + +@Component +public class DefaultSharedEventListenerRegistry implements + ApplicationListener, SharedEventListenerRegistry { + + private final List> listeners; + + public DefaultSharedEventListenerRegistry() { + listeners = new CopyOnWriteArrayList<>(); + } + + @Override + public void onApplicationEvent(ApplicationEvent event) { + if (!event.getClass().isAnnotationPresent(SharedEvent.class)) { + return; + } + listeners.forEach(listener -> listener.onApplicationEvent(event)); + } + + public void register(ApplicationListener listener) { + this.listeners.add(listener); + } + + public void unregister(ApplicationListener listener) { + this.listeners.remove(listener); + } +} diff --git a/application/src/main/java/run/halo/app/PluginApplicationContextFactory.java b/application/src/main/java/run/halo/app/plugin/PluginApplicationContextFactory.java similarity index 91% rename from application/src/main/java/run/halo/app/PluginApplicationContextFactory.java rename to application/src/main/java/run/halo/app/plugin/PluginApplicationContextFactory.java index cccb9cebbf..e8f3ef0c7b 100644 --- a/application/src/main/java/run/halo/app/PluginApplicationContextFactory.java +++ b/application/src/main/java/run/halo/app/plugin/PluginApplicationContextFactory.java @@ -1,4 +1,4 @@ -package run.halo.app; +package run.halo.app.plugin; import org.springframework.context.ApplicationContext; diff --git a/application/src/main/java/run/halo/app/plugin/SharedEventListenerRegistry.java b/application/src/main/java/run/halo/app/plugin/SharedEventListenerRegistry.java new file mode 100644 index 0000000000..8ad73ce44c --- /dev/null +++ b/application/src/main/java/run/halo/app/plugin/SharedEventListenerRegistry.java @@ -0,0 +1,12 @@ +package run.halo.app.plugin; + +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ApplicationListener; + +public interface SharedEventListenerRegistry { + + void register(ApplicationListener listener); + + void unregister(ApplicationListener listener); + +} diff --git a/application/src/main/java/run/halo/app/plugin/SpringPlugin.java b/application/src/main/java/run/halo/app/plugin/SpringPlugin.java index c80842128f..2f6624f8dc 100644 --- a/application/src/main/java/run/halo/app/plugin/SpringPlugin.java +++ b/application/src/main/java/run/halo/app/plugin/SpringPlugin.java @@ -3,7 +3,6 @@ import org.pf4j.Plugin; import org.springframework.context.ApplicationContext; import org.springframework.context.ConfigurableApplicationContext; -import run.halo.app.PluginApplicationContextFactory; import run.halo.app.plugin.event.SpringPluginStartedEvent; import run.halo.app.plugin.event.SpringPluginStartingEvent; import run.halo.app.plugin.event.SpringPluginStoppingEvent; diff --git a/application/src/main/java/run/halo/app/plugin/SpringPluginFactory.java b/application/src/main/java/run/halo/app/plugin/SpringPluginFactory.java index da4fa015b2..f463e1d457 100644 --- a/application/src/main/java/run/halo/app/plugin/SpringPluginFactory.java +++ b/application/src/main/java/run/halo/app/plugin/SpringPluginFactory.java @@ -4,7 +4,6 @@ import org.pf4j.Plugin; import org.pf4j.PluginFactory; import org.pf4j.PluginWrapper; -import run.halo.app.PluginApplicationContextFactory; /** * The default implementation for PluginFactory.