Tuesday, June 5, 2012

Using Guava EventBus with Spring

Recently, I started working on VeloxMUD again. The time away has really given me a new perspective on the code. Usually, that perspective goes like this:
  • What the heck was I thinking?
The first thing I looked at was the code that handled connections for the MUD. There is a thread that waits on socket.accept() and then passes the Socket objects off to a handler service (injected by Spring). Although this isn't terrible, I thought I could improve it by using a Publish-Subscribe pattern to decouple the two.

I started my search with Observable and found that it wasn't going to fit. Although the Observer interface isn't bad, the need to derive from Observable wasn't going to work for me. My publisher (the "observable" object) already derived from Thread. And it seemed there had to be a better way.

I stumbled upon Guava's EventBus. Ah ha! This is what I need! But wait, where are all the publish-subscribe guts that I need to implement? Gone... Publishers post events of any arbitrary type, subscribers add a method (with any name) and a @Subscribe annotation. Done. Wow! This is observable done right!

Oops... one small problem. Most of the stuff that needs to register is created and managed by Spring. How do I register my subscribers without tackling all sorts of nasty lifecycle problems?

BeanPostProcessor comes to the rescue! We can provide custom bean-processing code to Spring. With the right piece of code, Spring can do all the necessary registration for us.

/*
 * EventBusPostProcessor.java
 * Author: Patrick Meade
 *
 * EventBusPostProcessor.java is hereby placed into the public domain.
 * Use it as you see fit, and entirely at your own risk.
 */

package com.example.spring.guava;

import com.google.common.eventbus.EventBus;
import com.google.common.eventbus.Subscribe;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanPostProcessor;

/**
 * EventBusPostProcessor registers Spring beans with EventBus. All beans
 * containing Guava's @Subscribe annotation are registered.
 * @author pmeade
 */
public class EventBusPostProcessor implements BeanPostProcessor
{
    private final Logger log = LoggerFactory.getLogger(this.getClass());
    
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName)
            throws BeansException
    {
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName)
            throws BeansException
    {
        // for each method in the bean
        Method[] methods = bean.getClass().getMethods();
        for(Method method : methods)
        {
            // check the annotations on that method
            Annotation[] annotations = method.getAnnotations();
            for(Annotation annotation : annotations)
            {
                // if it contains the Subscribe annotation
                if(annotation.annotationType().equals(Subscribe.class))
                {
                    // register it with the event bus
                    eventBus.register(bean);
                    log.trace("Bean {} containing method {} was subscribed to {}",
                        new Object[] {
                            beanName, method.getName(),
                            EventBus.class.getCanonicalName()
                        });
                    // we only need to register once
                    return bean;
                }
            }
        }
        
        return bean;
    }
    
    @Autowired
    private EventBus eventBus;
}

We add the following XML stanzas to Spring's application context:

<bean id="eventBus"
      class="com.google.common.eventbus.EventBus"/>
<bean id="eventBusPostProcessor"
      class="com.example.spring.guava.EventBusPostProcessor"/>

And it works! Spring registers every bean with a @Subscribe annotation.

2012-06-05 00:50:09,084 TRACE [main] com.example.spring.guava.EventBusPostProcessor (EventBusPostProcessor.java:64) - Bean connectionService containing method handleConnection was subscribed to com.google.common.eventbus.EventBus