背景
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の設定
- pom.xmlの設定は、以下のKotlinの公式サイトを参考にしました。
- using-maven
- compiler-plugins
※今回の投稿に関係ある部分を中心に記載しています。
- Kotlinバージョンの定義
<properties>
...
<java.version>1.8</java.version>
+ <kotlin.version>1.2.10</kotlin.version>
+ <kotlin.compiler.incremental>true</kotlin.compiler.incremental>
</properties>
- Kotlinの依存ライブラリを追加
<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の定義は不要になるようです。
<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
も同様
- プロジェクトを右クリックし、
Configure Kotlin > Add Kotlin Nature
を実行
下図では、既にAdd Kotlin Nature済みのため選択できなくなっていますが、実行前であれば選択できます。
これでSTS上でもコンパイルが通り、Kotlinでの開発が行えるようになりました。
Kotlinを書く
JavaコードからKotlinコードへコンバート
IntelliJのコンバート機能が優秀なので、基本的にはIntelliJでコンバートしました。
一気にまとめては怖いので、1コードもしくは数コードづつ変換しました。
変換したいコードを選択して、Code > Convert Java File to Kotlin File
でKotlinのコードにコンバートします。
※たまにおかしな変換をするので都度手で修正します。
また、パッケージが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化を解除してくれます。
- 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楽しい!!