Prilagođeno rukovanje porukama o pogrešci za REST API
Upravo sam najavio novo Uči proljeće tečaj, usredotočen na osnove Spring 5 i Spring Boot 2:
>> PROVJERITE TEČAJ1. 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