Ralat Mengendalikan REST dengan Spring

REST Teratas

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

>> SEMAK KURSUS

1. Gambaran keseluruhan

Tutorial ini akan menggambarkan cara melaksanakan Pengecualian Pengendalian dengan Musim Semi untuk REST API. Kami juga akan mendapat gambaran keseluruhan sejarah dan melihat pilihan baru yang diperkenalkan oleh pelbagai versi.

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

Sejak 3.2, kami mempunyai anotasi @ControllerAdvice untuk mengatasi batasan dua penyelesaian sebelumnya dan untuk mempromosikan pengendalian pengecualian terpadu di seluruh keseluruhan aplikasi.

Sekarang Spring 5 memperkenalkan kelas ResponseStatusException - cara pantas untuk pengendalian ralat asas dalam REST API kami.

Semua ini mempunyai satu kesamaan: Mereka menangani pemisahan kebimbangan dengan sangat baik. Aplikasi ini boleh membuat pengecualian secara normal untuk menunjukkan kegagalan semacam itu, yang kemudiannya akan dikendalikan secara berasingan.

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

2. Penyelesaian 1: Level-Controller @ExceptionHandler

Penyelesaian pertama berfungsi di peringkat @Controller . Kami akan menentukan kaedah untuk menangani pengecualian dan memberi penjelasan dengan @ExceptionHandler :

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

Pendekatan ini mempunyai kelemahan utama: Kaedah dia teran @ExceptionHandler hanya aktif untuk Pengawal tertentu , bukan secara global untuk keseluruhan aplikasi. Sudah tentu, menambahkan ini ke setiap pengawal menjadikannya tidak sesuai untuk mekanisme pengendalian pengecualian umum.

Kami dapat mengatasi had ini dengan meminta semua Pengawal melanjutkan kelas Pengawal Asas.

Walau bagaimanapun, penyelesaian ini boleh menjadi masalah bagi aplikasi di mana, dengan alasan apa pun, itu tidak mungkin dilakukan. Sebagai contoh, Pengawal mungkin sudah meluas dari kelas asas lain, yang mungkin ada di dalam balang lain atau tidak dapat diubah secara langsung, atau mungkin mereka sendiri tidak dapat diubah secara langsung.

Seterusnya, kami akan mencari cara lain untuk menyelesaikan masalah pengendalian pengecualian - yang bersifat global dan tidak termasuk perubahan pada artifak yang ada seperti Pengawal.

3. Penyelesaian 2: HandlerExceptionResolver

Penyelesaian kedua adalah menentukan HandlerExceptionResolver. Ini akan menyelesaikan sebarang pengecualian yang dilemparkan oleh aplikasi. Ini juga membolehkan kita menerapkan mekanisme pengendalian pengecualian yang seragam di REST API kami.

Sebelum mencari penyelesaian khusus, mari kita lihat implementasi yang ada.

3.1. ExceptionHandlerExceptionResolver

Penyelesai ini diperkenalkan pada Spring 3.1 dan diaktifkan secara lalai di DispatcherServlet . Ini sebenarnya merupakan komponen teras bagaimana mekanisme @ ExceptionHandler yang dikemukakan sebelumnya berfungsi.

3.2. DefaultHandlerExceptionResolver

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

Ini digunakan untuk menyelesaikan pengecualian Spring standard ke Kod Status HTTP yang sesuai, iaitu ralat Pelanggan 4xx dan kod status ralat 5xx Pelayan . Berikut adalah senarai penuh Pengecualian Musim Semi yang dikendalikannya dan cara mereka memetakan ke kod status.

Walaupun ia menetapkan Kod Status Respons dengan betul, satu batasannya ialah tidak menetapkan apa-apa pada badan Respons. Dan untuk REST API - Kod Status sebenarnya tidak cukup maklumat untuk disampaikan kepada Pelanggan - tindak balas harus mempunyai badan juga, untuk membolehkan aplikasi memberikan maklumat tambahan mengenai kegagalan tersebut.

Ini dapat diselesaikan dengan mengkonfigurasi resolusi pandangan dan membuat kandungan ralat melalui ModelAndView , tetapi penyelesaiannya jelas tidak optimal. Itulah sebabnya Spring 3.2 memperkenalkan pilihan yang lebih baik yang akan kita bincangkan di bahagian kemudian.

3.3. ResponsStatusExceptionResolver

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

Tanggungjawab utamanya adalah menggunakan anotasi @ResponseStatus yang tersedia pada pengecualian khusus dan untuk memetakan pengecualian ini ke kod status HTTP.

Pengecualian khusus seperti ini mungkin kelihatan 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 , penyelesai ini terhad dalam cara ia menangani badan tindak balas - ia memetakan Kod Status pada respons, tetapi badannya masih kosong.

3.4. SimpleMappingExceptionResolver and AnnotationM MethodHandlerExceptionResolver

The SimpleMappingExceptionResolver telah wujud sekian lama. Ia keluar dari model Spring MVC yang lebih lama dan tidak begitu relevan untuk Perkhidmatan REST. Kami pada dasarnya menggunakannya untuk memetakan nama kelas pengecualian untuk melihat nama.

The AnnotationMethodHandlerExceptionResolver diperkenalkan pada Spring 3.0 untuk menangani pengecualian melalui anotasi @ExceptionHandler tetapi telah ditamatkan oleh ExceptionHandlerExceptionResolver pada Spring 3.2.

3.5. Custom HandlerExceptionResolver

Gabungan DefaultHandlerExceptionResolver dan ResponseStatusExceptionResolver banyak membantu menyediakan mekanisme pengendalian ralat yang baik untuk Spring RESTful Service. Kelemahan adalah, seperti yang disebutkan sebelumnya, tidak ada kawalan terhadap badan tindak balas.

Sebaik-baiknya, kami ingin dapat mengeluarkan JSON atau XML, bergantung pada format apa yang diminta pelanggan (melalui tajuk Terima ).

Ini semata-mata membenarkan membuat penyelesaian baru pengecualian khusus :

@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 perincian yang perlu diperhatikan di sini ialah kita dapat mengakses permintaan itu sendiri, jadi kita dapat mempertimbangkan nilai header Terima yang dihantar oleh pelanggan.

Sebagai contoh, jika pelanggan meminta aplikasi / json , maka, sekiranya berlaku keadaan ralat, kami ingin memastikan kami mengembalikan badan respons yang dikodkan dengan aplikasi / json .

Perincian pelaksanaan penting yang lain adalah kita mengembalikan ModelAndView - ini adalah badan tindak balas , dan ini akan membolehkan kita menetapkan apa sahaja yang diperlukan di atasnya.

Pendekatan ini adalah mekanisme yang konsisten dan mudah dikonfigurasi untuk pengendalian ralat Spring REST Service.

Namun, ia mempunyai batasan: Ia berinteraksi dengan HtttpServletResponse tahap rendah dan sesuai dengan model MVC lama yang menggunakan ModelAndView , jadi masih ada ruang untuk diperbaiki.

4. Penyelesaian 3: @ControllerAdvice

Spring 3.2 memberikan sokongan untuk @ExceptionHandler global dengan anotasi @ControllerAdvice .

Ini membolehkan mekanisme yang melepaskan diri dari model MVC yang lebih lama dan menggunakan ResponseEntity bersama dengan jenis keselamatan dan fleksibiliti @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.

Sebagai contoh, bayangkan kita ingin menyesuaikan bagaimana aplikasi kita menangani kesalahan yang dicetuskan di titik akhir XML. Yang harus kita buat ialah menentukan kaedah awam menggunakan @RequestMapping , dan menyatakan bahawa ia menghasilkan jenis media aplikasi / 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. Kesimpulannya

Artikel ini membincangkan beberapa cara untuk melaksanakan mekanisme pengendalian pengecualian untuk REST API pada Spring, bermula dengan mekanisme yang lebih lama dan meneruskan sokongan Spring 3.2 dan menjadi 4.x dan 5.x.

Seperti biasa, kod yang disajikan dalam artikel ini terdapat di GitHub.

Untuk kod yang berkaitan dengan Keselamatan Musim Bunga, anda boleh menyemak modul rehat keselamatan pegas.

REST bawah

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

>> SEMAK KURSUS