Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Devoxx Belgium 2019 報告会 Vol.1

Yosuke Hirakida
January 15, 2020
310

Devoxx Belgium 2019 報告会 Vol.1

Yosuke Hirakida

January 15, 2020
Tweet

Transcript

  1. 5BMLT • Reactive Revolution • Deepdive into Reactive Spring with

    Coroutines and Kotlin Flow • Bootiful Kotlin • Bootiful Testing
  2. 3%#$ https://r2dbc.io/ • Reactive Relational Database Connectivity • R2DBC is

    an endeavor to bring a reactive programming API to SQL databases. • ΧϯϑΝϨϯεظؒதʢ11/4ʙ11/8ʣ͸·ͩGAͰ͸ͳ ͔ͬͨͷͰ͕͢ɺ12্݄०ʹGAʹͳΓ·ͨ͠ • https://r2dbc.io/2019/12/02/r2dbc-0-8-0-goes-ga
  3. 3%#$%SJWFST • First-party • PostgreSQL • Microsoft SQL Server •

    H2 Database ※ first-party https://github.com/r2dbc • Community • MySQL • https://github.com/mirromutth/r2dbc-mysql • https://github.com/jasync-sql/jasync-sql • SAP HANA • Google Cloud Spanner
  4. 4QSJOH%BUB3%#$ https://github.com/spring-projects/spring-data-r2dbc • Spring Data R2DBC͸12݄ʹGA • Spring Boot R2DBC

    Starter͸·ͩexperimentalʢૣͯ͘΋ Spring Boot 2.3ʹͳΔΑ͏Ͱ͢ʣ • https://twitter.com/mp911de/status/ 1202987229024522245?s=20 • https://github.com/spring-projects-experimental/spring- boot-r2dbc
  5. 4QSJOH%BUB3%#$ !5SBOTBDUJPOBM 4QSJOH%BUB3FQPTJUPSZ public class User { @Id private Integer

    id; … } public interface UserRepository extends ReactiveCrudRepository<User, Integer> { } @Service @Transactional public class UserService { private final UserRepository userRepository; public UserService(UserRepository userRepository) { this.userRepository = userRepository; } public Flux<User> findAll() { return userRepository.findAll(); } public Mono<User> findById(int id) { return userRepository.findById(id); } } )ͷ৔߹ TQSJOHSECDVSMSECDINFNEBUBCBTF PQUJPOT%#@$-04&@%&-":%#@$-04&@0/@&9*5'"-4&
  6. 4QSJOH%BUB3%#$ 'VODUJPOBM5SBOTBDUJPOT @Service public class UserService { private final UserRepository

    userRepository; private final TransactionalOperator transactionalOperator; public UserService(UserRepository userRepository, TransactionalOperator transactionOperator) { this.userRepository = userRepository; this.transactionalOperator = transactionOperator; } public Flux<User> saveAll(String... names) { return transactionalOperator.transactional( Flux.fromArray(names) .map(name -> new User(null, name)) .flatMap(reservationRepository::save)); } }
  7. 4QSJOH%BUB3%#$ 3FBDUJWF"1*T @Component public class UserRepository { private final DatabaseClient

    client; public UserRepository(DatabaseClient client) { this.client = client; } public Flux<User> findAll() { return client.select().from(User.class).fetch().all(); } public Mono<User> findById(Integer id) { return client.select().from(User.class).matching(where("id").is(id)).fetch().one(); } public Mono<Void> insert(User user) { return client.insert().into(User.class).using(user).then(); } public Mono<Void> update(User user) { return client.update().table(User.class).using(user).then(); } public Mono<Void> deleteById(Integer id) { return client.delete().from(User.class).matching(where("id").is(id)).then(); } }
  8. 34PDLFU http://rsocket.io/ • Application protocol providing Reactive Streams semantics •

    RSocket is a binary protocol for use on byte stream transports such as TCP, WebSockets, and Aeron(UDP). • marking͞Μͷࢿྉ͕೔ຊޠͰॻ͔Ε͍ͯͯ෼͔Γ΍͍͢ Ͱ͢ • https://docs.google.com/presentation/d/1ygSM85- RQ3NZjCg6RaZ52mGzxbWiItVwzlCpr1vaWBw/ edit#slide=id.g75f375afa0_0_0
  9. 34PDLFU • Duplex • Server͕requesterʹͳΕΔʢServer͕σʔλΛૹ৴ͨ͠ΓϨεϙϯεΛड৴Ͱ͖Δʣ • Reactive Streams • JVM಺͚ͩͰ͸ͳ͘ɺωοτϫʔΫ্ͰReactive

    StreamΛ࣮૷Ͱ͖Δ • Back Pressure • Session/Stream resumption (Session/Streamͷ࠶։) • ࠶઀ଓޙʹSession͕࠶։͢Δ • Back pressure͸stream͝ͱ • Leasing • ϦΫΤετͷεϩοτϦϯά • Fragmentation And Reassembly
  10. JOUFSBDUJPONPEFMT • Request Response (stream of 1) • Request Stream

    (finite stream of many) • Request Channel (bi-directional streams) • Request Fire-and-Forget (no response)
  11. H31$ gRPCʹ΋ࣅͨΑ͏ͳ4छྨͷαʔϏεϝιου͕͋Δ • Unary RPC (send one / receive one)

    • Server streaming RPC (send one / receive many) • Client streaming RPC (send many / receive one) • Bidirectional streaming RPC (send many / receive many) https://grpc.io/docs/guides/concepts/
  12. H31$ͱ34PDLFUͷҧ͍ https://medium.com/netifi/differences-between-grpc-and- rsocket-e736c954e60 H31$ 34PDLFU 04*-BZFS 04*MBZFS 31$MBZFSCVJMUPOUPQPG)551 04*MBZFS 1SPUPDPM

    OPUBQSPUPDPM *UJTDPNQSJTFEPG)551IFBEFST  HFOFSBUFEDPEF BOEDPOWFOUJPOTJO QSPUPCVG CJOBSZQSPUPDPM 'MPX $POUSPM )551qPXDPOUSPM BQQMJDBUJPOMFWFMqPXDPOUSPM
  13. 3FRVFTU3FTQPOTF public class HelloRequest { private String name; } public

    class HelloResponse { private String message; private LocalDateTime dateTime; } @Bean public RSocketRequester rSocketRequester(RSocketRequester.Builder builder) { return builder.connectTcp("localhost", 7000).block(); } public Mono<HelloResponse> requestResponse(String name) { return rSocketRequester.route("requestResponse") .data(new HelloRequest(name)) .retrieveMono(HelloResponse.class); } @MessageMapping("requestResponse") public Mono<HelloResponse> requestResponse(HelloRequest request) { return Mono.just(new HelloResponse(String.format("Hello %s!", request.getName()), LocalDateTime.now())); } 3FRVFTUFS 3FTQPOEFS 4FOEPOF3FDFJWFPOF
  14. 3FRVFTU4USFBN public Flux<HelloResponse> requestStream(String name) { return rSocketRequester.route("requestStream") .data(new HelloRequest(name))

    .retrieveFlux(HelloResponse.class); } @MessageMapping("requestStream") public Flux<HelloResponse> requestStream(HelloRequest request) { Stream<HelloResponse> responses = Stream.generate(() -> new HelloResponse("Hello " + request.getName() + "!"), LocalDateTime.now())); return Flux.fromStream(responses); } 3FRVFTUFS 3FTQPOEFS 4FOEPOF3FDFJWFNBOZ
  15. 3FRVFTU$IBOOFM public Flux<HelloResponse> requestChannel(String name) { Flux<HelloRequest> requests = Flux.fromStream(Stream.generate(()

    -> new HelloRequest(name))); return rSocketRequester.route("requestChannel") .data(requests) .retrieveFlux(HelloResponse.class); } @MessageMapping("requestChannel") public Flux<HelloResponse> requestChannel(Flux<HelloRequest> requests) { return Flux.from(requests) .map(request -> new HelloResponse("Hello " + request.getName() + "!"), LocalDateTime.now())); } 3FRVFTUFS 3FTQPOEFS 4FOENBOZ3FDFJWFNBOZ
  16. 3FRVFTU'JSFBOE'PSHFU public Mono<Void> fireAndForget(String name) { return rSocketRequester.route("fireAndForget") .data(new HelloRequest(name))

    .send(); } @MessageMapping("fireAndForget") public Mono<Void> fireAndForget(HelloRequest request) { return Mono.empty(); } 3FRVFTUFS 3FTQPOEFS 4FOEPOF
  17. 5BMLT • Reactive Revolution • Deepdive into Reactive Spring with

    Coroutines and Kotlin Flow • Bootiful Kotlin • Bootiful Testing
  18. 3FBDUJWF3FWPMVUJPO https://devoxx.be/talk/?id=25802 • 3࣌ؒͷDeep Dive • SpringͷReactiveϓϩάϥϛϯάʹ͍ͭͯͷϥΠϒίʔσΟϯά • YouTube •

    https://www.youtube.com/watch?v=4-vEW8Ck_6g • Sample Code • https://github.com/joshlong/reactive-revolution • https://github.com/joshlong/reactive-spring-livelessons-2e
  19. 3FBDUJWF3FWPMVUJPO ηογϣϯͷ಺༰ • Java + Spring Boot 2.2 + Spring

    Data Reactive MongoDB • Java + Spring Boot 2.2 + Spring Data R2DBC • Java + Spring Boot 2.2 + WebFlux + WebSocket • Kotlin + Spring Boot 2.2 + Spring Cloud Gateway • Kotlin + Spring Boot 2.2 + Spring Cloud Gateway + Spring Security + Spring Data Reactive Redis(Redis Rate Limiter) • Kotlin + Spring Boot 2.2 + WebFlux.fn + WebClient • Kotlin + Spring Boot 2.2 + RSocket
  20. 4QSJOH%BUB3FBDUJWF .POHP%# @Document @Data @AllArgsConstructor public class Reservation { @Id

    private String id; private String name; } public interface ReservationRepository extends ReactiveCrudRepository<Reservation, String> { } @Component @RequiredArgsConstructor @Slf4j public class DataInitializer { private final ReservationRepository reservationRepository; @EventListener(ApplicationReadyEvent.class) public void ready() { Flux<Reservation> saved = Flux.just("AAA", "BBB", "CCC") .map(name -> new Reservation(null, name)) .flatMap(reservationRepository::save); reservationRepository.deleteAll() .thenMany(saved) .thenMany(reservationRepository.findAll()) .subscribe(reservation -> log.info("{}", reservation)); } }
  21. 4QSJOH%BUB3%#$ • Spring InitializrͰҎԼΛબ୒ • Spring Data R2DBC [Experimental] •

    H2 Database ※ ͜ͷηογϣϯͰ͸PostgreSQLΛ࢖͍ͬͯ·͕ͨ͠ɺH2Ͱઆ໌͠·͢
  22. 4QSJOH%BUB3%#$ @Data @NoArgsConstructor @AllArgsConstructor public class Reservation { @Id private

    Integer id; private String name; } public interface ReservationRepository extends ReactiveCrudRepository<Reservation, Integer> { } @Service @RequiredArgsConstructor @Slf4j public class ReservationService { private final ReservationRepository reservationRepository; private final TransactionalOperator transactionalOperator; public Flux<Reservation> saveAll(String... names) { return transactionalOperator.transactional( Flux.fromArray(names) .map(name -> new Reservation(null, name)) .flatMap(reservationRepository::save) .doOnNext(r -> log.info("{}", r))); } } TQSJOHSECDVSMSECDINFNEBUBCBTF PQUJPOT%#@$-04&@%&-":%#@$-04&@0/@&9*5'"-4& TQSJOHSECDVTFSOBNFVTFSOBNF TQSJOHSECDQBTTXPSEQBTTXPSE
  23. 8FC'MVY 8FC4PDLFU KBWB @Data @AllArgsConstructor public class GreetingRequest { private

    String name; } @Data @AllArgsConstructor public class GreetingResponse { private String message; } @Service public class GreetingService { public Flux<GreetingResponse> greet(GreetingRequest request) { return Flux .fromStream(Stream.generate(() -> new GreetingResponse( String.format("%s @ %s", request.getName(), Instant.now())))) .delayElements(Duration.ofSeconds(1)); } }
  24. 8FC'MVY 8FC4PDLFU KBWB @Configuration public class WebSocketConfig { @Bean public

    WebSocketHandler webSocketHandler(GreetingService greetingService) { return session -> { Flux<WebSocketMessage> messages = session.receive() .map(WebSocketMessage::getPayloadAsText) .map(GreetingRequest::new) .flatMap(greetingService::greet) .map(GreetingResponse::getMessage) .map(session::textMessage); return session.send(messages); }; } @Bean public SimpleUrlHandlerMapping simpleUrlHandlerMapping(WebSocketHandler webSocketHandler) { return new SimpleUrlHandlerMapping(Map.of("/ws/greetings", webSocketHandler), 1); } @Bean public WebSocketHandlerAdapter webSocketHandlerAdapter() { return new WebSocketHandlerAdapter(); } }
  25. 8FC'MVY 8FC4PDLFU KT <!DOCTYPE html> <html lang="en"> <head> </head> <body>

    <script> window.addEventListener('load', () => { const ws = new WebSocket('ws://localhost:8080/ws/greetings'); ws.addEventListener('open', () => { ws.send('Devoxx Belgium') }); ws.addEventListener('message', (msg) => { console.log(msg.data) }) }); </script> </body> </html>
  26. ,PUMJO 4QSJOH$MPVE (BUFXBZ @SpringBootApplication class Application { @Bean fun gateway(rlb:

    RouteLocatorBuilder) = rlb.routes { route { path("/proxy") and host("*.spring.io") filters { setPath("/reservations") addResponseHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, "*") } uri("http://localhost:8080") } } } fun main(args: Array<String>) { runApplication<Application>(*args) } ҎԼΛ࣮ߦ͢ΔͱɺIUUQMPDBMIPTUSFTFSWBUJPOTʹసૹ͞ΕΔ DVSMW)IPTUEFWPYYTQSJOHJPIUUQMPDBMIPTUQSPYZ TFSWFSQPSU
  27. ,PUMJO 4QSJOH$MPVE(BUFXBZ  4QSJOH4FDVSJUZ 3FEJT3BUF-JNJUFS • Spring InitializrͰҎԼΛબ୒ • Language

    • Kotlin • Dependencies • Gateway • Spring Security • Spring Data Reactive Redis
  28. ,PUMJO 4QSJOH$MPVE(BUFXBZ  4QSJOH4FDVSJUZ 3FEJT3BUF-JNJUFS @Bean fun authorization(http: ServerHttpSecurity) =

    http.httpBasic(Customizer.withDefaults()) .csrf { it.disable() } .authorizeExchange { it.pathMatchers("/proxy").authenticated() .anyExchange().permitAll() } .build() @Bean fun authentication(passwordEncoder: PasswordEncoder) = MapReactiveUserDetailsService( User.withUsername("user1") .password(passwordEncoder.encode("pass1")) .roles("USER", "ADMIN") .build()) @Bean fun passwordEncoder() = BCryptPasswordEncoder() σϞ༻ͷϢʔβʔΛ௥Ճ
  29. ,PUMJO 4QSJOH$MPVE(BUFXBZ  4QSJOH4FDVSJUZ 3FEJT3BUF-JNJUFS @Bean fun redisRateLimiter() = RedisRateLimiter(5,

    7) @Bean fun gateway(rlb: RouteLocatorBuilder, redisRateLimiter: RedisRateLimiter) = rlb.routes { route { path("/proxy") and host("*.spring.io") filters { setPath("/reservations") addResponseHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, "*") requestRateLimiter { it.rateLimiter = redisRateLimiter } } uri("http://localhost:8080") } } !QBSBNEFGBVMU3FQMFOJTI3BUFIPXNBOZUPLFOTQFSTFDPOEJOUPLFOCVDLFUBMHPSJUIN !QBSBNEFGBVMU#VSTU$BQBDJUZIPXNBOZUPLFOTUIFCVDLFUDBOIPMEJOUPLFOCVDLFU ҎԼΛ࣮ߦ͢ΔͱSBUFMJNJUUFS͕༗ޮʹͳΓɺϨεϙϯεʹʮ93BUF-JNJU3FNBJOJOHʯ ͳͲͷϔομʔ͕ૠೖ͞ΕΔ DVSMWVVTFSQBTT)IPTUEFWPYYTQSJOHJPIUUQMPDBMIPTUQSPYZ
  30. ,PUMJO 4QSJOH$MPVE(BUFXBZ  4QSJOH4FDVSJUZ 3FEJT3BUF-JNJUFS fun main(args: Array<String>) { runApplication<Application>(*args)

    { val context = beans { bean { RedisRateLimiter(5, 7) } } addInitializers(context) } } ,PUMJOͷ৔߹ɺCFBO͸,PUMJO%4-Λ࢖ͬͯ͜ͷΑ͏ʹॻ͘͜ ͱ΋Ͱ͖·͢
  31. ,PUMJO 8FC'MVYGO  8FC$MJFOU data class Reservation(val id: Int, val

    name: String) @Bean fun webClient(builder: WebClient.Builder) = builder.build() @Bean fun route(webClient: WebClient) = router { GET("/reservations/names") { val reservations = webClient.get() .uri("http://localhost:8080/reservations") .retrieve() .bodyToFlux<Reservation>() .map { it.name } .retryBackoff(10, Duration.ofSeconds(1)) ServerResponse.ok().body(reservations) } }
  32. ,PUMJO 34PDLFU DMJFOU data class GreetingRequest(val name: String) data class

    GreetingResponse(val message: String) @SpringBootApplication class Application { @Bean fun rSocketClient(rSocketRequester: RSocketRequester.Builder) = rSocketRequester.connectTcp("localhost", 7777).block() @Bean fun route(rSocketClient: RSocketRequester) = router { GET("/greetings/{name}") { val request = GreetingRequest(it.pathVariable("name")) val greetings: Flux<GreetingResponse> = rSocketClient.route("greetings") .data(request) .retrieveFlux() ServerResponse.ok() .contentType(MediaType.TEXT_EVENT_STREAM) .body(greetings) } } } 3FRVFTU4USFBN
  33. ,PUMJO 34PDLFU TFSWFS data class GreetingRequest(val name: String) data class

    GreetingResponse(val message: String) @Controller class RSocketController { @MessageMapping("greetings") fun greet(request: GreetingRequest): Flux<GreetingResponse> { return Flux.fromStream(Stream.generate { GreetingResponse("${request.name} @ $ {Instant.now()}") }) .delayElements(Duration.ofSeconds(1)) } } TQSJOHSTPDLFUTFSWFSQPSU
  34. 5BMLT • Reactive Revolution • Deepdive into Reactive Spring with

    Coroutines and Kotlin Flow • Bootiful Kotlin • Bootiful Testing
  35. %FFQEJWFJOUP3FBDUJWF4QSJOH XJUI$PSPVUJOFTBOE,PUMJO'MPX https://devoxx.be/talk/?id=40958 • 3࣌ؒͷDeep Diveʢ࣮ࡍ͸2࣌ؒ͘Β͍Ͱͨ͠ʣ • Kotlin + Reactive

    Springͷηογϣϯ • YouTube • https://www.youtube.com/watch?v=BoidEr_ZCGc • Sample Code • https://github.com/sdeleuze/spring-messenger
  36. ,PUMJO !$POpHVSBUJPO1SPQFSUJFT 4QSJOH#PPU·Ͱ @ConfigurationProperties("example.kotlin") class KotlinExampleProperties { lateinit var name:

    String lateinit var description: String val myService = MyService() class MyService { lateinit var apiToken: String lateinit var uri: URI } } example: kotlin: name: xxxxx description: xxxxx my-service: api-token: xxxxx uri: xxxxx
  37. ,PUMJO !$POpHVSBUJPO1SPQFSUJFT 4QSJOH#PPU @ConfigurationPropertiesScan // or @EnableConfigurationProperties(KotlinExampleProperties::class) class Application @ConstructorBinding

    @ConfigurationProperties("example.kotlin") data class KotlinExampleProperties(val name: String, val description: String, val myService: MyService) { data class MyService(val apiToken: String, val uri: URI) }
  38. ,PUMJO 8FC.WDGO @SpringBootApplication class Application { @Bean fun routes() =

    router { GET("/hello", ::hello) } fun hello(request: ServerRequest) = ServerResponse.ok().body("Hello world!") }
  39. ,PUMJO 8FC.WDGO data class User(val id: Int, val name: String)

    @SpringBootApplication class Application { @Bean fun routes(handler: UserHandler) = router { accept(MediaType.APPLICATION_JSON).nest { GET("/users", handler::findAll) GET("/users/{id}", handler::findById) } } } @Component class UserHandler(private val userRepository: UserRepository) { fun findAll(request: ServerRequest): ServerResponse = ok().body(userRepository.findAll()) fun findById(request: ServerRequest): ServerResponse { val id = request.pathVariable("id").toInt() return ok().body(userRepository.findById(id)) } }
  40. $PSPVUJOFT • Coroutineʹ͍ͭͯ͸ɺผͷηογϣϯʮCoroutines for Java Developersʯ͕෼͔Γ΍͍͢Ͱ͢ • YouTube • https://www.youtube.com/watch?v=wBpKAv4i8Ug

    • Slide • https://docs.google.com/presentation/d/ 192JLr64isB5WCmeqo4Fvf3snsDLWxrfkZ7A2nsNxb84/ edit#slide=id.p
  41. 5ISFBET$PSPVUJOFT • Threads • Context Switch • OS decision •

    Coroutines • Suspend & Resume • Coroutine Decision
  42. $POUFYU4XJUDI4VTQFOE • Context Switch • Save CPU state: stack, registers,

    EIP • Done by OS, no control • Suspend of a coroutine • Saves the function state, stack • Done by the code, 100% control
  43. 4VTQFOEJOHGVODUJPOT fun main() = runBlocking { launch { doWorld() }

    println("Hello,") } suspend fun doWorld() { delay(1000L) println("World!") } • ؔ਺ʹsuspendम০ࢠΛ෇͚Δ • Suspending function͸ɺcoroutine಺Ͱ௨ৗͷؔ਺ͷΑ͏ʹ࢖༻͞ΕΔ • Suspending function͸ɺdelayͷΑ͏ͳଞͷsuspending functionΛॱ൪ʹ࢖༻Ͱ͖Δ https://kotlinlang.org/docs/reference/coroutines/basics.html#extract-function-refactoring
  44. 4USVDUVSFEDPODVSSFODZ fun main() = runBlocking { // this: CoroutineScope launch

    { // launch a new coroutine in the scope of runBlocking delay(1000L) println("World!") } println("Hello,") } • ࣮ߦ͢ΔΦϖϨʔγϣϯͷಛఆͷείʔϓ಺ͰcoroutinesΛىಈͰ͖ΔʢGlobalScope಺Ͱىಈ͠ͳ͍ʣ • ্هͷྫͰ͸ɺmainؔ਺͸runBlocking coroutine builderΛ࢖༻ͯ͠coroutineʹม׵͞ΕΔ • શͯͷcoroutine builderʢrunBlocking, launchͳͲʣ͸ɺͦͷbuilderͷίʔυϒϩοΫͷείʔϓʹ CoroutineScopeͷΠϯελϯεΛ௥Ճ͢Δ • ֎ଆͷcoroutineʢ͜ͷྫͰ͸runBlockingʣ͸ɺείʔϓ಺Ͱىಈ͞Εͨશͯͷcoroutines͕׬ྃ͢Δ·Ͱ׬ྃ ͠ͳ͍ https://kotlinlang.org/docs/reference/coroutines/basics.html#structured-concurrency ग़ྗ݁Ռ͸ )FMMP  8PSME
  45. 'MPX5 • Suspending functions͸୯Ұͷ஋Λඇಉظʹฦ͕͢ɺFlow͸ෳ਺ͷ஋Λඇಉظʹฦ͢ • flow builderϒϩοΫ಺ͰαεϖϯυͰ͖Δ • FlowΛฦؔ͢਺ʹ͸suspendम০ࢠΛ෇͚ͳ͍ •

    Flow͸cold streamsͰɺFlow͕collect͞ΕΔ·Ͱflow builder಺ͷίʔυ͸࣮ߦ͞Εͳ͍ • Flow͸Reactive Streamsͱ૬ޓӡ༻࡞༻ͯ͠ɺbackpressueΛαϙʔτ͢Δ https://kotlinlang.org/docs/reference/coroutines/flow.html fun foo(): Flow<Int> = flow { // flow builder for (i in 1..3) { delay(100) // pretend we are doing something useful here emit(i) // emit next value } } fun main() = runBlocking { // Collect the flow foo().collect { value -> println(value) } }
  46. 4QSJOH $PSPVUJOFT • Coroutines allows to consume Spring Reactive stack

    with a nice balance between imperative and declarative style • Context interoperability between Coroutines and Reactor • Allows to support Reactive transactions and security
  47. 4QSJOHQSPWJEFTP⒏DJBM $PSPVUJOFTTVQQPSU • Spring WebFlux • Spring Data Redis •

    Spring Data MongoDB • Spring Data Cassandra • Spring Data R2DBC • Spring Vault • RSocket
  48. )PX3FBDUPS"1*T USBOTMBUFUP$PSPVUJOFT fun handler(): Mono<Void> → suspend fun handler() fun

    handler(): Mono<T> → suspend fun handler(): T fun handler(): Flux<T> → suspend fun handler(): Flow<T>
  49. $PSPVUJOFT 8FC'MVY data class User(val id: Int, val message: String)

    @RestController class UserController { @GetMapping("/users") suspend fun findAll(): Flow<User> { delay(1000) return flowOf(User(1, "name1"), User(2, "name2"), User(3, "name3")) } @GetMapping("/users/{id}") suspend fun findOne(@PathVariable id: Int): User { delay(1000) return User(id, "name$id") } }
  50. $PSPVUJOFT 8FC'MVYGO data class User(val id: Int, val message: String)

    @SpringBootApplication class Application { @Bean fun routes() = coRouter { "/users".nest { GET("/", ::findAll) GET("/{id}", ::findOne) } } suspend fun findAll(request: ServerRequest): ServerResponse { delay(1000) val users: Flow<User> = (1..5).map { User(it, "name$it") }.asFlow() return ok().bodyAndAwait(users) } suspend fun findOne(request: ServerRequest): ServerResponse { delay(1000) val id = request.pathVariable("id").toInt() return ok().bodyValueAndAwait(User(id, "name$id")) } }
  51. $PSPVUJOFT 8FC$MJFOU @RestController class UserController(private val webClient: WebClient) { @GetMapping("/users")

    suspend fun findAll(): Flow<User> = webClient.get() .uri("/users") .accept(MediaType.APPLICATION_JSON) .retrieve() .bodyToFlow() @GetMapping("/users/{id}") suspend fun findOne(@PathVariable id: Int): User = webClient.get() .uri("/users/{id}", id) .accept(MediaType.APPLICATION_JSON) .retrieve() .awaitBody() }
  52. $PSPVUJOFT 34PDLFU @Bean fun route(requester: RSocketRequester) = coRouter { GET("/greetings/{name}")

    { val request = GreetingRequest(it.pathVariable("name")) val greetings: Flow<String> = requester.route("greetings") .data(request) .retrieveFlow<GreetingResponse>() .map { response -> response.message } ok().sse().bodyAndAwait(greetings) } } @Controller class RSocketController { @MessageMapping("greetings") fun greet(request: GreetingRequest) = flow { while (true) { emit(GreetingResponse("${request.name} @ ${Instant.now()}")) delay(1000) } } } 4QSJOH#PPUͰ͸όά͕͋ΔͷͰSFUSJFWF'MPX͸࢖͑ͳ͍Ͱ͢ IUUQTHJUIVCDPNTQSJOHQSPKFDUTTQSJOHGSBNFXPSLJTTVFT
  53. $PSPVUJOFT 4QSJOH%BUB3%#$ 'VODUJPOBM5SBOTBDUJPOT @SpringBootApplication class Application(private val operator: TransactionalOperator, private

    val repository: UserRepository) { @EventListener(ApplicationReadyEvent::class) fun readyEvent() = runBlocking { operator.executeAndAwait { repository.insert(User(1, "name1")) repository.insert(User(2, "name2")) repository.insert(User(3, "name3")) } } } !5SBOTBDUJPOBM͸ݱ࣌఺Ͱ͸ະαϙʔτ
  54. $PSPVUJOFT 4QSJOH%BUB3%#$ 3FBDUJWF"1*T data class User(@field:Id val id: Int, val

    name: String) @Component class UserRepository(private val client: DatabaseClient) { fun findAll(): Flow<User> = client.select().from<User>().orderBy(asc("id")).fetch().flow() suspend fun findOne(id: Int) = client.select().from<User>().matching(where("id").`is`(id)).fetch().awaitOneOrNull() suspend fun insert(user: User) = client.insert().into<User>().using(user).await() suspend fun insertUntyped(name: String) = client.insert().into("user").value("name", name).await() suspend fun update(user: User) = client.update().table<User>().using(user).then().awaitFirstOrNull() suspend fun delete(id: Int) = client.delete().from<User>().matching(where("id").`is`(id)).then().awaitFirstOrNull() } 4QSJOH%BUB3FQPTJUPSJFT͸ݱ࣌఺Ͱ͸ະαϙʔτ IUUQTKJSBTQSJOHJPCSPXTF%"5"$./4
  55. $PSPVUJOFT 4QSJOH%BUB.POHP%# 3FBDUJWF"1*T @Component class UserRepository(private val operations: ReactiveFluentMongoOperations) {

    fun findAll(): Flow<User> = operations.query<User>().flow() suspend fun findOne(id: String): User = operations.query<User>() .matching(query(where(User::id).isEqualTo(id))) .awaitOne() suspend fun insert(user: User): User = operations.insert<User>().oneAndAwait(user) suspend fun update(user: User): User = operations.update<User>() .replaceWith(user) .asType<User>() .findReplaceAndAwait() }
  56. 4QSJOH'V val app = application(WebApplicationType.REACTIVE) { logging { level =

    LogLevel.INFO } beans { bean<UserHandler>() bean<UserRepository>() } webFlux { port = if (profiles.contains("test")) 8181 else 8080 router { val userHandler = ref<UserHandler>() GET("/users", userHandler::findAll) } codecs { string() jackson() } } } fun main() { app.run() } data class User(val id: Int, val name: String) @Suppress("UNUSED_PARAMETER") class UserHandler(private val repository: UserRepository) { fun findAll(request: ServerRequest): Mono<ServerResponse> = ok().body(fromValue(repository.findAll())) }
  57. 4QSJOH'V $PSPVUJOFT val app = application(WebApplicationType.REACTIVE) { logging { level

    = LogLevel.INFO } beans { bean<UserHandler>() bean<UserRepository>() } webFlux { port = if (profiles.contains("test")) 8181 else 8080 coRouter { val userHandler = ref<UserHandler>() GET("/users", userHandler::findAll) } codecs { string() jackson() } } } fun main() { app.run() } data class User(val id: Int, val name: String) @Suppress("UNUSED_PARAMETER") class UserHandler(private val repository: UserRepository) { suspend fun findAll(request: ServerRequest): ServerResponse = ok().bodyAndAwait(repository.findAll()) }
  58. 5BMLT • Reactive Revolution • Deepdive into Reactive Spring with

    Coroutines and Kotlin Flow • Bootiful Kotlin • Bootiful Testing
  59. #PPUJGVM,PUMJO https://devoxx.be/talk/?id=128352 • 50෼ͷηογϣϯ • Kotlin + Spring BootͷϥΠϒίʔσΟϯά •

    YouTube • https://www.youtube.com/watch?v=etwrkcqIMnk • Sample Code • https://github.com/joshlong/kotlin-and-spring-livelessons
  60. #PPUJGVM,PUMJO 1. Kotlin + Spring Boot 2.2 + Spring JDBC

    2. Kotlin + Spring Boot 2.2 + Exposed 3. Kotlin + Spring Boot 2.2 + Spring Data Reactive MongoDB + Spring Gateway + Coroutines ※ 3͸લड़ͷηογϣϯͱࣅ͍ͯΔͨΊׂѪ͠·͢
  61. ,PUMJO 4QSJOH+%#$ @SpringBootApplication class Application fun main(args: Array<String>) { runApplication<Application>(*args)

    } data class Customer(val id: Int, val name: String) @Service class CustomerService(private val jdbcTemplate: JdbcTemplate) { fun findAll(): List<Customer> = jdbcTemplate.query("SELECT * FROM customer") { rs, _ -> Customer(rs.getInt("id"), rs.getString("name")) } }
  62. 4QSJOH#PPU &YQPTFE https://github.com/JetBrains/Exposed • KotlinͰॻ͔ΕͨJetBrains੡ͷܰྔͳSQLϥΠϒϥϦ • ݱࡏ͸Maven Centralʹ͸ͳ͘ɺBintray (JCenter)ͷΈ •

    exposed-spring-boot-starter͕͋ΓɺҎԼͷػೳ͕ఏڙ͞Ε͍ͯΔ • SpringTransactionManagerͷࣗಈઃఆ • schemaͷࣗಈੜ੒ ※ ͜ͷηογϣϯͰ͸exposed-spring-boot-starter͸࢖͍ͬͯ·ͤΜͰ͕ͨ͠ɺ starterΛ࢖ͬͨํ๏Λ঺հ͠·͢
  63. &YQPTFE CVJMEHSBEMFLUT repositories { mavenCentral() jcenter() } dependencies { …

    implementation(“org.jetbrains.exposed:exposed-spring-boot-starter:0.20.2") … }
  64. &YQPTFE LU import org.jetbrains.exposed.sql.Table object Customers : Table() { val

    id = integer("id").autoIncrement() val name = varchar("name", 255) override val primaryKey = PrimaryKey(id) } data class Customer(val id: Int, val name: String) TQSJOHFYQPTFEHFOFSBUFEEEMUSVFʹ͢Ε͹ɺҎԼͷ%%-͕࣮ߦ͞ΕΔ $3&"5&5"#-&*'/05&9*454 $6450.&34 *%*/5"650@*/$3&.&/513*."3:,&: /".&7"3$)"3  /05/6--
  65. &YQPTFE LU @Service @Transactional class CustomerService { fun selectAll(): List<Customer>

    = Customers.selectAll().map { Customer(it[Customers.id], it[Customers.name]) } fun insert(customer: Customer) = Customers.insert { it[name] = customer.name } fun update(customer: Customer) = Customers.update({ Customers.id eq customer.id }) { it[name] = customer.name } fun delete(id: Int) = Customers.deleteWhere { Customers.id eq id } } 4QSJOHͷ!5SBOTBDUJPOBM͕࢖༻Ͱ͖Δ
  66. 5BMLT • Reactive Revolution • Deepdive into Reactive Spring with

    Coroutines and Kotlin Flow • Bootiful Kotlin • Bootiful Testing
  67. #PPUJGVM5FTUJOH https://devoxx.be/talk/?id=25801 • 50෼ͷηογϣϯ • Spring Bootͷςετʹ͍ͭͯͷϥΠϒίʔσΟϯά • YouTube •

    https://www.youtube.com/watch?v=wAYt4Z4SF7g • Sample Code • https://github.com/joshlong/bootiful-testing
  68. #PPUJGVM5FTUJOH 1. Spring Boot 2.2 + WebFlux + Spring Data

    Reactive MongoDB 2. Spring Boot 2.2 + WebClient + WireMock (Contract Stub Runner) 3. Spring Boot 2.2 + WebFlux,WebClient + Spring Cloud Contract (Contract Verifier, Contract Stub Runner) ※ ͜ͷηογϣϯͰ͸JUnit 4Λ࢖͍ͬͯ·͕ͨ͠ɺJUnit 5Ͱઆ໌͠·͢
  69. 8FC'MVY5FTU @WebFluxTest public class ControllerTest { @Autowired private WebTestClient webTestClient;

    @Test public void test() { webTestClient.get() .uri("http://localhost:8080/reservations") .exchange() .expectHeader().contentType(MediaType.APPLICATION_JSON) .expectStatus().isOk() .expectBody().json("[{\"id\":1,\"name\":\"name1\"},...]"); } } ςετର৅ͷ FOEQPJOU
  70. 3FBDUJWF.POHP%#5FTU @DataMongoTest public class ReservationRepositoryTest { @Autowired private ReservationRepository reservationRepository;

    @Test public void test() { Reservation reservation = new Reservation(null, "name1"); StepVerifier.create(reservationRepository(reservation)) .expectNextMatches(result -> result.getId() != null && "name1".equals(result.getName())) .verifyComplete(); } } ςετର৅ͷ SFQPTJUPSZ
  71. 8FC$MJFOU5FTU 8JSF.PDL http://wiremock.org/ • WebClientͷςετʹ͸ɺOkHttp MockWebServer΍ WireMockͳͲͷMock Web Server͕࢖༻Ͱ͖Δ •

    RestTemplateͷςετͰ࢖༻Ͱ͖ΔMockRestServiceServer ͸ɺWebClientΛαϙʔτ͍ͯ͠ͳ͍ • https://docs.spring.io/spring-framework/docs/current/spring- framework-reference/web-reactive.html#webflux-client-testing • https://github.com/spring-projects/spring-framework/issues/ 19852
  72. 8FC$MJFOU TPVSDFDPEF @Component public class GitHubApiClient { private final WebClient

    webClient; public GitHubApiClient(WebClient.Builder builder, @Value("${github.baseUrl:https://api.github.com}") String baseUrl) { webClient = builder.baseUrl(baseUrl).build(); } public Mono<User> getUser(String username) { return webClient.get() .uri("/users/{username}", username) .retrieve() .bodyToMono(User.class); } }
  73. 8JSF.PDL UFTUDPEF @SpringBootTest(properties = "github.baseUrl=http://localhost:8080") @AutoConfigureWireMock(port = 8080) public class

    GitHubApiClientTest { @Autowired private GitHubApiClient client; @Test public void getUserTest() { String body = "{\"login\":\"hirakida\"}"; stubFor(get(urlEqualTo("/users/hirakida")) .willReturn(aResponse() .withBody(body) .withHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) .withStatus(HttpStatus.OK.value()))); Mono<User> user = client.getUser("hirakida"); StepVerifier.create(user) .expectNextMatches(result -> result != null && "hirakida".equals(result.getLogin())) .verifyComplete(); } }
  74. 1SPEVDFS TPVSDFDPEF @RestController @RequiredArgsConstructor public class UserController { private final

    UserService userService; @GetMapping("/users/{id}") public Mono<User> getUser(@PathVariable long id) { return userService.getUser(id); } } @Data @NoArgsConstructor @AllArgsConstructor public class User { private long id; private String name; }
  75. 1SPEVDFS CVJMEHSBEMF import org.springframework.cloud.contract.verifier.config.TestFramework plugins { id 'org.springframework.boot' version '2.2.2.RELEASE'

    id 'org.springframework.cloud.contract' version '2.2.0.RELEASE' } dependencies { … testImplementation('org.springframework.boot:spring-boot-starter-test') { exclude group: 'org.junit.vintage', module: 'junit-vintage-engine' } testImplementation 'org.springframework.cloud:spring-cloud-starter-contract-verifier' } contracts { testFramework = TestFramework.JUNIT5 testMode = 'EXPLICIT' baseClassForTests = 'com.example.BaseClass' } ޙड़͢Δ#BTF$MBTTͷQBUIΛࢦఆ ͢Δ 4QSJOH$MPVE$POUSBDU (SBEMF1MVHJO
  76. 1SPEVDFS #BTF$MBTT'PS5FTUT @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = "server.port=0") public class

    BaseClass { @LocalServerPort private int port; @MockBean private UserService userService; @BeforeEach public void setup() { RestAssured.baseURI = "http://localhost"; RestAssured.port = port; when(userService.getUser(1)).thenReturn(Mono.just(new User(1, "name1"))); } }
  77. 1SPEVDFS HSPPWZ import org.springframework.cloud.contract.spec.Contract Contract.make { description('User API test') request

    { method "GET" url "/users/1" } response { status 200 headers { contentType(applicationJson()) } body """ { "id": 1, "name": "name1" } """ } } ͜ͷHSPPWZϑΝΠϧΛʮTSDUFTUSFTPVSDFTDPOUSBDUTʯʹஔ͘ CVJME͢Δͱ<BSUJGBDU*E><WFSTJPO>TUVCTKBS͕Ͱ͖ΔͷͰɺ͜ΕΛ DPOTVNFSଆ͕࢖༻͢Δ $POTVNFSଆ͕ظ଴͢Δ݁Ռ
  78. $POTVNFS TPVSDFDPEF @Component public class UserApiClient { private final WebClient

    webClient; public UserApiClient(WebClient.Builder builder, @Value("${producer.host:localhost}") String host, @Value("${producer.port:8080}") int port) { webClient = builder.baseUrl("http://" + host + ':' + port).build(); } public Mono<User> getUser(long id) { return webClient.get() .uri("/users/{id}", id) .retrieve() .bodyToMono(User.class); } } @Data @NoArgsConstructor @AllArgsConstructor public class User { private long id; private String name; } 1SPEVDFSͷ"1*Λݺͼग़͢ίʔυ
  79. $POTVNFS UFTUDPEF @SpringBootTest(webEnvironment = WebEnvironment.NONE, properties = "producer.port=${stubrunner.runningstubs.contract-producer.port}") @AutoConfigureStubRunner(ids =

    "com.example:contract-producer:+", stubsMode = StubsMode.LOCAL) public class UserApiClientTest { @Autowired private UserApiClient client; @Test public void getUserTest() { Mono<User> response = client.getUser(1); StepVerifier.create(response) .expectNextMatches(result -> result != null && result.getId() == 1 && "name1".equals(result.getName())) .verifyComplete(); } } ىಈ͍ͯ͠ΔTUVCͷQPSU͕औಘͰ͖Δ DPOUSBDUQSPEVDFS͸BSUJGBDU*E JETʹTUVCΛࢦఆ͢Δ <HSPVQ*E>BSUJGBDU*E<WFSTJPO><DMBTTJpFS><QPSU>