wake-up-neo.com

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

Ich verwende Spring MVC für eine einfache JSON-API mit einem @ResponseBody-basierten 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 ist die einfachste und sauberste Methode, um 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.

351
Jonik

ä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 korrekte Anfrage

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

PDATE 1

ab frühling 4.1 gibt es hilfemethoden die in responseentity als verwendet werden könnten

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

und

return ResponseEntity.ok(json);
569

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

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

Nicht unbedingt die kompakteste Art, dies zu tun, aber ziemlich sauber, 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");
}

Bearbeiten Sie können @ResponseBody in der Ausnahmebehandlungsmethode verwenden, wenn Sie Spring 3.1+ verwenden, andernfalls verwenden Sie ModelAndView oder etwas anderes.

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

51
Zutty

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 ResponseStatusExceptionResolver erkannt wird. Wenn die Ausnahme ausgelöst wird, wird eine Antwort mit dem entsprechenden Antwortstatus erstellt. (Ich habe mir auch erlaubt, den Statuscode in 404 - Not Found zu ändern, was ich für diesen Anwendungsfall besser finde. Sie können sich aber auch an HttpStatus.BAD_REQUEST halten, wenn Sie möchten.)


Als nächstes würde ich das MatchService so ändern, dass es die folgende Signatur hat:

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

Schließlich würde ich den Controller aktualisieren und an Spring 's MappingJackson2HttpMessageConverter delegieren, um die JSON-Serialisierung automatisch zu handhaben (dies wird standardmäßig hinzugefügt, wenn Sie Jackson zum Klassenpfad hinzufügen und Ihrer Konfiguration entweder @EnableWebMvc oder <mvc:annotation-driven /> 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);
}

Es ist üblich, 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);
}
46
matsev

Hier ist ein anderer Ansatz. Erstellen Sie ein benutzerdefiniertes Exception, das mit @ResponseStatus wie folgt kommentiert ist.

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

    public NotFoundException() {
    }
}

Und werfen Sie es bei Bedarf.

@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 hier die Dokumentation zu Spring: http://docs.spring.io/spring/docs/current/spring-framework-reference/htmlsingle/#mvc-ann-annotated-exceptions .

31
danidemi

Wie in einigen Antworten erwähnt, können Sie für jeden HTTP-Status, den Sie zurückgeben möchten, eine Ausnahmeklasse erstellen. Mir gefällt 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 allgemeine Ausnahme, die einen HTTP-Status akzeptiert
  • Erstellen Sie einen Controller Advice-Ausnahmehandler

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/

17
Norris

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);
}
10
Aamir Faried

Bei Spring Boot bin ich mir nicht ganz sicher, warum dies notwendig war (ich habe den Fallback für /error erhalten, obwohl @ResponseBody für @ExceptionHandler definiert wurde), aber das Folgende an sich hat 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 ErrorMessage fehlerhaft war:

public class ErrorMessage {
    int code;

    String message;
}

JacksonMapper behandelte es nicht als "umwandelbar", also musste ich Getter/Setter hinzufügen, und ich fügte auch @JsonProperty Annotation hinzu

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 habe ich meine Nachricht wie vorgesehen erhalten

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

Sie können auch einfach throw new HttpMessageNotReadableException("error description") verwenden, um von Spring Standardfehlerbehandlung zu profitieren.

Genau wie bei diesen Standardfehlern wird jedoch kein Antworttext festgelegt.

Ich finde diese Informationen hilfreich, wenn Sie Anfragen ablehnen, die nur von Hand bearbeitet werden konnten, was möglicherweise auf eine böswillige Absicht hindeutet, da sie die Tatsache verdecken, dass die Anfrage aufgrund einer eingehenderen benutzerdefinierten Validierung und ihrer Kriterien abgelehnt wurde.

Hth, dtk

0
dtk