Geçmiş: Java 7'den önce, kaynak yönetimi, geliştiricilerin finally blokları içinde close() çağrısını manuel olarak yaptığı ayrıntılı try-catch-finally yapısına dayanıyordu. Bu model, özellikle birden fazla kaynak veya temizlik sırasında meydana gelen istisnaları yönetmek için hatalara açıktı. Java 7, kaynak kapatmayı otomatikleştiren try-with-resources ifadesini Project Coin aracılığıyla tanıttı; derleyici bunu, istisna zinciri bütünlüğünü koruyarak kaynak kapatmaktan sorumlu olan karmaşık bir bayt koduna dönüştürmektedir.
Problem: Birden fazla kaynak AutoCloseable'ı uyguladığında, JVM, bağımlılık hiyerarşilerini dikkate alarak kapatmayı başlatma sırasının tersine garanti etmelidir. Örneğin, bir dosya akışını saran bir çıkış akışının önce kapatılması gerekir; bu, tamponları boşaltmak içindir. Ayrıca, hem try bloğu hem de close() yöntemi istisnaları atarsa, spesifikasyon, blok içindeki birincil istisnanın propagation edilmesini ve temizleme istisnasının Throwable.addSuppressed() aracılığıyla bastırılmış bir istisna olarak eklenmesini zorunlu kılar. Bu, derleyicinin her kaynak kapatması etrafında sentetik try-catch blokları oluşturmasını ve istisnaları tutmak için geçici değişkenler yönetmesini gerektirir.
Çözüm: Derleyici, try-with-resources ifadesini orijinal mantığı içeren bir ana try bloğuna ve LIFO sırasına göre kaynakları kapatan bir dizi iç içe finally bloğuna dönüştürmektedir. Her kaynak için, derleyici Throwable'ı yakalayıp bunu sentetik bir değişkende saklayan, close()'u çağıran ve eğer close() bir istisna atarsa, yakalanan istisnaya addSuppressed()'ı çağırarak yeniden fırlatır. Java 9+ ile birlikte, derleyici, geçici sentetik değişkenler kullanarak etkili bir şekilde sonlandırma kaynaklarını da yönetmektedir; bu, oluşturulan temizlik blokları içinde erişilebilirliği garanti eder.
// Kaynak kodu public String readFirstLine(String path) throws IOException { try (BufferedReader br = new BufferedReader(new FileReader(path))) { return br.readLine(); } } // Kavramsal bayt kodu dönüşümü public String readFirstLine(String path) throws IOException { BufferedReader br = new BufferedReader(new FileReader(path)); Throwable primaryException = null; try { return br.readLine(); } catch (Throwable t) { primaryException = t; throw t; } finally { if (br != null) { if (primaryException != null) { try { br.close(); } catch (Throwable suppressed) { primaryException.addSuppressed(suppressed); } } else { br.close(); } } } }
Yüksek yük altında kalıcı olarak veritabanı bağlantı sızıntılarının meydana geldiği bir üretim olayı ile karşılaştık. Kod tabanı, geliştiricilerin finally blokları içinde close() çağrısını yaptığı manuel try-catch-finally yapısını kullanıyordu, ancak bu uygulamalar, temizlik işlemleri için yeterli istisna yönetimi eksikliği taşıyordu. Close() istisna attığında, iş mantığından gelen orijinal SQLException kaybolmuş, kök nedenleri gizlemiş ve uygun bağlantı havuzu dönüşlerini engellemişti.
İlk düzeltme stratejisi, manuel temizleme kalıplarını sağlamlaştırmak için titiz kod incelemeleri ve SonarQube gibi statik analiz araçlarını kullanmayı düşünüyordu. Bu yaklaşım, geliştiricilerin her close() çağrısını iç içe geçmiş try-catch blokları içinde sarmalayarak ikincil istisnaları bastıracak şekilde savunmacı kod yazmalarını gerektiriyordu, ancak bu hızlı geliştirme döngüleri sırasında hata yapmaya açıktı ve okunabilirliği karmaşıklaştıran önemli bir fazlalık getirdi. Bu nedenle, insan denetiminin büyüyen bir kod tabanında tutarlı bir şekilde uygulanmasının garanti edilemeyeceğini düşünerek bu yaklaşımı reddettik.
İkinci strateji olarak Guava'nın Closer yardımcı aracını değerlendirdik; bu araç, kaynakları kaydetmek için akıcı bir API sağlar ve otomatik kapanma sırasını yönetir. Closer, istisna bastırmayı ve ters sıralı temizliği doğru bir şekilde yönetmesine rağmen, hafifliği azaltmaya çalışan bir mikro hizmete ağır bir dış bağımlılık getirmiştir. Ayrıca, Closer'ın belirli çalışma zamanı istisna sarma yapılarına uyacak şekilde istisna türlerini yeniden yapılandırmayı gerektiriyordu. Bu nedenle, bağımlılık yükünü ve dayatılan standart olmayan istisna yönetim kalıplarını dikkate alarak bu yolu izlememeye karar verdik.
Üçüncü yaklaşım, tüm kaynak yönetimini standart try-with-resources ifadelerine geçirmeyi önerdi. Bu, derleyici tarafından oluşturulan bayt kodunu kullanarak temizliği otomatikleştirmiştir. Bu çözüm, manuel fazlalığı ortadan kaldırdı, sentetik bayt kodu blokları aracılığıyla LIFO kapanma sırasını garanti etti ve istisna hiyerarşilerini Throwable.addSuppressed() aracılığıyla otomatik olarak korudu; bu da kütüphane bağımlılıklarını gerektirmiyordu. Bu yaklaşımı seçtik çünkü derleyici düzeyinde kök nedeni ele aldı, kod karmaşıklığını yaklaşık üç yüz satır azalttı ve modern Java en iyi uygulamaları ile uyum sağladı.
Geçişin ardından, bağlantı sızıntıları üretim izlemelerinde sıfıra düştü ve hata ayıklama verimliliği dramatik şekilde arttı çünkü mühendisler artık orijinal SQLException'ı görebiliyorlardı ve temizlik hataları bastırılmış izler olarak eklenmişti. Bu hizmet, bayt kodu düzeyindeki garantilerin farklı JVM sürümleri arasında tutarlı çalışması sayesinde sıfır kesinti ile dağıtım uyumluluğunu elde etti.
Normal bir şekilde tamamlanan bir try bloğunda close() yönteminin attığı istisnaları try-with-resources nasıl yönetir?
Try bloğu istisna atmadan çalıştığında, derleyici tarafından oluşturulan finally bloğu, her kaynağı close() ile kapatır. Eğer close() bir istisna atarsa, bu istisna, baskı için önceden var olan bir istisna olmadığı için çağrılana iletilen birincil istisna haline gelir. JVM bu istisnayı sarmalayamaz veya atamaz; tam olarak atıldığı gibi iletilir, bu da zincir içindeki sonraki kaynak kapamalarını kesintiye uğratabilir. Bu ayrımın anlaşılması önemlidir çünkü kaynak uygulamalarının close()'un idempotent ve minimum etkili kalmasını sağlaması gerektiğini açıklar; zira başarısız bir close(), iş mantığının başarılı bir şekilde tamamlanmasını gizleyebilir.
Kaynakların kapatılması neden başlangıçta ters sırada olmalıdır ve bu nasıl bir bayt kodu mekanizmasıyla zorlanmaktadır?
Kaynaklar genellikle dış sarmalayıcıların (örneğin BufferedWriter) altta yatan akışlara (örneğin FileOutputStream) referanslar taşıdığı kapsülleme bağımlılıkları gösterir. Alttaki akışın önce kapatılması, sarmalayıcının tutarsız bir durumda kalmasına neden olur; bu, tamponlanmış verilerin kaybolmasına veya sarmalayıcı boşaltmaya çalıştığında IOException oluşmasına yol açabilir. Derleyici, ters sıradaki kapanmayı (LIFO) zorlamak için iç içe geçmiş finally blokları üretmektedir; burada en içteki finally (son tanımlanan kaynağa karşılık gelen) dıştaki finally bloklarından önce çalışır. Bu yapı, BufferedWriter.close()'un, dosya tutamacını serbest bırakmadan önce alttaki akışa tamponunu boşaltmasını garanti eder, böylece veri kaybı ve kaynak bozulmasını önler.
Java 7 ile Java 9 arasında kaynak beyanı kapsamındaki bayt kodu üretiminde ne değişti?
Java 7, try başlığında tanımlanan kaynak değişkenlerinin açıkça final olmasını gerektiriyordu; bu, kaynakların yeniden atanması veya karmaşık ifadelerden türetilmesi gerektiğinde esnekliği sınırlıyordu. Java 9, bu kısıtlamayı gevşeterek etkili olarak sonlandırılmış kaynakların try başlığında tanımlanmasına izin verdi, ancak derleyici, oluşturulan temizlik blokları içinde referansları tutmak için hala sentetik değişkenler üretmektedir. Özellikle, bir kaynak r değişkenine dışarıda atanmışsa, derleyici, temizlikte referansın istikrarlı kalmasını sağlamak için final AutoCloseable resource$1 = r; biçiminde bir bayt kodu üretir (ancak değişiklik, etkili olarak sonlandırma durumunu ihlal eder). Bu sentetik değişken enjeksiyonu, temizlik kodunun her zaman orijinal nesne örneğini referans almasını sağlar; bu da finally bloğu yürütülürken null gösterici istisnalarının veya eski referansların önlenmesine yardımcı olur.