Gelişmiş Java Sınıfı Eğitimi: Sınıf Yeniden Yükleme Kılavuzu
Yayınlanan: 2022-03-11Java 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.
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.
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.
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.
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:
- 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, ancakConnectionPool
Context
öğesine referansı yoktur. - 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 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
, 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!