Browse Source
This commit turns Coroutines suspending methods to `Mono` which can be handled natively by WebFlux. See gh-19975pull/22606/head
6 changed files with 261 additions and 3 deletions
@ -0,0 +1,43 @@
@@ -0,0 +1,43 @@
|
||||
/* |
||||
* 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.result.method.annotation; |
||||
|
||||
import reactor.core.publisher.Mono; |
||||
|
||||
import org.springframework.core.MethodParameter; |
||||
import org.springframework.web.reactive.BindingContext; |
||||
import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolver; |
||||
import org.springframework.web.server.ServerWebExchange; |
||||
|
||||
/** |
||||
* No-op resolver for method arguments of type {@link kotlin.coroutines.Continuation}. |
||||
* |
||||
* @author Sebastien Deleuze |
||||
* @since 5.2 |
||||
*/ |
||||
public class ContinuationHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver { |
||||
|
||||
@Override |
||||
public boolean supportsParameter(MethodParameter parameter) { |
||||
return "kotlin.coroutines.Continuation".equals(parameter.getParameterType().getName()); |
||||
} |
||||
|
||||
@Override |
||||
public Mono<Object> resolveArgument(MethodParameter parameter, BindingContext bindingContext, ServerWebExchange exchange) { |
||||
return Mono.empty(); |
||||
} |
||||
} |
||||
@ -0,0 +1,43 @@
@@ -0,0 +1,43 @@
|
||||
/* |
||||
* 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.result.method |
||||
|
||||
import kotlinx.coroutines.GlobalScope |
||||
import kotlinx.coroutines.reactor.mono |
||||
import reactor.core.publisher.onErrorMap |
||||
import java.lang.reflect.InvocationTargetException |
||||
import java.lang.reflect.Method |
||||
import kotlin.reflect.full.callSuspend |
||||
import kotlin.reflect.jvm.kotlinFunction |
||||
|
||||
/** |
||||
* Invoke an handler method converting suspending method to {@link Mono} if necessary. |
||||
* |
||||
* @author Sebastien Deleuze |
||||
* @since 5.2 |
||||
*/ |
||||
internal fun invokeHandlerMethod(method: Method, bean: Any, vararg args: Any?): Any? { |
||||
val function = method.kotlinFunction!! |
||||
return if (function.isSuspend) { |
||||
GlobalScope.mono { function.callSuspend(bean, *args.sliceArray(0..(args.size-2))) |
||||
.let { if (it == Unit) null else it} } |
||||
.onErrorMap(InvocationTargetException::class) { it.targetException } |
||||
} |
||||
else { |
||||
function.call(bean, *args) |
||||
} |
||||
} |
||||
@ -0,0 +1,155 @@
@@ -0,0 +1,155 @@
|
||||
/* |
||||
* 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.result |
||||
|
||||
import io.mockk.every |
||||
import io.mockk.mockk |
||||
import kotlinx.coroutines.delay |
||||
import org.hamcrest.CoreMatchers.`is` |
||||
import org.hamcrest.MatcherAssert.assertThat |
||||
import org.junit.Assert.assertEquals |
||||
import org.junit.Test |
||||
import org.springframework.http.HttpStatus |
||||
import org.springframework.http.server.reactive.ServerHttpResponse |
||||
import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest.get |
||||
import org.springframework.mock.web.test.server.MockServerWebExchange |
||||
import org.springframework.web.bind.annotation.ResponseStatus |
||||
import org.springframework.web.reactive.BindingContext |
||||
import org.springframework.web.reactive.HandlerResult |
||||
import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolver |
||||
import org.springframework.web.reactive.result.method.InvocableHandlerMethod |
||||
import org.springframework.web.reactive.result.method.annotation.ContinuationHandlerMethodArgumentResolver |
||||
import reactor.core.publisher.Mono |
||||
import reactor.test.StepVerifier |
||||
import reactor.test.expectError |
||||
import java.lang.reflect.Method |
||||
import kotlin.reflect.jvm.javaMethod |
||||
|
||||
class KotlinInvocableHandlerMethodTests { |
||||
|
||||
private val exchange = MockServerWebExchange.from(get("http://localhost:8080/path")) |
||||
|
||||
private val resolvers = mutableListOf<HandlerMethodArgumentResolver>(ContinuationHandlerMethodArgumentResolver()) |
||||
|
||||
@Test |
||||
fun resolveNoArg() { |
||||
this.resolvers.add(stubResolver(Mono.empty())) |
||||
val method = CoroutinesController::singleArg.javaMethod!! |
||||
val result = invoke(CoroutinesController(), method, null) |
||||
assertHandlerResultValue(result, "success:null") |
||||
} |
||||
|
||||
@Test |
||||
fun resolveArg() { |
||||
this.resolvers.add(stubResolver("foo")) |
||||
val method = CoroutinesController::singleArg.javaMethod!! |
||||
val result = invoke(CoroutinesController(), method,"foo") |
||||
assertHandlerResultValue(result, "success:foo") |
||||
} |
||||
|
||||
@Test |
||||
fun resolveNoArgs() { |
||||
val method = CoroutinesController::noArgs.javaMethod!! |
||||
val result = invoke(CoroutinesController(), method) |
||||
assertHandlerResultValue(result, "success") |
||||
} |
||||
|
||||
@Test |
||||
fun invocationTargetException() { |
||||
val method = CoroutinesController::exceptionMethod.javaMethod!! |
||||
val result = invoke(CoroutinesController(), method) |
||||
|
||||
StepVerifier.create(result) |
||||
.consumeNextWith { StepVerifier.create(it.returnValue as Mono<*>).expectError(IllegalStateException::class).verify() } |
||||
.verifyComplete() |
||||
} |
||||
|
||||
@Test |
||||
fun responseStatusAnnotation() { |
||||
val method = CoroutinesController::created.javaMethod!! |
||||
val result = invoke(CoroutinesController(), method) |
||||
|
||||
assertHandlerResultValue(result, "created") |
||||
assertThat<HttpStatus>(this.exchange.response.statusCode, `is`(HttpStatus.CREATED)) |
||||
} |
||||
|
||||
@Test |
||||
fun voidMethodWithResponseArg() { |
||||
val response = this.exchange.response |
||||
this.resolvers.add(stubResolver(response)) |
||||
val method = CoroutinesController::response.javaMethod!! |
||||
val result = invoke(CoroutinesController(), method) |
||||
|
||||
StepVerifier.create(result) |
||||
.consumeNextWith { StepVerifier.create(it.returnValue as Mono<*>).verifyComplete() } |
||||
.verifyComplete() |
||||
assertEquals("bar", this.exchange.response.headers.getFirst("foo")) |
||||
} |
||||
|
||||
private fun invoke(handler: Any, method: Method, vararg providedArgs: Any?): Mono<HandlerResult> { |
||||
val invocable = InvocableHandlerMethod(handler, method) |
||||
invocable.setArgumentResolvers(this.resolvers) |
||||
return invocable.invoke(this.exchange, BindingContext(), *providedArgs) |
||||
} |
||||
|
||||
private fun stubResolver(stubValue: Any?): HandlerMethodArgumentResolver { |
||||
return stubResolver(Mono.justOrEmpty(stubValue)) |
||||
} |
||||
|
||||
private fun stubResolver(stubValue: Mono<Any>): HandlerMethodArgumentResolver { |
||||
val resolver = mockk<HandlerMethodArgumentResolver>() |
||||
every { resolver.supportsParameter(any()) } returns true |
||||
every { resolver.resolveArgument(any(), any(), any()) } returns stubValue |
||||
return resolver |
||||
} |
||||
|
||||
private fun assertHandlerResultValue(mono: Mono<HandlerResult>, expected: String) { |
||||
StepVerifier.create(mono) |
||||
.consumeNextWith { StepVerifier.create(it.returnValue as Mono<*>).expectNext(expected).verifyComplete() } |
||||
.verifyComplete() |
||||
} |
||||
|
||||
class CoroutinesController { |
||||
|
||||
suspend fun singleArg(q: String?): String { |
||||
delay(10) |
||||
return "success:$q" |
||||
} |
||||
|
||||
suspend fun noArgs(): String { |
||||
delay(10) |
||||
return "success" |
||||
} |
||||
|
||||
suspend fun exceptionMethod() { |
||||
throw IllegalStateException("boo") |
||||
} |
||||
|
||||
@ResponseStatus(HttpStatus.CREATED) |
||||
suspend fun created(): String { |
||||
delay(10) |
||||
return "created" |
||||
} |
||||
|
||||
suspend fun response(response: ServerHttpResponse) { |
||||
delay(10) |
||||
response.headers.add("foo", "bar") |
||||
} |
||||
|
||||
|
||||
} |
||||
} |
||||
Loading…
Reference in new issue