这个坑是在导数据的时候撞上的。
测试环境的订单表要灌数,一万两千条,走到八千多行,程序没声了。
日志里刨出来这么一行:
java.lang.NullPointerExceptionat 往后的帧全是业务包名,我就不抄了。
栈指到的代码长这样:
String path = tmpPath + "/upload";tmpPath 这个字段头顶是有注解的:
@Value("${file.tmp.path}") private String tmpPath;说实话,我当时头一个念头就是配置写漏了。
翻出 application.yml 一看,这个 key 躺得好好的, 测试环境里那份也一模一样, 本地的单测也没报错。
折腾了半个钟头,我才盯着调用方那一行:
ExcelHelper helper = new ExcelHelper();就这么个写法。
在我原来的印象里,@Value 属于编译阶段就完成赋值的那类注解。
事实正相反:容器把 bean 造好之后,才有回填这一步, 把配置值写进字段。
bean 这词听着玄,说白了就是容器替你创建、也替你持有的对象。
对象要是你自己 new 出来的,容器管不着, 回填这步自然没有,字段维持 Java 默认初值,引用类型就是 null。
本地单测为什么是绿的?用的实例来自容器:
@Autowired private ExcelHelper helper;类还是同一个类,只换了获取方式,一边有值,一边为空。
版本是 Spring Boot 2.1.6,这里真不怪版本, 要怪就怪对象来路不对。
这种实例,现场有没有快办法认出来?我试过看日志。
思路一:打一行,看类名尾巴有没有 CGLIB 那串东西。 CGLIB 是 Spring 生成代理类用的工具,经它手的类名会变长。
这条路走死了:类上没挂任何 AOP, 容器发下来的实例,类名干干净净,和 new 出来的看不出区别。
思路二:数构造日志出现几次:
public ExcelHelper() { log.info("ExcelHelper init"); }在容器里它是单例,一个进程只飘一行。 谁在偷偷 new,每调一次多一行,谁干的立刻现形。
顺带把界限划清,容易混的一共有两处。
先看 final,我起初也把它归进同一个坑,后来证伪了。
既不给初值、构造函数里也不赋值,编译这关就过不去, javac 会先拦下来:
FinalBlank.java:2: error: variable tmpPath not initialized in the default constructor连编译都过不了,null 根本拿不到,防它属于多余。
再看 static。我只敢确定一点: 类根本没被容器收管时,它和实例字段一个待遇,没人赋值,null 到底。
如果是容器里的 bean 往静态字段写值,我没验过。 网上那种把 static 说成一律不可注入的绝对话,我不敢全收。
还有一处容易误会:${key}想带默认值,关键在冒号。
@Value("${file.tmp.path:}") @Value("${file.tmp.path:/data}")冒号后头什么都不写,也算给了,值是空字符串。
要是冒号也省了、配置偏偏缺,这种最好查,启动直接报:
Could not resolve placeholder 'file.tmp.path'进程根本起不来,轮不到跑数据时才炸。 真正吓人的,是没进容器那一类:一点动静都不给。
后来数据我重新灌了一次,把八千行之后的缺口补齐。
tmpPath 这一劫总算过去了。
头疼的是测试环境:路径改一下,服务就得重启, 一次七八分钟起不来,到现在没人接这个活。