前言
关于数据字典,有很多种说法,我们这次要将的字典是我们常用应用里数据字典。引用别的人文章 blog.csdn.net/u012373815/… ,这里与我们这次讨论的字典是一致的。
数据字典在库里存的是字典的code或key,但我们展示的时候需要转化成用户能够识别的字典含义。关于这个转化有很多种方式。有人说前端解决,说明前端展示的问题。有人说是后端解决,前端只负责渲染。这里我们说几种常规的解决方式。
-
前端解决
后端只负责查询业务表的数据,不负责转化,类似于这样JS
if(value =='1'){ return "卫生局"; }else if(value =='2'){ return "卫生院"; }else if(value =='3'){ return "卫生室"; }
这样处理后端自然省事,前端对每个字典单独处理,在遇到字典改变的时候,需要修改代码重新上线,虽说现在修改前端重新发布都是无感的,但毕竟还是要修改代码的。
-
后端解决
后端解决的方式有很多种,但毕竟处理起来都不是特别方便。以下列两种常规的解决方式。
-
解决方式一
SQL查询,面对查询业务列表,或者查询明细,需要关联字典去查询。如果一行数据里有多个字典,需要关联多次。这种方式将字典带到sql查询,嵌入太深,如果修改一个字段的字典code或key,也许需要从一个很长的sql里去修改他,造成不可控的风险。
-
解决方式二
业务代码里处理。写一个公用方法,在我们查询列表或查询明细后,再调用这个方法去处理,比如方法:
//dictCode为字典code //value为此次业务值 public static String getDictByCode(String dictCode, String value) { //。。。 }
业务里可以这样处理,假设,我们取得的是列表,可以这样处理
for(BizDTO bizDTO : bizDTOs) { bizDTO.setSex(getDictByCode("sex", bizDTO.getSex())); }
这样就不用改sql,使用了字典与sql的分离,但其实并没有实现字典与业务的分离。我们应该只关心业务,而不应该被业务无关的部分困扰,这样容易对我们业务产生干扰,也容易出错。
以上就是常规的解决方式,几种方式不做太多的好坏评判,但实际上都是会干扰业务。这是我们最不想处理的,大量的重复的工作,而且不是业务相关。
-
实现目标
- 解决数据字典问题
- 不干扰业务
实现思路
我们既不要前端解决,前端只负责渲染。后端也不想做与业务无关的sql或代码。后端只要告诉这个字段是用哪个字典即可。这里我们就可以用最小的操作,解决掉这个问题。对于后端来讲,前端不管是用什么来渲染,后端的渲染就是json,后端给前端返回数据用的json。那我们能不能在后端渲染(返回json,或者理解为json序列化)来处理这样事。因为这事应该跟业务没有关系,只在渲染的时候处理一下就可以了。
Spring Boot默认的序列化是Jackson,所以以下讲的部分都跟jackson相关,如果读者的项目并没有用Jackson序列化,以下方式是无效的。这点要注意。
好,言归正传。既然要对Jackson下手,当然要对Jackson常用的一些扩展方式了解。这里提一下常规的扩展方式。Jackson为我们提供一个很好的扩展方式,比如,通过定义序列化类或反序列化类来实现扩展,比如:
public class MyJsonSerializer extends JsonSerializer<String> {
@Override
public void serialize(String value, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
jsonGenerator.writeString("Hello," + value);
}
}
在属性上面使用
@JsonSerialize(using = MyJsonSerializer.class)
String name;
比如我们在序列化的name是Jack,给前端的应该是就是Hello,Jack
。
但对于字典来说我们需要更多的扩展,比如需要指定字典key,需要指定字典转化后的property等,这些单靠这么个类来实现扩展好像有点困难啊。其实最好的方式就是和时间序列化一样,用一个注解就解决了,比如:
@JsonFormat(pattern="yyyy-MM-dd",timezone = "GMT+8")
private Date startDate;
@JsonFormat(pattern="yyyy-MM-dd",timezone = "GMT+8")
private Date endDate;
我们只要指定是什么类型,哪个时区就能根据值进行序列化。而不需要我们自己转换。借鉴这种方式,我们可以定义一个注解,比如:
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DictFormat {
/**
* 字典名称编码, 默认为当前属性的名称
*/
String dictKey() default "";
/**
* 字典的属性原始值是否转String输出
*/
boolean dictKeyToString() default false;
/**
* 目标属性, 一个字典对应有个值,这个也要有个属性存在,默认为dicKey+"Name"
*/
String targetFiled() default "";
/**
* 默认值,当字典里没有对应的值时,显示的字典值
*/
String defaultValue() default "";
}
这样通过这个注解,只需要指定一些关键信息,就可以实现字典的序列化。
那么重点来了,如何扩展Jackson序列化。
我们来看一下类com.fasterxml.jackson.databind.ser.BeanSerializerModifier
,从类的字面意思来看BeanSerializer
就是bean的序列化器。BeanSerializerModifier
就是bean的序列化更改点。也就是通过这个类可以更改不同类型的序列化器。我们来解释一下源码:
/**
* Abstract class that defines API for objects that can be registered (for {@link BeanSerializerFactory}
* to participate in constructing {@link BeanSerializer} instances.
* 我的直译:用来参与构造BeanSerializer实例,为BeanSerializerFactory提供注册的定义API的抽象类
* 理解:参与构造BeanSerializer,BeanSerializerFactory会使用他
* 。。。
*/
public abstract class BeanSerializerModifier
{
/**
* Method called by {@link BeanSerializerFactory} with tentative set
* of discovered properties.
* Implementations can add, remove or replace any of passed properties.
* 这个方法是由BeanSerializerFactory调用来尝试发现属性。
* 实现者(继承方法)可以添加、移除、替换(增删改)任何通过的(可被序列化的)属性。
*
*/
public List<BeanPropertyWriter> changeProperties(SerializationConfig config,
BeanDescription beanDesc, List<BeanPropertyWriter> beanProperties) {
return beanProperties;
}
//。。。
}
由此可见,我们通过这个类的这个方法有可能改变类属性的序列化器(BeanSerializer
)。尝试一下:
public class DictFormatSerializerModifier extends BeanSerializerModifier {
@Override
public List<BeanPropertyWriter> changeProperties(SerializationConfig config, BeanDescription beanDesc, List<BeanPropertyWriter> beanProperties) {
for (BeanPropertyWriter writer : beanProperties) {
//查找是否有{@link DictFormat}注解,有就更改序列化器
DictFormat dictFormat = writer.getAnnotation(DictFormat.class);
if (dictFormat != null) {
String sourceFileName = ...
String targetFiledName = ...
String dicKey = ...
String defaultValue = dictFormat.defaultValue();
//更改序列化器
writer.assignSerializer(new DictJsonSerializer(targetFiledName, dicKey, dictFormat.dictKeyToString(), defaultValue));
}
}
return beanProperties;
}
这个方法在类第一次序列化时会调用一次,确定后就不会再更改了。看一下DictJsonSerializer
的定义:
protected class DictJsonSerializer extends JsonSerializer<Object> {
String sourceFileName = ...;
String targetFiledName = ...;
String dicKey = ...;
String defaultValue = ...;
DictJsonSerializer(String sourceFileName ,
String targetFiledName ,
String dicKey,
String defaultValue) {
//。。。
}
@Override
public void serialize(Object value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
//写原始值
gen.writeObject(value);
//写字典值(转化后的字典含义)
gen.writeFieldName(targetFiledName);
String dictValue = ...//可以调用某些方法得出结果
gen.writeObject(dictValue);
}
}
以上就是此次的重点了。也有人说定义了这个BeanSerializerModifier
如何让Jackson知道呢。我们来提供以下示例:
ObjectMapper objectMapper = ... ;//我们拿到ObjectMapper
//给SerializerFactory注册一下
objectMapper.setSerializerFactory(objectMapper.getSerializerFactory().withSerializerModifier(new dictFormatSerializerModifier()));
下面就是锦上添花的事了。我们跟Spring结合,可以定义一个接口如DictCollector
字典收集器,实现这个接口就可以将返回的字典收集起来,这样就对外实现扩展了,我们不管是后台管理的数据字典,还是一些接口里的字典,都可以实现这个接口就可以了。
这里就不一一详细描述了。代码已开源,文档已提供:
源码: github.com/zhouxx/boot… 模块之boot-plus-web
文档: zhouxx.github.io/boot-plus/#…
欢迎提出问题一起讨论。