Jak odpowiedzieć błędem HTTP 400 w metodzie Spring MVC @ResponseBody zwracającej String?

389

Używam Spring MVC do prostego interfejsu API JSON, z @ResponseBodypodejściem opartym na poniższym. (Mam już warstwę usługi produkującą bezpośrednio 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;
}

Pytanie, w danym scenariuszu, jaki jest najprostszy i najczystszy sposób odpowiedzi na błąd HTTP 400 ?

Natknąłem się na takie podejścia jak:

return new ResponseEntity(HttpStatus.BAD_REQUEST);

... ale nie mogę go tutaj użyć, ponieważ typem zwracanym przez moją metodę jest String, a nie ResponseEntity.

Jonik
źródło

Odpowiedzi:

624

zmień typ zwrotu na ResponseEntity<>, wtedy możesz użyć poniżej dla 400

return new ResponseEntity<>(HttpStatus.BAD_REQUEST);

i na prawidłowe żądanie

return new ResponseEntity<>(json,HttpStatus.OK);

AKTUALIZACJA 1

po wiośnie 4.1 istnieją metody pomocnicze w ResponseEntity mogą być używane jako

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

i

return ResponseEntity.ok(json);
Bassem Reda Zohdy
źródło
Ach, więc możesz też tak używać ResponseEntity. Działa to ładnie i jest tylko prostą zmianą oryginalnego kodu - dzięki!
Jonik
zapraszamy za każdym razem, gdy możesz dodać niestandardowy nagłówek. Sprawdź wszystkie konstruktory ResponseEntity
Bassem Reda Zohdy 27.04.2013
7
Co się stanie, jeśli przekażesz coś innego niż sznurek? Jak w POJO lub innym obiekcie?
mrshickadance
11
będzie to „ResponseEntity <Twojaklasa>”
Bassem Reda Zohdy,
5
Przy takim podejściu nie potrzebujesz już adnotacji
@ResponseBody
108

Coś takiego powinno działać, nie jestem pewien, czy istnieje prostszy sposób:

@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;
}
układacz
źródło
5
Dzięki! To działa i jest dość proste. (W tym przypadku można to jeszcze bardziej uprościć, usuwając nieużywane bodyi parametry request).
Jonik 27.04.13
54

Niekoniecznie najbardziej kompaktowy sposób na zrobienie tego, ale dość czysty 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");
}

Edytuj, możesz użyć @ResponseBody w metodzie obsługi wyjątków, jeśli używasz Spring 3.1+, w przeciwnym razie użyj ModelAndViewczegoś lub czegoś.

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

Zutty
źródło
1
Przepraszamy, to nie działa. Wywołuje „błąd serwera” HTTP 500 ze śledzeniem długiego stosu w logach: 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 representationCzy w odpowiedzi brakuje czegoś?
Jonik
Ponadto nie do końca zrozumiałem sens definiowania innego niestandardowego typu (MyError). Czy to konieczne? Używam najnowszej wiosny (3.2.2).
Jonik
1
Mi to pasuje. Używam javax.validation.ValidationExceptionzamiast tego. (Wiosna 3.1.4)
Jerry Chen
Jest to bardzo przydatne w sytuacjach, w których istnieje warstwa pośrednia między usługą a klientem, w której warstwa pośrednia ma własne możliwości obsługi błędów. Dziękuję za ten przykład @Zutty
StormeHawke
To powinna być zaakceptowana odpowiedź, ponieważ
usuwa
48

Chciałbym nieznacznie zmienić implementację:

Najpierw tworzę UnknownMatchException:

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

Zwróć uwagę na użycie @ResponseStatus , który zostanie rozpoznany przez Spring's ResponseStatusExceptionResolver. Zgłoszenie wyjątku spowoduje utworzenie odpowiedzi o odpowiednim statusie odpowiedzi. (Pozwoliłem sobie również na zmianę kodu stanu, 404 - Not Foundktóry uważam za bardziej odpowiedni dla tego przypadku użycia, ale możesz się go trzymać, HttpStatus.BAD_REQUESTjeśli chcesz).


Następnie zmieniłbym MatchServicena następujący podpis:

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

Wreszcie, chciałbym zaktualizować sterownik i delegatem na sprężyny MappingJackson2HttpMessageConverterdo obsługi serializacji JSON automatycznie (jest ona dodawana domyślnie jeśli dodać Jackson na ścieżce klas i dodać albo @EnableWebMvclub <mvc:annotation-driven />do swojej konfiguracji, patrz dokumenty referencyjne ):

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

Uwaga: bardzo często oddzielane są obiekty domeny od obiektów widoku lub obiektów DTO. Można to łatwo osiągnąć, dodając małą fabrykę DTO, która zwraca obiekt JSON możliwy do serializacji:

@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
źródło
Mam 500 i dzienniki: 28 maja 2015 17:23:31 org.apache.cxf.interceptor.AbstractFaultChainInitiatorObserver onMessage SEVERE: Wystąpił błąd podczas obsługi błędu, zrezygnuj! org.apache.cxf.interceptor.Fault
razor
Idealne rozwiązanie, chcę tylko dodać, że mam nadzieję, że DTO jest kompozycją Matchi jakimś innym przedmiotem.
Marco Sulla
32

Oto inne podejście. Utwórz niestandardową Exceptionadnotację z @ResponseStatus, podobnie jak następująca.

@ResponseStatus(code = HttpStatus.NOT_FOUND, reason = "Not Found")
public class NotFoundException extends Exception {

    public NotFoundException() {
    }
}

I w razie potrzeby wyrzuć.

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

Sprawdź dokumentację Spring tutaj: http://docs.spring.io/spring/docs/current/spring-framework-reference/htmlsingle/#mvc-ann-annotated-exceptions .

danidemi
źródło
Takie podejście pozwala zakończyć wykonywanie w dowolnym miejscu stosu śledzenia bez konieczności zwracania „wartości specjalnej”, która powinna określać kod statusu HTTP, który chcesz zwrócić.
Muhammad Gelbana
21

Jak wspomniano w niektórych odpowiedziach, istnieje możliwość utworzenia klasy wyjątku dla każdego statusu HTTP, który chcesz zwrócić. Nie podoba mi się pomysł stworzenia klasy dla każdego statusu dla każdego projektu. Oto, co wymyśliłem zamiast tego.

  • Utwórz ogólny wyjątek, który akceptuje status HTTP
  • Utwórz moduł obsługi wyjątków Porady administratora

Przejdźmy do kodu

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

Następnie tworzę klasę porad kontrolera

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

Aby go użyć

throw new ResourceException(HttpStatus.BAD_REQUEST, "My message");

http://javaninja.net/2016/06/throwing-exceptions-messages-spring-mvc-controller/

Norris
źródło
Bardzo dobra metoda .. Zamiast prostego ciągu wolę zwracać jSON z polem errorCode i komunikatem ..
İsmail Yavuz
1
To powinna być poprawna odpowiedź, ogólny i globalny moduł obsługi wyjątków z niestandardowym kodem statusu i komunikatem: D
Pedro Silva
10

Używam tego w mojej aplikacji do rozruchu wiosennego

@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);
}
Aamir Faried
źródło
9

Najłatwiej jest rzucić 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
źródło
3
Najlepsza odpowiedź: nie trzeba zmieniać typu zwrotu i nie trzeba tworzyć własnego wyjątku. Ponadto ResponseStatusException pozwala w razie potrzeby dodać komunikat przyczyny.
Migs,
Należy pamiętać, że wyjątek ResponseStatusException jest dostępny tylko w wersji wiosennej 5+
Ethan Conner
2

W przypadku Spring Boot nie jestem do końca pewien, dlaczego było to konieczne (otrzymałem /errorawarię, mimo że @ResponseBodyzostało zdefiniowane na @ExceptionHandler), ale samo w sobie nie działało:

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

Nadal zwrócił wyjątek, najwyraźniej dlatego, że nie zdefiniowano żadnych produktywnych typów mediów jako atrybutu żądania:

// 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);

Więc dodałem je.

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

I to doprowadziło mnie do posiadania „obsługiwanego kompatybilnego typu nośnika”, ale nadal nie działało, ponieważ mój ErrorMessagebył wadliwy:

public class ErrorMessage {
    int code;

    String message;
}

JacksonMapper nie traktował tego jako „konwertowalnego”, więc musiałem dodać getters / setters, a także dodałem @JsonPropertyadnotację

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

Następnie otrzymałem wiadomość zgodnie z przeznaczeniem

{"code":400,"message":"An \"url\" parameter must be defined."}
EpicPandaForce
źródło
0

Możesz także throw new HttpMessageNotReadableException("error description")skorzystać z domyślnej obsługi błędów Springa .

Jednak, podobnie jak w przypadku domyślnych błędów, nie zostanie ustawiona treść odpowiedzi.

Uważam, że są one przydatne podczas odrzucania żądań, które mogły zostać jedynie ręcznie wykonane, potencjalnie wskazując na wrogie zamiary, ponieważ ukrywają fakt, że żądanie zostało odrzucone na podstawie głębszej, niestandardowej weryfikacji i jego kryteriów.

Hth, dtk

dtk
źródło
HttpMessageNotReadableException("error description")jest przestarzałe.
Kuba Šimonovský
0

Innym sposobem jest użycie @ExceptionHandlerz @ControllerAdvicescentralizować wszystkie ładowarki w tej samej klasie, jeśli nie trzeba umieścić metod obsługi w każdym kontrolerze chcesz zarządzać wyjątek.

Twoja klasa obsługi:

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

Twój niestandardowy wyjątek:

public class MyBadRequestException extends RuntimeException {

  private String description;

  public MyBadRequestException(String description) {
    this.description = description;
  }

  public String getDescription() {
    return this.description;
  }
}

Teraz możesz zgłaszać wyjątki od dowolnego kontrolera i definiować inne moduły obsługi w swojej klasie porad.

Gonzalo
źródło