Fejlhåndtering til REST med Spring

REST Top

Jeg har lige annonceret det nye Lær foråret kursus med fokus på det grundlæggende i Spring 5 og Spring Boot 2:

>> KONTROLLER KURSEN

1. Oversigt

Denne vejledning illustrerer hvordan man implementerer Exception Handling with Spring til en REST API. Vi får også lidt historisk overblik og se, hvilke nye muligheder de forskellige versioner introducerede.

Før foråret 3.2 var de to vigtigste tilgange til håndtering af undtagelser i en Spring MVC-applikation HandlerExceptionResolver eller den @ExceptionHandler kommentar. Begge har nogle klare ulemper.

Siden 3.2 har vi haft @ControllerAdvice kommentar at imødegå begrænsningerne i de to foregående løsninger og fremme en samlet håndtering af undtagelser gennem en hel applikation.

Nu Forår 5 introducerer ResponseStatusException klasse - en hurtig måde til grundlæggende fejlhåndtering i vores REST API'er.

Alle disse har én ting til fælles: De beskæftiger sig med adskillelse af bekymringer meget godt. Appen kan kaste undtagelser normalt for at indikere en fejl af en slags, som derefter håndteres separat.

Endelig ser vi, hvad Spring Boot bringer til bordet, og hvordan vi kan konfigurere det, så det passer til vores behov.

2. Løsning 1: Controller-niveau @ExceptionHandler

Den første løsning fungerer på @Kontrol niveau. Vi definerer en metode til at håndtere undtagelser og kommentere den med @ExceptionHandler:

offentlig klasse FooController {// ... @ExceptionHandler ({CustomException1.class, CustomException2.class}) offentlig ugyldig handleException () {//}}

Denne tilgang har en stor ulempe: Than @ExceptionHandler kommenteret metode er kun aktiv for den pågældende controller, ikke globalt for hele applikationen. At tilføje dette til hver controller gør det naturligvis ikke velegnet til en generel mekanisme til undtagelseshåndtering.

Vi kan omgå denne begrænsning ved at have alle controllere udvider en Base Controller-klasse.

Denne løsning kan dog være et problem for applikationer, hvor det af en eller anden grund ikke er muligt. For eksempel kan controllerne allerede strække sig fra en anden basisklasse, som kan være i en anden krukke eller ikke direkte kan ændres, eller måske ikke selv kan ændres direkte.

Dernæst ser vi på en anden måde at løse undtagelseshåndteringsproblemet på - en, der er global og ikke inkluderer ændringer til eksisterende artefakter såsom controllere.

3. Løsning 2: den HandlerExceptionResolver

Den anden løsning er at definere en HandlerExceptionResolver. Dette løser enhver undtagelse fra applikationen. Det vil også give os mulighed for at implementere en ensartet undtagelseshåndteringsmekanisme i vores REST API.

Før vi går efter en brugerdefineret resolver, lad os gå igennem de eksisterende implementeringer.

3.1. ExceptionHandlerExceptionResolver

Denne resolver blev introduceret i foråret 3.1 og er som standard aktiveret i DispatcherServlet. Dette er faktisk kernekomponenten i, hvordan @Undtagelse Håndterer mekanisme præsenteret tidligere værker.

3.2. StandardHandlerExceptionResolver

Denne resolver blev introduceret i Spring 3.0, og den er som standard aktiveret i DispatcherServlet.

Det bruges til at løse standard Spring-undtagelser fra deres tilsvarende HTTP-statuskoder, nemlig klientfejl 4xx og serverfejl 5xx statuskoder. Her er den fuld liste af forårets undtagelser, det håndterer, og hvordan de kortlægges til statuskoder.

Selvom det indstiller statuskoden for svaret korrekt, en begrænsning er, at det ikke indstiller noget til reaktionens krop. Og for en REST API - statuskoden er virkelig ikke nok information til at præsentere for klienten - svaret skal også have et organ for at give applikationen mulighed for at give yderligere oplysninger om fejlen.

Dette kan løses ved at konfigurere visningsopløsning og gengive fejlindhold igennem ModelAndView, men løsningen er tydeligvis ikke optimal. Derfor introducerede Spring 3.2 en bedre mulighed, som vi vil diskutere i et senere afsnit.

3.3. ResponseStatusExceptionResolver

Denne resolver blev også introduceret i Spring 3.0 og er som standard aktiveret i DispatcherServlet.

Dets hovedansvar er at bruge @ResponseStatus kommentar tilgængelig på tilpassede undtagelser og til at kortlægge disse undtagelser til HTTP-statuskoder.

En sådan brugerdefineret undtagelse kan se ud:

@ResponseStatus (værdi = HttpStatus.NOT_FOUND) offentlig klasse MyResourceNotFoundException udvider RuntimeException {public MyResourceNotFoundException () {super (); } offentlig MyResourceNotFoundException (streng besked, kastbar årsag) {super (besked, årsag); } offentlig MyResourceNotFoundException (streng besked) {super (besked); } offentlig MyResourceNotFoundException (Throwable årsag) {super (årsag); }}

Det samme som StandardHandlerExceptionResolver, denne resolver er begrænset i den måde, den behandler svarets krop på - den kortlægger statuskoden på svaret, men kroppen er stadig nul.

3.4. SimpleMappingExceptionResolver og AnnotationMethodeHandlerExceptionResolver

Det SimpleMappingExceptionResolver har eksisteret i nogen tid. Det kommer ud af den ældre Spring MVC-model og er ikke særlig relevant for en REST-tjeneste. Vi bruger det grundlæggende til at kortlægge undtagelsesklassenavne for at se navne.

Det AnnotationMethodeHandlerExceptionResolver blev introduceret i foråret 3.0 for at håndtere undtagelser gennem @ExceptionHandler kommentar, men er udfaset af ExceptionHandlerExceptionResolver fra og med foråret 3.2.

3.5. Brugerdefinerede HandlerExceptionResolver

Kombinationen af StandardHandlerExceptionResolver og ResponseStatusExceptionResolver går langt i retning af at levere en god fejlhåndteringsmekanisme til en Spring RESTful Service. Ulempen er som nævnt før ingen kontrol over kroppen af ​​svaret.

Ideelt set vil vi gerne kunne udsende enten JSON eller XML, afhængigt af hvilket format klienten har bedt om (via Acceptere header).

Dette alene retfærdiggør oprettelsen en ny, tilpasset undtagelsesopløsning:

@Komponent offentlig klasse RestResponseStatusExceptionResolver udvider AbstractHandlerExceptionResolver {@Override-beskyttet ModelAndView doResolveException (HttpServletRequest-anmodning, HttpServletResponse-svar, Objektbehandler, Exception ex) {prøv {if (ex instanceof IllegalArgal) } ...} catch (Exception handlerException) {logger.warn ("Håndtering af [" + ex.getClass (). getName () + "] resulterede i Exception", handlerException); } returnere null; } privat ModelAndView handleIllegalArgument (IllegalArgumentException ex, HttpServletResponse respons) kaster IOException {respons.sendError (HttpServletResponse.SC_CONFLICT); String accept = request.getHeader (HttpHeaders.ACCEPT); ... returner ny ModelAndView (); }}

En detalje at bemærke her er, at vi har adgang til anmodning sig selv, så vi kan overveje værdien af Acceptere header sendt af klienten.

For eksempel hvis klienten beder om ansøgning / json, så i tilfælde af en fejltilstand, vil vi sørge for, at vi returnerer et svarorgan kodet med ansøgning / json.

Den anden vigtige implementeringsdetalje er, at vi vender tilbage a ModelAndView - dette er svarets krop, og det giver os mulighed for at sætte det, der er nødvendigt, på det.

Denne tilgang er en konsekvent og let konfigurerbar mekanisme til fejlhåndtering af en Spring REST-service.

Det har dog begrænsninger: Det interagerer med lavt niveau HtttpServletResponse og passer ind i den gamle MVC-model, der bruger ModelAndView, så der er stadig plads til forbedringer.

4. Løsning 3: @ControllerAdvice

Forår 3.2 giver støtte til en global @ExceptionHandler med @ControllerAdvice kommentar.

Dette muliggør en mekanisme, der bryder væk fra den ældre MVC-model og gør brug af SvarEnhed sammen med typen sikkerhed og fleksibilitet @ExceptionHandler:

@ControllerAdvice offentlig klasse RestResponseEntityExceptionHandler udvider ResponseEntityExceptionHandler {@ExceptionHandler (værdi = {IllegalArgumentException.class, IllegalStateException.class}) beskyttet ResponseEntity handleConflict (RuntimeException-anmodning). return handleExceptionInternal (ex, bodyOfResponse, nye HttpHeaders (), HttpStatus.CONFLICT, anmodning); }}

Det@ControllerAdvice kommentar tillader os at konsolidere vores flere, spredte @ExceptionHandlers fra før til en enkelt, global fejlhåndteringskomponent.

Den aktuelle mekanisme er ekstremt enkel, men også meget fleksibel:

  • Det giver os fuld kontrol over reaktionens krop samt statuskoden.
  • Det giver kortlægning af flere undtagelser fra den samme metode, der skal håndteres sammen.
  • Det gør god brug af den nyere RESTful ResposeEntity respons.

En ting at huske på her er at matche de undtagelser, der er angivet med @ExceptionHandler til undtagelsen brugt som argument for metoden.

Hvis disse ikke stemmer overens, vil compileren ikke klage - ingen grund til det - og Spring vil heller ikke klage.

Når undtagelsen faktisk kastes ved kørsel, undtagelsesløsningsmekanismen mislykkes med:

java.lang.IllegalStateException: Ingen passende resolver til argument [0] [type = ...] HandlerMethod detaljer: ...

5. Løsning 4: ResponseStatusException (Forår 5 og derover)

Forår 5 introducerede ResponseStatusException klasse.

Vi kan oprette en forekomst af det, der giver en HttpStatus og eventuelt en grund og en årsag:

@GetMapping (værdi = "/ {id}") offentlig Foo findById (@PathVariable ("id") Lang id, HttpServletResponse svar) {prøv {Foo resourceById = RestPreconditions.checkFound (service.findOne (id)); eventPublisher.publishEvent (nyt SingleResourceRetrievedEvent (dette svar)); return resourceById; } fange (MyResourceNotFoundException exc) {throw new ResponseStatusException (HttpStatus.NOT_FOUND, "Foo Not Found", exc); }}

Hvad er fordelene ved at bruge ResponseStatusException?

  • Fremragende til prototyping: Vi kan implementere en grundlæggende løsning ret hurtigt.
  • Én type, flere statuskoder: En undtagelsestype kan føre til flere forskellige svar. Dette reducerer tæt kobling sammenlignet med @ExceptionHandler.
  • Vi behøver ikke oprette så mange tilpassede undtagelsesklasser.
  • Vi har mere kontrol over undtagelseshåndtering da undtagelserne kan oprettes programmatisk.

Og hvad med afvejningerne?

  • Der er ingen ensartet måde at håndtere undtagelser på: Det er sværere at håndhæve nogle anvendelsesdækkende konventioner i modsætning til @ControllerAdvice, som giver en global tilgang.
  • Kopiering af kode: Vi kan finde os selv i at replikere kode i flere controllere.

Vi skal også bemærke, at det er muligt at kombinere forskellige tilgange inden for en applikation.

For eksempel kan vi implementere en @ControllerAdvice globalt, men også ResponseStatusExceptions lokalt.

Vi skal dog være forsigtige: Hvis den samme undtagelse kan håndteres på flere måder, bemærker vi muligvis en overraskende opførsel. En mulig konvention er at håndtere en bestemt slags undtagelse altid på én måde.

For flere detaljer og yderligere eksempler, se vores vejledning om ResponseStatusException.

6. Håndter adgangen nægtet i forårssikkerhed

Adgang nægtet opstår, når en godkendt bruger forsøger at få adgang til ressourcer, som han ikke har nok autoriteter til at få adgang til.

6.1. MVC - side med brugerdefineret fejl

Lad os først se på MVC-stilen på løsningen og se, hvordan man tilpasser en fejlside til Access Denied.

XML-konfigurationen:

  ...  

Og Java-konfigurationen:

@ Override beskyttet ugyldig konfiguration (HttpSecurity http) kaster undtagelse {http.authorizeRequests () .antMatchers ("/ admin / *"). HasAnyRole ("ROLE_ADMIN") ... .og () .exceptionHandling (). AccessDeniedPage ("/ min-fejl-side "); }

Når brugere forsøger at få adgang til en ressource uden at have nok myndigheder, omdirigeres de til “/ Min-fejl-side”.

6.2. Brugerdefinerede AccessDeniedHandler

Lad os derefter se, hvordan vi skriver vores brugerdefinerede AccessDeniedHandler:

@Komponent offentlig klasse CustomAccessDeniedHandler implementerer AccessDeniedHandler {@Override offentligt ugyldigt håndtag (HttpServletRequest anmodning, HttpServletResponse svar, AccessDeniedException ex) kaster IOException, ServletException {respons.sendRedirect ("/ min-fejl-side") }}

Og lad os nu konfigurere det ved hjælp af XML-konfiguration:

  ...  

0r ved hjælp af Java-konfiguration:

@Autowired privat CustomAccessDeniedHandler accessDeniedHandler; @ Override beskyttet ugyldig konfiguration (HttpSecurity http) kaster undtagelse {http.authorizeRequests () .antMatchers ("/ admin / *"). HasAnyRole ("ROLE_ADMIN") ... .og () .exceptionHandling (). AccessDeniedHandler (accessDeniedHandler) }

Bemærk hvordan i vores CustomAccessDeniedHandler, kan vi tilpasse svaret som vi ønsker ved at omdirigere eller vise en brugerdefineret fejlmeddelelse.

6.3. SIKKERHED og sikkerhedsmetode

Lad os endelig se, hvordan man håndterer sikkerhed på metodeniveau @PreAuthorize, @PostAuthorizeog @Sikker Adgang nægtet.

Selvfølgelig bruger vi den globale undtagelseshåndteringsmekanisme, som vi diskuterede tidligere, til at håndtere AccessDeniedException såvel:

@ControllerAdvice offentlig klasse RestResponseEntityExceptionHandler udvider ResponseEntityExceptionHandler {@ExceptionHandler ({AccessDeniedException.class}) public ResponseEntity handleAccessDeniedException (Exception ex, WebRequest request) {return new ResponseEntity ("Adgang nægtet her," Http. } ...}

7. Spring Boot Support

Spring Boot giver en ErrorController implementering for at håndtere fejl på en fornuftig måde.

I en nøddeskal serverer den en fejlsidefejlside for browsere (altså Whitelabel Error Page) og et JSON-svar til RESTful, ikke-HTML-anmodninger:

{"tidsstempel": "2019-01-17T16: 12: 45.977 + 0000", "status": 500, "error": "Intern serverfejl", "meddelelse": "Fejl ved behandling af anmodningen!", "sti" : "/ mit-slutpunkt-med-undtagelser"}

Som normalt tillader Spring Boot at konfigurere disse funktioner med egenskaber:

  • server.error.whitelabel.enabled: kan bruges til at deaktivere Whitelabel Error Page og stole på servletbeholderen for at give en HTML-fejlmeddelelse
  • server.error.include-stacktrace: med en altid værdi; inkluderer stacktrace i både HTML- og JSON-standardsvaret

Bortset fra disse egenskaber, vi kan levere vores egen kortlægning af view-resolver til /fejl, tilsidesætter Whitelabel-siden.

Vi kan også tilpasse de attributter, som vi vil vise i svaret, ved at inkludere en Fejlattributter bønne i sammenhængen. Vi kan udvide StandardErrorAttributter klasse leveret af Spring Boot for at gøre tingene lettere:

@Komponent offentlig klasse MyCustomErrorAttributes udvider DefaultErrorAttributter {@Override public Map getErrorAttributes (WebRequest webRequest, boolean includeStackTrace) {Map errorAttributes = super.getErrorAttributes (webRequest, includeStackTrace) errorAttributes.put ("locale", webRequest.getLocale () .toString ()); errorAttributes.remove ("fejl"); // ... return errorAttributter; }}

Hvis vi vil gå videre og definere (eller tilsidesætte), hvordan applikationen håndterer fejl for en bestemt indholdstype, kan vi registrere en ErrorController bønne.

Igen kan vi gøre brug af standard BasicErrorController leveret af Spring Boot for at hjælpe os.

Forestil dig f.eks., At vi vil tilpasse, hvordan vores applikation håndterer fejl udløst i XML-slutpunkter. Alt hvad vi skal gøre er at definere en offentlig metode ved hjælp af @RequestMappingog med angivelse af, at det producerer applikation / xml medietype:

@Komponent offentlig klasse MyErrorController udvider BasicErrorController {public MyErrorController (ErrorAttributes errorAttributes) {super (errorAttributes, new ErrorProperties ()); } @RequestMapping (producerer = MediaType.APPLICATION_XML_VALUE) offentlig ResponseEntity xmlError (HttpServletRequest anmodning) {// ...}}

8. Konklusion

Denne artikel diskuterede flere måder at implementere en undtagelseshåndteringsmekanisme for en REST API om foråret, begyndende med den ældre mekanisme og fortsatte med Spring 3.2-support og ind i 4.x og 5.x.

Som altid er koden præsenteret i denne artikel tilgængelig på GitHub.

For forårssikkerhedsrelateret kode kan du kontrollere fjeder-sikkerheds-hvilemodulet.

REST bunden

Jeg har lige annonceret det nye Lær foråret kursus med fokus på det grundlæggende i Spring 5 og Spring Boot 2:

>> KONTROLLER KURSEN