Java
Maven
Kotlin
SpringBoot

Javaで書かれたSpring BootプロジェクトをKotlin対応した話

More than 1 year has passed since last update.

背景

NORELチームのエンジニア間で使用技術のモダナイズを進めており、「Java / SpringBootで書かれたシステムをどうせならKotlinで書きたいよね」という話が出てきたので、Kotlinで開発できるように対応を進めています。

最初からKotlinで始める(Gradle)といった記事はよく見かけますが、Javaで書かれている途中からKotlinに対応する(Maven)という記事があまり見つからなかったので、手探りで対応していきました。

既存の環境

  • macOS High Sierra
  • JDK version => 1.8
  • Spring Boot => 1.5.8
  • Lombokを使用している
  • ビルドシステム => Maven
  • IntelliJ派とSTS派がいる
    • 私は前者です。

基本的にはSpringBootではREST APIのみを実装しています。

Kotlin対応させる方針

  • 並行でJavaでの開発が進んでいるため、全コードをJavaからKotlinに変換するのは今回は見送り
    • Controller / ServiceはKotlinで書けるようにする
    • JPAなどのデータアクセス層はJavaのまま残しておく
    • 最終的には全部Kotlinにしたい

また、今回の作業はIntelliJ上で行いました。

移行作業

pom.xmlの設定

※今回の投稿に関係ある部分を中心に記載しています。

  • Kotlinバージョンの定義
pom.xml
  <properties>
...
     <java.version>1.8</java.version>
+    <kotlin.version>1.2.10</kotlin.version>
+    <kotlin.compiler.incremental>true</kotlin.compiler.incremental>
  </properties>
  • Kotlinの依存ライブラリを追加
pom.xml
  <dependencies>
・・・
+    <dependency>
+      <groupId>org.jetbrains.kotlin</groupId>
+      <artifactId>kotlin-stdlib</artifactId>
+      <version>${kotlin.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.jetbrains.kotlin</groupId>
+      <artifactId>kotlin-reflect</artifactId>
+      <version>${kotlin.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.jetbrains.kotlin</groupId>
+      <artifactId>kotlin-test</artifactId>
+      <version>${kotlin.version}</version>
+      <scope>test</scope>
+    </dependency>
  </dependencies>
  • ビルド設定

元々maven-compiler-pluginは使用していませんでしたが、JavaとKotlinのどちらでも開発ができるように、パッケージをそれぞれ

  • src/main/java
  • src/main/kotlin
  • scr/test/java
  • src/test/kotlin

で分けため、maven-compiler-pluginを使用しています。

また、Springで使用する各種アノテーションを使用できるようにするため、compilerPluginsとしてspringを定義しています。
ドキュメントにも記載されていますが、springプラグインを定義するとall-openの定義は不要になるようです。

pom.xml
  <build>
     <finalName>${project.name}</finalName>
-    <sourceDirectory>src/main/java</sourceDirectory>
-    <testSourceDirectory>src/test/java</testSourceDirectory>
    <resources>
      <resource>
        <directory>${resources.directory}</directory>
      </resource>
      <resource>
        <directory>src/main/resources</directory>
      </resource>
     </resources>
     <testResources>
       <testResource>
         <directory>src/test/resources</directory>
       </testResource>
     </testResources>
     <plugins>
       <plugin>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-maven-plugin</artifactId>
         <configuration>
           <executable>true</executable>
         </configuration>
       </plugin>
+      <plugin>
+        <artifactId>kotlin-maven-plugin</artifactId>
+        <groupId>org.jetbrains.kotlin</groupId>
+        <version>${kotlin.version}</version>
+        <configuration>
+          <compilerPlugins>
+            <plugin>spring</plugin>
+          </compilerPlugins>
+        </configuration>
+        <executions>
+          <execution>
+            <id>compile</id>
+            <goals> <goal>compile</goal> </goals>
+            <configuration>
+              <sourceDirs>
+                <sourceDir>${project.basedir}/src/main/kotlin</sourceDir>
+                <sourceDir>${project.basedir}/src/main/java</sourceDir>
+              </sourceDirs>
+            </configuration>
+          </execution>
+          <execution>
+            <id>test-compile</id>
+            <goals> <goal>test-compile</goal> </goals>
+            <configuration>
+              <sourceDirs>
+                <sourceDir>${project.basedir}/src/test/kotlin</sourceDir>
+                <sourceDir>${project.basedir}/src/test/java</sourceDir>
+              </sourceDirs>
+            </configuration>
+          </execution>
+        </executions>
+        <dependencies>
+          <dependency>
+            <groupId>org.jetbrains.kotlin</groupId>
+            <artifactId>kotlin-maven-allopen</artifactId>
+            <version>${kotlin.version}</version>
+          </dependency>
+        </dependencies>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-compiler-plugin</artifactId>
+        <version>3.5.1</version>
+        <executions>
+          <!-- Replacing default-compile as it is treated specially by maven -->
+          <execution>
+            <id>default-compile</id>
+            <phase>none</phase>
+          </execution>
+          <!-- Replacing default-testCompile as it is treated specially by maven -->
+          <execution>
+            <id>default-testCompile</id>
+            <phase>none</phase>
+          </execution>
+          <execution>
+            <id>java-compile</id>
+            <phase>compile</phase>
+            <goals> <goal>compile</goal> </goals>
+          </execution>
+          <execution>
+            <id>java-test-compile</id>
+            <phase>test-compile</phase>
+            <goals> <goal>testCompile</goal> </goals>
+          </execution>
+        </executions>
+      </plugin>
+    </plugins>
  </build>

これでIntelliJ上でKotlinを書く準備が整いました。

STS(Eclipse)の対応

ここまでの対応で普段STSを使用しているメンバーに確認してもらったところ、うまく動作せずに少しハマったのでSTSでの対応方法も記載します。

  • EclipseマーケットプレイスからKotlin Plugin for Eclipseをインストールします。

  • src/main/kotlinがビルドパスとして認識されていないので、Projects > Properties > Java Build PathのSourceからAdd Fileでkotlinのフォルダをチェック。
    src/test/kotlinも同様

buildPath.png

  • プロジェクトを右クリックし、Configure Kotlin > Add Kotlin Natureを実行

下図では、既にAdd Kotlin Nature済みのため選択できなくなっていますが、実行前であれば選択できます。

AddKotlinNature.png

これでSTS上でもコンパイルが通り、Kotlinでの開発が行えるようになりました。

Kotlinを書く

JavaコードからKotlinコードへコンバート

IntelliJのコンバート機能が優秀なので、基本的にはIntelliJでコンバートしました。
一気にまとめては怖いので、1コードもしくは数コードづつ変換しました。

変換したいコードを選択して、Code > Convert Java File to Kotlin FileでKotlinのコードにコンバートします。

ConvertToKotlin.png

※たまにおかしな変換をするので都度手で修正します。

また、パッケージがsrc/main/java配下のままであるため、src/main/kotlin配下の同パッケージ以下に移動します。
これもIntelliJでRefactor > Move...でいい感じにリファクタリングしてくれます。

DI(Autowiredなど)

たとえば以下のコードは

@Service
public class HogeService {
    @Autowired
    private HogeRepository hogeRepository;
}

Kotlinではlateinitでも宣言できますが、今回はconstructor injectionで定義するようにしました。

@Service
class HogeService(
    private val hogeRepository: HogeRepository
) {
}

Lombokまわり

Lombokを使っていると、Kotlinにコンバートした際にコンパイルエラーが出ることがありました。

基本的には、Kotlinにコンバートするクラス、Kotlinから参照するクラスは、Delombokすることで解消させました。

  • Delombok

IntelliJのlombokプラグインを使用します。
Refactor > DelombokからLombok化を解除してくれます。

Delombok.png

  • Slf4jアノテーション

loggerの宣言を省略して、いきなりログ出力するコードを書くことができます。
Kotlinにした場合、以下のコードでlog変数が解決できずコンパイルエラーとなってしまいます。

@Service
@Slf4j
public class HogeService {
    public void hoge() {
        log.info("hoge");
    }
}

kotlinではcompanion objectでロガーを初期化することで利用できるようになります。

@Service
class HogeService {
    companion object {
        private val log = LoggerFactory.getLogger(HogeService::class.java)
    }
}

アノテーションでのDIの方法などを試されている方もいたので、楽にできるよう検討したいと思います。
http://saiya-moebius.hatenablog.com/entry/2017/11/08/033932

  • Dataアノテーション

以下のようなLombok化されたJavaのコードをKotlinから読み、hogeを取得しようとしてもprivateでアクセスでいないと言われてしまいます。
この場合、上に書いたようにDelombok化して対応しました。
(全部KotlinになればそもそもLombok使わなくても・・)

@Entity
@Data
@EqualsAndHashCode(callSuper = false)
@ToString(callSuper = true)
public class Hoge extends AbstractEntity {
    @Column(name = "hoge")
    private String hoge;
}

今後について

現時点で一部のコードはKotlin化して動作するようになりました。
しかし、実際にコードを書いている途中で問題が起こることが何度かあったので、今後も問題が発生する可能性があります。
そこでの対処方などはどんどん追記していければなと思います。

※Kotlin楽しい!!