mirror of https://github.com/halo-dev/halo
Add support for publishing events among plugins (#6081)
#### What type of PR is this? /kind feature /area core /area plugin #### What this PR does / why we need it: This PR enhance usage of SharedEvent annotation to add support for publishing events among plugins. #### How to test? 1. Clone repository https://github.com/halo-dev/plugin-starter 2. Change build.gradle as following: ```gradle dependencies { implementation platform('run.halo.tools.platform:plugin:2.17.0-SNAPSHOT') ``` 3. Change StarterPlugin as following: ```java @Component public class StarterPlugin extends BasePlugin { private final ApplicationContext appContext; public StarterPlugin(PluginContext pluginContext, ApplicationContext appContext) { super(pluginContext); this.appContext = appContext; } @Override public void start() { appContext.publishEvent(new PostDeletedEvent(this, "fake-plugin")); } @Override public void stop() { } @EventListener(PostDeletedEvent.class) public void onApplicationEvent(PostDeletedEvent event) { System.out.println("Post deleted event received in plugin: " + event.getName()); } } ``` 4. Add a listener to Halo core ```java @EventListener(PostDeletedEvent.class) public void onApplicationEvent(PostDeletedEvent event) { System.out.println("Post deleted event received in core: " + event.getName()); } ``` 5. Build plugin and install plugin 6. Enable the plugin and see the result #### Does this PR introduce a user-facing change? ```release-note None ```pull/6143/head
parent
b445b505be
commit
a94596a9f8
|
@ -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<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;
|
||||
|
|
|
@ -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<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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
|
@ -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<ApplicationEvent> listener);
|
||||
|
||||
void unregister(ApplicationListener<ApplicationEvent> listener);
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -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<PostPublishedEvent> {
|
||||
|
||||
@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 仓库中,供其他插件引用。
|
Loading…
Reference in New Issue