Problem: yoxa çıxan xəta
Ödəniş servisi valyuta məzənnəsi klientini çağırır. Məzənnə provayderi əlçatmaz olanda klient IOException atır. Checked exception-dan bezən developer çağırışı belə sarıyır:
try { rate = rateClient.fetch(currency);} catch (IOException e) { rate = BigDecimal.ONE; // "müvəqqəti" ehtiyat}Heç nə loglanmır, heç nə bildirilmir və fasilə zamanı hər ödəniş vahid məzənnə ilə çevrilir. Səhv testlərdə tapılmır, çünki uğurlu ssenari keçir. Həftələr sonra mühasibat uzlaşmasında tapılır. Exception sistemin həqiqəti söyləməsi idi, kod isə onu susdurdu.
Yaxşı exception dizaynı əsasən iki şeydir: xətaların lazımi qata görünən olmasını təmin etmək və onları anlamaq üçün lazım olan məlumatın yolda itməməsini təmin etmək.
Checked və unchecked
Java-da iki ailə var. Checked exception-lar (RuntimeException olmayan Exception alt sinifləri) elan edilməli və ya tutulmalıdır; kompilyator bunu məcbur edir. Unchecked exception-lar (RuntimeException və alt sinifləri, həmçinin Error) elan tələb etmir.
İlkin ideya belə idi: checked exception-lar çağıranın ağlabatan şəkildə idarə edə biləcəyi bərpa olunan hallar, unchecked isə proqramlaşdırma səhvləri üçündür. Tətbiq kodunda bu sərhəd bulanıqdır və müasir Java üslubu, o cümlədən Spring, bir neçə səbəbdən əksər xətalar üçün unchecked-ə meyl edir:
- Checked exception-lar implementasiya detallarını imzalar vasitəsilə sızdırır. Repository başqa saxlama istifadə etməyə başlasa, bütün çağıranların
throwsbəndi dəyişir. - Lambda və stream-lərlə pis işləyirlər, çünki standart funksional interfeyslər onları elan etmir.
- Çağıranlar çox vaxt mənalı bir şey edə bilmir, tutub görməzdən gəlirlər, bu isə xətanın yoluna icazə verməkdən pisdir.
Praktik qayda: default olaraq unchecked işlədin. Checked-i yalnız çağıranın real bərpa yolu olanda və kompilyatorun onu düşünməyə məcbur etməsini istəyəndə işlədin.
Cause ilə sarın, orijinalı itirməyin
Aşağı səviyyəli exception-u yuxarı səviyyəliyə çevirəndə orijinalı həmişə cause kimi ötürün. Cause-un stack trace-i xətanın həqiqətən haradan başladığını göstərir.
public BigDecimal rateFor(String currency) { try { return rateClient.fetch(currency); } catch (IOException e) { throw new RateUnavailableException( "Cannot fetch rate for " + currency, e); // cause saxlanıb }}Yalnız e.getMessage() ilə yeni exception yaratmaq stack trace-i və çox vaxt ən faydalı detalı atır. Stack trace-dəki "Caused by: ..." sətirləri məhz sarğının normal olduğuna görə mövcuddur.
Udmayın
Üç nümunə xətaları gizlədir:
- Boş
catchbloku. catch (Exception e) { e.printStackTrace(); }: standart xəta axınına yazır, logging konfiqurasiyasını keçir və sonra heç nə olmamış kimi davam edir.- Konkret tip nəzərdə tutulan yerdə geniş
Throwablevə yaExceptiontutmaq.
Exception-u həqiqətən görməzdən gəlmək istəyirsinizsə, səbəbi şərhdə yazın və dəyişənə ignored adı verin. Tutub loglayırsınızsa, eyni exception-u hər qatda yenidən atmayın, əks halda bir xəta beş eyni stack trace yaradır. Loglamaq üçün bir yer seçin: adətən xətanın son idarə olunduğu yer.
InterruptedException-u da unutmayın. Onu tutub yuxarı ötürə bilmirsinizsə, Thread.currentThread().interrupt() ilə flag-i bərpa edin ki, yuxarıdakı kod kəsilməni hələ də görsün. Səssizcə udmaq shutdown və ləğvi pozur.
Domen exception-ları
RuntimeException("not found") atmaq hər çağıranı string təhlil etməyə məcbur edir. Biznes nəticələrini təsvir edən az sayda mənalı exception təyin edin.
public class OrderNotFoundException extends RuntimeException { private final long orderId; public OrderNotFoundException(long orderId) { super("Order " + orderId + " not found"); this.orderId = orderId; } public long orderId() { return orderId; }}Bu siniflər üçün qaydalar: onlara texnologiyaya yox, biznes vəziyyətinə görə ad verin (InsufficientFundsException, SqlConstraintException yox); problemi idarə etmək və ya bildirmək üçün lazım olan datanı daşıyın; iyerarxiyanı dayaz saxlayın. DomainException kimi ümumi baza sinif tək handler-ə hamısını tanımağa kömək edir. Exception-ları adi idarəetmə axını üçün, məsələn dövrün sonunu bildirmək üçün işlətməyin; onlar adi qaytarmadan yavaşdır və niyyəti bulandırır.
Uyğun gələn yerdə standart exception-lardan istifadə edin: pis arqument üçün IllegalArgumentException, yanlış vaxtda çağırış üçün IllegalStateException, gözlənilməz null üçün Objects.requireNonNull vasitəsilə NullPointerException, dəstəklənməyən əməliyyat üçün UnsupportedOperationException.
Sərhəddə HTTP-yə çevirin
Domen kodu HTTP status kodlarını bilməməlidir. Xəritələməni web qatının olduğu sərhəddə saxlayın. Spring Boot-da bu @RestControllerAdvice-dır.
@RestControllerAdviceclass ApiExceptionHandler { @ExceptionHandler(OrderNotFoundException.class) ProblemDetail notFound(OrderNotFoundException e) { var p = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, e.getMessage()); p.setProperty("orderId", e.orderId()); return p; } @ExceptionHandler(Exception.class) ProblemDetail unexpected(Exception e) { log.error("Unhandled error", e); // bir dəfə, stack trace ilə loglanır return ProblemDetail.forStatusAndDetail( HttpStatus.INTERNAL_SERVER_ERROR, "Unexpected error"); }}İki məqam. Birincisi, ümumi handler neytral mesaj qaytarır: stack trace, SQL və daxili sinif adları klientə çatmamalıdır. İkincisi, gözlənilməz xəta serverdə tam exception ilə loglanır, "tapılmadı" kimi tanınan biznes nəticəsi isə adətən loglarda stack trace tələb etmir.
try-with-resources
Bağlanmalı hər şey (stream, connection, statement) try-with-resources ilə işlənməlidir. Resurs tərs ardıcıllıqla avtomatik bağlanır, hətta gövdə exception atsa da.
try (var in = Files.newBufferedReader(path); var out = Files.newBufferedWriter(target)) { in.transferTo(out);}O, əl ilə yazılmış finally-nin səhv etdiyi incə halı da idarə edir: həm gövdə, həm close() exception atsa, gövdənin exception-u əsas olaraq qalır, close() exception-u isə orijinalı əvəzləmək əvəzinə suppressed kimi qoşulur (getSuppressed()). Resurslar AutoCloseable implementasiya etməlidir.
Uğursuzluq yollarını test edin
Uğursuzluq davranışı müqavilənin hissəsidir, ona görə onu test edin. Exception tipini, çağıranların istinad etdiyi mesaj datasını və cause-un qorunduğunu yoxlayın. Sərhəd testləri hər xəritələnmiş exception üçün HTTP statusunu və cavab gövdəsini yoxlamalıdır, çünki çatışmayan handler heç bir kompilyasiya xətası olmadan təmiz 404-ü ümumi 500-ə çevirir.
@Testvoid unknownOrderIsReportedWithItsId() { var e = assertThrows(OrderNotFoundException.class, () -> service.find(42L)); assertEquals(42L, e.orderId());}Yalnız uğurlu ssenarini əhatə edən test ən təhlükəli kodu, yəni xəta idarəsini yoxlanmamış qoyur.
Tez-tez edilən səhvlər
- "Təhlükəsiz olsun" deyə
Exceptiontutmaq; bu, görmək istədiyiniz proqramlaşdırma səhvlərini də tutur. - Girişdəki hekayədəki kimi, xətanı bildirmək əvəzinə
nullvə ya default dəyər qaytarmaq. finally-dən exception atmaq; bu, orijinal exception-u gizlədə bilər.- Hər qatda loglayıb yenidən atmaq, təkrar küy yaratmaq.
- Loglara və cavablara düşən exception mesajlarına sirr və ya şəxsi məlumat qoymaq.
- Exception mesajlarını API müqaviləsi kimi işlətmək. Klientlərə sabit xəta kodu verin.
- Hər metoda
throws Exceptionyazmaq; bu, imzadan hər cür məlumatı silir.
Güzəştlər
Unchecked exception-lar şablon kodu azaldır, amma nəyin uğursuz ola biləcəyini bilmək yükünü kompilyatordan sənədləşdirməyə və testlərə keçirir. Checked exception-lar uğursuzluq rejimlərini açıq edir, amma sürtünmə əlavə edir. Domen exception-ları sinif sayını artırır, ona görə onları çağıranın və ya sərhəddin fərqli yanaşacağı hallar üçün yaradın, hər mesaj üçün yox. Heç kimin fərqli idarə etmədiyi zəngin iyerarxiya sadəcə küydür.
Yoxlama siyahısı
- Boş catch yoxdur; görməzdən gəlinən hər exception izah olunub.
- Orijinal exception həmişə cause kimi ötürülür.
- Domen exception-ları biznes vəziyyətinə görə adlandırılıb.
- Exception-ları HTTP cavabına bir mərkəzi yer çevirir.
- Daxili detallar klientə çatmır.
- Bağlanabilən hər şey üçün try-with-resources.
InterruptedExceptionötürülür və ya flag bərpa olunur.
Növbəti addım
Əsas Java qərarları üçün kolleksiyaların seçilməsinə, executor tapşırıqlarında exception-ların davranışını görmək üçün concurrency əsaslarına və növbəti bacarıqlar üçün middle Java developer yol xəritəsinə baxın.