Dize döndüren bir Spring MVC @ResponseBody yönteminde HTTP 400 hatasıyla nasıl yanıt verilir?


389

@ResponseBodyAşağıdaki gibi temel yaklaşım ile basit bir JSON API için Spring MVC kullanıyorum . (Zaten doğrudan JSON üreten bir hizmet katmanım var.)

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

Soru, verilen senaryoda, HTTP 400 hatasıyla yanıt vermenin en basit ve en temiz yolu nedir?

Şöyle yaklaşımlarla karşılaştım:

return new ResponseEntity(HttpStatus.BAD_REQUEST);

... ama benim yönteminin dönüş türü ResponseEntity değil, String olduğundan burada kullanamıyorum.

Yanıtlar:


624

dönüş türünüzü değiştirin ResponseEntity<>, ardından 400 için aşağıda kullanabilirsiniz

return new ResponseEntity<>(HttpStatus.BAD_REQUEST);

ve doğru istek için

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

GÜNCELLEME 1

4.1 yayından sonra ResponseEntity'de yardımcı yöntemler kullanılabilir

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

ve

return ResponseEntity.ok(json);

Ah, böyle de kullanabilirsiniz ResponseEntity. Bu güzel çalışıyor ve orijinal kodda sadece basit bir değişiklik - teşekkürler!
Jonik

Eğer ResponseEntity tüm kurucular kontrol çok özel üstbilgi ekleyebilirsiniz her zaman bekliyoruz
Bassem Reda Zohdy

7
Dizeden başka bir şey geçiriyorsanız ne olur? Bir POJO veya başka bir nesnede olduğu gibi?
mrshickadance

11
'ResponseEntity <Sınıfınız>' olacak
Bassem Reda Zohdy

5
Bu yaklaşımı kullanarak @ResponseBody ek açıklamasına artık ihtiyacınız yok
Lu55

108

Böyle bir şey işe yaramalı, daha basit bir yol olup olmadığından emin değilim:

@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
Teşekkürler! Bu işe yarıyor ve oldukça basit. (Bu durumda, kullanılmayan bodyve requestparametrelerin kaldırılmasıyla daha da basitleştirilebilir .)
Jonik

54

Bunu yapmanın en kompakt yolu değil, ama oldukça temiz 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");
}

Düzenleme, Spring 3.1+ kullanıyorsanız istisna işleyici yönteminde @ResponseBody'yi kullanabilirsiniz, aksi takdirde bir ModelAndViewveya başka bir şey kullanın .

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


1
Üzgünüz, bu işe yaramıyor. Günlüklerde uzun yığın izlemesi ile HTTP 500 "sunucu hatası" üretir: 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 representationCevapta eksik bir şey var mı?
Jonik

Ayrıca, başka bir özel tür (MyError) tanımlama noktasını tam olarak anlamadım. Bu gerekli mi? En son Baharı kullanıyorum (3.2.2).
Jonik

1
Benim için çalışıyor. Bunun javax.validation.ValidationExceptionyerine kullanıyorum. (Bahar 3.1.4)
Jerry Chen

Bu, hizmetinizle ara katmanın kendi hata işleme yeteneklerine sahip olduğu istemci arasında bir ara katmanın olduğu durumlarda oldukça kullanışlıdır. Bu örnek için teşekkür ederiz @Zutty
StormeHawke


48

Uygulamayı biraz değiştirirdim:

İlk olarak, bir UnknownMatchException:

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

Spring tarafından tanınacak olan @ResponseStatus kullanımına dikkat edinResponseStatusExceptionResolver . Kural dışı durum atılırsa, karşılık gelen yanıt durumuyla bir yanıt oluşturur. ( 404 - Not FoundBu kullanım durumu için daha uygun bulduğum durum kodunu değiştirme özgürlüğünü de aldım, ancak isterseniz yapıştırabilirsiniz HttpStatus.BAD_REQUEST.)


Sonra, MatchServiceaşağıdaki imzaya sahip olacak şekilde değiştiririm :

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

Son olarak, denetleyiciyi güncelleyecek ve MappingJackson2HttpMessageConverterJSON serileştirmesini otomatik olarak işlemek için Spring'lere delege edeceğim (Jackson'ı sınıf yoluna ekleyip yapılandırmanıza @EnableWebMvcveya <mvc:annotation-driven />yapılandırmanıza eklerseniz varsayılan olarak eklenir ): referans belgelerine bakın ):

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

Etki alanı nesnelerini görünüm nesnelerinden veya DTO nesnelerinden ayırmak çok yaygındır. Bu, serileştirilebilir JSON nesnesini döndüren küçük bir DTO fabrikası ekleyerek kolayca gerçekleştirilebilir:

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

500 ve i günlüklerim var: 28 ay 2015 17:23:31 org.apache.cxf.interceptor.AbstractFaultChainInitiatorObserver onMessage SEVERE: Hata işleme sırasında hata oluştu, vazgeç! org.apache.cxf.interceptor.Fault
jilet

Mükemmel bir çözüm, sadece DTO'nun bir bileşimi Matchve başka bir nesnenin olduğunu umuyorum .
Marco Sulla

32

İşte farklı bir yaklaşım. Aşağıdaki gibi, Exceptionaçıklamalı bir özel oluşturun @ResponseStatus.

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

    public NotFoundException() {
    }
}

Ve gerektiğinde atın.

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

Bahar dokümanlarına buradan göz atın: http://docs.spring.io/spring/docs/current/spring-framework-reference/htmlsingle/#mvc-ann-annotated-exceptions .


Bu yaklaşım, döndürmek istediğiniz HTTP durum kodunu belirtmesi gereken bir "özel değer" döndürmek zorunda kalmadan yığın izinde nerede olursanız olun yürütmeyi sonlandırmanıza olanak tanır.
Muhammed Gelbana

21

Bazı yanıtlarda belirtildiği gibi, dönmek istediğiniz her HTTP durumu için bir istisna sınıfı oluşturma yeteneği vardır. Her proje için statü başına bir sınıf yaratmak zorunda kalmaktan hoşlanmıyorum. İşte onun yerine geldim.

  • HTTP durumunu kabul eden genel bir istisna oluşturma
  • Denetleyici Önerileri özel durum işleyicisi oluşturma

Koda geçelim

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

Sonra bir kontrolör tavsiye sınıfı oluşturuyorum

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

Kullanmak için

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

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


Çok iyi bir yöntem .. Basit bir String yerine errorCode ve mesaj alanlarıyla bir jSON döndürmeyi tercih ederim ..
İsmail Yavuz

1
Bu doğru yanıt, özel durum kodu ve mesajı olan genel ve global bir istisna işleyicisi olmalıdır: D
Pedro Silva

10

Bunu bahar önyükleme uygulamamda kullanıyorum

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

9

En kolay yol, 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;
    }

3
En iyi cevap: dönüş türünü değiştirmenize ve kendi istisnanızı oluşturmanıza gerek yoktur. Ayrıca, ResponseStatusException gerekirse bir neden mesajı eklemenize izin verir.
Migs

ResponseStatusException'ın yalnızca Bahar 5+ sürümünde kullanılabileceğini unutmayın
Ethan Conner

2

Spring Boot ile bunun neden gerekli olduğundan tam olarak emin değilim (bir tanesinde tanımlanmış /errorolsa bile yedeklemeyi aldım ), ancak kendi başına aşağıdakiler işe yaramadı:@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;
}

Görünüşe göre, üretilebilir medya türleri istek özelliği olarak tanımlanmadığı için hala bir istisna attı:

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

Bu yüzden onları ekledim.

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

Ve bu beni "desteklenen uyumlu bir ortam türü" ne sahip oldu, ama sonra hala işe yaramadı, çünkü ErrorMessagehatalıydım:

public class ErrorMessage {
    int code;

    String message;
}

JacksonMapper bunu "dönüştürülebilir" olarak ele almadı, bu yüzden getters / setters eklemek zorunda kaldım ve ayrıca ek @JsonPropertyaçıklama ekledim

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

Sonra istediğim gibi mesajımı aldım

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

0

Ayrıca throw new HttpMessageNotReadableException("error description")Spring'in varsayılan hata işlemesinden de yararlanabilirsiniz .

Ancak, bu varsayılan hatalarda olduğu gibi, hiçbir yanıt gövdesi ayarlanmayacaktır.

Bunları, yalnızca el yapımı olabilecek talepleri reddederken, potansiyel olarak kötü niyetli bir niyet olduğunu gösterirken yararlı buluyorum, çünkü talebin daha derin, özel bir geçerliliğe ve kriterlerine göre reddedildiğini gizliyorlar.

Hth, dtk


HttpMessageNotReadableException("error description")kullanımdan kaldırıldı.
Kuba Šimonovský

0

Diğer bir yaklaşım kullanmaktır @ExceptionHandlerile @ControllerAdvicedeğil size bir istisna yönetmek istediğiniz her denetleyicisi işleyici yöntemlerini koymak gerekiyorsa, aynı sınıftaki tüm işleyicileri merkezileştirme.

İşleyici sınıfınız:

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

Özel istisnanız:

public class MyBadRequestException extends RuntimeException {

  private String description;

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

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

Artık herhangi bir denetleyicinizden istisnalar atabilir ve tavsiye sınıfınızdaki diğer işleyicileri tanımlayabilirsiniz.


Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.