Anker PowerCor
旅行には必須の大容量モバイルバッテリー!
【最新機種】GoPro hero11 Black
最新機種でVlogの思い出を撮影しよう!
[ノースフェイス] THE NORTH FACE メンズ アウター マウンテンライトジャケット
防水暴風で耐久性抜群なので旅行で大活躍です!
モンスターエナジー 355ml×24本 [エナジードリンク]
脳を活性化させるにはこれ!
BANDAI SPIRITS ULTIMAGEAR 遊戯王 千年パズル 1/1スケール
もう一人の僕を呼び覚ませ!!
MOFT X 【新型 ミニマム版】 iPhone対応 スマホスタンド
Amazon一番人気のスマホスタンド!カード類も収納出来てかさ張らないのでオススメです!
サンディスク microSD 128GB
スマホからSwitchまで使える大容量MicroSDカード!
スポンサーリンク
目次
エラーハンドリングを共通化しよう
SpringBoot2を使っていAPI開発をしていると、システム内部で発生したException単位で共通処理を簡単に実装する事が出来ます
実装するには@ControllerAdvice
を使います。
今回は独自定義したエラークラスを使った共通ハンドリングの実装方法をご紹介しようと思います。
手順
目的
今回はHttpStatus単位のエラークラスと、その中のエラーメッセージ種別を定義して実装していこうと思います。
この粒度で用意しておけば、筆者的には汎用性はそれなりに高いかな?と思った次第です。
独自エラークラスの作成
まずはハンドリングしたいエラークラスを作成しましょう。
ちなみに、独自のエラークラスでなくてもハンドリングは可能ですが、今回は分かりやすくするために実装します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
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する際にメッセージを出し分けるために渡す定数クラスを用意します。
値はプロパティファイルのキーを定義しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
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
を付与したクラスを実装します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
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の用意
次にエラーメッセージを定義するプロパティファイルを用意しましょう。
1 2 |
resourceNotFound.common.code=404_00001 resourceNotFound.common.message=共通エラーメッセージ |
なお、プロパティファイルの読み込みについては以下の記事でご紹介しているので、ベースパスにだけ今回のmessages/error
階層を追加しておいてください。
データソースを変更
次に、JPAのID指定の単一取得の戻り値をOptional
型に変えましょう。
そうすることで、呼び出し元のサービス層でのハンドリングが簡単になります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
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型に変更 } |
サービスクラスにハンドリングを追加
最後に、サービス層でリソースデータが取得出来なかった場合のハンドリング処理を追加しましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
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(); } } |
確認
では、これで実際にリクエストを投げてみましょう。
リソースが存在する場合
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
{ "accountId": { "value": 10001 }, "accountInfomationId": { "value": 20002 }, "name": { "value": "山田 太郎" }, "age": { "value": 19 }, "sex": { "value": "男性" }, "displayName": { "value": "山田 ローカル 太郎(新)" } } |
リソースが存在しない場合
1 2 3 4 5 |
{ "code": "404_00001", "message": "共通エラーメッセージ", "timestamp": "2019-04-27T10:16:04.396+09:00" } |
もちろん、サーバー側にはしっかりとログが出ているので調査も問題ありません。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
[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の標準アノテーションを利用することで簡単にエラーハンドリングを実装することが出来ました。
エラー時のハンドリングルールをしっかり行っておけば、開発や調査の効率化が狙えるので、ぜひ試してみてください♪