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

Я использую 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.

Ответ 1

измените тип возврата на 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);

Ответ 2

Что-то вроде этого должно работать, я не уверен, есть ли более простой способ:

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

Ответ 3

Не обязательно самый компактный способ сделать это, но довольно чистый 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");
}

Редактировать, вы можете использовать @ResponseBody в методе обработчика исключений при использовании Spring 3.1+, иначе используйте ModelAndView или что-то в этом роде.

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

Ответ 4

Я бы немного изменил реализацию:

Сначала я создаю 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 /> к вашему config, см. справочные документы):

@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 factory, который возвращает сериализуемый объект 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);
}

Ответ 5

Здесь другой подход. Создайте пользовательский 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.

Ответ 6

Как уже упоминалось в некоторых ответах, существует возможность создать класс исключения для каждого 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/

Ответ 7

Я использую это в своем загрузочном приложении spring

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

Ответ 8

С 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 не обрабатывал его как "конвертируемый", поэтому мне пришлось добавлять getters/setters, а также добавил @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."}

Ответ 9

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

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

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

Hth, DTK

Ответ 10

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

fooobar.com/info/28513/...