Tutorial Kelas Java Tingkat Lanjut: Panduan untuk Pemuatan Ulang Kelas

Diterbitkan: 2022-03-11

Dalam 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.

Memeriksa cara kerja Java ClassLoader default versus DynamicClassLoader adalah kunci untuk mendapatkan manfaat dari tutorial kelas Java ini.

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.

Sangat penting bahwa pengembang Java tingkat lanjut memahami pemuatan ulang kelas dinamis, baik aktif maupun tidak terhubung.

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.

Pemuatan ulang kelas Java sulit bahkan untuk insinyur Java tingkat lanjut.

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.

Kecuali ditangani dengan benar, pemisahan pemuatan kelas Java ini dapat menyebabkan kegagalan.

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:

  1. 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 kelas ConnectionPool yang bertahan, tetapi ConnectionPool tidak memiliki referensi ke Context
  2. 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:

Pemahaman menyeluruh tentang ruang yang dapat dimuat ulang dalam struktur server web akan membantu Anda menguasai pemuatan kelas Java.

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 menangani servlet rintisan ke server web dalam proses reload kelas Java.

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!