Replace Actuator @Nullable with @OptionalParameter

Closes gh-45389
This commit is contained in:
Phillip Webb
2025-05-07 14:01:22 -07:00
parent a0944b00d0
commit 36fb1e9b4b
28 changed files with 309 additions and 93 deletions

View File

@@ -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<AuditEvent> events = this.auditEventRepository.find(principal, getInstant(after), type);
return new AuditEventsDescriptor(events);
}

View File

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

View File

@@ -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<CacheEntryDescriptor> cache(@Selector String cache, @Nullable String cacheManager) {
public WebEndpointResponse<CacheEntryDescriptor> 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<Void> clearCache(@Selector String cache, @Nullable String cacheManager) {
public WebEndpointResponse<Void> 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);

View File

@@ -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<String> 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 <E extends Enum<E> & Producible<E>> List<String> getProducesFromProducible(
AnnotationAttributes annotationAttributes) {
Class<?> type = getProducesFrom(annotationAttributes);

View File

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

View File

@@ -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<Parameter> 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);
}
/**

View File

@@ -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<Parameter> 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<Parameter> 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 extends Annotation> T getAnnotation(Class<T> annotation) {
return this.parameter.getAnnotation(annotation);

View File

@@ -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<Parameter> 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<OperationParameter> getOperationParameters(Parameter[] parameters, String[] names) {
private List<OperationParameter> getOperationParameters(Parameter[] parameters, String[] names,
Predicate<Parameter> optionalParameters) {
List<OperationParameter> 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);
}

View File

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

View File

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

View File

@@ -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()) {

View File

@@ -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<Resource> heapDump(@Nullable Boolean live) {
public WebEndpointResponse<Resource> heapDump(@OptionalParameter Boolean live) {
try {
if (this.lock.tryLock(this.timeout, TimeUnit.MILLISECONDS)) {
try {

View File

@@ -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<String> tag) {
public MetricDescriptor metric(@Selector String requiredMetricName, @OptionalParameter List<String> tag) {
List<Tag> tags = parseTags(tag);
Collection<Meter> meters = findFirstMatchingMeters(this.registry, requiredMetricName, tags);
if (meters.isEmpty()) {

View File

@@ -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<byte[]> scrape(PrometheusOutputFormat format, @Nullable Set<String> includedNames) {
public WebEndpointResponse<byte[]> scrape(PrometheusOutputFormat format,
@OptionalParameter Set<String> includedNames) {
try {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream(this.nextMetricsScrapeSize);
MetricSnapshots metricSnapshots = (includedNames != null)

View File

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

View File

@@ -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<Parameter> 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<OperationParameter> 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());
}

View File

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

View File

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

View File

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

View File

@@ -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<T extends Configurable
}
@WriteOperation
void write(@Nullable String foo, @Nullable String bar) {
void write(@OptionalParameter String foo, @OptionalParameter String bar) {
this.endpointDelegate.write(foo, bar);
}
@@ -1167,7 +1167,7 @@ public abstract class AbstractWebEndpointIntegrationTests<T extends Configurable
static class RequiredParametersEndpoint {
@ReadOperation
String read(String foo, @Nullable String bar) {
String read(String foo, @OptionalParameter String bar) {
return foo;
}
@@ -1177,7 +1177,7 @@ public abstract class AbstractWebEndpointIntegrationTests<T extends Configurable
static class PrincipalEndpoint {
@ReadOperation
String read(@Nullable Principal principal) {
String read(@OptionalParameter Principal principal) {
return (principal != null) ? principal.getName() : "None";
}

View File

@@ -106,6 +106,8 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor
static final String READ_OPERATION_ANNOTATION = "org.springframework.boot.actuate.endpoint.annotation.ReadOperation";
static final String OPTIONAL_PARAMETER_ANNOTATION = "org.springframework.boot.actuate.endpoint.annotation.OptionalParameter";
static final String NAME_ANNOTATION = "org.springframework.boot.context.properties.bind.Name";
static final String ENDPOINT_ACCESS_ENUM = "org.springframework.boot.actuate.endpoint.Access";
@@ -155,6 +157,10 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor
return NAME_ANNOTATION;
}
protected String optionalParameterAnnotation() {
return OPTIONAL_PARAMETER_ANNOTATION;
}
protected String endpointAccessEnum() {
return ENDPOINT_ACCESS_ENUM;
}
@@ -177,7 +183,7 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor
this.metadataEnv = new MetadataGenerationEnvironment(env, configurationPropertiesAnnotation(),
nestedConfigurationPropertyAnnotation(), deprecatedConfigurationPropertyAnnotation(),
constructorBindingAnnotation(), autowiredAnnotation(), defaultValueAnnotation(), endpointAnnotations(),
readOperationAnnotation(), nameAnnotation());
readOperationAnnotation(), optionalParameterAnnotation(), nameAnnotation());
}
@Override
@@ -355,13 +361,18 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor
private boolean hasNoOrOptionalParameters(ExecutableElement method) {
for (VariableElement parameter : method.getParameters()) {
if (!this.metadataEnv.hasNullableAnnotation(parameter)) {
if (!isOptionalParameter(parameter)) {
return false;
}
}
return true;
}
private boolean isOptionalParameter(VariableElement parameter) {
return this.metadataEnv.hasNullableAnnotation(parameter)
|| this.metadataEnv.hasOptionalParameterAnnotation(parameter);
}
private String getPrefix(AnnotationMirror annotation) {
String prefix = this.metadataEnv.getAnnotationElementStringValue(annotation, "prefix");
if (prefix != null) {

View File

@@ -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.
@@ -91,6 +91,8 @@ class MetadataGenerationEnvironment {
private final String readOperationAnnotation;
private final String optionalParameterAnnotation;
private final String nameAnnotation;
private final String autowiredAnnotation;
@@ -98,7 +100,8 @@ class MetadataGenerationEnvironment {
MetadataGenerationEnvironment(ProcessingEnvironment environment, String configurationPropertiesAnnotation,
String nestedConfigurationPropertyAnnotation, String deprecatedConfigurationPropertyAnnotation,
String constructorBindingAnnotation, String autowiredAnnotation, String defaultValueAnnotation,
Set<String> endpointAnnotations, String readOperationAnnotation, String nameAnnotation) {
Set<String> 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);

View File

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

View File

@@ -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<ProcessingEnviron
TestConfigurationMetadataAnnotationProcessor.AUTOWIRED_ANNOTATION,
TestConfigurationMetadataAnnotationProcessor.DEFAULT_VALUE_ANNOTATION, endpointAnnotations,
TestConfigurationMetadataAnnotationProcessor.READ_OPERATION_ANNOTATION,
TestConfigurationMetadataAnnotationProcessor.OPTIONAL_PARAMETER_ANNOTATION,
TestConfigurationMetadataAnnotationProcessor.NAME_ANNOTATION);
}

View File

@@ -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.
@@ -72,6 +72,8 @@ public class TestConfigurationMetadataAnnotationProcessor extends ConfigurationM
public static final String READ_OPERATION_ANNOTATION = "org.springframework.boot.configurationsample.ReadOperation";
public static final String OPTIONAL_PARAMETER_ANNOTATION = "org.springframework.boot.configurationsample.OptionalParameter";
public static final String NAME_ANNOTATION = "org.springframework.boot.configurationsample.Name";
public static final String ENDPOINT_ACCESS_ENUM = "org.springframework.boot.configurationsample.Access";
@@ -120,6 +122,11 @@ public class TestConfigurationMetadataAnnotationProcessor extends ConfigurationM
return READ_OPERATION_ANNOTATION;
}
@Override
protected String optionalParameterAnnotation() {
return OPTIONAL_PARAMETER_ANNOTATION;
}
@Override
protected String nameAnnotation() {
return NAME_ANNOTATION;

View File

@@ -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.configurationsample;
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;
/**
* Alternative to Spring Boot's {@code @OptionalParameter} for testing (removes the need
* for a dependency on the real annotation).
*
* @author Phillip Webb
*/
@Target({ ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface OptionalParameter {
}

View File

@@ -16,9 +16,9 @@
package org.springframework.boot.configurationsample.endpoint;
import org.springframework.boot.configurationsample.OptionalParameter;
import org.springframework.boot.configurationsample.ReadOperation;
import org.springframework.boot.configurationsample.WebEndpoint;
import org.springframework.lang.Nullable;
/**
* A meta-annotated endpoint. Also with a package private read operation that has an
@@ -31,7 +31,7 @@ import org.springframework.lang.Nullable;
public class SpecificEndpoint {
@ReadOperation
String invoke(@Nullable String param) {
String invoke(@OptionalParameter String param) {
return "test";
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 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.
@@ -17,8 +17,8 @@
package org.springframework.boot.configurationsample.endpoint.incremental;
import org.springframework.boot.configurationsample.Endpoint;
import org.springframework.boot.configurationsample.OptionalParameter;
import org.springframework.boot.configurationsample.ReadOperation;
import org.springframework.lang.Nullable;
/**
* An endpoint that is enabled by default.
@@ -29,7 +29,7 @@ import org.springframework.lang.Nullable;
public class IncrementalEndpoint {
@ReadOperation
public String invoke(@Nullable String param) {
public String invoke(@OptionalParameter String param) {
return "test";
}