Персонализирана обработка на съобщения за грешки за REST API

ПОЧИВКА Най-горе

Току що обявих новия курс Learn Spring , фокусиран върху основите на Spring 5 и Spring Boot 2:

>> ПРЕГЛЕД НА КУРСА

1. Общ преглед

В този урок - ще обсъдим как да внедрите глобален манипулатор на грешки за Spring REST API.

Ще използваме семантиката на всяко изключение, за да изградим значими съобщения за грешки за клиента, с ясната цел да предоставим на този клиент цялата информация за лесно диагностициране на проблема.

2. Персонализирано съобщение за грешка

Нека започнем с внедряването на проста структура за изпращане на грешки по проводника - 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); } }

Информацията тук трябва да е ясна:

  • статус : HTTP кодът на състоянието
  • съобщение : съобщението за грешка, свързано с изключение
  • грешка : Списък на изградените съобщения за грешка

И разбира се, за действителната логика за обработка на изключения през пролетта ще използваме анотацията @ControllerAdvice :

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

3. Обработвайте изключения с лоши заявки

3.1. Обработка на изключенията

Сега нека видим как можем да се справим с най-често срещаните грешки на клиента - основно сценарии на клиент, изпратили невалидна заявка към API:

  • BindException : Това изключение се появява, когато възникнат фатални грешки при свързване.
  • MethodArgumentNotValidException : Това изключение се изхвърля, когато аргументът е анотиран с @Valid неуспешно проверяване :

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

Както можете да видите, ние заместваме базовия метод от ResponseEntityExceptionHandler и предоставяме наше собствено изпълнение .

Това не винаги ще бъде така - понякога ще трябва да обработим персонализирано изключение, което няма изпълнение по подразбиране в базовия клас, както ще видим по-късно тук.

Следващия:

  • MissingServletRequestPartException : Това изключение се изхвърля, когато частта от заявка от няколко части не е намерена

  • MissingServletRequestParameterException : Това изключение се извежда, когато липсва параметър на заявката:

@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 : Това изключение отчита резултата от нарушения на ограниченията:

@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 : Това изключение се изхвърля, когато се опитате да зададете свойство на боб с грешен тип.

  • MethodArgumentTypeMismatchException : Това изключение се изхвърля, когато аргументът на метода не е очакваният тип:

@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. Консумиране на API от клиента

Нека сега разгледаме тест, който се сблъсква с MethodArgumentTypeMismatchException : ще изпратим заявка с идентификатор като String вместо long :

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

И накрая - разглеждайки същата молба::

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

Ето как ще изглежда този вид отговор на JSON грешка:

{ "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. Обработвайте NoHandlerFoundException

След това можем да персонализираме нашия сървлет да изхвърля това изключение, вместо да изпраща отговор 404 - както следва:

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

След това, след като това се случи, можем просто да се справим с него, както и всяко друго изключение:

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

Ето един прост тест:

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

Нека да разгледаме пълната заявка:

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

И отговорът на JSON за грешка:

{ "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. Обработвайте HttpRequestMethodNotSupportedException

След това нека да разгледаме още едно интересно изключение - HttpRequestMethodNotSupportedException - което се случва, когато изпращате заявено с неподдържан HTTP метод:

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

Ето един прост тест, възпроизвеждащ това изключение:

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

И ето пълната заявка:

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

И отговорът на JSON за грешка:

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

6. Обработвайте HttpMediaTypeNotSupportedException

Нека сега се справим с HttpMediaTypeNotSupportedException - което се случва, когато клиентът изпрати заявка с неподдържан тип носител - както следва:

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

Ето един прост тест, който се провежда в този брой:

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

И накрая - ето примерна заявка:

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

И отговорът на JSON за грешка:

{ "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. Манипулатор по подразбиране

И накрая, нека приложим резервен манипулатор - общ тип логика, която се занимава с всички други изключения, които нямат конкретни манипулатори:

@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. Заключение

Изграждането на подходящ, зрял манипулатор на грешки за Spring REST API е труден и определено итеративен процес. Надяваме се, че този урок ще бъде добра отправна точка за това за вашия API, а също така и добра основа за това как трябва да гледате на това да помагате на клиентите на вашия API бързо и лесно да диагностицират грешки и да преминават покрай тях.

Най- пълното прилагане на този урок може да се намери в проекта Github - това е Eclipse проект на базата, така че трябва да бъде лесен за внос и работи като такъв.

ПОЧИВКА отдолу

Току що обявих новия курс Learn Spring , фокусиран върху основите на Spring 5 и Spring Boot 2:

>> ПРЕГЛЕД НА КУРСА