背景
java接口返json时,会有字段为空,客户端不希望有为null的字段。
实现
最终可行方案
另起一个配置类,继承 WebMvcConfigurerAdapter ,重写 configureMessageConverters ,并解决方法一中遇到的问题。
@ControllerAdvice
@Configuration
@Slf4j
public class WebConfig extends WebMvcConfigurerAdapter {
// 由于接口数量是有限的(当前背景下不到10个接口,故有10个不同对象),所以每一个接口给出对象的所有字段都存到内存中,永驻内存,避免每次都反射取字段
private Map> classFieldsMap = new ConcurrentHashMap<>();
@Override
public void configureMessageConverters(List> converters) {
//字符串转换器
StringHttpMessageConverter converter = new StringHttpMessageConverter(Charset.forName("UTF-8"));
converters.add(converter);
// FastJson转换器
//1.需要定义一个convert转换消息的对象;
FastJsonHttpMessageConverter fastJsonHttpMessageConverter = new FastJsonHttpMessageConverter();
//2.添加fastJson的配置信息,比如:是否要格式化返回的json数据;
FastJsonConfig fastJsonConfig = new FastJsonConfig();
fastJsonConfig.setSerializerFeatures(SerializerFeature.PrettyFormat,
// 循环/重复引用问题,关闭引用监测
SerializerFeature.DisableCircularReferenceDetect,
// 将不是String类型的key转换成String类型
SerializerFeature.WriteNonStringKeyAsString);
// 将数字类型转0,字符串转"",list转空数组,其他(空Map、空对象等)转Object,表现上是转为了{}。
fastJsonConfig.setSerializeFilters((ValueFilter) (o, fieldName, source) -> {
if (source == null) {
List fieldList = this.getObjectAllFields(o) ;
Class clazz = Object.class;
for (Field field : fieldList) {
if (field.getName().equals(fieldName)) {
clazz = field.getType();
}
}
// Number是否是clazz的父类,或是否和clazz继承了相同父类
if (Number.class.isAssignableFrom(clazz)) {
return 0;
} else if (String.class.equals(clazz)){
return "";
} else if (List.class.equals(clazz)) {
return new int[]{};
} else {
return new Object();
}
}
return source;
});
//3处理中文乱码问题
List fastMediaTypes = new ArrayList<>();
fastMediaTypes.add(MediaType.APPLICATION_JSON_UTF8);
//4.在convert中添加配置信息.
fastJsonHttpMessageConverter.setSupportedMediaTypes(fastMediaTypes);
fastJsonHttpMessageConverter.setFastJsonConfig(fastJsonConfig);
//5.将convert添加到converters当中.
converters.add(fastJsonHttpMessageConverter);
super.configureMessageConverters(converters);
}
@Override
public void extendMessageConverters(List> converters) {
log.info("converters size:"+converters.size());
for (HttpMessageConverter> messageConverter : converters) {
log.info(messageConverter.toString());
}
}
// 获取某对象的所有字段,如果内存中没有,则通过反射获取。
private List getObjectAllFields(Object o) {
List fieldList = classFieldsMap.get(o.getClass());
if (CollectionUtils.isEmpty(fieldList)) {
fieldList = new ArrayList<>();
Class tempClass = o.getClass();
while (tempClass != null) {
fieldList.addAll(Arrays.asList(tempClass.getDeclaredFields()));
tempClass = tempClass.getSuperclass();
}
classFieldsMap.put(o.getClass(), fieldList);
return fieldList;
}
return fieldList;
}
}
方法一 使用统一json配置 (未采用)
程序启动方法继承 WebMvcConfigurerAdapter , 重写 configureMessageConverters 。
使用该方法遇到的问题:
① 在使用过程中遇到map或者list值出现类似{"$ref":"$.data.list[0].batchInfo"} 的值。
网上搜索该现象为循环引用,解决方法可使用SerializerFeature.DisableCircularReferenceDetect。
循环引用:当一个对象包含另一个对象时,fastjson就会把该对象解析成引用。引用是通过$ref标示的,下面介绍一些引用的描述
"$ref":".." 上一级 "$ref":"@" 当前对象,也就是自引用 "$ref":"$" 根对象" $ref":"$.children.0" 基于路径的引用,相当于 root.getChildren().get(0)
② 基础对象(Integer、String、List、Map等)可以不输出null,但java实体类确不知道如何才能输出为{}。另外如果map的key为Integer,输出则也为数字(不使用该方法时json格式化会带引号,即为字符串),postman认为输出不可格式化,这块不知道是否会对客户端产生影响。
由于问题2未解决,故此方法未使用。
public class AppLauncher extends WebMvcConfigurerAdapter {
public static void main(String[] args) {
SpringApplication.run(AppLauncher.class);
}
@Override
public void configureMessageConverters(List> converters) {
super.configureMessageConverters(converters); // 不知道这句有什么用
FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter();
FastJsonConfig fastJsonConfig = new FastJsonConfig();
fastJsonConfig.setSerializerFeatures(
SerializerFeature.PrettyFormat,
SerializerFeature.WriteMapNullValue, // 空map转{}
SerializerFeature.WriteNullNumberAsZero, // 空Integer转0
SerializerFeature.WriteNullStringAsEmpty, // 空String转""
SerializerFeature.WriteNullListAsEmpty // 空List转[]
);
// 处理中文乱码问题
List fastMediaTypes = new ArrayList<>();
fastMediaTypes.add(MediaType.APPLICATION_JSON_UTF8);
fastConverter.setSupportedMediaTypes(fastMediaTypes);
fastConverter.setFastJsonConfig(fastJsonConfig);
//处理字符串, 避免直接返回字符串的时候被添加了引号——不知道这句有什么用
StringHttpMessageConverter smc = new StringHttpMessageConverter(Charset.forName("UTF-8"));
converters.add(smc);
}
}
方法二 针对部分可能为空的数据手动转map(采用)
该方法比较low,场景使用比较有限。但由于目前背景中,能确定只有几个对象可能为null,其他一定不为null。故可采用。
具体实现方式:1. 初始化赋值。2. 代码层面对map/list赋值过程中注意,如果为空则不赋值。(初始化已经为空map/list了)。3. 部分实体类使用工具类转为map。
public class ConvertUtil {
/**
* java对象转map
*/
public static Map javaBeanToMap(Object obj) {
if (obj == null) {
return MapUtils.emptyMap();
}
try {
// 通过intropestor分析出字节码对象的信息beaninfo
// 如果不想把父类的属性也列出来的话,那getBeanInfo的第二个参数填写父类的信息
// BeanInfo beanInfo = Introspector.getBeanInfo(user.getClass(), Object.class);
BeanInfo beanInfo = Introspector.getBeanInfo(obj.getClass());
// 通过调用getPro....方法获取对象的属性描述器
PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();
// 网上找的代码中没有判空,点进源码中看了下注释,是可能为空的,所以此处做判空校验
// 实现类 SimpleBeanInfo 直接返的null,其他实现类返的数组
if (propertyDescriptors == null || propertyDescriptors.length <= 0) {
return MapUtils.emptyMap();
}
Map map = new HashMap<>(propertyDescriptors.length);
for (PropertyDescriptor property : propertyDescriptors) {
String key = property.getName();
// 过滤掉class属性
if (key.compareToIgnoreCase("class") == 0) {
continue;
}
Method getter = property.getReadMethod();
Object value;
if (getter != null) {
value = getter.invoke(obj);
if (value == null) {
value = MapUtils.emptyMap();
}
} else {
value = MapUtils.emptyMap();
}
map.put(key, value);
}
return map;
} catch (Exception e) {
log.error("javaBeanToMap failed : {}", e.getMessage());
return MapUtils.emptyMap();
}
}
}
在这之前用了个比较low的方法,这里面是反射获取的类的所有方法,然后获取属性名的时候是根据get切割来的,众所周知,get方法会把属性名首字母大写,于是又有了把首字母转为小写各种切割拼接,感觉很不友好,所以用了上面的方法。low方法如下:
public class ConvertUtil {
public static Map javaBeanToMap(Object obj) {
// 这里map初始化还不能设置初始值,因为不知道设为几,不友好
Map map = new HashMap<>();
List methods = getAllMethods(obj);
for (Method m : methods) {
String methodName = m.getName();
if (methodName.startsWith("get")) {
try {
//获取属性名,首字母小写
String propertyName = methodName.substring(3);
propertyName = (new StringBuilder()).append(Character.toLowerCase(propertyName.charAt(0)))
.append(propertyName.substring(1)).toString();
if (Objects.isNull(m.invoke(obj))) {
map.put(propertyName, MapUtils.emptyMap());
} else {
map.put(propertyName, m.invoke(obj));
}
} catch (Exception e) {
log.error("javaBeanToMap failed : {}", e.getMessage());
return MapUtils.emptyMap();
}
}
}
return map;
}
/**
* 获取obj中的所有方法
*/
private static List getAllMethods(Object obj) {
List methods = new ArrayList<>();
Class> clazz = obj.getClass();
while (!clazz.getName().equals("java.lang.Object")) {
methods.addAll(Arrays.asList(clazz.getDeclaredMethods()));
clazz = clazz.getSuperclass();
}
return methods;
}
}
方法三 写配置类
该方法会把所有未空的都转为"",个人测试Integer、String、Map、List,都输出为了"",不是我所想要的输出。
@Configuration
public class JacksonConfig {
@Bean
@Primary
@ConditionalOnMissingBean(ObjectMapper.class)
public ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) {
ObjectMapper objectMapper = builder.createXmlMapper(false).build();
SerializerProvider serializerProvider = objectMapper.getSerializerProvider();
serializerProvider.setNullValueSerializer(new JsonSerializer() {
@Override
public void serialize(Object o, JsonGenerator jsonGenerator, SerializerProvider serializerProvider)
throws IOException, JsonProcessingException {
jsonGenerator.writeString("");
}
});
return objectMapper;
}
}
测试接口返回数据对象Vo中添加未赋值的各种类型:
public class VoA extends VoB {
private XXVo testBean;
private Map testMap;
private List testList;
private Integer testInt;
private String testString;
}
输出结果(postman截图):
方法四 空对象不输出
在该背景下,其实null对象不输出也是一个不错的办法,但是由于null不输出会导致一些问题,比如:list中多个对象,有的对象有A字段,有的对象没有A字段,这个我们认为不方便测试也不好维护,故没有采用。但可能以后会有需要,此处记一笔。
实现方式为,在类上加注解@JsonInclude(JsonInclude.Include.NON_NULL)
测试:接口返回数据对象Vo中添加未赋值的各种类型
@JsonInclude(JsonInclude.Include.NON_NULL)
public class VoA extends VoB {
private XXVo testBean;
private Map testMap;
private List testList;
private Integer testInt;
private String testString;
}
输出结果:
没有各属性字段
注意!!踩坑:
可以看到上面我的VoA继承了VoB,且@JsonInclude(JsonInclude.Include.NON_NULL)注解在VoA上。如果这些test属性写在了VoB里,且注解在VoA上,那么该注解是不生效的,也就是各属性都会输出且为null。
具体解决方案没有进一步尝试,不过猜测应该是在VoB上也加上@JsonInclude(JsonInclude.Include.NON_NULL)注解就可以了。
关于继承的坑,想起对象的toString()方法也遇到过一个小坑。现在为了代码简便,实体类对象都使用了package lombok.@Data注解,写后台的时候,部分更新操作的请求对象都打了info日志,以便出问题可以排查,无意间发现A继承了B,打印A的时候,B的属性并没有打印出来。查了原因发现是@Data注解生成的toString()方法是默认不包含父类的,所以要在A上面加上package lombok.@ToString(callSuper=true)注解。还好我这是管理后台,日志没有很常用,如果是服务日志要打印全,一定要注意。