spring boot 接口返json 空字段處理


背景

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認為輸出不可格式化,這塊不知道是否會對客戶端產生影響。
  • 由於問題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)注解。還好我這是管理后台,日志沒有很常用,如果是服務日志要打印全,一定要注意。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM