Saat runtime, temukan semua kelas di aplikasi Java yang memperluas kelas dasar

147

Saya ingin melakukan sesuatu seperti ini:

List<Animal> animals = new ArrayList<Animal>();

for( Class c: list_of_all_classes_available_to_my_app() )
   if (c is Animal)
      animals.add( new c() );

Jadi, saya ingin melihat semua kelas di alam semesta aplikasi saya, dan ketika saya menemukan satu yang turun dari Animal, saya ingin membuat objek baru dari jenis itu dan menambahkannya ke daftar. Ini memungkinkan saya untuk menambahkan fungsionalitas tanpa harus memperbarui daftar hal-hal. Saya dapat menghindari yang berikut ini:

List<Animal> animals = new ArrayList<Animal>();
animals.add( new Dog() );
animals.add( new Cat() );
animals.add( new Donkey() );
...

Dengan pendekatan di atas, saya cukup membuat kelas baru yang memperluas Animal dan itu akan diambil secara otomatis.

UPDATE: 10/16/2008 9:00 pagi Waktu Standar Pasifik:

Pertanyaan ini telah menghasilkan banyak tanggapan hebat - terima kasih. Dari tanggapan dan penelitian saya, saya telah menemukan bahwa apa yang sebenarnya ingin saya lakukan tidak mungkin dilakukan di Jawa. Ada pendekatan, seperti mekanisme ServiceLoader ddimitrov yang dapat bekerja - tetapi mereka sangat berat untuk apa yang saya inginkan, dan saya percaya saya hanya memindahkan masalah dari kode Java ke file konfigurasi eksternal. Perbarui 5/10/19 (11 tahun kemudian!) Sekarang ada beberapa perpustakaan yang dapat membantu dengan ini menurut jawaban @ IvanNik org.refleksi terlihat bagus. Juga ClassGraph dari jawaban @Luke Hutchison terlihat menarik. Ada beberapa kemungkinan lain dalam jawaban juga.

Cara lain untuk menyatakan apa yang saya inginkan: fungsi statis di kelas Animal saya menemukan dan membuat instance semua kelas yang mewarisi dari Animal - tanpa konfigurasi / coding lebih lanjut. Jika saya harus mengkonfigurasi, saya mungkin juga hanya instantiate di kelas Animal saja. Saya mengerti itu karena program Java hanyalah federasi longgar dari file .class yang memang seperti itu.

Menariknya, sepertinya ini cukup sepele di C #.

JohnnyLambada
sumber
1
Setelah mencari beberapa saat, tampaknya ini adalah kacang yang sulit untuk retak di Jawa. Berikut utas yang memiliki beberapa info: forums.sun.com/thread.jspa?threadID=341935&start=15 Implementasi di dalamnya lebih dari yang saya butuhkan, saya kira saya akan tetap menggunakan implementasi kedua untuk saat ini.
JohnnyLambada
letakkan itu sebagai jawaban dan biarkan pembaca memberikan suaranya
VonC
3
Tautan untuk melakukan ini di C # tidak menunjukkan sesuatu yang istimewa. AC # Assembly setara dengan file Java JAR. Ini adalah kumpulan file kelas yang dikompilasi (dan sumber daya). Tautan tersebut menunjukkan cara mengeluarkan kelas dari satu perakitan. Anda dapat melakukannya hampir dengan mudah dengan Java. Masalahnya bagi Anda adalah bahwa Anda perlu melihat semua file JAR dan file yang benar-benar longgar (direktori); Anda perlu melakukan hal yang sama dengan .NET (cari PATH dari beberapa jenis atau berbagai jenis).
Kevin Brock

Jawaban:

156

Saya menggunakan org.refleksi :

Reflections reflections = new Reflections("com.mycompany");    
Set<Class<? extends MyInterface>> classes = reflections.getSubTypesOf(MyInterface.class);

Contoh lain:

public static void main(String[] args) throws IllegalAccessException, InstantiationException {
    Reflections reflections = new Reflections("java.util");
    Set<Class<? extends List>> classes = reflections.getSubTypesOf(java.util.List.class);
    for (Class<? extends List> aClass : classes) {
        System.out.println(aClass.getName());
        if(aClass == ArrayList.class) {
            List list = aClass.newInstance();
            list.add("test");
            System.out.println(list.getClass().getName() + ": " + list.size());
        }
    }
}
IvanNik
sumber
6
Terima kasih atas tautan tentang org.refleksi. Saya keliru mengira ini akan semudah di dunia .Net dan jawaban ini telah menyelamatkan saya banyak waktu.
akmad
1
Terima kasih atas tautan bermanfaat ini ke paket hebat "org.reflections"! Dengan itu akhirnya saya menemukan solusi yang praktis dan rapi untuk masalah saya.
Hartmut P.
3
Apakah ada cara untuk mendapatkan semua refleksi, yaitu tanpa memperhatikan paket tertentu, tetapi memuat semua kelas yang tersedia?
Joey Baruch
Ingatlah bahwa menemukan kelas tidak akan menyelesaikan masalah Anda membuat instance (baris 5 animals.add( new c() );dari kode Anda), karena ketika Class.newInstance()ada, Anda tidak memiliki jaminan bahwa kelas spesifik memiliki konstruktor tanpa parameter (C # memungkinkan Anda mengharuskannya dalam generik)
usr-local-ΕΨΗΕΛΩΝ
Lihat juga ClassGraph: github.com/classgraph/classgraph (Penafian, saya penulisnya). Saya akan memberikan contoh kode dalam jawaban terpisah.
Luke Hutchison
34

Cara Java untuk melakukan apa yang Anda inginkan adalah menggunakan mekanisme ServiceLoader .

Juga banyak orang menggulung sendiri dengan memiliki file di lokasi classpath yang terkenal (yaitu /META-INF/services/myplugin.properties) dan kemudian menggunakan ClassLoader.getResources () untuk menghitung semua file dengan nama ini dari semua guci. Ini memungkinkan setiap toples mengekspor penyedianya sendiri dan Anda dapat membuat instantiate mereka dengan refleksi menggunakan Class.forName ()

ddimitrov
sumber
1
Untuk menghasilkan file layanan META-INF dengan mudah berdasarkan anotasi pada kelas, Anda dapat memeriksa: metainf-services.kohsuke.org atau code.google.com/p/spi
elek
Bleah. Hanya menambahkan tingkat kerumitan, tetapi tidak menghilangkan kebutuhan untuk membuat kelas dan kemudian mendaftarkannya di tanah yang jauh.
kevin cline
Silakan lihat jawaban di bawah ini dengan 26 upvote, jauh lebih baik
SobiborTreblinka
4
Memang, jika Anda memerlukan semacam solusi kerja, Refleksi / Scannotations adalah pilihan yang baik, namun keduanya memaksakan ketergantungan eksternal, tidak deterministik dan tidak didukung oleh Java Community Process. Selain itu, saya ingin menggarisbawahi bahwa Anda tidak akan pernah dapat andal SEMUA kelas yang mengimplementasikan antarmuka, karena itu sepele untuk membuat yang baru keluar dari udara tipis setiap saat. Untuk semua kutilnya, service loader Java mudah dipahami dan digunakan. Saya akan menjauh darinya karena berbagai alasan, tetapi kemudian pemindaian classpath lebih buruk lagi: stackoverflow.com/a/7237152/18187
ddimitrov
11

Pikirkan ini dari sudut pandang berorientasi aspek; apa yang ingin Anda lakukan, adalah mengetahui semua kelas saat runtime yang telah memperluas kelas Animal. (Saya pikir itu deskripsi yang sedikit lebih akurat tentang masalah Anda daripada judul Anda; jika tidak, saya tidak berpikir Anda memiliki pertanyaan runtime.)

Jadi apa yang saya pikir Anda inginkan adalah membuat konstruktor dari kelas dasar Anda (Hewan) yang menambah array statis Anda (saya lebih suka ArrayLists, saya sendiri, tetapi untuk masing-masing mereka sendiri) jenis Kelas saat ini yang sedang dipakai.

Jadi, kira-kira;

public abstract class Animal
    {
    private static ArrayList<Class> instantiatedDerivedTypes;
    public Animal() {
        Class derivedClass = this.getClass();
        if (!instantiatedDerivedClass.contains(derivedClass)) {
            instantiatedDerivedClass.Add(derivedClass);
        }
    }

Tentu saja, Anda memerlukan konstruktor statis pada Animal untuk menginisialisasi instantiatedDerivedClass ... Saya pikir ini akan melakukan apa yang Anda inginkan. Perhatikan bahwa ini tergantung pada jalur eksekusi; jika Anda memiliki kelas Anjing yang berasal dari Hewan yang tidak pernah dipanggil, Anda tidak akan memilikinya dalam daftar Kelas Hewan Anda.

Paul Sonier
sumber
6

Sayangnya ini tidak sepenuhnya mungkin karena ClassLoader tidak akan memberi tahu Anda kelas apa yang tersedia. Namun, Anda bisa melakukan sesuatu seperti ini:

for (String classpathEntry : System.getProperty("java.class.path").split(System.getProperty("path.separator"))) {
    if (classpathEntry.endsWith(".jar")) {
        File jar = new File(classpathEntry);

        JarInputStream is = new JarInputStream(new FileInputStream(jar));

        JarEntry entry;
        while( (entry = is.getNextJarEntry()) != null) {
            if(entry.getName().endsWith(".class")) {
                // Class.forName(entry.getName()) and check
                //   for implementation of the interface
            }
        }
    }
}

Sunting: johnstok benar (dalam komentar) bahwa ini hanya berfungsi untuk aplikasi Java mandiri, dan tidak akan berfungsi di bawah server aplikasi.

jonathan-stafford
sumber
Ini tidak berfungsi dengan baik ketika kelas dimuat dengan cara lain, Misalnya, dalam wadah web atau J2EE.
johnstok
Anda mungkin bisa melakukan pekerjaan yang lebih baik, bahkan di bawah sebuah wadah, jika Anda menanyakan jalur (sering URL[]tetapi tidak selalu jadi tidak mungkin) dari hierarki ClassLoader. Seringkali meskipun Anda harus memiliki izin karena biasanya Anda akan SecurityManagerdimuat dalam JVM.
Kevin Brock
Bekerja seperti pesona bagi saya! Saya khawatir ini akan menjadi terlalu berat dan lambat, melewati semua kelas di classpath, tetapi sebenarnya saya membatasi file yang saya periksa untuk yang berisi "-ejb-" atau nama file yang dapat dikenali lainnya, dan itu masih 100 kali lebih cepat daripada memulai glassfish atau EJBContainer tertanam. Dalam situasi khusus saya, saya kemudian menganalisis kelas mencari anotasi "Stateless", "Stateful", dan "Singleton", dan jika saya melihatnya, saya menambahkan nama JNDI Dipetakan ke MockInitialContextFactory saya. Terima kasih lagi!
cs94njw
4

Anda bisa menggunakan ResolverUtil ( sumber mentah ) dari Stripes Framework
jika Anda membutuhkan sesuatu yang sederhana dan cepat tanpa refactoring kode yang ada.

Berikut adalah contoh sederhana yang tidak memuat kelas apa pun:

package test;

import java.util.Set;
import net.sourceforge.stripes.util.ResolverUtil;

public class BaseClassTest {
    public static void main(String[] args) throws Exception {
        ResolverUtil<Animal> resolver = new ResolverUtil<Animal>();
        resolver.findImplementations(Animal.class, "test");
        Set<Class<? extends Animal>> classes = resolver.getClasses();

        for (Class<? extends Animal> clazz : classes) {
            System.out.println(clazz);
        }
    }
}

class Animal {}
class Dog extends Animal {}
class Cat extends Animal {}
class Donkey extends Animal {}

Ini juga berfungsi di server aplikasi juga karena di situlah ia dirancang untuk bekerja;)

Pada dasarnya kode melakukan hal berikut:

  • ulangi semua sumber daya dalam paket yang Anda tentukan
  • hanya menyimpan sumber daya yang berakhiran .class
  • Muat kelas-kelas itu menggunakan ClassLoader#loadClass(String fullyQualifiedName)
  • Cek apakah Animal.class.isAssignableFrom(loadedClass);
DJDaveMark
sumber
4

Mekanisme yang paling kuat untuk mendaftar semua subclass dari kelas yang diberikan saat ini adalah ClassGraph , karena menangani array seluas mungkin dari mekanisme spesifikasi classpath , termasuk sistem modul JPMS yang baru. (Akulah penulisnya.)

List<Class<Animal>> animals;
try (ScanResult scanResult = new ClassGraph().whitelistPackages("com.zoo.animals")
        .enableClassInfo().scan()) {
    animals = scanResult
        .getSubclasses(Animal.class.getName())
        .loadClasses(Animal.class);
}
Luke Hutchison
sumber
Hasilnya adalah tipe List <Class <Animal>>
hiaclibe
2

Java secara dinamis memuat kelas, jadi semesta kelas Anda hanya yang sudah dimuat (dan belum diturunkan). Mungkin Anda dapat melakukan sesuatu dengan pemuat kelas kustom yang dapat memeriksa supertipe dari setiap kelas yang dimuat. Saya tidak berpikir ada API untuk meminta set kelas yang dimuat.

sepenuhnya
sumber
2

Gunakan ini

public static Set<Class> getExtendedClasses(Class superClass)
{
    try
    {
        ResolverUtil resolver = new ResolverUtil();
        resolver.findImplementations(superClass, superClass.getPackage().getName());
        return resolver.getClasses();  
    }
    catch(Exception e)
    {Log.d("Log:", " Err: getExtendedClasses() ");}

    return null;
}

getExtendedClasses(Animals.class);

Edit:

  • perpustakaan untuk (ResolverUtil): Stripes
Ali Bagheri
sumber
2
Anda harus mengatakan lebih banyak tentang ResolverUtil. Apa itu? Dari mana perpustakaan itu berasal?
JohnnyLambada
1

Terima kasih semua yang menjawab pertanyaan ini.

Sepertinya ini memang kacang yang sulit retak. Saya akhirnya menyerah dan membuat array dan pengambil statis di baseclass saya.

public abstract class Animal{
    private static Animal[] animals= null;
    public static Animal[] getAnimals(){
        if (animals==null){
            animals = new Animal[]{
                new Dog(),
                new Cat(),
                new Lion()
            };
        }
        return animals;
    }
}

Tampaknya Java tidak diatur untuk dapat ditemukan sendiri seperti halnya C #. Saya kira masalahnya adalah karena aplikasi Java hanya kumpulan file .class di direktori / file jar di suatu tempat, runtime tidak tahu tentang kelas sampai direferensikan. Pada saat itu loader memuatnya - apa yang saya coba lakukan adalah menemukannya sebelum saya referensi yang tidak mungkin tanpa pergi ke sistem file dan mencari.

Saya selalu suka kode yang dapat menemukan sendiri alih-alih saya harus menceritakannya sendiri, tetapi sayangnya ini berhasil juga.

Terima kasih lagi!

JohnnyLambada
sumber
1
Java diatur untuk penemuan diri. Anda hanya perlu melakukan lebih banyak pekerjaan. Lihat jawaban saya untuk detailnya.
Dave Jarvis
1

Menggunakan OpenPojo Anda dapat melakukan hal berikut:

String package = "com.mycompany";
List<Animal> animals = new ArrayList<Animal>();

for(PojoClass pojoClass : PojoClassFactory.enumerateClassesByExtendingType(package, Animal.class, null) {
  animals.add((Animal) InstanceFactory.getInstance(pojoClass));
}
Osman Shoukry
sumber
0

Ini adalah masalah yang sulit dan Anda harus mencari tahu informasi ini menggunakan analisis statis, itu tidak tersedia dengan mudah saat runtime. Pada dasarnya dapatkan classpath dari aplikasi Anda dan pindai seluruh kelas yang tersedia dan baca informasi bytecode dari kelas yang menjadi asal kelasnya. Perhatikan bahwa Anjing kelas mungkin tidak secara langsung mewarisi dari Hewan tetapi mungkin mewarisi dari Pet yang berubah mewarisi dari Hewan, jadi Anda harus melacak hierarki itu.

pdeva
sumber
0

Salah satu caranya adalah membuat kelas menggunakan inisialisasi statis ... Saya tidak berpikir ini diwarisi (tidak akan berhasil jika):

public class Dog extends Animal{

static
{
   Animal a = new Dog();
   //add a to the List
}

Anda harus menambahkan kode ini ke semua kelas yang terlibat. Tapi itu menghindari loop jelek yang besar di suatu tempat, menguji setiap kelas mencari anak-anak Animal.


sumber
3
Ini tidak berfungsi dalam kasus saya. Karena kelas tidak direferensikan di mana saja itu tidak pernah dimuat sehingga penginisialisasi statis tidak pernah dipanggil. Tampaknya penginisialisasi statis dipanggil sebelum yang lainnya ketika kelas dimuat - tetapi dalam kasus saya kelas tidak pernah dimuat.
JohnnyLambada
0

Saya memecahkan masalah ini cukup elegan menggunakan Package Level Annotations dan kemudian membuat anotasi itu sebagai argumen daftar kelas.

Temukan kelas Java yang mengimplementasikan antarmuka

Implementasi hanya perlu membuat package-info.java dan memasukkan penjelasan ajaib dengan daftar kelas yang ingin mereka dukung.

Adam Gent
sumber