Polish null-safety documentation
This commit is contained in:
@@ -3,18 +3,18 @@
|
||||
|
||||
Although Java does not let you express nullness markers with its type system yet, the Spring Framework codebase is
|
||||
annotated with https://jspecify.dev/docs/start-here/[JSpecify] annotations to declare the nullability of its APIs,
|
||||
fields and related type usages. Reading the https://jspecify.dev/docs/user-guide/[JSpecify user guide] is highly
|
||||
fields, and related type usages. Reading the https://jspecify.dev/docs/user-guide/[JSpecify user guide] is highly
|
||||
recommended in order to get familiar with those annotations and semantics.
|
||||
|
||||
The primary goal of this null-safety arrangement is to prevent `NullPointerException` to be thrown at runtime via build
|
||||
time checks and to turn explicit nullability into a way to express the possible absence of value. It is useful in both
|
||||
The primary goal of this null-safety arrangement is to prevent a `NullPointerException` from being thrown at runtime via build
|
||||
time checks and to use explicit nullability as a way to express the possible absence of value. It is useful in both
|
||||
Java by leveraging some tooling (https://github.com/uber/NullAway[NullAway] or IDEs supporting JSpecify annotations
|
||||
such as IntelliJ IDEA for example) and Kotlin where JSpecify annotations are automatically translated to
|
||||
such as IntelliJ IDEA) and Kotlin where JSpecify annotations are automatically translated to
|
||||
{kotlin-docs}/null-safety.html[Kotlin's null safety].
|
||||
|
||||
The {spring-framework-api}/core/Nullness.html[`Nullness` Spring API] can be used at runtime to detect the nullness of a
|
||||
type usage, a field, a method return type or a parameter. It provides full support for JSpecify annotations,
|
||||
Kotlin null safety, Java primitive types, as well as a pragmatic check on any `@Nullable` annotation (regardless of the
|
||||
type usage, a field, a method return type, or a parameter. It provides full support for JSpecify annotations,
|
||||
Kotlin null safety, and Java primitive types, as well as a pragmatic check on any `@Nullable` annotation (regardless of the
|
||||
package).
|
||||
|
||||
[[null-safety-libraries]]
|
||||
@@ -22,35 +22,35 @@ package).
|
||||
|
||||
As of Spring Framework 7, the Spring Framework codebase leverages JSpecify annotations to expose null-safe APIs and
|
||||
to check the consistency of those nullability declarations with https://github.com/uber/NullAway[NullAway] as part of
|
||||
its build. It is recommended for each library depending on Spring Framework (Spring portfolio projects), as
|
||||
well as other libraries related to the Spring ecosystem (Reactor, Micrometer and Spring community projects), to do the
|
||||
its build. It is recommended for each library depending on Spring Framework and Spring portfolio projects, as
|
||||
well as other libraries related to the Spring ecosystem (Reactor, Micrometer, and Spring community projects), to do the
|
||||
same.
|
||||
|
||||
[[null-safety-applications]]
|
||||
== Leveraging JSpecify annotations in Spring applications
|
||||
|
||||
Developing applications with IDEs supporting nullness annotations will provide warnings in Java and errors in Kotlin
|
||||
Developing applications with IDEs that support nullness annotations will provide warnings in Java and errors in Kotlin
|
||||
when the nullability contracts are not honored, allowing Spring application developers to refine their null handling to
|
||||
prevent `NullPointerException` to be thrown at runtime.
|
||||
prevent a `NullPointerException` from being thrown at runtime.
|
||||
|
||||
Optionally, Spring application developers can annotate their codebase and use build plugins like
|
||||
https://github.com/uber/NullAway[NullAway] to enforce null-safety during build time at application level.
|
||||
https://github.com/uber/NullAway[NullAway] to enforce null-safety at the application level during build time.
|
||||
|
||||
[[null-safety-guidelines]]
|
||||
== Guidelines
|
||||
|
||||
The purpose of this section is to share some guidelines proposed for specifying explicitly the nullability of
|
||||
The purpose of this section is to share some proposed guidelines for explicitly specifying the nullability of
|
||||
Spring-related libraries or applications.
|
||||
|
||||
|
||||
[[null-safety-guidelines-jspecify]]
|
||||
=== JSpecify
|
||||
|
||||
The key points to understand is that by default, the nullness of types is unknown in Java, and that non-null type
|
||||
usages are by far more frequent than nullable ones. In order to keep codebases readable, we typically want to define
|
||||
that by default, type usages are non-null unless marked as nullable for a specific scope. This is exactly the purpose of
|
||||
https://jspecify.dev/docs/api/org/jspecify/annotations/NullMarked.html[`@NullMarked`] that is typically set with Spring
|
||||
at package level via a `package-info.java` file, for example:
|
||||
The key points to understand are that the nullness of types is unknown in Java by default and that non-null type
|
||||
usage is by far more frequent than nullable usage. In order to keep codebases readable, we typically want to define
|
||||
by default that type usage is non-null unless marked as nullable for a specific scope. This is exactly the purpose of
|
||||
https://jspecify.dev/docs/api/org/jspecify/annotations/NullMarked.html[`@NullMarked`] which is typically set in Spring
|
||||
projects at the package level via a `package-info.java` file, for example:
|
||||
|
||||
[source,java,subs="verbatim,quotes",chomp="-packages",fold="none"]
|
||||
----
|
||||
@@ -60,7 +60,7 @@ package org.springframework.core;
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
----
|
||||
|
||||
In the various Java files belonging to the package, nullable type usages are defined explicitly with
|
||||
In the various Java files belonging to the package, nullable type usage is defined explicitly with
|
||||
https://jspecify.dev/docs/api/org/jspecify/annotations/Nullable.html[`@Nullable`]. It is recommended that this
|
||||
annotation is specified just before the related type on the same line.
|
||||
|
||||
@@ -71,7 +71,7 @@ For example, for a field:
|
||||
private @Nullable String fileEncoding;
|
||||
----
|
||||
|
||||
Or for method parameters and return value:
|
||||
Or for method parameters and method return types:
|
||||
|
||||
[source,java,subs="verbatim,quotes"]
|
||||
----
|
||||
@@ -81,19 +81,23 @@ public static @Nullable String buildMessage(@Nullable String message,
|
||||
}
|
||||
----
|
||||
|
||||
When overriding a method, JSpecify annotations are not inherited from the superclass method. That means they should be
|
||||
repeated if you just want to override the implementation and keep the same nullability.
|
||||
[NOTE]
|
||||
====
|
||||
When overriding a method, JSpecify annotations are not inherited from the original
|
||||
method. That means the JSpecify annotations should be copied to the overriding method if
|
||||
you want to override the implementation and keep the same nullability semantics.
|
||||
====
|
||||
|
||||
With arrays and varargs, you need to be able to differentiate the nullness of the elements from the nullness of
|
||||
the array itself. Pay attention to the syntax
|
||||
https://docs.oracle.com/javase/specs/jls/se17/html/jls-9.html#jls-9.7.4[defined by the Java specification] which may be
|
||||
initially surprising:
|
||||
|
||||
- `@Nullable Object[] array` means individual elements can be null but the array itself can't.
|
||||
- `Object @Nullable [] array` means individual elements can't be null but the array itself can.
|
||||
- `@Nullable Object[] array` means individual elements can be null but the array itself cannot.
|
||||
- `Object @Nullable [] array` means individual elements cannot be null but the array itself can.
|
||||
- `@Nullable Object @Nullable [] array` means both individual elements and the array can be null.
|
||||
|
||||
The Java specifications also enforces that annotations defined with `@Target(ElementType.TYPE_USE)` like JSpecify
|
||||
The Java specification also enforces that annotations defined with `@Target(ElementType.TYPE_USE)` like JSpecify
|
||||
`@Nullable` should be specified after the last `.` with inner or fully qualified types:
|
||||
|
||||
- `Cache.@Nullable ValueWrapper`
|
||||
@@ -113,12 +117,12 @@ The recommended configuration is:
|
||||
- `NullAway:OnlyNullMarked=true` in order to perform nullability checks only for packages annotated with `@NullMarked`.
|
||||
- `NullAway:CustomContractAnnotations=org.springframework.lang.Contract` which makes NullAway aware of the
|
||||
{spring-framework-api}/lang/Contract.html[@Contract] annotation in the `org.springframework.lang` package which
|
||||
can be used to express complementary semantics to avoid non-relevant warnings in your codebase.
|
||||
can be used to express complementary semantics to avoid irrelevant warnings in your codebase.
|
||||
|
||||
A good example of `@Contract` benefits is
|
||||
{spring-framework-api}/util/Assert.html#notNull(java.lang.Object,java.lang.String)[`Assert#notnull`] which is annotated
|
||||
with `@Contract("null, _ -> fail")`. With the configuration above, NullAway will understand that after a successful
|
||||
invocation, the value passed as a parameter is not null.
|
||||
A good example of the benefits of a `@Contract` declaration can be seen with
|
||||
{spring-framework-api}/util/Assert.html#notNull(java.lang.Object,java.lang.String)[`Assert.notNull()`] which is annotated
|
||||
with `@Contract("null, _ -> fail")`. With that contract declaration, NullAway will understand that the value passed as a
|
||||
parameter cannot be null after a successful invocation of `Assert.notNull()`.
|
||||
|
||||
Optionally, it is possible to set `NullAway:JSpecifyMode=true` to enable
|
||||
https://github.com/uber/NullAway/wiki/JSpecify-Support[checks on the full JSpecify semantics], including annotations on
|
||||
@@ -126,26 +130,26 @@ generic types. Be aware that this mode is
|
||||
https://github.com/uber/NullAway/issues?q=is%3Aissue+is%3Aopen+label%3Ajspecify[still under development] and requires
|
||||
using JDK 22 or later (typically combined with the `--release` Java compiler flag to configure the
|
||||
expected baseline). It is recommended to enable the JSpecify mode only as a second step, after making sure the codebase
|
||||
generates no warning with the recommended configuration mentioned above.
|
||||
generates no warning with the recommended configuration mentioned previously in this section.
|
||||
|
||||
==== Warnings suppression
|
||||
|
||||
There are a few valid use cases where NullAway will wrongly detect nullability problems. In such case, it is recommended
|
||||
There are a few valid use cases where NullAway will incorrectly detect nullability problems. In such case, it is recommended
|
||||
to suppress related warnings and to document the reason:
|
||||
|
||||
- `@SuppressWarnings("NullAway.Init")` at field, constructor or class level can be used to avoid unnecessary warnings
|
||||
due to the lazy initialization of fields, for example due to a class implementing
|
||||
- `@SuppressWarnings("NullAway.Init")` at field, constructor, or class level can be used to avoid unnecessary warnings
|
||||
due to the lazy initialization of fields – for example, due to a class implementing
|
||||
{spring-framework-api}/beans/factory/InitializingBean.html[`InitializingBean`].
|
||||
- `@SuppressWarnings("NullAway") // Dataflow analysis limitation` can be used when NullAway dataflow analysis is not
|
||||
able to detect that the path involving a nullability problem will never happen.
|
||||
- `@SuppressWarnings("NullAway") // Lambda` can be used when NullAway does not take into account assertions performed
|
||||
outside of a lambda for the code path within the lambda.
|
||||
- `@SuppressWarnings("NullAway") // Reflection` can be used for some reflection operations that are known returning
|
||||
non-null values even if that can't be expressed by the API.
|
||||
- `@SuppressWarnings("NullAway") // Well-known map keys` can be used when `Map#get` invocations are done with keys known
|
||||
to be present and non-null related values inserted previously.
|
||||
- `@SuppressWarnings("NullAway") // Overridden method does not define nullability` can be used when the super class does
|
||||
not define nullability (typically when the super class is coming from a dependency).
|
||||
- `@SuppressWarnings("NullAway") // Reflection` can be used for some reflection operations that are known to return
|
||||
non-null values even if that cannot be expressed by the API.
|
||||
- `@SuppressWarnings("NullAway") // Well-known map keys` can be used when `Map#get` invocations are performed with keys that are known
|
||||
to be present and when non-null related values have been inserted previously.
|
||||
- `@SuppressWarnings("NullAway") // Overridden method does not define nullability` can be used when the superclass does
|
||||
not define nullability (typically when the superclass comes from a dependency).
|
||||
|
||||
|
||||
[[null-safety-migrating]]
|
||||
@@ -154,30 +158,30 @@ not define nullability (typically when the super class is coming from a dependen
|
||||
Spring null-safety annotations {spring-framework-api}/lang/Nullable.html[`@Nullable`],
|
||||
{spring-framework-api}/lang/NonNull.html[`@NonNull`],
|
||||
{spring-framework-api}/lang/NonNullApi.html[`@NonNullApi`], and
|
||||
{spring-framework-api}/lang/NonNullFields.html[`@NonNullFields`] in the `org.springframework.lang` package have been
|
||||
introduced in Spring Framework 5 when JSpecify did not exist and the best option was to leverage JSR 305 (a dormant
|
||||
but widespread JSR) meta-annotations. They are deprecated as of Spring Framework 7 in favor of
|
||||
{spring-framework-api}/lang/NonNullFields.html[`@NonNullFields`] in the `org.springframework.lang` package were
|
||||
introduced in Spring Framework 5 when JSpecify did not exist, and the best option at that time was to leverage
|
||||
meta-annotations from JSR 305 (a dormant but widespread JSR). They are deprecated as of Spring Framework 7 in favor of
|
||||
https://jspecify.dev/docs/start-here/[JSpecify] annotations, which provide significant enhancements such as properly
|
||||
defined specifications, a canonical dependency with no split-package issue, better tooling, better Kotlin integration
|
||||
and the capability to specify the nullability more precisely for more use cases.
|
||||
defined specifications, a canonical dependency with no split-package issues, better tooling, better Kotlin integration,
|
||||
and the capability to specify nullability more precisely for more use cases.
|
||||
|
||||
A key difference is that Spring deprecated null-safety annotations, following JSR 305 semantics, apply to fields,
|
||||
parameters and return values while JSpecify annotations apply to type usages. This subtle difference
|
||||
is in practice pretty significant, as it allows for example to differentiate the nullness of elements from the
|
||||
nullness of arrays/varargs as well as defining the nullness of generic types.
|
||||
A key difference is that Spring's deprecated null-safety annotations, which follow JSR 305 semantics, apply to fields,
|
||||
parameters, and return values; while JSpecify annotations apply to type usage. This subtle difference
|
||||
is in practice pretty significant, as it allows developers to differentiate between the nullness of elements and the
|
||||
nullness of arrays/varargs as well as to define the nullness of generic types.
|
||||
|
||||
That means array and varargs null-safety declarations have to be updated to keep the same semantic. For example
|
||||
That means array and varargs null-safety declarations have to be updated to keep the same semantics. For example
|
||||
`@Nullable Object[] array` with Spring annotations needs to be changed to `Object @Nullable [] array` with JSpecify
|
||||
annotations. Same for varargs.
|
||||
annotations. The same applies to varargs.
|
||||
|
||||
It is also recommended to move field and return value annotations closer to the type on the same line, for example:
|
||||
It is also recommended to move field and return value annotations closer to the type and on the same line, for example:
|
||||
|
||||
- For fields, instead of `@Nullable private String field` with Spring annotations, use `private @Nullable String field`
|
||||
with JSpecify annotations.
|
||||
- For return values, instead of `@Nullable public String method()` with Spring annotations, use
|
||||
- For method return types, instead of `@Nullable public String method()` with Spring annotations, use
|
||||
`public @Nullable String method()` with JSpecify annotations.
|
||||
|
||||
Also, with JSpecify, you don't need to specify `@NonNull` when overriding a type usage annotated with `@Nullable` in the
|
||||
Also, with JSpecify, you do not need to specify `@NonNull` when overriding a type usage annotated with `@Nullable` in the
|
||||
super method to "undo" the nullable declaration in null-marked code. Just declare it unannotated and the null-marked
|
||||
defaults (a type usage is considered non-null unless explicitly annotated as nullable) will apply.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user