feat: add shared application context for plugin (#2174)

* feat: add shared application context for plugin

* chore: remove todo notes

* fix: router function composite
pull/2180/head
guqing 2022-06-21 15:34:25 +08:00 committed by GitHub
parent 7cd1282ad3
commit b9e5ed2f4c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 137 additions and 75 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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