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 4b3e1ef320e2..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 @@ -52,10 +52,13 @@ * @author Stephane Nicoll * @author Scott Frederick * @author Moritz Halbritter + * @author Wonyong Hwang */ 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", @@ -265,6 +268,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; } @@ -368,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/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..bff98e579458 --- /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.jspecify.annotations.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; + } + + }