Java, Classpath, Classloading => Beberapa Versi dari jar / proyek yang sama

118

Saya tahu ini mungkin pertanyaan konyol bagi pembuat kode berpengalaman. Tetapi saya memiliki pustaka (klien http) yang dibutuhkan oleh beberapa kerangka kerja / botol lain yang digunakan dalam proyek saya. Tetapi semuanya membutuhkan versi utama yang berbeda seperti:

httpclient-v1.jar => Required by cralwer.jar
httpclient-v2.jar => Required by restapi.jar
httpclient-v3.jar => required by foobar.jar

Apakah classloader cukup cerdas untuk memisahkannya? Kemungkinan besar tidak? Bagaimana Classloader menangani ini, jika Kelas sama di ketiga toples. Yang mana yang dimuat dan mengapa?

Apakah Classloader hanya mengambil satu botol atau mencampur kelas secara sembarangan? Jadi misalnya jika sebuah kelas dimuat dari Versi-1.jar, semua kelas lain yang dimuat dari classloader yang sama akan masuk ke dalam jar yang sama?

Bagaimana Anda menangani masalah ini?

Apakah ada beberapa trik untuk entah bagaimana "memasukkan" toples ke dalam "required.jar" sehingga terlihat sebagai "satu unit / paket" oleh Classloader, atau entah bagaimana terkait?

jens
sumber

Jawaban:

57

Masalah terkait classloader adalah masalah yang cukup kompleks. Bagaimanapun, Anda harus mengingat beberapa fakta:

  • Classloader dalam aplikasi biasanya lebih dari satu. Pemuat kelas bootstrap mendelegasikan ke yang sesuai. Saat Anda membuat instance kelas baru, classloader yang lebih spesifik akan dipanggil. Jika tidak menemukan referensi ke kelas yang Anda coba muat, ia akan mendelegasikannya ke induknya, dan seterusnya, hingga Anda mencapai pemuat kelas bootstrap. Jika tidak ada yang menemukan referensi ke kelas yang Anda coba muat, Anda akan mendapatkan ClassNotFoundException.

  • Jika Anda memiliki dua kelas dengan nama biner yang sama, dapat ditelusuri oleh classloader yang sama, dan Anda ingin tahu kelas mana yang Anda muat, Anda hanya dapat memeriksa cara classloader tertentu mencoba menyelesaikan nama kelas.

  • Menurut spesifikasi bahasa java, tidak ada batasan keunikan untuk nama biner kelas, tetapi sejauh yang saya lihat, nama tersebut harus unik untuk setiap classloader.

Saya dapat menemukan cara untuk memuat dua kelas dengan nama biner yang sama, dan itu melibatkan agar mereka dimuat (dan semua ketergantungannya) oleh dua classloader berbeda yang menimpa perilaku default. Contoh kasar:

    ClassLoader loaderA = new MyClassLoader(libPathOne);
    ClassLoader loaderB = new MyClassLoader(libPathTwo);
    Object1 obj1 = loaderA.loadClass("first.class.binary.name", true)
    Object2 obj2 = loaderB.loadClass("second.class.binary.name", true);

Saya selalu menemukan kustomisasi classloader sebagai tugas yang rumit. Saya lebih suka menyarankan untuk menghindari beberapa dependensi yang tidak kompatibel jika memungkinkan.

Luca Putzu
sumber
13
Pemuat kelas bootstrap mendelegasikan ke yang sesuai. Saat Anda membuat instance kelas baru, classloader yang lebih spesifik akan dipanggil. Jika tidak menemukan referensi ke kelas yang Anda coba muat, ia akan mendelegasikan ke induknya Harap bersabarlah, tetapi itu tergantung pada kebijakan classloader yang secara default adalah Parent First. Dengan kata lain kelas anak pertama-tama akan meminta induknya untuk memuat kelas dan hanya akan memuat jika seluruh hierarki gagal memuatnya, bukan ??
deckingraj
5
Tidak - biasanya classloader mendelegasikan ke induknya sebelum mencari kelas itu sendiri. Lihat kelas javadoc untuk Classloader.
Joe Kearney
1
Saya pikir tomcat melakukannya dengan cara yang dijelaskan di sini, tetapi delegasi "konvensional" adalah bertanya kepada orang tua terlebih dahulu
rogerdpack
@deckingraj: setelah beberapa googling saya menemukan ini dari oracle docs: "Dalam desain delegasi, pemuat kelas mendelegasikan pemuatan kelas ke induknya sebelum mencoba memuat kelas itu sendiri. [...] Jika pemuat kelas induk tidak dapat memuat kelas, pemuat kelas mencoba memuat kelas itu sendiri. Akibatnya, pemuat kelas bertanggung jawab untuk memuat hanya kelas-kelas yang tidak tersedia untuk induknya ". Saya akan menyelidiki lebih lanjut. Jika ini akan muncul sebagai implementasi default, saya akan memperbarui respons yang sesuai. ( docs.oracle.com/cd/E19501-01/819-3659/beadf/index.html )
Luca Putzu
20

Setiap beban kelas memilih tepat satu kelas. Biasanya yang pertama ditemukan.

OSGi bertujuan untuk memecahkan masalah beberapa versi dari jar yang sama. Equinox dan Apache Felix adalah implementasi sumber terbuka yang umum untuk OSGi.

Tarlog
sumber
6

Classloader akan memuat kelas dari jar yang kebetulan berada di jalur kelas terlebih dahulu. Biasanya, versi pustaka yang tidak kompatibel akan memiliki perbedaan dalam paket, Tetapi dalam kasus yang tidak mungkin mereka benar-benar tidak kompatibel dan tidak dapat diganti dengan satu - coba jarjar.

Alex Gitelman
sumber
6

Classloader memuat kelas sesuai permintaan. Ini berarti bahwa kelas yang diperlukan terlebih dahulu oleh aplikasi Anda dan pustaka terkait akan dimuat sebelum kelas lain; permintaan untuk memuat kelas-kelas dependen biasanya dikeluarkan selama proses pemuatan dan penautan dari kelas yang bergantung.

Anda kemungkinan besar akan menemukan LinkageErrorpernyataan bahwa definisi kelas duplikat telah ditemukan karena classloader biasanya tidak mencoba menentukan kelas mana yang harus dimuat terlebih dahulu (jika ada dua atau lebih kelas dengan nama yang sama yang ada di classpath loader). Terkadang, classloader akan memuat kelas pertama yang terjadi di classpath dan mengabaikan kelas duplikat, tetapi ini bergantung pada implementasi loader.

Praktik yang disarankan untuk mengatasi jenis error tersebut adalah dengan menggunakan classloader terpisah untuk setiap kumpulan pustaka yang memiliki dependensi yang bertentangan. Dengan begitu, jika classloader mencoba memuat class dari library, class dependen akan dimuat oleh classloader yang sama yang tidak memiliki akses ke library dan dependensi lain.

Vineet Reynolds
sumber
1

Anda dapat menggunakan URLClassLoaderfor untuk memuat kelas dari versi diff-2 jars:

URLClassLoader loader1 = new URLClassLoader(new URL[] {new File("httpclient-v1.jar").toURL()}, Thread.currentThread().getContextClassLoader());
URLClassLoader loader2 = new URLClassLoader(new URL[] {new File("httpclient-v2.jar").toURL()}, Thread.currentThread().getContextClassLoader());

Class<?> c1 = loader1.loadClass("com.abc.Hello");

Class<?> c2 = loader2.loadClass("com.abc.Hello");

BaseInterface i1 = (BaseInterface) c1.newInstance();

BaseInterface i2 = (BaseInterface) c2.newInstance();
Pankaj Kalra
sumber