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

Server-Side Kotlin by Ktor

kaonash
August 24, 2019

Server-Side Kotlin by Ktor

kaonash

August 24, 2019
Tweet

More Decks by kaonash

Other Decks in Programming

Transcript

  1. SELF INTRODUCTION • ਗ਼ਫݦ(@kaonash_) • Lead Software Engineer @ UPSIDER

    • Ҡॅ͍ͨ͠ܥΤϯδχΞʢ2022೥ɺ௕໺ʹҠॅ༧ఆʣ • Love: ୌ / Kitkat / Kotlin
  2. • Language : Kotlin • WebϑϨʔϜϫʔΫ : Ktor server •

    O/R Mapper : Exposed • DI : Koin • Http client : Ktor client • JSON Parser : Jackson • Testing : JUnit5 / AssertJ / MockK ฐࣾͷٕज़ελοΫ Server ΄΅Pure Kotlin!
  3. WHAT IS • JetBrains͕։ൃͨܰ͠ྔɾඇಉظWebϑϨʔϜϫʔΫ • ಡΈํ͸ʮ͚͍ͨʔʯʢ࠷ॳ͸ʮͨ͜ʔʯͩͱࢥͬͯͨʣ • Pure Kotlin •

    PluggableʹඞཁͳػೳͷΈΛ௥Ճ͍͚ͯ͠Δ • 2018೥11݄ʹ1.0͕ϦϦʔεʂʢݱࡏͷ࠷৽͸1.2.3ʣ • Server͚ͩ͡Όͳͯ͘Client΋͋Δ https://ktor.io/clients/index.html
  4. SpringBoot • ͍Θͣͱ஌ΕͨJavaքͷσϑΝΫτελϯμʔυɻServer-side KotlinͰ΋࠾༻ࣄྫ͕ଟ͘ଘࡏɻ • KotlinରԠ • લ৬ʢ౰࣌͸μϒϧϫʔΫͩͬͨͷͰݱ৬ʣ΋SpringBootͰServer-Side Kotlin΍ͬͯͨͷͰܦݧ͸͋ Δ

    • શ෦ೖΓͰศརͳͷ͕ͩͦͷ෼ىಈ͕࣌ؒ௕͘ɺͦͷؒʹTwitterͱ͔Twitterͱ͔Twitterͱ͔Λݟͨ Γͯ͠͠·͏ • Ξϊςʔγϣϯ஍ࠈ͕͋·Γ޷͖ʹͳΕͳ͍ɻɻɻʢݸਓͷײ૝Ͱ͢ʣ • ΫϦʔϯΞʔΩςΫνϟΛݕ౼͍ͯͨͨ͠Ίɺશମ͕ΨοπϦSpringʹґଘ͢Δͷ͕ͪΐͬͱ໰୊
  5. Hello World! Ktor serverͷىಈํ๏͸͍͔ͭ͋͘Δ͕ɺҰ൪؆୯ͳํ๏ fun main() { embeddedServer(Netty, 8080) {

    install(Routing) { get("/") { call.respondText("Hello World!") } } }.start() }
  6. Hello World! fun main() { embeddedServer(Netty, 8080) { install(Routing) {

    get("/") { call.respondText("Hello World!") } } }.start() } NettyΛར༻ͯ͠ΞϓϦέʔγϣϯΛ࡞੒ port൪߸͸8080 Get methodͰpath"/"͕ݺͼग़͞ΕͨΒ responseͱͯ͠"Hello World!"ͱ͍͏จࣈྻΛฦ٫͢Δ ͜ͷblockͰroutingΛఆٛ
  7. EngineMain • EngineMain͸Ktor͕༻ҙͨ͠Ϋϥε • ར༻͢ΔEngineʹΑͬͯ4छྨ༻ҙ͞Ε͍ͯΔ • io.ktor.server.cio.EngineMain • io.ktor.server.tomcat.EngineMain •

    io.ktor.server.jetty.EngineMain • io.ktor.server.netty.EngineMain •mainؔ਺Λ͍࣋ͬͯΔͷͰɺαʔόʔىಈ࣌͸͜ͷϝιουΛ࣮ߦ •ىಈ࣌ʹࣗಈతʹapplication.confͱ͍͏ઃఆϑΝΠϧΛಡΈࠐΈɺͦͷઃఆ ಺༰ʹΑͬͯىಈ࣌ͷॲཧΛܾఆ
  8. EngineMain • ར༻͢Δػೳ͸ɺApplicationΫϥεͷ֦ுؔ਺ʹͯఆٛ͢Δ fun Application.module() { install(Routing) { get("/") {

    call.respondText("Hello World!") } } } Application.kt ͜ͷϝιου໊͸ͳΜͰ΋͍͍ installϝιουΛ௥Ճ͍ͯ͘͜͠ͱͰɺඞཁͳػೳΛ޷͖ʹ૊ΈࠐΊΔ
  9. EngineMain • ઃఆϑΝΠϧapplication.confʹɺىಈͷઃఆ৘ใΛهड़ ktor { deployment { port = 8080

    } application { modules = [ ApplicationKt.module ] } } application.conf (ύοέʔδ໊.)Ϋϥε໊(ϑΝΠϧ໊ͷ຤ඌʹKtΛ͚ͭΔ).֦ுؔ਺໊
  10. Locations(※ݱࡏ͸·ͩexperimental) • ύϥϝʔλΛλΠϓηʔϑʹѻ͑Δ • Int, Long, Float, Double, Boolean, String,

    enum͕࢖༻Մೳ @Location("/users/{userId}") data class UserLocation(val userId: Long) fun Application.module() { install(Locations) install(Routing) { get<UserLocation> { param -> // param͸UserLocationܕʹͳΔͷͰɺuserIdΛͦͷ··LongܕͰऔಘͰ͖Δ val userId = param.userId } } }
  11. Locationͷωετ΋Ͱ͖Δ @Location("/users/{userId}") data class UserLocation(val userId: Long) { @Location("/email") data

    class EmailLocation(val userLocation: UserLocation) } • ؔ࿈ͨ͠routeΛ·ͱΊΒΕΔͷ͸ϝϦοτ͕ͩɺ਺͕૿͑ͯ͘ Δͱͪ͝Όͪ͝Όͯ͠Θ͔ΓͮΒ͘ͳΔ • ʮKtorͷRoutingఆٛͷϕετϓϥΫςΟεΛߟ͑ͯΈΔʯʹৄ ͘͠ॻ͖·ͨ͠ʢͪͳΈʹલ৬ͷTechϒϩάʣ
 https://tech.bizreach.co.jp/posts/324/ktor-routing/
  12. ͨͱ͑͹ValueObject • ͜Μͳײ͡ͰɺValueObject΋ࣗಈͰม׵Ͱ͖Δͱศར data class UserId(val userId: Long) @Location("/users/{userId}") data

    class UserLocation(val userId: UserId) fun Application.module() { install(Locations) install(Routing) { get<UserLocation> { param -> val userId = param.userId // UserIdܕͰͦͷ··औಘͰ͖Δʂ } } }
  13. DataConversionΛ࢖͏ͱͰ͖·͢ fun Application.module() { install(DataConversion) { convert<UserId> { decode {

    values, _ -> values.singleOrNull()?.let { UserId(it.toLong()) } } encode { id -> when (id) { is UserId -> listOf(id.value.toString()) else -> throw DataConversionException("Can not convert.") } } } } }
  14. ͿͬͪΌ͚ɺSpringBootͱൺ΂ͯͲ͏ʁ • ֤Controllerʹ͚ͭΔ@RequestMapping ΞϊςʔγϣϯʹrouteΛهࡌ͢Δํࣜ • ਺͕૿͑Δͱʮ͜ͷAPIɺͲ͜ͷ ControllerݺΜͰΔΜ͚ͩͬʯ͕୳ͮ͠ Β͍໰୊ SpringBoot Ktor

    • routeΛ·ͱΊͯఆٛ͢Δํࣜ • LocationsΛ࢖Θͳ͍ͱλΠϓηʔϑʹ ѻ͑ͳͯͭ͘ΒΈ • Locations͸ఆٛͷ࢓ํΛؾΛ͚ͭͳ͍ ͱΊͬͪΌಡΈͮΒ͘ͳΔ Ұ௕Ұ୹ɻݸਓతʹ͸Ktorͷ΍Γํͷ΄͏͕޷͖͕ͩɺ Locationsͷ࢖͍উखΛ΋͏গ͠վળͯ͠΄͍͠
  15. ೝূॲཧ • ϕʔγοΫೝূ / Digestೝূ / JWT / LDAPೝূ /

    OAuthΛαϙʔτɻ • install(Authentication)͢Ε͹ར༻Մೳʢ௨ৗͷktorҎ֎ʹktor_authΛ dependenciesʹ௥Ճ͢Δ͜ͱ͕ඞཁʣ • Routingఆٛʹͯೝূ͕ඞཁͳrouteʹରͯ͠authenticate{}ϒϩοΫͰғΉͱɺ ೝূ͕௨͍ͬͯΔ৔߹͚ͩॲཧΛ࣮ߦ͢Δ͜ͱ͕Մೳɻ
  16. Routingଆ fun Application.module() { install(Routing) { // ͜͜͸ೝূෆཁ post(“/login") {

    // ϩάΠϯॲཧɻ੒ޭ࣌ʹ͸tokenΛੜ੒ͯ͠ฦ٫ } authenticate { // ͜ͷϒϩοΫ಺͸ೝূ͕௨ͬͯͳ͍ͱ࣮ߦͰ͖ͳ͍ get("/users/{userId}") { // ͜ΕͰϩάΠϯϢʔβʔͷ৘ใΛऔಘͰ͖Δ val user = authentication.principal<LoginUser>()!! ... } } } }
  17. KoinΛ࢖ͬͨྫ fun Application.module() { install(Koin) { modules( module { single

    { UserController() } } ) } install(Routing) { val userController: UserController by inject() get("users") { call.respond(userController.find()) } } } ͜ͷதͰDI͢ΔϞδϡʔϧΛఆ͓ٛͯ͘͠ͱ औಘॲཧΛinject()ʹҠৡ͢Δ͜ͱͰΦϒδΣΫτ͕औಘͰ͖Δʂ
  18. TestEngineͷ࢖༻ྫ withTestApplication(Application::module) { handleRequest(HttpMethod.Post, "/login") { addHeader(HttpHeaders.ContentType, ContentType.Application.FormUrlEncoded.toString()) setBody(listOf( "email"

    to "[email protected]", "password" to "loginPass" ).formUrlEncode()) }.apply { assertThat(response.status()).isEqualTo(HttpStatusCode.OK) } } ىಈ͢ΔΞϓϦέʔγϣϯͷϝιουΛࢦఆ Httpϝιουͱpathͷࢦఆ HeaderͱBodyͷࢦఆ handleRequestͷ໭Γ஋TestApplicationCallܕ͕ ϓϩύςΟͱͯ͠responseΛอ͍࣋ͯ͠Δ
  19. ͨͱ͑͹ɺ͜͏͍͏ॲཧΛ୯ମͰςετ͢Δʹ͸ʁ install(Routing) { val userController: UserController by inject() @Location("/users/{userId}") data

    class UserLocation(val userId: UserId) get<UserLocation> { call.respond(userController.find(userId)) } } ͍ͭ͜ΛMockԽ͍ͨ͠
  20. MockKͱKoin val userController = mockk<UserController>(relaxed = true) val module =

    module { single { userController } } @Test fun `Return user data when called request with user id`() { val userId = UserId(1) every { userController.find(userId) } returns UserData(1, "ࢁాଠ࿠") withTestApplication(Application::module) { startKoin { modules(module) } handleRequest(HttpMethod.Get, "/users/1").apply { assertThat(response.status()).isEqualTo(HttpStatusCode.OK) … } } } MockΦϒδΣΫτΛ࡞੒͠ɺKoinϞδϡʔϧʹొ࿥ MockΦϒδΣΫτ͕ݺ͹Εͨ࣌ͷڍಈΛఆٛ KoinΛىಈ
  21. ͳΜͰʁ • Application.moduleͰinstall(Koin)͢Δͱɺىಈ࣌ʹࣗಈతʹstartKoin͕ݺ͹ΕΔ fun Application.module() { install(Koin) { modules( module

    { single { UserController() } } ) } } • TestEngineͰ΋Ծ૝తʹKtorΛىಈ͍ͯ͠ΔͨΊɺಉ͡Α͏ʹstartKoin͕૸Δ • MockΦϒδΣΫτΛΠϯδΣΫτ͢ΔͨΊʹstartKoin͢ΔͱೋॏىಈʹͳΔ
  22. ͱ͍͏͜ͱͰɺ1ߦ௥Ճ val userController = mockk<UserController>(relaxed = true) val module =

    module { single { userController } } @Test fun `Return user data when called request with user id`() { val userId = UserId(1) every { userController.find(userId) } returns UserData(1, "ࢁాଠ࿠") withTestApplication(Application::module) { stopKoin() startKoin { modules(module) } handleRequest(HttpMethod.Get, "/users/1").apply { assertThat(response.status()).isEqualTo(HttpStatusCode.OK) … } } }
  23. WE ARE HIRING!! • Server side: Kotlin(Ktor) • Front: Nuxt.js,

    Typescript • Backend: Go • ࣄۀ಺༰ɿ๏ਓ޲͚VISAΧʔυΛ࢖ͬͨBtoBαʔϏεʢৄࡉ͸ۙ೔ެ։ʣ • ෭ۀ׻ܴ(Քಇि2ఔ౓ʙOKʣ • ϑϧϦϞʔτOKʢΦϑΟε͸࿡ຊ໦ʣ • ͪΐͬͱͰ΋ؾʹͳΔਓ͸࠙਌ձɺ΋͘͠͸Twitterʹͯ@kaonash_·Ͱʂ