7 changed files with 311 additions and 3 deletions
@ -0,0 +1,42 @@
@@ -0,0 +1,42 @@
|
||||
/* |
||||
* 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 |
||||
* |
||||
* https://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.messaging.handler.annotation.support.reactive; |
||||
|
||||
import reactor.core.publisher.Mono; |
||||
|
||||
import org.springframework.core.MethodParameter; |
||||
import org.springframework.messaging.Message; |
||||
import org.springframework.messaging.handler.invocation.reactive.HandlerMethodArgumentResolver; |
||||
|
||||
/** |
||||
* 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, Message<?> message) { |
||||
return Mono.empty(); |
||||
} |
||||
} |
||||
@ -0,0 +1,225 @@
@@ -0,0 +1,225 @@
|
||||
/* |
||||
* 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 |
||||
* |
||||
* https://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.messaging.rsocket |
||||
|
||||
import java.time.Duration |
||||
|
||||
import io.netty.buffer.PooledByteBufAllocator |
||||
import io.rsocket.RSocketFactory |
||||
import io.rsocket.frame.decoder.PayloadDecoder |
||||
import io.rsocket.transport.netty.server.CloseableChannel |
||||
import io.rsocket.transport.netty.server.TcpServerTransport |
||||
import kotlinx.coroutines.FlowPreview |
||||
import kotlinx.coroutines.delay |
||||
import kotlinx.coroutines.flow.Flow |
||||
import kotlinx.coroutines.flow.flow |
||||
import kotlinx.coroutines.flow.map |
||||
import org.junit.AfterClass |
||||
import org.junit.BeforeClass |
||||
import org.junit.Test |
||||
import reactor.core.publisher.Flux |
||||
import reactor.core.publisher.ReplayProcessor |
||||
import reactor.test.StepVerifier |
||||
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext |
||||
import org.springframework.context.annotation.Bean |
||||
import org.springframework.context.annotation.Configuration |
||||
import org.springframework.core.codec.CharSequenceEncoder |
||||
import org.springframework.core.codec.StringDecoder |
||||
import org.springframework.core.io.buffer.NettyDataBufferFactory |
||||
import org.springframework.messaging.handler.annotation.MessageExceptionHandler |
||||
import org.springframework.messaging.handler.annotation.MessageMapping |
||||
import org.springframework.stereotype.Controller |
||||
|
||||
/** |
||||
* Coroutines server-side handling of RSocket requests. |
||||
* |
||||
* @author Sebastien Deleuze |
||||
* @author Rossen Stoyanchev |
||||
*/ |
||||
class RSocketClientToServerCoroutinesIntegrationTests { |
||||
|
||||
@Test |
||||
fun echoAsync() { |
||||
val result = Flux.range(1, 3).concatMap { i -> requester.route("echo-async").data("Hello " + i!!).retrieveMono(String::class.java) } |
||||
|
||||
StepVerifier.create(result) |
||||
.expectNext("Hello 1 async").expectNext("Hello 2 async").expectNext("Hello 3 async") |
||||
.expectComplete() |
||||
.verify(Duration.ofSeconds(5)) |
||||
} |
||||
|
||||
@Test |
||||
fun echoStream() { |
||||
val result = requester.route("echo-stream").data("Hello").retrieveFlux(String::class.java) |
||||
|
||||
StepVerifier.create(result) |
||||
.expectNext("Hello 0").expectNextCount(6).expectNext("Hello 7") |
||||
.thenCancel() |
||||
.verify(Duration.ofSeconds(5)) |
||||
} |
||||
|
||||
@Test |
||||
fun echoChannel() { |
||||
val result = requester.route("echo-channel") |
||||
.data(Flux.range(1, 10).map { i -> "Hello " + i!! }, String::class.java) |
||||
.retrieveFlux(String::class.java) |
||||
|
||||
StepVerifier.create(result) |
||||
.expectNext("Hello 1 async").expectNextCount(8).expectNext("Hello 10 async") |
||||
.thenCancel() // https://github.com/rsocket/rsocket-java/issues/613 |
||||
.verify(Duration.ofSeconds(5)) |
||||
} |
||||
|
||||
@Test |
||||
fun unitReturnValue() { |
||||
val result = requester.route("unit-return-value").data("Hello").retrieveFlux(String::class.java) |
||||
StepVerifier.create(result).expectComplete().verify(Duration.ofSeconds(5)) |
||||
} |
||||
|
||||
@Test |
||||
fun unitReturnValueFromExceptionHandler() { |
||||
val result = requester.route("unit-return-value").data("bad").retrieveFlux(String::class.java) |
||||
StepVerifier.create(result).expectComplete().verify(Duration.ofSeconds(5)) |
||||
} |
||||
|
||||
@Test |
||||
fun handleWithThrownException() { |
||||
val result = requester.route("thrown-exception").data("a").retrieveMono(String::class.java) |
||||
StepVerifier.create(result) |
||||
.expectNext("Invalid input error handled") |
||||
.expectComplete() |
||||
.verify(Duration.ofSeconds(5)) |
||||
} |
||||
|
||||
@FlowPreview |
||||
@Controller |
||||
class ServerController { |
||||
|
||||
val fireForgetPayloads = ReplayProcessor.create<String>() |
||||
|
||||
@MessageMapping("echo-async") |
||||
suspend fun echoAsync(payload: String): String { |
||||
delay(10) |
||||
return "$payload async" |
||||
} |
||||
|
||||
@MessageMapping("echo-stream") |
||||
fun echoStream(payload: String): Flow<String> { |
||||
var i = 0 |
||||
return flow { |
||||
while(true) { |
||||
delay(10) |
||||
emit("$payload ${i++}") |
||||
} |
||||
} |
||||
} |
||||
|
||||
@MessageMapping("echo-channel") |
||||
fun echoChannel(payloads: Flow<String>) = payloads.map { |
||||
delay(10) |
||||
"$it async" |
||||
} |
||||
|
||||
@MessageMapping("thrown-exception") |
||||
suspend fun handleAndThrow(payload: String): String { |
||||
delay(10) |
||||
throw IllegalArgumentException("Invalid input error") |
||||
} |
||||
|
||||
@MessageMapping("unit-return-value") |
||||
suspend fun unitReturnValue(payload: String) = |
||||
if (payload != "bad") delay(10) else throw IllegalStateException("bad") |
||||
|
||||
@MessageExceptionHandler |
||||
suspend fun handleException(ex: IllegalArgumentException): String { |
||||
delay(10) |
||||
return "${ex.message} handled" |
||||
} |
||||
|
||||
@MessageExceptionHandler |
||||
suspend fun handleExceptionWithVoidReturnValue(ex: IllegalStateException) { |
||||
delay(10) |
||||
} |
||||
} |
||||
|
||||
|
||||
@Configuration |
||||
open class ServerConfig { |
||||
|
||||
@Bean |
||||
open fun controller(): ServerController { |
||||
return ServerController() |
||||
} |
||||
|
||||
@Bean |
||||
open fun messageHandlerAcceptor(): MessageHandlerAcceptor { |
||||
val acceptor = MessageHandlerAcceptor() |
||||
acceptor.rSocketStrategies = rsocketStrategies() |
||||
return acceptor |
||||
} |
||||
|
||||
@Bean |
||||
open fun rsocketStrategies(): RSocketStrategies { |
||||
return RSocketStrategies.builder() |
||||
.decoder(StringDecoder.allMimeTypes()) |
||||
.encoder(CharSequenceEncoder.allMimeTypes()) |
||||
.dataBufferFactory(NettyDataBufferFactory(PooledByteBufAllocator.DEFAULT)) |
||||
.build() |
||||
} |
||||
} |
||||
|
||||
companion object { |
||||
|
||||
private lateinit var context: AnnotationConfigApplicationContext |
||||
|
||||
private lateinit var server: CloseableChannel |
||||
|
||||
private val interceptor = FireAndForgetCountingInterceptor() |
||||
|
||||
private lateinit var requester: RSocketRequester |
||||
|
||||
|
||||
@BeforeClass |
||||
@JvmStatic |
||||
fun setupOnce() { |
||||
context = AnnotationConfigApplicationContext(ServerConfig::class.java) |
||||
|
||||
server = RSocketFactory.receive() |
||||
.addServerPlugin(interceptor) |
||||
.frameDecoder(PayloadDecoder.ZERO_COPY) |
||||
.acceptor(context.getBean(MessageHandlerAcceptor::class.java)) |
||||
.transport(TcpServerTransport.create("localhost", 7000)) |
||||
.start() |
||||
.block()!! |
||||
|
||||
requester = RSocketRequester.builder() |
||||
.rsocketFactory { factory -> factory.frameDecoder(PayloadDecoder.ZERO_COPY) } |
||||
.rsocketStrategies(context.getBean(RSocketStrategies::class.java)) |
||||
.connectTcp("localhost", 7000) |
||||
.block()!! |
||||
} |
||||
|
||||
@AfterClass |
||||
@JvmStatic |
||||
fun tearDownOnce() { |
||||
requester.rsocket().dispose() |
||||
server.dispose() |
||||
} |
||||
} |
||||
|
||||
} |
||||
Loading…
Reference in new issue