Spring Boot 增强之 Jackson扩展数据字典

3,201 阅读6分钟

前言

关于数据字典,有很多种说法,我们这次要将的字典是我们常用应用里数据字典。引用别的人文章 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/#…

欢迎提出问题一起讨论。