Spring conversion service - from List<A> to List<B>
I have registered a custom conversion service in a Spring 3 application. It works well for POJOs but it does not work on Lists.
For example, I convert from String
to Role
an开发者_如何学JAVAd it works fine, but not for List<String>
to List<Role>
.
All kind of ClassCastExceptions
fly in the application when trying to inject Lists, no matter what they contain. The Conversion service calls the convertor for List<String>
to List<Role>
for all.
This makes sense if you think about it. Type erasure is the culprit here and the convertion service actually sees List
to List
.
Is there a way to tell the conversion service to work with generics?
What other options do I have?
Another way to convert from List<A>
to List<B>
in Spring is to use ConversionService#convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType)
. Javadoc
This approach just needs a Converter<A,B>
.
Call to conversionService for collection types:
List<A> source = Collections.emptyList();
TypeDescriptor sourceType = TypeDescriptor.collection(List.class, TypeDescriptor.valueOf(A.class));
TypeDescriptor targetType = TypeDescriptor.collection(List.class, TypeDescriptor.valueOf(B.class));
List<B> target = (List<B>) conversionService.convert(source, sourceType, targetType);
The converter:
public class ExampleConverter implements Converter<A, B> {
@Override
public B convert(A source) {
//convert
}
}
I've encountered same problem, and by performing a little investigation find a solution (works for me). If you have two classes A and B, and have a registered converter e.g. SomeConverter implements Converter, than, to convert list of A to list of B you should do next:
List<A> listOfA = ...
List<B> listOfB = (List<B>)conversionService.convert(listOfA,
TypeDescriptor.collection(List.class, TypeDescriptor.valueOf(A.class)),
TypeDescriptor.collection(List.class, TypeDescriptor.valueOf(B.class)));
I'm using the Spring GenericConversionService.
The convert method in question has the following signature:
public <T> T convert(Object source, Class<T> targetType)
List<B>.class
is not valid Java syntax.
This worked for me:
List<A> sourceList = ...;
conversionService.convert(sourceList, (Class<List<B>>)(Class<?>)List.class);
Got the idea from here: StackOverflow - Class object of generic class
Edit:
The above did not truly work. No compile errors, however it resulted in the sourceList not being converted, and being assigned to the targetList. This resulted in various exceptions downstream while attempting to use the targetList.
My current solution is to extend Spring's GenericConversionService and add my own convert method to handle lists.
Here's the convert method:
@SuppressWarnings({"rawtypes", "unchecked"})
public <T> List<T> convert(List<?> sourceList, Class<T> targetClass) {
Assert.notNull(sourceList, "Cannot convert null list.");
List<Object> targetList = new ArrayList();
for (int i = 0; i < sourceList.size(); i++) {
Object o = super.convert(sourceList.get(i), targetClass);
targetList.add(o);
}
return (List<T>) targetList;
}
And it can be called like the following:
List<A> sourceList = ...;
List<B> targetList = conversionService.convert(sourceList, B.class);
Love to see if anyone has a better way to handle this common scenario.
Ralph was correct, it works if you have a converter that converts from A
To B
.
I didn't need a converter for List<A>
to List<B>
.
I have a work-around for you. It's not the prettiest thing in the world, but if using the Spring conversion service is important to you it might just do the trick.
As you pointed out the problem is type erasure. The best you can tell Spring via the ConversionService interface is that you can convert List to List, which to Spring makes no sense and that's why the converter doesn't work (that's a guess on my part .. I don't think it's a bug, in other words).
To do what you want you'll need to create and use a type-specific implementation of the generic interface and/or class:
public interface StringList extends List<String> { }
public class StringArrayList extends ArrayList<String> implements StringList { }
In this example you would create your list using StringArrayList instead of ArrayList and register either the implementation class (StringArrayList.class) or the interface class (StringList.class) via the ConversionService interface. It seems like you want to register the interface .. but if you only want to register the implementation class then you don't need to define the interface at all.
I hope this helps.
I had this same problem with dozer and found this: How to map collections in dozer
So to do this with spring conversion service I wrote a simple utility method:
public static <T, U> List<U> convertList(final ConversionService service, final List<T> source, final Class<U> destType) {
final List<U> dest = new ArrayList<U>();
if (source != null) {
for (final T element : source) {
dest.add(service.convert(element, destType));
}
}
return dest;
}
Essentially the only difference is that this takes in a spring conversion service instead of a dozer mapper instance. You can now use this method to convert List's with type safety. This is similar to Jeff B's answer above except that you don't need to suppress warnings and this method doesn't necessarily need to belong to a given Convert...it's a utility method.
And, if you're beyond Java 7, there's always a way to do it with streams:
List<B> listB = listA.stream().map(a -> converterService.convert(a, B.class)).collect(Collectors.toList());
One relatively clean work-around I found was to create a proxy converter class which accepts the raw object to be converted and delegates to an extended version of the Converter interface which supports a boolean canConvert
semantics so as to allow the implementation to decide whether or not it can convert that source data or not.
e.g.:
The interface:
public interface SqlRowSetToCollectionConverter<T> extends Converter<SqlRowSet,Collection<T>> {
boolean canConvert (SqlRowSet aSqlRowSet);
}
The proxy class:
public class SqlRowSetToCollectionConverterService implements Converter<SqlRowSet, Collection<?>> {
private SqlRowSetToCollectionConverter<?>[] converters;
public void setConverters (SqlRowSetToCollectionConverter<?>[] aConverters) {
converters = aConverters;
}
@Override
public Collection<?> convert (SqlRowSet aSource) {
for (SqlRowSetToCollectionConverter<?> converter : converters) {
if (converter.canConvert (aSource)) {
return (converter.convert(aSource));
}
}
return null;
}
}
Then I would register the proxy class with Spring's Conversion Service and register with the proxy class all the extended interface implementations:
<bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
<property name="converters">
<set>
<bean class="com.hilton.hms.data.convert.SqlRowSetToCollectionConverterService">
<property name="converters">
<bean class="com.hilton.hms.data.convert.SqlRowSetToConfigCollectionConverter" />
</property>
</bean>
</set>
</property>
</bean>
GenericConversionService has now the method
@Nullable
public Object convert(@Nullable Object source, TypeDescriptor targetType) {
return convert(source, TypeDescriptor.forObject(source), targetType);
}
so now, we can pass only one TypeDescriptor
public <S, @NonNull T> Collection<T> convert(Collection<S> source, Class<T> targetType) {
return (Collection<@NonNull T>) requireNonNull(conversionService.convert(source, TypeDescriptor.collection(List.class, TypeDescriptor.valueOf(targetType))));
}
精彩评论