Bagaimana merespons dengan kesalahan HTTP 400 dalam metode Spring MVC @ResponseBody yang mengembalikan String?

389

Saya menggunakan Spring MVC untuk API JSON sederhana, dengan @ResponseBodypendekatan berbasis seperti berikut. (Saya sudah memiliki lapisan layanan yang memproduksi JSON secara langsung.)

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

Pertanyaannya adalah, dalam skenario yang diberikan, apa cara paling sederhana, paling bersih untuk merespons dengan kesalahan HTTP 400 ?

Saya menemukan beberapa pendekatan seperti:

return new ResponseEntity(HttpStatus.BAD_REQUEST);

... tapi saya tidak bisa menggunakannya di sini karena tipe pengembalian metode saya adalah String, bukan ResponseEntity.

Jonik
sumber

Jawaban:

624

ubah tipe pengembalian menjadi ResponseEntity<>, lalu Anda bisa gunakan di bawah ini untuk 400

return new ResponseEntity<>(HttpStatus.BAD_REQUEST);

dan untuk permintaan yang benar

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

PEMBARUAN 1

setelah musim semi 4.1 ada metode pembantu di ResponseEntity dapat digunakan sebagai

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

dan

return ResponseEntity.ok(json);
Bassem Reda Zohdy
sumber
Ah, jadi kamu bisa menggunakan ResponseEntityseperti ini juga. Ini berfungsi dengan baik dan hanya perubahan sederhana ke kode asli — terima kasih!
Jonik
Anda dipersilakan kapan saja Anda dapat menambahkan tajuk khusus juga periksa semua konstruktor dari ResponseEntity
Bassem Reda Zohdy
7
Bagaimana jika Anda melewatkan sesuatu selain string kembali? Seperti dalam POJO atau objek lain?
mrshickadance
11
itu akan menjadi 'ResponseEntity <YourClass>'
Bassem Reda Zohdy
5
Dengan menggunakan pendekatan ini Anda tidak perlu lagi penjelasan
@ResponseBody
108

Sesuatu seperti ini seharusnya bekerja, saya tidak yakin apakah ada cara yang lebih sederhana:

@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;
}
stacker
sumber
5
Terima kasih! Ini berfungsi dan juga cukup sederhana. (Dalam hal ini dapat lebih disederhanakan dengan menghapus yang tidak terpakai bodydan requestparams.)
Jonik
54

Tidak harus cara paling ringkas untuk melakukan ini, tetapi IMO cukup bersih

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

Sunting Anda dapat menggunakan @ResponseBody dalam metode handler pengecualian jika menggunakan Spring 3.1+, jika tidak gunakan a ModelAndViewatau sesuatu.

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

Zutty
sumber
1
Maaf, ini sepertinya tidak berhasil. Ini menghasilkan HTTP 500 "kesalahan server" dengan jejak tumpukan panjang di log: 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 representationApakah ada sesuatu yang hilang dari jawabannya?
Jonik
Selain itu, saya tidak sepenuhnya memahami titik mendefinisikan jenis kustom lain (MyError). Apakah itu perlu? Saya menggunakan Spring terbaru (3.2.2).
Jonik
1
Ini bekerja untuk saya. Saya menggunakan javax.validation.ValidationExceptionsebagai gantinya. (Musim semi 3.1.4)
Jerry Chen
Ini cukup berguna dalam situasi di mana Anda memiliki lapisan perantara antara layanan Anda dan klien di mana lapisan menengah memiliki kemampuan penanganan kesalahan sendiri. Terima kasih atas contoh ini @Zutty
StormeHawke
Ini harus menjadi jawaban yang diterima, karena memindahkan pengecualian penanganan kode dari aliran normal dan menyembunyikan HttpServlet *
lilalinux
48

Saya akan sedikit mengubah implementasinya:

Pertama, saya membuat UnknownMatchException:

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

Perhatikan penggunaan @ResponseStatus , yang akan dikenali oleh Spring's ResponseStatusExceptionResolver. Jika pengecualian dilemparkan, itu akan membuat respons dengan status respons yang sesuai. (Saya juga mengambil kebebasan untuk mengubah kode status 404 - Not Foundyang menurut saya lebih sesuai untuk kasus penggunaan ini, tetapi Anda dapat tetap melakukannya HttpStatus.BAD_REQUESTjika Anda mau.)


Selanjutnya, saya akan mengubah MatchServiceuntuk memiliki tanda tangan berikut:

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

Akhirnya, saya akan memperbarui controller dan mendelegasikan ke Spring MappingJackson2HttpMessageConverteruntuk menangani serialisasi JSON secara otomatis (ditambahkan secara default jika Anda menambahkan Jackson ke classpath dan menambahkan salah satu @EnableWebMvcatau <mvc:annotation-driven />ke konfigurasi Anda, lihat dokumen referensi ):

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

Catatan, sangat umum untuk memisahkan objek domain dari objek tampilan atau objek DTO. Ini dapat dengan mudah dicapai dengan menambahkan pabrik DTO kecil yang mengembalikan objek JSON berseri:

@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);
}
matsev
sumber
Saya punya 500 dan saya log: ay 28, 2015 5:23:31 PM org.apache.cxf.interceptor.AbstractFaultChainInitiatorObserver onMessage SEVERE: Kesalahan terjadi selama penanganan kesalahan, menyerah! org.apache.cxf.interceptor.Fault
silet
Solusi sempurna, saya hanya ingin menambahkan bahwa saya berharap bahwa DTO adalah komposisi Matchdan beberapa objek lainnya.
Marco Sulla
32

Ini pendekatan yang berbeda. Buat custom Exceptionberanotasi dengan @ResponseStatus, seperti yang berikut.

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

    public NotFoundException() {
    }
}

Dan membuangnya saat dibutuhkan.

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

Lihat dokumentasi Spring di sini: http://docs.spring.io/spring/docs/current/spring-framework-reference/htmlsingle/#mvc-ann-ann-annotated-exceptions .

danidemi
sumber
Pendekatan ini memungkinkan Anda untuk menghentikan eksekusi di mana pun Anda berada di stacktrace tanpa harus mengembalikan "nilai khusus" yang harus menentukan kode status HTTP yang ingin Anda kembalikan.
Muhammad Gelbana
21

Seperti disebutkan dalam beberapa jawaban, ada kemampuan untuk membuat kelas pengecualian untuk setiap status HTTP yang ingin Anda kembalikan. Saya tidak suka gagasan harus membuat kelas per status untuk setiap proyek. Inilah yang saya pikirkan sebagai gantinya.

  • Buat pengecualian umum yang menerima status HTTP
  • Buat pengendali pengecualian Saran Pengendali

Mari kita menuju kodenya

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

Kemudian saya membuat kelas saran pengontrol

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

Untuk menggunakannya

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

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

Norris
sumber
Metode yang sangat bagus .. Alih-alih String sederhana saya lebih suka mengembalikan jSON dengan bidang errorCode dan pesan ..
İsmail Yavuz
1
Ini harus menjadi jawaban yang benar, penangan pengecualian umum dan global dengan kode status khusus dan pesan: D
Pedro Silva
10

Saya menggunakan ini dalam aplikasi boot musim semi saya

@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);
}
Aamir Faried
sumber
9

Cara termudah adalah dengan melempar a 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;
    }
MevlütÖzdemir
sumber
3
Jawaban terbaik: tidak perlu mengubah jenis kembali dan tidak perlu membuat pengecualian Anda sendiri. Juga, ResponseStatusException memungkinkan untuk menambahkan pesan alasan jika diperlukan.
Migs
Penting untuk dicatat bahwa ResponseStatusException hanya tersedia di Spring versi 5+
Ethan Conner
2

Dengan Spring Boot, saya tidak sepenuhnya yakin mengapa ini diperlukan (saya mendapatkan /errorfallback meskipun @ResponseBodydidefinisikan pada a @ExceptionHandler), tetapi berikut ini dengan sendirinya tidak berfungsi:

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

Itu masih melemparkan pengecualian, tampaknya karena tidak ada jenis media yang dapat diproduksi didefinisikan sebagai atribut permintaan:

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

Jadi saya menambahkannya.

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

Dan ini membuat saya memiliki "tipe media yang kompatibel yang didukung", tetapi kemudian masih tidak berfungsi, karena saya ErrorMessagesalah:

public class ErrorMessage {
    int code;

    String message;
}

JacksonMapper tidak menanganinya sebagai "convertable", jadi saya harus menambahkan getter / setter, dan saya juga menambahkan @JsonPropertyanotasi

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

Kemudian saya menerima pesan saya sebagaimana dimaksud

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

Anda juga bisa throw new HttpMessageNotReadableException("error description")mendapatkan manfaat dari penanganan kesalahan default Spring .

Namun, seperti halnya dengan kesalahan standar tersebut, tidak ada badan respons yang akan ditetapkan.

Saya menemukan ini berguna ketika menolak permintaan yang hanya bisa dibuat dengan tangan, berpotensi menunjukkan niat jahat, karena mereka mengaburkan fakta bahwa permintaan tersebut ditolak berdasarkan pada validasi khusus dan validasinya menurut kriteria.

Hth, dtk

dtk
sumber
HttpMessageNotReadableException("error description")sudah ditinggalkan.
Kuba Šimonovský
0

Pendekatan lain adalah dengan menggunakan @ExceptionHandlerdengan @ControllerAdviceuntuk memusatkan semua penangan Anda di kelas yang sama, jika tidak Anda harus menempatkan metode handler di setiap kontroler Anda ingin mengelola pengecualian.

Kelas penangan Anda:

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

Pengecualian khusus Anda:

public class MyBadRequestException extends RuntimeException {

  private String description;

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

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

Sekarang Anda dapat melempar pengecualian dari salah satu pengendali Anda, dan Anda dapat menentukan penangan lain di dalam kelas saran Anda.

Gonzalo
sumber
-1

Saya pikir utas ini sebenarnya memiliki solusi termudah dan terbersih, yang tidak mengorbankan alat bela diri JSON yang disediakan oleh Spring:

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

Ryan S
sumber