问题描述
我的基本 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()' 方法(如预期的那样)。我必须创建自己的转换器...但是因为:
...我需要创建一个自定义的 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:
在哪里/如何告诉正确的 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 (将#修改为@)