読者です 読者をやめる 読者になる 読者になる

Kotlin Compiler Plugin (kotlin-allopen) を追いかける

Kotlin

これは Kotlin AdventCalendar 2016 の 24 日目の記事です。メリークリスマスイブ!時間的にギリギリアウトでした・・・。

さて、少し前に Kotlin 1.0.6 RC が出ましたが、そのタイミングで kotlin-allopenkotlin-noarg という 2 つのコンパイラプラグインが発表されました。

discuss.kotlinlang.org

それぞれ、以下の働きをするコンパイラプラグインとなっています。

  • 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 向けのプラグイン実装が用意されています。

今回はプラグイン本体のみ追いかけます。

プラグイン本体

ここから、プラグイン本体のコードを読んでいきます。コードは 2 ファイルのみで、大きくないためすぐ読めます。

AllOpenPlugin.kt

AllOpenPlugin.kt には、コンパイラプラグインに必要な 2 つのインターフェースを実装したクラスが定義されています。 コンパイラプラグインに必要なインターフェースは以下の 2 つです。

/*
 * 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 向けのテストライブラリを作りました。

さて、Kotlin で JUnit + hamcrest を使ってテストを書くと、予約後である is に悩まされます。

assertThat(actual, `is`(expected))

これを解決するために kotlintestknit というライブラリが出ています。

knit はご存知 @ngsw_taro さんのライブラリです。

taro.hatenablog.jp

// kotlintest
actual shouldBe expected

// knit
actual.should be expected

上記で予約後問題は解決されるわけですが、エイゴチットモデキナイ日本人的にはぱっと見て分かるメソッド名にしたいわけですよ。

しばらく悩んで・・・

予約語がだめなら、大文字にすればいいじゃない。」

ということで、メソッド名を大文字にした assertion を呼び出せるテストライブラリ kotassert を作りました。

github.com

使い方はとても簡単ですし、日本人でも読みやすいと思います。

ちなみに 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

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 のカスタムフィールドには、リストを選択するための書式「リスト」があります。(そのまんま)

その書式で定義したカスタムフィールドを「チェックリスト」として使いたかったのですが、デフォルトの Redmine では使いにくかったので、プラグイン「View Customize Plugin」を使ってカスタマイズした話を書きます。

※これ以降、書式「リスト」のカスタムフィールドのことを 「リストフィールド」 と記載します。

前提

上記プラグインがあれば何でもできるので、ぜひ導入しましょう。

デフォルト Redmine を使ったチェックリストの問題点

デフォルト Redmine で、リストフィールドを利用したチェックリストを実装しようとすると、以下の問題に気が付きます。

  • チケット表示画面では、リストフィールドのチェックされている項目は見えるが、チェックされていない項目が見えない。

具体的に、以下のようなリストフィールドを作成し、チケット入力画面と表示画面を見比べてみます。

カスタムフィールド定義

チェックリスト項目「テスト用リストフィールド」を定義します。

f:id:rabitarochan:20160905184214p:plain

項番 説明
(1) 書式に「リスト」を指定します
(2) 複数選択可にチェックを付けます
(3) 選択肢に、チェックリストの項目を指定します
(4) 表示に「チェックボックス」を指定します

チケット入力画面

作成したリストフィールドは、入力画面では以下のとおり表示されます。

この画面だけだと、チェックリストのように使えるのではないかと思ってしまいます。

f:id:rabitarochan:20160905184219p:plain

チケット表示画面

問題のチケット表示画面です。

赤枠で囲まれている箇所がリストフィールドの表示箇所ですが、チェックした項目のみ表示されており全体が見えません。また、改行もされず読みにくいです。

f:id:rabitarochan:20160905184224p:plain

解決案

解決案としては、チェックボックスをそのまま表示するだけで格段に見やすくなると思います。

今回は、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;
  }

});

適用後の画面

圧倒的見やすさ!

チェックしていない項目も表示されていて、チェック項目の見た目も入力画面と統一できました。これならチェックリストとして使えそうです。

f:id:rabitarochan:20160905185815p:plain

まとめ

Redmine のカスタムフィールドを使ってチェックリストを実現する方法についてまとめました。

ほぼ View Customize Plugin のサンプルみたいな記事ですが、とても便利ですし Redmine魔改造しやすいのでとてもおすすめです。 (時間があれば、カスタムフィールドをグループ化 / 並べ替えするスクリプトも作っているので紹介しようと思います。)

みなさんも便利なスクリプトがあればぜひ教えてください!

Kotlinで特定のブロック内でのみ有効な拡張関数を定義する

Kotlin

Kotlinのドキュメントに、Type-Safe Buildersという記事があります。 これは、Groovyでよく使われている builders パターンというものを、Kotlinで表現した際にどうかるかを示したドキュメントです。

kotlinlang.org

Groovy使ったことないですが、build.gradleの定義にも使われているこういう書き方を指します。

buildscript {

    ext {
        
        kotlinVersion = '1.0.0'

    }

}

要は、独自のブロックを定義して、その中で初期化処理を実行しましょうということです。 Kotlinのブロックについては、親子関係を持つ独自のブロックを作るという内容で「逆引きKotlin」に追加されています。

親子関係のある独自のブロックを作りたい - 逆引きKotlin - 逆引きKotlin

今回は、この機能を利用した「特定のブロック内でのみ有効な拡張関数」を定義する方法について紹介したいと思います。

元ネタはこちらです。

discuss.kotlinlang.org

やりたいこと

例として、ブロックを抜けたら指定したリソースを開放するブロック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で未実装を表す方法

Kotlin

小ネタです。

Scalaでは ??? という関数を実行することで、未実装を表す scala.NotImplementedError を返すことができます。

Kotlinにも例外 kotlin.NotImplementedError があり、それを呼び出す関数として TODO() が定義されています。

github.com

使い方

メソッドに対して、コンパイルを通しつつ未実装を表す場合は、以下のように書きます。 引数に文字列を指定することで、例外にコメントを追加することも可能です。

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")

}

f:id:rabitarochan:20160225082936p:plain

まとめ

未実装を表す kotlin.NotImplementedErrorTODO() について紹介しました。

TODO() のように、例外のみを返すメソッドをインライン展開させるという書き方も便利なので、ぜひ使ってみてください。

SpringBoot + Kotlin + Devtools を IntelliJ IDEA で上手く動かす

Kotlin SpringBoot 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.rabitarochanspringboot-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 マクロを作成する

勝手に名前つけましたが、以下の手順で上で書いたマクロを作成していきます。

  1. IntelliJ IDEA を起動します
  2. メニューの [Edit] → [Macros] → [Start Macro Recording] をクリックします。(これ以降の操作がマクロの内容となります。)
  3. 【操作1】メニューの [File] → [Save All] をクリックします。
  4. 【操作2】メニューの [Build] → [Make Project] をクリックします。
  5. メニューの [Edit] → [Macros] → [Stop Macro Recording] をクリックし、マクロの記録を停止します。
  6. マクロ名を入力するダイアログが表示されるので、適当に名前を入力します。(ここでは Save all and make と入力します。)

3.2. マクロを実行するショートカットを設定する

マクロを Ctrl (or Cmd) + Shift + S で実行できるようにするため、以下の手順でショートカットを登録します。

  1. メニューの [IntelliJ IDEA] → [Preferences...] をクリックします。(これは OS X の場合です。Windows は [File] → [Preferences...] だったはず・・・)
  2. 設定一覧の [Keymap] を選択します。
  3. 右側に検索ボックスが表示されているので、[save all] で検索すると、先ほど作成したマクロが表示されるので、[右クリック] → [Add Keyboard Shortcut] をクリックします。
  4. ダイアログが表示されるので、設定するショートカットを入力します。ショートカットの内容を確認して [OK] ボタンをクリックします。

これで IntelliJ IDEA の設定は完了です。

4. IntelliJ IDEA のプロジェクトを作成する

gradle のコマンドで IntelliJ IDEA のプロジェクトファイルを作成します。

ターミナルを起動して ./gradlew idea を実行するだけです。Windows の場合は gradlew idea です。

あとは、IntelliJ IDEA からプロジェクトのディレクトリを指定して Open します。

5. 実行する

まだ Kotlin は書いてませんが、実行方法を書いておきます。

  1. Applicationクラスを [右クリック] → [Debug ...] をクリックします。
  2. あとはソース、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 で実装したソースもありますので、記事を書くまではこちらをご参照ください。

github.com

でわ。