文字列を返すSpring MVC @ResponseBodyメソッドでHTTP 400エラーで応答する方法は?


389

シンプルなJSON APIにSpring MVCを使用し@ResponseBodyていますが、次のようなアプローチに基づいています。(私はすでにJSONを直接生成するサービス層を持っています。)

@RequestMapping(value = "/matches/{matchId}", produces = "application/json")
@ResponseBody
public String match(@PathVariable String matchId) {
    String json = matchService.getMatchJson(matchId);
    if (json == null) {
        // TODO: how to respond with e.g. 400 "bad request"?
    }
    return json;
}

質問は、与えられたシナリオで、HTTP 400エラーで応答する最も簡単でクリーンな方法は何ですか?

私は次のようなアプローチに出くわしました:

return new ResponseEntity(HttpStatus.BAD_REQUEST);

...しかし、メソッドの戻り値の型がResponseEntityではなくStringであるため、ここでは使用できません。

回答:


624

戻り値の型をに変更するとResponseEntity<>、400で以下を使用できます

return new ResponseEntity<>(HttpStatus.BAD_REQUEST);

そして正しい要求のために

return new ResponseEntity<>(json,HttpStatus.OK);

アップデート1

Spring 4.1以降、ResponseEntityには次のように使用できるヘルパーメソッドがあります。

return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(null);

そして

return ResponseEntity.ok(json);

ああ、ResponseEntityこんな風に使えます。これはうまく機能し、元のコードへの単純な変更です。ありがとうございます。
Jonik 2013

カスタムヘッダーを追加できるときはいつでも大歓迎です
。ResponseEntityの

7
文字列以外のものを渡す場合はどうなりますか?POJOまたは他のオブジェクトのように?
mrshickadance 2014年

11
'ResponseEntity <YourClass>'になります
Bassem Reda Zohdy '19

5
このアプローチを使用すると、@ ResponseBodyアノテーションは不要になります
Lu55

108

このようなものがうまくいくはずですが、もっと簡単な方法があるかどうかはわかりません:

@RequestMapping(value = "/matches/{matchId}", produces = "application/json")
@ResponseBody
public String match(@PathVariable String matchId, @RequestBody String body,
            HttpServletRequest request, HttpServletResponse response) {
    String json = matchService.getMatchJson(matchId);
    if (json == null) {
        response.setStatus( HttpServletResponse.SC_BAD_REQUEST  );
    }
    return json;
}

5
ありがとう!これも機能し、非常に簡単です。(この場合、未使用bodyrequestparamsを削除することでさらに簡略化できます。)
Jonik 2013

54

これを行うための最もコンパクトな方法とは限りませんが、非常にクリーンなIMO

if(json == null) {
    throw new BadThingException();
}
...

@ExceptionHandler(BadThingException.class)
@ResponseStatus(value = HttpStatus.BAD_REQUEST)
public @ResponseBody MyError handleException(BadThingException e) {
    return new MyError("That doesnt work");
}

編集して、Spring 3.1以降を使用している場合は例外ハンドラーメソッドで@ResponseBodyを使用できますModelAndView。それ以外の場合は、何かを使用します。

https://jira.springsource.org/browse/SPR-6902


1
これは機能していないようです。ログに長いスタックトレースがあるHTTP 500の「サーバーエラー」が生成されます ERROR org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver - Failed to invoke @ExceptionHandler method: public controller.TestController$MyError controller.TestController.handleException(controller.TestController$BadThingException) org.springframework.web.HttpMediaTypeNotAcceptableException: Could not find acceptable representation。答えに何か不足していますか?
Jonik 2013

また、さらに別のカスタムタイプ(MyError)を定義するポイントを完全には理解していませんでした。それは必要ですか?最新のSpring(3.2.2)を使用しています。
Jonik 2013

1
わたしにはできる。javax.validation.ValidationException代わりに使用します。(春3.1.4)
Jerry Chen

これは、サービスとクライアントの間に中間層があり、中間層に独自のエラー処理機能がある場合に非常に役立ちます。この例をありがとう@Zutty
StormeHawke 14

それはそれの皮のHttpServlet *通常の流れのうち、例外処理コードを移動し、これは、受け入れ答えなければなりません
lilalinux

48

実装を少し変更します。

最初に、私は作成しますUnknownMatchException

@ResponseStatus(HttpStatus.NOT_FOUND)
public class UnknownMatchException extends RuntimeException {
    public UnknownMatchException(String matchId) {
        super("Unknown match: " + matchId);
    }
}

Springによって認識される@ResponseStatusの使用に注意してくださいResponseStatusExceptionResolver。例外がスローされると、対応する応答ステータスを持つ応答が作成されます。(私はまた404 - Not Found、このユースケースに適したステータスコードを自由に変更しましたが、必要に応じてそのまま使用できHttpStatus.BAD_REQUESTます。)


次に、を変更MatchServiceして次の署名を付けます。

interface MatchService {
    public Match findMatch(String matchId);
}

最後に、私は春のようにコントローラとデリゲートを更新しますMappingJackson2HttpMessageConverter(あなたがいずれかのクラスパスにジャクソンを追加し、追加した場合、それはデフォルトで追加され、自動的にJSONのシリアライズを処理するために、@EnableWebMvcまたは<mvc:annotation-driven />あなたの設定に、参照リファレンスドキュメントを):

@RequestMapping(value = "/matches/{matchId}", produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
public Match match(@PathVariable String matchId) {
    // throws an UnknownMatchException if the matchId is not known 
    return matchService.findMatch(matchId);
}

ドメインオブジェクトをビューオブジェクトまたはDTOオブジェクトから分離することは非常に一般的です。これは、シリアル化可能なJSONオブジェクトを返す小さなDTOファクトリを追加することで簡単に実現できます。

@RequestMapping(value = "/matches/{matchId}", produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
public MatchDTO match(@PathVariable String matchId) {
    Match match = matchService.findMatch(matchId);
    return MatchDtoFactory.createDTO(match);
}

私は500とログを持っています:2015年28月5日23:31:31 org.apache.cxf.interceptor.AbstractFaultChainInitiatorObserver onMessage SEVERE:エラー処理中にエラーが発生しました、あきらめてください!org.apache.cxf.interceptor.Fault
かみそり

完璧なソリューションです。DTOがMatch他のオブジェクトで構成されていることを願っています。
Marco Sulla

32

ここに別のアプローチがあります。次のように、でException注釈を付けたカスタムを作成します@ResponseStatus

@ResponseStatus(code = HttpStatus.NOT_FOUND, reason = "Not Found")
public class NotFoundException extends Exception {

    public NotFoundException() {
    }
}

そして、必要なときにそれを投げます。

@RequestMapping(value = "/matches/{matchId}", produces = "application/json")
@ResponseBody
public String match(@PathVariable String matchId) {
    String json = matchService.getMatchJson(matchId);
    if (json == null) {
        throw new NotFoundException();
    }
    return json;
}

ここでSpringのドキュメントを確認してください:http : //docs.spring.io/spring/docs/current/spring-framework-reference/htmlsingle/#mvc-ann-annotated-exceptions


このアプローチを使用すると、スタックトレース内のどこにいても、返すHTTPステータスコードを指定する「特別な値」を返す必要なく、実行を終了できます。
Muhammad Gelbana 2017年

21

いくつかの回答で述べたように、返すHTTPステータスごとに例外クラスを作成する機能があります。各プロジェクトのステータスごとにクラスを作成する必要があるという考えは好きではありません。これが代わりに私が思いついたものです。

  • HTTPステータスを受け入れる一般的な例外を作成する
  • コントローラアドバイスの例外ハンドラを作成する

コードに行きましょう

package com.javaninja.cam.exception;

import org.springframework.http.HttpStatus;


/**
 * The exception used to return a status and a message to the calling system.
 * @author norrisshelton
 */
@SuppressWarnings("ClassWithoutNoArgConstructor")
public class ResourceException extends RuntimeException {

    private HttpStatus httpStatus = HttpStatus.INTERNAL_SERVER_ERROR;

    /**
     * Gets the HTTP status code to be returned to the calling system.
     * @return http status code.  Defaults to HttpStatus.INTERNAL_SERVER_ERROR (500).
     * @see HttpStatus
     */
    public HttpStatus getHttpStatus() {
        return httpStatus;
    }

    /**
     * Constructs a new runtime exception with the specified HttpStatus code and detail message.
     * The cause is not initialized, and may subsequently be initialized by a call to {@link #initCause}.
     * @param httpStatus the http status.  The detail message is saved for later retrieval by the {@link
     *                   #getHttpStatus()} method.
     * @param message    the detail message. The detail message is saved for later retrieval by the {@link
     *                   #getMessage()} method.
     * @see HttpStatus
     */
    public ResourceException(HttpStatus httpStatus, String message) {
        super(message);
        this.httpStatus = httpStatus;
    }
}

次に、コントローラーアドバイスクラスを作成します。

package com.javaninja.cam.spring;


import com.javaninja.cam.exception.ResourceException;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;


/**
 * Exception handler advice class for all SpringMVC controllers.
 * @author norrisshelton
 * @see org.springframework.web.bind.annotation.ControllerAdvice
 */
@org.springframework.web.bind.annotation.ControllerAdvice
public class ControllerAdvice {

    /**
     * Handles ResourceExceptions for the SpringMVC controllers.
     * @param e SpringMVC controller exception.
     * @return http response entity
     * @see ExceptionHandler
     */
    @ExceptionHandler(ResourceException.class)
    public ResponseEntity handleException(ResourceException e) {
        return ResponseEntity.status(e.getHttpStatus()).body(e.getMessage());
    }
}

使用するには

throw new ResourceException(HttpStatus.BAD_REQUEST, "My message");

http://javaninja.net/2016/06/throwing-exceptions-messages-spring-mvc-controller/


代わりに、私はのerrorCodeとメッセージフィールドにJSONを返すことを好む単純な文字列...の非常に良い方法..
イスマイル・ヤウス

1
これは正解である必要があります。カスタムのステータスコードとメッセージを備えた汎用のグローバル例外ハンドラ:D
Pedro Silva

10

私はこれを私のスプリングブートアプリケーションで使用しています

@RequestMapping(value = "/matches/{matchId}", produces = "application/json")
@ResponseBody
public ResponseEntity<?> match(@PathVariable String matchId, @RequestBody String body,
            HttpServletRequest request, HttpServletResponse response) {

    Product p;
    try {
      p = service.getProduct(request.getProductId());
    } catch(Exception ex) {
       return new ResponseEntity<String>(HttpStatus.BAD_REQUEST);
    }

    return new ResponseEntity(p, HttpStatus.OK);
}

9

最も簡単な方法は、 ResponseStatusException

    @RequestMapping(value = "/matches/{matchId}", produces = "application/json")
    @ResponseBody
    public String match(@PathVariable String matchId, @RequestBody String body) {
        String json = matchService.getMatchJson(matchId);
        if (json == null) {
            throw new ResponseStatusException(HttpStatus.NOT_FOUND);
        }
        return json;
    }

3
ベストアンサー:戻り値の型を変更したり、独自の例外を作成したりする必要はありません。また、必要に応じてResponseStatusExceptionで理由メッセージを追加できます。
ミグ

ResponseStatusExceptionはSpringバージョン5以降でのみ利用可能であることに注意してください
Ethan Conner

2

Spring Bootでは、なぜこれが必要なのかは完全にはわかりませんが(で定義され/errorていてもフォールバックを取得し@ResponseBodyました@ExceptionHandler)、次のように機能しませんでした。

@ResponseBody
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(IllegalArgumentException.class)
public ErrorMessage handleIllegalArguments(HttpServletRequest httpServletRequest, IllegalArgumentException e) {
    log.error("Illegal arguments received.", e);
    ErrorMessage errorMessage = new ErrorMessage();
    errorMessage.code = 400;
    errorMessage.message = e.getMessage();
    return errorMessage;
}

生成可能なメディアタイプがリクエスト属性として定義されていないためと思われますが、それでも例外はスローされました。

// AbstractMessageConverterMethodProcessor
@SuppressWarnings("unchecked")
protected <T> void writeWithMessageConverters(T value, MethodParameter returnType,
        ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage)
        throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {

    Class<?> valueType = getReturnValueType(value, returnType);
    Type declaredType = getGenericType(returnType);
    HttpServletRequest request = inputMessage.getServletRequest();
    List<MediaType> requestedMediaTypes = getAcceptableMediaTypes(request);
    List<MediaType> producibleMediaTypes = getProducibleMediaTypes(request, valueType, declaredType);
if (value != null && producibleMediaTypes.isEmpty()) {
        throw new IllegalArgumentException("No converter found for return value of type: " + valueType);   // <-- throws
    }

// ....

@SuppressWarnings("unchecked")
protected List<MediaType> getProducibleMediaTypes(HttpServletRequest request, Class<?> valueClass, Type declaredType) {
    Set<MediaType> mediaTypes = (Set<MediaType>) request.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);
    if (!CollectionUtils.isEmpty(mediaTypes)) {
        return new ArrayList<MediaType>(mediaTypes);

そこで追加しました。

@ResponseBody
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(IllegalArgumentException.class)
public ErrorMessage handleIllegalArguments(HttpServletRequest httpServletRequest, IllegalArgumentException e) {
    Set<MediaType> mediaTypes = new HashSet<>();
    mediaTypes.add(MediaType.APPLICATION_JSON_UTF8);
    httpServletRequest.setAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE, mediaTypes);
    log.error("Illegal arguments received.", e);
    ErrorMessage errorMessage = new ErrorMessage();
    errorMessage.code = 400;
    errorMessage.message = e.getMessage();
    return errorMessage;
}

そして、「サポートされている互換性のあるメディアタイプ」を使用できるようになりましたが、それでも機能しませんでしたErrorMessage

public class ErrorMessage {
    int code;

    String message;
}

JacksonMapperはそれを「変換可能」として処理しなかったため、getter / setterを追加する必要があり、@JsonPropertyアノテーションも追加しました

public class ErrorMessage {
    @JsonProperty("code")
    private int code;

    @JsonProperty("message")
    private String message;

    public int getCode() {
        return code;
    }

    public void setCode(int code) {
        this.code = code;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }
}

次に、意図したとおりにメッセージを受け取りました

{"code":400,"message":"An \"url\" parameter must be defined."}

0

またthrow new HttpMessageNotReadableException("error description")、Springのデフォルトのエラー処理を利用することもできます

ただし、これらのデフォルトエラーの場合と同様に、応答本文は設定されません。

これらは、手作業で合理的にのみ作成でき、悪意のある意図を示している可能性のある要求を拒否する場合に役立ちます。これは、より深いカスタム検証とその基準に基づいて要求が拒否されたという事実を覆い隠すためです。

Hth、dtk


HttpMessageNotReadableException("error description")廃止予定です。
KubaŠimonovský19年

0

別のアプローチは、例外を管理するすべてのコントローラーにハンドラーメソッドを配置する必要がない場合は、@ExceptionHandlerwith を使用し@ControllerAdviceてすべてのハンドラーを同じクラスに集中させることです。

あなたのハンドラークラス:

@ControllerAdvice
public class MyExceptionHandler extends ResponseEntityExceptionHandler {

  @ExceptionHandler(MyBadRequestException.class)
  public ResponseEntity<MyError> handleException(MyBadRequestException e) {
    return ResponseEntity
        .badRequest()
        .body(new MyError(HttpStatus.BAD_REQUEST, e.getDescription()));
  }
}

あなたのカスタム例外:

public class MyBadRequestException extends RuntimeException {

  private String description;

  public MyBadRequestException(String description) {
    this.description = description;
  }

  public String getDescription() {
    return this.description;
  }
}

これで、任意のコントローラーから例外をスローでき、アドバイスクラス内で他のハンドラーを定義できます。


弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.