8

First of all ... Im relatively new in Spring, I use spring 3.x and I DONT LIKE SPRING'S XML CONFIGURATION FILES ... I dont want for every refactoring I do, to run into XML file for updates ...

I'm trying to configure spring in a way that for any request, if I have some @RequestParam/@RequestBody/@PathVariable etc with type other than String in my hadlers, spring will convert values to that type correctly or put null to handler's args (I never use primitive types in handler arguments). So far so good ...

Until now I've registered all converter/converterFactory classes like this:

 <bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean">
     <property name="converters">
         <list>
             <!-- converters is a set of both converters and converterfactories -->
             <bean class="controller.converters.enumConverter" />
             <bean class="controller.converters.integerConverter" />
             <bean class="controller.converters.objects.FooConverter" />
             ...
         </list>
     </property>
 </bean>

Is there any way to register converters with annotations?

Can anything (or just basic stuff) about spring XML be done with annotations only, and get rid of XML configuration once and for all? ... and how?

ApollonDigital
  • 913
  • 3
  • 15
  • 24

7 Answers7

9

Spring does not have annotation support for Converters, but you can build your own.

All you need is an custom qualifier annotation (lets call it @AutoRegistered ) and some kind of Converter/Formatter Registrar (implements FormatterRegistrar) that registers all the Spring Beans with this @AutoRegistered annotation (and some xml to register this registration service).

Then you need to annotate your conveter with this annotation (and some other annotation to make it a spring bean) and that is all.

@AutoRegistered annotation:

@Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface AutoRegistered {}

Registration service:

import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.convert.converter.Converter;
import org.springframework.format.FormatterRegistrar;
import org.springframework.format.FormatterRegistry;

public class AutoregisterFormatterRegistrar implements FormatterRegistrar {

    /**
     * All {@link Converter} Beans with {@link AutoRegistered} annotation.
     * If spring does not find any matching bean, then the List is {@code null}!.
     */
    @Autowired(required = false)
    @AutoRegistered
    private List<Converter<?, ?>> autoRegisteredConverters;


    @Override
    public void registerFormatters(final FormatterRegistry registry) {
        if (this.autoRegisteredConverters != null) {
            for (Converter<?, ?> converter : this.autoRegisteredConverters) {
                registry.addConverter(converter);
            }
        }
    }
}

XML configuration for the registrar:

<bean id="applicationConversionService"
    class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
    <property name="formatterRegistrars">
        <set>
            <bean
                class="AutoregisterFormatterRegistrar"
                autowire="byType" />
        </set>
    </property>
</bean>

BTW for your enum converter you do not need a ConversionFactory - a simple converter is enough:

@AutoRegistered
@Component
public class EnumConverter implements Converter<Enum<?>, String> {

    /** Use the same immutable value instead of creating an new array every time. */
    private static final Object[] NO_PARAM = new Object[0];

    /** The prefix of all message codes. */
    private static final String PREFIX = "label_";

    /** The separator in the message code, between different packages
        as well as between package can class. */
    private static final String PACKAGE_SEPARATOR = "_";

    /** The separator in the message code, between the class name
        and the enum case name. */
    private static final String ENUM_CASE_SEPARATOR = "_";

    /** The message source. */
    private MessageSource messageSource;

    @Autowired
    public EnumConverter(final MessageSource messageSource) {
        if (messageSource == null) {
            throw new RuntimeException("messageSource must not be null");
        }

        this.messageSource = messageSource;
    }

    @Override
    public String convert(final Enum<?> source) {
        if (source != null) {
            String enumValueName = source.name();
            String code = PREFIX + source.getClass().getName().toLowerCase().
                  replace(".", PACKAGE_SEPARATOR)
            + ENUM_CASE_SEPARATOR + enumValueName.toLowerCase();

            String message = messageSource.getMessage(code, NO_PARAM, enumValueName,
                                                  LocaleContextHolder.getLocale());

             return message;
         } else {
            return "";
         }
     }   
}
Steve Chambers
  • 37,270
  • 24
  • 156
  • 208
Ralph
  • 118,862
  • 56
  • 287
  • 383
  • FormatterRegistrar? I guess you mean FormatterRegistry ... Is List> autoRegisteredConverters filled automatically? ps. The ConverterFactory solution for enums is more compact ... check this [implementation](http://static.springsource.org/spring/docs/3.0.x/spring-framework-reference/html/validation.html#core-convert-ConverterFactory-SPI) – ApollonDigital Dec 08 '12 at 15:22
  • It is FormatterRegistrar (a Spring 3.1 Feature) http://static.springsource.org/spring/docs/3.1.x/javadoc-api/org/springframework/format/FormatterRegistrar.html – Ralph Dec 08 '12 at 20:55
7

Automatic registration of Converter beans is also provided by Spring Boot when @EnableAutoConfiguration is turned on - see Spring Boot features. It appears that no additional annotations (other than marking each converter bean as a @Component) is required for this.

Steve Chambers
  • 37,270
  • 24
  • 156
  • 208
  • Usuful hint. do you know how to enable this in junit test? – Heri Jun 04 '15 at 07:46
  • Sory @Heri, I came across this when looking at the source code for a different project but Spring Boot isn't used on the project I'm working on and I ended up going for a solution that was closer to the accepted answer. So couldn't advise about the jUnit tests (assume here you mean integration tests that use jUnit?) – Steve Chambers Jun 04 '15 at 11:30
  • Yes, exactly. I did not succeed to have the Converters automtically registered. And since i didn't succeed I didn't try it neither in a normal bootrun of the application because I reverted the code changes (I have a solution similar to gmateo with a custom annotation which are added to the spring created ConversionService (retrieved in getConverters()):. – Heri Jun 04 '15 at 17:05
  • @Bean public ConversionService getConversionService() { ConversionServiceFactoryBean bean = new ConversionServiceFactoryBean(); bean.setConverters( getConverters() ); bean.afterPropertiesSet(); ConversionService object = bean.getObject(); return object; } – Heri Jun 04 '15 at 17:05
  • For JUnit based integration testing (with spring-boot 1.2.7): @RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(classes = Application.class) @WebIntegrationTest("server.port:0") public abstract class ApplicationTestBase { ... } – Ogmios Feb 11 '16 at 12:28
6

First you have to define an annotation: TypeConverter

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

Then you have to register the conversion service and add all of the beans that have the annotation. This will be done with the following post processor:

public class ConverterRegistryPostProcessor implements BeanDefinitionRegistryPostProcessor {

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

public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
    Map<String, Object> beansWithAnnotation = beanFactory.getBeansWithAnnotation(TypeConverter.class);
    Collection converters = beansWithAnnotation.values();
    DefaultConversionService conversionService = (DefaultConversionService) beanFactory.getBean("conversionService");
    for (Object converter : converters) {
        conversionService.addConverter((Converter<?, ?>) converter);
    }
}
}

If you need more details check this blog entry

gmateo
  • 71
  • 2
  • 2
  • 1
    Welcome to Stack Overflow! Thanks for posting your answer! Please be sure to read the [FAQ on Self-Promotion](http://stackoverflow.com/faq#promotion) carefully. Also note that it is *required* that you post a disclaimer every time you link to your own site/product. – Andrew Barber Mar 15 '13 at 02:42
5

The approach outlined by @Ralph is neat, I have +1'd his answer. Let me also recommend an alternative approach which is using @Configuration support - essentially a way to configure Spring beans using Java instead of xml. With this approach the message converters can be registered this way:

 @Configuration
 @EnableWebMvc
 @ComponentScan(...)
 public class CustomConfig extends WebMvcConfigurerAdapter {


    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        converters.add(new EnumConverter());
                converters.add(new FooConverter());
                ...
    }

 }
Biju Kunjummen
  • 49,138
  • 14
  • 112
  • 125
1

With Spring MVC 3.2, you can create a conversion service class that extends the DefaultFormattingConversionService e.g

ApplicationConversionService.java

import org.springframework.format.support.DefaultFormattingConversionService;
import org.springframework.stereotype.Component;

@Component("conversionService")
public class ApplicationConversionService extends DefaultFormattingConversionService  { 

    public ApplicationConversionService(){
        //DefaultFormattingConversionService's default constructor
        //creates default formatters and converters
        super(); //no need for explicit super()?

        //add custom formatters and converters
        addConverter(new MyConverter());
    }

}

and specify it in the spring config e.g

dispatcher-servlet.xml

<mvc:annotation-driven conversion-service="conversionService"/>
Timothy Anyona
  • 342
  • 4
  • 14
  • Yes, above solution worked like a charm for me and its best suited for hybrid configurations which has XML based configuration and annotation driven configuration is also enabled. – abhijeet badale Jan 04 '20 at 14:42
0

I am not sure if this works in Spring 3 but this is the solution for Spring 4:

@Configuration
@EnableWebMvc
class WebMvcContext extends WebMvcConfigurerAdapter {

    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addConverter(new DateConverter("yyyy-MM-dd HH:mm:ss"));
        //registry.addConverter(anotherConverter);
    }
}

DateConverter is a custom converter:

public class DateConverter implements Converter<String, Date>{
    private static final Logger LOGGER = LoggerFactory.getLogger(DateConverter.class);
    private final String dateFormat;
    private final SimpleDateFormat formatter;
    public DateConverter(String dateFormatPattern) {
        this.dateFormat = dateFormatPattern;
        this.formatter = new SimpleDateFormat(dateFormatPattern);
    }

    @Override
    public Date convert(String source) {
        Date date = null;
        try {
            date = formatter.parse(source);
        } catch (ParseException e) {
            e.printStackTrace();
        }
        return date;
    }
}
fall
  • 984
  • 11
  • 33
0

Do the following:

@SpringBootApplication
@PropertySources({ @PropertySource("classpath:application.properties") })
public class YourApplication {

public static void main(String[] args) {
    SpringApplication.run(YourApplication.class, args);

    ConversionService conversionService = DefaultConversionService.getSharedInstance();
    ConverterRegistry converters = (ConverterRegistry) conversionService;
    converters.addConverter(new LocalDateToStringConverter());

    }

}

Where LocalDateToStringConverter looks like this.

public class LocalDateToStringConverter implements Converter<LocalDate, String> {

@Override
public String convert(LocalDate localDate) {
    try {

        String date = localDate.format(DateTimeFormatter.ofPattern("dd-MM-yyyy"));
        return date;
    } catch(DateTimeParseException ex) {
    }
    return null;
  }
}
Boertie
  • 11
  • 3