Gelişmiş Java Sınıfı Eğitimi: Sınıf Yeniden Yükleme Kılavuzu

Yayınlanan: 2022-03-11

Java geliştirme projelerinde, tipik bir iş akışı, her sınıf değişikliğinde sunucunun yeniden başlatılmasını içerir ve kimse bundan şikayet etmez. Bu, Java geliştirme ile ilgili bir gerçektir. Java ile ilk günden beri böyle çalışıyoruz. Ancak Java sınıfının yeniden yüklenmesi bu kadar zor mu? Ve yetenekli Java geliştiricileri için bu sorunu çözmek hem zorlayıcı hem de heyecan verici olabilir mi? Bu Java sınıfı eğitiminde, sorunu çözmeye, anında sınıf yeniden yüklemenin tüm avantajlarını elde etmenize yardımcı olmaya ve üretkenliğinizi son derece artırmaya çalışacağım.

Java sınıfının yeniden yüklenmesi genellikle tartışılmaz ve bu süreci araştıran çok az belge vardır. Bunu değiştirmek için buradayım. Bu Java sınıfları öğreticisi, bu sürecin adım adım açıklamasını sağlayacak ve bu inanılmaz teknikte ustalaşmanıza yardımcı olacaktır. Java sınıfı yeniden yüklemeyi uygulamanın çok fazla özen gerektirdiğini unutmayın, ancak bunun nasıl yapıldığını öğrenmek sizi hem Java geliştiricisi hem de yazılım mimarı olarak büyük liglere taşıyacaktır. Ayrıca en yaygın 10 Java hatasından nasıl kaçınılacağını anlamak da zarar vermez.

Çalışma Alanı Kurulumu

Bu eğitimin tüm kaynak kodları GitHub'a buradan yüklenir.

Bu öğreticiyi takip ederken kodu çalıştırmak için Maven, Git ve Eclipse veya IntelliJ IDEA'ya ihtiyacınız olacak.

Eclipse kullanıyorsanız:

  • Eclipse'in proje dosyalarını oluşturmak için mvn eclipse:eclipse komutunu çalıştırın.
  • Oluşturulan projeyi yükleyin.
  • Çıktı yolunu target/classes olarak ayarlayın.

IntelliJ kullanıyorsanız:

  • Projenin pom dosyasını içe aktarın.
  • Herhangi bir örnek çalıştırırken IntelliJ otomatik olarak derlenmeyecektir, bu nedenle aşağıdakilerden birini yapmanız gerekir:
  • Örnekleri IntelliJ içinde çalıştırın, ardından her derlemek istediğinizde Alt+BE basmanız gerekecek.
  • IntelliJ dışındaki örnekleri run_example*.bat ile çalıştırın. IntelliJ'in derleyici otomatik derlemesini true olarak ayarlayın. Ardından, herhangi bir Java dosyasını her değiştirdiğinizde, IntelliJ onu otomatik olarak derleyecektir.

Örnek 1: Java Class Loader ile Sınıfı Yeniden Yükleme

İlk örnek size Java sınıfı yükleyici hakkında genel bir fikir verecektir. İşte kaynak kodu.

Aşağıdaki User sınıfı tanımı verildiğinde:

 public static class User { public static int age = 10; }

Aşağıdakileri yapabiliriz:

 public static void main(String[] args) { Class<?> userClass1 = User.class; Class<?> userClass2 = new DynamicClassLoader("target/classes") .load("qj.blog.classreloading.example1.StaticInt$User"); ...

Bu öğretici örneğinde, belleğe yüklenen iki User sınıfı olacaktır. userClass1 , JVM'nin varsayılan sınıf yükleyicisi tarafından ve userClass2 , GitHub projesinde kaynak kodu da sağlanan ve aşağıda ayrıntılı olarak açıklayacağım özel bir sınıf yükleyici olan DynamicClassLoader kullanılarak yüklenecektir.

İşte main yöntemin geri kalanı:

 out.println("Seems to be the same class:"); out.println(userClass1.getName()); out.println(userClass2.getName()); out.println(); out.println("But why there are 2 different class loaders:"); out.println(userClass1.getClassLoader()); out.println(userClass2.getClassLoader()); out.println(); User.age = 11; out.println("And different age values:"); out.println((int) ReflectUtil.getStaticFieldValue("age", userClass1)); out.println((int) ReflectUtil.getStaticFieldValue("age", userClass2)); }

Ve çıktı:

 Seems to be the same class: qj.blog.classreloading.example1.StaticInt$User qj.blog.classreloading.example1.StaticInt$User But why there are 2 different class loaders: qj.util.lang.DynamicClassLoader@3941a79c sun.misc.Launcher$AppClassLoader@1f32e575 And different age values: 11 10

Burada görebileceğiniz gibi, User sınıfları aynı ada sahip olsalar da aslında iki farklı sınıftırlar ve bağımsız olarak yönetilebilir ve manipüle edilebilirler. Yaş değeri, statik olarak bildirilmiş olmasına rağmen, her sınıfa ayrı ayrı eklenerek iki versiyonda bulunur ve bağımsız olarak da değiştirilebilir.

Normal bir Java programında ClassLoader , sınıfları JVM'ye getiren portaldır. Bir sınıf başka bir sınıfın yüklenmesini gerektirdiğinde, yüklemeyi yapmak ClassLoader görevidir.

Ancak bu Java sınıfı örneğinde, User sınıfının ikinci sürümünü yüklemek için DynamicClassLoader adlı özel ClassLoader kullanılır. DynamicClassLoader yerine varsayılan sınıf yükleyiciyi tekrar kullanacak StaticInt.class.getClassLoader() komutuyla), yüklenen tüm sınıflar önbelleğe alındığından aynı User sınıfı kullanılacaktır.

Varsayılan Java ClassLoader'ın DynamicClassLoader'a karşı çalışma şeklini incelemek, bu Java sınıfları öğreticisinden yararlanmanın anahtarıdır.

DynamicClassLoader

Normal bir Java programında birden çok sınıf yükleyici olabilir. Ana sınıfınızı yükleyen ClassLoader varsayılandır ve kodunuzdan istediğiniz kadar sınıf yükleyici oluşturabilir ve kullanabilirsiniz. Bu, Java'da sınıf yeniden yüklemenin anahtarıdır. DynamicClassLoader muhtemelen tüm bu öğreticinin en önemli kısmıdır, bu nedenle hedefimize ulaşmadan önce dinamik sınıf yüklemenin nasıl çalıştığını anlamamız gerekir.

ClassLoader varsayılan davranışından farklı olarak, DynamicClassLoader daha agresif bir stratejiyi devralır. Normal bir sınıf yükleyici, üst ClassLoader öncelik verir ve yalnızca üst öğesinin yükleyemediği sınıfları yükler. Bu normal şartlar için uygundur, ancak bizim durumumuzda değil. Bunun yerine, DynamicClassLoader , tüm sınıf yollarına bakmaya ve ebeveyni hakkından vazgeçmeden önce hedef sınıfı çözmeye çalışacaktır.

Yukarıdaki örneğimizde, DynamicClassLoader yalnızca bir sınıf yolu ile oluşturulmuştur: "target/classes" (geçerli dizinimizde), bu nedenle o konumda bulunan tüm sınıfları yükleyebilir. Orada olmayan tüm sınıflar için, ana sınıf yükleyiciye başvurması gerekecektir. Örneğin, StaticInt sınıfımıza String sınıfını yüklememiz gerekiyor ve sınıf yükleyicimizin JRE klasörümüzdeki rt.jar erişimi yok, bu nedenle üst sınıf yükleyicinin String sınıfı kullanılacak.

Aşağıdaki kod, DynamicClassLoader öğesinin üst sınıfı olan AggressiveClassLoader ve bu davranışın nerede tanımlandığını gösterir.

 byte[] newClassData = loadNewClass(name); if (newClassData != null) { loadedClasses.add(name); return loadClass(newClassData, name); } else { unavaiClasses.add(name); return parent.loadClass(name); }

DynamicClassLoader aşağıdaki özelliklerine dikkat edin:

  • Yüklenen sınıflar, varsayılan sınıf yükleyici tarafından yüklenen diğer sınıflarla aynı performansa ve diğer niteliklere sahiptir.
  • DynamicClassLoader , yüklenen tüm sınıfları ve nesneleri ile birlikte çöp olarak toplanabilir.

Aynı sınıfın iki sürümünü yükleme ve kullanma yeteneği ile artık eski sürümü atıp yerine yenisini yüklemeyi düşünüyoruz. Bir sonraki örnekte, tam da bunu yapacağız…sürekli olarak.

Örnek 2: Bir Sınıfı Sürekli Olarak Yeniden Yükleme

Bu sonraki Java örneği, JRE'nin sınıfları sonsuza kadar yükleyebileceğini ve yeniden yükleyebileceğini, eski sınıfların boşaltılıp çöplerin toplandığını ve sabit sürücüden yüklenen ve kullanıma sunulan yepyeni sınıflarla birlikte gösterecek. İşte kaynak kodu.

İşte ana döngü:

 public static void main(String[] args) { for (;;) { Class<?> userClass = new DynamicClassLoader("target/classes") .load("qj.blog.classreloading.example2.ReloadingContinuously$User"); ReflectUtil.invokeStatic("hobby", userClass); ThreadUtil.sleep(2000); } }

Her iki saniyede bir, eski User sınıfı atılır, yenisi yüklenir ve metot hobby çağrılır.

İşte User sınıfı tanımı:

 @SuppressWarnings("UnusedDeclaration") public static class User { public static void hobby() { playFootball(); // will comment during runtime // playBasketball(); // will uncomment during runtime } // will comment during runtime public static void playFootball() { System.out.println("Play Football"); } // will uncomment during runtime // public static void playBasketball() { // System.out.println("Play Basketball"); // } }

Bu uygulamayı çalıştırırken, User sınıfında belirtilen kodu yorumlamaya ve yorumunu kaldırmaya çalışmalısınız. Her zaman en yeni tanımın kullanılacağını göreceksiniz.

İşte bazı örnek çıktı:

 ... Play Football Play Football Play Football Play Basketball Play Basketball Play Basketball

DynamicClassLoader her yeni örneği oluşturulduğunda, Eclipse veya IntelliJ'i en son sınıf dosyasını çıkaracak şekilde ayarladığımız target/classes klasöründen User sınıfını yükler. Tüm eski DynamicClassLoader s ve eski User sınıflarının bağlantısı kaldırılacak ve çöp toplayıcıya tabi tutulacaktır.

Gelişmiş Java geliştiricilerinin, etkin veya bağlantısız olsun, dinamik sınıf yeniden yüklemeyi anlamaları çok önemlidir.

JVM HotSpot'a aşina iseniz, burada sınıf yapısının da değiştirilip yeniden yüklenebileceğini belirtmekte fayda var: playFootball yöntemi kaldırılacak ve playBasketball yöntemi eklenecek. Bu, yalnızca yöntem içeriğinin değiştirilmesine izin veren veya sınıf yeniden yüklenemeyen HotSpot'tan farklıdır.

Artık bir sınıfı yeniden yükleyebileceğimize göre, aynı anda birçok sınıfı yeniden yüklemeyi denemenin zamanı geldi. Bir sonraki örnekte deneyelim.

Örnek 3: Birden Çok Sınıfı Yeniden Yükleme

Bu örneğin çıktısı Örnek 2 ile aynı olacaktır, ancak bu davranışın bağlam, hizmet ve model nesneleri ile daha uygulama benzeri bir yapıda nasıl uygulanacağını gösterecektir. Bu örneğin kaynak kodu oldukça büyük, bu yüzden burada sadece bir kısmını gösterdim. Tam kaynak kodu burada.

İşte main yöntem:

 public static void main(String[] args) { for (;;) { Object context = createContext(); invokeHobbyService(context); ThreadUtil.sleep(2000); } }

Ve createContext yöntemi:

 private static Object createContext() { Class<?> contextClass = new DynamicClassLoader("target/classes") .load("qj.blog.classreloading.example3.ContextReloading$Context"); Object context = newInstance(contextClass); invoke("init", context); return context; }

invokeHobbyService yöntemi:

 private static void invokeHobbyService(Object context) { Object hobbyService = getFieldValue("hobbyService", context); invoke("hobby", hobbyService); }

Ve işte Context sınıfı:

 public static class Context { public HobbyService hobbyService = new HobbyService(); public void init() { // Init your services here hobbyService.user = new User(); } }

Ve HobbyService sınıfı:

 public static class HobbyService { public User user; public void hobby() { user.hobby(); } }

Bu örnekteki Context sınıfı, önceki örneklerdeki User sınıfından çok daha karmaşıktır: diğer sınıflara bağlantıları vardır ve her başlatıldığında çağrılacak init yöntemine sahiptir. Temel olarak, gerçek dünya uygulamasının bağlam sınıflarına çok benzer (uygulamanın modüllerini takip eder ve bağımlılık enjeksiyonu yapar). Dolayısıyla, bu Context sınıfını tüm bağlantılı sınıflarıyla birlikte yeniden yükleyebilmek, bu tekniği gerçek hayata uygulamak için büyük bir adımdır.

Java sınıfının yeniden yüklenmesi, ileri düzey Java mühendisleri için bile zordur.

Sınıfların ve nesnelerin sayısı arttıkça, “eski sürümleri bırakma” adımımız da daha karmaşık hale gelecektir. Bu aynı zamanda sınıf yeniden yüklemenin bu kadar zor olmasının en büyük nedenidir. Eski sürümleri muhtemelen bırakmak için, yeni bağlam oluşturulduktan sonra eski sınıflara ve nesnelere yapılan tüm referansların bırakıldığından emin olmamız gerekecek. Bununla zarif bir şekilde nasıl başa çıkarız?

Buradaki main yöntem, bağlam nesnesini tutacaktır ve bu, bırakılması gereken tüm şeylere yönelik tek bağlantıdır . Bu bağlantıyı koparırsak, bağlam nesnesi ve bağlam sınıfı ve hizmet nesnesi… tümü çöp toplayıcıya tabi olacaktır.

Normalde sınıfların neden bu kadar kalıcı olduğu ve çöplerin toplanmadığı hakkında küçük bir açıklama:

  • Normalde, tüm sınıflarımızı varsayılan Java sınıf yükleyicisine yükleriz.
  • Sınıf-sınıf yükleyici ilişkisi, sınıf yükleyicinin yüklediği tüm sınıfları da önbelleğe aldığı iki yönlü bir ilişkidir.
  • Sınıf yükleyici hala herhangi bir canlı iş parçacığına bağlı olduğu sürece, her şey (tüm yüklenen sınıflar) çöp toplayıcıya karşı bağışık olacaktır.
  • Bununla birlikte, yeniden yüklemek istediğimiz kodu, varsayılan sınıf yükleyici tarafından zaten yüklenen koddan ayıramazsak, yeni kod değişikliklerimiz çalışma zamanında asla uygulanmayacaktır.

Bu örnekle, tüm uygulamaların sınıflarını yeniden yüklemenin aslında oldukça kolay olduğunu görüyoruz. Amaç yalnızca, canlı iş parçacığından kullanımda olan dinamik sınıf yükleyiciye ince, bırakılabilir bir bağlantı tutmaktır. Peki ya bazı nesnelerin (ve sınıflarının) yeniden yüklenmemesini ve yeniden yükleme döngüleri arasında yeniden kullanılmasını istiyorsak? Bir sonraki örneğe bakalım.

Örnek 4: Kalıcı ve Yeniden Yüklenen Sınıf Alanlarını Ayırma

İşte kaynak kodu..

main yöntem:

 public static void main(String[] args) { ConnectionPool pool = new ConnectionPool(); for (;;) { Object context = createContext(pool); invokeService(context); ThreadUtil.sleep(2000); } }

Böylece buradaki hilenin ConnectionPool sınıfını yüklemek ve onu yeniden yükleme döngüsünün dışında başlatmak, kalıcı alanda tutmak ve referansı Context nesnelerine iletmek olduğunu görebilirsiniz.

createContext yöntemi de biraz farklıdır:

 private static Object createContext(ConnectionPool pool) { ExceptingClassLoader classLoader = new ExceptingClassLoader( (className) -> className.contains(".crossing."), "target/classes"); Class<?> contextClass = classLoader.load("qj.blog.classreloading.example4.reloadable.Context"); Object context = newInstance(contextClass); setFieldValue(pool, "pool", context); invoke("init", context); return context; }

Artık her döngüde yeniden yüklenen nesnelere ve sınıflara “yeniden yüklenebilir alan” ve diğerlerine - yeniden yükleme döngüleri sırasında geri dönüştürülemeyen ve yenilenmeyen nesne ve sınıflara - “kalıcı alan” diyeceğiz. Hangi nesnelerin veya sınıfların hangi uzayda kaldığı konusunda çok net olmamız, böylece bu iki alan arasında bir ayrım çizgisi çizmemiz gerekecek.

Düzgün bir şekilde ele alınmadıkça, Java sınıfı yüklemesinin bu ayrımı başarısızlığa neden olabilir.

Resimden görüldüğü gibi, yalnızca Context nesnesi ve UserService nesnesi ConnectionPool nesnesine atıfta bulunmaz, Context ve UserService sınıfları da ConnectionPool sınıfına atıfta bulunur. Bu, genellikle kafa karışıklığına ve başarısızlığa yol açan çok tehlikeli bir durumdur. ConnectionPool sınıfı, DynamicClassLoader tarafından yüklenmemelidir, bellekte yalnızca ClassLoader tarafından yüklenen yalnızca bir ConnectionPool sınıfı olmalıdır. Bu, Java'da bir sınıf yeniden yükleme mimarisi tasarlarken dikkatli olmanın neden bu kadar önemli olduğunun bir örneğidir.

DynamicClassLoader yanlışlıkla ConnectionPool sınıfını yüklerse ne olur? Ardından, kalıcı alandan ConnectionPool nesnesi Context nesnesine geçirilemez, çünkü Context nesnesi, ConnectionPool olarak da adlandırılan, ancak aslında farklı bir sınıf olan farklı bir sınıftan bir nesne bekler!

Peki DynamicClassLoader ConnectionPool sınıfını yüklemesini nasıl önleyebiliriz? Bu örnek, DynamicClassLoader kullanmak yerine, bir koşul işlevine dayalı olarak yüklemeyi süper sınıf yükleyiciye ExceptingClassLoader adlı bir alt sınıfını kullanır:

 (className) -> className.contains("$Connection")

Burada ExceptingClassLoader kullanmazsak, DynamicClassLoader ConnectionPool sınıfını yükler çünkü o sınıf “ target/classes ” klasöründe bulunur. ConnectionPool sınıfının DynamicClassLoader tarafından alınmasını önlemenin bir başka yolu da ConnectionPool sınıfını farklı bir klasörde, belki farklı bir modülde derlemektir ve ayrı olarak derlenecektir.

Alan Seçme Kuralları

Şimdi, Java sınıfı yükleme işi gerçekten kafa karıştırıcı hale geliyor. Kalıcı alanda hangi sınıfların ve yeniden yüklenebilir alanda hangi sınıfların olması gerektiğini nasıl belirleriz? İşte kurallar:

  1. Yeniden yüklenebilir alandaki bir sınıf, kalıcı alandaki bir sınıfa başvurabilir, ancak kalıcı alandaki bir sınıf, yeniden yüklenebilir alandaki bir sınıfa asla başvuramaz. Önceki örnekte, yeniden yüklenebilir Context sınıfı, kalıcı ConnectionPool sınıfına başvurur, ancak ConnectionPool Context öğesine referansı yoktur.
  2. Bir sınıf, diğer uzaydaki herhangi bir sınıfa referans vermiyorsa, her iki uzayda da var olabilir. Örneğin, StringUtils gibi tüm statik yöntemlere sahip bir yardımcı program sınıfı, kalıcı alana bir kez yüklenebilir ve yeniden yüklenebilir alana ayrı olarak yüklenebilir.

Böylece kuralların çok kısıtlayıcı olmadığını görebilirsiniz. İki boşlukta referans verilen nesnelere sahip çapraz sınıflar dışında, diğer tüm sınıflar kalıcı alanda veya yeniden yüklenebilir alanda veya her ikisinde de serbestçe kullanılabilir. Tabii ki, yalnızca yeniden yüklenebilir alandaki sınıflar, yeniden yükleme döngüleriyle yeniden yüklenmenin keyfini çıkaracaktır.

Böylece sınıfın yeniden yüklenmesiyle ilgili en zorlu sorunla ilgilenilir. Bir sonraki örnekte, bu tekniği basit bir web uygulamasına uygulamaya çalışacağız ve tıpkı herhangi bir betik dili gibi Java sınıflarını yeniden yüklemenin keyfini çıkaracağız.

Örnek 5: Küçük Telefon Rehberi

İşte kaynak kodu..

Bu örnek, normal bir web uygulamasının nasıl görünmesi gerektiğine çok benzer olacaktır. AngularJS, SQLite, Maven ve Jetty Gömülü Web Sunucusu ile Tek Sayfa Uygulamasıdır.

İşte web sunucusunun yapısındaki yeniden yüklenebilir alan:

Web sunucusunun yapısındaki yeniden yüklenebilir alanın tam olarak anlaşılması, Java sınıfı yüklemede ustalaşmanıza yardımcı olacaktır.

Web sunucusu, yeniden yüklenebilmek için yeniden yüklenebilir alanda kalması gereken gerçek sunucu uygulamalarına referanslar tutmayacaktır. Tuttuğu şey, hizmet yöntemine yapılan her çağrıda, asıl sunucu uygulamasını gerçek bağlamda çalışacak şekilde çözecek olan saplama sunucu uygulamalarıdır.

Bu örnek ayrıca, web sunucusuna normal bir Context gibi tüm değerleri sağlayan, ancak dahili olarak bir DynamicClassLoader tarafından yeniden yüklenebilen gerçek bir bağlam nesnesine referanslar tutan yeni bir ReloadingWebContext nesnesini tanıtmaktadır. Web sunucusuna saplama sunucu uygulamaları sağlayan bu ReloadingWebContext .

ReloadingWebContext, Java sınıfı yeniden yükleme işleminde web sunucusuna giden saplama sunucu uygulamalarını işler.

ReloadingWebContext , gerçek bağlamın sarıcısı olacaktır ve:

  • Bir HTTP GET'i “/” olarak çağrıldığında gerçek bağlamı yeniden yükleyecektir.
  • Web sunucusuna saplama sunucu uygulamaları sağlar.
  • Gerçek bağlam her başlatıldığında veya yok edildiğinde değerleri ayarlar ve yöntemleri çağırır.
  • Bağlamı yeniden yükleyip yüklemeyecek ve yeniden yükleme için hangi sınıf yükleyicinin kullanılacağı yapılandırılabilir. Bu, uygulamayı üretimde çalıştırırken yardımcı olacaktır.

Kalıcı alanı ve yeniden yüklenebilir alanı nasıl yalıttığımızı anlamak çok önemli olduğundan, iki alan arasında kesişen iki sınıf şunlardır:

Context public F0<Connection> connF nesnesi için sınıf qj.util.funct.F0

  • İşlev nesnesi, işlev her çağrıldığında bir Bağlantı döndürür. Bu sınıf, DynamicClassLoader hariç tutulan qj.util paketinde bulunur.

Context public F0<Connection> connF nesnesi için java.sql.Connection sınıfı

  • Normal SQL bağlantı nesnesi. Bu sınıf, DynamicClassLoader sınıf yolunda bulunmadığından alınmayacaktır.

Özet

Bu Java sınıfları eğitiminde, tek bir sınıfın nasıl yeniden yükleneceğini, tek bir sınıfın sürekli olarak nasıl yeniden yükleneceğini, birden çok sınıftan oluşan bir alanın tamamının nasıl yeniden yükleneceğini ve kalıcı olması gereken sınıflardan ayrı olarak birden çok sınıfın nasıl yeniden yükleneceğini gördük. Bu araçlarla, güvenilir sınıf yeniden yükleme elde etmek için anahtar faktör, süper temiz bir tasarıma sahip olmaktır. Ardından sınıflarınızı ve tüm JVM'yi özgürce değiştirebilirsiniz.

Java sınıfı yeniden yüklemeyi uygulamak dünyadaki en kolay şey değil. Ama bir şans verirseniz ve bir noktada sınıflarınızın anında yüklendiğini görürseniz, neredeyse oradasınız demektir. Sisteminiz için tamamen mükemmel temiz bir tasarım elde etmeden önce yapmanız gereken çok az şey olacak.

İyi şanslar dostlarım ve yeni keşfettiğiniz süper gücün tadını çıkarın!