今天在测试环境的运营后台查询商品库存时发现后端接口报错,返回code为904,该错误码表示内部错误。于是在微服务日志里查看,发现某方法报了NPE(java.lang.NullPointer)。
方法里关键的报错代码如下:
public Integer queryXxx(String xx, String yy) { ... XxxRo xxxRo = queryXxxRo(xx, yy); return xxxRo != null ? xxxRo.getQuantity() : 0; }
日志里错误异常堆栈里看到抛NPE异常的行号对应这一行代码:
return xxxRo != null ? xxxRo.getQuantity() : 0;
初一看如果变量xxxRo
为null,那么xxxRo.getQuantity()
会抛NPE。
可语句里判断了的xxxRo
不为null才执行,否则返回0,按理说变量xxxRo
为null应返回0。
queryXxxRo(xx, yy)
是从Redis里查询数据,将相关参数拼好key在Redis去查发现有数据。
Redis里存储类型为hash,对应XxxRo里的每个字段,其中hget xxx quantity
值为4000000012。
本地启动库存服务,通过dubbo支持的telnet里invoke
命令调用该接口,也是那一行代码抛NPE。
我在return xxxRo != null ? xxxRo.getQuantity() : 0;
这一行代码打了个断点调试,
发现xxxRo
不为null,在IDEA里展开该对象,其中各字段都有值,只有quantity
字段为null。
通过Fn + Option + F8调出Evalute窗口,将xxxRo != null ? xxxRo.getQuantity() : 0
复制进去执行,结果为null,并没有抛NPE。
F9放开断点进行执行,日志里打印NPE,跟测试环境一致。
仔细审视这行代码,它用到了三元表达式来判断,表达式执行后直接return,而方法的返回是Integer类型,
而xxxRo.getQuantity()
也是Integer类型,好像没问题。
注意到三元表达式的另一个分支返回的是0,想起Java在装箱/拆箱时(boxing/unboxing)可能会有NPE,
刚才本地复现时通过断点在IDEA的看到xxxRo的quantity
字段为null,因为返回值是0,
可能这里先进行了拆箱,然后进行装箱的转换。
int b = (Integer) null;
这行代码null经过装箱,然后自动拆箱时抛了NPE
继续通过几个例子来模拟:
import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.io.Serializable; import java.util.Optional; /** * @author cdfive */ public class SpecialNPETest { public static void main(String[] args) { // Basic test basic(); // OK case1(); // NPE case2(); // NPE case3(); // OK case4(); // OK case5(); // OK case6(); } private static void basic() { System.out.println("basic start"); // OK Integer a = (Integer) null; // NPE try { int b = (Integer) null; } catch (Exception e) { System.out.println("exception=" + e.getClass().getName() + ",msg=" + e.getMessage()); } System.out.println(); System.out.println("basic end"); } private static void case1() { System.out.println("case1 start"); try { Item item = new Item(); item.setId(1); item.setQuantity(5); // OK Integer result = item != null ? item.getQuantity() : 0; System.out.println(result); } catch (Exception e) { System.out.println("case1 error,exception=" + e.getClass().getName() + ",msg=" + e.getMessage()); } System.out.println("case1 end"); System.out.println(); } private static void case2() { System.out.println("case2 start"); try { Item item = new Item(); item.setId(1); // NPE Integer result = item != null ? item.getQuantity() : 0; } catch (Exception e) { System.out.println("case2 error,exception=" + e.getClass().getName() + ",msg=" + e.getMessage()); } System.out.println("case2 end"); System.out.println(); } private static void case3() { System.out.println("case3 start"); try { Item item = new Item(); item.setId(1); // NPE int result = item != null ? item.getQuantity() : 0; System.out.println(result); } catch (Exception e) { System.out.println("case3 error,exception=" + e.getClass().getName() + ",msg=" + e.getMessage()); } System.out.println("case3 end"); System.out.println(); } private static void case4() { System.out.println("case4 start"); try { Item item = new Item(); item.setId(1); // OK Integer result; if (item != null) { result = item.getQuantity(); } else { result = 0; } System.out.println(result); } catch (Exception e) { System.out.println("case4 error,exception=" + e.getClass().getName() + ",msg=" + e.getMessage()); } System.out.println("case4 end"); System.out.println(); } private static void case5() { System.out.println("case5 start"); try { Item item = new Item(); item.setId(1); // OK Integer result = item != null ? item.getQuantity() : Integer.valueOf(0); System.out.println(result); } catch (Exception e) { System.out.println("case5 error,exception=" + e.getClass().getName() + ",msg=" + e.getMessage()); } System.out.println("case5 end"); System.out.println(); } private static void case6() { System.out.println("case6 start"); try { Item item = new Item(); item.setId(1); // OK Integer result = Optional.ofNullable(item).map(o -> o.getQuantity()).orElse(null); System.out.println(result); } catch (Exception e) { System.out.println("case6 error,exception=" + e.getClass().getName() + ",msg=" + e.getMessage()); } System.out.println("case6 end"); System.out.println(); } @NoArgsConstructor @AllArgsConstructor @Data private static class Item implements Serializable { private Integer id; private Integer quantity; } }
return xxxRo != null ? xxxRo.getQuantity() : 0;
修改这行代码。
3种思路:
Integer result; if (xxxRo != null) { result = xxxRo.getQuantity(); } else { result = 0; }
Integer result = xxxRo != null ? xxxRo.getQuantity() : Integer.valueOf(0);
Integer result = Optional.ofNullable(xxxRo).map(o -> o.getQuantity()).orElse(0);
注意:
刚才有个点忽略了,通过hget xxx quantity
在Redis查出来值为4000000012,为何xxxRo里的quantity字段为null?
注意到日志里还有一个异常:
java.lang.NumberFormatException: For input string: "4000000012"
这里因为4000000012超过了Integer.MAX
的值2147483647,项目框架里Redis的hash转换为Ro对象时用的Integer.valueOf()
,
该方法抛的NumberFormatException
,转换单个字段失败后记录了错误日志并继续执行。
4000000012是其它系统推过来的值,经检查日志和沟通,是测试同学另外一个系统的界面上设置的值过大。