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}: * *

* *

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