Groovy(Spock)からKotlinのCompanion Objectを呼び出す #groovy #spock #kotlin

概要

最近はJavaではなくKotlinを書いていますが、Spockが好きすぎるのでテストはGroovyで書いたりします。

KotlinのCompanion Objectをテストするときに、Groovy側から呼び出す方法です。

Companion Object

Kotlinではクラスに静的なメソッドを定義する時はCompanion Objectを使います。

Javaのstaticみたいなものと自分は理解しています。

class Sample {
    companion object {
        // 大文字にするメソッド
        fun toUpper(s: String): String = s.toUpperCase()
    }
}

こうするとKotlin側では Sample.toUpper("hoge") のように呼び出せるようになります。

Groovy側での呼び出し方

GroovyではCompanionのメソッドを呼び出すには Direct field access operator を使います。

class SampleSpec extends Specification {
    def 'hogeが大文字HOGEになる'() {
        when:
        def actual = Sample.@Companion.toUpper('hoge')

        then:
        actual == 'HOGE'
    }
}

Groovyからの呼び出しにはCompanionオブジェクトを経由する必要があり、なおかつダイレクトアクセスである必要があります。

ダイレクトアクセスする方法は、アクセスするフィールドの前に @ を付けるだけです。

Companionを経由しない Sample.toUpper('hoge') や、通常のフィールドアクセス Sample.Companion.toUpper('hoge') では MissingMethodException が発生します。

ダイレクトアクセスと通常のフィールドアクセスの違いですが、Groovyのフィールドアクセスは暗黙的にGetterが呼ばれるのに対し、ダイレクトアクセスの場合Getterは呼ばれずフィールドの直アクセスになります。

CompanionオブジェクトにはGetterがないので、直アクセスする必要があるのかなと想像しています。

JvmStatic

JvmStaticアノテーションを使えば、Groovyからでも直接メソッドを呼び出せるようになります。

@JvmStatic
fun toUpper(s: String): String = s.toUpperCase()

ただ、テストコードのためにプロダクションコードに手を入れることになるので今回のようなケースではおすすめしません。

参考

Problems about accessing Kotlin companion object in Groovy? - Stack Overflow The Apache Groovy programming language - Operators

Nuxtで画面に横断的にMiddlewareを適用する #nuxtjs #vuejs

概要

Nuxtにはミドルウェアという機構がある。

ミドルウェアを使うと、ページがレンダリングされる前に、実行されるカスタム関数を定義できる。

複数のページでミドルウェアを実行するには3つ方法がある。

  1. nuxt.config.js でrouterに読み込ませる
  2. layoutでミドルウェアを読み込む
  3. layoutを使用しているページ全てで読み込まれる
  4. 各ページでミドルウェアを読み込む

nuxt.config.js で読み込まれる方法を使うと、すべての画面遷移時にミドルウェアが実行されるようになる。
(ブラウザの更新ボタンによるリロード含む)

この方法を試してみる。

なおMiddlewareは nuxt.config.js → レイアウト → page の順で実行されるそうだ。

Middlewareの作成

単純にログだけ吐くミドルウェアmiddleware/sample.js を作成する

export default function({ route }) {
  console.log('サンプルミドルウェア', route.path)
}

routerでの読み込み

nuxt.config.js で作成したミドルウェアを読み込む。

module.exports = {
  router: {
    middleware: 'sample'
  }  
}

これで、全画面遷移時にパスをログ出力するようになる。

いろいろやってみる

せっかくなので middleware/sample.js 修正して色々試してみる。

  • パスの値を判定
  • Vuexストアの値を操作
  • ストアの値を見てリダイレクト
export default function({ route, redirect, store }) {
  // ログ出力
  console.log('サンプルミドルウェア', route.path)

  // 現在のパスを取得
  const currentPath = route.path
  if (currentPath !== '/hoge') {
    // hogeページでは何もしない
    return
  }

  // storeに数値を保持してカウントアップする
  store.dispatch('count/countUp')
  const count = store.getters['count/count']
  if (count % 2 === 0) {
    // storeの値が偶数のときだけhogeページは表示できる
    console.log('hogeページに行けるのはstoreが偶数のときだけ', count)
    // fugaページにリダイレクト
    redirect('/fuga')
  }
}

この例で使用しているVuexストアは store/count.js で下記の通り。

export const state = () => ({
  count: 0,
})

export const mutations = {
  increment(state) {
    state.count++
  },
}

export const getters = {
  count(state) {
    return state.count
  },
}

export const actions = {
  countUp(context) {
    context.commit('increment')
  },
}

参考

ja.nuxtjs.org ja.nuxtjs.org

SpringBootで独自例外を作ってControllerAdviceでハンドリングする

概要

  • 独自RuntimeExceptionを作る
  • 独自例外はControllerAdviceで横断的にハンドリングする
  • テストを書く

前提

  • Kotlin
  • RestControllerを想定
    • ゆえにRestControllerAdviceを使う
  • テストはSpock

独自例外の作成

  • RuntimeExceptionを作成
  • コンストラクタ引数はnullを許可したくなかったので、superのコンストラクタにそれぞれ渡している
class MyException : RuntimeException {
    constructor(message: String, cause: Throwable) : super(message, cause)
    constructor(message: String) : super(message)
    constructor(cause: Throwable) : super(cause)
    constructor() : super()
}

ハンドラークラスの作成

  • クラスに @RestControllerAdvice を付与
  • ハンドルするメソッドに @ExceptionHandler を付与
  • ハンドルするメソッドに @ResponseStatus を付与
    • 指定したHTTPステータスがレスポンスに設定される
    • この例だとINTERNAL_SERVER_ERROR(500)
  • ハンドルするメソッドは対象の例外を引数で受け取る
    • 返り値の型がレスポンスボディに設定される
    • デフォルトだとRestControllerならJSONになると思う
@RestControllerAdvice
class MyExceptionHandler {
    @ExceptionHandler(MyException::class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    fun handleMyException(e: MyException): Map<String, String> = mapOf("message" to "Error!!")
}

テストの作成

  • クラスに @SpringBootTest@AutoConfigureMockMvc を付与
    • MockMvcをAutowiredしてテストする
  • スタブコントローラを作成する
    • CotrollerAdviceはコントローラに横断的に適用されるので、個別のコントローラに対しては(ユニット)テストしない
    • 常にテスト対象の例外をthrowするようなダミーのエンドポイントを用意する
  • テストクラスに @ActiveProfiles を、スタブに @Profile を付与する
    • 必須ではない
    • スタブのエンドポイントが重複した場合を避けるため
@SpringBootTest
@AutoConfigureMockMvc
@ActiveProfiles("myExceptionHandlerSpec")
class MyExceptionHandlerSpec extends Specification {
    @Autowired
    MockMvc mockMvc

    def 'MyExceptionが発生した場合はHTTP:500になる'() {
        expect:
        mockMvc.perform(MockMvcRequestBuilders.get('/mock'))
                .andExpect(MockMvcResultMatchers.status().is(HttpStatus.INTERNAL_SERVER_ERROR.value()))
                .andExpect(MockMvcResultMatchers.jsonPath("\$.message").value("Error!!"))
    }
}

// ControllerAdviceをテストするためのStub
@RestController
@RequestMapping("/mock")
@Profile("exceptionHandlerSpec")
class StubController {
    @GetMapping
    def test() {
        throw new MyException()
    }
}

参考

Mavenで実行するテストをIDEAでデバッグする #Maven #IntelliJ

概要

IntteliJ IDEAで、Mavenプロジェクトのtestフェーズをデバッグ実行した場合に、ブレークポイントで止まってくれない(ステップ実行ができない)現象に遭遇した。

Mavenはデフォルトだとテストが別の(forkした)プロセスで実行されるため、IDEAがテストの実行プロセスを捕まえられないようだ。

実行時に -DforkCount=0 オプションをつけるとデバッグできるようになる。

$ mvn -DforkCount=0 test

補足

Maven Surefire Pluginmaven.surefire.debug プロパティを使えば、リモートデバッグの要領でforkしたプロセスでもデバッグはできるよう。

ただテストをデバッグしたいのって、IDEで開発中のことがほとんどだろうし、いっそforkさせないようにした方が楽と思われる。

forkMode=never というパラメータもあるが、これは互換性のために残された古いものなので、forkCountを利用したほうがよい。

参考

AWS CodeDeployを使う時のEC2のセットアップ #AWS

概要

CodeDeployを使ってEC2にデプロイするときに準備すること。

既にEC2インスタンス自体は存在することを想定。

AWS CodeDeployエージェントのインストール

docs.aws.amazon.com

  • rubywgetを先に入れておく必要があるようだ
# ステータス
sudo service codedeploy-agent status
# 起動
sudo service codedeploy-agent start
# 停止
sudo service codedeploy-agent stop
# 再起動
sudo service codedeploy-agent restart
  • エージェントのログ
    • /var/log/aws/codedeploy-agent/codedeploy-agent.log
  • デプロイするartifact
    • /opt/codedeploy-agent/deployment-root/{デプロイグループID}/{デプロイID}/deployment-archive/
  • hookで実行したスクリプトのログ
    • /opt/codedeploy-agent/deployment-root/{デプロイグループID}/{デプロイID}/logs/scripts.log

IAMインスタンスプロファイルの作成

docs.aws.amazon.com

IAMロールをインスタンスにアタッチする

docs.aws.amazon.com

associate-iam-instance-profile — AWS CLI 1.15.17 Command Reference

# こんな感じ
aws ec2 associate-iam-instance-profile \
  --instance-id {EC2のインスタンスID} \
  --iam-instance-profile Arn={IAMロールのARN}
  • ArnじゃなくてNameのオプションもある
  • アタッチした後、Codeデプロイエージェントは再起動しないとダメっぽい

参考

qiita.com

AWS CodeDeployのApplicationStopは旧リビジョンのスクリプトが使われる #AWS

概要

CodeDeployのApplicationStopは旧リビジョンのスクリプトが使われる。

どういう問題が起きるか?

一度デプロイに成功した後、二度目以降で発生するようなバグがApplicationStopのスクリプトで発生すると、再試行してもエラーになり続ける。

(新しい修正したスクリプトを使ってくれないので)

あんまり発生しなそうだけど、自分が踏んだのは下記のようなケース。

  • アプリケーションが停止できなかった場合はエラーにするような停止スクリプト
  • EC2インスタンスを別件(スケールアップ)のために再起動した
  • 再起動のタイミングでCodeDeployに関係なくアプリケーションは停止
  • CodeDeployを動かすと、アプリが停止できない(そもそも起動してない)のでスクリプトがエラー
  • 停止スクリプトを修正するも、反映されない(古いスクリプトが使われるから)

どう対応したか

もう全てを最初からやり直すことにした。

  1. /opt/codedeploy-agent/deployment-root/[デプロイグループID]/ 配下にartifactのキャッシュがデプロイID単位で配置されているので、丸っと消す。
  2. /opt/codedeploy-agent/deployment-root/deployment-instructions/ 配下に前回デプロイ情報が残されてるので、丸っと消す。
  3. 前回デプロイしたモジュールを削除する

これで再デプロイする。

前回デプロイしたモジュールを削除するのは、CodeDeployは自身がデプロイしたモジュールが既に配置されていた場合、エラーになるため。 (つまり、上書きデプロイに制限がある)

キャッシュ等を削除することにより、今あるモジュールがCodeDeployによって配備されたものだと認識できなくなってしまうので削除する必要がある。

参考

qiita.com qiita.com

AWS CodeDeployでSpringBootのデプロイが終わらない #AWS

概要

AWS CodeDeployを使ってSpringBootをEC2にデプロイするようにした。

bashスクリプトを使って、javaコマンドのバックグラウンド実行(&)で起動したところデプロイが終わらなかった。

もう少し正確にいうと、スクリプトは期待通りに動作し、Bootアプリケーションも起動したのだが、CodeDeployがアプリが起動済なことを認識してくれなかった。

原因と対策

どうも単にjavaコマンドでバックグラウンド実行すると、標準入出力を掴んでしまい、そうするとCodeDeployがスクリプトの終了を検知してくれない模様。

標準入出力を適当にリダイレクトするようにしたら解決した。

プロダクション環境なんかではログ設計・出力が精査された状態なら、 /dev/null に捨ててもいいかもしれない。

詳細

appspec.ymlはこんな感じ

version: 0.0
os: linux
files:
  - source: sample-app.jar
    destination: /home/ubuntu
permissions:
  - object: /home/ubuntu
    pattern: "*.jar"
    owner: ubuntu
hooks:
  ApplicationStop:
    - location: stop.bash
      timeout: 300
      runas: ubuntu
  ApplicationStart:
    - location: start.bash
      timeout: 300
      runas: ubuntu

起動スクリプトはこんな感じ

#!/usr/bin/env bash

set -eu

readonly APPLICATION_ROOT_DIR="/home/ubuntu"

# 単純なバックグラウンド実行だとCodeDeploy側で終了を検知してくれない
# java -jar "${APPLICATION_ROOT_DIR}/sample-app.jar &"

# 標準入出力をログにリダイレクトするようにした
java -jar "${APPLICATION_ROOT_DIR}/sample-app.jar" \
    >> "${APPLICATION_ROOT_DIR}/sample-app.log" \
    2>&1 &

補足

serviceとして起動する方法もあるみたい

qiita.com

参考

ApplicationStart hook is pending after starting process in background