SystemDictStarter系統(tǒng)數(shù)據(jù)字典自動轉(zhuǎn)換工具
0. 項(xiàng)目地址 0.1 依賴坐標(biāo) 1. 開始使用 1.1 數(shù)據(jù)準(zhǔn)備 1.2 字典緩存存儲 1.3 DictProvider 中的字典信息變動如何刷新字典? 2. 用法示例 2.1 基礎(chǔ)用法示例 2.2 靜態(tài)工具直接獲取字典信息 3. 其他 3.1 SpringBoot Actuator 端點(diǎn)支持 3.2 默認(rèn) Controller 接口 3.3 面對大量數(shù)據(jù)需要轉(zhuǎn)換的場景,是否會頻繁去調(diào)用接口獲取實(shí)際字典文本? 3.4 配置說明
在日常項(xiàng)目開發(fā)中,不免都會用到一些數(shù)據(jù)字典的信息,以及前端展示的時候通常也需要把這些數(shù)據(jù)字典值轉(zhuǎn)換成具體字典文本信息。遇到這種場景通常都是后端把字典的文本轉(zhuǎn)換好一起返回給前端,前端只需要直接轉(zhuǎn)換即可。一般情況下后端可能需要單獨(dú)給返回對象創(chuàng)建一個字段來存儲對應(yīng)的字典文本值,然后進(jìn)行手動的處理,這種方式通常比較繁瑣,在字段多的時候會增加更多的工作量。
本文基于 Jackson 的自定義注解功能實(shí)現(xiàn)了這一自動轉(zhuǎn)換過程,在字段上使用特定的注解配置,Jackson序列化的時候即可自動把字典值轉(zhuǎn)換成字典文本。
0. 項(xiàng)目地址
0.1 依賴坐標(biāo)
<dependency> <groupId>com.houkunlin</groupId> <artifactId>system-dict-starter</artifactId> <!-- 當(dāng)前版本:1.4.3 --> <version>${latest.version}</version> </dependency>
1. 開始使用
使用數(shù)據(jù)字典通常有兩種字典,一種是存儲在數(shù)據(jù)庫中的動態(tài)形式數(shù)據(jù)字典,一種是用枚舉對象硬編碼在代碼中的系統(tǒng)字典,本工具為了適應(yīng)第二種枚舉對象字典的情況,定義了一個枚舉字典掃描注解,需要在啟動類上使用注解,并定義要掃描的包信息。
// 啟動類上加注解,這一個步驟是必須的 @SystemDictScan(basePackages = "test.application.dict") public class Application { public static void main(String[] args) { SpringApplication.run(Application.class); } }
1.1 數(shù)據(jù)準(zhǔn)備
直接使用枚舉對象來做字典場景,枚舉對象需要實(shí)現(xiàn)一個 DictEnum<V> 接口才能被正常掃描到,枚舉對象有兩個自定義的注解 @DictConverter 和 @DictType 可以做一些相關(guān)配置
-
@DictType用來標(biāo)記枚舉對象的字典類型代碼 -
@DictConverter用來標(biāo)記是否對這個枚舉對象生成org.springframework.core.convert.converter.Converter轉(zhuǎn)換對象,提供使用枚舉接收參數(shù)時自動轉(zhuǎn)換字典值到相應(yīng)枚舉對象類型的功能,未加此注解將不會生成轉(zhuǎn)換器對象。
@DictConverter @DictType(value = "PeopleType", comment = "用戶類型") @Getter @AllArgsConstructor public enum PeopleType implements DictEnum<Integer> { /** 系統(tǒng)管理員 */ ADMIN(0, "系統(tǒng)管理"), /** 普通用戶 */ USER(1, "普通用戶"), ; private final Integer value; private final String title; ? @JsonCreator public static PeopleType getItem(Integer code) { return DictEnum.valueOf(values(), code); } }
前面在啟動類上加了注解功能僅僅只是啟用了基礎(chǔ)的功能,我們的字典可能還會存儲在數(shù)據(jù)庫或本地文件等其他地方,因此需要向系統(tǒng)提供一個 DictProvider 對象
@Component public class MyProvider implements DictProvider { @Override public boolean isStoreDictType() { return true; } ? @Override public Iterator<DictTypeVo> dictTypeIterator() { // 從其他地方(其他服務(wù)、數(shù)據(jù)庫、本地文件)加載完整的數(shù)據(jù)字典信息(字典類型+字典值列表) // 從這里返回的數(shù)據(jù)字典信息將會被存入緩存中,以便下次直接調(diào)用,當(dāng)有數(shù)據(jù)變動時可以發(fā)起 RefreshDictEvent 事件通知更新字典信息 final DictTypeVo typeVo = DictTypeVo.newBuilder("name", "測試字典") .add("1", "測試1") .add("2", "測試2") .build(); return Collections.singletonList(typeVo).iterator(); } }
上面 DictProvider 中返回的字典信息會被存儲在緩存中,但是可能我們會有一些數(shù)據(jù)量特別大的場景不適合直接把數(shù)據(jù)存儲在緩存中,有可能需要直接從數(shù)據(jù)庫中讀取,甚至去請求遠(yuǎn)程服務(wù)的信息,此時可以提供一個 RemoteDict 對象來處理這種情況,當(dāng)在緩存中找不到字典文本值的時候,會調(diào)用 RemoteDict 對象來嘗試進(jìn)一步讀取字典文本信息。
@Component public class MyRemoteDict implements RemoteDict { @Override public DictTypeVo getDictType(final String type) { // 從其他地方(其他服務(wù)、數(shù)據(jù)庫、本地文件)加載一個完整的數(shù)據(jù)字典信息(字典類型+字典值列表) return null; } ? @Override public String getDictText(final String type, final String value) { // 從其他地方(其他服務(wù)、數(shù)據(jù)庫、本地文件)加載一個字典文本信息 return null; } } ?
1.2 字典緩存存儲
在前面說到系統(tǒng)的枚舉字典和 DictProvider 提供的字典會被緩存,工具中已經(jīng)默認(rèn)提供了兩個緩存對象
-
LocalDictStore本地 Map 緩存存儲使用了ConcurrentHashMap來緩存字典值/字典文本信息 -
RedisDictStore使用了 Redis 來存儲字典值/字典文本信息,當(dāng)想啟用 Redis 存儲字典的時候只需要在項(xiàng)目中引入org.springframework.boot:spring-boot-starter-data-redis依賴并配置好 Redis 連接信息即可
有時候,上面提供的兩個緩存對象可能并不適用自己的業(yè)務(wù)場景,那么我們還可以手動實(shí)現(xiàn)一個緩存存儲對象 DictStore ,在手動實(shí)現(xiàn)緩存對象時前面的 RemoteDict 并不會生效,因此需要在 DictStore 中自行處理此種情況。
// 可參考 LocalDictStore 自行實(shí)現(xiàn)相關(guān)功能 @Component @AllArgsConstructor public class MyDictStore implements DictStore { private final RemoteDict remoteDict; ? @Override public void store(final DictTypeVo dictType) { ? } ? @Override public void store(final Iterator<DictValueVo> iterator) { ? } ? @Override public Set<String> dictTypeKeys() { return null; } ? @Override public DictTypeVo getDictType(final String type) { return remoteDict.getDictType(type); } ? @Override public String getDictText(final String type, final String value) { return remoteDict.getDictText(type, value); } }
1.3 DictProvider 中的字典信息變動如何刷新字典?
DictProvider 提供的字典信息是從其他地方讀取的,其字典數(shù)據(jù)有可能會產(chǎn)生變動,當(dāng)字典變動后可以發(fā)起 RefreshDictEvent 事件來觸發(fā)字典刷新。
@Component @AllArgsConstructor public class CommandRunnerTests implements CommandLineRunner { private final ApplicationEventPublisher publisher; ? @Override public void run(final String... args) throws Exception { // 發(fā)起 RefreshDictEvent 事件通知刷新字典信息 publisher.publishEvent(new RefreshDictEvent("test", true, true)); } }
2. 用法示例
2.1 基礎(chǔ)用法示例
為了正常能夠轉(zhuǎn)換數(shù)據(jù),因此需要使用一個 Jackson 的自定義注解 @DictText ,把此注解用在需要轉(zhuǎn)換的字段上即可。
@Data @AllArgsConstructor class Bean { @DictText("PeopleType") private String userType; private String userType1; } final Bean bean = new Bean("1", null); final String value = objectMapper.writeValueAsString(bean); System.out.println(bean); // Bean(userType=1,userType1=null) System.out.println(value); // {"userType":"1","userTypeText":"普通用戶","userType1":null}
我們不需要在對象中為字典文本創(chuàng)建一個單獨(dú)的字段,@DictText 會自動生成一個 字段名 + Text 的字段輸出到前端。但是有時候我們覺得 字段名 + Text 這個字段不行,想要用另外一個字段名稱,此時可以用下面這種方式:
@Data @AllArgsConstructor class Bean { @DictText(value = "PeopleType", fieldName = "typeText") private String userType; } final Bean bean = new Bean("1"); final String value = objectMapper.writeValueAsString(bean); System.out.println(bean); // Bean(userType=1) System.out.println(value); // {"userType":"1","typeText":"普通用戶"}
有時候我們可能用一個字符串字段來存儲多個字典文本信息,并通過特定的符號來分隔,例如:
@Data @AllArgsConstructor class Bean { @DictText(value = "PeopleType", array = @Array(split = ",")) private String userType; } final Bean bean = new Bean("0,1"); final String value = objectMapper.writeValueAsString(bean); System.out.println(bean); // Bean(userType=0,1) System.out.println(value); // {"userType":"0,1","userTypeText":"系統(tǒng)管理、普通用戶"}
當(dāng)然也有可能使用一個集合來存儲多個字典文本信息:
@Data @AllArgsConstructor class Bean { @DictText("PeopleType") private List<String> userType; } final Bean bean = new Bean(Arrays.asList("0", "1")); final String value = objectMapper.writeValueAsString(bean); System.out.println(bean); // Bean(userType=["0","1"]) System.out.println(value); // {"userType":["0","1"],"userTypeText":"系統(tǒng)管理、普通用戶"}
也許對于這種字典值列表可能需要輸出文本列表信息
@Data @AllArgsConstructor class Bean { @DictText(value = "PeopleType", array = @Array(toText = false)) private List<String> userType; } final Bean bean = new Bean(Arrays.asList("0", "1")); final String value = objectMapper.writeValueAsString(bean); System.out.println(bean); // Bean(userType=[0, 1]) System.out.println(value); // {"userType":["0","1"],"userTypeText":["系統(tǒng)管理","普通用戶"]}
2.2 靜態(tài)工具直接獲取字典信息
有時候不僅僅是用在返回給前端時自動轉(zhuǎn)換,可能在程序中也需要直接用到這些字典文本,此時可以通過靜態(tài)工具類來直接獲取字典文本信息
@Component @AllArgsConstructor public class CommandRunnerTests implements CommandLineRunner { @Override public void run(final String... args) throws Exception { System.out.println(DictUtil.getDictText("PeopleType", "1")) } }
靜態(tài)工具類無法處理多個字典的情況,也就是無法對 "0,1" 這種數(shù)據(jù)進(jìn)行自動分割,這種場景需要自行分割并獲取數(shù)據(jù)
3. 其他
3.1 SpringBoot Actuator 端點(diǎn)支持
提供了 dict 和 dict-system 兩個端點(diǎn)信息
// 獲取所有的字典名稱列表和一些配置的對象名稱 GET /actuator/dict/ ? // 獲取某個字典類型的完整信息 GET /actuator/dict/PeopleType ? // 獲取某個字典值的字典文本信息 GET /actuator/dict/PeopleType/1 ? // 獲取系統(tǒng)字典的名稱列表(枚舉對象) GET /actuator/dict-system ? // 獲取系統(tǒng)字典的完整信息 GET /actuator/dict-system/PeopleType
3.2 默認(rèn) Controller 接口
可通過一個配置 system.dict.controller.enabled 來配置是否啟用默認(rèn)接口,使用 system.dict.controller.prefix 來配置路徑前綴信息,啟用后將提供以下4個接口
-
${prefix}/{dict}通過字典類型代碼獲取字典類型信息 -
${prefix}/{dict}/{value}通過字典類型代碼和字典值獲取字典文本信息 -
${prefix}/?dict={dict}通過字典類型代碼獲取字典類型信息 -
${prefix}/?dict={dict}&value={value}通過字典類型代碼和字典值獲取字典文本信息
3.3 面對大量數(shù)據(jù)需要轉(zhuǎn)換的場景,是否會頻繁去調(diào)用接口獲取實(shí)際字典文本?
在 DictUtil 工具中增加了一層緩存,緩存使用了 Caffeine 并配置了一定的緩存過期時間 ,當(dāng)我們獲取一個字典文本的時候并不會直接去調(diào)用 DictStore 讀取字典文本,而是先從緩存中查找是否存在,如果存在則直接返回字典文本信息,并且當(dāng)從 DictStore 讀取失敗次數(shù)達(dá)到一定量時也不會繼續(xù)從 DictStore 中讀取數(shù)據(jù)。
這在使用 Redis 存儲的場景時可以有效的減少網(wǎng)絡(luò)請求,雖然 Redis 很快,但是也有可能會造成一定的網(wǎng)絡(luò)延時,這在轉(zhuǎn)換數(shù)量大的時候可以有效的縮短因轉(zhuǎn)換帶來的延時問題。
3.4 配置說明
-
system.dict字典配置-
raw-value=false是否顯示原生數(shù)據(jù)字典值。true 實(shí)際類型輸出,false 轉(zhuǎn)換成字符串值 -
text-value-default-null=false字典文本的值是否默認(rèn)為null,true 默認(rèn)為null,false 默認(rèn)為空字符串 -
on-boot-refresh-dict=true是否在啟動的時候刷新字典 -
map-value=false是否把字典值轉(zhuǎn)換成 Map 形式,包含字典值和文本。false 時在 json 中插入字段顯示字典文本;true 時把原字段的值變成 Map 數(shù)據(jù) -
refresh-dict-interval=60000兩次刷新字典事件的時間間隔;兩次刷新事件時間間隔小于配置參數(shù)將不會刷新。單位:毫秒
-
-
system.dict.cacheDictUtil 工具字典緩存-
enabled=true是否啟用緩存 -
maximumSize=500緩存最大容量 -
initialCapacity=50緩存初始化容量 -
duration=30s有效期時長 -
missNum=50在有效期內(nèi)同一個字典值未命中指定次數(shù)將快速返回,不再重復(fù)請求獲取數(shù)據(jù)字典信息
-
-
system.dict.controller默認(rèn)控制器-
enabled=true是否啟用 WEB 請求接口 -
prefix=/dictWEB 請求接口前綴
-
