Spring Boot で @ControllerAdvice, @ExceptionHandler, HandlerExceptionResolver を利用して例外を捕捉する

## 概要

- Spring Boot アプリケーションにて、コントローラークラスで発生した例外を捕捉する
- @ControllerAdvice を付与したクラスにて、@ExceptionHandler を付与したメソッドで例外クラスごとに捕捉する
- @ExceptionHandler を付与したメソッドで処理しない例外は、HandlerExceptionResolver を implements したクラスで捕捉する

## 今回の動作確認環境

- OpenJDK 11.0.2
- Spring Boot 2.1.7
- Spring Web MVC 5.1.9

### ソースコード一覧

```
├── pom.xml
└── src
    └── main
        ├── java
        │   └── com
        │       └── example
        │           └── my
        │               ├── MyApplication.java
        │               ├── MyController.java
        │               ├── MyControllerAdvice.java
        │               ├── MyException.java
        │               └── MyHandlerExceptionResolver.java
        └── resources
            └── templates
                └── myview.html
```

### MyApplication.java

Spring Boot 起動クラス。

```java
package com.example.my;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class MyApplication {

  public static void main(String[] args) {
    SpringApplication.run(MyApplication.class, args);
  }
}
```

### MyException.java

今回の動作確認用に用意したシンプルな例外クラス。

```java
package com.example.my;

public class MyException extends Exception {
}
```

### MyController.java

ルーティングを処理するコントローラークラス。
http://localhost:8080/myexception へアクセスが来たら MyException 例外を発生させる。
http://localhost:8080/exception へアクセスが来たら Exception 例外を発生させる。

```java
package com.example.my;

import org.springframework.boot.SpringApplication;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

@Controller
public class MyController {

  public static void main(String[] args) {
    SpringApplication.run(MyController.class, args);
  }

  @RequestMapping("/")
  public ModelAndView handleTop(ModelAndView mav) {
    mav.addObject("mymessage", "Hello, world.");
    mav.setViewName("myview");
    return mav;
  }

  @RequestMapping("/myexception")
  public ModelAndView handleMyException(ModelAndView mav) throws MyException {
    throw new MyException();
  }

  @RequestMapping("/exception")
  public ModelAndView handleException(ModelAndView mav) throws Exception {
    throw new Exception();
  }
}
```

### MyControllerAdvice.java

MyException 例外を捕捉するためのクラス。
クラスには @ControllerAdvice アノテーションを付与する。
例外を捕捉するためのメソッドに @ExceptionHandler アノテーションを付与し、 MyException.class を指定している。
参考: [ExceptionHandler \(Spring Framework 5\.1\.9\.RELEASE API\)](https://docs.spring.io/spring/docs/5.1.9.RELEASE/javadoc-api/org/springframework/web/bind/annotation/ExceptionHandler.html)


```java
package com.example.my;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.ModelAndView;

@ControllerAdvice
public class MyControllerAdvice {

  @ExceptionHandler({MyException.class})
  @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
  public ModelAndView handleMyException(Exception e, WebRequest req) {
    System.out.println("MyControllerAdvice#handleMyException");
    ModelAndView mav = new ModelAndView();
    mav.addObject("myerror", "MyControllerAdvice#handleMyException");
    mav.setViewName("myview");
    return mav;
  }
}
```

### MyHandlerExceptionResolver.java

@ExceptionHandler で処理しない例外を捕捉するためのクラス。
HandlerExceptionResolver インターフェースを implements する。
Bean として DI コンテナに登録するため @Component アノテーションをクラスに付与する。
参考: [HandlerExceptionResolver \(Spring Framework 5\.1\.9\.RELEASE API\)](https://docs.spring.io/spring/docs/5.1.9.RELEASE/javadoc-api/org/springframework/web/servlet/HandlerExceptionResolver.html)

```java
package com.example.my;

import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Component
public class MyHandlerExceptionResolver implements HandlerExceptionResolver {

  @Override
  public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
    System.out.println("MyHandlerExceptionResolver#resolveException");
    System.out.println(handler.getClass());
    System.out.println(handler);
    ModelAndView mav = new ModelAndView();
    mav.addObject("myerror", "MyHandlerExceptionResolver#resolveException");
    mav.setViewName("myview");
    mav.setStatus(HttpStatus.INTERNAL_SERVER_ERROR);
    return mav;
  }
}
```

### myview.html

HTML 出力用 Thymeleaf テンプレートファイル。
エラー等の情報を表示する。

```html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<div th:if="${myerror}">
    <div>Error: <span th:text="${myerror}"></span></div>
</div>
<div th:if="${mymessage}">
    <div>Message: <span th:text="${mymessage}"></span></div>
</div>
</body>
</html>
```

### pom.xml

Maven でビルドするための設定ファイル。

```xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.1.7.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
  </parent>

  <groupId>com.example</groupId>
  <artifactId>my</artifactId>
  <version>0.0.1</version>
  <name>my</name>
  <description>My project for Spring Boot</description>

  <properties>
    <java.version>11</java.version>
  </properties>

  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
    </plugins>
  </build>

</project>
```

## JAR ファイルの作成と Spring Boot の起動

Maven の mvn package コマンドで JAR ファイルを生成する。

```
$ mvn package
```

生成された JAR ファイルを指定して java コマンドで Spring Boot による Web サーバを起動する。

```
$ java -jar target/my-0.0.1.jar
```

## curl でアクセスして挙動を確認する

### http://localhost:8080/myexception

curl でアクセスする。
MyControllerAdvice クラスの handleMyException メソッドで処理されていることがわかる。

```
$ curl http://localhost:8080/myexception
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<div>
    <div>Error: <span>MyControllerAdvice#handleMyException</span></div>
</div>

</body>
</html>
```

Spring Boot サーバ側の出力。

```
MyControllerAdvice#handleMyException
```

### http://localhost:8080/exception へアクセスして挙動を見る

curl でアクセスする。
MyHandlerExceptionResolver クラスの resolveException メソッドで処理されていることがわかる。

```
$ curl http://localhost:8080/exception
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<div>
    <div>Error: <span>MyHandlerExceptionResolver#resolveException</span></div>
</div>

</body>
</html>
```

Spring Boot サーバ側の出力。

```
MyHandlerExceptionResolver#resolveException
class org.springframework.web.method.HandlerMethod
public org.springframework.web.servlet.ModelAndView com.example.my.MyController.handleException(org.springframework.web.servlet.ModelAndView) throws java.lang.Exception
```

## Spring Web MVC 内での例外処理の流れ

DispatcherServlet クラスが複数の HandlerExceptionResolver オブジェクトを処理している。

ExceptionHandlerExceptionResolver クラス、ResponseStatusExceptionResolver クラス、DefaultHandlerExceptionResolver クラスが用意されており、これらのクラスがそれぞれの役割を持って例外を処理している。

その中でも ExceptionHandlerExceptionResolver クラスは @ExceptionHandler アノテーションの付与されたメソッドを呼び出す処理をしている。

[DispatcherServlet \(Spring Framework 5\.1\.9\.RELEASE API\)](https://docs.spring.io/spring/docs/5.1.9.RELEASE/javadoc-api/org/springframework/web/servlet/DispatcherServlet.html)

> The dispatcher's exception resolution strategy can be specified via a HandlerExceptionResolver, for example mapping certain exceptions to error pages. Default are ExceptionHandlerExceptionResolver, ResponseStatusExceptionResolver, and DefaultHandlerExceptionResolver. These HandlerExceptionResolvers can be overridden through the application context. HandlerExceptionResolver can be given any bean name (they are tested by type).

Spring Web MVC の DispatcherServlet クラスのソースコードを見る。

[spring\-framework/DispatcherServlet\.java at v5\.1\.9\.RELEASE · spring\-projects/spring\-framework](https://github.com/spring-projects/spring-framework/blob/v5.1.9.RELEASE/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java#L1288)

```java
protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response,
    @Nullable Object handler, Exception ex) throws Exception {

  // Success and error responses may use different content types
  request.removeAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);

  // Check registered HandlerExceptionResolvers...
  ModelAndView exMv = null;
  if (this.handlerExceptionResolvers != null) {
    for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers) {
      exMv = resolver.resolveException(request, response, handler, ex);
      if (exMv != null) {
        break;
      }
    }
  }
```

processHandlerException メソッドの中で、this.handlerExceptionResolvers からひとつずつ HandlerExceptionResolver オブジェクトを取得し resolveException メソッドを呼び出している。
それぞれのオブジェクトの resolveException メソッドが例外に対して処理を行い ModelAndView オブジェクトを返している。

IntelliJ IDEA のデバッガで処理中のオブジェクトを見てみる。

<img width="906" alt="dispatcherservlet-1.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/139365/464b2fe5-482b-cc7b-7a19-8ab95f12a7d3.png">

<img width="944" alt="dispatcherservlet-2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/139365/a83a7dbb-cac4-78bf-1745-5a5d3a5d3eb4.png">

HandlerExceptionResolverComposite オブジェクトが ExceptionHandlerExceptionResolver、ResponseStatusExceptionResolver、DefaultHandlerExceptionResolver を管理しているのが見える。

ExceptionHandlerExceptionResolver オブジェクトは exceptionHandlerAdviceCache というインスタンス変数を持っており、ここに @ControllerAdvice アノテーションを付与したクラスのオブジェクトがある。

ExceptionHandlerExceptionResolver のソースコードを見ると、exceptionHandlerAdviceCache に登録している様子がわかる。

[spring\-framework/ExceptionHandlerExceptionResolver\.java at v5\.1\.9\.RELEASE · spring\-projects/spring\-framework](https://github.com/spring-projects/spring-framework/blob/v5.1.9.RELEASE/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerExceptionResolver.java#L273)

```java
private void initExceptionHandlerAdviceCache() {
  if (getApplicationContext() == null) {
    return;
  }

  List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
  AnnotationAwareOrderComparator.sort(adviceBeans);

  for (ControllerAdviceBean adviceBean : adviceBeans) {
    Class<?> beanType = adviceBean.getBeanType();
    if (beanType == null) {
      throw new IllegalStateException("Unresolvable type for ControllerAdviceBean: " + adviceBean);
    }
    ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(beanType);
    if (resolver.hasExceptionMappings()) {
      this.exceptionHandlerAdviceCache.put(adviceBean, resolver);
    }
    if (ResponseBodyAdvice.class.isAssignableFrom(beanType)) {
      this.responseBodyAdvice.add(adviceBean);
    }
  }
```

[spring\-framework/ExceptionHandlerMethodResolver\.java at v5\.1\.9\.RELEASE · spring\-projects/spring\-framework](https://github.com/spring-projects/spring-framework/blob/v5.1.9.RELEASE/spring-web/src/main/java/org/springframework/web/method/annotation/ExceptionHandlerMethodResolver.java#L92)

```java
private void detectAnnotationExceptionMappings(Method method, List<Class<? extends Throwable>> result) {
  ExceptionHandler ann = AnnotatedElementUtils.findMergedAnnotation(method, ExceptionHandler.class);
  Assert.state(ann != null, "No ExceptionHandler annotation");
  result.addAll(Arrays.asList(ann.value()));
}
```

## 参考資料

- [DispatcherServlet \(Spring Framework 5\.1\.9\.RELEASE API\)](https://docs.spring.io/spring/docs/5.1.9.RELEASE/javadoc-api/org/springframework/web/servlet/DispatcherServlet.html)
- [ExceptionHandler \(Spring Framework 5\.1\.9\.RELEASE API\)](https://docs.spring.io/spring/docs/5.1.9.RELEASE/javadoc-api/org/springframework/web/bind/annotation/ExceptionHandler.html)
- [HandlerExceptionResolver \(Spring Framework 5\.1\.9\.RELEASE API\)](https://docs.spring.io/spring/docs/5.1.9.RELEASE/javadoc-api/org/springframework/web/servlet/HandlerExceptionResolver.html)
- [4\.2\. 例外ハンドリング — TERASOLUNA Server Framework for Java \(5\.x\) Development Guideline 5\.5\.1\.RELEASE documentation](http://terasolunaorg.github.io/guideline/5.5.1.RELEASE/ja/ArchitectureInDetail/WebApplicationDetail/ExceptionHandling.html)
- [SpringMVCのHandlerExceptionResolverの適用順 \- Qiita](https://qiita.com/nenokido2000/items/91c39e4aa5cbd9dfacef)