Spring Java Config and ProxyFactoryBean
July 5, 2010
I really like spring remoting. I really like spring java config. Recently I had to marry the two. Here is how.
I have a value-object:
public class Datum
implements Serializable
{
private int id;
private String name;
public Datum(int id, String name)
{
this.id = id;
this.name = name;
}
public int getId()
{
return id;
}
public String getName()
{
return name;
}
@Override
public String toString()
{
return "Datum{" + "id=" + id + "name=" + name + '}';
}
}
I have a service that produces Datum objects. The service interface is:
public interface IDatumProducer
{
public Datum produce();
}
and a sample implementation might be:
public class RandomDatumProducerImpl
implements IDatumProducer
{
private static final Logger log =
LoggerFactory.getLogger(RandomDatumProducerImpl.class);
private Random rnd = new Random(System.currentTimeMillis());
@Override
public Datum produce()
{
int i = rnd.nextInt();
Datum datum = new Datum(i, String.valueOf(i));
log.info("Produced {}", datum);
return datum;
}
}
Here is a client for the service:
public class DatumProducerClient
{
private static final Logger log =
LoggerFactory.getLogger(DatumProducerClient.class);
private final IDatumProducer producer;
public DatumProducerClient(IDatumProducer producer)
{
this.producer = producer;
}
@PostConstruct
public void init()
{
while (true)
{
try
{
Datum datum = producer.produce();
log.info("Got {}", datum);
Thread.sleep(1000L);
}
catch (InterruptedException ex)
{
log.error("Interrupted", ex);
}
}
}
}
Now I want to run the DatumProducerClient and the IDatumProducer on different machines. The client should connect remotely to the producer, invoke the produce method, and do its stuff.
We can expose the service easily using Spring. The spring remoting (JMS) manual section gives the spring configuration XML to do this, but no example for doing this in Java Config. Turns out there is one small detail to take care of when using Java config.
Producer (Server)
Here is the application config for the producer side:
@Configuration
public class ProducerConfig
{
@Bean
public AbstractMessageListenerContainer messageListenerContainer()
{
SimpleMessageListenerContainer container =
new SimpleMessageListenerContainer();
container.setConnectionFactory(jmsConnectionFactory());
container.setDestination(jmsQueue());
container.setMessageListener(jmsInvokerServiceExporter());
container.setConcurrentConsumers(3);
return container;
}
@Bean
public JmsInvokerServiceExporter jmsInvokerServiceExporter()
{
JmsInvokerServiceExporter serviceExporter =
new JmsInvokerServiceExporter();
serviceExporter.setService(datumProducer());
serviceExporter.setServiceInterface(IDatumProducer.class);
return serviceExporter;
}
@Bean
public ConnectionFactory jmsConnectionFactory()
{
ActiveMQConnectionFactory baseConnectionFactory =
new ActiveMQConnectionFactory();
baseConnectionFactory.setBrokerURL("tcp://localhost:61616");
return baseConnectionFactory;
}
@Bean
public Queue jmsQueue()
{
ActiveMQQueue queue = new ActiveMQQueue("dataChannel");
return queue;
}
@Bean
public IDatumProducer datumProducer()
{
return new RandomDatumProducerImpl();
}
}
And here is a main method that kicks off our application.
public class PublisherMain
{
public static void main(String[] args)
{
AnnotationConfigApplicationContext ctx =
new AnnotationConfigApplicationContext(ProducerConfig.class);
}
}
Consumer (Client)
Here is how the consumer is wired up.
@Configuration
public class ConsumerConfig
{
@Bean
public DatumProducerClient datumProducerClient()
{
return new DatumProducerClient(datumProducer());
}
@Bean
public IDatumProducer datumProducer()
{
// producerFactory.getObject() will return null if we don't define
// it as a separate bean; if we copy over the code from
// jmsInvokerProxyFactoryBean here, it won't work: the proxy won't
// get "enhanced" by spring, and its getObject() method will always
// return null.
JmsInvokerProxyFactoryBean producerProxy = jmsInvokerProxyFactoryBean();
return (IDatumProducer) producerProxy.getObject();
}
// We can't fold this code into datumProducer above
// The proxy bean must be defined separately, otherwise spring won't get
// a chance to enhance it.
@Bean
public JmsInvokerProxyFactoryBean jmsInvokerProxyFactoryBean()
{
JmsInvokerProxyFactoryBean producerProxy = new JmsInvokerProxyFactoryBean();
producerProxy.setServiceInterface(IDatumProducer.class);
producerProxy.setConnectionFactory(jmsConnectionFactory());
producerProxy.setQueue(jmsQueue());
return producerProxy;
}
@Bean
public ConnectionFactory jmsConnectionFactory()
{
ActiveMQConnectionFactory baseConnectionFactory =
new ActiveMQConnectionFactory();
baseConnectionFactory.setBrokerURL("tcp://localhost:61616");
return baseConnectionFactory;
}
@Bean
public Queue jmsQueue()
{
ActiveMQQueue queue = new ActiveMQQueue("dataChannel");
return queue;
}
}
and here is how the main method on the client side looks like:
public class ConsumerMain
{
public static void main(String[] args)
{
AnnotationConfigApplicationContext ctx =
new AnnotationConfigApplicationContext(ConsumerConfig.class);
}
}
The important bit to note here is that the JmsInvokerProxyFactoryBean is configured as a bean in its own right. This allows spring to “enhance” it at runtime to do all the proxying for us. If this were not a separate bean, things would not work. For e.g., the following code won’t work:
// Wrong way to use a ProxyFactoryBean
@Bean
public IDatumProducer datumProducer()
{
JmsInvokerProxyFactoryBean producerProxy = new JmsInvokerProxyFactoryBean();
producerProxy.setServiceInterface(IDatumProducer.class);
producerProxy.setConnectionFactory(jmsConnectionFactory());
producerProxy.setQueue(jmsQueue());
return (IDatumProducer) producerProxy.getObject(); // Wrong: getObject always returns null
}
That’s all!