SpringによるRESTのエラー処理

SpringでのRESTのエラー処理

1. 概要

この記事では、how to implement Exception Handling with Spring for a REST APIについて説明します。 また、歴史的な概要を少し把握し、さまざまなバージョンで導入された新しいオプションを確認します。

Before Spring 3.2, the two main approaches to handling exceptions in a Spring MVC application were: HandlerExceptionResolver or the @ExceptionHandler annotation.これらの両方にいくつかの明らかな欠点があります。

Since 3.2 we’ve had the @ControllerAdvice annotationは、前の2つのソリューションの制限に対処し、アプリケーション全体で統一された例外処理を促進します。

さて、Spring 5 introduces the ResponseStatusException class:RESTAPIでの基本的なエラー処理の高速な方法。

これらすべてに共通することが1つあります。それは、separation of concernsを非常にうまく処理することです。 アプリは通常、例外をスローして、何らかの種類の例外を示すことができます。例外は個別に処理されます。

最後に、Spring Bootがテーブルにもたらすものと、ニーズに合わせてSpringBootを構成する方法を説明します。

参考文献:

REST APIのカスタムエラーメッセージ処理

Springを使用してREST APIのグローバル例外ハンドラーを実装します。

Spring Data REST Validatorsのガイド

Spring Data REST Validatorsのクイックで実用的なガイド

Spring MVCカスタム検証

カスタム検証アノテーションを作成し、Spring MVCで使用する方法を学びます。

2. 解決策1–コントローラーレベル@ExceptionHandler

最初のソリューションは@Controllerレベルで機能します。例外を処理するメソッドを定義し、@ExceptionHandlerで注釈を付けます。

public class FooController{

    //...
    @ExceptionHandler({ CustomException1.class, CustomException2.class })
    public void handleException() {
        //
    }
}

このアプローチには大きな欠点があります–the @ExceptionHandler annotated method is only active for that particular Controllerであり、アプリケーション全体に対してグローバルではありません。 もちろん、これをすべてのコントローラーに追加すると、一般的な例外処理メカニズムにはあまり適していません。

all Controllers extend a Base Controller classを使用することでこの制限を回避できますが、これは、何らかの理由でこれが不可能なアプリケーションでは問題になる可能性があります。 たとえば、コントローラーは、別のjarにあるか、直接変更できない別の基本クラスから既に拡張されているか、またはそれ自体が直接変更できない場合があります。

次に、例外処理の問題を解決する別の方法を見ていきます。これはグローバルであり、コントローラーなどの既存のアーティファクトへの変更が含まれていません。

3. 解決策2 –The HandlerExceptionResolver

2番目の解決策は、HandlerExceptionResolverを定義することです。これにより、アプリケーションによってスローされた例外が解決されます。 また、REST APIにuniform exception handling mechanismを実装することもできます。

カスタムリゾルバーを使用する前に、既存の実装について見ていきましょう。

3.1. ExceptionHandlerExceptionResolver

このリゾルバはSpring3.1で導入され、DispatcherServletでデフォルトで有効になっています。 これは実際には、前に示した@ExceptionHandlerメカニズムがどのように機能するかのコアコンポーネントです。

3.2. DefaultHandlerExceptionResolver

このリゾルバはSpring3.0で導入され、DispatcherServletでデフォルトで有効になっています。 これは、標準のSpring例外を対応するHTTPステータスコード、つまりクライアントエラー–4xxおよびサーバーエラー–5xxステータスコードに解決するために使用されます。 処理するSpringExceptionのHere’s the full listと、それらがステータスコードにどのようにマップされるか。

応答のステータスコードは適切に設定されますが、1limitation is that it doesn’t set anything to the body of the Responseです。 また、REST APIの場合-ステータスコードはクライアントに提示するのに十分な情報ではありません-アプリケーションが障害に関する追加情報を提供できるように、応答にも本文が必要です。

これは、ビューの解像度を構成し、ModelAndViewを使用してエラーコンテンツをレンダリングすることで解決できますが、解決策は明らかに最適ではありません。 そのため、Spring 3.2では、後のセクションで説明するより優れたオプションが導入されました。

3.3. ResponseStatusExceptionResolver

このリゾルバはSpring3.0でも導入され、DispatcherServletでデフォルトで有効になっています。 その主な責任は、カスタム例外で使用可能な@ResponseStatusアノテーションを使用し、これらの例外をHTTPステータスコードにマップすることです。

このようなカスタム例外は次のようになります。

@ResponseStatus(value = HttpStatus.NOT_FOUND)
public class MyResourceNotFoundException extends RuntimeException {
    public MyResourceNotFoundException() {
        super();
    }
    public MyResourceNotFoundException(String message, Throwable cause) {
        super(message, cause);
    }
    public MyResourceNotFoundException(String message) {
        super(message);
    }
    public MyResourceNotFoundException(Throwable cause) {
        super(cause);
    }
}

DefaultHandlerExceptionResolverと同じように、このリゾルバーは応答の本文を処理する方法に制限があります。応答にステータスコードをマップしますが、本文はnull.のままです。

3.4. SimpleMappingExceptionResolverおよび AnnotationMethodHandlerExceptionResolver

SimpleMappingExceptionResolverはかなり前から存在しています。これは、古いSpring MVCモデルからのものであり、not very relevant for a REST Serviceです。 基本的に、例外クラス名をビュー名にマッピングするために使用します。

AnnotationMethodHandlerExceptionResolverは、@ExceptionHandlerアノテーションを介して例外を処理するためにSpring 3.0で導入されましたが、Spring 3.2ではExceptionHandlerExceptionResolverによって非推奨になりました。

3.5. カスタムHandlerExceptionResolver

DefaultHandlerExceptionResolverResponseStatusExceptionResolverの組み合わせは、SpringRESTfulサービスに優れたエラー処理メカニズムを提供するのに大いに役立ちます。 欠点は、前述のように、no control over the body of the responseです。

理想的には、クライアントが要求した形式に応じて(Acceptヘッダーを介して)JSONまたはXMLのいずれかを出力できるようにする必要があります。

これだけで、a new, custom exception resolverを作成することが正当化されます。

@Component
public class RestResponseStatusExceptionResolver extends AbstractHandlerExceptionResolver {

    @Override
    protected ModelAndView doResolveException(
      HttpServletRequest request,
      HttpServletResponse response,
      Object handler,
      Exception ex) {
        try {
            if (ex instanceof IllegalArgumentException) {
                return handleIllegalArgument((IllegalArgumentException) ex, response, handler);
            }
            ...
        } catch (Exception handlerException) {
            logger.warn("Handling of [" + ex.getClass().getName() + "]
              resulted in Exception", handlerException);
        }
        return null;
    }

    private ModelAndView
      handleIllegalArgument(IllegalArgumentException ex, HttpServletResponse response)
      throws IOException {
        response.sendError(HttpServletResponse.SC_CONFLICT);
        String accept = request.getHeader(HttpHeaders.ACCEPT);
        ...
        return new ModelAndView();
    }
}

ここで注意すべき詳細の1つは、request自体にアクセスできるため、クライアントから送信されたAcceptヘッダーの値を検討できることです。

たとえば、クライアントがapplication/jsonを要求した場合、エラー状態の場合は、application/jsonでエンコードされた応答本文を返すようにします。

他の重要な実装の詳細は、we return a ModelAndView – this is the body of the responseであり、必要なものは何でも設定できます。

このアプローチは、Spring RESTサービスのエラー処理のための一貫性があり、簡単に構成可能なメカニズムです。 ただし、制限があります。低レベルのHtttpServletResponseと相互作用し、ModelAndViewを使用する古いMVCモデルに適合します。したがって、まだ改善の余地があります。

4. 解決策3 –@ControllerAdvice

Spring 3.2はa global @ExceptionHandler with the @ControllerAdvice annotationのサポートをもたらします。 これにより、古いMVCモデルから脱却し、@ExceptionHandlerの型安全性と柔軟性とともにResponseEntityを利用するメカニズムが可能になります。

@ControllerAdvice
public class RestResponseEntityExceptionHandler
  extends ResponseEntityExceptionHandler {

    @ExceptionHandler(value
      = { IllegalArgumentException.class, IllegalStateException.class })
    protected ResponseEntity handleConflict(
      RuntimeException ex, WebRequest request) {
        String bodyOfResponse = "This should be application specific";
        return handleExceptionInternal(ex, bodyOfResponse,
          new HttpHeaders(), HttpStatus.CONFLICT, request);
    }
}


@ControllerAdviceアノテーションを使用すると、consolidate our multiple, scattered @ExceptionHandlers from before into a single, global error handling componentを実行できます。

実際のメカニズムは非常にシンプルですが、非常に柔軟です。 それは私たちに与えます:

  • 応答の本文とステータスコードを完全に制御

  • 複数の例外を同じメソッドにマッピングし、一緒に処理します。

  • 新しいRESTfulResposeEntity応答をうまく利用します__

ここで覚えておくべきことの1つは、match the exceptions declared with @ExceptionHandler with the exception used as the argument of the methodです。 これらが一致しない場合、コンパイラは文句を言いません–理由はなく、Springも文句を言いません。

ただし、実行時に例外が実際にスローされると、the exception resolving mechanism will fail with

java.lang.IllegalStateException: No suitable resolver for argument [0] [type=...]
HandlerMethod details: ...

5. 解決策4 –ResponseStatusException(春5以降)

Spring 5ではResponseStatusExceptionクラスが導入されました。 HttpStatus、オプションでreasonおよびcauseを提供するインスタンスを作成できます。

@GetMapping(value = "/{id}")
public Foo findById(@PathVariable("id") Long id, HttpServletResponse response) {
    try {
        Foo resourceById = RestPreconditions.checkFound(service.findOne(id));

        eventPublisher.publishEvent(new SingleResourceRetrievedEvent(this, response));
        return resourceById;
     }
    catch (MyResourceNotFoundException exc) {
         throw new ResponseStatusException(
           HttpStatus.NOT_FOUND, "Foo Not Found", exc);
    }
}

ResponseStatusExceptionを使用する利点は何ですか?

  • プロトタイピングに最適:基本的なソリューションを非常に迅速に実装できます

  • 1つのタイプ、複数のステータスコード:1つの例外タイプは、複数の異なる応答につながる可能性があります。 This reduces tight coupling compared to the @ExceptionHandler

  • 多くのカスタム例外クラスを作成する必要はありません

  • 例外はプログラムで作成できるため、More control over exception handling

そして、トレードオフはどうですか?

  • 例外処理の統一された方法はありません。グローバルなアプローチを提供する@ControllerAdviceとは対照的に、アプリケーション全体の規則を適用することはより困難です。

  • コードの複製:複数のコントローラーでコードを複製している場合があります

また、1つのアプリケーション内でさまざまなアプローチを組み合わせることができることにも注意してください。

For example, we can implement a @ControllerAdvice globally, but also ResponseStatusExceptions locally.ただし、注意が必要です。同じ例外を複数の方法で処理できる場合、予期しない動作が発生する可能性があります。 可能な規則は、特定の種類の例外を常に1つの方法で処理することです。

詳細およびその他の例については、tutorial on ResponseStatusExceptionを参照してください。

6. SpringSecurityで拒否されたアクセスを処理する

アクセス拒否は、認証されたユーザーが、アクセスするのに十分な権限がないリソースにアクセスしようとしたときに発生します。

6.1. MVC –カスタムエラーページ

まず、ソリューションのMVCスタイルを見て、AccessDeniedのエラーページをカスタマイズする方法を見てみましょう。

XML構成:


    
    ...
    

Javaの構成:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
        .antMatchers("/admin/*").hasAnyRole("ROLE_ADMIN")
        ...
        .and()
        .exceptionHandling().accessDeniedPage("/my-error-page");
}

ユーザーが十分な権限を持たずにリソースにアクセスしようとすると、「/my-error-page」にリダイレクトされます。

6.2. カスタムAccessDeniedHandler

次に、カスタムAccessDeniedHandlerの記述方法を見てみましょう。

@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {

    @Override
    public void handle
      (HttpServletRequest request, HttpServletResponse response, AccessDeniedException ex)
      throws IOException, ServletException {
        response.sendRedirect("/my-error-page");
    }
}

それでは、XML Configurationを使用して構成しましょう。


    
    ...
    

または、Java構成を使用:

@Autowired
private CustomAccessDeniedHandler accessDeniedHandler;

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
        .antMatchers("/admin/*").hasAnyRole("ROLE_ADMIN")
        ...
        .and()
        .exceptionHandling().accessDeniedHandler(accessDeniedHandler)
}

方法に注意してください–CustomAccessDeniedHandlerでは、カスタムエラーメッセージをリダイレクトまたは表示することで、必要に応じて応答をカスタマイズできます。

6.3. RESTとメソッドレベルのセキュリティ

最後に、メソッドレベルのセキュリティ@PreAuthorize@PostAuthorize、および@Secureのアクセス拒否を処理する方法を見てみましょう。

もちろん、前に説明したグローバル例外処理メカニズムを使用して、AccessDeniedExceptionも処理します。

@ControllerAdvice
public class RestResponseEntityExceptionHandler
  extends ResponseEntityExceptionHandler {

    @ExceptionHandler({ AccessDeniedException.class })
    public ResponseEntity handleAccessDeniedException(
      Exception ex, WebRequest request) {
        return new ResponseEntity(
          "Access denied message here", new HttpHeaders(), HttpStatus.FORBIDDEN);
    }

    ...
}





7. スプリングブートサポート

Spring Bootは、適切な方法でエラーを処理するためのErrorController実装を提供します。

一言で言えば、ブラウザのフォールバックエラーページ(別名ホワイトラベルエラーページ)と、RESTfulな非HTMLリクエストのJSONレスポンスを提供します。

{
    "timestamp": "2019-01-17T16:12:45.977+0000",
    "status": 500,
    "error": "Internal Server Error",
    "message": "Error processing the request!",
    "path": "/my-endpoint-with-exceptions"
}

通常どおり、Spring Bootではこれらの機能をプロパティで構成できます。

  • server.error.whitelabel.enabled:を使用して、ホワイトラベルエラーページを無効にし、サーブレットコンテナに依存してHTMLエラーメッセージを提供できます。

  • server.error.include-stacktrace: always valueを指定すると、HTMLとJSONの両方のデフォルト応答にスタックトレースが含まれます

これらのプロパティとは別に、we can provide our own view-resolver mapping for /error, overriding the Whitelabel Page.

コンテキストにErrorAttributes beanを含めることで、応答に表示する属性をカスタマイズすることもできます。 Spring Bootが提供するDefaultErrorAttributesクラスを拡張して、作業を簡単にすることができます。

@Component
public class MyCustomErrorAttributes extends DefaultErrorAttributes {

    @Override
    public Map getErrorAttributes(
      WebRequest webRequest, boolean includeStackTrace) {
        Map errorAttributes =
          super.getErrorAttributes(webRequest, includeStackTrace);
        errorAttributes.put("locale", webRequest.getLocale()
            .toString());
        errorAttributes.remove("error");

        //...

        return errorAttributes;
    }
}

さらに進んで、アプリケーションが特定のコンテンツタイプのエラーを処理する方法を定義(またはオーバーライド)する場合は、ErrorController beanを登録できます。

繰り返しになりますが、Spring Bootが提供するデフォルトのBasicErrorController を利用して支援することができます。

たとえば、アプリケーションがXMLエンドポイントでトリガーされたエラーを処理する方法をカスタマイズしたいとします。 @RequestMappingを使用してパブリックメソッドを定義し、それがapplication/xmlメディアタイプを生成することを示すだけです。

@Component
public class MyErrorController extends BasicErrorController {

    public MyErrorController(ErrorAttributes errorAttributes) {
        super(errorAttributes, new ErrorProperties());
    }

    @RequestMapping(produces = MediaType.APPLICATION_XML_VALUE)
    public ResponseEntity> xmlError(HttpServletRequest request) {

    // ...

    }
}

8. 結論

このチュートリアルでは、SpringでREST APIの例外処理メカニズムを実装するいくつかの方法について説明しました。古いメカニズムから始めて、Spring 3.2サポートを4.xおよび5.xに続けます。

いつものように、この記事で紹介されているコードはover on Githubで利用できます。

Spring Security関連のコードについては、spring-security-restモジュールを確認できます。