4
6

Gradle x JavaをKotlinに浮気させる

Last updated at Posted at 2023-09-28

こんにちは。 株式会社クアンドのhakatakinocoです。

私たちの会社では、遠隔支援で現場の判断を加速するSynQ Remote(シンクリモート)というサービスを開発しており、バックエンドをKotlinで作成しています。
いわゆるサーバーサイドKotlinです。

synqremote_kotlin.png

JavaからIT業界に入門&修行し、JavaJavaの実を食べたJava人間になった私でもKotlinは扱いやすく、文法も簡素で可読性が高い
かつ、古くから受け継がれた大量のJavaライブラリがほぼ全て使える素敵な開発体験を得られました٩( 'ω' )و

そんな素敵なKotlinですが、いざサーバーサイドで使おうとすると意外と本格的な導入事例は少なく、ちょっと勿体無いな〜と思ったので 既存のJavaシステムにだって段階的にKotlinに置き換えていける んだぜ:muscle:というのを流布したくこの記事を執筆させていただきました。

Kotlinの入門や細かいTipsについてはQiitaにおいても良質な記事がたくさん執筆されておりましたので、この記事では割愛させていただきます。
この記事は、Java製のWebシステムを少しづつKotlinに置き換えていけるという気づきが得られればとの思いで執筆しました。

Kotlinは100% Javaと互換性があると謳われていたと思いますが、それはKotlinからJavaを使う場合です。(それでも注意点はありますが)
JavaからKotlinを使う場合、Kotlin側に少し工夫が必要なところがありますのでご注意ください。
素晴らしい記事がありましたので、こちらを一例として掲載させていただきます:pray:

:seedling: お話の前提

今回紹介するサンプルJavaアプリケーションは次の構成で作成されていることを前提としています。
とはいえ、Gradleプロジェクトであればほぼ同じ手口で実現できるはずです。

  • SpringBoot3 with Gradle
    • Gradle: Kotlin KTS
  • Java 17(OpenJDK)
  • プログラム内容はHelloWorld。。。に申し訳程度のロジックを添えただけもの

実行結果もいたってシンプル!
image.png

※IDEにはKotlinとの相性抜群なAndroidStudioを使っています。が、なんでもいいと思います。

サンプルリポジトリ

こちらに実際のプロジェクトを置いています。ご査収ください。

プロジェクト構成

このようになんの変哲もない、ControllerとBusiness Logicです。

image.png

初期状態のサンプルコードはこちら

今回はこのうちLogicをKotlin化してみたいと思います。

:seedling: 手口

JavaをKotlinに浮気させる手口にはいくつか考えられますが、今回ご紹介するのは
1つのリポジトリのままマルチプロジェクト化して、JavaプロジェクトとKotlinプロジェクトの2本立て状態にする ものです。

リポジトリの全体構成に手を加えるのでややこわい所もあるかと思いますが、Gradleとテストコードがあればまぁまぁ安心ではなかろうかと思います。(でもちゃんと手動でもテストしましょう!)

:seedling: STEP1: マルチプロジェクト化する

これはGraldeプロジェクトの構成によって必要可否が異なりますが、サンプルの場合トップレベルのプロジェクトに直接Javaプロジェクトが乗っていますので、このままでは1プロジェクトの中に Java用、Kotlin用それぞれのソース&設定がごちゃ混ぜになってしまいます。

混ざってもいいのでしょうが、ビルドエラーが起きた時などにおそらく分離しておいた方が分かりやすいので、まずマルチプロジェクト化作業を行います。

まずはファイル達を移動

  • javapp のような新しいディレクトリをルートに作成する
  • srcフォルダと、build.gradle.ktsを javaapp に移動する
    • つまりこうなります
      image.png

試してみる

この状態で実行してみましょう

% ./gradlew bootRun

FAILURE: Build failed with an exception.

* What went wrong:
Task 'bootRun' not found in root project 'demo'.

* Try:
> Run gradlew tasks to get a list of available tasks.
> For more on name expansion, please refer to https://docs.gradle.org/8.2.1/userguide/command_line_interface.html#sec:name_abbreviation in the Gradle documentation.
> Run with --stacktrace option to get the stack trace.
> Run with --info or --debug option to get more log output.
> Run with --scan to get full insights.
> Get more help at https://help.gradle.org.

BUILD FAILED in 801ms

エラーになりましたね:frowning2:

Task 'bootRun' not found in root project 'demo'. とあるので、ルートプロジェクトであるdemoにbootRunの対象が見るからない=プロジェクトの切り離し成功しています。

今までプロジェクトはどこに行ったんじゃーーい(屮゚Д゚)屮
と焦る必要はありません。

今までのプロジェクトは javaapp サププロジェクトに移動してします。

ビルドパスを通す

先ほどbootRunが失敗した理由はひとえにビルドパスが通されていなかったせいです。
Javaにはよくありました、クラスパスを俺にパスしろと、コンパイラからもJVMからもよく怒られた記憶があります。。。

Gradleはそれと同じ類のエラーを出しています。

なのでそれを settings.gradle.kts を通じて設定します。

つまり、これを

settings.gradle.kts
rootProject.name = "demo"

こう

settings.gradle.kts
rootProject.name = "demo"

include(":javaapp")

この include を宣言することでルートプロジェクトに対する指令が、 javaapp サブプロジェクトにまで波及します。

では、実行してみましょう

% ./gradlew bootRun

> Task :javaapp:bootRun

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v3.1.4)

2023-09-25T15:38:15.073+09:00  INFO 71377 --- [           main] com.example.demo.DemoApplication         : Starting DemoApplication using Java 17.0.4.1 with PID 71377 (/***/javakotlin/javaapp/build/classes/java/main started by *** in /***/javakotlin/javaapp)
2023-09-25T15:38:15.074+09:00  INFO 71377 --- [           main] com.example.demo.DemoApplication         : No active profile set, falling back to 1 default profile: "default"
2023-09-25T15:38:15.418+09:00  INFO 71377 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
2023-09-25T15:38:15.423+09:00  INFO 71377 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2023-09-25T15:38:15.423+09:00  INFO 71377 --- [           main] o.apache.catalina.core.StandardEngine    : Starting Servlet engine: [Apache Tomcat/10.1.13]
2023-09-25T15:38:15.467+09:00  INFO 71377 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2023-09-25T15:38:15.467+09:00  INFO 71377 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 375 ms
2023-09-25T15:38:15.587+09:00  INFO 71377 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2023-09-25T15:38:15.591+09:00  INFO 71377 --- [           main] com.example.demo.DemoApplication         : Started DemoApplication in 0.654 seconds (process running for 0.787)
<==========---> 80% EXECUTING [10s]
> :javaapp:bootRun

元気にSpringしてくれましたね:relaxed:

実行結果も正常です。

image.png

:seedling: STEP2: Kotlinのサブプロジェクトを作成する

Kotlin向けのサブプロジェクト構成を作ります。
先ほどの javaapp と同じ要領で kotlinlib というディレクトリを作りました。
image.png

この時点ではただのディレクトリです。
ここに

  • build.gradle.kts
    • 今回はjavaappからコピーしました
  • srcディレクトリ
    • mainディレクトリ
    • testディレクトリ(必須ではないかも)

を作ります。
つまり、こうなる。
image.png

ビルドパスを通す

Kotlinビルドの設定の前に、忘れがちなGradleビルドパスを通しちゃいます。
ルートプロジェクトのsettings.gradle.ktsをこのように変更します。

settings.gradle.kts
rootProject.name = "demo"

include(":javaapp")

settings.gradle.kts
rootProject.name = "demo"

include(":javaapp")
include(":kotlinlib")

kotlinlibサブプロジェクトのbuild.gradle.ktsを編集する

ついに。。。!
Kotlin用にkotlinlib/build.gradle.ktsを編集します。

編集点は大きく2点

  • pluginsセクション
    • java Gradleプラグインの 削除
    • kotlin Gradleプラグインの設定を 追加
  • ライブラリであるという宣言
    • SpringBootプロジェクトのコピーとしてKotlinプロジェクトを作成したのでデフォルトだとエラーが出る。その設定を少し変更してあげる

pluginsセクション

build.gradle.kts
plugins {
	java
	id("org.springframework.boot") version "3.1.4"
	id("io.spring.dependency-management") version "1.1.3"
}

build.gradle.kts
plugins {
	id("org.springframework.boot") version "3.1.4"
	id("io.spring.dependency-management") version "1.1.3"
	kotlin("jvm") version "1.8.22"
	kotlin("plugin.spring") version "1.8.22"
}

ライブラリであるという宣言

Add

build.gradle.kts
import org.springframework.boot.gradle.tasks.bundling.BootJar
import org.springframework.boot.gradle.tasks.run.BootRun

// ...

/* -- ライブリ化設定 -- */
tasks.named<BootJar>("bootJar") {
	enabled = false
}
tasks.named<BootRun>("bootRun") {
	enabled = false
}

tasks.named<Jar>("jar") {
	archiveClassifier.set("")
	enabled = true
}

java {
	withSourcesJar()
}

設定の意味

これらの設定は、bootRun, bootJarではkotlinlib単体で起動するものを生成するのはやめておきます。という設定と
ビルドされてJarになる時にはソース付きのJarを提供します。という設定を行なっています。

これだけで、SpringBootの一部でありながらライブラリとして機能できるようになります。

もしかしたら他にアプローチがあるのでは、、、と思ってはいますが、今のところ私が知っているのはこの方法だけでした:thinking:
詳しい方教えてください:open_mouth:

これでKotlinサブプロジェクトの準備は完了です。

:seedling: STEP3: Kotlin(SpringBoot)実装

kotlinlib/src/main/の中身をSpringBoot向け、およびHelloWorldアプリ向けに実装していきます。

main直下にkotlinと、resourcesディレクトリを作り
kotlinの方にはJavaのパッケージ感覚でパッケージを作成します。

このように

image.png

HelloWorldLogicの中身(Kotlin)

HelloWorldLogic.kt
// この例では完全な置き換えを目指しているので、logicのパッケージ名を揃えていますが、お好みで!
package com.example.demo.logic

import org.springframework.stereotype.Component

@Component
class HelloWorldLogic {
    /**
     * HelloWorldを取得
     * @return Hello World
     */
    fun getHelloWorld(): String {
        val datas = getDatas()

        // 合計を計算
        val sum = datas.sum()

        return "Welcome to Kotlin x $sum"
    }

    /**
     * 特に意味のない1〜100の入った配列を作ります
     * @return 特に意味のない1〜100の入った配列
     */
    private fun getDatas(): List<Int> {
        return (1..100).toList()
    }
}

ここまでの状態で一応ビルドは通るはずです。
次はJava側からこのKotlinコードを呼び出すように変更します。

:seedling: STEP4: JavaappからKotlinlibを呼び出す

こちらのステップには、次の工程が必要です。

  • javaapp/build.gradle.ktsの編集
  • (お好みで)javaappの方に実装していたLogicクラスの削除

今回の場合、Logicクラスを丸ごとKotlinに移植してしまうので呼び出し元であるControllerの改修は必要ありません。

javaapp/build.gradle.ktsの編集

具体的にはdependenciesセクションの依存関係にkotlinlibプロジェクトを追加します。
JARとかではなく、サブプロジェクトに依存しているよ。と宣言するのです。

build.gradle.kts
dependencies {
	implementation("org.springframework.boot:spring-boot-starter-web")
	testImplementation("org.springframework.boot:spring-boot-starter-test")
}

build.gradle.kts
dependencies {
	implementation("org.springframework.boot:spring-boot-starter-web")
	testImplementation("org.springframework.boot:spring-boot-starter-test")
	// import kotlinlib
	implementation(project(":kotlinlib"))
}

Note:project文法は普通のGradleに備わっている機能で、コロン区切りで次々にプロジェクトの階層構造を表現できるようです。こちらについてもたくさん資料が見つかったので説明は割愛します。

(お好みで)javaappの方に実装していたLogicクラスの削除

元々のサンプルに作成していたHelloWorldLogic.javaファイルを削除してしまいます。

記事の簡素化のためになんの確認もせずに削除しますが、実際の作業においてはきちんと移植できたことを確認の上で削除する場合は削除してください。
削除しないでSpringのDIで読み込ませるLogicクラスを選択することは可能なので落ち着いて作業しましょう〜

完&成

これで移植作業完了です。

結果として次のようなファイル構造になりました。
image.png

では

% ./gradlew bootRun

> Task :javaapp:bootRun

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v3.1.4)

2023-09-25T21:26:45.825+09:00  INFO 9576 --- [           main] com.example.demo.DemoApplication         : Starting DemoApplication using Java 17.0.4.1 with PID 9576 (/*※*/gradle-java-and-kotlin/javaapp/build/classes/java/main started by *** in /***/gradle-java-and-kotlin/javaapp)
2023-09-25T21:26:45.827+09:00  INFO 9576 --- [           main] com.example.demo.DemoApplication         : No active profile set, falling back to 1 default profile: "default"
2023-09-25T21:26:46.178+09:00  INFO 9576 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
2023-09-25T21:26:46.183+09:00  INFO 9576 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2023-09-25T21:26:46.183+09:00  INFO 9576 --- [           main] o.apache.catalina.core.StandardEngine    : Starting Servlet engine: [Apache Tomcat/10.1.13]
2023-09-25T21:26:46.228+09:00  INFO 9576 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2023-09-25T21:26:46.228+09:00  INFO 9576 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 383 ms
2023-09-25T21:26:46.360+09:00  INFO 9576 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2023-09-25T21:26:46.364+09:00  INFO 9576 --- [           main] com.example.demo.DemoApplication         : Started DemoApplication in 0.673 seconds (process running for 0.826)

ブラウザからアクセスしてみると、きちんとKotlinに記述したLogicの処理が実行されていることがわかります。
image.png

まとめ

手順が多く、いざやってみると不安はあるものの、これだけで流暢な文法をもつKotlinをバックエンドに導入できると思うと、割とコスト低いんじゃなかろうかと個人的には思います。

特にSynQ Remoteの場合、スマホアプリの提供もあるのでバックエンドのためだけにJavaを扱う必要がなくなり、AndroidとバックエンドはKotlinだけで表現できてしまいます。つまりアプリエンジニアをバックエンドの世界に引きづり込めるわけです(知らんけど)

ちょっとしたTipsですが、活用いただけたら嬉しいです。

最終形態にしたサンプルアプリケーションはこちらにありますので、参考にどうぞ!


私が勤めている株式会社クアンドでは、日本の産業をアップデートする事をミッションとして社会課題に取り組んでいます。
クアンドという会社をもっと知れるEntrance Bookはこちら!

そんな私たちと一緒に働いてくれる仲間を大募集中です!

特に、今まさにSynQというプロダクト開発を進めており、一緒に働いてくれるメンバーを募集中です!
もしご興味を持っていただいてお話だけでも!という方もぜひご連絡ください!

▼ご連絡はこちらから

4
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
6