概要
- 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 起動クラス。
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
今回の動作確認用に用意したシンプルな例外クラス。
package com.example.my;
public class MyException extends Exception {
}
MyController.java
ルーティングを処理するコントローラークラス。
http://localhost:8080/myexception へアクセスが来たら MyException 例外を発生させる。
http://localhost:8080/exception へアクセスが来たら Exception 例外を発生させる。
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)
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)
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 テンプレートファイル。
エラー等の情報を表示する。
<!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 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)
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
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 のデバッガで処理中のオブジェクトを見てみる。
HandlerExceptionResolverComposite オブジェクトが ExceptionHandlerExceptionResolver、ResponseStatusExceptionResolver、DefaultHandlerExceptionResolver を管理しているのが見える。
ExceptionHandlerExceptionResolver オブジェクトは exceptionHandlerAdviceCache というインスタンス変数を持っており、ここに @ControllerAdvice アノテーションを付与したクラスのオブジェクトがある。
ExceptionHandlerExceptionResolver のソースコードを見ると、exceptionHandlerAdviceCache に登録している様子がわかる。
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);
}
}
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)
- ExceptionHandler (Spring Framework 5.1.9.RELEASE API)
- HandlerExceptionResolver (Spring Framework 5.1.9.RELEASE API)
- 4.2. 例外ハンドリング — TERASOLUNA Server Framework for Java (5.x) Development Guideline 5.5.1.RELEASE documentation
- SpringMVCのHandlerExceptionResolverの適用順 - Qiita