目录
- 问题引入
- 解决问题
- 查看 @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字符串序列化的问题,但又会引入如下问题
- 模型类上每个字段都需要增加序列化注解
- 人工识别字段类型,使用正确得序列化类(不能将List序列化成"")
解决问题
查看 @JsonSerialize(nullsUsing = StringNullSerializer.class) nullsUsing 的实现逻辑
- 下载源码
https://github.com/FasterXML/jackson-databind - 全局检索 nullsUsing 关键字
- 真好,只有4个地方有这些关键字,在对应的地方加上断点
- 开始调试
第2步检索到测试类中有对应的注解,入口就从测试类开始 - idea 直接F9 跳到对应的读取注解代码段
- 进入属性构建类这个地方,主要看这一段,给属性类分配 _nullSerializer
// How about custom null serializer?
Object serDef = _annotationIntrospector.findNullSerializer(am);
if (serDef != null) {
bpw.assignNullSerializer(prov.serializerInstance(am, serDef));
}
继续跟踪发现啥时候使用这个类
自定义注解解决问题
通过直接的源码跟踪,发现一个可行的途径,可以自定义个查找 findNullSerializer 值的方法,按照自定义的解析规则分配 _nullSerializer。自定义的注解应该可以有以下功能
- 只要在类上面配置一个注解,自动识别每个字段的类型,使用对应的 null 值序列化器,同时可以忽略不支持的类型
- 支持增加自己配置的序列化类
- 字段上的注解优先级大于类上面的
于是,注解类出来了, 如下
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 这个类,如下图所示
启动时候配置引入自定义的注解解析规则
配置类
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);
}
}