mirror of https://github.com/halo-dev/halo
fix: optional plugin dependencies not working correctly (#7094)
#### What type of PR is this? /kind bug /area core /milestone 2.20.x #### What this PR does / why we need it: 修复可选插件依赖功能无法正常工作的问题 #### Special notes for your reviewer: 使用以下两个插件测试可选依赖: [测试插件集合.zip](https://github.com/user-attachments/files/17989250/default.zip) 使用以下测试用例进行测试: 测试用例1:plugin-feed 插件提供 RSS 扩展功能 - **前置条件:** 安装并启用 `plugin-feed` 插件。 - **操作步骤:** 访问 `http://localhost:8090/feed/rss.xml`。 - **期望结果:** 返回 `plugin-feed` 提供的 RSS 内容。 --- 测试用例 2: plugin-moments 扩展了 plugin-feed 的 RSS 功能(依赖于 plugin-feed) - **前置条件:** 安装并启用 `plugin-feed` 和 `plugin-moments` 插件。 - **操作步骤:** 访问 `http://localhost:8090/feed/moments/rss.xml`。 - **期望结果:** 返回 `plugin-moments` 提供的 RSS 内容。 --- 测试用例 3: plugin-feed 启用时安装 plugin-moments - **前置条件:** 启用 `plugin-feed` 插件。 - **操作步骤:** 1. 安装 `plugin-moments` 插件。 2. 访问 `http://localhost:8090/feed/moments/rss.xml`。 - **期望结果:** `plugin-moments` 提供的 RSS 路由可访问,并返回正确内容。 --- 测试用例 4: plugin-feed 未启用时安装 plugin-moments - **前置条件:** 未安装或未启用 `plugin-feed` 插件。 - **操作步骤:** 1. 安装并启用 `plugin-moments` 插件。 2. 访问 `http://localhost:8090/feed/moments/rss.xml`。 - **期望结果:** - `plugin-moments` 的 RSS 路由不可访问,返回 404。 - `plugin-moments` 的其他功能正常运行。 --- 测试用例 5: plugin-moments 启用后安装 plugin-feed - **前置条件:** 已安装并启用 `plugin-moments` 插件。 - **操作步骤:** 1. 安装并启用 `plugin-feed` 插件。 2. 访问 `http://localhost:8090/feed/moments/rss.xml`。 - **期望结果:** `plugin-moments` 提供的 RSS 路由可访问,并返回正确内容。 --- 测试用例 6: 停止 plugin-feed 后验证 RSS 路由 - **前置条件:** 已启用 `plugin-feed` 和 `plugin-moments` 插件。 - **操作步骤:** 1. 停止 `plugin-feed` 插件。 2. 访问 `http://localhost:8090/feed/moments/rss.xml`。 - **期望结果:** - `plugin-feed` 停止成功。 - `plugin-moments` 提供的 RSS 路由不可访问,返回 404。 --- 测试用例 7: 重启 Halo 后验证可选依赖插件的启动顺序 - **前置条件:** 已启用 `plugin-feed` 和 `plugin-moments` 插件。 - **操作步骤:** 1. 重启 Halo 服务。 2. 访问 `http://localhost:8090/feed/moments/rss.xml`。 - **期望结果:** - `plugin-moments` 提供的 RSS 路由**始终可访问**。 --- 测试用例 8: 必选依赖插件验证 - **场景 1: 安装 seo 插件时未安装应用市场** - **前置条件:** 未安装 `app-store-integration` 插件。 - **操作步骤:** 安装 `plugin-seo` 插件。 - **期望结果:** 提示需要先安装 `app-store-integration` 插件。 - **场景 2: 停止应用市场插件时 seo 插件仍启用** - **前置条件:** 已启用 `app-store-integration` 和 `plugin-seo` 插件。 - **操作步骤:** 停止 `app-store-integration` 插件。 - **期望结果:** 提示需要先停止 `plugin-seo` 插件。 #### Does this PR introduce a user-facing change? ```release-note 修复可选插件依赖功能无法正常工作的问题 ```pull/7103/head
parent
d06b40cb0c
commit
fef06edcd8
|
@ -32,7 +32,6 @@ import java.util.function.Predicate;
|
|||
import java.util.function.Supplier;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.pf4j.PluginDependency;
|
||||
import org.pf4j.PluginState;
|
||||
import org.pf4j.PluginWrapper;
|
||||
import org.pf4j.RuntimeMode;
|
||||
|
@ -48,6 +47,7 @@ import run.halo.app.extension.ConfigMap;
|
|||
import run.halo.app.extension.ExtensionClient;
|
||||
import run.halo.app.extension.ExtensionUtil;
|
||||
import run.halo.app.extension.Metadata;
|
||||
import run.halo.app.extension.MetadataUtil;
|
||||
import run.halo.app.extension.Unstructured;
|
||||
import run.halo.app.extension.controller.Controller;
|
||||
import run.halo.app.extension.controller.ControllerBuilder;
|
||||
|
@ -60,8 +60,10 @@ import run.halo.app.infra.ConditionStatus;
|
|||
import run.halo.app.infra.utils.PathUtils;
|
||||
import run.halo.app.infra.utils.SettingUtils;
|
||||
import run.halo.app.infra.utils.YamlUnstructuredLoader;
|
||||
import run.halo.app.plugin.OptionalDependentResolver;
|
||||
import run.halo.app.plugin.PluginConst;
|
||||
import run.halo.app.plugin.PluginProperties;
|
||||
import run.halo.app.plugin.PluginService;
|
||||
import run.halo.app.plugin.SpringPluginManager;
|
||||
|
||||
/**
|
||||
|
@ -85,13 +87,16 @@ public class PluginReconciler implements Reconciler<Request> {
|
|||
|
||||
private final PluginProperties pluginProperties;
|
||||
|
||||
private final PluginService pluginService;
|
||||
|
||||
private Clock clock;
|
||||
|
||||
public PluginReconciler(ExtensionClient client, SpringPluginManager pluginManager,
|
||||
PluginProperties pluginProperties) {
|
||||
PluginProperties pluginProperties, PluginService pluginService) {
|
||||
this.client = client;
|
||||
this.pluginManager = pluginManager;
|
||||
this.pluginProperties = pluginProperties;
|
||||
this.pluginService = pluginService;
|
||||
this.clock = Clock.systemUTC();
|
||||
}
|
||||
|
||||
|
@ -280,18 +285,9 @@ public class PluginReconciler implements Reconciler<Request> {
|
|||
status.setPhase(Plugin.Phase.STARTING);
|
||||
|
||||
// check if the parent plugin is started
|
||||
var pw = pluginManager.getPlugin(pluginName);
|
||||
var unstartedDependencies = pw.getDescriptor().getDependencies()
|
||||
.stream()
|
||||
.filter(pd -> {
|
||||
if (pd.isOptional()) {
|
||||
return false;
|
||||
}
|
||||
var parent = pluginManager.getPlugin(pd.getPluginId());
|
||||
return parent == null || !PluginState.STARTED.equals(parent.getPluginState());
|
||||
})
|
||||
.map(PluginDependency::getPluginId)
|
||||
.toList();
|
||||
var unstartedDependencies = pluginService.getRequiredDependencies(plugin, pw ->
|
||||
pw == null || !PluginState.STARTED.equals(pw.getPluginState())
|
||||
);
|
||||
var conditions = status.getConditions();
|
||||
if (!CollectionUtils.isEmpty(unstartedDependencies)) {
|
||||
removeConditionBy(conditions, ConditionType.READY);
|
||||
|
@ -337,9 +333,31 @@ public class PluginReconciler implements Reconciler<Request> {
|
|||
status.setPhase(Plugin.Phase.STARTED);
|
||||
|
||||
log.info("Started plugin {}", pluginName);
|
||||
|
||||
requestToReloadPluginsOptionallyDependentOn(pluginName);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
void requestToReloadPluginsOptionallyDependentOn(String pluginName) {
|
||||
var startedPlugins = pluginManager.getStartedPlugins()
|
||||
.stream()
|
||||
.map(PluginWrapper::getDescriptor)
|
||||
.toList();
|
||||
var resolver = new OptionalDependentResolver(startedPlugins);
|
||||
var dependents = resolver.getOptionalDependents(pluginName);
|
||||
for (String dependentName : dependents) {
|
||||
client.fetch(Plugin.class, dependentName)
|
||||
.ifPresent(childPlugin -> {
|
||||
var annotations = MetadataUtil.nullSafeAnnotations(childPlugin);
|
||||
// loadLocation never be null for started plugins
|
||||
annotations.put(RELOAD_ANNO,
|
||||
childPlugin.getStatus().getLoadLocation().toString());
|
||||
client.update(childPlugin);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private Result disablePlugin(Plugin plugin) {
|
||||
var pluginName = plugin.getMetadata().getName();
|
||||
var status = plugin.getStatus();
|
||||
|
@ -515,15 +533,9 @@ public class PluginReconciler implements Reconciler<Request> {
|
|||
}
|
||||
|
||||
// check dependencies before loading
|
||||
var unresolvedParentPlugins = plugin.getSpec().getPluginDependencies().keySet()
|
||||
.stream()
|
||||
.filter(dependency -> {
|
||||
var parentPlugin = pluginManager.getPlugin(dependency);
|
||||
return parentPlugin == null
|
||||
|| pluginManager.getUnresolvedPlugins().contains(parentPlugin);
|
||||
})
|
||||
.sorted()
|
||||
.toList();
|
||||
var unresolvedParentPlugins = pluginService.getRequiredDependencies(plugin,
|
||||
pw -> pw == null || pluginManager.getUnresolvedPlugins().contains(pw)
|
||||
);
|
||||
if (!unresolvedParentPlugins.isEmpty()) {
|
||||
// requeue if the parent plugin is not loaded yet.
|
||||
removeConditionBy(conditions, ConditionType.INITIALIZED);
|
||||
|
|
|
@ -11,6 +11,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.beans.factory.support.DefaultListableBeanFactory;
|
||||
import org.springframework.boot.env.PropertySourceLoader;
|
||||
import org.springframework.boot.env.YamlPropertySourceLoader;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
|
@ -65,13 +66,33 @@ public class DefaultPluginApplicationContextFactory implements PluginApplication
|
|||
|
||||
var sw = new StopWatch("CreateApplicationContextFor" + pluginId);
|
||||
sw.start("Create");
|
||||
var context = new PluginApplicationContext(pluginId, pluginManager);
|
||||
|
||||
var pluginWrapper = pluginManager.getPlugin(pluginId);
|
||||
var classLoader = pluginWrapper.getPluginClassLoader();
|
||||
|
||||
/*
|
||||
* Manually creating a BeanFactory and setting the plugin's ClassLoader is necessary
|
||||
* to ensure that conditional annotations (e.g., @ConditionalOnClass) within the plugin
|
||||
* context can correctly load classes.
|
||||
* When PluginApplicationContext is created, its constructor initializes an
|
||||
* AnnotatedBeanDefinitionReader, which in turn creates a ConditionEvaluator.
|
||||
* ConditionEvaluator is responsible for evaluating conditional annotations such as
|
||||
* @ConditionalOnClass.
|
||||
* It relies on the BeanDefinitionRegistry's ClassLoader to load the classes specified in
|
||||
* the annotations.
|
||||
* Without explicitly setting the plugin's ClassLoader, the default application
|
||||
* ClassLoader is used, which fails to load classes from the plugin.
|
||||
* Therefore, a custom DefaultListableBeanFactory with the plugin ClassLoader is required
|
||||
* to resolve this issue.
|
||||
*/
|
||||
var beanFactory = new DefaultListableBeanFactory();
|
||||
beanFactory.setBeanClassLoader(classLoader);
|
||||
|
||||
var context = new PluginApplicationContext(pluginId, pluginManager, beanFactory);
|
||||
context.setBeanNameGenerator(DefaultBeanNameGenerator.INSTANCE);
|
||||
context.registerShutdownHook();
|
||||
context.setParent(pluginManager.getSharedContext());
|
||||
|
||||
var pluginWrapper = pluginManager.getPlugin(pluginId);
|
||||
var classLoader = pluginWrapper.getPluginClassLoader();
|
||||
var resourceLoader = new DefaultResourceLoader(classLoader);
|
||||
context.setResourceLoader(resourceLoader);
|
||||
sw.stop();
|
||||
|
@ -84,7 +105,6 @@ public class DefaultPluginApplicationContextFactory implements PluginApplication
|
|||
sw.stop();
|
||||
|
||||
sw.start("RegisterBeans");
|
||||
var beanFactory = context.getBeanFactory();
|
||||
beanFactory.registerSingleton("pluginWrapper", pluginWrapper);
|
||||
context.registerBean(AggregatedRouterFunction.class);
|
||||
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
package run.halo.app.plugin;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.pf4j.PluginDependency;
|
||||
import org.pf4j.PluginDescriptor;
|
||||
import org.pf4j.util.DirectedGraph;
|
||||
|
||||
/**
|
||||
* <p>Pass in a list of started plugin names to resolve dependency relationships, and return a
|
||||
* list of plugin names that depend on the specified plugin.</p>
|
||||
* <p>Do not consider the problem of circular dependency, because all the plugins that have been
|
||||
* started must not have this problem.</p>
|
||||
*/
|
||||
public class OptionalDependentResolver {
|
||||
private final DirectedGraph<String> dependentsGraph;
|
||||
private final List<PluginDescriptor> plugins;
|
||||
|
||||
public OptionalDependentResolver(List<PluginDescriptor> startedPlugins) {
|
||||
this.plugins = startedPlugins;
|
||||
this.dependentsGraph = new DirectedGraph<>();
|
||||
resolve();
|
||||
}
|
||||
|
||||
private void resolve() {
|
||||
// populate graphs
|
||||
for (PluginDescriptor plugin : plugins) {
|
||||
addPlugin(plugin);
|
||||
}
|
||||
}
|
||||
|
||||
public List<String> getOptionalDependents(String pluginId) {
|
||||
return new ArrayList<>(dependentsGraph.getNeighbors(pluginId));
|
||||
}
|
||||
|
||||
private void addPlugin(PluginDescriptor descriptor) {
|
||||
String pluginId = descriptor.getPluginId();
|
||||
List<PluginDependency> dependencies = descriptor.getDependencies();
|
||||
if (dependencies.isEmpty()) {
|
||||
dependentsGraph.addVertex(pluginId);
|
||||
} else {
|
||||
boolean edgeAdded = false;
|
||||
for (PluginDependency dependency : dependencies) {
|
||||
if (dependency.isOptional()) {
|
||||
edgeAdded = true;
|
||||
dependentsGraph.addEdge(dependency.getPluginId(), pluginId);
|
||||
}
|
||||
}
|
||||
|
||||
// Register the plugin without dependencies, if all of its dependencies are optional.
|
||||
if (!edgeAdded) {
|
||||
dependentsGraph.addVertex(pluginId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@ package run.halo.app.plugin;
|
|||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.locks.StampedLock;
|
||||
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
|
||||
import org.springframework.context.ApplicationEvent;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import org.springframework.core.ResolvableType;
|
||||
|
@ -27,7 +28,9 @@ public class PluginApplicationContext extends AnnotationConfigApplicationContext
|
|||
|
||||
private final SpringPluginManager pluginManager;
|
||||
|
||||
public PluginApplicationContext(String pluginId, SpringPluginManager pluginManager) {
|
||||
public PluginApplicationContext(String pluginId, SpringPluginManager pluginManager,
|
||||
DefaultListableBeanFactory beanFactory) {
|
||||
super(beanFactory);
|
||||
this.pluginId = pluginId;
|
||||
this.pluginManager = pluginManager;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
package run.halo.app.plugin;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.function.Predicate;
|
||||
import org.pf4j.PluginWrapper;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.web.server.ServerWebInputException;
|
||||
|
@ -102,4 +105,14 @@ public interface PluginService {
|
|||
*/
|
||||
Mono<Plugin> changeState(String pluginName, boolean requestToEnable, boolean wait);
|
||||
|
||||
/**
|
||||
* Gets required dependencies of the given plugin.
|
||||
*
|
||||
* @param plugin the plugin to get dependencies from
|
||||
* {@link Plugin.PluginSpec#getPluginDependencies()}
|
||||
* @param predicate the predicate to filter by {@link PluginWrapper},such as enabled or disabled
|
||||
* @return plugin names of required dependencies
|
||||
*/
|
||||
List<String> getRequiredDependencies(Plugin plugin,
|
||||
Predicate<PluginWrapper> predicate);
|
||||
}
|
||||
|
|
|
@ -21,10 +21,12 @@ import java.util.HashMap;
|
|||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.pf4j.DependencyResolver;
|
||||
import org.pf4j.PluginDependency;
|
||||
import org.pf4j.PluginDescriptor;
|
||||
import org.pf4j.PluginWrapper;
|
||||
import org.reactivestreams.Publisher;
|
||||
|
@ -269,12 +271,7 @@ public class PluginServiceImpl implements PluginService, InitializingBean, Dispo
|
|||
.sort(Comparator.comparing(PluginWrapper::getPluginId))
|
||||
.flatMapSequential(pluginWrapper -> {
|
||||
var pluginId = pluginWrapper.getPluginId();
|
||||
return Mono.<Resource>fromSupplier(
|
||||
() -> BundleResourceUtils.getJsBundleResource(
|
||||
pluginManager, pluginId, BundleResourceUtils.JS_BUNDLE
|
||||
)
|
||||
)
|
||||
.filter(Resource::isReadable)
|
||||
return getBundleResource(pluginId, BundleResourceUtils.JS_BUNDLE)
|
||||
.flatMapMany(resource -> {
|
||||
var head = Mono.<DataBuffer>fromSupplier(
|
||||
() -> dataBufferFactory.wrap(
|
||||
|
@ -297,10 +294,7 @@ public class PluginServiceImpl implements PluginService, InitializingBean, Dispo
|
|||
.flatMapSequential(pluginWrapper -> {
|
||||
var pluginId = pluginWrapper.getPluginId();
|
||||
var dataBufferFactory = DefaultDataBufferFactory.sharedInstance;
|
||||
return Mono.<Resource>fromSupplier(() -> BundleResourceUtils.getJsBundleResource(
|
||||
pluginManager, pluginId, BundleResourceUtils.CSS_BUNDLE
|
||||
))
|
||||
.filter(Resource::isReadable)
|
||||
return getBundleResource(pluginId, BundleResourceUtils.CSS_BUNDLE)
|
||||
.flatMapMany(resource -> {
|
||||
var head = Mono.<DataBuffer>fromSupplier(() -> dataBufferFactory.wrap(
|
||||
("/* Generated from plugin " + pluginId + " */\n").getBytes()
|
||||
|
@ -313,6 +307,13 @@ public class PluginServiceImpl implements PluginService, InitializingBean, Dispo
|
|||
});
|
||||
}
|
||||
|
||||
private Mono<Resource> getBundleResource(String pluginName, String bundleName) {
|
||||
return Mono.fromSupplier(
|
||||
() -> BundleResourceUtils.getJsBundleResource(pluginManager, pluginName, bundleName)
|
||||
)
|
||||
.filter(Resource::isReadable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<String> generateBundleVersion() {
|
||||
if (pluginManager.isDevelopment()) {
|
||||
|
@ -344,14 +345,9 @@ public class PluginServiceImpl implements PluginService, InitializingBean, Dispo
|
|||
// preflight check
|
||||
if (requestToEnable) {
|
||||
// make sure the dependencies are enabled
|
||||
var dependencies = plugin.getSpec().getPluginDependencies().keySet();
|
||||
var notStartedDependencies = dependencies.stream()
|
||||
.filter(dependency -> {
|
||||
var pluginWrapper = pluginManager.getPlugin(dependency);
|
||||
return pluginWrapper == null
|
||||
|| !Objects.equals(STARTED, pluginWrapper.getPluginState());
|
||||
})
|
||||
.toList();
|
||||
var notStartedDependencies = getRequiredDependencies(plugin,
|
||||
pw -> pw == null || !Objects.equals(STARTED, pw.getPluginState())
|
||||
);
|
||||
if (!CollectionUtils.isEmpty(notStartedDependencies)) {
|
||||
return Mono.error(
|
||||
new PluginDependenciesNotEnabledException(notStartedDependencies)
|
||||
|
@ -416,6 +412,28 @@ public class PluginServiceImpl implements PluginService, InitializingBean, Dispo
|
|||
return updatedPlugin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getRequiredDependencies(Plugin plugin,
|
||||
Predicate<PluginWrapper> predicate) {
|
||||
return getPluginDependency(plugin)
|
||||
.stream()
|
||||
.filter(dependency -> !dependency.isOptional())
|
||||
.map(PluginDependency::getPluginId)
|
||||
.filter(dependencyPlugin -> {
|
||||
var pluginWrapper = pluginManager.getPlugin(dependencyPlugin);
|
||||
return predicate.test(pluginWrapper);
|
||||
})
|
||||
.sorted()
|
||||
.toList();
|
||||
}
|
||||
|
||||
private static List<PluginDependency> getPluginDependency(Plugin plugin) {
|
||||
return plugin.getSpec().getPluginDependencies().keySet()
|
||||
.stream()
|
||||
.map(PluginDependency::new)
|
||||
.toList();
|
||||
}
|
||||
|
||||
Mono<Plugin> findPluginManifest(Path path) {
|
||||
return Mono.fromSupplier(
|
||||
() -> {
|
||||
|
|
|
@ -1,20 +1,25 @@
|
|||
package run.halo.app.plugin.extensionpoint;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.pf4j.ExtensionPoint;
|
||||
import org.pf4j.PluginManager;
|
||||
import org.pf4j.PluginWrapper;
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.stereotype.Component;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
|
||||
import run.halo.app.infra.SystemSetting.ExtensionPointEnabled;
|
||||
import run.halo.app.plugin.SpringPlugin;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class DefaultExtensionGetter implements ExtensionGetter {
|
||||
|
@ -31,7 +36,7 @@ public class DefaultExtensionGetter implements ExtensionGetter {
|
|||
|
||||
@Override
|
||||
public <T extends ExtensionPoint> Flux<T> getExtensions(Class<T> extensionPoint) {
|
||||
return Flux.fromIterable(pluginManager.getExtensions(extensionPoint))
|
||||
return Flux.fromIterable(lookExtensions(extensionPoint))
|
||||
.concatWith(
|
||||
Flux.fromStream(() -> beanFactory.getBeanProvider(extensionPoint).orderedStream())
|
||||
)
|
||||
|
@ -41,8 +46,7 @@ public class DefaultExtensionGetter implements ExtensionGetter {
|
|||
@Override
|
||||
public <T extends ExtensionPoint> List<T> getExtensionList(Class<T> extensionPoint) {
|
||||
var extensions = new LinkedList<T>();
|
||||
Optional.ofNullable(pluginManager.getExtensions(extensionPoint))
|
||||
.ifPresent(extensions::addAll);
|
||||
extensions.addAll(lookExtensions(extensionPoint));
|
||||
extensions.addAll(beanFactory.getBeanProvider(extensionPoint).orderedStream().toList());
|
||||
extensions.sort(new AnnotationAwareOrderComparator());
|
||||
return extensions;
|
||||
|
@ -96,4 +100,29 @@ public class DefaultExtensionGetter implements ExtensionGetter {
|
|||
return extensionPointDefinitionGetter.getByClassName(extensionPoint.getName());
|
||||
}
|
||||
|
||||
@NonNull
|
||||
protected <T> List<T> lookExtensions(Class<T> type) {
|
||||
List<T> beans = new ArrayList<>();
|
||||
// avoid concurrent modification
|
||||
var startedPlugins = List.copyOf(pluginManager.getStartedPlugins());
|
||||
for (PluginWrapper startedPlugin : startedPlugins) {
|
||||
if (startedPlugin.getPlugin() instanceof SpringPlugin springPlugin) {
|
||||
var pluginApplicationContext = springPlugin.getApplicationContext();
|
||||
if (pluginApplicationContext == null) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
pluginApplicationContext.getBeansOfType(type)
|
||||
.forEach((name, bean) -> beans.add(bean));
|
||||
} catch (Throwable e) {
|
||||
// Ignore
|
||||
log.error("Error while looking for extensions of type {}", type, e);
|
||||
}
|
||||
} else {
|
||||
var extensions = pluginManager.getExtensions(type, startedPlugin.getPluginId());
|
||||
beans.addAll(extensions);
|
||||
}
|
||||
}
|
||||
return beans;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -48,7 +48,6 @@ import org.springframework.core.io.DefaultResourceLoader;
|
|||
import run.halo.app.core.extension.Plugin;
|
||||
import run.halo.app.core.extension.ReverseProxy;
|
||||
import run.halo.app.core.extension.Setting;
|
||||
import run.halo.app.core.reconciler.PluginReconciler;
|
||||
import run.halo.app.extension.ConfigMap;
|
||||
import run.halo.app.extension.ExtensionClient;
|
||||
import run.halo.app.extension.Metadata;
|
||||
|
@ -58,6 +57,7 @@ import run.halo.app.extension.controller.RequeueException;
|
|||
import run.halo.app.infra.Condition;
|
||||
import run.halo.app.infra.ConditionStatus;
|
||||
import run.halo.app.plugin.PluginProperties;
|
||||
import run.halo.app.plugin.PluginService;
|
||||
import run.halo.app.plugin.SpringPluginManager;
|
||||
|
||||
/**
|
||||
|
@ -78,6 +78,9 @@ class PluginReconcilerTest {
|
|||
@Mock
|
||||
PluginProperties pluginProperties;
|
||||
|
||||
@Mock
|
||||
private PluginService pluginService;
|
||||
|
||||
@InjectMocks
|
||||
PluginReconciler reconciler;
|
||||
|
||||
|
@ -111,6 +114,12 @@ class PluginReconcilerTest {
|
|||
@TempDir
|
||||
Path tempPath;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
lenient().when(pluginService.getRequiredDependencies(any(), any()))
|
||||
.thenReturn(List.of());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotStartPluginWithDevModeInNonDevEnv() {
|
||||
var fakePlugin = createPlugin(name, plugin -> {
|
||||
|
@ -277,7 +286,7 @@ class PluginReconcilerTest {
|
|||
.thenReturn(mockPluginWrapperForSetting())
|
||||
.thenReturn(mockPluginWrapperForStaticResources())
|
||||
// before starting
|
||||
.thenReturn(mockPluginWrapper(PluginState.STOPPED))
|
||||
.thenReturn(mockPluginWrapper(PluginState.STARTED))
|
||||
// sync plugin state
|
||||
.thenReturn(mockPluginWrapper(PluginState.STARTED));
|
||||
when(pluginManager.startPlugin(name)).thenReturn(PluginState.STARTED);
|
||||
|
@ -308,7 +317,7 @@ class PluginReconcilerTest {
|
|||
|
||||
verify(pluginManager).startPlugin(name);
|
||||
verify(pluginManager).loadPlugin(loadLocation);
|
||||
verify(pluginManager, times(5)).getPlugin(name);
|
||||
verify(pluginManager, times(4)).getPlugin(name);
|
||||
verify(client).update(fakePlugin);
|
||||
verify(client).fetch(Setting.class, settingName);
|
||||
verify(client).create(any(Setting.class));
|
||||
|
|
|
@ -0,0 +1,126 @@
|
|||
package run.halo.app.plugin;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.pf4j.PluginDependency;
|
||||
import org.pf4j.PluginDescriptor;
|
||||
|
||||
/**
|
||||
* Tests for {@link OptionalDependentResolver}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.20.11
|
||||
*/
|
||||
class OptionalDependentResolverTest {
|
||||
|
||||
@Test
|
||||
void testNoPlugins() {
|
||||
OptionalDependentResolver resolver = new OptionalDependentResolver(List.of());
|
||||
assertTrue(resolver.getOptionalDependents("nonexistent").isEmpty(),
|
||||
"No dependents expected for non-existent plugin");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSinglePluginNoDependencies() {
|
||||
var pluginA = createpluginDescriptor("A", List.of());
|
||||
OptionalDependentResolver resolver = new OptionalDependentResolver(List.of(pluginA));
|
||||
|
||||
assertTrue(resolver.getOptionalDependents("A").isEmpty(), "A has no dependents");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSingleOptionalDependency() {
|
||||
var pluginA = createpluginDescriptor("A", List.of(new PluginDependency("B?")));
|
||||
var pluginB = createpluginDescriptor("B", List.of());
|
||||
OptionalDependentResolver resolver =
|
||||
new OptionalDependentResolver(List.of(pluginA, pluginB));
|
||||
|
||||
assertEquals(List.of("A"), resolver.getOptionalDependents("B"),
|
||||
"B should have A as dependent");
|
||||
assertTrue(resolver.getOptionalDependents("A").isEmpty(), "A has no dependents");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMultipleOptionalDependencies() {
|
||||
var pluginA = createpluginDescriptor("A", List.of(
|
||||
new PluginDependency("B?"),
|
||||
new PluginDependency("C?"))
|
||||
);
|
||||
|
||||
var pluginB = createpluginDescriptor("B", List.of());
|
||||
|
||||
var pluginC = createpluginDescriptor("C", List.of());
|
||||
|
||||
OptionalDependentResolver resolver =
|
||||
new OptionalDependentResolver(List.of(pluginA, pluginB, pluginC));
|
||||
|
||||
assertEquals(List.of("A"), resolver.getOptionalDependents("B"),
|
||||
"B should have A as dependent");
|
||||
assertEquals(List.of("A"), resolver.getOptionalDependents("C"),
|
||||
"C should have A as dependent");
|
||||
assertTrue(resolver.getOptionalDependents("A").isEmpty(), "A has no dependents");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testNestedDependencies() {
|
||||
var pluginA = createpluginDescriptor("A", List.of(
|
||||
new PluginDependency("B?")
|
||||
));
|
||||
var pluginB = createpluginDescriptor("B", List.of(
|
||||
new PluginDependency("C?")
|
||||
));
|
||||
var pluginC = createpluginDescriptor("C", List.of());
|
||||
|
||||
OptionalDependentResolver resolver =
|
||||
new OptionalDependentResolver(List.of(pluginA, pluginB, pluginC));
|
||||
|
||||
assertEquals(List.of("B"), resolver.getOptionalDependents("C"),
|
||||
"C should have B as dependent");
|
||||
assertEquals(List.of("A"), resolver.getOptionalDependents("B"),
|
||||
"B should have A as dependent");
|
||||
assertTrue(resolver.getOptionalDependents("A").isEmpty(), "A has no dependents");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCircularDependencies() {
|
||||
var pluginA = createpluginDescriptor("A", List.of(
|
||||
new PluginDependency("B?")
|
||||
));
|
||||
var pluginB = createpluginDescriptor("B", List.of(
|
||||
new PluginDependency("A?")
|
||||
));
|
||||
OptionalDependentResolver resolver =
|
||||
new OptionalDependentResolver(List.of(pluginA, pluginB));
|
||||
|
||||
assertEquals(List.of("B"), resolver.getOptionalDependents("A"),
|
||||
"A should have B as dependent");
|
||||
assertEquals(List.of("A"), resolver.getOptionalDependents("B"),
|
||||
"B should have A as dependent");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testNonOptionalDependencies() {
|
||||
var pluginA = createpluginDescriptor("A", List.of(
|
||||
new PluginDependency("B")
|
||||
));
|
||||
var pluginB = createpluginDescriptor("B", List.of());
|
||||
OptionalDependentResolver resolver =
|
||||
new OptionalDependentResolver(List.of(pluginA, pluginB));
|
||||
|
||||
assertTrue(resolver.getOptionalDependents("B").isEmpty(), "B should have no dependents");
|
||||
assertTrue(resolver.getOptionalDependents("A").isEmpty(), "A should have no dependents");
|
||||
}
|
||||
|
||||
PluginDescriptor createpluginDescriptor(String pluginName,
|
||||
List<PluginDependency> dependencies) {
|
||||
var descriptor = mock(PluginDescriptor.class);
|
||||
lenient().when(descriptor.getPluginId()).thenReturn(pluginName);
|
||||
lenient().when(descriptor.getDependencies()).thenReturn(dependencies);
|
||||
return descriptor;
|
||||
}
|
||||
}
|
|
@ -3,7 +3,9 @@ package run.halo.app.plugin.extensionpoint;
|
|||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static run.halo.app.infra.SystemSetting.ExtensionPointEnabled.GROUP;
|
||||
|
||||
|
@ -82,10 +84,12 @@ class DefaultExtensionGetterTest {
|
|||
when(beanFactory.getBeanProvider(FakeExtensionPoint.class)).thenReturn(objectProvider);
|
||||
|
||||
var extensionImpl = new FakeExtensionPointImpl();
|
||||
when(pluginManager.getExtensions(FakeExtensionPoint.class))
|
||||
.thenReturn(List.of(extensionImpl));
|
||||
|
||||
getter.getEnabledExtensions(FakeExtensionPoint.class)
|
||||
var spyGetter = spy(getter);
|
||||
doReturn(List.of(extensionImpl)).when(spyGetter)
|
||||
.lookExtensions(eq(FakeExtensionPoint.class));
|
||||
|
||||
spyGetter.getEnabledExtensions(FakeExtensionPoint.class)
|
||||
.as(StepVerifier::create)
|
||||
.expectNext(extensionImpl)
|
||||
.verifyComplete();
|
||||
|
@ -110,10 +114,11 @@ class DefaultExtensionGetterTest {
|
|||
.thenReturn(Stream.of(extensionDefaultImpl));
|
||||
when(beanFactory.getBeanProvider(FakeExtensionPoint.class)).thenReturn(objectProvider);
|
||||
|
||||
when(pluginManager.getExtensions(FakeExtensionPoint.class))
|
||||
.thenReturn(List.of());
|
||||
var spyGetter = spy(getter);
|
||||
doReturn(List.of()).when(spyGetter)
|
||||
.lookExtensions(eq(FakeExtensionPoint.class));
|
||||
|
||||
getter.getEnabledExtensions(FakeExtensionPoint.class)
|
||||
spyGetter.getEnabledExtensions(FakeExtensionPoint.class)
|
||||
.as(StepVerifier::create)
|
||||
.expectNext(extensionDefaultImpl)
|
||||
.verifyComplete();
|
||||
|
@ -159,10 +164,12 @@ class DefaultExtensionGetterTest {
|
|||
var extensionImpl = new FakeExtensionPointImpl();
|
||||
var anotherExtensionImpl = new FakeExtensionPoint() {
|
||||
};
|
||||
when(pluginManager.getExtensions(FakeExtensionPoint.class))
|
||||
.thenReturn(List.of(extensionImpl, anotherExtensionImpl));
|
||||
|
||||
getter.getEnabledExtensions(FakeExtensionPoint.class)
|
||||
var spyGetter = spy(getter);
|
||||
doReturn(List.of(extensionImpl, anotherExtensionImpl)).when(spyGetter)
|
||||
.lookExtensions(eq(FakeExtensionPoint.class));
|
||||
|
||||
spyGetter.getEnabledExtensions(FakeExtensionPoint.class)
|
||||
.as(StepVerifier::create)
|
||||
// should keep the order of enabled extensions
|
||||
.expectNext(extensionDefaultImpl)
|
||||
|
@ -194,10 +201,12 @@ class DefaultExtensionGetterTest {
|
|||
var extensionImpl = new FakeExtensionPointImpl();
|
||||
var anotherExtensionImpl = new FakeExtensionPoint() {
|
||||
};
|
||||
when(pluginManager.getExtensions(FakeExtensionPoint.class))
|
||||
.thenReturn(List.of(extensionImpl, anotherExtensionImpl));
|
||||
|
||||
getter.getEnabledExtensions(FakeExtensionPoint.class)
|
||||
var spyGetter = spy(getter);
|
||||
doReturn(List.of(extensionImpl, anotherExtensionImpl)).when(spyGetter)
|
||||
.lookExtensions(eq(FakeExtensionPoint.class));
|
||||
|
||||
spyGetter.getEnabledExtensions(FakeExtensionPoint.class)
|
||||
.as(StepVerifier::create)
|
||||
// should keep the order according to @Order annotation
|
||||
// order is 1
|
||||
|
@ -213,13 +222,16 @@ class DefaultExtensionGetterTest {
|
|||
void shouldGetExtensionsFromPluginManagerAndApplicationContext() {
|
||||
var extensionFromPlugin = new FakeExtensionPointDefaultImpl();
|
||||
var extensionFromAppContext = new FakeExtensionPointImpl();
|
||||
when(pluginManager.getExtensions(FakeExtensionPoint.class))
|
||||
.thenReturn(List.of(extensionFromPlugin));
|
||||
|
||||
var spyGetter = spy(getter);
|
||||
doReturn(List.of(extensionFromPlugin)).when(spyGetter)
|
||||
.lookExtensions(eq(FakeExtensionPoint.class));
|
||||
|
||||
when(beanFactory.getBeanProvider(FakeExtensionPoint.class))
|
||||
.thenReturn(extensionPointObjectProvider);
|
||||
when(extensionPointObjectProvider.orderedStream())
|
||||
.thenReturn(Stream.of(extensionFromAppContext));
|
||||
var extensions = getter.getExtensionList(FakeExtensionPoint.class);
|
||||
var extensions = spyGetter.getExtensionList(FakeExtensionPoint.class);
|
||||
assertEquals(List.of(extensionFromAppContext, extensionFromPlugin), extensions);
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue