基于AOP的数据字典实现:实现前端下拉框的可配置更新

1. 前言

在现代软件开发中,数据字典作为管理系统常量和配置项的重要工具,其灵活性和可维护性对系统的健壮性起着至关重要的作用。然而,传统的数据字典与业务模块的整合方式往往存在着严重的耦合问题。通常情况下,为了在业务模块中使用数据字典的标签(label),我们不得不在VO类中添加字段,并通过查询数据字典来获取对应的标签值,这种做法不仅增加了代码的复杂性,还使得业务模块与数据字典的耦合度过高,不利于系统的模块化和扩展。

本文将探讨如何利用面向切面编程(AOP)的思想,通过注解的方式实现数据字典与其他业务模块的无侵入性整合。我们将重点关注如何通过AOP技术,使数据字典的值(value)在业务模块中自动转换为其对应的标签(label),从而实现业务逻辑与数据字典的松耦合,为系统的可维护性和拓展性提供新的解决方案。

2. 数据字典

2.1. 数据字典简介

数据字典是软件系统中用于管理常量、配置项或者枚举值的集合。它通常包括标签(label)和值(value)两部分,标签用于展示给用户或者其他系统模块,而值则是实际的业务逻辑中使用的数据标识。我举个例子吧,比如前端下拉框的渲染:

51caab6146837bc96ad7d54cb12a8df.png

 我们来看一下前端代码:

 

js

复制代码

<template> <el-select v-model="value" placeholder="请选择"> <el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value"> </el-option> </el-select> </template> <script> export default { data() { return { options: [{ value: '选项1', label: '黄金糕' }, { value: '选项2', label: '双皮奶' }, { value: '选项3', label: '蚵仔煎' }, { value: '选项4', label: '龙须面' }, { value: '选项5', label: '北京烤鸭' }], value: '' } } } </script>

从前端代码可看出 下拉框的渲染主要依靠valuelabel,常规的做法有枚举,或者后端建表后从表中获取,这两种方法都有许多弊端,枚举的话需要开发人员写死在代码中,再来看建表,如果每个下拉框都建表,那就会浪费大量后端资源,采用数据字典,统一管理各个功能模块的下拉框是较优的选择。

2.2. 数据字典如何管理各模块的下拉框

数据字典中是如何把各模块的下拉框管理起来的,在数据字典中一共管理三块内

 整理了这份面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题

需要全套面试笔记【点击此处】即可免费获取

容,分别是实体类(表),属性字段,属性字段值(数据字典value和label);以前端的视角来看就是表单,下拉框,下拉框的值(数据字典label和value)。

数据字典.drawio.png

3. 数据字典核心内容解读

3.1. 表结构

数据字典一共涵盖两张表,分别为dictionary_type和dictionary_value,下面将分别对这两张表进行解释。

小肥肠-dict[数据字典]-202477115537.png

 

sql

复制代码

CREATE TABLE "public"."dictionary_type" ( "id" varchar(32) COLLATE "pg_catalog"."default" NOT NULL, "type_name" varchar(50) COLLATE "pg_catalog"."default", "type_description" varchar(100) COLLATE "pg_catalog"."default", "parent_id" varchar(32) COLLATE "pg_catalog"."default", "create_time" timestamp(6), "update_time" timestamp(6), "version" int4 DEFAULT 1, "type_label" varchar(50) COLLATE "pg_catalog"."default", "is_deleted" int2 DEFAULT 0, CONSTRAINT "dictionary_type_pkey" PRIMARY KEY ("id") ) ; ALTER TABLE "public"."dictionary_type" OWNER TO "postgres"; COMMENT ON COLUMN "public"."dictionary_type"."id" IS '主键ID'; COMMENT ON COLUMN "public"."dictionary_type"."type_name" IS '字典类型名称'; COMMENT ON COLUMN "public"."dictionary_type"."type_description" IS '字典类型描述'; COMMENT ON COLUMN "public"."dictionary_type"."parent_id" IS '父节点id'; COMMENT ON COLUMN "public"."dictionary_type"."create_time" IS '创建时间'; COMMENT ON COLUMN "public"."dictionary_type"."update_time" IS '更新时间'; COMMENT ON COLUMN "public"."dictionary_type"."version" IS '乐观锁'; COMMENT ON COLUMN "public"."dictionary_type"."type_label" IS '字典类型标签'; COMMENT ON TABLE "public"."dictionary_type" IS '字典类型表';

 dictionary_type表管理实体类和属性字段,当parent_id为null时则该数据为实体类,否则为归属某实体类下的属性字段。

dictionary_value

 

sql

复制代码

CREATE TABLE "public"."dictionary_value" ( "id" varchar(32) COLLATE "pg_catalog"."default" NOT NULL, "value_name" varchar(50) COLLATE "pg_catalog"."default", "type_id" varchar(32) COLLATE "pg_catalog"."default", "create_time" timestamp(6), "update_time" timestamp(6), "version" int4 DEFAULT 1, "value_label" varchar(50) COLLATE "pg_catalog"."default", "value_sort" int4, "is_deleted" int2, CONSTRAINT "dictionary_value_pkey" PRIMARY KEY ("id") ) ; ALTER TABLE "public"."dictionary_value" OWNER TO "postgres"; COMMENT ON COLUMN "public"."dictionary_value"."id" IS '主键ID'; COMMENT ON COLUMN "public"."dictionary_value"."value_name" IS '字典值名称'; COMMENT ON COLUMN "public"."dictionary_value"."type_id" IS '字典类型id'; COMMENT ON COLUMN "public"."dictionary_value"."create_time" IS '创建时间'; COMMENT ON COLUMN "public"."dictionary_value"."update_time" IS '更新时间'; COMMENT ON COLUMN "public"."dictionary_value"."version" IS '乐观锁'; COMMENT ON COLUMN "public"."dictionary_value"."value_label" IS '字典值标签'; COMMENT ON COLUMN "public"."dictionary_value"."value_sort" IS '字典值排序'; COMMENT ON TABLE "public"."dictionary_value" IS '字典值表';

dictionary_value 中管理某实体类下属性字段多对应的数据字典(label和value)。dictionary_value 和dictionary_type为多对一的关系(一个属性字段下对应多个数据字典值)。

3.2. 核心代码 

3.2.1. 根据实体类名称获取下属数据字典

controller层

 

java

复制代码

/** * 获取模块数据字典 * @param typeName * @return */ @ApiOperation("获取某个模块下的数据字典") @GetMapping("/parameter/{typeName}") Map<String, Object> getCompleteParameter(@PathVariable("typeName") String typeName){ return iDictionaryValueService.getParameters(typeName); }

在上述代码中typeName为实体类名称。 

service层

 

java

复制代码

public Map<String, Object> getParameters(String typeName) { List<Map<String, Object>> dictParameters=baseMapper.getDictParameters(typeName); Set<Object> typeSet= new HashSet<>(); Map<String,Object>resParam=new HashMap<>(); for (Map<String, Object> dictParameter : dictParameters) { typeSet.add(dictParameter.get("type_name").toString()); } for (Object o : typeSet) { List<ParameterVO> parameterVoList = new ArrayList<>(); for (Map<String, Object> dictParameter : dictParameters) { if(dictParameter.get("type_name").toString().equals(o.toString())){ ParameterVO parameterVO=new ParameterVO(dictParameter.get("value_name").toString(),dictParameter.get("value_label").toString()); parameterVoList.add(parameterVO); } } resParam.put(o.toString(),parameterVoList); } return resParam; }

mapper层

 

java

复制代码

@Select("select a.value_name,a.value_label,a.type_name from dictionary_type d JOIN (select v.value_name,v.value_label,t.type_name,t.parent_id from dictionary_value v,dictionary_type t where v.type_id=t.id and v.is_deleted = 0 and t.is_deleted = 0)a on a.parent_id=d.id where d.type_name =#{typeName} AND d.is_deleted = 0") List<Map<String, Object>> getDictParameters(@Param("typeName") String typeName);

3.2.2. 数据字典AOP切面
3.2.2.1. 场景模拟

先预设一个场景,假设有一张学生表需要整合数据字典,表结构如下:

 

sql

复制代码

CREATE TABLE "public"."student" ( "id" varchar(32) COLLATE "pg_catalog"."default" NOT NULL, "name" varchar(50) COLLATE "pg_catalog"."default", "blood_type" varchar(10) COLLATE "pg_catalog"."default", "constellation_type" varchar(10) COLLATE "pg_catalog"."default", "create_time" timestamp(6), "update_time" timestamp(6), "version" int4 DEFAULT 1, "is_deleted" int2 DEFAULT 0, CONSTRAINT "student_pkey" PRIMARY KEY ("id") ) ; ALTER TABLE "public"."student" OWNER TO "postgres"; COMMENT ON COLUMN "public"."student"."blood_type" IS '血型'; COMMENT ON COLUMN "public"."student"."constellation_type" IS '星座类型';

在上表中星座和血型为需要和数据字典集成的字段。 

3.2.2.2. 数据字典交互流程

AOP切面主要使用在分页查询和查询详情时。与数据字典有交集的实体类在分页或查询详情时技术流程图如下:

数据字典交互.drawio.png

在上图中可看出与数据字典有交集的模块要进行分页或查询详情时,需要远程调用数据字典模块的相关接口,通过数据表中的value查询数据字典对应的label,最后封装为vo类返回给前端,如果把这个逻辑以硬编码的形式内嵌到查询详情代码中的话,有个比较致命的缺点就是代码的耦合性太高了,不利于模块的迁移复用。

image.png

上述代码为查看详情的部分代码,在封装VO类时进行了硬编码,可以看出,在耦合性极高的同时,代码的可读性也较差,故引入AOP切面,将远程调用label和将label值更新至VO类写入AOP切面。

3.2.2.3. AOP代码

数据字典AOP注解,它的作用是用于标记类的字段,指示字段的字典类型,并且在序列化过程中使用自定义的序列化器进行处理。

 

java

复制代码

@Target({ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) @JacksonAnnotationsInside @JsonSerialize(using = DictSerializer.class) public @interface Dict { /** 字典类型 */ String type(); }

通过 @JsonSerialize(using = DictSerializer.class),我们告诉 Jackson 在对带有 @Dict 注解的字段进行序列化时,使用 DictSerializer 类来处理序列化过程。这种方式可以根据实际需求,灵活地控制字段的输出格式,实现定制化的序列化逻辑。

数据字典序列化类:

 

java

复制代码

@Component public class DictSerializer extends StdSerializer<Object> implements ContextualSerializer { private IDictionaryValueService dictionaryValueService; private String type; @Autowired public DictSerializer(IDictionaryValueService dictionaryValueService) { super(Object.class); this.dictionaryValueService = dictionaryValueService; } public DictSerializer(String type, IDictionaryValueService dictionaryValueService) { super(Object.class); this.type = type; this.dictionaryValueService = dictionaryValueService; } @Override public void serialize(Object value, JsonGenerator gen, SerializerProvider provider) throws IOException { if (Objects.isNull(value)) { gen.writeObject(value); return; } String label = null; if (dictionaryValueService != null && type != null) { try { String response = dictionaryValueService.getLabelByValue(value.toString()); label = response; // 设置为空时返回 "null" } catch (RuntimeException e) { label = null; } } gen.writeObject(value); gen.writeFieldName(gen.getOutputContext().getCurrentName() + "Label"); gen.writeObject(label); } @Override public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException { if (property != null) { Dict dict = property.getAnnotation(Dict.class); if (dict != null) { return new DictSerializer(dict.type(), dictionaryValueService); } } return this; } }

DictSerializer 是一个用于处理带有 @Dict 注解字段的自定义 Jackson 序列化器。它利用注入的 IDictionaryValueService 接口,根据字段值获取对应的标签,并将原始值与标签作为新字段输出,实现了动态字典值的序列化处理。

我写的示例代码把AOP相关代码写到了数据字典模块,但是实际项目中应当放到common模块,方便所有和数据字典有交集的业务模块调用。

4. 数据字典使用

基于第3章预设的场景,我们这章直接实操来看一下如何使用数据字典(ps,我将Student类相关代码写到了数据字典中,实际应该是在别的模块,这里为了方便我就写到了一个模块)。

4.1. 新增Student类对应数据字典值

新增dictionary_type表数据:

image.png

新增dictionary_value 表数据:

image.png

根据实体类名获取该实体类对应的数据字典,返回至前端进行下拉框动态渲染:

image.png

4.2. 新增学生数据

这里新增和平时操作无异:

 

java

复制代码

@PostMapping("") public boolean saveStudent(@RequestBody Student student){ return studentService.save(student); }

在传数据字典值时只需要传入value值即可:

image.png

4.3. 根据id查询学生数据详细信息

编写VO类:

 

java

复制代码

@Data public class StudentVO { private String id; private String name; @Dict(type = "bloodType") private String bloodType; @Dict(type = "constellationType") private String constellationType; @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") @JsonFormat(pattern="yyyy-MM-dd HH:mm:ss",timezone="GMT+8") private Date createTime; }

查看详情方法:

 

java

复制代码

public StudentVO getStudentInfoById(String id) { Student student = baseMapper.selectById(id); StudentVO studentVO= BeanCopyUtils.copyBean(student,StudentVO.class); return studentVO; }

运行结果:

image.png

5. 结语

本文探讨了如何通过面向切面编程(AOP)实现数据字典与业务模块的无侵入整合。通过自定义注解和序列化器,我们有效地降低了系统中业务模块与数据字典的耦合度,提升了系统的灵活性和可维护性。希望本文能为读者在实际项目中应用这些技术提供启发,进一步提升软件开发的效率和质量。若本文对你有帮助,别忘记三连哦~~

  • 10
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值