1

In Spring, when I write an EventHandler, I can set a conditon, to filter out uninteresting events, like this:

// I use lombok
public class TopicEvent extends ApplicationEvent {
    @Getter @Setter private String topic;
    @Getter @Setter private PayloadObject payload;
}

...

@EventListener(condition = "#event.topic eq \"ecology\"")
public void onEcologyTopicEvent(TopicEvent e) {
    ...
}

Which is already nice. But it has little benefit over

@EventListener
public void onEcologyTopicEvent(TopicEvent e) {
    if (!e.getTopic().equals("ecology") { return; }
    ...
}

What I'd like to provide to the users of my TopicEvent is an Annotation

@TopicEventListener(topic = "ecology")
public void onEcologyTopicEvent(TopicEvent e) {
    ...
}

I have three ideas for that:

1: Spring offers thos synthesized Annotations and @AliasFor. Maybe it is possible to use that

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@EventListener
public @interface TopicEventListner {

    @AliasFor(annotation = EventListener.class, /* can I tweak topic to the string #event.topic eq $topic? */)
    String topic;
}

2: (What seems more plausible) Can I register some infrastructure components, maybe a custom ApplicationEventMulticaster or add filters to EventListeners on runtime? If so, where would be a good place to start, i.e. which would be the class/component I would need to implement to register it where?, respectively - where could I hook into?

3: Replace @TopicEventListener(topic = "ecology") by @EventListener(condition = "#event.topic eq \"ecology\"") on compile time. But this approach seems to be... maybe a slight overkill, and I have not the slightest clue about such things, and expect it to be terribly complex.
... But it might be the way I would solve it in C++ (with a macro)

2 Answers2

1

How about defining @EcologyTopicEventListener?

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@EventListener(condition = "#event.topic eq ecology")
public @interface EcologyTopicEventListener {
}

If you have a pre-defined list of topics, than this approach can be even better than @TopicEventListener(topic="ecology") since it eliminates possible issues in "ecology"

If you don't have this list known in compile time, than probably you can't go with the first approach that you've presented.

In this case if you want to define beans in runtime (Well to be more precise during the application context startup), you can use bean factory post processors. In a nutshell they allow registering bean definitions into the application context in a dynamic way. So you could create beans of listeners by yourself and even generate them dynamically.

As for the third approach I also think its an overkill if you ask me :)

Mark Bramnik
  • 39,963
  • 4
  • 57
  • 97
  • Thank you for that post. For me it is interesting in two reagards. First, the idea is a nice fallback if I can not solve it with (topic = "..."), which I would still prefere since I would like to use the solution for a bridge between external events from the message broker to internal events. The second it the `@Inherited`-annotation which is new to me, and I will continue to read more about :-). +1 for the idea. – derM - not here for BOT dreams Jun 25 '19 at 09:22
  • What I do not understand is how the beans come into this equation? Are there beans generated for the @EventListeners? – derM - not here for BOT dreams Jun 25 '19 at 09:23
  • 1
    Nope, I meant that @EventListeners could be also created as bean. Its a relatively new feature (since 4.2) before that you had to create beans implement interface and it worked. See https://www.baeldung.com/spring-events (chapter 2.3). So you could create beans like this dynamically and add to application context given some kind of predefined configuration. But frankly its also an overkill :) – Mark Bramnik Jun 25 '19 at 09:45
  • That's great. I don't even have to create beans for that, I can just use `applicationContext.addApplicationListener(ApplicationListener)` to register a lambda that does the filtering and calls the method of the bean if the conditions are met :-) – derM - not here for BOT dreams Jun 25 '19 at 11:05
0

Thanks to the ideas of Mark Bramnik I sketched up this solution:

The Annotation does not inherrit @EventListener

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyEventAnnotation
{
   String topic() default "";
}

instead I use a BeanPostProcessor and the ApplicationContext to create the ApplicationListeners and add them for each Bean created:

@Component
public class MyEventAnnotationBeanPostProcessor implements BeanPostProcessor
{
   private static Logger logger = LoggerFactory.getLogger(MyEventAnnotationBeanPostProcessor.class);

   @Autowired
   AbstractApplicationContext ctx;

   @Override
   public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException
   {
      for (Method method: bean.getClass().getMethods()) {
         if (method.isAnnotationPresent(MyEventAnnotation.class)) {
            MyEventAnnotation annotation = method.getAnnotation(MyEventAnnotation.class);
            ctx.addApplicationListener(createApplicationListener(method, bean, annotation.topic()));
         }
      }
      return bean;
   }

   private ApplicationListener<MyEvent> createApplicationListener(Method m, Object bean, String topic) {
      return (MyEvent e) -> {
         if (topic.equals("") || e.getTopic().equals(topic)) { // Filter here!
            try {
               m.invoke(bean, e);
            } catch (IllegalAccessException e1) {
               e1.printStackTrace();
            } catch (InvocationTargetException e1) {
               e1.printStackTrace();
            }
         }
      };
   }
}

This is just a rough sketch of the idea, and might contain unsafe operations. I am but a beginner in Java so don't blame me, if you copy this code. But feel free to suggest fixes :D