Thursday, March 14, 2013

Springframework - Using annotations to register Custom Converters

Source Code: Example. Springframework 3.2.1.RELEASE

Problem

In order to register custom converters we have to do the following:

<bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean">
     <property name="converters">
         <list>
             <!-- list of converters-->
             <bean class="org.anotes.springexample.converter.PersonToClient" />
             <bean class="org.anotes.springexample.converter.ClientToPerson" />
             ...
         </list>
     </property>
 </bean>

We want to register the converters using only some custom annotation.

Solution

We have to follow the next steps:

Create the annotation

This annotation will be used to mark the classes that are converters

package org.anotes.spring.stereotype;
import org.springframework.stereotype.Component;
import java.lang.annotation.*;

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface TypeConverter {

}

Create the custom BeanDefinitionRegistryPostProcessor

We need to do the following tasks: 1. Add programmatically the bean "conversionService" to the applicationContext 2. Add all the custom converters (identified by the annotation TypeConverter) to the bean "conversionService" define in the previous step.

To complete these tasks we need to create a custom BeanDefinitionRegistryPostProcessor

public class ConverterRegistryPostProcessor implements  BeanDefinitionRegistryPostProcessor, BeanPostProcessor, ApplicationContextAware {

private final String CONVERSION_SERVICE_NAME = "conversionService";

public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry                    registry) throws BeansException {
    registry.registerBeanDefinition(CONVERSION_SERVICE_NAME, BeanDefinitionBuilder.rootBeanDefinition(ConversionServiceFactoryBean.class).getBeanDefinition());
}

public void postProcessBeanFactory(ConfigurableListableBeanFactory          beanFactory) throws BeansException {    
}

public Object postProcessBeforeInitialization(Object bean, String           beanName) throws BeansException {
    if (CONVERSION_SERVICE_NAME.equals(beanName)) {
        Map<String, Converter> beansOfType = appCtx.getBeansOfType(Converter.class);
        ConversionServiceFactoryBean conversionfactoryBean = (ConversionServiceFactoryBean) bean;
        Set converters = new HashSet(beansOfType.values());
        conversionfactoryBean.setConverters(converters);
    }
    return bean;
}

public Object postProcessAfterInitialization(Object bean, String            beanName) throws BeansException {
    return bean;
}

ApplicationContext appCtx;

public void setApplicationContext(ApplicationContext                        applicationContext) throws BeansException {
    appCtx = applicationContext;
}

}

In the method: "postProcessBeanDefinitionRegistry" the "conversionService" is added to the context.

In the method: "postProcessBeanFactory" we collect all the beans that have the annotation "TypeConverter" and then add all of these to the conversion service.

Take into account that as the converters are beans you can autowired other beans in these converters.

Register the PostProcessor

We have to include in the application context the post processor as bean:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.2.xsd">

    <context:component-scan base-package="org.anotes.springexample"/>

    <bean class="org.anotes.spring.postprocessor.ConverterRegistryPostProcessor"/>

</beans>

Test

We have the following converter:

@TypeConverter
public class ClientToPerson implements Converter<Client,Person> {
    @Override
    public Person convert(Client source) {
        Person person = new Person();
        BeanUtils.copyProperties(source,person);
        return person;
    }
}

We test that the converter function properly with :

public static void main(String[] args) {
    String configFiles = "classpath*:/app-context.xml";
    ApplicationContext context = new ClassPathXmlApplicationContext(configFiles);
    Client client = createClient();
    ConversionService conversionService = (ConversionService) context.getBean("conversionService");
    Person person = conversionService.convert(client, Person.class);
    logger.info("Client:{}", client );
    logger.info("Person:{}", person );
}

And we get the following:

springexample.Main Client:Client{name='Joseph', gender='M', address='St Main Square', subscriptionDate=Thu Mar 14 20:37:29 COT 2013} [INFO ]
springexample.Main Person:Person{name='Joseph', gender='M', address='St Main Square'} [INFO ]

With the above we see that all function correctly.