【问题标题】:No default constructor found with Java record and BeanPropertyRowMapper没有找到带有 Java 记录和 BeanPropertyRowMapper 的默认构造函数
【发布时间】:2020-06-25 13:19:34
【问题描述】:

我正在使用新的 Java 14 和 Spring Boot。对于数据持有者,我使用了新的酷记录而不是常规的 Java 类。

public record City(Long id, String name, Integer population) {}

稍后在我的服务类中,我使用 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]

如何为记录添加默认构造函数,或者有没有其他方法可以解决这个问题?

【问题讨论】:

  • 如何实现java.io.Serializable?比如public record City(Long id, String name, Integer population) implements java.io.Serializable {}?
  • @Naman 还是同样的错误。我已经设法使用自定义 RowMapper 运行示例。

标签: java spring-boot java-14 java-record


【解决方案1】:

只需通过为字段提供默认值来明确声明它:

public record City(Long id, String name, Integer population) {
    public City() {
        this(0L, "", 0)
    }
}

重要说明。 BeanPropertyRowMapper 扫描 setter/getter 以扩充您的记录实例,因为 record 是不可变的,没有 setter 并且它与 java bean 规范不兼容,您将获取并清空记录.请阅读此SO。创建记录的唯一方法是使用构造函数。所以,你有两个选择:要么使用普通的 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:

反射 API

以下公共方法将被添加到 java.lang.Class:

RecordComponent[] getRecordComponents()
boolean isRecord()

getRecordComponents() 方法返回一个数组 java.lang.reflect.RecordComponent 对象,其中 java.lang.reflect.RecordComponent 是一个新类。这其中的元素 数组对应于记录的组件,与它们的顺序相同 出现在记录声明中。附加信息可以是 从数组中的每个 RecordComponent 中提取,包括其名称, 类型、泛型类型、注解及其访问器方法。

如果给定的类被声明为,则方法 isRecord() 返回 true 一个记录。 (与 isEnum() 比较。)

使用这些方法和Class#getConstructor(Class... parameterTypes)Constructor#newInstance(Object... initargs),您可以动态创建记录。但请记住,反射可能会带来一些开销并影响您的表现。

我添加了一个使用反射的RecordRowMapper 示例和几个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) {
    }
}

【讨论】:

  • 报错消失但未获取数据; findById 返回空记录。知道可能出了什么问题吗?
  • 这是因为记录与java beans 规范不兼容。不确定您是否可以在您的案例中使用记录。请阅读此SO
  • BeanPropertyRowMapper 扫描 setter/getter 以扩充您的模型。记录是不可变的。创建记录的唯一方法是使用构造函数。所以,你有两个选择:要么使用普通的 java bean,要么实现你的自定义行映射器。
  • 来自 Java Architect 的 explanation 为什么他们不遵循 JavaBeans 约定。
猜你喜欢
  • 2012-03-06
  • 1970-01-01
  • 2012-01-10
  • 2023-03-20
  • 1970-01-01
  • 2016-07-18
  • 1970-01-01
  • 2011-04-09
  • 1970-01-01
相关资源
最近更新 更多