SpringJDBCTemplate无法将字符串转换为枚举(扩展特定接口),因为即使删除了默认转换器,也会调用它

noj0wjuj  于 2021-07-15  发布在  Java
关注(0)|答案(0)|浏览(179)

我的基本spring启动应用程序有一个端点,它接受一个字符串,并将其作为键从db中读取一个char(1)值,名称为namedparameterjdbctemplate。我的代码的简化版本:
应用程序.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)。而不是只有一个“private string value;”的响应对象,我创造了一个只会说chars的髓鞘。

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 ... 
}

枚举ID.java
我创建了一个接口'enumbyid',这样我就有了一个通用的方法来处理具有相同问题的所有枚举(我要表示为枚举的值不能或不应该用作变量名)。

public interface EnumById {
    String getId();
}

这个代码当然不起作用。当spring试图转换从db中读取的任何值时,默认的stringtoenumconverter按枚举名进行转换,而不是调用我的getid()方法(如预期的那样)。我必须创建自己的转换器。。。但自从:
enumbyid可以应用于多个枚举
我需要类引用来调用clazz.getenumconstants()
我不想为每个扩展enumbyid的枚举编写自定义转换器
... 我需要创建一个自定义转换器工厂
StringToEnumbydConverterFactory.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());
    }
}

网络配置.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类现在是这样的:
网络配置.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:

在哪里/如何我不能告诉正确的转换服务(有多个?)使用我的转换器?

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

我用 new BeanPropertyRowMapper(Response.class) 它有自己的转换服务: 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

这又蠢又丑。。。这真的是欺骗beanpropertyrowapper使用我的转换器而不必创建一个全新类的唯一方法吗?

暂无答案!

目前还没有任何答案,快来回答吧!

相关问题