From f1261bf48728e239810bb046e4c2e681b61e9e67 Mon Sep 17 00:00:00 2001 From: wonyongg <111210881+wonyongg@users.noreply.github.com> Date: Sun, 17 Aug 2025 04:44:31 +0900 Subject: [PATCH 1/3] configuration-processor: detect @Nullable on TYPE_USE parameters Update MetadataGenerationEnvironment#getAnnotation to also inspect type-use annotations (element.asType().getAnnotationMirrors()) in addition to declaration annotations. This ensures that @Nullable on method parameter types is correctly detected when generating configuration metadata for Actuator endpoints. Previously, parameters annotated with @Nullable in TYPE_USE position could be missed, causing them to be marked as required in metadata. With this change, both declaration-level and type-use @Nullable annotations are recognized. No new dependencies are introduced, and existing public APIs remain unchanged. Signed-off-by: wonyongg <111210881+wonyongg@users.noreply.github.com> --- .../MetadataGenerationEnvironment.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/configuration-metadata/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/MetadataGenerationEnvironment.java b/configuration-metadata/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/MetadataGenerationEnvironment.java index 4b3e1ef320e2..984e0b3f4a75 100644 --- a/configuration-metadata/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/MetadataGenerationEnvironment.java +++ b/configuration-metadata/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/MetadataGenerationEnvironment.java @@ -265,6 +265,12 @@ AnnotationMirror getAnnotation(Element element, String type) { return annotation; } } + + for (AnnotationMirror annotation : element.asType().getAnnotationMirrors()) { + if (type.equals(annotation.getAnnotationType().toString())) { + return annotation; + } + } } return null; } From 9cdbf91cb8820b4bb83c6e2ffdb9cfd1d6fc87f8 Mon Sep 17 00:00:00 2001 From: wonyongg <111210881+wonyongg@users.noreply.github.com> Date: Mon, 18 Aug 2025 01:06:49 +0900 Subject: [PATCH 2/3] Add tests for @Nullable parameter detection in Actuator endpoints - Add NullableParameterEndpoint test sample with @Nullable parameter - Add OptionalParameterEndpoint test sample for comparison - Add test to verify @Nullable and @OptionalParameter are treated equivalently - Ensures TYPE_USE annotation detection works correctly for optional parameters Signed-off-by: wonyongg <111210881+wonyongg@users.noreply.github.com> --- .../MetadataGenerationEnvironment.java | 1 + .../EndpointMetadataGenerationTests.java | 35 ++++++++++++++++++ .../endpoint/NullableParameterEndpoint.java | 36 +++++++++++++++++++ .../endpoint/OptionalParameterEndpoint.java | 36 +++++++++++++++++++ .../org/springframework/lang/Nullable.java | 35 ++++++++++++++++++ 5 files changed, 143 insertions(+) create mode 100644 configuration-metadata/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/endpoint/NullableParameterEndpoint.java create mode 100644 configuration-metadata/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/endpoint/OptionalParameterEndpoint.java create mode 100644 configuration-metadata/spring-boot-configuration-processor/src/test/java/org/springframework/lang/Nullable.java diff --git a/configuration-metadata/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/MetadataGenerationEnvironment.java b/configuration-metadata/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/MetadataGenerationEnvironment.java index 984e0b3f4a75..3e38358385d7 100644 --- a/configuration-metadata/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/MetadataGenerationEnvironment.java +++ b/configuration-metadata/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/MetadataGenerationEnvironment.java @@ -52,6 +52,7 @@ * @author Stephane Nicoll * @author Scott Frederick * @author Moritz Halbritter + * @author Wonyong Hwang */ class MetadataGenerationEnvironment { diff --git a/configuration-metadata/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/EndpointMetadataGenerationTests.java b/configuration-metadata/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/EndpointMetadataGenerationTests.java index 0cada46bc484..4058e7fae6e6 100644 --- a/configuration-metadata/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/EndpointMetadataGenerationTests.java +++ b/configuration-metadata/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/EndpointMetadataGenerationTests.java @@ -35,6 +35,8 @@ import org.springframework.boot.configurationsample.endpoint.SpecificEndpoint; import org.springframework.boot.configurationsample.endpoint.UnrestrictedAccessEndpoint; import org.springframework.boot.configurationsample.endpoint.incremental.IncrementalEndpoint; +import org.springframework.boot.configurationsample.endpoint.NullableParameterEndpoint; +import org.springframework.boot.configurationsample.endpoint.OptionalParameterEndpoint; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatRuntimeException; @@ -45,6 +47,7 @@ * @author Stephane Nicoll * @author Scott Frederick * @author Moritz Halbritter + * @author Wonyong Hwang */ class EndpointMetadataGenerationTests extends AbstractMetadataGenerationTests { @@ -192,6 +195,38 @@ void shouldFailIfEndpointWithSameIdButWithConflictingEnabledByDefaultSetting() { "Existing property 'management.endpoint.simple.access' from type org.springframework.boot.configurationsample.endpoint.SimpleEndpoint has a conflicting value. Existing value: unrestricted, new value from type org.springframework.boot.configurationsample.endpoint.SimpleEndpoint3: none"); } + @Test + void nullableParameterEndpoint() { + ConfigurationMetadata metadata = compile(NullableParameterEndpoint.class); + assertThat(metadata).has(Metadata.withGroup("management.endpoint.nullable").fromSource(NullableParameterEndpoint.class)); + assertThat(metadata).has(access("nullable", Access.UNRESTRICTED)); + assertThat(metadata).has(cacheTtl("nullable")); + assertThat(metadata.getItems()).hasSize(3); + } + + @Test + void optionalParameterEndpoint() { + ConfigurationMetadata metadata = compile(OptionalParameterEndpoint.class); + assertThat(metadata).has(Metadata.withGroup("management.endpoint.optional").fromSource(OptionalParameterEndpoint.class)); + assertThat(metadata).has(access("optional", Access.UNRESTRICTED)); + assertThat(metadata).has(cacheTtl("optional")); + assertThat(metadata.getItems()).hasSize(3); + } + + @Test + void nullableAndOptionalParameterEquivalence() { + ConfigurationMetadata nullableMetadata = compile(NullableParameterEndpoint.class); + ConfigurationMetadata optionalMetadata = compile(OptionalParameterEndpoint.class); + + assertThat(nullableMetadata.getItems()).hasSize(3); + assertThat(optionalMetadata.getItems()).hasSize(3); + + assertThat(nullableMetadata).has(access("nullable", Access.UNRESTRICTED)); + assertThat(optionalMetadata).has(access("optional", Access.UNRESTRICTED)); + assertThat(nullableMetadata).has(cacheTtl("nullable")); + assertThat(optionalMetadata).has(cacheTtl("optional")); + } + private Metadata.MetadataItemCondition access(String endpointId, Access defaultValue) { return defaultAccess(endpointId, endpointId, defaultValue); } diff --git a/configuration-metadata/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/endpoint/NullableParameterEndpoint.java b/configuration-metadata/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/endpoint/NullableParameterEndpoint.java new file mode 100644 index 000000000000..329f47fa5ea4 --- /dev/null +++ b/configuration-metadata/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/endpoint/NullableParameterEndpoint.java @@ -0,0 +1,36 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.configurationsample.endpoint; + +import org.springframework.boot.configurationsample.Endpoint; +import org.springframework.boot.configurationsample.ReadOperation; +import org.springframework.lang.Nullable; + +/** + * An endpoint with @Nullable parameter to test. + * + * @author Wonyong Hwang + */ +@Endpoint(id = "nullable") +public class NullableParameterEndpoint { + + @ReadOperation + public String invoke(@Nullable String parameter) { + return "test with " + parameter; + } + +} diff --git a/configuration-metadata/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/endpoint/OptionalParameterEndpoint.java b/configuration-metadata/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/endpoint/OptionalParameterEndpoint.java new file mode 100644 index 000000000000..96e92b8c4a0b --- /dev/null +++ b/configuration-metadata/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/endpoint/OptionalParameterEndpoint.java @@ -0,0 +1,36 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + package org.springframework.boot.configurationsample.endpoint; + + import org.springframework.boot.configurationsample.Endpoint; + import org.springframework.boot.configurationsample.ReadOperation; + import org.springframework.boot.configurationsample.OptionalParameter; + + /** + * An endpoint with @OptionalParameter to compare with @Nullable behavior. + * + * @author Wonyong Hwang + */ + @Endpoint(id = "optional") + public class OptionalParameterEndpoint { + + @ReadOperation + public String invoke(@OptionalParameter String parameter) { + return "test with " + parameter; + } + + } diff --git a/configuration-metadata/spring-boot-configuration-processor/src/test/java/org/springframework/lang/Nullable.java b/configuration-metadata/spring-boot-configuration-processor/src/test/java/org/springframework/lang/Nullable.java new file mode 100644 index 000000000000..695617e656e2 --- /dev/null +++ b/configuration-metadata/spring-boot-configuration-processor/src/test/java/org/springframework/lang/Nullable.java @@ -0,0 +1,35 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.lang; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Test-time equivalent of Spring Framework's @Nullable annotation. + * + * @author Wonyong Hwang + */ +@Target({ ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.TYPE_USE }) +@Retention(RetentionPolicy.SOURCE) +@Documented +public @interface Nullable { + +} From d5a8615e9d97981581a606358185df6a6a30b14a Mon Sep 17 00:00:00 2001 From: wonyongg <111210881+wonyongg@users.noreply.github.com> Date: Tue, 19 Aug 2025 01:13:11 +0900 Subject: [PATCH 3/3] Support jspecify @Nullable in configuration metadata processor and tests - Switch tests to org.jspecify.annotations.Nullable and remove local org.springframework.lang.Nullable - Recognize both org.springframework.lang.Nullable and org.jspecify.annotations.Nullable as optional in the processor (backward compatible) - Add org.jspecify:jspecify as testCompileOnly (no runtime impact) Signed-off-by: wonyongg <111210881+wonyongg@users.noreply.github.com> --- .../build.gradle | 1 + .../MetadataGenerationEnvironment.java | 11 ++++-- .../endpoint/NullableParameterEndpoint.java | 2 +- .../org/springframework/lang/Nullable.java | 35 ------------------- 4 files changed, 11 insertions(+), 38 deletions(-) delete mode 100644 configuration-metadata/spring-boot-configuration-processor/src/test/java/org/springframework/lang/Nullable.java diff --git a/configuration-metadata/spring-boot-configuration-processor/build.gradle b/configuration-metadata/spring-boot-configuration-processor/build.gradle index a71e6abbe6fa..0e3a12a78e0f 100644 --- a/configuration-metadata/spring-boot-configuration-processor/build.gradle +++ b/configuration-metadata/spring-boot-configuration-processor/build.gradle @@ -36,6 +36,7 @@ architectureCheck { dependencies { testCompileOnly("com.google.code.findbugs:jsr305:3.0.2") + testCompileOnly("org.jspecify:jspecify") testImplementation(enforcedPlatform(project(":platform:spring-boot-dependencies"))) testImplementation(project(":test-support:spring-boot-test-support")) diff --git a/configuration-metadata/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/MetadataGenerationEnvironment.java b/configuration-metadata/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/MetadataGenerationEnvironment.java index 3e38358385d7..8b11b6bade69 100644 --- a/configuration-metadata/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/MetadataGenerationEnvironment.java +++ b/configuration-metadata/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/MetadataGenerationEnvironment.java @@ -56,7 +56,9 @@ */ class MetadataGenerationEnvironment { - private static final String NULLABLE_ANNOTATION = "org.springframework.lang.Nullable"; + private static final Set NULLABLE_ANNOTATIONS = Set.of( + "org.springframework.lang.Nullable", + "org.jspecify.annotations.Nullable"); private static final Set TYPE_EXCLUDES = Set.of("com.zaxxer.hikari.IConnectionCustomizer", "groovy.lang.MetaClass", "groovy.text.markup.MarkupTemplateEngine", "java.io.Writer", "java.io.PrintWriter", @@ -375,7 +377,12 @@ AnnotationMirror getNameAnnotation(Element element) { } boolean hasNullableAnnotation(Element element) { - return getAnnotation(element, NULLABLE_ANNOTATION) != null; + for (String nullableAnnotation : NULLABLE_ANNOTATIONS) { + if (getAnnotation(element, nullableAnnotation) != null) { + return true; + } + } + return false; } boolean hasOptionalParameterAnnotation(Element element) { diff --git a/configuration-metadata/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/endpoint/NullableParameterEndpoint.java b/configuration-metadata/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/endpoint/NullableParameterEndpoint.java index 329f47fa5ea4..bff98e579458 100644 --- a/configuration-metadata/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/endpoint/NullableParameterEndpoint.java +++ b/configuration-metadata/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/endpoint/NullableParameterEndpoint.java @@ -18,7 +18,7 @@ import org.springframework.boot.configurationsample.Endpoint; import org.springframework.boot.configurationsample.ReadOperation; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * An endpoint with @Nullable parameter to test. diff --git a/configuration-metadata/spring-boot-configuration-processor/src/test/java/org/springframework/lang/Nullable.java b/configuration-metadata/spring-boot-configuration-processor/src/test/java/org/springframework/lang/Nullable.java deleted file mode 100644 index 695617e656e2..000000000000 --- a/configuration-metadata/spring-boot-configuration-processor/src/test/java/org/springframework/lang/Nullable.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2012-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.lang; - -import java.lang.annotation.Documented; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * Test-time equivalent of Spring Framework's @Nullable annotation. - * - * @author Wonyong Hwang - */ -@Target({ ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.TYPE_USE }) -@Retention(RetentionPolicy.SOURCE) -@Documented -public @interface Nullable { - -}