一种优雅的数据字典文本转换方式
0. 项目地址 0.1 依赖坐标 1. 开始使用 1.1 数据准备 1.2 字典缓存存储 1.3 DictProvider 中的字典信息变动如何刷新字典? 2. 用法示例 2.1 基础用法示例 2.2 静态工具直接获取字典信息 3. 其他 3.1 SpringBoot Actuator 端点支持 3.2 默认 Controller 接口 3.3 面对大量数据需要转换的场景,是否会频繁去调用接口获取实际字典文本? 3.4 配置说明
在日常项目开发中,不免都会用到一些数据字典的信息,以及前端展示的时候通常也需要把这些数据字典值转换成具体字典文本信息。遇到这种场景通常都是后端把字典的文本转换好一起返回给前端,前端只需要直接转换即可。一般情况下后端可能需要单独给返回对象创建一个字段来存储对应的字典文本值,然后进行手动的处理,这种方式通常比较繁琐,在字段多的时候会增加更多的工作量。 本文基于 Jackson 的自定义注解功能实现了这一自动转换过程,在字段上使用特定的注解配置,Jackson序列化的时候即可自动把字典值转换成字典文本。
0. 项目地址
0.1 依赖坐标
<dependency> <groupId>com.houkunlin</groupId> <artifactId>system-dict-starter</artifactId> <!-- 当前版本:1.4.3 --> <version>${latest.version}</version> </dependency>
1. 开始使用
使用数据字典通常有两种字典,一种是存储在数据库中的动态形式数据字典,一种是用枚举对象硬编码在代码中的系统字典,本工具为了适应第二种枚举对象字典的情况,定义了一个枚举字典扫描注解,需要在启动类上使用注解,并定义要扫描的包信息。
// 启动类上加注解,这一个步骤是必须的 @SystemDictScan(basePackages = "test.application.dict") public class Application { public static void main(String[] args) { SpringApplication.run(Application.class); } }
1.1 数据准备
直接使用枚举对象来做字典场景,枚举对象需要实现一个 DictEnum<V>
接口才能被正常扫描到,枚举对象有两个自定义的注解 @DictConverter
和 @DictType
可以做一些相关配置
@DictType
用来标记枚举对象的字典类型代码@DictConverter
用来标记是否对这个枚举对象生成org.springframework.core.convert.converter.Converter
转换对象,提供使用枚举接收参数时自动转换字典值到相应枚举对象类型的功能,未加此注解将不会生成转换器对象。
@DictConverter @DictType(value = "PeopleType", comment = "用户类型") @Getter @AllArgsConstructor public enum PeopleType implements DictEnum<Integer> { /** 系统管理员 */ ADMIN(0, "系统管理"), /** 普通用户 */ USER(1, "普通用户"), ; private final Integer value; private final String title; @JsonCreator public static PeopleType getItem(Integer code) { return DictEnum.valueOf(values(), code); } }
前面在启动类上加了注解功能仅仅只是启用了基础的功能,我们的字典可能还会存储在数据库或本地文件等其他地方,因此需要向系统提供一个 DictProvider
对象
@Component public class MyProvider implements DictProvider { @Override public boolean isStoreDictType() { return true; } @Override public Iterator<DictTypeVo> dictTypeIterator() { // 从其他地方(其他服务、数据库、本地文件)加载完整的数据字典信息(字典类型+字典值列表) // 从这里返回的数据字典信息将会被存入缓存中,以便下次直接调用,当有数据变动时可以发起 RefreshDictEvent 事件通知更新字典信息 final DictTypeVo typeVo = DictTypeVo.newBuilder("name", "测试字典") .add("1", "测试1") .add("2", "测试2") .build(); return Collections.singletonList(typeVo).iterator(); } }
上面 DictProvider
中返回的字典信息会被存储在缓存中,但是可能我们会有一些数据量特别大的场景不适合直接把数据存储在缓存中,有可能需要直接从数据库中读取,甚至去请求远程服务的信息,此时可以提供一个 RemoteDict
对象来处理这种情况,当在缓存中找不到字典文本值的时候,会调用 RemoteDict
对象来尝试进一步读取字典文本信息。
@Component public class MyRemoteDict implements RemoteDict { @Override public DictTypeVo getDictType(final String type) { // 从其他地方(其他服务、数据库、本地文件)加载一个完整的数据字典信息(字典类型+字典值列表) return null; } @Override public String getDictText(final String type, final String value) { // 从其他地方(其他服务、数据库、本地文件)加载一个字典文本信息 return null; } }
1.2 字典缓存存储
在前面说到系统的枚举字典和 DictProvider
提供的字典会被缓存,工具中已经默认提供了两个缓存对象
LocalDictStore
本地 Map 缓存存储使用了ConcurrentHashMap
来缓存字典值/字典文本信息RedisDictStore
使用了 Redis 来存储字典值/字典文本信息,当想启用 Redis 存储字典的时候只需要在项目中引入org.springframework.boot:spring-boot-starter-data-redis
依赖并配置好 Redis 连接信息即可
有时候,上面提供的两个缓存对象可能并不适用自己的业务场景,那么我们还可以手动实现一个缓存存储对象 DictStore
,在手动实现缓存对象时前面的 RemoteDict
并不会生效,因此需要在 DictStore
中自行处理此种情况。
// 可参考 LocalDictStore 自行实现相关功能 @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
提供的字典信息是从其他地方读取的,其字典数据有可能会产生变动,当字典变动后可以发起 RefreshDictEvent
事件来触发字典刷新。
@Component @AllArgsConstructor public class CommandRunnerTests implements CommandLineRunner { private final ApplicationEventPublisher publisher; @Override public void run(final String... args) throws Exception { // 发起 RefreshDictEvent 事件通知刷新字典信息 publisher.publishEvent(new RefreshDictEvent("test", true, true)); } }
2. 用法示例
2.1 基础用法示例
为了正常能够转换数据,因此需要使用一个 Jackson 的自定义注解 @DictText
,把此注解用在需要转换的字段上即可。
@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}
我们不需要在对象中为字典文本创建一个单独的字段,@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":"系统管理、普通用户"}
当然也有可能使用一个集合来存储多个字典文本信息:
@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":"系统管理、普通用户"}
也许对于这种字典值列表可能需要输出文本列表信息
@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":["系统管理","普通用户"]}
2.2 静态工具直接获取字典信息
有时候不仅仅是用在返回给前端时自动转换,可能在程序中也需要直接用到这些字典文本,此时可以通过静态工具类来直接获取字典文本信息
@Component @AllArgsConstructor public class CommandRunnerTests implements CommandLineRunner { @Override public void run(final String... args) throws Exception { System.out.println(DictUtil.getDictText("PeopleType", "1")) } }
静态工具类无法处理多个字典的情况,也就是无法对 "0,1"
这种数据进行自动分割,这种场景需要自行分割并获取数据
3. 其他
3.1 SpringBoot Actuator 端点支持
提供了 dict
和 dict-system
两个端点信息
// 获取所有的字典名称列表和一些配置的对象名称 GET /actuator/dict/ // 获取某个字典类型的完整信息 GET /actuator/dict/PeopleType // 获取某个字典值的字典文本信息 GET /actuator/dict/PeopleType/1 // 获取系统字典的名称列表(枚举对象) GET /actuator/dict-system // 获取系统字典的完整信息 GET /actuator/dict-system/PeopleType
3.2 默认 Controller 接口
可通过一个配置 system.dict.controller.enabled
来配置是否启用默认接口,使用 system.dict.controller.prefix
来配置路径前缀信息,启用后将提供以下4个接口
${prefix}/{dict}
通过字典类型代码获取字典类型信息${prefix}/{dict}/{value}
通过字典类型代码和字典值获取字典文本信息${prefix}/?dict={dict}
通过字典类型代码获取字典类型信息${prefix}/?dict={dict}&value={value}
通过字典类型代码和字典值获取字典文本信息
3.3 面对大量数据需要转换的场景,是否会频繁去调用接口获取实际字典文本?
在 DictUtil
工具中增加了一层缓存,缓存使用了 Caffeine
并配置了一定的缓存过期时间 ,当我们获取一个字典文本的时候并不会直接去调用 DictStore
读取字典文本,而是先从缓存中查找是否存在,如果存在则直接返回字典文本信息,并且当从 DictStore
读取失败次数达到一定量时也不会继续从 DictStore
中读取数据。
这在使用 Redis 存储的场景时可以有效的减少网络请求,虽然 Redis 很快,但是也有可能会造成一定的网络延时,这在转换数量大的时候可以有效的缩短因转换带来的延时问题。
3.4 配置说明
system.dict
字典配置raw-value=false
是否显示原生数据字典值。true 实际类型输出,false 转换成字符串值text-value-default-null=false
字典文本的值是否默认为null,true 默认为null,false 默认为空字符串on-boot-refresh-dict=true
是否在启动的时候刷新字典map-value=false
是否把字典值转换成 Map 形式,包含字典值和文本。false 时在 json 中插入字段显示字典文本;true 时把原字段的值变成 Map 数据refresh-dict-interval=60000
两次刷新字典事件的时间间隔;两次刷新事件时间间隔小于配置参数将不会刷新。单位:毫秒
system.dict.cache
DictUtil 工具字典缓存enabled=true
是否启用缓存maximumSize=500
缓存最大容量initialCapacity=50
缓存初始化容量duration=30s
有效期时长missNum=50
在有效期内同一个字典值未命中指定次数将快速返回,不再重复请求获取数据字典信息
system.dict.controller
默认控制器enabled=true
是否启用 WEB 请求接口prefix=/dict
WEB 请求接口前缀

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
GRPC: 如何添加 API Prometheus 监控拦截器/中间件?
介绍 本文将介绍如何在 gRPC 微服务中添加 API Prometheus(普罗米修斯)拦截器/中间件。也就是可以在 Grafana 里做的 API 监控。 什么是 API Prometheus(普罗米修斯)拦截器/中间件? Prometheus(普罗米修斯)拦截器会对每一个 API 请求记录 Prometheus(普罗米修斯)监控。 我们将会使用 rk-boot 来启动 gRPC 服务。 请访问如下地址获取完整教程: https://rkdev.info/cn https://rkdocs.netlify.app/cn (备用) 安装 go get github.com/rookie-ninja/rk-boot 快速开始 rk-boot 默认集成如下两个开源库。 rk-prom 作为普罗米修斯(Prometheus)客户端启动库。 注意!为了例子能够顺滑进行,请务必在 go.mod 文件里,module 的后缀设置成 rk-demo。 例如:module github.com/your-repo/rk-demo 1.创建 boot.yaml 为了验证,我们启动了如下几个选项: co...
- 下一篇
是谁在Go标准库的源码中植入了色情网站?
昨天,有网友在群里说在GitHub上发现了色情网站! GitHub上怎么会有色情网站呢?网友给出了下面的截图: 这个出现在Go标准库中的Issue里面,有一个url... 该Issue地址:https://github.com/golang/go/issues/48886 DD小心翼翼的复制到浏览器,打开...不可描述的画面出现了... 这个事情传开后,也引来了一些中国开发者的围观: 也有群友表示,这网站中文的,肯定是中国人提交的,真给国人丢脸... 谁提交了这段代码? 那么到底是谁提交了这段代码呢? 从issue中,我们可以看到这个url出现在这个文件中:go/src/sync/example_test.go 可惜点开“History”,并没发现相关的提交信息: 仔细看提交时间!居然这个黄色站点的URL在7年前就被植入了! 继续翻看历史,DD找到了10年前的代码: 点开它,惊人的一幕出现,居然10年前就有了! 再看History,看来了两个提交 点开它们,终于被DD发现谁提交了这段代码! 这段代码是一名为adg的开发者在2012年2月18日提交的! 谁是adg? 到这里,我们就很好奇...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- Jdk安装(Linux,MacOS,Windows),包含三大操作系统的最全安装
- CentOS8,CentOS7,CentOS6编译安装Redis5.0.7
- CentOS6,7,8上安装Nginx,支持https2.0的开启
- CentOS关闭SELinux安全模块
- 2048小游戏-低调大师作品
- Windows10,CentOS7,CentOS8安装MongoDB4.0.16
- 设置Eclipse缩进为4个空格,增强代码规范
- CentOS8安装MyCat,轻松搞定数据库的读写分离、垂直分库、水平分库
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- SpringBoot2全家桶,快速入门学习开发网站教程