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

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

提问人:Jonik 提问时间:4/26/2013 最后编辑:Peter MortensenJonik 更新时间:7/6/2022 访问量:527170

问:

我正在使用 Spring MVC 作为一个简单的 JSON API,其基于的方法如下所示。(我已经有一个直接生成 JSON 的服务层。@ResponseBody

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

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

java spring-mvc http错误

评论


答:

57赞 Zutty 4/26/2013 #1

这不一定是最紧凑的方式,但在我看来相当干净:

if(json == null) {
    throw new BadThingException();
}
...

@ExceptionHandler(BadThingException.class)
@ResponseStatus(value = HttpStatus.BAD_REQUEST)
public @ResponseBody MyError handleException(BadThingException e) {
    return new MyError("That doesn’t work");
}

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

@ResponseBody不适用于@ExceptionHandler [SPR-6902] #11567

评论

2赞 Jonik 4/27/2013
对不起,这似乎不起作用。它会产生 HTTP 500“服务器错误”,并在日志中显示长堆栈跟踪:答案中是否缺少某些内容?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 representation
1赞 Jonik 4/27/2013
此外,我没有完全理解定义另一个自定义类型(MyError)的意义。有必要吗?我正在使用最新的 Spring (3.2.2)。
1赞 Jerry Chen 10/1/2013
它对我有用。我改用。(春季 3.1.4)javax.validation.ValidationException
0赞 StormeHawke 12/9/2014
这在服务与客户端之间有一个中间层的情况下非常有用,其中中间层具有自己的错误处理功能。感谢您提供此示例@Zutty
1赞 lilalinux 9/14/2016
这应该是公认的答案,因为它将异常处理代码移出正常流程,并隐藏了 HttpServlet*
118赞 stacker 4/26/2013 #2

这样的东西应该有效,但我不确定是否有更简单的方法:

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

评论

5赞 Jonik 4/27/2013
谢谢!这有效,也很简单。(在这种情况下,可以通过删除未使用的参数来进一步简化它。bodyrequest
0赞 MiguelMunoz 12/3/2022
我不清楚的是如何为成功返回而不是错误设置 Http 状态。
711赞 Bassem Reda Zohdy 4/27/2013 #3

将返回类型更改为 ,然后您可以将以下内容用于 400:ResponseEntity<>

return new ResponseEntity<>(HttpStatus.BAD_REQUEST);

对于正确的请求:

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

在 Spring 4.1 之后,ResponseEntity 中有一些辅助方法,可以用作:

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

return ResponseEntity.ok(json);

评论

0赞 Jonik 4/28/2013
啊,所以你也可以这样使用。这很好用,只是对原始代码的简单更改——谢谢!ResponseEntity
0赞 Bassem Reda Zohdy 4/28/2013
欢迎您随时添加自定义标头,检查 ResponseEntity 的所有构造函数
9赞 mrshickadance 11/19/2014
如果您要传回字符串以外的其他内容怎么办?就像在 POJO 或其他对象中一样?
13赞 Bassem Reda Zohdy 11/19/2014
它将是“ResponseEntity<YourClass>”
5赞 Ilya Serbis 8/3/2015
使用这种方法,您不再需要@ResponseBody注释
51赞 matsev 4/27/2013 #4

我会稍微改变实现:

首先,我创建一个:UnknownMatchException

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

请注意 @ResponseStatus 的使用,Spring 的 .如果抛出异常,它将创建具有相应响应状态的响应。(我还冒昧地更改了我认为更适合此用例的状态代码,但如果您愿意,您可以坚持使用。ResponseStatusExceptionResolver404 - Not FoundHttpStatus.BAD_REQUEST


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

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

最后,我会更新控制器并委托给 Spring 以自动处理 JSON 序列化(如果您将 Jackson 添加到类路径并添加其中任何一个或您的配置,则默认情况下会添加它。请参阅参考文档):MappingJackson2HttpMessageConverter@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);
}

评论

0赞 razor 5/29/2015
我有 500 个,志:ay 28, 2015 5:23:31 PM org.apache.cxf.interceptor.AbstractFaultChainInitiatorObserver onMessage SEVERE: Error occurred during error handling, give up!org.apache.cxf.interceptor.Fault
0赞 Marco Sulla 4/17/2019
完美的解决方案,我只想补充一点,我希望 DTO 是和其他一些对象的组合。Match
0赞 MiguelMunoz 12/3/2022
这种方法的问题在于,注解的 JavaDocs 告诉我们不要对 RESTful 服务器执行此操作。使用此注解会导致服务器发送 HTML 响应,这对于 Web 应用程序来说很好,但对于 Restful 服务则不然。但是,定义了一个新的 RuntimeException,它可能具有相同的目的,尽管我还没有太多经验,但它没有与 @ResponseStatus 相同的警告。@ResponseStatusResponseStatusException
13赞 Aamir Faried 10/13/2015 #5

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

评论

0赞 Peter Mortensen 6/19/2022
解释是有道理的。例如,想法/要点是什么?从帮助中心:“......始终解释您提出的解决方案为什么合适以及它是如何工作的”。请通过编辑(更改)您的答案来回复,而不是在评论中(没有“编辑:”,“更新:”或类似内容 - 答案应该看起来好像是今天写的)。
40赞 danidemi 8/6/2016 #6

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

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

评论

1赞 Muhammad Gelbana 11/28/2017
此方法允许您在堆栈跟踪中的任意位置终止执行,而无需返回一个“特殊值”,该值应指定要返回的 HTTP 状态代码。
1赞 Peter Mortensen 6/19/2022
链接(实际上)已断开。它没有任何例外。
0赞 danidemi 8/17/2022
嗨@PeterMortensen我无法找到损坏链接的替代品,所以我删除了它。
0赞 MiguelMunoz 12/3/2022
这种方法的问题在于,注解的 JavaDocs 告诉我们不要对 RESTful 服务器执行此操作。使用此注解会导致服务器发送 HTML 响应,这对于 Web 应用程序来说很好,但对于 Restful 服务则不然。但是,定义了一个新的 RuntimeException,它可能具有相同的目的,尽管我还没有太多经验,但它没有与 @ResponseStatus 相同的警告。@ResponseStatusResponseStatusException
25赞 Norris 11/10/2016 #7

正如某些答案中提到的,可以为要返回的每个 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/

评论

2赞 Ismail Yavuz 12/13/2017
很好的方法..比起简单的字符串,我更喜欢返回带有 errorCode 和 message 字段的 jSON。
2赞 Pedro Silva 9/29/2018
这应该是正确的答案,一个具有自定义状态代码和消息:D的通用和全局异常处理程序
0赞 Peter Mortensen 6/19/2022
浏览器不链接链接:“警告:前方存在潜在安全风险......javaninja.net 证书已于 2021 年 11 月 18 日到期。
3赞 EpicPandaForce 12/1/2016 #8

对于Spring Boot,我不完全确定为什么这是必要的(即使我得到了回退,即使定义在 ),但以下内容本身不起作用:/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 没有将其处理为“可转换”,所以我不得不添加 getters/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."}
0赞 dtk 7/10/2019 #9

你也可以抛出新的 HttpMessageNotReadableException(“error description”) 来从 Spring 的默认错误处理中受益。

但是,与这些默认错误一样,不会设置响应正文。

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

评论

1赞 Kuba Šimonovský 9/9/2019
HttpMessageNotReadableException("error description")已弃用。
36赞 MevlütÖzdemir 1/15/2020 #10

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

评论

10赞 Migs 2/10/2020
最佳答案: 无需更改返回类型,也无需创建自己的异常。此外,ResponseStatusException 允许根据需要添加原因消息。
6赞 Ethan Conner 5/19/2020
需要注意的是,ResponseStatusException 仅在 Spring 版本 5+ 中可用
0赞 Joaquín L. Robles 8/4/2021
这个答案应该在顶部
0赞 Emy Blacksmith 1/19/2022
不能将 JSON 正文作为响应。
0赞 Cheung 4/19/2022
这应该被标记为答案,在 Web 控制器上很有用,无需更改返回类型。
2赞 Gonzalo 1/16/2020 #11

另一种方法是使用 with 将所有处理程序集中到同一个类中。否则,必须将处理程序方法放在要为其管理异常的每个控制器中。@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;
  }
}

现在,您可以从任何控制器引发异常,并且可以在建议类中定义其他处理程序。

-1赞 Nankai 6/28/2021 #12

使用带有状态代码的自定义响应。

喜欢这个:

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)
1赞 xlm 12/9/2021 #13

在控制器中处理异常而不必显式返回的最简单、最干净的方法是添加方法。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) == nullgetMatchJsonIllegalArgumentExceptionnull@ExceptionHandler

  • 如果用于测试其他条件,那么我将有一个特定的方法,例如, .一般来说,如果可能的话,我会避免,以避免意外。nullmatchService.hasMatchJson(matchId)nullNullPointerException