mirror of https://github.com/halo-dev/halo
feat: add shared application context for plugin (#2174)
* feat: add shared application context for plugin * chore: remove todo notes * fix: router function compositepull/2180/head
parent
7cd1282ad3
commit
b9e5ed2f4c
|
@ -1,17 +1,6 @@
|
||||||
package run.halo.app.plugin;
|
package run.halo.app.plugin;
|
||||||
|
|
||||||
import java.lang.reflect.Field;
|
|
||||||
import java.util.Set;
|
|
||||||
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
|
|
||||||
import org.springframework.context.ApplicationEvent;
|
|
||||||
import org.springframework.context.PayloadApplicationEvent;
|
|
||||||
import org.springframework.context.event.ApplicationEventMulticaster;
|
|
||||||
import org.springframework.context.support.AbstractApplicationContext;
|
|
||||||
import org.springframework.context.support.GenericApplicationContext;
|
import org.springframework.context.support.GenericApplicationContext;
|
||||||
import org.springframework.core.ResolvableType;
|
|
||||||
import org.springframework.lang.NonNull;
|
|
||||||
import org.springframework.lang.Nullable;
|
|
||||||
import org.springframework.util.Assert;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The generic IOC container for plugins.
|
* The generic IOC container for plugins.
|
||||||
|
@ -25,56 +14,6 @@ public class PluginApplicationContext extends GenericApplicationContext {
|
||||||
|
|
||||||
private String pluginId;
|
private String pluginId;
|
||||||
|
|
||||||
/**
|
|
||||||
* <p>覆盖父类方法中判断context parent不为空时使用parent context广播事件的逻辑。
|
|
||||||
* 如果主应用桥接事件到插件中且设置了parent会导致发布事件时死循环.</p>
|
|
||||||
*
|
|
||||||
* @param event the event to publish (may be an {@link ApplicationEvent} or a payload object
|
|
||||||
* to be turned into a {@link PayloadApplicationEvent})
|
|
||||||
* @param eventType the resolved event type, if known
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected void publishEvent(@NonNull Object event, @Nullable ResolvableType eventType) {
|
|
||||||
Assert.notNull(event, "Event must not be null");
|
|
||||||
|
|
||||||
// Decorate event as an ApplicationEvent if necessary
|
|
||||||
ApplicationEvent applicationEvent;
|
|
||||||
if (event instanceof ApplicationEvent) {
|
|
||||||
applicationEvent = (ApplicationEvent) event;
|
|
||||||
} else {
|
|
||||||
applicationEvent = new PayloadApplicationEvent<>(this, event);
|
|
||||||
if (eventType == null) {
|
|
||||||
eventType = ((PayloadApplicationEvent<?>) applicationEvent).getResolvableType();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Multicast right now if possible - or lazily once the multicaster is initialized
|
|
||||||
Set<ApplicationEvent> earlyApplicationEvents = getEarlyApplicationEvents();
|
|
||||||
if (earlyApplicationEvents != null) {
|
|
||||||
earlyApplicationEvents.add(applicationEvent);
|
|
||||||
} else {
|
|
||||||
getApplicationEventMulticaster().multicastEvent(applicationEvent, eventType);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private ApplicationEventMulticaster getApplicationEventMulticaster() {
|
|
||||||
ConfigurableListableBeanFactory beanFactory = getBeanFactory();
|
|
||||||
return beanFactory.getBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME,
|
|
||||||
ApplicationEventMulticaster.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
protected Set<ApplicationEvent> getEarlyApplicationEvents() {
|
|
||||||
try {
|
|
||||||
Field earlyApplicationEventsField =
|
|
||||||
AbstractApplicationContext.class.getDeclaredField("earlyApplicationEvents");
|
|
||||||
return (Set<ApplicationEvent>) earlyApplicationEventsField.get(this);
|
|
||||||
} catch (NoSuchFieldException | IllegalAccessException e) {
|
|
||||||
// ignore this exception
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getPluginId() {
|
public String getPluginId() {
|
||||||
return pluginId;
|
return pluginId;
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,9 +24,12 @@ public class PluginApplicationInitializer {
|
||||||
protected final HaloPluginManager haloPluginManager;
|
protected final HaloPluginManager haloPluginManager;
|
||||||
|
|
||||||
private final ExtensionContextRegistry contextRegistry = ExtensionContextRegistry.getInstance();
|
private final ExtensionContextRegistry contextRegistry = ExtensionContextRegistry.getInstance();
|
||||||
|
private final SharedApplicationContextHolder sharedApplicationContextHolder;
|
||||||
|
|
||||||
public PluginApplicationInitializer(HaloPluginManager springPluginManager) {
|
public PluginApplicationInitializer(HaloPluginManager springPluginManager) {
|
||||||
this.haloPluginManager = springPluginManager;
|
this.haloPluginManager = springPluginManager;
|
||||||
|
sharedApplicationContextHolder = springPluginManager.getRootApplicationContext()
|
||||||
|
.getBean(SharedApplicationContextHolder.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ApplicationContext getRootApplicationContext() {
|
public ApplicationContext getRootApplicationContext() {
|
||||||
|
@ -40,7 +43,11 @@ public class PluginApplicationInitializer {
|
||||||
StopWatch stopWatch = new StopWatch("initialize-plugin-context");
|
StopWatch stopWatch = new StopWatch("initialize-plugin-context");
|
||||||
stopWatch.start("Create PluginApplicationContext");
|
stopWatch.start("Create PluginApplicationContext");
|
||||||
PluginApplicationContext pluginApplicationContext = new PluginApplicationContext();
|
PluginApplicationContext pluginApplicationContext = new PluginApplicationContext();
|
||||||
pluginApplicationContext.setParent(getRootApplicationContext());
|
|
||||||
|
if (sharedApplicationContextHolder != null) {
|
||||||
|
pluginApplicationContext.setParent(sharedApplicationContextHolder.getInstance());
|
||||||
|
}
|
||||||
|
|
||||||
pluginApplicationContext.setClassLoader(pluginClassLoader);
|
pluginApplicationContext.setClassLoader(pluginClassLoader);
|
||||||
// populate plugin to plugin application context
|
// populate plugin to plugin application context
|
||||||
pluginApplicationContext.setPluginId(pluginId);
|
pluginApplicationContext.setPluginId(pluginId);
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package run.halo.app.plugin;
|
package run.halo.app.plugin;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
@ -75,17 +76,19 @@ public class PluginCompositeRouterFunction implements RouterFunction<ServerRespo
|
||||||
RouterFunction<ServerResponse> reverseProxyRouterFunction =
|
RouterFunction<ServerResponse> reverseProxyRouterFunction =
|
||||||
reverseProxyRouterFunctionFactory.create(pluginApplicationContext);
|
reverseProxyRouterFunctionFactory.create(pluginApplicationContext);
|
||||||
|
|
||||||
routerFunctions(pluginApplicationContext)
|
List<RouterFunction<ServerResponse>> routerFunctions =
|
||||||
.stream()
|
routerFunctions(pluginApplicationContext);
|
||||||
.reduce(RouterFunction::and)
|
|
||||||
.map(compositeRouterFunction -> {
|
List<RouterFunction<ServerResponse>> combinedRouterFunctions =
|
||||||
|
new ArrayList<>(routerFunctions);
|
||||||
if (reverseProxyRouterFunction != null) {
|
if (reverseProxyRouterFunction != null) {
|
||||||
compositeRouterFunction.andOther(reverseProxyRouterFunction);
|
combinedRouterFunctions.add(reverseProxyRouterFunction);
|
||||||
}
|
}
|
||||||
return compositeRouterFunction;
|
|
||||||
})
|
combinedRouterFunctions.stream()
|
||||||
.ifPresent(routerFunction -> {
|
.reduce(RouterFunction::and)
|
||||||
routerFunctionRegistry.put(plugin.getPluginId(), routerFunction);
|
.ifPresent(compositeRouterFunction -> {
|
||||||
|
routerFunctionRegistry.put(plugin.getPluginId(), compositeRouterFunction);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -98,9 +101,6 @@ public class PluginCompositeRouterFunction implements RouterFunction<ServerRespo
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
private List<RouterFunction<ServerResponse>> routerFunctions(
|
private List<RouterFunction<ServerResponse>> routerFunctions(
|
||||||
PluginApplicationContext applicationContext) {
|
PluginApplicationContext applicationContext) {
|
||||||
// TODO: Since the parent of the ApplicationContext of the plugin is RootApplicationContext
|
|
||||||
// obtaining the RouterFunction here will obtain the existing in the parent
|
|
||||||
// resulting in a loop when there is no matching route
|
|
||||||
List<RouterFunction<ServerResponse>> functions =
|
List<RouterFunction<ServerResponse>> functions =
|
||||||
applicationContext.getBeanProvider(RouterFunction.class)
|
applicationContext.getBeanProvider(RouterFunction.class)
|
||||||
.orderedStream()
|
.orderedStream()
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
package run.halo.app.plugin;
|
||||||
|
|
||||||
|
import org.springframework.context.ApplicationContext;
|
||||||
|
import org.springframework.context.support.GenericApplicationContext;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>An {@link ApplicationContext} implementation shared by plugins.</p>
|
||||||
|
* <p>Beans in the Core that need to be shared with plugins will be injected into this
|
||||||
|
* {@link SharedApplicationContext}.</p>
|
||||||
|
*
|
||||||
|
* @author guqing
|
||||||
|
* @since 2.0.0
|
||||||
|
*/
|
||||||
|
public class SharedApplicationContext extends GenericApplicationContext {
|
||||||
|
}
|
|
@ -0,0 +1,64 @@
|
||||||
|
package run.halo.app.plugin;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
|
||||||
|
import org.springframework.context.ApplicationContext;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import run.halo.app.extension.DefaultSchemeManager;
|
||||||
|
import run.halo.app.extension.ExtensionClient;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <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
|
||||||
|
ExtensionClient extensionClient = rootApplicationContext.getBean(ExtensionClient.class);
|
||||||
|
beanFactory.registerSingleton("extensionClient", extensionClient);
|
||||||
|
DefaultSchemeManager defaultSchemeManager =
|
||||||
|
rootApplicationContext.getBean(DefaultSchemeManager.class);
|
||||||
|
beanFactory.registerSingleton("schemeManager", defaultSchemeManager);
|
||||||
|
// TODO add more shared instance here
|
||||||
|
|
||||||
|
return sharedApplicationContext;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue