You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Boot项目中过滤JSON空值解决数字格式异常问题

解决Spring Boot中加密货币API返回空值导致的数字格式异常

问题背景

调用加密货币API时,返回1-60条数据解析正常,但返回100条时部分字段(如change)会出现字符串形式的"null",手动解析JSON时直接调用getFloat()等方法会触发NumberFormatException,导致程序报错。

现有APIService代码

public class APIService {
    private List<CoinDTO> coinList = new ArrayList<>();

    private final TrackerConfigProperties trackerConfig;

    //构造函数注入配置属性
    public APIService(TrackerConfigProperties trackerConfig) {
        this.trackerConfig = trackerConfig;
    }

    public List<CoinDTO> getCoinList() {
        return coinList;
    }

    public void setCoinList(List<CoinDTO> coinList) {
        this.coinList = coinList;
    }

    @PostConstruct
    @Scheduled(cron = "* * 1 * * *")
    public void getAPIData() throws IOException, JSONException{     
        List<CoinDTO> newData = new ArrayList<>();
        OkHttpClient client = new OkHttpClient();
        
        JSONObject obj = null;
        JSONObject data = null;

        //使用请求构建器配置API
        Request request = new Request.Builder()
            .url(trackerConfig.apiUrl())
            .get()
            .addHeader("X-RapidAPI-Key", trackerConfig.apiKey())
            .addHeader("X-RapidAPI-Host", trackerConfig.apiHost())
            .build();
        
        Response response = null;
        
        try {
            response = client.newCall(request).execute();
                        
            String jsonResponse = response.body().string();
            System.out.println(jsonResponse);

            
            obj = new JSONObject(jsonResponse);
            data = obj.getJSONObject("data");
            JSONArray arr = data.getJSONArray("coins");
                        
            System.out.println(arr);

            for(int i=0; i<arr.length(); i++) {
                CoinDTO coinRecords = new CoinDTO();
                coinRecords.setName(arr.getJSONObject(i).getString("name"));
                coinRecords.setSymbol(arr.getJSONObject(i).getString("symbol"));
                coinRecords.setPrice(arr.getJSONObject(i).getFloat("price"));
                coinRecords.setRank(arr.getJSONObject(i).getInt("rank"));
                coinRecords.setChange(arr.getJSONObject(i).getFloat("change"));
                coinRecords.setUrl(arr.getJSONObject(i).getString("iconUrl"));
                coinRecords.setMarketCap(arr.getJSONObject(i).getString("marketCap"));
                newData.add(coinRecords);
            }
            this.coinList = newData;
            
        }catch(IOException e) {
            e.printStackTrace();
        }
        
        //多次调用response.body().string()会触发非法状态异常   
    }
}

报错信息

Caused by: org.json.JSONException: JSONObject["change"] is not a number.
    at org.json.JSONObject.getFloat(JSONObject.java:655) ~[json-20180130.jar:na]
    at com.vish.trackerapp.service.APIService.getAPIData(APIService.java:86) ~[classes/:na]
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) ~[na:na]
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
    at java.base/java.lang.reflect.Method.invoke(Method.java:568) ~[na:na]
    at org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor$LifecycleElement.invoke(InitDestroyAnnotationBeanPostProcessor.java:424) ~[spring-beans-6.0.2.jar:6.0.2]
    at org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor$LifecycleMetadata.invokeInitMethods(InitDestroyAnnotationBeanPostProcessor.java:368) ~[spring-beans-6.0.2.jar:6.0.2]
    at org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor.postProcessBeforeInitialization(InitDestroyAnnotationBeanPostProcessor.java:192) ~[spring-beans-6.0.2.jar:6.0.2]
    ... 37 common frames omitted
Caused by: java.lang.NumberFormatException: For input string: "null"
    at java.base/jdk.internal.math.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:2054) ~[na:na]
    at java.base/jdk.internal.math.FloatingDecimal.parseFloat(FloatingDecimal.java:122) ~[na:na]
    at java.base/java.lang.Float.parseFloat(Float.java:476) ~[na:na]
    at org.json.JSONObject.getFloat(JSONObject.java:653) ~[json-20180130.jar:na]
    ... 45 common frames omitted

解决方案

方案1:手动处理字段空值(适配现有手动解析逻辑)

在解析每个JSON对象时,先判断字段值是否为"null"或null,再决定设置默认值或跳过该条数据。可以封装工具方法简化逻辑:

步骤1:添加安全解析工具方法

在APIService中新增工具方法,处理数字字段的空值:

private Float getSafeFloat(JSONObject json, String key) throws JSONException {
    String value = json.getString(key);
    if ("null".equals(value) || value == null) {
        return 0.0f; // 或返回null,后续过滤
    }
    return Float.parseFloat(value);
}

private Integer getSafeInt(JSONObject json, String key) throws JSONException {
    String value = json.getString(key);
    if ("null".equals(value) || value == null) {
        return 0; // 或返回null
    }
    return Integer.parseInt(value);
}

步骤2:修改循环解析逻辑

替换原循环内的字段赋值代码,使用工具方法处理空值:

for(int i=0; i<arr.length(); i++) {
    JSONObject coinJson = arr.getJSONObject(i);
    // 可选:如果关键字段为空,直接跳过该条数据
    if ("null".equals(coinJson.getString("change")) || coinJson.isNull("change")) {
        continue;
    }
    
    CoinDTO coinRecords = new CoinDTO();
    coinRecords.setName(coinJson.getString("name"));
    coinRecords.setSymbol(coinJson.getString("symbol"));
    coinRecords.setPrice(getSafeFloat(coinJson, "price"));
    coinRecords.setRank(getSafeInt(coinJson, "rank"));
    coinRecords.setChange(getSafeFloat(coinJson, "change"));
    coinRecords.setUrl(coinJson.getString("iconUrl"));
    coinRecords.setMarketCap(coinJson.getString("marketCap"));
    newData.add(coinRecords);
}

方案2:改用Jackson自动序列化(Spring Boot推荐方案)

放弃手动解析JSON,使用Spring默认的Jackson库自动处理空值和类型转换,更简洁且符合最佳实践。

步骤1:修改CoinDTO类,添加Jackson注解

import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.deser.std.NumberDeserializers;

public class CoinDTO {
    private String name;
    private String symbol;
    private Float price;
    private Integer rank;
    private Float change;
    @JsonProperty("iconUrl") // 映射JSON中的iconUrl字段到url属性
    private String url;
    private String marketCap;

    // Getter和Setter方法
    @JsonDeserialize(using = NumberDeserializers.FloatDeserializer.class)
    public void setPrice(Float price) {
        this.price = price != null ? price : 0.0f; // 空值设置默认值
    }

    @JsonDeserialize(using = NumberDeserializers.FloatDeserializer.class)
    public void setChange(Float change) {
        this.change = change != null ? change : 0.0f;
    }

    @JsonDeserialize(using = NumberDeserializers.IntegerDeserializer.class)
    public void setRank(Integer rank) {
        this.rank = rank != null ? rank : 0;
    }

    // 其他Getter和Setter省略
}

步骤2:新增响应结构类

创建对应API返回结构的类,方便Jackson解析:

class CoinResponse {
    private CoinData data;

    public CoinData getData() { return data; }
    public void setData(CoinData data) { this.data = data; }
}

class CoinData {
    private List<CoinDTO> coins;

    public List<CoinDTO> getCoins() { return coins; }
    public void setCoins(List<CoinDTO> coins) { this.coins = coins; }
}

步骤3:修改APIService的解析逻辑

import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.Objects;
import java.util.stream.Collectors;

// ...

@PostConstruct
@Scheduled(cron = "* * 1 * * *")
public void getAPIData() throws IOException {     
    List<CoinDTO> newData = new ArrayList<>();
    OkHttpClient client = new OkHttpClient();

    Request request = new Request.Builder()
        .url(trackerConfig.apiUrl())
        .get()
        .addHeader("X-RapidAPI-Key", trackerConfig.apiKey())
        .addHeader("X-RapidAPI-Host", trackerConfig.apiHost())
        .build();
    
    try (Response response = client.newCall(request).execute()) {
        String jsonResponse = response.body().string();
        ObjectMapper objectMapper = new ObjectMapper();
        // 忽略API返回的未知字段,避免新增字段导致解析失败
        objectMapper.configure(com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        // 自动解析JSON到响应结构
        CoinResponse responseObj = objectMapper.readValue(jsonResponse, CoinResponse.class);
        // 过滤掉可能的null对象
        newData = responseObj.getData().getCoins().stream()
                            .filter(Objects::nonNull)
                            .collect(Collectors.toList());
        this.coinList = newData;
    } catch(IOException e) {
        e.printStackTrace();
    }
}

内容的提问来源于stack exchange,提问作者Viswanadh bhaskarla

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.07 11:15:36