没有找到 Java 记录和 BeanPropertyRowMapper 的默认构造函数
No default constructor found with Java record and BeanPropertyRowMapper
我正在玩新的 Java 14 和 Spring 引导。对于数据持有者,我使用了新的酷记录而不是常规的 Java class。
public record City(Long id, String name, Integer population) {}
稍后在我的服务 class 中,我使用 Spring BeanPropertyRowMapper
来获取数据。
@Override
public City findById(Long id) {
String sql = "SELECT * FROM cities WHERE id = ?";
return jtm.queryForObject(sql, new Object[]{id},
new BeanPropertyRowMapper<>(City.class));
}
我最终遇到以下错误:
Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [com.zetcode.model.City]: No default constructor found; nested exception is java.lang.NoSuchMethodException: com.zetcode.model.City.<init>()
at org.springframework.beans.BeanUtils.instantiateClass(BeanUtils.java:145) ~[spring-beans-5.2.3.RELEASE.jar:5.2.3.RELEASE]
如何为记录添加默认构造函数或者是否有任何其他方法可以解决此问题?
只需通过为字段提供默认值来显式声明它:
public record City(Long id, String name, Integer population) {
public City() {
this(0L, "", 0)
}
}
重要说明。 BeanPropertyRowMapper 扫描 setters/getters 来膨胀你的记录实例,因为 record 是不可变的,没有 setter 并且它不兼容使用 java beans 规范,您将获得空记录。请阅读此 。创建记录的唯一方法是使用构造函数。因此,您有两个选择:使用普通 java bean 或实现您的自定义行映射器。
它最简单的样子是这样的:
@Override
public City findById(final Long id) {
final var sql = "SELECT * FROM cities WHERE id = ?";
return jtm.queryForObject(
sql,
new Object[]{ id },
(rs, rowNum) -> new City(
rs.getLong("id"),
rs.getString("name"),
rs.getInt("population")));
}
或者您可以使用 reflection:
Reflection API
The following public methods will be added to java.lang.Class:
RecordComponent[] getRecordComponents()
boolean isRecord()
The method getRecordComponents() returns an array of
java.lang.reflect.RecordComponent objects, where
java.lang.reflect.RecordComponent is a new class. The elements of this
array correspond to the record’s components, in the same order as they
appear in the record declaration. Additional information can be
extracted from each RecordComponent in the array, including its name,
type, generic type, annotations, and its accessor method.
The method isRecord() returns true if the given class was declared as
a record. (Compare with isEnum().)
使用这些方法 Class#getConstructor(Class... parameterTypes) and Constructor#newInstance(Object... initargs) 您可以动态创建记录。但请记住,反射可能会带来一些开销并影响您的性能。
我添加了一个示例 RecordRowMapper using reflection and a couple of tests:
package by.slesh.spring.jdbc.core;
import org.springframework.jdbc.IncorrectResultSetColumnCountException;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.support.JdbcUtils;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.RecordComponent;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.util.*;
public class RecordRowMapper<T> implements RowMapper<T> {
private final Constructor<T> ctor;
private final List<Arg> args;
public RecordRowMapper(final Class<T> model) {
if (!model.isRecord()) {
throw new IllegalArgumentException(
model + " should be a record class");
}
final RecordComponent[] components = model.getRecordComponents();
this.args = new ArrayList<>(components.length);
final Class<?>[] argTypes = new Class[components.length];
for (int i = 0; i < components.length; ++i) {
final RecordComponent c = components[i];
this.args.add(new Arg(i, c.getName(), c.getType()));
argTypes[i] = c.getType();
}
try {
this.ctor = model.getConstructor(argTypes);
} catch (NoSuchMethodException e) {
throw new RuntimeException(
"Couldn resolve constructor for types " + Arrays.toString(argTypes));
}
}
@Override
public T mapRow(final ResultSet resultSet, final int rowNumber) throws SQLException {
final var metaData = resultSet.getMetaData();
final int columnCount = metaData.getColumnCount();
if (columnCount < args.size()) {
throw new IncorrectResultSetColumnCountException(
args.size(), columnCount);
}
try {
return ctor.newInstance(extractCtorParams(
resultSet, createPropertyToColumnIndexMap(
metaData, columnCount)));
} catch (InstantiationException
| IllegalAccessException
| InvocationTargetException e) {
throw new RuntimeException(e);
}
}
private Object[] extractCtorParams(
final ResultSet resultSet,
final Map<String, Integer> propertyToColumnIndexMap)
throws SQLException {
final var params = new Object[args.size()];
for (final var arg : args) {
final int columnIndex = propertyToColumnIndexMap.get(arg.name);
params[arg.order] = JdbcUtils.getResultSetValue(
resultSet, columnIndex, arg.type);
}
return params;
}
private Map<String, Integer> createPropertyToColumnIndexMap(
final ResultSetMetaData metaData,
final int columnCount)
throws SQLException {
final Map<String, Integer> columnPropertyToIndexMap = new HashMap<>(columnCount);
for (int columnIndex = 1; columnIndex <= columnCount; ++columnIndex) {
final String propertyName = JdbcUtils.convertUnderscoreNameToPropertyName(
JdbcUtils.lookupColumnName(metaData, columnIndex));
columnPropertyToIndexMap.put(propertyName, columnIndex);
}
return columnPropertyToIndexMap;
}
private static record Arg(int order, String name, Class<?>type) {
}
}
我正在玩新的 Java 14 和 Spring 引导。对于数据持有者,我使用了新的酷记录而不是常规的 Java class。
public record City(Long id, String name, Integer population) {}
稍后在我的服务 class 中,我使用 Spring BeanPropertyRowMapper
来获取数据。
@Override
public City findById(Long id) {
String sql = "SELECT * FROM cities WHERE id = ?";
return jtm.queryForObject(sql, new Object[]{id},
new BeanPropertyRowMapper<>(City.class));
}
我最终遇到以下错误:
Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [com.zetcode.model.City]: No default constructor found; nested exception is java.lang.NoSuchMethodException: com.zetcode.model.City.<init>()
at org.springframework.beans.BeanUtils.instantiateClass(BeanUtils.java:145) ~[spring-beans-5.2.3.RELEASE.jar:5.2.3.RELEASE]
如何为记录添加默认构造函数或者是否有任何其他方法可以解决此问题?
只需通过为字段提供默认值来显式声明它:
public record City(Long id, String name, Integer population) {
public City() {
this(0L, "", 0)
}
}
重要说明。 BeanPropertyRowMapper 扫描 setters/getters 来膨胀你的记录实例,因为 record 是不可变的,没有 setter 并且它不兼容使用 java beans 规范,您将获得空记录。请阅读此
它最简单的样子是这样的:
@Override
public City findById(final Long id) {
final var sql = "SELECT * FROM cities WHERE id = ?";
return jtm.queryForObject(
sql,
new Object[]{ id },
(rs, rowNum) -> new City(
rs.getLong("id"),
rs.getString("name"),
rs.getInt("population")));
}
或者您可以使用 reflection:
Reflection API
The following public methods will be added to java.lang.Class:
RecordComponent[] getRecordComponents() boolean isRecord()
The method getRecordComponents() returns an array of java.lang.reflect.RecordComponent objects, where java.lang.reflect.RecordComponent is a new class. The elements of this array correspond to the record’s components, in the same order as they appear in the record declaration. Additional information can be extracted from each RecordComponent in the array, including its name, type, generic type, annotations, and its accessor method.
The method isRecord() returns true if the given class was declared as a record. (Compare with isEnum().)
使用这些方法 Class#getConstructor(Class... parameterTypes) and Constructor#newInstance(Object... initargs) 您可以动态创建记录。但请记住,反射可能会带来一些开销并影响您的性能。
我添加了一个示例 RecordRowMapper using reflection and a couple of tests:
package by.slesh.spring.jdbc.core;
import org.springframework.jdbc.IncorrectResultSetColumnCountException;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.support.JdbcUtils;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.RecordComponent;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.util.*;
public class RecordRowMapper<T> implements RowMapper<T> {
private final Constructor<T> ctor;
private final List<Arg> args;
public RecordRowMapper(final Class<T> model) {
if (!model.isRecord()) {
throw new IllegalArgumentException(
model + " should be a record class");
}
final RecordComponent[] components = model.getRecordComponents();
this.args = new ArrayList<>(components.length);
final Class<?>[] argTypes = new Class[components.length];
for (int i = 0; i < components.length; ++i) {
final RecordComponent c = components[i];
this.args.add(new Arg(i, c.getName(), c.getType()));
argTypes[i] = c.getType();
}
try {
this.ctor = model.getConstructor(argTypes);
} catch (NoSuchMethodException e) {
throw new RuntimeException(
"Couldn resolve constructor for types " + Arrays.toString(argTypes));
}
}
@Override
public T mapRow(final ResultSet resultSet, final int rowNumber) throws SQLException {
final var metaData = resultSet.getMetaData();
final int columnCount = metaData.getColumnCount();
if (columnCount < args.size()) {
throw new IncorrectResultSetColumnCountException(
args.size(), columnCount);
}
try {
return ctor.newInstance(extractCtorParams(
resultSet, createPropertyToColumnIndexMap(
metaData, columnCount)));
} catch (InstantiationException
| IllegalAccessException
| InvocationTargetException e) {
throw new RuntimeException(e);
}
}
private Object[] extractCtorParams(
final ResultSet resultSet,
final Map<String, Integer> propertyToColumnIndexMap)
throws SQLException {
final var params = new Object[args.size()];
for (final var arg : args) {
final int columnIndex = propertyToColumnIndexMap.get(arg.name);
params[arg.order] = JdbcUtils.getResultSetValue(
resultSet, columnIndex, arg.type);
}
return params;
}
private Map<String, Integer> createPropertyToColumnIndexMap(
final ResultSetMetaData metaData,
final int columnCount)
throws SQLException {
final Map<String, Integer> columnPropertyToIndexMap = new HashMap<>(columnCount);
for (int columnIndex = 1; columnIndex <= columnCount; ++columnIndex) {
final String propertyName = JdbcUtils.convertUnderscoreNameToPropertyName(
JdbcUtils.lookupColumnName(metaData, columnIndex));
columnPropertyToIndexMap.put(propertyName, columnIndex);
}
return columnPropertyToIndexMap;
}
private static record Arg(int order, String name, Class<?>type) {
}
}