Support @HttpExchange for server-side handling

See gh-30980
This commit is contained in:
Olga MaciaszekSharma 2023-08-02 16:39:53 +02:00 committed by rstoyanchev
parent 646fd3edcc
commit d1d5b54f12
8 changed files with 668 additions and 11 deletions

View File

@ -0,0 +1,215 @@
[[webflux-ann-httpexchange]]
= HttpExchange
[.small]#xref:web/webmvc/mvc-controller/ann-httpexchange.adoc[See equivalent in the Servlet stack]#
Similarly to
xref:web/webflux/controller/ann-requestmapping.adoc[`@RequestMapping`],
you can use the `@HttpExchange` annotation to map requests to controllers
methods. However, while `@RequestMapping` is only supported on the server side, `@HttpExchange` can be used both to create a server-side mapping and
xref:integration/rest-clients.adoc#rest-http-interface[an HTTP
Interface Client] that allows making requests.
`@HttpExchange` has various attributes to match by URL, HTTP method, and media
types. You can use it at the class level to express shared mappings or at the
method level to narrow down to a specific endpoint mapping.
There are also HTTP method specific shortcut variants of `@HttpExchange`:
* `@GetExchange`
* `@PostExchange`
* `@PutExchange`
* `@DeleteExchange`
* `@PatchExchange`
// TODO
The shortcuts are xref:web/webflux/controller/ann-httpexchange.adoc#webflux-ann-httpexchange-composed[Custom Annotations] that are provided
because, arguably, most controller methods should be mapped to a specific
HTTP method versus using `@HttpExchange`, which, by default, matches
to all HTTP methods.
An `@HttpExchange` is still needed at the class level to express shared mappings.
The following example has type and method level mappings:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
----
@RestController
@HttpExchange("/persons")
class PersonController {
@GetExchange("/{id}")
public Person getPerson(@PathVariable Long id) {
// ...
}
@PostExchange
@ResponseStatus(HttpStatus.CREATED)
public void add(@RequestBody Person person) {
// ...
}
}
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
----
@RestController
@HttpExchange("/persons")
class PersonController {
@GetExchange("/{id}")
fun getPerson(@PathVariable id: Long): Person {
// ...
}
@PostExchange
@ResponseStatus(HttpStatus.CREATED)
fun add(@RequestBody person: Person) {
// ...
}
}
----
======
`@HttpExhange` supports a very similar method signature to `@MessageMapping`,
however, since it needs to be suitable both for requester and responder use,
there are slight differences.
[[webflux-ann-httpexchange-uri-templates]]
== URI patterns
[.small]#xref:web/webmvc/mvc-controller/ann-httpexchange.adoc#mvc-ann-httpexchange-uri-templates[See equivalent in the Servlet stack]#
URI patterns resolution support is very similar to the one offered by xref:web/webflux/controller/ann-requestmapping.adoc#webflux-ann-requestmapping-uri-templates[`@RequestMapping`], with the difference
that while `@RequestMapping` accepts a `String` array as its `value` or `path`
parameter that is used to specify the URI patterns, only a single `String` can be passed
as the `value` of `@HttpExchange`.
[[webflux-ann-httpexchange-contenttype]]
== Consumable Media Types
[.small]#xref:web/webmvc/mvc-controller/ann-httpexchange.adoc#mvc-ann-httpexchange-contenttype[See equivalent in the Servlet stack]#
You can narrow the request mapping based on the `Content-Type` of the request,
as the following example shows:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
----
@PostExchange(path = "/pets", contentType = "application/json") // <1>
public void addPet(@RequestBody Pet pet) {
// ...
}
----
<1> Using a `contentType` attribute to narrow the mapping by the content type.
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
----
@PostExchange("/pets", contentType = "application/json") // <1>
fun addPet(@RequestBody pet: Pet) {
// ...
}
----
<1> Using a `contentType` attribute to narrow the mapping by the content type.
======
The `contentType` attribute accepts a single `String` as the attribute value.
You can also declare a shared `contentType` attribute at the class level.
Unlike most other request-mapping attributes, however, when used at the
class level, a method-level `contentType` attribute overrides rather than
extends the class-level declaration.
TIP: `MediaType` provides constants for commonly used media types, such as
`APPLICATION_JSON_VALUE` and `APPLICATION_XML_VALUE`.
[[webflux-ann-httpexchange-accept]]
== Producible Media Types
[.small]#xref:web/webmvc/mvc-controller/ann-httpexchange.adoc#mvc-ann-httpexchange-accept[See equivalent in the Servlet stack]#
You can narrow the request mapping based on the `Accept` request header and the list of
content types that a controller method produces, as the following example shows:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
----
@GetExchange(path = "/pets/{petId}", accept = "application/json") // <1>
@ResponseBody
public Pet getPet(@PathVariable String petId) {
// ...
}
----
<1> Using an `accept` attribute to narrow the mapping by the content type that
can be served.
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
----
@GetExchange("/pets/{petId}", accept = ["application/json"]) // <1>
@ResponseBody
fun getPet(@PathVariable petId: String): Pet {
// ...
}
----
<1> Using an `accept` attribute to narrow the mapping by the content type that
can be served.
======
The `accept` attribute accepts a `String` array as the attribute value.
You can declare a shared `accept` attribute at the class level. Unlike most
other request-mapping attributes, however, when used at the class level,
a method-level `accept` attribute
overrides rather than extends the class-level declaration.
TIP: `MediaType` provides constants for commonly used media types, such as
`APPLICATION_JSON_VALUE` and `APPLICATION_XML_VALUE`.
[[webflux-ann-httpexchange-params-and-headers]]
== Parameters, headers
[.small]#xref:web/webmvc/mvc-controller/ann-httpexchange.adoc#mvc-ann-httpexchange-params-and-headers[See equivalent in the Servlet stack]#
You can narrow request mappings based on request parameter and headers
conditions. It is supported for `@HttpExchange` in the same way as in xref:web/webflux/controller/ann-requestmapping.adoc#webflux-ann-requestmapping-params-and-headers[`@RequestMapping` parameters and headers support].
[[webflux-ann-httpexchange-head-options]]
== HTTP HEAD, OPTIONS
[.small]#xref:web/webmvc/mvc-controller/ann-httpexchange.adoc#mvc-ann-httpexchange-head-options[See equivalent in the Servlet stack]#
The support of `HTTP HEAD` and `HTTP OPTIONS` in `@HttpExchange` annotated
controllers is the same xref:web/webflux/controller/ann-requestmapping.adoc#webflux-ann-requestmapping-head-options[
as in `@RequestMapping` annotated controllers].
[[webflux-ann-httpexchange-composed]]
== Custom Annotations
[.small]#xref:web/webmvc/mvc-controller/ann-httpexchange.adoc#mvc-ann-httpexchange-composed[See equivalent in the Servlet stack]#
`@HttpExchange` annotated controllers support the use of xref:core/beans/classpath-scanning.adoc#beans-meta-annotations[composed annotations]
for request mapping. Those are annotations that are themselves meta-annotated
with `@HttpExchange` and composed to redeclare a subset (or all) of the
`@HttpExchange` attributes with a narrower, more specific purpose.
`@GetExchange`, `@PostExchange`, `@PutExchange`, `@DeleteExchange`,
and `@PatcExchange` are examples of composed annotations. They are provided
because, arguably, most controller methods should be mapped to a specific
HTTP method versus using `@HttpExchange`, which, by default,
matches to all HTTP methods. If you need an example of composed annotations,
look at how those are declared.

View File

@ -4,7 +4,7 @@
[.small]#xref:web/webmvc/mvc-controller/ann-methods.adoc[See equivalent in the Servlet stack]# [.small]#xref:web/webmvc/mvc-controller/ann-methods.adoc[See equivalent in the Servlet stack]#
`@RequestMapping` handler methods have a flexible signature and can choose from a range of `@RequestMapping` and `@HttpExchange` handler methods have a flexible signature and can choose from a range of
supported controller method arguments and return values. supported controller method arguments and return values.

View File

@ -0,0 +1,215 @@
[[mvc-ann-httpexchange]]
= HttpExchange
[.small]#xref:web/webflux/controller/ann-httpexchange.adoc[
See equivalent in the Reactive stack]#
Similarly to
xref:web/webmvc/mvc-controller/ann-requestmapping.adoc[`@RequestMapping`],
you can use the `@HttpExchange` annotation to map requests to controllers
methods. However, while `@RequestMapping` is only supported on the server side, `@HttpExchange` can be used both to create a server-side mapping and
xref:integration/rest-clients.adoc#rest-http-interface[an HTTP
Interface Client] that allows making requests.
`@HttpExchange` has various attributes to match by URL, HTTP method, and media
types. You can use it at the class level to express shared mappings or at the
method level to narrow down to a specific endpoint mapping.
There are also HTTP method specific shortcut variants of `@HttpExchange`:
* `@GetExchange`
* `@PostExchange`
* `@PutExchange`
* `@DeleteExchange`
* `@PatchExchange`
The shortcuts are xref:web/webmvc/mvc-controller/ann-httpexchange.adoc#mvc-ann-httpexchange-composed[Custom Annotations] that are provided
because, arguably, most controller methods should be mapped to a specific
HTTP method versus using `@HttpExchange`, which, by default, matches
to all HTTP methods.
An `@HttpExchange` is still needed at the class level to express shared mappings.
The following example has type and method level mappings:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
----
@RestController
@HttpExchange("/persons")
class PersonController {
@GetExchange("/{id}")
public Person getPerson(@PathVariable Long id) {
// ...
}
@PostExchange
@ResponseStatus(HttpStatus.CREATED)
public void add(@RequestBody Person person) {
// ...
}
}
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
----
@RestController
@HttpExchange("/persons")
class PersonController {
@GetExchange("/{id}")
fun getPerson(@PathVariable id: Long): Person {
// ...
}
@PostExchange
@ResponseStatus(HttpStatus.CREATED)
fun add(@RequestBody person: Person) {
// ...
}
}
----
======
`@HttpExhange` supports a very similar method signature to `@MessageMapping`,
however, since it needs to be suitable both for requester and responder use,
there are slight differences, which are discussed below.
[[mvc-ann-httpexchange-uri-templates]]
== URI patterns
[.small]#xref:web/webflux/controller/ann-httpexchange.adoc#webflux-ann-httpexchange-uri-templates[See equivalent in the Reactive stack]#
URI patterns resolution support is very similar to the one offered by xref:web/webmvc/mvc-controller/ann-requestmapping.adoc#mvc-ann-requestmapping-uri-templates[`@RequestMapping`], with the difference
that while `@RequestMapping` accepts a `String` array as its `value` or `path`
parameter that is used to specify the URI patterns, only a single `String` can be passed
as the `value` of `@HttpExchange`.
[[mvc-ann-httpexchange-contenttype]]
== Consumable Media Types
[.small]#xref:web/webflux/controller/ann-httpexchange.adoc#webflux-ann-httpexchange-contenttype[See equivalent in the Reactive stack]#
You can narrow the request mapping based on the `Content-Type` of the request,
as the following example shows:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
----
@PostExchange(path = "/pets", contentType = "application/json") // <1>
public void addPet(@RequestBody Pet pet) {
// ...
}
----
<1> Using a `contentType` attribute to narrow the mapping by the content type.
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
----
@PostExchange("/pets", contentType = "application/json") // <1>
fun addPet(@RequestBody pet: Pet) {
// ...
}
----
<1> Using a `contentType` attribute to narrow the mapping by the content type.
======
The `contentType` attribute accepts a single `String` as the attribute value.
You can also declare a shared `contentType` attribute at the class level.
Unlike most other request-mapping attributes, however, when used at the
class level, a method-level `contentType` attribute overrides rather than
extends the class-level declaration.
TIP: `MediaType` provides constants for commonly used media types, such as
`APPLICATION_JSON_VALUE` and `APPLICATION_XML_VALUE`.
[[mvc-ann-httpexchange-accept]]
== Producible Media Types
[.small]#xref:web/webflux/controller/ann-httpexchange.adoc#webflux-ann-httpexchange-accept[See equivalent in the Reactive stack]#
You can narrow the request mapping based on the `Accept` request header and the list of
content types that a controller method produces, as the following example shows:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
----
@GetExchange(path = "/pets/{petId}", accept = "application/json") // <1>
@ResponseBody
public Pet getPet(@PathVariable String petId) {
// ...
}
----
<1> Using an `accept` attribute to narrow the mapping by the content type that
can be served.
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
----
@GetExchange("/pets/{petId}", accept = ["application/json"]) // <1>
@ResponseBody
fun getPet(@PathVariable petId: String): Pet {
// ...
}
----
<1> Using an `accept` attribute to narrow the mapping by the content type that
can be served.
======
The `accept` attribute accepts a `String` array as the attribute value.
You can declare a shared `accept` attribute at the class level. Unlike most
other request-mapping attributes, however, when used at the class level,
a method-level `accept` attribute
overrides rather than extends the class-level declaration.
TIP: `MediaType` provides constants for commonly used media types, such as
`APPLICATION_JSON_VALUE` and `APPLICATION_XML_VALUE`.
[[mvc-ann-httpexchange-params-and-headers]]
== Parameters, headers
[.small]#xref:web/webflux/controller/ann-httpexchange.adoc#webflux-ann-httpexchange-params-and-headers[See equivalent in the Reactive stack]#
You can narrow request mappings based on request parameter and headers
conditions. It is supported for `@HttpExchange` in the same way as in xref:web/webmvc/mvc-controller/ann-requestmapping.adoc#mvc-ann-requestmapping-params-and-headers[`@RequestMapping` parameter and header support] .
[[mvc-ann-httpexchange-head-options]]
== HTTP HEAD, OPTIONS
[.small]#xref:web/webflux/controller/ann-httpexchange.adoc#webflux-ann-httpexchange-head-options[See equivalent in the Reactive stack]#
The support of `HTTP HEAD` and `HTTP OPTIONS` in `@HttpExchange` annotated
controllers is the same xref:web/webmvc/mvc-controller/ann-requestmapping.adoc#mvc-ann-requestmapping-head-options[
as in `@RequestMapping` annotated controllers].
[[mvc-ann-httpexchange-composed]]
== Custom Annotations
[.small]#xref:web/webflux/controller/ann-httpexchange.adoc#webflux-ann-httpexchange-head-options[See equivalent in the Reactive stack]#
`@HttpExchange` annotated controllers support the use of xref:core/beans/classpath-scanning.adoc#beans-meta-annotations[composed annotations]
for request mapping. Those are annotations that are themselves meta-annotated
with `@HttpExchange` and composed to redeclare a subset (or all) of the
`@HttpExchange` attributes with a narrower, more specific purpose.
`@GetExchange`, `@PostExchange`, `@PutExchange`, `@DeleteExchange`,
and `@PatcExchange` are examples of composed annotations. They are provided
because, arguably, most controller methods should be mapped to a specific
HTTP method versus using `@HttpExchange`, which, by default,
matches to all HTTP methods. If you need an example of composed annotations,
look at how those are declared.

View File

@ -4,7 +4,7 @@
[.small]#xref:web/webflux/controller/ann-methods.adoc[See equivalent in the Reactive stack]# [.small]#xref:web/webflux/controller/ann-methods.adoc[See equivalent in the Reactive stack]#
`@RequestMapping` handler methods have a flexible signature and can choose from a range of `@RequestMapping` and `@HttpExchange` handler methods have a flexible signature and can choose from a range of
supported controller method arguments and return values. supported controller method arguments and return values.

View File

@ -46,6 +46,7 @@ import org.springframework.web.reactive.result.condition.ConsumesRequestConditio
import org.springframework.web.reactive.result.condition.RequestCondition; import org.springframework.web.reactive.result.condition.RequestCondition;
import org.springframework.web.reactive.result.method.RequestMappingInfo; import org.springframework.web.reactive.result.method.RequestMappingInfo;
import org.springframework.web.reactive.result.method.RequestMappingInfoHandlerMapping; import org.springframework.web.reactive.result.method.RequestMappingInfoHandlerMapping;
import org.springframework.web.service.annotation.HttpExchange;
/** /**
* An extension of {@link RequestMappingInfoHandlerMapping} that creates * An extension of {@link RequestMappingInfoHandlerMapping} that creates
@ -54,6 +55,7 @@ import org.springframework.web.reactive.result.method.RequestMappingInfoHandlerM
* *
* @author Rossen Stoyanchev * @author Rossen Stoyanchev
* @author Sam Brannen * @author Sam Brannen
* @author Olga Maciaszek-Sharma
* @since 5.0 * @since 5.0
*/ */
public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMapping public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMapping
@ -171,18 +173,27 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi
} }
/** /**
* Delegates to {@link #createRequestMappingInfo(RequestMapping, RequestCondition)}, * Delegates to {@link #createRequestMappingInfo(RequestMapping, RequestCondition)}
* supplying the appropriate custom {@link RequestCondition} depending on whether * or {@link #createRequestMappingInfo(HttpExchange, RequestCondition)},
* depending on which annotation is found on the element being processed,
* and supplying the appropriate custom {@link RequestCondition} depending on whether
* the supplied {@code annotatedElement} is a class or method. * the supplied {@code annotatedElement} is a class or method.
* @see #getCustomTypeCondition(Class) * @see #getCustomTypeCondition(Class)
* @see #getCustomMethodCondition(Method) * @see #getCustomMethodCondition(Method)
*/ */
@Nullable @Nullable
private RequestMappingInfo createRequestMappingInfo(AnnotatedElement element) { private RequestMappingInfo createRequestMappingInfo(AnnotatedElement element) {
RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(element, RequestMapping.class);
RequestCondition<?> condition = (element instanceof Class<?> clazz ? RequestCondition<?> condition = (element instanceof Class<?> clazz ?
getCustomTypeCondition(clazz) : getCustomMethodCondition((Method) element)); getCustomTypeCondition(clazz) : getCustomMethodCondition((Method) element));
return (requestMapping != null ? createRequestMappingInfo(requestMapping, condition) : null); RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(element, RequestMapping.class);
if(requestMapping != null){
return createRequestMappingInfo(requestMapping, condition);
}
HttpExchange httpExchange = AnnotatedElementUtils.findMergedAnnotation(element, HttpExchange.class);
if(httpExchange != null){
return createRequestMappingInfo(httpExchange, condition);
}
return null;
} }
/** /**
@ -246,6 +257,28 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi
return builder.options(this.config).build(); return builder.options(this.config).build();
} }
/**
* Create a {@link RequestMappingInfo} from the supplied
* {@link HttpExchange @HttpExchange} annotation, which is either
* a directly declared annotation, a meta-annotation, or the synthesized
* result of merging annotation attributes within an annotation hierarchy.
*/
protected RequestMappingInfo createRequestMappingInfo(
HttpExchange httpExchange,
@Nullable RequestCondition<?> customCondition) {
RequestMappingInfo.Builder builder = RequestMappingInfo
.paths(resolveEmbeddedValuesInPatterns(
toTextArray(httpExchange.value())))
.methods(toMethodArray(httpExchange.method()))
.consumes(toTextArray(httpExchange.contentType()))
.produces(httpExchange.accept());
if (customCondition != null) {
builder.customCondition(customCondition);
}
return builder.options(this.config).build();
}
/** /**
* Resolve placeholder values in the given array of patterns. * Resolve placeholder values in the given array of patterns.
* @return a new array with updated patterns * @return a new array with updated patterns
@ -263,6 +296,24 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi
} }
} }
private static String[] toTextArray(String string) {
if (StringUtils.hasText(string)) {
return new String[] { string };
}
return new String[] {};
}
private static RequestMethod[] toMethodArray(String method) {
RequestMethod requestMethod = null;
if (StringUtils.hasText(method)) {
requestMethod = RequestMethod.resolve(method);
}
return requestMethod != null ? new RequestMethod[] { requestMethod }
: new RequestMethod[] {};
}
@Override @Override
public void registerMapping(RequestMappingInfo mapping, Object handler, Method method) { public void registerMapping(RequestMappingInfo mapping, Object handler, Method method) {
super.registerMapping(mapping, handler, method); super.registerMapping(mapping, handler, method);

View File

@ -44,8 +44,10 @@ import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.support.StaticWebApplicationContext; import org.springframework.web.context.support.StaticWebApplicationContext;
import org.springframework.web.method.HandlerTypePredicate; import org.springframework.web.method.HandlerTypePredicate;
import org.springframework.web.reactive.result.condition.ConsumesRequestCondition; import org.springframework.web.reactive.result.condition.ConsumesRequestCondition;
import org.springframework.web.reactive.result.condition.MediaTypeExpression;
import org.springframework.web.reactive.result.condition.PatternsRequestCondition; import org.springframework.web.reactive.result.condition.PatternsRequestCondition;
import org.springframework.web.reactive.result.method.RequestMappingInfo; import org.springframework.web.reactive.result.method.RequestMappingInfo;
import org.springframework.web.service.annotation.HttpExchange;
import org.springframework.web.util.pattern.PathPattern; import org.springframework.web.util.pattern.PathPattern;
import org.springframework.web.util.pattern.PathPatternParser; import org.springframework.web.util.pattern.PathPatternParser;
@ -56,6 +58,7 @@ import static org.mockito.Mockito.mock;
* Unit tests for {@link RequestMappingHandlerMapping}. * Unit tests for {@link RequestMappingHandlerMapping}.
* *
* @author Rossen Stoyanchev * @author Rossen Stoyanchev
* @author Olga Maciaszek-Sharma
*/ */
class RequestMappingHandlerMappingTests { class RequestMappingHandlerMappingTests {
@ -150,6 +153,52 @@ class RequestMappingHandlerMappingTests {
assertComposedAnnotationMapping(RequestMethod.PATCH); assertComposedAnnotationMapping(RequestMethod.PATCH);
} }
@SuppressWarnings("DataFlowIssue")
@Test
void httpExchangeWithDefaultValues() throws NoSuchMethodException {
this.handlerMapping.afterPropertiesSet();
RequestMappingInfo mappingInfo = this.handlerMapping.getMappingForMethod(
HttpExchangeController.class.getMethod("defaultValuesExchange"),
HttpExchangeController.class);
assertThat(mappingInfo.getPatternsCondition().getPatterns())
.extracting(PathPattern::toString)
.containsOnly("/exchange");
assertThat(mappingInfo.getMethodsCondition().getMethods()).isEmpty();
assertThat(mappingInfo.getParamsCondition().getExpressions()).isEmpty();
assertThat(mappingInfo.getHeadersCondition().getExpressions()).isEmpty();
assertThat(mappingInfo.getConsumesCondition().getExpressions()).isEmpty();
assertThat(mappingInfo.getProducesCondition().getExpressions()).isEmpty();
}
@SuppressWarnings("DataFlowIssue")
@Test
void httpExchangeWithCustomValues() throws NoSuchMethodException {
this.handlerMapping.afterPropertiesSet();
RequestMappingHandlerMapping mapping = new RequestMappingHandlerMapping();
mapping.setApplicationContext(new StaticWebApplicationContext());
mapping.afterPropertiesSet();
RequestMappingInfo mappingInfo = mapping.getMappingForMethod(
HttpExchangeController.class.getMethod("customValuesExchange"),
HttpExchangeController.class);
assertThat(mappingInfo.getPatternsCondition().getPatterns())
.extracting(PathPattern::toString)
.containsOnly("/exchange/custom");
assertThat(mappingInfo.getMethodsCondition().getMethods())
.containsOnly(RequestMethod.POST);
assertThat(mappingInfo.getParamsCondition().getExpressions()).isEmpty();
assertThat(mappingInfo.getHeadersCondition().getExpressions()).isEmpty();
assertThat(mappingInfo.getConsumesCondition().getExpressions())
.extracting(MediaTypeExpression::getMediaType)
.containsOnly(MediaType.APPLICATION_JSON);
assertThat(mappingInfo.getProducesCondition().getExpressions())
.extracting(MediaTypeExpression::getMediaType)
.containsOnly(MediaType.valueOf("text/plain;charset=UTF-8"));
}
private RequestMappingInfo assertComposedAnnotationMapping(RequestMethod requestMethod) throws Exception { private RequestMappingInfo assertComposedAnnotationMapping(RequestMethod requestMethod) throws Exception {
String methodName = requestMethod.name().toLowerCase(); String methodName = requestMethod.name().toLowerCase();
@ -238,4 +287,16 @@ class RequestMappingHandlerMappingTests {
} }
} }
@RestController
@HttpExchange("/exchange")
static class HttpExchangeController {
@HttpExchange
public void defaultValuesExchange(){}
@HttpExchange(value = "/custom", accept = "text/plain;charset=UTF-8",
method = "POST", contentType = "application/json")
public void customValuesExchange(){}
}
} }

View File

@ -35,6 +35,7 @@ import org.springframework.lang.Nullable;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils; import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.util.StringValueResolver; import org.springframework.util.StringValueResolver;
import org.springframework.web.accept.ContentNegotiationManager; import org.springframework.web.accept.ContentNegotiationManager;
import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.CrossOrigin;
@ -43,6 +44,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.method.HandlerMethod; import org.springframework.web.method.HandlerMethod;
import org.springframework.web.service.annotation.HttpExchange;
import org.springframework.web.servlet.handler.MatchableHandlerMapping; import org.springframework.web.servlet.handler.MatchableHandlerMapping;
import org.springframework.web.servlet.handler.RequestMatchResult; import org.springframework.web.servlet.handler.RequestMatchResult;
import org.springframework.web.servlet.mvc.condition.AbstractRequestCondition; import org.springframework.web.servlet.mvc.condition.AbstractRequestCondition;
@ -71,6 +73,7 @@ import org.springframework.web.util.pattern.PathPatternParser;
* @author Arjen Poutsma * @author Arjen Poutsma
* @author Rossen Stoyanchev * @author Rossen Stoyanchev
* @author Sam Brannen * @author Sam Brannen
* @author Olga Maciaszek-Sharma
* @since 3.1 * @since 3.1
*/ */
public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMapping public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMapping
@ -331,18 +334,27 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi
} }
/** /**
* Delegates to {@link #createRequestMappingInfo(RequestMapping, RequestCondition)}, * Delegates to {@link #createRequestMappingInfo(RequestMapping, RequestCondition)}
* supplying the appropriate custom {@link RequestCondition} depending on whether * or {@link #createRequestMappingInfo(HttpExchange, RequestCondition)},
* depending on which annotation is found on the element being processed,
* and supplying the appropriate custom {@link RequestCondition} depending on whether
* the supplied {@code annotatedElement} is a class or method. * the supplied {@code annotatedElement} is a class or method.
* @see #getCustomTypeCondition(Class) * @see #getCustomTypeCondition(Class)
* @see #getCustomMethodCondition(Method) * @see #getCustomMethodCondition(Method)
*/ */
@Nullable @Nullable
private RequestMappingInfo createRequestMappingInfo(AnnotatedElement element) { private RequestMappingInfo createRequestMappingInfo(AnnotatedElement element) {
RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(element, RequestMapping.class);
RequestCondition<?> condition = (element instanceof Class<?> clazz ? RequestCondition<?> condition = (element instanceof Class<?> clazz ?
getCustomTypeCondition(clazz) : getCustomMethodCondition((Method) element)); getCustomTypeCondition(clazz) : getCustomMethodCondition((Method) element));
return (requestMapping != null ? createRequestMappingInfo(requestMapping, condition) : null); RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(element, RequestMapping.class);
if(requestMapping != null){
return createRequestMappingInfo(requestMapping, condition);
}
HttpExchange httpExchange = AnnotatedElementUtils.findMergedAnnotation(element, HttpExchange.class);
if(httpExchange != null){
return createRequestMappingInfo(httpExchange, condition);
}
return null;
} }
/** /**
@ -400,6 +412,28 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi
return builder.options(this.config).build(); return builder.options(this.config).build();
} }
/**
* Create a {@link RequestMappingInfo} from the supplied
* {@link HttpExchange @HttpExchange} annotation, which is either
* a directly declared annotation, a meta-annotation, or the synthesized
* result of merging annotation attributes within an annotation hierarchy.
*/
protected RequestMappingInfo createRequestMappingInfo(
HttpExchange httpExchange,
@Nullable RequestCondition<?> customCondition) {
RequestMappingInfo.Builder builder = RequestMappingInfo
.paths(resolveEmbeddedValuesInPatterns(
toTextArray(httpExchange.value())))
.methods(toMethodArray(httpExchange.method()))
.consumes(toTextArray(httpExchange.contentType()))
.produces(httpExchange.accept());
if (customCondition != null) {
builder.customCondition(customCondition);
}
return builder.options(this.config).build();
}
/** /**
* Resolve placeholder values in the given array of patterns. * Resolve placeholder values in the given array of patterns.
* @return a new array with updated patterns * @return a new array with updated patterns
@ -417,6 +451,24 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi
} }
} }
private static String[] toTextArray(String string) {
if (StringUtils.hasText(string)) {
return new String[] { string };
}
return new String[] {};
}
private static RequestMethod[] toMethodArray(String method) {
RequestMethod requestMethod = null;
if (StringUtils.hasText(method)) {
requestMethod = RequestMethod.resolve(method);
}
return requestMethod != null ? new RequestMethod[] { requestMethod }
: new RequestMethod[] {};
}
@Override @Override
public void registerMapping(RequestMappingInfo mapping, Object handler, Method method) { public void registerMapping(RequestMappingInfo mapping, Object handler, Method method) {
super.registerMapping(mapping, handler, method); super.registerMapping(mapping, handler, method);

View File

@ -47,11 +47,14 @@ import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.support.StaticWebApplicationContext; import org.springframework.web.context.support.StaticWebApplicationContext;
import org.springframework.web.method.HandlerTypePredicate; import org.springframework.web.method.HandlerTypePredicate;
import org.springframework.web.service.annotation.HttpExchange;
import org.springframework.web.servlet.handler.PathPatternsParameterizedTest; import org.springframework.web.servlet.handler.PathPatternsParameterizedTest;
import org.springframework.web.servlet.mvc.condition.ConsumesRequestCondition; import org.springframework.web.servlet.mvc.condition.ConsumesRequestCondition;
import org.springframework.web.servlet.mvc.condition.MediaTypeExpression;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo; import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.testfixture.servlet.MockHttpServletRequest; import org.springframework.web.testfixture.servlet.MockHttpServletRequest;
import org.springframework.web.util.ServletRequestPathUtils; import org.springframework.web.util.ServletRequestPathUtils;
import org.springframework.web.util.pattern.PathPattern;
import org.springframework.web.util.pattern.PathPatternParser; import org.springframework.web.util.pattern.PathPatternParser;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
@ -62,6 +65,7 @@ import static org.mockito.Mockito.mock;
* *
* @author Rossen Stoyanchev * @author Rossen Stoyanchev
* @author Sam Brannen * @author Sam Brannen
* @author Olga Maciaszek-Sharma
*/ */
public class RequestMappingHandlerMappingTests { public class RequestMappingHandlerMappingTests {
@ -278,6 +282,53 @@ public class RequestMappingHandlerMappingTests {
assertComposedAnnotationMapping(RequestMethod.PATCH); assertComposedAnnotationMapping(RequestMethod.PATCH);
} }
@SuppressWarnings("DataFlowIssue")
@Test
void httpExchangeWithDefaultValues() throws NoSuchMethodException {
RequestMappingHandlerMapping mapping = new RequestMappingHandlerMapping();
mapping.setApplicationContext(new StaticWebApplicationContext());
mapping.afterPropertiesSet();
RequestMappingInfo mappingInfo = mapping.getMappingForMethod(
HttpExchangeController.class.getMethod("defaultValuesExchange"),
HttpExchangeController.class);
assertThat(mappingInfo.getPathPatternsCondition().getPatterns())
.extracting(PathPattern::toString)
.containsOnly("/exchange");
assertThat(mappingInfo.getMethodsCondition().getMethods()).isEmpty();
assertThat(mappingInfo.getParamsCondition().getExpressions()).isEmpty();
assertThat(mappingInfo.getHeadersCondition().getExpressions()).isEmpty();
assertThat(mappingInfo.getConsumesCondition().getExpressions()).isEmpty();
assertThat(mappingInfo.getProducesCondition().getExpressions()).isEmpty();
}
@SuppressWarnings("DataFlowIssue")
@Test
void httpExchangeWithCustomValues() throws NoSuchMethodException {
RequestMappingHandlerMapping mapping = new RequestMappingHandlerMapping();
mapping.setApplicationContext(new StaticWebApplicationContext());
mapping.afterPropertiesSet();
RequestMappingInfo mappingInfo = mapping.getMappingForMethod(
HttpExchangeController.class.getMethod("customValuesExchange"),
HttpExchangeController.class);
assertThat(mappingInfo.getPathPatternsCondition().getPatterns())
.extracting(PathPattern::toString)
.containsOnly("/exchange/custom");
assertThat(mappingInfo.getMethodsCondition().getMethods())
.containsOnly(RequestMethod.POST);
assertThat(mappingInfo.getParamsCondition().getExpressions()).isEmpty();
assertThat(mappingInfo.getHeadersCondition().getExpressions()).isEmpty();
assertThat(mappingInfo.getConsumesCondition().getExpressions())
.extracting(MediaTypeExpression::getMediaType)
.containsOnly(MediaType.APPLICATION_JSON);
assertThat(mappingInfo.getProducesCondition().getExpressions())
.extracting(MediaTypeExpression::getMediaType)
.containsOnly(MediaType.valueOf("text/plain;charset=UTF-8"));
}
private RequestMappingInfo assertComposedAnnotationMapping(RequestMethod requestMethod) throws Exception { private RequestMappingInfo assertComposedAnnotationMapping(RequestMethod requestMethod) throws Exception {
RequestMappingHandlerMapping mapping = new RequestMappingHandlerMapping(); RequestMappingHandlerMapping mapping = new RequestMappingHandlerMapping();
@ -367,6 +418,18 @@ public class RequestMappingHandlerMappingTests {
} }
} }
@RestController
@HttpExchange("/exchange")
static class HttpExchangeController {
@HttpExchange
public void defaultValuesExchange(){}
@HttpExchange(value = "/custom", accept = "text/plain;charset=UTF-8",
method = "POST", contentType = "application/json")
public void customValuesExchange(){}
}
private static class Foo { private static class Foo {
} }