From 36fb1e9b4b0ebd55844cf1003819b15b198bdb14 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Wed, 7 May 2025 14:01:22 -0700 Subject: [PATCH] Replace Actuator @Nullable with @OptionalParameter Closes gh-45389 --- .../actuate/audit/AuditEventsEndpoint.java | 6 +- .../boot/actuate/cache/CachesEndpoint.java | 8 +-- .../cache/CachesEndpointWebExtension.java | 9 +-- .../annotation/DiscoveredOperationMethod.java | 8 ++- .../annotation/OptionalParameter.java | 36 ++++++++++++ .../invoke/reflect/OperationMethod.java | 19 +++++- .../reflect/OperationMethodParameter.java | 17 ++++-- .../reflect/OperationMethodParameters.java | 13 +++-- .../boot/actuate/env/EnvironmentEndpoint.java | 6 +- .../env/EnvironmentEndpointWebExtension.java | 6 +- .../boot/actuate/logging/LoggersEndpoint.java | 4 +- .../management/HeapDumpWebEndpoint.java | 4 +- .../boot/actuate/metrics/MetricsEndpoint.java | 6 +- .../prometheus/PrometheusScrapeEndpoint.java | 5 +- .../OperationMethodParameterTests.java | 58 +++++++++++++++---- .../OperationMethodParametersTests.java | 22 ++++--- .../invoke/reflect/OperationMethodTests.java | 16 +++-- .../ReflectiveOperationInvokerTests.java | 30 ++++++++-- .../CachingOperationInvokerAdvisorTests.java | 30 +++++++--- .../AbstractWebEndpointIntegrationTests.java | 10 ++-- ...figurationMetadataAnnotationProcessor.java | 15 ++++- .../MetadataGenerationEnvironment.java | 12 +++- .../EndpointMetadataGenerationTests.java | 4 +- .../MetadataGenerationEnvironmentFactory.java | 3 +- ...figurationMetadataAnnotationProcessor.java | 9 ++- .../OptionalParameter.java | 36 ++++++++++++ .../endpoint/SpecificEndpoint.java | 4 +- .../incremental/IncrementalEndpoint.java | 6 +- 28 files changed, 309 insertions(+), 93 deletions(-) create mode 100644 spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/OptionalParameter.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/OptionalParameter.java diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/audit/AuditEventsEndpoint.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/audit/AuditEventsEndpoint.java index cf001c1d44..c0e86c3662 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/audit/AuditEventsEndpoint.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/audit/AuditEventsEndpoint.java @@ -22,8 +22,8 @@ import java.util.List; import org.springframework.boot.actuate.endpoint.OperationResponseBody; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; +import org.springframework.boot.actuate.endpoint.annotation.OptionalParameter; import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -43,8 +43,8 @@ public class AuditEventsEndpoint { } @ReadOperation - public AuditEventsDescriptor events(@Nullable String principal, @Nullable OffsetDateTime after, - @Nullable String type) { + public AuditEventsDescriptor events(@OptionalParameter String principal, @OptionalParameter OffsetDateTime after, + @OptionalParameter String type) { List events = this.auditEventRepository.find(principal, getInstant(after), type); return new AuditEventsDescriptor(events); } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cache/CachesEndpoint.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cache/CachesEndpoint.java index 478b098a3b..f62e3fd81f 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cache/CachesEndpoint.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cache/CachesEndpoint.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2025 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. @@ -25,11 +25,11 @@ import java.util.function.Predicate; import org.springframework.boot.actuate.endpoint.OperationResponseBody; import org.springframework.boot.actuate.endpoint.annotation.DeleteOperation; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; +import org.springframework.boot.actuate.endpoint.annotation.OptionalParameter; import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; import org.springframework.boot.actuate.endpoint.annotation.Selector; import org.springframework.cache.Cache; import org.springframework.cache.CacheManager; -import org.springframework.lang.Nullable; /** * {@link Endpoint @Endpoint} to expose available {@link Cache caches}. @@ -79,7 +79,7 @@ public class CachesEndpoint { * {@code cacheManager} was provided to identify a unique candidate */ @ReadOperation - public CacheEntryDescriptor cache(@Selector String cache, @Nullable String cacheManager) { + public CacheEntryDescriptor cache(@Selector String cache, @OptionalParameter String cacheManager) { return extractUniqueCacheEntry(cache, getCacheEntries((name) -> name.equals(cache), isNameMatch(cacheManager))); } @@ -101,7 +101,7 @@ public class CachesEndpoint { * {@code cacheManager} was provided to identify a unique candidate */ @DeleteOperation - public boolean clearCache(@Selector String cache, @Nullable String cacheManager) { + public boolean clearCache(@Selector String cache, @OptionalParameter String cacheManager) { CacheEntryDescriptor entry = extractUniqueCacheEntry(cache, getCacheEntries((name) -> name.equals(cache), isNameMatch(cacheManager))); return (entry != null && clearCache(entry)); diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cache/CachesEndpointWebExtension.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cache/CachesEndpointWebExtension.java index f2e200be6c..d2007f402c 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cache/CachesEndpointWebExtension.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cache/CachesEndpointWebExtension.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2025 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. @@ -18,11 +18,11 @@ package org.springframework.boot.actuate.cache; import org.springframework.boot.actuate.cache.CachesEndpoint.CacheEntryDescriptor; import org.springframework.boot.actuate.endpoint.annotation.DeleteOperation; +import org.springframework.boot.actuate.endpoint.annotation.OptionalParameter; import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; import org.springframework.boot.actuate.endpoint.annotation.Selector; import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse; import org.springframework.boot.actuate.endpoint.web.annotation.EndpointWebExtension; -import org.springframework.lang.Nullable; /** * {@link EndpointWebExtension @EndpointWebExtension} for the {@link CachesEndpoint}. @@ -40,7 +40,8 @@ public class CachesEndpointWebExtension { } @ReadOperation - public WebEndpointResponse cache(@Selector String cache, @Nullable String cacheManager) { + public WebEndpointResponse cache(@Selector String cache, + @OptionalParameter String cacheManager) { try { CacheEntryDescriptor entry = this.delegate.cache(cache, cacheManager); int status = (entry != null) ? WebEndpointResponse.STATUS_OK : WebEndpointResponse.STATUS_NOT_FOUND; @@ -52,7 +53,7 @@ public class CachesEndpointWebExtension { } @DeleteOperation - public WebEndpointResponse clearCache(@Selector String cache, @Nullable String cacheManager) { + public WebEndpointResponse clearCache(@Selector String cache, @OptionalParameter String cacheManager) { try { boolean cleared = this.delegate.clearCache(cache, cacheManager); int status = (cleared ? WebEndpointResponse.STATUS_NO_CONTENT : WebEndpointResponse.STATUS_NOT_FOUND); diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/DiscoveredOperationMethod.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/DiscoveredOperationMethod.java index e72059365c..ea1f46c8ed 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/DiscoveredOperationMethod.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/DiscoveredOperationMethod.java @@ -17,6 +17,7 @@ package org.springframework.boot.actuate.endpoint.annotation; import java.lang.reflect.Method; +import java.lang.reflect.Parameter; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -26,6 +27,7 @@ import org.springframework.boot.actuate.endpoint.OperationType; import org.springframework.boot.actuate.endpoint.Producible; import org.springframework.boot.actuate.endpoint.invoke.reflect.OperationMethod; import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.core.annotation.MergedAnnotations; import org.springframework.util.Assert; /** @@ -40,7 +42,7 @@ public class DiscoveredOperationMethod extends OperationMethod { public DiscoveredOperationMethod(Method method, OperationType operationType, AnnotationAttributes annotationAttributes) { - super(method, operationType); + super(method, operationType, DiscoveredOperationMethod::isOptionalParameter); Assert.notNull(annotationAttributes, "'annotationAttributes' must not be null"); List producesMediaTypes = new ArrayList<>(); producesMediaTypes.addAll(Arrays.asList(annotationAttributes.getStringArray("produces"))); @@ -48,6 +50,10 @@ public class DiscoveredOperationMethod extends OperationMethod { this.producesMediaTypes = Collections.unmodifiableList(producesMediaTypes); } + private static boolean isOptionalParameter(Parameter parameter) { + return MergedAnnotations.from(parameter).isPresent(OptionalParameter.class); + } + private & Producible> List getProducesFromProducible( AnnotationAttributes annotationAttributes) { Class type = getProducesFrom(annotationAttributes); diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/OptionalParameter.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/OptionalParameter.java new file mode 100644 index 0000000000..9114c694d9 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/OptionalParameter.java @@ -0,0 +1,36 @@ +/* + * Copyright 2012-2025 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.actuate.endpoint.annotation; + +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; + +/** + * Annotation that indicates that an operation parameter is optional. + * + * @author Phillip Webb + * @since 4.0.0 + */ +@Target({ ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface OptionalParameter { + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoke/reflect/OperationMethod.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoke/reflect/OperationMethod.java index 441b4334c9..795f39ae81 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoke/reflect/OperationMethod.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoke/reflect/OperationMethod.java @@ -17,7 +17,9 @@ package org.springframework.boot.actuate.endpoint.invoke.reflect; import java.lang.reflect.Method; +import java.lang.reflect.Parameter; import java.util.Locale; +import java.util.function.Predicate; import org.springframework.boot.actuate.endpoint.OperationType; import org.springframework.boot.actuate.endpoint.invoke.OperationParameters; @@ -46,13 +48,28 @@ public class OperationMethod { * Create a new {@link OperationMethod} instance. * @param method the source method * @param operationType the operation type + * @deprecated since 4.0.0 for removal in 4.2.0 in favor of + * {@link #OperationMethod(Method, OperationType, Predicate)}p */ + @Deprecated(since = "4.0.0", forRemoval = true) public OperationMethod(Method method, OperationType operationType) { + this(method, operationType, (parameter) -> false); + } + + /** + * Create a new {@link OperationMethod} instance. + * @param method the source method + * @param operationType the operation type + * @param optionalParameters predicate to test if a parameter is optional + * @since 4.0.0 + */ + public OperationMethod(Method method, OperationType operationType, Predicate optionalParameters) { Assert.notNull(method, "'method' must not be null"); Assert.notNull(operationType, "'operationType' must not be null"); this.method = method; this.operationType = operationType; - this.operationParameters = new OperationMethodParameters(method, DEFAULT_PARAMETER_NAME_DISCOVERER); + this.operationParameters = new OperationMethodParameters(method, DEFAULT_PARAMETER_NAME_DISCOVERER, + optionalParameters); } /** diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoke/reflect/OperationMethodParameter.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoke/reflect/OperationMethodParameter.java index 6b3e038859..68f9492c4f 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoke/reflect/OperationMethodParameter.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoke/reflect/OperationMethodParameter.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2024 the original author or authors. + * Copyright 2012-2025 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. @@ -18,6 +18,7 @@ package org.springframework.boot.actuate.endpoint.invoke.reflect; import java.lang.annotation.Annotation; import java.lang.reflect.Parameter; +import java.util.function.Predicate; import javax.annotation.Nonnull; import javax.annotation.meta.When; @@ -27,7 +28,6 @@ import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.annotation.MergedAnnotations; import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; -import org.springframework.util.ObjectUtils; /** * {@link OperationParameter} created from an {@link OperationMethod}. @@ -43,14 +43,18 @@ class OperationMethodParameter implements OperationParameter { private final Parameter parameter; + private final Predicate optional; + /** * Create a new {@link OperationMethodParameter} instance. * @param name the parameter name * @param parameter the parameter + * @param optionalParameters predicate to test if a parameter is optional */ - OperationMethodParameter(String name, Parameter parameter) { + OperationMethodParameter(String name, Parameter parameter, Predicate optionalParameters) { this.name = name; this.parameter = parameter; + this.optional = optionalParameters; } @Override @@ -65,7 +69,7 @@ class OperationMethodParameter implements OperationParameter { @Override public boolean isMandatory() { - if (!ObjectUtils.isEmpty(this.parameter.getAnnotationsByType(Nullable.class))) { + if (isOptional()) { return false; } if (jsr305Present) { @@ -74,6 +78,11 @@ class OperationMethodParameter implements OperationParameter { return true; } + @SuppressWarnings("deprecation") + private boolean isOptional() { + return this.parameter.getAnnotationsByType(Nullable.class).length > 0 || this.optional.test(this.parameter); + } + @Override public T getAnnotation(Class annotation) { return this.parameter.getAnnotation(annotation); diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoke/reflect/OperationMethodParameters.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoke/reflect/OperationMethodParameters.java index 171b8ffb51..eccf871774 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoke/reflect/OperationMethodParameters.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoke/reflect/OperationMethodParameters.java @@ -22,6 +22,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.List; +import java.util.function.Predicate; import java.util.stream.Stream; import org.springframework.boot.actuate.endpoint.invoke.OperationParameter; @@ -42,20 +43,24 @@ class OperationMethodParameters implements OperationParameters { * Create a new {@link OperationMethodParameters} instance. * @param method the source method * @param parameterNameDiscoverer the parameter name discoverer + * @param optionalParameters predicate to test if a parameter is optional */ - OperationMethodParameters(Method method, ParameterNameDiscoverer parameterNameDiscoverer) { + OperationMethodParameters(Method method, ParameterNameDiscoverer parameterNameDiscoverer, + Predicate optionalParameters) { Assert.notNull(method, "'method' must not be null"); Assert.notNull(parameterNameDiscoverer, "'parameterNameDiscoverer' must not be null"); + Assert.notNull(optionalParameters, "'optionalParameters' must not be null"); String[] parameterNames = parameterNameDiscoverer.getParameterNames(method); Parameter[] parameters = method.getParameters(); Assert.state(parameterNames != null, () -> "Failed to extract parameter names for " + method); - this.operationParameters = getOperationParameters(parameters, parameterNames); + this.operationParameters = getOperationParameters(parameters, parameterNames, optionalParameters); } - private List getOperationParameters(Parameter[] parameters, String[] names) { + private List getOperationParameters(Parameter[] parameters, String[] names, + Predicate optionalParameters) { List operationParameters = new ArrayList<>(parameters.length); for (int i = 0; i < names.length; i++) { - operationParameters.add(new OperationMethodParameter(names[i], parameters[i])); + operationParameters.add(new OperationMethodParameter(names[i], parameters[i], optionalParameters)); } return Collections.unmodifiableList(operationParameters); } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/env/EnvironmentEndpoint.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/env/EnvironmentEndpoint.java index 348c9a6546..563789e6bf 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/env/EnvironmentEndpoint.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/env/EnvironmentEndpoint.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2024 the original author or authors. + * Copyright 2012-2025 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. @@ -33,6 +33,7 @@ import org.springframework.boot.actuate.endpoint.Sanitizer; import org.springframework.boot.actuate.endpoint.SanitizingFunction; import org.springframework.boot.actuate.endpoint.Show; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; +import org.springframework.boot.actuate.endpoint.annotation.OptionalParameter; import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; import org.springframework.boot.actuate.endpoint.annotation.Selector; import org.springframework.boot.context.properties.bind.PlaceholdersResolver; @@ -47,7 +48,6 @@ import org.springframework.core.env.Environment; import org.springframework.core.env.MutablePropertySources; import org.springframework.core.env.PropertySource; import org.springframework.core.env.StandardEnvironment; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; @@ -80,7 +80,7 @@ public class EnvironmentEndpoint { } @ReadOperation - public EnvironmentDescriptor environment(@Nullable String pattern) { + public EnvironmentDescriptor environment(@OptionalParameter String pattern) { boolean showUnsanitized = this.showValues.isShown(true); return getEnvironmentDescriptor(pattern, showUnsanitized); } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/env/EnvironmentEndpointWebExtension.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/env/EnvironmentEndpointWebExtension.java index 417c41bc7a..021ae2bc7d 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/env/EnvironmentEndpointWebExtension.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/env/EnvironmentEndpointWebExtension.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2025 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. @@ -20,13 +20,13 @@ import java.util.Set; import org.springframework.boot.actuate.endpoint.SecurityContext; import org.springframework.boot.actuate.endpoint.Show; +import org.springframework.boot.actuate.endpoint.annotation.OptionalParameter; import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; import org.springframework.boot.actuate.endpoint.annotation.Selector; import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse; import org.springframework.boot.actuate.endpoint.web.annotation.EndpointWebExtension; import org.springframework.boot.actuate.env.EnvironmentEndpoint.EnvironmentDescriptor; import org.springframework.boot.actuate.env.EnvironmentEndpoint.EnvironmentEntryDescriptor; -import org.springframework.lang.Nullable; /** * {@link EndpointWebExtension @EndpointWebExtension} for the {@link EnvironmentEndpoint}. @@ -51,7 +51,7 @@ public class EnvironmentEndpointWebExtension { } @ReadOperation - public EnvironmentDescriptor environment(SecurityContext securityContext, @Nullable String pattern) { + public EnvironmentDescriptor environment(SecurityContext securityContext, @OptionalParameter String pattern) { boolean showUnsanitized = this.showValues.isShown(securityContext, this.roles); return this.delegate.getEnvironmentDescriptor(pattern, showUnsanitized); } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/logging/LoggersEndpoint.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/logging/LoggersEndpoint.java index dc25b1cf7f..8971f075d1 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/logging/LoggersEndpoint.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/logging/LoggersEndpoint.java @@ -27,6 +27,7 @@ import java.util.TreeSet; import org.springframework.aot.hint.annotation.RegisterReflectionForBinding; import org.springframework.boot.actuate.endpoint.OperationResponseBody; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; +import org.springframework.boot.actuate.endpoint.annotation.OptionalParameter; import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; import org.springframework.boot.actuate.endpoint.annotation.Selector; import org.springframework.boot.actuate.endpoint.annotation.WriteOperation; @@ -39,7 +40,6 @@ import org.springframework.boot.logging.LoggerConfiguration.LevelConfiguration; import org.springframework.boot.logging.LoggerGroup; import org.springframework.boot.logging.LoggerGroups; import org.springframework.boot.logging.LoggingSystem; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -98,7 +98,7 @@ public class LoggersEndpoint { } @WriteOperation - public void configureLogLevel(@Selector String name, @Nullable LogLevel configuredLevel) { + public void configureLogLevel(@Selector String name, @OptionalParameter LogLevel configuredLevel) { Assert.notNull(name, "'name' must not be empty"); LoggerGroup group = this.loggerGroups.get(name); if (group != null && group.hasMembers()) { diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/management/HeapDumpWebEndpoint.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/management/HeapDumpWebEndpoint.java index 820e345059..edbc5d1cef 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/management/HeapDumpWebEndpoint.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/management/HeapDumpWebEndpoint.java @@ -37,12 +37,12 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; +import org.springframework.boot.actuate.endpoint.annotation.OptionalParameter; import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse; import org.springframework.boot.actuate.endpoint.web.annotation.WebEndpoint; import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.Resource; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ReflectionUtils; @@ -74,7 +74,7 @@ public class HeapDumpWebEndpoint { } @ReadOperation - public WebEndpointResponse heapDump(@Nullable Boolean live) { + public WebEndpointResponse heapDump(@OptionalParameter Boolean live) { try { if (this.lock.tryLock(this.timeout, TimeUnit.MILLISECONDS)) { try { diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/MetricsEndpoint.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/MetricsEndpoint.java index bd677ce400..97f649630d 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/MetricsEndpoint.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/MetricsEndpoint.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2025 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. @@ -36,9 +36,9 @@ import io.micrometer.core.instrument.composite.CompositeMeterRegistry; import org.springframework.boot.actuate.endpoint.InvalidEndpointRequestException; import org.springframework.boot.actuate.endpoint.OperationResponseBody; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; +import org.springframework.boot.actuate.endpoint.annotation.OptionalParameter; import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; import org.springframework.boot.actuate.endpoint.annotation.Selector; -import org.springframework.lang.Nullable; /** * An {@link Endpoint @Endpoint} for exposing the metrics held by a {@link MeterRegistry}. @@ -77,7 +77,7 @@ public class MetricsEndpoint { } @ReadOperation - public MetricDescriptor metric(@Selector String requiredMetricName, @Nullable List tag) { + public MetricDescriptor metric(@Selector String requiredMetricName, @OptionalParameter List tag) { List tags = parseTags(tag); Collection meters = findFirstMatchingMeters(this.registry, requiredMetricName, tags); if (meters.isEmpty()) { diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusScrapeEndpoint.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusScrapeEndpoint.java index 1f925949dd..d95718da12 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusScrapeEndpoint.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusScrapeEndpoint.java @@ -28,10 +28,10 @@ import io.prometheus.metrics.model.registry.PrometheusRegistry; import io.prometheus.metrics.model.snapshots.MetricSnapshots; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; +import org.springframework.boot.actuate.endpoint.annotation.OptionalParameter; import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse; import org.springframework.boot.actuate.endpoint.web.annotation.WebEndpoint; -import org.springframework.lang.Nullable; /** * {@link Endpoint @Endpoint} that outputs metrics in a format that can be scraped by the @@ -68,7 +68,8 @@ public class PrometheusScrapeEndpoint { } @ReadOperation(producesFrom = PrometheusOutputFormat.class) - public WebEndpointResponse scrape(PrometheusOutputFormat format, @Nullable Set includedNames) { + public WebEndpointResponse scrape(PrometheusOutputFormat format, + @OptionalParameter Set includedNames) { try { ByteArrayOutputStream outputStream = new ByteArrayOutputStream(this.nextMetricsScrapeSize); MetricSnapshots metricSnapshots = (includedNames != null) diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/invoke/reflect/OperationMethodParameterTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/invoke/reflect/OperationMethodParameterTests.java index 8b6e2aee83..36a58f00f3 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/invoke/reflect/OperationMethodParameterTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/invoke/reflect/OperationMethodParameterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2025 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. @@ -16,9 +16,13 @@ package org.springframework.boot.actuate.endpoint.invoke.reflect; +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; import java.lang.reflect.Method; +import java.lang.reflect.Parameter; import javax.annotation.Nonnull; import javax.annotation.meta.TypeQualifier; @@ -28,6 +32,7 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.actuate.endpoint.annotation.Selector; import org.springframework.boot.actuate.endpoint.annotation.Selector.Match; +import org.springframework.core.annotation.MergedAnnotations; import org.springframework.lang.Nullable; import org.springframework.util.ReflectionUtils; @@ -43,6 +48,9 @@ class OperationMethodParameterTests { private final Method example = ReflectionUtils.findMethod(getClass(), "example", String.class, String.class); + private final Method exampleSpringNullable = ReflectionUtils.findMethod(getClass(), "exampleSpringNullable", + String.class, String.class); + private final Method exampleJsr305 = ReflectionUtils.findMethod(getClass(), "exampleJsr305", String.class, String.class); @@ -56,59 +64,78 @@ class OperationMethodParameterTests { @Test void getNameShouldReturnName() { - OperationMethodParameter parameter = new OperationMethodParameter("name", this.example.getParameters()[0]); + OperationMethodParameter parameter = new OperationMethodParameter("name", this.example.getParameters()[0], + this::isOptionalParameter); assertThat(parameter.getName()).isEqualTo("name"); } @Test void getTypeShouldReturnType() { - OperationMethodParameter parameter = new OperationMethodParameter("name", this.example.getParameters()[0]); + OperationMethodParameter parameter = new OperationMethodParameter("name", this.example.getParameters()[0], + this::isOptionalParameter); assertThat(parameter.getType()).isEqualTo(String.class); } @Test void isMandatoryWhenNoAnnotationShouldReturnTrue() { - OperationMethodParameter parameter = new OperationMethodParameter("name", this.example.getParameters()[0]); + OperationMethodParameter parameter = new OperationMethodParameter("name", this.example.getParameters()[0], + this::isOptionalParameter); assertThat(parameter.isMandatory()).isTrue(); } @Test - void isMandatoryWhenNullableAnnotationShouldReturnFalse() { - OperationMethodParameter parameter = new OperationMethodParameter("name", this.example.getParameters()[1]); + void isMandatoryWhenOptionalAnnotationShouldReturnFalse() { + OperationMethodParameter parameter = new OperationMethodParameter("name", this.example.getParameters()[1], + this::isOptionalParameter); + assertThat(parameter.isMandatory()).isFalse(); + } + + @Test + void isMandatoryWhenSpringNullableAnnotationShouldReturnFalse() { + OperationMethodParameter parameter = new OperationMethodParameter("name", + this.exampleSpringNullable.getParameters()[1], this::isOptionalParameter); assertThat(parameter.isMandatory()).isFalse(); } @Test void isMandatoryWhenJsrNullableAnnotationShouldReturnFalse() { - OperationMethodParameter parameter = new OperationMethodParameter("name", - this.exampleJsr305.getParameters()[1]); + OperationMethodParameter parameter = new OperationMethodParameter("name", this.exampleJsr305.getParameters()[1], + this::isOptionalParameter); assertThat(parameter.isMandatory()).isFalse(); } @Test void isMandatoryWhenJsrMetaNullableAnnotationShouldReturnFalse() { OperationMethodParameter parameter = new OperationMethodParameter("name", - this.exampleMetaJsr305.getParameters()[1]); + this.exampleMetaJsr305.getParameters()[1], this::isOptionalParameter); assertThat(parameter.isMandatory()).isFalse(); } @Test void isMandatoryWhenJsrNonnullAnnotationShouldReturnTrue() { OperationMethodParameter parameter = new OperationMethodParameter("name", - this.exampleJsr305NonNull.getParameters()[1]); + this.exampleJsr305NonNull.getParameters()[1], this::isOptionalParameter); assertThat(parameter.isMandatory()).isTrue(); } @Test void getAnnotationShouldReturnAnnotation() { OperationMethodParameter parameter = new OperationMethodParameter("name", - this.exampleAnnotation.getParameters()[0]); + this.exampleAnnotation.getParameters()[0], this::isOptionalParameter); Selector annotation = parameter.getAnnotation(Selector.class); assertThat(annotation).isNotNull(); assertThat(annotation.match()).isEqualTo(Match.ALL_REMAINING); } - void example(String one, @Nullable String two) { + private boolean isOptionalParameter(Parameter parameter) { + return MergedAnnotations.from(parameter).isPresent(TestOptional.class); + } + + void example(String one, @TestOptional String two) { + } + + @SuppressWarnings("deprecation") + void exampleSpringNullable(String one, @Nullable String two) { } void exampleJsr305(String one, @javax.annotation.Nullable String two) { @@ -130,4 +157,11 @@ class OperationMethodParameterTests { } + @Target({ ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD }) + @Retention(RetentionPolicy.RUNTIME) + @Documented + public @interface TestOptional { + + } + } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/invoke/reflect/OperationMethodParametersTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/invoke/reflect/OperationMethodParametersTests.java index 8c2d7ebf11..62485c9b90 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/invoke/reflect/OperationMethodParametersTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/invoke/reflect/OperationMethodParametersTests.java @@ -17,10 +17,12 @@ package org.springframework.boot.actuate.endpoint.invoke.reflect; import java.lang.reflect.Method; +import java.lang.reflect.Parameter; import java.util.Iterator; import java.util.List; import java.util.Spliterator; import java.util.Spliterators; +import java.util.function.Predicate; import java.util.stream.Stream; import java.util.stream.StreamSupport; @@ -43,6 +45,8 @@ import static org.mockito.Mockito.mock; */ class OperationMethodParametersTests { + private static final Predicate NON_OPTIONAL = (parameter) -> false; + private final Method exampleMethod = ReflectionUtils.findMethod(getClass(), "example", String.class); private final Method exampleNoParamsMethod = ReflectionUtils.findMethod(getClass(), "exampleNoParams"); @@ -50,48 +54,50 @@ class OperationMethodParametersTests { @Test void createWhenMethodIsNullShouldThrowException() { assertThatIllegalArgumentException() - .isThrownBy(() -> new OperationMethodParameters(null, mock(ParameterNameDiscoverer.class))) + .isThrownBy(() -> new OperationMethodParameters(null, mock(ParameterNameDiscoverer.class), NON_OPTIONAL)) .withMessageContaining("'method' must not be null"); } @Test void createWhenParameterNameDiscovererIsNullShouldThrowException() { - assertThatIllegalArgumentException().isThrownBy(() -> new OperationMethodParameters(this.exampleMethod, null)) + assertThatIllegalArgumentException() + .isThrownBy(() -> new OperationMethodParameters(this.exampleMethod, null, NON_OPTIONAL)) .withMessageContaining("'parameterNameDiscoverer' must not be null"); } @Test void createWhenParameterNameDiscovererReturnsNullShouldThrowException() { assertThatIllegalStateException() - .isThrownBy(() -> new OperationMethodParameters(this.exampleMethod, mock(ParameterNameDiscoverer.class))) + .isThrownBy(() -> new OperationMethodParameters(this.exampleMethod, mock(ParameterNameDiscoverer.class), + NON_OPTIONAL)) .withMessageContaining("Failed to extract parameter names"); } @Test void hasParametersWhenHasParametersShouldReturnTrue() { OperationMethodParameters parameters = new OperationMethodParameters(this.exampleMethod, - new DefaultParameterNameDiscoverer()); + new DefaultParameterNameDiscoverer(), NON_OPTIONAL); assertThat(parameters.hasParameters()).isTrue(); } @Test void hasParametersWhenHasNoParametersShouldReturnFalse() { OperationMethodParameters parameters = new OperationMethodParameters(this.exampleNoParamsMethod, - new DefaultParameterNameDiscoverer()); + new DefaultParameterNameDiscoverer(), NON_OPTIONAL); assertThat(parameters.hasParameters()).isFalse(); } @Test void getParameterCountShouldReturnParameterCount() { OperationMethodParameters parameters = new OperationMethodParameters(this.exampleMethod, - new DefaultParameterNameDiscoverer()); + new DefaultParameterNameDiscoverer(), NON_OPTIONAL); assertThat(parameters.getParameterCount()).isOne(); } @Test void iteratorShouldIterateOperationParameters() { OperationMethodParameters parameters = new OperationMethodParameters(this.exampleMethod, - new DefaultParameterNameDiscoverer()); + new DefaultParameterNameDiscoverer(), NON_OPTIONAL); Iterator iterator = parameters.iterator(); assertParameters( StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator, Spliterator.ORDERED), false)); @@ -100,7 +106,7 @@ class OperationMethodParametersTests { @Test void streamShouldStreamOperationParameters() { OperationMethodParameters parameters = new OperationMethodParameters(this.exampleMethod, - new DefaultParameterNameDiscoverer()); + new DefaultParameterNameDiscoverer(), NON_OPTIONAL); assertParameters(parameters.stream()); } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/invoke/reflect/OperationMethodTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/invoke/reflect/OperationMethodTests.java index bc4856fbd2..15165f9af1 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/invoke/reflect/OperationMethodTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/invoke/reflect/OperationMethodTests.java @@ -17,6 +17,8 @@ package org.springframework.boot.actuate.endpoint.invoke.reflect; import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.util.function.Predicate; import org.junit.jupiter.api.Test; @@ -34,35 +36,39 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException */ class OperationMethodTests { + private static final Predicate NON_OPTIONAL = (parameter) -> false; + private final Method exampleMethod = ReflectionUtils.findMethod(getClass(), "example", String.class); @Test void createWhenMethodIsNullShouldThrowException() { - assertThatIllegalArgumentException().isThrownBy(() -> new OperationMethod(null, OperationType.READ)) + assertThatIllegalArgumentException() + .isThrownBy(() -> new OperationMethod(null, OperationType.READ, NON_OPTIONAL)) .withMessageContaining("'method' must not be null"); } @Test void createWhenOperationTypeIsNullShouldThrowException() { - assertThatIllegalArgumentException().isThrownBy(() -> new OperationMethod(this.exampleMethod, null)) + assertThatIllegalArgumentException() + .isThrownBy(() -> new OperationMethod(this.exampleMethod, null, NON_OPTIONAL)) .withMessageContaining("'operationType' must not be null"); } @Test void getMethodShouldReturnMethod() { - OperationMethod operationMethod = new OperationMethod(this.exampleMethod, OperationType.READ); + OperationMethod operationMethod = new OperationMethod(this.exampleMethod, OperationType.READ, NON_OPTIONAL); assertThat(operationMethod.getMethod()).isEqualTo(this.exampleMethod); } @Test void getOperationTypeShouldReturnOperationType() { - OperationMethod operationMethod = new OperationMethod(this.exampleMethod, OperationType.READ); + OperationMethod operationMethod = new OperationMethod(this.exampleMethod, OperationType.READ, NON_OPTIONAL); assertThat(operationMethod.getOperationType()).isEqualTo(OperationType.READ); } @Test void getParametersShouldReturnParameters() { - OperationMethod operationMethod = new OperationMethod(this.exampleMethod, OperationType.READ); + OperationMethod operationMethod = new OperationMethod(this.exampleMethod, OperationType.READ, NON_OPTIONAL); OperationParameters parameters = operationMethod.getParameters(); assertThat(parameters.getParameterCount()).isOne(); assertThat(parameters.iterator().next().getName()).isEqualTo("name"); diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/invoke/reflect/ReflectiveOperationInvokerTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/invoke/reflect/ReflectiveOperationInvokerTests.java index a59cbabd6b..a1b45b1712 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/invoke/reflect/ReflectiveOperationInvokerTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/invoke/reflect/ReflectiveOperationInvokerTests.java @@ -16,6 +16,12 @@ package org.springframework.boot.actuate.endpoint.invoke.reflect; +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; +import java.lang.reflect.Parameter; import java.util.Collections; import org.junit.jupiter.api.BeforeEach; @@ -27,7 +33,7 @@ import org.springframework.boot.actuate.endpoint.OperationType; import org.springframework.boot.actuate.endpoint.SecurityContext; import org.springframework.boot.actuate.endpoint.invoke.MissingParametersException; import org.springframework.boot.actuate.endpoint.invoke.ParameterValueMapper; -import org.springframework.lang.Nullable; +import org.springframework.core.annotation.MergedAnnotations; import org.springframework.util.ReflectionUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -52,7 +58,7 @@ class ReflectiveOperationInvokerTests { void setup() { this.target = new Example(); this.operationMethod = new OperationMethod(ReflectionUtils.findMethod(Example.class, "reverse", - ApiVersion.class, SecurityContext.class, String.class), OperationType.READ); + ApiVersion.class, SecurityContext.class, String.class), OperationType.READ, this::isOptional); this.parameterValueMapper = (parameter, value) -> (value != null) ? value.toString() : null; } @@ -87,7 +93,7 @@ class ReflectiveOperationInvokerTests { } @Test - void invokeWhenMissingNonNullableArgumentShouldThrowException() { + void invokeWhenMissingNonOptionalArgumentShouldThrowException() { ReflectiveOperationInvoker invoker = new ReflectiveOperationInvoker(this.target, this.operationMethod, this.parameterValueMapper); assertThatExceptionOfType(MissingParametersException.class).isThrownBy(() -> invoker @@ -95,9 +101,10 @@ class ReflectiveOperationInvokerTests { } @Test - void invokeWhenMissingNullableArgumentShouldInvoke() { + void invokeWhenMissingOptionalArgumentShouldInvoke() { OperationMethod operationMethod = new OperationMethod(ReflectionUtils.findMethod(Example.class, - "reverseNullable", ApiVersion.class, SecurityContext.class, String.class), OperationType.READ); + "reverseOptional", ApiVersion.class, SecurityContext.class, String.class), OperationType.READ, + this::isOptional); ReflectiveOperationInvoker invoker = new ReflectiveOperationInvoker(this.target, operationMethod, this.parameterValueMapper); Object result = invoker @@ -114,6 +121,10 @@ class ReflectiveOperationInvokerTests { assertThat(result).isEqualTo("4321"); } + private boolean isOptional(Parameter parameter) { + return MergedAnnotations.from(parameter).isPresent(TestOptional.class); + } + static class Example { String reverse(ApiVersion apiVersion, SecurityContext securityContext, String name) { @@ -122,7 +133,7 @@ class ReflectiveOperationInvokerTests { return new StringBuilder(name).reverse().toString(); } - String reverseNullable(ApiVersion apiVersion, SecurityContext securityContext, @Nullable String name) { + String reverseOptional(ApiVersion apiVersion, SecurityContext securityContext, @TestOptional String name) { assertThat(apiVersion).isEqualTo(ApiVersion.LATEST); assertThat(securityContext).isNotNull(); return new StringBuilder(String.valueOf(name)).reverse().toString(); @@ -130,4 +141,11 @@ class ReflectiveOperationInvokerTests { } + @Target({ ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD }) + @Retention(RetentionPolicy.RUNTIME) + @Documented + public @interface TestOptional { + + } + } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/invoker/cache/CachingOperationInvokerAdvisorTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/invoker/cache/CachingOperationInvokerAdvisorTests.java index 63b4d50226..12deaa0dbf 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/invoker/cache/CachingOperationInvokerAdvisorTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/invoker/cache/CachingOperationInvokerAdvisorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2025 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. @@ -16,6 +16,11 @@ package org.springframework.boot.actuate.endpoint.invoker.cache; +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; import java.lang.reflect.Method; import java.util.function.Function; @@ -33,7 +38,7 @@ import org.springframework.boot.actuate.endpoint.invoke.OperationInvoker; import org.springframework.boot.actuate.endpoint.invoke.OperationParameters; import org.springframework.boot.actuate.endpoint.invoke.reflect.OperationMethod; import org.springframework.boot.actuate.endpoint.web.WebServerNamespace; -import org.springframework.lang.Nullable; +import org.springframework.core.annotation.MergedAnnotations; import org.springframework.util.ReflectionUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -158,32 +163,34 @@ class CachingOperationInvokerAdvisorTests { private OperationMethod getOperationMethod(String methodName, Class... parameterTypes) { Method method = ReflectionUtils.findMethod(TestOperations.class, methodName, parameterTypes); - return new OperationMethod(method, OperationType.READ); + return new OperationMethod(method, OperationType.READ, + (parameter) -> MergedAnnotations.from(parameter).isPresent(TestOptional.class)); } + @SuppressWarnings("deprecation") static class TestOperations { String get() { return ""; } - String getWithParameters(@Nullable String foo, String bar) { + String getWithParameters(@TestOptional String foo, String bar) { return ""; } - String getWithAllOptionalParameters(@Nullable String foo, @Nullable String bar) { + String getWithAllOptionalParameters(@TestOptional String foo, @TestOptional String bar) { return ""; } - String getWithSecurityContext(SecurityContext securityContext, @Nullable String bar) { + String getWithSecurityContext(SecurityContext securityContext, @TestOptional String bar) { return ""; } - String getWithApiVersion(ApiVersion apiVersion, @Nullable String bar) { + String getWithApiVersion(ApiVersion apiVersion, @TestOptional String bar) { return ""; } - String getWithServerNamespace(WebServerNamespace serverNamespace, @Nullable String bar) { + String getWithServerNamespace(WebServerNamespace serverNamespace, @TestOptional String bar) { return ""; } @@ -193,4 +200,11 @@ class CachingOperationInvokerAdvisorTests { } + @Target({ ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD }) + @Retention(RetentionPolicy.RUNTIME) + @Documented + public @interface TestOptional { + + } + } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/annotation/AbstractWebEndpointIntegrationTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/annotation/AbstractWebEndpointIntegrationTests.java index dbdf5569ca..9215ad9eda 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/annotation/AbstractWebEndpointIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/annotation/AbstractWebEndpointIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2024 the original author or authors. + * Copyright 2012-2025 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. @@ -34,6 +34,7 @@ import reactor.core.publisher.Mono; import org.springframework.boot.actuate.endpoint.SecurityContext; import org.springframework.boot.actuate.endpoint.annotation.DeleteOperation; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; +import org.springframework.boot.actuate.endpoint.annotation.OptionalParameter; import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; import org.springframework.boot.actuate.endpoint.annotation.Selector; import org.springframework.boot.actuate.endpoint.annotation.Selector.Match; @@ -51,7 +52,6 @@ import org.springframework.core.io.ByteArrayResource; import org.springframework.core.io.Resource; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; -import org.springframework.lang.Nullable; import org.springframework.test.web.reactive.server.WebTestClient; import org.springframework.util.StringUtils; @@ -982,7 +982,7 @@ public abstract class AbstractWebEndpointIntegrationTests endpointAnnotations, String readOperationAnnotation, String nameAnnotation) { + Set endpointAnnotations, String readOperationAnnotation, String optionalParameterAnnotation, + String nameAnnotation) { this.typeUtils = new TypeUtils(environment); this.elements = environment.getElementUtils(); this.messager = environment.getMessager(); @@ -111,6 +114,7 @@ class MetadataGenerationEnvironment { this.defaultValueAnnotation = defaultValueAnnotation; this.endpointAnnotations = endpointAnnotations; this.readOperationAnnotation = readOperationAnnotation; + this.optionalParameterAnnotation = optionalParameterAnnotation; this.nameAnnotation = nameAnnotation; } @@ -337,6 +341,10 @@ class MetadataGenerationEnvironment { return getAnnotation(element, NULLABLE_ANNOTATION) != null; } + boolean hasOptionalParameterAnnotation(Element element) { + return getAnnotation(element, this.optionalParameterAnnotation) != null; + } + private boolean isElementDeprecated(Element element) { return hasAnnotation(element, "java.lang.Deprecated") || hasAnnotation(element, this.deprecatedConfigurationPropertyAnnotation); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/EndpointMetadataGenerationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/EndpointMetadataGenerationTests.java index 76c64f8627..ecde354bb4 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/EndpointMetadataGenerationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/EndpointMetadataGenerationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2024 the original author or authors. + * Copyright 2012-2025 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. @@ -171,7 +171,7 @@ class EndpointMetadataGenerationTests extends AbstractMetadataGenerationTests { assertThat(metadata).has(access("incremental", Access.UNRESTRICTED)); assertThat(metadata).has(cacheTtl("incremental")); assertThat(metadata.getItems()).hasSize(4); - project.replaceText(IncrementalEndpoint.class, "@Nullable String param", "String param"); + project.replaceText(IncrementalEndpoint.class, "@OptionalParameter String param", "String param"); metadata = project.compile(); assertThat(metadata) .has(Metadata.withGroup("management.endpoint.incremental").fromSource(IncrementalEndpoint.class)); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/MetadataGenerationEnvironmentFactory.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/MetadataGenerationEnvironmentFactory.java index 50a3714530..8a171d271e 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/MetadataGenerationEnvironmentFactory.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/MetadataGenerationEnvironmentFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2025 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. @@ -48,6 +48,7 @@ class MetadataGenerationEnvironmentFactory implements Function