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