エラーハンドリングを共通化しよう
SpringBoot2を使っていAPI開発をしていると、システム内部で発生したException単位で共通処理を簡単に実装する事が出来ます
実装するには@ControllerAdviceを使います。
今回は独自定義したエラークラスを使った共通ハンドリングの実装方法をご紹介しようと思います。
手順
目的
今回はHttpStatus単位のエラークラスと、その中のエラーメッセージ種別を定義して実装していこうと思います。
この粒度で用意しておけば、筆者的には汎用性はそれなりに高いかな?と思った次第です。
独自エラークラスの作成
まずはハンドリングしたいエラークラスを作成しましょう。
ちなみに、独自のエラークラスでなくてもハンドリングは可能ですが、今回は分かりやすくするために実装します。
package tech.blogenist.service.account.api.infrastructure.error.exeption;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import tech.blogenist.service.account.api.infrastructure.message.ErrorMessageConst;
@Builder
@AllArgsConstructor(
staticName = "of" )
@Data
public class ResourceNotFoundException extends RuntimeException
{
private ErrorMessageConst.ErrorType errorType;
public ResourceNotFoundException(Throwable cause, ErrorMessageConst.ErrorType errorType)
{
super( cause );
this.errorType = errorType;
}
}
メッセージ用の定数クラスを追加
次に、呼び出し元でThrowする際にメッセージを出し分けるために渡す定数クラスを用意します。
値はプロパティファイルのキーを定義しています。
package tech.blogenist.service.account.api.infrastructure.message.error;
import lombok.AllArgsConstructor;
import lombok.Getter;
public class ErrorMessageConst
{
public static final String JOIN_FORMAT = "%s.%s";
public static final String CODE = "code";
public static final String MESSAGE = "message";
@AllArgsConstructor
public enum ErrorType
{
RESOURCE_NOT_FOUND_COMMON( "resourceNotFound.common" );
@Getter
private String key;
public String code()
{
return String.format( JOIN_FORMAT, key, CODE );
}
public String message()
{
return String.format( JOIN_FORMAT, key, MESSAGE );
}
}
}
Adviceクラスを実装
次に@ControllerAdviceを付与したクラスを実装します。
package tech.blogenist.service.account.api.infrastructure.error.advice;
import java.io.IOException;
import java.time.OffsetDateTime;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
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.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import lombok.extern.slf4j.Slf4j;
import tech.blogenist.service.account.api.infrastructure.error.attributes.CustomErrorAttributes;
import tech.blogenist.service.account.api.infrastructure.error.exeption.ResourceNotFoundException;
import tech.blogenist.service.account.api.infrastructure.message.ErrorMessageConst;
@Slf4j
@ControllerAdvice // アノテーションを付与
public class ResourceNotFoundExceptionAdvice
{
@Autowired
private MessageSource messageSource; // プロパティファイルから値を取るため
@ExceptionHandler(
value = ResourceNotFoundException.class )// ハンドリングしたエラークラスを指定
@ResponseStatus( HttpStatus.NOT_FOUND )
public @ResponseBody Map< String, Object > resourceNotFoundException(
HttpServletResponse response,
ResourceNotFoundException exception,
Locale locale )
throws IOException
{
log.warn( this.getClass().getSimpleName(), exception );
Map< String, Object > errors = new HashMap<>();
String code = messageSource
.getMessage(
exception.getErrorType().code(),
null,
locale );
String message = messageSource
.getMessage(
exception.getErrorType().message(),
null,
locale );
errors.put( MessageSourceConst.CODE, code );
errors.put( MessageSourceConst.MESSAGE, message );
errors.put( CustomErrorAttributes.TIME_STAMP, OffsetDateTime.now() );
return errors;
}
}
error.propertiesの用意
次にエラーメッセージを定義するプロパティファイルを用意しましょう。
resourceNotFound.common.code=404_00001
resourceNotFound.common.message=共通エラーメッセージ
なお、プロパティファイルの読み込みについては以下の記事でご紹介しているので、ベースパスにだけ今回のmessages/error階層を追加しておいてください。
関連記事
【SpringBoot2】メッセージプロパティファイルをロジック内部から呼び出すための設定【他言語化対応】
IT
Java
Mac
Windows
アプリケーションデータソースを変更
次に、JPAのID指定の単一取得の戻り値をOptional型に変えましょう。
そうすることで、呼び出し元のサービス層でのハンドリングが簡単になります。
package tech.blogenist.service.account.datasource.db.current_account.account_infomations;
import java.util.List;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import tech.blogenist.service.account.entity.db.account.CurrentAccountInfomationsTableEntity;
import tech.blogenist.service.account.value.account.AccountIdValue;
@Repository
public interface CurrentAccountInfomationsRepository
extends JpaRepository< CurrentAccountInfomationsTableEntity, AccountIdValue >
{
public List< CurrentAccountInfomationsTableEntity > findAll();
public Optional< CurrentAccountInfomationsTableEntity > findByAccountId( AccountIdValue id );// Optional型に変更
}
サービスクラスにハンドリングを追加
最後に、サービス層でリソースデータが取得出来なかった場合のハンドリング処理を追加しましょう。
package tech.blogenist.service.account.api.service.account.detail;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import tech.blogenist.service.account.api.infrastructure.error.exeption.ResourceNotFoundException;
import tech.blogenist.service.account.api.infrastructure.message.error.ErrorMessageConst;
import tech.blogenist.service.account.api.model.account.detail.response.AccountDetailResponseModel;
import tech.blogenist.service.account.datasource.db.current_account.account_infomations.CurrentAccountInfomationsRepository;
import tech.blogenist.service.account.entity.db.account.CurrentAccountInfomationsTableEntity;
import tech.blogenist.service.account.value.account.AccountIdValue;
@Service
public class AccountDetailService
{
@Autowired
private CurrentAccountInfomationsRepository currentAccountInfomationsRepository;
public AccountDetailResponseModel findBy( AccountIdValue id )
{
CurrentAccountInfomationsTableEntity entity = currentAccountInfomationsRepository
.findByAccountId( id )
.orElseThrow( // ハンドリングを追加
() -> ResourceNotFoundException.of( ErrorMessageConst.ErrorType.RESOURCE_NOT_FOUND_COMMON ) ); // エラーメッセージ種別を渡す
return AccountDetailResponseModel.builder()
.accountId( entity.getAccountId() )
.accountInfomationId( entity.getAccountInfomations().getId() )
.name( entity.getAccountInfomations().getName() )
.age( entity.getAccountInfomations().getAge() )
.displayName( entity.getAccountInfomations().getDisplayName() )
.sex( entity.getAccountInfomations().getSexType().getName() )
.build();
}
}
確認
では、これで実際にリクエストを投げてみましょう。
リソースが存在する場合
{
"accountId": {
"value": 10001
},
"accountInfomationId": {
"value": 20002
},
"name": {
"value": "山田 太郎"
},
"age": {
"value": 19
},
"sex": {
"value": "男性"
},
"displayName": {
"value": "山田 ローカル 太郎(新)"
}
}
リソースが存在しない場合
{
"code": "404_00001",
"message": "共通エラーメッセージ",
"timestamp": "2019-04-27T10:16:04.396+09:00"
}
もちろん、サーバー側にはしっかりとログが出ているので調査も問題ありません。
[local][WARN ] 2019-04-29 10:18:19.196 t.b.s.a.a.i.e.a.ResourceNotFoundExceptionAdvice - ResourceNotFoundExceptionAdvice
tech.blogenist.service.account.api.infrastructure.error.exeption.ResourceNotFoundException: null
at tech.blogenist.service.account.api.infrastructure.error.exeption.ResourceNotFoundException.of(ResourceNotFoundException.java:9)
at tech.blogenist.service.account.api.service.account.detail.AccountDetailService.lambda$0(AccountDetailService.java:24)
at java.util.Optional.orElseThrow(Optional.java:290)
at tech.blogenist.service.account.api.service.account.detail.AccountDetailService.findBy(AccountDetailService.java:23)
at tech.blogenist.service.account.api.controller.account.detail.AccountDetailController.detail(AccountDetailController.java:28)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:189)
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:138)
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:102)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:895)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:800)
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1038)
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:942)
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1005)
at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:897)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:634)
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:882)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:741)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:99)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:92)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:93)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:200)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:200)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96)
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:490)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:139)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343)
at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:408)
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:66)
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:834)
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1415)
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.lang.Thread.run(Thread.java:745)
[local][WARN ] 2019-04-29 10:18:19.227 o.s.w.s.m.m.a.ExceptionHandlerExceptionResolver - Resolved [ResourceNotFoundException(errorType=RESOURCE_NOT_FOUND_COMMON)]
参考
現場至上主義 Spring Boot2 徹底活用
【ケース販売】日清 カップヌードル シーフードヌードル 75g×20個
終わりに
以上のように、SpringBootの標準アノテーションを利用することで簡単にエラーハンドリングを実装することが出来ました。
エラー時のハンドリングルールをしっかり行っておけば、開発や調査の効率化が狙えるので、ぜひ試してみてください♪
