Spring Boot起動中に出た例外をFailureAnalyzerでつかむ


経緯

こちらを参照
https://qiita.com/ShassBeleth/items/476436b7c81fe81ddbb2

まぁ指定のポートがすでに使われてた場合のExceptionは
もともとSpring Bootに用意されてるんだけどね

上書きもできるし追加もできるよっていう話

環境

Spring Boot 2.2.2.RELEASE - 2.2.4.RELEASE
Kotlin 1.3.61
Gradle

環境は以下の記事のまんま

Spring Boot側が用意してくれてるFailureAnalyzer

例外つかむにはAbstractFailureAnalyzerを継承する必要がある
検索かけて出てきたクラスがこれ

全部で17個
さらにそれぞれのクラスを継承するかもしれないからもっとあるかも

ポートが既に使われてた時に呼ばれるExceptionはPortInUseException
それをつかむのがPortInUseFailureAnalyzer

PortInUseFailureAnalyzer.java
class PortInUseFailureAnalyzer extends AbstractFailureAnalyzer<PortInUseException> {

    @Override
    protected FailureAnalysis analyze(Throwable rootFailure, PortInUseException cause) {
        return new FailureAnalysis("Web server failed to start. Port " + cause.getPort() + " was already in use.",
                "Identify and stop the process that's listening on port " + cause.getPort() + " or configure this "
                        + "application to listen on another port.",
                cause);
    }

}

中のクラスはこんな感じ

適当に裏でポート8080使っといて

Boot Runでアプリを起動すると・・・

こんな感じ

FailureAnalyzerにあるDescriptionとActionがログに書き込まれてるっていうね

Spring Bootが用意してるFailureAnalyzerを独自のものに書き換える

正直おすすめはできない

MyPortInUseFailureAnalyzer.kt
class MyPortInUseExceptionFailureAnalyzer : AbstractFailureAnalyzer<PortInUseException>() {
    override fun analyze(rootFailure: Throwable, cause: PortInUseException): FailureAnalysis {
        return FailureAnalysis(
                cause.message,
                "Port already in use!!!",
                cause
        )
    }
}

AbstractFailureAnalyzer<PortInUseException>を継承したクラスを作っておく

resources/META-INF/フォルダ配下にspring.factoriesファイルを作って

resources/META-INF/spring.factories
org.springframework.boot.diagnostics.FailureAnalyzer =\
  com.example.spring.boot.web.failure.analyzer.MyPortInUseFailureAnalyzer 

その中身はこんな感じ
これで登録OK

また適当にポート8080を裏で使っておいてからBoot Runすると

上書きできましたー

おすすめしないっていうのも例外時に出力される情報を変更できちゃうので、実際に例外が発生したときに何があったかわからなくなるから
出力される内容は変えずに処理をはさんだりとかするのがいいのかな・・・?

FailureAnalyzerのカスタマイズの使い道としては以下の方が実用的かも

独自のExceptionをFailureAnalyzerでつかむ

SampleFailureAnalyzer.kt
/**
 * 独自の例外
 */
class SampleException(message: String?) : RuntimeException(message)

/**
 * カスタムFailureAnalyzerのサンプル
 * アプリ起動中に発生したSampleExceptionをつかむ
 */
class SampleFailureAnalyzer : AbstractFailureAnalyzer<SampleException>() {
    override fun analyze(rootFailure: Throwable, cause: SampleException): FailureAnalysis {
        return FailureAnalysis(
                cause.message,
                "Sample Exception!",
                cause
        )
    }
}

こんな感じに独自の例外を作っておいて、それをつかむFailureAnalyzerを作成しておく

spring.factoriesはこんな感じ

resources/META-INF/spring.factories
org.springframework.boot.diagnostics.FailureAnalyzer =\
  com.example.spring.boot.web.failure.analyzer.MyPortInUseFailureAnalyzer ,\
  com.example.spring.boot.web.failure.analyzer.SampleFailureAnalyzer

複数のFailureAnalyzerを登録することもできます

で、作ったSampleFailureAnalyzerがちゃんとつかめるか確認したいから
以下を適当に作っておく

SampleFailureAnalyzer.kt
/**
 * アプリ起動中に読み込まれるサンプルComponent
 */
@Component
class SampleComponent {
    fun run() {
        throw SampleException("exception!")
    }
}

/**
 * リフレッシュ時にイベント発火するListenerクラス
 */
@Component
class SampleApplicationListener : ApplicationListener<ContextRefreshedEvent?> {

    @Autowired
    private val sampleComponent: SampleComponent? = null

    /**
     * リフレッシュ時にSampleComponentのrunを実行する(例外を発生させる)
     */
    override fun onApplicationEvent(event: ContextRefreshedEvent) {
        sampleComponent!!.run()
    }

}

  1. リフレッシュ時にSampleApplicationListenerのonApplicationEventが呼ばれる
  2. sampleComponentのrunが呼ばれる
  3. runの中でExceptionが発生する

って感じ
これでアプリ起動時にエラーが発生するようになりました

実行してみると

ちゃんとメッセージ出せてます!

spring.factoriesを登録していない場合はスタックトレースが表示されます

まとめ

  • FailureAnalyzerでアプリ起動中の例外もつかめる!
  • 既にSpring Bootが用意してくれてるものも上書きできる!
  • 自分で作った例外もつかめる!

スタックトレースのほうがわかりやすい場合もあるし、一概にすべてのExceptionをつかんだほうがいいとは言えないのかな・・・?
でも把握できてるExceptionかどうか、発生しうる例外かどうかの判断はできるし、まぁまぁ便利だなとは思う