目录

  • 问题引入
  • 解决问题
  • 查看 @JsonSerialize(nullsUsing = StringNullSerializer.class) nullsUsing 的实现逻辑
  • 自定义注解解决问题
  • 如何整合到 SpringBoot 项目
  • 项目地址

问题引入

jackson 有原生的注解去处理 null 值

@JsonSerialize(nullsUsing = StringNullSerializer.class)

示例代码

package com.catdou.formatter.model;

import com.catdou.formatter.mvc.jackson.serializer.StringNullSerializer;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import lombok.Data;

import java.util.List;

/**
 * @author James
 */

@JsonSerialize(nullsUsing = StringNullSerializer.class)
@Data
public class User {
    private String name;

    private String password;

    private List<String> addressList;
}

序列化类

package com.catdou.formatter.mvc.jackson.serializer;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;

import java.io.IOException;

/**
 * @author James
 */
public class StringNullSerializer extends JsonSerializer<String> {
    @Override
    public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
        gen.writeString("");
    }
}
package com.catdou.formatter.serializer;

import com.catdou.formatter.model.User;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

/**
 * @author James
 */
public class NullValSerializerTest {

    private static final ObjectMapper objectMapper = new ObjectMapper();

    @Test
    public void testSerializer() throws JsonProcessingException {
        User user = new User();
        user.setName("chengdu");
        String json = objectMapper.writeValueAsString(user);
        Assertions.assertEquals("{\"name\":\"chengdu\",\"password\":null,\"addressList\":null}", json);
    }
}

运行上面这个测试用例,发现 null 值没有处理成功, 序列化的结果为

{"name":"chengdu","password":null,"addressList":null}

然后将类上的注解@JsonSerialize 调整到字段上,调整之后的类如下

package com.catdou.formatter.model;

import com.catdou.formatter.mvc.jackson.serializer.StringNullSerializer;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import lombok.Data;

import java.util.List;

/**
 * @author James
 */
@Data
public class UserChange {
    @JsonSerialize(nullsUsing = StringNullSerializer.class)
    private String name;

    @JsonSerialize(nullsUsing = StringNullSerializer.class)
    private String password;

    private List<String> addressList;
}

测试用例

package com.catdou.formatter.serializer;

import com.catdou.formatter.model.User;
import com.catdou.formatter.model.UserChange;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

/**
 * @author James
 */
public class NullValSerializerTest {

    private static final ObjectMapper objectMapper = new ObjectMapper();

    @Test
    public void testSerializer() throws JsonProcessingException {
        User user = new User();
        user.setName("chengdu");
        String json = objectMapper.writeValueAsString(user);
        Assertions.assertEquals("{\"name\":\"chengdu\",\"password\":null,\"addressList\":null}", json);
    }

    @Test
    public void testFieldSerializer() throws JsonProcessingException {
        UserChange user = new UserChange();
        user.setName("chengdu");
        String json = objectMapper.writeValueAsString(user);
        Assertions.assertEquals("{\"name\":\"chengdu\",\"password\":\"\",\"addressList\":null}", json);
    }
}

将注解调整到字段上,解决了null字符串序列化的问题,但又会引入如下问题

  1. 模型类上每个字段都需要增加序列化注解
  2. 人工识别字段类型,使用正确得序列化类(不能将List序列化成"")

解决问题

查看 @JsonSerialize(nullsUsing = StringNullSerializer.class) nullsUsing 的实现逻辑

  1. 下载源码
    https://github.com/FasterXML/jackson-databind
  2. 全局检索 nullsUsing 关键字
  3. Jackson2JsonRedisSerializer 序列化boolean类型问题_java

  4. 真好,只有4个地方有这些关键字,在对应的地方加上断点
  5. 开始调试
    第2步检索到测试类中有对应的注解,入口就从测试类开始
  6. Jackson2JsonRedisSerializer 序列化boolean类型问题_mvc_02

  7. idea 直接F9 跳到对应的读取注解代码段
  8. Jackson2JsonRedisSerializer 序列化boolean类型问题_java_03

  9. 进入属性构建类这个地方,主要看这一段,给属性类分配 _nullSerializer
// How about custom null serializer?
        Object serDef = _annotationIntrospector.findNullSerializer(am);
        if (serDef != null) {
            bpw.assignNullSerializer(prov.serializerInstance(am, serDef));
        }

Jackson2JsonRedisSerializer 序列化boolean类型问题_java_04


继续跟踪发现啥时候使用这个类

Jackson2JsonRedisSerializer 序列化boolean类型问题_jackson_05

自定义注解解决问题

通过直接的源码跟踪,发现一个可行的途径,可以自定义个查找 findNullSerializer 值的方法,按照自定义的解析规则分配 _nullSerializer。自定义的注解应该可以有以下功能

  1. 只要在类上面配置一个注解,自动识别每个字段的类型,使用对应的 null 值序列化器,同时可以忽略不支持的类型
  2. 支持增加自己配置的序列化类
  3. 字段上的注解优先级大于类上面的

于是,注解类出来了, 如下

package com.catdou.formatter.annotations;

import com.fasterxml.jackson.annotation.JacksonAnnotation;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @author James
 */
@Target({ElementType.FIELD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotation
public @interface NullValueHandler {
    Class<? extends JsonSerializer> using() default JsonSerializer.None.class;

    /**
     * 用在类上面。忽略某些注解
     * @return
     */
    Class<?>[] ignore() default JsonSerializer.None.class;
}

读取注解规则部份如下

package com.catdou.formatter.mvc.jackson;

import com.catdou.formatter.annotations.NullValueHandler;
import com.catdou.formatter.mvc.jackson.serializer.IntergerNullSerializer;
import com.catdou.formatter.mvc.jackson.serializer.ListNullSerializer;
import com.catdou.formatter.mvc.jackson.serializer.StringNullSerializer;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.introspect.Annotated;
import com.fasterxml.jackson.databind.introspect.AnnotatedMethod;
import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector;
import com.fasterxml.jackson.databind.ser.BeanPropertyWriter;
import com.fasterxml.jackson.databind.ser.PropertyBuilder;

import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * @author James
 */
public class NullValueSerializerInterceptor extends JacksonAnnotationIntrospector {

    private static Map<Class<?>, Class<? extends JsonSerializer>> serializerMap = new HashMap<>();

    static {
        serializerMap.put(String.class, StringNullSerializer.class);
        serializerMap.put(Integer.class, IntergerNullSerializer.class);
        serializerMap.put(List.class, ListNullSerializer.class);
    }

    /**
     *  返回 null 就不不处理 null
     *  @see BeanPropertyWriter#assignNullSerializer(com.fasterxml.jackson.databind.JsonSerializer)
     *  @see PropertyBuilder#buildWriter
     * @param a Annotated
     * @return
     */
    @Override
    public Object findNullSerializer(Annotated a) {
        NullValueHandler nullValueHandler = a.getAnnotation(NullValueHandler.class);
        if (nullValueHandler == null) {
            if (a instanceof AnnotatedMethod) {
                AnnotatedMethod annotatedMethod = (AnnotatedMethod) a;
                nullValueHandler = annotatedMethod.getDeclaringClass().getAnnotation(NullValueHandler.class);
            }
        }
        if (nullValueHandler != null) {
            Class<? extends JsonSerializer> using = nullValueHandler.using();
            if (using != JsonSerializer.None.class) {
                return using;
            } else {
                Class<?> filedType = a.getRawType();
                Set<Class<?>> ignoredSet = Arrays.stream(nullValueHandler.ignore()).collect(Collectors.toSet());
                if (ignoredSet.contains(filedType)) {
                    return null;
                }
                return serializerMap.get(filedType);
            }
        }
        return null;
    }

}

如何整合到 SpringBoot 项目

SpringBoot 加载 jackson 是自动装载的,在 org.springframework.boot.autoconfigure.http.JacksonHttpMessageConvertersConfiguration 这个类,如下图所示

Jackson2JsonRedisSerializer 序列化boolean类型问题_jackson_06


启动时候配置引入自定义的注解解析规则

配置类

package com.catdou.formatter.mvc.config;

import com.catdou.formatter.mvc.jackson.NullValueSerializerInterceptor;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;

/**
 * @author James
 */
public class JacksonConfig {

    public JacksonConfig(MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter) {
        mappingJackson2HttpMessageConverter.getObjectMapper()
                .setAnnotationIntrospector(new NullValueSerializerInterceptor());
    }
}

启动类

package com.catdou.formatter;

import com.catdou.formatter.mvc.config.JacksonConfig;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Import;

@SpringBootApplication
@Import(value = JacksonConfig.class)
public class FormatterApplication {

    public static void main(String[] args) {
        SpringApplication.run(FormatterApplication.class, args);
    }

}