diff --git a/spring-web/src/main/java/org/springframework/web/service/annotation/HeadExchange.java b/spring-web/src/main/java/org/springframework/web/service/annotation/HeadExchange.java
deleted file mode 100644
index a1034fb83f..0000000000
--- a/spring-web/src/main/java/org/springframework/web/service/annotation/HeadExchange.java
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * Copyright 2002-2022 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.web.service.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;
-
-import org.springframework.core.annotation.AliasFor;
-
-/**
- * Shortcut for {@link HttpExchange @HttpExchange} for HTTP HEAD requests.
- *
- * @author Rossen Stoyanchev
- * @since 6.0
- */
-@Target(ElementType.METHOD)
-@Retention(RetentionPolicy.RUNTIME)
-@Documented
-@HttpExchange(method = "HEAD")
-public @interface HeadExchange {
-
- /**
- * Alias for {@link HttpExchange#value}.
- */
- @AliasFor(annotation = HttpExchange.class)
- String value() default "";
-
- /**
- * Alias for {@link HttpExchange#url()}.
- */
- @AliasFor(annotation = HttpExchange.class)
- String url() default "";
-
- /**
- * Alias for {@link HttpExchange#accept()}.
- */
- @AliasFor(annotation = HttpExchange.class)
- String[] accept() default {};
-
-}
diff --git a/spring-web/src/main/java/org/springframework/web/service/annotation/HttpExchange.java b/spring-web/src/main/java/org/springframework/web/service/annotation/HttpExchange.java
index 5cd8016712..58d6b8cae6 100644
--- a/spring-web/src/main/java/org/springframework/web/service/annotation/HttpExchange.java
+++ b/spring-web/src/main/java/org/springframework/web/service/annotation/HttpExchange.java
@@ -26,13 +26,16 @@ import org.springframework.core.annotation.AliasFor;
import org.springframework.web.bind.annotation.Mapping;
/**
- * Annotation that declares an HTTP service method as an HTTP endpoint defined
- * through attributes of the annotation and method argument values.
+ * Annotation to declare a method on an HTTP service interface as an HTTP
+ * endpoint. The endpoint details are defined statically through attributes of
+ * the annotation, as well as through the input method argument values.
*
- *
The annotation may only be used at the type level — for example to
- * specify a base URL path. At the method level, use one of the HTTP method
- * specific, shortcut annotations, each of which is meta-annotated with
- * {@code HttpExchange}:
+ *
Supported at the type level to express common attributes, to be inherited
+ * by all methods, such as a base URL path.
+ *
+ *
At the method level, it's more common to use one of the below HTTP method
+ * specific, shortcut annotation, each of which is itself meta-annotated
+ * with {@code HttpExchange}:
*
*
* - {@link GetExchange}
@@ -40,8 +43,6 @@ import org.springframework.web.bind.annotation.Mapping;
*
- {@link PutExchange}
*
- {@link PatchExchange}
*
- {@link DeleteExchange}
- *
- {@link OptionsExchange}
- *
- {@link HeadExchange}
*
*
* Supported method arguments:
@@ -100,7 +101,7 @@ import org.springframework.web.bind.annotation.Mapping;
* @author Rossen Stoyanchev
* @since 6.0
*/
-@Target(ElementType.TYPE)
+@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
diff --git a/spring-web/src/main/java/org/springframework/web/service/annotation/OptionsExchange.java b/spring-web/src/main/java/org/springframework/web/service/annotation/OptionsExchange.java
deleted file mode 100644
index 981e358366..0000000000
--- a/spring-web/src/main/java/org/springframework/web/service/annotation/OptionsExchange.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Copyright 2002-2022 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.web.service.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;
-
-import org.springframework.core.annotation.AliasFor;
-
-/**
- * Shortcut for {@link HttpExchange @HttpExchange} for HTTP OPTIONS requests.
- *
- * @author Rossen Stoyanchev
- * @since 6.0
- */
-@Target(ElementType.METHOD)
-@Retention(RetentionPolicy.RUNTIME)
-@Documented
-@HttpExchange(method = "OPTIONS")
-public @interface OptionsExchange {
-
- /**
- * Alias for {@link HttpExchange#value}.
- */
- @AliasFor(annotation = HttpExchange.class)
- String value() default "";
-
- /**
- * Alias for {@link HttpExchange#url()}.
- */
- @AliasFor(annotation = HttpExchange.class)
- String url() default "";
-
-}
diff --git a/spring-web/src/main/java/org/springframework/web/service/invoker/HttpMethodArgumentResolver.java b/spring-web/src/main/java/org/springframework/web/service/invoker/HttpMethodArgumentResolver.java
index 50b84441a3..9a6f966f08 100644
--- a/spring-web/src/main/java/org/springframework/web/service/invoker/HttpMethodArgumentResolver.java
+++ b/spring-web/src/main/java/org/springframework/web/service/invoker/HttpMethodArgumentResolver.java
@@ -22,12 +22,14 @@ import org.apache.commons.logging.LogFactory;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpMethod;
import org.springframework.lang.Nullable;
+import org.springframework.util.Assert;
/**
* {@link HttpServiceArgumentResolver} that resolves the target
* request's HTTP method from an {@link HttpMethod} argument.
*
* @author Olga Maciaszek-Sharma
+ * @author Rossen Stoyanchev
* @since 6.0
*/
public class HttpMethodArgumentResolver implements HttpServiceArgumentResolver {
@@ -43,12 +45,11 @@ public class HttpMethodArgumentResolver implements HttpServiceArgumentResolver {
return false;
}
- if (argument != null) {
- HttpMethod httpMethod = (HttpMethod) argument;
- requestValues.setHttpMethod(httpMethod);
- if (logger.isTraceEnabled()) {
- logger.trace("Resolved HTTP method to: " + httpMethod.name());
- }
+ Assert.notNull(argument, "HttpMethod is required");
+ HttpMethod httpMethod = (HttpMethod) argument;
+ requestValues.setHttpMethod(httpMethod);
+ if (logger.isTraceEnabled()) {
+ logger.trace("Resolved HTTP method to: " + httpMethod.name());
}
return true;
diff --git a/spring-web/src/main/java/org/springframework/web/service/invoker/HttpRequestValues.java b/spring-web/src/main/java/org/springframework/web/service/invoker/HttpRequestValues.java
index 05c465deeb..688e7cf7ed 100644
--- a/spring-web/src/main/java/org/springframework/web/service/invoker/HttpRequestValues.java
+++ b/spring-web/src/main/java/org/springframework/web/service/invoker/HttpRequestValues.java
@@ -56,6 +56,7 @@ public final class HttpRequestValues {
CollectionUtils.toMultiValueMap(Collections.emptyMap());
+ @Nullable
private final HttpMethod httpMethod;
@Nullable
@@ -82,7 +83,7 @@ public final class HttpRequestValues {
private final ParameterizedTypeReference> bodyElementType;
- private HttpRequestValues(HttpMethod httpMethod,
+ private HttpRequestValues(@Nullable HttpMethod httpMethod,
@Nullable URI uri, @Nullable String uriTemplate, Map uriVariables,
HttpHeaders headers, MultiValueMap cookies, Map attributes,
@Nullable Object bodyValue,
@@ -106,6 +107,7 @@ public final class HttpRequestValues {
/**
* Return the HTTP method to use for the request.
*/
+ @Nullable
public HttpMethod getHttpMethod() {
return this.httpMethod;
}
@@ -187,8 +189,8 @@ public final class HttpRequestValues {
}
- public static Builder builder(HttpMethod httpMethod) {
- return new Builder(httpMethod);
+ public static Builder builder() {
+ return new Builder();
}
@@ -199,6 +201,7 @@ public final class HttpRequestValues {
private static final Function, byte[]> FORM_DATA_SERIALIZER = new FormDataSerializer();
+ @Nullable
private HttpMethod httpMethod;
@Nullable
@@ -231,16 +234,10 @@ public final class HttpRequestValues {
@Nullable
private ParameterizedTypeReference> bodyElementType;
- private Builder(HttpMethod httpMethod) {
- Assert.notNull(httpMethod, "HttpMethod is required");
- this.httpMethod = httpMethod;
- }
-
/**
* Set the HTTP method for the request.
*/
public Builder setHttpMethod(HttpMethod httpMethod) {
- Assert.notNull(httpMethod, "HttpMethod is required");
this.httpMethod = httpMethod;
return this;
}
diff --git a/spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceMethod.java b/spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceMethod.java
index 3b33f5a8af..438a82980e 100644
--- a/spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceMethod.java
+++ b/spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceMethod.java
@@ -131,7 +131,7 @@ final class HttpServiceMethod {
* and method-level {@link HttpExchange @HttpRequest} annotations.
*/
private record HttpRequestValuesInitializer(
- HttpMethod httpMethod, @Nullable String url,
+ @Nullable HttpMethod httpMethod, @Nullable String url,
@Nullable MediaType contentType, @Nullable List acceptMediaTypes) {
private HttpRequestValuesInitializer(
@@ -145,7 +145,10 @@ final class HttpServiceMethod {
}
public HttpRequestValues.Builder initializeRequestValuesBuilder() {
- HttpRequestValues.Builder requestValues = HttpRequestValues.builder(this.httpMethod);
+ HttpRequestValues.Builder requestValues = HttpRequestValues.builder();
+ if (this.httpMethod != null) {
+ requestValues.setHttpMethod(this.httpMethod);
+ }
if (this.url != null) {
requestValues.setUriTemplate(this.url);
}
@@ -178,7 +181,7 @@ final class HttpServiceMethod {
return new HttpRequestValuesInitializer(httpMethod, url, contentType, acceptableMediaTypes);
}
-
+ @Nullable
private static HttpMethod initHttpMethod(@Nullable HttpExchange typeAnnot, HttpExchange annot) {
String value1 = (typeAnnot != null ? typeAnnot.method() : null);
@@ -192,7 +195,7 @@ final class HttpServiceMethod {
return HttpMethod.valueOf(value1);
}
- throw new IllegalStateException("HttpMethod is required");
+ return null;
}
@Nullable
diff --git a/spring-web/src/test/java/org/springframework/web/service/invoker/HttpMethodArgumentResolverTests.java b/spring-web/src/test/java/org/springframework/web/service/invoker/HttpMethodArgumentResolverTests.java
index 57d6b02016..23988e3641 100644
--- a/spring-web/src/test/java/org/springframework/web/service/invoker/HttpMethodArgumentResolverTests.java
+++ b/spring-web/src/test/java/org/springframework/web/service/invoker/HttpMethodArgumentResolverTests.java
@@ -20,9 +20,12 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.http.HttpMethod;
+import org.springframework.lang.Nullable;
import org.springframework.web.service.annotation.GetExchange;
+import org.springframework.web.service.annotation.HttpExchange;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
/**
@@ -47,11 +50,17 @@ public class HttpMethodArgumentResolverTests {
@Test
- void requestMethodOverride() {
+ void httpMethodFromArgument() {
this.service.execute(HttpMethod.POST);
assertThat(getActualMethod()).isEqualTo(HttpMethod.POST);
}
+ @Test
+ void httpMethodFromAnnotation() {
+ this.service.executeHttpHead();
+ assertThat(getActualMethod()).isEqualTo(HttpMethod.HEAD);
+ }
+
@Test
void notHttpMethod() {
assertThatIllegalStateException()
@@ -63,11 +72,11 @@ public class HttpMethodArgumentResolverTests {
}
@Test
- void ignoreNull() {
- this.service.execute(null);
- assertThat(getActualMethod()).isEqualTo(HttpMethod.GET);
+ void nullHttpMethod() {
+ assertThatIllegalArgumentException().isThrownBy(() -> this.service.execute(null));
}
+ @Nullable
private HttpMethod getActualMethod() {
return this.client.getRequestValues().getHttpMethod();
}
@@ -75,9 +84,12 @@ public class HttpMethodArgumentResolverTests {
private interface Service {
- @GetExchange
+ @HttpExchange
void execute(HttpMethod method);
+ @HttpExchange(method = "HEAD")
+ void executeHttpHead();
+
@GetExchange
void executeNotHttpMethod(String test);
diff --git a/spring-web/src/test/java/org/springframework/web/service/invoker/HttpRequestValuesTests.java b/spring-web/src/test/java/org/springframework/web/service/invoker/HttpRequestValuesTests.java
index f7074c871c..28a8399be7 100644
--- a/spring-web/src/test/java/org/springframework/web/service/invoker/HttpRequestValuesTests.java
+++ b/spring-web/src/test/java/org/springframework/web/service/invoker/HttpRequestValuesTests.java
@@ -38,7 +38,7 @@ public class HttpRequestValuesTests {
@Test
void defaultUri() {
- HttpRequestValues requestValues = HttpRequestValues.builder(HttpMethod.GET).build();
+ HttpRequestValues requestValues = HttpRequestValues.builder().setHttpMethod(HttpMethod.GET).build();
assertThat(requestValues.getUri()).isNull();
assertThat(requestValues.getUriTemplate()).isEqualTo("");
@@ -48,7 +48,7 @@ public class HttpRequestValuesTests {
@ValueSource(strings = {"POST", "PUT", "PATCH"})
void requestParamAsFormData(String httpMethod) {
- HttpRequestValues requestValues = HttpRequestValues.builder(HttpMethod.valueOf(httpMethod))
+ HttpRequestValues requestValues = HttpRequestValues.builder().setHttpMethod(HttpMethod.valueOf(httpMethod))
.setContentType(MediaType.APPLICATION_FORM_URLENCODED)
.addRequestParameter("param1", "1st value")
.addRequestParameter("param2", "2nd value A", "2nd value B")
@@ -62,7 +62,7 @@ public class HttpRequestValuesTests {
@Test
void requestParamAsQueryParamsInUriTemplate() {
- HttpRequestValues requestValues = HttpRequestValues.builder(HttpMethod.POST)
+ HttpRequestValues requestValues = HttpRequestValues.builder().setHttpMethod(HttpMethod.POST)
.setUriTemplate("/path")
.addRequestParameter("param1", "1st value")
.addRequestParameter("param2", "2nd value A", "2nd value B")
@@ -96,7 +96,7 @@ public class HttpRequestValuesTests {
@Test
void requestParamAsQueryParamsInUri() {
- HttpRequestValues requestValues = HttpRequestValues.builder(HttpMethod.POST)
+ HttpRequestValues requestValues = HttpRequestValues.builder().setHttpMethod(HttpMethod.POST)
.setUri(URI.create("/path"))
.addRequestParameter("param1", "1st value")
.addRequestParameter("param2", "2nd value A", "2nd value B")
diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/support/WebClientAdapter.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/support/WebClientAdapter.java
index ecb9dfa850..f9d9f7c90b 100644
--- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/support/WebClientAdapter.java
+++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/support/WebClientAdapter.java
@@ -86,7 +86,7 @@ public class WebClientAdapter implements HttpClientAdapter {
private WebClient.RequestBodySpec toBodySpec(HttpRequestValues requestValues) {
HttpMethod httpMethod = requestValues.getHttpMethod();
- Assert.notNull(httpMethod, "No HttpMethod");
+ Assert.notNull(httpMethod, "HttpMethod is required");
WebClient.RequestBodyUriSpec uriSpec = this.webClient.method(httpMethod);