私は単純な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なので、ここでは使用できません。
戻り値の型をResponseEntity<>
に変更すると、400の場合は以下を使用できます。
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
そして正しい要求のために
return new ResponseEntity<>(json,HttpStatus.OK);
アップデート1
4.1以降のResponseEntityにはヘルパーメソッドがあります。
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(null);
そして
return ResponseEntity.ok(json);
このようなことでうまくいくはずですが、もっと簡単な方法があるかどうかはわかりません。
@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;
}
必ずしもこれを行うための最もコンパクトな方法ではありませんが、非常にきれいな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
などを使用します。
実装を少し変更します。
まず、UnknownMatchException
を作成します:
@ResponseStatus(HttpStatus.NOT_FOUND)
public class UnknownMatchException extends RuntimeException {
public UnknownMatchException(String matchId) {
super("Unknown match: " + matchId);
}
}
@ ResponseStatus の使用に注意してください。これは、SpringのResponseStatusExceptionResolver
によって認識されます。例外がスローされると、対応する応答ステータスを持つ応答が作成されます。 (また、ステータスコードを404 - Not Found
に変更する自由を取りましたが、この使用例により適していると思いますが、必要に応じてHttpStatus.BAD_REQUEST
に固執することができます。)
次に、MatchService
を次のシグネチャを持つように変更します。
interface MatchService {
public Match findMatch(String matchId);
}
最後に、コントローラーを更新してSpringのMappingJackson2HttpMessageConverter
に委任し、JSONシリアル化を自動的に処理します(Jacksonをクラスパスに追加し、@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);
}
これは別のアプローチです。次のように、@ResponseStatus
でアノテーションを付けたカスタムのException
を作成します。
@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ステータスごとに例外クラスを作成する機能があります。プロジェクトごとにステータスごとにクラスを作成しなければならないという考えは嫌いです。これは私が代わりに思いついたものです。
コードに行きましょう
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/ /
私は春のブートアプリケーションでこれを使っています
@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);
}
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はそれを "convertable"として扱わなかった、それで私はゲッター/セッターを加えなければならなかった、そしてまた私は@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."}
また、Springの デフォルトのエラー処理 を活用するために、単に throw new HttpMessageNotReadableException("error description")
にすることもできます。
ただし、これらのデフォルトエラーの場合と同様に、応答本文は設定されません。
これらは、より深いカスタム検証とその基準に基づいてリクエストが拒否されたという事実を曖昧にするため、手作業でしか合理的に作成できなかったリクエストを拒否するときに役立ちます。
Hth、dtk