背景
java接口返json時,會有字段為空,客戶端不希望有為null的字段。
實現
最終可行方案
另起一個配置類,繼承 WebMvcConfigurerAdapter ,重寫 configureMessageConverters ,並解決方法一中遇到的問題。
@ControllerAdvice
@Configuration
@Slf4j
public class WebConfig extends WebMvcConfigurerAdapter {
// 由於接口數量是有限的(當前背景下不到10個接口,故有10個不同對象),所以每一個接口給出對象的所有字段都存到內存中,永駐內存,避免每次都反射取字段
private Map<Class, List<Field>> classFieldsMap = new ConcurrentHashMap<>();
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> 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<Field> 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<MediaType> 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<HttpMessageConverter<?>> converters) {
log.info("converters size:"+converters.size());
for (HttpMessageConverter<?> messageConverter : converters) {
log.info(messageConverter.toString());
}
}
// 獲取某對象的所有字段,如果內存中沒有,則通過反射獲取。
private List<Field> getObjectAllFields(Object o) {
List<Field> 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認為輸出不可格式化,這塊不知道是否會對客戶端產生影響。
- ① 在使用過程中遇到map或者list值出現類似
- 由於問題2未解決,故此方法未使用。
public class AppLauncher extends WebMvcConfigurerAdapter {
public static void main(String[] args) {
SpringApplication.run(AppLauncher.class);
}
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> 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<MediaType> 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。
- 使用內省方法處理的工具類如下:
關於內省和JavaBeanInfo的理解可參考
https://nicky-chen.github.io/2018/03/13/introspector/
public class ConvertUtil {
/**
* java對象轉map
*/
public static Map<String, Object> 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<String, Object> 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<String, Object> javaBeanToMap(Object obj) {
// 這里map初始化還不能設置初始值,因為不知道設為幾,不友好
Map<String, Object> map = new HashMap<>();
List<Method> 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<Method> getAllMethods(Object obj) {
List<Method> 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<Object>() {
@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<Integer, XXVo> testMap;
private List<XXVo> 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<Integer, XXVo> testMap;
private List<XXVo> 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)注解。還好我這是管理后台,日志沒有很常用,如果是服務日志要打印全,一定要注意。
