简体   繁体   English

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

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

I'm using Spring MVC for a simple JSON API, with @ResponseBody based approach like the following.我将 Spring MVC 用于简单的 JSON API,使用基于@ResponseBody的方法,如下所示。 (I already have a service layer producing JSON directly.) (我已经有一个直接生成 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;
}

In the given scenario, what is the simplest, cleanest way to respond with a HTTP 400 error?在给定的场景中,响应 HTTP 400 错误的最简单、最干净的方法是什么?

I did come across approaches like:我确实遇到过以下方法:

return new ResponseEntity(HttpStatus.BAD_REQUEST);

...but I can't use it here since my method's return type is String, not ResponseEntity. ...但我不能在这里使用它,因为我的方法的返回类型是字符串,而不是 ResponseEntity。

change your return type to ResponseEntity<> , then you can use below for 400将您的返回类型更改为ResponseEntity<> ,然后您可以在下面使用 400

return new ResponseEntity<>(HttpStatus.BAD_REQUEST);

and for correct request和正确的请求

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

UPDATE 1更新 1

after spring 4.1 there are helper methods in ResponseEntity could be used as在 spring 4.1 之后,ResponseEntity 中的辅助方法可以用作

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

and

return ResponseEntity.ok(json);

Something like this should work, I'm not sure whether or not there is a simpler way:这样的事情应该可行,我不确定是否有更简单的方法:

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

Not necessarily the most compact way of doing this, but quite clean IMO不一定是最紧凑的方式,但非常干净 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");
}

Edit you can use @ResponseBody in the exception handler method if using Spring 3.1+, otherwise use a ModelAndView or something.编辑如果使用 Spring 3.1+,您可以在异常处理程序方法中使用 @ResponseBody,否则使用ModelAndView或其他东西。

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

I would change the implementation slightly:我会稍微改变一下实现:

First, I create a UnknownMatchException :首先,我创建一个UnknownMatchException

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

Note the use of @ResponseStatus , which will be recognized by Spring's ResponseStatusExceptionResolver .注意@ResponseStatus的使用,它会被 Spring 的ResponseStatusExceptionResolver识别。 If the exception is thrown, it will create a response with the corresponding response status.如果抛出异常,它将创建一个具有相应响应状态的响应。 (I also took the liberty of changing the status code to 404 - Not Found which I find more appropriate for this use case, but you can stick to HttpStatus.BAD_REQUEST if you like.) (我还冒昧地将状态代码更改为404 - Not Found ,我认为这更适合此用例,但如果您愿意,可以坚持使用HttpStatus.BAD_REQUEST 。)


Next, I would change the MatchService to have the following signature:接下来,我将更改MatchService以具有以下签名:

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

Finally, I would update the controller and delegate to Spring's MappingJackson2HttpMessageConverter to handle the JSON serialization automatically (it is added by default if you add Jackson to the classpath and add either @EnableWebMvc or <mvc:annotation-driven /> to your config, see the reference docs ):最后,我将更新控制器并委托给 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);
}

Note, it is very common to separate the domain objects from the view objects or DTO objects.请注意,将域对象与视图对象或 DTO 对象分开是很常见的。 This can easily be achieved by adding a small DTO factory that returns the serializable JSON object:这可以通过添加一个返回可序列化 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);
}

Here's a different approach.这是一种不同的方法。 Create a custom Exception annotated with @ResponseStatus , like the following one.创建一个用@ResponseStatus注释的自定义Exception ,如下所示。

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

    public NotFoundException() {
    }
}

And throw it when needed.并在需要时扔掉它。

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

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

As mentioned in some answers, there is the ability to create an exception class for each HTTP status that you want to return.正如一些答案中提到的,可以为您想要返回的每个 HTTP 状态创建一个异常类。 I don't like the idea of having to create a class per status for each project.我不喜欢必须为每个项目为每个状态创建一个类的想法。 Here is what I came up with instead.这是我想出来的。

  • Create a generic exception that accepts an HTTP status创建一个接受 HTTP 状态的通用异常
  • Create an Controller Advice exception handler创建 Controller Advice 异常处理程序

Let's get to the 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;
    }
}

Then I create a controller advice class然后我创建了一个控制器建议类

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 use it使用它

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

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

Easiest way is to throw a ResponseStatusException最简单的方法是抛出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;
    }

I m using this in my spring boot application我在 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);
}

With Spring Boot, I'm not entirely sure why this was necessary (I got the /error fallback even though @ResponseBody was defined on an @ExceptionHandler ), but the following in itself did not work:随着春天的引导,我不完全知道为什么,这是必要的(我得到了/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;
}

It still threw an exception, apparently because no producible media types were defined as a request attribute:它仍然抛出异常,显然是因为没有可生产的媒体类型被定义为请求属性:

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

So I added them.所以我添加了它们。

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

And this got me through to have a "supported compatible media type", but then it still didn't work, because my ErrorMessage was faulty:这让我获得了“支持的兼容媒体类型”,但它仍然无法正常工作,因为我的ErrorMessage有问题:

public class ErrorMessage {
    int code;

    String message;
}

JacksonMapper did not handle it as "convertable", so I had to add getters/setters, and I also added @JsonProperty annotation 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;
    }
}

Then I received my message as intended然后我按预期收到了我的消息

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

Another approach is to use @ExceptionHandler with @ControllerAdvice to centralize all your handlers in the same class, if not you must put the handler methods in every controller you want to manage an exception.另一种方法是使用@ExceptionHandler@ControllerAdvice将所有处理程序集中在同一个类中,否则您必须将处理程序方法放在要管理异常的每个控制器中。

Your handler class:您的处理程序类:

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

Your custom exception:您的自定义异常:

public class MyBadRequestException extends RuntimeException {

  private String description;

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

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

Now you can throw exceptions from any of your controllers, and you can define other handlers inside you advice class.现在,您可以从任何控制器抛出异常,并且可以在通知类中定义其他处理程序。

You also could just throw new HttpMessageNotReadableException("error description") to benefit from Spring's default error handling .您也可以throw new HttpMessageNotReadableException("error description")以从 Spring 的默认错误处理中受益。

However, just as is the case with those default errors, no response body will be set.但是,就像这些默认错误的情况一样,不会设置响应正文。

I find these useful when rejecting requests that could reasonably only have been handcrafted, potentially indicating a malevolent intent, since they obscure the fact that the request was rejected based on a deeper, custom validation and its criteria.我发现这些在拒绝只能合理地手工制作的请求时很有用,可能表明恶意意图,因为它们掩盖了请求被拒绝基于更深层次的自定义验证及其标准的事实。

Hth, dtk hth, dtk

Custom response with the status code带有状态码的自定义响应

like that:像那样:

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)

Simplest and cleanest way to handle exceptions in your controller without having to explicitly return ResponseEntity is to just add @ExceptionHandler methods.在控制器中处理异常而无需显式返回ResponseEntity最简单、最干净的方法是添加@ExceptionHandler方法。

Example snippet using Spring Boot 2.0.3.RELEASE:使用 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;
}

As an aside:作为旁白:

  • If in all contexts/usages matchService.getMatchJson(matchId) == null is invalid, then my suggestion would be to have getMatchJson throw an exception eg IllegalArgumentException instead of returning null and let it bubble up to the controller's @ExceptionHandler .如果在所有上下文/用法中matchService.getMatchJson(matchId) == null无效,那么我的建议是让getMatchJson抛出异常,例如IllegalArgumentException而不是返回null并让它冒泡到控制器的@ExceptionHandler
  • If null is used to test other conditions then I would have a specific method eg matchService.hasMatchJson(matchId) .如果null用于测试其他条件,那么我将有一个特定的方法,例如matchService.hasMatchJson(matchId) In general, I avoid null if I possible in order to avoid unexpected NullPointerException一般来说,如果可能的话,我会避免使用null以避免意外的NullPointerException

I think this thread actually has the easiest, cleanest solution, that does not sacrifice the JSON martialing tools that Spring provides:我认为这个线程实际上有最简单、最干净的解决方案,它不会牺牲 Spring 提供的 JSON 武术工具:

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

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

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