首页 / 资讯中心 / 文章详情

Java空串转数字:NumberFormatException排查与防御

Java空串转数字:NumberFormatException排查与防御 ★ FEATURED ARTICLE
凌晨两点一条告警弹出来堆栈第一行就是java.lang.NumberFormatException: empty String。很多人的第一反应是去搜这条消息然后照着改Integer.parseInt改完发现根本不报这个错——因为Integer.parseInt()抛出来的消息压根不是这行字。这条 NumberFormatException 的文案其实是一条“指路牌”它能直接把排查范围从几十个数字转换入口收窄到寥寥几个。这篇东西就是想把这条异常拆开给你看它到底由谁抛出、为什么会走到那一步、从请求入口到落库的整条链路上有哪些“变成空串”的姿势以及我常用的四层防御改造和几套现成的排查手段。不管你是刚接手线上告警的新手还是已经写了不少年业务代码、但每次看到空串转换还是习惯性加个! null的老手下面的内容都能直接用上。1. 先搞清楚这行异常到底是谁抛的1.1 从堆栈第一行读起别急着改代码排查任何NumberFormatException第一步永远是看堆栈最顶上那几行 frame而不是看消息。消息只是结果frame 才是来源。我习惯把堆栈分成三段来看最上面 3 到 5 行是“出事地点”告诉你具体调用了哪个 parse 方法中间一段是“路过的地方”通常能看到 Spring 的TypeConverterDelegate、Jackson 的StdDeserializer、MyBatis 的TypeHandler这类框架类最下面是“发车地点”往往是 Controller 方法或者某个定时任务的入口。这条消息之所以有价值是因为不同转换入口抛出的文案是不一样的。Integer.parseInt()给你的是For input string: Long.parseLong()也一样但Double.parseDouble()、Float.parseFloat()、new BigDecimal()抛出来的恰恰就是empty String。所以一旦你在堆栈里看到empty String这个原文基本可以把 Integer、Long 这条线先放一放重点怀疑浮点数、BigDecimal、BigInteger 相关的路径或者某个框架内部先做了 trim 再转数字。这里有个容易被忽略的细节Short.parseShort()和Byte.parseByte()走的也是NumberFormatException.forInputString文案同样是For input string: 。所以“消息文案”这把尺子虽然好用但它只帮你排除不帮你锁定真正的定位还是要靠堆栈里的类名和方法名。1.2 源码级对照同一句空串七种不同死法为了把这件事说清楚我专门把 JDK 里几个高频转换方法对空串的处理逐个走了一遍。这张表建议存下来下次看到异常消息先对照一下就能省半小时。调用方式空串的异常消息关键源码位置Integer.parseInt()/Integer.valueOf()For input string: Integer.parseInt里len 0走forInputStringLong.parseLong()/Long.valueOf()For input string: Long.parseLong同样的分支Short.parseShort()/Byte.parseByte()For input string: 同上复用forInputStringInteger.decode()Zero length stringInteger.decode开头单独判断nm.length() 0Double.parseDouble()/Double.valueOf()empty StringFloatingDecimal.readJavaFormatString里l 0分支Float.parseFloat()/Float.valueOf()empty String同样复用FloatingDecimal的读取逻辑new BigDecimal()empty StringBigDecimal(char[], int, int, MathContext)里len 0new BigInteger()Zero length BigIntegerBigInteger(String, int)里len 0分支从这张表能读出两个关键信息。第一empty String这个文案集中在浮点数和BigDecimal上说明你遇到的极大概率是“价格、金额、比率、坐标、评分”这类带小数的字段而不是分页页码这种整型字段。第二Double.parseDouble在读取之前会先做一次in.trim()也就是说传入 纯空格和传入会抛完全一样的empty String。这一点在排查时特别要命——日志里看着明明有内容实际上是一串空格。顺带说一句new BigDecimal()的调用链BigDecimal(String val)会先执行val.toCharArray()如果 val 是 null你拿到的是NullPointerException而不是NumberFormatException。所以看到empty String反而可以放心说明入参不是 null是一个长度为 0 或者 trim 后长度为 0 的字符串。这个信息对定位很关键它把“没传参数”和“传了空参数”两种情况彻底区分开了。注意不要因为Double.parseDouble会 trim 就以为所有转换都容错空格。Integer.parseInt( 1 )是会直接抛For input string: 1 的它只处理正负号不处理空格。所以“加个 trim 就万事大吉”这个想法在整型路径上不成立。2. 为什么空字符串会一路走到转换这一步2.1 数据在链路上“变空”的五种常见姿势空串不会凭空出现在 parse 方法里它一定是在前面某一环被制造出来、或者被放行进来的。我在实际项目里总结下来主要就这五种来源。第一种是表单默认值。前端表单初始化时把数字输入框统一设成用户没填就直接提交后端拿到的是一个空字符串而不是null。这在新增和编辑共用一个表单页面的场景里特别常见因为编辑时字段有值新增时是空串两套逻辑混在一起很容易漏掉某一分支。第二种是前端序列化约定。有些前端工具或者封装的请求库会把undefined和null统一转成空串再发出去理由是这样对后端更“友好”。结果就是后端永远收不到 null只能收到空串所有! null判断全部失效。第三种是Excel 或 CSV 导入。表格里某一列没填读出来的单元格值是空串如果这一列中间还夹杂着用户从别处粘贴过来的全角空格或者不换行空格那就是“看着像空的实际有两个字符”。这类问题在批量导入功能上线后集中爆发而且很难复现因为同一个模板不同人填出来的脏数据完全不一样。第四种是数据库里的空串与 NULL 混用。某些数据库对空串和 NULL 的处理并不一致字段允许为空但写入时用了空串查询出来就是。ORM 框架把映射到一个Double或者BigDecimal属性时就会在类型转换这一步炸掉。而且这种问题往往只在存量数据上出现新数据因为加了校验反而是好的排查时容易误判成“偶发”。第五种是字符串拼接和占位符。日志里为了格式整齐用String.format拼接参数某个参数本身是 null格式化后变成了字面量null再被送进转换方法。这时候报的通常是For input string: null但如果拼接后是就会走空串这条路。我见过一个坑配置中心里某个比例参数的值为空白不是删除是把值清空了但保留了配置项服务启动读取配置后直接new BigDecimal(configValue)启动即失败。2.2 哪些框架会替你悄悄调用转换方法真正让人困惑的地方在于很多时候你自己代码里根本没写parseDouble异常却从框架深处冒出来。所以有必要把“谁在替你转换”这件事捋清楚。Spring MVC 的参数绑定是头号嫌疑人。当你在 Controller 上写RequestParam Double rate或者在一个 DTO 里定义private BigDecimal amount;请求参数是空串时WebDataBinder会去找对应的PropertyEditor或ConverterCustomNumberEditor内部调用的就是Double.valueOf或BigDecimal的构造。这时候堆栈里出现的会是TypeMismatchException或者MethodArgumentTypeMismatchException往 cause 里翻一层才看到NumberFormatException: empty String。很多人只看了最外层的异常就以为是参数校验问题其实根子在转换。Jackson 这类 JSON 库是第二号嫌疑人。反序列化时把 JSON 里的赋给一个double基本类型字段不同版本的默认行为不完全一样有的版本会尝试强制转换然后失败有的会先转成 null 再因为基本类型不能为 null 而报错。这块行为在不同小版本之间确实存在差异我的建议是不要依赖默认值而是显式配置反序列化规则把行为固定下来。第三号是模板引擎和表达式语言。在页面渲染层做数字格式化时如果传入的是空串格式化函数内部会走数字转换这时候抛出的异常被包装成渲染异常堆栈会很长第一眼根本看不到NumberFormatException。类似的还有各类规则引擎、报表引擎它们在处理单元格数据时经常会“尽力而为”地转数字转不动就抛。提示判断是不是框架在替你转换看堆栈里有没有Converter、Editor、Deserializer、TypeHandler这类字眼。有的话去查框架的转换配置比逐行改业务代码更省事。3. 五类高频触发场景与最小复现代码3.1 接口参数绑定空串是怎么绕过非空校验的先说最常见的一种。前端提交{amount: }后端 DTO 里写的是private BigDecimal amount;还加了NotNull注解。很多人以为这样就安全了实际上NotNull只拦 null不拦空串参数校验这一关直接放行等到框架做类型转换的时候才炸。下面这段是最小复现你可以直接拿去跑public class AmountDemo { public static void main(String[] args) { String raw ; // 前端传来的空串 BigDecimal amount new BigDecimal(raw); // 这一行直接抛 System.out.println(amount); } }控制台输出就是我们要找的那句Exception in thread main java.lang.NumberFormatException: empty String at java.math.BigDecimal.init(BigDecimal.java:491) at java.math.BigDecimal.init(BigDecimal.java:597) at AmountDemo.main(AmountDemo.java:3)注意堆栈里两个BigDecimal.init的顺序第一个是char[]版本第二个是String版本这跟上一节讲的调用链完全对得上。看到这个堆栈形状就可以确定是直接构造而不是框架转换。那怎么防第一层当然是入口拦。但别用NotNull要用能识别空串的注解。参数校验规范里NotBlank是给字符串用的对BigDecimal类型字段本身不生效正确的做法是把接收参数写成String用NotBlank校验再在业务层显式转换或者保持BigDecimal类型但把请求参数的默认值约定成“不传该字段”而不是“传空串”。这两种做法各有代价我后面第 4 节会展开讲取舍。3.2 JSON 反序列化默认值和基本类型的坑第二种场景是 JSON 反序列化。这里最隐蔽的一种是字段声明成double基本类型不是Double包装类型JSON 里给的是空串。基本类型不能为 null所以框架没法用“转成 null”这个兜底方案只能硬转然后就抛了。public class RateConfig { private double rate; // 基本类型不能为 null // getter / setter 省略 }如果一定要在反序列化层做兼容我一般会写一个专门的反序列化器把空串统一映射成 null然后再由业务层决定 null 是否合法public class BlankToNullDoubleDeserializer extends JsonDeserializerDouble { Override public Double deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { String text p.getValueAsString(); if (text null) { return null; } String trimmed text.trim(); if (trimmed.isEmpty()) { return null; } try { return Double.valueOf(trimmed); } catch (NumberFormatException e) { // 抛框架自己的异常类型保留字段名等上下文方便前端定位 throw InvalidFormatException.from(p, 字段值不是合法数字: [ text ], text, Double.class); } } }这个写法的好处是异常信息里带上了原始值而且用方括号包住空格、换行、制表符一眼就能看出来。我踩过一次坑日志里打印值不是合法数字: 1.5看着完全正常实际上是1.5\u00A0后面跟了一个不换行空格加方括号之后就现形了。注意给Double注册反序列化器的时候如果字段是double基本类型反序列化器返回 null 之后还会再多一层错误需要在字段上配合默认值处理或者在业务层判空。别指望一个反序列化器解决所有问题。3.3 数据库与 ORM 之间NULL 和空串不是一回事第三种场景在数据访问层。有些团队在表设计时把数值列定义成字符串类型比如为了兼容超长小数或者历史遗留ORM 映射时又声明成BigDecimal读写两侧的类型对不上就会出现“库里存的是空串代码里当数字用”的情况。更麻烦的是空串和 NULL 在业务语义上经常被混着用。比如“折扣率”字段NULL 表示没设置空串表示设置成了空——这两种在业务上可能都该当作“未设置”处理但代码里如果不统一就会出现同一个页面有的数据正常、有的数据报错。我的建议是在数据访问层做一次统一收口把空串归一成 null别让空串流到业务层。public class QuantityRowMapper implements RowMapperQuantity { Override public Quantity mapRow(ResultSet rs, int rowNum) throws SQLException { Quantity q new Quantity(); q.setId(rs.getLong(id)); q.setAmount(parseAmount(rs.getString(amount))); return q; } private BigDecimal parseAmount(String raw) { if (raw null) { return null; } String trimmed raw.trim(); if (trimmed.isEmpty()) { return null; // 空串当成未设置和 NULL 语义对齐 } return new BigDecimal(trimmed); } }这里用rs.getString而不是rs.getBigDecimal是刻意的。用getString能拿到原始文本这样空串和 NULL 的差异才可见用getBigDecimal的话驱动可能已经替你做了隐式转换返回 0 或者 null问题就被掩盖了等哪天上游数据变了才爆出来。3.4 批量导入与外部数据看不见的字符才是主角第四种场景是 Excel、CSV、接口对接这类外部数据源。这类数据的共同特点是“你无法控制输入”用户从各种地方复制粘贴带进来的字符五花八门。我见过的高频隐形字符有这么几个不换行空格\u00A0从网页复制最常见、零宽空格\u200B、字节顺序标记\uFEFF从某些文本编辑器导出的 CSV 开头会带、全角空格\u3000、以及制表符和换行符。这些字符在trim()眼里都不算空白所以 经过 trim 之后长度可能还是 2。排查这类问题光靠打印字符串不够用要把它转成可见形式public static String visible(String s) { if (s null) { return null; } StringBuilder sb new StringBuilder(); for (int i 0; i s.length(); i) { char c s.charAt(i); boolean invisible c 0x20 || c 0x7F || c \u00A0 || c \u200B || c \uFEFF || c \u3000; if (invisible) { sb.append(String.format(\\u%04X, (int) c)); } else { sb.append(c); } } return [ sb ]; }把这段工具方法挂到日志里从visible(1.5)打印出[1.5]从visible(1.5\u00A0)打印出[1.5\u00A0]问题立刻清晰。这套方法我在三个项目里都用过几乎每次都能在十分钟内锁定脏字符。处理方式我一般分两步先做一次全面的字符清洗把常见隐形字符替换成普通空格或者直接删掉再做一次严格的合法性校验清洗之后还不合法的直接拒绝并记录原始值别让它继续往下走。清洗用正则比较省事private static final Pattern INVISIBLE Pattern.compile([\\u00A0\\u200B\\uFEFF\\u3000\\t\\r\\n]); public static String clean(String raw) { if (raw null) { return null; } String result INVISIBLE.matcher(raw).replaceAll( ).trim(); return result.isEmpty() ? null : result; }3.5 前端心智模型错位Number()是 0parseDouble()不是第五种场景是我觉得最值得单独拎出来说的前后端对“空串转数字”的认知完全不一样。在 JavaScript 里Number()的结果是0Number( )也是0parseInt()是NaNparseFloat()也是NaN。也就是说前端开发者很容易形成“空串就是 0”的直觉写代码时随手就Number(input)了然后把这个值提交给后端。后端拿到的如果是数字 0一切正常但如果中间某一环把 0 又转回了空串比如表单清空、序列化时把 0 当作 falsy 值丢弃后端就会拿到空串。反过来Java 侧是严格抛异常两者行为差异巨大。这种差异导致的问题是前端本地测试一切正常联调时偶发报错而且很难解释为什么。我在排查这类问题时第一件事就是让前端把实际发出的请求体完整打出来很多时候一看就发现是某个字段被序列化成了。规避办法是约定清楚数字字段要么不传字段缺失要么传合法数字绝不允许传空串。前端提交前做一次显式的空值判断把空串转成null或者干脆删掉这个 key。后端则在入口做兜底收到空串当作缺失处理。两边各退一步问题就收敛了。4. 从源头到出口的四层防御改造4.1 第一层入口转换把空串挡在类型系统之外最省事的做法是在参数进入业务代码之前就转换掉。Spring MVC 提供了Converter机制可以注册一个全局的String到数字类型的转换器把空串和纯空白统一处理成 null让后续的校验注解和业务逻辑只需要面对 null 这一种“缺失”状态。Configuration public class NumberConvertConfig implements WebMvcConfigurer { Override public void addFormatters(FormatterRegistry registry) { registry.addConverter(String.class, Integer.class, source - { if (source null) { return null; } String trimmed source.trim(); return trimmed.isEmpty() ? null : Integer.valueOf(trimmed); }); registry.addConverter(String.class, BigDecimal.class, source - { if (source null) { return null; } String trimmed source.trim(); return trimmed.isEmpty() ? null : new BigDecimal(trimmed); }); } }这个方案的优点是改动集中、影响面可控所有走 Spring MVC 参数绑定的入口都能受益。缺点有两个一是全局注册转换器会覆盖默认行为如果有地方依赖“空串转换失败”来触发异常行为会变二是它管不到 JSON body 的反序列化因为 body 是 Jackson 在处理不经过FormatterRegistry。所以我一般会把这个改造和参数校验配合用转换器负责把空串变成 nullDTO 上的NotNull负责把 null 拦下来两层职责分明。这样前端传空串时会得到一条清晰的参数校验提示而不是一个带堆栈的 500 错误。提示注册全局转换器前先在测试环境把核心接口跑一遍重点看那些“参数可选”的接口。空串变 null 之后如果业务逻辑里有value ! null就当作“已传值”的判断可能会出现逻辑偏差。4.2 第二层工具方法收口让每个 parse 都有兜底第二层是业务代码里的工具方法。我不太喜欢在每个调用点写 try-catch一来啰嗦二来容易漏三来 catch 块里写什么全靠当时心情。更好的做法是写一组语义明确的转换工具把“怎么处理异常”这件事变成一个有明确命名的选择。public final class Numbers { private Numbers() { } /** 解析失败或为空时返回给定的默认值适合有兜底语义的配置项 */ public static int toIntOr(String raw, int fallback) { String cleaned clean(raw); if (cleaned null) { return fallback; } try { return Integer.parseInt(cleaned); } catch (NumberFormatException e) { return fallback; } } /** 解析失败时抛出带原始值的业务异常适合必须校验的用户输入 */ public static BigDecimal requireDecimal(String raw, String fieldName) { String cleaned clean(raw); if (cleaned null) { throw new IllegalArgumentException(fieldName 不能为空); } try { return new BigDecimal(cleaned); } catch (NumberFormatException e) { throw new IllegalArgumentException(fieldName 不是合法数字: [ cleaned ], e); } } /** 解析失败时返回 null把决策权交给调用方 */ public static Double toDoubleOrNull(String raw) { String cleaned clean(raw); if (cleaned null) { return null; } try { return Double.valueOf(cleaned); } catch (NumberFormatException e) { return null; } } }这三个方法覆盖了我遇到的绝大多数场景配置项用toIntOr用户输入用requireDecimal可选字段用toDoubleOrNull。命名本身就是文档看一眼就知道失败之后会发生什么比满地 try-catch 可读性强得多。注意clean方法要放在工具类内部或者单独抽出来别让调用方自己 trim。我见过有人在调用处写toIntOr(raw.trim(), 0)结果 raw 是 null 时直接 NPE反而引入了新问题。4.3 第三层序列化容错把行为固定下来第三层是序列化层。前面说过JSON 库对空串转数字的默认行为在不同版本之间有差异最稳妥的办法是显式配置别依赖默认值。如果你的项目里数字字段尤其是金额类字段比较多我建议直接注册一个全局的反序列化器把空串和纯空白统一映射成 null同时对非法值抛出带上下文的异常。这样前端调试的时候能直接从响应里看到是哪个字段、原始值是什么而不是一个笼统的“请求参数错误”。Configuration public class JacksonConfig { Bean public Jackson2ObjectMapperBuilderCustomizer numberTolerance() { return builder - { SimpleModule module new SimpleModule(); module.addDeserializer(Double.class, new BlankToNullDoubleDeserializer()); module.addDeserializer(BigDecimal.class, new BlankToNullBigDecimalDeserializer()); builder.modules(module); }; } }这里有个取舍要讲清楚把空串当 null 处理等于承认“空串是合法的缺失表达”。如果你的接口规范里明确要求数字字段不许传空串那就不要做这个容错直接让它报错靠校验提示引导前端改。容错和严格选一个就行别两边都做一半——既容错又在业务层判空最后谁也不知道某个字段到底是必填还是可选。4.4 第四层数据与约定从根上减少空串产生前三层都是代码层面的防御第四层是约定层面的。我越来越觉得很多空串问题的根源不在于代码写得好不好而在于上下游对“空”的表达没有统一。数据库层面我的习惯是数值字段一律不允许存空串要么有值要么 NULL。如果字段是字符串类型但业务上是数字就在写入前做校验非法值直接拒绝别让它进库。表结构上加约束虽然会带来一些改造成本但比起事后排查数据问题这点成本很值。接口层面数字字段的统一约定是缺少该 key 表示未传null表示显式清空空串一律视为非法。这条约定要写进接口文档前端做统一处理后端做统一拦截。我见过一个团队把这条写进了联调检查清单之后这类报错几乎绝迹。配置层面配置中心里的数值项建议加上类型约束或者在启动时做一次全量校验把空值、纯空白、非法字符在启动阶段就打出来。启动失败总比运行到一半出事强尤其是那种“只在某个特定配置组合下才触发”的问题。5. 排查现场实录三个典型案例的完整思路5.1 偶发的对账解析失败某公司的对账服务每隔几天就会报一次empty String频率很低但每次都要人工介入。最初的排查方向是怀疑上游数据有问题加了一堆日志还是定位不到。转折点是把原始字符串用可见化方法打印出来。日志显示原始值是[1.0050\u00A0]末尾跟了一个不换行空格。上游是从网页系统导出的对账文件某一列的格式在不同浏览器下导出结果不一样只有用某类浏览器导出时才会带这个字符。因为Double.parseDouble会先 trim而trim()不处理\u00A0所以 trim 之后长度不为 0继续往下解析解析到那个字符时失败。这里有个细节值得说为什么报的是empty String而不是常见的格式错误消息因为这批数据里有一部分行的该列完全是空的只有一个\u00A0trim 之后长度确实为 0直接命中空串分支另一部分带实际数字的行报的是For input string: 1.0050\u00A0。两种错误混在一起日志分类没做好看起来就像是同一种错误偶发。解决办法是在读取环节统一做字符清洗同时把错误日志按异常消息分类统计这样一眼就能看出是两类问题。改完之后这类告警归零。5.2 线上 5% 请求报错的隐藏字符第二个案例是接口报错率稳定在 5% 左右不高不低持续了好几天。抓了几个请求的入参肉眼看着完全正常。最后是在反序列化器里加了一行原始字节打印才发现的出问题的请求体里数字字段的值是1.5但字符串后面跟了一个零宽空格\u200B。这个字符在大多数编辑器里完全不可见长度也不为 0trim()不掉Double.valueOf直接失败。问题来源是某个运营后台的输入框用户从聊天工具里复制数字粘贴进去带上了零宽字符。前端没有任何校验原样提交。这个案例的教训是隐形字符的清洗不能只做在后端前端提交前也应该做一次归一化两边都做才稳。5.3 定时任务里的空单元格第三个案例最简单也最典型。一个每天凌晨跑的导入任务读取表格后直接new BigDecimal(cell.getStringValue())某天开始每天失败。查下来是上游改了模板新增了一列可选的“调整系数”大部分行是空的代码没有判空直接转。这类问题的修复很简单但我想强调的不是修复本身而是为什么它上线前没被发现。原因是测试数据里这一列全都填了值空值分支从来没被执行过。所以后面我在做导入类功能时会专门准备一份“全空模板”和一份“部分为空模板”作为固定测试用例这个习惯帮我拦下过不少类似问题。6. 常见问题速查表与踩坑心得6.1 问题定位速查表现象最可能的原因快速验证方式消息是empty String浮点数或 BigDecimal 转换输入为空或纯空白在转换前打印可见化字符串消息是For input string: 整型转换输入为空串检查Integer/Long相关调用消息是For input string: 1 整型转换带空格parseInt不 trim确认是否漏了清洗消息是For input string: null字符串拼接把 null 拼成了字面量检查String.format或拼接只在高并发或特定时段出现数据源差异而非代码逻辑问题按异常消息分类统计看是否混了两类错误本地无法复现测试数据没有空值分支补一份全空和部分为空的用例前端说传了值序列化把 falsy 值丢了或转了抓完整请求体比对堆栈里有Converter/Deserializer框架在替你转换查框架转换配置别改业务代码6.2 几条踩出来的经验第一条别用! null判断“有没有值”。空串、纯空白、隐形字符都能通过这个判断然后在后面某一刻炸掉。判断字符串有没有实际内容用trim之后判长度或者直接用现成的空白判断工具。这条看起来是常识但我 review 代码时还是经常看到。第二条错误日志里永远打印原始值的可见形式。打包成方括号、转义隐形字符多写这几行代码能省掉后面几小时的排查。我在所有涉及外部输入解析的地方都加了这个处理回报率极高。第三条Double.parseDouble会 trimInteger.parseInt不会。这个差异导致同一个空串在不同类型上表现不同写通用清洗逻辑的时候一定要统一处理别指望 parse 方法自己兜。第四条别在 catch 块里写空实现。我见过太多catch (NumberFormatException e) { }问题被吞掉等到数据错乱才发现。要么给默认值并打日志要么包装成带上下文的异常往上抛总之要让问题可见。第五条修复要修在产生空串的那一环而不是消费它的那一环。在 parse 前面加判空只能止血真正的问题是上游为什么会产生空串。把上游改掉下游的判空就变成了防御性冗余这才是健康的状态。6.3 一个容易被忽略的扩展方向如果你的系统里数字转换点特别多可以考虑在编译期做一层检查或者用自定义的静态分析规则扫描所有parseXxx和数字构造调用把没有做空值保护的调用点列出来。这件事听起来麻烦但一次性投入之后新代码的问题能在提交阶段就被拦住比等到线上报警强太多。我个人的体会是NumberFormatException: empty String这类异常本身并不难修难的是搞清楚“为什么会有空串流到这里”。每次遇到我都会顺手问一句这个空串是从哪来的上游有没有可能产生别的脏数据往往顺着这条线查下去会发现一批同类问题。修一个异常顺手把一整类隐患堵住这才是排查的真正价值。
阅读完成 · 觉得有帮助?
咨询建站