Support @HttpExchange for server-side handling
See gh-30980
This commit is contained in:
parent
646fd3edcc
commit
d1d5b54f12
|
@ -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.
|
||||
|
||||
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
[.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.
|
||||
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
[.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.
|
||||
|
||||
|
||||
|
|
|
@ -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.method.RequestMappingInfo;
|
||||
import org.springframework.web.reactive.result.method.RequestMappingInfoHandlerMapping;
|
||||
import org.springframework.web.service.annotation.HttpExchange;
|
||||
|
||||
/**
|
||||
* An extension of {@link RequestMappingInfoHandlerMapping} that creates
|
||||
|
@ -54,6 +55,7 @@ import org.springframework.web.reactive.result.method.RequestMappingInfoHandlerM
|
|||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Sam Brannen
|
||||
* @author Olga Maciaszek-Sharma
|
||||
* @since 5.0
|
||||
*/
|
||||
public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMapping
|
||||
|
@ -171,18 +173,27 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi
|
|||
}
|
||||
|
||||
/**
|
||||
* Delegates to {@link #createRequestMappingInfo(RequestMapping, RequestCondition)},
|
||||
* supplying the appropriate custom {@link RequestCondition} depending on whether
|
||||
* Delegates to {@link #createRequestMappingInfo(RequestMapping, RequestCondition)}
|
||||
* 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.
|
||||
* @see #getCustomTypeCondition(Class)
|
||||
* @see #getCustomMethodCondition(Method)
|
||||
*/
|
||||
@Nullable
|
||||
private RequestMappingInfo createRequestMappingInfo(AnnotatedElement element) {
|
||||
RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(element, RequestMapping.class);
|
||||
RequestCondition<?> condition = (element instanceof Class<?> clazz ?
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @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
|
||||
public void registerMapping(RequestMappingInfo mapping, Object handler, Method method) {
|
||||
super.registerMapping(mapping, handler, method);
|
||||
|
|
|
@ -44,8 +44,10 @@ import org.springframework.web.bind.annotation.RestController;
|
|||
import org.springframework.web.context.support.StaticWebApplicationContext;
|
||||
import org.springframework.web.method.HandlerTypePredicate;
|
||||
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.method.RequestMappingInfo;
|
||||
import org.springframework.web.service.annotation.HttpExchange;
|
||||
import org.springframework.web.util.pattern.PathPattern;
|
||||
import org.springframework.web.util.pattern.PathPatternParser;
|
||||
|
||||
|
@ -56,6 +58,7 @@ import static org.mockito.Mockito.mock;
|
|||
* Unit tests for {@link RequestMappingHandlerMapping}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Olga Maciaszek-Sharma
|
||||
*/
|
||||
class RequestMappingHandlerMappingTests {
|
||||
|
||||
|
@ -150,6 +153,52 @@ class RequestMappingHandlerMappingTests {
|
|||
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 {
|
||||
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(){}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -35,6 +35,7 @@ import org.springframework.lang.Nullable;
|
|||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.util.StringValueResolver;
|
||||
import org.springframework.web.accept.ContentNegotiationManager;
|
||||
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.cors.CorsConfiguration;
|
||||
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.RequestMatchResult;
|
||||
import org.springframework.web.servlet.mvc.condition.AbstractRequestCondition;
|
||||
|
@ -71,6 +73,7 @@ import org.springframework.web.util.pattern.PathPatternParser;
|
|||
* @author Arjen Poutsma
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Sam Brannen
|
||||
* @author Olga Maciaszek-Sharma
|
||||
* @since 3.1
|
||||
*/
|
||||
public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMapping
|
||||
|
@ -331,18 +334,27 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi
|
|||
}
|
||||
|
||||
/**
|
||||
* Delegates to {@link #createRequestMappingInfo(RequestMapping, RequestCondition)},
|
||||
* supplying the appropriate custom {@link RequestCondition} depending on whether
|
||||
* Delegates to {@link #createRequestMappingInfo(RequestMapping, RequestCondition)}
|
||||
* 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.
|
||||
* @see #getCustomTypeCondition(Class)
|
||||
* @see #getCustomMethodCondition(Method)
|
||||
*/
|
||||
@Nullable
|
||||
private RequestMappingInfo createRequestMappingInfo(AnnotatedElement element) {
|
||||
RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(element, RequestMapping.class);
|
||||
RequestCondition<?> condition = (element instanceof Class<?> clazz ?
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @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
|
||||
public void registerMapping(RequestMappingInfo mapping, Object handler, Method method) {
|
||||
super.registerMapping(mapping, handler, method);
|
||||
|
|
|
@ -47,11 +47,14 @@ import org.springframework.web.bind.annotation.RequestMethod;
|
|||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.context.support.StaticWebApplicationContext;
|
||||
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.mvc.condition.ConsumesRequestCondition;
|
||||
import org.springframework.web.servlet.mvc.condition.MediaTypeExpression;
|
||||
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
|
||||
import org.springframework.web.testfixture.servlet.MockHttpServletRequest;
|
||||
import org.springframework.web.util.ServletRequestPathUtils;
|
||||
import org.springframework.web.util.pattern.PathPattern;
|
||||
import org.springframework.web.util.pattern.PathPatternParser;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
@ -62,6 +65,7 @@ import static org.mockito.Mockito.mock;
|
|||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Sam Brannen
|
||||
* @author Olga Maciaszek-Sharma
|
||||
*/
|
||||
public class RequestMappingHandlerMappingTests {
|
||||
|
||||
|
@ -278,6 +282,53 @@ public class RequestMappingHandlerMappingTests {
|
|||
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 {
|
||||
|
||||
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 {
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue