mirror of https://github.com/halo-dev/halo
Simplify halo plugin manager (#5251)
#### What type of PR is this? /kind improvement /area core /area plugin /milestone 2.12.x #### What this PR does / why we need it: This PR mainly simplifies halo plugin manager. Before this, - we have too many repeat code from super class, which is uncessary - we maintain plugin application context in ExtensionComponentsFinder, which is uncessary and is hard to manage - we fire halo plugin event in halo plugin manager, which is complicated and leads to too many repeat code This PR does: - refactor halo plugin manager - wrap base plugin with spring plugin which contains application context - remove ExtensionComponentsFinder - bridge halo plugin event and spring plugin event - wait extensions fully deleted when stopping Meanwhile, this PR will supersede PR <https://github.com/halo-dev/halo/pull/5236>. #### Which issue(s) this PR fixes: Fixes https://github.com/halo-dev/halo/issues/5226 #### Special notes for your reviewer: Test installing, enabing, disabling, upgrading, reloading and deleting plugins. #### Does this PR introduce a user-facing change? ```release-note None ```pull/5256/head
parent
17a0fb9e05
commit
8288e4edf8
|
@ -262,8 +262,8 @@ public class PluginReconciler implements Reconciler<Request> {
|
|||
var p = pluginManager.getPlugin(pluginName);
|
||||
var classLoader = p.getPluginClassLoader();
|
||||
var resLoader = new DefaultResourceLoader(classLoader);
|
||||
var entryRes = resLoader.getResource("classpath:/console/main.js");
|
||||
var cssRes = resLoader.getResource("classpath:/console/style.css");
|
||||
var entryRes = resLoader.getResource("classpath:console/main.js");
|
||||
var cssRes = resLoader.getResource("classpath:console/style.css");
|
||||
if (entryRes.exists()) {
|
||||
var entry = UriComponentsBuilder.newInstance()
|
||||
.pathSegment("plugins", pluginName, "assets", "console", "main.js")
|
||||
|
|
|
@ -55,12 +55,12 @@ public class ReverseProxyReconciler implements Reconciler<Reconciler.Request> {
|
|||
|
||||
private void registerReverseProxy(ReverseProxy reverseProxy) {
|
||||
String pluginId = getPluginId(reverseProxy);
|
||||
routerFunctionRegistry.register(pluginId, reverseProxy).block();
|
||||
routerFunctionRegistry.register(pluginId, reverseProxy);
|
||||
}
|
||||
|
||||
private void cleanUpResources(ReverseProxy reverseProxy) {
|
||||
String pluginId = getPluginId(reverseProxy);
|
||||
routerFunctionRegistry.remove(pluginId, reverseProxy.getMetadata().getName()).block();
|
||||
routerFunctionRegistry.remove(pluginId, reverseProxy.getMetadata().getName());
|
||||
}
|
||||
|
||||
private void addFinalizerIfNecessary(ReverseProxy oldReverseProxy) {
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
package run.halo.app.infra;
|
||||
|
||||
import com.github.zafarkhaja.semver.Version;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import java.util.Objects;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.boot.info.BuildProperties;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
|
@ -17,21 +16,19 @@ import org.springframework.stereotype.Component;
|
|||
public class DefaultSystemVersionSupplier implements SystemVersionSupplier {
|
||||
private static final String DEFAULT_VERSION = "0.0.0";
|
||||
|
||||
@Nullable
|
||||
private BuildProperties buildProperties;
|
||||
private final ObjectProvider<BuildProperties> buildProperties;
|
||||
|
||||
@Autowired(required = false)
|
||||
public void setBuildProperties(@Nullable BuildProperties buildProperties) {
|
||||
public DefaultSystemVersionSupplier(ObjectProvider<BuildProperties> buildProperties) {
|
||||
this.buildProperties = buildProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Version get() {
|
||||
if (buildProperties == null) {
|
||||
var properties = buildProperties.getIfUnique();
|
||||
if (properties == null) {
|
||||
return Version.valueOf(DEFAULT_VERSION);
|
||||
}
|
||||
String projectVersion =
|
||||
StringUtils.defaultString(buildProperties.getVersion(), DEFAULT_VERSION);
|
||||
var projectVersion = Objects.toString(properties.getVersion(), DEFAULT_VERSION);
|
||||
return Version.valueOf(projectVersion);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,52 +0,0 @@
|
|||
package run.halo.app.plugin;
|
||||
|
||||
import java.util.Optional;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.pf4j.Plugin;
|
||||
import org.pf4j.PluginFactory;
|
||||
import org.pf4j.PluginWrapper;
|
||||
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
|
||||
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
|
||||
|
||||
/**
|
||||
* The default implementation for PluginFactory.
|
||||
* <p>Get a {@link BasePlugin} instance from the {@link PluginApplicationContext}.</p>
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Slf4j
|
||||
public class BasePluginFactory implements PluginFactory {
|
||||
|
||||
@Override
|
||||
public Plugin create(PluginWrapper pluginWrapper) {
|
||||
return getPluginContext(pluginWrapper)
|
||||
.map(context -> {
|
||||
try {
|
||||
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();
|
||||
var pluginContext = beanFactory.getBean(PluginContext.class);
|
||||
BasePlugin pluginInstance = new BasePlugin(pluginContext);
|
||||
beanFactory.registerSingleton(Plugin.class.getName(), pluginInstance);
|
||||
return pluginInstance;
|
||||
}
|
||||
})
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
private Optional<PluginApplicationContext> getPluginContext(PluginWrapper pluginWrapper) {
|
||||
try {
|
||||
return Optional.of(ExtensionContextRegistry.getInstance())
|
||||
.map(registry -> registry.getByPluginId(pluginWrapper.getPluginId()));
|
||||
} catch (IllegalArgumentException e) {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,380 @@
|
|||
package run.halo.app.plugin;
|
||||
|
||||
import static org.springframework.util.ResourceUtils.CLASSPATH_URL_PREFIX;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Stream;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.pf4j.PluginRuntimeException;
|
||||
import org.springframework.beans.factory.support.DefaultBeanNameGenerator;
|
||||
import org.springframework.boot.env.PropertySourceLoader;
|
||||
import org.springframework.boot.env.YamlPropertySourceLoader;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationEvent;
|
||||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.context.event.ContextClosedEvent;
|
||||
import org.springframework.context.event.ContextRefreshedEvent;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.env.PropertySource;
|
||||
import org.springframework.core.io.DefaultResourceLoader;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.util.StopWatch;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import reactor.core.Exceptions;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.infra.properties.HaloProperties;
|
||||
import run.halo.app.plugin.event.HaloPluginBeforeStopEvent;
|
||||
import run.halo.app.plugin.event.HaloPluginStartedEvent;
|
||||
import run.halo.app.plugin.event.HaloPluginStoppedEvent;
|
||||
import run.halo.app.plugin.event.SpringPluginStartedEvent;
|
||||
import run.halo.app.plugin.event.SpringPluginStoppedEvent;
|
||||
import run.halo.app.plugin.event.SpringPluginStoppingEvent;
|
||||
import run.halo.app.theme.DefaultTemplateNameResolver;
|
||||
import run.halo.app.theme.ViewNameResolver;
|
||||
import run.halo.app.theme.finders.FinderRegistry;
|
||||
|
||||
@Slf4j
|
||||
public class DefaultPluginApplicationContextFactory implements PluginApplicationContextFactory {
|
||||
|
||||
private final SpringPluginManager pluginManager;
|
||||
|
||||
public DefaultPluginApplicationContextFactory(SpringPluginManager pluginManager) {
|
||||
this.pluginManager = pluginManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and refresh application context. Make sure the plugin has already loaded
|
||||
* before.
|
||||
*
|
||||
* @param pluginId plugin id
|
||||
* @return refresh application context for the plugin.
|
||||
*/
|
||||
@Override
|
||||
public ApplicationContext create(String pluginId) {
|
||||
log.debug("Preparing to create application context for plugin {}", pluginId);
|
||||
var pluginWrapper = pluginManager.getPlugin(pluginId);
|
||||
|
||||
var sw = new StopWatch("CreateApplicationContextFor" + pluginId);
|
||||
|
||||
sw.start("Create");
|
||||
var context = new PluginApplicationContext(pluginId);
|
||||
context.setBeanNameGenerator(DefaultBeanNameGenerator.INSTANCE);
|
||||
context.registerShutdownHook();
|
||||
context.setParent(pluginManager.getSharedContext());
|
||||
|
||||
var classLoader = pluginWrapper.getPluginClassLoader();
|
||||
var resourceLoader = new DefaultResourceLoader(classLoader);
|
||||
context.setResourceLoader(resourceLoader);
|
||||
sw.stop();
|
||||
|
||||
sw.start("LoadPropertySources");
|
||||
var mutablePropertySources = context.getEnvironment().getPropertySources();
|
||||
|
||||
resolvePropertySources(pluginId, resourceLoader)
|
||||
.forEach(mutablePropertySources::addLast);
|
||||
sw.stop();
|
||||
|
||||
sw.start("RegisterBeans");
|
||||
var beanFactory = context.getBeanFactory();
|
||||
context.registerBean(AggregatedRouterFunction.class);
|
||||
beanFactory.registerSingleton("pluginWrapper", pluginWrapper);
|
||||
|
||||
if (pluginWrapper.getPlugin() instanceof SpringPlugin springPlugin) {
|
||||
beanFactory.registerSingleton("pluginContext", springPlugin.getPluginContext());
|
||||
}
|
||||
|
||||
var rootContext = pluginManager.getRootContext();
|
||||
rootContext.getBeanProvider(ViewNameResolver.class)
|
||||
.ifAvailable(viewNameResolver -> {
|
||||
var templateNameResolver =
|
||||
new DefaultTemplateNameResolver(viewNameResolver, context);
|
||||
beanFactory.registerSingleton("templateNameResolver", templateNameResolver);
|
||||
});
|
||||
|
||||
rootContext.getBeanProvider(ReactiveExtensionClient.class)
|
||||
.ifUnique(client -> {
|
||||
var reactiveSettingFetcher = new DefaultReactiveSettingFetcher(client, pluginId);
|
||||
var settingFetcher = new DefaultSettingFetcher(reactiveSettingFetcher);
|
||||
beanFactory.registerSingleton("reactiveSettingFetcher", reactiveSettingFetcher);
|
||||
beanFactory.registerSingleton("settingFetcher", settingFetcher);
|
||||
});
|
||||
|
||||
rootContext.getBeanProvider(PluginRequestMappingHandlerMapping.class)
|
||||
.ifAvailable(handlerMapping -> {
|
||||
var handlerMappingManager =
|
||||
new PluginHandlerMappingManager(pluginId, handlerMapping);
|
||||
beanFactory.registerSingleton("pluginHandlerMappingManager", handlerMappingManager);
|
||||
});
|
||||
|
||||
context.registerBean(PluginControllerManager.class);
|
||||
beanFactory.registerSingleton("springPluginStoppedEventAdapter",
|
||||
new SpringPluginStoppedEventAdapter(pluginId));
|
||||
beanFactory.registerSingleton("haloPluginEventBridge", new HaloPluginEventBridge());
|
||||
|
||||
rootContext.getBeanProvider(FinderRegistry.class)
|
||||
.ifAvailable(finderRegistry -> {
|
||||
var finderManager = new FinderManager(pluginId, finderRegistry);
|
||||
beanFactory.registerSingleton("finderManager", finderManager);
|
||||
});
|
||||
|
||||
rootContext.getBeanProvider(PluginRouterFunctionRegistry.class)
|
||||
.ifUnique(registry -> {
|
||||
var pluginRouterFunctionManager = new PluginRouterFunctionManager(registry);
|
||||
beanFactory.registerSingleton(
|
||||
"pluginRouterFunctionManager",
|
||||
pluginRouterFunctionManager
|
||||
);
|
||||
});
|
||||
|
||||
rootContext.getBeanProvider(SharedEventListenerRegistry.class)
|
||||
.ifUnique(listenerRegistry -> {
|
||||
var shareEventListenerAdapter = new ShareEventListenerAdapter(listenerRegistry);
|
||||
beanFactory.registerSingleton(
|
||||
"shareEventListenerAdapter",
|
||||
shareEventListenerAdapter
|
||||
);
|
||||
});
|
||||
sw.stop();
|
||||
|
||||
sw.start("LoadComponents");
|
||||
var classNames = pluginManager.getExtensionClassNames(pluginId);
|
||||
classNames.stream()
|
||||
.map(className -> {
|
||||
try {
|
||||
return classLoader.loadClass(className);
|
||||
} catch (ClassNotFoundException e) {
|
||||
throw new PluginRuntimeException(String.format("""
|
||||
Failed to load class %s for plugin %s.\
|
||||
""", className, pluginId), e);
|
||||
}
|
||||
})
|
||||
.forEach(clazzName -> context.registerBean(clazzName));
|
||||
sw.stop();
|
||||
log.debug("Created application context for plugin {}", pluginId);
|
||||
|
||||
log.debug("Refreshing application context for plugin {}", pluginId);
|
||||
sw.start("Refresh");
|
||||
context.refresh();
|
||||
sw.stop();
|
||||
log.debug("Refreshed application context for plugin {}", pluginId);
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("\n{}", sw.prettyPrint(TimeUnit.MILLISECONDS));
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
private static class ShareEventListenerAdapter {
|
||||
|
||||
private final SharedEventListenerRegistry listenerRegistry;
|
||||
|
||||
private ApplicationListener<ApplicationEvent> listener;
|
||||
|
||||
private ShareEventListenerAdapter(SharedEventListenerRegistry listenerRegistry) {
|
||||
this.listenerRegistry = listenerRegistry;
|
||||
}
|
||||
|
||||
@EventListener
|
||||
public void onApplicationEvent(ContextRefreshedEvent event) {
|
||||
this.listener = sharedEvent -> event.getApplicationContext().publishEvent(sharedEvent);
|
||||
listenerRegistry.register(this.listener);
|
||||
}
|
||||
|
||||
@EventListener(ContextClosedEvent.class)
|
||||
public void onApplicationEvent() {
|
||||
if (this.listener != null) {
|
||||
this.listenerRegistry.unregister(this.listener);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class FinderManager {
|
||||
|
||||
private final String pluginId;
|
||||
|
||||
private final FinderRegistry finderRegistry;
|
||||
|
||||
private FinderManager(String pluginId, FinderRegistry finderRegistry) {
|
||||
this.pluginId = pluginId;
|
||||
this.finderRegistry = finderRegistry;
|
||||
}
|
||||
|
||||
@EventListener
|
||||
public void onApplicationEvent(ContextClosedEvent ignored) {
|
||||
this.finderRegistry.unregister(this.pluginId);
|
||||
}
|
||||
|
||||
@EventListener
|
||||
public void onApplicationEvent(ContextRefreshedEvent event) {
|
||||
this.finderRegistry.register(this.pluginId, event.getApplicationContext());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class PluginRouterFunctionManager {
|
||||
|
||||
private final PluginRouterFunctionRegistry routerFunctionRegistry;
|
||||
|
||||
private Collection<RouterFunction<ServerResponse>> routerFunctions;
|
||||
|
||||
private PluginRouterFunctionManager(PluginRouterFunctionRegistry routerFunctionRegistry) {
|
||||
this.routerFunctionRegistry = routerFunctionRegistry;
|
||||
}
|
||||
|
||||
@EventListener
|
||||
public void onApplicationEvent(ContextClosedEvent ignored) {
|
||||
if (routerFunctions != null) {
|
||||
routerFunctionRegistry.unregister(routerFunctions);
|
||||
}
|
||||
}
|
||||
|
||||
@EventListener
|
||||
public void onApplicationEvent(ContextRefreshedEvent event) {
|
||||
var routerFunctions = event.getApplicationContext()
|
||||
.<RouterFunction<ServerResponse>>getBeanProvider(
|
||||
ResolvableType.forClassWithGenerics(RouterFunction.class, ServerResponse.class)
|
||||
)
|
||||
.orderedStream()
|
||||
.toList();
|
||||
routerFunctionRegistry.register(routerFunctions);
|
||||
this.routerFunctions = routerFunctions;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static class PluginHandlerMappingManager {
|
||||
private final String pluginId;
|
||||
|
||||
private final PluginRequestMappingHandlerMapping handlerMapping;
|
||||
|
||||
private PluginHandlerMappingManager(String pluginId,
|
||||
PluginRequestMappingHandlerMapping handlerMapping) {
|
||||
this.pluginId = pluginId;
|
||||
this.handlerMapping = handlerMapping;
|
||||
}
|
||||
|
||||
@EventListener
|
||||
public void onApplicationEvent(ContextRefreshedEvent event) {
|
||||
var context = event.getApplicationContext();
|
||||
context.getBeansWithAnnotation(Controller.class)
|
||||
.values()
|
||||
.forEach(controller ->
|
||||
handlerMapping.registerHandlerMethods(this.pluginId, controller)
|
||||
);
|
||||
}
|
||||
|
||||
@EventListener
|
||||
public void onApplicationEvent(ContextClosedEvent ignored) {
|
||||
handlerMapping.unregister(this.pluginId);
|
||||
}
|
||||
}
|
||||
|
||||
private class SpringPluginStoppedEventAdapter
|
||||
implements ApplicationListener<ContextClosedEvent> {
|
||||
|
||||
private final String pluginId;
|
||||
|
||||
private SpringPluginStoppedEventAdapter(String pluginId) {
|
||||
this.pluginId = pluginId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplicationEvent(ContextClosedEvent event) {
|
||||
var plugin = pluginManager.getPlugin(pluginId).getPlugin();
|
||||
if (plugin instanceof SpringPlugin springPlugin) {
|
||||
event.getApplicationContext()
|
||||
.publishEvent(new SpringPluginStoppedEvent(this, springPlugin));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class HaloPluginEventBridge {
|
||||
|
||||
@EventListener
|
||||
public void onApplicationEvent(SpringPluginStartedEvent event) {
|
||||
var pluginContext = event.getSpringPlugin().getPluginContext();
|
||||
var pluginWrapper = pluginManager.getPlugin(pluginContext.getName());
|
||||
|
||||
pluginManager.getRootContext()
|
||||
.publishEvent(new HaloPluginStartedEvent(this, pluginWrapper));
|
||||
}
|
||||
|
||||
@EventListener
|
||||
public void onApplicationEvent(SpringPluginStoppingEvent event) {
|
||||
var pluginContext = event.getSpringPlugin().getPluginContext();
|
||||
var pluginWrapper = pluginManager.getPlugin(pluginContext.getName());
|
||||
pluginManager.getRootContext()
|
||||
.publishEvent(new HaloPluginBeforeStopEvent(this, pluginWrapper));
|
||||
}
|
||||
|
||||
@EventListener
|
||||
public void onApplicationEvent(SpringPluginStoppedEvent event) {
|
||||
var pluginContext = event.getSpringPlugin().getPluginContext();
|
||||
var pluginWrapper = pluginManager.getPlugin(pluginContext.getName());
|
||||
pluginManager.getRootContext()
|
||||
.publishEvent(new HaloPluginStoppedEvent(this, pluginWrapper));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private List<PropertySource<?>> resolvePropertySources(String pluginId,
|
||||
ResourceLoader resourceLoader) {
|
||||
var haloProperties = pluginManager.getRootContext()
|
||||
.getBeanProvider(HaloProperties.class)
|
||||
.getIfAvailable();
|
||||
if (haloProperties == null) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
var propertySourceLoader = new YamlPropertySourceLoader();
|
||||
var propertySources = new ArrayList<PropertySource<?>>();
|
||||
var configsPath = haloProperties.getWorkDir().resolve("plugins").resolve("configs");
|
||||
// resolve user defined config
|
||||
Stream.of(
|
||||
configsPath.resolve(pluginId + ".yaml"),
|
||||
configsPath.resolve(pluginId + ".yml")
|
||||
)
|
||||
.map(path -> resourceLoader.getResource(path.toUri().toString()))
|
||||
.forEach(resource -> {
|
||||
var sources =
|
||||
loadPropertySources("user-defined-config", resource, propertySourceLoader);
|
||||
propertySources.addAll(sources);
|
||||
});
|
||||
|
||||
// resolve default config
|
||||
Stream.of(
|
||||
CLASSPATH_URL_PREFIX + "/config.yaml",
|
||||
CLASSPATH_URL_PREFIX + "/config.yml"
|
||||
)
|
||||
.map(resourceLoader::getResource)
|
||||
.forEach(resource -> {
|
||||
var sources = loadPropertySources("default-config", resource, propertySourceLoader);
|
||||
propertySources.addAll(sources);
|
||||
});
|
||||
return propertySources;
|
||||
}
|
||||
|
||||
private List<PropertySource<?>> loadPropertySources(String propertySourceName,
|
||||
Resource resource,
|
||||
PropertySourceLoader propertySourceLoader) {
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Loading property sources from {}", resource);
|
||||
}
|
||||
if (!resource.exists()) {
|
||||
return List.of();
|
||||
}
|
||||
try {
|
||||
return propertySourceLoader.load(propertySourceName, resource);
|
||||
} catch (IOException e) {
|
||||
throw Exceptions.propagate(e);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
package run.halo.app.plugin;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.reactive.function.server.HandlerFunction;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
import org.springframework.web.reactive.function.server.RouterFunctions;
|
||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
* A composite {@link RouterFunction} implementation for plugin.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Component
|
||||
public class DefaultPluginRouterFunctionRegistry
|
||||
implements RouterFunction<ServerResponse>, PluginRouterFunctionRegistry {
|
||||
|
||||
private final Collection<RouterFunction<ServerResponse>> routerFunctions;
|
||||
|
||||
public DefaultPluginRouterFunctionRegistry() {
|
||||
this.routerFunctions = new CopyOnWriteArraySet<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public Mono<HandlerFunction<ServerResponse>> route(@NonNull ServerRequest request) {
|
||||
return Flux.fromIterable(this.routerFunctions)
|
||||
.concatMap(routerFunction -> routerFunction.route(request))
|
||||
.next();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void accept(@NonNull RouterFunctions.Visitor visitor) {
|
||||
this.routerFunctions.forEach(routerFunction -> routerFunction.accept(visitor));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void register(Collection<RouterFunction<ServerResponse>> routerFunctions) {
|
||||
this.routerFunctions.addAll(routerFunctions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unregister(Collection<RouterFunction<ServerResponse>> routerFunctions) {
|
||||
this.routerFunctions.removeAll(routerFunctions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Only for testing.
|
||||
*
|
||||
* @return maintained router functions.
|
||||
*/
|
||||
Collection<RouterFunction<ServerResponse>> getRouterFunctions() {
|
||||
return routerFunctions;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
package run.halo.app.plugin;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import org.springframework.context.ApplicationEvent;
|
||||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class DefaultSharedEventListenerRegistry implements
|
||||
ApplicationListener<ApplicationEvent>, SharedEventListenerRegistry {
|
||||
|
||||
private final List<ApplicationListener<ApplicationEvent>> listeners;
|
||||
|
||||
public DefaultSharedEventListenerRegistry() {
|
||||
listeners = new CopyOnWriteArrayList<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplicationEvent(ApplicationEvent event) {
|
||||
if (!event.getClass().isAnnotationPresent(SharedEvent.class)) {
|
||||
return;
|
||||
}
|
||||
listeners.forEach(listener -> listener.onApplicationEvent(event));
|
||||
}
|
||||
|
||||
public void register(ApplicationListener<ApplicationEvent> listener) {
|
||||
this.listeners.add(listener);
|
||||
}
|
||||
|
||||
public void unregister(ApplicationListener<ApplicationEvent> listener) {
|
||||
this.listeners.remove(listener);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
package run.halo.app.plugin;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import org.pf4j.DevelopmentPluginLoader;
|
||||
import org.pf4j.PluginDescriptor;
|
||||
import org.pf4j.PluginManager;
|
||||
|
||||
public class DevPluginLoader extends DevelopmentPluginLoader {
|
||||
|
||||
private final PluginProperties pluginProperties;
|
||||
|
||||
public DevPluginLoader(
|
||||
PluginManager pluginManager,
|
||||
PluginProperties pluginProperties
|
||||
) {
|
||||
super(pluginManager);
|
||||
this.pluginProperties = pluginProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClassLoader loadPlugin(Path pluginPath, PluginDescriptor pluginDescriptor) {
|
||||
var classesDirectories = pluginProperties.getClassesDirectories();
|
||||
if (classesDirectories != null) {
|
||||
classesDirectories.forEach(
|
||||
classesDirectory -> pluginClasspath.addClassesDirectories(classesDirectory)
|
||||
);
|
||||
}
|
||||
var libDirectories = pluginProperties.getLibDirectories();
|
||||
if (libDirectories != null) {
|
||||
libDirectories.forEach(
|
||||
libDirectory -> pluginClasspath.addJarsDirectories(libDirectory)
|
||||
);
|
||||
}
|
||||
return super.loadPlugin(pluginPath, pluginDescriptor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(Path pluginPath) {
|
||||
// Currently we only support a plugin loading from directory in dev mode.
|
||||
return Files.isDirectory(pluginPath);
|
||||
}
|
||||
}
|
|
@ -3,6 +3,7 @@ package run.halo.app.plugin;
|
|||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.pf4j.ExtensionPoint;
|
||||
import org.pf4j.PluginManager;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
|
@ -15,12 +16,12 @@ import org.springframework.stereotype.Component;
|
|||
@Component
|
||||
public class ExtensionComponentsFinder {
|
||||
public static final String SYSTEM_PLUGIN_ID = "system";
|
||||
private final HaloPluginManager haloPluginManager;
|
||||
private final PluginManager pluginManager;
|
||||
private final ApplicationContext applicationContext;
|
||||
|
||||
public ExtensionComponentsFinder(HaloPluginManager haloPluginManager,
|
||||
public ExtensionComponentsFinder(PluginManager pluginManager,
|
||||
ApplicationContext applicationContext) {
|
||||
this.haloPluginManager = haloPluginManager;
|
||||
this.pluginManager = pluginManager;
|
||||
this.applicationContext = applicationContext;
|
||||
}
|
||||
|
||||
|
@ -33,7 +34,7 @@ public class ExtensionComponentsFinder {
|
|||
*/
|
||||
public <T> List<T> getExtensions(Class<T> type) {
|
||||
assertExtensionPoint(type);
|
||||
List<T> components = new ArrayList<>(haloPluginManager.getExtensions(type));
|
||||
List<T> components = new ArrayList<>(pluginManager.getExtensions(type));
|
||||
components.addAll(applicationContext.getBeansOfType(type).values());
|
||||
return List.copyOf(components);
|
||||
}
|
||||
|
@ -53,7 +54,7 @@ public class ExtensionComponentsFinder {
|
|||
components.addAll(applicationContext.getBeansOfType(type).values());
|
||||
return components;
|
||||
} else {
|
||||
components.addAll(haloPluginManager.getExtensions(type, pluginId));
|
||||
components.addAll(pluginManager.getExtensions(type, pluginId));
|
||||
}
|
||||
return components;
|
||||
}
|
||||
|
|
|
@ -1,130 +0,0 @@
|
|||
package run.halo.app.plugin;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||
import org.springframework.lang.NonNull;
|
||||
|
||||
/**
|
||||
* <p>Plugin application context registrar.</p>
|
||||
* <p>It contains a map, the key is the plugin id and the value is application context of plugin
|
||||
* .</p>
|
||||
* <p>when the plugin is enabled, an application context will be registered in the map by plugin id.
|
||||
* it will be deleted according to its id when the plugin is disabled.</p>
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2021-11-15
|
||||
*/
|
||||
public class ExtensionContextRegistry {
|
||||
private static final ExtensionContextRegistry INSTANCE = new ExtensionContextRegistry();
|
||||
|
||||
private final Map<String, PluginApplicationContext> registry = new HashMap<>();
|
||||
private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
|
||||
|
||||
public static ExtensionContextRegistry getInstance() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
private ExtensionContextRegistry() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Acquire the read lock when using getPluginApplicationContexts and getByPluginId.
|
||||
*/
|
||||
public void acquireReadLock() {
|
||||
this.readWriteLock.readLock().lock();
|
||||
}
|
||||
|
||||
/**
|
||||
* Release the read lock after using getPluginApplicationContexts and getByPluginId.
|
||||
*/
|
||||
public void releaseReadLock() {
|
||||
this.readWriteLock.readLock().unlock();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register plugin application context to registry map.
|
||||
*
|
||||
* @param pluginId plugin id(name)
|
||||
* @param context plugin application context
|
||||
*/
|
||||
public void register(String pluginId, PluginApplicationContext context) {
|
||||
this.readWriteLock.writeLock().lock();
|
||||
try {
|
||||
registry.put(pluginId, context);
|
||||
} finally {
|
||||
this.readWriteLock.writeLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove plugin application context from registry map.
|
||||
*
|
||||
* @param pluginId plugin id
|
||||
*/
|
||||
public void remove(String pluginId) {
|
||||
this.readWriteLock.writeLock().lock();
|
||||
try {
|
||||
PluginApplicationContext removed = registry.remove(pluginId);
|
||||
if (removed != null) {
|
||||
removed.close();
|
||||
}
|
||||
} finally {
|
||||
this.readWriteLock.writeLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets plugin application context by plugin id from registry map.
|
||||
* Note: ensure call {@link #containsContext(String)} after call this method.
|
||||
*
|
||||
* @param pluginId plugin id
|
||||
* @return plugin application context
|
||||
* @throws IllegalArgumentException if plugin id not found in registry
|
||||
*/
|
||||
@NonNull
|
||||
public PluginApplicationContext getByPluginId(String pluginId) {
|
||||
this.readWriteLock.readLock().lock();
|
||||
try {
|
||||
PluginApplicationContext context = registry.get(pluginId);
|
||||
if (context == null) {
|
||||
throw new IllegalArgumentException(
|
||||
String.format("The plugin [%s] can not be found.", pluginId));
|
||||
}
|
||||
return context;
|
||||
} finally {
|
||||
this.readWriteLock.readLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the registry contains the plugin application context by plugin id.
|
||||
*
|
||||
* @param pluginId plugin id
|
||||
* @return true if contains, otherwise false
|
||||
*/
|
||||
public boolean containsContext(String pluginId) {
|
||||
this.readWriteLock.readLock().lock();
|
||||
try {
|
||||
return registry.containsKey(pluginId);
|
||||
} finally {
|
||||
this.readWriteLock.readLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all plugin application contexts from registry map.
|
||||
*
|
||||
* @return plugin application contexts
|
||||
*/
|
||||
public List<PluginApplicationContext> getPluginApplicationContexts() {
|
||||
this.readWriteLock.readLock().lock();
|
||||
try {
|
||||
return new ArrayList<>(registry.values());
|
||||
} finally {
|
||||
this.readWriteLock.readLock().unlock();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,60 +1,65 @@
|
|||
package run.halo.app.plugin;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.pf4j.CompoundPluginLoader;
|
||||
import org.pf4j.CompoundPluginRepository;
|
||||
import org.pf4j.DefaultPluginManager;
|
||||
import org.pf4j.DefaultPluginRepository;
|
||||
import org.pf4j.ExtensionFactory;
|
||||
import org.pf4j.ExtensionFinder;
|
||||
import org.pf4j.PluginDependency;
|
||||
import org.pf4j.JarPluginLoader;
|
||||
import org.pf4j.JarPluginRepository;
|
||||
import org.pf4j.PluginDescriptor;
|
||||
import org.pf4j.PluginDescriptorFinder;
|
||||
import org.pf4j.PluginFactory;
|
||||
import org.pf4j.PluginRuntimeException;
|
||||
import org.pf4j.PluginState;
|
||||
import org.pf4j.PluginStateEvent;
|
||||
import org.pf4j.PluginLoader;
|
||||
import org.pf4j.PluginRepository;
|
||||
import org.pf4j.PluginStatusProvider;
|
||||
import org.pf4j.PluginWrapper;
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.DisposableBean;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationContextAware;
|
||||
import org.springframework.lang.NonNull;
|
||||
import run.halo.app.plugin.event.HaloPluginBeforeStopEvent;
|
||||
import run.halo.app.plugin.event.HaloPluginLoadedEvent;
|
||||
import run.halo.app.plugin.event.HaloPluginStartedEvent;
|
||||
import run.halo.app.plugin.event.HaloPluginStateChangedEvent;
|
||||
import run.halo.app.plugin.event.HaloPluginStoppedEvent;
|
||||
import org.springframework.data.util.Lazy;
|
||||
import run.halo.app.infra.SystemVersionSupplier;
|
||||
|
||||
/**
|
||||
* PluginManager to hold the main ApplicationContext.
|
||||
* It provides methods for managing the plugin lifecycle.
|
||||
*
|
||||
* @author guqing
|
||||
* @author johnniang
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Slf4j
|
||||
public class HaloPluginManager extends DefaultPluginManager
|
||||
implements ApplicationContextAware, InitializingBean, DisposableBean {
|
||||
public class HaloPluginManager extends DefaultPluginManager implements SpringPluginManager {
|
||||
|
||||
private final Map<String, PluginStartingError> startingErrors = new HashMap<>();
|
||||
private final ApplicationContext rootContext;
|
||||
|
||||
private ApplicationContext rootApplicationContext;
|
||||
private final Lazy<ApplicationContext> sharedContext;
|
||||
|
||||
private PluginApplicationInitializer pluginApplicationInitializer;
|
||||
private final PluginProperties pluginProperties;
|
||||
|
||||
private PluginRequestMappingManager requestMappingManager;
|
||||
public HaloPluginManager(ApplicationContext rootContext,
|
||||
PluginProperties pluginProperties,
|
||||
SystemVersionSupplier systemVersionSupplier) {
|
||||
this.pluginProperties = pluginProperties;
|
||||
this.rootContext = rootContext;
|
||||
// We have to initialize share context lazily because the root context has not refreshed
|
||||
this.sharedContext = Lazy.of(() -> SharedApplicationContextFactory.create(rootContext));
|
||||
super.runtimeMode = pluginProperties.getRuntimeMode();
|
||||
|
||||
public HaloPluginManager() {
|
||||
super();
|
||||
setExactVersionAllowed(pluginProperties.isExactVersionAllowed());
|
||||
setSystemVersion(systemVersionSupplier.get().getNormalVersion());
|
||||
|
||||
super.initialize();
|
||||
}
|
||||
|
||||
public HaloPluginManager(Path pluginsRoot) {
|
||||
super(pluginsRoot);
|
||||
@Override
|
||||
protected void initialize() {
|
||||
// Leave the implementation empty because the super#initialize eagerly initializes
|
||||
// components before properties set.
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -64,31 +69,15 @@ public class HaloPluginManager extends DefaultPluginManager
|
|||
|
||||
@Override
|
||||
protected ExtensionFinder createExtensionFinder() {
|
||||
return new SpringComponentsFinder(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void setApplicationContext(@NonNull ApplicationContext rootApplicationContext)
|
||||
throws BeansException {
|
||||
this.rootApplicationContext = rootApplicationContext;
|
||||
}
|
||||
|
||||
final PluginApplicationContext getPluginApplicationContext(String pluginId) {
|
||||
return pluginApplicationInitializer.getPluginApplicationContext(pluginId);
|
||||
var finder = new SpringComponentsFinder(this);
|
||||
addPluginStateListener(finder);
|
||||
return finder;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PluginFactory createPluginFactory() {
|
||||
return new BasePluginFactory();
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void afterPropertiesSet() {
|
||||
this.pluginApplicationInitializer =
|
||||
new PluginApplicationInitializer(this, rootApplicationContext);
|
||||
|
||||
this.requestMappingManager =
|
||||
rootApplicationContext.getBean(PluginRequestMappingManager.class);
|
||||
var contextFactory = new DefaultPluginApplicationContextFactory(this);
|
||||
return new SpringPluginFactory(contextFactory);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -101,257 +90,71 @@ public class HaloPluginManager extends DefaultPluginManager
|
|||
ClassLoader pluginClassLoader) {
|
||||
// create the plugin wrapper
|
||||
log.debug("Creating wrapper for plugin '{}'", pluginPath);
|
||||
HaloPluginWrapper pluginWrapper =
|
||||
var pluginWrapper =
|
||||
new HaloPluginWrapper(this, pluginDescriptor, pluginPath, pluginClassLoader);
|
||||
pluginWrapper.setPluginFactory(getPluginFactory());
|
||||
return pluginWrapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void firePluginStateEvent(PluginStateEvent event) {
|
||||
rootApplicationContext.publishEvent(
|
||||
new HaloPluginStateChangedEvent(this, event.getPlugin(), event.getOldState()));
|
||||
super.firePluginStateEvent(event);
|
||||
protected PluginLoader createPluginLoader() {
|
||||
var compoundLoader = new CompoundPluginLoader();
|
||||
compoundLoader.add(new DevPluginLoader(this, this.pluginProperties), this::isDevelopment);
|
||||
compoundLoader.add(new JarPluginLoader(this));
|
||||
return compoundLoader;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PluginState stopPlugin(String pluginId, boolean stopDependents) {
|
||||
checkPluginId(pluginId);
|
||||
PluginWrapper pluginWrapper = getPlugin(pluginId);
|
||||
PluginDescriptor pluginDescriptor = pluginWrapper.getDescriptor();
|
||||
PluginState pluginState = pluginWrapper.getPluginState();
|
||||
if (PluginState.STOPPED == pluginState) {
|
||||
log.debug("Already stopped plugin '{}'", getPluginLabel(pluginDescriptor));
|
||||
return PluginState.STOPPED;
|
||||
protected PluginStatusProvider createPluginStatusProvider() {
|
||||
if (PropertyPluginStatusProvider.isPropertySet(pluginProperties)) {
|
||||
return new PropertyPluginStatusProvider(pluginProperties);
|
||||
}
|
||||
|
||||
// test for disabled plugin
|
||||
if (PluginState.DISABLED == pluginState) {
|
||||
// do nothing
|
||||
return pluginState;
|
||||
}
|
||||
|
||||
rootApplicationContext.publishEvent(new HaloPluginBeforeStopEvent(this, pluginWrapper));
|
||||
|
||||
if (stopDependents) {
|
||||
List<String> dependents = dependencyResolver.getDependents(pluginId);
|
||||
while (!dependents.isEmpty()) {
|
||||
String dependent = dependents.remove(0);
|
||||
stopPlugin(dependent, false);
|
||||
dependents.addAll(0, dependencyResolver.getDependents(dependent));
|
||||
}
|
||||
}
|
||||
try {
|
||||
log.info("Stop plugin '{}'", getPluginLabel(pluginDescriptor));
|
||||
if (pluginWrapper.getPlugin() != null) {
|
||||
pluginWrapper.getPlugin().stop();
|
||||
}
|
||||
pluginWrapper.setPluginState(PluginState.STOPPED);
|
||||
// release plugin resources
|
||||
releaseAdditionalResources(pluginId);
|
||||
|
||||
startedPlugins.remove(pluginWrapper);
|
||||
|
||||
rootApplicationContext.publishEvent(new HaloPluginStoppedEvent(this, pluginWrapper));
|
||||
firePluginStateEvent(new PluginStateEvent(this, pluginWrapper, pluginState));
|
||||
} catch (Exception e) {
|
||||
log.error(e.getMessage(), e);
|
||||
startingErrors.put(pluginWrapper.getPluginId(), PluginStartingError.of(
|
||||
pluginWrapper.getPluginId(), e.getMessage(), e.toString()));
|
||||
}
|
||||
return pluginWrapper.getPluginState();
|
||||
return super.createPluginStatusProvider();
|
||||
}
|
||||
|
||||
@Override
|
||||
public PluginState stopPlugin(String pluginId) {
|
||||
return this.stopPlugin(pluginId, true);
|
||||
protected PluginRepository createPluginRepository() {
|
||||
var developmentPluginRepository =
|
||||
new DefaultDevelopmentPluginRepository(getPluginsRoots());
|
||||
developmentPluginRepository
|
||||
.setFixedPaths(pluginProperties.getFixedPluginPath());
|
||||
return new CompoundPluginRepository()
|
||||
.add(developmentPluginRepository, this::isDevelopment)
|
||||
.add(new JarPluginRepository(getPluginsRoots()))
|
||||
.add(new DefaultPluginRepository(getPluginsRoots()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<Path> createPluginsRoot() {
|
||||
var pluginsRoot = pluginProperties.getPluginsRoot();
|
||||
if (StringUtils.isNotBlank(pluginsRoot)) {
|
||||
return List.of(Paths.get(pluginsRoot));
|
||||
}
|
||||
return super.createPluginsRoot();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startPlugins() {
|
||||
startingErrors.clear();
|
||||
long ts = System.currentTimeMillis();
|
||||
|
||||
for (PluginWrapper pluginWrapper : resolvedPlugins) {
|
||||
checkExtensionFinderReady(pluginWrapper);
|
||||
PluginState pluginState = pluginWrapper.getPluginState();
|
||||
if ((PluginState.DISABLED != pluginState) && (PluginState.STARTED != pluginState)) {
|
||||
try {
|
||||
log.info("Start plugin '{}'", getPluginLabel(pluginWrapper.getDescriptor()));
|
||||
// inject bean
|
||||
pluginApplicationInitializer.onStartUp(pluginWrapper.getPluginId());
|
||||
|
||||
pluginWrapper.getPlugin().start();
|
||||
|
||||
requestMappingManager.registerHandlerMappings(pluginWrapper);
|
||||
|
||||
pluginWrapper.setPluginState(PluginState.STARTED);
|
||||
pluginWrapper.setFailedException(null);
|
||||
startedPlugins.add(pluginWrapper);
|
||||
|
||||
rootApplicationContext.publishEvent(
|
||||
new HaloPluginStartedEvent(this, pluginWrapper));
|
||||
} catch (Exception | LinkageError e) {
|
||||
pluginWrapper.setPluginState(PluginState.FAILED);
|
||||
pluginWrapper.setFailedException(e);
|
||||
startingErrors.put(pluginWrapper.getPluginId(), PluginStartingError.of(
|
||||
pluginWrapper.getPluginId(), e.getMessage(), e.toString()));
|
||||
releaseAdditionalResources(pluginWrapper.getPluginId());
|
||||
log.error("Unable to start plugin '{}'",
|
||||
getPluginLabel(pluginWrapper.getDescriptor()), e);
|
||||
} finally {
|
||||
firePluginStateEvent(new PluginStateEvent(this, pluginWrapper, pluginState));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.info("[Halo] {} plugins are started in {}ms. {} failed",
|
||||
getPlugins(PluginState.STARTED).size(),
|
||||
System.currentTimeMillis() - ts, startingErrors.size());
|
||||
}
|
||||
|
||||
@Override
|
||||
public PluginState startPlugin(String pluginId) {
|
||||
return doStartPlugin(pluginId);
|
||||
throw new UnsupportedOperationException(
|
||||
"The operation of starting all plugins is not supported."
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stopPlugins() {
|
||||
doStopPlugins();
|
||||
}
|
||||
|
||||
private PluginState doStartPlugin(String pluginId) {
|
||||
checkPluginId(pluginId);
|
||||
|
||||
PluginWrapper pluginWrapper = getPlugin(pluginId);
|
||||
|
||||
checkExtensionFinderReady(pluginWrapper);
|
||||
|
||||
PluginDescriptor pluginDescriptor = pluginWrapper.getDescriptor();
|
||||
PluginState pluginState = pluginWrapper.getPluginState();
|
||||
if (PluginState.STARTED == pluginState) {
|
||||
log.debug("Already started plugin '{}'", getPluginLabel(pluginDescriptor));
|
||||
return PluginState.STARTED;
|
||||
}
|
||||
|
||||
if (!resolvedPlugins.contains(pluginWrapper)) {
|
||||
log.warn("Cannot start an unresolved plugin '{}'", getPluginLabel(pluginDescriptor));
|
||||
return pluginState;
|
||||
}
|
||||
|
||||
if (PluginState.DISABLED == pluginState) {
|
||||
// automatically enable plugin on manual plugin start
|
||||
if (!enablePlugin(pluginId)) {
|
||||
return pluginState;
|
||||
}
|
||||
}
|
||||
|
||||
for (PluginDependency dependency : pluginDescriptor.getDependencies()) {
|
||||
// start dependency only if it marked as required (non-optional) or if it's optional
|
||||
// and loaded
|
||||
if (!dependency.isOptional() || plugins.containsKey(dependency.getPluginId())) {
|
||||
startPlugin(dependency.getPluginId());
|
||||
}
|
||||
}
|
||||
log.info("Start plugin '{}'", getPluginLabel(pluginDescriptor));
|
||||
|
||||
try {
|
||||
// load and inject bean
|
||||
pluginApplicationInitializer.onStartUp(pluginId);
|
||||
|
||||
// create plugin instance and start it
|
||||
pluginWrapper.getPlugin().start();
|
||||
|
||||
requestMappingManager.registerHandlerMappings(pluginWrapper);
|
||||
|
||||
pluginWrapper.setPluginState(PluginState.STARTED);
|
||||
startedPlugins.add(pluginWrapper);
|
||||
|
||||
rootApplicationContext.publishEvent(new HaloPluginStartedEvent(this, pluginWrapper));
|
||||
} catch (Exception e) {
|
||||
log.error("Unable to start plugin '{}'",
|
||||
getPluginLabel(pluginWrapper.getDescriptor()), e);
|
||||
pluginWrapper.setPluginState(PluginState.FAILED);
|
||||
startingErrors.put(pluginWrapper.getPluginId(), PluginStartingError.of(
|
||||
pluginWrapper.getPluginId(), e.getMessage(), e.toString()));
|
||||
releaseAdditionalResources(pluginId);
|
||||
} finally {
|
||||
firePluginStateEvent(new PluginStateEvent(this, pluginWrapper, pluginState));
|
||||
}
|
||||
return pluginWrapper.getPluginState();
|
||||
}
|
||||
|
||||
private void checkExtensionFinderReady(PluginWrapper pluginWrapper) {
|
||||
if (extensionFinder instanceof SpringComponentsFinder springComponentsFinder) {
|
||||
springComponentsFinder.readPluginStorageToMemory(pluginWrapper);
|
||||
return;
|
||||
}
|
||||
// should never happen
|
||||
throw new PluginRuntimeException("Plugin component classes may not loaded yet.");
|
||||
}
|
||||
|
||||
private void doStopPlugins() {
|
||||
startingErrors.clear();
|
||||
// stop started plugins in reverse order
|
||||
Collections.reverse(startedPlugins);
|
||||
Iterator<PluginWrapper> itr = startedPlugins.iterator();
|
||||
while (itr.hasNext()) {
|
||||
PluginWrapper pluginWrapper = itr.next();
|
||||
PluginState pluginState = pluginWrapper.getPluginState();
|
||||
if (PluginState.STARTED == pluginState) {
|
||||
try {
|
||||
rootApplicationContext.publishEvent(
|
||||
new HaloPluginBeforeStopEvent(this, pluginWrapper));
|
||||
log.info("Stop plugin '{}'", getPluginLabel(pluginWrapper.getDescriptor()));
|
||||
if (pluginWrapper.getPlugin() != null) {
|
||||
pluginWrapper.getPlugin().stop();
|
||||
}
|
||||
pluginWrapper.setPluginState(PluginState.STOPPED);
|
||||
itr.remove();
|
||||
releaseAdditionalResources(pluginWrapper.getPluginId());
|
||||
|
||||
rootApplicationContext.publishEvent(
|
||||
new HaloPluginStoppedEvent(this, pluginWrapper));
|
||||
firePluginStateEvent(new PluginStateEvent(this, pluginWrapper, pluginState));
|
||||
} catch (PluginRuntimeException e) {
|
||||
log.error(e.getMessage(), e);
|
||||
startingErrors.put(pluginWrapper.getPluginId(), PluginStartingError.of(
|
||||
pluginWrapper.getPluginId(), e.getMessage(), e.toString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Release plugin holding release on stop.
|
||||
*/
|
||||
public void releaseAdditionalResources(String pluginId) {
|
||||
removePluginComponentsCache(pluginId);
|
||||
// release request mapping
|
||||
requestMappingManager.removeHandlerMappings(pluginId);
|
||||
try {
|
||||
pluginApplicationInitializer.contextDestroyed(pluginId);
|
||||
} catch (Exception e) {
|
||||
log.warn("Plugin application context close failed. ", e);
|
||||
}
|
||||
throw new UnsupportedOperationException(
|
||||
"The operation of stopping all plugins is not supported."
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PluginWrapper loadPluginFromPath(Path pluginPath) {
|
||||
PluginWrapper pluginWrapper = super.loadPluginFromPath(pluginPath);
|
||||
rootApplicationContext.publishEvent(new HaloPluginLoadedEvent(this, pluginWrapper));
|
||||
return pluginWrapper;
|
||||
}
|
||||
|
||||
private void removePluginComponentsCache(String pluginId) {
|
||||
if (extensionFinder instanceof SpringComponentsFinder springComponentsFinder) {
|
||||
springComponentsFinder.removeComponentsStorage(pluginId);
|
||||
}
|
||||
public ApplicationContext getRootContext() {
|
||||
return rootContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() throws Exception {
|
||||
stopPlugins();
|
||||
public ApplicationContext getSharedContext() {
|
||||
return sharedContext.get();
|
||||
}
|
||||
// end-region
|
||||
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ package run.halo.app.plugin;
|
|||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.locks.StampedLock;
|
||||
import org.springframework.context.support.GenericApplicationContext;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
|
@ -16,20 +16,20 @@ import run.halo.app.extension.GroupVersionKind;
|
|||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public class PluginApplicationContext extends GenericApplicationContext {
|
||||
public class PluginApplicationContext extends AnnotationConfigApplicationContext {
|
||||
|
||||
private final GvkExtensionMapping gvkExtensionMapping = new GvkExtensionMapping();
|
||||
|
||||
private String pluginId;
|
||||
private final String pluginId;
|
||||
|
||||
public PluginApplicationContext(String pluginId) {
|
||||
this.pluginId = pluginId;
|
||||
}
|
||||
|
||||
public String getPluginId() {
|
||||
return pluginId;
|
||||
}
|
||||
|
||||
public void setPluginId(String pluginId) {
|
||||
this.pluginId = pluginId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the gvk-extension mapping.
|
||||
* It is thread safe
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
package run.halo.app.plugin;
|
||||
|
||||
import org.springframework.context.ApplicationContext;
|
||||
|
||||
public interface PluginApplicationContextFactory {
|
||||
|
||||
/**
|
||||
* Create and refresh application context.
|
||||
*
|
||||
* @param pluginId plugin id
|
||||
* @return refresh application context for the plugin.
|
||||
*/
|
||||
ApplicationContext create(String pluginId);
|
||||
|
||||
}
|
|
@ -1,47 +0,0 @@
|
|||
package run.halo.app.plugin;
|
||||
|
||||
import java.lang.reflect.AnnotatedElement;
|
||||
import java.util.List;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.ApplicationEvent;
|
||||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.core.annotation.AnnotationUtils;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* <p>The main application bridges the events marked with {@code SharedEvent} annotation to the
|
||||
* enabled plug-in so that it can be listened by the plugin.</p>
|
||||
*
|
||||
* @author guqing
|
||||
* @see SharedEvent
|
||||
* @see PluginApplicationContext
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class PluginApplicationEventBridgeDispatcher
|
||||
implements ApplicationListener<ApplicationEvent> {
|
||||
|
||||
@Override
|
||||
public void onApplicationEvent(ApplicationEvent event) {
|
||||
if (!isSharedEventAnnotationPresent(event.getClass())) {
|
||||
return;
|
||||
}
|
||||
ExtensionContextRegistry.getInstance().acquireReadLock();
|
||||
try {
|
||||
List<PluginApplicationContext> pluginApplicationContexts =
|
||||
ExtensionContextRegistry.getInstance().getPluginApplicationContexts();
|
||||
for (PluginApplicationContext pluginApplicationContext : pluginApplicationContexts) {
|
||||
log.debug("Bridging broadcast event [{}] to plugin [{}]", event,
|
||||
pluginApplicationContext.getPluginId());
|
||||
pluginApplicationContext.publishEvent(event);
|
||||
}
|
||||
} finally {
|
||||
ExtensionContextRegistry.getInstance().releaseReadLock();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isSharedEventAnnotationPresent(AnnotatedElement annotatedElement) {
|
||||
return AnnotationUtils.findAnnotation(annotatedElement, SharedEvent.class) != null;
|
||||
}
|
||||
}
|
|
@ -1,264 +0,0 @@
|
|||
package run.halo.app.plugin;
|
||||
|
||||
import static org.springframework.util.ResourceUtils.CLASSPATH_URL_PREFIX;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Stream;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.pf4j.PluginRuntimeException;
|
||||
import org.pf4j.PluginWrapper;
|
||||
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
|
||||
import org.springframework.boot.env.PropertySourceLoader;
|
||||
import org.springframework.boot.env.YamlPropertySourceLoader;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.AnnotationConfigUtils;
|
||||
import org.springframework.core.env.PropertySource;
|
||||
import org.springframework.core.io.DefaultResourceLoader;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StopWatch;
|
||||
import reactor.core.Exceptions;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.infra.properties.HaloProperties;
|
||||
import run.halo.app.theme.DefaultTemplateNameResolver;
|
||||
import run.halo.app.theme.DefaultViewNameResolver;
|
||||
|
||||
/**
|
||||
* Plugin application initializer will create plugin application context by plugin id and
|
||||
* register beans to plugin application context.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2021-11-01
|
||||
*/
|
||||
@Slf4j
|
||||
public class PluginApplicationInitializer {
|
||||
|
||||
protected final HaloPluginManager haloPluginManager;
|
||||
|
||||
private final ExtensionContextRegistry contextRegistry = ExtensionContextRegistry.getInstance();
|
||||
private final SharedApplicationContextHolder sharedApplicationContextHolder;
|
||||
private final ApplicationContext rootApplicationContext;
|
||||
|
||||
private final HaloProperties haloProperties;
|
||||
|
||||
public PluginApplicationInitializer(HaloPluginManager haloPluginManager,
|
||||
ApplicationContext rootApplicationContext) {
|
||||
Assert.notNull(haloPluginManager, "The haloPluginManager must not be null");
|
||||
Assert.notNull(rootApplicationContext, "The rootApplicationContext must not be null");
|
||||
this.haloPluginManager = haloPluginManager;
|
||||
this.rootApplicationContext = rootApplicationContext;
|
||||
sharedApplicationContextHolder = rootApplicationContext
|
||||
.getBean(SharedApplicationContextHolder.class);
|
||||
haloProperties = rootApplicationContext.getBean(HaloProperties.class);
|
||||
}
|
||||
|
||||
private PluginApplicationContext createPluginApplicationContext(String pluginId) {
|
||||
PluginWrapper plugin = haloPluginManager.getPlugin(pluginId);
|
||||
ClassLoader pluginClassLoader = plugin.getPluginClassLoader();
|
||||
|
||||
StopWatch stopWatch = new StopWatch("initialize-plugin-context");
|
||||
stopWatch.start("Create PluginApplicationContext");
|
||||
PluginApplicationContext pluginApplicationContext = new PluginApplicationContext();
|
||||
pluginApplicationContext.setClassLoader(pluginClassLoader);
|
||||
|
||||
if (sharedApplicationContextHolder != null) {
|
||||
pluginApplicationContext.setParent(sharedApplicationContextHolder.getInstance());
|
||||
}
|
||||
|
||||
// populate plugin to plugin application context
|
||||
pluginApplicationContext.setPluginId(pluginId);
|
||||
stopWatch.stop();
|
||||
|
||||
stopWatch.start("Create DefaultResourceLoader");
|
||||
DefaultResourceLoader defaultResourceLoader = new DefaultResourceLoader(pluginClassLoader);
|
||||
pluginApplicationContext.setResourceLoader(defaultResourceLoader);
|
||||
|
||||
var mutablePropertySources = pluginApplicationContext.getEnvironment().getPropertySources();
|
||||
resolvePropertySources(pluginId, pluginApplicationContext)
|
||||
.forEach(mutablePropertySources::addLast);
|
||||
|
||||
stopWatch.stop();
|
||||
|
||||
DefaultListableBeanFactory beanFactory =
|
||||
(DefaultListableBeanFactory) pluginApplicationContext.getBeanFactory();
|
||||
|
||||
stopWatch.start("registerAnnotationConfigProcessors");
|
||||
AnnotationConfigUtils.registerAnnotationConfigProcessors(beanFactory);
|
||||
stopWatch.stop();
|
||||
|
||||
pluginApplicationContext.registerBean(AggregatedRouterFunction.class);
|
||||
|
||||
beanFactory.registerSingleton("pluginContext", createPluginContext(plugin));
|
||||
|
||||
// TODO deprecated
|
||||
beanFactory.registerSingleton("pluginWrapper", haloPluginManager.getPlugin(pluginId));
|
||||
|
||||
beanFactory.registerSingleton("templateNameResolver",
|
||||
new DefaultTemplateNameResolver(
|
||||
rootApplicationContext.getBean(DefaultViewNameResolver.class),
|
||||
pluginApplicationContext));
|
||||
populateSettingFetcher(pluginId, beanFactory);
|
||||
|
||||
log.debug("Total millis: {} ms -> {}", stopWatch.getTotalTimeMillis(),
|
||||
stopWatch.prettyPrint());
|
||||
|
||||
return pluginApplicationContext;
|
||||
}
|
||||
|
||||
private void initApplicationContext(String pluginId) {
|
||||
if (contextRegistry.containsContext(pluginId)) {
|
||||
log.debug("Plugin application context for [{}] has bean initialized.", pluginId);
|
||||
return;
|
||||
}
|
||||
StopWatch stopWatch = new StopWatch();
|
||||
|
||||
stopWatch.start("createPluginApplicationContext");
|
||||
PluginApplicationContext pluginApplicationContext =
|
||||
createPluginApplicationContext(pluginId);
|
||||
stopWatch.stop();
|
||||
|
||||
stopWatch.start("findCandidateComponents");
|
||||
Set<Class<?>> candidateComponents = findCandidateComponents(pluginId);
|
||||
stopWatch.stop();
|
||||
|
||||
stopWatch.start("registerBean");
|
||||
for (Class<?> component : candidateComponents) {
|
||||
log.debug("Register a plugin component class [{}] to context", component);
|
||||
pluginApplicationContext.registerBean(component);
|
||||
}
|
||||
stopWatch.stop();
|
||||
|
||||
stopWatch.start("refresh plugin application context");
|
||||
pluginApplicationContext.refresh();
|
||||
stopWatch.stop();
|
||||
|
||||
contextRegistry.register(pluginId, pluginApplicationContext);
|
||||
|
||||
log.debug("initApplicationContext total millis: {} ms -> {}",
|
||||
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 =
|
||||
rootApplicationContext.getBean(ReactiveExtensionClient.class);
|
||||
ReactiveSettingFetcher reactiveSettingFetcher =
|
||||
new DefaultReactiveSettingFetcher(extensionClient, pluginName);
|
||||
listableBeanFactory.registerSingleton("settingFetcher",
|
||||
new DefaultSettingFetcher(reactiveSettingFetcher));
|
||||
listableBeanFactory.registerSingleton("reactiveSettingFetcher", reactiveSettingFetcher);
|
||||
}
|
||||
|
||||
public void onStartUp(String pluginId) {
|
||||
initApplicationContext(pluginId);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public PluginApplicationContext getPluginApplicationContext(String pluginId) {
|
||||
return contextRegistry.getByPluginId(pluginId);
|
||||
}
|
||||
|
||||
public void contextDestroyed(String pluginId) {
|
||||
Assert.notNull(pluginId, "pluginId must not be null");
|
||||
contextRegistry.remove(pluginId);
|
||||
}
|
||||
|
||||
private Set<Class<?>> findCandidateComponents(String pluginId) {
|
||||
StopWatch stopWatch = new StopWatch("findCandidateComponents");
|
||||
|
||||
stopWatch.start("getExtensionClassNames");
|
||||
Set<String> extensionClassNames = haloPluginManager.getExtensionClassNames(pluginId);
|
||||
if (extensionClassNames == null) {
|
||||
log.debug("No components class names found for plugin [{}]", pluginId);
|
||||
extensionClassNames = Set.of();
|
||||
}
|
||||
stopWatch.stop();
|
||||
|
||||
// add extensions for each started plugin
|
||||
PluginWrapper plugin = haloPluginManager.getPlugin(pluginId);
|
||||
log.debug("Registering extensions of the plugin '{}' as beans", pluginId);
|
||||
Set<Class<?>> candidateComponents = new HashSet<>();
|
||||
for (String extensionClassName : extensionClassNames) {
|
||||
log.debug("Load extension class '{}'", extensionClassName);
|
||||
try {
|
||||
stopWatch.start("loadClass");
|
||||
Class<?> extensionClass =
|
||||
plugin.getPluginClassLoader().loadClass(extensionClassName);
|
||||
stopWatch.stop();
|
||||
|
||||
candidateComponents.add(extensionClass);
|
||||
} catch (ClassNotFoundException e) {
|
||||
log.error(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
log.debug("total millis: {}ms -> {}", stopWatch.getTotalTimeMillis(),
|
||||
stopWatch.prettyPrint());
|
||||
return candidateComponents;
|
||||
}
|
||||
|
||||
private List<PropertySource<?>> resolvePropertySources(String pluginId,
|
||||
ResourceLoader resourceLoader) {
|
||||
var propertySourceLoader = new YamlPropertySourceLoader();
|
||||
var propertySources = new ArrayList<PropertySource<?>>();
|
||||
var configsPath = haloProperties.getWorkDir().resolve("plugins").resolve("configs");
|
||||
|
||||
// resolve user defined config
|
||||
Stream.of(
|
||||
configsPath.resolve(pluginId + ".yaml"),
|
||||
configsPath.resolve(pluginId + ".yml")
|
||||
)
|
||||
.map(path -> resourceLoader.getResource(path.toUri().toString()))
|
||||
.forEach(resource -> {
|
||||
var sources =
|
||||
loadPropertySources("user-defined-config", resource, propertySourceLoader);
|
||||
propertySources.addAll(sources);
|
||||
});
|
||||
|
||||
// resolve default config
|
||||
Stream.of(
|
||||
CLASSPATH_URL_PREFIX + "/config.yaml",
|
||||
CLASSPATH_URL_PREFIX + "/config.yaml"
|
||||
)
|
||||
.map(resourceLoader::getResource)
|
||||
.forEach(resource -> {
|
||||
var sources = loadPropertySources("default-config", resource, propertySourceLoader);
|
||||
propertySources.addAll(sources);
|
||||
});
|
||||
return propertySources;
|
||||
}
|
||||
|
||||
private List<PropertySource<?>> loadPropertySources(String propertySourceName,
|
||||
Resource resource,
|
||||
PropertySourceLoader propertySourceLoader) {
|
||||
logConfigLocation(resource);
|
||||
if (resource.exists()) {
|
||||
try {
|
||||
return propertySourceLoader.load(propertySourceName, resource);
|
||||
} catch (IOException e) {
|
||||
throw Exceptions.propagate(e);
|
||||
}
|
||||
}
|
||||
return List.of();
|
||||
}
|
||||
|
||||
private void logConfigLocation(Resource resource) {
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Loading property sources from {}", resource);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,29 +2,14 @@ package run.halo.app.plugin;
|
|||
|
||||
import static run.halo.app.plugin.resources.BundleResourceUtils.getJsBundleResource;
|
||||
|
||||
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;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.pf4j.CompoundPluginLoader;
|
||||
import org.pf4j.CompoundPluginRepository;
|
||||
import org.pf4j.DefaultPluginRepository;
|
||||
import org.pf4j.DevelopmentPluginLoader;
|
||||
import org.pf4j.JarPluginLoader;
|
||||
import org.pf4j.JarPluginRepository;
|
||||
import org.pf4j.PluginDescriptor;
|
||||
import org.pf4j.PluginLoader;
|
||||
import org.pf4j.PluginManager;
|
||||
import org.pf4j.PluginRepository;
|
||||
import org.pf4j.PluginStatusProvider;
|
||||
import org.pf4j.RuntimeMode;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.boot.autoconfigure.web.WebProperties;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.reactive.accept.RequestedContentTypeResolver;
|
||||
|
@ -46,23 +31,11 @@ import run.halo.app.infra.SystemVersionSupplier;
|
|||
@EnableConfigurationProperties(PluginProperties.class)
|
||||
public class PluginAutoConfiguration {
|
||||
|
||||
private final PluginProperties pluginProperties;
|
||||
|
||||
private final SystemVersionSupplier systemVersionSupplier;
|
||||
|
||||
@Qualifier("webFluxContentTypeResolver")
|
||||
private final RequestedContentTypeResolver requestedContentTypeResolver;
|
||||
|
||||
public PluginAutoConfiguration(PluginProperties pluginProperties,
|
||||
SystemVersionSupplier systemVersionSupplier,
|
||||
RequestedContentTypeResolver requestedContentTypeResolver) {
|
||||
this.pluginProperties = pluginProperties;
|
||||
this.systemVersionSupplier = systemVersionSupplier;
|
||||
this.requestedContentTypeResolver = requestedContentTypeResolver;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public PluginRequestMappingHandlerMapping pluginRequestMappingHandlerMapping() {
|
||||
public PluginRequestMappingHandlerMapping pluginRequestMappingHandlerMapping(
|
||||
@Qualifier("webFluxContentTypeResolver")
|
||||
RequestedContentTypeResolver requestedContentTypeResolver
|
||||
) {
|
||||
PluginRequestMappingHandlerMapping mapping = new PluginRequestMappingHandlerMapping();
|
||||
mapping.setContentTypeResolver(requestedContentTypeResolver);
|
||||
mapping.setOrder(-1);
|
||||
|
@ -70,113 +43,14 @@ public class PluginAutoConfiguration {
|
|||
}
|
||||
|
||||
@Bean
|
||||
public PluginRequestMappingManager pluginRequestMappingManager() {
|
||||
return new PluginRequestMappingManager(pluginRequestMappingHandlerMapping());
|
||||
public PluginManager pluginManager(ApplicationContext context,
|
||||
SystemVersionSupplier systemVersionSupplier,
|
||||
PluginProperties pluginProperties) {
|
||||
return new HaloPluginManager(context, pluginProperties, systemVersionSupplier);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public HaloPluginManager pluginManager() {
|
||||
// Setup RuntimeMode
|
||||
System.setProperty("pf4j.mode", pluginProperties.getRuntimeMode().toString());
|
||||
|
||||
// Setup Plugin folder
|
||||
String pluginsRoot =
|
||||
StringUtils.defaultString(pluginProperties.getPluginsRoot(), "plugins");
|
||||
|
||||
System.setProperty("pf4j.pluginsDir", pluginsRoot);
|
||||
String appHome = System.getProperty("app.home");
|
||||
if (RuntimeMode.DEPLOYMENT == pluginProperties.getRuntimeMode()
|
||||
&& StringUtils.isNotBlank(appHome)) {
|
||||
System.setProperty("pf4j.pluginsDir", appHome + File.separator + pluginsRoot);
|
||||
}
|
||||
|
||||
HaloPluginManager pluginManager =
|
||||
new HaloPluginManager(new File(pluginsRoot).toPath()) {
|
||||
@Override
|
||||
protected PluginLoader createPluginLoader() {
|
||||
if (pluginProperties.getCustomPluginLoader() != null) {
|
||||
Class<PluginLoader> clazz = pluginProperties.getCustomPluginLoader();
|
||||
try {
|
||||
Constructor<?> constructor = clazz.getConstructor(PluginManager.class);
|
||||
return (PluginLoader) constructor.newInstance(this);
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalArgumentException(
|
||||
String.format("Create custom PluginLoader %s failed. Make sure"
|
||||
+ "there is a constructor with one argument that accepts "
|
||||
+ "PluginLoader",
|
||||
clazz.getName()));
|
||||
}
|
||||
} else {
|
||||
return new CompoundPluginLoader()
|
||||
.add(createDevelopmentPluginLoader(this), this::isDevelopment)
|
||||
.add(new JarPluginLoader(this));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PluginStatusProvider createPluginStatusProvider() {
|
||||
if (PropertyPluginStatusProvider.isPropertySet(pluginProperties)) {
|
||||
return new PropertyPluginStatusProvider(pluginProperties);
|
||||
}
|
||||
return super.createPluginStatusProvider();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PluginRepository createPluginRepository() {
|
||||
var developmentPluginRepository =
|
||||
new DefaultDevelopmentPluginRepository(getPluginsRoots());
|
||||
developmentPluginRepository
|
||||
.setFixedPaths(pluginProperties.getFixedPluginPath());
|
||||
return new CompoundPluginRepository()
|
||||
.add(developmentPluginRepository, this::isDevelopment)
|
||||
.add(new JarPluginRepository(getPluginsRoots()))
|
||||
.add(new DefaultPluginRepository(getPluginsRoots()));
|
||||
}
|
||||
};
|
||||
|
||||
pluginManager.setExactVersionAllowed(pluginProperties.isExactVersionAllowed());
|
||||
// only for development mode
|
||||
if (RuntimeMode.DEPLOYMENT.equals(pluginManager.getRuntimeMode())) {
|
||||
pluginManager.setSystemVersion(getSystemVersion());
|
||||
}
|
||||
return pluginManager;
|
||||
}
|
||||
|
||||
DevelopmentPluginLoader createDevelopmentPluginLoader(PluginManager pluginManager) {
|
||||
return new DevelopmentPluginLoader(pluginManager) {
|
||||
|
||||
@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();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public RouterFunction<ServerResponse> pluginJsBundleRoute(HaloPluginManager haloPluginManager,
|
||||
public RouterFunction<ServerResponse> pluginJsBundleRoute(PluginManager pluginManager,
|
||||
WebProperties webProperties) {
|
||||
var cacheProperties = webProperties.getResources().getCache();
|
||||
return RouterFunctions.route()
|
||||
|
@ -184,7 +58,7 @@ public class PluginAutoConfiguration {
|
|||
String pluginName = request.pathVariable("name");
|
||||
String fileName = request.pathVariable("resource");
|
||||
|
||||
var jsBundle = getJsBundleResource(haloPluginManager, pluginName, fileName);
|
||||
var jsBundle = getJsBundleResource(pluginManager, pluginName, fileName);
|
||||
if (jsBundle == null || !jsBundle.exists()) {
|
||||
return ServerResponse.notFound().build();
|
||||
}
|
||||
|
|
|
@ -1,10 +1,15 @@
|
|||
package run.halo.app.plugin;
|
||||
|
||||
import java.time.Duration;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.retry.RetryException;
|
||||
import org.springframework.stereotype.Component;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.util.retry.Retry;
|
||||
import run.halo.app.extension.GroupVersionKind;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.plugin.event.HaloPluginBeforeStopEvent;
|
||||
|
||||
|
@ -14,6 +19,7 @@ import run.halo.app.plugin.event.HaloPluginBeforeStopEvent;
|
|||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class PluginBeforeStopSyncListener {
|
||||
|
||||
|
@ -24,14 +30,17 @@ public class PluginBeforeStopSyncListener {
|
|||
}
|
||||
|
||||
@EventListener
|
||||
public Mono<Void> onApplicationEvent(@NonNull HaloPluginBeforeStopEvent event) {
|
||||
public void onApplicationEvent(@NonNull HaloPluginBeforeStopEvent event) {
|
||||
var pluginWrapper = event.getPlugin();
|
||||
ExtensionContextRegistry registry = ExtensionContextRegistry.getInstance();
|
||||
if (!registry.containsContext(pluginWrapper.getPluginId())) {
|
||||
return Mono.empty();
|
||||
var p = pluginWrapper.getPlugin();
|
||||
if (!(p instanceof SpringPlugin springPlugin)) {
|
||||
return;
|
||||
}
|
||||
var pluginContext = registry.getByPluginId(pluginWrapper.getPluginId());
|
||||
return cleanUpPluginExtensionResources(pluginContext);
|
||||
var applicationContext = springPlugin.getApplicationContext();
|
||||
if (!(applicationContext instanceof PluginApplicationContext pluginApplicationContext)) {
|
||||
return;
|
||||
}
|
||||
cleanUpPluginExtensionResources(pluginApplicationContext).block(Duration.ofMinutes(1));
|
||||
}
|
||||
|
||||
private Mono<Void> cleanUpPluginExtensionResources(PluginApplicationContext context) {
|
||||
|
@ -39,7 +48,26 @@ public class PluginBeforeStopSyncListener {
|
|||
return Flux.fromIterable(gvkExtensionNames.entrySet())
|
||||
.flatMap(entry -> Flux.fromIterable(entry.getValue())
|
||||
.flatMap(extensionName -> client.fetch(entry.getKey(), extensionName))
|
||||
.flatMap(client::delete))
|
||||
.flatMap(client::delete)
|
||||
.flatMap(e -> waitForDeleted(e.groupVersionKind(), e.getMetadata().getName())))
|
||||
.then();
|
||||
}
|
||||
|
||||
private Mono<Void> waitForDeleted(GroupVersionKind gvk, String name) {
|
||||
return client.fetch(gvk, name)
|
||||
.flatMap(e -> {
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Wait for {}/{} deleted", gvk, name);
|
||||
}
|
||||
return Mono.error(new RetryException("Wait for extension deleted"));
|
||||
})
|
||||
.retryWhen(Retry.backoff(10, Duration.ofMillis(100))
|
||||
.filter(RetryException.class::isInstance))
|
||||
.then()
|
||||
.doOnSuccess(v -> {
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("{}/{} was deleted successfully.", gvk, name);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,70 +0,0 @@
|
|||
package run.halo.app.plugin;
|
||||
|
||||
import static run.halo.app.plugin.ExtensionContextRegistry.getInstance;
|
||||
|
||||
import com.google.common.collect.Iterables;
|
||||
import java.util.List;
|
||||
import org.springframework.context.support.AbstractApplicationContext;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.reactive.function.server.HandlerFunction;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
import org.springframework.web.reactive.function.server.RouterFunctions;
|
||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.plugin.resources.ReverseProxyRouterFunctionRegistry;
|
||||
|
||||
/**
|
||||
* A composite {@link RouterFunction} implementation for plugin.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Component
|
||||
public class PluginCompositeRouterFunction implements RouterFunction<ServerResponse> {
|
||||
|
||||
private final ReverseProxyRouterFunctionRegistry reverseProxyRouterFunctionFactory;
|
||||
|
||||
public PluginCompositeRouterFunction(
|
||||
ReverseProxyRouterFunctionRegistry reverseProxyRouterFunctionFactory) {
|
||||
this.reverseProxyRouterFunctionFactory = reverseProxyRouterFunctionFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public Mono<HandlerFunction<ServerResponse>> route(@NonNull ServerRequest request) {
|
||||
return Flux.fromIterable(routerFunctions())
|
||||
.concatMap(routerFunction -> routerFunction.route(request))
|
||||
.next();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void accept(@NonNull RouterFunctions.Visitor visitor) {
|
||||
routerFunctions().forEach(routerFunction -> routerFunction.accept(visitor));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Iterable<RouterFunction<ServerResponse>> routerFunctions() {
|
||||
getInstance().acquireReadLock();
|
||||
try {
|
||||
List<PluginApplicationContext> contexts = getInstance().getPluginApplicationContexts()
|
||||
.stream()
|
||||
.filter(AbstractApplicationContext::isActive)
|
||||
.toList();
|
||||
var rawRouterFunctions = contexts
|
||||
.stream()
|
||||
.flatMap(applicationContext -> applicationContext
|
||||
.getBeanProvider(RouterFunction.class)
|
||||
.orderedStream())
|
||||
.map(router -> (RouterFunction<ServerResponse>) router)
|
||||
.toList();
|
||||
var reverseProxies = reverseProxyRouterFunctionFactory.getRouterFunctions();
|
||||
|
||||
return Iterables.concat(rawRouterFunctions, reverseProxies);
|
||||
} finally {
|
||||
getInstance().releaseReadLock();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,58 +1,49 @@
|
|||
package run.halo.app.plugin;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.stream.Stream;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.stereotype.Component;
|
||||
import run.halo.app.extension.ExtensionClient;
|
||||
import run.halo.app.extension.controller.ControllerManager;
|
||||
import run.halo.app.extension.controller.DefaultControllerManager;
|
||||
import run.halo.app.extension.controller.Reconciler;
|
||||
import run.halo.app.extension.controller.Reconciler.Request;
|
||||
import run.halo.app.plugin.event.HaloPluginBeforeStopEvent;
|
||||
import run.halo.app.plugin.event.HaloPluginStartedEvent;
|
||||
import static org.springframework.core.ResolvableType.forClassWithGenerics;
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import org.springframework.context.event.ContextClosedEvent;
|
||||
import org.springframework.context.event.ContextRefreshedEvent;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import reactor.core.Disposable;
|
||||
import run.halo.app.extension.ExtensionClient;
|
||||
import run.halo.app.extension.controller.Controller;
|
||||
import run.halo.app.extension.controller.ControllerBuilder;
|
||||
import run.halo.app.extension.controller.Reconciler;
|
||||
|
||||
@Component
|
||||
public class PluginControllerManager {
|
||||
|
||||
private final Map<String, ControllerManager> controllerManagerMap;
|
||||
private final ConcurrentHashMap<String, Controller> controllers;
|
||||
|
||||
private final ExtensionClient client;
|
||||
|
||||
public PluginControllerManager(ExtensionClient client) {
|
||||
this.client = client;
|
||||
controllerManagerMap = new ConcurrentHashMap<>();
|
||||
controllers = new ConcurrentHashMap<>();
|
||||
}
|
||||
|
||||
@EventListener
|
||||
public void onPluginStarted(HaloPluginStartedEvent event) {
|
||||
var plugin = event.getPlugin();
|
||||
|
||||
var controllerManager = controllerManagerMap.computeIfAbsent(plugin.getPluginId(),
|
||||
id -> new DefaultControllerManager(client));
|
||||
|
||||
getReconcilers(plugin.getPluginId())
|
||||
.forEach(controllerManager::start);
|
||||
public void onApplicationEvent(ContextRefreshedEvent event) {
|
||||
event.getApplicationContext()
|
||||
.<Reconciler<Reconciler.Request>>getBeanProvider(
|
||||
forClassWithGenerics(Reconciler.class, Reconciler.Request.class))
|
||||
.orderedStream()
|
||||
.forEach(this::start);
|
||||
}
|
||||
|
||||
@EventListener
|
||||
public void onPluginBeforeStop(HaloPluginBeforeStopEvent event) {
|
||||
// remove controller manager
|
||||
var plugin = event.getPlugin();
|
||||
var controllerManager = controllerManagerMap.remove(plugin.getPluginId());
|
||||
if (controllerManager != null) {
|
||||
// stop all reconcilers
|
||||
getReconcilers(plugin.getPluginId())
|
||||
.forEach(controllerManager::stop);
|
||||
}
|
||||
public void onApplicationEvent(ContextClosedEvent event) throws Exception {
|
||||
controllers.values()
|
||||
.forEach(Disposable::dispose);
|
||||
controllers.clear();
|
||||
}
|
||||
|
||||
private Stream<Reconciler<Request>> getReconcilers(String pluginId) {
|
||||
var context = ExtensionContextRegistry.getInstance().getByPluginId(pluginId);
|
||||
return context.<Reconciler<Request>>getBeanProvider(
|
||||
ResolvableType.forClassWithGenerics(Reconciler.class, Request.class))
|
||||
.orderedStream();
|
||||
private void start(Reconciler<Reconciler.Request> reconciler) {
|
||||
var builder = new ControllerBuilder(reconciler, client);
|
||||
var controller = reconciler.setupWith(builder);
|
||||
controllers.put(reconciler.getClass().getName(), controller);
|
||||
controller.start();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,107 +0,0 @@
|
|||
package run.halo.app.plugin;
|
||||
|
||||
import io.micrometer.common.util.StringUtils;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.SmartLifecycle;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.stereotype.Component;
|
||||
import reactor.core.Exceptions;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.core.extension.Plugin;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.extension.controller.Controller;
|
||||
import run.halo.app.extension.controller.ControllerBuilder;
|
||||
import run.halo.app.extension.controller.DefaultController;
|
||||
import run.halo.app.extension.controller.DefaultQueue;
|
||||
import run.halo.app.extension.controller.Reconciler;
|
||||
import run.halo.app.extension.controller.RequestQueue;
|
||||
import run.halo.app.plugin.event.PluginCreatedEvent;
|
||||
|
||||
/**
|
||||
* Plugin event reconciler.
|
||||
* If other plugin events need to be reconciled, consider sharing this reconciler.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.2.0
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class PluginCreatedEventReconciler
|
||||
implements Reconciler<PluginCreatedEvent>, SmartLifecycle {
|
||||
|
||||
private final RequestQueue<PluginCreatedEvent> pluginEventQueue;
|
||||
|
||||
private final ReactiveExtensionClient client;
|
||||
|
||||
private final Controller pluginEventController;
|
||||
|
||||
private boolean running = false;
|
||||
|
||||
public PluginCreatedEventReconciler(ReactiveExtensionClient client) {
|
||||
this.client = client;
|
||||
pluginEventQueue = new DefaultQueue<>(Instant::now);
|
||||
pluginEventController = this.setupWith(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result reconcile(PluginCreatedEvent pluginCreatedEvent) {
|
||||
String pluginName = pluginCreatedEvent.getPluginName();
|
||||
try {
|
||||
ensureConfigMapNameNotEmptyIfSettingIsNotBlank(pluginName);
|
||||
} catch (InterruptedException e) {
|
||||
throw Exceptions.propagate(e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Controller setupWith(ControllerBuilder builder) {
|
||||
return new DefaultController<>(
|
||||
this.getClass().getName(),
|
||||
this,
|
||||
pluginEventQueue,
|
||||
null,
|
||||
Duration.ofMillis(100),
|
||||
Duration.ofSeconds(1000)
|
||||
);
|
||||
}
|
||||
|
||||
@EventListener(PluginCreatedEvent.class)
|
||||
public void handlePluginCreated(PluginCreatedEvent pluginCreatedEvent) {
|
||||
pluginEventQueue.addImmediately(pluginCreatedEvent);
|
||||
}
|
||||
|
||||
void ensureConfigMapNameNotEmptyIfSettingIsNotBlank(String pluginName)
|
||||
throws InterruptedException {
|
||||
client.fetch(Plugin.class, pluginName)
|
||||
.switchIfEmpty(Mono.error(new IllegalStateException("Plugin not found: " + pluginName)))
|
||||
.filter(plugin -> StringUtils.isNotBlank(plugin.getSpec().getSettingName()))
|
||||
.filter(plugin -> StringUtils.isBlank(plugin.getSpec().getConfigMapName()))
|
||||
.doOnNext(plugin -> {
|
||||
// has settingName value but configMapName not configured
|
||||
plugin.getSpec().setConfigMapName(UUID.randomUUID().toString());
|
||||
})
|
||||
.flatMap(client::update)
|
||||
.block();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
pluginEventController.start();
|
||||
running = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
running = false;
|
||||
pluginEventController.dispose();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRunning() {
|
||||
return running;
|
||||
}
|
||||
}
|
|
@ -5,6 +5,7 @@ import static run.halo.app.extension.MetadataUtil.nullSafeAnnotations;
|
|||
import java.nio.file.Path;
|
||||
import java.time.Duration;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.pf4j.PluginManager;
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.dao.OptimisticLockingFailureException;
|
||||
|
@ -23,22 +24,22 @@ import run.halo.app.extension.ReactiveExtensionClient;
|
|||
@Component
|
||||
public class PluginDevelopmentInitializer implements ApplicationListener<ApplicationReadyEvent> {
|
||||
|
||||
private final HaloPluginManager haloPluginManager;
|
||||
private final PluginManager pluginManager;
|
||||
|
||||
private final PluginProperties pluginProperties;
|
||||
|
||||
private final ReactiveExtensionClient extensionClient;
|
||||
|
||||
public PluginDevelopmentInitializer(HaloPluginManager haloPluginManager,
|
||||
public PluginDevelopmentInitializer(PluginManager pluginManager,
|
||||
PluginProperties pluginProperties, ReactiveExtensionClient extensionClient) {
|
||||
this.haloPluginManager = haloPluginManager;
|
||||
this.pluginManager = pluginManager;
|
||||
this.pluginProperties = pluginProperties;
|
||||
this.extensionClient = extensionClient;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplicationEvent(@NonNull ApplicationReadyEvent event) {
|
||||
if (!haloPluginManager.isDevelopment()) {
|
||||
public void onApplicationEvent(@NonNull ApplicationReadyEvent ignored) {
|
||||
if (!pluginManager.isDevelopment()) {
|
||||
return;
|
||||
}
|
||||
createFixedPluginIfNecessary();
|
||||
|
|
|
@ -4,7 +4,6 @@ import java.nio.file.Path;
|
|||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import lombok.Data;
|
||||
import org.pf4j.PluginLoader;
|
||||
import org.pf4j.RuntimeMode;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
|
@ -67,8 +66,4 @@ public class PluginProperties {
|
|||
*/
|
||||
private String pluginsRoot;
|
||||
|
||||
/**
|
||||
* Allows providing custom plugin loaders.
|
||||
*/
|
||||
private Class<PluginLoader> customPluginLoader;
|
||||
}
|
||||
|
|
|
@ -1,42 +0,0 @@
|
|||
package run.halo.app.plugin;
|
||||
|
||||
import java.util.Collection;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.pf4j.PluginWrapper;
|
||||
import org.springframework.context.support.GenericApplicationContext;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerMapping;
|
||||
|
||||
/**
|
||||
* Plugin mapping manager.
|
||||
*
|
||||
* @author guqing
|
||||
* @see RequestMappingHandlerMapping
|
||||
*/
|
||||
@Slf4j
|
||||
public class PluginRequestMappingManager {
|
||||
|
||||
private final PluginRequestMappingHandlerMapping requestMappingHandlerMapping;
|
||||
|
||||
public PluginRequestMappingManager(
|
||||
PluginRequestMappingHandlerMapping pluginRequestMappingHandlerMapping) {
|
||||
this.requestMappingHandlerMapping = pluginRequestMappingHandlerMapping;
|
||||
}
|
||||
|
||||
public void registerHandlerMappings(PluginWrapper pluginWrapper) {
|
||||
String pluginId = pluginWrapper.getPluginId();
|
||||
getControllerBeans(pluginId)
|
||||
.forEach(handler ->
|
||||
requestMappingHandlerMapping.registerHandlerMethods(pluginId, handler));
|
||||
}
|
||||
|
||||
public void removeHandlerMappings(String pluginId) {
|
||||
requestMappingHandlerMapping.unregister(pluginId);
|
||||
}
|
||||
|
||||
private Collection<Object> getControllerBeans(String pluginId) {
|
||||
GenericApplicationContext pluginContext =
|
||||
ExtensionContextRegistry.getInstance().getByPluginId(pluginId);
|
||||
return pluginContext.getBeansWithAnnotation(Controller.class).values();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
package run.halo.app.plugin;
|
||||
|
||||
import java.util.Collection;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
|
||||
public interface PluginRouterFunctionRegistry {
|
||||
void register(Collection<RouterFunction<ServerResponse>> routerFunctions);
|
||||
|
||||
void unregister(Collection<RouterFunction<ServerResponse>> routerFunctions);
|
||||
|
||||
}
|
|
@ -45,9 +45,14 @@ public class PluginStartedListener {
|
|||
@EventListener
|
||||
public Mono<Void> onApplicationEvent(HaloPluginStartedEvent event) {
|
||||
var pluginWrapper = event.getPlugin();
|
||||
var pluginApplicationContext = ExtensionContextRegistry.getInstance()
|
||||
.getByPluginId(pluginWrapper.getPluginId());
|
||||
|
||||
var p = pluginWrapper.getPlugin();
|
||||
if (!(p instanceof SpringPlugin springPlugin)) {
|
||||
return Mono.empty();
|
||||
}
|
||||
var applicationContext = springPlugin.getApplicationContext();
|
||||
if (!(applicationContext instanceof PluginApplicationContext pluginApplicationContext)) {
|
||||
return Mono.empty();
|
||||
}
|
||||
var pluginName = pluginWrapper.getPluginId();
|
||||
|
||||
return client.get(Plugin.class, pluginName)
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
package run.halo.app.plugin;
|
||||
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.support.GenericApplicationContext;
|
||||
import org.springframework.security.web.server.context.ServerSecurityContextRepository;
|
||||
import run.halo.app.core.extension.service.AttachmentService;
|
||||
import run.halo.app.extension.DefaultSchemeManager;
|
||||
import run.halo.app.extension.ExtensionClient;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.infra.BackupRootGetter;
|
||||
import run.halo.app.infra.ExternalLinkProcessor;
|
||||
import run.halo.app.infra.ExternalUrlSupplier;
|
||||
import run.halo.app.notification.NotificationCenter;
|
||||
import run.halo.app.notification.NotificationReasonEmitter;
|
||||
|
||||
/**
|
||||
* Utility for creating shared application context.
|
||||
*
|
||||
* @author guqing
|
||||
* @author johnniang
|
||||
* @since 2.12.0
|
||||
*/
|
||||
public enum SharedApplicationContextFactory {
|
||||
;
|
||||
|
||||
public static ApplicationContext create(ApplicationContext rootContext) {
|
||||
// TODO Optimize creation timing
|
||||
var sharedContext = new GenericApplicationContext();
|
||||
sharedContext.registerShutdownHook();
|
||||
|
||||
var beanFactory = sharedContext.getBeanFactory();
|
||||
|
||||
// register shared object here
|
||||
var extensionClient = rootContext.getBean(ExtensionClient.class);
|
||||
var reactiveExtensionClient = rootContext.getBean(ReactiveExtensionClient.class);
|
||||
beanFactory.registerSingleton("extensionClient", extensionClient);
|
||||
beanFactory.registerSingleton("reactiveExtensionClient", reactiveExtensionClient);
|
||||
|
||||
DefaultSchemeManager defaultSchemeManager =
|
||||
rootContext.getBean(DefaultSchemeManager.class);
|
||||
beanFactory.registerSingleton("schemeManager", defaultSchemeManager);
|
||||
beanFactory.registerSingleton("externalUrlSupplier",
|
||||
rootContext.getBean(ExternalUrlSupplier.class));
|
||||
beanFactory.registerSingleton("serverSecurityContextRepository",
|
||||
rootContext.getBean(ServerSecurityContextRepository.class));
|
||||
beanFactory.registerSingleton("attachmentService",
|
||||
rootContext.getBean(AttachmentService.class));
|
||||
beanFactory.registerSingleton("backupRootGetter",
|
||||
rootContext.getBean(BackupRootGetter.class));
|
||||
beanFactory.registerSingleton("notificationReasonEmitter",
|
||||
rootContext.getBean(NotificationReasonEmitter.class));
|
||||
beanFactory.registerSingleton("notificationCenter",
|
||||
rootContext.getBean(NotificationCenter.class));
|
||||
beanFactory.registerSingleton("externalLinkProcessor",
|
||||
rootContext.getBean(ExternalLinkProcessor.class));
|
||||
// TODO add more shared instance here
|
||||
|
||||
sharedContext.refresh();
|
||||
return sharedContext;
|
||||
}
|
||||
}
|
|
@ -1,89 +0,0 @@
|
|||
package run.halo.app.plugin;
|
||||
|
||||
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.security.web.server.context.ServerSecurityContextRepository;
|
||||
import org.springframework.stereotype.Component;
|
||||
import run.halo.app.core.extension.service.AttachmentService;
|
||||
import run.halo.app.extension.DefaultSchemeManager;
|
||||
import run.halo.app.extension.ExtensionClient;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.infra.BackupRootGetter;
|
||||
import run.halo.app.infra.ExternalLinkProcessor;
|
||||
import run.halo.app.infra.ExternalUrlSupplier;
|
||||
import run.halo.app.notification.NotificationCenter;
|
||||
import run.halo.app.notification.NotificationReasonEmitter;
|
||||
|
||||
/**
|
||||
* <p>This {@link SharedApplicationContextHolder} class is used to hold a singleton instance of
|
||||
* {@link SharedApplicationContext}.</p>
|
||||
* <p>If sharedApplicationContext cache is null when calling the {@link #getInstance()} method,
|
||||
* then it will call {@link #createSharedApplicationContext()} to create and cache it. Otherwise,
|
||||
* it will be obtained directly.</p>
|
||||
* <p>It is thread safe.</p>
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Component
|
||||
public class SharedApplicationContextHolder {
|
||||
|
||||
private final ApplicationContext rootApplicationContext;
|
||||
private volatile SharedApplicationContext sharedApplicationContext;
|
||||
|
||||
public SharedApplicationContextHolder(ApplicationContext applicationContext) {
|
||||
this.rootApplicationContext = applicationContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get singleton instance of {@link SharedApplicationContext}.
|
||||
*
|
||||
* @return a singleton instance of {@link SharedApplicationContext}.
|
||||
*/
|
||||
public SharedApplicationContext getInstance() {
|
||||
if (this.sharedApplicationContext == null) {
|
||||
synchronized (SharedApplicationContextHolder.class) {
|
||||
if (this.sharedApplicationContext == null) {
|
||||
this.sharedApplicationContext = createSharedApplicationContext();
|
||||
}
|
||||
}
|
||||
}
|
||||
return this.sharedApplicationContext;
|
||||
}
|
||||
|
||||
SharedApplicationContext createSharedApplicationContext() {
|
||||
// TODO Optimize creation timing
|
||||
SharedApplicationContext sharedApplicationContext = new SharedApplicationContext();
|
||||
sharedApplicationContext.refresh();
|
||||
|
||||
DefaultListableBeanFactory beanFactory =
|
||||
(DefaultListableBeanFactory) sharedApplicationContext.getBeanFactory();
|
||||
|
||||
// register shared object here
|
||||
var extensionClient = rootApplicationContext.getBean(ExtensionClient.class);
|
||||
var reactiveExtensionClient = rootApplicationContext.getBean(ReactiveExtensionClient.class);
|
||||
beanFactory.registerSingleton("extensionClient", extensionClient);
|
||||
beanFactory.registerSingleton("reactiveExtensionClient", reactiveExtensionClient);
|
||||
|
||||
DefaultSchemeManager defaultSchemeManager =
|
||||
rootApplicationContext.getBean(DefaultSchemeManager.class);
|
||||
beanFactory.registerSingleton("schemeManager", defaultSchemeManager);
|
||||
beanFactory.registerSingleton("externalUrlSupplier",
|
||||
rootApplicationContext.getBean(ExternalUrlSupplier.class));
|
||||
beanFactory.registerSingleton("serverSecurityContextRepository",
|
||||
rootApplicationContext.getBean(ServerSecurityContextRepository.class));
|
||||
beanFactory.registerSingleton("attachmentService",
|
||||
rootApplicationContext.getBean(AttachmentService.class));
|
||||
beanFactory.registerSingleton("backupRootGetter",
|
||||
rootApplicationContext.getBean(BackupRootGetter.class));
|
||||
beanFactory.registerSingleton("notificationReasonEmitter",
|
||||
rootApplicationContext.getBean(NotificationReasonEmitter.class));
|
||||
beanFactory.registerSingleton("notificationCenter",
|
||||
rootApplicationContext.getBean(NotificationCenter.class));
|
||||
beanFactory.registerSingleton("externalLinkProcessor",
|
||||
rootApplicationContext.getBean(ExternalLinkProcessor.class));
|
||||
// TODO add more shared instance here
|
||||
|
||||
return sharedApplicationContext;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
package run.halo.app.plugin;
|
||||
|
||||
import org.springframework.context.ApplicationEvent;
|
||||
import org.springframework.context.ApplicationListener;
|
||||
|
||||
public interface SharedEventListenerRegistry {
|
||||
|
||||
void register(ApplicationListener<ApplicationEvent> listener);
|
||||
|
||||
void unregister(ApplicationListener<ApplicationEvent> listener);
|
||||
|
||||
}
|
|
@ -6,7 +6,6 @@ import java.io.InputStreamReader;
|
|||
import java.io.Reader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
|
@ -15,10 +14,7 @@ import java.util.Set;
|
|||
import java.util.concurrent.locks.StampedLock;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.pf4j.AbstractExtensionFinder;
|
||||
import org.pf4j.PluginDependency;
|
||||
import org.pf4j.PluginManager;
|
||||
import org.pf4j.PluginState;
|
||||
import org.pf4j.PluginStateEvent;
|
||||
import org.pf4j.PluginWrapper;
|
||||
import org.pf4j.processor.ExtensionStorage;
|
||||
import org.springframework.util.Assert;
|
||||
|
@ -49,67 +45,47 @@ public class SpringComponentsFinder extends AbstractExtensionFinder {
|
|||
|
||||
@Override
|
||||
public Map<String, Set<String>> readPluginsStorages() {
|
||||
// We have to copy the source code from `org.pf4j.LegacyExtensionFinder.readPluginsStorages`
|
||||
// because we have to adapt to the new extensions resource location
|
||||
// `META-INF/plugin-components.idx`.
|
||||
log.debug("Reading components storages from plugins");
|
||||
Map<String, Set<String>> result = new LinkedHashMap<>();
|
||||
|
||||
List<PluginWrapper> plugins = pluginManager.getPlugins();
|
||||
for (PluginWrapper plugin : plugins) {
|
||||
readPluginStorageToMemory(plugin);
|
||||
String pluginId = plugin.getDescriptor().getPluginId();
|
||||
log.debug("Reading extensions storage from plugin '{}'", pluginId);
|
||||
Set<String> bucket = new HashSet<>();
|
||||
|
||||
try {
|
||||
log.debug("Read '{}'", EXTENSIONS_RESOURCE);
|
||||
ClassLoader pluginClassLoader = plugin.getPluginClassLoader();
|
||||
try (var resourceStream =
|
||||
pluginClassLoader.getResourceAsStream(EXTENSIONS_RESOURCE)) {
|
||||
if (resourceStream == null) {
|
||||
log.debug("Cannot find '{}'", EXTENSIONS_RESOURCE);
|
||||
} else {
|
||||
collectExtensions(resourceStream, bucket);
|
||||
}
|
||||
}
|
||||
|
||||
debugExtensions(bucket);
|
||||
|
||||
result.put(pluginId, bucket);
|
||||
} catch (IOException e) {
|
||||
log.error("Failed to read components from " + EXTENSIONS_RESOURCE, e);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void pluginStateChanged(PluginStateEvent event) {
|
||||
// see supper class for more details
|
||||
if (checkForExtensionDependencies == null && PluginState.STARTED.equals(
|
||||
event.getPluginState())) {
|
||||
for (PluginDependency dependency : event.getPlugin().getDescriptor()
|
||||
.getDependencies()) {
|
||||
if (dependency.isOptional()) {
|
||||
log.debug("Enable check for extension dependencies via ASM.");
|
||||
checkForExtensionDependencies = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void collectExtensions(InputStream inputStream, Set<String> bucket) throws IOException {
|
||||
try (Reader reader = new InputStreamReader(inputStream, StandardCharsets.UTF_8)) {
|
||||
ExtensionStorage.read(reader, bucket);
|
||||
}
|
||||
}
|
||||
|
||||
protected void readPluginStorageToMemory(PluginWrapper pluginWrapper) {
|
||||
String pluginId = pluginWrapper.getPluginId();
|
||||
if (containsComponentsStorage(pluginId)) {
|
||||
return;
|
||||
}
|
||||
log.debug("Reading components storage from plugin '{}'", pluginId);
|
||||
Set<String> bucket = new HashSet<>();
|
||||
|
||||
try {
|
||||
log.debug("Read '{}'", EXTENSIONS_RESOURCE);
|
||||
ClassLoader pluginClassLoader = pluginWrapper.getPluginClassLoader();
|
||||
try (InputStream resourceStream = pluginClassLoader.getResourceAsStream(
|
||||
EXTENSIONS_RESOURCE)) {
|
||||
if (resourceStream == null) {
|
||||
log.debug("Cannot find '{}'", EXTENSIONS_RESOURCE);
|
||||
} else {
|
||||
collectExtensions(resourceStream, bucket);
|
||||
}
|
||||
}
|
||||
|
||||
debugExtensions(bucket);
|
||||
|
||||
putComponentsStorage(pluginId, bucket);
|
||||
} catch (IOException e) {
|
||||
log.error(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
protected boolean containsComponentsStorage(String pluginId) {
|
||||
Assert.notNull(pluginId, "The pluginId cannot be null");
|
||||
long stamp = entryStampedLock.tryOptimisticRead();
|
||||
|
@ -125,42 +101,5 @@ public class SpringComponentsFinder extends AbstractExtensionFinder {
|
|||
return contains;
|
||||
}
|
||||
|
||||
protected void putComponentsStorage(String pluginId, Set<String> components) {
|
||||
Assert.notNull(pluginId, "The pluginId cannot be null");
|
||||
// When the lock remains in write mode, the read lock cannot be obtained
|
||||
long stamp = entryStampedLock.writeLock();
|
||||
try {
|
||||
Map<String, Set<String>> componentNamesMap;
|
||||
if (super.entries == null) {
|
||||
componentNamesMap = new HashMap<>();
|
||||
} else {
|
||||
componentNamesMap = new HashMap<>(super.entries);
|
||||
}
|
||||
log.debug("Load [{}] component names into storage cache for plugin [{}].",
|
||||
components.size(), pluginId);
|
||||
componentNamesMap.put(pluginId, components);
|
||||
super.entries = componentNamesMap;
|
||||
} finally {
|
||||
entryStampedLock.unlockWrite(stamp);
|
||||
}
|
||||
}
|
||||
|
||||
protected void removeComponentsStorage(String pluginId) {
|
||||
Assert.notNull(pluginId, "The pluginId cannot be null");
|
||||
long stamp = entryStampedLock.writeLock();
|
||||
try {
|
||||
Map<String, Set<String>> componentNamesMap;
|
||||
if (super.entries == null) {
|
||||
componentNamesMap = new HashMap<>();
|
||||
} else {
|
||||
componentNamesMap = new HashMap<>(super.entries);
|
||||
}
|
||||
log.debug("Removing components storage from cache [{}].", pluginId);
|
||||
componentNamesMap.remove(pluginId);
|
||||
super.entries = componentNamesMap;
|
||||
} finally {
|
||||
entryStampedLock.unlockWrite(stamp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -3,7 +3,6 @@ package run.halo.app.plugin;
|
|||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.util.Comparator;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Stream;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
@ -11,7 +10,6 @@ import lombok.extern.slf4j.Slf4j;
|
|||
import org.pf4j.Extension;
|
||||
import org.pf4j.ExtensionFactory;
|
||||
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;
|
||||
|
@ -65,16 +63,9 @@ public class SpringExtensionFactory implements ExtensionFactory {
|
|||
@Override
|
||||
@Nullable
|
||||
public <T> T create(Class<T> extensionClass) {
|
||||
Optional<PluginApplicationContext> contextOptional =
|
||||
getPluginApplicationContextBy(extensionClass);
|
||||
if (contextOptional.isPresent()) {
|
||||
// When the plugin starts, the class has been loaded into the plugin application
|
||||
// context,
|
||||
// so you only need to get it directly
|
||||
PluginApplicationContext pluginApplicationContext = contextOptional.get();
|
||||
return pluginApplicationContext.getBean(extensionClass);
|
||||
}
|
||||
return createWithoutSpring(extensionClass);
|
||||
return getPluginApplicationContextBy(extensionClass)
|
||||
.map(context -> context.getBean(extensionClass))
|
||||
.orElseGet(() -> createWithoutSpring(extensionClass));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -127,47 +118,13 @@ public class SpringExtensionFactory implements ExtensionFactory {
|
|||
return new Object[constructor.getParameterCount()];
|
||||
}
|
||||
|
||||
protected <T> Optional<PluginApplicationContext> getPluginApplicationContextBy(
|
||||
protected <T> Optional<ApplicationContext> getPluginApplicationContextBy(
|
||||
final Class<T> extensionClass) {
|
||||
return Optional.ofNullable(this.pluginManager.whichPlugin(extensionClass))
|
||||
.map(PluginWrapper::getPlugin)
|
||||
.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) {
|
||||
if (log.isTraceEnabled()) {
|
||||
log.trace(" 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);
|
||||
}
|
||||
if (log.isTraceEnabled()) {
|
||||
log.trace(
|
||||
" 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 BasePlugin plugin) {
|
||||
return Objects.nonNull(plugin)
|
||||
? plugin.getContext().getName()
|
||||
: "system";
|
||||
.filter(SpringPlugin.class::isInstance)
|
||||
.map(plugin -> (SpringPlugin) plugin)
|
||||
.map(SpringPlugin::getApplicationContext);
|
||||
}
|
||||
|
||||
private <T> String nameOf(final Class<T> clazz) {
|
||||
|
|
|
@ -0,0 +1,75 @@
|
|||
package run.halo.app.plugin;
|
||||
|
||||
import org.pf4j.Plugin;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import run.halo.app.plugin.event.SpringPluginStartedEvent;
|
||||
import run.halo.app.plugin.event.SpringPluginStartingEvent;
|
||||
import run.halo.app.plugin.event.SpringPluginStoppingEvent;
|
||||
|
||||
public class SpringPlugin extends Plugin {
|
||||
|
||||
private ApplicationContext context;
|
||||
|
||||
private Plugin delegate;
|
||||
|
||||
private final PluginApplicationContextFactory contextFactory;
|
||||
|
||||
private final PluginContext pluginContext;
|
||||
|
||||
public SpringPlugin(PluginApplicationContextFactory contextFactory,
|
||||
PluginContext pluginContext) {
|
||||
this.contextFactory = contextFactory;
|
||||
this.pluginContext = pluginContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
// initialize context
|
||||
var pluginId = pluginContext.getName();
|
||||
this.context = contextFactory.create(pluginId);
|
||||
|
||||
var pluginOpt = context.getBeanProvider(Plugin.class)
|
||||
.stream()
|
||||
.findFirst();
|
||||
context.publishEvent(new SpringPluginStartingEvent(this, this));
|
||||
if (pluginOpt.isPresent()) {
|
||||
this.delegate = pluginOpt.get();
|
||||
if (this.delegate instanceof BasePlugin basePlugin) {
|
||||
basePlugin.setContext(pluginContext);
|
||||
}
|
||||
this.delegate.start();
|
||||
}
|
||||
context.publishEvent(new SpringPluginStartedEvent(this, this));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
if (context != null) {
|
||||
context.publishEvent(new SpringPluginStoppingEvent(this, this));
|
||||
}
|
||||
if (this.delegate != null) {
|
||||
this.delegate.stop();
|
||||
}
|
||||
if (context instanceof ConfigurableApplicationContext configurableContext) {
|
||||
configurableContext.close();
|
||||
}
|
||||
// reset application context
|
||||
context = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete() {
|
||||
if (delegate != null) {
|
||||
delegate.delete();
|
||||
}
|
||||
}
|
||||
|
||||
public ApplicationContext getApplicationContext() {
|
||||
return context;
|
||||
}
|
||||
|
||||
public PluginContext getPluginContext() {
|
||||
return pluginContext;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
package run.halo.app.plugin;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.pf4j.Plugin;
|
||||
import org.pf4j.PluginFactory;
|
||||
import org.pf4j.PluginWrapper;
|
||||
|
||||
/**
|
||||
* The default implementation for PluginFactory.
|
||||
* <p>Get a {@link BasePlugin} instance from the {@link PluginApplicationContext}.</p>
|
||||
*
|
||||
* @author guqing
|
||||
* @author johnniang
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Slf4j
|
||||
public class SpringPluginFactory implements PluginFactory {
|
||||
|
||||
|
||||
private final PluginApplicationContextFactory contextFactory;
|
||||
|
||||
public SpringPluginFactory(PluginApplicationContextFactory contextFactory) {
|
||||
this.contextFactory = contextFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Plugin create(PluginWrapper pluginWrapper) {
|
||||
var pluginContext = new PluginContext(
|
||||
pluginWrapper.getPluginId(),
|
||||
pluginWrapper.getDescriptor().getVersion(),
|
||||
pluginWrapper.getRuntimeMode()
|
||||
);
|
||||
return new SpringPlugin(
|
||||
contextFactory,
|
||||
pluginContext
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
package run.halo.app.plugin;
|
||||
|
||||
import org.pf4j.PluginManager;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
|
||||
public interface SpringPluginManager extends PluginManager {
|
||||
|
||||
ApplicationContext getRootContext();
|
||||
|
||||
ApplicationContext getSharedContext();
|
||||
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
package run.halo.app.plugin.event;
|
||||
|
||||
import lombok.Getter;
|
||||
import org.springframework.context.ApplicationEvent;
|
||||
import run.halo.app.core.extension.Plugin;
|
||||
|
||||
/**
|
||||
* The {@link Plugin} created event.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Getter
|
||||
public class PluginCreatedEvent extends ApplicationEvent {
|
||||
private final String pluginName;
|
||||
|
||||
public PluginCreatedEvent(Object source, String pluginName) {
|
||||
super(source);
|
||||
this.pluginName = pluginName;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package run.halo.app.plugin.event;
|
||||
|
||||
import org.springframework.context.ApplicationEvent;
|
||||
import run.halo.app.plugin.SpringPlugin;
|
||||
|
||||
public class SpringPluginStartedEvent extends ApplicationEvent {
|
||||
|
||||
private final SpringPlugin springPlugin;
|
||||
|
||||
public SpringPluginStartedEvent(Object source, SpringPlugin springPlugin) {
|
||||
super(source);
|
||||
this.springPlugin = springPlugin;
|
||||
}
|
||||
|
||||
public SpringPlugin getSpringPlugin() {
|
||||
return springPlugin;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package run.halo.app.plugin.event;
|
||||
|
||||
import org.springframework.context.ApplicationEvent;
|
||||
import run.halo.app.plugin.SpringPlugin;
|
||||
|
||||
public class SpringPluginStartingEvent extends ApplicationEvent {
|
||||
|
||||
private final SpringPlugin springPlugin;
|
||||
|
||||
public SpringPluginStartingEvent(Object source, SpringPlugin springPlugin) {
|
||||
super(source);
|
||||
this.springPlugin = springPlugin;
|
||||
}
|
||||
|
||||
public SpringPlugin getSpringPlugin() {
|
||||
return springPlugin;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package run.halo.app.plugin.event;
|
||||
|
||||
import org.springframework.context.ApplicationEvent;
|
||||
import run.halo.app.plugin.SpringPlugin;
|
||||
|
||||
public class SpringPluginStoppedEvent extends ApplicationEvent {
|
||||
|
||||
private final SpringPlugin springPlugin;
|
||||
|
||||
public SpringPluginStoppedEvent(Object source, SpringPlugin springPlugin) {
|
||||
super(source);
|
||||
this.springPlugin = springPlugin;
|
||||
}
|
||||
|
||||
public SpringPlugin getSpringPlugin() {
|
||||
return springPlugin;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package run.halo.app.plugin.event;
|
||||
|
||||
import org.springframework.context.ApplicationEvent;
|
||||
import run.halo.app.plugin.SpringPlugin;
|
||||
|
||||
public class SpringPluginStoppingEvent extends ApplicationEvent {
|
||||
|
||||
private final SpringPlugin springPlugin;
|
||||
|
||||
public SpringPluginStoppingEvent(Object source, SpringPlugin springPlugin) {
|
||||
super(source);
|
||||
this.springPlugin = springPlugin;
|
||||
}
|
||||
|
||||
public SpringPlugin getSpringPlugin() {
|
||||
return springPlugin;
|
||||
}
|
||||
}
|
|
@ -6,6 +6,7 @@ import java.util.Set;
|
|||
import java.util.stream.Stream;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.pf4j.ExtensionPoint;
|
||||
import org.pf4j.PluginManager;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
|
||||
import org.springframework.lang.NonNull;
|
||||
|
@ -15,7 +16,6 @@ import reactor.core.publisher.Mono;
|
|||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
|
||||
import run.halo.app.infra.SystemSetting.ExtensionPointEnabled;
|
||||
import run.halo.app.plugin.HaloPluginManager;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
|
@ -23,7 +23,7 @@ public class DefaultExtensionGetter implements ExtensionGetter {
|
|||
|
||||
private final SystemConfigurableEnvironmentFetcher systemConfigFetcher;
|
||||
|
||||
private final HaloPluginManager pluginManager;
|
||||
private final PluginManager pluginManager;
|
||||
|
||||
private final ApplicationContext applicationContext;
|
||||
|
||||
|
|
|
@ -4,15 +4,18 @@ import static org.springframework.http.MediaType.ALL;
|
|||
import static org.springframework.web.reactive.function.server.RequestPredicates.GET;
|
||||
import static org.springframework.web.reactive.function.server.RequestPredicates.accept;
|
||||
|
||||
import java.util.List;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.pf4j.PluginManager;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.core.io.DefaultResourceLoader;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.http.server.PathContainer;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
|
@ -20,15 +23,11 @@ import org.springframework.web.reactive.function.server.RouterFunctions;
|
|||
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import org.springframework.web.util.pattern.PathPatternParser;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.core.extension.ReverseProxy;
|
||||
import run.halo.app.core.extension.ReverseProxy.FileReverseProxyProvider;
|
||||
import run.halo.app.core.extension.ReverseProxy.ReverseProxyRule;
|
||||
import run.halo.app.infra.exception.NotFoundException;
|
||||
import run.halo.app.infra.utils.PathUtils;
|
||||
import run.halo.app.plugin.ExtensionContextRegistry;
|
||||
import run.halo.app.plugin.HaloPluginManager;
|
||||
import run.halo.app.plugin.PluginConst;
|
||||
|
||||
/**
|
||||
|
@ -44,7 +43,7 @@ import run.halo.app.plugin.PluginConst;
|
|||
@AllArgsConstructor
|
||||
public class ReverseProxyRouterFunctionFactory {
|
||||
|
||||
private final HaloPluginManager haloPluginManager;
|
||||
private final PluginManager pluginManager;
|
||||
private final ApplicationContext applicationContext;
|
||||
|
||||
/**
|
||||
|
@ -56,18 +55,17 @@ public class ReverseProxyRouterFunctionFactory {
|
|||
* @param pluginName plugin name(nullable if system)
|
||||
* @return A reverse proxy RouterFunction handle(nullable)
|
||||
*/
|
||||
@NonNull
|
||||
public Mono<RouterFunction<ServerResponse>> create(ReverseProxy reverseProxy,
|
||||
String pluginName) {
|
||||
@Nullable
|
||||
public RouterFunction<ServerResponse> create(ReverseProxy reverseProxy, String pluginName) {
|
||||
return createReverseProxyRouterFunction(reverseProxy, nullSafePluginName(pluginName));
|
||||
}
|
||||
|
||||
private Mono<RouterFunction<ServerResponse>> createReverseProxyRouterFunction(
|
||||
@Nullable
|
||||
private RouterFunction<ServerResponse> createReverseProxyRouterFunction(
|
||||
ReverseProxy reverseProxy, @NonNull String pluginName) {
|
||||
Assert.notNull(reverseProxy, "The reverseProxy must not be null.");
|
||||
var rules = getReverseProxyRules(reverseProxy);
|
||||
|
||||
return rules.map(rule -> {
|
||||
return rules.stream().map(rule -> {
|
||||
String routePath = buildRoutePath(pluginName, rule);
|
||||
log.debug("Plugin [{}] registered reverse proxy route path [{}]", pluginName,
|
||||
routePath);
|
||||
|
@ -81,15 +79,15 @@ public class ReverseProxyRouterFunctionFactory {
|
|||
return ServerResponse.ok()
|
||||
.bodyValue(resource);
|
||||
});
|
||||
}).reduce(RouterFunction::and);
|
||||
}).reduce(RouterFunction::and).orElse(null);
|
||||
}
|
||||
|
||||
private String nullSafePluginName(String pluginName) {
|
||||
return pluginName == null ? PluginConst.SYSTEM_PLUGIN_NAME : pluginName;
|
||||
}
|
||||
|
||||
private Flux<ReverseProxyRule> getReverseProxyRules(ReverseProxy reverseProxy) {
|
||||
return Flux.fromIterable(reverseProxy.getRules());
|
||||
private List<ReverseProxyRule> getReverseProxyRules(ReverseProxy reverseProxy) {
|
||||
return reverseProxy.getRules();
|
||||
}
|
||||
|
||||
public static String buildRoutePath(String pluginId, ReverseProxyRule reverseProxyRule) {
|
||||
|
@ -137,15 +135,11 @@ public class ReverseProxyRouterFunctionFactory {
|
|||
}
|
||||
|
||||
private ResourceLoader getResourceLoader(String pluginName) {
|
||||
ExtensionContextRegistry registry = ExtensionContextRegistry.getInstance();
|
||||
if (registry.containsContext(pluginName)) {
|
||||
return registry.getByPluginId(pluginName);
|
||||
}
|
||||
if (PluginConst.SYSTEM_PLUGIN_NAME.equals(pluginName)) {
|
||||
return applicationContext;
|
||||
}
|
||||
DefaultResourceLoader resourceLoader =
|
||||
BundleResourceUtils.getResourceLoader(haloPluginManager, pluginName);
|
||||
BundleResourceUtils.getResourceLoader(pluginManager, pluginName);
|
||||
if (resourceLoader == null) {
|
||||
throw new NotFoundException("Plugin [" + pluginName + "] not found.");
|
||||
}
|
||||
|
|
|
@ -2,7 +2,6 @@ package run.halo.app.plugin.resources;
|
|||
|
||||
import com.google.common.collect.LinkedHashMultimap;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.locks.StampedLock;
|
||||
|
@ -10,8 +9,8 @@ import org.springframework.stereotype.Component;
|
|||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.core.extension.ReverseProxy;
|
||||
import run.halo.app.plugin.PluginRouterFunctionRegistry;
|
||||
|
||||
/**
|
||||
* A registry for {@link RouterFunction} of plugin.
|
||||
|
@ -21,6 +20,9 @@ import run.halo.app.core.extension.ReverseProxy;
|
|||
*/
|
||||
@Component
|
||||
public class ReverseProxyRouterFunctionRegistry {
|
||||
|
||||
private final PluginRouterFunctionRegistry pluginRouterFunctionRegistry;
|
||||
|
||||
private final ReverseProxyRouterFunctionFactory reverseProxyRouterFunctionFactory;
|
||||
private final StampedLock lock = new StampedLock();
|
||||
private final Map<String, RouterFunction<ServerResponse>> proxyNameRouterFunctionRegistry =
|
||||
|
@ -29,7 +31,9 @@ public class ReverseProxyRouterFunctionRegistry {
|
|||
LinkedHashMultimap.create();
|
||||
|
||||
public ReverseProxyRouterFunctionRegistry(
|
||||
PluginRouterFunctionRegistry pluginRouterFunctionRegistry,
|
||||
ReverseProxyRouterFunctionFactory reverseProxyRouterFunctionFactory) {
|
||||
this.pluginRouterFunctionRegistry = pluginRouterFunctionRegistry;
|
||||
this.reverseProxyRouterFunctionFactory = reverseProxyRouterFunctionFactory;
|
||||
}
|
||||
|
||||
|
@ -38,20 +42,18 @@ public class ReverseProxyRouterFunctionRegistry {
|
|||
*
|
||||
* @param pluginId plugin id
|
||||
* @param reverseProxy reverse proxy
|
||||
* @return a mono
|
||||
*/
|
||||
public Mono<Void> register(String pluginId, ReverseProxy reverseProxy) {
|
||||
public void register(String pluginId, ReverseProxy reverseProxy) {
|
||||
Assert.notNull(pluginId, "The plugin id must not be null.");
|
||||
final String proxyName = reverseProxy.getMetadata().getName();
|
||||
long stamp = lock.writeLock();
|
||||
try {
|
||||
pluginIdReverseProxyMap.put(pluginId, proxyName);
|
||||
return reverseProxyRouterFunctionFactory.create(reverseProxy, pluginId)
|
||||
.map(routerFunction -> {
|
||||
proxyNameRouterFunctionRegistry.put(proxyName, routerFunction);
|
||||
return routerFunction;
|
||||
})
|
||||
.then();
|
||||
var routerFunction = reverseProxyRouterFunctionFactory.create(reverseProxy, pluginId);
|
||||
if (routerFunction != null) {
|
||||
proxyNameRouterFunctionRegistry.put(proxyName, routerFunction);
|
||||
pluginRouterFunctionRegistry.register(Set.of(routerFunction));
|
||||
}
|
||||
} finally {
|
||||
lock.unlockWrite(stamp);
|
||||
}
|
||||
|
@ -60,65 +62,25 @@ public class ReverseProxyRouterFunctionRegistry {
|
|||
/**
|
||||
* Only for test.
|
||||
*/
|
||||
protected int reverseProxySize(String pluginId) {
|
||||
int reverseProxySize(String pluginId) {
|
||||
Set<String> names = pluginIdReverseProxyMap.get(pluginId);
|
||||
return names.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove reverse proxy router function by plugin id.
|
||||
*
|
||||
* @param pluginId plugin id
|
||||
*/
|
||||
public Mono<Void> remove(String pluginId) {
|
||||
Assert.notNull(pluginId, "The plugin id must not be null.");
|
||||
long stamp = lock.writeLock();
|
||||
try {
|
||||
Set<String> proxyNames = pluginIdReverseProxyMap.removeAll(pluginId);
|
||||
for (String proxyName : proxyNames) {
|
||||
proxyNameRouterFunctionRegistry.remove(proxyName);
|
||||
}
|
||||
return Mono.empty();
|
||||
} finally {
|
||||
lock.unlockWrite(stamp);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove reverse proxy router function by pluginId and reverse proxy name.
|
||||
*/
|
||||
public Mono<Void> remove(String pluginId, String reverseProxyName) {
|
||||
public void remove(String pluginId, String reverseProxyName) {
|
||||
long stamp = lock.writeLock();
|
||||
try {
|
||||
pluginIdReverseProxyMap.remove(pluginId, reverseProxyName);
|
||||
proxyNameRouterFunctionRegistry.remove(reverseProxyName);
|
||||
return Mono.empty();
|
||||
var removedRouterFunction = proxyNameRouterFunctionRegistry.remove(reverseProxyName);
|
||||
if (removedRouterFunction != null) {
|
||||
pluginRouterFunctionRegistry.unregister(Set.of(removedRouterFunction));
|
||||
}
|
||||
} finally {
|
||||
lock.unlockWrite(stamp);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets reverse proxy {@link RouterFunction} by reverse proxy name.
|
||||
*/
|
||||
public RouterFunction<ServerResponse> getRouterFunction(String proxyName) {
|
||||
long stamp = lock.readLock();
|
||||
try {
|
||||
return proxyNameRouterFunctionRegistry.get(proxyName);
|
||||
} finally {
|
||||
lock.unlockRead(stamp);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all reverse proxy {@link RouterFunction}.
|
||||
*/
|
||||
public List<RouterFunction<ServerResponse>> getRouterFunctions() {
|
||||
long stamp = lock.readLock();
|
||||
try {
|
||||
return List.copyOf(proxyNameRouterFunctionRegistry.values());
|
||||
} finally {
|
||||
lock.unlockRead(stamp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,110 @@
|
|||
package run.halo.app.theme.finders;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* Finder registry for class annotated with {@link Finder}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Component
|
||||
public class DefaultFinderRegistry implements FinderRegistry, InitializingBean {
|
||||
private final Map<String, List<String>> pluginFindersLookup = new ConcurrentHashMap<>();
|
||||
private final Map<String, Object> finders = new ConcurrentHashMap<>(64);
|
||||
|
||||
private final ApplicationContext applicationContext;
|
||||
|
||||
public DefaultFinderRegistry(ApplicationContext applicationContext) {
|
||||
this.applicationContext = applicationContext;
|
||||
}
|
||||
|
||||
Object get(String name) {
|
||||
return finders.get(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a name, register a Finder for it.
|
||||
*
|
||||
* @param name the canonical name
|
||||
* @param finder the finder to be registered
|
||||
* @throws IllegalStateException if the name is already existing
|
||||
*/
|
||||
void putFinder(String name, Object finder) {
|
||||
if (finders.containsKey(name)) {
|
||||
throw new IllegalStateException(
|
||||
"Finder with name '" + name + "' is already registered");
|
||||
}
|
||||
finders.put(name, finder);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a finder.
|
||||
*
|
||||
* @param finder register a finder that annotated with {@link Finder}
|
||||
* @return the name of the finder
|
||||
*/
|
||||
String putFinder(Object finder) {
|
||||
var name = getFinderName(finder);
|
||||
this.putFinder(name, finder);
|
||||
return name;
|
||||
}
|
||||
|
||||
private String getFinderName(Object finder) {
|
||||
var annotation = finder.getClass().getAnnotation(Finder.class);
|
||||
if (annotation == null) {
|
||||
// should never happen
|
||||
throw new IllegalStateException("Finder must be annotated with @Finder");
|
||||
}
|
||||
String name = annotation.value();
|
||||
if (name == null) {
|
||||
name = finder.getClass().getSimpleName();
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
public void removeFinder(String name) {
|
||||
finders.remove(name);
|
||||
}
|
||||
|
||||
public Map<String, Object> getFinders() {
|
||||
return Map.copyOf(finders);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() {
|
||||
// initialize finders from application context
|
||||
applicationContext.getBeansWithAnnotation(Finder.class)
|
||||
.forEach((beanName, finder) -> {
|
||||
var finderName = getFinderName(finder);
|
||||
this.putFinder(finderName, finder);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void register(String pluginId, ApplicationContext pluginContext) {
|
||||
pluginContext.getBeansWithAnnotation(Finder.class)
|
||||
.forEach((beanName, finder) -> {
|
||||
var finderName = getFinderName(finder);
|
||||
this.putFinder(finderName, finder);
|
||||
pluginFindersLookup
|
||||
.computeIfAbsent(pluginId, ignored -> new ArrayList<>())
|
||||
.add(finderName);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unregister(String pluginId) {
|
||||
var finderNames = pluginFindersLookup.remove(pluginId);
|
||||
if (finderNames != null) {
|
||||
finderNames.forEach(finders::remove);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,17 +1,7 @@
|
|||
package run.halo.app.theme.finders;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.stereotype.Component;
|
||||
import run.halo.app.plugin.ExtensionContextRegistry;
|
||||
import run.halo.app.plugin.PluginApplicationContext;
|
||||
import run.halo.app.plugin.event.HaloPluginStartedEvent;
|
||||
import run.halo.app.plugin.event.HaloPluginStoppedEvent;
|
||||
|
||||
/**
|
||||
* Finder registry for class annotated with {@link Finder}.
|
||||
|
@ -19,115 +9,12 @@ import run.halo.app.plugin.event.HaloPluginStoppedEvent;
|
|||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Component
|
||||
public class FinderRegistry implements InitializingBean {
|
||||
private final Map<String, List<String>> pluginFindersLookup = new ConcurrentHashMap<>();
|
||||
private final Map<String, Object> finders = new ConcurrentHashMap<>(64);
|
||||
public interface FinderRegistry {
|
||||
|
||||
private final ApplicationContext applicationContext;
|
||||
Map<String, Object> getFinders();
|
||||
|
||||
public FinderRegistry(ApplicationContext applicationContext) {
|
||||
this.applicationContext = applicationContext;
|
||||
}
|
||||
void register(String pluginId, ApplicationContext pluginContext);
|
||||
|
||||
Object get(String name) {
|
||||
return finders.get(name);
|
||||
}
|
||||
void unregister(String pluginId);
|
||||
|
||||
/**
|
||||
* Given a name, register a Finder for it.
|
||||
*
|
||||
* @param name the canonical name
|
||||
* @param finder the finder to be registered
|
||||
* @throws IllegalStateException if the name is already existing
|
||||
*/
|
||||
public void registerFinder(String name, Object finder) {
|
||||
if (finders.containsKey(name)) {
|
||||
throw new IllegalStateException(
|
||||
"Finder with name '" + name + "' is already registered");
|
||||
}
|
||||
finders.put(name, finder);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a finder.
|
||||
*
|
||||
* @param finder register a finder that annotated with {@link Finder}
|
||||
* @return the name of the finder
|
||||
*/
|
||||
public String registerFinder(Object finder) {
|
||||
Finder annotation = finder.getClass().getAnnotation(Finder.class);
|
||||
if (annotation == null) {
|
||||
throw new IllegalStateException("Finder must be annotated with @Finder");
|
||||
}
|
||||
String name = annotation.value();
|
||||
if (name == null) {
|
||||
name = finder.getClass().getSimpleName();
|
||||
}
|
||||
this.registerFinder(name, finder);
|
||||
return name;
|
||||
}
|
||||
|
||||
public void removeFinder(String name) {
|
||||
finders.remove(name);
|
||||
}
|
||||
|
||||
public Map<String, Object> getFinders() {
|
||||
return Map.copyOf(finders);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
// initialize finders from application context
|
||||
applicationContext.getBeansWithAnnotation(Finder.class)
|
||||
.forEach((k, v) -> {
|
||||
registerFinder(v);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Register finders for a plugin.
|
||||
*
|
||||
* @param event plugin started event
|
||||
*/
|
||||
@EventListener(HaloPluginStartedEvent.class)
|
||||
public void onPluginStarted(HaloPluginStartedEvent event) {
|
||||
String pluginId = event.getPlugin().getPluginId();
|
||||
PluginApplicationContext pluginApplicationContext = ExtensionContextRegistry.getInstance()
|
||||
.getByPluginId(pluginId);
|
||||
pluginApplicationContext.getBeansWithAnnotation(Finder.class)
|
||||
.forEach((beanName, finderObject) -> {
|
||||
// register finder
|
||||
String finderName = registerFinder(finderObject);
|
||||
// add to plugin finder lookup
|
||||
pluginFindersLookup.computeIfAbsent(pluginId, k -> new ArrayList<>())
|
||||
.add(finderName);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove finders registered by the plugin.
|
||||
*
|
||||
* @param event plugin stopped event
|
||||
*/
|
||||
@EventListener(HaloPluginStoppedEvent.class)
|
||||
public void onPluginStopped(HaloPluginStoppedEvent event) {
|
||||
String pluginId = event.getPlugin().getPluginId();
|
||||
boolean containsKey = pluginFindersLookup.containsKey(pluginId);
|
||||
if (!containsKey) {
|
||||
return;
|
||||
}
|
||||
pluginFindersLookup.get(pluginId).forEach(this::removeFinder);
|
||||
}
|
||||
|
||||
/**
|
||||
* Only for test.
|
||||
*
|
||||
* @param pluginId plugin id
|
||||
* @param finderName finder name
|
||||
*/
|
||||
void addPluginFinder(String pluginId, String finderName) {
|
||||
pluginFindersLookup.computeIfAbsent(pluginId, k -> new ArrayList<>())
|
||||
.add(finderName);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ package run.halo.app.core.extension.reconciler;
|
|||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.doNothing;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
@ -17,7 +18,6 @@ import org.junit.jupiter.api.extension.ExtendWith;
|
|||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.core.extension.ReverseProxy;
|
||||
import run.halo.app.extension.ExtensionClient;
|
||||
import run.halo.app.extension.Metadata;
|
||||
|
@ -54,7 +54,7 @@ class ReverseProxyReconcilerTest {
|
|||
.setLabels(Map.of(PluginConst.PLUGIN_NAME_LABEL_NAME, "fake-plugin"));
|
||||
reverseProxy.setRules(List.of());
|
||||
|
||||
when(routerFunctionRegistry.remove(anyString(), anyString())).thenReturn(Mono.empty());
|
||||
doNothing().when(routerFunctionRegistry).remove(anyString(), anyString());
|
||||
when(client.fetch(ReverseProxy.class, "fake-reverse-proxy"))
|
||||
.thenReturn(Optional.of(reverseProxy));
|
||||
|
||||
|
@ -62,7 +62,6 @@ class ReverseProxyReconcilerTest {
|
|||
|
||||
verify(routerFunctionRegistry, never()).register(anyString(), any(ReverseProxy.class));
|
||||
|
||||
verify(routerFunctionRegistry, never()).remove(eq("fake-plugin"));
|
||||
verify(routerFunctionRegistry, times(1))
|
||||
.remove(eq("fake-plugin"), eq("fake-reverse-proxy"));
|
||||
}
|
||||
|
|
|
@ -1,11 +1,16 @@
|
|||
package run.halo.app.infra;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.github.zafarkhaja.semver.Version;
|
||||
import java.util.Properties;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.boot.info.BuildProperties;
|
||||
|
||||
/**
|
||||
|
@ -15,14 +20,14 @@ import org.springframework.boot.info.BuildProperties;
|
|||
* @since 2.0.0
|
||||
*/
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class DefaultSystemVersionSupplierTest {
|
||||
|
||||
@InjectMocks
|
||||
private DefaultSystemVersionSupplier systemVersionSupplier;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
systemVersionSupplier = new DefaultSystemVersionSupplier();
|
||||
}
|
||||
@Mock
|
||||
ObjectProvider<BuildProperties> buildPropertiesProvider;
|
||||
|
||||
@Test
|
||||
void getWhenBuildPropertiesNotSet() {
|
||||
|
@ -34,7 +39,7 @@ class DefaultSystemVersionSupplierTest {
|
|||
void getWhenBuildPropertiesButVersionIsNull() {
|
||||
Properties properties = new Properties();
|
||||
BuildProperties buildProperties = new BuildProperties(properties);
|
||||
systemVersionSupplier.setBuildProperties(buildProperties);
|
||||
when(buildPropertiesProvider.getIfUnique()).thenReturn(buildProperties);
|
||||
|
||||
Version version = systemVersionSupplier.get();
|
||||
assertThat(version.toString()).isEqualTo("0.0.0");
|
||||
|
@ -45,14 +50,14 @@ class DefaultSystemVersionSupplierTest {
|
|||
Properties properties = new Properties();
|
||||
properties.put("version", "2.0.0");
|
||||
BuildProperties buildProperties = new BuildProperties(properties);
|
||||
systemVersionSupplier.setBuildProperties(buildProperties);
|
||||
when(buildPropertiesProvider.getIfUnique()).thenReturn(buildProperties);
|
||||
|
||||
Version version = systemVersionSupplier.get();
|
||||
assertThat(version.toString()).isEqualTo("2.0.0");
|
||||
|
||||
properties.put("version", "2.0.0-SNAPSHOT");
|
||||
buildProperties = new BuildProperties(properties);
|
||||
systemVersionSupplier.setBuildProperties(buildProperties);
|
||||
when(buildPropertiesProvider.getIfUnique()).thenReturn(buildProperties);
|
||||
version = systemVersionSupplier.get();
|
||||
assertThat(version.toString()).isEqualTo("2.0.0-SNAPSHOT");
|
||||
assertThat(version.getPreReleaseVersion()).isEqualTo("SNAPSHOT");
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
package run.halo.app.plugin;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
import java.util.Set;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.invocation.InvocationOnMock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
|
||||
/**
|
||||
* Tests for {@link DefaultPluginRouterFunctionRegistry}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class DefaultPluginRouterFunctionRegistryTest {
|
||||
|
||||
@InjectMocks
|
||||
DefaultPluginRouterFunctionRegistry routerFunctionRegistry;
|
||||
|
||||
@Test
|
||||
void shouldRegisterRouterFunction() {
|
||||
RouterFunction<ServerResponse> routerFunction = mock(InvocationOnMock::getMock);
|
||||
routerFunctionRegistry.register(Set.of(routerFunction));
|
||||
assertEquals(Set.of(routerFunction), routerFunctionRegistry.getRouterFunctions());
|
||||
}
|
||||
|
||||
}
|
|
@ -1,96 +0,0 @@
|
|||
package run.halo.app.plugin;
|
||||
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.http.codec.ServerCodecConfigurer;
|
||||
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
|
||||
import org.springframework.mock.web.server.MockServerWebExchange;
|
||||
import org.springframework.web.reactive.function.server.HandlerFunction;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import org.springframework.web.reactive.function.server.support.RouterFunctionMapping;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
import run.halo.app.plugin.resources.ReverseProxyRouterFunctionRegistry;
|
||||
|
||||
/**
|
||||
* Tests for {@link PluginCompositeRouterFunction}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class PluginCompositeRouterFunctionTest {
|
||||
ServerCodecConfigurer codecConfigurer = ServerCodecConfigurer.create();
|
||||
|
||||
@Mock
|
||||
ReverseProxyRouterFunctionRegistry reverseProxyRouterFunctionRegistry;
|
||||
|
||||
@Mock
|
||||
ObjectProvider<RouterFunction> rawRouterFunctionsProvider;
|
||||
|
||||
@InjectMocks
|
||||
PluginCompositeRouterFunction compositeRouterFunction;
|
||||
|
||||
HandlerFunction<ServerResponse> handlerFunction;
|
||||
|
||||
@BeforeEach
|
||||
@SuppressWarnings("unchecked")
|
||||
void setUp() {
|
||||
var fakeContext = mock(PluginApplicationContext.class);
|
||||
when(fakeContext.isActive()).thenReturn(true);
|
||||
ExtensionContextRegistry.getInstance().register("fake-plugin", fakeContext);
|
||||
|
||||
when(rawRouterFunctionsProvider.orderedStream()).thenReturn(Stream.empty());
|
||||
|
||||
when(fakeContext.getBeanProvider(RouterFunction.class))
|
||||
.thenReturn(rawRouterFunctionsProvider);
|
||||
|
||||
compositeRouterFunction =
|
||||
new PluginCompositeRouterFunction(reverseProxyRouterFunctionRegistry);
|
||||
|
||||
handlerFunction = request -> ServerResponse.ok().build();
|
||||
RouterFunction<ServerResponse> routerFunction = request -> Mono.just(handlerFunction);
|
||||
|
||||
when(reverseProxyRouterFunctionRegistry.getRouterFunctions())
|
||||
.thenReturn(List.of(routerFunction));
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void cleanUp() {
|
||||
ExtensionContextRegistry.getInstance().remove("fake-plugin");
|
||||
}
|
||||
|
||||
@Test
|
||||
void route() {
|
||||
RouterFunctionMapping mapping = new RouterFunctionMapping(compositeRouterFunction);
|
||||
mapping.setMessageReaders(this.codecConfigurer.getReaders());
|
||||
|
||||
Mono<Object> result = mapping.getHandler(createExchange("https://example.com/match"));
|
||||
|
||||
StepVerifier.create(result)
|
||||
.expectNext(handlerFunction)
|
||||
.expectComplete()
|
||||
.verify();
|
||||
|
||||
verify(rawRouterFunctionsProvider).orderedStream();
|
||||
}
|
||||
|
||||
private ServerWebExchange createExchange(String urlTemplate) {
|
||||
return MockServerWebExchange.from(MockServerHttpRequest.get(urlTemplate));
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
package run.halo.app.plugin;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
|
||||
/**
|
||||
* Tests for {@link SharedApplicationContextFactory}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@SpringBootTest
|
||||
@AutoConfigureTestDatabase
|
||||
class SharedApplicationContextFactoryTest {
|
||||
|
||||
@Autowired
|
||||
ApplicationContext applicationContext;
|
||||
|
||||
@Test
|
||||
void createSharedApplicationContext() {
|
||||
var sharedContext = SharedApplicationContextFactory.create(applicationContext);
|
||||
assertNotNull(sharedContext);
|
||||
}
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
package run.halo.app.plugin;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
/**
|
||||
* Tests for {@link SharedApplicationContextHolder}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@SpringBootTest
|
||||
@AutoConfigureTestDatabase
|
||||
class SharedApplicationContextHolderTest {
|
||||
|
||||
@Autowired
|
||||
SharedApplicationContextHolder sharedApplicationContextHolder;
|
||||
|
||||
@Test
|
||||
void getInstance() {
|
||||
SharedApplicationContext instance1 = sharedApplicationContextHolder.getInstance();
|
||||
SharedApplicationContext instance2 = sharedApplicationContextHolder.getInstance();
|
||||
assertThat(instance1).isNotNull();
|
||||
assertThat(instance1).isEqualTo(instance2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void createSharedApplicationContext() {
|
||||
SharedApplicationContext sharedApplicationContext =
|
||||
sharedApplicationContextHolder.createSharedApplicationContext();
|
||||
assertThat(sharedApplicationContext).isNotNull();
|
||||
}
|
||||
}
|
|
@ -3,15 +3,9 @@ package run.halo.app.plugin;
|
|||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.util.Set;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
@ -47,28 +41,6 @@ class SpringComponentsFinderTest {
|
|||
testFile = ResourceUtils.getFile("classpath:plugin/test-plugin-components.idx");
|
||||
}
|
||||
|
||||
@Test
|
||||
void readPluginStorageToMemory() throws FileNotFoundException {
|
||||
boolean contains = springComponentsFinder.containsComponentsStorage("fakePlugin");
|
||||
assertThat(contains).isFalse();
|
||||
|
||||
when(pluginWrapper.getPluginId()).thenReturn("fakePlugin");
|
||||
when(pluginWrapper.getPluginClassLoader()).thenReturn(pluginClassLoader);
|
||||
when(pluginClassLoader.getResourceAsStream(any()))
|
||||
.thenReturn(new FileInputStream(testFile));
|
||||
|
||||
springComponentsFinder.readPluginStorageToMemory(pluginWrapper);
|
||||
|
||||
contains = springComponentsFinder.containsComponentsStorage("fakePlugin");
|
||||
assertThat(contains).isTrue();
|
||||
|
||||
verify(pluginClassLoader, times(1)).getResourceAsStream(any());
|
||||
|
||||
// repeat it
|
||||
springComponentsFinder.readPluginStorageToMemory(pluginWrapper);
|
||||
verify(pluginClassLoader, times(1)).getResourceAsStream(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void containsPlugin() {
|
||||
boolean exist = springComponentsFinder.containsComponentsStorage("NotExist");
|
||||
|
@ -78,15 +50,4 @@ class SpringComponentsFinderTest {
|
|||
.hasMessage("The pluginId cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void removeComponentsCache() {
|
||||
springComponentsFinder.putComponentsStorage("fakePlugin", Set.of("A"));
|
||||
boolean contains = springComponentsFinder.containsComponentsStorage("fakePlugin");
|
||||
assertThat(contains).isTrue();
|
||||
|
||||
springComponentsFinder.removeComponentsStorage("fakePlugin");
|
||||
|
||||
contains = springComponentsFinder.containsComponentsStorage("fakePlugin");
|
||||
assertThat(contains).isFalse();
|
||||
}
|
||||
}
|
|
@ -1,5 +1,7 @@
|
|||
package run.halo.app.plugin.resources;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
@ -7,11 +9,10 @@ import org.junit.jupiter.api.extension.ExtendWith;
|
|||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.pf4j.PluginManager;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import reactor.test.StepVerifier;
|
||||
import run.halo.app.core.extension.ReverseProxy;
|
||||
import run.halo.app.extension.Metadata;
|
||||
import run.halo.app.plugin.HaloPluginManager;
|
||||
import run.halo.app.plugin.PluginConst;
|
||||
|
||||
/**
|
||||
|
@ -24,7 +25,7 @@ import run.halo.app.plugin.PluginConst;
|
|||
class ReverseProxyRouterFunctionFactoryTest {
|
||||
|
||||
@Mock
|
||||
private HaloPluginManager haloPluginManager;
|
||||
private PluginManager pluginManager;
|
||||
|
||||
@Mock
|
||||
private ApplicationContext applicationContext;
|
||||
|
@ -34,11 +35,8 @@ class ReverseProxyRouterFunctionFactoryTest {
|
|||
|
||||
@Test
|
||||
void create() {
|
||||
var routerFunction =
|
||||
reverseProxyRouterFunctionFactory.create(mockReverseProxy(), "fakeA");
|
||||
StepVerifier.create(routerFunction)
|
||||
.expectNextCount(1)
|
||||
.verifyComplete();
|
||||
var routerFunction = reverseProxyRouterFunctionFactory.create(mockReverseProxy(), "fakeA");
|
||||
assertNotNull(routerFunction);
|
||||
}
|
||||
|
||||
private ReverseProxy mockReverseProxy() {
|
||||
|
|
|
@ -6,8 +6,6 @@ import static org.mockito.Mockito.times;
|
|||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
|
@ -17,11 +15,9 @@ import org.mockito.junit.jupiter.MockitoExtension;
|
|||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
import run.halo.app.core.extension.ReverseProxy;
|
||||
import run.halo.app.extension.Metadata;
|
||||
import run.halo.app.plugin.ExtensionContextRegistry;
|
||||
import run.halo.app.plugin.PluginApplicationContext;
|
||||
import run.halo.app.plugin.PluginRouterFunctionRegistry;
|
||||
|
||||
/**
|
||||
* Tests for {@link ReverseProxyRouterFunctionRegistry}.
|
||||
|
@ -33,61 +29,35 @@ import run.halo.app.plugin.PluginApplicationContext;
|
|||
class ReverseProxyRouterFunctionRegistryTest {
|
||||
|
||||
@InjectMocks
|
||||
private ReverseProxyRouterFunctionRegistry registry;
|
||||
ReverseProxyRouterFunctionRegistry registry;
|
||||
|
||||
@Mock
|
||||
private ReverseProxyRouterFunctionFactory reverseProxyRouterFunctionFactory;
|
||||
ReverseProxyRouterFunctionFactory reverseProxyRouterFunctionFactory;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
ExtensionContextRegistry instance = ExtensionContextRegistry.getInstance();
|
||||
instance.register("fake-plugin", Mockito.mock(PluginApplicationContext.class));
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
ExtensionContextRegistry.getInstance().remove("fake-plugin");
|
||||
}
|
||||
@Mock
|
||||
PluginRouterFunctionRegistry pluginRouterFunctionRegistry;
|
||||
|
||||
@Test
|
||||
void register() {
|
||||
ReverseProxy mock = getMockReverseProxy();
|
||||
registry.register("fake-plugin", mock)
|
||||
.as(StepVerifier::create)
|
||||
.verifyComplete();
|
||||
registry.register("fake-plugin", mock);
|
||||
|
||||
assertThat(registry.reverseProxySize("fake-plugin")).isEqualTo(1);
|
||||
|
||||
// repeat register a same reverse proxy
|
||||
registry.register("fake-plugin", mock)
|
||||
.as(StepVerifier::create)
|
||||
.verifyComplete();
|
||||
registry.register("fake-plugin", mock);
|
||||
|
||||
assertThat(registry.reverseProxySize("fake-plugin")).isEqualTo(1);
|
||||
|
||||
verify(reverseProxyRouterFunctionFactory, times(2)).create(any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void remove() {
|
||||
ReverseProxy mock = getMockReverseProxy();
|
||||
registry.register("fake-plugin", mock)
|
||||
.as(StepVerifier::create)
|
||||
.verifyComplete();
|
||||
|
||||
registry.remove("fake-plugin").block();
|
||||
|
||||
assertThat(registry.reverseProxySize("fake-plugin")).isEqualTo(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void removeByKeyValue() {
|
||||
ReverseProxy mock = getMockReverseProxy();
|
||||
registry.register("fake-plugin", mock)
|
||||
.as(StepVerifier::create)
|
||||
.verifyComplete();
|
||||
registry.register("fake-plugin", mock);
|
||||
|
||||
registry.remove("fake-plugin", "test-reverse-proxy").block();
|
||||
registry.remove("fake-plugin", "test-reverse-proxy");
|
||||
|
||||
assertThat(registry.reverseProxySize("fake-plugin")).isEqualTo(0);
|
||||
}
|
||||
|
@ -100,7 +70,7 @@ class ReverseProxyRouterFunctionRegistryTest {
|
|||
RouterFunction<ServerResponse> routerFunction = request -> Mono.empty();
|
||||
|
||||
when(reverseProxyRouterFunctionFactory.create(any(), any()))
|
||||
.thenReturn(Mono.just(routerFunction));
|
||||
.thenReturn(routerFunction);
|
||||
return mock;
|
||||
}
|
||||
}
|
|
@ -2,18 +2,14 @@ package run.halo.app.theme.finders;
|
|||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.util.Map;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.pf4j.PluginWrapper;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import run.halo.app.plugin.event.HaloPluginStoppedEvent;
|
||||
|
||||
/**
|
||||
* Tests for {@link FinderRegistry}.
|
||||
|
@ -24,29 +20,29 @@ import run.halo.app.plugin.event.HaloPluginStoppedEvent;
|
|||
@ExtendWith(MockitoExtension.class)
|
||||
class FinderRegistryTest {
|
||||
|
||||
private FinderRegistry finderRegistry;
|
||||
private DefaultFinderRegistry finderRegistry;
|
||||
@Mock
|
||||
private ApplicationContext applicationContext;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
finderRegistry = new FinderRegistry(applicationContext);
|
||||
finderRegistry = new DefaultFinderRegistry(applicationContext);
|
||||
}
|
||||
|
||||
@Test
|
||||
void registerFinder() {
|
||||
assertThatThrownBy(() -> {
|
||||
finderRegistry.registerFinder(new Object());
|
||||
finderRegistry.putFinder(new Object());
|
||||
}).isInstanceOf(IllegalStateException.class)
|
||||
.hasMessage("Finder must be annotated with @Finder");
|
||||
|
||||
String s = finderRegistry.registerFinder(new FakeFinder());
|
||||
String s = finderRegistry.putFinder(new FakeFinder());
|
||||
assertThat(s).isEqualTo("test");
|
||||
}
|
||||
|
||||
@Test
|
||||
void removeFinder() {
|
||||
String s = finderRegistry.registerFinder(new FakeFinder());
|
||||
String s = finderRegistry.putFinder(new FakeFinder());
|
||||
assertThat(s).isEqualTo("test");
|
||||
Object test = finderRegistry.get("test");
|
||||
assertThat(test).isNotNull();
|
||||
|
@ -60,25 +56,11 @@ class FinderRegistryTest {
|
|||
void getFinders() {
|
||||
assertThat(finderRegistry.getFinders()).hasSize(0);
|
||||
|
||||
finderRegistry.registerFinder(new FakeFinder());
|
||||
finderRegistry.putFinder(new FakeFinder());
|
||||
Map<String, Object> finders = finderRegistry.getFinders();
|
||||
assertThat(finders).hasSize(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void onPluginStopped() {
|
||||
finderRegistry.registerFinder("a", new Object());
|
||||
finderRegistry.addPluginFinder("fake", "a");
|
||||
|
||||
HaloPluginStoppedEvent event = Mockito.mock(HaloPluginStoppedEvent.class);
|
||||
PluginWrapper pluginWrapper = Mockito.mock(PluginWrapper.class);
|
||||
when(event.getPlugin()).thenReturn(pluginWrapper);
|
||||
when(pluginWrapper.getPluginId()).thenReturn("fake");
|
||||
|
||||
finderRegistry.onPluginStopped(event);
|
||||
assertThat(finderRegistry.get("a")).isNull();
|
||||
}
|
||||
|
||||
@Finder("test")
|
||||
static class FakeFinder {
|
||||
|
||||
|
|
Loading…
Reference in New Issue