Slide 1

Slide 1 text

%FWPYY#FMHJVN ใࠂձ7PM ೥݄೔ ։ాཅհ

Slide 2

Slide 2 text

ࣗݾ঺հ • ։ా ཅհ (@hirakida_) • LINE Fukuokaגࣜձࣾ ։ൃ1ࣨ • αʔόʔαΠυΤϯδχΞ

Slide 3

Slide 3 text

Vol.1Ͱ͸ɺSpring FrameworkͷηογϣϯΛ঺ հ͠·͢ Vol.2Ͱ͸ɺVictor Rentea͞ΜͷClean Codeʹͭ ͍ͯ࿩͢༧ఆͰ͢

Slide 4

Slide 4 text

5BMLT • Reactive Revolution • Deepdive into Reactive Spring with Coroutines and Kotlin Flow • Bootiful Kotlin • Bootiful Testing

Slide 5

Slide 5 text

R2DBCͱRSocket͸ෳ਺ͷηογϣϯͰऔΓ্ ͛ΒΕ͍ͯͨͷͰɺઌʹ঺հ͠·͢

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

3%#$$MJFOUT ˞,PUZTB ,PUMJOUZQFTBGF IUUQTHJUIVCDPNQVMMWFSULPUZTB pSTUQBSUZͷSECDDMJFOU΋͋Γ·͢ IUUQTHJUIVCDPNSECDSECDDMJFOU

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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 saveAll(String... names) { return transactionalOperator.transactional( Flux.fromArray(names) .map(name -> new User(null, name)) .flatMap(reservationRepository::save)); } }

Slide 12

Slide 12 text

4QSJOH%BUB3%#$ 3FBDUJWF"1*T @Component public class UserRepository { private final DatabaseClient client; public UserRepository(DatabaseClient client) { this.client = client; } public Flux findAll() { return client.select().from(User.class).fetch().all(); } public Mono findById(Integer id) { return client.select().from(User.class).matching(where("id").is(id)).fetch().one(); } public Mono insert(User user) { return client.insert().into(User.class).using(user).then(); } public Mono update(User user) { return client.update().table(User.class).using(user).then(); } public Mono deleteById(Integer id) { return client.delete().from(User.class).matching(where("id").is(id)).then(); } }

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

JOUFSBDUJPONPEFMT • Request Response (stream of 1) • Request Stream (finite stream of many) • Request Channel (bi-directional streams) • Request Fire-and-Forget (no response)

Slide 16

Slide 16 text

3FRVFTU3FTQPOTF send one / receive one 3&26&45 3&410/4& 1":-0"%

Slide 17

Slide 17 text

3FRVFTU4USFBN send one / receive many 3&26&45 453&". 1":-0"% 1":-0"% 1":-0"%

Slide 18

Slide 18 text

3FRVFTU$IBOOFM send many / receive many 3&26&45 $)"//&- 1":-0"% 1":-0"% 1":-0"% 1":-0"% 1":-0"%

Slide 19

Slide 19 text

3FRVFTU'JSFBOE'PSHFU send one 3&26&45 '/'

Slide 20

Slide 20 text

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/

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

34PDLFU4QSJOH#PPU • Spring BootͰ͸Transport ProtocolʹTCPͱWebSocketΛ αϙʔτ͍ͯ͠Δ https://docs.spring.io/spring/docs/current/spring-framework- reference/pdf/rsocket.pdf

Slide 23

Slide 23 text

3FRVFTU3FTQPOTF 5SBOTQPSUʹ5$1Λ࢖ͬͨ৔߹ 5$1ͷ࣍ͷϨΠϠ͕)551Ͱ͸ͳ͘34PDLFU 3&26&45@3&410/4& 1":-0"%

Slide 24

Slide 24 text

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 requestResponse(String name) { return rSocketRequester.route("requestResponse") .data(new HelloRequest(name)) .retrieveMono(HelloResponse.class); } @MessageMapping("requestResponse") public Mono requestResponse(HelloRequest request) { return Mono.just(new HelloResponse(String.format("Hello %s!", request.getName()), LocalDateTime.now())); } 3FRVFTUFS 3FTQPOEFS 4FOEPOF3FDFJWFPOF

Slide 25

Slide 25 text

3FRVFTU4USFBN 3FRVFTU@/ͳͷͰɺ3FTQPOEFS͸1BZMPBEΛૹ৴ ͦͷޙɺ3FTQPOEFS͸৽͍͠3FRVFTU@/͕དྷΔ·Ͱ଴ͭ

Slide 26

Slide 26 text

3FRVFTU4USFBN public Flux requestStream(String name) { return rSocketRequester.route("requestStream") .data(new HelloRequest(name)) .retrieveFlux(HelloResponse.class); } @MessageMapping("requestStream") public Flux requestStream(HelloRequest request) { Stream responses = Stream.generate(() -> new HelloResponse("Hello " + request.getName() + "!"), LocalDateTime.now())); return Flux.fromStream(responses); } 3FRVFTUFS 3FTQPOEFS 4FOEPOF3FDFJWFNBOZ

Slide 27

Slide 27 text

3FRVFTU$IBOOFM

Slide 28

Slide 28 text

3FRVFTU$IBOOFM public Flux requestChannel(String name) { Flux requests = Flux.fromStream(Stream.generate(() -> new HelloRequest(name))); return rSocketRequester.route("requestChannel") .data(requests) .retrieveFlux(HelloResponse.class); } @MessageMapping("requestChannel") public Flux requestChannel(Flux requests) { return Flux.from(requests) .map(request -> new HelloResponse("Hello " + request.getName() + "!"), LocalDateTime.now())); } 3FRVFTUFS 3FTQPOEFS 4FOENBOZ3FDFJWFNBOZ

Slide 29

Slide 29 text

3FRVFTU'JSFBOE'PSHFU

Slide 30

Slide 30 text

3FRVFTU'JSFBOE'PSHFU public Mono fireAndForget(String name) { return rSocketRequester.route("fireAndForget") .data(new HelloRequest(name)) .send(); } @MessageMapping("fireAndForget") public Mono fireAndForget(HelloRequest request) { return Mono.empty(); } 3FRVFTUFS 3FTQPOEFS 4FOEPOF

Slide 31

Slide 31 text

34PDLFU 8FC4PDLFU REQUEST RESPONSE 5SBOTQPSUʹ8FC4PDLFUΛ࢖ͬͨ৔߹ 8FC4PDLFUͷ1BZMPBE͕34PDLFUϑϨʔϜ

Slide 32

Slide 32 text

5BMLT • Reactive Revolution • Deepdive into Reactive Spring with Coroutines and Kotlin Flow • Bootiful Kotlin • Bootiful Testing

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

4QSJOH%BUB3FBDUJWF .POHP%# • Spring InitializrͰҎԼΛબ୒ • Dependencies • Spring Data Reactive MongoDB • Lombok

Slide 36

Slide 36 text

4QSJOH%BUB3FBDUJWF .POHP%# @Document @Data @AllArgsConstructor public class Reservation { @Id private String id; private String name; } public interface ReservationRepository extends ReactiveCrudRepository { } @Component @RequiredArgsConstructor @Slf4j public class DataInitializer { private final ReservationRepository reservationRepository; @EventListener(ApplicationReadyEvent.class) public void ready() { Flux 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)); } }

Slide 37

Slide 37 text

4QSJOH%BUB3%#$ • Spring InitializrͰҎԼΛબ୒ • Spring Data R2DBC [Experimental] • H2 Database ※ ͜ͷηογϣϯͰ͸PostgreSQLΛ࢖͍ͬͯ·͕ͨ͠ɺH2Ͱઆ໌͠·͢

Slide 38

Slide 38 text

4QSJOH%BUB3%#$ @Data @NoArgsConstructor @AllArgsConstructor public class Reservation { @Id private Integer id; private String name; } public interface ReservationRepository extends ReactiveCrudRepository { } @Service @RequiredArgsConstructor @Slf4j public class ReservationService { private final ReservationRepository reservationRepository; private final TransactionalOperator transactionalOperator; public Flux 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

Slide 39

Slide 39 text

8FC'MVY8FC4PDLFU • Spring InitializrͰҎԼΛબ୒ • Dependencies • Spring Reactive Web • Lombok

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

8FC'MVY8FC4PDLFU KBWB @Configuration public class WebSocketConfig { @Bean public WebSocketHandler webSocketHandler(GreetingService greetingService) { return session -> { Flux 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(); } }

Slide 42

Slide 42 text

8FC'MVY8FC4PDLFU KT 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) }) });

Slide 43

Slide 43 text

,PUMJO4QSJOH$MPVE (BUFXBZ • Spring InitializrͰҎԼΛબ୒ • Language • Kotlin • Dependencies • Gateway

Slide 44

Slide 44 text

,PUMJO4QSJOH$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) { runApplication(*args) } ҎԼΛ࣮ߦ͢ΔͱɺIUUQMPDBMIPTUSFTFSWBUJPOTʹసૹ͞ΕΔ DVSMW)IPTUEFWPYYTQSJOHJPIUUQMPDBMIPTUQSPYZ TFSWFSQPSU

Slide 45

Slide 45 text

,PUMJO4QSJOH$MPVE(BUFXBZ 4QSJOH4FDVSJUZ3FEJT3BUF-JNJUFS • Spring InitializrͰҎԼΛબ୒ • Language • Kotlin • Dependencies • Gateway • Spring Security • Spring Data Reactive Redis

Slide 46

Slide 46 text

,PUMJO4QSJOH$MPVE(BUFXBZ 4QSJOH4FDVSJUZ3FEJT3BUF-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() σϞ༻ͷϢʔβʔΛ௥Ճ

Slide 47

Slide 47 text

,PUMJO4QSJOH$MPVE(BUFXBZ 4QSJOH4FDVSJUZ3FEJT3BUF-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

Slide 48

Slide 48 text

,PUMJO4QSJOH$MPVE(BUFXBZ 4QSJOH4FDVSJUZ3FEJT3BUF-JNJUFS fun main(args: Array) { runApplication(*args) { val context = beans { bean { RedisRateLimiter(5, 7) } } addInitializers(context) } } ,PUMJOͷ৔߹ɺCFBO͸,PUMJO%4-Λ࢖ͬͯ͜ͷΑ͏ʹॻ͘͜ ͱ΋Ͱ͖·͢

Slide 49

Slide 49 text

,PUMJO8FC'MVYGO 8FC$MJFOU • Spring InitializrͰҎԼΛબ୒ • Language • Kotlin • Dependencies • Spring Reactive Web

Slide 50

Slide 50 text

,PUMJO8FC'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() .map { it.name } .retryBackoff(10, Duration.ofSeconds(1)) ServerResponse.ok().body(reservations) } }

Slide 51

Slide 51 text

,PUMJO34PDLFU • Spring InitializrͰҎԼΛબ୒ • Language • Kotlin • Dependencies • RSocket • Spring Reactive Web

Slide 52

Slide 52 text

,PUMJO34PDLFU 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 = rSocketClient.route("greetings") .data(request) .retrieveFlux() ServerResponse.ok() .contentType(MediaType.TEXT_EVENT_STREAM) .body(greetings) } } } 3FRVFTU4USFBN

Slide 53

Slide 53 text

,PUMJO34PDLFU TFSWFS data class GreetingRequest(val name: String) data class GreetingResponse(val message: String) @Controller class RSocketController { @MessageMapping("greetings") fun greet(request: GreetingRequest): Flux { return Flux.fromStream(Stream.generate { GreetingResponse("${request.name} @ $ {Instant.now()}") }) .delayElements(Duration.ofSeconds(1)) } } TQSJOHSTPDLFUTFSWFSQPSU

Slide 54

Slide 54 text

5BMLT • Reactive Revolution • Deepdive into Reactive Spring with Coroutines and Kotlin Flow • Bootiful Kotlin • Bootiful Testing

Slide 55

Slide 55 text

%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

Slide 56

Slide 56 text

,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

Slide 57

Slide 57 text

,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) }

Slide 58

Slide 58 text

,PUMJO8FC.WDGO @SpringBootApplication class Application { @Bean fun routes() = router { GET("/hello", ::hello) } fun hello(request: ServerRequest) = ServerResponse.ok().body("Hello world!") }

Slide 59

Slide 59 text

,PUMJO8FC.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)) } }

Slide 60

Slide 60 text

$PSPVUJOFT • Coroutinesͱ͸ϓϩάϥϛϯάͷߏ଄ͷҰछ • Subroutines͕ΤϯτϦʔ͔ΒϦλʔϯ·ͰΛҰͭͷॲཧ୯Ґͱ ͢Δͷʹର͠ɺCoroutines͸͍ͬͨΜॲཧΛதஅʢsuspendʣ͠ ͨޙɺଓ͖͔ΒॲཧΛ࠶։ʢresumeʣͰ͖Δ • Coroutineͱ͍͏໊শ͸ɺMelvin Conwayͷ1963೥ͷ࿦จ͕ىݯ https://en.wikipedia.org/wiki/Coroutine https://ja.wikipedia.org/wiki/ίϧʔνϯ

Slide 61

Slide 61 text

$PSPVUJOFT • Kotlinͷ৔߹͸ɺJetBrainsʹΑͬͯfirst-party libraryͱͯ͠ kotlinx.coroutinesʹ࣮૷͞Ε͍ͯΔ • KotlinͰ͸ɺlight-weight threadsͱఆٛ͞Ε͍ͯΔ https://kotlinlang.org/docs/reference/coroutines- overview.html

Slide 62

Slide 62 text

$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

Slide 63

Slide 63 text

5ISFBET$PSPVUJOFT • Threads • Context Switch • OS decision • Coroutines • Suspend & Resume • Coroutine Decision

Slide 64

Slide 64 text

$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

Slide 65

Slide 65 text

NBJOCVJMEJOHCMPDLT • Suspending Functions • Structured concurrency • Flow

Slide 66

Slide 66 text

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

Slide 67

Slide 67 text

(MPCBM4DPQF • ͜ͷྫͰ͸ɺGlobalScopeʹ৽͍͠coroutineΛىಈ͍ͯ͠Δ • ͜ͷcoroutineͷlifetime͸ɺΞϓϦέʔγϣϯશମͷlifetimeʹΑͬͯͷΈ੍ݶ͞ΕΔ • https://kotlinlang.org/docs/reference/coroutines/basics.html#your-first-coroutine fun main() { GlobalScope.launch { delay(1000L) println("World!") } println("Hello,") Thread.sleep(2000L) }

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

'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 = 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) } }

Slide 70

Slide 70 text

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

Slide 71

Slide 71 text

4QSJOHQSPWJEFTP⒏DJBM $PSPVUJOFTTVQQPSU • Spring WebFlux • Spring Data Redis • Spring Data MongoDB • Spring Data Cassandra • Spring Data R2DBC • Spring Vault • RSocket

Slide 72

Slide 72 text

4UBUVTPG$PSPVUJOFT TVQQPSU

Slide 73

Slide 73 text

)PX3FBDUPS"1*T USBOTMBUFUP$PSPVUJOFT fun handler(): Mono → suspend fun handler() fun handler(): Mono → suspend fun handler(): T fun handler(): Flux → suspend fun handler(): Flow

Slide 74

Slide 74 text

$PSPVUJOFT8FC'MVY data class User(val id: Int, val message: String) @RestController class UserController { @GetMapping("/users") suspend fun findAll(): Flow { 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") } }

Slide 75

Slide 75 text

$PSPVUJOFT8FC'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 = (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")) } }

Slide 76

Slide 76 text

$PSPVUJOFT8FC$MJFOU @RestController class UserController(private val webClient: WebClient) { @GetMapping("/users") suspend fun findAll(): Flow = 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() }

Slide 77

Slide 77 text

$PSPVUJOFT34PDLFU @Bean fun route(requester: RSocketRequester) = coRouter { GET("/greetings/{name}") { val request = GreetingRequest(it.pathVariable("name")) val greetings: Flow = requester.route("greetings") .data(request) .retrieveFlow() .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

Slide 78

Slide 78 text

$PSPVUJOFT4QSJOH%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͸ݱ࣌఺Ͱ͸ະαϙʔτ

Slide 79

Slide 79 text

$PSPVUJOFT4QSJOH%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 = client.select().from().orderBy(asc("id")).fetch().flow() suspend fun findOne(id: Int) = client.select().from().matching(where("id").`is`(id)).fetch().awaitOneOrNull() suspend fun insert(user: User) = client.insert().into().using(user).await() suspend fun insertUntyped(name: String) = client.insert().into("user").value("name", name).await() suspend fun update(user: User) = client.update().table().using(user).then().awaitFirstOrNull() suspend fun delete(id: Int) = client.delete().from().matching(where("id").`is`(id)).then().awaitFirstOrNull() } 4QSJOH%BUB3FQPTJUPSJFT͸ݱ࣌఺Ͱ͸ະαϙʔτ IUUQTKJSBTQSJOHJPCSPXTF%"5"$./4

Slide 80

Slide 80 text

$PSPVUJOFT4QSJOH%BUB.POHP%# 3FBDUJWF"1*T @Component class UserRepository(private val operations: ReactiveFluentMongoOperations) { fun findAll(): Flow = operations.query().flow() suspend fun findOne(id: String): User = operations.query() .matching(query(where(User::id).isEqualTo(id))) .awaitOne() suspend fun insert(user: User): User = operations.insert().oneAndAwait(user) suspend fun update(user: User): User = operations.update() .replaceWith(user) .asType() .findReplaceAndAwait() }

Slide 81

Slide 81 text

4QSJOH'V https://github.com/spring-projects-experimental/spring-fu • Spring BootͷઃఆΛSpring Kotlin DSLͰߦ͑Δ • ىಈ͕ૣͯ͘ϝϞϦফඅྔ͕গͳ͍ • Experimental

Slide 82

Slide 82 text

4QSJOH'V val app = application(WebApplicationType.REACTIVE) { logging { level = LogLevel.INFO } beans { bean() bean() } webFlux { port = if (profiles.contains("test")) 8181 else 8080 router { val userHandler = ref() 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 = ok().body(fromValue(repository.findAll())) }

Slide 83

Slide 83 text

4QSJOH'V $PSPVUJOFT val app = application(WebApplicationType.REACTIVE) { logging { level = LogLevel.INFO } beans { bean() bean() } webFlux { port = if (profiles.contains("test")) 8181 else 8080 coRouter { val userHandler = ref() 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()) }

Slide 84

Slide 84 text

5BMLT • Reactive Revolution • Deepdive into Reactive Spring with Coroutines and Kotlin Flow • Bootiful Kotlin • Bootiful Testing

Slide 85

Slide 85 text

#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

Slide 86

Slide 86 text

#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͸લड़ͷηογϣϯͱࣅ͍ͯΔͨΊׂѪ͠·͢

Slide 87

Slide 87 text

,PUMJO4QSJOH+%#$ • Spring InitializrͰҎԼΛબ୒ • Language • Kotlin • Dependencies • JDBC API

Slide 88

Slide 88 text

,PUMJO4QSJOH+%#$ @SpringBootApplication class Application fun main(args: Array) { runApplication(*args) } data class Customer(val id: Int, val name: String) @Service class CustomerService(private val jdbcTemplate: JdbcTemplate) { fun findAll(): List = jdbcTemplate.query("SELECT * FROM customer") { rs, _ -> Customer(rs.getInt("id"), rs.getString("name")) } }

Slide 89

Slide 89 text

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Λ࢖ͬͨํ๏Λ঺հ͠·͢

Slide 90

Slide 90 text

&YQPTFE CVJMEHSBEMFLUT repositories { mavenCentral() jcenter() } dependencies { … implementation(“org.jetbrains.exposed:exposed-spring-boot-starter:0.20.2") … }

Slide 91

Slide 91 text

&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--

Slide 92

Slide 92 text

&YQPTFE LU @Service @Transactional class CustomerService { fun selectAll(): List = 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͕࢖༻Ͱ͖Δ

Slide 93

Slide 93 text

5BMLT • Reactive Revolution • Deepdive into Reactive Spring with Coroutines and Kotlin Flow • Bootiful Kotlin • Bootiful Testing

Slide 94

Slide 94 text

#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

Slide 95

Slide 95 text

#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Ͱઆ໌͠·͢

Slide 96

Slide 96 text

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

Slide 97

Slide 97 text

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

Slide 98

Slide 98 text

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

Slide 99

Slide 99 text

8FC$MJFOU5FTU 8JSF.PDL •Spring InitializrͰҎԼΛબ୒ • Spring Reactive Web • Contract Stub Runner

Slide 100

Slide 100 text

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 getUser(String username) { return webClient.get() .uri("/users/{username}", username) .retrieve() .bodyToMono(User.class); } }

Slide 101

Slide 101 text

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 = client.getUser("hirakida"); StepVerifier.create(user) .expectNextMatches(result -> result != null && "hirakida".equals(result.getLogin())) .verifyComplete(); } }

Slide 102

Slide 102 text

4QSJOH$MPVE$POUSBDU https://spring.io/projects/spring-cloud-contract • Consumer Driven Contract(CDC) Testing͕Ͱ͖Δ • Producer͸Contract VerifierΛ࢖͏ • Consumer͸Contract Stub RunnerΛ࢖͏

Slide 103

Slide 103 text

1SPEVDFS •Spring InitializrͰҎԼΛબ୒ •Contract Verifier

Slide 104

Slide 104 text

1SPEVDFS TPVSDFDPEF @RestController @RequiredArgsConstructor public class UserController { private final UserService userService; @GetMapping("/users/{id}") public Mono getUser(@PathVariable long id) { return userService.getUser(id); } } @Data @NoArgsConstructor @AllArgsConstructor public class User { private long id; private String name; }

Slide 105

Slide 105 text

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

Slide 106

Slide 106 text

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"))); } }

Slide 107

Slide 107 text

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͢ΔͱTUVCTKBS͕Ͱ͖ΔͷͰɺ͜ΕΛ DPOTVNFSଆ͕࢖༻͢Δ $POTVNFSଆ͕ظ଴͢Δ݁Ռ

Slide 108

Slide 108 text

$POTVNFS •Spring InitializrͰҎԼΛબ୒ •Contract Stub Runner

Slide 109

Slide 109 text

$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 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*Λݺͼग़͢ίʔυ

Slide 110

Slide 110 text

$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 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Λࢦఆ͢Δ BSUJGBDU*E<WFSTJPO><DMBTTJpFS><QPSU>