Events and listeners in Spring Framework

Events, and generally callback idea, are very popular in technologies associated to GUI (JavaScript, Swing). On the server side of web applications they're less popular. However it doesn't mean that we can't implement one event-oriented architecture on it. Above all when we're working with Spring.

In this article we'll focus on events handling in Spring framework. In the introduction we'll present the concept of event-driven programming. The second part will be dedicated to event handling in Spring framework. We'll discover the main methods to implement event dispatch and listening. We'll terminate this article by showing how to use basic listeners in Spring application.

Event-driven programming

Before starting to talk about programming aspects of event-driven programming, let's begin by present an image which can help to better understand the concept. In a city they're only two wear shops, A and B. In the first one the customers are served one by one, ie. only one customer can make a shopping. In the B shop, several customers can make the shopping simultaneously and when some customer needs help of the seller, he needs to raise his right hand. Seller comes to him and helps to make better choice. It's all about event-driven programming is: making some actions as the response for some behavior.

As we saw, event-driven programming, also known as event-based programming, is a programming paradigm based on reactions to received signals. These signals must transport the information with some signification. An example of this information can be the click on button. These signals are called events. They can be generated by user action (click, touch) or by program (for example: the end of loading of one element can start another action).

To understand it better, take a look on below sample which imitates user action in GUI:

public class EventBasedTest {

  @Test
  public void test() {
    Mouse mouse = new Mouse();
    mouse.addListener(new MouseListener() {
      @Override
      public void onClick(Mouse mouse) {
        System.out.println("Listener#1 called");
        mouse.addListenerCallback();
      }
    });
    mouse.addListener(new MouseListener() {
      @Override
      public void onClick(Mouse mouse) {
        System.out.println("Listener#2 called");
        mouse.addListenerCallback();
      }
    });
    mouse.click();
    assertTrue("2 listeners should be invoked but only "+mouse.getListenerCallbacks()+" were", mouse.getListenerCallbacks() == 2);
  }
}


class Mouse {
  private List listeners = new ArrayList();
  private int listenerCallbacks = 0;
  
  public void addListenerCallback() {
    listenerCallbacks++;
  }
  
  public int getListenerCallbacks() {
    return listenerCallbacks;
  }
  
  public void addListener(MouseListener listener) {
    listeners.add(listener);
  }
  
  public void click() {
    System.out.println("Clicked !");
    for (MouseListener listener : listeners) {
      listener.onClick(this);
    }
  }
}

interface MouseListener {
  public void onClick(Mouse source);
}

The JUnit case should print following output (and the test should pass by the way) :

Clicked !
Listener#1 called
Listener#2 called

Events in Spring framework

Spring bases its events handling on beans implementing org.springframework.context.ApplicationListener interface. It defines only one method, onApplicationEvent which is trigerred when an event is sent. This interface can be generic by specifying the event to which it has to be applied. Thanks to it, Spring will filter itself which listeners can receive given event. Event is represented by org.springframework.context.ApplicationEvent instances. This abstract class extends java.util.EventObject, so with getSource method we can easily get the object on which given event occurred. Two types of events exist:
- associated with application context: all these events inherit from org.springframework.context.event.ApplicationContextEvent class. They're applied to events raised by org.springframework.context.ApplicationContext. As you can imagine, we retrieve the events trigerred directly with application context's lifecycle: ContextStartedEvent is launched when the context starts, ContextStoppedEvent when it stops, ContextRefreshedEvent when the context is refreshed and finally ContextClosedEvent which occurs on context closing.
- associated with requests: represented by org.springframework.web.context.support.RequestHandledEvent, they're raised when the request is handled within ApplicationContext.

How does Spring dispatch the events to appropriated listeners ? This process is assured by event multicaster, represented by the implementation of org.springframework.context.event.ApplicationEventMulticaster. This interface defines 3 kind of methods, used for:
- add new listeners: two methods are defined to add new listener: addApplicationListener(ApplicationListener listener) and addApplicationListenerBean(String listenerBeanName). The first one can be applied when listener object is known. If the second one is used, we need to resolve bean into listener object before adding it into list of listeners.
- remove listeners: as adding methods, we can remove one listener either by passing the object (removeApplicationListener(ApplicationListener listener) or by passing bean name (removeApplicationListenerBean(String listenerBeanName). The third method, removeAllListeners() permits to remove all registered listeners.
- dispatch event to registered listeners: defined by multicastEvent(ApplicationEvent event), it allows to send given event to all registered listeners. The example of implementation is done in org.springframework.context.event.SimpleApplicationEventMulticaster as below:

public void multicastEvent(final ApplicationEvent event) {
  for (final ApplicationListener listener : getApplicationListeners(event)) {
    Executor executor = getTaskExecutor();
    if (executor != null) {
      executor.execute(new Runnable() {
        @Override
        public void run() {
          listener.onApplicationEvent(event);
        }
      });
    }
    else {
      listener.onApplicationEvent(event);
    }
  }
}

Let's take a look on how event multicaster is placed in application context. Some methods defined in AbstractApplicationContext (abstract context class extended directly or indirectly by popularly used context as XmlWebApplicationContext) contain the invocation of public publishEvent method. According to the commentary of this method, it's in charge of sending given event to all listeners. This method looks like:

@Override
public void publishEvent(ApplicationEvent event) {
  Assert.notNull(event, "Event must not be null");
  if (logger.isTraceEnabled()) {
    logger.trace("Publishing event in " + getDisplayName() + ": " + event);
  }
  getApplicationEventMulticaster().multicastEvent(event);
  if (this.parent != null) {
    this.parent.publishEvent(event);
  }
}

This method is called by the methods that: start the context (ContextStartedEvent is published), stop the context (ContextStoppedEvent published), refresh the context (ContextRefreshedEvent is trigerred) and close the context (with ContextClosedEvent).

Events associated with application context are common for all Spring-based applications. Web applications using Spring can also handle another type of event, associated with request (already seen RequestHandledEvent). It's handled similary to context-oriented events. First, all happen in org.springframework.web.servlet.FrameworkServlet which manages request events in the method that processing the request (processRequest). At the end of this method is placed the invocation of private publishRequestHandledEvent(HttpServletRequest request, long startTime, Throwable failureCause). As its name indicates, this method will publish given RequestHandledEvent to all listeners. The event is after passed to application context's publishEvent method to be dispatched by the event multicaster. It's possible because RequestHandledEvent extends the same class as ApplicationContextEvent, ie. ApplicationEvent. You can see the publishing method here:

private void publishRequestHandledEvent(HttpServletRequest request, long startTime, Throwable failureCause) {
  if (this.publishEvents) {
    // Whether or not we succeeded, publish an event.
    long processingTime = System.currentTimeMillis() - startTime;
    this.webApplicationContext.publishEvent(
      new ServletRequestHandledEvent(this,
        request.getRequestURI(), request.getRemoteAddr(),
        request.getMethod(), getServletConfig().getServletName(),
        WebUtils.getSessionId(request), getUsernameForRequest(request),
        processingTime, failureCause));
  }
}

Note that you can turn off dispatching of request-based events. setPublishEvents(boolean publishEvents) of FrameworkServlet permits to disable the event dispatching to, for example, improve application performances. By default, events dispatching is activated.

As we mentioned earlier, events can have bad performance on application reactivity. It's because, by default, they're dispatched synchronously, by the calling thread (ie. the same thread that handle transactions, treat the requests, prepare views to output). So if one listener takes a some seconds to respond, whole application can be penalized. Luckily, they're a possibility to specify asynchronous execution of events handling. However, note that so handled events won't be able to interact with caller's context (class loader or transactions). The method of managing events is represented by the Executor used by event multicaster. By default, org.springframework.core.task.SyncTaskExecutor is used to invoke listeners.

Implement listeners in Spring

To understand well event-listener paradigm, let's write a small test case. Through this case we want to prove that by default, listeners execute dispatched events in the caller thread. So, instead of getting the view result almost immediately, we'll get it upon 5 seconds because the listener sleeps for 5 seconds (calls Thread.sleep(5000)). The test checks 3 things: if the result of controller corresponds to expected view name, if event listener took 5 seconds to response (Thread.sleep executed without any problem) and if controller's action took 5 seconds to generate the view (because of listener's sleep) too. The second defined test will verify if our listener is trigerred at another event than the event defined in its generics. First, they're the definition of our beans in configuration file:

< -- This bean will catch SampleCustomEvent launched in tested controller -->

< -- Thanks to this bean we'll able to get the execution times of tested controller and listener -->

They're the code of event and listener:

public class SampleCustomEvent extends ApplicationContextEvent {

  private static final long serialVersionUID = 4236181525834402987L;

  public SampleCustomEvent(ApplicationContext source) {
    super(source);
  }
}

public class OtherCustomEvent extends ApplicationContextEvent {

  private static final long serialVersionUID = 5236181525834402987L;

  public OtherCustomEvent(ApplicationContext source) {
    super(source);
  }
}

public class SampleCustomEventListener implements ApplicationListener {

  @Override
  public void onApplicationEvent(SampleCustomEvent event) {
    long start = System.currentTimeMillis();
    try {
      Thread.sleep(5000);
    } catch (Exception e) {
      e.printStackTrace();
    }
    long end = System.currentTimeMillis();
    int testTime = Math.round((end - start) / 1000);
    ((TimeExecutorHolder) event.getApplicationContext().getBean("timeExecutorHolder")).addNewTime("sampleCustomEventListener", new Integer(testTime));
  }
}

Nothing complicated. Events are only initialized. The listener tests the execution time by getting current time in milliseconds and saving it, after conversion, in seconds. TimeExecutorHolder used by the listener isn't complicated too:

public class TimeExecutorHolder {

  private Map testTimes = new HashMap();
  
  public void addNewTime(String key, Integer value) {
    testTimes.put(key, value);
  }
  
  public Integer getTestTime(String key) {
    return testTimes.get(key);
  }
}

This object holds only a map with execution times of tested elements (identified by String). Tested controller looks similar to listener. The only difference is that it publishes an event (catched by already defined listener) and returns a view called "success":

@Controller
public class TestController {
  @Autowired
  private ApplicationContext context;
  
  @RequestMapping(value = "/testEvent")
  public String testEvent() {
    long start = System.currentTimeMillis();
    context.publishEvent(new SampleCustomEvent(context));
    long end = System.currentTimeMillis();
    int testTime = (int)((end - start) / 1000);
    ((TimeExecutorHolder) context.getBean("timeExecutorHolder")).addNewTime("testController", new Integer(testTime));
    return "success";
  }

  @RequestMapping(value = "/testOtherEvent")
  public String testOtherEvent() {
    context.publishEvent(new OtherCustomEvent(context));
    return "success";
  }
}

And finally, this is our test case which calls /testEvent and checks after TimeExecutorHolder bean to verify the execution time of both parts:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations={"file:applicationContext-test.xml"})
@WebAppConfiguration
public class SpringEventsTest {

  @Autowired
  private WebApplicationContext wac;

  private MockMvc mockMvc;

  @Before
  public void setUp() {
    this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build();
  }

  @Test
  public void test() {
    try {
      MvcResult result = mockMvc.perform(get("/testEvent")).andReturn();
      ModelAndView view = result.getModelAndView();
      String expectedView = "success";
      assertTrue("View name from /testEvent should be '"+expectedView+"' but was '"+view.getViewName()+"'", view.getViewName().equals(expectedView));
    } catch (Exception e) {
      e.printStackTrace();
    }
    TimeExecutorHolder timeHolder = (TimeExecutorHolder) this.wac.getBean("timeExecutorHolder");
    int controllerSec = timeHolder.getTestTime("testController").intValue();
    int eventSec = timeHolder.getTestTime("sampleCustomEventListener").intValue();
    assertTrue("Listener for SampleCustomEvent should take 5 seconds before treating the request but it took "+eventSec+" instead",  eventSec == 5);
    assertTrue("Because listener took 5 seconds to response, controller should also take 5 seconds before generating the view, but it took "+controllerSec+ " instead", controllerSec == 5);
  }

  @Test
  public void otherTest() {
    TimeExecutorHolder timeHolder = (TimeExecutorHolder) this.wac.getBean("timeExecutorHolder");
    timeHolder.addNewTime("sampleCustomEventListener", -34);
    try {
      MvcResult result = mockMvc.perform(get("/testOtherEvent")).andReturn();
      ModelAndView view = result.getModelAndView();
      String expectedView = "success";
      assertTrue("View name from /testEvent should be '"+expectedView+"' but was '"+view.getViewName()+"'", view.getViewName().equals(expectedView));
    } catch (Exception e) {
      e.printStackTrace();
    }
    Integer eventSecObject = timeHolder.getTestTime("sampleCustomEventListener");
    assertTrue("SampleCustomEventListener shouldn't be trigerred on OtherEvent but it was", eventSecObject.intValue() == -34);
  }
}

The test should pass without any problems. It proved many hypothesis defined in this article. First, we saw that event-diven programming consists on triggering the execution of some actions when a signal is send to the application. This signal must hold a sense. In Spring, the events can be easily captured by listeners thanks to generics in listeners definition. Thanks to it, we don't need to check inside if trigerred event corresponds to the event expected by the listener. We also discovered that by default, the listeners are executed in synchronous maneer. So they can block the operations executed in calling thread as view generation or database processing.


If you liked it, you should read:

📚 Newsletter Get new posts, recommended reading and other exclusive information every week. SPAM free - no 3rd party ads, only the information about waitingforcode!