I’m trying to read a record with JdbcClient.
One column should be converted to a custom type.
From the documentation and multiple web searches I understand I should create and register a Converter to accomplish this.
Unfortunately I always and up with the same org.springframework.core.convert.ConverterNotFoundException: No converter found capable of converting from type [java.lang.String] to type [com.example.demo.DemoApplication$PersonName]
exception.
How do I customize the ConversionService
used by JdbcClient
?
I know I can work around this problem by using a RowMapper
but I’m trying to avoid it.
This is the code I’m running with java 17 and spring boot 3.3.0.
package com.example.demo;
import java.util.List;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesBinding;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.ConverterRegistry;
import org.springframework.core.convert.support.GenericConversionService;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.format.FormatterRegistry;
import org.springframework.jdbc.core.simple.JdbcClient;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import jakarta.annotation.PostConstruct;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
System.out.println("*** DemoApplication.main");
SpringApplication.run(new Class[]{
MyWebConfiguration.class,
MyOtherConfiguration.class,
DemoApplication.class
},
args);
}
private final JdbcClient jdbcClient;
public DemoApplication(JdbcClient jdbcClient) {
System.out.println("*** DemoApplication.DemoApplication");
this.jdbcClient = jdbcClient;
}
@PostConstruct
public void init() {
final List<Person> list = jdbcClient.sql("select 'John;Doe' as "name" from dual")
.query(Person.class)
.list();
System.out.println("list = " + list);
}
@Configuration
public static class MyWebConfiguration implements WebMvcConfigurer {
private final StringToPersonNameConverter stringToPersonNameConverter;
public MyWebConfiguration(StringToPersonNameConverter stringToPersonNameConverter) {
System.out.println("*** MyConfiguration1.MyConfiguration1");
this.stringToPersonNameConverter = stringToPersonNameConverter;
}
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(stringToPersonNameConverter);
}
}
@Configuration
public static class MyOtherConfiguration {
private final ConfigurableEnvironment environment;
private final ConverterRegistry converterRegistry;
private final GenericConversionService conversionService;
private final StringToPersonNameConverter stringToPersonNameConverter;
public MyOtherConfiguration(
ConfigurableEnvironment environment,
ConverterRegistry converterRegistry,
GenericConversionService conversionService,
StringToPersonNameConverter stringToPersonNameConverter) {
System.out.println("*** MyConfiguration2.MyConfiguration2");
this.environment = environment;
this.converterRegistry = converterRegistry;
this.conversionService = conversionService;
this.stringToPersonNameConverter = stringToPersonNameConverter;
}
@PostConstruct
public void addCustomConverters() {
environment.getConversionService().addConverter(stringToPersonNameConverter);
converterRegistry.addConverter(stringToPersonNameConverter);
conversionService.addConverter(stringToPersonNameConverter);
}
}
@Component
@ConfigurationPropertiesBinding
public static class StringToPersonNameConverter implements Converter<String, PersonName> {
public StringToPersonNameConverter() {
System.out.println("*** StringToPersonNameConverter.StringToPersonNameConverter");
}
@Override
public PersonName convert(String source) {
if (source.contains(";")) {
String[] parts = source.split(";", 2);
return new PersonName(parts[0], parts[1]);
}
return new PersonName(source, "?");
}
}
public record Person(PersonName name) {
}
public record PersonName(String firstName, String lastName) {
}
}
The output looks like this:
*** DemoApplication.main
2024-06-21T16:16:20.049+02:00 INFO 13856 --- [demo] [ main] com.example.demo.DemoApplication : Starting DemoApplication using Java 17.0.11 with PID 13856 (C:Workspacedemobuildclassesjavamain started by kristof in C:Workspacedemo)
2024-06-21T16:16:20.055+02:00 INFO 13856 --- [demo] [ main] com.example.demo.DemoApplication : No active profile set, falling back to 1 default profile: "default"
*** StringToPersonNameConverter.StringToPersonNameConverter
2024-06-21T16:16:20.615+02:00 INFO 13856 --- [demo] [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port 8080 (http)
2024-06-21T16:16:20.621+02:00 INFO 13856 --- [demo] [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2024-06-21T16:16:20.621+02:00 INFO 13856 --- [demo] [ main] o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/10.1.24]
2024-06-21T16:16:20.657+02:00 INFO 13856 --- [demo] [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2024-06-21T16:16:20.657+02:00 INFO 13856 --- [demo] [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 575 ms
*** MyConfiguration1.MyConfiguration1
*** MyConfiguration2.MyConfiguration2
*** DemoApplication.DemoApplication
2024-06-21T16:16:21.179+02:00 WARN 13856 --- [demo] [ main] ConfigServletWebServerApplicationContext : Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'demoApplication': Invocation of init method failed
2024-06-21T16:16:21.182+02:00 INFO 13856 --- [demo] [ main] o.apache.catalina.core.StandardService : Stopping service [Tomcat]
2024-06-21T16:16:21.189+02:00 INFO 13856 --- [demo] [ main] .s.b.a.l.ConditionEvaluationReportLogger :
Error starting ApplicationContext. To display the condition evaluation report re-run your application with 'debug' enabled.
2024-06-21T16:16:21.198+02:00 ERROR 13856 --- [demo] [ main] o.s.boot.SpringApplication : Application run failed
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'demoApplication': Invocation of init method failed
at org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor.postProcessBeforeInitialization(InitDestroyAnnotationBeanPostProcessor.java:222) ~[spring-beans-6.1.8.jar:6.1.8]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyBeanPostProcessorsBeforeInitialization(AbstractAutowireCapableBeanFactory.java:422) ~[spring-beans-6.1.8.jar:6.1.8]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1780) ~[spring-beans-6.1.8.jar:6.1.8]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:600) ~[spring-beans-6.1.8.jar:6.1.8]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:522) ~[spring-beans-6.1.8.jar:6.1.8]
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:337) ~[spring-beans-6.1.8.jar:6.1.8]
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[spring-beans-6.1.8.jar:6.1.8]
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:335) ~[spring-beans-6.1.8.jar:6.1.8]
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:200) ~[spring-beans-6.1.8.jar:6.1.8]
at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:975) ~[spring-beans-6.1.8.jar:6.1.8]
at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:962) ~[spring-context-6.1.8.jar:6.1.8]
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:624) ~[spring-context-6.1.8.jar:6.1.8]
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:146) ~[spring-boot-3.3.0.jar:3.3.0]
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:754) ~[spring-boot-3.3.0.jar:3.3.0]
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:456) ~[spring-boot-3.3.0.jar:3.3.0]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:335) ~[spring-boot-3.3.0.jar:3.3.0]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1363) ~[spring-boot-3.3.0.jar:3.3.0]
at com.example.demo.DemoApplication.main(DemoApplication.java:25) ~[main/:na]
Caused by: org.springframework.core.convert.ConverterNotFoundException: No converter found capable of converting from type [java.lang.String] to type [com.example.demo.DemoApplication$PersonName]
at org.springframework.core.convert.support.GenericConversionService.handleConverterNotFound(GenericConversionService.java:294) ~[spring-core-6.1.8.jar:6.1.8]
at org.springframework.core.convert.support.GenericConversionService.convert(GenericConversionService.java:185) ~[spring-core-6.1.8.jar:6.1.8]
at org.springframework.core.convert.ConversionService.convert(ConversionService.java:92) ~[spring-core-6.1.8.jar:6.1.8]
at org.springframework.jdbc.core.SimplePropertyRowMapper.mapRow(SimplePropertyRowMapper.java:141) ~[spring-jdbc-6.1.8.jar:6.1.8]
at org.springframework.jdbc.core.RowMapperResultSetExtractor.extractData(RowMapperResultSetExtractor.java:94) ~[spring-jdbc-6.1.8.jar:6.1.8]
at org.springframework.jdbc.core.RowMapperResultSetExtractor.extractData(RowMapperResultSetExtractor.java:61) ~[spring-jdbc-6.1.8.jar:6.1.8]
at org.springframework.jdbc.core.JdbcTemplate$1.doInPreparedStatement(JdbcTemplate.java:733) ~[spring-jdbc-6.1.8.jar:6.1.8]
at org.springframework.jdbc.core.JdbcTemplate.execute(JdbcTemplate.java:658) ~[spring-jdbc-6.1.8.jar:6.1.8]
at org.springframework.jdbc.core.JdbcTemplate.query(JdbcTemplate.java:723) ~[spring-jdbc-6.1.8.jar:6.1.8]
at org.springframework.jdbc.core.JdbcTemplate.query(JdbcTemplate.java:754) ~[spring-jdbc-6.1.8.jar:6.1.8]
at org.springframework.jdbc.core.JdbcTemplate.query(JdbcTemplate.java:767) ~[spring-jdbc-6.1.8.jar:6.1.8]
at org.springframework.jdbc.core.JdbcTemplate.query(JdbcTemplate.java:825) ~[spring-jdbc-6.1.8.jar:6.1.8]
at org.springframework.jdbc.core.simple.DefaultJdbcClient$DefaultStatementSpec$IndexedParamMappedQuerySpec.list(DefaultJdbcClient.java:346) ~[spring-jdbc-6.1.8.jar:6.1.8]
at com.example.demo.DemoApplication.init(DemoApplication.java:44) ~[main/:na]
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) ~[na:na]
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
at java.base/java.lang.reflect.Method.invoke(Method.java:568) ~[na:na]
at org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor$LifecycleMethod.invoke(InitDestroyAnnotationBeanPostProcessor.java:457) ~[spring-beans-6.1.8.jar:6.1.8]
at org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor$LifecycleMetadata.invokeInitMethods(InitDestroyAnnotationBeanPostProcessor.java:401) ~[spring-beans-6.1.8.jar:6.1.8]
at org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor.postProcessBeforeInitialization(InitDestroyAnnotationBeanPostProcessor.java:219) ~[spring-beans-6.1.8.jar:6.1.8]
... 17 common frames omitted