Tutorial Kelas Java Tingkat Lanjut: Panduan untuk Pemuatan Ulang Kelas
Diterbitkan: 2022-03-11Dalam proyek pengembangan Java, alur kerja tipikal melibatkan memulai ulang server dengan setiap perubahan kelas, dan tidak ada yang mengeluh tentang hal itu. Itu adalah fakta tentang pengembangan Java. Kami telah bekerja seperti itu sejak hari pertama kami dengan Java. Tetapi apakah memuat ulang kelas Java itu sulit untuk dicapai? Dan mungkinkah masalah itu menantang sekaligus mengasyikkan untuk dipecahkan bagi pengembang Java yang terampil? Dalam tutorial kelas Java ini, saya akan mencoba untuk mengatasi masalah tersebut, membantu Anda mendapatkan semua manfaat dari reload kelas on-the-fly, dan sangat meningkatkan produktivitas Anda.
Pemuatan ulang kelas Java tidak sering dibahas, dan sangat sedikit dokumentasi yang mengeksplorasi proses ini. Saya di sini untuk mengubah itu. Tutorial kelas Java ini akan memberikan penjelasan langkah demi langkah dari proses ini dan membantu Anda menguasai teknik yang luar biasa ini. Ingatlah bahwa mengimplementasikan reload kelas Java membutuhkan banyak perhatian, tetapi mempelajari cara melakukannya akan menempatkan Anda di liga besar, baik sebagai pengembang Java, dan sebagai arsitek perangkat lunak. Juga tidak ada salahnya untuk memahami cara menghindari 10 kesalahan Java yang paling umum.
Pengaturan Ruang Kerja
Semua kode sumber untuk tutorial ini diunggah di GitHub di sini.
Untuk menjalankan kode saat Anda mengikuti tutorial ini, Anda memerlukan Maven, Git dan Eclipse atau IntelliJ IDEA.
Jika Anda menggunakan Eclipse:
- Jalankan perintah
mvn eclipse:eclipse
untuk menghasilkan file proyek Eclipse. - Muat proyek yang dihasilkan.
- Tetapkan jalur keluaran ke
target/classes
.
Jika Anda menggunakan IntelliJ:
- Impor file
pom
proyek. - IntelliJ tidak akan mengkompilasi otomatis saat Anda menjalankan contoh apa pun, jadi Anda harus:
- Jalankan contoh di dalam IntelliJ, lalu setiap kali Anda ingin mengkompilasi, Anda harus menekan
Alt+BE
- Jalankan contoh di luar IntelliJ dengan
run_example*.bat
. Setel kompilasi otomatis kompiler IntelliJ ke true. Kemudian, setiap kali Anda mengubah file java, IntelliJ akan mengompilasinya secara otomatis.
Contoh 1: Memuat Ulang Kelas dengan Java Class Loader
Contoh pertama akan memberi Anda pemahaman umum tentang pemuat kelas Java. Berikut adalah kode sumbernya.
Diberikan definisi kelas User
berikut:
public static class User { public static int age = 10; }
Kita dapat melakukan hal berikut:
public static void main(String[] args) { Class<?> userClass1 = User.class; Class<?> userClass2 = new DynamicClassLoader("target/classes") .load("qj.blog.classreloading.example1.StaticInt$User"); ...
Dalam contoh tutorial ini, akan ada dua kelas User
yang dimuat ke dalam memori. userClass1
akan dimuat oleh pemuat kelas default JVM, dan userClass2
menggunakan DynamicClassLoader
, pemuat kelas khusus yang kode sumbernya juga disediakan dalam proyek GitHub, dan yang akan saya jelaskan secara rinci di bawah ini.
Berikut adalah sisa metode main
:
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)); }
Dan keluarannya:
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
Seperti yang Anda lihat di sini, meskipun kelas User
memiliki nama yang sama, mereka sebenarnya adalah dua kelas yang berbeda, dan mereka dapat dikelola, dan dimanipulasi, secara independen. Nilai usia, meskipun dinyatakan sebagai statis, ada dalam dua versi, dilampirkan secara terpisah ke setiap kelas, dan dapat diubah secara independen juga.
Dalam program Java normal, ClassLoader
adalah portal yang membawa kelas ke JVM. Ketika satu kelas membutuhkan kelas lain untuk dimuat, itu adalah tugas ClassLoader
untuk melakukan pemuatan.
Namun, dalam contoh kelas Java ini, ClassLoader
kustom bernama DynamicClassLoader
digunakan untuk memuat versi kedua dari kelas User
. Jika alih-alih DynamicClassLoader
, kami menggunakan pemuat kelas default lagi ( dengan perintah StaticInt.class.getClassLoader()
) maka kelas User
yang sama akan digunakan, karena semua kelas yang dimuat di-cache.
DynamicClassLoader
Mungkin ada beberapa classloader dalam program Java normal. Salah satu yang memuat kelas utama Anda, ClassLoader
, adalah yang default, dan dari kode Anda, Anda dapat membuat dan menggunakan classloader sebanyak yang Anda suka. Ini, kemudian, adalah kunci untuk memuat ulang kelas di Jawa. DynamicClassLoader
mungkin merupakan bagian terpenting dari keseluruhan tutorial ini, jadi kita harus memahami cara kerja pemuatan kelas dinamis sebelum kita dapat mencapai tujuan kita.
Tidak seperti perilaku default ClassLoader
, DynamicClassLoader
kami mewarisi strategi yang lebih agresif. Classloader normal akan memberikan prioritas ClassLoader
induknya dan hanya memuat kelas yang tidak dapat dimuat oleh induknya. Itu cocok untuk keadaan normal, tetapi tidak dalam kasus kami. Sebagai gantinya, DynamicClassLoader
akan mencoba untuk melihat melalui semua jalur kelasnya dan menyelesaikan kelas target sebelum memberikan hak kepada induknya.
Dalam contoh kami di atas, DynamicClassLoader
dibuat dengan hanya satu jalur kelas: "target/classes"
(dalam direktori kami saat ini), sehingga mampu memuat semua kelas yang berada di lokasi itu. Untuk semua kelas yang tidak ada di sana, itu harus merujuk ke classloader induk. Misalnya, kita perlu memuat kelas String
di kelas StaticInt
kita, dan pemuat kelas kita tidak memiliki akses ke rt.jar
di folder JRE kita, jadi kelas String
dari pemuat kelas induk akan digunakan.
Kode berikut berasal dari AggressiveClassLoader
, kelas induk dari DynamicClassLoader
, dan menunjukkan di mana perilaku ini didefinisikan.
byte[] newClassData = loadNewClass(name); if (newClassData != null) { loadedClasses.add(name); return loadClass(newClassData, name); } else { unavaiClasses.add(name); return parent.loadClass(name); }
Perhatikan properti DynamicClassLoader
berikut:
- Kelas yang dimuat memiliki kinerja dan atribut lain yang sama dengan kelas lain yang dimuat oleh pemuat kelas default.
-
DynamicClassLoader
dapat dikumpulkan bersama dengan semua kelas dan objek yang dimuat.
Dengan kemampuan untuk memuat dan menggunakan dua versi dari kelas yang sama, kami sekarang berpikir untuk membuang versi lama dan memuat yang baru untuk menggantikannya. Pada contoh berikutnya, kita akan melakukan hal itu… terus menerus.
Contoh 2: Memuat Ulang Kelas Secara Terus-menerus
Contoh Java berikut ini akan menunjukkan kepada Anda bahwa JRE dapat memuat dan memuat ulang kelas selamanya, dengan kelas lama dibuang dan sampah dikumpulkan, dan kelas baru dimuat dari hard drive dan digunakan. Berikut adalah kode sumbernya.
Berikut adalah loop utama:
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); } }
Setiap dua detik, kelas User
lama akan dibuang, yang baru akan dimuat dan hobby
metodenya dipanggil.
Berikut adalah definisi kelas User
:
@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"); // } }
Saat menjalankan aplikasi ini, Anda harus mencoba memberi komentar dan menghapus komentar pada kode yang ditunjukkan kode di kelas User
. Anda akan melihat bahwa definisi terbaru akan selalu digunakan.
Berikut adalah beberapa contoh keluaran:
... Play Football Play Football Play Football Play Basketball Play Basketball Play Basketball
Setiap kali instance baru DynamicClassLoader
dibuat, itu akan memuat kelas User
dari folder target/classes
, di mana kami telah mengatur Eclipse atau IntelliJ untuk menampilkan file kelas terbaru. Semua kelas DynamicClassLoader
dan User
lama akan dibatalkan tautannya dan tunduk pada pengumpul sampah.
Jika Anda sudah familiar dengan JVM HotSpot, maka perlu diperhatikan di sini bahwa struktur kelas juga dapat diubah dan dimuat ulang: metode playFootball
harus dihapus dan metode playBasketball
ditambahkan. Ini berbeda dengan HotSpot, yang memungkinkan hanya konten metode yang diubah, atau kelas tidak dapat dimuat ulang.
Sekarang kita mampu memuat ulang kelas, sekarang saatnya untuk mencoba memuat ulang banyak kelas sekaligus. Mari kita coba pada contoh berikutnya.
Contoh 3: Memuat Ulang Beberapa Kelas
Output dari contoh ini akan sama dengan Contoh 2, tetapi akan menunjukkan bagaimana menerapkan perilaku ini dalam struktur yang lebih mirip aplikasi dengan objek konteks, layanan, dan model. Kode sumber contoh ini agak besar, jadi saya hanya menunjukkan sebagiannya di sini. Kode sumber lengkapnya ada di sini.
Inilah metode main
:
public static void main(String[] args) { for (;;) { Object context = createContext(); invokeHobbyService(context); ThreadUtil.sleep(2000); } }
Dan metode createContext
:
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; }
Metode invokeHobbyService
:
private static void invokeHobbyService(Object context) { Object hobbyService = getFieldValue("hobbyService", context); invoke("hobby", hobbyService); }
Dan inilah kelas Context
:

public static class Context { public HobbyService hobbyService = new HobbyService(); public void init() { // Init your services here hobbyService.user = new User(); } }
Dan kelas HobbyService
:
public static class HobbyService { public User user; public void hobby() { user.hobby(); } }
Kelas Context
dalam contoh ini jauh lebih rumit daripada kelas User
dalam contoh sebelumnya: kelas ini memiliki tautan ke kelas lain, dan memiliki metode init
untuk dipanggil setiap kali dibuat. Pada dasarnya, ini sangat mirip dengan kelas konteks aplikasi dunia nyata (yang melacak modul aplikasi dan melakukan injeksi ketergantungan). Jadi dapat memuat ulang kelas Context
ini bersama dengan semua kelas terkaitnya adalah langkah yang bagus untuk menerapkan teknik ini ke kehidupan nyata.
Seiring bertambahnya jumlah kelas dan objek, langkah kita untuk "meninggalkan versi lama" juga akan menjadi lebih rumit. Ini juga merupakan alasan terbesar mengapa reload kelas sangat sulit. Untuk kemungkinan menjatuhkan versi lama, kita harus memastikan bahwa, setelah konteks baru dibuat, semua referensi ke kelas dan objek lama dihapus. Bagaimana kita menangani ini dengan elegan?
Metode main
di sini akan memegang objek konteks, dan itu adalah satu-satunya tautan ke semua hal yang perlu dibuang. Jika kita memutuskan tautan itu, objek konteks dan kelas konteks, dan objek layanan ... semuanya akan dikenakan pengumpul sampah.
Sedikit penjelasan tentang mengapa biasanya kelas begitu gigih, dan tidak mengumpulkan sampah:
- Biasanya, kami memuat semua kelas kami ke dalam classloader Java default.
- Hubungan class-classloader adalah hubungan dua arah, dengan class loader juga men-cache semua kelas yang telah dimuatnya.
- Jadi selama classloader masih terhubung ke utas langsung apa pun, semuanya (semua kelas yang dimuat) akan kebal terhadap pengumpul sampah.
- Yang mengatakan, kecuali kita dapat memisahkan kode yang ingin kita muat ulang dari kode yang sudah dimuat oleh pemuat kelas default, perubahan kode baru kita tidak akan pernah diterapkan selama runtime.
Dengan contoh ini, kita melihat bahwa memuat ulang semua kelas aplikasi sebenarnya cukup mudah. Tujuannya hanyalah untuk menjaga koneksi yang tipis dan dapat dijatuhkan dari utas langsung ke pemuat kelas dinamis yang digunakan. Tetapi bagaimana jika kita ingin beberapa objek (dan kelasnya) tidak dimuat ulang, dan digunakan kembali di antara siklus pemuatan ulang? Mari kita lihat contoh berikutnya.
Contoh 4: Memisahkan Ruang Kelas yang Bertahan dan Dimuat Ulang
Berikut source codenya..
Metode main
:
public static void main(String[] args) { ConnectionPool pool = new ConnectionPool(); for (;;) { Object context = createContext(pool); invokeService(context); ThreadUtil.sleep(2000); } }
Jadi Anda dapat melihat bahwa triknya di sini adalah memuat kelas ConnectionPool
dan membuat instance di luar siklus pemuatan ulang, menyimpannya di ruang yang dipertahankan, dan meneruskan referensi ke objek Context
Metode createContext
juga sedikit berbeda:
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; }
Mulai sekarang, kita akan menyebut objek dan kelas yang dimuat ulang dengan setiap siklus sebagai "ruang yang dapat dimuat ulang" dan lainnya - objek dan kelas yang tidak didaur ulang dan tidak diperbarui selama siklus pemuatan ulang - "ruang yang bertahan". Kita harus sangat jelas tentang objek atau kelas mana yang tinggal di ruang mana, sehingga menggambar garis pemisah antara dua ruang ini.
Seperti yang terlihat dari gambar, tidak hanya objek Context
dan objek UserService
yang merujuk ke objek ConnectionPool
, tetapi kelas Context
dan UserService
juga merujuk ke kelas ConnectionPool
. Ini adalah situasi yang sangat berbahaya yang sering menyebabkan kebingungan dan kegagalan. Kelas ConnectionPool
tidak boleh dimuat oleh DynamicClassLoader
kami, harus ada hanya satu kelas ConnectionPool
di memori, yang dimuat oleh ClassLoader
default. Ini adalah salah satu contoh mengapa sangat penting untuk berhati-hati saat mendesain arsitektur class-reload di Java.
Bagaimana jika DynamicClassLoader
kami secara tidak sengaja memuat kelas ConnectionPool
? Kemudian objek ConnectionPool
dari ruang bertahan tidak dapat diteruskan ke objek Context
, karena objek Context
mengharapkan objek dari kelas yang berbeda, yang juga bernama ConnectionPool
, tetapi sebenarnya adalah kelas yang berbeda!
Jadi bagaimana kita mencegah DynamicClassLoader
kita memuat kelas ConnectionPool
? Alih-alih menggunakan DynamicClassLoader
, contoh ini menggunakan subkelasnya bernama: ExceptingClassLoader
, yang akan meneruskan pemuatan ke super classloader berdasarkan fungsi kondisi:
(className) -> className.contains("$Connection")
Jika kita tidak menggunakanExceptioningClassLoader di sini, maka ExceptingClassLoader
akan DynamicClassLoader
kelas ConnectionPool
karena kelas tersebut berada di folder “ target/classes
”. Cara lain untuk mencegah kelas ConnectionPool
diambil oleh DynamicClassLoader
kami adalah dengan mengkompilasi kelas ConnectionPool
ke folder yang berbeda, mungkin dalam modul yang berbeda, dan itu akan dikompilasi secara terpisah.
Aturan Memilih Ruang
Sekarang, pekerjaan pemuatan kelas Java menjadi sangat membingungkan. Bagaimana kita menentukan kelas mana yang harus berada di ruang bertahan, dan kelas mana di ruang yang dapat dimuat ulang? Berikut aturannya:
- Kelas di ruang yang dapat dimuat ulang dapat mereferensikan kelas di ruang tetap, tetapi kelas di ruang bertahan mungkin tidak pernah mereferensikan kelas di ruang yang dapat dimuat ulang. Dalam contoh sebelumnya, kelas
Context
yang dapat dimuat ulang merujuk ke kelasConnectionPool
yang bertahan, tetapiConnectionPool
tidak memiliki referensi keContext
- Sebuah kelas bisa eksis di salah satu ruang jika tidak mereferensikan kelas mana pun di ruang lain. Misalnya, kelas utilitas dengan semua metode statis seperti
StringUtils
dapat dimuat satu kali di ruang tetap, dan dimuat secara terpisah di ruang yang dapat dimuat ulang.
Jadi Anda dapat melihat bahwa aturannya tidak terlalu membatasi. Kecuali untuk kelas persilangan yang memiliki objek yang direferensikan melintasi dua ruang, semua kelas lain dapat digunakan secara bebas baik di ruang tetap atau ruang yang dapat diisi ulang atau keduanya. Tentu saja, hanya kelas di ruang yang dapat diisi ulang yang akan menikmati pemuatan ulang dengan siklus pemuatan ulang.
Jadi masalah yang paling menantang dengan reload kelas ditangani. Pada contoh berikutnya, kita akan mencoba menerapkan teknik ini ke aplikasi web sederhana, dan menikmati memuat ulang kelas Java seperti bahasa skrip lainnya.
Contoh 5: Buku Telepon Kecil
Berikut source codenya..
Contoh ini akan sangat mirip dengan tampilan aplikasi web biasa. Ini adalah Aplikasi Halaman Tunggal dengan AngularJS, SQLite, Maven, dan Server Web Tertanam Jetty.
Berikut adalah ruang yang dapat diisi ulang dalam struktur server web:
Server web tidak akan menyimpan referensi ke servlet yang sebenarnya, yang harus tetap berada di ruang yang dapat diisi ulang, agar dapat dimuat ulang. Apa yang dipegangnya adalah servlet rintisan, yang, dengan setiap panggilan ke metode layanannya, akan menyelesaikan servlet aktual dalam konteks aktual untuk dijalankan.
Contoh ini juga memperkenalkan objek baru ReloadingWebContext
, yang memberikan ke server web semua nilai seperti Context normal, tetapi secara internal menyimpan referensi ke objek konteks aktual yang dapat dimuat ulang oleh DynamicClassLoader
. ReloadingWebContext
inilah yang menyediakan servlet rintisan ke server web.
ReloadingWebContext
akan menjadi pembungkus dari konteks yang sebenarnya, dan:
- Akan memuat ulang konteks aktual saat HTTP GET ke “/” dipanggil.
- Akan memberikan servlet rintisan ke server web.
- Akan menetapkan nilai dan memanggil metode setiap kali konteks aktual diinisialisasi atau dihancurkan.
- Dapat dikonfigurasi untuk memuat ulang konteks atau tidak, dan pemuat kelas mana yang digunakan untuk memuat ulang. Ini akan membantu saat menjalankan aplikasi dalam produksi.
Karena sangat penting untuk memahami bagaimana kami mengisolasi ruang yang bertahan dan ruang yang dapat diisi ulang, berikut adalah dua kelas yang melintasi antara dua ruang:
Kelas qj.util.funct.F0
untuk objek public F0<Connection> connF
in Context
- Objek fungsi, akan mengembalikan Koneksi setiap kali fungsi dipanggil. Kelas ini berada dalam paket qj.util, yang dikecualikan dari
DynamicClassLoader
.
Kelas java.sql.Connection
untuk objek public F0<Connection> connF
in Context
- Objek koneksi SQL normal. Kelas ini tidak berada di jalur kelas
DynamicClassLoader
kami sehingga tidak akan diambil.
Ringkasan
Dalam tutorial kelas Java ini, kita telah melihat cara memuat ulang satu kelas, memuat ulang satu kelas secara terus-menerus, memuat ulang seluruh ruang dari beberapa kelas, dan memuat ulang beberapa kelas secara terpisah dari kelas yang harus dipertahankan. Dengan alat ini, faktor kunci untuk mencapai pemuatan ulang kelas yang andal adalah memiliki desain yang sangat bersih. Kemudian Anda dapat dengan bebas memanipulasi kelas Anda dan seluruh JVM.
Menerapkan reload kelas Java bukanlah hal termudah di dunia. Tetapi jika Anda mencobanya, dan pada titik tertentu menemukan kelas Anda dimuat dengan cepat, maka Anda sudah hampir sampai. Akan ada sangat sedikit yang harus dilakukan sebelum Anda dapat mencapai desain bersih yang benar-benar luar biasa untuk sistem Anda.
Semoga berhasil teman-teman saya dan nikmati kekuatan super yang baru Anda temukan!