Support additional nullness signal for Actuator endpoints

This commit expands the detection of optional parameters for Actuator
Endpoints. Before this commit, JSpecify's `@Nullable` annotation was
not detected.

See gh-46854

Signed-off-by: wonyongg <111210881+wonyongg@users.noreply.github.com>
This commit is contained in:
wonyongg 2025-08-17 04:44:31 +09:00 committed by Stéphane Nicoll
parent 088ef836c1
commit 75bcc2e118
5 changed files with 124 additions and 2 deletions

View File

@ -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"))

View File

@ -52,10 +52,13 @@ import org.springframework.boot.configurationprocessor.metadata.ItemDeprecation;
* @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<String> NULLABLE_ANNOTATIONS = Set.of(
"org.springframework.lang.Nullable",
"org.jspecify.annotations.Nullable");
private static final Set<String> 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 @@ class MetadataGenerationEnvironment {
return annotation;
}
}
for (AnnotationMirror annotation : element.asType().getAnnotationMirrors()) {
if (type.equals(annotation.getAnnotationType().toString())) {
return annotation;
}
}
}
return null;
}
@ -368,7 +377,12 @@ class MetadataGenerationEnvironment {
}
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) {

View File

@ -35,6 +35,8 @@ import org.springframework.boot.configurationsample.endpoint.SimpleEndpoint3;
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 @@ import static org.assertj.core.api.Assertions.assertThatRuntimeException;
* @author Stephane Nicoll
* @author Scott Frederick
* @author Moritz Halbritter
* @author Wonyong Hwang
*/
class EndpointMetadataGenerationTests extends AbstractMetadataGenerationTests {
@ -192,6 +195,38 @@ class EndpointMetadataGenerationTests extends AbstractMetadataGenerationTests {
"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);
}

View File

@ -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;
}
}

View File

@ -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;
}
}