Events in spring v2
Last time I wrote about events in a spring based application I’ve introduced some basics on how events can be dispatched using spring infrastructure. In this post, I’m going to dig deeper into an order of handlers, exceptions and asynchronous events handlers.
Order of handlers
To execute event handlers in the order you need to make sure that your handlers are not asynchronous. If they are no one can really guarantee the order in which events will be processed. Once you are sure that you need to handle the event in particular order all you need to do is to annotate a @EventListener method with an @Order annotation.
@SpringBootTest
class HandlersOrderTest extends Specification {
@Autowired
ApplicationEventPublisher publisher
@AutowiredSpy
SecondHandler secondHandler
@AutowiredSpy
FirstHandler firstHandler
def "handlers should be invoked in order"() {
given:
final event = new AnyEvent()
when:
publisher.publishEvent(event)
then:
1 * firstHandler.handleEvent(event)
then:
1 * secondHandler.handleEvent(event)
}
@Configuration
private static class Config {
}
@Component
private static class FirstHandler {
@EventListener
@Order(1)
void handleEvent(AnyEvent event) {
println("First handler " + event)
}
}
@Component
private static class SecondHandler {
@EventListener
@Order(2)
void handleEvent(AnyEvent event) {
println("Second handler " + event)
}
}
}
Note that each method must be annotated with an @Order annotation to work. Spring will create new bean for each handler method and therefore if methods are not annotated order is not guaranteed.
If you need to handle events in order and think that @Order of handlers is tricky and might cause issues in the future there are other ways to do it. You can return yet another event from the handler itself which will be processed next.
@SpringBootTest
class EventFromHandlerTest extends Specification {
@AutowiredSpy
EventHandler eventHandler
@Autowired
ApplicationEventPublisher eventPublisher
def "should handle events in order"() {
given:
final event = new FirstEvent()
when:
eventPublisher.publishEvent(event)
then:
1 * eventHandler.handleFirstEvent(event)
then:
1 * eventHandler.handleAnotherEvent({ YetAnotherEvent anotherEvent ->
anotherEvent.originalEvent == event
})
}
@Configuration
private static class Config {
}
static class FirstEvent {
}
static class YetAnotherEvent {
private final FirstEvent originalEvent
YetAnotherEvent(FirstEvent originalEvent) {
this.originalEvent = originalEvent
}
}
@Service
private static class EventHandler {
@EventListener
YetAnotherEvent handleFirstEvent(FirstEvent event) {
println("First event " + event)
new YetAnotherEvent(event)
}
@EventListener
void handleAnotherEvent(YetAnotherEvent event) {
println("Another event " + event)
}
}
}
If you need something even more complex handler can return a collection of events and each of events in the collection will be broadcasted separately.
Asynchronous event handlers
Handling events can be time consuming. The first thing you might think of is to handle events asynchronously. The basic asynchronous usage might be achieved annotating handler methods with @Async
@SpringBootTest
class AsyncEventHandlerTest extends Specification {
@Autowired
ApplicationEventPublisher eventPublisher
@Autowired
CountDownLatch latch
def "should execute all asynchronous handlers"() {
given:
final event = new AnyEvent()
when:
eventPublisher.publishEvent(event)
then:
latch.await(2, TimeUnit.SECONDS)
}
@Configuration
@EnableAsync
private static class Config {
@Bean
CountDownLatch latch() {
new CountDownLatch(2)
}
@Bean
DoNothingHandler doNothingHandler() {
new DoNothingHandler(latch())
}
@Bean
AnotherDoNothingHandler anotherDoNothingHandler() {
new AnotherDoNothingHandler(latch())
}
}
@Slf4j
private static class DoNothingHandler {
private final CountDownLatch latch
DoNothingHandler(CountDownLatch latch) {
this.latch = latch
}
@Async
@EventListener
void handleEvent(AnyEvent event) {
log.info("Do nothing handler {}", event)
latch.countDown()
}
}
@Slf4j
private static class AnotherDoNothingHandler {
private final CountDownLatch latch
AnotherDoNothingHandler(CountDownLatch latch) {
this.latch = latch
}
@Async
@EventListener
void handleEvent(AnyEvent event) {
log.info("Another do nothing handler {}", event)
latch.countDown()
}
}
}
Output:
2017-09-24 12:01:29.439 INFO 6455 --- [cTaskExecutor-2] EventHandlerTest$AnotherDoNothingHandler : Another do nothing handler com.pchudzik.blog.example.springevents.more.AnyEvent@5e2a3259
2017-09-24 12:01:29.439 INFO 6455 --- [cTaskExecutor-1] a.AsyncEventHandlerTest$DoNothingHandler : Do nothing handler com.pchudzik.blog.example.springevents.more.AnyEvent@5e2a3259
In case your default spring executor is not meant to handle application events you can create custom ApplicationEventMulticaster with custom executor:
@SpringBootTest
class WithTaskExecutorEventHandlerTest extends Specification {
@Autowired
ApplicationEventPublisher eventPublisher
@AutowiredSpy
DoNothingHandler doNothingHandler
@AutowiredSpy
ExceptionThrowingHandler exceptionThrowingHandler
def "should execute all asynchronous handlers"() {
given:
final latch = new CountDownLatch(2)
when:
eventPublisher.publishEvent(latch)
then:
latch.await(2, TimeUnit.SECONDS)
then:
1 * exceptionThrowingHandler.handleEvent(latch)
1 * doNothingHandler.handleEvent(latch)
}
@Configuration
private static class Config {
private static final int FOUR_THREADS = 4
@Bean
ExceptionThrowingHandler exceptionThrowingHandler() {
new ExceptionThrowingHandler()
}
@Bean
DoNothingHandler doNothingHandler() {
new DoNothingHandler()
}
@Bean
ApplicationEventMulticaster applicationEventMulticaster() {
final multicaster = new SimpleApplicationEventMulticaster()
multicaster.setErrorHandler({ ex -> ex.printStackTrace()})
multicaster.setTaskExecutor(Executors.newFixedThreadPool(FOUR_THREADS))
return multicaster
}
}
@Slf4j
static class ExceptionThrowingHandler {
@EventListener
void handleEvent(CountDownLatch latch) {
log.info("Exception throwing handler " + latch.toString())
latch.countDown()
throw new Exception("Expected exception")
}
}
@Slf4j
static class DoNothingHandler {
@EventListener
void handleEvent(CountDownLatch latch) {
log.info("Do nothing handler " + latch.toString())
latch.countDown()
}
}
}
Output:
2017-09-24 12:07:01.263 INFO 6874 --- [pool-1-thread-3] ventHandlerTest$ExceptionThrowingHandler : Exception throwing handler java.util.concurrent.CountDownLatch@5658d7a9[Count = 2]
2017-09-24 12:07:01.270 INFO 6874 --- [pool-1-thread-4] xecutorEventHandlerTest$DoNothingHandler : Do nothing handler java.util.concurrent.CountDownLatch@5658d7a9[Count = 1]
Exceptions
Last but not least. When something goes wrong exceptions are what you get. By default exception thrown from event handler will stop further event propagation:
@SpringBootTest
class EventExceptionTest extends Specification {
@AutowiredSpy
EventHandler eventHandler
@Autowired
ApplicationEventPublisher eventPublisher
def "Exception thrown from handler should break handlers chain"() {
given:
final anyEvent = new AnyEvent()
when:
try {
eventPublisher.publishEvent(anyEvent)
} catch (Exception ex) {
assert ex.getCause().getMessage() == "expected exception"
}
then:
0 * eventHandler.handleEvent(_)
}
@Configuration
private static class Config {
@Bean
ExceptionThrowingEventHandler exceptionThrowingEventHandler() {
new ExceptionThrowingEventHandler()
}
@Bean
EventHandler eventHandler() {
new EventHandler()
}
}
private static class ExceptionThrowingEventHandler {
@Order(1)
@EventListener
void handleEvent(AnyEvent anyEvent) throws Exception {
println("exception handler " + anyEvent)
throw new Exception("expected exception")
}
}
private static class EventHandler {
@Order(2)
@EventListener
void handleEvent(AnyEvent anyEvent) {
println("default handler " + anyEvent)
}
}
}
Of course, it is possible to inject custom ErrorHandler into ApplicationEventMulticaster which will be responsible for event handling.
@SpringBootTest
class EventExceptionHandlerTest extends Specification {
@AutowiredSpy
SimpleEventHandler defaultEventHandler
@Autowired
ApplicationEventPublisher eventPublisher
def "Exception thrown from handler should break handlers chain"() {
given:
final anyEvent = new AnyEvent()
when:
eventPublisher.publishEvent(anyEvent)
then:
1 * defaultEventHandler.handleEvent(_)
}
@Configuration
private static class Config {
@Bean
ApplicationEventMulticaster applicationEventMulticaster() {
final multicaster = new SimpleApplicationEventMulticaster()
multicaster.setErrorHandler(TaskUtils.LOG_AND_SUPPRESS_ERROR_HANDLER)
return multicaster
}
}
@Component
private static class ExceptionThrowingEventHandler {
@Order(1)
@EventListener
void handleEvent(AnyEvent anyEvent) throws Exception {
println("exception handler " + anyEvent)
throw new Exception("expected exception")
}
}
@Component
private static class SimpleEventHandler {
@Order(2)
@EventListener
void handleEvent(AnyEvent anyEvent) {
println("default handler " + anyEvent)
}
}
}
Now at least you’ll know what’s going on with the errors and propagation will not be terminated too early. Which might cause unexpected troubles in case you’ve got multiple handlers registered for this particular event. Note that the same principles apply to web MVC even if you have @ExceptionHandler it is not Mulitcasters error handler, therefore, event propagation will be aborted.
Yet again events handling in spring can save you some extra coding on your site but it’s important to know a little bit more on how they do work before you jump head first into it. What I’ve presented here is only part of the things you should consider. There are also @TransactionalEventListeners which deserve an explanation on their own.
All code samples can be found on my GitHub.
@Slf4j is groovy transform adding logback’s log field to class
@AutowiredSpy is springmock’s annotation which allows to create spock based spy and inject it into spock specification from spring context
Everything else is standard spring and spring boot annotations.
If you've enjoyed or found this post useful you might also like: