diff --git a/spring-beans/src/main/java/org/springframework/beans/TypeConverter.java b/spring-beans/src/main/java/org/springframework/beans/TypeConverter.java index 5e38a1e93e..8b8b8dedfc 100644 --- a/spring-beans/src/main/java/org/springframework/beans/TypeConverter.java +++ b/spring-beans/src/main/java/org/springframework/beans/TypeConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2019 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. @@ -19,6 +19,7 @@ package org.springframework.beans; import java.lang.reflect.Field; import org.springframework.core.MethodParameter; +import org.springframework.core.convert.TypeDescriptor; import org.springframework.lang.Nullable; /** @@ -93,4 +94,27 @@ public interface TypeConverter { T convertIfNecessary(@Nullable Object value, @Nullable Class requiredType, @Nullable Field field) throws TypeMismatchException; + /** + * Convert the value to the required type (if necessary from a String). + *

Conversions from String to any type will typically use the {@code setAsText} + * method of the PropertyEditor class, or a Spring Converter in a ConversionService. + * @param value the value to convert + * @param requiredType the type we must convert to + * (or {@code null} if not known, for example in case of a collection element) + * @param typeDescriptor the type descriptor to use (may be {@code null})) + * @return the new value, possibly the result of type conversion + * @throws TypeMismatchException if type conversion failed + * @since 5.1.4 + * @see java.beans.PropertyEditor#setAsText(String) + * @see java.beans.PropertyEditor#getValue() + * @see org.springframework.core.convert.ConversionService + * @see org.springframework.core.convert.converter.Converter + */ + @Nullable + default T convertIfNecessary(@Nullable Object value, @Nullable Class requiredType, + @Nullable TypeDescriptor typeDescriptor) throws TypeMismatchException { + + throw new UnsupportedOperationException("TypeDescriptor resolution not supported"); + } + } diff --git a/spring-beans/src/main/java/org/springframework/beans/TypeConverterDelegate.java b/spring-beans/src/main/java/org/springframework/beans/TypeConverterDelegate.java index 1485bef7f8..343ba8c42d 100644 --- a/spring-beans/src/main/java/org/springframework/beans/TypeConverterDelegate.java +++ b/spring-beans/src/main/java/org/springframework/beans/TypeConverterDelegate.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2019 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. @@ -30,7 +30,6 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.core.CollectionFactory; -import org.springframework.core.MethodParameter; import org.springframework.core.convert.ConversionFailedException; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.TypeDescriptor; @@ -82,42 +81,6 @@ class TypeConverterDelegate { } - /** - * Convert the value to the specified required type. - * @param newValue the proposed new value - * @param requiredType the type we must convert to - * (or {@code null} if not known, for example in case of a collection element) - * @param methodParam the method parameter that is the target of the conversion - * (may be {@code null}) - * @return the new value, possibly the result of type conversion - * @throws IllegalArgumentException if type conversion failed - */ - @Nullable - public T convertIfNecessary(@Nullable Object newValue, @Nullable Class requiredType, - @Nullable MethodParameter methodParam) throws IllegalArgumentException { - - return convertIfNecessary(null, null, newValue, requiredType, - (methodParam != null ? new TypeDescriptor(methodParam) : TypeDescriptor.valueOf(requiredType))); - } - - /** - * Convert the value to the specified required type. - * @param newValue the proposed new value - * @param requiredType the type we must convert to - * (or {@code null} if not known, for example in case of a collection element) - * @param field the reflective field that is the target of the conversion - * (may be {@code null}) - * @return the new value, possibly the result of type conversion - * @throws IllegalArgumentException if type conversion failed - */ - @Nullable - public T convertIfNecessary(@Nullable Object newValue, @Nullable Class requiredType, @Nullable Field field) - throws IllegalArgumentException { - - return convertIfNecessary(null, null, newValue, requiredType, - (field != null ? new TypeDescriptor(field) : TypeDescriptor.valueOf(requiredType))); - } - /** * Convert the value to the required type for the specified property. * @param propertyName name of the property diff --git a/spring-beans/src/main/java/org/springframework/beans/TypeConverterSupport.java b/spring-beans/src/main/java/org/springframework/beans/TypeConverterSupport.java index 809cd61250..bf1a794d46 100644 --- a/spring-beans/src/main/java/org/springframework/beans/TypeConverterSupport.java +++ b/spring-beans/src/main/java/org/springframework/beans/TypeConverterSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2019 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. @@ -21,6 +21,7 @@ import java.lang.reflect.Field; import org.springframework.core.MethodParameter; import org.springframework.core.convert.ConversionException; import org.springframework.core.convert.ConverterNotFoundException; +import org.springframework.core.convert.TypeDescriptor; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -41,15 +42,16 @@ public abstract class TypeConverterSupport extends PropertyEditorRegistrySupport @Override @Nullable public T convertIfNecessary(@Nullable Object value, @Nullable Class requiredType) throws TypeMismatchException { - return doConvert(value, requiredType, null, null); + return convertIfNecessary(value, requiredType, TypeDescriptor.valueOf(requiredType)); } @Override @Nullable - public T convertIfNecessary(@Nullable Object value, @Nullable Class requiredType, @Nullable MethodParameter methodParam) - throws TypeMismatchException { + public T convertIfNecessary(@Nullable Object value, @Nullable Class requiredType, + @Nullable MethodParameter methodParam) throws TypeMismatchException { - return doConvert(value, requiredType, methodParam, null); + return convertIfNecessary(value, requiredType, + (methodParam != null ? new TypeDescriptor(methodParam) : TypeDescriptor.valueOf(requiredType))); } @Override @@ -57,21 +59,18 @@ public abstract class TypeConverterSupport extends PropertyEditorRegistrySupport public T convertIfNecessary(@Nullable Object value, @Nullable Class requiredType, @Nullable Field field) throws TypeMismatchException { - return doConvert(value, requiredType, null, field); + return convertIfNecessary(value, requiredType, + (field != null ? new TypeDescriptor(field) : TypeDescriptor.valueOf(requiredType))); } @Nullable - private T doConvert(@Nullable Object value,@Nullable Class requiredType, - @Nullable MethodParameter methodParam, @Nullable Field field) throws TypeMismatchException { + @Override + public T convertIfNecessary(@Nullable Object value, @Nullable Class requiredType, + @Nullable TypeDescriptor typeDescriptor) throws TypeMismatchException { Assert.state(this.typeConverterDelegate != null, "No TypeConverterDelegate"); try { - if (field != null) { - return this.typeConverterDelegate.convertIfNecessary(value, requiredType, field); - } - else { - return this.typeConverterDelegate.convertIfNecessary(value, requiredType, methodParam); - } + return this.typeConverterDelegate.convertIfNecessary(null, null, value, requiredType, typeDescriptor); } catch (ConverterNotFoundException | IllegalStateException ex) { throw new ConversionNotSupportedException(value, requiredType, ex); diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/DependencyDescriptor.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/DependencyDescriptor.java index 6f8506649d..a07c81646c 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/DependencyDescriptor.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/DependencyDescriptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2019 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. @@ -38,6 +38,7 @@ import org.springframework.core.KotlinDetector; import org.springframework.core.MethodParameter; import org.springframework.core.ParameterNameDiscoverer; import org.springframework.core.ResolvableType; +import org.springframework.core.convert.TypeDescriptor; import org.springframework.lang.Nullable; import org.springframework.util.ObjectUtils; @@ -77,6 +78,9 @@ public class DependencyDescriptor extends InjectionPoint implements Serializable @Nullable private transient volatile ResolvableType resolvableType; + @Nullable + private transient volatile TypeDescriptor typeDescriptor; + /** * Create a new descriptor for a method or constructor parameter. @@ -301,7 +305,7 @@ public class DependencyDescriptor extends InjectionPoint implements Serializable } /** - * Build a ResolvableType object for the wrapped parameter/field. + * Build a {@link ResolvableType} object for the wrapped parameter/field. * @since 4.0 */ public ResolvableType getResolvableType() { @@ -315,6 +319,21 @@ public class DependencyDescriptor extends InjectionPoint implements Serializable return resolvableType; } + /** + * Build a {@link TypeDescriptor} object for the wrapped parameter/field. + * @since 5.1.4 + */ + public TypeDescriptor getTypeDescriptor() { + TypeDescriptor typeDescriptor = this.typeDescriptor; + if (typeDescriptor == null) { + typeDescriptor = (this.field != null ? + new TypeDescriptor(getResolvableType(), getDependencyType(), getAnnotations()) : + new TypeDescriptor(obtainMethodParameter())); + this.typeDescriptor = typeDescriptor; + } + return typeDescriptor; + } + /** * Return whether a fallback match is allowed. *

This is {@code false} by default but may be overridden to return {@code true} in order diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java index 4adf7a775f..416a40bd19 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2019 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. @@ -1183,13 +1183,20 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto if (value != null) { if (value instanceof String) { String strVal = resolveEmbeddedValue((String) value); - BeanDefinition bd = (beanName != null && containsBean(beanName) ? getMergedBeanDefinition(beanName) : null); + BeanDefinition bd = (beanName != null && containsBean(beanName) ? + getMergedBeanDefinition(beanName) : null); value = evaluateBeanDefinitionString(strVal, bd); } TypeConverter converter = (typeConverter != null ? typeConverter : getTypeConverter()); - return (descriptor.getField() != null ? - converter.convertIfNecessary(value, type, descriptor.getField()) : - converter.convertIfNecessary(value, type, descriptor.getMethodParameter())); + try { + return converter.convertIfNecessary(value, type, descriptor.getTypeDescriptor()); + } + catch (UnsupportedOperationException ex) { + // A custom TypeConverter which does not support TypeDescriptor resolution... + return (descriptor.getField() != null ? + converter.convertIfNecessary(value, type, descriptor.getField()) : + converter.convertIfNecessary(value, type, descriptor.getMethodParameter())); + } } Object multipleBeans = resolveMultipleBeans(descriptor, beanName, autowiredBeanNames, typeConverter); @@ -1687,7 +1694,8 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto super.resolveCandidate(beanName, requiredType, beanFactory)); } }; - return Optional.ofNullable(doResolveDependency(descriptorToUse, beanName, null, null)); + Object result = doResolveDependency(descriptorToUse, beanName, null, null); + return (result instanceof Optional ? (Optional) result : Optional.ofNullable(result)); } @@ -1915,8 +1923,7 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto private Stream resolveStream(boolean ordered) { DependencyDescriptor descriptorToUse = new StreamDependencyDescriptor(this.descriptor, ordered); Object result = doResolveDependency(descriptorToUse, this.beanName, null, null); - Assert.state(result instanceof Stream, "Stream expected"); - return (Stream) result; + return (result instanceof Stream ? (Stream) result : Stream.of(result)); } } diff --git a/spring-context/src/main/java/org/springframework/validation/DataBinder.java b/spring-context/src/main/java/org/springframework/validation/DataBinder.java index c9c0e4acda..21101c1a6a 100644 --- a/spring-context/src/main/java/org/springframework/validation/DataBinder.java +++ b/spring-context/src/main/java/org/springframework/validation/DataBinder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2019 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. @@ -41,6 +41,7 @@ import org.springframework.beans.TypeConverter; import org.springframework.beans.TypeMismatchException; import org.springframework.core.MethodParameter; import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; import org.springframework.format.Formatter; import org.springframework.format.support.FormatterPropertyEditorAdapter; import org.springframework.lang.Nullable; @@ -700,6 +701,14 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter { return getTypeConverter().convertIfNecessary(value, requiredType, field); } + @Nullable + @Override + public T convertIfNecessary(@Nullable Object value, @Nullable Class requiredType, + @Nullable TypeDescriptor typeDescriptor) throws TypeMismatchException { + + return getTypeConverter().convertIfNecessary(value, requiredType, typeDescriptor); + } + /** * Bind the given property values to this binder's target. diff --git a/spring-context/src/test/java/org/springframework/context/expression/ApplicationContextExpressionTests.java b/spring-context/src/test/java/org/springframework/context/expression/ApplicationContextExpressionTests.java index 1c2ffdcf29..3eb87d7006 100644 --- a/spring-context/src/test/java/org/springframework/context/expression/ApplicationContextExpressionTests.java +++ b/spring-context/src/test/java/org/springframework/context/expression/ApplicationContextExpressionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2019 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. @@ -25,11 +25,11 @@ import java.net.URI; import java.net.URL; import java.security.AccessControlException; import java.security.Permission; +import java.util.Optional; import java.util.Properties; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; - import org.junit.Test; import org.springframework.beans.factory.ObjectFactory; @@ -46,7 +46,7 @@ import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.AnnotationConfigUtils; import org.springframework.context.support.GenericApplicationContext; -import org.springframework.core.convert.converter.Converter; +import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.core.convert.support.GenericConversionService; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; @@ -102,6 +102,8 @@ public class ApplicationContextExpressionTests { } }); + ac.getBeanFactory().setConversionService(new DefaultConversionService()); + PropertyPlaceholderConfigurer ppc = new PropertyPlaceholderConfigurer(); Properties placeholders = new Properties(); placeholders.setProperty("code", "123"); @@ -176,6 +178,9 @@ public class ApplicationContextExpressionTests { System.getProperties().put("country", "UK"); assertEquals("123 UK", tb3.country); assertEquals("123 UK", tb3.countryFactory.getObject()); + assertEquals("123", tb3.optionalValue1.get()); + assertEquals("123", tb3.optionalValue2.get()); + assertFalse(tb3.optionalValue3.isPresent()); assertSame(tb0, tb3.tb); tb3 = (ValueTestBean) SerializationTestUtils.serializeAndDeserialize(tb3); @@ -209,12 +214,7 @@ public class ApplicationContextExpressionTests { GenericApplicationContext ac = new GenericApplicationContext(); AnnotationConfigUtils.registerAnnotationConfigProcessors(ac); GenericConversionService cs = new GenericConversionService(); - cs.addConverter(String.class, String.class, new Converter() { - @Override - public String convert(String source) { - return source.trim(); - } - }); + cs.addConverter(String.class, String.class, String::trim); ac.getBeanFactory().registerSingleton(GenericApplicationContext.CONVERSION_SERVICE_BEAN_NAME, cs); RootBeanDefinition rbd = new RootBeanDefinition(PrototypeTestBean.class); rbd.setScope(RootBeanDefinition.SCOPE_PROTOTYPE); @@ -276,8 +276,7 @@ public class ApplicationContextExpressionTests { @Test public void systemPropertiesSecurityManager() { - GenericApplicationContext ac = new GenericApplicationContext(); - AnnotationConfigUtils.registerAnnotationConfigProcessors(ac); + AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(); GenericBeanDefinition bd = new GenericBeanDefinition(); bd.setBeanClass(TestBean.class); @@ -313,8 +312,7 @@ public class ApplicationContextExpressionTests { @Test public void stringConcatenationWithDebugLogging() { - GenericApplicationContext ac = new GenericApplicationContext(); - AnnotationConfigUtils.registerAnnotationConfigProcessors(ac); + AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(); GenericBeanDefinition bd = new GenericBeanDefinition(); bd.setBeanClass(String.class); @@ -365,6 +363,15 @@ public class ApplicationContextExpressionTests { @Value("${code} #{systemProperties.country}") public ObjectFactory countryFactory; + @Value("${code}") + private transient Optional optionalValue1; + + @Value("${code:#{null}}") + private transient Optional optionalValue2; + + @Value("${codeX:#{null}}") + private transient Optional optionalValue3; + @Autowired @Qualifier("original") public transient TestBean tb; } diff --git a/spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java b/spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java index 9dfcbad236..8e2ca54b02 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java +++ b/spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2019 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. @@ -80,7 +80,7 @@ public class TypeDescriptor implements Serializable { */ public TypeDescriptor(MethodParameter methodParameter) { this.resolvableType = ResolvableType.forMethodParameter(methodParameter); - this.type = this.resolvableType.resolve(methodParameter.getParameterType()); + this.type = this.resolvableType.resolve(methodParameter.getNestedParameterType()); this.annotatedElement = new AnnotatedElementAdapter(methodParameter.getParameterIndex() == -1 ? methodParameter.getMethodAnnotations() : methodParameter.getParameterAnnotations()); } @@ -118,7 +118,7 @@ public class TypeDescriptor implements Serializable { * @param annotations the type annotations * @since 4.0 */ - protected TypeDescriptor(ResolvableType resolvableType, @Nullable Class type, @Nullable Annotation[] annotations) { + public TypeDescriptor(ResolvableType resolvableType, @Nullable Class type, @Nullable Annotation[] annotations) { this.resolvableType = resolvableType; this.type = (type != null ? type : resolvableType.toClass()); this.annotatedElement = new AnnotatedElementAdapter(annotations); @@ -616,7 +616,7 @@ public class TypeDescriptor implements Serializable { } /** - * Creates a type descriptor for a nested type declared within the method parameter. + * Create a type descriptor for a nested type declared within the method parameter. *

For example, if the methodParameter is a {@code List} and the * nesting level is 1, the nested type descriptor will be String.class. *

If the methodParameter is a {@code List>} and the nesting @@ -647,7 +647,7 @@ public class TypeDescriptor implements Serializable { } /** - * Creates a type descriptor for a nested type declared within the field. + * Create a type descriptor for a nested type declared within the field. *

For example, if the field is a {@code List} and the nesting * level is 1, the nested type descriptor will be {@code String.class}. *

If the field is a {@code List>} and the nesting level is @@ -656,8 +656,9 @@ public class TypeDescriptor implements Serializable { * is 1, the nested type descriptor will be String, derived from the map value. *

If the field is a {@code List>} and the nesting * level is 2, the nested type descriptor will be String, derived from the map value. - *

Returns {@code null} if a nested type cannot be obtained because it was not declared. - * For example, if the field is a {@code List}, the nested type descriptor returned will be {@code null}. + *

Returns {@code null} if a nested type cannot be obtained because it was not + * declared. For example, if the field is a {@code List}, the nested type + * descriptor returned will be {@code null}. * @param field the field * @param nestingLevel the nesting level of the collection/array element or * map key/value declaration within the field @@ -672,7 +673,7 @@ public class TypeDescriptor implements Serializable { } /** - * Creates a type descriptor for a nested type declared within the property. + * Create a type descriptor for a nested type declared within the property. *

For example, if the property is a {@code List} and the nesting * level is 1, the nested type descriptor will be {@code String.class}. *

If the property is a {@code List>} and the nesting level @@ -681,9 +682,9 @@ public class TypeDescriptor implements Serializable { * is 1, the nested type descriptor will be String, derived from the map value. *

If the property is a {@code List>} and the nesting * level is 2, the nested type descriptor will be String, derived from the map value. - *

Returns {@code null} if a nested type cannot be obtained because it was not declared. - * For example, if the property is a {@code List}, the nested type descriptor - * returned will be {@code null}. + *

Returns {@code null} if a nested type cannot be obtained because it was not + * declared. For example, if the property is a {@code List}, the nested type + * descriptor returned will be {@code null}. * @param property the property * @param nestingLevel the nesting level of the collection/array element or * map key/value declaration within the property