我在大厂做 CR——如何体系化防控空指针非常

打印 上一主题 下一主题

主题 852|帖子 852|积分 2556

大家好,我是木宛哥,今天和大家分享下——代码 CR 时针对恼人的空指针非常(NullPointerException)如何做到体系化去防控;
什么是空指针非常

从内存角度看,对象的实例化必要在堆内存中分配空间。如果一个对象没有被创建,那也就没有分配内存,当应用程序访问空对象时,实际上是访问一个“无效”的内存区域,从而导致系统抛出非常。
我们在 Java 编程时,空指针非常是一个常见的运行时错误,严峻乃至会导致进程退出。所以这也是为什么我们要在 CR 时如此器重它的缘故起因。
CR 我们要做什么

木宛哥认为 CR 应该重点关注三点:

  • 业务逻辑正确性
  • 代码的可读性和可维护性
  • 代码的结实性和稳定性
OK,再回过头来看,针对空指针非常,在 CR 时,更多要从代码的结实性和稳定性切入,可分为:

  • 防御性去杜绝空指针非常的出现(大多是访问了不存在的对象)
  • 对三方框架大概 JDK 认知不完善导致的潜伏空指针非常发生(更多靠评审参与者经验分享)
防御性编程

防御性编程是非常有必要的,一方面可以提高系统稳定性和结实性,另一方面可以形成比力好的代码规范;同时也是非常紧张的头脑,每个人都会如此去实践,在 CR 时针对空指针非常的防御是 common sense;例如:
1.防御利用了未初始化的对象:
  1. MyObject obj = null;
  2. if (obj!= null){
  3.     obj.someMethod();
  4. }
复制代码
2.防御利用了对象没有初始化的字段;
  1. class MyClass {
  2.    String name;
  3. }
  4. MyClass obj = new MyClass();
  5. if (StringUtils.isNotBlank(obj.getName())){
  6.     // do something
  7. }
复制代码
3.防御当调用方法返回 null 后,试图对返回的对象调用其方法或属性:
  1. MyObject obj = getMyObject(); // 假设返回 null
  2. if (obj != null) {
  3.     obj.someMethod();
  4. }
复制代码
4.防御访问了集合中不存在的元素:

  • Map.get() 方法返回 null
  • Queue 的方法如 poll() 或 peek() 返回 null
  1. Map<String ,String> dummyMap = new HashMap<>();
  2. String value = dummyMap.get("key");
  3. if (org.apache.commons.lang3.StringUtils.isNotBlank(value)){
  4.     // do something
  5. }
复制代码
三方框架或 JDK 利用不妥引发空指针非常提前排雷

这一类更多是三方框架或 JDK 的内部机制不清楚导致的踩坑,只有踩了这种类,
才会恍然大悟:“哦,原来这样啊,下回得注意了”;
所以针对这类题目,更多必要评审参与人的经验去发现,必要团队去共创,共建知识体系,例如:在团队空间维护“ TOP 100 踩坑记”等等;
在上篇文章《为什么建议利用枚举来替换布尔值》中,木宛哥提到过 Boolean 为 null 时产生的第三种效果,易造成 if 条件判断拆箱引发空指针题目,今天再继续分享其他:
1.三目运算符拆箱空指针题目
  1. int var1 = 20;
  2. Integer var2 = null;
  3. boolean condition = true;
  4. // 三目运算符拆箱问题,发生 NullPointerException
  5. System.out.println(condition ? var2 : var1);
复制代码
这里:condition 为 true,所以三目运算符选择了 var2(即 null)。即: var2(Integer 类型)赋值给 num(Integer 类型)。理论上在这里应该是 num 被赋值为 null。
但在 Java 中,三目运算符的返回类型必要通过类型来推导:

  • 如果 var1 是 int 类型,而 var2 是 Integer 类型,三目运算符会将它们的类型推导合并,令返回值为 int 类型。
  • 这意味着,如果 condition 为 true,则会尝试将 var2(null)拆箱成 int。由于 null 不能拆箱成 int,因此会抛出 NullPointerException
这类范例的题目更多必要在 CR 时提前暴露出来,保证一致的参数类型来避免拆箱;
2.日志打印利用 fastjson 序列化时造成的空指针题目
大部分程序员编程开发习惯,喜欢打印参数到日志里,但有时候一个不起眼的 log.info 打印日志有可能导致接口非常;
如下打印日志,效果 fastjson 序列化非常,发生 NullPointerException
  1. @Test
  2. public void testJSONString() {
  3.     Employee employee = new Employee("jack", 100);
  4.     //fastjson 序列化异常,发生 NullPointerException
  5.     LoggerUtil.info(logger,"{}",JSON.toJSONString(employee));
  6. }
  7. static class Employee {
  8.     private EmployeeId employeeId;
  9.     private String name;
  10.     private Integer salary;
  11.     public Employee(String name, Integer salary) {
  12.         this.name = name;
  13.         this.salary = salary;
  14.     }
  15.     public String getName() {
  16.         return name;
  17.     }
  18.     public Integer getSalary() {
  19.         return salary;
  20.     }
  21.     public String getEmployeeId() {
  22.         return this.employeeId.getId();
  23.     }
  24. }
  25. static class EmployeeId {
  26.     private String id;
  27.     public String getId() {
  28.         return id;
  29.     }
  30.     public void setId(String id) {
  31.         this.id = id;
  32.     }
  33. }
复制代码
缘故起因在于 fastjson 利用 JSON.toJSONString(employee) 序列化成 JSON 时,底层实际通过分析 get 开头方法来识别属性,即:调用 get 方法获取属性的 value 值;上述代码:employeeId 为 null ,但序列化时实行了 getEmployeeId 引发的空指针非常;
所以:特别是大家在实践 DDD 的时候,因为领域模型往往是充血模型,不仅有数据还包含了行为,对于行为可能习惯有 get 开头命名,要特别器重在打印领域模型时序列化题目;
3.对 Stream 流操作认知不完善导致的空指针非常
如果 Stream 流中存在空值,必要非常小心。
例如,如果第一个元素恰恰为 null,findFirst() 将抛出 NullPointerException。这是因为 findFirst() 返回一个 Optional,而 Optional 不能包含空值。
  1. Arrays.asList(null, 1, 2).stream().findFirst();//发生 NullPointerException
复制代码
max()、min() 和 reduce(),也表现出类似的行为。如果 null 是终极效果,则会抛出非常。
  1. List<Integer> list = Arrays.asList(null, 1, 2);
  2. var comparator = Comparator.<Integer>nullsLast(Comparator.naturalOrder());
  3. System.out.println(list.stream().max(comparator));//发生 NullPointerException
复制代码
再例如:我们在利用 Stream 流式编程时,如果流包含 null,可以转换为 toList() 或 toSet();
然而,toMap() 要注意, 不允许空值(允许空Key):
  1. Employee employee1 = new Employee("Jack", 10000);
  2. Employee employee2 = new Employee(null, 10000);
  3. //toMap的Value不能为空,此处异常
  4. Map<Integer, String> salaryMap = Arrays.asList(employee1, employee2)
  5.     .stream()
  6.     .collect(Collectors.toMap(Employee::getSalary, Employee::getName));
复制代码
以及:groupingBy() 不允许空 Key:
  1. Employee employee1 = new Employee("Jack", 10000);
  2. Employee employee2 = new Employee(null, 10000);
  3. //groupingBy的Key不能为空,此处抛异常
  4. Map<String, List<Employee>> result = Stream.of(employee1, employee2)
  5.     .collect(Collectors.groupingBy(Employee::getName));
复制代码
可见在流中利用了空对象存在很多陷阱;所以,在 CR 时,要重点关注 Stream 流的数据来源,避免在流中存在 null,不确定的话建议用 filter(Objects::nonNull) 将它们过滤掉。
再谈空指针防控手段

上一章更多还是从防御空指针去解题目,但能保证每个人都是认知一样吗,同时在 CR 时也会有丧家之犬;下面代码我想每个人都会这样去避免空指针,但难免在某个加班到凌晨的日子,脑壳一抽筋写反了
  1. if("DEFAULT".equals(var)){
  2.     //do something
  3. }
复制代码
所以,在这一章,木宛哥从数据来源切入,回答:“可否数据天生就是存在非空的、方法天生就是不会返回 null”?
从程序角度来看,是合理的;很多变量永久不包含 null,很多方法也永久不返回 null。我们可以分别称它们为“非空变量”和“非空方法”(NonNull);
其他变量和方法在某些情况下可能会包含或返回 null,它们称为“可空”(Nullable);
基于这个理论,在解空指针题目时,提供了另一种方式解法:

  • 数据来自三方系统,控制权不在我们,故:不可信托,必要做好防御编程;
  • 数据来自自身,控制权在我们,控制数据创建即非空,故:可信托;如下:
尽可能屏蔽 null 值

对输入值举行校验——在公共方法和构造函数中。需在每个 set 字段的入口处添加 Objects.requireNonNull() 调用。requireNonNull() 方法会在其参数为 null 时抛出 NullPointerException。
  1. public class Employee {
  2.     public Employee(String name, Integer salary) {
  3.         this.name = Objects.requireNonNull(name);
  4.         this.salary = Objects.requireNonNull(salary);
  5.     }
  6. }
复制代码
这样做有助于在入口处屏蔽 null 值的写入
如果你的方法接受集合作为输入,也可以在方法入口遍历该集合以确保它不包含 null 值:
  1. public void check(Collection<String> data) {
  2.     data.forEach(Objects::requireNonNull);
  3. }
复制代码
注:此处必要视详细集合的大小以及评估性能损耗;
同样的类型场景,不详细举例了:

  • 当你的类型是集合时,返回一个空容器,而不是返回 null,可以避免消费方出现空指针非常;
  • 利用枚举常量来替换 Boolean 来避免拆箱引入的空指针非常
  • 非法数据状态,直接短路抛出非常而不是返回 null;
善用静态分析工具来辅助

介绍两个紧张的注解:@Nullable和@NotNull 注解:

  • @Nullable 注解意味着预期被表明的变量可能包含null,大概被表明的方法可能返回null;
  • @NotNull 表明意味着预期的值绝不是null。并为静态分析提供了提示
这类注解,可以在静态分析工具实时分析潜伏的非常;
  1. interface Processor {
  2.     @NotNull
  3.     String getNotNullValue();
  4.    
  5.     @Nullable
  6.     String getNullable();
  7.    
  8.     public void process() {
  9.         //此处警告:条件永远为假,不用多次一举
  10.         if (getNotNullValue() == null) {
  11.             //do something
  12.         }
  13.         //此处警告:trim() 调用可能导致 NullPointerException
  14.         System.out.println(getNullable().trim());
  15.     }
  16. }
复制代码
再谈利用 Optional 替代 null 的一些注意事项

为了避免利用 null ,一些开发者倾向于利用 Optional 类型。可以将 Optional 想象成一个盒子,它要么是空的,要么包含一个非 null 的值:
获取Optional对象有三种尺度方式:

  • Optional.empty() —— 获取一个空的 Optional
  • Optional.of(value) —— 获取一个非空的 Optional,如果值为 null 则抛出NullPointerException
  • Optional.ofNullable(value) —— 如果值为 null 则获取一个空的 Optional,否则获取一个包含值的非空 Optional
利用 Optional 来防备空指针,大题目没有,但有几个细节必要注意
1.勿滥用 ofNullable
一些开发者喜欢在所有地方利用 ofNullable(),因为它被认为是更安全的,它从不抛出非常。但不能滥用,如果你已经知道你的值永久不会为null,最好利用 Optional.of()。在这种情况下,如果你看到一个非常,你会立即知道错了并且修复;
2.Optional 造成的代码可读性降低
如下代码获取员工地址,固然简洁,但可读性很差,对于嵌套特别深的情况下,我还是不建议利用 Opinional,毕竟代码除了给自己看还得让别人也一眼明确意图
  1. String employeeAddress = Optional.ofNullable(employee)
  2.         .map(Employee::getAddress)
  3.         .map(Address::getStreet)
  4.         .map(Street::getNo)
  5.         .map(No::getNumber).orElseThrow(() -> new IllegalArgumentException("非法参数"));
复制代码
事后的非常监控

事前禁止写入 null,事中防御性编程空指针非常,但真的高枕无忧了吗?
未必,事后所以创建一套好的非常告警机制是非常紧张的;
我建议针对关键字:NullPointerException 做单独的日志收罗,同时配上相应的告警级别:理论上出现 1 次空指针非常就应该参与定位;
当然,特别是在发布周期内,如果 N 分钟内出现超过 M 次空指针非常那就肯定要快速定位和回滚了;
写在最后

接待关注我的公众号:编程启示录,第一时间获取最新消息;
微信公众号

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

惊落一身雪

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表