From fed163689b59bda956468d7c6abfdf69a90eb5c2 Mon Sep 17 00:00:00 2001 From: guqing Date: Mon, 11 Sep 2023 15:04:39 +0800 Subject: [PATCH] feat: support running plugins from JAR in development mode --- .../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 f513514fec..b6f07b519a 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 @@ 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 @@ @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. + */ + 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 0000000000..01bae2f967 --- /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 2e2f8161e8..ca7e9441b4 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.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 @@ Optional lookupPluginSetting(String name, String settingName) { 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 @@ boolean waitForSettingCreation(Plugin plugin) { 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. @@ -804,11 +807,19 @@ static String initialReverseProxyName(String pluginName) { } 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(); + } + if (pluginWrapper instanceof HaloPluginWrapper haloPluginWrapper) { + return haloPluginWrapper.getRuntimeMode(); } - return RuntimeMode.DEVELOPMENT.equals(runtimeMode); + 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 c769ab8e29..211cdee0d7 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 Plugin create(PluginWrapper pluginWrapper) { 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 7abcb1d5f4..41a61a86e7 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 @@ protected PluginDescriptorFinder createPluginDescriptorFinder() { 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 0000000000..f784400ab4 --- /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 d437b61981..45c9d367f6 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.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 @@ private PluginApplicationContext createPluginApplicationContext(String pluginId) 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 @@ private void initApplicationContext(String pluginId) { 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 40d4df246f..bd74031438 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 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 @@ protected PluginLoader createPluginLoader() { } } 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 ClassLoader loadPlugin(Path pluginPath, getClass().getClassLoader(), ClassLoadingStrategy.APD); pluginClassLoader.addFile(pluginPath.toFile()); return pluginClassLoader; - } - }, this::isNotDevelopment); + }); } } @@ -167,9 +141,8 @@ protected PluginRepository createPluginRepository() { .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 @@ protected PluginRepository createPluginRepository() { 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 4bf0c5b22f..6477f48115 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.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 @@ * @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 @@ private Object[] nullParameters(final Constructor constructor) { 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 00228014f1..02b9ffc017 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 @@ -51,6 +51,7 @@ 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; @@ -70,7 +71,7 @@ class PluginReconcilerTest { ExtensionClient extensionClient; @Mock - PluginWrapper pluginWrapper; + HaloPluginWrapper pluginWrapper; @Mock ApplicationEventPublisher eventPublisher;