Не найден конструктор по умолчанию с записью Java и BeanPropertyRowMapper - PullRequest
3 голосов
/ 13 марта 2020

Я играю с новыми 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]

Как добавить конструктор по умолчанию для записи или есть другой способ исправить это?

1 Ответ

5 голосов
/ 13 марта 2020

Просто объявите это явно, указав поля по умолчанию:

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

Важное примечание. BeanPropertyRowMapper сканирует сеттеры / геттеры для раздувания вашего экземпляра записи, так как запись неизменна, нет сеттера и она не совместима со спецификацией 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

Следующие публичные c методы будут добавлены к java .lang.Class:

RecordComponent[] getRecordComponents()
boolean isRecord()

Метод getRecordComponents () возвращает массив объектов java .lang.reflect.RecordComponent, где java .lang.reflect.RecordComponent - новый класс. Элементы этого массива соответствуют компонентам записи в том же порядке, в котором они указаны в объявлении записи. Дополнительная информация может быть извлечена из каждого RecordComponent в массиве, включая его имя, тип, тип generi c, аннотации и его метод доступа.

Метод isRecord () возвращает true, если данный класс был объявлен как запись. (Сравните с isEnum ().)

Используя эти методы и Class # getConstructor (Class ... parameterTypes) и Constructor # newInstance (Object ... initargs ) вы можете динамически создавать записи. Но имейте в виду, что рефлексия может принести некоторые накладные расходы и повлиять на вашу производительность.

Я добавил пример RecordRowMapper с использованием рефлексии и пару тестов :

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) {
    }
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...