Kotlin Compiler Plugin (kotlin-allopen) を追いかける
これは Kotlin AdventCalendar 2016 の 24 日目の記事です。メリークリスマスイブ!時間的にギリギリアウトでした・・・。
さて、少し前に Kotlin 1.0.6 RC が出ましたが、そのタイミングで kotlin-allopen
と kotlin-noarg
という 2 つのコンパイラープラグインが発表されました。
それぞれ、以下の働きをするコンパイラープラグインとなっています。
kotlin-allopen
任意のアノテーション (ユーザーにて指定できます) が付いているクラスのメンバーに、暗黙的にopen
修飾子を付与します。 Spring 等のフレームワークや Mockit などのライブラリに有益な物となっています。普段 Web アプリ開発に Kotlin + Spring Framework を使っているのでとても助かりそうです!kotlin-noarg
任意のアノテーション (ユーザーにて指定できます) が付いているクラスに、暗黙的に引数ゼロのコンストラクターを生成します。 主に JPA 対応として作成されたようです。
コンパイラープラグインの使い方は上のリンクを参照していただくとして、今回は kotlin-allopen
プラグインを題材に、コードにコメントを追加する形でコンパイラープラグインの作りについて追いかけてみたいと思います。リンクは、タグ v1.1-M04
です。 (v1.0.6 RC はタグが切られてないようなので、2016/12/24 時点で一番新しいタグをベースにしました。)
全体の仕組み
kotlin-allopen
プラグインに関するコードは以下のディレクトリにあります。kotlin-allopen
の本体は共通化されており、Gradle と Maven 向けのプラグイン実装が用意されています。
- plugins/allopen/allopen-cli
kotlin-allopen
の本体です。 - libraries/tools/kotlin-allopen
kotlin-allopen
の Gradle プラグインです。 - libraries/tools/kotlin-maven-allopen
kotlin-allopen
の Maven プラグインです。
今回はプラグイン本体のみ追いかけます。
プラグイン本体
ここから、プラグイン本体のコードを読んでいきます。コードは 2 ファイルのみで、大きくないためすぐ読めます。
- AllOpenPlugin.kt
プラグインのオプション引数を処理するクラスと、プラグインを登録するクラスが実装されています。 - AllOpenDeclarationAttributeAltererExtension.kt
プラグインの処理をおこなうクラスが実装されています。
AllOpenPlugin.kt
AllOpenPlugin.kt には、コンパイラープラグインに必要な 2 つのインターフェースを実装したクラスが定義されています。 コンパイラープラグインに必要なインターフェースは以下の 2 つです。
CommandLineProcessor
インターフェースComponentRegistrar
インターフェース
/* * Copyright 2010-2016 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.jetbrains.kotlin.allopen import com.intellij.mock.MockProject import org.jetbrains.kotlin.compiler.plugin.CliOption import org.jetbrains.kotlin.compiler.plugin.CliOptionProcessingException import org.jetbrains.kotlin.compiler.plugin.CommandLineProcessor import org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar import org.jetbrains.kotlin.config.CompilerConfiguration import org.jetbrains.kotlin.config.CompilerConfigurationKey import org.jetbrains.kotlin.extensions.DeclarationAttributeAltererExtension /** * コンパイラープラグインで扱う設定のキーを定義するオブジェクトです。 */ object AllOpenConfigurationKeys { /** * kotlin-allopen の対象とするアノテーションのリストを設定するキーです。 */ val ANNOTATION: CompilerConfigurationKey<List<String>> = CompilerConfigurationKey.create("annotation qualified name") } /** * kotlin-allopen プラグインに指定されたオプション引数を処理するクラスです。 */ class AllOpenCommandLineProcessor : CommandLineProcessor { /** * AllOpenCommandLineProcessor のコンパニオンオブジェクトです。 */ companion object { /** * オプション引数 annotation の定義です。 * * - オプション引数名は annotation です。 * - オプション引数には、プラグイン処理の対象とするアノテーションの完全修飾名を指定します。 * - オプション引数は複数指定可能です。 * * コマンドラインコンパイラーを使用する場合、以下の引数を指定してコンパイラープラグインを有効にします。 * * `-P <jar ファイルのパス>:<プラグイン ID>:<オプション引数名>=<アノテーションの完全修飾名>[, 複数指定する場合はカンマで区切る]` */ val ANNOTATION_OPTION = CliOption("annotation", "<fqname>", "Annotation qualified names", required = false, allowMultipleOccurrences = true) /** * コマンドラインコンパイラーで指定するプラグイン ID を定義します。 */ val PLUGIN_ID = "org.jetbrains.kotlin.allopen" } /** * CommandLineProcessor インターフェースで定義されている変数です。 * コンパニオンオブジェクトで定義したプラグイン ID を返します。 */ override val pluginId = PLUGIN_ID /** * CommandLineProcessor インターフェースで定義されている変数です。 * オプション引数のリストを返します。 */ override val pluginOptions = listOf(ANNOTATION_OPTION) /** * CommandLineProcessor インターフェースで定義されているメソッドです。 * 指定されたオプション引数を処理します。 * * オプション引数に指定した数だけ繰り返し実行されます。 * * @param option オプション引数を判別するための CliOption です。 * @param value オプション引数に指定された値です。 * @param configuration コンパイラーの設定等を保持するオブジェクトです。 */ override fun processOption(option: CliOption, value: String, configuration: CompilerConfiguration) = when (option) { /* オプション引数 annotation が指定された場合の処理です。 */ ANNOTATION_OPTION -> { /* これまでに設定されたオプション引数 annotation のリストを、CompilerConfiguration から取得します。 * 設定を取得するキーは、AllOpenConfigurationKeys オブジェクトで定義しています。 */ val paths = configuration.getList(AllOpenConfigurationKeys.ANNOTATION).toMutableList() /* 今回の値をリストに追加します。 */ paths.add(value) /* CompilerConfiguration の値を上書きします。 */ configuration.put(AllOpenConfigurationKeys.ANNOTATION, paths) } /* kotlin-allopen プラグインが想定していないオプションが指定された場合は例外をスローします。 */ else -> throw CliOptionProcessingException("Unknown option: ${option.name}") } } /** * kotlin-allopen プラグインの処理を登録します。 */ class AllOpenComponentRegistrar : ComponentRegistrar { /** * ComponentRegistrar インターフェースで定義されているメソッドです。 * コンパイラープラグインの処理を登録します。 * * @param project Intellij IDEA で用意しているオブジェクトのようですが、あまり情報がないため不明です・・・。 * @param configuration コンパイラーの設定等を保持するオブジェクトです。 * AllOpenCommandLineProcessor で処理済みのものです。 */ override fun registerProjectComponents(project: MockProject, configuration: CompilerConfiguration) { /* CompilerConfiguration から、kotlin-allopen 処理の対象とするアノテーションのリストを取得します。 * 取得できない場合はプラグイン処理を登録しません。 */ val annotations = configuration.get(AllOpenConfigurationKeys.ANNOTATION) ?: return if (annotations.isEmpty()) return /* kotlin-allopen 処理を登録します。 */ DeclarationAttributeAltererExtension.registerExtension(project, CliAllOpenDeclarationAttributeAltererExtension(annotations)) } }
AllOpenDeclarationAttributeAltererExtension.kt
AllOpenDeclarationAttributeAltererExtension.kt では、kotlin-allopen の処理をおこなうクラスを定義しています。
コンパイラーの拡張ポイント (という名前でいいのか?) は DeclarationAttributeAltererExtension
というインターフェースです。
コンパイラーのフロントエンドの org.jetbrains.kotlin.extensions
パッケージに定義されているインターフェース / クラスは数少ないのですが、まだ十分に用意されていないのかそれともこれで十分なのか、理解が足りず分かりません・・・。
/* * Copyright 2010-2016 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.jetbrains.kotlin.allopen import org.jetbrains.annotations.TestOnly import org.jetbrains.kotlin.descriptors.ClassDescriptor import org.jetbrains.kotlin.descriptors.DeclarationDescriptor import org.jetbrains.kotlin.descriptors.Modality import org.jetbrains.kotlin.extensions.AnnotationBasedExtension import org.jetbrains.kotlin.extensions.DeclarationAttributeAltererExtension import org.jetbrains.kotlin.lexer.KtTokens import org.jetbrains.kotlin.psi.KtModifierListOwner import org.jetbrains.kotlin.resolve.BindingContext /** * コンストラクターで指定されたアノテーションに対してプラグイン処理をおこなうクラスです。 * * @property allOpenAnnotationFqNames プラグイン処理をおこなうアノテーションのリストです。 */ class CliAllOpenDeclarationAttributeAltererExtension( private val allOpenAnnotationFqNames: List<String> ) : AbstractAllOpenDeclarationAttributeAltererExtension() { /** * AnnotationBasedExtension インターフェースで定義されているメソッドです。 * プラグイン処理をするアノテーションのリストを返します。 */ override fun getAnnotationFqNames(modifierListOwner: KtModifierListOwner?) = allOpenAnnotationFqNames } /** * プラグイン処理をおこなうクラスです。 * * 2 つのインターフェースを実装しています。 * * - DeclarationAttributeAltererExtension * - AnnotationBasedExtension */ abstract class AbstractAllOpenDeclarationAttributeAltererExtension : DeclarationAttributeAltererExtension, AnnotationBasedExtension { /** * AbstractAllOpenDeclarationAttributeAltererExtension のコンパニオンオブジェクトです。 */ companion object { /** * テスト用のアノテーションリストを定義します。 */ val ANNOTATIONS_FOR_TESTS = listOf("AllOpen", "AllOpen2", "test.AllOpen") } /** * DeclarationAttributeAltererExtension インターフェースで定義されているメソッドです。 * * Modality を変更するプラグイン処理を実装します。(たぶん・・・) * * @property modifierListOwner * @property declaration * @property containingDeclaration * @property currentModality * @property bindingContext */ override fun refineDeclarationModality( modifierListOwner: KtModifierListOwner, declaration: DeclarationDescriptor?, containingDeclaration: DeclarationDescriptor?, currentModality: Modality, bindingContext: BindingContext ): Modality? { /* 現在の Modality が FINAI 以外の場合はプラグインの処理をしません。 * Modality は以下のものがあります。 * Modality.FINAL * Modality.SEALED * Modality.OPEN * Modality.ABSTRACT */ if (currentModality != Modality.FINAL) { return null } /* ここは理解していません。コンパイラーを追いかけないと分からなそうです・・・。 */ val descriptor = declaration as? ClassDescriptor ?: containingDeclaration ?: return null /* コンストラクターで指定されたアノテーションが指定されている場合 */ if (descriptor.hasSpecialAnnotation(modifierListOwner)) { /* final 修飾子が付いている場合は final のまま、それ以外の場合は open に変更します。 */ return if (modifierListOwner.hasModifier(KtTokens.FINAL_KEYWORD)) Modality.FINAL // Explicit final else Modality.OPEN } /* null を返すと、変更なし。 */ return null } }
まとめ
ソースにコメントを付けるという簡易的な形で、コンパイラープラグインの処理について追いかけてみました。
このプラグインの他にも、plugins ディレクトリにいくつか実装があります。またプラグインの他にも、Intellij IDEA 用のプラグインのようなコードもあるので、コンパイラープラグインに興味のある方は追いかけてみてください。
Kotlin 向けのテストライブラリ kotassert を作りました
何番煎じか分かりませんが、Kotlin 向けのテストライブラリを作りました。
さて、Kotlin で JUnit + hamcrest を使ってテストを書くと、予約後である is
に悩まされます。
assertThat(actual, `is`(expected))
これを解決するために kotlintest や knit というライブラリが出ています。
knit はご存知 @ngsw_taro さんのライブラリです。
// kotlintest actual shouldBe expected // knit actual.should be expected
上記で予約後問題は解決されるわけですが、エイゴチットモデキナイ日本人的にはぱっと見て分かるメソッド名にしたいわけですよ。
しばらく悩んで・・・
「予約語がだめなら、大文字にすればいいじゃない。」
ということで、メソッド名を大文字にした assertion を呼び出せるテストライブラリ kotassert
を作りました。
使い方はとても簡単ですし、日本人でも読みやすいと思います。
ちなみに API は、普段 C# で開発するときに使っている Chaining Assertion を パクって インスパイアしています。いつもありがとうございます。
// assertThat(actual, `is`(expected)) と同じ actual.Is(expected) // assertThat(actual, `is`(not(expected))) と同じ actual.IsNot(expected) // assertThat(actual, `is`(true)) と同じ actual.IsTrue() // assertThat(actual, `is`(notNullValue())) と同じ actual.IsNotNull()
「メソッド名の先頭が大文字なんて邪道だ!!」という声もあると思います。私も重々承知しています。けどテストコードということでそこは大目に見てもらって・・・。
ある程度、私が必要だと思った hamcrest の Matcher は用意しています。どういう API が用意されているかは README や テスト をご覧ください。
実装自体も、ただ JUnit + hamcrest を呼び出しているだけなので、すぐに読めると思います。拡張メソッド万歳。
良かったらお使いください。気に入ったらスターくれると喜びます。
Kotlin プロパティの Getter / Setter
Kotlin で、プロパティの Getter / Setter は以下の通りに定義できます。
var name: String get() { return ... } set(value) { ... }
定義する上でインデントは関係ないので、こんな感じでも書けちゃいます。
var name: String get() { return ... } set(value) { ... }
で?
せめてこうだったら読みやすかったのに、という感想です・・・。それだけです。
var name: String { get() { return ... } set(value) { ... } }
Redmine のカスタムフィールド「リスト」をチェックリストとして使う
Redmine のカスタムフィールドには、リストを選択するための書式「リスト」があります。(そのまんま)
その書式で定義したカスタムフィールドを「チェックリスト」として使いたかったのですが、デフォルトの Redmine では使いにくかったので、プラグイン「View Customize Plugin」を使ってカスタマイズした話を書きます。
※これ以降、書式「リスト」のカスタムフィールドのことを 「リストフィールド」 と記載します。
前提
- View Customize Plugin を導入している Redmine
https://redmine.org/plugins/redmine_view_customize
上記プラグインがあれば何でもできるので、ぜひ導入しましょう。
デフォルト Redmine を使ったチェックリストの問題点
デフォルト Redmine で、リストフィールドを利用したチェックリストを実装しようとすると、以下の問題に気が付きます。
- チケット表示画面では、リストフィールドのチェックされている項目は見えるが、チェックされていない項目が見えない。
具体的に、以下のようなリストフィールドを作成し、チケット入力画面と表示画面を見比べてみます。
カスタムフィールド定義
チェックリスト項目「テスト用リストフィールド」を定義します。
項番 | 説明 |
---|---|
(1) | 書式に「リスト」を指定します |
(2) | 複数選択可にチェックを付けます |
(3) | 選択肢に、チェックリストの項目を指定します |
(4) | 表示に「チェックボックス」を指定します |
チケット入力画面
作成したリストフィールドは、入力画面では以下のとおり表示されます。
この画面だけだと、チェックリストのように使えるのではないかと思ってしまいます。
チケット表示画面
問題のチケット表示画面です。
赤枠で囲まれている箇所がリストフィールドの表示箇所ですが、チェックした項目のみ表示されており全体が見えません。また、改行もされず読みにくいです。
解決案
解決案としては、チェックボックスをそのまま表示するだけで格段に見やすくなると思います。
今回は、View Customize Plugin を使ってチケット表示画面に JavaScript を適用して解決しようと思います。
解決したスクリプト
早速答え合わせですが、以下の JavaScript を登録することで、カスタムフィールド名に「チェックリスト」が含まれている場合のみチェックリスト表示にしてくれます。
カスタムフィールド名に「チェックリスト」を含むことを条件とした理由は、リストフィールドの本来の使い方をしたい場合と差別化するためです。
Path pattern: /issues/[0-9]+
Type: JavaScript
// カスタムフィールド名に「チェックリスト」が含まれる場合、表示時にチェックボックスを表示する。 $(function () { // 処理対象のカスタムフィールドを抽出する。 var $checklists = []; $('.attribute .label span').each(function () { var $this = $(this); if (!isChecklist($this)) return ; var $parent = $this.parent().parent(); $checklists.push($parent); }); // カスタムフィールドをチェックリスト形式で表示する。 $.each($checklists, function (i, $this) { var id = getCfId($this); if (id === null) return; var $items = getChecklistItems(id); var newHtml = createChecklistHtml($items); $this.find('.value').html(newHtml); }); // fun: 処理対象のカスタムフィールドかどうかを判定する function isChecklist($this) { var label = $this.text(); return (label.indexOf('チェックリスト') >= 0); } // fun: カスタムフィールドの id を取得する function getCfId($this) { var id = $this.attr('class').match(/\d+/); if (id === null) return null; return id[0]; } // fun: カスタムフィールドのすべての項目を取得します。 function getChecklistItems(id) { var selector = '#issue_custom_field_values_' + id; var $items = $(selector).parent().parent().find('label'); return $items; } // fun: 表示用の HTML 要素を作成します。 function createChecklistHtml($items) { var html = []; $items.each(function () { var $this = $(this); var $input = $this.find('input'); var result = '<input type="checkbox" value="test" disabled="true" /> '; if ($input.attr('checked')) { result = '<input type="checkbox" value="test" checked="checked" disabled="true" /> '; } result += $input.attr('value'); html.push(result); }); var newHtml = html.join('<br />') return newHtml; } });
適用後の画面
圧倒的見やすさ!
チェックしていない項目も表示されていて、チェック項目の見た目も入力画面と統一できました。これならチェックリストとして使えそうです。
まとめ
Redmine のカスタムフィールドを使ってチェックリストを実現する方法についてまとめました。
ほぼ View Customize Plugin のサンプルみたいな記事ですが、とても便利ですし Redmine を魔改造しやすいのでとてもおすすめです。 (時間があれば、カスタムフィールドをグループ化 / 並べ替えするスクリプトも作っているので紹介しようと思います。)
みなさんも便利なスクリプトがあればぜひ教えてください!
Kotlinで特定のブロック内でのみ有効な拡張関数を定義する
Kotlinのドキュメントに、Type-Safe Buildersという記事があります。 これは、Groovyでよく使われている builders パターンというものを、Kotlinで表現した際にどうかるかを示したドキュメントです。
Groovy使ったことないですが、build.gradle
の定義にも使われているこういう書き方を指します。
buildscript {
ext {
kotlinVersion = '1.0.0'
}
}
要は、独自のブロックを定義して、その中で初期化処理を実行しましょうということです。 Kotlinのブロックについては、親子関係を持つ独自のブロックを作るという内容で「逆引きKotlin」に追加されています。
親子関係のある独自のブロックを作りたい - 逆引きKotlin - 逆引きKotlin
今回は、この機能を利用した「特定のブロック内でのみ有効な拡張関数」を定義する方法について紹介したいと思います。
元ネタはこちらです。
やりたいこと
例として、ブロックを抜けたら指定したリソースを開放するブロックusing
を定義します。
開放するリソースを指定する拡張関数autoClose()
を、using
ブロック内でのみ有効化します。
using { val in = Files.newInputStream("in.txt").autoClose() val out = Files.newOutputStream("out.txt").autoClose() // ごにょごにょ }
定義
ポイントは、関数のレシーバーに指定したクラス内で拡張関数を定義することです。 こうすることで、クラス内でのみ拡張関数が有効となります。
usingに渡されたブロックは、レシーバーを指定したことによりレシーバー内のスコープとなり、拡張関数が有効となる仕組みです。
// usingブロック fun <R> using(block: ResourceHolder.() -> R): R { ResourceHolder().use { return it.block() } } // レシーバークラス class ResourceHolder : Closeable { val resources = mutableListOf<Any>() // Closeable インターフェースを実装したクラスに対する拡張関数 fun <T: Closeable> T.autoClose(): T { resources.add(this) return this } // AutoCloseable インターフェースを実装したクラスに対する拡張関数 fun <T: AutoCloseable> T.autoClose(): T { resources.add(this) return this } override fun close() { resources.reverses().forEach { when (it) { is Closeable -> it.close() is AutoCloseable -> it.close() } } } }
使い方
使い方は「やりたいこと」に書いたとおりです。
定義した拡張関数 autoClose()
は、using
ブロック以外からは呼び出すことが出来ず、コンパイルエラーとなります。
// テスト用のクラス class TestCloseable : Closeable { override fun close() { println("TestCloseable") } } class TestAutoCloseable : AutoCloseable { override fun close() { println("TestAutoCloseable") } } @Test fun usingTest() { TestCloseable().autoClose() // コンパイルエラー! using { TestCloseable().autoClose() TestAutoCloseable().autoClose() TestCloseable().autoClose() TestAutoCloseable().autoClose() TestCloseable().autoClose() TestCloseable().autoClose() TestAutoCloseable().autoClose() } // 出力 // TestAutoCloseable // TestCloseable // TestCloseable // TestAutoCloseable // TestCloseable // TestAutoCloseable // TestCloseable }
まとめ
特定のブロック内でのみ有効な拡張関数を定義する方法について紹介しました。
上記のようなリソース開放の他、トランザクションブロック内ではStringを直接SQLとして実行できるようにする、という使い方もできます。
拡張関数は便利だけど、使わせる範囲を明示的に指定したい場合に使ってみてください。
Kotlinで未実装を表す方法
小ネタです。
Scalaでは ???
という関数を実行することで、未実装を表す scala.NotImplementedError
を返すことができます。
Kotlinにも例外 kotlin.NotImplementedError
があり、それを呼び出す関数として TODO()
が定義されています。
使い方
メソッドに対して、コンパイルを通しつつ未実装を表す場合は、以下のように書きます。 引数に文字列を指定することで、例外にコメントを追加することも可能です。
fun hoge(s: String): String = TODO() // 未実装コメントを追加することも可能。 fun fuga(s: String): String = TODO("未実装ですよ")
また、inline修飾子が付いているため、メソッドの途中に書くことでIntellijの場合はそれ以降の処理が実行されないという警告が表示されます。
fun foo(s: String): String = { println("before: $s") TODO() println("after: $s") }
まとめ
未実装を表す kotlin.NotImplementedError
と TODO()
について紹介しました。
TODO()
のように、例外のみを返すメソッドをインライン展開させるという書き方も便利なので、ぜひ使ってみてください。
SpringBoot + Kotlin + Devtools を IntelliJ IDEA で上手く動かす
Kotlin で Web 開発をするために、とりあえず今は Spring Boot を使っています。
Eclipse の HTML, JavaScript エディタが自分に合わない(合うプラグインを見つけられなかった)ため、IntelliJ IDEA を使おうと思って設定を始めたのですが、上手く動かす方法を見つけるのが大変だったのでここでまとめておこうと思います。備忘録。
1. プロジェクト作成
Spring Boot のプロジェクトについては、Spring Initializr を使って作ります。
今回は以下の内容で作ります。
- Gradle project
- Spring Boot Version 1.3.1
- Dependencies:
- Web
- Devtools
- Thymeleaf
Group と Artifact はご自由に。今回は com.rabitarochan
、 springboot-kotlin-intellij
で作ってみます。
プロジェクトファイル一式を ZIP ファイルでダウンロードしたら、任意のディレクトリに展開します。
2. build.gradle 修正
次に、build.gradle に Kotlin 用の設定を追加していきます。
先に、変更箇所をコメントで記載した build.gradle 全体を載せます。
buildscript { ext { springBootVersion = '1.3.1.RELEASE' // (1) Kotlin のバージョンを指定 kotlinVersion = '1.0.0-beta-4584' } repositories { mavenCentral() } dependencies { classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") // (2) Kotlin の Gradle plugin を追加 classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion") } } apply plugin: 'java' // (3) Kotlin plugin を有効化 apply plugin: 'kotlin' apply plugin: 'eclipse' apply plugin: 'idea' apply plugin: 'spring-boot' jar { baseName = 'springboot-kotlin-intellij' version = '0.0.1-SNAPSHOT' } sourceCompatibility = 1.8 targetCompatibility = 1.8 repositories { mavenCentral() } dependencies { // (4) Kotlin のライブラリを参照追加 compile("org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion") compile('org.springframework.boot:spring-boot-devtools') compile('org.springframework.boot:spring-boot-starter-thymeleaf') compile('org.springframework.boot:spring-boot-starter-web') testCompile('org.springframework.boot:spring-boot-starter-test') } // (5) IntelliJ IDEA 用の設定を追加 idea { module { inheritOutputDirs = false outputDir = file("$buildDir/classes/main/") } } eclipse { classpath { containers.remove('org.eclipse.jdt.launching.JRE_CONTAINER') containers 'org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8' } } task wrapper(type: Wrapper) { gradleVersion = '2.9' }
(1) で、Kotlin のバージョンをプロパティとして定義します。Kotlin は現時点 (2016/1/12) でバージョン 1.0 のベータ版という状態のため、新しいバージョンが頻繁に出ます。今回の build.gradle でも 2 箇所で Kotlin のバージョンが必要となりますので、プロパティにしておいたほうがいいでしょう。
Kotlin のバージョンは、Kotlin の Twitter アカウント や Using Gradle で確認しています。
(2)、(3) で、Kotlin の Gradle プラグインを有効化しています。
(4) で、Kotlin の標準ライブラリを依存ライブラリに追加します。
(5) では、IntelliJ IDEA で make した際の出力ディレクトリを上書きしています。これは、Spring Boot Devtools の自動再起動を有効化するために必要です。
IntelliJ IDEA では make した際のデフォルト出力先は out
ディレクトリとなっていますが、Spring Boot Devtools はこのディレクトリではなく、idea.module.outputDirs
で指定した $buildDir/classes/main/
を見ているようです。IntelliJ IDEA プラグインの設定については IdeaModule - Gradle DSL Version 2.10 をご参照ください。
3. IntelliJ IDEA のショートカット登録
Spring Boot + IntelliJ IDEA で調べるとすぐ発覚するのですが、IntelliJ IDEA で実行 / デバッグ実行中に自動 make が起動しないという仕様があります。これに対応するために、Ctrl (or Cmd) + Shift + S
でファイル保存と make をするマクロを実行するように設定していきます。
3.1. Save all and make
マクロを作成する
勝手に名前つけましたが、以下の手順で上で書いたマクロを作成していきます。
- IntelliJ IDEA を起動します
- メニューの [Edit] → [Macros] → [Start Macro Recording] をクリックします。(これ以降の操作がマクロの内容となります。)
- 【操作1】メニューの [File] → [Save All] をクリックします。
- 【操作2】メニューの [Build] → [Make Project] をクリックします。
- メニューの [Edit] → [Macros] → [Stop Macro Recording] をクリックし、マクロの記録を停止します。
- マクロ名を入力するダイアログが表示されるので、適当に名前を入力します。(ここでは
Save all and make
と入力します。)
3.2. マクロを実行するショートカットを設定する
マクロを Ctrl (or Cmd) + Shift + S
で実行できるようにするため、以下の手順でショートカットを登録します。
- メニューの [IntelliJ IDEA] → [Preferences...] をクリックします。(これは OS X の場合です。Windows は [File] → [Preferences...] だったはず・・・)
- 設定一覧の [Keymap] を選択します。
- 右側に検索ボックスが表示されているので、[save all] で検索すると、先ほど作成したマクロが表示されるので、[右クリック] → [Add Keyboard Shortcut] をクリックします。
- ダイアログが表示されるので、設定するショートカットを入力します。ショートカットの内容を確認して [OK] ボタンをクリックします。
これで IntelliJ IDEA の設定は完了です。
4. IntelliJ IDEA のプロジェクトを作成する
gradle のコマンドで IntelliJ IDEA のプロジェクトファイルを作成します。
ターミナルを起動して ./gradlew idea
を実行するだけです。Windows の場合は gradlew idea
です。
あとは、IntelliJ IDEA からプロジェクトのディレクトリを指定して Open します。
5. 実行する
まだ Kotlin は書いてませんが、実行方法を書いておきます。
Application
クラスを [右クリック] → [Debug ...] をクリックします。- あとはソース、HTML ファイル等を編集して、上で登録したショートカットを押すと、デバッグ実行中でも自動再起動やホットスワップがおこなわれ、スムーズに開発できます!
また、コントローラーにルーティングを追加した場合や、クラスのメソッドを追加した際には、ホットスワップが上手く動かないらしく以下のメッセージが表示されます。
Hot Swap failed: com.rabitarochan.MyApplication: add method not implemented; com.rabitarochan.MyApplication: Operation not supported by VM
メッセージ的には failed と書いてありますが、Devtools の自動再起動にてちゃんと認識してくれるようですので、あまり気にしなくてもいいでしょう。ちゃんと認識しない場合はちゃんと再起動してあげたほうがいいです。
まとめ
あまり大きなアプリを開発しているわけではありませんが、以上の手順で今のところスムーズに開発できてます。
Kotlin で Spring Boot を書くためには少し工夫が必要だったりするので、それについてはまた後で記事を書こうと思います。まずは開発環境を構築するということで、ご査収ください。
今回のソースは以下のリポジトリです。Application クラスと Controller を Kotlin で実装したソースもありますので、記事を書くまではこちらをご参照ください。
でわ。