ConversionService detects generic type declaration on target class behind proxy as well

Issue: SPR-14822
This commit is contained in:
Juergen Hoeller 2016-10-21 12:24:12 +02:00
parent 52b029d71d
commit f7d740fa69
5 changed files with 129 additions and 41 deletions

View File

@ -24,6 +24,7 @@ import java.util.concurrent.ConcurrentHashMap;
import org.springframework.context.EmbeddedValueResolverAware;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.core.DecoratingProxy;
import org.springframework.core.GenericTypeResolver;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.TypeDescriptor;
@ -97,9 +98,13 @@ public class FormattingConversionService extends GenericConversionService
static Class<?> getFieldType(Formatter<?> formatter) {
Class<?> fieldType = GenericTypeResolver.resolveTypeArgument(formatter.getClass(), Formatter.class);
if (fieldType == null && formatter instanceof DecoratingProxy) {
fieldType = GenericTypeResolver.resolveTypeArgument(
((DecoratingProxy) formatter).getDecoratedClass(), Formatter.class);
}
if (fieldType == null) {
throw new IllegalArgumentException("Unable to extract parameterized field type argument from Formatter [" +
formatter.getClass().getName() + "]; does the formatter parameterize the <T> generic type?");
throw new IllegalArgumentException("Unable to extract the parameterized field type from Formatter [" +
formatter.getClass().getName() + "]; does the class parameterize the <T> generic type?");
}
return fieldType;
}

View File

@ -32,7 +32,7 @@ import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.BeanUtils;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.beans.ConfigurablePropertyAccessor;
import org.springframework.beans.PropertyAccessorFactory;
import org.springframework.beans.factory.annotation.Value;
@ -45,6 +45,7 @@ import org.springframework.context.support.GenericApplicationContext;
import org.springframework.core.convert.ConversionFailedException;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.ConverterFactory;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.format.Formatter;
import org.springframework.format.Printer;
@ -81,7 +82,7 @@ public class FormattingConversionServiceTests {
@Test
public void testFormatFieldForTypeWithFormatter() throws ParseException {
public void formatFieldForTypeWithFormatter() throws ParseException {
formattingService.addFormatterForFieldType(Number.class, new NumberStyleFormatter());
String formatted = formattingService.convert(3, String.class);
assertEquals("3", formatted);
@ -90,7 +91,7 @@ public class FormattingConversionServiceTests {
}
@Test
public void testFormatFieldForTypeWithPrinterParserWithCoercion() throws ParseException {
public void formatFieldForTypeWithPrinterParserWithCoercion() throws ParseException {
formattingService.addConverter(new Converter<DateTime, LocalDate>() {
@Override
public LocalDate convert(DateTime source) {
@ -107,7 +108,7 @@ public class FormattingConversionServiceTests {
@Test
@SuppressWarnings("resource")
public void testFormatFieldForValueInjection() {
public void formatFieldForValueInjection() {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext();
ac.registerBeanDefinition("valueBean", new RootBeanDefinition(ValueBean.class));
ac.registerBeanDefinition("conversionService", new RootBeanDefinition(FormattingConversionServiceFactoryBean.class));
@ -118,7 +119,7 @@ public class FormattingConversionServiceTests {
@Test
@SuppressWarnings("resource")
public void testFormatFieldForValueInjectionUsingMetaAnnotations() {
public void formatFieldForValueInjectionUsingMetaAnnotations() {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext();
RootBeanDefinition bd = new RootBeanDefinition(MetaValueBean.class);
bd.setScope(BeanDefinition.SCOPE_PROTOTYPE);
@ -140,20 +141,20 @@ public class FormattingConversionServiceTests {
}
@Test
public void testFormatFieldForAnnotation() throws Exception {
public void formatFieldForAnnotation() throws Exception {
formattingService.addFormatterForFieldAnnotation(new JodaDateTimeFormatAnnotationFormatterFactory());
doTestFormatFieldForAnnotation(Model.class, false);
}
@Test
public void testFormatFieldForAnnotationWithDirectFieldAccess() throws Exception {
public void formatFieldForAnnotationWithDirectFieldAccess() throws Exception {
formattingService.addFormatterForFieldAnnotation(new JodaDateTimeFormatAnnotationFormatterFactory());
doTestFormatFieldForAnnotation(Model.class, true);
}
@Test
@SuppressWarnings("resource")
public void testFormatFieldForAnnotationWithPlaceholders() throws Exception {
public void formatFieldForAnnotationWithPlaceholders() throws Exception {
GenericApplicationContext context = new GenericApplicationContext();
PropertyPlaceholderConfigurer ppc = new PropertyPlaceholderConfigurer();
Properties props = new Properties();
@ -169,7 +170,7 @@ public class FormattingConversionServiceTests {
@Test
@SuppressWarnings("resource")
public void testFormatFieldForAnnotationWithPlaceholdersAndFactoryBean() throws Exception {
public void formatFieldForAnnotationWithPlaceholdersAndFactoryBean() throws Exception {
GenericApplicationContext context = new GenericApplicationContext();
PropertyPlaceholderConfigurer ppc = new PropertyPlaceholderConfigurer();
Properties props = new Properties();
@ -239,61 +240,61 @@ public class FormattingConversionServiceTests {
}
@Test
public void testPrintNull() throws ParseException {
public void printNull() throws ParseException {
formattingService.addFormatterForFieldType(Number.class, new NumberStyleFormatter());
assertEquals("", formattingService.convert(null, TypeDescriptor.valueOf(Integer.class), TypeDescriptor.valueOf(String.class)));
}
@Test
public void testParseNull() throws ParseException {
public void parseNull() throws ParseException {
formattingService.addFormatterForFieldType(Number.class, new NumberStyleFormatter());
assertNull(formattingService
.convert(null, TypeDescriptor.valueOf(String.class), TypeDescriptor.valueOf(Integer.class)));
}
@Test
public void testParseEmptyString() throws ParseException {
public void parseEmptyString() throws ParseException {
formattingService.addFormatterForFieldType(Number.class, new NumberStyleFormatter());
assertNull(formattingService.convert("", TypeDescriptor.valueOf(String.class), TypeDescriptor.valueOf(Integer.class)));
}
@Test
public void testParseBlankString() throws ParseException {
public void parseBlankString() throws ParseException {
formattingService.addFormatterForFieldType(Number.class, new NumberStyleFormatter());
assertNull(formattingService.convert(" ", TypeDescriptor.valueOf(String.class), TypeDescriptor.valueOf(Integer.class)));
}
@Test(expected = ConversionFailedException.class)
public void testParseParserReturnsNull() throws ParseException {
public void parseParserReturnsNull() throws ParseException {
formattingService.addFormatterForFieldType(Integer.class, new NullReturningFormatter());
assertNull(formattingService.convert("1", TypeDescriptor.valueOf(String.class), TypeDescriptor.valueOf(Integer.class)));
}
@Test(expected = ConversionFailedException.class)
public void testParseNullPrimitiveProperty() throws ParseException {
public void parseNullPrimitiveProperty() throws ParseException {
formattingService.addFormatterForFieldType(Integer.class, new NumberStyleFormatter());
assertNull(formattingService.convert(null, TypeDescriptor.valueOf(String.class), TypeDescriptor.valueOf(int.class)));
}
@Test
public void testPrintNullDefault() throws ParseException {
public void printNullDefault() throws ParseException {
assertEquals(null, formattingService
.convert(null, TypeDescriptor.valueOf(Integer.class), TypeDescriptor.valueOf(String.class)));
}
@Test
public void testParseNullDefault() throws ParseException {
public void parseNullDefault() throws ParseException {
assertNull(formattingService
.convert(null, TypeDescriptor.valueOf(String.class), TypeDescriptor.valueOf(Integer.class)));
}
@Test
public void testParseEmptyStringDefault() throws ParseException {
public void parseEmptyStringDefault() throws ParseException {
assertNull(formattingService.convert("", TypeDescriptor.valueOf(String.class), TypeDescriptor.valueOf(Integer.class)));
}
@Test
public void testFormatFieldForAnnotationWithSubclassAsFieldType() throws Exception {
public void formatFieldForAnnotationWithSubclassAsFieldType() throws Exception {
formattingService.addFormatterForFieldAnnotation(new JodaDateTimeFormatAnnotationFormatterFactory() {
@Override
public Printer<?> getPrinter(org.springframework.format.annotation.DateTimeFormat annotation, Class<?> fieldType) {
@ -319,7 +320,7 @@ public class FormattingConversionServiceTests {
}
@Test
public void testRegisterDefaultValueViaFormatter() {
public void registerDefaultValueViaFormatter() {
registerDefaultValue(Date.class, new Date());
}
@ -340,6 +341,45 @@ public class FormattingConversionServiceTests {
});
}
@Test
public void introspectedFormatter() throws ParseException {
formattingService.addFormatter(new NumberStyleFormatter());
assertNull(formattingService.convert(null, TypeDescriptor.valueOf(String.class), TypeDescriptor.valueOf(Integer.class)));
}
@Test
public void proxiedFormatter() throws ParseException {
Formatter<?> formatter = new NumberStyleFormatter();
formattingService.addFormatter((Formatter<?>) new ProxyFactory(formatter).getProxy());
assertNull(formattingService.convert(null, TypeDescriptor.valueOf(String.class), TypeDescriptor.valueOf(Integer.class)));
}
@Test
public void introspectedConverter() {
formattingService.addConverter(new IntegerConverter());
assertEquals(Integer.valueOf(1), formattingService.convert("1", Integer.class));
}
@Test
public void proxiedConverter() {
Converter<?, ?> converter = new IntegerConverter();
formattingService.addConverter((Converter<?, ?>) new ProxyFactory(converter).getProxy());
assertEquals(Integer.valueOf(1), formattingService.convert("1", Integer.class));
}
@Test
public void introspectedConverterFactory() {
formattingService.addConverterFactory(new IntegerConverterFactory());
assertEquals(Integer.valueOf(1), formattingService.convert("1", Integer.class));
}
@Test
public void proxiedConverterFactory() {
ConverterFactory<?, ?> converterFactory = new IntegerConverterFactory();
formattingService.addConverterFactory((ConverterFactory<?, ?>) new ProxyFactory(converterFactory).getProxy());
assertEquals(Integer.valueOf(1), formattingService.convert("1", Integer.class));
}
public static class ValueBean {
@ -348,6 +388,7 @@ public class FormattingConversionServiceTests {
public Date date;
}
public static class MetaValueBean {
@MyDateAnn
@ -357,18 +398,21 @@ public class FormattingConversionServiceTests {
public Double number;
}
@Value("${myDate}")
@org.springframework.format.annotation.DateTimeFormat(pattern="MM-d-yy")
@Retention(RetentionPolicy.RUNTIME)
public static @interface MyDateAnn {
public @interface MyDateAnn {
}
@Value("${myNumber}")
@NumberFormat(style = NumberFormat.Style.PERCENT)
@Retention(RetentionPolicy.RUNTIME)
public static @interface MyNumberAnn {
public @interface MyNumberAnn {
}
public static class Model {
@org.springframework.format.annotation.DateTimeFormat(style="S-")
@ -386,6 +430,7 @@ public class FormattingConversionServiceTests {
}
}
public static class ModelWithPlaceholders {
@org.springframework.format.annotation.DateTimeFormat(style="${dateStyle}")
@ -403,11 +448,13 @@ public class FormattingConversionServiceTests {
}
}
@org.springframework.format.annotation.DateTimeFormat(pattern="${datePattern}")
@Retention(RetentionPolicy.RUNTIME)
public static @interface MyDatePattern {
public @interface MyDatePattern {
}
public static class NullReturningFormatter implements Formatter<Integer> {
@Override
@ -419,17 +466,42 @@ public class FormattingConversionServiceTests {
public Integer parse(String text, Locale locale) throws ParseException {
return null;
}
}
@SuppressWarnings("serial")
public static class MyDate extends Date {
}
private static class ModelWithSubclassField {
@org.springframework.format.annotation.DateTimeFormat(style = "S-")
public MyDate date;
}
private static class IntegerConverter implements Converter<String, Integer> {
@Override
public Integer convert(String source) {
return Integer.parseInt(source);
}
}
private static class IntegerConverterFactory implements ConverterFactory<String, Number> {
@Override
@SuppressWarnings("unchecked")
public <T extends Number> Converter<String, T> getConverter(Class<T> targetType) {
if (Integer.class == targetType) {
return (Converter<String, T>) new IntegerConverter();
}
else {
throw new IllegalStateException();
}
}
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2015 the original author or authors.
* Copyright 2002-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -34,7 +34,7 @@ public interface ConverterFactory<S, R> {
* Get the converter to convert from S to target type T, where T is also an instance of R.
* @param <T> the target type
* @param targetType the target type to convert to
* @return A converter from S to T
* @return a converter from S to T
*/
<T extends R> Converter<S, T> getConverter(Class<T> targetType);

View File

@ -49,12 +49,12 @@ public interface ConverterRegistry {
/**
* Add a ranged converter factory to this registry.
* The convertible source/target type pair is derived from the ConverterFactory's parameterized types.
* @throws IllegalArgumentException if the parameterized types could not be resolved.
* @throws IllegalArgumentException if the parameterized types could not be resolved
*/
void addConverterFactory(ConverterFactory<?, ?> converterFactory);
void addConverterFactory(ConverterFactory<?, ?> factory);
/**
* Remove any converters from sourceType to targetType.
* Remove any converters from {@code sourceType} to {@code targetType}.
* @param sourceType the source type
* @param targetType the target type
*/

View File

@ -28,6 +28,7 @@ import java.util.Map;
import java.util.Optional;
import java.util.Set;
import org.springframework.core.DecoratingProxy;
import org.springframework.core.ResolvableType;
import org.springframework.core.convert.ConversionException;
import org.springframework.core.convert.ConversionFailedException;
@ -83,9 +84,14 @@ public class GenericConversionService implements ConfigurableConversionService {
@Override
public void addConverter(Converter<?, ?> converter) {
ResolvableType[] typeInfo = getRequiredTypeInfo(converter, Converter.class);
Assert.notNull(typeInfo, "Unable to the determine sourceType <S> and targetType " +
"<T> which your Converter<S, T> converts between; declare these generic types.");
ResolvableType[] typeInfo = getRequiredTypeInfo(converter.getClass(), Converter.class);
if (typeInfo == null && converter instanceof DecoratingProxy) {
typeInfo = getRequiredTypeInfo(((DecoratingProxy) converter).getDecoratedClass(), Converter.class);
}
if (typeInfo == null) {
throw new IllegalArgumentException("Unable to determine source type <S> and target type <T> for your " +
"Converter [" + converter.getClass().getName() + "]; does the class parameterize those types?");
}
addConverter(new ConverterAdapter(converter, typeInfo[0], typeInfo[1]));
}
@ -102,11 +108,16 @@ public class GenericConversionService implements ConfigurableConversionService {
}
@Override
public void addConverterFactory(ConverterFactory<?, ?> converterFactory) {
ResolvableType[] typeInfo = getRequiredTypeInfo(converterFactory, ConverterFactory.class);
Assert.notNull(typeInfo, "Unable to the determine source type <S> and target range type R which your " +
"ConverterFactory<S, R> converts between; declare these generic types.");
addConverter(new ConverterFactoryAdapter(converterFactory,
public void addConverterFactory(ConverterFactory<?, ?> factory) {
ResolvableType[] typeInfo = getRequiredTypeInfo(factory.getClass(), ConverterFactory.class);
if (typeInfo == null && factory instanceof DecoratingProxy) {
typeInfo = getRequiredTypeInfo(((DecoratingProxy) factory).getDecoratedClass(), ConverterFactory.class);
}
if (typeInfo == null) {
throw new IllegalArgumentException("Unable to determine source type <S> and target type <T> for your " +
"ConverterFactory [" + factory.getClass().getName() + "]; does the class parameterize those types?");
}
addConverter(new ConverterFactoryAdapter(factory,
new ConvertiblePair(typeInfo[0].resolve(), typeInfo[1].resolve())));
}
@ -271,8 +282,8 @@ public class GenericConversionService implements ConfigurableConversionService {
// Internal helpers
private ResolvableType[] getRequiredTypeInfo(Object converter, Class<?> genericIfc) {
ResolvableType resolvableType = ResolvableType.forClass(converter.getClass()).as(genericIfc);
private ResolvableType[] getRequiredTypeInfo(Class<?> converterClass, Class<?> genericIfc) {
ResolvableType resolvableType = ResolvableType.forClass(converterClass).as(genericIfc);
ResolvableType[] generics = resolvableType.getGenerics();
if (generics.length < 2) {
return null;