Как ответить ошибкой HTTP 400 в методе Spring MVC @ResponseBody, возвращающем String?

389

Я использую Spring MVC для простого JSON API с @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.

Jonik
источник

Ответы:

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);
Bassem Reda Zohdy
источник
Ах, так ты тоже можешь использовать ResponseEntityэто. Это прекрасно работает и представляет собой простое изменение исходного кода - спасибо!
Jonik
Добро пожаловать в любое время, когда вы можете добавить свой собственный заголовок. Проверьте все конструкторы 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и requestparams.)
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

Zutty
источник
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вместо этого. (Весна 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'омResponseStatusExceptionResolver . Если выброшено исключение, оно создаст ответ с соответствующим статусом ответа. (Я также позволил себе сменить код состояния, 404 - Not Foundкоторый я считаю более подходящим для этого варианта использования, но вы можете придерживаться его, HttpStatus.BAD_REQUESTесли хотите.)


Далее я бы изменил, MatchServiceчтобы иметь следующую подпись:

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

Наконец, я хотел бы обновить контроллер и делегат в Spring 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. Этого легко достичь, добавив небольшую фабрику 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);
}
matsev
источник
У меня 500 журналов и я: ay 28, 2015 17:23:31 org.apache.cxf.interceptor.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;
}

Ознакомьтесь с документацией Spring здесь: http://docs.spring.io/spring/docs/current/spring-framework-reference/htmlsingle/#mvc-ann-annotated-exceptions .

danidemi
источник
Этот подход позволяет вам прекратить выполнение, где бы вы ни находились в трассировке стека, без необходимости возвращать «специальное значение», которое должно указывать код состояния HTTP, который вы хотите вернуть.
Мухаммед Гелбана
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/

Норрис
источник
Очень хороший метод .. Вместо простой строки я предпочитаю возвращать jSON с errorCode и полями сообщений ..
Исмаил Явуз
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;
    }
MevlütÖzdemir
источник
3
Лучший ответ: нет необходимости менять тип возвращаемого значения и не нужно создавать собственное исключение. Кроме того, ResponseStatusException позволяет добавить сообщение о причине, если это необходимо.
Migs
Важно отметить, что ResponseStatusException доступна только в версии Spring 5+
Этан Коннер
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 не считал его «конвертируемым», поэтому мне пришлось добавить геттеры / сеттеры, и я также добавил @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."}
EpicPandaForce
источник
0

Вы также можете просто throw new HttpMessageNotReadableException("error description")извлечь выгоду из обработки ошибок Spring по умолчанию .

Однако, как и в случае с этими ошибками по умолчанию, тело ответа не будет установлено.

Я нахожу это полезным при отклонении запросов, которые могли быть разумно обработаны только вручную, что может указывать на злонамеренное намерение, поскольку они скрывают тот факт, что запрос был отклонен на основе более глубокой пользовательской проверки и ее критериев.

Hth, DTK

ДТК
источник
HttpMessageNotReadableException("error description")устарел.
Куба Шимоновский
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;
  }
}

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

Гонсало
источник
-1

Я думаю, что этот поток на самом деле имеет самое простое и чистое решение, которое не жертвует инструментами боевых действий JSON, которые предоставляет Spring:

https://stackoverflow.com/a/16986372/1278921

Райан С
источник