Spring JdbcTemplate 无法将 String 转换为 Enum扩展特定接口,因为即使删除了默认转换器也会调用

问题描述

我的基本 Spring Boot 应用程序有一个端点,它接受一个字符串并将其用作从 DB 读取 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

由于在 db 表中,列 ONE_CHAR 只能有 3 个可能的值(A、B 或 C)。我创建了一个仅包含字符的 MyEnum,而不是只有“私有字符串值;”的 Response 对象。

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 尝试转换从 DB 读取的任何值时,认的 StringToEnumConverter 通过枚举 name 进行转换,而不是通过调用我的 'getId()' 方法(如预期的那样)。我必须创建自己的转换器...但是因为:

  • EnumById 可以应用于多个枚举
  • 我需要 Class 引用来调用 clazz.getEnumConstants()
  • 我不想为每个 Enum 编写一个自定义转换器来扩展 EnumById

...我需要创建一个自定义的 ConverterFactory

StringToEnumByIdConverterFactory.java

public class StringToEnumByIdConverterFactory implements ConverterFactory<String,EnumById> {

    private static class StringToEnumByIdConverter<T extends EnumById> implements Converter<String,T> {

        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 <T extends EnumById> Converter<String,T> getConverter(final Class<T> 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());
    }
}

哪个......错了......我的转换器永远不会被调用,因为认的字符串到枚举转换器具有优先级(因为它是在我的转换器之前添加到格式化程序注册表中的)。

我的解决方案是删除认转换器(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 is not public(甚至不是the ConversionUtils class used inside it,为什么?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 是问题所在。

我去调试 and found out that the ConversionService used 没有我在调试 WebConfig 类时看到的相同转换器(我类中的寄存器有大约 155 个转换器,这里使用的 ConversionService 有只有 52)。我可以清楚地看到认的 StringToEnumConverterFactory:

IDE Screenshot

在哪里/如何告诉正确的 ConversionService(不止一个?)使用我的转换器?


更新:发现问题,但仍未解决

我使用 new 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 使用我的转换器而无需创建全新类的唯一方法吗?

解决方法

暂无找到可以解决该程序问题的有效方法,小编努力寻找整理中!

如果你已经找到好的解决方法,欢迎将解决方案带上本链接一起发送给小编。

小编邮箱:dio#foxmail.com (将#修改为@)