Wie kann ich mit einem HTTP 400-Fehler in einer Spring MVC @ ResponseBody-Methode antworten, die einen String zurückgibt?

389

Ich verwende Spring MVC für eine einfache JSON-API mit einem @ResponseBodybasierten Ansatz wie dem folgenden. (Ich habe bereits eine Service-Schicht, die JSON direkt produziert.)

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

Die Frage ist im gegebenen Szenario, was die einfachste und sauberste Möglichkeit ist, mit einem HTTP 400-Fehler zu antworten .

Ich bin auf Ansätze gestoßen wie:

return new ResponseEntity(HttpStatus.BAD_REQUEST);

... aber ich kann es hier nicht verwenden, da der Rückgabetyp meiner Methode String und nicht ResponseEntity ist.

Jonik
quelle

Antworten:

624

Ändern Sie Ihren Rückgabetyp in ResponseEntity<>, dann können Sie unten für 400 verwenden

return new ResponseEntity<>(HttpStatus.BAD_REQUEST);

und für die richtige Anfrage

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

UPDATE 1

Nach Spring 4.1 gibt es Hilfsmethoden in ResponseEntity, die als verwendet werden können

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

und

return ResponseEntity.ok(json);
Bassem Reda Zohdy
quelle
Ah, also kannst du das auch so benutzen ResponseEntity. Dies funktioniert gut und ist nur eine einfache Änderung des Originalcodes - danke!
Jonik
Sie sind jederzeit willkommen, wenn Sie einen benutzerdefinierten Header hinzufügen können. Überprüfen Sie alle Konstruktoren von ResponseEntity
Bassem Reda Zohdy
7
Was ist, wenn Sie etwas anderes als eine Zeichenfolge zurückgeben? Wie in einem POJO oder einem anderen Objekt?
Mrshickadance
11
es wird 'ResponseEntity <YourClass>' sein
Bassem Reda Zohdy
5
Mit diesem Ansatz brauchen Sie keine
@
108

So etwas sollte funktionieren, ich bin mir nicht sicher, ob es einen einfacheren Weg gibt oder nicht:

@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;
}
Stapler
quelle
5
Vielen Dank! Das funktioniert und ist auch ziemlich einfach. (In diesem Fall könnte es weiter vereinfacht werden, indem die nicht verwendeten bodyund requestParameter entfernt werden.)
Jonik
54

Nicht unbedingt die kompakteste Art dies zu tun, aber IMO ziemlich sauber

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

Bearbeiten Sie können @ResponseBody in der Exception-Handler-Methode verwenden, wenn Sie Spring 3.1+ verwenden. Andernfalls verwenden Sie a ModelAndView oder etwas.

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

Zutty
quelle
1
Entschuldigung, das scheint nicht zu funktionieren. Es erzeugt einen HTTP 500 "Serverfehler" mit einer langen Stapelverfolgung in Protokollen: 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 representationFehlt etwas in der Antwort?
Jonik
Außerdem habe ich den Punkt, einen weiteren benutzerdefinierten Typ (MyError) zu definieren, nicht vollständig verstanden. Ist das notwendig Ich verwende den neuesten Frühling (3.2.2).
Jonik
1
Für mich geht das. Ich benutze javax.validation.ValidationExceptionstattdessen. (Frühling 3.1.4)
Jerry Chen
Dies ist sehr nützlich in Situationen, in denen Sie eine Zwischenschicht zwischen Ihrem Dienst und dem Client haben, in der die Zwischenschicht über eigene Fehlerbehandlungsfunktionen verfügt. Vielen Dank für dieses Beispiel @Zutty
StormeHawke
Dies sollte die akzeptierte Antwort sein, da der Code für die Ausnahmebehandlung aus dem normalen Ablauf verschoben wird und HttpServlet *
lilalinux
48

Ich würde die Implementierung leicht ändern:

Zuerst erstelle ich ein UnknownMatchException:

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

Beachten Sie die Verwendung von @ResponseStatus , die von Spring's erkannt wird ResponseStatusExceptionResolver. Wenn die Ausnahme ausgelöst wird, wird eine Antwort mit dem entsprechenden Antwortstatus erstellt. (Ich habe mir auch erlaubt, den Statuscode zu ändern, 404 - Not Foundden ich für diesen Anwendungsfall besser finde, aber Sie können sich daran halten, HttpStatus.BAD_REQUESTwenn Sie möchten.)


Als nächstes würde ich das ändern MatchService, um die folgende Signatur zu haben:

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

Schließlich würde ich den Controller aktualisieren und an Spring delegieren, MappingJackson2HttpMessageConverterum die JSON-Serialisierung automatisch durchzuführen (sie wird standardmäßig hinzugefügt, wenn Sie Jackson zum Klassenpfad hinzufügen und entweder @EnableWebMvcoder <mvc:annotation-driven />zu Ihrer Konfiguration hinzufügen , siehe Referenzdokumente ):

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

Beachten Sie, dass es sehr häufig vorkommt, die Domänenobjekte von den Ansichtsobjekten oder DTO-Objekten zu trennen. Dies kann leicht erreicht werden, indem eine kleine DTO-Factory hinzugefügt wird, die das serialisierbare JSON-Objekt zurückgibt:

@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
quelle
Ich habe 500 und ich protokolliere: ay 28, 2015 5:23:31 PM org.apache.cxf.interceptor.AbstractFaultChainInitiatorObserver onMessage SEVERE: Fehler während der Fehlerbehandlung aufgetreten, aufgeben! org.apache.cxf.interceptor.Fault
Rasiermesser
Perfekte Lösung, ich möchte nur hinzufügen, dass ich hoffe, dass das DTO eine Komposition von Matchund ein anderes Objekt ist.
Marco Sulla
32

Hier ist ein anderer Ansatz. Erstellen Sie eine benutzerdefinierte ExceptionAnnotation mit @ResponseStatus, wie die folgende.

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

    public NotFoundException() {
    }
}

Und werfen Sie es, wenn nötig.

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

Lesen Sie die Spring-Dokumentation hier: http://docs.spring.io/spring/docs/current/spring-framework-reference/htmlsingle/#mvc-ann-annotated-exceptions .

danidemi
quelle
Mit diesem Ansatz können Sie die Ausführung überall dort beenden, wo Sie sich im Stacktrace befinden, ohne einen "speziellen Wert" zurückgeben zu müssen, der den HTTP-Statuscode angeben soll, den Sie zurückgeben möchten.
Muhammad Gelbana
21

Wie in einigen Antworten erwähnt, besteht die Möglichkeit, für jeden HTTP-Status, den Sie zurückgeben möchten, eine Ausnahmeklasse zu erstellen. Ich mag die Idee nicht, für jedes Projekt eine Klasse pro Status erstellen zu müssen. Hier ist, was ich mir stattdessen ausgedacht habe.

  • Erstellen Sie eine generische Ausnahme, die einen HTTP-Status akzeptiert
  • Erstellen Sie einen Controller Advice-Ausnahmebehandler

Kommen wir zum Code

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

Dann erstelle ich eine Controller-Beratungsklasse

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

Um es zu benutzen

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

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

Norris
quelle
Sehr gute Methode. Anstelle eines einfachen Strings ziehe ich es vor, einen jSON mit errorCode und Nachrichtenfeldern zurückzugeben.
İsmail Yavuz
1
Dies sollte die richtige Antwort sein, ein generischer und globaler Ausnahmebehandler mit benutzerdefiniertem Statuscode und Meldung: D
Pedro Silva
10

Ich verwende dies in meiner Spring Boot-Anwendung

@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
quelle
9

Am einfachsten ist es, einen zu werfen 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
quelle
3
Beste Antwort: Sie müssen den Rückgabetyp nicht ändern und keine eigene Ausnahme erstellen. Außerdem kann mit der ResponseStatusException bei Bedarf eine Grundmeldung hinzugefügt werden.
Migs
Es ist wichtig zu beachten, dass ResponseStatusException nur in Spring Version 5+ verfügbar ist
Ethan Conner
2

Bei Spring Boot bin ich mir nicht ganz sicher, warum dies notwendig war (ich habe den /errorFallback erhalten, obwohl er @ResponseBodyauf einem definiert wurde @ExceptionHandler), aber Folgendes hat an sich nicht funktioniert:

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

Es gab immer noch eine Ausnahme, anscheinend, weil keine produzierbaren Medientypen als Anforderungsattribut definiert wurden:

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

Also habe ich sie hinzugefügt.

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

Und das brachte mich dazu, einen "unterstützten kompatiblen Medientyp" zu haben, aber dann funktionierte es immer noch nicht, weil mein ErrorMessageFehler war:

public class ErrorMessage {
    int code;

    String message;
}

JacksonMapper hat es nicht als "konvertierbar" behandelt, daher musste ich Getter / Setter hinzufügen, und ich habe auch @JsonPropertyAnmerkungen hinzugefügt

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

Dann erhielt ich meine Nachricht wie beabsichtigt

{"code":400,"message":"An \"url\" parameter must be defined."}
EpicPandaForce
quelle
0

Sie können auch nur throw new HttpMessageNotReadableException("error description")von der Standardfehlerbehandlung von Spring profitieren .

Genau wie bei diesen Standardfehlern wird jedoch kein Antworttext festgelegt.

Ich finde diese nützlich, wenn Anfragen abgelehnt werden, die vernünftigerweise nur von Hand hergestellt werden konnten, was möglicherweise auf eine böswillige Absicht hinweist, da sie die Tatsache verschleiern, dass die Anfrage aufgrund einer tieferen, benutzerdefinierten Validierung und ihrer Kriterien abgelehnt wurde.

Hth, dtk

dtk
quelle
HttpMessageNotReadableException("error description")ist veraltet.
Kuba Šimonovský
0

Ein anderer Ansatz ist die Verwendung @ExceptionHandlermit@ControllerAdvice allen Ihren Handler in der gleichen Klasse zu zentralisieren, wenn nicht Sie die Handler Methoden in jedem Controller setzen müssen Sie eine Ausnahme verwalten möchten.

Ihre Handlerklasse:

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

Ihre benutzerdefinierte Ausnahme:

public class MyBadRequestException extends RuntimeException {

  private String description;

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

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

Jetzt können Sie Ausnahmen von jedem Ihrer Controller auslösen und andere Handler in Ihrer Beratungsklasse definieren.

Gonzalo
quelle
-1

Ich denke, dieser Thread hat tatsächlich die einfachste und sauberste Lösung, die die von Spring bereitgestellten JSON-Martialing-Tools nicht opfert:

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

Ryan S.
quelle