Spring JdbcTemplate не может преобразовать String в Enum (который расширяет определенный интерфейс), поскольку преобразоJAVA

Программисты JAVA общаются здесь
Ответить
Anonymous
 Spring JdbcTemplate не может преобразовать String в Enum (который расширяет определенный интерфейс), поскольку преобразо

Сообщение Anonymous »

Мое базовое загрузочное приложение Spring имеет конечную точку, которая принимает одну строку и использует ее в качестве ключа для чтения значения char(1) из БД с помощью NamedParameterJdbcTemplate. Упрощенная версия моего кода:
Application.java

Код: Выделить всё

@SpringBootApplication
@EnableCaching
@EnableScheduling
@ComponentScan(basePackages = {"my.package"})
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
Controller.java

Код: Выделить всё

@RestController
@RequestMapping(path = "enpoint", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
public MyController {

@Autowired
private NamedParameterJdbcTemplate namedJdbcTemplate;

@PostMapping("do")
public Response doStuff(@RequestBody Request request) {
return this.namedJdbcTemplate.queryForObject(
"SELECT ONE_CHAR as value FROM TABLE WHERE KEY = :KEY",
new MapSqlParameterSource("KEY", request.getKey()),
new BeanPropertyRowMapper(Response.class)
);
}
}
Request.java

Код: Выделить всё

public class Request {
private String key;
// ... constructor / getter / setters ...
}
Response.java
Поскольку в таблице БД столбец ONE_CHAR может иметь только 3 возможных значения (A, B или С). Вместо того, чтобы иметь объект Response только с «частным строковым значением;», я создал MyEnum, который содержит только символы.

Код: Выделить всё

public class Response {
private MyEnum value;
// ... constructor / getter / setters ...
}
MyEnum.java
'A', 'B' и 'C' - неподходящие имена для моего перечисления значения, поэтому я помещаю эту информацию в переменную 'id' в своем перечислении.

Код: Выделить всё

public enum MyEnum implements EnumById {

GOOD_NAME("A"), NICE_NAME("B"), PERFECT_NAME("C");

private final String id;
// ... constructor / getter / setters ...
}
EnumById.java
Я создал интерфейс EnumById, поэтому у меня есть общий способ обработки всех перечислений с помощью та же проблема (значения, которые я хочу представить в виде перечислений, не могут или не должны использоваться в качестве имени переменной).

Код: Выделить всё

public interface EnumById {
String getId();
}
Этот код, конечно, не работает. Когда Spring пытается преобразовать любое значение, прочитанное из БД, StringToEnumConverter по умолчанию выполняет преобразование с помощью перечисления name, а не путем вызова моего метода getId() (как ожидалось). Мне нужно создать свой собственный конвертер... Но поскольку:
  • EnumById можно применять к нескольким перечислениям
  • Я нужна ссылка на класс для вызова clazz.getEnumConstants()
  • Я не хочу писать собственный преобразователь для каждого Enum, который будет расширять EnumById
...Мне нужно создать собственный ConverterFactory
StringToEnumByIdConverterFactory.java

Код: Выделить всё

public class StringToEnumByIdConverterFactory implements ConverterFactory {

private static class StringToEnumByIdConverter implements Converter {

private final T[] values;

public StringToEnumByIdConverter(final T[] values) {
this.values = values;
}

public T convert(final String source) {
for (final T value : values) {
if (source.equals(value.getId())) {
return value;
}
}
return null;
}
}

public  Converter getConverter(final Class targetType) {
return new StringToEnumByIdConverter(targetType.getEnumConstants());
}
}
WebConfig.java
Как сообщает нам https://www.baeldung.com/spring-type-conversions Я должен добавить свою фабрику преобразователей в регистр формата Spring... поэтому я написал:

Код: Выделить всё

@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverterFactory(new StringToEnumByIdConverterFactory());
}
}
Что.... неправильно... Мой конвертер никогда не будет вызываться, поскольку конвертер String-to-Enum по умолчанию имеет приоритет (поскольку он был добавлен ранее, что мой конвертер в форматтер реестр).
Мое решение — удалить конвертер по умолчанию (org.springframework.core.convert.support.StringToEnumConverterFactory), поставить свой и снова добавить конвертер по умолчанию, поэтому мой класс WebConfig теперь нравится это:
WebConfig.java

Код: Выделить всё

@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
registry.removeConvertible(String.class, Enum.class);
registry.addConverterFactory(new StringToEnumByIdConverterFactory());
registry.addConverterFactory(new StringToEnumConverterFactory());
}
}
Что снова неверно, потому что по какой-то причине StringToEnumConverterFactory не является общедоступным (даже класс ConversionUtils, используемый внутри него, почему? StringToEnumConverterFactory даже объявлен как окончательный) ... Поэтому мне пришлось скопировать это код в моем проекте.

Теперь, когда хотя бы мое приложение заработало, я отладил его и проверил, что реестр правильно обновляется при добавлении addFormatters. вызывается.
Когда я вызываю конечную точку, по-прежнему вызывается преобразователь по умолчанию. Я получаю то же исключение, которое получил бы, если бы код, связанный с конвертером, никогда не был написан/использован/вызван:

Код: Выделить всё

org.springframework.beans.TypeMismatchException: Failed to convert property value of type 'java.lang.String' to required type 'my.package.constants.Constants$MyEnum' for property 'value'; nested exception is org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.String] to type [my.package.constants.Constants$MyEnum] for value 'A'; nested exception is java.lang.IllegalArgumentException: No enum constant 'my.package.constants.Constants.MyEnum.A'
Посмотрев трассировку стека, я обнаружил, что проблема в org.springframework.beans.TypeConverterDelegate.convertIfNecessary.
Я приступил к отладке и обнаружил обнаружил, что используемый ConversionService не имеет тех же преобразователей, которые я видел при отладке моего класса WebConfig (в регистре моего класса было ~ 155 преобразователей, используемый здесь ConversionService имеет всего 52). Я ясно вижу StringToEnumConverterFactory по умолчанию:
Изображение

Где /Как я не могу указать правильному сервису ConversionService (их несколько?) использовать мои конвертеры?

ОБНОВЛЕНИЕ: НАЙДЕНО ПРОБЛЕМА, НО ЕЩЕ НЕРЕШЕННАЯ
Я использую новый BeanPropertyRowMapper(Response.class), который имеет собственный ConversionService:

Код: Выделить всё

private ConversionService conversionService = DefaultConversionService.getSharedInstance();
Это позволяет обойти любую магию Spring и напрямую получить DefaultConversionService... Почему, черт возьми, это норма? Чтобы правильно добавить преобразование Enum, мне пришлось бы создать свой собственный класс, расширяющий BeanPropertyRowMapper... на данный момент это кажется чрезвычайно запутанным. Я что-то упустил?

ОБНОВЛЕНИЕ: НАЙДЕНО «ПОДДЕЛОЧНОЕ» РЕШЕНИЕ (ПОСКОЛЬКУ ОЧЕНЬ УЖАСНО)
Написал это перед запросом, и теперь преобразование работает... Я переместил его куда-нибудь, оно будет вызываться только один раз и все равно работает.

Код: Выделить всё

// this is what BeanPropertyRowMapper use
DefaultConversionService sharedInstance = (DefaultConversionService) DefaultConversionService.getSharedInstance();
// remove default String to Enum conversion
sharedInstance.removeConvertible(String.class, Enum.class);
// addd my converter factory
sharedInstance.addConverterFactory(new StringToEnumByCodeConverterFactory());
// add again all default converter back, since:
//  - I removed only the String->Enum one
//  - Converters are placed in a LinkedMap
// No duplicates are inserted, I only get that the default Enum converter is added AFTER my converter
DefaultConversionService.addDefaultConverters(sharedInstance);
// Now my converter has priority on the default Strin->Enum one and BeanPropertyRowMapper can use it
Это глупо и некрасиво... действительно ли это единственный способ заставить BeanPropertyRowMapper использовать мой конвертер без необходимости создавать совершенно новый класс?

Подробнее здесь: https://stackoverflow.com/questions/667 ... ic-interfa
Ответить

Быстрый ответ

Изменение регистра текста: 
Смайлики
:) :( :oops: :roll: :wink: :muza: :clever: :sorry: :angel: :read: *x)
Ещё смайлики…
   
К этому ответу прикреплено по крайней мере одно вложение.

Если вы не хотите добавлять вложения, оставьте поля пустыми.

Максимально разрешённый размер вложения: 15 МБ.

Вернуться в «JAVA»