Prilagođeno rukovanje porukama o pogrešci za REST API

OSTALO Vrh

Upravo sam najavio novo Uči proljeće tečaj, usredotočen na osnove Spring 5 i Spring Boot 2:

>> PROVJERITE TEČAJ

1. Pregled

U ovom vodiču - razgovarat ćemo o tome kako implementirati globalni rukovatelj pogreškama za Spring REST API.

Upotrijebit ćemo semantiku svake iznimke za izradu značajnih poruka o pogreškama za klijenta, s jasnim ciljem da tom klijentu pružimo sve informacije kako bi lako dijagnosticirao problem.

2. Prilagođena poruka o pogrešci

Počnimo s primjenom jednostavne strukture za slanje pogrešaka putem žice - ApiError:

javna klasa ApiError {status privatnog HttpStatusa; privatna string poruka; pogreške privatnog popisa; javna ApiError (status HttpStatus, poruka niza, pogreške popisa) {super (); this.status = status; this.message = poruka; this.errors = pogreške; } javna ApiError (status HttpStatus, niz poruke, pogreška niza) {super (); this.status = status; this.message = poruka; pogreške = Arrays.asList (pogreška); }}

Ovdje bi trebali biti jasni podaci:

  • status: HTTP statusni kôd
  • poruka: poruka o pogrešci povezana s iznimkom
  • pogreška: Popis izgrađenih poruka o pogreškama

I naravno, za stvarnu logiku rukovanja iznimkama u proljeće koristit ćemo @ControllerAdvice napomena:

@ControllerAdvice javna klasa CustomRestExceptionHandler proširuje ResponseEntityExceptionHandler {...}

3. Obraditi loše iznimke zahtjeva

3.1. Rukovanje iznimkama

Sada, da vidimo kako se možemo nositi s najčešćim pogreškama klijenta - u osnovi scenariji klijenta poslali su nevažeći zahtjev API-ju:

  • BindException: Ova se iznimka izbacuje kada se pojave fatalne pogreške u vezivanju.
  • MethodArgumentNotValidException: Ova se iznimka baca kada je argument označen s @Valid neuspjela provjera valjanosti:

@Override zaštićen ResponseEntity handleMethodArgumentNotValid (MethodArgumentNotValidException ex, zaglavlja HttpHeaders, status HttpStatus, zahtjev WebRequest) {Pogreške popisa = novi ArrayList (); za (pogreška FieldError: ex.getBindingResult (). getFieldErrors ()) {error.add (error.getField () + ":" + error.getDefaultMessage ()); } za (ObjectError error: ex.getBindingResult (). getGlobalErrors ()) {error.add (error.getObjectName () + ":" + error.getDefaultMessage ()); } ApiError apiError = nova ApiError (HttpStatus.BAD_REQUEST, ex.getLocalizedMessage (), pogreške); return handleExceptionInternal (ex, apiError, zaglavlja, apiError.getStatus (), zahtjev); } 

Kao što vidiš, nadjačavamo osnovnu metodu iz ResponseEntityExceptionHandler i pružanje vlastite prilagođene implementacije.

To neće uvijek biti slučaj - ponekad ćemo trebati obraditi prilagođenu iznimku koja nema zadanu implementaciju u osnovnoj klasi, kao što ćemo vidjeti kasnije ovdje.

Sljedeći:

  • MissingServletRequestPartException: Ova se iznimka baca kada nije pronađen dio višedijelnog zahtjeva

  • MissingServletRequestParameterException: Ova se iznimka baca kada zahtjevu nedostaje parametar:

@Override zaštićen ResponseEntity handleMissingServletRequestParameter (MissingServletRequestParameterException ex, HttpHeaders zaglavlja, HttpStatus status, WebRequest zahtjev) {Greška niza = ex.getParameterName () + "parametar nedostaje"; ApiError apiError = nova ApiError (HttpStatus.BAD_REQUEST, ex.getLocalizedMessage (), pogreška); vrati novi ResponseEntity (apiError, novi HttpHeaders (), apiError.getStatus ()); }
  • ConstrainViolationException: Ova iznimka izvještava o rezultatima kršenja ograničenja:

@ExceptionHandler ({ConstraintViolationException.class}) javni ResponseEntity handleConstraintViolation (ConstraintViolationException ex, zahtjev za WebRequest) {Pogreške popisa = novi ArrayList (); za (kršenje ConstraintViolation: ex.getConstraintViolations ()) {error.add (povreda.getRootBeanClass (). getName () + "" + порушення.getPropertyPath () + ":" + kršenje.getMessage ()); } ApiError apiError = nova ApiError (HttpStatus.BAD_REQUEST, ex.getLocalizedMessage (), pogreške); vrati novi ResponseEntity (apiError, novi HttpHeaders (), apiError.getStatus ()); }
  • TypeMismatchException: Ova se iznimka baca kada pokušavate postaviti svojstvo graha s pogrešnom vrstom.

  • MethodArgumentTypeMismatchException: Ova se iznimka baca kada argument metode nije očekivani tip:

@ExceptionHandler ({MethodArgumentTypeMismatchException.class}) javni ResponseEntity handleMethodArgumentTypeMismatch (MethodArgumentTypeMismatchException ex, WebRequest zahtjev) {Greška niza = ex.getName () + "treba biti tipa" + ex.getRequired; getRequired. ApiError apiError = nova ApiError (HttpStatus.BAD_REQUEST, ex.getLocalizedMessage (), pogreška); vrati novi ResponseEntity (apiError, novi HttpHeaders (), apiError.getStatus ()); }

3.2. Konzumiranje API-ja od klijenta

Pogledajmo sada test koji nailazi na MethodArgumentTypeMismatchException: dobro poslati zahtjev s iskaznica kao Niz umjesto dugo:

@Test public void whenMethodArgumentMismatch_thenBadRequest () {Response response = givenAuth (). Get (URL_PREFIX + "/ api / foos / ccc"); ApiError error = response.as (ApiError.class); assertEquals (HttpStatus.BAD_REQUEST, pogreška.getStatus ()); assertEquals (1, error.getErrors (). size ()); assertTrue (error.getErrors (). get (0) .contains ("trebao bi biti tipa")); }

I na kraju - s obzirom na isti zahtjev::

Način zahtjeva: GET Put zahtjeva: // localhost: 8080 / spring-security-rest / api / foos / ccc 

Evo kako će izgledati ovakav odgovor na JSON pogrešku:

{"status": "BAD_REQUEST", "message": "Nije uspjelo pretvoriti vrijednost tipa [java.lang.String] u traženi tip [java.lang.Long]; ugniježđena iznimka je java.lang.NumberFormatException: Za ulazni niz : \ "ccc \" "," error ": [" id treba biti tipa java.lang.Long "]}

4. Ručka NoHandlerFoundException

Dalje, možemo prilagoditi naš servlet tako da baca ovu iznimku umjesto da pošalje odgovor 404 - kako slijedi:

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

Tada, kad se to dogodi, možemo to jednostavno riješiti kao i bilo koju drugu iznimku:

@Override protected ResponseEntity handleNoHandlerFoundException (NoHandlerFoundException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {String error = "Nije pronađen rukovatelj za" + ex.getHttpMethod () + "" + ex.getRequestURL ApiError apiError = nova ApiError (HttpStatus.NOT_FOUND, ex.getLocalizedMessage (), pogreška); vrati novi ResponseEntity (apiError, novi HttpHeaders (), apiError.getStatus ()); }

Evo jednostavnog testa:

@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, pogreška.getErrors (). size ()); assertTrue (error.getErrors (). get (0) .contains ("Nije pronađen rukovatelj")); }

Pogledajmo puni zahtjev:

Način zahtjeva: DELETE Put zahtjeva: // localhost: 8080 / spring-security-rest / api / xx

I pogreška JSON odgovor:

{"status": "NOT_FOUND", "message": "Nije pronađen obrađivač za DELETE / spring-security-rest / api / xx", "error": ["Nije pronađen obrađivač za DELETE / spring-security-rest / api / xx "]}

5. Ručka HttpRequestMethodNotSupportedException

Dalje, pogledajmo još jednu zanimljivu iznimku - HttpRequestMethodNotSupportedException - što se događa kada šaljete traženi nepodržanom HTTP metodom:

@Override zaštićen ResponseEntity handleHttpRequestMethodNotSupported (HttpRequestMethodNotSupportedException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {StringBuilder builder = new StringBuilder (); builder.append (ex.getMethod ()); builder.append ("metoda nije podržana za ovaj zahtjev. Podržane metode su"); ex.getSupportedHttpMethods (). forEach (t -> builder.append (t + "")); ApiError apiError = nova ApiError (HttpStatus.METHOD_NOT_ALLOWED, ex.getLocalizedMessage (), builder.toString ()); vrati novi ResponseEntity (apiError, novi HttpHeaders (), apiError.getStatus ()); }

Evo jednostavnog testa koji reproducira ovu iznimku:

@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, pogreška.getStatus ()); assertEquals (1, pogreška.getErrors (). size ()); assertTrue (error.getErrors (). get (0) .contains ("Podržane metode su")); }

I evo cijelog zahtjeva:

Način zahtjeva: DELETE Put zahtjeva: // localhost: 8080 / spring-security-rest / api / foos / 1

I JSON odgovor na pogrešku:

{"status": "METHOD_NOT_ALLOWED", "message": "Metoda zahtjeva 'DELETE' nije podržana", "error": ["Metoda DELETE nije podržana za ovaj zahtjev. Podržane metode su GET"]}

6. Ručka HttpMediaTypeNotSupportedException

Ajmo sada HttpMediaTypeNotSupportedException - što se događa kada klijent pošalje zahtjev s nepodržanom vrstom medija - kako slijedi:

@Override zaštićen ResponseEntity handleHttpMediaTypeNotSupported (HttpMediaTypeNotSupportedException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {StringBuilder builder = new StringBuilder (); builder.append (ex.getContentType ()); builder.append ("vrsta medija nije podržana. Podržane vrste medija su"); ex.getSupportedMediaTypes (). forEach (t -> builder.append (t + ",")); ApiError apiError = nova ApiError (HttpStatus.UNSUPPORTED_MEDIA_TYPE, ex.getLocalizedMessage (), builder.substring (0, builder.length () - 2)); vrati novi ResponseEntity (apiError, novi HttpHeaders (), apiError.getStatus ()); }

Evo jednostavnog testa koji se pokreće u ovom izdanju:

@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, pogreška.getStatus ()); assertEquals (1, pogreška.getErrors (). size ()); assertTrue (error.getErrors (). get (0) .contains ("vrsta medija nije podržana")); }

Napokon - evo primjera zahtjeva:

Način zahtjeva: POST Put zahtjeva: // localhost: 8080 / spring-security- Zaglavlja: Content-Type = text / plain; charset = ISO-8859-1

I JSON odgovor na pogrešku:

{"status": "UNSUPPORTED_MEDIA_TYPE", "message": "Tip sadržaja 'text / plain; charset = ISO-8859-1' not supported", "error": ["text / plain; charset = ISO-8859-1 vrsta medija nije podržana. Podržane vrste medija su text / xml application / x-www-form-urlencoded application / * + xml application / json; charset = UTF-8 application / * + json; charset = UTF-8 * / " ]}

7. Zadani rukovatelj

Na kraju, primijenimo zamjenski rukovatelj - sveobuhvatnu vrstu logike koja se bavi svim ostalim iznimkama koje nemaju određene obrađivače:

@ExceptionHandler ({Exception.class}) javni ResponseEntity handleAll (Iznimka ex, zahtjev WebRequest) {ApiError apiError = novi ApiError (HttpStatus.INTERNAL_SERVER_ERROR, ex.getLocalizedMessage (), "dogodila se pogreška"); vrati novi ResponseEntity (apiError, novi HttpHeaders (), apiError.getStatus ()); }

8. Zaključak

Izgradnja ispravnog, zrelog rukovaoca pogreškama za Spring REST API težak je i definitivno iterativni postupak. Nadamo se da će ovaj vodič biti dobra polazna točka za to za vaš API, a također i dobro sidro za to kako trebate pomoći klijentima svog API-ja da brzo i lako dijagnosticiraju pogreške i prijeđu ih dalje.

The puna provedba ovog vodiča možete pronaći u projektu Github - ovo je projekt zasnovan na Eclipseu, pa bi ga trebalo lako uvesti i pokrenuti kakav jest.

OSTALO dno

Upravo sam najavio novo Uči proljeće tečaj, usredotočen na osnove Spring 5 i Spring Boot 2:

>> PROVJERITE TEČAJ