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
相关产品推荐
相关产品推荐

