java boot - ¿Cómo responder con el error HTTP 400 en un método Spring MVC @ResponseBody que devuelve String?




return status (9)

Estoy usando Spring MVC para una API JSON simple, con @ResponseBody enfoque basado en @ResponseBody como el siguiente. (Ya tengo una capa de servicio que produce JSON directamente).

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

La pregunta es, en el escenario dado, ¿cuál es la forma más simple y limpia de responder con un error HTTP 400 ?

Me encontré con enfoques como:

return new ResponseEntity(HttpStatus.BAD_REQUEST);

... pero no puedo usarlo aquí porque el tipo de retorno de mi método es String, no ResponseEntity.


Answers

Creo que este hilo en realidad tiene la solución más fácil y limpia, que no sacrifica las herramientas de marcaje JSON que proporciona Spring:

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


Aquí hay un enfoque diferente. Cree una Exception personalizada anotada con @ResponseStatus , como la siguiente.

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

    public NotFoundException() {
    }
}

Y tirarlo cuando sea necesario.

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

Consulte la documentación de Spring aquí: http://docs.spring.io/spring/docs/current/spring-framework-reference/htmlsingle/#mvc-ann-annotated-exceptions .


Cambiaría ligeramente la implementación:

Primero, creo una UnknownMatchException :

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

Tenga en cuenta el uso de @ResponseStatus , que será reconocido por el ResponseStatusExceptionResolver de Spring. Si se lanza la excepción, se creará una respuesta con el estado de respuesta correspondiente. (También me tomé la libertad de cambiar el código de estado a 404 - Not Found que me parece más apropiado para este caso de uso, pero puede atenerse a HttpStatus.BAD_REQUEST si lo desea).

A continuación, cambiaría el MatchService para tener la siguiente firma:

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

Finalmente, actualizaría el controlador y delegaría a MappingJackson2HttpMessageConverter de Spring para manejar la serialización JSON automáticamente (se agrega de manera predeterminada si agrega Jackson a la ruta de @EnableWebMvc y agrega @EnableWebMvc o <mvc:annotation-driven /> @EnableWebMvc <mvc:annotation-driven /> a su configuración, vea los documentos de referencia ):

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

Tenga en cuenta que es muy común separar los objetos de dominio de los objetos de vista u objetos DTO. Esto se puede lograr fácilmente agregando una pequeña fábrica DTO que devuelve el objeto JSON serializable:

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

cambie su tipo de devolución a ResponseEntity<> , luego puede usar a continuación para 400

return new ResponseEntity<>(HttpStatus.BAD_REQUEST);

y para la correcta solicitud

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

ACTUALIZACIÓN 1

después de la primavera 4.1 hay métodos de ayuda en ResponseEntity que podrían usarse como

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

y

return ResponseEntity.ok(json);

No necesariamente la forma más compacta de hacer esto, pero IMO bastante limpio

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

Edite puede usar @ResponseBody en el método del controlador de excepciones si usa Spring 3.1+, de lo contrario use un ModelAndView o algo así.

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


Algo como esto debería funcionar, no estoy seguro de si hay o no una forma más simple:

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

Con Spring Boot, no estoy completamente seguro de por qué fue necesario (obtuve el /error @ResponseBody pesar de que @ResponseBody se definió en un @ExceptionHandler ), pero lo siguiente en sí no funcionó:

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

Todavía lanzó una excepción, aparentemente porque no se definieron tipos de medios que se pudieran producir como un atributo de solicitud:

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

Así que los agregué.

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

Y esto me ayudó a tener un "tipo de medio compatible compatible", pero aún así no funcionó, porque mi ErrorMessage era defectuoso:

public class ErrorMessage {
    int code;

    String message;
}

JacksonMapper no lo manejó como "convertible", así que tuve que agregar getters / setters, y también @JsonProperty anotación @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;
    }
}

Entonces recibí mi mensaje como estaba previsto

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

Como se mencionó en algunas respuestas, existe la posibilidad de crear una clase de excepción para cada estado HTTP que desea devolver. No me gusta la idea de tener que crear una clase por estado para cada proyecto. Aquí está lo que se me ocurrió en su lugar.

  • Cree una excepción genérica que acepte un estado HTTP
  • Crear un controlador de excepciones de consejos de controlador

Vamos al código

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

Entonces creo una clase de consejos de controlador.

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

Para usarlo

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

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


Debe usar el objeto TransactionTemplate para administrar la transacción de forma imperativa:

transactionTemplate.execute(new TransactionCallbackWithoutResult() {
        @Override
        protected void doInTransactionWithoutResult(TransactionStatus status) {
            em.createNativeQuery("TRUNCATE TABLE MyTable).executeUpdate();
        }
    });

Para crear TransactionTemplate solo use PlatformTransactionManager inyectado:

transactionTemplate = new TransactionTemplate(platformTransactionManager);

Y si quieres usar una nueva transacción solo invoca

transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);




java spring spring-mvc http-error