Revert "Only support @OptionalParameter annotation with endpoint methods"

This reverts commit 450eb48303.

See gh-47136
This commit is contained in:
Stéphane Nicoll
2025-09-10 14:31:35 +02:00
parent d0c742bc7b
commit ee83ff5f6a
12 changed files with 143 additions and 86 deletions
@@ -34,7 +34,7 @@ import org.springframework.util.Assert;
* @since 2.0.0
* @see ReflectiveOperationInvoker
*/
public abstract class OperationMethod {
public class OperationMethod {
private static final ParameterNameDiscoverer DEFAULT_PARAMETER_NAME_DISCOVERER = new DefaultParameterNameDiscoverer();
@@ -44,6 +44,18 @@ public abstract class OperationMethod {
private final OperationParameters operationParameters;
/**
* 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)}
*/
@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
@@ -51,7 +63,7 @@ public abstract class OperationMethod {
* @param optionalParameters predicate to test if a parameter is optional
* @since 4.0.0
*/
protected OperationMethod(Method method, OperationType operationType, Predicate<Parameter> optionalParameters) {
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;
@@ -21,6 +21,7 @@ import java.lang.reflect.Parameter;
import java.util.function.Predicate;
import org.springframework.boot.actuate.endpoint.invoke.OperationParameter;
import org.springframework.core.Nullness;
/**
* {@link OperationParameter} created from an {@link OperationMethod}.
@@ -60,7 +61,11 @@ class OperationMethodParameter implements OperationParameter {
@Override
public boolean isMandatory() {
return !this.optional.test(this.parameter);
return !isOptional();
}
private boolean isOptional() {
return Nullness.NULLABLE == Nullness.forParameter(this.parameter) || this.optional.test(this.parameter);
}
@Override
@@ -43,6 +43,12 @@ class OperationMethodParameterTests {
private final Method example = ReflectionUtils.findMethod(getClass(), "example", String.class, String.class);
private final Method exampleJSpecifyNullable = ReflectionUtils.findMethod(getClass(), "exampleJSpecifyNullable",
String.class, String.class);
private final Method exampleSpringNullable = ReflectionUtils.findMethod(getClass(), "exampleSpringNullable",
String.class, String.class);
private Method exampleAnnotation = ReflectionUtils.findMethod(getClass(), "exampleAnnotation", String.class);
@Test
@@ -73,6 +79,20 @@ class OperationMethodParameterTests {
assertThat(parameter.isMandatory()).isFalse();
}
@Test
void isMandatoryWhenJSpecifyNullableAnnotationShouldReturnFalse() {
OperationMethodParameter parameter = new OperationMethodParameter("name",
this.exampleJSpecifyNullable.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 getAnnotationShouldReturnAnnotation() {
OperationMethodParameter parameter = new OperationMethodParameter("name",
@@ -89,6 +109,13 @@ class OperationMethodParameterTests {
void example(String one, @TestOptional String two) {
}
void exampleJSpecifyNullable(String one, @org.jspecify.annotations.Nullable String two) {
}
@SuppressWarnings("deprecation")
void exampleSpringNullable(String one, @org.springframework.lang.Nullable String two) {
}
void exampleAnnotation(@Selector(match = Match.ALL_REMAINING) String allRemaining) {
}
@@ -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 TestOperationMethod(null, OperationType.READ))
assertThatIllegalArgumentException()
.isThrownBy(() -> new OperationMethod(null, OperationType.READ, NON_OPTIONAL))
.withMessageContaining("'method' must not be null");
}
@Test
void createWhenOperationTypeIsNullShouldThrowException() {
assertThatIllegalArgumentException().isThrownBy(() -> new TestOperationMethod(this.exampleMethod, null))
assertThatIllegalArgumentException()
.isThrownBy(() -> new OperationMethod(this.exampleMethod, null, NON_OPTIONAL))
.withMessageContaining("'operationType' must not be null");
}
@Test
void getMethodShouldReturnMethod() {
OperationMethod operationMethod = new TestOperationMethod(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 TestOperationMethod(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 TestOperationMethod(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");
@@ -57,7 +57,7 @@ class ReflectiveOperationInvokerTests {
@BeforeEach
void setup() {
this.target = new Example();
this.operationMethod = new TestOperationMethod(ReflectionUtils.findMethod(Example.class, "reverse",
this.operationMethod = new OperationMethod(ReflectionUtils.findMethod(Example.class, "reverse",
ApiVersion.class, SecurityContext.class, String.class), OperationType.READ, this::isOptional);
this.parameterValueMapper = (parameter, value) -> (value != null) ? value.toString() : null;
}
@@ -102,7 +102,7 @@ class ReflectiveOperationInvokerTests {
@Test
void invokeWhenMissingOptionalArgumentShouldInvoke() {
OperationMethod operationMethod = new TestOperationMethod(ReflectionUtils.findMethod(Example.class,
OperationMethod operationMethod = new OperationMethod(ReflectionUtils.findMethod(Example.class,
"reverseOptional", ApiVersion.class, SecurityContext.class, String.class), OperationType.READ,
this::isOptional);
ReflectiveOperationInvoker invoker = new ReflectiveOperationInvoker(this.target, operationMethod,
@@ -1,42 +0,0 @@
/*
* Copyright 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.endpoint.invoke.reflect;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.function.Predicate;
import org.springframework.boot.actuate.endpoint.OperationType;
/**
* Test {@link OperationMethod}.
*
* @author Phillip Webb
*/
public class TestOperationMethod extends OperationMethod {
public static final Predicate<Parameter> NON_OPTIONAL = (parameter) -> false;
public TestOperationMethod(Method method, OperationType operationType) {
this(method, operationType, NON_OPTIONAL);
}
public TestOperationMethod(Method method, OperationType operationType, Predicate<Parameter> optionalParameters) {
super(method, operationType, optionalParameters);
}
}
@@ -37,7 +37,6 @@ import org.springframework.boot.actuate.endpoint.SecurityContext;
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.invoke.reflect.TestOperationMethod;
import org.springframework.boot.actuate.endpoint.web.WebServerNamespace;
import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.util.ReflectionUtils;
@@ -164,7 +163,7 @@ class CachingOperationInvokerAdvisorTests {
private OperationMethod getOperationMethod(String methodName, Class<?>... parameterTypes) {
Method method = ReflectionUtils.findMethod(TestOperations.class, methodName, parameterTypes);
return new TestOperationMethod(method, OperationType.READ,
return new OperationMethod(method, OperationType.READ,
(parameter) -> MergedAnnotations.from(parameter).isPresent(TestOptional.class));
}