From d37eaa5941f2346b5bbbdaae1aee1867cd6633f6 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Fri, 28 Aug 2020 18:52:35 +0200 Subject: [PATCH] Introduce DataClassRowMapper with record-style constructor binding support Closes gh-24695 --- .../org/springframework/beans/BeanUtils.java | 34 +++- .../jdbc/core/BeanPropertyRowMapper.java | 71 +++++--- .../jdbc/core/DataClassRowMapper.java | 151 ++++++++++++++++++ .../jdbc/core/SingleColumnRowMapper.java | 10 +- .../jdbc/core/AbstractRowMapperTests.java | 68 +++++--- .../jdbc/core/BeanPropertyRowMapperTests.java | 5 +- .../jdbc/core/DataClassRowMapperTests.java | 45 ++++++ .../jdbc/core/test/ConstructorPerson.java | 60 +++++++ .../ModelAttributeMethodProcessor.java | 9 +- .../ModelAttributeMethodArgumentResolver.java | 11 +- 10 files changed, 386 insertions(+), 78 deletions(-) create mode 100644 spring-jdbc/src/main/java/org/springframework/jdbc/core/DataClassRowMapper.java create mode 100644 spring-jdbc/src/test/java/org/springframework/jdbc/core/DataClassRowMapperTests.java create mode 100644 spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConstructorPerson.java diff --git a/spring-beans/src/main/java/org/springframework/beans/BeanUtils.java b/spring-beans/src/main/java/org/springframework/beans/BeanUtils.java index 039784a6c14..26c8e920499 100644 --- a/spring-beans/src/main/java/org/springframework/beans/BeanUtils.java +++ b/spring-beans/src/main/java/org/springframework/beans/BeanUtils.java @@ -16,6 +16,7 @@ package org.springframework.beans; +import java.beans.ConstructorProperties; import java.beans.PropertyDescriptor; import java.beans.PropertyEditor; import java.lang.reflect.Constructor; @@ -43,8 +44,10 @@ import kotlin.reflect.jvm.ReflectJvmMapping; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.springframework.core.DefaultParameterNameDiscoverer; import org.springframework.core.KotlinDetector; import org.springframework.core.MethodParameter; +import org.springframework.core.ParameterNameDiscoverer; import org.springframework.core.ResolvableType; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -74,6 +77,9 @@ public abstract class BeanUtils { private static final Log logger = LogFactory.getLog(BeanUtils.class); + private static final ParameterNameDiscoverer parameterNameDiscoverer = + new DefaultParameterNameDiscoverer(); + private static final Set> unknownEditorTypes = Collections.newSetFromMap(new ConcurrentReferenceHashMap<>(64)); @@ -443,8 +449,7 @@ public abstract class BeanUtils { * @throws BeansException if PropertyDescriptor look fails */ public static PropertyDescriptor[] getPropertyDescriptors(Class clazz) throws BeansException { - CachedIntrospectionResults cr = CachedIntrospectionResults.forClass(clazz); - return cr.getPropertyDescriptors(); + return CachedIntrospectionResults.forClass(clazz).getPropertyDescriptors(); } /** @@ -455,11 +460,8 @@ public abstract class BeanUtils { * @throws BeansException if PropertyDescriptor lookup fails */ @Nullable - public static PropertyDescriptor getPropertyDescriptor(Class clazz, String propertyName) - throws BeansException { - - CachedIntrospectionResults cr = CachedIntrospectionResults.forClass(clazz); - return cr.getPropertyDescriptor(propertyName); + public static PropertyDescriptor getPropertyDescriptor(Class clazz, String propertyName) throws BeansException { + return CachedIntrospectionResults.forClass(clazz).getPropertyDescriptor(propertyName); } /** @@ -588,6 +590,24 @@ public abstract class BeanUtils { } } + /** + * Determine required parameter names for the given constructor, + * considering the JavaBeans {@link ConstructorProperties} annotation + * as well as Spring's {@link DefaultParameterNameDiscoverer}. + * @param ctor the constructor to find parameter names for + * @return the parameter names (matching the constructor's parameter count) + * @throws IllegalStateException if the parameter names are not resolvable + * @since 5.3 + */ + public static String[] getParameterNames(Constructor ctor) { + ConstructorProperties cp = ctor.getAnnotation(ConstructorProperties.class); + String[] paramNames = (cp != null ? cp.value() : parameterNameDiscoverer.getParameterNames(ctor)); + Assert.state(paramNames != null, () -> "Cannot resolve parameter names for constructor " + ctor); + Assert.state(paramNames.length == ctor.getParameterCount(), + () -> "Invalid number of parameter names: " + paramNames.length + " for constructor " + ctor); + return paramNames; + } + /** * Check if the given type represents a "simple" property: a simple value * type or an array of simple value types. diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/BeanPropertyRowMapper.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/BeanPropertyRowMapper.java index ada11f687b0..4beec4a505a 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/BeanPropertyRowMapper.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/BeanPropertyRowMapper.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 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. @@ -31,8 +31,9 @@ import org.apache.commons.logging.LogFactory; import org.springframework.beans.BeanUtils; import org.springframework.beans.BeanWrapper; +import org.springframework.beans.BeanWrapperImpl; import org.springframework.beans.NotWritablePropertyException; -import org.springframework.beans.PropertyAccessorFactory; +import org.springframework.beans.TypeConverter; import org.springframework.beans.TypeMismatchException; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.support.DefaultConversionService; @@ -114,8 +115,6 @@ public class BeanPropertyRowMapper implements RowMapper { /** * Create a new {@code BeanPropertyRowMapper}, accepting unpopulated * properties in the target bean. - *

Consider using the {@link #newInstance} factory method instead, - * which allows for specifying the mapped type once only. * @param mappedClass the class that each row should be mapped to */ public BeanPropertyRowMapper(Class mappedClass) { @@ -222,8 +221,8 @@ public class BeanPropertyRowMapper implements RowMapper { this.mappedClass = mappedClass; this.mappedFields = new HashMap<>(); this.mappedProperties = new HashSet<>(); - PropertyDescriptor[] pds = BeanUtils.getPropertyDescriptors(mappedClass); - for (PropertyDescriptor pd : pds) { + + for (PropertyDescriptor pd : BeanUtils.getPropertyDescriptors(mappedClass)) { if (pd.getWriteMethod() != null) { this.mappedFields.put(lowerCaseName(pd.getName()), pd); String underscoredName = underscoreName(pd.getName()); @@ -250,12 +249,12 @@ public class BeanPropertyRowMapper implements RowMapper { StringBuilder result = new StringBuilder(); for (int i = 0; i < name.length(); i++) { - char s = name.charAt(i); - if (Character.isUpperCase(s)) { - result.append('_').append(Character.toLowerCase(s)); + char c = name.charAt(i); + if (Character.isUpperCase(c)) { + result.append('_').append(Character.toLowerCase(c)); } else { - result.append(s); + result.append(c); } } return result.toString(); @@ -280,11 +279,12 @@ public class BeanPropertyRowMapper implements RowMapper { */ @Override public T mapRow(ResultSet rs, int rowNumber) throws SQLException { - Assert.state(this.mappedClass != null, "Mapped class was not specified"); - T mappedObject = BeanUtils.instantiateClass(this.mappedClass); - BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(mappedObject); + BeanWrapperImpl bw = new BeanWrapperImpl(); initBeanWrapper(bw); + T mappedObject = constructMappedInstance(rs, bw); + bw.setBeanInstance(mappedObject); + ResultSetMetaData rsmd = rs.getMetaData(); int columnCount = rsmd.getColumnCount(); Set populatedProperties = (isCheckFullyPopulated() ? new HashSet<>() : null); @@ -336,13 +336,25 @@ public class BeanPropertyRowMapper implements RowMapper { if (populatedProperties != null && !populatedProperties.equals(this.mappedProperties)) { throw new InvalidDataAccessApiUsageException("Given ResultSet does not contain all fields " + - "necessary to populate object of class [" + this.mappedClass.getName() + "]: " + - this.mappedProperties); + "necessary to populate object of " + this.mappedClass + ": " + this.mappedProperties); } return mappedObject; } + /** + * Construct an instance of the mapped class for the current row. + * @param rs the ResultSet to map (pre-initialized for the current row) + * @param tc a TypeConverter with this RowMapper's conversion service + * @return a corresponding instance of the mapped class + * @throws SQLException if an SQLException is encountered + * @since 5.3 + */ + protected T constructMappedInstance(ResultSet rs, TypeConverter tc) throws SQLException { + Assert.state(this.mappedClass != null, "Mapped class was not specified"); + return BeanUtils.instantiateClass(this.mappedClass); + } + /** * Initialize the given BeanWrapper to be used for row mapping. * To be called for each row. @@ -359,6 +371,22 @@ public class BeanPropertyRowMapper implements RowMapper { } } + /** + * Retrieve a JDBC object value for the specified column. + *

The default implementation delegates to + * {@link #getColumnValue(ResultSet, int, Class)}. + * @param rs is the ResultSet holding the data + * @param index is the column index + * @param pd the bean property that each result object is expected to match + * @return the Object value + * @throws SQLException in case of extraction failure + * @see #getColumnValue(ResultSet, int, Class) + */ + @Nullable + protected Object getColumnValue(ResultSet rs, int index, PropertyDescriptor pd) throws SQLException { + return JdbcUtils.getResultSetValue(rs, index, pd.getPropertyType()); + } + /** * Retrieve a JDBC object value for the specified column. *

The default implementation calls @@ -367,20 +395,20 @@ public class BeanPropertyRowMapper implements RowMapper { * or to post-process values return from {@code getResultSetValue}. * @param rs is the ResultSet holding the data * @param index is the column index - * @param pd the bean property that each result object is expected to match + * @param paramType the target parameter type * @return the Object value * @throws SQLException in case of extraction failure + * @since 5.3 * @see org.springframework.jdbc.support.JdbcUtils#getResultSetValue(java.sql.ResultSet, int, Class) */ @Nullable - protected Object getColumnValue(ResultSet rs, int index, PropertyDescriptor pd) throws SQLException { - return JdbcUtils.getResultSetValue(rs, index, pd.getPropertyType()); + protected Object getColumnValue(ResultSet rs, int index, Class paramType) throws SQLException { + return JdbcUtils.getResultSetValue(rs, index, paramType); } /** - * Static factory method to create a new {@code BeanPropertyRowMapper} - * (with the mapped class specified only once). + * Static factory method to create a new {@code BeanPropertyRowMapper}. * @param mappedClass the class that each row should be mapped to * @see #newInstance(Class, ConversionService) */ @@ -389,8 +417,7 @@ public class BeanPropertyRowMapper implements RowMapper { } /** - * Static factory method to create a new {@code BeanPropertyRowMapper} - * (with the required type specified only once). + * Static factory method to create a new {@code BeanPropertyRowMapper}. * @param mappedClass the class that each row should be mapped to * @param conversionService the {@link ConversionService} for binding * JDBC values to bean properties, or {@code null} for none diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/DataClassRowMapper.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/DataClassRowMapper.java new file mode 100644 index 00000000000..599f151c72f --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/DataClassRowMapper.java @@ -0,0 +1,151 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.jdbc.core; + +import java.lang.reflect.Constructor; +import java.sql.ResultSet; +import java.sql.SQLException; + +import org.springframework.beans.BeanUtils; +import org.springframework.beans.TypeConverter; +import org.springframework.core.DefaultParameterNameDiscoverer; +import org.springframework.core.ParameterNameDiscoverer; +import org.springframework.core.convert.ConversionService; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * {@link RowMapper} implementation that converts a row into a new instance + * of the specified mapped target class. The mapped target class must be a + * top-level class and may either expose a data class constructor with named + * parameters corresponding to column names or classic bean property setters + * (or even a combination of both). + * + *

Note that this class extends {@link BeanPropertyRowMapper} and can + * therefore serve as a common choice for any mapped target class, flexibly + * adapting to constructor style versus setter methods in the mapped class. + * + * @author Juergen Hoeller + * @since 5.3 + * @param the result type + */ +public class DataClassRowMapper extends BeanPropertyRowMapper { + + private static final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer(); + + @Nullable + private Constructor mappedConstructor; + + @Nullable + private String[] constructorParameterNames; + + @Nullable + private Class[] constructorParameterTypes; + + + /** + * Create a new {@code DataClassRowMapper} for bean-style configuration. + * @see #setMappedClass + * @see #setConversionService + */ + public DataClassRowMapper() { + } + + /** + * Create a new {@code DataClassRowMapper}. + * @param mappedClass the class that each row should be mapped to + */ + public DataClassRowMapper(Class mappedClass) { + super(mappedClass); + } + + + @SuppressWarnings("unchecked") + @Override + protected void initialize(Class mappedClass) { + super.initialize(mappedClass); + + this.mappedConstructor = BeanUtils.findPrimaryConstructor(mappedClass); + + if (this.mappedConstructor == null) { + Constructor[] ctors = mappedClass.getConstructors(); + if (ctors.length == 1) { + this.mappedConstructor = (Constructor) ctors[0]; + } + else { + try { + this.mappedConstructor = mappedClass.getDeclaredConstructor(); + } + catch (NoSuchMethodException ex) { + throw new IllegalStateException("No primary or default constructor found for " + mappedClass, ex); + } + } + } + + if (this.mappedConstructor.getParameterCount() > 0) { + this.constructorParameterNames = BeanUtils.getParameterNames(this.mappedConstructor); + this.constructorParameterTypes = this.mappedConstructor.getParameterTypes(); + } + } + + @Override + protected T constructMappedInstance(ResultSet rs, TypeConverter tc) throws SQLException { + Assert.state(this.mappedConstructor != null, "Mapped constructor was not initialized"); + + Object[] args; + if (this.constructorParameterNames != null && this.constructorParameterTypes != null) { + args = new Object[this.constructorParameterNames.length]; + for (int i = 0; i < args.length; i++) { + String name = underscoreName(this.constructorParameterNames[i]); + Class type = this.constructorParameterTypes[i]; + args[i] = tc.convertIfNecessary(getColumnValue(rs, rs.findColumn(name), type), type); + } + } + else { + args = new Object[0]; + } + + return BeanUtils.instantiateClass(this.mappedConstructor, args); + } + + + /** + * Static factory method to create a new {@code DataClassRowMapper}. + * @param mappedClass the class that each row should be mapped to + * @see #newInstance(Class, ConversionService) + */ + public static DataClassRowMapper newInstance(Class mappedClass) { + return new DataClassRowMapper<>(mappedClass); + } + + /** + * Static factory method to create a new {@code DataClassRowMapper}. + * @param mappedClass the class that each row should be mapped to + * @param conversionService the {@link ConversionService} for binding + * JDBC values to bean properties, or {@code null} for none + * @see #newInstance(Class) + * @see #setConversionService + */ + public static DataClassRowMapper newInstance( + Class mappedClass, @Nullable ConversionService conversionService) { + + DataClassRowMapper rowMapper = newInstance(mappedClass); + rowMapper.setConversionService(conversionService); + return rowMapper; + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/SingleColumnRowMapper.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/SingleColumnRowMapper.java index 0a2e9cb491c..3b762906d80 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/SingleColumnRowMapper.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/SingleColumnRowMapper.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 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. @@ -62,8 +62,6 @@ public class SingleColumnRowMapper implements RowMapper { /** * Create a new {@code SingleColumnRowMapper}. - *

Consider using the {@link #newInstance} factory method instead, - * which allows for specifying the required type once only. * @param requiredType the type that each result object is expected to match */ public SingleColumnRowMapper(Class requiredType) { @@ -216,8 +214,7 @@ public class SingleColumnRowMapper implements RowMapper { /** - * Static factory method to create a new {@code SingleColumnRowMapper} - * (with the required type specified only once). + * Static factory method to create a new {@code SingleColumnRowMapper}. * @param requiredType the type that each result object is expected to match * @since 4.1 * @see #newInstance(Class, ConversionService) @@ -227,8 +224,7 @@ public class SingleColumnRowMapper implements RowMapper { } /** - * Static factory method to create a new {@code SingleColumnRowMapper} - * (with the required type specified only once). + * Static factory method to create a new {@code SingleColumnRowMapper}. * @param requiredType the type that each result object is expected to match * @param conversionService the {@link ConversionService} for converting a * fetched value, or {@code null} for none diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/AbstractRowMapperTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/AbstractRowMapperTests.java index a0f97306395..93716e5e9d0 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/core/AbstractRowMapperTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/AbstractRowMapperTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 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,7 +25,10 @@ import java.sql.Statement; import java.sql.Timestamp; import java.util.Date; +import org.springframework.beans.BeanWrapper; +import org.springframework.beans.PropertyAccessorFactory; import org.springframework.jdbc.core.test.ConcretePerson; +import org.springframework.jdbc.core.test.ConstructorPerson; import org.springframework.jdbc.core.test.DatePerson; import org.springframework.jdbc.core.test.Person; import org.springframework.jdbc.core.test.SpacePerson; @@ -48,32 +51,50 @@ import static org.mockito.Mockito.verify; */ public abstract class AbstractRowMapperTests { - protected void verifyPerson(Person bean) throws Exception { - assertThat(bean.getName()).isEqualTo("Bubba"); - assertThat(bean.getAge()).isEqualTo(22L); - assertThat(bean.getBirth_date()).usingComparator(Date::compareTo).isEqualTo(new java.util.Date(1221222L)); - assertThat(bean.getBalance()).isEqualTo(new BigDecimal("1234.56")); + protected void verifyPerson(Person person) { + assertThat(person.getName()).isEqualTo("Bubba"); + assertThat(person.getAge()).isEqualTo(22L); + assertThat(person.getBirth_date()).usingComparator(Date::compareTo).isEqualTo(new java.util.Date(1221222L)); + assertThat(person.getBalance()).isEqualTo(new BigDecimal("1234.56")); + verifyPersonViaBeanWrapper(person); } - protected void verifyPerson(ConcretePerson bean) throws Exception { - assertThat(bean.getName()).isEqualTo("Bubba"); - assertThat(bean.getAge()).isEqualTo(22L); - assertThat(bean.getBirth_date()).usingComparator(Date::compareTo).isEqualTo(new java.util.Date(1221222L)); - assertThat(bean.getBalance()).isEqualTo(new BigDecimal("1234.56")); + protected void verifyPerson(ConcretePerson person) { + assertThat(person.getName()).isEqualTo("Bubba"); + assertThat(person.getAge()).isEqualTo(22L); + assertThat(person.getBirth_date()).usingComparator(Date::compareTo).isEqualTo(new java.util.Date(1221222L)); + assertThat(person.getBalance()).isEqualTo(new BigDecimal("1234.56")); + verifyPersonViaBeanWrapper(person); } - protected void verifyPerson(SpacePerson bean) { - assertThat(bean.getLastName()).isEqualTo("Bubba"); - assertThat(bean.getAge()).isEqualTo(22L); - assertThat(bean.getBirthDate()).isEqualTo(new Timestamp(1221222L).toLocalDateTime()); - assertThat(bean.getBalance()).isEqualTo(new BigDecimal("1234.56")); + protected void verifyPerson(SpacePerson person) { + assertThat(person.getLastName()).isEqualTo("Bubba"); + assertThat(person.getAge()).isEqualTo(22L); + assertThat(person.getBirthDate()).isEqualTo(new Timestamp(1221222L).toLocalDateTime()); + assertThat(person.getBalance()).isEqualTo(new BigDecimal("1234.56")); } - protected void verifyPerson(DatePerson bean) { - assertThat(bean.getLastName()).isEqualTo("Bubba"); - assertThat(bean.getAge()).isEqualTo(22L); - assertThat(bean.getBirthDate()).isEqualTo(new java.sql.Date(1221222L).toLocalDate()); - assertThat(bean.getBalance()).isEqualTo(new BigDecimal("1234.56")); + protected void verifyPerson(DatePerson person) { + assertThat(person.getLastName()).isEqualTo("Bubba"); + assertThat(person.getAge()).isEqualTo(22L); + assertThat(person.getBirthDate()).isEqualTo(new java.sql.Date(1221222L).toLocalDate()); + assertThat(person.getBalance()).isEqualTo(new BigDecimal("1234.56")); + } + + protected void verifyPerson(ConstructorPerson person) { + assertThat(person.name()).isEqualTo("Bubba"); + assertThat(person.age()).isEqualTo(22L); + assertThat(person.birth_date()).usingComparator(Date::compareTo).isEqualTo(new java.util.Date(1221222L)); + assertThat(person.balance()).isEqualTo(new BigDecimal("1234.56")); + verifyPersonViaBeanWrapper(person); + } + + private void verifyPersonViaBeanWrapper(Object person) { + BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(person); + assertThat(bw.getPropertyValue("name")).isEqualTo("Bubba"); + assertThat(bw.getPropertyValue("age")).isEqualTo(22L); + assertThat((Date) bw.getPropertyValue("birth_date")).usingComparator(Date::compareTo).isEqualTo(new java.util.Date(1221222L)); + assertThat(bw.getPropertyValue("balance")).isEqualTo(new BigDecimal("1234.56")); } @@ -123,6 +144,11 @@ public abstract class AbstractRowMapperTests { given(resultSetMetaData.getColumnLabel(3)).willReturn("birth_date"); given(resultSetMetaData.getColumnLabel(4)).willReturn("balance"); + given(resultSet.findColumn("name")).willReturn(1); + given(resultSet.findColumn("age")).willReturn(2); + given(resultSet.findColumn("birth_date")).willReturn(3); + given(resultSet.findColumn("balance")).willReturn(4); + jdbcTemplate = new JdbcTemplate(); jdbcTemplate.setDataSource(new SingleConnectionDataSource(connection, false)); jdbcTemplate.setExceptionTranslator(new SQLStateSQLExceptionTranslator()); diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/BeanPropertyRowMapperTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/BeanPropertyRowMapperTests.java index 5672c88ca51..6e1f84a632d 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/core/BeanPropertyRowMapperTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/BeanPropertyRowMapperTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 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. @@ -37,9 +37,8 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType; */ public class BeanPropertyRowMapperTests extends AbstractRowMapperTests { - @Test - @SuppressWarnings({ "unchecked", "rawtypes" }) + @SuppressWarnings({"unchecked", "rawtypes"}) public void testOverridingDifferentClassDefinedForMapping() { BeanPropertyRowMapper mapper = new BeanPropertyRowMapper(Person.class); assertThatExceptionOfType(InvalidDataAccessApiUsageException.class).isThrownBy(() -> diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/DataClassRowMapperTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/DataClassRowMapperTests.java new file mode 100644 index 00000000000..bc2cae0f40e --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/DataClassRowMapperTests.java @@ -0,0 +1,45 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.jdbc.core; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +import org.springframework.jdbc.core.test.ConstructorPerson; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Juergen Hoeller + * @since 5.3 + */ +public class DataClassRowMapperTests extends AbstractRowMapperTests { + + @Test + public void testStaticQueryWithDataClass() throws Exception { + Mock mock = new Mock(); + List result = mock.getJdbcTemplate().query( + "select name, age, birth_date, balance from people", + new DataClassRowMapper<>(ConstructorPerson.class)); + assertThat(result.size()).isEqualTo(1); + verifyPerson(result.get(0)); + + mock.verifyClosed(); + } + +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConstructorPerson.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConstructorPerson.java new file mode 100644 index 00000000000..0e15987af63 --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConstructorPerson.java @@ -0,0 +1,60 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.jdbc.core.test; + +import java.math.BigDecimal; +import java.util.Date; + +/** + * @author Juergen Hoeller + */ +public class ConstructorPerson { + + private String name; + + private long age; + + private java.util.Date birth_date; + + private BigDecimal balance; + + + public ConstructorPerson(String name, long age, Date birth_date, BigDecimal balance) { + this.name = name; + this.age = age; + this.birth_date = birth_date; + this.balance = balance; + } + + + public String name() { + return name; + } + + public long age() { + return age; + } + + public Date birth_date() { + return birth_date; + } + + public BigDecimal balance() { + return balance; + } + +} diff --git a/spring-web/src/main/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessor.java b/spring-web/src/main/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessor.java index 38e6f981cb2..5d066659628 100644 --- a/spring-web/src/main/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessor.java +++ b/spring-web/src/main/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessor.java @@ -82,8 +82,6 @@ import org.springframework.web.multipart.support.StandardServletPartUtils; */ public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResolver, HandlerMethodReturnValueHandler { - private static final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer(); - protected final Log logger = LogFactory.getLog(getClass()); private final boolean annotationNotRequired; @@ -258,13 +256,8 @@ public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResol } // A single data class constructor -> resolve constructor arguments from request parameters. - ConstructorProperties cp = ctor.getAnnotation(ConstructorProperties.class); - String[] paramNames = (cp != null ? cp.value() : parameterNameDiscoverer.getParameterNames(ctor)); - Assert.state(paramNames != null, () -> "Cannot resolve parameter names for constructor " + ctor); + String[] paramNames = BeanUtils.getParameterNames(ctor); Class[] paramTypes = ctor.getParameterTypes(); - Assert.state(paramNames.length == paramTypes.length, - () -> "Invalid number of parameter names: " + paramNames.length + " for constructor " + ctor); - Object[] args = new Object[paramTypes.length]; WebDataBinder binder = binderFactory.createBinder(webRequest, null, attributeName); String fieldDefaultPrefix = binder.getFieldDefaultPrefix(); diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolver.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolver.java index f61a87c3a76..1afa1a131c9 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolver.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolver.java @@ -16,7 +16,6 @@ package org.springframework.web.reactive.result.method.annotation; -import java.beans.ConstructorProperties; import java.lang.annotation.Annotation; import java.lang.reflect.Constructor; import java.util.List; @@ -28,9 +27,7 @@ import reactor.core.publisher.MonoProcessor; import reactor.core.publisher.Sinks; import org.springframework.beans.BeanUtils; -import org.springframework.core.DefaultParameterNameDiscoverer; import org.springframework.core.MethodParameter; -import org.springframework.core.ParameterNameDiscoverer; import org.springframework.core.ReactiveAdapter; import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.core.ResolvableType; @@ -69,8 +66,6 @@ import org.springframework.web.server.ServerWebExchange; */ public class ModelAttributeMethodArgumentResolver extends HandlerMethodArgumentResolverSupport { - private static final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer(); - private final boolean useDefaultResolution; @@ -235,12 +230,8 @@ public class ModelAttributeMethodArgumentResolver extends HandlerMethodArgumentR // A single data class constructor -> resolve constructor arguments from request parameters. WebExchangeDataBinder binder = context.createDataBinder(exchange, null, attributeName); return getValuesToBind(binder, exchange).map(bindValues -> { - ConstructorProperties cp = ctor.getAnnotation(ConstructorProperties.class); - String[] paramNames = (cp != null ? cp.value() : parameterNameDiscoverer.getParameterNames(ctor)); - Assert.state(paramNames != null, () -> "Cannot resolve parameter names for constructor " + ctor); + String[] paramNames = BeanUtils.getParameterNames(ctor); Class[] paramTypes = ctor.getParameterTypes(); - Assert.state(paramNames.length == paramTypes.length, - () -> "Invalid number of parameter names: " + paramNames.length + " for constructor " + ctor); Object[] args = new Object[paramTypes.length]; String fieldDefaultPrefix = binder.getFieldDefaultPrefix(); String fieldMarkerPrefix = binder.getFieldMarkerPrefix();