Scoped Spring events possible?
Asked Answered
F

2

9

The Spring event mechanism supports publishing application events and listening to these events within Spring components via the @EventListener annotation. However, I cannot find anything about sending events within a specific scope in the documentation. My specific need for Vaadin is:

  • in context of a user interaction, send an event (e.g. logged-in event)
  • this event should only be consumed by beans in same @UIScope, i.e. other user UIs shouldn't be effected

Is that possible? NOTE: This is not really specific to Vaadin. I could also ask how it would be done with Spring web mvc request scope.

Forgery answered 18/7, 2018 at 7:33 Comment(10)
You have a specific use case? On my mind it is not possible as the event system is made to decouple producer and consumer. Here you want to have a coupling so why use it? Even if you want async processing with @Async, the thread that will process data will not be the one that you use to launch process, so context will not be the same. The solution I got, is to set all data you need on the event (bean from any context) and use it on process side. But be ware, some bean can have the @PreDestroy method already called when you asynchronously process the data.Bulldoze
How would that be coupled? It's just the scope that would be different: instead of publishing to all event listeners, only those event listeners that are active in the scope vaadin-ui would be notified.Forgery
Use case: The UI has a root layout, and a login form in the content area. Once the user logged in, the root layout needs to be adapted which I would like to do via events. The UI components reside in the Vaadin UI scope. So, the event triggered on log-in should be restricted to those UI components for that user (other user's UI components shouldn't change). @BulldozeForgery
You could perhaps try to experiment with the condition attribute of the @EventListener annotation (see here for some details). You can basically define a condition in SpEL that needs to be met for the listener to be invoked.Magna
Just a potential lead : can't you put a UI-id inside the event and have a UI-scoped filter that rejects all events whose ui_id doesn't match the one expected by the filter ?Agouti
@JeremyGrand, this could be working. However, isn't that kind of waste of resource to let all listener beans (for all users that have a session which could be a large number) be checked for that condition if I only want to target beans of a particular user (the one that does a request)?Forgery
@SteffenHarbich yeah, I never encountered your use-case but I find it interesting and far from splitting hairs ; I'm surprised it's not already handled within standard Spring. There's maybe a way to get an ApplicationEventPublisher ui-scoped though. But I'm not sure that the UI-scoped listener registration is a good trade-off for preventing too many listeners from performing a single check.Agouti
@SteffenHarbich by the way, I tested publishing an event from a RestController and having a request-scoped bean listening to it from two concurrent httprequest (concurrency handled via breakpoinbts). For both events, only the listener scoped to the current request was triggered. So I think that eventPublisher may already be scoped.Agouti
@JeremyGrand OK, I will try that with Vaadin's UI-scope and report back here. Thanks for your help.Forgery
@JeremyGrand That worked, indeed. Only beans from same Vaadin UI were notified. If you add this as an answer I will accept it. Thanks again!Forgery
A
3

TL;DR : I think it already works as you wanted it

Long version :

The documentation is somewhat unclear about this, but having tried a simple test :

A controller that publishes events :

@Controller
public class FooController {
    @Autowired
    private ApplicationEventPublisher publisher;

    @GetMapping("/fireEvent")
    public void fireEvent() {
        publisher.publishEvent(new FooEvent(this));
    }
}

And a scoped bean that listens :

@Scope(value = WebApplicationContext.SCOPE_REQUEST)
@Component
public class FooListener {
    @EventListener(FooEvent.class)
    public void listen() {
         System.out.println("I'm listening. PS : I am "+this.toString());
    }
}

And when running two concurrent requests, only the listener scoped to the the same httprequest gets the event.


My interpretation (without having looked at it really deeply, so take it with a grain of salt) :

It seems to me that the ApplicationEventMulticaster's ListenerRetriever uses the BeanFactory to get the beans that were previously registered as listeners (by their names). And obviously, the factory will return a bean in the current scope.

Agouti answered 2/8, 2018 at 9:6 Comment(1)
Yes, that is exactly how it works. There is a side effect of this too: it's effectively not possible to deliver events to existing beans using SCOPE_PROTOTYPE scope. The multicaster, in this case, will request a new bean from the context, which would result into creation of new bean and delivering the event to this new bean instead of your existing beans.Diaphoresis
U
0

Please see if this is what you are looking for:

Main application :

   package com.fg7.evision.EventList;

    import org.springframework.context.annotation.AnnotationConfigApplicationContext;
    import org.springframework.context.annotation.ComponentScan;

    @ComponentScan(basePackages = "com.fg7.evision.EventList")
    public class EventApplication {


        public static void main(String[] args) {
           MyScopedEvent event = new MyScopedEvent("hello world");
           AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(EventApplication.class);
            ctx.publishEvent(event);
        }

    }

Event :

 package com.fg7.evision.EventList;

    public class MyScopedEvent  {

        private String message;

        public MyScopedEvent( String message) {
            this.message = message;
        }

        public String getMessage() {
            return message;
        }
    }

Event listener scoped to Singleton scope only.

package com.fg7.evision.EventList;

import org.springframework.beans.factory.BeanNameAware;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

@Component(value = "listener")
public class ScopedEventListener implements BeanNameAware {

    @Autowired
    ConfigurableApplicationContext context;

    String beanName;

    @Override
    public void setBeanName(String name) {
        this.beanName = name;
    }

    @EventListener(condition = "@listener.getScope() == 'singleton'")
    public void handleEvent(MyScopedEvent myScopedEvent) {
        System.out.println(myScopedEvent.getMessage());

    }

    public String getScope(){
        return this.context.getBeanFactory().getBeanDefinition(this.beanName).getScope();
    }


}
Unbelievable answered 1/8, 2018 at 12:48 Comment(5)
Thanks your for answer but this is not what I was looking for. The condition should be always matched because the ScopedEventListener is a singleton. Have a look at the Vaadin UI-scope if you are further interested. The UI scope is activated on (HTTP-)request, so every user (session) has their "own" UI scope. For my problem, events should only be propagated to the user's own UI scoped beans instead of to beans of the whole application.Forgery
Just substitute singleton, with your own, custom scope(you can do that in Spring). I put singleton as a reference. You may create own combined annotation @Scope + @UIScope, i.e @UISpringAwareScopeUnbelievable
Can you describe why it won't?Unbelievable
It checks only the scope property of the bean definition. Please see the other answer for the solution.Forgery
I am checking scope property of bean which emitted event, which is contained in it's bean definition. Bean do not have scope, scope is defined in their respective Bean Definitions. I checked other solution, it is bound to general request scope, but I think you have specific scope, not defined in Spring. The things it is bound to any web request, regardless whether it's Vaadin UI Scope or not. Anyhow, I am glad you found solution for this problems. Cheers!Unbelievable

© 2022 - 2024 — McMap. All rights reserved.