Featured image of post SpringBoot2のControllerAdviceでエラーハンドリング【共通化】

SpringBoot2のControllerAdviceでエラーハンドリング【共通化】

SpringBoot2の@ControllerAdviceでエラーハンドリングを共通化する方法。HttpStatus単位の独自エラークラス、メッセージ用の定数クラス、Adviceクラス、error.propertiesを用意し、リソースの有無で動作確認します♪

1759文字

エラーハンドリングを共通化しよう

SpringBoot2を使っていAPI開発をしていると、システム内部で発生したException単位共通処理を簡単に実装する事が出来ます

実装するには@ControllerAdviceを使います。

今回は独自定義したエラークラスを使った共通ハンドリングの実装方法をご紹介しようと思います。

手順

目的

今回はHttpStatus単位のエラークラスと、その中のエラーメッセージ種別を定義して実装していこうと思います。

この粒度で用意しておけば、筆者的には汎用性はそれなりに高いかな?と思った次第です。

独自エラークラスの作成

まずはハンドリングしたいエラークラスを作成しましょう。
ちなみに、独自のエラークラスでなくてもハンドリングは可能ですが、今回は分かりやすくするために実装します。

ResourceNotFoundException
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する際にメッセージを出し分けるために渡す定数クラスを用意します。

値はプロパティファイルのキーを定義しています。

ErrorMessageConst
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を付与したクラスを実装します。

ResourceNotFoundExceptionAdvice
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の用意

次にエラーメッセージを定義するプロパティファイルを用意しましょう。

src/main/resources/messages/error/config.properties
resourceNotFound.common.code=404_00001
resourceNotFound.common.message=共通エラーメッセージ

なお、プロパティファイルの読み込みについては以下の記事でご紹介しているので、ベースパスにだけ今回のmessages/error階層を追加しておいてください。

関連記事 【SpringBoot2】メッセージプロパティファイルをロジック内部から呼び出すための設定【他言語化対応】 2019/04/27 1074文字 約3分で読めます IT Java Mac Windows アプリケーション

データソースを変更

次に、JPAのID指定の単一取得の戻り値をOptional型に変えましょう。
そうすることで、呼び出し元のサービス層でのハンドリングが簡単になります。

CurrentAccountInfomationsRepository
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型に変更
}

サービスクラスにハンドリングを追加

最後に、サービス層でリソースデータが取得出来なかった場合のハンドリング処理を追加しましょう。

AccountDetailService
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();
    }
}

確認

では、これで実際にリクエストを投げてみましょう。

リソースが存在する場合

/v1/accounts/10001/
{
    "accountId": {
        "value": 10001
    },
    "accountInfomationId": {
        "value": 20002
    },
    "name": {
        "value": "山田 太郎"
    },
    "age": {
        "value": 19
    },
    "sex": {
        "value": "男性"
    },
    "displayName": {
        "value": "山田 ローカル 太郎(新)"
    }
}

リソースが存在しない場合

/v1/accounts/999/
{
    "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の標準アノテーションを利用することで簡単にエラーハンドリングを実装することが出来ました。

エラー時のハンドリングルールをしっかり行っておけば、開発や調査の効率化が狙えるので、ぜひ試してみてください♪