Panduan untuk Penanganan Kesalahan Spring Boot REST API
Diterbitkan: 2022-03-11Menangani kesalahan dengan benar di API sambil memberikan pesan kesalahan yang berarti adalah fitur yang sangat diinginkan, karena dapat membantu klien API menanggapi masalah dengan benar. Perilaku default cenderung mengembalikan jejak tumpukan yang sulit dipahami dan pada akhirnya tidak berguna untuk klien API. Mempartisi informasi kesalahan ke dalam bidang juga memungkinkan klien API untuk menguraikannya dan memberikan pesan kesalahan yang lebih baik kepada pengguna. Pada artikel ini, kami akan membahas cara melakukan penanganan kesalahan yang tepat saat membangun REST API dengan Spring Boot.
Membangun REST API dengan Spring menjadi pendekatan standar untuk pengembang Java selama beberapa tahun terakhir. Menggunakan Spring Boot sangat membantu, karena menghapus banyak kode boilerplate dan memungkinkan konfigurasi otomatis berbagai komponen. Kami akan berasumsi bahwa Anda sudah familiar dengan dasar-dasar pengembangan API dengan teknologi tersebut sebelum menerapkan pengetahuan yang dijelaskan di sini. Jika Anda masih tidak yakin tentang cara mengembangkan API REST dasar, maka Anda harus mulai dengan artikel ini tentang Spring MVC atau artikel lain tentang membangun Layanan Spring REST.
Membuat Tanggapan Kesalahan Lebih Jelas
Sepanjang artikel ini, kita akan menggunakan kode sumber yang dihosting di GitHub dari aplikasi yang mengimplementasikan REST API untuk mengambil objek yang mewakili burung. Ini memiliki fitur yang dijelaskan dalam artikel ini dan beberapa contoh skenario penanganan kesalahan. Berikut ringkasan titik akhir yang diterapkan dalam aplikasi itu:
GET /birds/{birdId} | Mendapat informasi tentang burung dan memberikan pengecualian jika tidak ditemukan. |
GET /birds/noexception/{birdId} | Panggilan ini juga mendapatkan informasi tentang seekor burung, kecuali tidak mengeluarkan pengecualian jika burung itu tidak ditemukan. | POST /birds | Menciptakan burung. | </tr>
Modul MVC kerangka Spring hadir dengan beberapa fitur hebat untuk membantu penanganan kesalahan. Tetapi pengembang harus menggunakan fitur tersebut untuk menangani pengecualian dan mengembalikan respons yang berarti ke klien API.
Mari kita lihat contoh jawaban Spring Boot default ketika kita mengeluarkan HTTP POST ke titik akhir /birds
dengan objek JSON berikut, yang memiliki string "aaa" pada bidang "massa", yang seharusnya mengharapkan bilangan bulat:
{ "scientificName": "Common blackbird", "specie": "Turdus merula", "mass": "aaa", "length": 4 }
Jawaban default Spring Boot, tanpa penanganan kesalahan yang tepat:
{ "timestamp": 1500597044204, "status": 400, "error": "Bad Request", "exception": "org.springframework.http.converter.HttpMessageNotReadableException", "message": "JSON parse error: Unrecognized token 'three': was expecting ('true', 'false' or 'null'); nested exception is com.fasterxml.jackson.core.JsonParseException: Unrecognized token 'aaa': was expecting ('true', 'false' or 'null')\n at [Source: java.io.PushbackInputStream@cba7ebc; line: 4, column: 17]", "path": "/birds" }
Pesan tanggapan memiliki beberapa bidang yang bagus, tetapi terlalu fokus pada pengecualiannya. Omong-omong, ini adalah kelas DefaultErrorAttributes
dari Spring Boot. Bidang timestamp
adalah bilangan bulat yang bahkan tidak membawa informasi tentang unit pengukuran stempel waktu. Bidang exception
hanya menarik bagi pengembang Java dan pesan tersebut membuat konsumen API kehilangan semua detail implementasi yang tidak relevan bagi mereka. . Dan bagaimana jika ada detail lebih lanjut yang dapat kami ekstrak dari pengecualian tempat kesalahan berasal? Jadi, mari pelajari cara memperlakukan pengecualian tersebut dengan benar dan membungkusnya menjadi representasi JSON yang lebih baik untuk membuat hidup lebih mudah bagi klien API kami.
Karena kita akan menggunakan kelas tanggal dan waktu Java 8, pertama-tama kita perlu menambahkan ketergantungan Maven untuk konverter Jackson JSR310. Mereka menangani konversi kelas tanggal dan waktu Java 8 ke representasi JSON menggunakan anotasi @JsonFormat
:
<dependency> <groupId>com.fasterxml.jackson.datatype</groupId> <artifactId>jackson-datatype-jsr310</artifactId> </dependency>
Oke, jadi mari kita definisikan kelas untuk mewakili kesalahan API. Kami akan membuat kelas yang disebut ApiError
yang memiliki bidang yang cukup untuk menyimpan informasi yang relevan tentang kesalahan yang terjadi selama panggilan REST.
class ApiError { private HttpStatus status; @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "dd-MM-yyyy hh:mm:ss") private LocalDateTime timestamp; private String message; private String debugMessage; private List<ApiSubError> subErrors; private ApiError() { timestamp = LocalDateTime.now(); } ApiError(HttpStatus status) { this(); this.status = status; } ApiError(HttpStatus status, Throwable ex) { this(); this.status = status; this.message = "Unexpected error"; this.debugMessage = ex.getLocalizedMessage(); } ApiError(HttpStatus status, String message, Throwable ex) { this(); this.status = status; this.message = message; this.debugMessage = ex.getLocalizedMessage(); } }
Properti
status
menyimpan status panggilan operasi. Ini akan menjadi apa saja dari 4xx untuk menandakan kesalahan klien atau 5xx berarti kesalahan server. Skenario umum adalah kode http 400 yang berarti BAD_REQUEST, ketika klien, misalnya, mengirim bidang yang tidak diformat dengan benar, seperti alamat email yang tidak valid.Properti
timestamp
menyimpan instance tanggal-waktu ketika kesalahan terjadi.Properti
message
menyimpan pesan yang mudah digunakan tentang kesalahan.Properti
debugMessage
menyimpan pesan sistem yang menjelaskan kesalahan secara lebih rinci.Properti
subErrors
menyimpan larik sub-kesalahan yang terjadi. Ini digunakan untuk mewakili beberapa kesalahan dalam satu panggilan. Contohnya adalah kesalahan validasi di mana beberapa bidang gagal validasi. KelasApiSubError
digunakan untuk merangkumnya.
abstract class ApiSubError { } @Data @EqualsAndHashCode(callSuper = false) @AllArgsConstructor class ApiValidationError extends ApiSubError { private String object; private String field; private Object rejectedValue; private String message; ApiValidationError(String object, String message) { this.object = object; this.message = message; } }
Jadi, ApiValidationError
adalah kelas yang memperluas ApiSubError
dan mengungkapkan masalah validasi yang ditemui selama panggilan REST.
Di bawah ini, Anda akan melihat beberapa contoh respons JSON yang dihasilkan setelah kami menerapkan peningkatan yang dijelaskan di sini, hanya untuk mendapatkan gambaran tentang apa yang akan kami dapatkan di akhir artikel ini.
Berikut adalah contoh JSON yang dikembalikan ketika entitas tidak ditemukan saat memanggil titik akhir GET /birds/2
:
{ "apierror": { "status": "NOT_FOUND", "timestamp": "18-07-2017 06:20:19", "message": "Bird was not found for parameters {id=2}" } }
Berikut adalah contoh lain dari JSON yang dikembalikan saat mengeluarkan panggilan POST /birds
dengan nilai yang tidak valid untuk massa burung:
{ "apierror": { "status": "BAD_REQUEST", "timestamp": "18-07-2017 06:49:25", "message": "Validation errors", "subErrors": [ { "object": "bird", "field": "mass", "rejectedValue": 999999, "message": "must be less or equal to 104000" } ] } }
Penanganan Kesalahan Boot Musim Semi
Mari kita jelajahi beberapa anotasi Musim Semi yang akan digunakan untuk menangani pengecualian.
RestController
adalah anotasi dasar untuk kelas yang menangani operasi REST.

ExceptionHandler
adalah anotasi Spring yang menyediakan mekanisme untuk menangani pengecualian yang dilemparkan selama eksekusi penangan (operasi Pengendali). Anotasi ini, jika digunakan pada metode kelas pengontrol, akan berfungsi sebagai titik masuk untuk menangani pengecualian yang dilemparkan ke dalam pengontrol ini saja. Secara keseluruhan, cara paling umum adalah menggunakan @ExceptionHandler
pada metode kelas @ControllerAdvice
sehingga penanganan pengecualian akan diterapkan secara global atau ke subset pengontrol.
ControllerAdvice
adalah anotasi yang diperkenalkan di Spring 3.2, dan seperti namanya, adalah "Saran" untuk beberapa pengontrol. Ini digunakan untuk mengaktifkan ExceptionHandler
tunggal untuk diterapkan ke beberapa pengontrol. Dengan cara ini kita hanya di satu tempat dapat menentukan bagaimana memperlakukan pengecualian seperti itu dan handler ini akan dipanggil ketika pengecualian dilempar dari kelas yang dicakup oleh ControllerAdvice
ini. Subset pengontrol yang terpengaruh dapat ditentukan dengan menggunakan pemilih berikut pada @ControllerAdvice
: annotations()
, basePackageClasses()
, dan basePackages()
. Jika tidak ada pemilih yang disediakan, maka ControllerAdvice
diterapkan secara global ke semua pengontrol.
Jadi dengan menggunakan @ExceptionHandler
dan @ControllerAdvice
, kita akan dapat menentukan titik pusat untuk menangani pengecualian dan membungkusnya dalam objek ApiError
dengan organisasi yang lebih baik daripada mekanisme penanganan kesalahan Boot Musim Semi default.
Menangani Pengecualian
Langkah selanjutnya adalah membuat kelas yang akan menangani pengecualian. Untuk kesederhanaan, kami menyebutnya RestExceptionHandler
dan harus diperluas dari ResponseEntityExceptionHandler
Spring Boot. Kami akan memperluas ResponseEntityExceptionHandler
karena sudah menyediakan beberapa penanganan dasar pengecualian Spring MVC, jadi kami akan menambahkan penangan untuk pengecualian baru sambil meningkatkan yang sudah ada.
Mengesampingkan Pengecualian yang Ditangani Di ResponseEntityExceptionHandler
Jika Anda melihat kode sumber ResponseEntityExceptionHandler
, Anda akan melihat banyak metode yang disebut handle******()
seperti handleHttpMessageNotReadable()
atau handleHttpMessageNotWritable()
. Pertama-tama mari kita lihat bagaimana kita dapat memperluas handleHttpMessageNotReadable()
untuk menangani pengecualian HttpMessageNotReadableException
. Kami hanya perlu mengganti metode handleHttpMessageNotReadable()
di kelas RestExceptionHandler
kami:
@Order(Ordered.HIGHEST_PRECEDENCE) @ControllerAdvice public class RestExceptionHandler extends ResponseEntityExceptionHandler { @Override protected ResponseEntity<Object> handleHttpMessageNotReadable(HttpMessageNotReadableException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { String error = "Malformed JSON request"; return buildResponseEntity(new ApiError(HttpStatus.BAD_REQUEST, error, ex)); } private ResponseEntity<Object> buildResponseEntity(ApiError apiError) { return new ResponseEntity<>(apiError, apiError.getStatus()); } //other exception handlers below }
Kami telah menyatakan bahwa jika HttpMessageNotReadableException
dilemparkan, pesan kesalahannya adalah "Permintaan JSON yang salah" dan kesalahan akan dienkapsulasi di dalam objek ApiError
. Di bawah ini kita dapat melihat jawaban dari panggilan REST dengan metode baru ini diganti:
{ "apierror": { "status": "BAD_REQUEST", "timestamp": "21-07-2017 03:53:39", "message": "Malformed JSON request", "debugMessage": "JSON parse error: Unrecognized token 'aaa': was expecting ('true', 'false' or 'null'); nested exception is com.fasterxml.jackson.core.JsonParseException: Unrecognized token 'aaa': was expecting ('true', 'false' or 'null')\n at [Source: java.io.PushbackInputStream@7b5e8d8a; line: 4, column: 17]" } }
Menangani Pengecualian Khusus
Sekarang kita akan melihat cara membuat metode yang menangani pengecualian yang belum dideklarasikan di dalam ResponseEntityExceptionHandler
Spring Boot.
Skenario umum untuk aplikasi Spring yang menangani panggilan database adalah memiliki panggilan untuk menemukan catatan dengan ID-nya menggunakan kelas repositori. Tetapi jika kita melihat ke dalam metode CrudRepository.findOne()
, kita akan melihat bahwa metode tersebut mengembalikan null
jika objek tidak ditemukan. Artinya, jika layanan kami hanya memanggil metode ini dan kembali langsung ke controller, kami akan mendapatkan kode HTTP 200 (OK) bahkan jika sumber daya tidak ditemukan. Sebenarnya, pendekatan yang tepat adalah mengembalikan kode HTTP 404 (TIDAK DITEMUKAN) seperti yang ditentukan dalam spesifikasi HTTP/1.1.
Untuk menangani kasus ini, kami akan membuat pengecualian khusus yang disebut EntityNotFoundException
. Yang ini adalah pengecualian yang dibuat khusus dan berbeda dari javax.persistence.EntityNotFoundException
, karena menyediakan beberapa konstruktor yang memudahkan pembuatan objek, dan seseorang dapat memilih untuk menangani pengecualian javax.persistence
berbeda.
Karena itu, mari buat ExceptionHandler
untuk RestExceptionHandler
EntityNotFoundException
. Untuk melakukannya, buat metode yang disebut handleEntityNotFound()
dan beri anotasi dengan @ExceptionHandler
, dengan meneruskan objek kelas EntityNotFoundException.class
ke sana. Ini menandakan Spring bahwa setiap kali EntityNotFoundException
dilemparkan, Spring harus memanggil metode ini untuk menanganinya. Saat membuat anotasi metode dengan @ExceptionHandler
, metode tersebut akan menerima berbagai parameter yang disuntikkan secara otomatis seperti WebRequest
, Locale
, dan lainnya seperti yang dijelaskan di sini. Kami hanya akan memberikan pengecualian EntityNotFoundException
sendiri sebagai parameter untuk metode handleEntityNotFound
ini.
@Order(Ordered.HIGHEST_PRECEDENCE) @ControllerAdvice public class RestExceptionHandler extends ResponseEntityExceptionHandler { //other exception handlers @ExceptionHandler(EntityNotFoundException.class) protected ResponseEntity<Object> handleEntityNotFound( EntityNotFoundException ex) { ApiError apiError = new ApiError(NOT_FOUND); apiError.setMessage(ex.getMessage()); return buildResponseEntity(apiError); } }
Besar! Dalam metode handleEntityNotFound()
, kita menyetel kode status HTTP ke NOT_FOUND
dan menggunakan pesan pengecualian baru. Inilah yang terlihat seperti respons untuk titik akhir GET /birds/2
sekarang:
{ "apierror": { "status": "NOT_FOUND", "timestamp": "21-07-2017 04:02:22", "message": "Bird was not found for parameters {id=2}" } }
Kesimpulan
Penting untuk mengendalikan penanganan pengecualian sehingga kami dapat memetakan pengecualian tersebut dengan benar ke objek ApiError
dan memberikan informasi penting yang memungkinkan klien API mengetahui apa yang terjadi. Langkah selanjutnya dari sini adalah membuat lebih banyak metode penangan (yang memiliki @ExceptionHandler) untuk pengecualian yang dilemparkan ke dalam kode aplikasi. Ada lebih banyak contoh untuk beberapa pengecualian umum lainnya seperti MethodArgumentTypeMismatchException
, ConstraintViolationException
dan lainnya dalam kode GitHub.
Berikut adalah beberapa sumber tambahan yang membantu dalam penyusunan artikel ini:
Baeldung - Penanganan kesalahan untuk REST dengan Spring
Spring Blog - Penanganan pengecualian di Spring MVC