从零开始 Spring Boot 33:Null-safety

news/2024/12/1 0:31:36/

从零开始 Spring Boot 33:Null-safety

spring boot

图源:简书 (jianshu.com)

Null-safety(null安全)实际上是Java这个“古老”语言的历史包袱,很多新的语言(比如gokotlin)在诞生起就在语言层面提供对null安全的解决方案。

实际工作中有相当一部分bug都是“空指针异常”。

Spring框架提供一些注解作为null安全这一问题的解决方案,可以通过在Spring框架中使用这些注解来在编码阶段尽早发现一部分“空指针异常”引起的bug。

Spring框架提供以下注解:

  • @Nullable: 注解,表明一个特定的参数、返回值或字段可以是 null 的。
  • @NonNull: 注解表明特定的参数、返回值或字段不能为 null(在参数/返回值和字段上不需要,因为 @NonNullApi@NonNullFields 分别适用)。
  • @NonNullApi: 包级的注解,声明非null为参数和返回值的默认语义。
  • @NonNullFields: 在包一级的注解,声明非null为字段的默认语义。

这些注解都属于org.springframework.lang包。

@NonNull

看下面这个示例:

@Service
public class HelloService {public Integer plusWithNoAnnotation(Integer a, Integer b) {return a + b;}
}@RestController
@RequestMapping("/hello")
public class HelloController {@Autowiredprivate HelloService helloService;@GetMapping("")public Object hello() {helloService.plusWithNoAnnotation(1, null);return null;}
}

实际上HelloService.plusWithNoAnnotation()方法的两个参数我们都不希望是null,但这在编码中是很难发现的(如果你不仔细阅读将要调用的方法源码的话),大多数情况是要真正运行这段代码才会发现这里会产生一个空指针异常。

这种问题可以用@Nonull注解来改善:

@Service
public class HelloService {// ...@NonNullpublic Integer plus(@NonNull Integer a, @NonNull Integer b) {return a + b;}
}

示例中的@NonNull注解表明plus()方法的两个参数ab,以及返回值都应当是非null的。

当然,这种约束不是强制性的,目前并没有被Java社区采纳并实施,但依然可以利用IDE等相关工具来尽早发现此类问题,比如在 IntelliJ IDEA 中,如果调用示例中的方法且传入null,就会出现提示:

image-20230522171906874

这是一个warning:

image-20230522172103507

具体IDEA怎么处理这个注解,是否要显示相应的提示,提示是显示为warning还是error,这些都是可以在IDEA中设置的:

image-20230522172354134

@NonNull同样可以用于标记属性:

public class Person {@NonNullprivate String name;
}

image-20230522172740398

提示说的很清楚:@NonNull标记的属性必须被初始化。

@Nullable

@Nullable的意思是可以是null(也可以不是)。同样的,这个注解也可以用于标记属性、方法参数、返回值。

看起来似乎@Nullable没有什么意义,平时不使用这类注解的时候大多数参数都是这个隐含意思。但有没有用@Nullable明确表明是有意义的,比如下面这个示例:

@Service
public class HelloService {// ...@NonNullpublic Integer plusAllowNull(@Nullable Integer a, @Nullable Integer b) {return a + b;}
}

在IDEA中会有下面的提示:

image-20230522173535938

很显然,如果ab可能是null,你就需要进行处理,而不是直接利用包装类进行运算:

@Service
public class HelloService {// ...@NonNullpublic Integer plusAllowNull(@Nullable Integer a, @Nullable Integer b) {if (a == null) {a = 0;}if (b == null) {b = 0;}return a + b;}
}

@NonNullApi 和 @NonNullFields

虽然前面说的@NonNull@Nullable对我们发现“空指针异常”并减少bug很有用,但对于每个属性和方法都加上一堆@NonNull@Nullable注解无疑相当繁琐。对此,我们可以利用@NonNullApi@NonNullFields这两个注解进行一定程度上的简化。

@NonNullApi@NonNullFields是定义在包上的注解(其定义是@Target({ElementType.PACKAGE})),前者的意思是指定包下的方法默认返回的是非Null的值并且参数也是非null的,后者的意思是指定包下的类属性默认是非Null的。

看示例,假如我们的项目结构是这样的:

image-20230522181135895

要使用包注解,我们需要在相应的包(这里是com.example.nullsafe.util)下创建一个package-info.java文件,其内容大概像这样:

@NonNullApi
@NonNullFields
package com.example.nullsafe.util;import org.springframework.lang.NonNullApi;
import org.springframework.lang.NonNullFields;

这个文件只包含当前包名的信息,只不过我们可以选择使用@NonNullApi@NonNullFields进行标注。

现在,com.example.nullsafe.util这个包下的代码,默认都应该遵循下面的规则:

  • 类属性应当是非null的。
  • 方法的返回值都应当是非null的。
  • 方法参数应该是非null的。

如果你没有这么做,IDEA就会以warning的方式报告错误,比如:

package com.example.nullsafe.util;
// ...
@Component
public class MyUtil {private Integer test;public Integer plus(Integer a, Integer b) {Integer c = a + b;return null;}
}

image-20230522182238971

这相当于我们为该包下所有的属性和方法都添加了@NonNull注解,因此现在我们只要在必要的时候添加上@Nullable注解就可以了,比如:

@Component
public class MyUtil {@Nullableprivate Integer test;public Integer plus(@Nullable Integer a, @Nullable Integer b) {if (a == null) {a = 0;}if (b == null) {b = 0;}Integer c = a + b;return c;}
}

继承的影响

OOP中,继承可能会引发一些复杂的影响,比如协变反协变,Null-sfety也存在类似的问题,比如:

public class Parent {@Nullablepublic Integer plus(Integer a, Integer b) {Integer c = a + b;if (c <= 0) {return null;}return c;}
}public class Child extends Parent {@NonNull@Overridepublic Integer plus(Integer a, Integer b) {return a + b;}
}

Child覆盖了父类Parentplus方法,父类返回值用@Nullable标记,子类返回值用@NonNull标记,这样是没有问题的。但如果反过来:

public class GrandSon extends Child{@Nullable@Overridepublic Integer plus(Integer a, Integer b) {return super.plus(a, b);}
}

image-20230522184314861

提示说的很清楚:不能用@Nullable方法重写@NonNull方法。

对于参数,同样存在类似反协变的现象,比如:

public class Parent {@Nullablepublic Integer plus(@NonNull Integer a, @NonNull Integer b) {Integer c = a + b;if (c <= 0) {return null;}return c;}
}public class Child extends Parent {@NonNull@Overridepublic Integer plus(@Nullable Integer a, @Nullable Integer b) {return a + b;}
}

父类Parentplus方法的参数是@NonNull标记的,子类的plus方法是@Nullable标记的,这样是可行的。但如果反过来:

public class Child extends Parent {@NonNull@Overridepublic Integer plus(@Nullable Integer a, @Nullable Integer b) {return a + b;}
}public class GrandSon extends Child{@NonNull@Overridepublic Integer plus(@NonNull Integer a, @NonNull Integer b) {return super.plus(a, b);}
}

image-20230522185058546

错误提示是:@NonNull注解不能用于覆盖@Nullable标记的参数。

这些规定都类似于里氏替换原则

关于LSP的详细说明可以阅读里氏替换原则——面向对象设计原则 (biancheng.net)。

需要说明的是,这种限制是非强制的,不是语言层面的,可以看做是一种“符合李氏替换原则的编程建议”,你完全可以无视或者关闭IDEA的相关注解提示。

The End,谢谢阅读。

本文的所有示例可以从ch33/null-safe · 魔芋红茶/learn_spring_boot - 码云 - 开源中国 (gitee.com)获取。

参考资料

  • @Nullable和@NotNull注释的使用_w3cschool
  • Null安全(Null-safety) (springdoc.cn)
  • Spring 中的Null-Safety - 程序员cxuan - 博客园 (cnblogs.com)

http://www.ppmy.cn/news/88870.html

相关文章

C++基本介绍

文章目录 &#x1f96d;1.C基本介绍&#x1f9c2;1.1 C是什么&#x1f9c2;1.2 C发展史 &#x1f352;2. C的优势&#x1f954;2.1 语言的使用广泛度&#x1f954;2.2 C的应用领域 &#x1fad2;3. C学习计划 &#x1f96d;1.C基本介绍 &#x1f9c2;1.1 C是什么 C是一种通用…

苹果手机如何恢复删除的照片

在我们日常生活中&#xff0c;使用手机的时间越来越长&#xff0c;相应地也存储了更多的文件和数据。而这些数据中&#xff0c;照片无疑是我们最重要的一部分。但有时候&#xff0c;我们会误删除一些重要的照片&#xff0c;这时候&#xff0c;我们就需要知道苹果手机如何恢复删…

Android 差分包

一般而言&#xff0c;在开发过程中会有升级&#xff0c;目前主流使用的都是ota 差分包流程 那么如何编译差分包呢&#xff1f; 参考&#xff1a;https://blog.csdn.net/weixin_39641173/article/details/117569857 1、首先高通平台的编译流程与android原生态的编译流程一样&…

【Mysql】初识 Mysql

文章目录 【Mysql】初识 Mysql数据库初识主流关系型数据库理解数据库mysql基本操作连接服务器理解服务器&#xff0c;数据库&#xff0c;表关系小案例数据逻辑存储 mysql架构sql分类存储引擎 【Mysql】初识 Mysql 数据库初识 数据库概念 数据库是“按照数据结构来组织、存储和…

toString 自定义逻辑 过滤 limit ignore 字段 ReflectionToStringBuilder ToStringBuilder

ToStringBuilder 有自己的属性,style, 也有工具方法,直接代理了ReflectionToStringBuilder // ReflectionToStringBuilder 非并发安全,需每次都new ReflectionToStringBuilder reflectionToStringBuilder new ReflectionToStringBuilder(this, org.apache.commons.lang3.build…

如何将can消息映射到具体的信号值上,map规则制定,符合一定的规则

目的通过can数据来查看报文的信号值,构建具体的信号map 参考文章:【Matlab】在 CAN 通信中使用 DBC 文件(车辆网络工具箱) JSON在线编辑网站:中国JSON 1,消息map name value2,构建can规则的json格式,最终使用第三版模板V3(简化版本) 格式中部分内容不用,主要用到的是…

买卖股票的最佳时机

题目描述 给定一个数组 prices &#xff0c;它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。 你只能选择 某一天 买入这只股票&#xff0c;并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。 返回你可以从这笔交易中获取的最…

linuxOPS基础_操作系统概述

计算机发展史 第一台计算机是1946 年2 月14 日诞生日&#xff0c;第一台名称ENIAC。体积一间屋子的大小&#xff0c;重量高达28t。 第一代&#xff1a;1946 – 1958 > 12 年 &#xff08;电子管&#xff09; 第二代&#xff1a;1958 – 1964 > 6 年 &#xff08;晶体管…