java - 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()' 方法(如预期的那样)。我必须创建自己的转换器......但是因为:
- EnumById 可以应用于多个枚举
- 我需要类引用来调用 clazz.getEnumConstants()
- 我不想为每个将扩展 EnumById 的 Enum 编写自定义转换器
...我需要创建一个自定义 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:
我在哪里/如何不能告诉正确的 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 使用我的转换器而不必创建一个全新的类的唯一方法吗?
解决方案
推荐阅读
- c# - Visual stuido 2019 找不到 abd.exe
- tensorflow - tf.nn.ctc_loss 和 pytorch.nn.CTCLoss 有什么区别
- c# - 如何在组件的类中注入 DateTimeProvider / wrapper 依赖项但不使用构造函数?
- scala - Spark Dataframes 多个 JOIN
- c# - 用 C# 中的相应 html 替换多个引号标签
- bioinformatics - Snakemake 在两个不同的通配符子集上发布运行规则
- reactjs - 重新渲染组件后如何使酶预期错误
- c# - DateTime.TryParse 似乎正在丢失原始时区信息并转换为服务器时间
- python - 在 Python 中从给定的最大位数和小数位数创建最大可能的十进制数
- java - 将自定义 RequestBody 从 POST 请求发送到 java 中的外部 GET 请求