Penanganan Kesalahan untuk REST dengan Spring

REST Top

Saya baru saja mengumumkan kursus Learn Spring baru , yang berfokus pada dasar-dasar Spring 5 dan Spring Boot 2:

>> LIHAT KURSUSnya

1. Ikhtisar

Tutorial ini akan menggambarkan cara mengimplementasikan Exception Handling dengan Spring untuk REST API. Kami juga akan mendapatkan sedikit gambaran sejarah dan melihat opsi baru mana yang diperkenalkan oleh berbagai versi.

Sebelum Spring 3.2, dua pendekatan utama untuk menangani pengecualian dalam aplikasi MVC Spring adalah HandlerExceptionResolver atau anotasi @ExceptionHandler . Keduanya memiliki beberapa kelemahan yang jelas.

Sejak 3.2, kami memiliki anotasi @ControllerAdvice untuk mengatasi keterbatasan dua solusi sebelumnya dan untuk mempromosikan penanganan pengecualian terpadu di seluruh aplikasi.

Sekarang Spring 5 memperkenalkan kelas ResponseStatusException - cara cepat untuk penanganan kesalahan dasar di REST API kami.

Semua ini memiliki satu kesamaan: Mereka menangani pemisahan perhatian dengan sangat baik. Aplikasi dapat menampilkan pengecualian secara normal untuk menunjukkan beberapa jenis kegagalan, yang kemudian akan ditangani secara terpisah.

Terakhir, kita akan melihat apa yang dibawa Spring Boot dan bagaimana kita dapat mengkonfigurasinya agar sesuai dengan kebutuhan kita.

2. Solusi 1: Level-Kontrol @ExceptionHandler

Solusi pertama berfungsi di level @Controller . Kami akan menentukan metode untuk menangani pengecualian dan menjelaskannya dengan @ExceptionHandler :

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

Pendekatan ini memiliki kelemahan utama: T ia @ExceptionHandler metode dijelaskan hanya aktif untuk itu Pengontrol tertentu , tidak secara global untuk seluruh aplikasi. Tentu saja, menambahkan ini ke setiap pengontrol membuatnya tidak cocok untuk mekanisme penanganan pengecualian umum.

Kita dapat mengatasi batasan ini dengan meminta semua Pengontrol memperluas kelas Pengontrol Basis.

Namun, solusi ini bisa menjadi masalah untuk aplikasi yang tidak memungkinkan karena alasan apa pun. Misalnya, Pengendali mungkin sudah diperluas dari kelas dasar lain, yang mungkin ada di tabung lain atau tidak dapat dimodifikasi secara langsung, atau mungkin sendiri tidak dapat dimodifikasi secara langsung.

Selanjutnya, kita akan melihat cara lain untuk menyelesaikan masalah penanganan pengecualian - yang bersifat global dan tidak menyertakan perubahan apa pun pada artefak yang ada seperti Pengontrol.

3. Solusi 2: HandlerExceptionResolver

Solusi kedua adalah mendefinisikan HandlerExceptionResolver. Ini akan menyelesaikan semua pengecualian yang dilemparkan oleh aplikasi. Ini juga akan memungkinkan kita untuk mengimplementasikan mekanisme penanganan pengecualian yang seragam di REST API kita.

Sebelum menggunakan resolver khusus, mari kita bahas implementasi yang ada.

3.1. ExceptionHandlerExceptionResolver

Penyelesai ini diperkenalkan pada Spring 3.1 dan diaktifkan secara default di DispatcherServlet . Ini sebenarnya adalah komponen inti dari cara kerja mekanisme @ ExceptionHandler yang disajikan sebelumnya.

3.2. DefaultHandlerExceptionResolver

Penyelesai ini diperkenalkan di Spring 3.0, dan diaktifkan secara default di DispatcherServlet .

Ini digunakan untuk menyelesaikan pengecualian Spring standar ke Kode Status HTTP yang sesuai, yaitu kode status Kesalahan klien 4xx dan Kesalahan server 5xx . Berikut daftar lengkap Pengecualian Musim Semi yang ditangani dan bagaimana peta dipetakan ke kode status.

Meskipun itu mengatur Kode Status Tanggapan dengan benar, satu batasan adalah bahwa ia tidak mengatur apa pun ke tubuh Tanggapan. Dan untuk REST API - Kode Status sebenarnya tidak cukup informasi untuk disajikan kepada Klien - respons harus memiliki badan juga, untuk memungkinkan aplikasi memberikan informasi tambahan tentang kegagalan.

Ini dapat diselesaikan dengan mengonfigurasi resolusi tampilan dan menampilkan konten kesalahan melalui ModelAndView , tetapi solusinya jelas tidak optimal. Itulah mengapa Spring 3.2 memperkenalkan opsi yang lebih baik yang akan kita diskusikan di bagian selanjutnya.

3.3. ResponseStatusExceptionResolver

Penyelesai ini juga diperkenalkan di Spring 3.0 dan diaktifkan secara default di DispatcherServlet .

Tanggung jawab utamanya adalah menggunakan anotasi @ResponseStatus yang tersedia pada pengecualian khusus dan untuk memetakan pengecualian ini ke kode status HTTP.

Pengecualian khusus seperti itu mungkin terlihat seperti:

@ResponseStatus(value = HttpStatus.NOT_FOUND) public class MyResourceNotFoundException extends RuntimeException { public MyResourceNotFoundException() { super(); } public MyResourceNotFoundException(String message, Throwable cause) { super(message, cause); } public MyResourceNotFoundException(String message) { super(message); } public MyResourceNotFoundException(Throwable cause) { super(cause); } }

Sama seperti DefaultHandlerExceptionResolver , resolver ini terbatas dalam caranya menangani isi respons - ia memetakan Kode Status pada respons, tetapi isi masih null.

3.4. SimpleMappingExceptionResolver dan AnnotationMethodHandlerExceptionResolver

The SimpleMappingExceptionResolver telah ada selama beberapa waktu. Itu keluar dari model Spring MVC yang lebih lama dan tidak terlalu relevan untuk Layanan REST. Kami pada dasarnya menggunakannya untuk memetakan nama kelas pengecualian untuk melihat nama.

The AnnotationMethodHandlerExceptionResolver diperkenalkan di Spring 3.0 untuk menangani pengecualian melalui @ExceptionHandler anotasi tetapi telah usang oleh ExceptionHandlerExceptionResolver sebagai Spring 3.2.

3.5. Custom HandlerExceptionResolver

Kombinasi DefaultHandlerExceptionResolver dan ResponseStatusExceptionResolver sangat membantu dalam menyediakan mekanisme penanganan error yang baik untuk Spring RESTful Service. Sisi negatifnya adalah, seperti yang disebutkan sebelumnya, tidak ada kendali atas isi respons.

Idealnya, kami ingin dapat menampilkan JSON atau XML, bergantung pada format apa yang diminta klien (melalui header Terima ).

Ini saja membenarkan pembuatan pemecah pengecualian kustom baru :

@Component public class RestResponseStatusExceptionResolver extends AbstractHandlerExceptionResolver { @Override protected ModelAndView doResolveException( HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { try { if (ex instanceof IllegalArgumentException) { return handleIllegalArgument( (IllegalArgumentException) ex, response, handler); } ... } catch (Exception handlerException) { logger.warn("Handling of [" + ex.getClass().getName() + "] resulted in Exception", handlerException); } return null; } private ModelAndView handleIllegalArgument(IllegalArgumentException ex, HttpServletResponse response) throws IOException { response.sendError(HttpServletResponse.SC_CONFLICT); String accept = request.getHeader(HttpHeaders.ACCEPT); ... return new ModelAndView(); } }

Satu detail yang perlu diperhatikan di sini adalah bahwa kami memiliki akses ke permintaan itu sendiri, sehingga kami dapat mempertimbangkan nilai header Terima yang dikirim oleh klien.

Misalnya, jika klien meminta application / json , maka dalam kasus kondisi error, kami ingin memastikan bahwa kami mengembalikan respons body yang dikodekan dengan application / json .

Detail implementasi penting lainnya adalah kami mengembalikan ModelAndView - ini adalah isi respons , dan ini akan memungkinkan kami untuk menyetel apa pun yang diperlukan.

Pendekatan ini adalah mekanisme yang konsisten dan mudah dikonfigurasi untuk penanganan error Layanan REST Musim Semi.

Namun, ia memiliki keterbatasan: Ia berinteraksi dengan HtttpServletResponse tingkat rendah dan cocok dengan model MVC lama yang menggunakan ModelAndView , jadi masih ada ruang untuk perbaikan.

4. Solusi 3: @ControllerAdvice

Spring 3.2 menghadirkan dukungan untuk @ExceptionHandler global dengan anotasi @ControllerAdvice .

Ini memungkinkan mekanisme yang memisahkan diri dari model MVC lama dan memanfaatkan ResponseEntity bersama dengan jenis keamanan dan fleksibilitas @ExceptionHandler :

@ControllerAdvice public class RestResponseEntityExceptionHandler extends ResponseEntityExceptionHandler { @ExceptionHandler(value = { IllegalArgumentException.class, IllegalStateException.class }) protected ResponseEntity handleConflict( RuntimeException ex, WebRequest request) { String bodyOfResponse = "This should be application specific"; return handleExceptionInternal(ex, bodyOfResponse, new HttpHeaders(), HttpStatus.CONFLICT, request); } }

The@ControllerAdvice annotation allows us to consolidate our multiple, scattered @ExceptionHandlers from before into a single, global error handling component.

The actual mechanism is extremely simple but also very flexible:

  • It gives us full control over the body of the response as well as the status code.
  • It provides mapping of several exceptions to the same method, to be handled together.
  • It makes good use of the newer RESTful ResposeEntity response.

One thing to keep in mind here is to match the exceptions declared with @ExceptionHandler to the exception used as the argument of the method.

If these don't match, the compiler will not complain — no reason it should — and Spring will not complain either.

However, when the exception is actually thrown at runtime, the exception resolving mechanism will fail with:

java.lang.IllegalStateException: No suitable resolver for argument [0] [type=...] HandlerMethod details: ...

5. Solution 4: ResponseStatusException (Spring 5 and Above)

Spring 5 introduced the ResponseStatusException class.

We can create an instance of it providing an HttpStatus and optionally a reason and a cause:

@GetMapping(value = "/{id}") public Foo findById(@PathVariable("id") Long id, HttpServletResponse response) { try { Foo resourceById = RestPreconditions.checkFound(service.findOne(id)); eventPublisher.publishEvent(new SingleResourceRetrievedEvent(this, response)); return resourceById; } catch (MyResourceNotFoundException exc) { throw new ResponseStatusException( HttpStatus.NOT_FOUND, "Foo Not Found", exc); } }

What are the benefits of using ResponseStatusException?

  • Excellent for prototyping: We can implement a basic solution quite fast.
  • One type, multiple status codes: One exception type can lead to multiple different responses. This reduces tight coupling compared to the @ExceptionHandler.
  • We won't have to create as many custom exception classes.
  • We have more control over exception handling since the exceptions can be created programmatically.

And what about the tradeoffs?

  • There's no unified way of exception handling: It's more difficult to enforce some application-wide conventions as opposed to @ControllerAdvice, which provides a global approach.
  • Code duplication: We may find ourselves replicating code in multiple controllers.

We should also note that it's possible to combine different approaches within one application.

For example, we can implement a @ControllerAdvice globally but also ResponseStatusExceptions locally.

However, we need to be careful: If the same exception can be handled in multiple ways, we may notice some surprising behavior. A possible convention is to handle one specific kind of exception always in one way.

For more details and further examples, see our tutorial on ResponseStatusException.

6. Handle the Access Denied in Spring Security

The Access Denied occurs when an authenticated user tries to access resources that he doesn't have enough authorities to access.

6.1. MVC — Custom Error Page

First, let's look at the MVC style of the solution and see how to customize an error page for Access Denied.

The XML configuration:

  ...  

And the Java configuration:

@Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/admin/*").hasAnyRole("ROLE_ADMIN") ... .and() .exceptionHandling().accessDeniedPage("/my-error-page"); }

When users try to access a resource without having enough authorities, they will be redirected to “/my-error-page”.

6.2. Custom AccessDeniedHandler

Next, let's see how to write our custom AccessDeniedHandler:

@Component public class CustomAccessDeniedHandler implements AccessDeniedHandler { @Override public void handle (HttpServletRequest request, HttpServletResponse response, AccessDeniedException ex) throws IOException, ServletException { response.sendRedirect("/my-error-page"); } }

And now let's configure it using XML configuration:

  ...  

0r using Java configuration:

@Autowired private CustomAccessDeniedHandler accessDeniedHandler; @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/admin/*").hasAnyRole("ROLE_ADMIN") ... .and() .exceptionHandling().accessDeniedHandler(accessDeniedHandler) }

Note how in our CustomAccessDeniedHandler, we can customize the response as we wish by redirecting or displaying a custom error message.

6.3. REST and Method-Level Security

Finally, let's see how to handle method-level security @PreAuthorize, @PostAuthorize, and @Secure Access Denied.

Of course, we'll use the global exception handling mechanism that we discussed earlier to handle the AccessDeniedException as well:

@ControllerAdvice public class RestResponseEntityExceptionHandler extends ResponseEntityExceptionHandler { @ExceptionHandler({ AccessDeniedException.class }) public ResponseEntity handleAccessDeniedException( Exception ex, WebRequest request) { return new ResponseEntity( "Access denied message here", new HttpHeaders(), HttpStatus.FORBIDDEN); } ... }

7. Spring Boot Support

Spring Boot provides an ErrorController implementation to handle errors in a sensible way.

In a nutshell, it serves a fallback error page for browsers (a.k.a. the Whitelabel Error Page) and a JSON response for RESTful, non-HTML requests:

{ "timestamp": "2019-01-17T16:12:45.977+0000", "status": 500, "error": "Internal Server Error", "message": "Error processing the request!", "path": "/my-endpoint-with-exceptions" }

As usual, Spring Boot allows configuring these features with properties:

  • server.error.whitelabel.enabled: can be used to disable the Whitelabel Error Page and rely on the servlet container to provide an HTML error message
  • server.error.include-stacktrace: with an always value; includes the stacktrace in both the HTML and the JSON default response

Apart from these properties, we can provide our own view-resolver mapping for /error, overriding the Whitelabel Page.

We can also customize the attributes that we want to show in the response by including an ErrorAttributes bean in the context. We can extend the DefaultErrorAttributes class provided by Spring Boot to make things easier:

@Component public class MyCustomErrorAttributes extends DefaultErrorAttributes { @Override public Map getErrorAttributes( WebRequest webRequest, boolean includeStackTrace) { Map errorAttributes = super.getErrorAttributes(webRequest, includeStackTrace); errorAttributes.put("locale", webRequest.getLocale() .toString()); errorAttributes.remove("error"); //... return errorAttributes; } }

If we want to go further and define (or override) how the application will handle errors for a particular content type, we can register an ErrorController bean.

Again, we can make use of the default BasicErrorController provided by Spring Boot to help us out.

Misalnya, bayangkan kita ingin menyesuaikan cara aplikasi kita menangani kesalahan yang dipicu di titik akhir XML. Yang harus kita lakukan adalah mendefinisikan metode publik menggunakan @RequestMapping , dan menyatakan itu menghasilkan tipe media application / xml :

@Component public class MyErrorController extends BasicErrorController { public MyErrorController(ErrorAttributes errorAttributes) { super(errorAttributes, new ErrorProperties()); } @RequestMapping(produces = MediaType.APPLICATION_XML_VALUE) public ResponseEntity xmlError(HttpServletRequest request) { // ... } }

8. Kesimpulan

Artikel ini membahas beberapa cara untuk mengimplementasikan mekanisme penanganan pengecualian untuk REST API di Spring, dimulai dengan mekanisme yang lebih lama dan dilanjutkan dengan dukungan Spring 3.2 hingga 4.x dan 5.x.

Seperti biasa, kode yang disajikan dalam artikel ini tersedia di GitHub.

Untuk kode terkait Keamanan Pegas, Anda dapat memeriksa modul pegas-keamanan-istirahat.

REST bawah

Saya baru saja mengumumkan kursus Learn Spring baru , yang berfokus pada dasar-dasar Spring 5 dan Spring Boot 2:

>> LIHAT KURSUSnya