spring mybatis环境常量与枚举转换示例详解

 更新时间:2023年06月02日 08:49:10   作者:Mzoro  
这篇文章主要为大家介绍了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, 需要理解 BeanDefinitionRegistryPostProcessorBeanDefinition 以及 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环境常量枚举转换的资料请关注脚本之家其它相关文章!

相关文章

最新评论