From 1f0cfc18e39dbc22cb97a08f221b0f81f52875b9 Mon Sep 17 00:00:00 2001 From: guqing <38999863+guqing@users.noreply.github.com> Date: Wed, 27 Sep 2023 10:16:16 +0800 Subject: [PATCH] feat: support running plugins from JAR in development mode (#4589) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #### What type of PR is this? /kind feature /milestone 2.10.x /area core #### What this PR does / why we need it: 支持在开发模式下通过 JAR 运行插件 *从此版本开始 BasePlugin 的子类建议使用 BasePlugin(PluginContext context) 构造函数,而不要使用之前的 BasePlugin(PluginWrapper wrapper) 构造函数。BasePlugin(PluginWrapper wrapper) 构造函数将计划在后续版本移除* ,当移除构造函数后不再将 PluginWrapper 暴露给插件使用,它只应该在 halo core 使用。 how to test it? 1. 测试开发模式下配置的 `halo.plugin.fixed-plugin-path` 插件是否正确运行 2. 测试开发模式下通过 JAR 包安装插件是否正确运行 3. 测试生产模式下是否能通过项目目录的方式运行插件,期望是生产模式不可以运行开发模式的插件 4. 测试开发模式和生产模式的插件卸载功能是否正确 #### Which issue(s) this PR fixes: Fixes #2908 #### Does this PR introduce a user-facing change? ```release-note 支持在开发模式下通过 JAR 运行插件 ``` --- .../java/run/halo/app/plugin/BasePlugin.java | 36 ++++++ .../run/halo/app/plugin/PluginContext.java | 27 +++++ .../reconciler/PluginReconciler.java | 23 +++- .../halo/app/plugin/BasePluginFactory.java | 8 +- .../halo/app/plugin/HaloPluginManager.java | 11 ++ .../halo/app/plugin/HaloPluginWrapper.java | 34 ++++++ .../plugin/PluginApplicationInitializer.java | 12 ++ .../app/plugin/PluginAutoConfiguration.java | 72 ++++++------ .../app/plugin/SpringExtensionFactory.java | 103 ++++++------------ .../reconciler/PluginReconcilerTest.java | 3 +- 10 files changed, 216 insertions(+), 113 deletions(-) create mode 100644 api/src/main/java/run/halo/app/plugin/PluginContext.java create mode 100644 application/src/main/java/run/halo/app/plugin/HaloPluginWrapper.java diff --git a/api/src/main/java/run/halo/app/plugin/BasePlugin.java b/api/src/main/java/run/halo/app/plugin/BasePlugin.java index f513514fe..d010998c5 100644 --- a/api/src/main/java/run/halo/app/plugin/BasePlugin.java +++ b/api/src/main/java/run/halo/app/plugin/BasePlugin.java @@ -3,6 +3,8 @@ package run.halo.app.plugin; import lombok.extern.slf4j.Slf4j; import org.pf4j.Plugin; import org.pf4j.PluginWrapper; +import org.springframework.lang.NonNull; +import org.springframework.util.Assert; /** * This class will be extended by all plugins and serve as the common class between a plugin and @@ -14,12 +16,46 @@ import org.pf4j.PluginWrapper; @Slf4j public class BasePlugin extends Plugin { + protected PluginContext context; + @Deprecated public BasePlugin(PluginWrapper wrapper) { super(wrapper); log.info("Initialized plugin {}", wrapper.getPluginId()); } + /** + * Constructor a plugin with the given plugin context. + * TODO Mark {@link PluginContext} as final to prevent modification. + * + * @param pluginContext plugin context must not be null. + */ + public BasePlugin(PluginContext pluginContext) { + this.context = pluginContext; + } + + /** + * use {@link #BasePlugin(PluginContext)} instead of. + * + * @deprecated since 2.10.0 + */ public BasePlugin() { } + + /** + * Compatible with old constructors, if the plugin is not use + * {@link #BasePlugin(PluginContext)} constructor then base plugin factory will use this + * method to set plugin context. + * + * @param context plugin context must not be null. + */ + final void setContext(PluginContext context) { + Assert.notNull(context, "Plugin context must not be null"); + this.context = context; + } + + @NonNull + public PluginContext getContext() { + return context; + } } diff --git a/api/src/main/java/run/halo/app/plugin/PluginContext.java b/api/src/main/java/run/halo/app/plugin/PluginContext.java new file mode 100644 index 000000000..01bae2f96 --- /dev/null +++ b/api/src/main/java/run/halo/app/plugin/PluginContext.java @@ -0,0 +1,27 @@ +package run.halo.app.plugin; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.pf4j.RuntimeMode; + +/** + *

This class will provide a context for the plugin, which will be used to store some + * information about the plugin.

+ *

An instance of this class is provided to plugins in their constructor.

+ *

It's safe for plugins to keep a reference to the instance for later use.

+ *

This class facilitates communication with application and plugin manager.

+ *

Pf4j recommends that you use a custom PluginContext instead of PluginWrapper.

+ * Use application custom PluginContext instead of PluginWrapper + * + * @author guqing + * @since 2.10.0 + */ +@Getter +@RequiredArgsConstructor +public class PluginContext { + private final String name; + + private final String version; + + private final RuntimeMode runtimeMode; +} 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 92d0750bf..5361b08a8 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 @@ -60,6 +60,7 @@ import run.halo.app.infra.utils.JsonUtils; import run.halo.app.infra.utils.PathUtils; import run.halo.app.infra.utils.YamlUnstructuredLoader; import run.halo.app.plugin.HaloPluginManager; +import run.halo.app.plugin.HaloPluginWrapper; import run.halo.app.plugin.PluginConst; import run.halo.app.plugin.PluginExtensionLoaderUtils; import run.halo.app.plugin.PluginStartingError; @@ -184,11 +185,12 @@ public class PluginReconciler implements Reconciler { Assert.notNull(name, "Plugin name must not be null"); Assert.notNull(settingName, "Setting name must not be null"); PluginWrapper pluginWrapper = getPluginWrapper(name); + var runtimeMode = getRuntimeMode(name); var resourceLoader = new DefaultResourceLoader(pluginWrapper.getPluginClassLoader()); return PluginExtensionLoaderUtils.lookupExtensions(pluginWrapper.getPluginPath(), - pluginWrapper.getRuntimeMode()) + runtimeMode) .stream() .map(resourceLoader::getResource) .filter(Resource::exists) @@ -215,6 +217,7 @@ public class PluginReconciler implements Reconciler { return false; } + var runtimeMode = getRuntimeMode(pluginName); Optional settingOption = lookupPluginSetting(pluginName, settingName) .map(setting -> { // This annotation is added to prevent it from being deleted when stopped. @@ -802,11 +805,19 @@ public class PluginReconciler implements Reconciler { } private boolean isDevelopmentMode(String name) { - PluginWrapper pluginWrapper = haloPluginManager.getPlugin(name); - RuntimeMode runtimeMode = haloPluginManager.getRuntimeMode(); - if (pluginWrapper != null) { - runtimeMode = pluginWrapper.getRuntimeMode(); + return RuntimeMode.DEVELOPMENT.equals(getRuntimeMode(name)); + } + + private RuntimeMode getRuntimeMode(String name) { + var pluginWrapper = haloPluginManager.getPlugin(name); + if (pluginWrapper == null) { + return haloPluginManager.getRuntimeMode(); } - return RuntimeMode.DEVELOPMENT.equals(runtimeMode); + if (pluginWrapper instanceof HaloPluginWrapper haloPluginWrapper) { + return haloPluginWrapper.getRuntimeMode(); + } + return Files.isDirectory(pluginWrapper.getPluginPath()) + ? RuntimeMode.DEVELOPMENT + : RuntimeMode.DEPLOYMENT; } } diff --git a/application/src/main/java/run/halo/app/plugin/BasePluginFactory.java b/application/src/main/java/run/halo/app/plugin/BasePluginFactory.java index c769ab8e2..211cdee0d 100644 --- a/application/src/main/java/run/halo/app/plugin/BasePluginFactory.java +++ b/application/src/main/java/run/halo/app/plugin/BasePluginFactory.java @@ -23,13 +23,17 @@ public class BasePluginFactory implements PluginFactory { return getPluginContext(pluginWrapper) .map(context -> { try { - return context.getBean(BasePlugin.class); + 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(); - BasePlugin pluginInstance = new BasePlugin(); + var pluginContext = beanFactory.getBean(PluginContext.class); + BasePlugin pluginInstance = new BasePlugin(pluginContext); beanFactory.registerSingleton(Plugin.class.getName(), pluginInstance); return pluginInstance; } 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 7abcb1d5f..41a61a86e 100644 --- a/application/src/main/java/run/halo/app/plugin/HaloPluginManager.java +++ b/application/src/main/java/run/halo/app/plugin/HaloPluginManager.java @@ -108,6 +108,17 @@ public class HaloPluginManager extends DefaultPluginManager return new YamlPluginDescriptorFinder(); } + @Override + protected PluginWrapper createPluginWrapper(PluginDescriptor pluginDescriptor, Path pluginPath, + ClassLoader pluginClassLoader) { + // create the plugin wrapper + log.debug("Creating wrapper for plugin '{}'", pluginPath); + HaloPluginWrapper pluginWrapper = + new HaloPluginWrapper(this, pluginDescriptor, pluginPath, pluginClassLoader); + pluginWrapper.setPluginFactory(getPluginFactory()); + return pluginWrapper; + } + @Override protected void firePluginStateEvent(PluginStateEvent event) { rootApplicationContext.publishEvent( diff --git a/application/src/main/java/run/halo/app/plugin/HaloPluginWrapper.java b/application/src/main/java/run/halo/app/plugin/HaloPluginWrapper.java new file mode 100644 index 000000000..f784400ab --- /dev/null +++ b/application/src/main/java/run/halo/app/plugin/HaloPluginWrapper.java @@ -0,0 +1,34 @@ +package run.halo.app.plugin; + +import java.nio.file.Files; +import java.nio.file.Path; +import org.pf4j.PluginDescriptor; +import org.pf4j.PluginManager; +import org.pf4j.PluginWrapper; +import org.pf4j.RuntimeMode; + +/** + * A wrapper over plugin instance for Halo. + * + * @author guqing + * @since 2.10.0 + */ +public class HaloPluginWrapper extends PluginWrapper { + + private final RuntimeMode runtimeMode; + + /** + * Creates a new plugin wrapper to manage the specified plugin. + */ + public HaloPluginWrapper(PluginManager pluginManager, PluginDescriptor descriptor, + Path pluginPath, ClassLoader pluginClassLoader) { + super(pluginManager, descriptor, pluginPath, pluginClassLoader); + this.runtimeMode = Files.isDirectory(pluginPath) + ? RuntimeMode.DEVELOPMENT : RuntimeMode.DEPLOYMENT; + } + + @Override + public RuntimeMode getRuntimeMode() { + return runtimeMode; + } +} diff --git a/application/src/main/java/run/halo/app/plugin/PluginApplicationInitializer.java b/application/src/main/java/run/halo/app/plugin/PluginApplicationInitializer.java index d437b6198..45c9d367f 100644 --- a/application/src/main/java/run/halo/app/plugin/PluginApplicationInitializer.java +++ b/application/src/main/java/run/halo/app/plugin/PluginApplicationInitializer.java @@ -9,6 +9,7 @@ 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; @@ -88,6 +89,8 @@ public class PluginApplicationInitializer { AnnotationConfigUtils.registerAnnotationConfigProcessors(beanFactory); stopWatch.stop(); + beanFactory.registerSingleton("pluginContext", createPluginContext(plugin)); + // TODO deprecated beanFactory.registerSingleton("pluginWrapper", haloPluginManager.getPlugin(pluginId)); populateSettingFetcher(pluginId, beanFactory); @@ -131,6 +134,15 @@ public class PluginApplicationInitializer { 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 = 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 40d4df246..bd7403143 100644 --- a/application/src/main/java/run/halo/app/plugin/PluginAutoConfiguration.java +++ b/application/src/main/java/run/halo/app/plugin/PluginAutoConfiguration.java @@ -5,6 +5,7 @@ import static run.halo.app.plugin.resources.BundleResourceUtils.getJsBundleResou 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 lombok.extern.slf4j.Slf4j; @@ -109,33 +110,7 @@ public class PluginAutoConfiguration { } } else { return new CompoundPluginLoader() - .add(new DevelopmentPluginLoader(this) { - - @Override - protected PluginClassLoader createPluginClassLoader(Path pluginPath, - PluginDescriptor pluginDescriptor) { - return new PluginClassLoader(pluginManager, pluginDescriptor, - getClass().getClassLoader(), ClassLoadingStrategy.APD); - } - - @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); - } - }, this::isDevelopment) + .add(createDevelopmentPluginLoader(this), this::isDevelopment) .add(new JarPluginLoader(this) { @Override public ClassLoader loadPlugin(Path pluginPath, @@ -145,9 +120,8 @@ public class PluginAutoConfiguration { getClass().getClassLoader(), ClassLoadingStrategy.APD); pluginClassLoader.addFile(pluginPath.toFile()); return pluginClassLoader; - } - }, this::isNotDevelopment); + }); } } @@ -167,9 +141,8 @@ public class PluginAutoConfiguration { .setFixedPaths(pluginProperties.getFixedPluginPath()); return new CompoundPluginRepository() .add(developmentPluginRepository, this::isDevelopment) - .add(new JarPluginRepository(getPluginsRoots()), this::isNotDevelopment) - .add(new DefaultPluginRepository(getPluginsRoots()), - this::isNotDevelopment); + .add(new JarPluginRepository(getPluginsRoots())) + .add(new DefaultPluginRepository(getPluginsRoots())); } }; @@ -181,6 +154,41 @@ public class PluginAutoConfiguration { return pluginManager; } + DevelopmentPluginLoader createDevelopmentPluginLoader(PluginManager pluginManager) { + return new DevelopmentPluginLoader(pluginManager) { + @Override + protected PluginClassLoader createPluginClassLoader(Path pluginPath, + PluginDescriptor pluginDescriptor) { + return new PluginClassLoader(pluginManager, pluginDescriptor, + getClass().getClassLoader(), ClassLoadingStrategy.APD); + } + + @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(); } 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 4bf0c5b22..6477f4811 100644 --- a/application/src/main/java/run/halo/app/plugin/SpringExtensionFactory.java +++ b/application/src/main/java/run/halo/app/plugin/SpringExtensionFactory.java @@ -6,11 +6,12 @@ import java.util.Comparator; import java.util.Objects; import java.util.Optional; import java.util.stream.Stream; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.pf4j.Extension; import org.pf4j.ExtensionFactory; -import org.pf4j.Plugin; 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; @@ -52,46 +53,18 @@ import org.springframework.lang.Nullable; * @since 2.0.0 */ @Slf4j +@RequiredArgsConstructor public class SpringExtensionFactory implements ExtensionFactory { - public static final boolean AUTOWIRE_BY_DEFAULT = true; - /** * The plugin manager is used for retrieving a plugin from a given extension class and as a * fallback supplier of an application context. */ protected final PluginManager pluginManager; - /** - * Indicates if springs autowiring possibilities should be used. - */ - protected final boolean autowire; - - public SpringExtensionFactory(PluginManager pluginManager) { - this(pluginManager, AUTOWIRE_BY_DEFAULT); - } - - public SpringExtensionFactory(final PluginManager pluginManager, final boolean autowire) { - this.pluginManager = pluginManager; - this.autowire = autowire; - if (!autowire) { - log.warn( - "Autowiring is disabled although the only reason for existence of this special " - + "factory is" - + - " supporting spring and its application context."); - } - } - @Override @Nullable public T create(Class extensionClass) { - if (!this.autowire) { - log.warn("Create instance of '" + nameOf(extensionClass) - + "' without using springs possibilities as" - + " autowiring is disabled."); - return createWithoutSpring(extensionClass); - } Optional contextOptional = getPluginApplicationContextBy(extensionClass); if (contextOptional.isPresent()) { @@ -154,52 +127,38 @@ public class SpringExtensionFactory implements ExtensionFactory { protected Optional getPluginApplicationContextBy( final Class extensionClass) { - final Plugin plugin = Optional.ofNullable(this.pluginManager.whichPlugin(extensionClass)) + return Optional.ofNullable(this.pluginManager.whichPlugin(extensionClass)) .map(PluginWrapper::getPlugin) - .orElse(null); - - final PluginApplicationContext applicationContext; - - if (plugin instanceof BasePlugin) { - log.debug( - " Extension class ' " + nameOf(extensionClass) + "' belongs to halo-plugin '" - + nameOf(plugin) - + "' and will be autowired by using its application context."); - applicationContext = ExtensionContextRegistry.getInstance() - .getByPluginId(plugin.getWrapper().getPluginId()); - return Optional.of(applicationContext); - } else if (this.pluginManager instanceof HaloPluginManager && plugin != null) { - log.debug(" 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"); - String pluginId = plugin.getWrapper().getPluginId(); - applicationContext = ((HaloPluginManager) this.pluginManager) - .getPluginApplicationContext(pluginId); - } else { - log.warn(" No application contexts can be used for instantiating extension class '" - + nameOf(extensionClass) + "'." - + " This extension neither belongs to a halo-plugin (id: '" + nameOf(plugin) - + "') nor is the used" - + " plugin manager a spring-plugin-manager (used manager: '" - + nameOf(this.pluginManager.getClass()) + "')." - + " At perspective of PF4J this seems highly uncommon in combination with a factory" - + " which only reason for existence" - + " is using spring (and its application context) and should at least be reviewed. " - + "In fact no autowiring can be" - + " applied although autowire flag was set to 'true'. Instantiating will fallback " - + "to standard Java reflection."); - applicationContext = null; - } - - return Optional.ofNullable(applicationContext); + .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) { + log.debug(" 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); + } + log.debug( + " 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 Plugin plugin) { + private String nameOf(final BasePlugin plugin) { return Objects.nonNull(plugin) - ? plugin.getWrapper().getPluginId() + ? plugin.getContext().getName() : "system"; } diff --git a/application/src/test/java/run/halo/app/core/extension/reconciler/PluginReconcilerTest.java b/application/src/test/java/run/halo/app/core/extension/reconciler/PluginReconcilerTest.java index 811e2c927..a2e46134e 100644 --- a/application/src/test/java/run/halo/app/core/extension/reconciler/PluginReconcilerTest.java +++ b/application/src/test/java/run/halo/app/core/extension/reconciler/PluginReconcilerTest.java @@ -50,6 +50,7 @@ import run.halo.app.extension.controller.Reconciler; import run.halo.app.infra.utils.FileUtils; import run.halo.app.infra.utils.JsonUtils; import run.halo.app.plugin.HaloPluginManager; +import run.halo.app.plugin.HaloPluginWrapper; import run.halo.app.plugin.PluginConst; import run.halo.app.plugin.PluginStartingError; @@ -69,7 +70,7 @@ class PluginReconcilerTest { ExtensionClient extensionClient; @Mock - PluginWrapper pluginWrapper; + HaloPluginWrapper pluginWrapper; @Mock ApplicationEventPublisher eventPublisher;