Skip to content

Commit ff890bc

Browse files
committed
Support custom HTTP method for @HttpExchange
Closes gh-28504
1 parent 48c1746 commit ff890bc

File tree

9 files changed

+52
-146
lines changed

9 files changed

+52
-146
lines changed

spring-web/src/main/java/org/springframework/web/service/annotation/HeadExchange.java

Lines changed: 0 additions & 57 deletions
This file was deleted.

spring-web/src/main/java/org/springframework/web/service/annotation/HttpExchange.java

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,22 +26,23 @@
2626
import org.springframework.web.bind.annotation.Mapping;
2727

2828
/**
29-
* Annotation that declares an HTTP service method as an HTTP endpoint defined
30-
* through attributes of the annotation and method argument values.
29+
* Annotation to declare a method on an HTTP service interface as an HTTP
30+
* endpoint. The endpoint details are defined statically through attributes of
31+
* the annotation, as well as through the input method argument values.
3132
*
32-
* <p>The annotation may only be used at the type level &mdash; for example to
33-
* specify a base URL path. At the method level, use one of the HTTP method
34-
* specific, shortcut annotations, each of which is <em>meta-annotated</em> with
35-
* {@code HttpExchange}:
33+
* <p>Supported at the type level to express common attributes, to be inherited
34+
* by all methods, such as a base URL path.
35+
*
36+
* <p>At the method level, it's more common to use one of the below HTTP method
37+
* specific, shortcut annotation, each of which is itself <em>meta-annotated</em>
38+
* with {@code HttpExchange}:
3639
*
3740
* <ul>
3841
* <li>{@link GetExchange}
3942
* <li>{@link PostExchange}
4043
* <li>{@link PutExchange}
4144
* <li>{@link PatchExchange}
4245
* <li>{@link DeleteExchange}
43-
* <li>{@link OptionsExchange}
44-
* <li>{@link HeadExchange}
4546
* </ul>
4647
*
4748
* <p>Supported method arguments:
@@ -100,7 +101,7 @@
100101
* @author Rossen Stoyanchev
101102
* @since 6.0
102103
*/
103-
@Target(ElementType.TYPE)
104+
@Target({ElementType.TYPE, ElementType.METHOD})
104105
@Retention(RetentionPolicy.RUNTIME)
105106
@Documented
106107
@Mapping

spring-web/src/main/java/org/springframework/web/service/annotation/OptionsExchange.java

Lines changed: 0 additions & 51 deletions
This file was deleted.

spring-web/src/main/java/org/springframework/web/service/invoker/HttpMethodArgumentResolver.java

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,14 @@
2222
import org.springframework.core.MethodParameter;
2323
import org.springframework.http.HttpMethod;
2424
import org.springframework.lang.Nullable;
25+
import org.springframework.util.Assert;
2526

2627
/**
2728
* {@link HttpServiceArgumentResolver} that resolves the target
2829
* request's HTTP method from an {@link HttpMethod} argument.
2930
*
3031
* @author Olga Maciaszek-Sharma
32+
* @author Rossen Stoyanchev
3133
* @since 6.0
3234
*/
3335
public class HttpMethodArgumentResolver implements HttpServiceArgumentResolver {
@@ -43,12 +45,11 @@ public boolean resolve(
4345
return false;
4446
}
4547

46-
if (argument != null) {
47-
HttpMethod httpMethod = (HttpMethod) argument;
48-
requestValues.setHttpMethod(httpMethod);
49-
if (logger.isTraceEnabled()) {
50-
logger.trace("Resolved HTTP method to: " + httpMethod.name());
51-
}
48+
Assert.notNull(argument, "HttpMethod is required");
49+
HttpMethod httpMethod = (HttpMethod) argument;
50+
requestValues.setHttpMethod(httpMethod);
51+
if (logger.isTraceEnabled()) {
52+
logger.trace("Resolved HTTP method to: " + httpMethod.name());
5253
}
5354

5455
return true;

spring-web/src/main/java/org/springframework/web/service/invoker/HttpRequestValues.java

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ public final class HttpRequestValues {
5656
CollectionUtils.toMultiValueMap(Collections.emptyMap());
5757

5858

59+
@Nullable
5960
private final HttpMethod httpMethod;
6061

6162
@Nullable
@@ -82,7 +83,7 @@ public final class HttpRequestValues {
8283
private final ParameterizedTypeReference<?> bodyElementType;
8384

8485

85-
private HttpRequestValues(HttpMethod httpMethod,
86+
private HttpRequestValues(@Nullable HttpMethod httpMethod,
8687
@Nullable URI uri, @Nullable String uriTemplate, Map<String, String> uriVariables,
8788
HttpHeaders headers, MultiValueMap<String, String> cookies, Map<String, Object> attributes,
8889
@Nullable Object bodyValue,
@@ -106,6 +107,7 @@ private HttpRequestValues(HttpMethod httpMethod,
106107
/**
107108
* Return the HTTP method to use for the request.
108109
*/
110+
@Nullable
109111
public HttpMethod getHttpMethod() {
110112
return this.httpMethod;
111113
}
@@ -187,8 +189,8 @@ public ParameterizedTypeReference<?> getBodyElementType() {
187189
}
188190

189191

190-
public static Builder builder(HttpMethod httpMethod) {
191-
return new Builder(httpMethod);
192+
public static Builder builder() {
193+
return new Builder();
192194
}
193195

194196

@@ -199,6 +201,7 @@ public final static class Builder {
199201

200202
private static final Function<MultiValueMap<String, String>, byte[]> FORM_DATA_SERIALIZER = new FormDataSerializer();
201203

204+
@Nullable
202205
private HttpMethod httpMethod;
203206

204207
@Nullable
@@ -231,16 +234,10 @@ public final static class Builder {
231234
@Nullable
232235
private ParameterizedTypeReference<?> bodyElementType;
233236

234-
private Builder(HttpMethod httpMethod) {
235-
Assert.notNull(httpMethod, "HttpMethod is required");
236-
this.httpMethod = httpMethod;
237-
}
238-
239237
/**
240238
* Set the HTTP method for the request.
241239
*/
242240
public Builder setHttpMethod(HttpMethod httpMethod) {
243-
Assert.notNull(httpMethod, "HttpMethod is required");
244241
this.httpMethod = httpMethod;
245242
return this;
246243
}

spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceMethod.java

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ private static String formatArgumentError(MethodParameter param, String message)
131131
* and method-level {@link HttpExchange @HttpRequest} annotations.
132132
*/
133133
private record HttpRequestValuesInitializer(
134-
HttpMethod httpMethod, @Nullable String url,
134+
@Nullable HttpMethod httpMethod, @Nullable String url,
135135
@Nullable MediaType contentType, @Nullable List<MediaType> acceptMediaTypes) {
136136

137137
private HttpRequestValuesInitializer(
@@ -145,7 +145,10 @@ private HttpRequestValuesInitializer(
145145
}
146146

147147
public HttpRequestValues.Builder initializeRequestValuesBuilder() {
148-
HttpRequestValues.Builder requestValues = HttpRequestValues.builder(this.httpMethod);
148+
HttpRequestValues.Builder requestValues = HttpRequestValues.builder();
149+
if (this.httpMethod != null) {
150+
requestValues.setHttpMethod(this.httpMethod);
151+
}
149152
if (this.url != null) {
150153
requestValues.setUriTemplate(this.url);
151154
}
@@ -178,7 +181,7 @@ public static HttpRequestValuesInitializer create(
178181
return new HttpRequestValuesInitializer(httpMethod, url, contentType, acceptableMediaTypes);
179182
}
180183

181-
184+
@Nullable
182185
private static HttpMethod initHttpMethod(@Nullable HttpExchange typeAnnot, HttpExchange annot) {
183186

184187
String value1 = (typeAnnot != null ? typeAnnot.method() : null);
@@ -192,7 +195,7 @@ private static HttpMethod initHttpMethod(@Nullable HttpExchange typeAnnot, HttpE
192195
return HttpMethod.valueOf(value1);
193196
}
194197

195-
throw new IllegalStateException("HttpMethod is required");
198+
return null;
196199
}
197200

198201
@Nullable

spring-web/src/test/java/org/springframework/web/service/invoker/HttpMethodArgumentResolverTests.java

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,12 @@
2020
import org.junit.jupiter.api.Test;
2121

2222
import org.springframework.http.HttpMethod;
23+
import org.springframework.lang.Nullable;
2324
import org.springframework.web.service.annotation.GetExchange;
25+
import org.springframework.web.service.annotation.HttpExchange;
2426

2527
import static org.assertj.core.api.Assertions.assertThat;
28+
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
2629
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
2730

2831
/**
@@ -47,11 +50,17 @@ void setUp() throws Exception {
4750

4851

4952
@Test
50-
void requestMethodOverride() {
53+
void httpMethodFromArgument() {
5154
this.service.execute(HttpMethod.POST);
5255
assertThat(getActualMethod()).isEqualTo(HttpMethod.POST);
5356
}
5457

58+
@Test
59+
void httpMethodFromAnnotation() {
60+
this.service.executeHttpHead();
61+
assertThat(getActualMethod()).isEqualTo(HttpMethod.HEAD);
62+
}
63+
5564
@Test
5665
void notHttpMethod() {
5766
assertThatIllegalStateException()
@@ -63,21 +72,24 @@ void notHttpMethod() {
6372
}
6473

6574
@Test
66-
void ignoreNull() {
67-
this.service.execute(null);
68-
assertThat(getActualMethod()).isEqualTo(HttpMethod.GET);
75+
void nullHttpMethod() {
76+
assertThatIllegalArgumentException().isThrownBy(() -> this.service.execute(null));
6977
}
7078

79+
@Nullable
7180
private HttpMethod getActualMethod() {
7281
return this.client.getRequestValues().getHttpMethod();
7382
}
7483

7584

7685
private interface Service {
7786

78-
@GetExchange
87+
@HttpExchange
7988
void execute(HttpMethod method);
8089

90+
@HttpExchange(method = "HEAD")
91+
void executeHttpHead();
92+
8193
@GetExchange
8294
void executeNotHttpMethod(String test);
8395

spring-web/src/test/java/org/springframework/web/service/invoker/HttpRequestValuesTests.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public class HttpRequestValuesTests {
3838

3939
@Test
4040
void defaultUri() {
41-
HttpRequestValues requestValues = HttpRequestValues.builder(HttpMethod.GET).build();
41+
HttpRequestValues requestValues = HttpRequestValues.builder().setHttpMethod(HttpMethod.GET).build();
4242

4343
assertThat(requestValues.getUri()).isNull();
4444
assertThat(requestValues.getUriTemplate()).isEqualTo("");
@@ -48,7 +48,7 @@ void defaultUri() {
4848
@ValueSource(strings = {"POST", "PUT", "PATCH"})
4949
void requestParamAsFormData(String httpMethod) {
5050

51-
HttpRequestValues requestValues = HttpRequestValues.builder(HttpMethod.valueOf(httpMethod))
51+
HttpRequestValues requestValues = HttpRequestValues.builder().setHttpMethod(HttpMethod.valueOf(httpMethod))
5252
.setContentType(MediaType.APPLICATION_FORM_URLENCODED)
5353
.addRequestParameter("param1", "1st value")
5454
.addRequestParameter("param2", "2nd value A", "2nd value B")
@@ -62,7 +62,7 @@ void requestParamAsFormData(String httpMethod) {
6262
@Test
6363
void requestParamAsQueryParamsInUriTemplate() {
6464

65-
HttpRequestValues requestValues = HttpRequestValues.builder(HttpMethod.POST)
65+
HttpRequestValues requestValues = HttpRequestValues.builder().setHttpMethod(HttpMethod.POST)
6666
.setUriTemplate("/path")
6767
.addRequestParameter("param1", "1st value")
6868
.addRequestParameter("param2", "2nd value A", "2nd value B")
@@ -96,7 +96,7 @@ void requestParamAsQueryParamsInUriTemplate() {
9696
@Test
9797
void requestParamAsQueryParamsInUri() {
9898

99-
HttpRequestValues requestValues = HttpRequestValues.builder(HttpMethod.POST)
99+
HttpRequestValues requestValues = HttpRequestValues.builder().setHttpMethod(HttpMethod.POST)
100100
.setUri(URI.create("/path"))
101101
.addRequestParameter("param1", "1st value")
102102
.addRequestParameter("param2", "2nd value A", "2nd value B")

spring-webflux/src/main/java/org/springframework/web/reactive/function/client/support/WebClientAdapter.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ public <T> Mono<ResponseEntity<Flux<T>>> requestToEntityFlux(HttpRequestValues r
8686
private WebClient.RequestBodySpec toBodySpec(HttpRequestValues requestValues) {
8787

8888
HttpMethod httpMethod = requestValues.getHttpMethod();
89-
Assert.notNull(httpMethod, "No HttpMethod");
89+
Assert.notNull(httpMethod, "HttpMethod is required");
9090

9191
WebClient.RequestBodyUriSpec uriSpec = this.webClient.method(httpMethod);
9292

0 commit comments

Comments
 (0)