Як відповісти на помилку HTTP 400 у методі Spring MVC @ResponseBody, що повертає рядок?


389

Я використовую Spring MVC для простого API JSON з таким @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);

... але я не можу його використовувати тут, оскільки тип повернення мого методу - це String, а не ResponseEntity.

Відповіді:


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і це. Це чудово працює і є простою зміною вихідного коду — дякую!
Джонік

будь ласка, будь-коли ви можете додати спеціальний заголовок, щоб перевірити всіх конструкторів ResponseEntity
Bassem Reda Zohdy

7
Що робити, якщо ви передаєте щось інше, ніж рядок назад? Як у POJO чи іншому об'єкті?
mrshickadance

11
це буде 'ResponseEntity <YourClass>'
Bassem Reda Zohdy

5
Використовуючи цей підхід, вам більше не потрібна анотація
@ResponseBody

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
Дякую! Це працює і досить просто. (У цьому випадку це можна додатково спростити, видаливши невикористані bodyта requestпарами.)
Jonik

54

Не обов'язково найбільш компактний спосіб зробити це, але досить чистий ІМО

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");
}

Для редагування ви можете використовувати @ResponseBody в методі обробника винятків, якщо використовуєте Spring 3.1+, інакше використовуйте a 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чи щось не вистачає у відповіді?
Джонік

Крім того, я не повністю зрозумів сенс визначення ще одного користувальницького типу (MyError). Це потрібно? Я використовую останню весну (3.2.2).
Джонік

1
Це працює для мене. Я використовую javax.validation.ValidationExceptionзамість цього. (Весна 3.1.4)
Джеррі Чен

Це досить корисно в ситуаціях, коли у вас є проміжний рівень між вашою службою та клієнтом, де проміжний рівень має свої можливості управління помилками. Дякуємо за цей приклад @Zutty
StormeHawke

Це має бути прийнятою відповіддю, оскільки вона переміщує код обробки винятків із звичайного потоку і приховує HttpServlet *
lilalinux

48

Я трохи змінив би реалізацію:

Спочатку я створюю UnknownMatchException:

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

Зверніть увагу на використання @ResponseStatus , який буде розпізнаний Spring's ResponseStatusExceptionResolver. Якщо виключення буде викинуто, воно створить відповідь із відповідним статусом відповіді. (Я також взяв на себе змогу змінити код статусу, 404 - Not Foundякий я вважаю більш підходящим для цього випадку використання, але ви можете дотримуватися цього, HttpStatus.BAD_REQUESTякщо хочете.)


Далі я б змінив, MatchServiceщоб мати такий підпис:

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

Нарешті, я б оновив контролер і делегував Spring MappingJackson2HttpMessageConverterдля автоматичної обробки серіалізації JSON (вона додається за замовчуванням, якщо ви додаєте Джексона до classpath та додаєте @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. Це легко досягти, додавши невелику фабрику DTO, яка повертає серійний об'єкт JSON:

@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 і я журналів: 28 липня 2015 р. 17:23:31 org.apache.cxf.interceptor.Ab AbstractFaultChainInitiatorObserver onMessage SEVERE: Під час обробки помилок сталася помилка, здайте! org.apache.cxf.interceptor.Fault
бритва

Ідеальне рішення, я хочу лише додати, що я сподіваюся, що DTO є композицією Matchта деяким іншим об'єктом.
Марко Сулла

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;
}

Перегляньте весняну документацію тут: http://docs.spring.io/spring/docs/current/spring-framework-reference/htmlsingle/#mvc-ann-annotated-exceptions .


Цей підхід дозволяє вам припинити виконання, де б ви не знаходилися в стек-трасі, без необхідності повертати "спеціальне значення", яке повинно вказати код статусу HTTP, який ви хочете повернути.
Мухаммед Гелбана

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/


Дуже хороший метод .. Замість простого рядка я вважаю за краще повернути jSON з полями errorCode та повідомлення ..
İsmail Yavuz

1
Це має бути правильна відповідь, загальний та глобальний обробник винятків із спеціальним кодом статусу та повідомленням: D
Педро Сілва,

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 доступна лише у весняній версії 5+
Етан Коннер

2

З Spring Boot я не зовсім впевнений, для чого це було необхідним (я отримав /errorрезервну копію, хоча це @ResponseBodyбуло визначено на an @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 не сприймав це як "конвертоване", тому мені довелося додати геттерів / сеттерів, а також я додав @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;
  }
}

Тепер ви можете кидати винятки з будь-якого з ваших контролерів, а також можете визначити інших обробників всередині вас клас порад.


Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.