作者:后端小肥肠
创作不易,未经允许严禁转载。
目录
1. 前言
在现代软件开发中,数据字典作为管理系统常量和配置项的重要工具,其灵活性和可维护性对系统的健壮性起着至关重要的作用。然而,传统的数据字典与业务模块的整合方式往往存在着严重的耦合问题。通常情况下,为了在业务模块中使用数据字典的标签(label),我们不得不在VO类中添加字段,并通过查询数据字典来获取对应的标签值,这种做法不仅增加了代码的复杂性,还使得业务模块与数据字典的耦合度过高,不利于系统的模块化和扩展。
本文将探讨如何利用面向切面编程(AOP)的思想,通过注解的方式实现数据字典与其他业务模块的无侵入性整合。我们将重点关注如何通过AOP技术,使数据字典的值(value)在业务模块中自动转换为其对应的标签(label),从而实现业务逻辑与数据字典的松耦合,为系统的可维护性和拓展性提供新的解决方案。
2. 数据字典
2.1. 数据字典简介
数据字典是软件系统中用于管理常量、配置项或者枚举值的集合。它通常包括标签(label)和值(value)两部分,标签用于展示给用户或者其他系统模块,而值则是实际的业务逻辑中使用的数据标识。我举个例子吧,比如前端下拉框的渲染:
我们来看一下前端代码:
<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>
从前端代码可看出 下拉框的渲染主要依靠value和label,常规的做法有枚举,或者后端建表后从表中获取,这两种方法都有许多弊端,枚举的话需要开发人员写死在代码中,再来看建表,如果每个下拉框都建表,那就会浪费大量后端资源,采用数据字典,统一管理各个功能模块的下拉框是较优的选择。
2.2. 数据字典如何管理各模块的下拉框
数据字典中是如何把各模块的下拉框管理起来的,在数据字典中一共管理三块内容,分别是实体类(表),属性字段,属性字段值(数据字典value和label);以前端的视角来看就是表单,下拉框,下拉框的值(数据字典label和value)。
3. 数据字典核心内容解读
3.1. 表结构
数据字典一共涵盖两张表,分别为dictionary_type和dictionary_value,下面将分别对这两张表进行解释。
dictionary_type
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
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层
/**
* 获取模块数据字典
* @param typeName
* @return
*/
@ApiOperation("获取某个模块下的数据字典")
@GetMapping("/parameter/{typeName}")
Map<String, Object> getCompleteParameter(@PathVariable("typeName") String typeName){
return iDictionaryValueService.getParameters(typeName);
}
在上述代码中typeName为实体类名称。
service层
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层
@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. 场景模拟
先预设一个场景,假设有一张学生表需要整合数据字典,表结构如下:
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切面主要使用在分页查询和查询详情时。与数据字典有交集的实体类(Student)在分页或查询详情时技术流程图如下:
在上图中可看出与数据字典有交集的模块要进行分页或查询详情时,需要远程调用数据字典模块的相关接口,通过数据表中的value查询数据字典对应的label,最后封装为vo类返回给前端,如果把这个逻辑以硬编码的形式内嵌到查询详情代码中的话,有个比较致命的缺点就是代码的耦合性太高了,不利于模块的迁移复用。
上述代码为查看详情的部分代码,在封装VO类时进行了硬编码,可以看出,在耦合性极高的同时,代码的可读性也较差,故引入AOP切面,将远程调用label和将label值更新至VO类写入AOP切面。
3.2.2.3. AOP代码
数据字典AOP注解,它的作用是用于标记类的字段,指示字段的字典类型,并且在序列化过程中使用自定义的序列化器进行处理。
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside
@JsonSerialize(using = DictSerializer.class)
public @interface Dict {
/** 字典类型 */
String type();
}
通过 @JsonSerialize(using = DictSerializer.class)
,我们告诉 Jackson 在对带有 @Dict
注解的字段进行序列化时,使用 DictSerializer
类来处理序列化过程。
数据字典序列化类:
@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表数据:
新增dictionary_value 表数据:
根据实体类名获取该实体类对应的数据字典,返回至前端进行下拉框动态渲染:
4.2. 新增学生数据
这里新增和平时操作无异:
@PostMapping("")
public boolean saveStudent(@RequestBody Student student){
return studentService.save(student);
}
在传数据字典值时只需要传入value值即可:
4.3. 根据id查询学生数据详细信息
编写VO类:
@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;
}
查看详情方法:
public StudentVO getStudentInfoById(String id) {
Student student = baseMapper.selectById(id);
StudentVO studentVO= BeanCopyUtils.copyBean(student,StudentVO.class);
return studentVO;
}
运行结果:
5. 结语
本文探讨了如何通过面向切面编程(AOP)实现数据字典与业务模块的无侵入整合。通过自定义注解和序列化器,我们有效地降低了系统中业务模块与数据字典的耦合度,提升了系统的灵活性和可维护性。希望本文能为读者在实际项目中应用这些技术提供启发,进一步提升软件开发的效率和质量。若本文对你有帮助,别忘记三连哦~