spring?mybatis環(huán)境常量與枚舉轉換示例詳解
常量與枚舉對象的轉換
日常 web 接口開發(fā)中經(jīng)常遇到一些常量與枚舉對象的轉換,如:請求中 sex
值為 1
, 對應 java 程序中的枚舉為 Sex.BOY
。如果要進行值的比較,對于 java 開發(fā),枚舉最方便的比較方式就是使用 ==
直接進行比較,這時就要將 sex
轉為枚舉,然后再進行后序判斷。
而大多數(shù)程序中的枚舉不是一個兩個,可能上百上千個。用到以上處理邏輯的代碼會更多。那么是否可以將這一邏輯拆分到業(yè)務代碼之外,但又不改變接口調用方傳值,也不改變原有響應值的形式呢?
另一種情況與前面提到的情況類似。大多數(shù) web 程序中的數(shù)據(jù)最終會寫入數(shù)據(jù)庫,有些 DB 設計對存儲要求很嚴格,一些標識類字段希望占用空間越少越好。在上面的情況中 Sex.BOY
, 在數(shù)據(jù)庫中希望記錄為 1
而不是 BOY
。如果使用 mybatis
,這可能需要實現(xiàn)大量的 TypeHandler
接口進行數(shù)據(jù)轉換與逆向轉換。是否有辦法自動這一過程呢?
下面分別解決上面兩個需求。這兩個需求的實現(xiàn)都依賴類似下面這樣的一個接口。
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 , 一定要有 }
對接口的請求與響應數(shù)據(jù)進行進行自動轉換
通常我們寫一個 @RestController
接口, 方法定義的參數(shù)大概分為以下兩種情況:
- 使用
@RequestBody
修飾參數(shù) - 不使用
@RequestBody
修飾
當然還有其他的情況,暫不涉及。
使用 @RequestBody 的情況
當使用 @RequestBody
修飾時,方法定義大概如下:
public User create(@RequestBody User user){ // .... return user; }
如果你使用了 springboot 那更方便了,默認情況下(沒有對 pom 進行大的改動,排除 jackson 依賴),springboot 向工程中注入了 Jackson2ObjectMapperBuilderCustomizer
^2 用于方便用戶修改對 json 類型的請求體進行反序列化。我們要做的就是告訴 spring 如何對 Sex
這類的屬性進行反序列化。
這里需要用到的知識點是 jackson
的自定義序列化與反序列化 ^3。
需要針對 Sex
實現(xiàn) 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 環(huán)境中的 ObjectMapper 中
@Configuration public class CustomJsonSerializerConfig{ @Bean public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer(CustomJsonSerializerProvider provider) { return jacksonObjectMapperBuilder -> { jacksonObjectMapperBuilder.deserializerByType(Sex.class, new SexDeserializer()); } } }
這樣對 Sex 類型的屬性反序列化都會由 SexDeserializer
。
下一個問題是,工程中有大量的枚舉需要做類似的處理,有沒有辦法自動注冊這些類似的 Deserializer
? 答案是肯定的, 編程不解決類似的,重復性的工作,就不要編程了。
要完成自動向環(huán)境中注冊, 需要以下兩個前提
- 掃描到 classpath 下所有需要自定義反序列化的類型
- 對每個類型構造一個 Deserializer
第一個問題特別復雜,需要篇幅很長,不能在本文仔細說明, 涉及到自定義 ClassPathBeanDefinitionScanner
, 需要理解 BeanDefinitionRegistryPostProcessor
, BeanDefinition
以及 springboot 啟動的大致步驟??梢詤⒖?mybatis 如何自動構造 mapper:org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration
,org.mybatis.spring.mapper.ClassPathMapperScanner
。
掃描類時,我們需要收集所有在 classpath 下的 CodeData
的枚舉類型的實現(xiàn)類, 將這些枚舉類型的集合注入到 spring 環(huán)境中。我們構造下面這樣的一個類完成這件事。
public record CodeDataTypeProvider<T extends CodeData>(Set<T> codeDataTypes){}
解決第一個問題后,下一個問題就是為每一個 CodeData 類型構造一個 Deserializer
對象,我們需要設計一個抽象程序更高的類,這樣就不用為第一個 CodeData
實現(xiàn)類創(chuàng)建一個特別的 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
注冊到環(huán)境中的 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)); // 這一步是為了響應數(shù)據(jù)時,也能自動將CodeData按同樣的規(guī)則序列化 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 , 注冊到工程環(huán)境中。
同樣存在前面的問題, Formatter 數(shù)量多且邏輯重復。 但是有前面基礎,這里就比較輕松了, 共通的 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(); } }
注冊到環(huán)境中的方式如下
@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 將數(shù)據(jù)落庫與讀取
對于 mybatis 的處理與 @RequestBody
的處理方式類似, 使用 spring boot 的工程都會通過 mybatis-spring-boot-starter
引入 mybatis , 這個依賴引入后,程序啟動時,mybatis autoconfig 會獲取環(huán)境中的 ConfigurationCustomizer
對 mybatis 配置進行定制化, 所以我們環(huán)境中注入一個 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環(huán)境常量與枚舉轉換示例詳解的詳細內容,更多關于spring mybatis環(huán)境常量枚舉轉換的資料請關注腳本之家其它相關文章!
相關文章
spring cloud gateway如何獲取請求的真實地址
這篇文章主要介紹了spring cloud gateway如何獲取請求的真實地址問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-05-05Spring Security實現(xiàn)動態(tài)路由權限控制方式
這篇文章主要介紹了Spring Security實現(xiàn)動態(tài)路由權限控制方式,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-08-08解決idea刪除模塊后重新創(chuàng)建顯示該模塊已經(jīng)被注冊的問題
這篇文章主要介紹了解決idea刪除模塊后重新創(chuàng)建顯示該模塊已經(jīng)被注冊的問題,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2021-02-02SpringBoot 創(chuàng)建對象常見的幾種方式小結
Spring Boot中創(chuàng)建對象的幾種常見方式包括使用@Component、@Service、@Repository或@Controller注解,本文就來詳細的介紹一下,感興趣的可以了解一下2024-11-11