19

I writing application using spring-boot-starter-jdbc (v1.3.0).

The problem that I met: Instance of BeanPropertyRowMapper fails as it cannot convert from java.sql.Timestamp to java.time.LocalDateTime.

In order to copy this problem, I implemented org.springframework.core.convert.converter.Converter for these types.

public class TimeStampToLocalDateTimeConverter implements Converter<Timestamp, LocalDateTime> {

    @Override
    public LocalDateTime convert(Timestamp s) {
        return s.toLocalDateTime();
    }
}

My question is: How do I make available TimeStampToLocalDateTimeConverter for BeanPropertyRowMapper.

More general question, how do I register my converters, in order to make them available system wide?

The following code bring us to NullPointerException on initialization stage:

private Set<Converter> getConverters() {
    Set<Converter> converters = new HashSet<Converter>();
    converters.add(new TimeStampToLocalDateTimeConverter());
    converters.add(new LocalDateTimeToTimestampConverter());

    return converters;
}

@Bean(name="conversionService")
public ConversionService getConversionService() {
    ConversionServiceFactoryBean bean = new ConversionServiceFactoryBean();
    bean.setConverters(getConverters()); 
    bean.afterPropertiesSet();
    return bean.getObject();
}    

Thank you.

M. Deinum
  • 115,695
  • 22
  • 220
  • 224
  • 2
    Just add your converter as a bean... Remove everything else. – M. Deinum Dec 14 '15 at 11:00
  • Confusing `HttpMessageConverter` and `Converter` here. Just add a class that extends `WebMVcConfigurerAdapter` and implement the `addFormatters` method. On the `FormatterRegistry` call `addConverter` for the ones you want to add. – M. Deinum Dec 14 '15 at 11:14
  • 1
    I do not have web environment at all. –  Dec 15 '15 at 12:18
  • Then it indeed won't work :). But the code you have should work, can you add the full configuration class and the error you get (the stack trace) when you use this... – M. Deinum Dec 15 '15 at 12:25
  • To save some code I wouldn't call the `afterPropertiesSet()` and `getObject()`. instead let Spring do that for you, simply return the factory bean. – M. Deinum Dec 15 '15 at 12:34
  • Possible duplicated off https://stackoverflow.com/questions/35025550/register-spring-converter-programmatically-in-spring-boot/41205653#41205653 – deFreitas Dec 01 '17 at 23:29

5 Answers5

4

All custom conversion service has to be registered with the FormatterRegistry. Try creating a new configuration and register the conversion service by implementing the WebMvcConfigurer

@Configuration
public class WebConfig implements WebMvcConfigurer {
 
    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addConverter(new TimeStampToLocalDateTimeConverter());
    }
}

Hope this works.

René Link
  • 48,224
  • 13
  • 108
  • 140
  • 1
    ``` Thanks. But the method name should be addFormatters() ``` – PravyNandas Apr 27 '21 at 22:08
  • 3
    This only works in a WebMVC context. The question asker specifically asked to be able to use the convert system-wide. This does not accomplish that. – Frans Jun 27 '22 at 09:15
2

I'll copy my answer from https://stackoverflow.com/a/72781591/140707 since I think the two questions are similar (so the answer applies to both).


Existing answers didn't work for me:

  • Customizing via WebMvcConfigurerAdapter.addFormatters (or simply annotating the converter with @Component) only works in the WebMvc context and I want my custom converter to be available everywhere, including @Value injections on any bean.
  • Defining a ConversionService bean (via ConversionServiceFactoryBean @Bean or @Component) causes Spring Boot to replace the default ApplicationConversionService on the SpringApplication bean factory with the custom bean you've defined, which will probably be based on DefaultConversionService (in AbstractApplicationContext.finishBeanFactoryInitialization). The problem is that Spring Boot adds some handy converters such as StringToDurationConverter to the standard set in DefaultConversionService, so by replacing it you lose those conversions. This may not be an issue for you if you don't use them, but it means that solution won't work for everyone.

I created the following @Configuration class which did the trick for me. It basically adds custom converters to the ConversionService instance used by Environment (which is then passed on to BeanFactory). This maintains as much backwards compatibility as possible while still adding your custom converter into the conversion services in use.

@Configuration
public class ConversionServiceConfiguration {

    @Autowired
    private ConfigurableEnvironment environment;

    @PostConstruct
    public void addCustomConverters() {
        ConfigurableConversionService conversionService = environment.getConversionService();
        conversionService.addConverter(new MyCustomConverter());
    }
}

Obviously you can autowire a list of custom converters into this configuration class and loop over them to add them to the conversion service instead of the hard-coded way of doing it above, if you want the process to be more automatic.

To make sure this configuration class gets run before any beans are instantiated that might require the converter to have been added to the ConversionService, add it as a primary source in your spring application's run() call:

@SpringBootApplication
public class MySpringBootApplication {
    public static void main(String[] args) {
        SpringApplication.run(new Class<?>[] { MySpringBootApplication.class, ConversionServiceConfiguration.class }, args);
    }
}

If you don't do this, it might work, or not, depending on the order in which your classes end up in the Spring Boot JAR, which determines the order in which they are scanned. (I found this out the hard way: it worked when compiling locally with an Oracle JDK, but not on our CI server which was using a Azul Zulu JDK.)

Note that for this to work in @WebMvcTests, I had to also combine this configuration class along with my Spring Boot application class into a @ContextConfiguration:

@WebMvcTest(controllers = MyController.class)
@ContextConfiguration(classes = { MySpringBootApplication.class, ConversionServiceConfiguration.class })
@TestPropertySource(properties = { /* ... properties to inject into beans, possibly using your custom converter ... */ })
class MyControllerTest {
   // ...
}
Frans
  • 3,670
  • 1
  • 31
  • 29
  • This approach is what solved my problem, thank you @Frans! Using the `addFormatters` in the `WebMvcConfigurer` didn't work. In my case, I wanted to convert a string specified in `application.yml` to its corresponding enum when used with `@Value`, where the string I associate with the enum value is not the same as the string representation of the value (i.e. `public enum MyEnum { ONE("a-different-value") ... }`). – LarryW Aug 17 '23 at 18:50
0

I suggest to use @Autowired and the related dependency injection mechanism of spring to use a single ConversionService instance throughout your application. The ConversionService will be instantiated within the configuration.

All Converters to be available application wide receive an annotation (e.g. @AutoRegistered). On application start a @Component FormatterRegistrar (Type name itself is a bit misleading, yes it is "...Registrar" as it does the registering. And @Component as it is fully spring managed and requires dependency injection) will receive @AutoRegistered List of all annotated Converters.

See this thread for concrete implementation details. We use this mechanism within our project and it works out like a charm.

Community
  • 1
  • 1
s10z
  • 1,080
  • 11
  • 13
0

org.springframework.web.servlet.config.annotation.WebMvcConfigurer or any on its implementation is one stop place for any kind of customization in spring boot project. It prvoides various methods, for your Converter requirement.

Just create a new Converter by extending org.springframework.core.convert.converter.Converter<S, T>. Then register it with Spring by your class overriding method org.springframework.web.servlet.config.annotation.WebMvcConfigurer.addFormatters(FormatterRegistry)

Note there are Other types of Converter also which basically starts from ConditionalConverter.

UkFLSUI
  • 5,509
  • 6
  • 32
  • 47
SauriBabu
  • 414
  • 6
  • 15
  • This only works in a WebMVC context. The question asker specifically asked to be able to use the convert system-wide. This does not accomplish that. – Frans Jun 27 '22 at 09:15
-2

Trying adding

@Converter(autoApply = true)

Its needs to be placed over the convertor class. This works for me in case of Convertor needed for Localdate for interacting to DB.

@Converter(autoApply = true)
public class LocalDateAttributeConverter implements AttributeConverter<LocalDate, Date> {

    @Override
    public Date convertToDatabaseColumn(LocalDate locDate) {
      return (locDate == null ? null : Date.valueOf(locDate));
    }

    @Override
    public LocalDate convertToEntityAttribute(Date sqlDate) {
      return (sqlDate == null ? null : sqlDate.toLocalDate());
    }
}

This is now applied automatically while interacting with DB.

Ankit Bansal
  • 2,162
  • 8
  • 42
  • 79