1. 概要

このチュートリアルでは、 REST APIのSpringを使用して例外処理を実装する方法を説明します。また、歴史的な概要を少し説明し、さまざまなバージョンで導入された新しいオプションを確認します。

Spring 3.2以前は、Spring MVCアプリケーションで例外を処理するための2つの主なアプローチは、HandlerExceptionResolverまたは@ExceptionHandlerアノテーションでした。どちらにも明らかな欠点があります。

3.2以降、@ ControllerAdviceアノテーションを使用して、前の2つのソリューションの制限に対処し、アプリケーション全体で統一された例外処理を促進しています。

現在、 Spring 5ではResponseStatusExceptionクラスが導入されています。これは、RESTAPIでの基本的なエラー処理の高速な方法です。

これらすべてに共通することが1つあります。それは、関心の分離を非常にうまく処理することです。 アプリは通常、ある種の障害を示すために例外をスローすることができ、それはその後個別に処理されます。

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

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

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

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

このアプローチには大きな欠点があります。@ExceptionHandler注釈付きメソッドは、その特定のController に対してのみアクティブであり、アプリケーション全体に対してグローバルにアクティブになることはありません。 もちろん、これをすべてのコントローラーに追加すると、一般的な例外処理メカニズムにはあまり適していません。

すべてのコントローラーにベースコントローラークラスを拡張させることで、この制限を回避できます。

ただし、このソリューションは、何らかの理由でそれが不可能なアプリケーションでは問題になる可能性があります。 たとえば、コントローラーは、別のjar内にあるか、直接変更できない別の基本クラスから既に拡張されている場合や、それ自体が直接変更できない場合があります。

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

3. 解決策2: HandlerExceptionResolver

2番目の解決策は、 HandlerExceptionResolver。を定義することです。これにより、アプリケーションによってスローされた例外が解決されます。 また、RESTAPIに均一な例外処理メカニズムを実装することもできます。

カスタムリゾルバーを使用する前に、既存の実装を確認しましょう。

3.1. ExceptionHandlerExceptionResolver

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

3.2. DefaultHandlerExceptionResolver

このリゾルバーはSpring3.0で導入され、DispatcherServletでデフォルトで有効になっています。

これは、標準のSpring例外を対応する HTTPステータスコード、つまりクライアントエラー4xxおよびサーバーエラー5xxステータスコードに解決するために使用されます。 これが処理するSpring例外の完全なリストとそれらがステータスコードにどのようにマッピングされるかです。

応答のステータスコードは適切に設定されますが、の制限の1つは、応答の本文に何も設定されないことです。そして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のままです。[X211X ]

3.4. カスタムHandlerExceptionResolver

DefaultHandlerExceptionResolverResponseStatusExceptionResolverの組み合わせは、SpringRESTfulサービスに優れたエラー処理メカニズムを提供するのに大いに役立ちます。 欠点は、前述のように、応答の本体を制御できないことです。

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

これだけで、新しいカスタム例外リゾルバーを作成することが正当化されます。

@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でエンコードされた応答本文を返すようにします。 ]。

他の重要な実装の詳細は、 ModelAndViewを返すことです—これは応答の本体であり、必要なものを設定できるようになります。

このアプローチは、SpringRESTサービスのエラー処理のための一貫性のある簡単に構成可能なメカニズムです。

ただし、制限があります。低レベルの HtttpServletResponse と相互作用し、 ModelAndView を使用する古いMVCモデルに適合するため、まだ改善の余地があります。

4. 解決策3: @ControllerAdvice

Spring 3.2は、@ControllerAdviceアノテーションを使用したグローバル@ExceptionHandlerのサポートを提供します。

これにより、古いMVCモデルから脱却し、 ResponseEntity と、@ExceptionHandlerの型安全性と柔軟性を利用するメカニズムが可能になります。

@ControllerAdvice
public class RestResponseEntityExceptionHandler 
  extends ResponseEntityExceptionHandler {

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

@ControllerAdvice アノテーションを使用すると、以前の複数の分散した@ExceptionHandlerを単一のグローバルエラー処理コンポーネントに統合できます。

実際のメカニズムは非常に単純ですが、非常に柔軟です。

  • これにより、応答の本文とステータスコードを完全に制御できます。
  • 一緒に処理される、同じメソッドへのいくつかの例外のマッピングを提供します。
  • 新しいRESTfulResposeEntity応答をうまく利用します。

ここで覚えておくべきことの1つは、@ExceptionHandlerで宣言された例外をメソッドの引数として使用される例外と一致させることです。

これらが一致しない場合、コンパイラは文句を言いません—理由はありません—そしてSpringも文句を言いません。

ただし、実行時に例外が実際にスローされると、例外解決メカニズムはで失敗します。

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

5. 解決策4: ResponseStatusException (Spring 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つの例外タイプは、複数の異なる応答につながる可能性があります。 これにより、@ExceptionHandlerと比較して密結合が減少します。
  • 多くのカスタム例外クラスを作成する必要はありません。
  • 例外はプログラムで作成できるため、は例外処理をより細かく制御できます。

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

  • 例外処理の統一された方法はありません。グローバルなアプローチを提供する@ControllerAdviceとは対照的に、アプリケーション全体の規則を適用することはより困難です。
  • コードの重複:複数のコントローラーでコードを複製している場合があります。

また、1つのアプリケーション内でさまざまなアプローチを組み合わせることが可能であることに注意する必要があります。

たとえば、 @ControllerAdvice をグローバルに実装できますが、ResponseStatusExceptionをローカルに実装することもできます。

ただし、注意する必要があります。同じ例外を複数の方法で処理できる場合、驚くべき動作に気付く可能性があります。 考えられる規則は、特定の種類の例外を常に1つの方法で処理することです。

詳細とその他の例については、ResponseStatusExceptionに関するチュートリアルを参照してください。

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

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

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

最後に、メソッドレベルのセキュリティアノテーション( @PreAuthorize @PostAuthorize 、および @Secure)によってスローされたAccessDenied例外を処理する方法を見てみましょう。

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

@ControllerAdvice
public class RestResponseEntityExceptionHandler 
  extends ResponseEntityExceptionHandler {

    @ExceptionHandler({ AccessDeniedException.class })
    public ResponseEntity<Object> handleAccessDeniedException(
      Exception ex, WebRequest request) {
        return new ResponseEntity<Object>(
          "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 常に値を使用。 HTMLとJSONの両方のデフォルト応答にスタックトレースが含まれています
  • server.error.include-message:バージョン2.3以降、Spring Bootは、機密情報の漏洩を防ぐために、応答のmessageフィールドを非表示にします。 このプロパティを常に値で使用して有効にすることができます

これらのプロパティとは別に、ホワイトラベルページをオーバーライドして、/errorの独自のビューリゾルバーマッピングを提供できます。

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

@Component
public class MyCustomErrorAttributes extends DefaultErrorAttributes {

    @Override
    public Map<String, Object> getErrorAttributes(
      WebRequest webRequest, ErrorAttributeOptions options) {
        Map<String, Object> errorAttributes = 
          super.getErrorAttributes(webRequest, options);
        errorAttributes.put("locale", webRequest.getLocale()
            .toString());
        errorAttributes.remove("error");

        //...

        return errorAttributes;
    }
}

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

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

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

@Component
public class MyErrorController extends BasicErrorController {

    public MyErrorController(
      ErrorAttributes errorAttributes, ServerProperties serverProperties) {
        super(errorAttributes, serverProperties.getError());
    }

    @RequestMapping(produces = MediaType.APPLICATION_XML_VALUE)
    public ResponseEntity<Map<String, Object>> xmlError(HttpServletRequest request) {
        
    // ...

    }
}

注:ここでは、[X34X] ServerProperties beanにバインドされている、プロジェクトで定義されている可能性のあるserver.error。*ブートプロパティに引き続き依存しています。

8. 結論

この記事では、SpringでREST APIの例外処理メカニズムを実装するいくつかの方法について説明しました。古いメカニズムから始めて、Spring3.2のサポートを継続して4.xと5.xに移行します。

いつものように、この記事で紹介するコードは、GitHubから入手できます。

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