Spring Boot のデフォルトエラーメッセージを制御したい

## 目的
Spring Boot で作成した REST API でエラーが発生したときに返る、デフォルトのエラーメッセージを制御したい。

## 背景

以下のような RestController を用意したとする。

``` java:MyRestController.java
package demo;

import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class MyRestController {
    
    @RequestMapping(value="/test/{id}")
    private String hello(@PathVariable String id){
        if ("0000".equals(id)) {
            throw new MyCustomException(); // 想定される Exception
        }
        
        return "Hello world!!";
    }

    @RequestMapping(value="/test-null")
    private String hello(){
        String s = null;
        s.equals("hoge"); // うっかり NPE 発生
        
        return "Hello world!!";
    }
}
```
すると、Spring Boot はデフォルトで以下のようなエラーメッセージを返却してくれる。

``` sh
$ curl http://localhost:8080/test/0000 # 想定される MyCustomError
{"timestamp":1526654632290,"status":500,"error":"Internal Server Error","exception":"my.spring.MySpringBootSample.MyCustomError","message":"No message available","path":"/test/0000"}%
$ curl http://localhost:8080/test-null # うっかり出してしまった NPE
{"timestamp":1526654636310,"status":500,"error":"Internal Server Error","exception":"java.lang.NullPointerException","message":"No message available","path":"/test-null"}%
$ curl http://localhost:8080/wrong-path # パス間違い
{"timestamp":1526654639289,"status":404,"error":"Not Found","message":"No message available","path":"/wrong-path"}%
```

しかし、うっかりで発生してしまった NullPointerException がユーザに見えてしまうのはあまりにもかっこ悪いし、セキュリティ的にもよろしくない。Spring Boot を使っているのもばれてしまう。

## 想定されるエラーの制御

`@ControllerAdvice` アノテーションを付与したクラスを用意することで、RestController を横断してエラーメッセージを制御することが出来る。

``` java:MyControllerAdvice.java
package demo;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

@ControllerAdvice
public class MyControllerAdvice {

    /**
     * MyCustomException が発生した際のハンドラ
     * 
     * @return レスポンス
     */
    @ExceptionHandler(MyCustomException.class)
    public ResponseEntity<String> handleControllerException() {
        return new ResponseEntity<String>("customized message for MyCustomError.", HttpStatus.INTERNAL_SERVER_ERROR);
    }
}
```

``` sh
$ curl http://localhost:8080/test/0000
customized message for MyCustomError.%
```

## 想定外のエラーに対する制御

上記に加えて、Exception.class を受ける ExceptionHandler を作ってしまえば、むりやり、RestController で発生するあらゆる想定外のエラーに対して一律にカスタムエラーメッセージを返すことも出来なくはない。

``` java:MyControllerAdvice.java
package demo;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

@ControllerAdvice
public class MyControllerAdvice {

    /**
     * MyCustomException が発生した際のハンドラ
     * 
     * @return レスポンス
     */
    @ExceptionHandler(MyCustomException.class)
    public ResponseEntity<String> handleControllerException() {
        return new ResponseEntity<String>("customized message for MyCustomError.", HttpStatus.INTERNAL_SERVER_ERROR);
    }
    
    /**
     * Exception が発生した際のハンドラ
     * 
     * @return レスポンス
     */
    @ExceptionHandler(Exception.class)
    public ResponseEntity<String> handleControllerException2() {
        return new ResponseEntity<String>("customized message for any unexpected exception.", HttpStatus.INTERNAL_SERVER_ERROR);
    }
}
```

しかしこの方法では、パス間違いなど、RestController に入る以前のエラーは制御できない。

``` sh
$ curl http://localhost:8080/test/0000
customized message for MyCustomError.%
$ curl http://localhost:8080/test-null
customized message for any unexpected exception.%
$ curl http://localhost:8080/wrong-path # パス間違いに対してはデフォルトエラーが返ってしまう
{"timestamp":1526655182855,"status":404,"error":"Not Found","message":"No message available","path":"/wrong-path"}%
```

調べてみると、[公式](https://docs.spring.io/spring-boot/docs/1.5.3.RELEASE/reference/html/boot-features-developing-web-applications.html#boot-features-error-handling)に以下のような記述が。

> Spring Boot provides an /error mapping by default that handles all errors in a sensible way, and it is registered as a ‘global’ error page in the servlet container.

要は、あらゆるエラーはデフォルトで `/error` 以下にマッピングされるとのこと。
好きに制御したい場合は、ErrorController を実現せよ、とのことなので、以下のようなクラスを用意する。

``` java:MyErrorController.java
package demo;

import org.springframework.boot.autoconfigure.web.ErrorController;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class MyErrorController implements ErrorController {

    @RequestMapping("/error")
    public ResponseEntity<String> handleError() {
        return new ResponseEntity<String>("customized message for any unhandled error.", HttpStatus.INTERNAL_SERVER_ERROR);
    }

    @Override
    public String getErrorPath() {
        return "/error";
    }
}
```

すると無事、デフォルトのエラーメッセージが表示されなくなった。

``` sh
$ curl http://localhost:8080/test/0000
customized message for MyCustomError.%
$ curl http://localhost:8080/test-null
customized message for any unexpected exception.%
$ curl http://localhost:8080/wrong-path
customized message for any unhandled error.%
```