diff --git a/application/src/main/java/run/halo/app/plugin/DefaultPluginApplicationContextFactory.java b/application/src/main/java/run/halo/app/plugin/DefaultPluginApplicationContextFactory.java index 6ec14f8de..c4cc430f0 100644 --- a/application/src/main/java/run/halo/app/plugin/DefaultPluginApplicationContextFactory.java +++ b/application/src/main/java/run/halo/app/plugin/DefaultPluginApplicationContextFactory.java @@ -14,7 +14,6 @@ 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; @@ -62,16 +61,15 @@ public class DefaultPluginApplicationContextFactory implements PluginApplication @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); + var context = new PluginApplicationContext(pluginId, pluginManager); context.setBeanNameGenerator(DefaultBeanNameGenerator.INSTANCE); context.registerShutdownHook(); context.setParent(pluginManager.getSharedContext()); + var pluginWrapper = pluginManager.getPlugin(pluginId); var classLoader = pluginWrapper.getPluginClassLoader(); var resourceLoader = new DefaultResourceLoader(classLoader); context.setResourceLoader(resourceLoader); @@ -140,14 +138,6 @@ public class DefaultPluginApplicationContextFactory implements PluginApplication ); }); - rootContext.getBeanProvider(SharedEventListenerRegistry.class) - .ifUnique(listenerRegistry -> { - var shareEventListenerAdapter = new ShareEventListenerAdapter(listenerRegistry); - beanFactory.registerSingleton( - "shareEventListenerAdapter", - shareEventListenerAdapter - ); - }); sw.stop(); sw.start("LoadComponents"); @@ -177,31 +167,6 @@ public class DefaultPluginApplicationContextFactory implements PluginApplication return context; } - private static class ShareEventListenerAdapter { - - private final SharedEventListenerRegistry listenerRegistry; - - private ApplicationListener 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; diff --git a/application/src/main/java/run/halo/app/plugin/DefaultSharedEventListenerRegistry.java b/application/src/main/java/run/halo/app/plugin/DefaultSharedEventListenerRegistry.java deleted file mode 100644 index 28ab80841..000000000 --- a/application/src/main/java/run/halo/app/plugin/DefaultSharedEventListenerRegistry.java +++ /dev/null @@ -1,34 +0,0 @@ -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, SharedEventListenerRegistry { - - private final List> 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 listener) { - this.listeners.add(listener); - } - - public void unregister(ApplicationListener listener) { - this.listeners.remove(listener); - } -} diff --git a/application/src/main/java/run/halo/app/plugin/HaloSharedEventDelegator.java b/application/src/main/java/run/halo/app/plugin/HaloSharedEventDelegator.java new file mode 100644 index 000000000..189d7eb0c --- /dev/null +++ b/application/src/main/java/run/halo/app/plugin/HaloSharedEventDelegator.java @@ -0,0 +1,40 @@ +package run.halo.app.plugin; + +import java.util.Objects; +import lombok.Getter; +import org.springframework.context.ApplicationEvent; + +/** + * The event that delegates a shared event in core into all started plugins. + * + * @author johnniang + * @since 2.17 + */ +@Getter +class HaloSharedEventDelegator extends ApplicationEvent { + + private final ApplicationEvent delegate; + + public HaloSharedEventDelegator(Object source, ApplicationEvent delegate) { + super(source); + this.delegate = delegate; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + HaloSharedEventDelegator that = (HaloSharedEventDelegator) o; + return Objects.equals(delegate, that.delegate); + } + + @Override + public int hashCode() { + return Objects.hashCode(delegate); + } +} diff --git a/application/src/main/java/run/halo/app/plugin/PluginApplicationContext.java b/application/src/main/java/run/halo/app/plugin/PluginApplicationContext.java index 8352cec33..19b01ec1d 100644 --- a/application/src/main/java/run/halo/app/plugin/PluginApplicationContext.java +++ b/application/src/main/java/run/halo/app/plugin/PluginApplicationContext.java @@ -2,7 +2,10 @@ package run.halo.app.plugin; import java.util.List; import java.util.concurrent.locks.StampedLock; +import org.springframework.context.ApplicationEvent; import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.core.ResolvableType; +import org.springframework.core.annotation.AnnotationUtils; import org.springframework.util.Assert; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; @@ -22,8 +25,11 @@ public class PluginApplicationContext extends AnnotationConfigApplicationContext private final String pluginId; - public PluginApplicationContext(String pluginId) { + private final SpringPluginManager pluginManager; + + public PluginApplicationContext(String pluginId, SpringPluginManager pluginManager) { this.pluginId = pluginId; + this.pluginManager = pluginManager; } public String getPluginId() { @@ -105,6 +111,23 @@ public class PluginApplicationContext extends AnnotationConfigApplicationContext } } + @Override + protected void publishEvent(Object event, ResolvableType typeHint) { + if (event instanceof ApplicationEvent applicationEvent + && AnnotationUtils.findAnnotation(event.getClass(), SharedEvent.class) != null) { + // publish event via root context + var delegateEvent = new PluginSharedEventDelegator(this, applicationEvent); + pluginManager.getRootContext().publishEvent(delegateEvent); + return; + } + // unwrap event if needed + var originalEvent = event; + if (event instanceof HaloSharedEventDelegator delegator) { + originalEvent = delegator.getDelegate(); + } + super.publishEvent(originalEvent, typeHint); + } + @Override protected void onClose() { // For subclasses: do nothing by default. diff --git a/application/src/main/java/run/halo/app/plugin/PluginSharedEventDelegator.java b/application/src/main/java/run/halo/app/plugin/PluginSharedEventDelegator.java new file mode 100644 index 000000000..6ed66f273 --- /dev/null +++ b/application/src/main/java/run/halo/app/plugin/PluginSharedEventDelegator.java @@ -0,0 +1,44 @@ +package run.halo.app.plugin; + +import java.util.Objects; +import lombok.Getter; +import org.springframework.context.ApplicationEvent; +import org.springframework.lang.NonNull; + +/** + * The event that delegates to another shared event published by a plugin. + * + * @author johnniang + * @since 2.17 + */ +@Getter +class PluginSharedEventDelegator extends ApplicationEvent { + + /** + * The delegate event. + */ + private final ApplicationEvent delegate; + + public PluginSharedEventDelegator(@NonNull Object source, @NonNull ApplicationEvent delegate) { + super(source); + this.delegate = delegate; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + PluginSharedEventDelegator that = (PluginSharedEventDelegator) o; + return Objects.equals(delegate, that.delegate); + } + + @Override + public int hashCode() { + return Objects.hashCode(delegate); + } +} diff --git a/application/src/main/java/run/halo/app/plugin/SharedEventDispatcher.java b/application/src/main/java/run/halo/app/plugin/SharedEventDispatcher.java new file mode 100644 index 000000000..5c342f3f2 --- /dev/null +++ b/application/src/main/java/run/halo/app/plugin/SharedEventDispatcher.java @@ -0,0 +1,50 @@ +package run.halo.app.plugin; + +import java.util.ArrayList; +import org.pf4j.PluginManager; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.Lifecycle; +import org.springframework.context.event.EventListener; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.stereotype.Component; + +@Component +public class SharedEventDispatcher { + + private final PluginManager pluginManager; + + private final ApplicationEventPublisher publisher; + + public SharedEventDispatcher(PluginManager pluginManager, ApplicationEventPublisher publisher) { + this.pluginManager = pluginManager; + this.publisher = publisher; + } + + @EventListener(ApplicationEvent.class) + void onApplicationEvent(ApplicationEvent event) { + if (AnnotationUtils.findAnnotation(event.getClass(), SharedEvent.class) == null) { + return; + } + // we should copy the plugins list to avoid ConcurrentModificationException + var startedPlugins = new ArrayList<>(pluginManager.getStartedPlugins()); + // broadcast event to all started plugins except the publisher + for (var startedPlugin : startedPlugins) { + var plugin = startedPlugin.getPlugin(); + if (!(plugin instanceof SpringPlugin springPlugin)) { + continue; + } + var context = springPlugin.getApplicationContext(); + // make sure the context is running before publishing the event + if (context instanceof Lifecycle lifecycle && lifecycle.isRunning()) { + context.publishEvent(new HaloSharedEventDelegator(this, event)); + } + } + } + + @EventListener(PluginSharedEventDelegator.class) + void onApplicationEvent(PluginSharedEventDelegator event) { + publisher.publishEvent(event.getDelegate()); + } + +} diff --git a/application/src/main/java/run/halo/app/plugin/SharedEventListenerRegistry.java b/application/src/main/java/run/halo/app/plugin/SharedEventListenerRegistry.java deleted file mode 100644 index 8ad73ce44..000000000 --- a/application/src/main/java/run/halo/app/plugin/SharedEventListenerRegistry.java +++ /dev/null @@ -1,12 +0,0 @@ -package run.halo.app.plugin; - -import org.springframework.context.ApplicationEvent; -import org.springframework.context.ApplicationListener; - -public interface SharedEventListenerRegistry { - - void register(ApplicationListener listener); - - void unregister(ApplicationListener listener); - -} diff --git a/application/src/test/java/run/halo/app/plugin/SharedEventDispatcherTest.java b/application/src/test/java/run/halo/app/plugin/SharedEventDispatcherTest.java new file mode 100644 index 000000000..53cc238b5 --- /dev/null +++ b/application/src/test/java/run/halo/app/plugin/SharedEventDispatcherTest.java @@ -0,0 +1,108 @@ +package run.halo.app.plugin; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.mockito.Mockito.withSettings; + +import java.util.List; +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.pf4j.PluginManager; +import org.pf4j.PluginWrapper; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.Lifecycle; + +@ExtendWith(MockitoExtension.class) +class SharedEventDispatcherTest { + + @Mock + PluginManager pluginManager; + + @Mock + ApplicationEventPublisher publisher; + + @InjectMocks + SharedEventDispatcher dispatcher; + + @Test + void shouldNotDispatchEventIfNotSharedEvent() { + dispatcher.onApplicationEvent(new FakeEvent(this)); + verify(pluginManager, never()).getStartedPlugins(); + } + + @Test + void shouldDispatchEventToAllStartedPlugins() { + var pw = mock(PluginWrapper.class); + var plugin = mock(SpringPlugin.class); + var context = + mock(ApplicationContext.class, withSettings().extraInterfaces(Lifecycle.class)); + when(((Lifecycle) context).isRunning()).thenReturn(true); + when(plugin.getApplicationContext()).thenReturn(context); + when(pw.getPlugin()).thenReturn(plugin); + when(pluginManager.getStartedPlugins()).thenReturn(List.of(pw)); + + var event = new FakeSharedEvent(this); + dispatcher.onApplicationEvent(event); + + verify(context).publishEvent(new HaloSharedEventDelegator(dispatcher, event)); + } + + @Test + void shouldNotDispatchEventToAllStartedPluginsWhilePluginContextIsNotRunning() { + var pw = mock(PluginWrapper.class); + var plugin = mock(SpringPlugin.class); + var context = + mock(ApplicationContext.class, withSettings().extraInterfaces(Lifecycle.class)); + when(((Lifecycle) context).isRunning()).thenReturn(false); + when(plugin.getApplicationContext()).thenReturn(context); + when(pw.getPlugin()).thenReturn(plugin); + when(pluginManager.getStartedPlugins()).thenReturn(List.of(pw)); + var event = new FakeSharedEvent(this); + dispatcher.onApplicationEvent(event); + verify(context, never()).publishEvent(event); + } + + @Test + void shouldNotDispatchEventToAllStartedPluginsWhilePluginContextIsNotLifecycle() { + var pw = mock(PluginWrapper.class); + var plugin = mock(SpringPlugin.class); + var context = mock(ApplicationContext.class); + when(plugin.getApplicationContext()).thenReturn(context); + when(pw.getPlugin()).thenReturn(plugin); + when(pluginManager.getStartedPlugins()).thenReturn(List.of(pw)); + var event = new FakeSharedEvent(this); + dispatcher.onApplicationEvent(event); + verify(context, never()).publishEvent(event); + } + + @Test + void shouldUnwrapPluginSharedEventAndRepublish() { + var event = new PluginSharedEventDelegator(this, new FakeSharedEvent(this)); + dispatcher.onApplicationEvent(event); + verify(publisher).publishEvent(event.getDelegate()); + } + + class FakeEvent extends ApplicationEvent { + + public FakeEvent(Object source) { + super(source); + } + + } + + @SharedEvent + class FakeSharedEvent extends ApplicationEvent { + + public FakeSharedEvent(Object source) { + super(source); + } + + } +} \ No newline at end of file diff --git a/docs/plugin/shared-event.md b/docs/plugin/shared-event.md new file mode 100644 index 000000000..5e706ff5e --- /dev/null +++ b/docs/plugin/shared-event.md @@ -0,0 +1,83 @@ +# 插件中如何发送共享事件(SharedEvent) + +在插件中,可以通过共享事件(SharedEvent)来发送消息。 共享事件是一种特殊的事件,它可以被核心和所有插件订阅。 + +## 订阅共享事件 + +目前,核心中已经提供了不少的共享事件,例如 `run.halo.app.event.post.PostPublishedEvent`、`run.halo.app.event.post.PostUpdatedEvent` +,这些事件由核心发布,核心和插件均可订阅。请看下面的示例: + +```java + +@Component +public class PostPublishedEventListener implements ApplicationListener { + + @Override + public void onApplicationEvent(PostPublishedEvent event) { + // Do something + } + +} +``` + +或者通过 `@EventListener` 注解实现, + +```java + +@Component +public class PostPublishedEventListener { + + @EventListener + // @Async // 如果需要异步处理,可以添加此注解 + public void onPostPublished(PostPublishedEvent event) { + // Do something + } + +} +``` + +> 需要注意的是,只有被 `@SharedEvent` 注解标记的事件才能够被其他插件或者核心订阅。 + +## 发送共享事件 + +在插件中,我们可以通过 `ApplicationEventPublisher` 来发送共享事件,请看下面的示例: + +```java + +@Service +public class PostService { + + private final ApplicationEventPublisher eventPublisher; + + public PostService(ApplicationEventPublisher eventPublisher) { + this.eventPublisher = eventPublisher; + } + + public void publishPost(Post post) { + // Do something + eventPublisher.publishEvent(new PostPublishedEvent(post)); + } + +} +``` + +## 创建共享事件 + +在插件中,我们可以创建自定义的共享事件,供其他插件订阅,示例如下: + +```java + +@SharedEvent +public class MySharedEvent extends ApplicationEvent { + + public MySharedEvent(Object source) { + super(source); + } + +} +``` + +> 需要注意的是: +> 1. 共享事件必须继承 `ApplicationEvent`。 +> 2. 共享事件必须被 `@SharedEvent` 注解标记。 +> 3. 如果想要被其他插件订阅,则需要将该事件类发布到 Maven 仓库中,供其他插件引用。