How to create custom conversions for Spring Data Cassandra? - java

I'm not able to create custom conversions in order to use Currency and Locale as data types.
I'm using a #Configuration annotated class which will be auto-configured with META-INF/spring.factories:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.acme.autoconfigure.ConverterAutoConfiguration
I tried registering the converters directly as beans and also tried to create CassandraCustomConversions bean without success:
#Configuration
public class ConverterAutoConfiguration {
/*
#Bean
public Converter<String, Currency> currencyReadConverter() {
return new Converter<String, Currency>() {
#Override
public Currency convert(String source) {
return Currency.getInstance(source);
}
};
}
#Bean
public Converter<Currency, String> currencyWriteConverter() {
return new Converter<Currency, String>() {
#Override
public String convert(Currency source) {
return source.toString();
}
};
}
#Bean
public Converter<String, Locale> localeReadConverter() {
return new Converter<String, Locale>() {
#Override
public Locale convert(String source) {
return StringUtils.parseLocaleString(source);
}
};
}
#Bean
public Converter<Locale, String> localeWriteConverter() {
return new Converter<Locale, String>() {
#Override
public String convert(Locale source) {
return source.toString();
}
};
}
*/
#Bean
public CassandraCustomConversions cassandraCustomConversions() {
List<Converter<?, ?>> converters = new ArrayList<>();
converters.add(CurrencyReadConverter.INSTANCE);
converters.add(CurrencyWriteConverter.INSTANCE);
converters.add(LocaleReadConverter.INSTANCE);
converters.add(LocaleWriteConverter.INSTANCE);
return new CassandraCustomConversions(converters);
}
enum CurrencyReadConverter implements Converter<String, Currency> {
INSTANCE;
#Override
public Currency convert(String source) {
return Currency.getInstance(source);
}
}
enum CurrencyWriteConverter implements Converter<Currency, String> {
INSTANCE;
#Override
public String convert(Currency source) {
return source.toString();
}
}
enum LocaleReadConverter implements Converter<String, Locale> {
INSTANCE;
#Override
public Locale convert(String source) {
return StringUtils.parseLocaleString(source);
}
}
enum LocaleWriteConverter implements Converter<Locale, String> {
INSTANCE;
#Override
public String convert(Locale source) {
return source.toString();
}
}
}
With the CassandraCustomConversions bean I'm getting an exception directly at startup:
Caused by: org.springframework.data.mapping.MappingException: Cannot resolve DataType for type [class java.lang.String] for property [categoryId] in entity [com.acme.model.Category]; Consider registering a Converter or annotating the property with #CassandraType.
It seems its loosing all the default mappings.
When using the converter beans directly I'm getting the following exception:
org.springframework.data.repository.query.QueryCreationException: Could not create query for public abstract reactor.core.publisher.Flux com.acme.repository.CategoryLocaleReactiveRepository.findByUriNameInAndKeyLocale(java.util.Collection,java.util.Locale)! Reason: Could not inline literal of type java.util.Locale. This happens because the driver doesn't know how to map it to a CQL type. Try passing a TypeCodec or CodecRegistry to literal().
Based on this issue this should be possible somehow: https://github.com/spring-projects/spring-boot/issues/8411

Try adding #WritingConverter and #ReadingConverter to your converters. I believe spring struggles a bit in deciding which converter to use with non custom types.
I created a local project and managed to get the conversion of Locale and Currency working once those 2 annotations were added to the appropriate converter.
I can push my test project to Github and share if you are still having issues.
Reference: https://docs.spring.io/spring-data/cassandra/docs/current/reference/html/#customconversions.java
Example Project: https://github.com/michaelmcfadyen/spring-data-cassandra-custom-converter-example

Related

Save POJO with big Map inside via Spring Data repository leads to StackOverflowError

Generally: i'm reading serialized object (as JSONs) from Kafka Stream and trying to save it to Redis using Spring Data repository.
After a two calls (objects has not been saved to Redis) to repository.save() i get StackOverFlowError:
Exception in thread "processOffers-applicationId-1c24ef63-baae-47b9-beb7-5e6517736bc4-StreamThread-1" java.lang.StackOverflowError
at org.springframework.data.util.Lazy.get(Lazy.java:94)
at org.springframework.data.mapping.model.AnnotationBasedPersistentProperty.usePropertyAccess(AnnotationBasedPersistentProperty.java:277)
at org.springframework.data.mapping.model.BeanWrapper.getProperty(BeanWrapper.java:134)
at org.springframework.data.mapping.model.BeanWrapper.getProperty(BeanWrapper.java:115)
at org.springframework.data.redis.core.convert.MappingRedisConverter.lambda$writeInternal$2(MappingRedisConverter.java:601)
at org.springframework.data.mapping.model.BasicPersistentEntity.doWithProperties(BasicPersistentEntity.java:353)
at org.springframework.data.redis.core.convert.MappingRedisConverter.writeInternal(MappingRedisConverter.java:597)
at org.springframework.data.redis.core.convert.MappingRedisConverter.lambda$writeInternal$2(MappingRedisConverter.java:639)
Serialized POJO look like this:
#Data
#With
#NoArgsConstructor
#AllArgsConstructor
#RedisHash("students")
public class Student {
#Id
#JsonProperty("student_id")
private long id;
#JsonProperty("entities")
private Map<String, Object> entities = new HashMap<>();
}
Map entities contains 100+ Entries, with nested maps (objects).
Interesting part: if i make map empty everything works fine and data instantly saved to Redis.
Corresponding repository for POJO:
#Repository
public interface StudentRepository extends CrudRepository<Student, Long> {
}
Also, i've defined RedisCustomConversion for Long id field:
#Component
#ReadingConverter
public class BytesToLongConverter implements Converter<byte[], Long> {
#Override
public Long convert(final byte[] source) {
ByteBuffer buffer = ByteBuffer.allocate(Long.BYTES);
buffer.put(source);
buffer.flip();
return buffer.getLong();
}
}
#Component
#WritingConverter
public class LongToBytesConverter implements Converter<Long, byte[]> {
#Override
public byte[] convert(final Long source) {
ByteBuffer buffer = ByteBuffer.allocate(Long.BYTES);
buffer.putLong(source);
return buffer.array();
}
}
Redis configuration class looks like this:
#Configuration
#EnableRedisRepositories
public class RedisConfiguration {
#Bean
#Primary
public RedisProperties redisProperties() {
return new RedisProperties();
}
#Bean
public RedisConnectionFactory redisConnectionFactory() {
var config = new RedisStandaloneConfiguration();
var props = redisProperties();
config.setHostName(props.getHost());
config.setPort(props.getPort());
return new JedisConnectionFactory(config);
}
#Bean
public RedisTemplate<String, Object> redisTemplate() {
var template = new RedisTemplate<String, Object>();
template.setConnectionFactory(redisConnectionFactory());
template.setDefaultSerializer(new GenericJackson2JsonRedisSerializer());
return template;
}
#Bean
public RedisCustomConversions redisCustomConversions(LongToBytesConverter longToBytes,
BytesToLongConverter bytesToLong) {
return new RedisCustomConversions(Arrays.asList(longToBytes, bytesToLong));
}
}
UPD:
I've found this issue on Spring Data Redis Jira, but the resolution set as "Fixed", so it's seems strange to me.
I've defined custom WritingConverter and ReadingConverter for my inner map in POJO using GenericJackson2JsonRedisSerializer and everything worked out!
Code:
#Component
#WritingConverter
public class FieldsToBytesConverter implements Converter<Map<String, Object>, byte[]> {
private final RedisSerializer serializer;
public FieldsToBytesConverter() {
serializer = new GenericJackson2JsonRedisSerializer();
}
#Override
public byte[] convert(Map<String, Object> value) {
return serializer.serialize(value);
}
}
#Component
#ReadingConverter
public class BytesToFieldsConverter implements Converter<byte[], Map<String, Object>> {
private final GenericJackson2JsonRedisSerializer serializer;
public BytesToFieldsConverter() {
serializer = new GenericJackson2JsonRedisSerializer();
}
#Override
public Map<String, Object> convert(byte[] value) {
return (Map<String, Object>) serializer.deserialize(value);
}
}

SpringData Redis Repository with complex key

We try to use the Spring Data CrudRepository in our project to provide persistency for our domain objects.
For a start I chose REDIS as backend since in a first experiment with a CrudRepository<ExperimentDomainObject, String> it seemd, getting it running is easy.
When trying to put it in our production code, things got more complicated, because here our domain objects were not necesseriliy using a simple type as key so the repository was CrudRepository<TestObject, ObjectId>.
Now I got the exception:
No converter found capable of converting from type [...ObjectId] to type [byte[]]
Searching for this exception, this answer which led my to uneducated experimenting with the RedisTemplate configuration. (For my experiment I am using emdedded-redis)
My idea was, to provide a RedisTemplate<Object, Object> instead of RedisTemplate<String, Object> to allow using the Jackson2JsonRedisSerializer to do the work as keySerializer also.
Still, calling testRepository.save(testObject) fails.
Please see my code:
I have public fields and left out the imports for the brevity of this example. If they are required (to make this a MVCE) I will happily provide them. Just leave me a comment.
dependencies:
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
implementation group: 'redis.clients', name: "jedis", version: '2.9.0'
implementation group: 'it.ozimov', name: 'embedded-redis', version: '0.7.2'
}
RedisConfiguration:
#Configuration
#EnableRedisRepositories
public class RedisConfiguration {
#Bean
JedisConnectionFactory jedisConnectionFactory() {
return new JedisConnectionFactory();
}
#Bean
public RedisTemplate<Object, Object> redisTemplate() {
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
final RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(jedisConnectionFactory());
template.setDefaultSerializer(jackson2JsonRedisSerializer);
template.setKeySerializer(jackson2JsonRedisSerializer);
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.setValueSerializer(jackson2JsonRedisSerializer);
template.setEnableDefaultSerializer(true);
return template;
}
}
TestObject
#RedisHash("test")
public class TestObject
{
#Id public ObjectId testId;
public String value;
public TestObject(ObjectId id, String value)
{
this.testId = id;
this.value = value; // In experiment this is: "magic"
}
}
ObjectId
#EqualsAndHashCode
public class ObjectId {
public String creator; // In experiment, this is "me"
public String name; // In experiment, this is "fool"
}
TestRepository
#Repository
public interface TestRepository extends CrudRepository<TestObject, ObjectId>
{
}
EmbeddedRedisConfiguration
#Configuration
public class EmbeddedRedisConfiguration
{
private final redis.embedded.RedisServer redisServer;
EmbeddedRedisConfiguration(RedisProperties redisProperties)
{
this.redisServer = new redis.embedded.RedisServer(redisProperties.getPort());
}
#PostConstruct
public void init()
{
redisServer.start();
}
#PreDestroy
public void shutdown()
{
redisServer.stop();
}
}
Application:
#SpringBootApplication
public class ExperimentApplication
{
public static void main(String[] args)
{
SpringApplication.run(ExperimentApplication.class, args);
}
}
Not the desired Answer:
Of course, I might introduce some special ID which is a simple datatype, e.g. a JSON-String which I build manually using jacksons ObjectMapper and then use a CrudRepository<TestObject, String>.
What I also tried in the meantime:
RedisTemplate<String, String>
RedisTemplate<String, Object>
Autowireing a RedisTemplate and setting its default serializer
Registering a Converter<ObjectId, byte[]> to
An autowired ConverterRegistry
An autowired GenericConversionService
but apparently they have been the wrong ones.
Basically, the Redis repositories use the RedisKeyValueTemplate under the hood to store data as Key (Id) and Value pair. So your configuration of RedisTemplate will not work unless you directly use it.
So one way for you will be to use the RedistTemplate directly, something like this will work for you.
#Service
public class TestService {
#Autowired
private RedisTemplate redisTemplate;
public void saveIt(TestObject testObject){
ValueOperations<ObjectId, TestObject> values = redisTemplate.opsForValue();
values.set(testObject.testId, testObject);
}
}
So the above code will use your configuration and generate the string pair in the Redis using the Jackson as the mapper for both the key and the value.
But if you want to use the Redis Repositories via CrudRepository you need to create reading and writing converters for ObjectId from and to String and byte[] and register them as custom Redis conversions.
So let's create reading and writing converters for ObjectId <-> String
Reader
#Component
#ReadingConverter
#Slf4j
public class RedisReadingStringConverter implements Converter<String, ObjectId> {
private ObjectMapper objectMapper = new ObjectMapper();
#Override
public ObjectId convert(String source) {
try {
return objectMapper.readValue(source, ObjectId.class);
} catch (IOException e) {
log.warn("Error while converting to ObjectId.", e);
throw new IllegalArgumentException("Can not convert to ObjectId");
}
}
}
Writer
#Component
#WritingConverter
#Slf4j
public class RedisWritingStringConverter implements Converter<ObjectId, String> {
private ObjectMapper objectMapper = new ObjectMapper();
#Override
public String convert(ObjectId source) {
try {
return objectMapper.writeValueAsString(source);
} catch (JsonProcessingException e) {
log.warn("Error while converting ObjectId to String.", e);
throw new IllegalArgumentException("Can not convert ObjectId to String");
}
}
}
And the reading and writing converters for ObjectId <-> byte[]
Writer
#Component
#WritingConverter
public class RedisWritingByteConverter implements Converter<ObjectId, byte[]> {
Jackson2JsonRedisSerializer<ObjectId> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(ObjectId.class);
#Override
public byte[] convert(ObjectId source) {
return jackson2JsonRedisSerializer.serialize(source);
}
}
Reader
#Component
#ReadingConverter
public class RedisReadingByteConverter implements Converter<byte[], ObjectId> {
Jackson2JsonRedisSerializer<ObjectId> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(ObjectId.class);
#Override
public ObjectId convert(byte[] source) {
return jackson2JsonRedisSerializer.deserialize(source);
}
}
And last add the Redis custom conversations. Just put the code into the RedisConfiguration
#Bean
public RedisCustomConversions redisCustomConversions(RedisReadingByteConverter readingConverter,
RedisWritingByteConverter redisWritingConverter,
RedisWritingStringConverter redisWritingByteConverter,
RedisReadingStringConverter redisReadingByteConverter) {
return new RedisCustomConversions(Arrays.asList(readingConverter, redisWritingConverter, redisWritingByteConverter, redisReadingByteConverter));
}
So now after the converters are created and registered as custom Redis Converters the RedisKeyValueTemplate can use them and your code should work as expected.

Spring Data JPA method + REST: Enum to Integer conversion

I've got an endpoint:
/api/offers/search/findByType?type=X
where X should be an Integer value (an ordinal value of my OfferType instance), whereas Spring considers X a String and will be applying its StringToEnumConverterFactory with the StringToEnum convertor.
public interface OfferRepository extends PagingAndSortingRepository<Offer, Long> {
List<Offer> findByType(#Param("type") OfferType type);
}
So I wrote a custom Converter<Integer, OfferType> which simply get a instance by the given ordinal number:
public class IntegerToOfferTypeConverter implements Converter<Integer, OfferType> {
#Override
public OfferType convert(Integer source) {
return OfferType.class.getEnumConstants()[source];
}
}
Then I registered it properly with a Configuration:
#EnableWebMvc
#Configuration
#RequiredArgsConstructor
public class GlobalMVCConfiguration extends WebMvcConfigurerAdapter {
#Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new IntegerToOfferTypeConverter());
}
}
And I was expected that all requests to findByType?type=X will pass through my converter, but they do not.
Is any way to say that all enums defined as a request parameters have to be provided as an Integer? Furthermore, is any way to say it globally, not just for a specific enum?
EDIT: I've found IntegerToEnumConverterFactory in my classpath that does all I need. And it is registered with DefaultConversionService which is a default service for conversion. How can that be applied?
EDIT2: It's such a trivial thing, I was wondering if there is a property to turn enum conversion on.
EDIT3: I tried to write a Converter<String, OfferType> after I had got String from TypeDescriptor.forObject(value), it didn't help.
EDIT4: My problem was that I had placed custom converter registration into a MVC configuration (WebMvcConfigurerAdapter with addFormatters) instead of a REST Repositories one (RepositoryRestConfigurerAdapter with configureConversionService).
Spring parses the query parameters as Strings. I believe it always uses Converter<String, ?> converters to convert from the query parameters to your repository methods parameters. It uses an enhanced converter service, since it registers its own converters such as Converter<Entity, Resource>.
Therefore you have to create a Converter<String, OfferType>, e.g.:
#Component
public class StringToOfferTypeConverter implements Converter<String, OfferType> {
#Override
public OfferType convert(String source) {
return OfferType.class.getEnumConstants()[Integer.valueOf(source)];
}
}
And then configure this converter to be used by the Spring Data REST, in a class extending RepositoryRestConfigurerAdapter:
#Configuration
public class ConverterConfiguration extends RepositoryRestConfigurerAdapter {
#Autowired
StringToOfferTypeConverter converter;
#Override
public void configureConversionService(ConfigurableConversionService conversionService) {
conversionService.addConverter(converter);
super.configureConversionService(conversionService);
}
}
I tried to add this to the basic tutorial, added a simple enum to the Person class:
public enum OfferType {
ONE, TWO;
}
#Entity
public class Person {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private long id;
private OfferType type;
public OfferType getType() {
return type;
}
public void setType(OfferType type) {
this.type = type;
}
}
And when I call:
http://localhost:8080/people/search/findByType?type=1
I get the result without errors:
{
"_embedded" : {
"people" : [ ]
},
"_links" : {
"self" : {
"href" : "http://localhost:8080/people/search/findByType?type=1"
}
}
}
To implement a global Enum converter, you have to create a factory and register it in the configuration using the method: conversionService.addConverterFactory(). The code below is a modified example from the documentation:
public class StringToEnumFactory implements ConverterFactory<String, Enum> {
public <T extends Enum> Converter<String, T> getConverter(Class<T> targetType) {
return new StringToEnum(targetType);
}
private final class StringToEnum<T extends Enum> implements Converter<String, T> {
private Class<T> enumType;
public StringToEnum(Class<T> enumType) {
this.enumType = enumType;
}
public T convert(String source) {
Integer index = Integer.valueOf(source);
return enumType.getEnumConstants()[index];
}
}
}

Spring REST validation on custom annotation

I'm trying to add some extra validation logic on my REST beans using annotations. This is just an example, but the point is that the annotation is to be used on multiple REST resource objects / DTO's.
I was hoping for a solution like this:
public class Entity {
#NotNull // JSR-303
private String name;
#Phone // Custom phonenumber that has to exist in a database
private String phoneNumber;
}
#Component
public class PhoneNumberValidator implements Validator { // Spring Validator
#Autowired
private PhoneRepository repository;
public boolean supports(Class<?> clazz) {
return true;
}
public void validate(Object target, Errors errors) {
Phone annotation = // find fields with annotations by iterating over target.getClass().getFields().getAnnotation
Object fieldValue = // how do i do this? I can easily get the annotation, but now I wish to do a call to repository checking if the field value exists.
}
}
Did you try JSR 303 bean validator implementations like hibernate validator
e.g. is available here http://www.codejava.net/frameworks/spring/spring-mvc-form-validation-example-with-bean-validation-api
Maven Module A:
public interface RestValidator<A extends Annotation, T> extends ConstraintValidator<A, T>
public interface PhoneValidator extends RestValidator<PhoneNumber, String>
#Target(FIELD)
#Retention(RUNTIME)
#Constraint(validatedBy = PhoneValidator.class) // This usually doesnt work since its a interface
public #interface PhoneNumber {
// JSR-303 required fields (payload, message, group)
}
public class Person {
#PhoneNumber
private String phoneNumber;
}
Maven Module B:
#Bean
LocalValidatorFactoryBean configurationPropertiesValidator(ApplicationContext context, AutowireCapableBeanFactory factory) {
LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean();
factoryBean.setConstraintValidatorFactory(factory(context, factory));
return factoryBean;
}
private ConstraintValidatorFactory factory(final ApplicationContext context, final AutowireCapableBeanFactory factory) {
return new ConstraintValidatorFactory() {
#Override
public <T extends ConstraintValidator<?, ?>> T getInstance(Class<T> key) {
if (RestValidator.class.isAssignableFrom(key)) {
return context.getBean(key);
} else {
return factory.createBean(key);
}
}
#Override
public void releaseInstance(ConstraintValidator<?, ?> instance) {
if (!(instance instanceof RestValidator<?, ?>)) {
factory.destroyBean(instance);
}
}
};
}
#Bean
WebMvcConfigurerAdapter webMvcConfigurerAdapter(final LocalValidatorFactoryBean validatorFactoryBean) {
return new WebMvcConfigurerAdapter() { // Adds the validator to MVC
#Override
public Validator getValidator() {
return validatorFactoryBean;
}
};
}
Then I have a #Component implementation of PhoneValidator that has a Scope = Prototype.
I hate this solution, and I think Spring SHOULD look up on Interface implementations by default, but I'm sure some people that are a lot smarter than me made the decision not to.

Register Spring Converter Programmatically in Spring Boot

I want to register a Spring Converter in a Spring Boot project programmatically. In past Spring projects I've done it in XML like this...
<!-- Custom converters to allow automatic binding from Http requests parameters to objects -->
<!-- All converters are annotated w/#Component -->
<bean id="conversionService"
class="org.springframework.context.support.ConversionServiceFactoryBean">
<property name="converters">
<list>
<ref bean="stringToAssessmentConverter" />
</list>
</property>
</bean>
I'm trying to figure out how to do in Spring Boot's SpringBootServletInitializer
Update: I've made a little progress by passing the StringToAssessmentConverter as an argument to getConversionService, but now I'm getting a "No default constructor found" error for the StringToAssessmentConverter class. I'm not sure why Spring is not seeing the #Autowired constructor.
#SpringBootApplication
public class Application extends SpringBootServletInitializer {
...
#Bean(name="conversionService")
public ConversionServiceFactoryBean getConversionService(StringToAssessmentConverter stringToAssessmentConverter) {
ConversionServiceFactoryBean bean = new ConversionServiceFactoryBean();
Set<Converter> converters = new HashSet<>();
converters.add(stringToAssessmentConverter);
bean.setConverters(converters);
return bean;
}
}
Here's the code for the Converter...
#Component
public class StringToAssessmentConverter implements Converter<String, Assessment> {
private AssessmentService assessmentService;
#Autowired
public StringToAssessmentConverter(AssessmentService assessmentService) {
this.assessmentService = assessmentService;
}
public Assessment convert(String source) {
Long id = Long.valueOf(source);
try {
return assessmentService.find(id);
} catch (SecurityException ex) {
return null;
}
}
}
Full Error
Failed to execute goal org.springframework.boot:spring-boot-maven-
plugin:1.3.2.RELEASE:run (default-cli) on project yrdstick: An exception
occurred while running. null: InvocationTargetException: Error creating
bean with name
'org.springframework.boot.context.properties.ConfigurationPropertiesBindingPo
stProcessor': Invocation of init method failed; nested exception is
org.springframework.beans.factory.UnsatisfiedDependencyException: Error
creating bean with name 'conversionService' defined in
me.jpolete.yrdstick.Application: Unsatisfied dependency expressed through
constructor argument with index 0 of type
[me.jpolete.yrdstick.websupport.StringToAssessmentConverter]: : Error
creating bean with name 'stringToAssessmentConverter' defined in file
[/yrdstick/target/classes/me/jpolete/yrdstick/websupport
/StringToAssessmentConverter.class]: Instantiation of bean failed; nested
exception is org.springframework.beans.BeanInstantiationException: Failed
to instantiate
[me.jpolete.yrdstick.websupport.StringToAssessmentConverter]: No default
constructor found; nested exception is java.lang.NoSuchMethodException:
me.jpolete.yrdstick.websupport.StringToAssessmentConverter.<init>();
nested exception is
org.springframework.beans.factory.BeanCreationException: Error creating
bean with name 'stringToAssessmentConverter' defined in file [/yrdstick
/dev/yrdstick/target/classes/me/jpolete/yrdstick/websupport
/StringToAssessmentConverter.class]: Instantiation of bean failed; nested
exception is org.springframework.beans.BeanInstantiationException: Failed
to instantiate
[me.jpolete.yrdstick.websupport.StringToAssessmentConverter]: No default
constructor found; nested exception is java.lang.NoSuchMethodException:
me.jpolete.yrdstick.websupport.StringToAssessmentConverter.<init>()
The answer is, you only need to anotate your converter as #Component:
This is my converter example
import org.springframework.core.convert.converter.Converter;
#Component
public class DateUtilToDateSQLConverter implements Converter<java.util.Date, Date> {
#Override
public Date convert(java.util.Date source) {
return new Date(source.getTime());
}
}
Then when Spring needs to make convert, the converter is called.
My Spring Boot Version: 1.4.1
Here is my solution:
A TypeConverter Annotation:
#Target({ElementType.TYPE, ElementType.FIELD, ElementType.PARAMETER})
#Retention(RetentionPolicy.RUNTIME)
#Documented
#Component
public #interface TypeConverter {
}
A Converter Registrar:
#Configuration
public class ConverterConfiguration {
#Autowired(required = false)
#TypeConverter
private Set<Converter<?, ?>> autoRegisteredConverters;
#Autowired(required = false)
#TypeConverter
private Set<ConverterFactory<?, ?>> autoRegisteredConverterFactories;
#Autowired
private ConverterRegistry converterRegistry;
#PostConstruct
public void conversionService() {
if (autoRegisteredConverters != null) {
for (Converter<?, ?> converter : autoRegisteredConverters) {
converterRegistry.addConverter(converter);
}
}
if (autoRegisteredConverterFactories != null) {
for (ConverterFactory<?, ?> converterFactory : autoRegisteredConverterFactories) {
converterRegistry.addConverterFactory(converterFactory);
}
}
}
}
And then annotate your converters:
#SuppressWarnings("rawtypes")
#TypeConverter
public class StringToEnumConverterFactory implements ConverterFactory<String, Enum> {
#SuppressWarnings("unchecked")
public <T extends Enum> Converter<String, T> getConverter(Class<T> targetType) {
return new StringToEnum(targetType);
}
private final class StringToEnum<T extends Enum> implements Converter<String, T> {
private Class<T> enumType;
public StringToEnum(Class<T> enumType) {
this.enumType = enumType;
}
#SuppressWarnings("unchecked")
public T convert(String source) {
return (T) Enum.valueOf(this.enumType, source.trim().toUpperCase());
}
}
}
**If you are not on Spring Boot, where automatic registration of converters annotated with #Component (and similar stereotype annotations) is performed and you are not in Web Mvc environment :
#Bean
ConversionService conversionService(){
ConversionServiceFactoryBean factory = new ConversionServiceFactoryBean();
Set<Converter<?, ?>> convSet = new HashSet<Converter<?, ?>>();
convSet.add(new MyConverter()); // or reference bean convSet.add(myConverter());
factory.setConverters(convSet);
factory.afterPropertiesSet();
return factory.getObject();
}
try this:
#SpringBootApplication
public class Application extends SpringBootServletInitializer {
#Bean
public AssessmentService assessmentService(){
return new AssessmentService();
}
#Bean
public StringToAssessmentConverter stringToAssessmentConverter(){
return new StringToAssessmentConverter(assessmentService());
}
#Bean(name="conversionService")
public ConversionService getConversionService() {
ConversionServiceFactoryBean bean = new ConversionServiceFactoryBean();
Set<Converter> converters = new HashSet<Converter>();
//add the converter
converters.add(stringToAssessmentConverter());
bean.setConverters(converters);
return bean.getObject();
}
// separate these class into its own java file if necessary
// Assesment service
class AssessmentService {}
//converter
class StringToAssessmentConverter implements Converter<String, Assessment> {
private AssessmentService assessmentService;
#Autowired
public StringToAssessmentConverter(AssessmentService assessmentService) {
this.assessmentService = assessmentService;
}
public Assessment convert(String source) {
Long id = Long.valueOf(source);
try {
return assessmentService.find(id);
} catch (SecurityException ex) {
return null;
}
}
}
}
or if your StringToAssessmentConverter is already a spring bean:
#Autowired
#Bean(name="conversionService")
public ConversionService getConversionService(StringToAssessmentConverter stringToAssessmentConverter) {
ConversionServiceFactoryBean bean = new ConversionServiceFactoryBean();
Set<Converter> converters = new HashSet<Converter>();
//add the converter
converters.add(stringToAssessmentConverter);
bean.setConverters(converters);
return bean.getObject();
}
For Spring Boot it looks like:
public class MvcConfiguration implements WebMvcConfigurer {
#Override
public void addFormatters(FormatterRegistry registry) {
// do not replace with lambda as spring cannot determine source type <S> and target type <T>
registry.addConverter(new Converter<String, Integer>() {
#Override
public Integer convert(String text) {
if (text == null) {
return null;
}
String trimmed = StringUtils.trimWhitespace(text);
return trimmed.equals("null") ? null : Integer.valueOf(trimmed);
}
});
}
Also had problem with registration custom converter in xml config.
Should to add converter id to annotation-driver
<bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean">
<property name="converters">
<set>
<bean class="ru.javawebinar.topjava.util.StringToLocalDateConverter"/>
</set>
</property>
</bean>
<mvc:annotation-driven conversion-service="conversionService"/>
Reference links:
Spring MVC. Type Conversion,
Spring Core. Type Conversion
If the converter is needed to convert properties to specific objects registering it as component might be too late. I'm using spring-boot-2.3.11 and get an error No converter found capable of converting from type [java.lang.String] to type [org.raisercostin.jedio.WritableDirLocation]
***************************
APPLICATION FAILED TO START
***************************
Description:
Failed to bind properties under 'revobet.feed.lsports.rest.dump-dir' to org.raisercostin.jedio.WritableDirLocation:
Property: myapp.dump-dir
Value: file://localhost/C:\Users\raiser/.myapp/cache
Origin: class path resource [myapp.conf]:-1:1
Reason: No converter found capable of converting from type [java.lang.String] to type [org.raisercostin.jedio.WritableDirLocation]
Action:
Update your application's configuration
solution
public class MyApp implements ApplicationRunner {
public static void main(String[] args) {
LocationsConverterConfig.init();
SpringApplication.run(MyApp.class, args);
}
}
and the Converter
public class LocationsConverterConfig {
public static void init() {
ApplicationConversionService conversionService = (ApplicationConversionService) ApplicationConversionService
.getSharedInstance();
log.info("adding JacksonStringToObjectConverter to handle Locations serialization as soon as possible");
conversionService.addConverter(new JacksonStringToObjectConverter());
}
//#Component
public static class JacksonStringToObjectConverter implements ConditionalGenericConverter {
public JacksonStringToObjectConverter() {
log.info("adding {} to handle Locations serialization as soon as possible", JacksonStringToObjectConverter.class);
}
#Override
public Set<ConvertiblePair> getConvertibleTypes() {
return Collections.singleton(new ConvertiblePair(Object.class, Object.class));
}
#Override
public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
return true;
}
#Override
#Nullable
public Object convert(#Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
...
}
}
}

Categories

Resources