首页 > 解决方案 > Spring JdbcTemplate 无法将 String 转换为 Enum(扩展特定接口),因为即使删除了默认转换器也会被调用

问题描述

我的基本 Spring 启动应用程序有一个端点,它接受一个字符串并将其用作键以使用 NamedParameterJdbcTemplate 从 DB 读取 char(1) 值。我的代码的简化版本:

应用程序.java

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

控制器.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)
        );
    }
}

请求.java

public class Request {
    private String key;
    // ... constructor / getter / setters ... 
}

响应.java

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

public class Response {
    private MyEnum value;
    // ... constructor / getter / setters ... 
}

我的枚举.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 通过枚举名称进行转换,而不是通过调用我的 'getId()' 方法(如预期的那样)。我必须创建自己的转换器......但是因为:

...我需要创建一个自定义 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());
    }
}

哪个....是错误的...我的转换器永远不会被调用,因为默认的 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:IDE 截图

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


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

我使用new BeanPropertyRowMapper(Response.class)它有自己的 ConversionService: private ConversionService conversionService = DefaultConversionService.getSharedInstance();

这绕过了任何 Spring 魔法并直接获得 DefaultConversionService ......为什么这是常态?要正确添加我的枚举转换,我必须创建自己的扩展 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 使用我的转换器而不必创建一个全新的类的唯一方法吗?

标签: javaspringenumsjdbctemplateconverters

解决方案


推荐阅读