Pengendalian Mesej Ralat Khusus untuk API REST

REST Teratas

Saya baru sahaja mengumumkan kursus Learn Spring yang baru , yang berfokus pada asas-asas Spring 5 dan Spring Boot 2:

>> SEMAK KURSUS

1. Gambaran keseluruhan

Dalam tutorial ini - kita akan membincangkan cara menerapkan pengendali ralat global untuk Spring REST API.

Kami akan menggunakan semantik setiap pengecualian untuk membina mesej ralat yang bermakna bagi klien, dengan tujuan yang jelas untuk memberikan semua maklumat kepada pelanggan untuk mendiagnosis masalah dengan mudah.

2. Mesej Ralat Tersuai

Mari mulakan dengan menerapkan struktur mudah untuk menghantar kesalahan melalui wayar - ApiError :

public class ApiError { private HttpStatus status; private String message; private List errors; public ApiError(HttpStatus status, String message, List errors) { super(); this.status = status; this.message = message; this.errors = errors; } public ApiError(HttpStatus status, String message, String error) { super(); this.status = status; this.message = message; errors = Arrays.asList(error); } }

Maklumat di sini mestilah mudah:

  • status : kod status HTTP
  • mesej : mesej ralat yang berkaitan dengan pengecualian
  • ralat : Senarai mesej ralat yang dibina

Dan tentu saja, untuk logik pengendalian pengecualian sebenar pada musim bunga, kami akan menggunakan anotasi @ControllerAdvice :

@ControllerAdvice public class CustomRestExceptionHandler extends ResponseEntityExceptionHandler { ... }

3. Tangani Pengecualian Permintaan Buruk

3.1. Mengendalikan Pengecualian

Sekarang, mari kita lihat bagaimana kita dapat menangani kesalahan pelanggan yang paling biasa - pada dasarnya senario pelanggan menghantar permintaan yang tidak sah ke API:

  • BindException : Pengecualian ini dilemparkan apabila berlaku kesalahan mengikat yang membawa maut
  • MethodArgumentNotValidException : Pengecualian ini dilemparkan apabila hujah yang dianotasi dengan @Valid gagal disahkan :

@Override protected ResponseEntity handleMethodArgumentNotValid( MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { List errors = new ArrayList(); for (FieldError error : ex.getBindingResult().getFieldErrors()) { errors.add(error.getField() + ": " + error.getDefaultMessage()); } for (ObjectError error : ex.getBindingResult().getGlobalErrors()) { errors.add(error.getObjectName() + ": " + error.getDefaultMessage()); } ApiError apiError = new ApiError(HttpStatus.BAD_REQUEST, ex.getLocalizedMessage(), errors); return handleExceptionInternal( ex, apiError, headers, apiError.getStatus(), request); } 

Seperti yang anda lihat, kami mengesampingkan kaedah asas daripada ResponseEntityExceptionHandler dan menyediakan pelaksanaan tersuai kami sendiri .

Itu tidak selalu berlaku - kadang-kadang kita perlu menangani pengecualian khusus yang tidak mempunyai pelaksanaan lalai di kelas asas, seperti yang akan kita lihat nanti di sini.

Seterusnya:

  • MissingServletRequestPartException : Pengecualian ini dilemparkan apabila bahagian permintaan berbilang bahagian tidak dijumpai

  • MissingServletRequestParameterException : Pengecualian ini dilemparkan apabila permintaan parameter hilang:

@Override protected ResponseEntity handleMissingServletRequestParameter( MissingServletRequestParameterException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { String error = ex.getParameterName() + " parameter is missing"; ApiError apiError = new ApiError(HttpStatus.BAD_REQUEST, ex.getLocalizedMessage(), error); return new ResponseEntity( apiError, new HttpHeaders(), apiError.getStatus()); }
  • ConstrainViolationException : Pengecualian ini melaporkan hasil pelanggaran kekangan:

@ExceptionHandler({ ConstraintViolationException.class }) public ResponseEntity handleConstraintViolation( ConstraintViolationException ex, WebRequest request) { List errors = new ArrayList(); for (ConstraintViolation violation : ex.getConstraintViolations()) { errors.add(violation.getRootBeanClass().getName() + " " + violation.getPropertyPath() + ": " + violation.getMessage()); } ApiError apiError = new ApiError(HttpStatus.BAD_REQUEST, ex.getLocalizedMessage(), errors); return new ResponseEntity( apiError, new HttpHeaders(), apiError.getStatus()); }
  • TypeMismatchException : Pengecualian ini dilemparkan semasa cuba menetapkan harta kacang dengan jenis yang salah.

  • MethodArgumentTypeMismatchException : Pengecualian ini dilemparkan apabila argumen kaedah bukan jenis yang diharapkan:

@ExceptionHandler({ MethodArgumentTypeMismatchException.class }) public ResponseEntity handleMethodArgumentTypeMismatch( MethodArgumentTypeMismatchException ex, WebRequest request) { String error = ex.getName() + " should be of type " + ex.getRequiredType().getName(); ApiError apiError = new ApiError(HttpStatus.BAD_REQUEST, ex.getLocalizedMessage(), error); return new ResponseEntity( apiError, new HttpHeaders(), apiError.getStatus()); }

3.2. Menggunakan API dari Pelanggan

Sekarang mari kita lihat ujian yang berjalan ke MethodArgumentTypeMismatchException : kami akan menghantar permintaan dengan id sebagai String dan bukannya panjang :

@Test public void whenMethodArgumentMismatch_thenBadRequest() { Response response = givenAuth().get(URL_PREFIX + "/api/foos/ccc"); ApiError error = response.as(ApiError.class); assertEquals(HttpStatus.BAD_REQUEST, error.getStatus()); assertEquals(1, error.getErrors().size()); assertTrue(error.getErrors().get(0).contains("should be of type")); }

Dan akhirnya - mempertimbangkan permintaan yang sama:

Request method: GET Request path: //localhost:8080/spring-security-rest/api/foos/ccc 

Inilah rupa tindak balas ralat JSON seperti ini:

{ "status": "BAD_REQUEST", "message": "Failed to convert value of type [java.lang.String] to required type [java.lang.Long]; nested exception is java.lang.NumberFormatException: For input string: \"ccc\"", "errors": [ "id should be of type java.lang.Long" ] }

4. Tangani NoHandlerFoundException

Seterusnya, kami dapat menyesuaikan servlet kami untuk membuang pengecualian ini daripada menghantar respons 404 - seperti berikut:

 api  org.springframework.web.servlet.DispatcherServlet  throwExceptionIfNoHandlerFound true  

Kemudian, apabila ini berlaku, kita dapat mengatasinya sama seperti pengecualian lain:

@Override protected ResponseEntity handleNoHandlerFoundException( NoHandlerFoundException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { String error = "No handler found for " + ex.getHttpMethod() + " " + ex.getRequestURL(); ApiError apiError = new ApiError(HttpStatus.NOT_FOUND, ex.getLocalizedMessage(), error); return new ResponseEntity(apiError, new HttpHeaders(), apiError.getStatus()); }

Berikut adalah ujian mudah:

@Test public void whenNoHandlerForHttpRequest_thenNotFound() { Response response = givenAuth().delete(URL_PREFIX + "/api/xx"); ApiError error = response.as(ApiError.class); assertEquals(HttpStatus.NOT_FOUND, error.getStatus()); assertEquals(1, error.getErrors().size()); assertTrue(error.getErrors().get(0).contains("No handler found")); }

Mari lihat permintaan penuh:

Request method: DELETE Request path: //localhost:8080/spring-security-rest/api/xx

Dan tindak balas JSON ralat:

{ "status":"NOT_FOUND", "message":"No handler found for DELETE /spring-security-rest/api/xx", "errors":[ "No handler found for DELETE /spring-security-rest/api/xx" ] }

5. Tangani HttpRequestMethodNotSupportedException

Seterusnya, mari kita lihat pengecualian lain yang menarik - HttpRequestMethodNotSupportedException - yang berlaku apabila anda menghantar permintaan yang diminta dengan kaedah HTTP yang tidak disokong:

@Override protected ResponseEntity handleHttpRequestMethodNotSupported( HttpRequestMethodNotSupportedException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { StringBuilder builder = new StringBuilder(); builder.append(ex.getMethod()); builder.append( " method is not supported for this request. Supported methods are "); ex.getSupportedHttpMethods().forEach(t -> builder.append(t + " ")); ApiError apiError = new ApiError(HttpStatus.METHOD_NOT_ALLOWED, ex.getLocalizedMessage(), builder.toString()); return new ResponseEntity( apiError, new HttpHeaders(), apiError.getStatus()); }

Berikut adalah ujian mudah yang menghasilkan pengecualian ini:

@Test public void whenHttpRequestMethodNotSupported_thenMethodNotAllowed() { Response response = givenAuth().delete(URL_PREFIX + "/api/foos/1"); ApiError error = response.as(ApiError.class); assertEquals(HttpStatus.METHOD_NOT_ALLOWED, error.getStatus()); assertEquals(1, error.getErrors().size()); assertTrue(error.getErrors().get(0).contains("Supported methods are")); }

Dan inilah permintaan penuh:

Request method: DELETE Request path: //localhost:8080/spring-security-rest/api/foos/1

Dan tindak balas JSON ralat:

{ "status":"METHOD_NOT_ALLOWED", "message":"Request method 'DELETE' not supported", "errors":[ "DELETE method is not supported for this request. Supported methods are GET " ] }

6. Mengendalikan HttpMediaTypeNotSupportedException

Sekarang, mari kita menangani HttpMediaTypeNotSupportedException - yang berlaku apabila pelanggan menghantar permintaan dengan jenis media yang tidak disokong - seperti berikut:

@Override protected ResponseEntity handleHttpMediaTypeNotSupported( HttpMediaTypeNotSupportedException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { StringBuilder builder = new StringBuilder(); builder.append(ex.getContentType()); builder.append(" media type is not supported. Supported media types are "); ex.getSupportedMediaTypes().forEach(t -> builder.append(t + ", ")); ApiError apiError = new ApiError(HttpStatus.UNSUPPORTED_MEDIA_TYPE, ex.getLocalizedMessage(), builder.substring(0, builder.length() - 2)); return new ResponseEntity( apiError, new HttpHeaders(), apiError.getStatus()); }

Berikut adalah ujian mudah yang menghadapi masalah ini:

@Test public void whenSendInvalidHttpMediaType_thenUnsupportedMediaType() { Response response = givenAuth().body("").post(URL_PREFIX + "/api/foos"); ApiError error = response.as(ApiError.class); assertEquals(HttpStatus.UNSUPPORTED_MEDIA_TYPE, error.getStatus()); assertEquals(1, error.getErrors().size()); assertTrue(error.getErrors().get(0).contains("media type is not supported")); }

Akhirnya - berikut adalah contoh permintaan:

Request method: POST Request path: //localhost:8080/spring-security- Headers: Content-Type=text/plain; charset=ISO-8859-1

Dan tindak balas JSON ralat:

{ "status":"UNSUPPORTED_MEDIA_TYPE", "message":"Content type 'text/plain;charset=ISO-8859-1' not supported", "errors":["text/plain;charset=ISO-8859-1 media type is not supported. Supported media types are text/xml application/x-www-form-urlencoded application/*+xml application/json;charset=UTF-8 application/*+json;charset=UTF-8 */" ] }

7. Pengendali Lalai

Akhirnya, mari kita laksanakan pengendali fall-back - jenis logik yang sesuai dengan semua pengecualian lain yang tidak mempunyai pengendali khusus:

@ExceptionHandler({ Exception.class }) public ResponseEntity handleAll(Exception ex, WebRequest request) { ApiError apiError = new ApiError( HttpStatus.INTERNAL_SERVER_ERROR, ex.getLocalizedMessage(), "error occurred"); return new ResponseEntity( apiError, new HttpHeaders(), apiError.getStatus()); }

8. Kesimpulannya

Membangun pengendali ralat yang tepat dan matang untuk Spring REST API memang sukar dan pasti merupakan proses berulang. Mudah-mudahan, tutorial ini akan menjadi titik permulaan yang baik untuk melakukannya untuk API anda dan juga petunjuk yang baik untuk bagaimana anda seharusnya membantu klien API anda dengan cepat dan mudah mendiagnosis kesilapan dan mengatasi mereka.

The pelaksanaan penuh tutorial ini boleh didapati dalam projek Github - ini adalah projek berasaskan Eclipse, jadi ia harus mudah untuk import dan berjalan kerana ia adalah.

REST bawah

Saya baru sahaja mengumumkan kursus Learn Spring yang baru , yang berfokus pada asas-asas Spring 5 dan Spring Boot 2:

>> SEMAK KURSUS