Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support running plugins from JAR in development mode #4589

Merged
merged 6 commits into from
Sep 27, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions api/src/main/java/run/halo/app/plugin/BasePlugin.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
*/
final void setContext(PluginContext context) {
Assert.notNull(context, "Plugin context must not be null");
this.context = context;
}

@NonNull
public PluginContext getContext() {
return context;
}
}
27 changes: 27 additions & 0 deletions api/src/main/java/run/halo/app/plugin/PluginContext.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package run.halo.app.plugin;

import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.pf4j.RuntimeMode;

/**
* <p>This class will provide a context for the plugin, which will be used to store some
* information about the plugin.</p>
* <p>An instance of this class is provided to plugins in their constructor.</p>
* <p>It's safe for plugins to keep a reference to the instance for later use.</p>
* <p>This class facilitates communication with application and plugin manager.</p>
* <p>Pf4j recommends that you use a custom PluginContext instead of PluginWrapper.</p>
* <a href="https://github.com/pf4j/pf4j/blob/e4d7c7b9ea0c9a32179c3e33da1403228838944f/pf4j/src/main/java/org/pf4j/Plugin.java#L46">Use application custom PluginContext instead of PluginWrapper</a>
*
* @author guqing
* @since 2.10.0
*/
@Getter
@RequiredArgsConstructor
public class PluginContext {
private final String name;

private final String version;

private final RuntimeMode runtimeMode;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -184,11 +185,12 @@ Optional<Setting> 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)
Expand All @@ -215,6 +217,7 @@ boolean waitForSettingCreation(Plugin plugin) {
return false;
}

var runtimeMode = getRuntimeMode(pluginName);
Optional<Setting> settingOption = lookupPluginSetting(pluginName, settingName)
.map(setting -> {
// This annotation is added to prevent it from being deleted when stopped.
Expand Down Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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 =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand All @@ -145,9 +120,8 @@ public ClassLoader loadPlugin(Path pluginPath,
getClass().getClassLoader(), ClassLoadingStrategy.APD);
pluginClassLoader.addFile(pluginPath.toFile());
return pluginClassLoader;

}
}, this::isNotDevelopment);
});
}
}

Expand All @@ -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()));
}
};

Expand All @@ -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();
}
Expand Down
Loading