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
John Niang 2024-01-26 17:08:11 +08:00 committed by GitHub
parent 17a0fb9e05
commit 8288e4edf8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
53 changed files with 1286 additions and 1902 deletions

View File

@ -262,8 +262,8 @@ public class PluginReconciler implements Reconciler<Request> {
var p = pluginManager.getPlugin(pluginName); var p = pluginManager.getPlugin(pluginName);
var classLoader = p.getPluginClassLoader(); var classLoader = p.getPluginClassLoader();
var resLoader = new DefaultResourceLoader(classLoader); var resLoader = new DefaultResourceLoader(classLoader);
var entryRes = resLoader.getResource("classpath:/console/main.js"); var entryRes = resLoader.getResource("classpath:console/main.js");
var cssRes = resLoader.getResource("classpath:/console/style.css"); var cssRes = resLoader.getResource("classpath:console/style.css");
if (entryRes.exists()) { if (entryRes.exists()) {
var entry = UriComponentsBuilder.newInstance() var entry = UriComponentsBuilder.newInstance()
.pathSegment("plugins", pluginName, "assets", "console", "main.js") .pathSegment("plugins", pluginName, "assets", "console", "main.js")

View File

@ -55,12 +55,12 @@ public class ReverseProxyReconciler implements Reconciler<Reconciler.Request> {
private void registerReverseProxy(ReverseProxy reverseProxy) { private void registerReverseProxy(ReverseProxy reverseProxy) {
String pluginId = getPluginId(reverseProxy); String pluginId = getPluginId(reverseProxy);
routerFunctionRegistry.register(pluginId, reverseProxy).block(); routerFunctionRegistry.register(pluginId, reverseProxy);
} }
private void cleanUpResources(ReverseProxy reverseProxy) { private void cleanUpResources(ReverseProxy reverseProxy) {
String pluginId = getPluginId(reverseProxy); String pluginId = getPluginId(reverseProxy);
routerFunctionRegistry.remove(pluginId, reverseProxy.getMetadata().getName()).block(); routerFunctionRegistry.remove(pluginId, reverseProxy.getMetadata().getName());
} }
private void addFinalizerIfNecessary(ReverseProxy oldReverseProxy) { private void addFinalizerIfNecessary(ReverseProxy oldReverseProxy) {

View File

@ -1,10 +1,9 @@
package run.halo.app.infra; package run.halo.app.infra;
import com.github.zafarkhaja.semver.Version; import com.github.zafarkhaja.semver.Version;
import org.apache.commons.lang3.StringUtils; import java.util.Objects;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.info.BuildProperties; import org.springframework.boot.info.BuildProperties;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
/** /**
@ -17,21 +16,19 @@ import org.springframework.stereotype.Component;
public class DefaultSystemVersionSupplier implements SystemVersionSupplier { public class DefaultSystemVersionSupplier implements SystemVersionSupplier {
private static final String DEFAULT_VERSION = "0.0.0"; private static final String DEFAULT_VERSION = "0.0.0";
@Nullable private final ObjectProvider<BuildProperties> buildProperties;
private BuildProperties buildProperties;
@Autowired(required = false) public DefaultSystemVersionSupplier(ObjectProvider<BuildProperties> buildProperties) {
public void setBuildProperties(@Nullable BuildProperties buildProperties) {
this.buildProperties = buildProperties; this.buildProperties = buildProperties;
} }
@Override @Override
public Version get() { public Version get() {
if (buildProperties == null) { var properties = buildProperties.getIfUnique();
if (properties == null) {
return Version.valueOf(DEFAULT_VERSION); return Version.valueOf(DEFAULT_VERSION);
} }
String projectVersion = var projectVersion = Objects.toString(properties.getVersion(), DEFAULT_VERSION);
StringUtils.defaultString(buildProperties.getVersion(), DEFAULT_VERSION);
return Version.valueOf(projectVersion); return Version.valueOf(projectVersion);
} }
} }

View File

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

View File

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

View File

@ -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;
}
}

View File

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

View File

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

View File

@ -3,6 +3,7 @@ package run.halo.app.plugin;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import org.pf4j.ExtensionPoint; import org.pf4j.ExtensionPoint;
import org.pf4j.PluginManager;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@ -15,12 +16,12 @@ import org.springframework.stereotype.Component;
@Component @Component
public class ExtensionComponentsFinder { public class ExtensionComponentsFinder {
public static final String SYSTEM_PLUGIN_ID = "system"; public static final String SYSTEM_PLUGIN_ID = "system";
private final HaloPluginManager haloPluginManager; private final PluginManager pluginManager;
private final ApplicationContext applicationContext; private final ApplicationContext applicationContext;
public ExtensionComponentsFinder(HaloPluginManager haloPluginManager, public ExtensionComponentsFinder(PluginManager pluginManager,
ApplicationContext applicationContext) { ApplicationContext applicationContext) {
this.haloPluginManager = haloPluginManager; this.pluginManager = pluginManager;
this.applicationContext = applicationContext; this.applicationContext = applicationContext;
} }
@ -33,7 +34,7 @@ public class ExtensionComponentsFinder {
*/ */
public <T> List<T> getExtensions(Class<T> type) { public <T> List<T> getExtensions(Class<T> type) {
assertExtensionPoint(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()); components.addAll(applicationContext.getBeansOfType(type).values());
return List.copyOf(components); return List.copyOf(components);
} }
@ -53,7 +54,7 @@ public class ExtensionComponentsFinder {
components.addAll(applicationContext.getBeansOfType(type).values()); components.addAll(applicationContext.getBeansOfType(type).values());
return components; return components;
} else { } else {
components.addAll(haloPluginManager.getExtensions(type, pluginId)); components.addAll(pluginManager.getExtensions(type, pluginId));
} }
return components; return components;
} }

View File

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

View File

@ -1,60 +1,65 @@
package run.halo.app.plugin; package run.halo.app.plugin;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.Collections; import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Map;
import lombok.extern.slf4j.Slf4j; 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.DefaultPluginManager;
import org.pf4j.DefaultPluginRepository;
import org.pf4j.ExtensionFactory; import org.pf4j.ExtensionFactory;
import org.pf4j.ExtensionFinder; import org.pf4j.ExtensionFinder;
import org.pf4j.PluginDependency; import org.pf4j.JarPluginLoader;
import org.pf4j.JarPluginRepository;
import org.pf4j.PluginDescriptor; import org.pf4j.PluginDescriptor;
import org.pf4j.PluginDescriptorFinder; import org.pf4j.PluginDescriptorFinder;
import org.pf4j.PluginFactory; import org.pf4j.PluginFactory;
import org.pf4j.PluginRuntimeException; import org.pf4j.PluginLoader;
import org.pf4j.PluginState; import org.pf4j.PluginRepository;
import org.pf4j.PluginStateEvent; import org.pf4j.PluginStatusProvider;
import org.pf4j.PluginWrapper; 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.ApplicationContext;
import org.springframework.context.ApplicationContextAware; import org.springframework.data.util.Lazy;
import org.springframework.lang.NonNull; import run.halo.app.infra.SystemVersionSupplier;
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;
/** /**
* PluginManager to hold the main ApplicationContext. * PluginManager to hold the main ApplicationContext.
* It provides methods for managing the plugin lifecycle. * It provides methods for managing the plugin lifecycle.
* *
* @author guqing * @author guqing
* @author johnniang
* @since 2.0.0 * @since 2.0.0
*/ */
@Slf4j @Slf4j
public class HaloPluginManager extends DefaultPluginManager public class HaloPluginManager extends DefaultPluginManager implements SpringPluginManager {
implements ApplicationContextAware, InitializingBean, DisposableBean {
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() { setExactVersionAllowed(pluginProperties.isExactVersionAllowed());
super(); setSystemVersion(systemVersionSupplier.get().getNormalVersion());
super.initialize();
} }
public HaloPluginManager(Path pluginsRoot) { @Override
super(pluginsRoot); protected void initialize() {
// Leave the implementation empty because the super#initialize eagerly initializes
// components before properties set.
} }
@Override @Override
@ -64,31 +69,15 @@ public class HaloPluginManager extends DefaultPluginManager
@Override @Override
protected ExtensionFinder createExtensionFinder() { protected ExtensionFinder createExtensionFinder() {
return new SpringComponentsFinder(this); var finder = new SpringComponentsFinder(this);
} addPluginStateListener(finder);
return finder;
@Override
public final void setApplicationContext(@NonNull ApplicationContext rootApplicationContext)
throws BeansException {
this.rootApplicationContext = rootApplicationContext;
}
final PluginApplicationContext getPluginApplicationContext(String pluginId) {
return pluginApplicationInitializer.getPluginApplicationContext(pluginId);
} }
@Override @Override
protected PluginFactory createPluginFactory() { protected PluginFactory createPluginFactory() {
return new BasePluginFactory(); var contextFactory = new DefaultPluginApplicationContextFactory(this);
} return new SpringPluginFactory(contextFactory);
@Override
public final void afterPropertiesSet() {
this.pluginApplicationInitializer =
new PluginApplicationInitializer(this, rootApplicationContext);
this.requestMappingManager =
rootApplicationContext.getBean(PluginRequestMappingManager.class);
} }
@Override @Override
@ -101,257 +90,71 @@ public class HaloPluginManager extends DefaultPluginManager
ClassLoader pluginClassLoader) { ClassLoader pluginClassLoader) {
// create the plugin wrapper // create the plugin wrapper
log.debug("Creating wrapper for plugin '{}'", pluginPath); log.debug("Creating wrapper for plugin '{}'", pluginPath);
HaloPluginWrapper pluginWrapper = var pluginWrapper =
new HaloPluginWrapper(this, pluginDescriptor, pluginPath, pluginClassLoader); new HaloPluginWrapper(this, pluginDescriptor, pluginPath, pluginClassLoader);
pluginWrapper.setPluginFactory(getPluginFactory()); pluginWrapper.setPluginFactory(getPluginFactory());
return pluginWrapper; return pluginWrapper;
} }
@Override @Override
protected void firePluginStateEvent(PluginStateEvent event) { protected PluginLoader createPluginLoader() {
rootApplicationContext.publishEvent( var compoundLoader = new CompoundPluginLoader();
new HaloPluginStateChangedEvent(this, event.getPlugin(), event.getOldState())); compoundLoader.add(new DevPluginLoader(this, this.pluginProperties), this::isDevelopment);
super.firePluginStateEvent(event); compoundLoader.add(new JarPluginLoader(this));
return compoundLoader;
} }
@Override @Override
protected PluginState stopPlugin(String pluginId, boolean stopDependents) { protected PluginStatusProvider createPluginStatusProvider() {
checkPluginId(pluginId); if (PropertyPluginStatusProvider.isPropertySet(pluginProperties)) {
PluginWrapper pluginWrapper = getPlugin(pluginId); return new PropertyPluginStatusProvider(pluginProperties);
PluginDescriptor pluginDescriptor = pluginWrapper.getDescriptor();
PluginState pluginState = pluginWrapper.getPluginState();
if (PluginState.STOPPED == pluginState) {
log.debug("Already stopped plugin '{}'", getPluginLabel(pluginDescriptor));
return PluginState.STOPPED;
} }
return super.createPluginStatusProvider();
// 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();
} }
@Override @Override
public PluginState stopPlugin(String pluginId) { protected PluginRepository createPluginRepository() {
return this.stopPlugin(pluginId, true); 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 @Override
public void startPlugins() { public void startPlugins() {
startingErrors.clear(); throw new UnsupportedOperationException(
long ts = System.currentTimeMillis(); "The operation of starting all plugins is not supported."
);
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);
} }
@Override @Override
public void stopPlugins() { public void stopPlugins() {
doStopPlugins(); throw new UnsupportedOperationException(
} "The operation of stopping all plugins is not supported."
);
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);
}
} }
@Override @Override
protected PluginWrapper loadPluginFromPath(Path pluginPath) { public ApplicationContext getRootContext() {
PluginWrapper pluginWrapper = super.loadPluginFromPath(pluginPath); return rootContext;
rootApplicationContext.publishEvent(new HaloPluginLoadedEvent(this, pluginWrapper));
return pluginWrapper;
}
private void removePluginComponentsCache(String pluginId) {
if (extensionFinder instanceof SpringComponentsFinder springComponentsFinder) {
springComponentsFinder.removeComponentsStorage(pluginId);
}
} }
@Override @Override
public void destroy() throws Exception { public ApplicationContext getSharedContext() {
stopPlugins(); return sharedContext.get();
} }
// end-region
} }

View File

@ -2,7 +2,7 @@ package run.halo.app.plugin;
import java.util.List; import java.util.List;
import java.util.concurrent.locks.StampedLock; import java.util.concurrent.locks.StampedLock;
import org.springframework.context.support.GenericApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap; import org.springframework.util.MultiValueMap;
@ -16,20 +16,20 @@ import run.halo.app.extension.GroupVersionKind;
* @author guqing * @author guqing
* @since 2.0.0 * @since 2.0.0
*/ */
public class PluginApplicationContext extends GenericApplicationContext { public class PluginApplicationContext extends AnnotationConfigApplicationContext {
private final GvkExtensionMapping gvkExtensionMapping = new GvkExtensionMapping(); private final GvkExtensionMapping gvkExtensionMapping = new GvkExtensionMapping();
private String pluginId; private final String pluginId;
public PluginApplicationContext(String pluginId) {
this.pluginId = pluginId;
}
public String getPluginId() { public String getPluginId() {
return pluginId; return pluginId;
} }
public void setPluginId(String pluginId) {
this.pluginId = pluginId;
}
/** /**
* Gets the gvk-extension mapping. * Gets the gvk-extension mapping.
* It is thread safe * It is thread safe

View File

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

View File

@ -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;
}
}

View File

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

View File

@ -2,29 +2,14 @@ package run.halo.app.plugin;
import static run.halo.app.plugin.resources.BundleResourceUtils.getJsBundleResource; import static run.halo.app.plugin.resources.BundleResourceUtils.getJsBundleResource;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.lang.reflect.Constructor;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Instant; import java.time.Instant;
import lombok.extern.slf4j.Slf4j; 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.PluginManager;
import org.pf4j.PluginRepository;
import org.pf4j.PluginStatusProvider;
import org.pf4j.RuntimeMode;
import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.web.WebProperties; import org.springframework.boot.autoconfigure.web.WebProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.accept.RequestedContentTypeResolver; import org.springframework.web.reactive.accept.RequestedContentTypeResolver;
@ -46,23 +31,11 @@ import run.halo.app.infra.SystemVersionSupplier;
@EnableConfigurationProperties(PluginProperties.class) @EnableConfigurationProperties(PluginProperties.class)
public class PluginAutoConfiguration { 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 @Bean
public PluginRequestMappingHandlerMapping pluginRequestMappingHandlerMapping() { public PluginRequestMappingHandlerMapping pluginRequestMappingHandlerMapping(
@Qualifier("webFluxContentTypeResolver")
RequestedContentTypeResolver requestedContentTypeResolver
) {
PluginRequestMappingHandlerMapping mapping = new PluginRequestMappingHandlerMapping(); PluginRequestMappingHandlerMapping mapping = new PluginRequestMappingHandlerMapping();
mapping.setContentTypeResolver(requestedContentTypeResolver); mapping.setContentTypeResolver(requestedContentTypeResolver);
mapping.setOrder(-1); mapping.setOrder(-1);
@ -70,113 +43,14 @@ public class PluginAutoConfiguration {
} }
@Bean @Bean
public PluginRequestMappingManager pluginRequestMappingManager() { public PluginManager pluginManager(ApplicationContext context,
return new PluginRequestMappingManager(pluginRequestMappingHandlerMapping()); SystemVersionSupplier systemVersionSupplier,
PluginProperties pluginProperties) {
return new HaloPluginManager(context, pluginProperties, systemVersionSupplier);
} }
@Bean @Bean
public HaloPluginManager pluginManager() { public RouterFunction<ServerResponse> pluginJsBundleRoute(PluginManager 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,
WebProperties webProperties) { WebProperties webProperties) {
var cacheProperties = webProperties.getResources().getCache(); var cacheProperties = webProperties.getResources().getCache();
return RouterFunctions.route() return RouterFunctions.route()
@ -184,7 +58,7 @@ public class PluginAutoConfiguration {
String pluginName = request.pathVariable("name"); String pluginName = request.pathVariable("name");
String fileName = request.pathVariable("resource"); String fileName = request.pathVariable("resource");
var jsBundle = getJsBundleResource(haloPluginManager, pluginName, fileName); var jsBundle = getJsBundleResource(pluginManager, pluginName, fileName);
if (jsBundle == null || !jsBundle.exists()) { if (jsBundle == null || !jsBundle.exists()) {
return ServerResponse.notFound().build(); return ServerResponse.notFound().build();
} }

View File

@ -1,10 +1,15 @@
package run.halo.app.plugin; package run.halo.app.plugin;
import java.time.Duration;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener; import org.springframework.context.event.EventListener;
import org.springframework.lang.NonNull; import org.springframework.lang.NonNull;
import org.springframework.retry.RetryException;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import reactor.util.retry.Retry;
import run.halo.app.extension.GroupVersionKind;
import run.halo.app.extension.ReactiveExtensionClient; import run.halo.app.extension.ReactiveExtensionClient;
import run.halo.app.plugin.event.HaloPluginBeforeStopEvent; import run.halo.app.plugin.event.HaloPluginBeforeStopEvent;
@ -14,6 +19,7 @@ import run.halo.app.plugin.event.HaloPluginBeforeStopEvent;
* @author guqing * @author guqing
* @since 2.0.0 * @since 2.0.0
*/ */
@Slf4j
@Component @Component
public class PluginBeforeStopSyncListener { public class PluginBeforeStopSyncListener {
@ -24,14 +30,17 @@ public class PluginBeforeStopSyncListener {
} }
@EventListener @EventListener
public Mono<Void> onApplicationEvent(@NonNull HaloPluginBeforeStopEvent event) { public void onApplicationEvent(@NonNull HaloPluginBeforeStopEvent event) {
var pluginWrapper = event.getPlugin(); var pluginWrapper = event.getPlugin();
ExtensionContextRegistry registry = ExtensionContextRegistry.getInstance(); var p = pluginWrapper.getPlugin();
if (!registry.containsContext(pluginWrapper.getPluginId())) { if (!(p instanceof SpringPlugin springPlugin)) {
return Mono.empty(); return;
} }
var pluginContext = registry.getByPluginId(pluginWrapper.getPluginId()); var applicationContext = springPlugin.getApplicationContext();
return cleanUpPluginExtensionResources(pluginContext); if (!(applicationContext instanceof PluginApplicationContext pluginApplicationContext)) {
return;
}
cleanUpPluginExtensionResources(pluginApplicationContext).block(Duration.ofMinutes(1));
} }
private Mono<Void> cleanUpPluginExtensionResources(PluginApplicationContext context) { private Mono<Void> cleanUpPluginExtensionResources(PluginApplicationContext context) {
@ -39,7 +48,26 @@ public class PluginBeforeStopSyncListener {
return Flux.fromIterable(gvkExtensionNames.entrySet()) return Flux.fromIterable(gvkExtensionNames.entrySet())
.flatMap(entry -> Flux.fromIterable(entry.getValue()) .flatMap(entry -> Flux.fromIterable(entry.getValue())
.flatMap(extensionName -> client.fetch(entry.getKey(), extensionName)) .flatMap(extensionName -> client.fetch(entry.getKey(), extensionName))
.flatMap(client::delete)) .flatMap(client::delete)
.flatMap(e -> waitForDeleted(e.groupVersionKind(), e.getMetadata().getName())))
.then(); .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);
}
});
}
} }

View File

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

View File

@ -1,58 +1,49 @@
package run.halo.app.plugin; package run.halo.app.plugin;
import java.util.Map; import static org.springframework.core.ResolvableType.forClassWithGenerics;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Stream; import java.util.concurrent.ConcurrentHashMap;
import org.springframework.context.event.EventListener; import org.springframework.context.event.ContextClosedEvent;
import org.springframework.core.ResolvableType; import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.stereotype.Component; import org.springframework.context.event.EventListener;
import run.halo.app.extension.ExtensionClient; import reactor.core.Disposable;
import run.halo.app.extension.controller.ControllerManager; import run.halo.app.extension.ExtensionClient;
import run.halo.app.extension.controller.DefaultControllerManager; import run.halo.app.extension.controller.Controller;
import run.halo.app.extension.controller.Reconciler; import run.halo.app.extension.controller.ControllerBuilder;
import run.halo.app.extension.controller.Reconciler.Request; import run.halo.app.extension.controller.Reconciler;
import run.halo.app.plugin.event.HaloPluginBeforeStopEvent;
import run.halo.app.plugin.event.HaloPluginStartedEvent;
@Component
public class PluginControllerManager { public class PluginControllerManager {
private final Map<String, ControllerManager> controllerManagerMap; private final ConcurrentHashMap<String, Controller> controllers;
private final ExtensionClient client; private final ExtensionClient client;
public PluginControllerManager(ExtensionClient client) { public PluginControllerManager(ExtensionClient client) {
this.client = client; this.client = client;
controllerManagerMap = new ConcurrentHashMap<>(); controllers = new ConcurrentHashMap<>();
} }
@EventListener @EventListener
public void onPluginStarted(HaloPluginStartedEvent event) { public void onApplicationEvent(ContextRefreshedEvent event) {
var plugin = event.getPlugin(); event.getApplicationContext()
.<Reconciler<Reconciler.Request>>getBeanProvider(
var controllerManager = controllerManagerMap.computeIfAbsent(plugin.getPluginId(), forClassWithGenerics(Reconciler.class, Reconciler.Request.class))
id -> new DefaultControllerManager(client)); .orderedStream()
.forEach(this::start);
getReconcilers(plugin.getPluginId())
.forEach(controllerManager::start);
} }
@EventListener @EventListener
public void onPluginBeforeStop(HaloPluginBeforeStopEvent event) { public void onApplicationEvent(ContextClosedEvent event) throws Exception {
// remove controller manager controllers.values()
var plugin = event.getPlugin(); .forEach(Disposable::dispose);
var controllerManager = controllerManagerMap.remove(plugin.getPluginId()); controllers.clear();
if (controllerManager != null) {
// stop all reconcilers
getReconcilers(plugin.getPluginId())
.forEach(controllerManager::stop);
}
} }
private Stream<Reconciler<Request>> getReconcilers(String pluginId) { private void start(Reconciler<Reconciler.Request> reconciler) {
var context = ExtensionContextRegistry.getInstance().getByPluginId(pluginId); var builder = new ControllerBuilder(reconciler, client);
return context.<Reconciler<Request>>getBeanProvider( var controller = reconciler.setupWith(builder);
ResolvableType.forClassWithGenerics(Reconciler.class, Request.class)) controllers.put(reconciler.getClass().getName(), controller);
.orderedStream(); controller.start();
} }
} }

View File

@ -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;
}
}

View File

@ -5,6 +5,7 @@ import static run.halo.app.extension.MetadataUtil.nullSafeAnnotations;
import java.nio.file.Path; import java.nio.file.Path;
import java.time.Duration; import java.time.Duration;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.pf4j.PluginManager;
import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationListener; import org.springframework.context.ApplicationListener;
import org.springframework.dao.OptimisticLockingFailureException; import org.springframework.dao.OptimisticLockingFailureException;
@ -23,22 +24,22 @@ import run.halo.app.extension.ReactiveExtensionClient;
@Component @Component
public class PluginDevelopmentInitializer implements ApplicationListener<ApplicationReadyEvent> { public class PluginDevelopmentInitializer implements ApplicationListener<ApplicationReadyEvent> {
private final HaloPluginManager haloPluginManager; private final PluginManager pluginManager;
private final PluginProperties pluginProperties; private final PluginProperties pluginProperties;
private final ReactiveExtensionClient extensionClient; private final ReactiveExtensionClient extensionClient;
public PluginDevelopmentInitializer(HaloPluginManager haloPluginManager, public PluginDevelopmentInitializer(PluginManager pluginManager,
PluginProperties pluginProperties, ReactiveExtensionClient extensionClient) { PluginProperties pluginProperties, ReactiveExtensionClient extensionClient) {
this.haloPluginManager = haloPluginManager; this.pluginManager = pluginManager;
this.pluginProperties = pluginProperties; this.pluginProperties = pluginProperties;
this.extensionClient = extensionClient; this.extensionClient = extensionClient;
} }
@Override @Override
public void onApplicationEvent(@NonNull ApplicationReadyEvent event) { public void onApplicationEvent(@NonNull ApplicationReadyEvent ignored) {
if (!haloPluginManager.isDevelopment()) { if (!pluginManager.isDevelopment()) {
return; return;
} }
createFixedPluginIfNecessary(); createFixedPluginIfNecessary();

View File

@ -4,7 +4,6 @@ import java.nio.file.Path;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import lombok.Data; import lombok.Data;
import org.pf4j.PluginLoader;
import org.pf4j.RuntimeMode; import org.pf4j.RuntimeMode;
import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConfigurationProperties;
@ -67,8 +66,4 @@ public class PluginProperties {
*/ */
private String pluginsRoot; private String pluginsRoot;
/**
* Allows providing custom plugin loaders.
*/
private Class<PluginLoader> customPluginLoader;
} }

View File

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

View File

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

View File

@ -45,9 +45,14 @@ public class PluginStartedListener {
@EventListener @EventListener
public Mono<Void> onApplicationEvent(HaloPluginStartedEvent event) { public Mono<Void> onApplicationEvent(HaloPluginStartedEvent event) {
var pluginWrapper = event.getPlugin(); var pluginWrapper = event.getPlugin();
var pluginApplicationContext = ExtensionContextRegistry.getInstance() var p = pluginWrapper.getPlugin();
.getByPluginId(pluginWrapper.getPluginId()); if (!(p instanceof SpringPlugin springPlugin)) {
return Mono.empty();
}
var applicationContext = springPlugin.getApplicationContext();
if (!(applicationContext instanceof PluginApplicationContext pluginApplicationContext)) {
return Mono.empty();
}
var pluginName = pluginWrapper.getPluginId(); var pluginName = pluginWrapper.getPluginId();
return client.get(Plugin.class, pluginName) return client.get(Plugin.class, pluginName)

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

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

View File

@ -6,7 +6,6 @@ import java.io.InputStreamReader;
import java.io.Reader; import java.io.Reader;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
@ -15,10 +14,7 @@ import java.util.Set;
import java.util.concurrent.locks.StampedLock; import java.util.concurrent.locks.StampedLock;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.pf4j.AbstractExtensionFinder; import org.pf4j.AbstractExtensionFinder;
import org.pf4j.PluginDependency;
import org.pf4j.PluginManager; import org.pf4j.PluginManager;
import org.pf4j.PluginState;
import org.pf4j.PluginStateEvent;
import org.pf4j.PluginWrapper; import org.pf4j.PluginWrapper;
import org.pf4j.processor.ExtensionStorage; import org.pf4j.processor.ExtensionStorage;
import org.springframework.util.Assert; import org.springframework.util.Assert;
@ -49,67 +45,47 @@ public class SpringComponentsFinder extends AbstractExtensionFinder {
@Override @Override
public Map<String, Set<String>> readPluginsStorages() { 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"); log.debug("Reading components storages from plugins");
Map<String, Set<String>> result = new LinkedHashMap<>(); Map<String, Set<String>> result = new LinkedHashMap<>();
List<PluginWrapper> plugins = pluginManager.getPlugins(); List<PluginWrapper> plugins = pluginManager.getPlugins();
for (PluginWrapper plugin : plugins) { 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; 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 { private void collectExtensions(InputStream inputStream, Set<String> bucket) throws IOException {
try (Reader reader = new InputStreamReader(inputStream, StandardCharsets.UTF_8)) { try (Reader reader = new InputStreamReader(inputStream, StandardCharsets.UTF_8)) {
ExtensionStorage.read(reader, bucket); 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) { protected boolean containsComponentsStorage(String pluginId) {
Assert.notNull(pluginId, "The pluginId cannot be null"); Assert.notNull(pluginId, "The pluginId cannot be null");
long stamp = entryStampedLock.tryOptimisticRead(); long stamp = entryStampedLock.tryOptimisticRead();
@ -125,42 +101,5 @@ public class SpringComponentsFinder extends AbstractExtensionFinder {
return contains; 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);
}
}
} }

View File

@ -3,7 +3,6 @@ package run.halo.app.plugin;
import java.lang.reflect.Constructor; import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.InvocationTargetException;
import java.util.Comparator; import java.util.Comparator;
import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.stream.Stream; import java.util.stream.Stream;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@ -11,7 +10,6 @@ import lombok.extern.slf4j.Slf4j;
import org.pf4j.Extension; import org.pf4j.Extension;
import org.pf4j.ExtensionFactory; import org.pf4j.ExtensionFactory;
import org.pf4j.PluginManager; import org.pf4j.PluginManager;
import org.pf4j.PluginRuntimeException;
import org.pf4j.PluginWrapper; import org.pf4j.PluginWrapper;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory; import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
@ -65,16 +63,9 @@ public class SpringExtensionFactory implements ExtensionFactory {
@Override @Override
@Nullable @Nullable
public <T> T create(Class<T> extensionClass) { public <T> T create(Class<T> extensionClass) {
Optional<PluginApplicationContext> contextOptional = return getPluginApplicationContextBy(extensionClass)
getPluginApplicationContextBy(extensionClass); .map(context -> context.getBean(extensionClass))
if (contextOptional.isPresent()) { .orElseGet(() -> createWithoutSpring(extensionClass));
// 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);
} }
/** /**
@ -127,47 +118,13 @@ public class SpringExtensionFactory implements ExtensionFactory {
return new Object[constructor.getParameterCount()]; return new Object[constructor.getParameterCount()];
} }
protected <T> Optional<PluginApplicationContext> getPluginApplicationContextBy( protected <T> Optional<ApplicationContext> getPluginApplicationContextBy(
final Class<T> extensionClass) { final Class<T> extensionClass) {
return Optional.ofNullable(this.pluginManager.whichPlugin(extensionClass)) return Optional.ofNullable(this.pluginManager.whichPlugin(extensionClass))
.map(PluginWrapper::getPlugin) .map(PluginWrapper::getPlugin)
.map(plugin -> { .filter(SpringPlugin.class::isInstance)
if (plugin instanceof BasePlugin basePlugin) { .map(plugin -> (SpringPlugin) plugin)
return basePlugin; .map(SpringPlugin::getApplicationContext);
}
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";
} }
private <T> String nameOf(final Class<T> clazz) { private <T> String nameOf(final Class<T> clazz) {

View File

@ -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;
}
}

View File

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

View File

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

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -6,6 +6,7 @@ import java.util.Set;
import java.util.stream.Stream; import java.util.stream.Stream;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.pf4j.ExtensionPoint; import org.pf4j.ExtensionPoint;
import org.pf4j.PluginManager;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.lang.NonNull; import org.springframework.lang.NonNull;
@ -15,7 +16,6 @@ import reactor.core.publisher.Mono;
import run.halo.app.extension.ReactiveExtensionClient; import run.halo.app.extension.ReactiveExtensionClient;
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher; import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
import run.halo.app.infra.SystemSetting.ExtensionPointEnabled; import run.halo.app.infra.SystemSetting.ExtensionPointEnabled;
import run.halo.app.plugin.HaloPluginManager;
@Component @Component
@RequiredArgsConstructor @RequiredArgsConstructor
@ -23,7 +23,7 @@ public class DefaultExtensionGetter implements ExtensionGetter {
private final SystemConfigurableEnvironmentFetcher systemConfigFetcher; private final SystemConfigurableEnvironmentFetcher systemConfigFetcher;
private final HaloPluginManager pluginManager; private final PluginManager pluginManager;
private final ApplicationContext applicationContext; private final ApplicationContext applicationContext;

View File

@ -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.GET;
import static org.springframework.web.reactive.function.server.RequestPredicates.accept; import static org.springframework.web.reactive.function.server.RequestPredicates.accept;
import java.util.List;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.pf4j.PluginManager;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.core.io.DefaultResourceLoader; import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader; import org.springframework.core.io.ResourceLoader;
import org.springframework.http.server.PathContainer; import org.springframework.http.server.PathContainer;
import org.springframework.lang.NonNull; import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.web.reactive.function.server.RouterFunction; 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.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse; import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.util.pattern.PathPatternParser; 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;
import run.halo.app.core.extension.ReverseProxy.FileReverseProxyProvider; import run.halo.app.core.extension.ReverseProxy.FileReverseProxyProvider;
import run.halo.app.core.extension.ReverseProxy.ReverseProxyRule; import run.halo.app.core.extension.ReverseProxy.ReverseProxyRule;
import run.halo.app.infra.exception.NotFoundException; import run.halo.app.infra.exception.NotFoundException;
import run.halo.app.infra.utils.PathUtils; 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; import run.halo.app.plugin.PluginConst;
/** /**
@ -44,7 +43,7 @@ import run.halo.app.plugin.PluginConst;
@AllArgsConstructor @AllArgsConstructor
public class ReverseProxyRouterFunctionFactory { public class ReverseProxyRouterFunctionFactory {
private final HaloPluginManager haloPluginManager; private final PluginManager pluginManager;
private final ApplicationContext applicationContext; private final ApplicationContext applicationContext;
/** /**
@ -56,18 +55,17 @@ public class ReverseProxyRouterFunctionFactory {
* @param pluginName plugin name(nullable if system) * @param pluginName plugin name(nullable if system)
* @return A reverse proxy RouterFunction handle(nullable) * @return A reverse proxy RouterFunction handle(nullable)
*/ */
@NonNull @Nullable
public Mono<RouterFunction<ServerResponse>> create(ReverseProxy reverseProxy, public RouterFunction<ServerResponse> create(ReverseProxy reverseProxy, String pluginName) {
String pluginName) {
return createReverseProxyRouterFunction(reverseProxy, nullSafePluginName(pluginName)); return createReverseProxyRouterFunction(reverseProxy, nullSafePluginName(pluginName));
} }
private Mono<RouterFunction<ServerResponse>> createReverseProxyRouterFunction( @Nullable
private RouterFunction<ServerResponse> createReverseProxyRouterFunction(
ReverseProxy reverseProxy, @NonNull String pluginName) { ReverseProxy reverseProxy, @NonNull String pluginName) {
Assert.notNull(reverseProxy, "The reverseProxy must not be null."); Assert.notNull(reverseProxy, "The reverseProxy must not be null.");
var rules = getReverseProxyRules(reverseProxy); var rules = getReverseProxyRules(reverseProxy);
return rules.stream().map(rule -> {
return rules.map(rule -> {
String routePath = buildRoutePath(pluginName, rule); String routePath = buildRoutePath(pluginName, rule);
log.debug("Plugin [{}] registered reverse proxy route path [{}]", pluginName, log.debug("Plugin [{}] registered reverse proxy route path [{}]", pluginName,
routePath); routePath);
@ -81,15 +79,15 @@ public class ReverseProxyRouterFunctionFactory {
return ServerResponse.ok() return ServerResponse.ok()
.bodyValue(resource); .bodyValue(resource);
}); });
}).reduce(RouterFunction::and); }).reduce(RouterFunction::and).orElse(null);
} }
private String nullSafePluginName(String pluginName) { private String nullSafePluginName(String pluginName) {
return pluginName == null ? PluginConst.SYSTEM_PLUGIN_NAME : pluginName; return pluginName == null ? PluginConst.SYSTEM_PLUGIN_NAME : pluginName;
} }
private Flux<ReverseProxyRule> getReverseProxyRules(ReverseProxy reverseProxy) { private List<ReverseProxyRule> getReverseProxyRules(ReverseProxy reverseProxy) {
return Flux.fromIterable(reverseProxy.getRules()); return reverseProxy.getRules();
} }
public static String buildRoutePath(String pluginId, ReverseProxyRule reverseProxyRule) { public static String buildRoutePath(String pluginId, ReverseProxyRule reverseProxyRule) {
@ -137,15 +135,11 @@ public class ReverseProxyRouterFunctionFactory {
} }
private ResourceLoader getResourceLoader(String pluginName) { private ResourceLoader getResourceLoader(String pluginName) {
ExtensionContextRegistry registry = ExtensionContextRegistry.getInstance();
if (registry.containsContext(pluginName)) {
return registry.getByPluginId(pluginName);
}
if (PluginConst.SYSTEM_PLUGIN_NAME.equals(pluginName)) { if (PluginConst.SYSTEM_PLUGIN_NAME.equals(pluginName)) {
return applicationContext; return applicationContext;
} }
DefaultResourceLoader resourceLoader = DefaultResourceLoader resourceLoader =
BundleResourceUtils.getResourceLoader(haloPluginManager, pluginName); BundleResourceUtils.getResourceLoader(pluginManager, pluginName);
if (resourceLoader == null) { if (resourceLoader == null) {
throw new NotFoundException("Plugin [" + pluginName + "] not found."); throw new NotFoundException("Plugin [" + pluginName + "] not found.");
} }

View File

@ -2,7 +2,6 @@ package run.halo.app.plugin.resources;
import com.google.common.collect.LinkedHashMultimap; import com.google.common.collect.LinkedHashMultimap;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.concurrent.locks.StampedLock; import java.util.concurrent.locks.StampedLock;
@ -10,8 +9,8 @@ import org.springframework.stereotype.Component;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.web.reactive.function.server.RouterFunction; import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerResponse; import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;
import run.halo.app.core.extension.ReverseProxy; import run.halo.app.core.extension.ReverseProxy;
import run.halo.app.plugin.PluginRouterFunctionRegistry;
/** /**
* A registry for {@link RouterFunction} of plugin. * A registry for {@link RouterFunction} of plugin.
@ -21,6 +20,9 @@ import run.halo.app.core.extension.ReverseProxy;
*/ */
@Component @Component
public class ReverseProxyRouterFunctionRegistry { public class ReverseProxyRouterFunctionRegistry {
private final PluginRouterFunctionRegistry pluginRouterFunctionRegistry;
private final ReverseProxyRouterFunctionFactory reverseProxyRouterFunctionFactory; private final ReverseProxyRouterFunctionFactory reverseProxyRouterFunctionFactory;
private final StampedLock lock = new StampedLock(); private final StampedLock lock = new StampedLock();
private final Map<String, RouterFunction<ServerResponse>> proxyNameRouterFunctionRegistry = private final Map<String, RouterFunction<ServerResponse>> proxyNameRouterFunctionRegistry =
@ -29,7 +31,9 @@ public class ReverseProxyRouterFunctionRegistry {
LinkedHashMultimap.create(); LinkedHashMultimap.create();
public ReverseProxyRouterFunctionRegistry( public ReverseProxyRouterFunctionRegistry(
PluginRouterFunctionRegistry pluginRouterFunctionRegistry,
ReverseProxyRouterFunctionFactory reverseProxyRouterFunctionFactory) { ReverseProxyRouterFunctionFactory reverseProxyRouterFunctionFactory) {
this.pluginRouterFunctionRegistry = pluginRouterFunctionRegistry;
this.reverseProxyRouterFunctionFactory = reverseProxyRouterFunctionFactory; this.reverseProxyRouterFunctionFactory = reverseProxyRouterFunctionFactory;
} }
@ -38,20 +42,18 @@ public class ReverseProxyRouterFunctionRegistry {
* *
* @param pluginId plugin id * @param pluginId plugin id
* @param reverseProxy reverse proxy * @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."); Assert.notNull(pluginId, "The plugin id must not be null.");
final String proxyName = reverseProxy.getMetadata().getName(); final String proxyName = reverseProxy.getMetadata().getName();
long stamp = lock.writeLock(); long stamp = lock.writeLock();
try { try {
pluginIdReverseProxyMap.put(pluginId, proxyName); pluginIdReverseProxyMap.put(pluginId, proxyName);
return reverseProxyRouterFunctionFactory.create(reverseProxy, pluginId) var routerFunction = reverseProxyRouterFunctionFactory.create(reverseProxy, pluginId);
.map(routerFunction -> { if (routerFunction != null) {
proxyNameRouterFunctionRegistry.put(proxyName, routerFunction); proxyNameRouterFunctionRegistry.put(proxyName, routerFunction);
return routerFunction; pluginRouterFunctionRegistry.register(Set.of(routerFunction));
}) }
.then();
} finally { } finally {
lock.unlockWrite(stamp); lock.unlockWrite(stamp);
} }
@ -60,65 +62,25 @@ public class ReverseProxyRouterFunctionRegistry {
/** /**
* Only for test. * Only for test.
*/ */
protected int reverseProxySize(String pluginId) { int reverseProxySize(String pluginId) {
Set<String> names = pluginIdReverseProxyMap.get(pluginId); Set<String> names = pluginIdReverseProxyMap.get(pluginId);
return names.size(); 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. * 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(); long stamp = lock.writeLock();
try { try {
pluginIdReverseProxyMap.remove(pluginId, reverseProxyName); pluginIdReverseProxyMap.remove(pluginId, reverseProxyName);
proxyNameRouterFunctionRegistry.remove(reverseProxyName); var removedRouterFunction = proxyNameRouterFunctionRegistry.remove(reverseProxyName);
return Mono.empty(); if (removedRouterFunction != null) {
pluginRouterFunctionRegistry.unregister(Set.of(removedRouterFunction));
}
} finally { } finally {
lock.unlockWrite(stamp); 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);
}
}
} }

View File

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

View File

@ -1,17 +1,7 @@
package run.halo.app.theme.finders; package run.halo.app.theme.finders;
import java.util.ArrayList;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext; 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}. * Finder registry for class annotated with {@link Finder}.
@ -19,115 +9,12 @@ import run.halo.app.plugin.event.HaloPluginStoppedEvent;
* @author guqing * @author guqing
* @since 2.0.0 * @since 2.0.0
*/ */
@Component public interface FinderRegistry {
public class FinderRegistry implements InitializingBean {
private final Map<String, List<String>> pluginFindersLookup = new ConcurrentHashMap<>();
private final Map<String, Object> finders = new ConcurrentHashMap<>(64);
private final ApplicationContext applicationContext; Map<String, Object> getFinders();
public FinderRegistry(ApplicationContext applicationContext) { void register(String pluginId, ApplicationContext pluginContext);
this.applicationContext = applicationContext;
}
Object get(String name) { void unregister(String pluginId);
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
*/
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);
}
} }

View File

@ -3,6 +3,7 @@ package run.halo.app.core.extension.reconciler;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.never; import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times; import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
@ -17,7 +18,6 @@ import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks; import org.mockito.InjectMocks;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import reactor.core.publisher.Mono;
import run.halo.app.core.extension.ReverseProxy; import run.halo.app.core.extension.ReverseProxy;
import run.halo.app.extension.ExtensionClient; import run.halo.app.extension.ExtensionClient;
import run.halo.app.extension.Metadata; import run.halo.app.extension.Metadata;
@ -54,7 +54,7 @@ class ReverseProxyReconcilerTest {
.setLabels(Map.of(PluginConst.PLUGIN_NAME_LABEL_NAME, "fake-plugin")); .setLabels(Map.of(PluginConst.PLUGIN_NAME_LABEL_NAME, "fake-plugin"));
reverseProxy.setRules(List.of()); 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")) when(client.fetch(ReverseProxy.class, "fake-reverse-proxy"))
.thenReturn(Optional.of(reverseProxy)); .thenReturn(Optional.of(reverseProxy));
@ -62,7 +62,6 @@ class ReverseProxyReconcilerTest {
verify(routerFunctionRegistry, never()).register(anyString(), any(ReverseProxy.class)); verify(routerFunctionRegistry, never()).register(anyString(), any(ReverseProxy.class));
verify(routerFunctionRegistry, never()).remove(eq("fake-plugin"));
verify(routerFunctionRegistry, times(1)) verify(routerFunctionRegistry, times(1))
.remove(eq("fake-plugin"), eq("fake-reverse-proxy")); .remove(eq("fake-plugin"), eq("fake-reverse-proxy"));
} }

View File

@ -1,11 +1,16 @@
package run.halo.app.infra; package run.halo.app.infra;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;
import com.github.zafarkhaja.semver.Version; import com.github.zafarkhaja.semver.Version;
import java.util.Properties; import java.util.Properties;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; 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; import org.springframework.boot.info.BuildProperties;
/** /**
@ -15,14 +20,14 @@ import org.springframework.boot.info.BuildProperties;
* @since 2.0.0 * @since 2.0.0
*/ */
@ExtendWith(MockitoExtension.class)
class DefaultSystemVersionSupplierTest { class DefaultSystemVersionSupplierTest {
@InjectMocks
private DefaultSystemVersionSupplier systemVersionSupplier; private DefaultSystemVersionSupplier systemVersionSupplier;
@BeforeEach @Mock
void setUp() { ObjectProvider<BuildProperties> buildPropertiesProvider;
systemVersionSupplier = new DefaultSystemVersionSupplier();
}
@Test @Test
void getWhenBuildPropertiesNotSet() { void getWhenBuildPropertiesNotSet() {
@ -34,7 +39,7 @@ class DefaultSystemVersionSupplierTest {
void getWhenBuildPropertiesButVersionIsNull() { void getWhenBuildPropertiesButVersionIsNull() {
Properties properties = new Properties(); Properties properties = new Properties();
BuildProperties buildProperties = new BuildProperties(properties); BuildProperties buildProperties = new BuildProperties(properties);
systemVersionSupplier.setBuildProperties(buildProperties); when(buildPropertiesProvider.getIfUnique()).thenReturn(buildProperties);
Version version = systemVersionSupplier.get(); Version version = systemVersionSupplier.get();
assertThat(version.toString()).isEqualTo("0.0.0"); assertThat(version.toString()).isEqualTo("0.0.0");
@ -45,14 +50,14 @@ class DefaultSystemVersionSupplierTest {
Properties properties = new Properties(); Properties properties = new Properties();
properties.put("version", "2.0.0"); properties.put("version", "2.0.0");
BuildProperties buildProperties = new BuildProperties(properties); BuildProperties buildProperties = new BuildProperties(properties);
systemVersionSupplier.setBuildProperties(buildProperties); when(buildPropertiesProvider.getIfUnique()).thenReturn(buildProperties);
Version version = systemVersionSupplier.get(); Version version = systemVersionSupplier.get();
assertThat(version.toString()).isEqualTo("2.0.0"); assertThat(version.toString()).isEqualTo("2.0.0");
properties.put("version", "2.0.0-SNAPSHOT"); properties.put("version", "2.0.0-SNAPSHOT");
buildProperties = new BuildProperties(properties); buildProperties = new BuildProperties(properties);
systemVersionSupplier.setBuildProperties(buildProperties); when(buildPropertiesProvider.getIfUnique()).thenReturn(buildProperties);
version = systemVersionSupplier.get(); version = systemVersionSupplier.get();
assertThat(version.toString()).isEqualTo("2.0.0-SNAPSHOT"); assertThat(version.toString()).isEqualTo("2.0.0-SNAPSHOT");
assertThat(version.getPreReleaseVersion()).isEqualTo("SNAPSHOT"); assertThat(version.getPreReleaseVersion()).isEqualTo("SNAPSHOT");

View File

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

View File

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

View File

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

View File

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

View File

@ -3,15 +3,9 @@ package run.halo.app.plugin;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy; 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.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.util.Set;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
@ -47,28 +41,6 @@ class SpringComponentsFinderTest {
testFile = ResourceUtils.getFile("classpath:plugin/test-plugin-components.idx"); 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 @Test
void containsPlugin() { void containsPlugin() {
boolean exist = springComponentsFinder.containsComponentsStorage("NotExist"); boolean exist = springComponentsFinder.containsComponentsStorage("NotExist");
@ -78,15 +50,4 @@ class SpringComponentsFinderTest {
.hasMessage("The pluginId cannot be null"); .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();
}
} }

View File

@ -1,5 +1,7 @@
package run.halo.app.plugin.resources; package run.halo.app.plugin.resources;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -7,11 +9,10 @@ import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks; import org.mockito.InjectMocks;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import org.pf4j.PluginManager;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import reactor.test.StepVerifier;
import run.halo.app.core.extension.ReverseProxy; import run.halo.app.core.extension.ReverseProxy;
import run.halo.app.extension.Metadata; import run.halo.app.extension.Metadata;
import run.halo.app.plugin.HaloPluginManager;
import run.halo.app.plugin.PluginConst; import run.halo.app.plugin.PluginConst;
/** /**
@ -24,7 +25,7 @@ import run.halo.app.plugin.PluginConst;
class ReverseProxyRouterFunctionFactoryTest { class ReverseProxyRouterFunctionFactoryTest {
@Mock @Mock
private HaloPluginManager haloPluginManager; private PluginManager pluginManager;
@Mock @Mock
private ApplicationContext applicationContext; private ApplicationContext applicationContext;
@ -34,11 +35,8 @@ class ReverseProxyRouterFunctionFactoryTest {
@Test @Test
void create() { void create() {
var routerFunction = var routerFunction = reverseProxyRouterFunctionFactory.create(mockReverseProxy(), "fakeA");
reverseProxyRouterFunctionFactory.create(mockReverseProxy(), "fakeA"); assertNotNull(routerFunction);
StepVerifier.create(routerFunction)
.expectNextCount(1)
.verifyComplete();
} }
private ReverseProxy mockReverseProxy() { private ReverseProxy mockReverseProxy() {

View File

@ -6,8 +6,6 @@ import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; 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.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks; 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.RouterFunction;
import org.springframework.web.reactive.function.server.ServerResponse; import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import run.halo.app.core.extension.ReverseProxy; import run.halo.app.core.extension.ReverseProxy;
import run.halo.app.extension.Metadata; import run.halo.app.extension.Metadata;
import run.halo.app.plugin.ExtensionContextRegistry; import run.halo.app.plugin.PluginRouterFunctionRegistry;
import run.halo.app.plugin.PluginApplicationContext;
/** /**
* Tests for {@link ReverseProxyRouterFunctionRegistry}. * Tests for {@link ReverseProxyRouterFunctionRegistry}.
@ -33,61 +29,35 @@ import run.halo.app.plugin.PluginApplicationContext;
class ReverseProxyRouterFunctionRegistryTest { class ReverseProxyRouterFunctionRegistryTest {
@InjectMocks @InjectMocks
private ReverseProxyRouterFunctionRegistry registry; ReverseProxyRouterFunctionRegistry registry;
@Mock @Mock
private ReverseProxyRouterFunctionFactory reverseProxyRouterFunctionFactory; ReverseProxyRouterFunctionFactory reverseProxyRouterFunctionFactory;
@BeforeEach @Mock
void setUp() { PluginRouterFunctionRegistry pluginRouterFunctionRegistry;
ExtensionContextRegistry instance = ExtensionContextRegistry.getInstance();
instance.register("fake-plugin", Mockito.mock(PluginApplicationContext.class));
}
@AfterEach
void tearDown() {
ExtensionContextRegistry.getInstance().remove("fake-plugin");
}
@Test @Test
void register() { void register() {
ReverseProxy mock = getMockReverseProxy(); ReverseProxy mock = getMockReverseProxy();
registry.register("fake-plugin", mock) registry.register("fake-plugin", mock);
.as(StepVerifier::create)
.verifyComplete();
assertThat(registry.reverseProxySize("fake-plugin")).isEqualTo(1); assertThat(registry.reverseProxySize("fake-plugin")).isEqualTo(1);
// repeat register a same reverse proxy // repeat register a same reverse proxy
registry.register("fake-plugin", mock) registry.register("fake-plugin", mock);
.as(StepVerifier::create)
.verifyComplete();
assertThat(registry.reverseProxySize("fake-plugin")).isEqualTo(1); assertThat(registry.reverseProxySize("fake-plugin")).isEqualTo(1);
verify(reverseProxyRouterFunctionFactory, times(2)).create(any(), any()); 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 @Test
void removeByKeyValue() { void removeByKeyValue() {
ReverseProxy mock = getMockReverseProxy(); ReverseProxy mock = getMockReverseProxy();
registry.register("fake-plugin", mock) registry.register("fake-plugin", mock);
.as(StepVerifier::create)
.verifyComplete();
registry.remove("fake-plugin", "test-reverse-proxy").block(); registry.remove("fake-plugin", "test-reverse-proxy");
assertThat(registry.reverseProxySize("fake-plugin")).isEqualTo(0); assertThat(registry.reverseProxySize("fake-plugin")).isEqualTo(0);
} }
@ -100,7 +70,7 @@ class ReverseProxyRouterFunctionRegistryTest {
RouterFunction<ServerResponse> routerFunction = request -> Mono.empty(); RouterFunction<ServerResponse> routerFunction = request -> Mono.empty();
when(reverseProxyRouterFunctionFactory.create(any(), any())) when(reverseProxyRouterFunctionFactory.create(any(), any()))
.thenReturn(Mono.just(routerFunction)); .thenReturn(routerFunction);
return mock; return mock;
} }
} }

View File

@ -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.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.when;
import java.util.Map; import java.util.Map;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import org.pf4j.PluginWrapper;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import run.halo.app.plugin.event.HaloPluginStoppedEvent;
/** /**
* Tests for {@link FinderRegistry}. * Tests for {@link FinderRegistry}.
@ -24,29 +20,29 @@ import run.halo.app.plugin.event.HaloPluginStoppedEvent;
@ExtendWith(MockitoExtension.class) @ExtendWith(MockitoExtension.class)
class FinderRegistryTest { class FinderRegistryTest {
private FinderRegistry finderRegistry; private DefaultFinderRegistry finderRegistry;
@Mock @Mock
private ApplicationContext applicationContext; private ApplicationContext applicationContext;
@BeforeEach @BeforeEach
void setUp() { void setUp() {
finderRegistry = new FinderRegistry(applicationContext); finderRegistry = new DefaultFinderRegistry(applicationContext);
} }
@Test @Test
void registerFinder() { void registerFinder() {
assertThatThrownBy(() -> { assertThatThrownBy(() -> {
finderRegistry.registerFinder(new Object()); finderRegistry.putFinder(new Object());
}).isInstanceOf(IllegalStateException.class) }).isInstanceOf(IllegalStateException.class)
.hasMessage("Finder must be annotated with @Finder"); .hasMessage("Finder must be annotated with @Finder");
String s = finderRegistry.registerFinder(new FakeFinder()); String s = finderRegistry.putFinder(new FakeFinder());
assertThat(s).isEqualTo("test"); assertThat(s).isEqualTo("test");
} }
@Test @Test
void removeFinder() { void removeFinder() {
String s = finderRegistry.registerFinder(new FakeFinder()); String s = finderRegistry.putFinder(new FakeFinder());
assertThat(s).isEqualTo("test"); assertThat(s).isEqualTo("test");
Object test = finderRegistry.get("test"); Object test = finderRegistry.get("test");
assertThat(test).isNotNull(); assertThat(test).isNotNull();
@ -60,25 +56,11 @@ class FinderRegistryTest {
void getFinders() { void getFinders() {
assertThat(finderRegistry.getFinders()).hasSize(0); assertThat(finderRegistry.getFinders()).hasSize(0);
finderRegistry.registerFinder(new FakeFinder()); finderRegistry.putFinder(new FakeFinder());
Map<String, Object> finders = finderRegistry.getFinders(); Map<String, Object> finders = finderRegistry.getFinders();
assertThat(finders).hasSize(1); 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") @Finder("test")
static class FakeFinder { static class FakeFinder {