String을 반환하는 Spring MVC @ResponseBody 메소드에서 HTTP 400 오류로 응답하는 방법은 무엇입니까?


389

나는 함께 간단한 JSON API를위한 스프링 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

스프링 4.1 이후 ResponseEntity에 도우미 메소드가 다음과 같이 사용될 수 있습니다.

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

return ResponseEntity.ok(json);

아, 그래서 ResponseEntity이것도 사용할 수 있습니다 . 이것은 훌륭하게 작동하며 원본 코드의 간단한 변경입니다. 감사합니다!
Jonik

당신은 당신이 ResponseEntity의 모든 생성자 확인도 사용자 정의 헤더를 추가 할 수 있습니다 언제든지 환영
바셈 레다 Zohdy

7
문자열이 아닌 다른 것을 전달하면 어떻게 되나요? POJO 또는 다른 개체에서와 같이?
mrshickadance

11
그것은 것 'ResponseEntity <YourClass>'
바셈 레다 Zohdy

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
감사! 이것은 작동하며 매우 간단합니다. (이 경우 미사용 bodyrequest매개 변수 를 제거하여 더 단순화 할 수 있습니다 .)
Jonik

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

또한 또 다른 사용자 정의 유형 (MyError)을 정의하는 요점을 완전히 이해하지 못했습니다. 그게 필요한가요? 최신 Spring (3.2.2)을 사용하고 있습니다.
Jonik

1
그것은 나를 위해 작동합니다. javax.validation.ValidationException대신에 사용 합니다. (Spring 3.1.4)
Jerry Chen

이는 서비스와 클라이언트 사이에 중간 계층에 자체 오류 처리 기능이있는 중간 계층이있는 경우에 매우 유용합니다. 이 예제에 감사합니다 @Zutty
StormeHawke

예외 처리 코드를 일반 흐름에서 제외하고 HttpServlet *을 숨기므로 허용되는 답변이어야합니다.
lilalinux

48

구현을 약간 변경합니다.

먼저 UnknownMatchException:

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

@ResponseStatus 의 사용에 주목하십시오 ResponseStatusExceptionResolver. Spring의에 의해 인식됩니다 . 예외가 발생하면 해당 응답 상태로 응답이 작성됩니다. (또한 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);
}

500 및 i 로그가 있습니다. 2015 년 2 월 28 일 5시 23 분 31 초 org.apache.cxf.interceptor.AbstractFaultChainInitiatorObserver onMessage 심각 : 오류 처리 중 오류가 발생했습니다. 포기하십시오! 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

21

일부 답변에서 언급했듯이 반환하려는 각 HTTP 상태에 대해 예외 클래스를 만드는 기능이 있습니다. 각 프로젝트마다 상태별로 클래스를 만들어야한다는 생각이 마음에 들지 않습니다. 대신 내가 생각해 낸 것이 있습니다.

  • HTTP 상태를 승인하는 일반 예외를 작성하십시오.
  • Controller Advice 예외 핸들러 작성

코드를 보자

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을 반환하는 것을 선호합니다.
İsmail Yavuz

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을 통해 필요한 경우 이유 메시지를 추가 할 수 있습니다.
Migs

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ý

0

또 다른 방법은 사용하는 것입니다 @ExceptionHandler함께 @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;
  }
}

이제 컨트롤러 중 하나에서 예외를 발생시킬 수 있으며 advice 클래스 내부에 다른 핸들러를 정의 할 수 있습니다.


당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.