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 java.util.function.Supplier;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.pf4j.PluginDependency;
|
|
||||||
import org.pf4j.PluginState;
|
import org.pf4j.PluginState;
|
||||||
import org.pf4j.PluginWrapper;
|
import org.pf4j.PluginWrapper;
|
||||||
import org.pf4j.RuntimeMode;
|
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.ExtensionClient;
|
||||||
import run.halo.app.extension.ExtensionUtil;
|
import run.halo.app.extension.ExtensionUtil;
|
||||||
import run.halo.app.extension.Metadata;
|
import run.halo.app.extension.Metadata;
|
||||||
|
import run.halo.app.extension.MetadataUtil;
|
||||||
import run.halo.app.extension.Unstructured;
|
import run.halo.app.extension.Unstructured;
|
||||||
import run.halo.app.extension.controller.Controller;
|
import run.halo.app.extension.controller.Controller;
|
||||||
import run.halo.app.extension.controller.ControllerBuilder;
|
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.PathUtils;
|
||||||
import run.halo.app.infra.utils.SettingUtils;
|
import run.halo.app.infra.utils.SettingUtils;
|
||||||
import run.halo.app.infra.utils.YamlUnstructuredLoader;
|
import run.halo.app.infra.utils.YamlUnstructuredLoader;
|
||||||
|
import run.halo.app.plugin.OptionalDependentResolver;
|
||||||
import run.halo.app.plugin.PluginConst;
|
import run.halo.app.plugin.PluginConst;
|
||||||
import run.halo.app.plugin.PluginProperties;
|
import run.halo.app.plugin.PluginProperties;
|
||||||
|
import run.halo.app.plugin.PluginService;
|
||||||
import run.halo.app.plugin.SpringPluginManager;
|
import run.halo.app.plugin.SpringPluginManager;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -85,13 +87,16 @@ public class PluginReconciler implements Reconciler<Request> {
|
||||||
|
|
||||||
private final PluginProperties pluginProperties;
|
private final PluginProperties pluginProperties;
|
||||||
|
|
||||||
|
private final PluginService pluginService;
|
||||||
|
|
||||||
private Clock clock;
|
private Clock clock;
|
||||||
|
|
||||||
public PluginReconciler(ExtensionClient client, SpringPluginManager pluginManager,
|
public PluginReconciler(ExtensionClient client, SpringPluginManager pluginManager,
|
||||||
PluginProperties pluginProperties) {
|
PluginProperties pluginProperties, PluginService pluginService) {
|
||||||
this.client = client;
|
this.client = client;
|
||||||
this.pluginManager = pluginManager;
|
this.pluginManager = pluginManager;
|
||||||
this.pluginProperties = pluginProperties;
|
this.pluginProperties = pluginProperties;
|
||||||
|
this.pluginService = pluginService;
|
||||||
this.clock = Clock.systemUTC();
|
this.clock = Clock.systemUTC();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -280,18 +285,9 @@ public class PluginReconciler implements Reconciler<Request> {
|
||||||
status.setPhase(Plugin.Phase.STARTING);
|
status.setPhase(Plugin.Phase.STARTING);
|
||||||
|
|
||||||
// check if the parent plugin is started
|
// check if the parent plugin is started
|
||||||
var pw = pluginManager.getPlugin(pluginName);
|
var unstartedDependencies = pluginService.getRequiredDependencies(plugin, pw ->
|
||||||
var unstartedDependencies = pw.getDescriptor().getDependencies()
|
pw == null || !PluginState.STARTED.equals(pw.getPluginState())
|
||||||
.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 conditions = status.getConditions();
|
var conditions = status.getConditions();
|
||||||
if (!CollectionUtils.isEmpty(unstartedDependencies)) {
|
if (!CollectionUtils.isEmpty(unstartedDependencies)) {
|
||||||
removeConditionBy(conditions, ConditionType.READY);
|
removeConditionBy(conditions, ConditionType.READY);
|
||||||
|
@ -337,9 +333,31 @@ public class PluginReconciler implements Reconciler<Request> {
|
||||||
status.setPhase(Plugin.Phase.STARTED);
|
status.setPhase(Plugin.Phase.STARTED);
|
||||||
|
|
||||||
log.info("Started plugin {}", pluginName);
|
log.info("Started plugin {}", pluginName);
|
||||||
|
|
||||||
|
requestToReloadPluginsOptionallyDependentOn(pluginName);
|
||||||
|
|
||||||
return null;
|
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) {
|
private Result disablePlugin(Plugin plugin) {
|
||||||
var pluginName = plugin.getMetadata().getName();
|
var pluginName = plugin.getMetadata().getName();
|
||||||
var status = plugin.getStatus();
|
var status = plugin.getStatus();
|
||||||
|
@ -515,15 +533,9 @@ public class PluginReconciler implements Reconciler<Request> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// check dependencies before loading
|
// check dependencies before loading
|
||||||
var unresolvedParentPlugins = plugin.getSpec().getPluginDependencies().keySet()
|
var unresolvedParentPlugins = pluginService.getRequiredDependencies(plugin,
|
||||||
.stream()
|
pw -> pw == null || pluginManager.getUnresolvedPlugins().contains(pw)
|
||||||
.filter(dependency -> {
|
);
|
||||||
var parentPlugin = pluginManager.getPlugin(dependency);
|
|
||||||
return parentPlugin == null
|
|
||||||
|| pluginManager.getUnresolvedPlugins().contains(parentPlugin);
|
|
||||||
})
|
|
||||||
.sorted()
|
|
||||||
.toList();
|
|
||||||
if (!unresolvedParentPlugins.isEmpty()) {
|
if (!unresolvedParentPlugins.isEmpty()) {
|
||||||
// requeue if the parent plugin is not loaded yet.
|
// requeue if the parent plugin is not loaded yet.
|
||||||
removeConditionBy(conditions, ConditionType.INITIALIZED);
|
removeConditionBy(conditions, ConditionType.INITIALIZED);
|
||||||
|
|
|
@ -11,6 +11,7 @@ import java.util.stream.Stream;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.pf4j.PluginRuntimeException;
|
import org.pf4j.PluginRuntimeException;
|
||||||
import org.springframework.beans.factory.support.DefaultBeanNameGenerator;
|
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.PropertySourceLoader;
|
||||||
import org.springframework.boot.env.YamlPropertySourceLoader;
|
import org.springframework.boot.env.YamlPropertySourceLoader;
|
||||||
import org.springframework.context.ApplicationContext;
|
import org.springframework.context.ApplicationContext;
|
||||||
|
@ -65,13 +66,33 @@ public class DefaultPluginApplicationContextFactory implements PluginApplication
|
||||||
|
|
||||||
var sw = new StopWatch("CreateApplicationContextFor" + pluginId);
|
var sw = new StopWatch("CreateApplicationContextFor" + pluginId);
|
||||||
sw.start("Create");
|
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.setBeanNameGenerator(DefaultBeanNameGenerator.INSTANCE);
|
||||||
context.registerShutdownHook();
|
context.registerShutdownHook();
|
||||||
context.setParent(pluginManager.getSharedContext());
|
context.setParent(pluginManager.getSharedContext());
|
||||||
|
|
||||||
var pluginWrapper = pluginManager.getPlugin(pluginId);
|
|
||||||
var classLoader = pluginWrapper.getPluginClassLoader();
|
|
||||||
var resourceLoader = new DefaultResourceLoader(classLoader);
|
var resourceLoader = new DefaultResourceLoader(classLoader);
|
||||||
context.setResourceLoader(resourceLoader);
|
context.setResourceLoader(resourceLoader);
|
||||||
sw.stop();
|
sw.stop();
|
||||||
|
@ -84,7 +105,6 @@ public class DefaultPluginApplicationContextFactory implements PluginApplication
|
||||||
sw.stop();
|
sw.stop();
|
||||||
|
|
||||||
sw.start("RegisterBeans");
|
sw.start("RegisterBeans");
|
||||||
var beanFactory = context.getBeanFactory();
|
|
||||||
beanFactory.registerSingleton("pluginWrapper", pluginWrapper);
|
beanFactory.registerSingleton("pluginWrapper", pluginWrapper);
|
||||||
context.registerBean(AggregatedRouterFunction.class);
|
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.List;
|
||||||
import java.util.concurrent.locks.StampedLock;
|
import java.util.concurrent.locks.StampedLock;
|
||||||
|
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
|
||||||
import org.springframework.context.ApplicationEvent;
|
import org.springframework.context.ApplicationEvent;
|
||||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||||
import org.springframework.core.ResolvableType;
|
import org.springframework.core.ResolvableType;
|
||||||
|
@ -27,7 +28,9 @@ public class PluginApplicationContext extends AnnotationConfigApplicationContext
|
||||||
|
|
||||||
private final SpringPluginManager pluginManager;
|
private final SpringPluginManager pluginManager;
|
||||||
|
|
||||||
public PluginApplicationContext(String pluginId, SpringPluginManager pluginManager) {
|
public PluginApplicationContext(String pluginId, SpringPluginManager pluginManager,
|
||||||
|
DefaultListableBeanFactory beanFactory) {
|
||||||
|
super(beanFactory);
|
||||||
this.pluginId = pluginId;
|
this.pluginId = pluginId;
|
||||||
this.pluginManager = pluginManager;
|
this.pluginManager = pluginManager;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
package run.halo.app.plugin;
|
package run.halo.app.plugin;
|
||||||
|
|
||||||
import java.nio.file.Path;
|
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.Resource;
|
||||||
import org.springframework.core.io.buffer.DataBuffer;
|
import org.springframework.core.io.buffer.DataBuffer;
|
||||||
import org.springframework.web.server.ServerWebInputException;
|
import org.springframework.web.server.ServerWebInputException;
|
||||||
|
@ -102,4 +105,14 @@ public interface PluginService {
|
||||||
*/
|
*/
|
||||||
Mono<Plugin> changeState(String pluginName, boolean requestToEnable, boolean wait);
|
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.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.function.Predicate;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.pf4j.DependencyResolver;
|
import org.pf4j.DependencyResolver;
|
||||||
|
import org.pf4j.PluginDependency;
|
||||||
import org.pf4j.PluginDescriptor;
|
import org.pf4j.PluginDescriptor;
|
||||||
import org.pf4j.PluginWrapper;
|
import org.pf4j.PluginWrapper;
|
||||||
import org.reactivestreams.Publisher;
|
import org.reactivestreams.Publisher;
|
||||||
|
@ -269,12 +271,7 @@ public class PluginServiceImpl implements PluginService, InitializingBean, Dispo
|
||||||
.sort(Comparator.comparing(PluginWrapper::getPluginId))
|
.sort(Comparator.comparing(PluginWrapper::getPluginId))
|
||||||
.flatMapSequential(pluginWrapper -> {
|
.flatMapSequential(pluginWrapper -> {
|
||||||
var pluginId = pluginWrapper.getPluginId();
|
var pluginId = pluginWrapper.getPluginId();
|
||||||
return Mono.<Resource>fromSupplier(
|
return getBundleResource(pluginId, BundleResourceUtils.JS_BUNDLE)
|
||||||
() -> BundleResourceUtils.getJsBundleResource(
|
|
||||||
pluginManager, pluginId, BundleResourceUtils.JS_BUNDLE
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.filter(Resource::isReadable)
|
|
||||||
.flatMapMany(resource -> {
|
.flatMapMany(resource -> {
|
||||||
var head = Mono.<DataBuffer>fromSupplier(
|
var head = Mono.<DataBuffer>fromSupplier(
|
||||||
() -> dataBufferFactory.wrap(
|
() -> dataBufferFactory.wrap(
|
||||||
|
@ -297,10 +294,7 @@ public class PluginServiceImpl implements PluginService, InitializingBean, Dispo
|
||||||
.flatMapSequential(pluginWrapper -> {
|
.flatMapSequential(pluginWrapper -> {
|
||||||
var pluginId = pluginWrapper.getPluginId();
|
var pluginId = pluginWrapper.getPluginId();
|
||||||
var dataBufferFactory = DefaultDataBufferFactory.sharedInstance;
|
var dataBufferFactory = DefaultDataBufferFactory.sharedInstance;
|
||||||
return Mono.<Resource>fromSupplier(() -> BundleResourceUtils.getJsBundleResource(
|
return getBundleResource(pluginId, BundleResourceUtils.CSS_BUNDLE)
|
||||||
pluginManager, pluginId, BundleResourceUtils.CSS_BUNDLE
|
|
||||||
))
|
|
||||||
.filter(Resource::isReadable)
|
|
||||||
.flatMapMany(resource -> {
|
.flatMapMany(resource -> {
|
||||||
var head = Mono.<DataBuffer>fromSupplier(() -> dataBufferFactory.wrap(
|
var head = Mono.<DataBuffer>fromSupplier(() -> dataBufferFactory.wrap(
|
||||||
("/* Generated from plugin " + pluginId + " */\n").getBytes()
|
("/* 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
|
@Override
|
||||||
public Mono<String> generateBundleVersion() {
|
public Mono<String> generateBundleVersion() {
|
||||||
if (pluginManager.isDevelopment()) {
|
if (pluginManager.isDevelopment()) {
|
||||||
|
@ -344,14 +345,9 @@ public class PluginServiceImpl implements PluginService, InitializingBean, Dispo
|
||||||
// preflight check
|
// preflight check
|
||||||
if (requestToEnable) {
|
if (requestToEnable) {
|
||||||
// make sure the dependencies are enabled
|
// make sure the dependencies are enabled
|
||||||
var dependencies = plugin.getSpec().getPluginDependencies().keySet();
|
var notStartedDependencies = getRequiredDependencies(plugin,
|
||||||
var notStartedDependencies = dependencies.stream()
|
pw -> pw == null || !Objects.equals(STARTED, pw.getPluginState())
|
||||||
.filter(dependency -> {
|
);
|
||||||
var pluginWrapper = pluginManager.getPlugin(dependency);
|
|
||||||
return pluginWrapper == null
|
|
||||||
|| !Objects.equals(STARTED, pluginWrapper.getPluginState());
|
|
||||||
})
|
|
||||||
.toList();
|
|
||||||
if (!CollectionUtils.isEmpty(notStartedDependencies)) {
|
if (!CollectionUtils.isEmpty(notStartedDependencies)) {
|
||||||
return Mono.error(
|
return Mono.error(
|
||||||
new PluginDependenciesNotEnabledException(notStartedDependencies)
|
new PluginDependenciesNotEnabledException(notStartedDependencies)
|
||||||
|
@ -416,6 +412,28 @@ public class PluginServiceImpl implements PluginService, InitializingBean, Dispo
|
||||||
return updatedPlugin;
|
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) {
|
Mono<Plugin> findPluginManifest(Path path) {
|
||||||
return Mono.fromSupplier(
|
return Mono.fromSupplier(
|
||||||
() -> {
|
() -> {
|
||||||
|
|
|
@ -1,20 +1,25 @@
|
||||||
package run.halo.app.plugin.extensionpoint;
|
package run.halo.app.plugin.extensionpoint;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.pf4j.ExtensionPoint;
|
import org.pf4j.ExtensionPoint;
|
||||||
import org.pf4j.PluginManager;
|
import org.pf4j.PluginManager;
|
||||||
|
import org.pf4j.PluginWrapper;
|
||||||
import org.springframework.beans.factory.BeanFactory;
|
import org.springframework.beans.factory.BeanFactory;
|
||||||
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
|
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
|
||||||
|
import org.springframework.lang.NonNull;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import reactor.core.publisher.Flux;
|
import reactor.core.publisher.Flux;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
|
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
|
||||||
import run.halo.app.infra.SystemSetting.ExtensionPointEnabled;
|
import run.halo.app.infra.SystemSetting.ExtensionPointEnabled;
|
||||||
|
import run.halo.app.plugin.SpringPlugin;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
@Component
|
@Component
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class DefaultExtensionGetter implements ExtensionGetter {
|
public class DefaultExtensionGetter implements ExtensionGetter {
|
||||||
|
@ -31,7 +36,7 @@ public class DefaultExtensionGetter implements ExtensionGetter {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public <T extends ExtensionPoint> Flux<T> getExtensions(Class<T> extensionPoint) {
|
public <T extends ExtensionPoint> Flux<T> getExtensions(Class<T> extensionPoint) {
|
||||||
return Flux.fromIterable(pluginManager.getExtensions(extensionPoint))
|
return Flux.fromIterable(lookExtensions(extensionPoint))
|
||||||
.concatWith(
|
.concatWith(
|
||||||
Flux.fromStream(() -> beanFactory.getBeanProvider(extensionPoint).orderedStream())
|
Flux.fromStream(() -> beanFactory.getBeanProvider(extensionPoint).orderedStream())
|
||||||
)
|
)
|
||||||
|
@ -41,8 +46,7 @@ public class DefaultExtensionGetter implements ExtensionGetter {
|
||||||
@Override
|
@Override
|
||||||
public <T extends ExtensionPoint> List<T> getExtensionList(Class<T> extensionPoint) {
|
public <T extends ExtensionPoint> List<T> getExtensionList(Class<T> extensionPoint) {
|
||||||
var extensions = new LinkedList<T>();
|
var extensions = new LinkedList<T>();
|
||||||
Optional.ofNullable(pluginManager.getExtensions(extensionPoint))
|
extensions.addAll(lookExtensions(extensionPoint));
|
||||||
.ifPresent(extensions::addAll);
|
|
||||||
extensions.addAll(beanFactory.getBeanProvider(extensionPoint).orderedStream().toList());
|
extensions.addAll(beanFactory.getBeanProvider(extensionPoint).orderedStream().toList());
|
||||||
extensions.sort(new AnnotationAwareOrderComparator());
|
extensions.sort(new AnnotationAwareOrderComparator());
|
||||||
return extensions;
|
return extensions;
|
||||||
|
@ -96,4 +100,29 @@ public class DefaultExtensionGetter implements ExtensionGetter {
|
||||||
return extensionPointDefinitionGetter.getByClassName(extensionPoint.getName());
|
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.Plugin;
|
||||||
import run.halo.app.core.extension.ReverseProxy;
|
import run.halo.app.core.extension.ReverseProxy;
|
||||||
import run.halo.app.core.extension.Setting;
|
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.ConfigMap;
|
||||||
import run.halo.app.extension.ExtensionClient;
|
import run.halo.app.extension.ExtensionClient;
|
||||||
import run.halo.app.extension.Metadata;
|
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.Condition;
|
||||||
import run.halo.app.infra.ConditionStatus;
|
import run.halo.app.infra.ConditionStatus;
|
||||||
import run.halo.app.plugin.PluginProperties;
|
import run.halo.app.plugin.PluginProperties;
|
||||||
|
import run.halo.app.plugin.PluginService;
|
||||||
import run.halo.app.plugin.SpringPluginManager;
|
import run.halo.app.plugin.SpringPluginManager;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -78,6 +78,9 @@ class PluginReconcilerTest {
|
||||||
@Mock
|
@Mock
|
||||||
PluginProperties pluginProperties;
|
PluginProperties pluginProperties;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private PluginService pluginService;
|
||||||
|
|
||||||
@InjectMocks
|
@InjectMocks
|
||||||
PluginReconciler reconciler;
|
PluginReconciler reconciler;
|
||||||
|
|
||||||
|
@ -111,6 +114,12 @@ class PluginReconcilerTest {
|
||||||
@TempDir
|
@TempDir
|
||||||
Path tempPath;
|
Path tempPath;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
lenient().when(pluginService.getRequiredDependencies(any(), any()))
|
||||||
|
.thenReturn(List.of());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldNotStartPluginWithDevModeInNonDevEnv() {
|
void shouldNotStartPluginWithDevModeInNonDevEnv() {
|
||||||
var fakePlugin = createPlugin(name, plugin -> {
|
var fakePlugin = createPlugin(name, plugin -> {
|
||||||
|
@ -277,7 +286,7 @@ class PluginReconcilerTest {
|
||||||
.thenReturn(mockPluginWrapperForSetting())
|
.thenReturn(mockPluginWrapperForSetting())
|
||||||
.thenReturn(mockPluginWrapperForStaticResources())
|
.thenReturn(mockPluginWrapperForStaticResources())
|
||||||
// before starting
|
// before starting
|
||||||
.thenReturn(mockPluginWrapper(PluginState.STOPPED))
|
.thenReturn(mockPluginWrapper(PluginState.STARTED))
|
||||||
// sync plugin state
|
// sync plugin state
|
||||||
.thenReturn(mockPluginWrapper(PluginState.STARTED));
|
.thenReturn(mockPluginWrapper(PluginState.STARTED));
|
||||||
when(pluginManager.startPlugin(name)).thenReturn(PluginState.STARTED);
|
when(pluginManager.startPlugin(name)).thenReturn(PluginState.STARTED);
|
||||||
|
@ -308,7 +317,7 @@ class PluginReconcilerTest {
|
||||||
|
|
||||||
verify(pluginManager).startPlugin(name);
|
verify(pluginManager).startPlugin(name);
|
||||||
verify(pluginManager).loadPlugin(loadLocation);
|
verify(pluginManager).loadPlugin(loadLocation);
|
||||||
verify(pluginManager, times(5)).getPlugin(name);
|
verify(pluginManager, times(4)).getPlugin(name);
|
||||||
verify(client).update(fakePlugin);
|
verify(client).update(fakePlugin);
|
||||||
verify(client).fetch(Setting.class, settingName);
|
verify(client).fetch(Setting.class, settingName);
|
||||||
verify(client).create(any(Setting.class));
|
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.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.ArgumentMatchers.eq;
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.doReturn;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.spy;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
import static run.halo.app.infra.SystemSetting.ExtensionPointEnabled.GROUP;
|
import static run.halo.app.infra.SystemSetting.ExtensionPointEnabled.GROUP;
|
||||||
|
|
||||||
|
@ -82,10 +84,12 @@ class DefaultExtensionGetterTest {
|
||||||
when(beanFactory.getBeanProvider(FakeExtensionPoint.class)).thenReturn(objectProvider);
|
when(beanFactory.getBeanProvider(FakeExtensionPoint.class)).thenReturn(objectProvider);
|
||||||
|
|
||||||
var extensionImpl = new FakeExtensionPointImpl();
|
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)
|
.as(StepVerifier::create)
|
||||||
.expectNext(extensionImpl)
|
.expectNext(extensionImpl)
|
||||||
.verifyComplete();
|
.verifyComplete();
|
||||||
|
@ -110,10 +114,11 @@ class DefaultExtensionGetterTest {
|
||||||
.thenReturn(Stream.of(extensionDefaultImpl));
|
.thenReturn(Stream.of(extensionDefaultImpl));
|
||||||
when(beanFactory.getBeanProvider(FakeExtensionPoint.class)).thenReturn(objectProvider);
|
when(beanFactory.getBeanProvider(FakeExtensionPoint.class)).thenReturn(objectProvider);
|
||||||
|
|
||||||
when(pluginManager.getExtensions(FakeExtensionPoint.class))
|
var spyGetter = spy(getter);
|
||||||
.thenReturn(List.of());
|
doReturn(List.of()).when(spyGetter)
|
||||||
|
.lookExtensions(eq(FakeExtensionPoint.class));
|
||||||
|
|
||||||
getter.getEnabledExtensions(FakeExtensionPoint.class)
|
spyGetter.getEnabledExtensions(FakeExtensionPoint.class)
|
||||||
.as(StepVerifier::create)
|
.as(StepVerifier::create)
|
||||||
.expectNext(extensionDefaultImpl)
|
.expectNext(extensionDefaultImpl)
|
||||||
.verifyComplete();
|
.verifyComplete();
|
||||||
|
@ -159,10 +164,12 @@ class DefaultExtensionGetterTest {
|
||||||
var extensionImpl = new FakeExtensionPointImpl();
|
var extensionImpl = new FakeExtensionPointImpl();
|
||||||
var anotherExtensionImpl = new FakeExtensionPoint() {
|
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)
|
.as(StepVerifier::create)
|
||||||
// should keep the order of enabled extensions
|
// should keep the order of enabled extensions
|
||||||
.expectNext(extensionDefaultImpl)
|
.expectNext(extensionDefaultImpl)
|
||||||
|
@ -194,10 +201,12 @@ class DefaultExtensionGetterTest {
|
||||||
var extensionImpl = new FakeExtensionPointImpl();
|
var extensionImpl = new FakeExtensionPointImpl();
|
||||||
var anotherExtensionImpl = new FakeExtensionPoint() {
|
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)
|
.as(StepVerifier::create)
|
||||||
// should keep the order according to @Order annotation
|
// should keep the order according to @Order annotation
|
||||||
// order is 1
|
// order is 1
|
||||||
|
@ -213,13 +222,16 @@ class DefaultExtensionGetterTest {
|
||||||
void shouldGetExtensionsFromPluginManagerAndApplicationContext() {
|
void shouldGetExtensionsFromPluginManagerAndApplicationContext() {
|
||||||
var extensionFromPlugin = new FakeExtensionPointDefaultImpl();
|
var extensionFromPlugin = new FakeExtensionPointDefaultImpl();
|
||||||
var extensionFromAppContext = new FakeExtensionPointImpl();
|
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))
|
when(beanFactory.getBeanProvider(FakeExtensionPoint.class))
|
||||||
.thenReturn(extensionPointObjectProvider);
|
.thenReturn(extensionPointObjectProvider);
|
||||||
when(extensionPointObjectProvider.orderedStream())
|
when(extensionPointObjectProvider.orderedStream())
|
||||||
.thenReturn(Stream.of(extensionFromAppContext));
|
.thenReturn(Stream.of(extensionFromAppContext));
|
||||||
var extensions = getter.getExtensionList(FakeExtensionPoint.class);
|
var extensions = spyGetter.getExtensionList(FakeExtensionPoint.class);
|
||||||
assertEquals(List.of(extensionFromAppContext, extensionFromPlugin), extensions);
|
assertEquals(List.of(extensionFromAppContext, extensionFromPlugin), extensions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue