Add Coroutines support for WebClient and WebFlux.fn

This commit is the first part of a more complete Coroutines
support coming in Spring Framework 5.2. It introduces suspendable
Kotlin extensions for Mono based methods in WebFlux classes like
WebClient, ServerRequest, ServerResponse as well as a Coroutines
router usable via `coRouter { }`.

Coroutines extensions use `await` prefix or `AndAwait` suffix,
and most are using names close to their Reactive counterparts,
except `exchange` in `WebClient.RequestHeadersSpec`
which translates to `awaitResponse`.

Upcoming expected changes are:
 - Leverage `Dispatchers.Unconfined` (Kotlin/kotlinx.coroutines#972)
 - Expose extensions for `Flux` based API (Kotlin/kotlinx.coroutines#254)
 - Introduce interop with `CoroutineContext` (Kotlin/kotlinx.coroutines#284)
 - Support Coroutines in `ReactiveAdapterRegistry`
 - Support Coroutines for WebFlux annotated controllers
 - Fix return type of Kotlin suspending functions (gh-21058)

See gh-19975
This commit is contained in:
Sebastien Deleuze 2019-02-18 09:11:12 +01:00
parent 04bb114f05
commit 19f792db66
16 changed files with 1182 additions and 23 deletions

View File

@ -40,6 +40,8 @@ dependencies {
optional("org.jetbrains.kotlin:kotlin-reflect:${kotlinVersion}")
optional("org.jetbrains.kotlin:kotlin-stdlib:${kotlinVersion}")
optional("com.google.protobuf:protobuf-java-util:3.6.1")
optional("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.1.1")
optional("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:1.1.1")
testCompile("javax.xml.bind:jaxb-api:2.3.1")
testCompile("com.fasterxml:aalto-xml:1.1.1")
testCompile("org.hibernate:hibernate-validator:6.0.14.Final")

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2017 the original author or authors.
* Copyright 2002-2019 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.
@ -16,6 +16,7 @@
package org.springframework.web.reactive.function.client
import kotlinx.coroutines.reactive.awaitSingle
import org.springframework.core.ParameterizedTypeReference
import org.springframework.http.ResponseEntity
import reactor.core.publisher.Flux
@ -64,3 +65,30 @@ inline fun <reified T : Any> ClientResponse.toEntity(): Mono<ResponseEntity<T>>
*/
inline fun <reified T : Any> ClientResponse.toEntityList(): Mono<ResponseEntity<List<T>>> =
toEntityList(object : ParameterizedTypeReference<T>() {})
/**
* Coroutines variant of [ClientResponse.bodyToMono].
*
* @author Sebastien Deleuze
* @since 5.2
*/
suspend inline fun <reified T : Any> ClientResponse.awaitBody(): T =
bodyToMono<T>().awaitSingle()
/**
* Coroutines variant of [ClientResponse.toEntity].
*
* @author Sebastien Deleuze
* @since 5.2
*/
suspend inline fun <reified T : Any> ClientResponse.awaitEntity(): ResponseEntity<T> =
toEntity<T>().awaitSingle()
/**
* Coroutines variant of [ClientResponse.toEntityList].
*
* @author Sebastien Deleuze
* @since 5.2
*/
suspend inline fun <reified T : Any> ClientResponse.awaitEntityList(): ResponseEntity<List<T>> =
toEntityList<T>().awaitSingle()

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2017 the original author or authors.
* Copyright 2002-2019 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.
@ -16,6 +16,9 @@
package org.springframework.web.reactive.function.client
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.reactive.awaitSingle
import kotlinx.coroutines.reactor.mono
import org.reactivestreams.Publisher
import org.springframework.core.ParameterizedTypeReference
import org.springframework.web.reactive.function.client.WebClient.RequestBodySpec
@ -57,3 +60,30 @@ inline fun <reified T : Any> WebClient.ResponseSpec.bodyToMono(): Mono<T> =
*/
inline fun <reified T : Any> WebClient.ResponseSpec.bodyToFlux(): Flux<T> =
bodyToFlux(object : ParameterizedTypeReference<T>() {})
/**
* Coroutines variant of [WebClient.RequestHeadersSpec.exchange].
*
* @author Sebastien Deleuze
* @since 5.2
*/
suspend fun WebClient.RequestHeadersSpec<out WebClient.RequestHeadersSpec<*>>.awaitResponse(): ClientResponse =
exchange().awaitSingle()
/**
* Coroutines variant of [WebClient.RequestBodySpec.body].
*
* @author Sebastien Deleuze
* @since 5.2
*/
inline fun <reified T: Any> WebClient.RequestBodySpec.body(crossinline supplier: suspend () -> T)
= body(GlobalScope.mono { supplier.invoke() })
/**
* Coroutines variant of [WebClient.ResponseSpec.bodyToMono].
*
* @author Sebastien Deleuze
* @since 5.2
*/
suspend inline fun <reified T : Any> WebClient.ResponseSpec.awaitBody() : T =
bodyToMono<T>().awaitSingle()

View File

@ -0,0 +1,488 @@
/*
* Copyright 2002-2019 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
*
* http://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.reactive.function.server
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.reactor.mono
import org.springframework.core.io.Resource
import org.springframework.http.HttpMethod
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
import org.springframework.web.reactive.function.server.RouterFunctions.nest
import java.net.URI
/**
* Coroutines variant of [RouterFunctionDsl].
*
* @author Sebastien Deleuze
* @since 5.2
*/
open class CoRouterFunctionDsl(private val init: (CoRouterFunctionDsl.() -> Unit)): () -> RouterFunction<ServerResponse> {
private val builder = RouterFunctions.route()
/**
* Return a composed request predicate that tests against both this predicate AND
* the [other] predicate (String processed as a path predicate). When evaluating the
* composed predicate, if this predicate is `false`, then the [other] predicate is not
* evaluated.
* @see RequestPredicate.and
* @see RequestPredicates.path
*/
infix fun RequestPredicate.and(other: String): RequestPredicate = this.and(path(other))
/**
* Return a composed request predicate that tests against both this predicate OR
* the [other] predicate (String processed as a path predicate). When evaluating the
* composed predicate, if this predicate is `true`, then the [other] predicate is not
* evaluated.
* @see RequestPredicate.or
* @see RequestPredicates.path
*/
infix fun RequestPredicate.or(other: String): RequestPredicate = this.or(path(other))
/**
* Return a composed request predicate that tests against both this predicate (String
* processed as a path predicate) AND the [other] predicate. When evaluating the
* composed predicate, if this predicate is `false`, then the [other] predicate is not
* evaluated.
* @see RequestPredicate.and
* @see RequestPredicates.path
*/
infix fun String.and(other: RequestPredicate): RequestPredicate = path(this).and(other)
/**
* Return a composed request predicate that tests against both this predicate (String
* processed as a path predicate) OR the [other] predicate. When evaluating the
* composed predicate, if this predicate is `true`, then the [other] predicate is not
* evaluated.
* @see RequestPredicate.or
* @see RequestPredicates.path
*/
infix fun String.or(other: RequestPredicate): RequestPredicate = path(this).or(other)
/**
* Return a composed request predicate that tests against both this predicate AND
* the [other] predicate. When evaluating the composed predicate, if this
* predicate is `false`, then the [other] predicate is not evaluated.
* @see RequestPredicate.and
*/
infix fun RequestPredicate.and(other: RequestPredicate): RequestPredicate = this.and(other)
/**
* Return a composed request predicate that tests against both this predicate OR
* the [other] predicate. When evaluating the composed predicate, if this
* predicate is `true`, then the [other] predicate is not evaluated.
* @see RequestPredicate.or
*/
infix fun RequestPredicate.or(other: RequestPredicate): RequestPredicate = this.or(other)
/**
* Return a predicate that represents the logical negation of this predicate.
*/
operator fun RequestPredicate.not(): RequestPredicate = this.negate()
/**
* Route to the given router function if the given request predicate applies. This
* method can be used to create *nested routes*, where a group of routes share a
* common path (prefix), header, or other request predicate.
* @see RouterFunctions.nest
*/
fun RequestPredicate.nest(r: (CoRouterFunctionDsl.() -> Unit)) {
builder.add(nest(this, CoRouterFunctionDsl(r).invoke()))
}
/**
* Route to the given router function if the given request predicate (String
* processed as a path predicate) applies. This method can be used to create
* *nested routes*, where a group of routes share a common path
* (prefix), header, or other request predicate.
* @see RouterFunctions.nest
* @see RequestPredicates.path
*/
fun String.nest(r: (CoRouterFunctionDsl.() -> Unit)) = path(this).nest(r)
/**
* Route to the given handler function if the given request predicate applies.
* @see RouterFunctions.route
*/
fun GET(pattern: String, f: suspend (ServerRequest) -> ServerResponse) {
builder.GET(pattern, asHandlerFunction(f))
}
/**
* Return a [RequestPredicate] that matches if request's HTTP method is `GET`
* and the given `pattern` matches against the request path.
* @see RequestPredicates.GET
*/
fun GET(pattern: String): RequestPredicate = RequestPredicates.GET(pattern)
/**
* Route to the given handler function if the given request predicate applies.
* @see RouterFunctions.route
*/
fun HEAD(pattern: String, f: suspend (ServerRequest) -> ServerResponse) {
builder.HEAD(pattern, asHandlerFunction(f))
}
/**
* Return a [RequestPredicate] that matches if request's HTTP method is `HEAD`
* and the given `pattern` matches against the request path.
* @see RequestPredicates.HEAD
*/
fun HEAD(pattern: String): RequestPredicate = RequestPredicates.HEAD(pattern)
/**
* Route to the given handler function if the given `POST` predicate applies.
* @see RouterFunctions.route
*/
fun POST(pattern: String, f: suspend (ServerRequest) -> ServerResponse) {
builder.POST(pattern, asHandlerFunction(f))
}
/**
* Return a [RequestPredicate] that matches if request's HTTP method is `POST`
* and the given `pattern` matches against the request path.
* @see RequestPredicates.POST
*/
fun POST(pattern: String): RequestPredicate = RequestPredicates.POST(pattern)
/**
* Route to the given handler function if the given `PUT` predicate applies.
* @see RouterFunctions.route
*/
fun PUT(pattern: String, f: suspend (ServerRequest) -> ServerResponse) {
builder.PUT(pattern, asHandlerFunction(f))
}
/**
* Return a [RequestPredicate] that matches if request's HTTP method is `PUT`
* and the given `pattern` matches against the request path.
* @see RequestPredicates.PUT
*/
fun PUT(pattern: String): RequestPredicate = RequestPredicates.PUT(pattern)
/**
* Route to the given handler function if the given `PATCH` predicate applies.
* @see RouterFunctions.route
*/
fun PATCH(pattern: String, f: suspend (ServerRequest) -> ServerResponse) {
builder.PATCH(pattern, asHandlerFunction(f))
}
/**
* Return a [RequestPredicate] that matches if request's HTTP method is `PATCH`
* and the given `pattern` matches against the request path.
* @param pattern the path pattern to match against
* @return a predicate that matches if the request method is `PATCH` and if the given pattern
* matches against the request path
*/
fun PATCH(pattern: String): RequestPredicate = RequestPredicates.PATCH(pattern)
/**
* Route to the given handler function if the given `DELETE` predicate applies.
* @see RouterFunctions.route
*/
fun DELETE(pattern: String, f: suspend (ServerRequest) -> ServerResponse) {
builder.DELETE(pattern, asHandlerFunction(f))
}
/**
* Return a [RequestPredicate] that matches if request's HTTP method is `DELETE`
* and the given `pattern` matches against the request path.
* @param pattern the path pattern to match against
* @return a predicate that matches if the request method is `DELETE` and if the given pattern
* matches against the request path
*/
fun DELETE(pattern: String): RequestPredicate = RequestPredicates.DELETE(pattern)
/**
* Route to the given handler function if the given OPTIONS predicate applies.
* @see RouterFunctions.route
*/
fun OPTIONS(pattern: String, f: suspend (ServerRequest) -> ServerResponse) {
builder.OPTIONS(pattern, asHandlerFunction(f))
}
/**
* Return a [RequestPredicate] that matches if request's HTTP method is `OPTIONS`
* and the given `pattern` matches against the request path.
* @param pattern the path pattern to match against
* @return a predicate that matches if the request method is `OPTIONS` and if the given pattern
* matches against the request path
*/
fun OPTIONS(pattern: String): RequestPredicate = RequestPredicates.OPTIONS(pattern)
/**
* Route to the given handler function if the given accept predicate applies.
* @see RouterFunctions.route
*/
fun accept(mediaType: MediaType, f: suspend (ServerRequest) -> ServerResponse) {
builder.add(RouterFunctions.route(RequestPredicates.accept(mediaType), asHandlerFunction(f)))
}
/**
* Return a [RequestPredicate] that tests if the request's
* [accept][ServerRequest.Headers.accept] } header is
* [compatible][MediaType.isCompatibleWith] with any of the given media types.
* @param mediaType the media types to match the request's accept header against
* @return a predicate that tests the request's accept header against the given media types
*/
fun accept(vararg mediaType: MediaType): RequestPredicate = RequestPredicates.accept(*mediaType)
/**
* Route to the given handler function if the given contentType predicate applies.
* @see RouterFunctions.route
*/
fun contentType(mediaType: MediaType, f: suspend (ServerRequest) -> ServerResponse) {
builder.add(RouterFunctions.route(RequestPredicates.contentType(mediaType), asHandlerFunction(f)))
}
/**
* Return a [RequestPredicate] that tests if the request's
* [content type][ServerRequest.Headers.contentType] is
* [included][MediaType.includes] by any of the given media types.
* @param mediaTypes the media types to match the request's content type against
* @return a predicate that tests the request's content type against the given media types
*/
fun contentType(vararg mediaTypes: MediaType): RequestPredicate = RequestPredicates.contentType(*mediaTypes)
/**
* Route to the given handler function if the given headers predicate applies.
* @see RouterFunctions.route
*/
fun headers(headersPredicate: (ServerRequest.Headers) -> Boolean, f: suspend (ServerRequest) -> ServerResponse) {
builder.add(RouterFunctions.route(RequestPredicates.headers(headersPredicate), asHandlerFunction(f)))
}
/**
* Return a [RequestPredicate] that tests the request's headers against the given headers predicate.
* @param headersPredicate a predicate that tests against the request headers
* @return a predicate that tests against the given header predicate
*/
fun headers(headersPredicate: (ServerRequest.Headers) -> Boolean): RequestPredicate =
RequestPredicates.headers(headersPredicate)
/**
* Route to the given handler function if the given method predicate applies.
* @see RouterFunctions.route
*/
fun method(httpMethod: HttpMethod, f: suspend (ServerRequest) -> ServerResponse) {
builder.add(RouterFunctions.route(RequestPredicates.method(httpMethod), asHandlerFunction(f)))
}
/**
* Return a [RequestPredicate] that tests against the given HTTP method.
* @param httpMethod the HTTP method to match to
* @return a predicate that tests against the given HTTP method
*/
fun method(httpMethod: HttpMethod): RequestPredicate = RequestPredicates.method(httpMethod)
/**
* Route to the given handler function if the given path predicate applies.
* @see RouterFunctions.route
*/
fun path(pattern: String, f: suspend (ServerRequest) -> ServerResponse) {
builder.add(RouterFunctions.route(RequestPredicates.path(pattern), asHandlerFunction(f)))
}
/**
* Return a [RequestPredicate] that tests the request path against the given path pattern.
* @see RequestPredicates.path
*/
fun path(pattern: String): RequestPredicate = RequestPredicates.path(pattern)
/**
* Route to the given handler function if the given pathExtension predicate applies.
* @see RouterFunctions.route
*/
fun pathExtension(extension: String, f: suspend (ServerRequest) -> ServerResponse) {
builder.add(RouterFunctions.route(RequestPredicates.pathExtension(extension), asHandlerFunction(f)))
}
/**
* Return a [RequestPredicate] that matches if the request's path has the given extension.
* @param extension the path extension to match against, ignoring case
* @return a predicate that matches if the request's path has the given file extension
*/
fun pathExtension(extension: String): RequestPredicate = RequestPredicates.pathExtension(extension)
/**
* Route to the given handler function if the given pathExtension predicate applies.
* @see RouterFunctions.route
*/
fun pathExtension(predicate: (String) -> Boolean, f: suspend (ServerRequest) -> ServerResponse) {
builder.add(RouterFunctions.route(RequestPredicates.pathExtension(predicate), asHandlerFunction(f)))
}
/**
* Return a [RequestPredicate] that matches if the request's path matches the given
* predicate.
* @see RequestPredicates.pathExtension
*/
fun pathExtension(predicate: (String) -> Boolean): RequestPredicate =
RequestPredicates.pathExtension(predicate)
/**
* Route to the given handler function if the given queryParam predicate applies.
* @see RouterFunctions.route
*/
fun queryParam(name: String, predicate: (String) -> Boolean, f: suspend (ServerRequest) -> ServerResponse) {
builder.add(RouterFunctions.route(RequestPredicates.queryParam(name, predicate), asHandlerFunction(f)))
}
/**
* Return a [RequestPredicate] that tests the request's query parameter of the given name
* against the given predicate.
* @param name the name of the query parameter to test against
* @param predicate predicate to test against the query parameter value
* @return a predicate that matches the given predicate against the query parameter of the given name
* @see ServerRequest#queryParam
*/
fun queryParam(name: String, predicate: (String) -> Boolean): RequestPredicate =
RequestPredicates.queryParam(name, predicate)
/**
* Route to the given handler function if the given request predicate applies.
* @see RouterFunctions.route
*/
operator fun RequestPredicate.invoke(f: suspend (ServerRequest) -> ServerResponse) {
builder.add(RouterFunctions.route(this, asHandlerFunction(f)))
}
/**
* Route to the given handler function if the given predicate (String
* processed as a path predicate) applies.
* @see RouterFunctions.route
*/
operator fun String.invoke(f: suspend (ServerRequest) -> ServerResponse) {
builder.add(RouterFunctions.route(RequestPredicates.path(this), asHandlerFunction(f)))
}
/**
* Route requests that match the given pattern to resources relative to the given root location.
* @see RouterFunctions.resources
*/
fun resources(path: String, location: Resource) {
builder.resources(path, location)
}
/**
* Route to resources using the provided lookup function. If the lookup function provides a
* [Resource] for the given request, it will be it will be exposed using a
* [HandlerFunction] that handles GET, HEAD, and OPTIONS requests.
*/
fun resources(lookupFunction: suspend (ServerRequest) -> Resource?) {
builder.resources {
GlobalScope.mono {
lookupFunction.invoke(it)
}
}
}
override fun invoke(): RouterFunction<ServerResponse> {
init()
return builder.build()
}
private fun asHandlerFunction(init: suspend (ServerRequest) -> ServerResponse) = HandlerFunction {
GlobalScope.mono {
init(it)
}
}
/**
* @see ServerResponse.from
*/
fun from(other: ServerResponse) =
ServerResponse.from(other)
/**
* @see ServerResponse.created
*/
fun created(location: URI) =
ServerResponse.created(location)
/**
* @see ServerResponse.ok
*/
fun ok() = ServerResponse.ok()
/**
* @see ServerResponse.noContent
*/
fun noContent() = ServerResponse.noContent()
/**
* @see ServerResponse.accepted
*/
fun accepted() = ServerResponse.accepted()
/**
* @see ServerResponse.permanentRedirect
*/
fun permanentRedirect(location: URI) = ServerResponse.permanentRedirect(location)
/**
* @see ServerResponse.temporaryRedirect
*/
fun temporaryRedirect(location: URI) = ServerResponse.temporaryRedirect(location)
/**
* @see ServerResponse.seeOther
*/
fun seeOther(location: URI) = ServerResponse.seeOther(location)
/**
* @see ServerResponse.badRequest
*/
fun badRequest() = ServerResponse.badRequest()
/**
* @see ServerResponse.notFound
*/
fun notFound() = ServerResponse.notFound()
/**
* @see ServerResponse.unprocessableEntity
*/
fun unprocessableEntity() = ServerResponse.unprocessableEntity()
/**
* @see ServerResponse.status
*/
fun status(status: HttpStatus) = ServerResponse.status(status)
/**
* @see ServerResponse.status
*/
fun status(status: Int) = ServerResponse.status(status)
}
operator fun <T: ServerResponse> RouterFunction<T>.plus(other: RouterFunction<T>) =
this.and(other)
/**
* Coroutines variant of [router].
*
* @author Sebastien Deleuze
* @since 5.2
*/
fun coRouter(routes: (CoRouterFunctionDsl.() -> Unit)) =
CoRouterFunctionDsl(routes).invoke()

View File

@ -0,0 +1,27 @@
/*
* Copyright 2002-2019 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
*
* http://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.reactive.function.server
import kotlinx.coroutines.reactive.awaitSingle
/**
* Coroutines variant of [RenderingResponse.Builder.build].
*
* @author Sebastien Deleuze
* @since 5.2
*/
suspend fun RenderingResponse.Builder.buildAndAwait(): RenderingResponse = build().awaitSingle()

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2017 the original author or authors.
* Copyright 2002-2019 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.
@ -16,9 +16,15 @@
package org.springframework.web.reactive.function.server
import kotlinx.coroutines.reactive.awaitFirstOrNull
import kotlinx.coroutines.reactive.awaitSingle
import org.springframework.core.ParameterizedTypeReference
import org.springframework.http.codec.multipart.Part
import org.springframework.util.MultiValueMap
import org.springframework.web.server.WebSession
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import java.security.Principal
/**
* Extension for [ServerRequest.bodyToMono] providing a `bodyToMono<Foo>()` variant
@ -41,3 +47,48 @@ inline fun <reified T : Any> ServerRequest.bodyToMono(): Mono<T> =
*/
inline fun <reified T : Any> ServerRequest.bodyToFlux(): Flux<T> =
bodyToFlux(object : ParameterizedTypeReference<T>() {})
/**
* Coroutines variant of [ServerRequest.bodyToMono].
*
* @author Sebastien Deleuze
* @since 5.2
*/
suspend inline fun <reified T : Any> ServerRequest.awaitBody(): T? =
bodyToMono<T>().awaitFirstOrNull()
/**
* Coroutines variant of [ServerRequest.formData].
*
* @author Sebastien Deleuze
* @since 5.2
*/
suspend fun ServerRequest.awaitFormData(): MultiValueMap<String, String> =
formData().awaitSingle()
/**
* Coroutines variant of [ServerRequest.multipartData].
*
* @author Sebastien Deleuze
* @since 5.2
*/
suspend fun ServerRequest.awaitMultipartData(): MultiValueMap<String, Part> =
multipartData().awaitSingle()
/**
* Coroutines variant of [ServerRequest.principal].
*
* @author Sebastien Deleuze
* @since 5.2
*/
suspend fun ServerRequest.awaitPrincipal(): Principal =
principal().awaitSingle()
/**
* Coroutines variant of [ServerRequest.session].
*
* @author Sebastien Deleuze
* @since 5.2
*/
suspend fun ServerRequest.awaitSession(): WebSession =
session().awaitSingle()

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2019 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.
@ -16,6 +16,7 @@
package org.springframework.web.reactive.function.server
import kotlinx.coroutines.reactive.awaitSingle
import org.reactivestreams.Publisher
import org.springframework.core.ParameterizedTypeReference
import org.springframework.http.MediaType
@ -63,3 +64,42 @@ fun ServerResponse.BodyBuilder.xml() = contentType(MediaType.APPLICATION_XML)
* @since 5.1
*/
fun ServerResponse.BodyBuilder.html() = contentType(MediaType.TEXT_HTML)
/**
* Coroutines variant of [ServerResponse.HeadersBuilder.build].
*
* @author Sebastien Deleuze
* @since 5.2
*/
suspend fun ServerResponse.HeadersBuilder<out ServerResponse.HeadersBuilder<*>>.buildAndAwait(): ServerResponse =
build().awaitSingle()
/**
* Coroutines variant of [ServerResponse.BodyBuilder.syncBody].
*
* @author Sebastien Deleuze
* @since 5.2
*/
suspend fun ServerResponse.BodyBuilder.bodyAndAwait(body: Any): ServerResponse =
syncBody(body).awaitSingle()
/**
* Coroutines variant of [ServerResponse.BodyBuilder.syncBody] without the sync prefix since it is ok to use it within
* another suspendable function.
*
* @author Sebastien Deleuze
* @since 5.2
*/
suspend fun ServerResponse.BodyBuilder.renderAndAwait(name: String, vararg modelAttributes: String): ServerResponse =
render(name, *modelAttributes).awaitSingle()
/**
* Coroutines variant of [ServerResponse.BodyBuilder.syncBody] without the sync prefix since it is ok to use it within
* another suspendable function.
*
* @author Sebastien Deleuze
* @since 5.2
*/
suspend fun ServerResponse.BodyBuilder.renderAndAwait(name: String, model: Map<String, *>): ServerResponse =
render(name, model).awaitSingle()

View File

@ -0,0 +1,71 @@
/*
* Copyright 2002-2019 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
*
* http://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.reactive.server
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.reactive.awaitSingle
import kotlinx.coroutines.reactor.mono
import org.springframework.http.codec.multipart.Part
import org.springframework.util.MultiValueMap
import org.springframework.web.server.ServerWebExchange
import org.springframework.web.server.WebSession
import java.security.Principal
/**
* Coroutines variant of [ServerWebExchange.getFormData].
*
* @author Sebastien Deleuze
* @since 5.2
*/
suspend fun ServerWebExchange.awaitFormData(): MultiValueMap<String, String> =
this.formData.awaitSingle()
/**
* Coroutines variant of [ServerWebExchange.getMultipartData].
*
* @author Sebastien Deleuze
* @since 5.2
*/
suspend fun ServerWebExchange.awaitMultipartData(): MultiValueMap<String, Part> =
this.multipartData.awaitSingle()
/**
* Coroutines variant of [ServerWebExchange.getPrincipal].
*
* @author Sebastien Deleuze
* @since 5.2
*/
suspend fun <T : Principal> ServerWebExchange.awaitPrincipal(): T =
this.getPrincipal<T>().awaitSingle()
/**
* Coroutines variant of [ServerWebExchange.getSession].
*
* @author Sebastien Deleuze
* @since 5.2
*/
suspend fun ServerWebExchange.awaitSession(): WebSession =
this.session.awaitSingle()
/**
* Coroutines variant of [ServerWebExchange.Builder.principal].
*
* @author Sebastien Deleuze
* @since 5.2
*/
fun ServerWebExchange.Builder.principal(supplier: suspend () -> Principal): ServerWebExchange.Builder
= principal(GlobalScope.mono { supplier.invoke() })

View File

@ -16,10 +16,16 @@
package org.springframework.web.reactive.function.client
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Test
import org.springframework.core.ParameterizedTypeReference
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import reactor.core.publisher.Mono
/**
* Mock object based tests for [ClientResponse] Kotlin extensions.
@ -28,7 +34,7 @@ import org.springframework.core.ParameterizedTypeReference
*/
class ClientResponseExtensionsTests {
val response = mockk<ClientResponse>(relaxed = true)
private val response = mockk<ClientResponse>(relaxed = true)
@Test
fun `bodyToMono with reified type parameters`() {
@ -54,5 +60,34 @@ class ClientResponseExtensionsTests {
verify { response.toEntityList(object : ParameterizedTypeReference<List<Foo>>() {}) }
}
@Test
fun awaitBody() {
val response = mockk<ClientResponse>()
every { response.bodyToMono<String>() } returns Mono.just("foo")
runBlocking {
assertEquals("foo", response.awaitBody<String>())
}
}
@Test
fun awaitEntity() {
val response = mockk<ClientResponse>()
val entity = ResponseEntity("foo", HttpStatus.OK)
every { response.toEntity<String>() } returns Mono.just(entity)
runBlocking {
assertEquals(entity, response.awaitEntity<String>())
}
}
@Test
fun awaitEntityList() {
val response = mockk<ClientResponse>()
val entity = ResponseEntity(listOf("foo"), HttpStatus.OK)
every { response.toEntityList<String>() } returns Mono.just(entity)
runBlocking {
assertEquals(entity, response.awaitEntityList<String>())
}
}
class Foo
}

View File

@ -16,11 +16,15 @@
package org.springframework.web.reactive.function.client
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Test
import org.reactivestreams.Publisher
import org.springframework.core.ParameterizedTypeReference
import reactor.core.publisher.Mono
/**
* Mock object based tests for [WebClient] Kotlin extensions
@ -29,9 +33,9 @@ import org.springframework.core.ParameterizedTypeReference
*/
class WebClientExtensionsTests {
val requestBodySpec = mockk<WebClient.RequestBodySpec>(relaxed = true)
private val requestBodySpec = mockk<WebClient.RequestBodySpec>(relaxed = true)
val responseSpec = mockk<WebClient.ResponseSpec>(relaxed = true)
private val responseSpec = mockk<WebClient.ResponseSpec>(relaxed = true)
@Test
@ -53,5 +57,36 @@ class WebClientExtensionsTests {
verify { responseSpec.bodyToFlux(object : ParameterizedTypeReference<List<Foo>>() {}) }
}
@Test
fun awaitResponse() {
val response = mockk<ClientResponse>()
every { requestBodySpec.exchange() } returns Mono.just(response)
runBlocking {
assertEquals(response, requestBodySpec.awaitResponse())
}
}
@Test
fun body() {
val headerSpec = mockk<WebClient.RequestHeadersSpec<*>>()
val supplier: suspend () -> String = mockk()
every { requestBodySpec.body(ofType<Mono<String>>()) } returns headerSpec
runBlocking {
requestBodySpec.body(supplier)
}
verify {
requestBodySpec.body(ofType<Mono<String>>())
}
}
@Test
fun awaitBody() {
val spec = mockk<WebClient.ResponseSpec>()
every { spec.bodyToMono<String>() } returns Mono.just("foo")
runBlocking {
assertEquals("foo", spec.awaitBody<String>())
}
}
class Foo
}

View File

@ -0,0 +1,166 @@
/*
* Copyright 2002-2019 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
*
* http://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.reactive.function.server
import org.junit.Test
import org.springframework.core.io.ClassPathResource
import org.springframework.http.HttpHeaders.*
import org.springframework.http.HttpMethod.*
import org.springframework.http.MediaType.*
import org.springframework.web.reactive.function.server.MockServerRequest.builder
import reactor.test.StepVerifier
import java.net.URI
/**
* Tests for [CoRouterFunctionDsl].
*
* @author Sebastien Deleuze
*/
class CoRouterFunctionDslTests {
@Test
fun header() {
val request = builder().header("bar", "bar").build()
StepVerifier.create(sampleRouter().route(request))
.expectNextCount(1)
.verifyComplete()
}
@Test
fun accept() {
val request = builder().uri(URI("/content")).header(ACCEPT, APPLICATION_ATOM_XML_VALUE).build()
StepVerifier.create(sampleRouter().route(request))
.expectNextCount(1)
.verifyComplete()
}
@Test
fun acceptAndPOST() {
val request = builder()
.method(POST)
.uri(URI("/api/foo/"))
.header(ACCEPT, APPLICATION_JSON_VALUE)
.build()
StepVerifier.create(sampleRouter().route(request))
.expectNextCount(1)
.verifyComplete()
}
@Test
fun contentType() {
val request = builder().uri(URI("/content")).header(CONTENT_TYPE, APPLICATION_OCTET_STREAM_VALUE).build()
StepVerifier.create(sampleRouter().route(request))
.expectNextCount(1)
.verifyComplete()
}
@Test
fun resourceByPath() {
val request = builder().uri(URI("/org/springframework/web/reactive/function/response.txt")).build()
StepVerifier.create(sampleRouter().route(request))
.expectNextCount(1)
.verifyComplete()
}
@Test
fun method() {
val request = builder().method(PATCH).build()
StepVerifier.create(sampleRouter().route(request))
.expectNextCount(1)
.verifyComplete()
}
@Test
fun path() {
val request = builder().uri(URI("/baz")).build()
StepVerifier.create(sampleRouter().route(request))
.expectNextCount(1)
.verifyComplete()
}
@Test
fun resource() {
val request = builder().uri(URI("/response.txt")).build()
StepVerifier.create(sampleRouter().route(request))
.expectNextCount(1)
.verifyComplete()
}
@Test
fun noRoute() {
val request = builder()
.uri(URI("/bar"))
.header(ACCEPT, APPLICATION_PDF_VALUE)
.header(CONTENT_TYPE, APPLICATION_PDF_VALUE)
.build()
StepVerifier.create(sampleRouter().route(request))
.verifyComplete()
}
@Test
fun rendering() {
val request = builder().uri(URI("/rendering")).build()
StepVerifier.create(sampleRouter().route(request).flatMap { it.handle(request) })
.expectNextMatches { it is RenderingResponse}
.verifyComplete()
}
@Test(expected = IllegalStateException::class)
fun emptyRouter() {
router { }
}
private fun sampleRouter() = coRouter {
(GET("/foo/") or GET("/foos/")) { req -> handle(req) }
"/api".nest {
POST("/foo/", ::handleFromClass)
PUT("/foo/", :: handleFromClass)
PATCH("/foo/") {
ok().buildAndAwait()
}
"/foo/" { handleFromClass(it) }
}
"/content".nest {
accept(APPLICATION_ATOM_XML, ::handle)
contentType(APPLICATION_OCTET_STREAM, ::handle)
}
method(PATCH, ::handle)
headers { it.accept().contains(APPLICATION_JSON) }.nest {
GET("/api/foo/", ::handle)
}
headers({ it.header("bar").isNotEmpty() }, ::handle)
resources("/org/springframework/web/reactive/function/**",
ClassPathResource("/org/springframework/web/reactive/function/response.txt"))
resources {
if (it.path() == "/response.txt") {
ClassPathResource("/org/springframework/web/reactive/function/response.txt")
}
else {
null
}
}
path("/baz", ::handle)
GET("/rendering") { RenderingResponse.create("index").buildAndAwait() }
}
}
@Suppress("UNUSED_PARAMETER")
private suspend fun handleFromClass(req: ServerRequest) = ServerResponse.ok().buildAndAwait()
@Suppress("UNUSED_PARAMETER")
private suspend fun handle(req: ServerRequest) = ServerResponse.ok().buildAndAwait()

View File

@ -0,0 +1,41 @@
/*
* Copyright 2002-2019 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
*
* http://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.reactive.function.server
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.runBlocking
import org.junit.Test
import reactor.core.publisher.Mono
class RenderingResponseExtensionsTests {
@Test
fun buildAndAwait() {
val builder = mockk<RenderingResponse.Builder>()
val response = mockk<RenderingResponse>()
every { builder.build() } returns Mono.just(response)
runBlocking {
builder.buildAndAwait()
}
verify {
builder.build()
}
}
}

View File

@ -27,7 +27,7 @@ import reactor.test.StepVerifier
import java.net.URI
/**
* Tests for [RouterFunction] Kotlin DSL.
* Tests for [RouterFunctionDsl].
*
* @author Sebastien Deleuze
*/
@ -161,7 +161,7 @@ class RouterFunctionDslTests {
}
@Suppress("UNUSED_PARAMETER")
fun handleFromClass(req: ServerRequest) = ServerResponse.ok().build()
private fun handleFromClass(req: ServerRequest) = ServerResponse.ok().build()
@Suppress("UNUSED_PARAMETER")
fun handle(req: ServerRequest) = ServerResponse.ok().build()
private fun handle(req: ServerRequest) = ServerResponse.ok().build()

View File

@ -16,13 +16,20 @@
package org.springframework.web.reactive.function.client
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Test
import org.springframework.core.ParameterizedTypeReference
import org.springframework.web.reactive.function.server.ServerRequest
import org.springframework.web.reactive.function.server.bodyToFlux
import org.springframework.web.reactive.function.server.bodyToMono
import org.springframework.http.codec.multipart.Part
import org.springframework.util.MultiValueMap
import org.springframework.web.reactive.function.server.*
import org.springframework.web.server.WebSession
import reactor.core.publisher.Mono
import java.security.Principal
/**
* Mock object based tests for [ServerRequest] Kotlin extensions.
@ -45,5 +52,58 @@ class ServerRequestExtensionsTests {
verify { request.bodyToFlux(object : ParameterizedTypeReference<List<Foo>>() {}) }
}
@Test
fun awaitBody() {
every { request.bodyToMono<String>() } returns Mono.just("foo")
runBlocking {
assertEquals("foo", request.awaitBody<String>())
}
}
@Test
fun awaitBodyNull() {
every { request.bodyToMono<String>() } returns Mono.empty()
runBlocking {
assertNull(request.awaitBody<String>())
}
}
@Test
fun awaitFormData() {
val map = mockk<MultiValueMap<String, String>>()
every { request.formData() } returns Mono.just(map)
runBlocking {
assertEquals(map, request.awaitFormData())
}
}
@Test
fun awaitMultipartData() {
val map = mockk<MultiValueMap<String, Part>>()
every { request.multipartData() } returns Mono.just(map)
runBlocking {
assertEquals(map, request.awaitMultipartData())
}
}
@Test
fun awaitPrincipal() {
val principal = mockk<Principal>()
every { request.principal() } returns Mono.just(principal)
runBlocking {
assertEquals(principal, request.awaitPrincipal())
}
}
@Test
fun awaitSession() {
val session = mockk<WebSession>()
every { request.session() } returns Mono.just(session)
runBlocking {
assertEquals(session, request.awaitSession())
}
}
class Foo
}

View File

@ -16,12 +16,16 @@
package org.springframework.web.reactive.function.server
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Test
import org.reactivestreams.Publisher
import org.springframework.core.ParameterizedTypeReference
import org.springframework.http.MediaType.*
import reactor.core.publisher.Mono
/**
* Mock object based tests for [ServerResponse] Kotlin extensions
@ -30,7 +34,7 @@ import org.springframework.http.MediaType.*
*/
class ServerResponseExtensionsTests {
val bodyBuilder = mockk<ServerResponse.BodyBuilder>(relaxed = true)
private val bodyBuilder = mockk<ServerResponse.BodyBuilder>(relaxed = true)
@Test
@ -65,5 +69,53 @@ class ServerResponseExtensionsTests {
verify { bodyBuilder.contentType(TEXT_HTML) }
}
@Test
fun await() {
val response = mockk<ServerResponse>()
val builder = mockk<ServerResponse.HeadersBuilder<*>>()
every { builder.build() } returns Mono.just(response)
runBlocking {
assertEquals(response, builder.buildAndAwait())
}
}
@Test
fun `bodyAndAwait with object parameter`() {
val response = mockk<ServerResponse>()
val body = "foo"
every { bodyBuilder.syncBody(ofType<String>()) } returns Mono.just(response)
runBlocking {
bodyBuilder.bodyAndAwait(body)
}
verify {
bodyBuilder.syncBody(ofType<String>())
}
}
@Test
fun `renderAndAwait with a vararg parameter`() {
val response = mockk<ServerResponse>()
every { bodyBuilder.render("foo", any(), any()) } returns Mono.just(response)
runBlocking {
bodyBuilder.renderAndAwait("foo", "bar", "baz")
}
verify {
bodyBuilder.render("foo", any(), any())
}
}
@Test
fun `renderAndAwait with a Map parameter`() {
val response = mockk<ServerResponse>()
val map = mockk<Map<String, *>>()
every { bodyBuilder.render("foo", map) } returns Mono.just(response)
runBlocking {
bodyBuilder.renderAndAwait("foo", map)
}
verify {
bodyBuilder.render("foo", map)
}
}
class Foo
}

View File

@ -20,10 +20,10 @@ https://stackoverflow.com/questions/tagged/spring+kotlin[Stackoverflow] if you n
[[kotlin-requirements]]
== Requirements
The Spring Framework supports Kotlin 1.1+ and requires
The Spring Framework supports Kotlin 1.3+ and requires
https://bintray.com/bintray/jcenter/org.jetbrains.kotlin%3Akotlin-stdlib[`kotlin-stdlib`]
(or one of its variants, such as https://bintray.com/bintray/jcenter/org.jetbrains.kotlin%3Akotlin-stdlib-jre8[`kotlin-stdlib-jre8`]
for Kotlin 1.1 or https://bintray.com/bintray/jcenter/org.jetbrains.kotlin%3Akotlin-stdlib-jdk8[`kotlin-stdlib-jdk8`] for Kotlin 1.2+)
(or one of its variants, such as
https://bintray.com/bintray/jcenter/org.jetbrains.kotlin%3Akotlin-stdlib-jdk8[`kotlin-stdlib-jdk8`])
and https://bintray.com/bintray/jcenter/org.jetbrains.kotlin%3Akotlin-reflect[`kotlin-reflect`]
to be present on the classpath. They are provided by default if you bootstrap a Kotlin project on
https://start.spring.io/#!language=kotlin[start.spring.io].
@ -255,17 +255,19 @@ for more details and up-to-date information.
== Web
=== WebFlux router DSL
=== WebFlux Functional DSL
Spring Framework comes with a Kotlin router DSL available in 2 flavors:
Spring Framework now comes with a
{doc-root}/spring-framework/docs/{spring-version}/kdoc-api/spring-framework/org.springframework.web.reactive.function.server/-router-function-dsl/[Kotlin routing DSL]
that lets you use the <<web-reactive#webflux-fn,WebFlux functional API>> to write clean and idiomatic Kotlin code,
as the following example shows:
- Reactive with {doc-root}/spring-framework/docs/{spring-version}/kdoc-api/spring-framework/org.springframework.web.reactive.function.server/-router-function-dsl/[router { }]
- <<Coroutines>>
These DSL let you use the <<web-reactive#webflux-fn,WebFlux functional API>> to write clean and idiomatic Kotlin code
to build a `RouterFunction` instance as the following example shows:
[source,kotlin,indent=0]
----
router {
val routes: RouterFunction<ServerResponse> = router {
accept(TEXT_HTML).nest {
GET("/") { ok().render("index") }
GET("/sse") { ok().render("sse") }
@ -290,7 +292,38 @@ depending on dynamic data (for example, from a database).
See https://github.com/mixitconf/mixit/tree/dafd5ccc92dfab6d9c306fcb60b28921a1ccbf79/src/main/kotlin/mixit/web/routes[MiXiT project routes]
for a concrete example.
=== Coroutines
As of Spring Framework 5.2, https://kotlinlang.org/docs/reference/coroutines-overview.html[Coroutines support]
is provided via extensions for WebFlux client and server functional API. A dedicated
{doc-root}/spring-framework/docs/{spring-version}/kdoc-api/spring-framework/org.springframework.web.reactive.function.server/-co-router-function-dsl/[`coRouter { }`]
router DSL is also available.
Coroutines extensions use `await` prefix or `AndAwait` suffix, and most are using similar name than their Reactive
counterparts except `exchange` in `WebClient.RequestHeadersSpec` which translates to `awaitResponse`.
[source,kotlin,indent=0]
----
fun routes(userHandler: UserHandler): RouterFunction<ServerResponse> = coRouter {
GET("/", userHandler::listView)
GET("/api/user", userHandler::listApi)
}
class UserHandler(private val client: WebClient) {
suspend fun listApi(request: ServerRequest): ServerResponse =
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON_UTF8).bodyAndAwait(
client.get().uri("...").awaitResponse().awaitBody<User>())
suspend fun listView(request: ServerRequest): ServerResponse =
ServerResponse.ok().renderAndAwait("users", mapOf("users" to
client.get().uri("...").awaitResponse().awaitBody<User>()))
}
----
Read this https://medium.com/@elizarov/structured-concurrency-722d765aa952[structured concurrency blog post]
to understand how to run code concurrently with Coroutines.
=== Kotlin Script Templates