从零开始 Spring Boot 33:Null-safety
图源:简书 (jianshu.com)
Null-safety(null
安全)实际上是Java这个“古老”语言的历史包袱,很多新的语言(比如go
或kotlin
)在诞生起就在语言层面提供对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 {
@Autowired
private HelloService helloService;
@GetMapping("")
public Object hello() {
helloService.plusWithNoAnnotation(1, null);
return null;
}
}
实际上HelloService.plusWithNoAnnotation()
方法的两个参数我们都不希望是null
,但这在编码中是很难发现的(如果你不仔细阅读将要调用的方法源码的话),大多数情况是要真正运行这段代码才会发现这里会产生一个空指针异常。
这种问题可以用@Nonull
注解来改善:
@Service
public class HelloService {
// ...
@NonNull
public Integer plus(@NonNull Integer a, @NonNull Integer b) {
return a + b;
}
}
示例中的@NonNull
注解表明plus()
方法的两个参数a
和b
,以及返回值都应当是非null
的。
当然,这种约束不是强制性的,目前并没有被Java社区采纳并实施,但依然可以利用IDE等相关工具来尽早发现此类问题,比如在 IntelliJ IDEA 中,如果调用示例中的方法且传入null
,就会出现提示:
这是一个warning:
具体IDEA怎么处理这个注解,是否要显示相应的提示,提示是显示为warning还是error,这些都是可以在IDEA中设置的:
@NonNull
同样可以用于标记属性:
public class Person {
@NonNull
private String name;
}
提示说的很清楚:@NonNull
标记的属性必须被初始化。
@Nullable
@Nullable
的意思是可以是null
(也可以不是)。同样的,这个注解也可以用于标记属性、方法参数、返回值。
看起来似乎@Nullable
没有什么意义,平时不使用这类注解的时候大多数参数都是这个隐含意思。但有没有用@Nullable
明确表明是有意义的,比如下面这个示例:
@Service
public class HelloService {
// ...
@NonNull
public Integer plusAllowNull(@Nullable Integer a, @Nullable Integer b) {
return a + b;
}
}
在IDEA中会有下面的提示:
很显然,如果a
和b
可能是null
,你就需要进行处理,而不是直接利用包装类进行运算:
@Service
public class HelloService {
// ...
@NonNull
public 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
的。
看示例,假如我们的项目结构是这样的:
要使用包注解,我们需要在相应的包(这里是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;
}
}
这相当于我们为该包下所有的属性和方法都添加了@NonNull
注解,因此现在我们只要在必要的时候添加上@Nullable
注解就可以了,比如:
@Component
public class MyUtil {
@Nullable
private 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 {
@Nullable
public Integer plus(Integer a, Integer b) {
Integer c = a + b;
if (c <= 0) {
return null;
}
return c;
}
}
public class Child extends Parent {
@NonNull
@Override
public Integer plus(Integer a, Integer b) {
return a + b;
}
}
Child
覆盖了父类Parent
的plus
方法,父类返回值用@Nullable
标记,子类返回值用@NonNull
标记,这样是没有问题的。但如果反过来:
public class GrandSon extends Child{
@Nullable
@Override
public Integer plus(Integer a, Integer b) {
return super.plus(a, b);
}
}
提示说的很清楚:不能用@Nullable
方法重写@NonNull
方法。
对于参数,同样存在类似反协变的现象,比如:
public class Parent {
@Nullable
public 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
@Override
public Integer plus(@Nullable Integer a, @Nullable Integer b) {
return a + b;
}
}
父类Parent
的plus
方法的参数是@NonNull
标记的,子类的plus
方法是@Nullable
标记的,这样是可行的。但如果反过来:
public class Child extends Parent {
@NonNull
@Override
public Integer plus(@Nullable Integer a, @Nullable Integer b) {
return a + b;
}
}
public class GrandSon extends Child{
@NonNull
@Override
public Integer plus(@NonNull Integer a, @NonNull Integer b) {
return super.plus(a, b);
}
}
错误提示是:@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)