繁体   English   中英

如何在返回 String 的 Spring MVC @ResponseBody 方法中响应 HTTP 400 错误

[英]How to respond with an HTTP 400 error in a Spring MVC @ResponseBody method returning String

我将 Spring MVC 用于简单的 JSON API,使用基于@ResponseBody的方法,如下所示。 (我已经有一个直接生成 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;
}

在给定的场景中,响应 HTTP 400 错误的最简单、最干净的方法是什么?

我确实遇到过以下方法:

return new ResponseEntity(HttpStatus.BAD_REQUEST);

...但我不能在这里使用它,因为我的方法的返回类型是字符串,而不是 ResponseEntity。

将您的返回类型更改为ResponseEntity<> ,然后您可以在下面使用 400

return new ResponseEntity<>(HttpStatus.BAD_REQUEST);

和正确的请求

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

更新 1

在 spring 4.1 之后,ResponseEntity 中的辅助方法可以用作

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

return ResponseEntity.ok(json);

这样的事情应该可行,我不确定是否有更简单的方法:

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

不一定是最紧凑的方式,但非常干净 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");
}

编辑如果使用 Spring 3.1+,您可以在异常处理程序方法中使用 @ResponseBody,否则使用ModelAndView或其他东西。

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

我会稍微改变一下实现:

首先,我创建一个UnknownMatchException

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

注意@ResponseStatus的使用,它会被 Spring 的ResponseStatusExceptionResolver识别。 如果抛出异常,它将创建一个具有相应响应状态的响应。 (我还冒昧地将状态代码更改为404 - Not Found ,我认为这更适合此用例,但如果您愿意,可以坚持使用HttpStatus.BAD_REQUEST 。)


接下来,我将更改MatchService以具有以下签名:

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

最后,我将更新控制器并委托给 Spring 的MappingJackson2HttpMessageConverter以自动处理 JSON 序列化(如果您将 Jackson 添加到类路径并将@EnableWebMvc<mvc:annotation-driven />到您的配置中,则默认情况下会添加它,请参阅参考文档):

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

请注意,将域对象与视图对象或 DTO 对象分开是很常见的。 这可以通过添加一个返回可序列化 JSON 对象的小型 DTO 工厂轻松实现:

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

这是一种不同的方法。 创建一个用@ResponseStatus注释的自定义Exception ,如下所示。

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

    public NotFoundException() {
    }
}

并在需要时扔掉它。

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

在此处查看 Spring 文档: http : //docs.spring.io/spring/docs/current/spring-framework-reference/htmlsingle/#mvc-ann-annotated-exceptions

正如一些答案中提到的,可以为您想要返回的每个 HTTP 状态创建一个异常类。 我不喜欢必须为每个项目为每个状态创建一个类的想法。 这是我想出来的。

  • 创建一个接受 HTTP 状态的通用异常
  • 创建 Controller Advice 异常处理程序

让我们来看看代码

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

然后我创建了一个控制器建议类

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

使用它

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

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

最简单的方法是抛出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;
    }

我在 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);
}

随着春天的引导,我不完全知道为什么,这是必要的(我得到了/error后备即使@ResponseBody是上定义@ExceptionHandler ),但其本身以下没有工作:

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

它仍然抛出异常,显然是因为没有可生产的媒体类型被定义为请求属性:

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

所以我添加了它们。

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

这让我获得了“支持的兼容媒体类型”,但它仍然无法正常工作,因为我的ErrorMessage有问题:

public class ErrorMessage {
    int code;

    String message;
}

JacksonMapper 没有将它作为“可转换”处理,所以我不得不添加 getter/setter,我还添加了@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;
    }
}

然后我按预期收到了我的消息

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

另一种方法是使用@ExceptionHandler@ControllerAdvice将所有处理程序集中在同一个类中,否则您必须将处理程序方法放在要管理异常的每个控制器中。

您的处理程序类:

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

您的自定义异常:

public class MyBadRequestException extends RuntimeException {

  private String description;

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

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

现在,您可以从任何控制器抛出异常,并且可以在通知类中定义其他处理程序。

您也可以throw new HttpMessageNotReadableException("error description")以从 Spring 的默认错误处理中受益。

但是,就像这些默认错误的情况一样,不会设置响应正文。

我发现这些在拒绝只能合理地手工制作的请求时很有用,可能表明恶意意图,因为它们掩盖了请求被拒绝基于更深层次的自定义验证及其标准的事实。

hth, dtk

带有状态码的自定义响应

像那样:

class Response<T>(
        val timestamp: String = DateTimeFormatter
                .ofPattern("yyyy-MM-dd HH:mm:ss.SSSSSS")
                .withZone(ZoneOffset.UTC)
                .format(Instant.now()),
        val code: Int = ResultCode.SUCCESS.code,
        val message: String? = ResultCode.SUCCESS.message,
        val status: HttpStatus = HttpStatus.OK,
        val error: String? = "",
        val token: String? = null,
        val data: T? = null
) : : ResponseEntity<Response.CustomResponseBody>(status) {

data class CustomResponseBody(
        val timestamp: String = DateTimeFormatter
                .ofPattern("yyyy-MM-dd HH:mm:ss.SSSSSS")
                .withZone(ZoneOffset.UTC)
                .format(Instant.now()),
        val code: Int = ResultCode.SUCCESS.code,
        val message: String? = ResultCode.SUCCESS.message,
        val error: String? = "",
        val token: String? = null,
        val data: Any? = null
)

override fun getBody(): CustomResponseBody? = CustomResponseBody(timestamp, code, message, error, token, data)

在控制器中处理异常而无需显式返回ResponseEntity最简单、最干净的方法是添加@ExceptionHandler方法。

使用 Spring Boot 2.0.3.RELEASE 的示例片段:

// Prefer static import of HttpStatus constants as it's cleaner IMHO

// Handle with no content returned
@ExceptionHandler(IllegalArgumentException.class)
@ResponseStatus(BAD_REQUEST)
void onIllegalArgumentException() {}

// Return 404 when JdbcTemplate does not return a single row
@ExceptionHandler(IncorrectResultSizeDataAccessException.class)
@ResponseStatus(NOT_FOUND)
void onIncorrectResultSizeDataAccessException() {}

// Catch all handler with the exception as content
@ExceptionHandler(Exception.class)
@ResponseStatus(I_AM_A_TEAPOT)
@ResponseBody Exception onException(Exception e) {
  return e;
}

作为旁白:

  • 如果在所有上下文/用法中matchService.getMatchJson(matchId) == null无效,那么我的建议是让getMatchJson抛出异常,例如IllegalArgumentException而不是返回null并让它冒泡到控制器的@ExceptionHandler
  • 如果null用于测试其他条件,那么我将有一个特定的方法,例如matchService.hasMatchJson(matchId) 一般来说,如果可能的话,我会避免使用null以避免意外的NullPointerException

我认为这个线程实际上有最简单、最干净的解决方案,它不会牺牲 Spring 提供的 JSON 武术工具:

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM