spring mybatis环境常量与枚举转换示例详解
常量与枚举对象的转换
日常 web 接口开发中经常遇到一些常量与枚举对象的转换,如:请求中 sex
值为 1
, 对应 java 程序中的枚举为 Sex.BOY
。如果要进行值的比较,对于 java 开发,枚举最方便的比较方式就是使用 ==
直接进行比较,这时就要将 sex
转为枚举,然后再进行后序判断。
而大多数程序中的枚举不是一个两个,可能上百上千个。用到以上处理逻辑的代码会更多。那么是否可以将这一逻辑拆分到业务代码之外,但又不改变接口调用方传值,也不改变原有响应值的形式呢?
另一种情况与前面提到的情况类似。大多数 web 程序中的数据最终会写入数据库,有些 DB 设计对存储要求很严格,一些标识类字段希望占用空间越少越好。在上面的情况中 Sex.BOY
, 在数据库中希望记录为 1
而不是 BOY
。如果使用 mybatis
,这可能需要实现大量的 TypeHandler
接口进行数据转换与逆向转换。是否有办法自动这一过程呢?
下面分别解决上面两个需求。这两个需求的实现都依赖类似下面这样的一个接口。
public interface CodeData { String getCode(); }
接口这样设计是因为以上需求抽象以后,都是将一个编码在合适的时机转化成一个枚举或者将一个枚举转为编码。所以问题的关键只是一个编码,至于编码对应的内容,是给人看的,而不是为了计算机。
假设 Sex
枚举如下定义
public enum Sex implements CodeData{ BOY("1","男"), GIRL("2", "女"); public final String code; public final String text; public(String code, String text){ this.code = code; this.text = text; } @Override public String getCode(){ return this.code; } public String getText(){ return this.text; } }
为方便说明其使用方式,再简单定义一个 User 类型
public Class User{ private String id; private String name; private Sex sex; // 又臭又长的getter/setter , 一定要有 }
对接口的请求与响应数据进行进行自动转换
通常我们写一个 @RestController
接口, 方法定义的参数大概分为以下两种情况:
- 使用
@RequestBody
修饰参数 - 不使用
@RequestBody
修饰
当然还有其他的情况,暂不涉及。
使用 @RequestBody 的情况
当使用 @RequestBody
修饰时,方法定义大概如下:
public User create(@RequestBody User user){ // .... return user; }
如果你使用了 springboot 那更方便了,默认情况下(没有对 pom 进行大的改动,排除 jackson 依赖),springboot 向工程中注入了 Jackson2ObjectMapperBuilderCustomizer
^2 用于方便用户修改对 json 类型的请求体进行反序列化。我们要做的就是告诉 spring 如何对 Sex
这类的属性进行反序列化。
这里需要用到的知识点是 jackson
的自定义序列化与反序列化 ^3。
需要针对 Sex
实现 JsonDeserializer
public class SexDeserializer extends JsonDeserializer<Sex> { @Override public Sex deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException { String text = jsonParser.getText(); if (StringUtils.isBlank(text)) { return null; } for (Sex e : Sex.values()) { if (text.equals(e.getCode())) { return e; } } return null; } }
然后将这个 Deserializer 注入到 spring 环境中的 ObjectMapper 中
@Configuration public class CustomJsonSerializerConfig{ @Bean public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer(CustomJsonSerializerProvider provider) { return jacksonObjectMapperBuilder -> { jacksonObjectMapperBuilder.deserializerByType(Sex.class, new SexDeserializer()); } } }
这样对 Sex 类型的属性反序列化都会由 SexDeserializer
。
下一个问题是,工程中有大量的枚举需要做类似的处理,有没有办法自动注册这些类似的 Deserializer
? 答案是肯定的, 编程不解决类似的,重复性的工作,就不要编程了。
要完成自动向环境中注册, 需要以下两个前提
- 扫描到 classpath 下所有需要自定义反序列化的类型
- 对每个类型构造一个 Deserializer
第一个问题特别复杂,需要篇幅很长,不能在本文仔细说明, 涉及到自定义 ClassPathBeanDefinitionScanner
, 需要理解 BeanDefinitionRegistryPostProcessor
, BeanDefinition
以及 springboot 启动的大致步骤。可以参考 mybatis 如何自动构造 mapper:org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration
,org.mybatis.spring.mapper.ClassPathMapperScanner
。
扫描类时,我们需要收集所有在 classpath 下的 CodeData
的枚举类型的实现类, 将这些枚举类型的集合注入到 spring 环境中。我们构造下面这样的一个类完成这件事。
public record CodeDataTypeProvider<T extends CodeData>(Set<T> codeDataTypes){}
解决第一个问题后,下一个问题就是为每一个 CodeData 类型构造一个 Deserializer
对象,我们需要设计一个抽象程序更高的类,这样就不用为第一个 CodeData
实现类创建一个特别的 Deserializer
了:
public class CodeDataDeserializer<T extends CodeData> extends JsonDeserializer<T> { private final T[] enums; public CodeDataDeserializer(Class<T> enumClass) { if (enumClass == null) { throw new IllegalArgumentException("Type argument cannot be null"); } if (!enumClass.isEnum()) { throw new IllegalArgumentException(enumClass.getName() + "is not a enum"); } this.enums = enumClass.getEnumConstants(); } @Override public T deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException { String text = jsonParser.getText(); if (StringUtils.isBlank(text)) { return null; } for (T e : enums) { if (text.equals(e.getCode())) { return e; } } return null; } }
最后一步就是将这些 Deserializer
注册到环境中的 ObjectMapper
@Bean public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer(CustomJsonSerializerProvider provider) { return jacksonObjectMapperBuilder -> { Set<Class<CodeData>> codeDataClass = provider.codeDataType(); if (CollectionUtils.isEmpty(codeDataClass)) { logger.warn("There is no JsonSerializer and CodeData type."); return; } if (codeDataClass != null) { for (Class<? extends CodeData> c : codeDataClass) { logger.debug("register custom json serializer {} for {}", "CodeDataSerializer", c); jacksonObjectMapperBuilder.deserializerByType(c, new CodeDataDeserializer<>(c)); // 这一步是为了响应数据时,也能自动将CodeData按同样的规则序列化 jacksonObjectMapperBuilder.serializerByType(c, new CodeDataSerializer<>()); } } }; }
不使用 @RequestBody 的情况
这种情况下,方法的定义通常是下面的样子
@GetMapping("/getUserBySex") public SearchUserResultBo getUserBySex(Sex sex){} @GetMapping("/getUserBySex") public SearchUserResultBo getUserBySex(@RequestParam Sex sex){} @GetMapping("/getUserBySex/{sex}") public SearchUserResultBo getUserBySex(@PathVariable Sex sex){} @GetMapping("/getUser") public User getUser(User user){}
这种情况情况下 spring 为我们预留了 org.springframework.format.Formatter
这个接口, 它的用法与 jackson 称之为序列化类型的用法类似, 为需要自定义转换的类型构造一个 Formatter , 注册到工程环境中。
同样存在前面的问题, Formatter 数量多且逻辑重复。 但是有前面基础,这里就比较轻松了, 共通的 Formatter 如下:
public class CodeDataFormatter<T extends CodeData> implements Formatter<T> { private final Class<T> tClass; public CodeDataFormatter(Class<T> tClass) { if (tClass == null) { throw new IllegalArgumentException("class can not be null."); } if (!tClass.isEnum()) { throw new IllegalArgumentException("class can not be null and must be a enum :" + tClass.getName()); } this.tClass = tClass; } @Override public T parse(@Nonnull String text, @Nonnull Locale locale) throws ParseException { if (StringUtils.isBlank(text)) { return null; } T[] values = tClass.getEnumConstants(); for (T t : values) { if (StringUtils.equal(t.getCode(), text)) { return t; } } return null; } @Override public String print(T object, Locale locale) { if (object == null) { return null; } return object.getCode(); } }
注册到环境中的方式如下
@Configuration public class WebConfig implements WebMvcConfigurer { @SuppressWarnings("unchecked") @Override public void addFormatters(FormatterRegistry registry) { Set<Class<CodeData>> codeDataClass = provider.codeDataType(); if (CollectionUtils.isEmpty(codeDataClass)) { logger.warn("There is no CodeData type."); return; } for (Class<? extends CodeData> c : codeDataClass) { logger.debug("register formatter {} for {}", "CodeDataFormatter", c); registry.addFormatterForFieldType(c, new CodeDataFormatter<>(c)); } } }
通过 mybatis 将数据落库与读取
对于 mybatis 的处理与 @RequestBody
的处理方式类似, 使用 spring boot 的工程都会通过 mybatis-spring-boot-starter
引入 mybatis , 这个依赖引入后,程序启动时,mybatis autoconfig 会获取环境中的 ConfigurationCustomizer
对 mybatis 配置进行定制化, 所以我们环境中注入一个 ConfigurationCustomizer
就可以了
@Bean ConfigurationCustomizer mybatisConfigurationCustomizer(MybatisTypeHandlerProvider provider) { return configuration -> { // customize ... Set<Class<CodeData>> codeDataClass = provider.codeDataClass(); if (CollectionUtils.isEmpty(codeDataClass)) { logger.warn("There is no TypeHandler and CodeData type."); return; } TypeHandlerRegistry registry = configuration.getTypeHandlerRegistry(); if (codeDataClass != null) { for (Class<CodeData> c : codeDataClass) { logger.debug("register type handler {} for {}", "CodeDataTypeHandler", c); registry.register(c, new CodeDataTypeHandler<>(c)); } } }; }
以上就是spring mybatis环境常量与枚举转换示例详解的详细内容,更多关于spring mybatis环境常量枚举转换的资料请关注脚本之家其它相关文章!
相关文章
ScheduledThreadPoolExecutor巨坑解决
这篇文章主要为大家介绍了使用ScheduledThreadPoolExecutor遇到的巨坑解决示例,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪2023-02-02maven依赖包冲突SLF4J: Class path contains multiple SLF4J bi
这篇文章主要给大家介绍了关于maven依赖包冲突SLF4J: Class path contains multiple SLF4J bindings的处理方法,这个问题通常是因为项目中存在多个SLF4J的实现绑定(bindings)导致的冲突,需要的朋友可以参考下2024-02-02
最新评论