Rukovanje pogreškama za REST s Springom

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

Ovaj vodič će ilustrirati kako implementirati rukovanje iznimkama s Springom za REST API. Također ćemo dobiti malo povijesnog pregleda i vidjeti koje su nove opcije uvedene u različitim verzijama.

Prije proljeća 3.2, bila su dva glavna pristupa rješavanju iznimaka u Spring MVC aplikaciji HandlerExceptionResolver ili @ExceptionHandler bilješka. Oboje imaju neke očite nedostatke.

Od 3.2. Imamo @ControllerAdvice bilješka za rješavanje ograničenja prethodna dva rješenja i promicanje jedinstvenog postupanja s iznimkama u cijeloj aplikaciji.

Sada Proljeće 5 predstavlja ResponseStatusException razred - brz način za osnovno rješavanje pogrešaka u našim REST API-ima.

Svima im je zajedničko jedno: bave se razdvajanje zabrinutosti vrlo dobro. Aplikacija može izbaciti iznimke kako bi ukazala na neku vrstu neuspjeha, koji će se zatim obrađivati ​​odvojeno.

Napokon ćemo vidjeti što Spring Boot donosi na stol i kako ga možemo konfigurirati tako da odgovara našim potrebama.

2. Rješenje 1: Razina kontrolera @ExceptionHandler

Prvo rješenje djeluje na @Controller nivo. Definirat ćemo metodu za rukovanje iznimkama i to označiti s @ExceptionHandler:

javna klasa FooController {// ... @ExceptionHandler ({CustomException1.class, CustomException2.class}) public void handleException () {//}}

Ovaj pristup ima glavni nedostatak: Ton @ExceptionHandler anotirana metoda aktivna je samo za taj određeni kontroler, ne globalno za cijelu aplikaciju. Naravno, dodavanjem ovog na svaki kontroler čini ga neprikladnim za opći mehanizam za rukovanje iznimkama.

To ograničenje možemo zaobići tako što ćemo svi kontroleri proširuju osnovnu klasu kontrolera.

Međutim, ovo rješenje može predstavljati problem aplikacijama u kojima to iz bilo kojeg razloga nije moguće. Na primjer, kontroleri se već mogu proširiti iz druge osnovne klase, koja se može nalaziti u drugoj posudi ili se ne može izravno mijenjati, ili se sami ne mogu izravno mijenjati.

Dalje ćemo pogledati još jedan način za rješavanje problema s rukovanjem iznimkama - onaj koji je globalni i ne uključuje nikakve promjene na postojećim artefaktima, poput Controllera.

3. Rješenje 2: HandlerExceptionResolver

Drugo rješenje je definiranje HandlerExceptionResolver. Ovo će riješiti svaku iznimku koju je izbacila aplikacija. Omogućit će nam i primjenu a jedinstveni mehanizam za rukovanje iznimkama u našem REST API-ju.

Prije nego što krenemo po prilagođavaču, prijeđimo na postojeće implementacije.

3.1. ExceptionHandlerExceptionResolver

Ovaj je rješivač predstavljen u proljeće 3.1. I omogućen je prema zadanim postavkama u DispatcherServlet. Ovo je zapravo srž komponente kako @ExceptionHandler mehanizam predstavljen u ranijim radovima.

3.2. DefaultHandlerExceptionResolver

Ovaj je rješivač predstavljen u proljeće 3.0, a omogućen je prema zadanim postavkama u DispatcherServlet.

Koristi se za rješavanje standardnih izuzetaka Spring za njihove odgovarajuće HTTP statusne kodove, odnosno grešku klijenta 4xx i pogreška poslužitelja 5xx statusni kodovi. Evo puni popis proljetnih iznimki kojima se bavi i kako se mapiraju u statusne kodove.

Iako ispravno postavlja statusni kod odgovora, jedan ograničenje je da on ničemu ne postavlja tijelo Odgovora. A za REST API - statusni kôd stvarno nije dovoljan podatak za predstavljanje klijentu - odgovor mora imati i tijelo, kako bi aplikacija mogla dati dodatne informacije o neuspjehu.

To se može riješiti konfiguriranjem razlučivosti pogleda i prikazivanjem sadržaja pogreške ModelAndView, ali rješenje očito nije optimalno. Zbog toga je proljeće 3.2 predstavilo bolju opciju o kojoj ćemo razgovarati u kasnijem odjeljku.

3.3. ResponseStatusExceptionResolver

Ovaj je rješivač također predstavljen u proljeće 3.0 i omogućen je prema zadanim postavkama u DispatcherServlet.

Njegova je glavna odgovornost koristiti @ResponseStatus napomena dostupna na prilagođenim iznimkama i za preslikavanje tih iznimki na HTTP statusne kodove.

Takva prilagođena iznimka može izgledati ovako:

@ResponseStatus (value = HttpStatus.NOT_FOUND) javna klasa MyResourceNotFoundException produžuje RuntimeException {public MyResourceNotFoundException () {super (); } javni MyResourceNotFoundException (String poruka, uzrok za bacanje) {super (poruka, uzrok); } javni MyResourceNotFoundException (String poruka) {super (poruka); } javni MyResourceNotFoundException (uzrok za bacanje) {super (uzrok); }}

Isto kao i DefaultHandlerExceptionResolver, ovaj je rješivač ograničen u načinu na koji se bavi tijelom odgovora - mapira statusni kod na odgovoru, ali tijelo je još uvijek null.

3.4. SimpleMappingExceptionResolver i AnnotationMethodHandlerExceptionResolver

The SimpleMappingExceptionResolver postoji već dosta dugo. Izlazi iz starijeg modela Spring MVC i jest nije vrlo relevantno za REST uslugu. U osnovi ga koristimo za mapiranje imena klasa izuzetaka za prikaz imena.

The AnnotationMethodHandlerExceptionResolver je predstavljen u proljeće 3.0 za rješavanje iznimaka putem @ExceptionHandler napomena, ali je zastario ExceptionHandlerExceptionResolver od proljeća 3.2.

3.5. Prilagođen HandlerExceptionResolver

Kombinacija DefaultHandlerExceptionResolver i ResponseStatusExceptionResolver ide daleko do pružanja dobrog mehanizma za rješavanje pogrešaka za Spring RESTful uslugu. Loša strana je, kao što je već spomenuto, nema kontrole nad tijelom odgovora.

Idealno bi bilo da imamo mogućnost izdavanja JSON-a ili XML-a, ovisno o formatu koji je klijent zatražio (putem Prihvatiti Zaglavlje).

To samo opravdava stvaranje novi, prilagođeni rješivač iznimki:

@Component javna klasa RestResponseStatusExceptionResolver proširuje AbstractHandlerExceptionResolver {@Override protected ModelAndView doResolveException (HttpServletRequest request, HttpServletRespoment response (return instance) } ...} catch (Exception handlerException) {logger.warn ("Rukovanje sa [" + ex.getClass (). getName () + "] rezultiralo je iznimkom", handlerException); } return null; } private ModelAndView handleIllegalArgument (IllegalArgumentException ex, HttpServletResponse response) baca IOException {response.sendError (HttpServletResponse.SC_CONFLICT); String accept = request.getHeader (HttpHeaders.ACCEPT); ... vrati novi ModelAndView (); }}

Ovdje treba primijetiti jedan detalj da imamo pristup zahtjev sam, pa možemo uzeti u obzir vrijednost Prihvatiti zaglavlje koje je poslao klijent.

Na primjer, ako klijent zatraži aplikacija / json, tada bismo, u slučaju stanja pogreške, htjeli biti sigurni da vraćamo tijelo odgovora kodirano s aplikacija / json.

Drugi važan detalj provedbe je taj vraćamo a ModelAndView - ovo je tijelo odgovora, i omogućit će nam da na njemu postavimo sve što je potrebno.

Ovaj pristup je dosljedan i lako podesiv mehanizam za rukovanje pogreškama Spring REST usluge.

Ipak, ima ograničenja: komunicira s niskom razinom HtttpServletResponse i uklapa se u stari MVC model koji koristi ModelAndView, tako da još ima prostora za poboljšanje.

4. Rješenje 3: @ControllerAdvice

Proljeće 3.2 donosi podršku za globalna @ExceptionHandler s @ControllerAdvice bilješka.

To omogućuje mehanizam koji se odvaja od starijeg MVC modela i koristi ResponseEntity zajedno s vrstom sigurnosti i fleksibilnosti @ExceptionHandler:

@ControllerAdvice javna klasa RestResponseEntityExceptionHandler proširuje ResponseEntityExceptionHandler {@ExceptionHandler (value = {IllegalArgumentException.class, IllegalStateException.class}) zaštićen ResponseEntition ResponseEntity bodyConfiscentRecEncetTextEntityRecEncetTextEntityTextConfrexcetTextEntityText return handleExceptionInternal (ex, bodyOfResponse, novi HttpHeaders (), HttpStatus.CONFLICT, zahtjev); }}

The@ControllerAdvice anotacija nam omogućuje objediniti naše višestruke, raštrkane @ExceptionHandlers od prije u jednu, globalnu komponentu za rukovanje pogreškama.

Stvarni mehanizam izuzetno je jednostavan, ali i vrlo fleksibilan:

  • Pruža nam potpunu kontrolu nad tijelom odgovora kao i statusnim kodom.
  • Pruža mapiranje nekoliko iznimaka na istu metodu, kojima se treba rukovati zajedno.
  • Dobro koristi noviji RESTful ResposeEntity odgovor.

Ovdje treba imati na umu jedno podudaranje izuzetaka deklariranih sa @ExceptionHandler do iznimke koja se koristi kao argument metode.

Ako se ovi ne podudaraju, prevoditelj se neće žaliti - bez razloga što bi to trebao činiti - a ni Spring se neće žaliti.

Međutim, kada se iznimka zapravo baci u vrijeme izvođenja, mehanizam za rješavanje iznimki neće uspjeti:

java.lang.IllegalStateException: Nema prikladnog razrješivača za argument [0] [type = ...] Pojedinosti metode obrađivača: ...

5. Rješenje 4: ResponseStatusException (Proljeće 5 i više)

Proljeće 5 je predstavilo ResponseStatusException razred.

Možemo stvoriti instancu da pruža HttpStatus i po želji a razlog i a uzrok:

@GetMapping (value = "/ {id}") javni Foo findById (@PathVariable ("id") Long id, HttpServletResponse odgovor) {try {Foo resourceById = RestPreconditions.checkFound (service.findOne (id)); eventPublisher.publishEvent (novi SingleResourceRetrievedEvent (ovaj, odgovor)); vratiti resourceById; } catch (MyResourceNotFoundException exc) {throw new ResponseStatusException (HttpStatus.NOT_FOUND, "Foo Not Found", osim); }}

Koje su prednosti korištenja ResponseStatusException?

  • Izvrsno za izradu prototipova: osnovno rješenje možemo implementirati vrlo brzo.
  • Jedna vrsta, više statusnih kodova: Jedna vrsta iznimke može dovesti do više različitih odgovora. To smanjuje čvrsto spajanje u usporedbi s @ExceptionHandler.
  • Nećemo morati stvoriti toliko prilagođenih klasa iznimki.
  • Imamo veća kontrola nad rukovanjem iznimkama budući da se iznimke mogu stvarati programski.

A što je s kompromisima?

  • Ne postoji jedinstveni način rješavanja iznimki: teže je provesti neke konvencije za cijelu aplikaciju, a ne @ControllerAdvice, koji pruža globalni pristup.
  • Dupliciranje koda: Možda se nalazimo kako repliciramo kôd u više kontrolera.

Također bismo trebali primijetiti da je moguće kombinirati različite pristupe unutar jedne aplikacije.

Na primjer, možemo implementirati a @ControllerAdvice globalno ali i ResponseStatusExceptions lokalno.

Međutim, moramo biti oprezni: ako se s istom iznimkom može postupati na više načina, mogli bismo primijetiti neko iznenađujuće ponašanje. Moguća konvencija je da se jedna posebna vrsta iznimke obrađuje uvijek na jedan način.

Za više detalja i daljnje primjere pogledajte naš vodič ResponseStatusException.

6. Rukovanje pristupom odbijenom u proljetnoj sigurnosti

Pristup odbijen događa se kada ovjereni korisnik pokuša pristupiti resursima kojima nema dovoljno ovlasti za pristup.

6.1. MVC - Stranica s prilagođenom pogreškom

Prvo, pogledajmo MVC stil rješenja i vidjet ćemo kako prilagoditi stranicu s pogreškom za Pristup odbijen.

Konfiguracija XML-a:

  ...  

I Java konfiguracija:

@Override zaštićena void konfiguracija (HttpSecurity http) baca iznimku {http.authorizeRequests () .antMatchers ("/ admin / *"). HasAnyRole ("ROLE_ADMIN") ... .and () .exceptionHandling (). AccessDeniedPage ("/ moja-pogreška-stranica "); }

Kad korisnici pokušaju pristupiti resursu bez dovoljno ovlasti, bit će preusmjereni na “/ Moja-pogreška-stranica”.

6.2. Prilagođen AccessDeniedHandler

Dalje, pogledajmo kako napisati svoj običaj AccessDeniedHandler:

@Component javna klasa CustomAccessDeniedHandler implementira AccessDeniedHandler {@Override javna ručka za poništavanje (zahtjev HttpServletRequest, odgovor HttpServletResponse, AccessDeniedException ex) baca IOException, ServletException {page.sendRedirect; "/" }}

A sada ga konfigurirajmo pomoću XML konfiguracija:

  ...  

0r pomoću Java konfiguracije:

@Autowired private CustomAccessDeniedHandler accessDeniedHandler; @Override zaštićena void konfiguracija (HttpSecurity http) baca izuzetak {http.authorizeRequests () .antMatchers ("/ admin / *"). HasAnyRole ("ROLE_ADMIN") ... .and () .exceptionHandling (). AccessDeniedHandler (accessDeniedHandler }

Primijetite kako u našem CustomAccessDeniedHandler, odgovor možemo prilagoditi kako želimo preusmjeravanjem ili prikazivanjem prilagođene poruke o pogrešci.

6.3. REST i sigurnost na razini metode

Napokon, pogledajmo kako postupati sa sigurnošću na razini metode @PreAuthorize, @PostAuthorize, i @Siguran Pristup odbijen.

Naravno, koristit ćemo globalni mehanizam za rukovanje iznimkama o kojem smo ranije razgovarali za rukovanje AccessDeniedException također:

@ControllerAdvice javna klasa RestResponseEntityExceptionHandler proširuje ResponseEntityExceptionHandler {@ExceptionHandler ({AccessDeniedException.class}) javni ResponseEntity handleAccessDeniedException (izuzetak ex, WebRequest ZahtjevEB (novi zahtev) } ...}

7. Proljetna podrška za podizanje sustava

Spring Boot nudi ErrorController implementacija za rukovanje pogreškama na razuman način.

Ukratko, služi rezervnu stranicu s pogreškama za preglednike (odnosno stranicu pogreške Whitelabel) i JSON odgovor za RESTful, ne-HTML zahtjeve:

{"timestamp": "2019-01-17T16: 12: 45.977 + 0000", "status": 500, "error": "Internal Server Error", "message": "Pogreška pri obradi zahtjeva!", "path" : "/ my-endpoint-with-exceptions"}

Kao i obično, Spring Boot omogućuje konfiguriranje ovih značajki sa svojstvima:

  • server.error.whitelabel.enabled: može se koristiti za onemogućavanje stranice pogreške Whitelabel i oslanjati se na spremnik servleta koji će pružiti HTML poruku o pogrešci
  • server.error.include-stacktrace: s an stalno vrijednost; uključuje stacktrace i u HTML i u JSON zadanom odgovoru

Osim ovih svojstava, možemo pružiti vlastito mapiranje rezolucije pogleda za /pogreška, nadjačavanje stranice Whitelabel.

Atribute koje želimo prikazati u odgovoru također možemo prilagoditi uključivanjem znaka ErrorAttributes grah u kontekstu. Možemo produžiti DefaultErrorAttributes klasa koju pruža Spring Boot kako bi vam olakšao stvari:

@Component javna klasa MyCustomErrorAttributes proširuje DefaultErrorAttributes {@Override javna karta getErrorAttributes (WebRequest webRequest, boolean includeStackTrace) {Map errorAttributes = super.getErrorAttributes (webRequest, includeStackTrace); errorAttributes.put ("locale", webRequest.getLocale () .toString ()); errorAttributes.remove ("pogreška"); // ... return errorAttributes; }}

Ako želimo ići dalje i definirati (ili poništiti) kako će aplikacija rješavati pogreške za određenu vrstu sadržaja, možemo registrirati ErrorController grah.

Opet, možemo se poslužiti zadanim BasicErrorController pruža Spring Boot da bi nam pomogao.

Na primjer, zamislimo da želimo prilagoditi način na koji naša aplikacija obrađuje pogreške pokrenute u XML krajnjim točkama. Sve što moramo učiniti je definirati javnu metodu pomoću @RequestMapping, i navodeći da proizvodi aplikacija / xml vrsta medija:

@Component javna klasa MyErrorController proširuje BasicErrorController {public MyErrorController (ErrorAttributes errorAttributes) {super (errorAttributes, nove ErrorProperties ()); } @RequestMapping (proizvodi = MediaType.APPLICATION_XML_VALUE) javni ResponseEntity xmlError (zahtjev za HttpServletRequest) {// ...}}

8. Zaključak

Ovaj je članak raspravljao o nekoliko načina primjene mehanizma za rukovanje iznimkama za REST API u proljeće, počevši od starijeg mehanizma i nastavljajući s podrškom za Spring 3.2 pa sve do 4.x i 5.x.

Kao i uvijek, kôd predstavljen u ovom članku dostupan je na GitHubu.

Za kod koji se odnosi na Spring Spring možete provjeriti modul Spring-Security-rest.

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