Jak odpowiedzieć na Błąd HTTP 400 W metodzie Spring MVC @ResponseBody zwracającej ciąg znaków?

Używam Spring MVC dla prostego JSON API, z @ResponseBody podejście oparte jak poniżej. (Mam już warstwę usług 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 brzmi, w danym scenariuszu, Jaki jest najprostszy, najczystszy sposób na odpowiedź błędem HTTP 400 ?

Natknąłem się na podejścia typu:

return new ResponseEntity(HttpStatus.BAD_REQUEST);

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

Author: abhi, 2013-04-26

9 answers

Zmień typ powrotu na ResponseEntity<>, Następnie możesz użyć poniżej dla 400

return new ResponseEntity<>(HttpStatus.BAD_REQUEST);

I dla poprawnego żądania

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

UPDATE 1

Po wiośnie 4.1 istnieją metody pomocnicze w odpowiedziach, które można wykorzystać jako

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

I

return ResponseEntity.ok(json);
 488
Author: Bassem Reda Zohdy,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-04-06 11:16:19

Coś takiego powinno działać, nie jestem pewien czy jest jakiś 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;
}
 86
Author: stacker,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-04-27 09:14:43

Niekoniecznie najbardziej kompaktowy sposób na zrobienie tego, ale całkiem 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 ModelAndView lub czegoś takiego.

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

 47
Author: Zutty,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-04-26 09:25:59

Zmieniłbym nieco 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óre będą rozpoznawane przez Springa ResponseStatusExceptionResolver. Jeśli wyjątek zostanie wyrzucony, utworzy on odpowiedź o odpowiednim statusie odpowiedzi. (Pozwoliłem sobie również zmienić kod statusu na 404 - Not Found, który uważam za bardziej odpowiedni dla tego przypadku użycia, ale możesz trzymać się HttpStatus.BAD_REQUEST, jeśli chcesz.)


Następnie zmieniłbym MatchService posiadać następujący podpis:

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

Na koniec chciałbym zaktualizować kontroler i delegować do MappingJackson2HttpMessageConverter Springa, aby automatycznie obsługiwać serializację JSON (jest to dodawane domyślnie, jeśli dodasz Jacksona do ścieżki klas i dodasz @EnableWebMvc lub <mvc:annotation-driven /> do swojej konfiguracji, zobacz 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 oddziela się obiekty domeny od obiektów widoku lub obiektów DTO. Można to łatwo osiągnąć poprzez dodanie małego DTO fabryka, która zwraca serializowalny obiekt 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);
}
 41
Author: matsev,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-10-11 21:18:49

Oto inne podejście. Utwórz niestandardową Exception z adnotacją @ResponseStatus, jak w poniższym.

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

    public NotFoundException() {
    }
}

I rzucać w razie potrzeby.

@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ę wiosenną tutaj: http://docs.spring.io/spring/docs/current/spring-framework-reference/htmlsingle/#mvc-ann-annotated-exceptions.

 29
Author: danidemi,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-08-05 16:20:39

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

  • Tworzenie ogólnego wyjątku, który akceptuje status HTTP
  • Utwórz kontroler Advice exception handler

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ę Controller advice

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

To użyj go

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

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

 12
Author: Norris,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-11-10 00:38:06

Używam tego w mojej aplikacji spring boot

@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
Author: Aamir Faried,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-03-24 18:10:16

W przypadku Spring Boot nie jestem do końca pewien, dlaczego było to konieczne (dostałem /error fallback, mimo że @ResponseBody został zdefiniowany 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 wyrzucił wyjątek, najwyraźniej dlatego, że żadne typy nośników producible nie zostały zdefiniowane jako atrybut żą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 je dodałem.

@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 dzięki temu udało mi się uzyskać "obsługiwany kompatybilny typ nośnika" , ale potem nadal nie działało, ponieważ mój ErrorMessage był wadliwy:

public class ErrorMessage {
    int code;

    String message;
}

JacksonMapper nie obsłużył tego jako "konwertowalny" , więc musiałem dodać gettery/settery, a także dodałem @JsonProperty adnotację

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

Wtedy otrzymałem wiadomość zgodnie z przeznaczeniem

{"code":400,"message":"An \"url\" parameter must be defined."}
 0
Author: EpicPandaForce,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-12-01 15:41:05

Myślę, że ten wątek rzeczywiście ma najprostsze, najczystsze rozwiązanie, które nie poświęca narzędzi JSON martialing, które dostarcza Spring:

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

 -2
Author: Ryan S,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:18:25