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

Devoxx Belgium 2019 報告会 Vol.1

Avatar for Yosuke Hirakida Yosuke Hirakida
January 15, 2020
340

Devoxx Belgium 2019 報告会 Vol.1

Avatar for Yosuke Hirakida

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>