Foutafhandeling voor REST met Spring

REST Top

Ik heb zojuist het nieuwe aangekondigd Leer de lente natuurlijk, gericht op de basisprincipes van Spring 5 en Spring Boot 2:

>> BEKIJK DE CURSUS

1. Overzicht

Deze tutorial zal illustreren hoe u Exception Handling met Spring implementeert voor een REST API. We krijgen ook een klein historisch overzicht en zien welke nieuwe opties de verschillende versies hebben geïntroduceerd.

Vóór Spring 3.2 waren de twee belangrijkste benaderingen voor het afhandelen van uitzonderingen in een Spring MVC-toepassing HandlerExceptionResolver of de @BuienRadarNL annotatie. Beiden hebben een aantal duidelijke nadelen.

Sinds 3.2 hebben we de @ControllerAdvice annotatie om de beperkingen van de vorige twee oplossingen aan te pakken en een uniforme afhandeling van uitzonderingen in een hele applicatie te bevorderen.

Nu Spring 5 introduceert de ResponseStatusException klasse - een snelle manier voor eenvoudige foutafhandeling in onze REST API's.

Al deze hebben één ding gemeen: ze hebben te maken met de scheiding van zorgen zeer goed. De app kan normaal gesproken uitzonderingen genereren om een ​​storing aan te geven, die dan afzonderlijk zal worden afgehandeld.

Ten slotte zullen we zien wat Spring Boot naar de tafel brengt en hoe we het kunnen configureren om aan onze behoeften te voldoen.

2. Oplossing 1: het controllerniveau @BuienRadarNL

De eerste oplossing werkt bij de @Controller niveau. We zullen een methode definiëren om uitzonderingen af ​​te handelen en die annoteren met @BuienRadarNL:

openbare klasse FooController {// ... @ExceptionHandler ({CustomException1.class, CustomException2.class}) openbare ongeldige handleException () {//}}

Deze benadering heeft een groot nadeel: T.hij @BuienRadarNL geannoteerde methode is alleen actief voor die specifieke controller, niet globaal voor de hele applicatie. Door dit aan elke controller toe te voegen, is het natuurlijk niet erg geschikt voor een algemeen mechanisme voor het afhandelen van uitzonderingen.

We kunnen deze beperking omzeilen door alle controllers breiden een basiscontrollerklasse uit.

Deze oplossing kan echter een probleem zijn voor toepassingen waar dat om wat voor reden dan ook niet mogelijk is. De controllers kunnen bijvoorbeeld al afkomstig zijn van een andere basisklasse, die zich in een andere pot bevindt of niet direct aanpasbaar is, of zelf niet direct aanpasbaar zijn.

Vervolgens gaan we kijken naar een andere manier om het probleem van het afhandelen van uitzonderingen op te lossen - een die globaal is en geen wijzigingen aan bestaande artefacten zoals controllers bevat.

3. Oplossing 2: het HandlerExceptionResolver

De tweede oplossing is om een HandlerExceptionResolver. Hiermee worden eventuele uitzonderingen opgelost die door de toepassing worden gegenereerd. Het stelt ons ook in staat om een uniform mechanisme voor het afhandelen van uitzonderingen in onze REST API.

Laten we, voordat we voor een aangepaste resolver gaan, de bestaande implementaties bekijken.

3.1. ExceptionHandlerExceptionResolver

Deze resolver is geïntroduceerd in Spring 3.1 en is standaard ingeschakeld in het DispatcherServlet. Dit is eigenlijk de kerncomponent van hoe de @ExceptionHandler mechanisme gepresenteerd eerdere werken.

3.2. DefaultHandlerExceptionResolver

Deze resolver is geïntroduceerd in Spring 3.0 en is standaard ingeschakeld in het DispatcherServlet.

Het wordt gebruikt om standaard Spring-uitzonderingen op te lossen voor hun overeenkomstige HTTP-statuscodes, namelijk Client-fout 4xx en serverfout 5xx statuscodes. Hier is de volle lijst van de Spring Uitzonderingen die het behandelt en hoe deze worden toegewezen aan statuscodes.

Hoewel het de statuscode van het antwoord correct instelt, is er één beperking is dat het niets instelt op de body van de Response. En voor een REST API - de statuscode is echt niet genoeg informatie om aan de klant te presenteren - moet het antwoord ook een body hebben, zodat de applicatie aanvullende informatie over de storing kan geven.

Dit kan worden opgelost door de weergaveresolutie te configureren en foutinhoud weer te geven via ModelAndView, maar de oplossing is duidelijk niet optimaal. Daarom introduceerde Spring 3.2 een betere optie die we in een later gedeelte zullen bespreken.

3.3. ResponseStatusExceptionResolver

Deze resolver is ook geïntroduceerd in Spring 3.0 en is standaard ingeschakeld in het DispatcherServlet.

De belangrijkste verantwoordelijkheid is het gebruik van de @ResponseStatus annotatie beschikbaar voor aangepaste uitzonderingen en om deze uitzonderingen toe te wijzen aan HTTP-statuscodes.

Zo'n aangepaste uitzondering kan er als volgt uitzien:

@ResponseStatus (waarde = HttpStatus.NOT_FOUND) openbare klasse MyResourceNotFoundException breidt RuntimeException uit {openbare MyResourceNotFoundException () {super (); } openbare MyResourceNotFoundException (String bericht, Throwable oorzaak) {super (bericht, oorzaak); } openbare MyResourceNotFoundException (String-bericht) {super (bericht); } openbare MyResourceNotFoundException (Throwable oorzaak) {super (oorzaak); }}

Hetzelfde als de DefaultHandlerExceptionResolver, deze resolver is beperkt in de manier waarop hij omgaat met de hoofdtekst van het antwoord - het brengt de statuscode op het antwoord in kaart, maar de hoofdtekst is nog steeds nul.

3.4. SimpleMappingExceptionResolver en AnnotationMethodHandlerExceptionResolver

De SimpleMappingExceptionResolver bestaat al geruime tijd. Het komt uit het oudere Spring MVC-model en is dat ook niet erg relevant voor een REST-service. We gebruiken het in feite om namen van uitzonderingsklassen in kaart te brengen om namen te bekijken.

De AnnotationMethodHandlerExceptionResolver werd in Spring 3.0 geïntroduceerd om uitzonderingen te verwerken via het @BuienRadarNL annotatie maar is verouderd door ExceptionHandlerExceptionResolver vanaf voorjaar 3.2.

3.5. Op maat HandlerExceptionResolver

De combinatie van DefaultHandlerExceptionResolver en ResponseStatusExceptionResolver gaat een lange weg naar het bieden van een goed foutafhandelingsmechanisme voor een Spring RESTful-service. Het nadeel is, zoals eerder vermeld, geen controle over de kern van de respons.

Idealiter zouden we JSON of XML willen kunnen uitvoeren, afhankelijk van het formaat dat de klant heeft gevraagd (via de Aanvaarden koptekst).

Dit alleen al rechtvaardigt het creëren een nieuwe, aangepaste uitzonderingsresolver:

@Component openbare klasse RestResponseStatusExceptionResolver breidt AbstractHandlerExceptionResolver {@Override beschermd ModelAndView doResolveException (HttpServletRequest-verzoek, HttpServletResponse-antwoord, Objecthandler, Exception ex) {try {if (ex instanceof IllegalArgumentArgumentArgumentArgumentArgumentArgumentArgumentArgumentArgumentArgumentArgumentArgumentArgumentArgumentArgumentArgumentArgumentArgumentArgumentArgumentArgumentArgumentException) } ...} catch (Uitzondering handlerException) {logger.warn ("Behandeling van [" + ex.getClass (). getName () + "] resulteerde in Uitzondering", handlerException); } retourneer null; } private ModelAndView handleIllegalArgument (IllegalArgumentException ex, HttpServletResponse response) gooit IOException {response.sendError (HttpServletResponse.SC_CONFLICT); String accept = request.getHeader (HttpHeaders.ACCEPT); ... retourneer nieuw ModelAndView (); }}

Een detail dat hier moet worden opgemerkt, is dat we toegang hebben tot het verzoek zelf, dus we kunnen de waarde van de Aanvaarden header verzonden door de klant.

Als de klant er bijvoorbeeld om vraagt applicatie / json, dan, in het geval van een foutconditie, willen we ervoor zorgen dat we een antwoordtekst retourneren die is gecodeerd met applicatie / json.

Het andere belangrijke implementatiedetail is dat we retourneren een ModelAndView - dit is de kern van de reactie, en het zal ons in staat stellen om er alles op te zetten wat nodig is.

Deze benadering is een consistent en gemakkelijk configureerbaar mechanisme voor de foutafhandeling van een Spring REST-service.

Het heeft echter beperkingen: het heeft interactie met het lage niveau HtttpServletResponse en past in het oude MVC-model dat ModelAndView, dus er is nog ruimte voor verbetering.

4. Oplossing 3: @ControllerAdvice

Spring 3.2 brengt ondersteuning voor een globale @BuienRadarNL met de @ControllerAdvice annotatie.

Dit maakt een mechanisme mogelijk dat loskomt van het oudere MVC-model en er gebruik van maakt ResponseEntity samen met de typeveiligheid en flexibiliteit van @BuienRadarNL:

@ControllerAdvice openbare klasse RestResponseEntityExceptionHandler breidt ResponseEntityExceptionHandler {@ExceptionHandler (waarde = {IllegalArgumentException.class, IllegalStateException.class}) beschermd ResponseEntity handleConflict (RuntimeException ex, WebRequest-verzoek moet toepassingsspecifiek zijn "; return handleExceptionInternal (ex, bodyOfResponse, nieuwe HttpHeaders (), HttpStatus.CONFLICT, verzoek); }}

De@ControllerAdvice annotatie stelt ons in staat consolideren onze veelvoud, verspreid @BuienRadarNLs van vroeger in een enkele, globale foutafhandelingscomponent.

Het eigenlijke mechanisme is uiterst eenvoudig maar ook zeer flexibel:

  • Het geeft ons volledige controle over de inhoud van het antwoord en de statuscode.
  • Het biedt toewijzing van verschillende uitzonderingen op dezelfde methode, die samen moeten worden afgehandeld.
  • Het maakt goed gebruik van de nieuwere RESTful ResposeEntity reactie.

Een ding om hier in gedachten te houden is om overeenkomen met de uitzonderingen gedeclareerd met @BuienRadarNL op de uitzondering die wordt gebruikt als het argument van de methode.

Als deze niet overeenkomen, zal de compiler niet klagen - zonder reden - en Spring zal ook niet klagen.

Wanneer de uitzondering echter tijdens runtime wordt gegenereerd, het mechanisme voor het oplossen van uitzonderingen zal mislukken:

java.lang.IllegalStateException: Geen geschikte resolver voor argument [0] [type = ...] Details van HandlerMethod: ...

5. Oplossing 4: ResponseStatusException (Lente 5 en hoger)

Spring 5 introduceerde de ResponseStatusException klasse.

We kunnen er een instantie van maken door een HttpStatus en optioneel een reden en een oorzaak:

@GetMapping (value = "/ {id}") openbare Foo findById (@PathVariable ("id") Lange id, HttpServletResponse-reactie) {probeer {Foo resourceById = RestPreconditions.checkFound (service.findOne (id)); eventPublisher.publishEvent (nieuwe SingleResourceRetrievedEvent (this, response)); terug resourceById; } catch (MyResourceNotFoundException exc) {throw nieuwe ResponseStatusException (HttpStatus.NOT_FOUND, "Foo Not Found", exc); }}

Wat zijn de voordelen van gebruik ResponseStatusException?

  • Uitstekend geschikt voor prototyping: we kunnen vrij snel een basisoplossing implementeren.
  • Eén type, meerdere statuscodes: één uitzonderingstype kan tot meerdere verschillende reacties leiden. Dit vermindert strakke koppeling in vergelijking met de @BuienRadarNL.
  • We hoeven niet zoveel aangepaste uitzonderingsklassen te maken.
  • We hebben meer controle over het afhandelen van uitzonderingen aangezien de uitzonderingen programmatisch kunnen worden gemaakt.

En hoe zit het met de afwegingen?

  • Er is geen uniforme manier om uitzonderingen af ​​te handelen: het is moeilijker om bepaalde toepassingsbrede conventies af te dwingen dan @ControllerAdvice, wat een globale benadering biedt.
  • Codeduplicatie: we kunnen merken dat we code repliceren in meerdere controllers.

We moeten ook opmerken dat het mogelijk is om verschillende benaderingen binnen één applicatie te combineren.

We kunnen bijvoorbeeld een @ControllerAdvice wereldwijd maar ook ResponseStatusExceptions lokaal.

We moeten echter voorzichtig zijn: als dezelfde uitzondering op meerdere manieren kan worden afgehandeld, kunnen we verrassend gedrag opmerken. Een mogelijke afspraak is om één specifiek soort uitzondering altijd op één manier af te handelen.

Zie onze tutorial over voor meer details en meer voorbeelden ResponseStatusException.

6. Verwerk de toegang geweigerd in Spring Security

Toegang geweigerd treedt op wanneer een geverifieerde gebruiker toegang probeert te krijgen tot bronnen waartoe hij onvoldoende bevoegdheden heeft.

6.1. MVC - Aangepaste foutpagina

Laten we eerst eens kijken naar de MVC-stijl van de oplossing en zien hoe we een foutpagina kunnen aanpassen voor Toegang geweigerd.

De XML-configuratie:

  ...  

En de Java-configuratie:

@Override protected void configure (HttpSecurity http) gooit uitzondering {http.authorizeRequests () .antMatchers ("/ admin / *"). HasAnyRole ("ROLE_ADMIN") ... .and () .exceptionHandling (). AccessDeniedPage ("/ mijn-foutpagina "); }

Wanneer gebruikers toegang proberen te krijgen tot een bron zonder voldoende bevoegdheden, worden ze omgeleid naar "/ My-error-page".

6.2. Op maat AccessDeniedHandler

Laten we vervolgens kijken hoe we onze gewoonte kunnen schrijven AccessDeniedHandler:

@Component openbare klasse CustomAccessDeniedHandler implementeert AccessDeniedHandler {@Override openbare ongeldige handle (HttpServletRequest-verzoek, HttpServletResponse-reactie, AccessDeniedException ex) gooit IOException, ServletException {response.sendRedirect ("/ mijn-fout-pagina"); }}

En laten we het nu configureren met XML-configuratie:

  ...  

0r met behulp van Java-configuratie:

@Autowired privé CustomAccessDeniedHandler accessDeniedHandler; @Override protected void configure (HttpSecurity http) gooit uitzondering {http.authorizeRequests () .antMatchers ("/ admin / *"). HasAnyRole ("ROLE_ADMIN") ... .and () .exceptionHandling (). AccessDeniedHandler (accessDeniedHandler) }

Merk op hoe in onze CustomAccessDeniedHandler, kunnen we het antwoord naar wens aanpassen door om te leiden of een aangepast foutbericht weer te geven.

6.3. Beveiliging op REST- en methodniveau

Laten we tot slot kijken hoe we om moeten gaan met beveiliging op methodniveau @PreAuthorize, @PostAuthorize, en @Veilig Toegang geweigerd.

Natuurlijk zullen we het globale afhandelingsmechanisme voor uitzonderingen gebruiken dat we eerder hebben besproken om het AccessDeniedException ook:

@ControllerAdvice openbare klasse RestResponseEntityExceptionHandler breidt ResponseEntityExceptionHandler {@ExceptionHandler ({AccessDeniedException.class}) openbare ResponseEntity handleAccessDeniedException (uitzondering ex, WebRequest-verzoek) {retourneer nieuwe ResponseEntity ("Toegang geweigerd bericht hier", nieuwe Httatus). } ...}

7. Spring Boot-ondersteuning

Spring Boot biedt een ErrorController implementatie om op een verstandige manier met fouten om te gaan.

In een notendop, het dient een fallback-foutpagina voor browsers (ook bekend als de Whitelabel-foutpagina) en een JSON-antwoord voor RESTful, niet-HTML-verzoeken:

{"timestamp": "2019-01-17T16: 12: 45.977 + 0000", "status": 500, "error": "Internal Server Error", "message": "Fout bij het verwerken van het verzoek!", "path" : "/ mijn-eindpunt-met-uitzonderingen"}

Zoals gewoonlijk staat Spring Boot het configureren van deze functies toe met eigenschappen:

  • server.error.whitelabel.enabled: kan worden gebruikt om de Whitelabel-foutpagina uit te schakelen en erop te vertrouwen dat de servlet-container een HTML-foutmelding geeft
  • server.error.include-stacktrace: Met een altijd waarde; bevat de stacktrace in zowel de HTML- als het JSON-standaardantwoord

Afgezien van deze eigenschappen, we kunnen onze eigen view-resolver-mapping leveren voor /fout, overschrijven van de Whitelabel-pagina.

We kunnen de attributen die we in het antwoord willen weergeven ook aanpassen door een Foutattributen boon in de context. We kunnen de DefaultErrorAttributes klasse aangeboden door Spring Boot om dingen gemakkelijker te maken:

@Component openbare klasse MyCustomErrorAttributes breidt DefaultErrorAttributes uit {@Override openbare kaart getErrorAttributes (WebRequest webRequest, boolean includeStackTrace) {Map errorAttributes = super.getErrorAttributes (webRequest, includeStackTrace); errorAttributes.put ("locale", webRequest.getLocale () .toString ()); errorAttributes.remove ("fout"); // ... return errorAttributes; }}

Als we verder willen gaan en definiëren (of overschrijven) hoe de applicatie fouten voor een bepaald inhoudstype zal afhandelen, kunnen we een ErrorController Boon.

Nogmaals, we kunnen gebruik maken van de standaard BasicErrorController geleverd door Spring Boot om ons te helpen.

Stel je voor dat we willen aanpassen hoe onze applicatie omgaat met fouten die zijn geactiveerd in XML-eindpunten. Het enige wat we hoeven te doen is een openbare methode definiëren met behulp van de @RequestMapping, en stellen dat het produceert application / xml mediatype:

@Component openbare klasse MyErrorController breidt BasicErrorController uit {openbare MyErrorController (ErrorAttributes errorAttributes) {super (errorAttributes, nieuwe ErrorProperties ()); } @RequestMapping (produceert = MediaType.APPLICATION_XML_VALUE) openbare ResponseEntity xmlError (HttpServletRequest-verzoek) {// ...}}

8. Conclusie

In dit artikel zijn verschillende manieren besproken om een ​​uitzonderingsafhandelingsmechanisme voor een REST API in Spring te implementeren, te beginnen met het oudere mechanisme en door te gaan met de Spring 3.2-ondersteuning en in 4.x en 5.x.

Zoals altijd is de code die in dit artikel wordt gepresenteerd, beschikbaar op GitHub.

Voor de Spring Security-gerelateerde code kunt u de spring-security-rest module raadplegen.

REST onder

Ik heb zojuist het nieuwe aangekondigd Leer de lente natuurlijk, gericht op de basisprincipes van Spring 5 en Spring Boot 2:

>> BEKIJK DE CURSUS