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;
|
||||
|
||||
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.core.ResolvableType;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* The generic IOC container for plugins.
|
||||
|
@ -25,56 +14,6 @@ public class PluginApplicationContext extends GenericApplicationContext {
|
|||
|
||||
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() {
|
||||
return pluginId;
|
||||
}
|
||||
|
|
|
@ -24,9 +24,12 @@ public class PluginApplicationInitializer {
|
|||
protected final HaloPluginManager haloPluginManager;
|
||||
|
||||
private final ExtensionContextRegistry contextRegistry = ExtensionContextRegistry.getInstance();
|
||||
private final SharedApplicationContextHolder sharedApplicationContextHolder;
|
||||
|
||||
public PluginApplicationInitializer(HaloPluginManager springPluginManager) {
|
||||
this.haloPluginManager = springPluginManager;
|
||||
sharedApplicationContextHolder = springPluginManager.getRootApplicationContext()
|
||||
.getBean(SharedApplicationContextHolder.class);
|
||||
}
|
||||
|
||||
public ApplicationContext getRootApplicationContext() {
|
||||
|
@ -40,7 +43,11 @@ public class PluginApplicationInitializer {
|
|||
StopWatch stopWatch = new StopWatch("initialize-plugin-context");
|
||||
stopWatch.start("Create PluginApplicationContext");
|
||||
PluginApplicationContext pluginApplicationContext = new PluginApplicationContext();
|
||||
pluginApplicationContext.setParent(getRootApplicationContext());
|
||||
|
||||
if (sharedApplicationContextHolder != null) {
|
||||
pluginApplicationContext.setParent(sharedApplicationContextHolder.getInstance());
|
||||
}
|
||||
|
||||
pluginApplicationContext.setClassLoader(pluginClassLoader);
|
||||
// populate plugin to plugin application context
|
||||
pluginApplicationContext.setPluginId(pluginId);
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package run.halo.app.plugin;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
@ -75,17 +76,19 @@ public class PluginCompositeRouterFunction implements RouterFunction<ServerRespo
|
|||
RouterFunction<ServerResponse> reverseProxyRouterFunction =
|
||||
reverseProxyRouterFunctionFactory.create(pluginApplicationContext);
|
||||
|
||||
routerFunctions(pluginApplicationContext)
|
||||
.stream()
|
||||
.reduce(RouterFunction::and)
|
||||
.map(compositeRouterFunction -> {
|
||||
List<RouterFunction<ServerResponse>> routerFunctions =
|
||||
routerFunctions(pluginApplicationContext);
|
||||
|
||||
List<RouterFunction<ServerResponse>> combinedRouterFunctions =
|
||||
new ArrayList<>(routerFunctions);
|
||||
if (reverseProxyRouterFunction != null) {
|
||||
compositeRouterFunction.andOther(reverseProxyRouterFunction);
|
||||
combinedRouterFunctions.add(reverseProxyRouterFunction);
|
||||
}
|
||||
return compositeRouterFunction;
|
||||
})
|
||||
.ifPresent(routerFunction -> {
|
||||
routerFunctionRegistry.put(plugin.getPluginId(), routerFunction);
|
||||
|
||||
combinedRouterFunctions.stream()
|
||||
.reduce(RouterFunction::and)
|
||||
.ifPresent(compositeRouterFunction -> {
|
||||
routerFunctionRegistry.put(plugin.getPluginId(), compositeRouterFunction);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -98,9 +101,6 @@ public class PluginCompositeRouterFunction implements RouterFunction<ServerRespo
|
|||
@SuppressWarnings("unchecked")
|
||||
private List<RouterFunction<ServerResponse>> routerFunctions(
|
||||
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 =
|
||||
applicationContext.getBeanProvider(RouterFunction.class)
|
||||
.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