Bagaimana Anda menemukan semua subclass dari kelas yang diberikan di Jawa?

207

Bagaimana caranya dan mencoba menemukan semua subclass dari kelas yang diberikan (atau semua implementator dari antarmuka yang diberikan) di Jawa? Sampai sekarang, saya punya metode untuk melakukan ini, tetapi saya merasa cukup tidak efisien (untuk sedikitnya). Metodenya adalah:

  1. Dapatkan daftar semua nama kelas yang ada di jalur kelas
  2. Muat masing-masing kelas dan uji untuk melihat apakah itu adalah subclass atau implementor dari kelas atau antarmuka yang diinginkan

Di Eclipse, ada fitur bagus yang disebut Hirarki Tipe yang berhasil menunjukkan ini dengan cukup efisien. Bagaimana caranya dan melakukannya secara terprogram?

Avrom
sumber
1
Meskipun solusi berdasarkan Refleksi dan Musim Semi terlihat menarik, saya membutuhkan beberapa solusi sederhana yang tidak memiliki ketergantungan. Tampaknya kode asli saya (dengan beberapa tweak) adalah cara untuk pergi.
Avrom
1
Tentunya Anda dapat menggunakan metode getSupeClass secara rekursif?
Saya secara khusus mencari semua subclass dari kelas yang diberikan. getSuperClass tidak akan memberi tahu Anda apa yang dimiliki subclass kelas, hanya mendapatkan kelas super langsung untuk subclass tertentu. Juga, metode isAssignableFrom on Class lebih cocok untuk apa yang Anda sarankan (tidak perlu rekursi).
Avrom
Pertanyaan ini ditautkan dari banyak duplikat lain, tetapi tidak mengandung jawaban yang berguna dan sederhana dari Java.
Huh

Jawaban:

77

Tidak ada cara lain untuk melakukannya selain apa yang Anda gambarkan. Pikirkan tentang hal ini - bagaimana bisa ada yang tahu kelas apa yang memperluas ClassX tanpa memindai setiap kelas di classpath?

Eclipse hanya dapat memberi tahu Anda tentang super dan subclass dalam apa yang tampaknya menjadi jumlah waktu "efisien" karena sudah memiliki semua tipe data yang dimuat pada titik di mana Anda menekan tombol "Tampilan dalam Jenis Hirarki" (karena itu adalah terus mengkompilasi kelas Anda, tahu tentang semua yang ada di classpath, dll).

matt b
sumber
23
Sekarang ada perpustakaan sederhana bernama org.refleksi yang membantu dengan ini dan tugas refleksi umum lainnya. Dengan perpustakaan ini, Anda bisa menelepon reflections.getSubTypesOf(aClazz)) tautan
Diinginkan
@matt b - jika harus memindai semua kelas, apakah ini berarti ada penurunan kinerja ketika Anda memiliki banyak kelas di proyek Anda, meskipun hanya sedikit dari mereka yang mengelompokkan kelas Anda?
LeTex
Persis. Itu hanya menyentuh semua kelas. Anda dapat menentukan pemindai Anda sendiri, Anda dapat mempercepatnya seperti mengecualikan paket-paket tertentu yang Anda tahu kelas Anda tidak akan diperpanjang atau hanya membuka file kelas dan memeriksa untuk menemukan nama kelas di bagian konstan kelas menghindari membiarkan pemindai refleksi membaca lebih banyak informasi tentang kelas yang bahkan tidak mengandung referensi yang diperlukan untuk kelas super Anda (langsung) Secara tidak langsung Anda perlu memindai lebih lanjut. Jadi itu yang terbaik yang ada saat ini.
Martin Kersten
Jawaban oleh fforw bekerja untuk saya dan harus ditandai sebagai jawaban yang benar. Jelas ini dimungkinkan dengan pemindaian classpath.
Farrukh Najmi
Anda salah, lihat respons di bawah tentang perpustakaan Burningwave
127

Memindai kelas tidak mudah dengan Java murni.

Kerangka kerja pegas menawarkan kelas yang disebut ClassPathScanningCandidateComponentProvider yang dapat melakukan apa yang Anda butuhkan. Contoh berikut akan menemukan semua subclass dari MyClass dalam paket org.example.package

ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false);
provider.addIncludeFilter(new AssignableTypeFilter(MyClass.class));

// scan in org.example.package
Set<BeanDefinition> components = provider.findCandidateComponents("org/example/package");
for (BeanDefinition component : components)
{
    Class cls = Class.forName(component.getBeanClassName());
    // use class cls found
}

Metode ini memiliki keuntungan tambahan dari menggunakan analisa bytecode untuk menemukan kandidat yang berarti akan tidak memuat semua kelas scan.

fforw
sumber
23
Salah harus diteruskan sebagai parameter saat membuat ClassPathScanningCandidateComponentProvider untuk menonaktifkan filter default. Filter default akan cocok dengan jenis kelas lain, misalnya apa pun yang dijelaskan dengan @Component. Kami hanya ingin AssignableTypeFilter aktif di sini.
MCDS
Anda bilang itu tidak mudah, tetapi bagaimana kita melakukannya jika kita ingin melakukannya dengan java murni?
Aequitas
49

Ini tidak mungkin dilakukan hanya dengan menggunakan Java Reflections API bawaan.

Ada sebuah proyek yang melakukan pemindaian dan pengindeksan classpath Anda yang diperlukan sehingga Anda dapat mengakses informasi ini ...

Refleksi

Analisis metadata runtime Java, dalam semangat Scannotations

Refleksi memindai classpath Anda, mengindeks metadata, memungkinkan Anda untuk menanyakannya saat runtime dan dapat menyimpan dan mengumpulkan informasi tersebut untuk banyak modul dalam proyek Anda.

Menggunakan Refleksi, Anda dapat meminta metadata Anda untuk:

  • dapatkan semua subtipe dari beberapa tipe
  • dapatkan semua jenis yang dianotasi dengan anotasi
  • dapatkan semua jenis yang dianotasi dengan beberapa anotasi, termasuk pencocokan parameter anotasi
  • dapatkan semua metode yang dijelaskan dengan beberapa

(Penafian: Saya belum menggunakannya, tetapi deskripsi proyek tampaknya cocok untuk kebutuhan Anda.)

Mark Renouf
sumber
1
Menarik. Proyek ini tampaknya memiliki beberapa ketergantungan yang sepertinya tidak disebutkan oleh dokumentasi mereka. Yaitu (yang saya temukan sejauh ini): javaassist, log4J, XStream
Avrom
3
Saya memasukkan projekt ini dengan maven dan bekerja dengan baik. Mendapatkan subclass sebenarnya adalah contoh kode sumber pertama dan panjangnya dua baris :-)
KarlsFriend
Apakah tidak mungkin hanya menggunakan Java Reflections API bawaan atau hanya sangat merepotkan untuk melakukannya?
Aliran
1
Harap berhati-hati ketika Anda akan menggunakan Refleksi dan kemudian gunakan PERANG aplikasi Anda ke GlassFish! Ada konflik di perpustakaan Guava dan penyebaran akan gagal dengan kegagalan penyebaran CDI: WELD-001408 - silakan lihat GLASSFISH-20579 untuk rincian lebih lanjut. FastClasspathScanner adalah solusi dalam hal ini.
lu_ko
Saya hanya mencoba proyek ini dan berhasil. Saya hanya menggunakannya untuk meningkatkan pola desain strategi dan mendapatkan semua strategi kelas konkret (subclass) Saya akan membagikan demo yang terakhir.
Xin Meng
10

Jangan lupa bahwa Javadoc yang dihasilkan untuk suatu kelas akan mencakup daftar subkelas yang dikenal (dan untuk antarmuka, kelas implementasi yang dikenal).

rampok
sumber
3
ini benar-benar salah, kelas super tidak boleh bergantung pada sub kelas mereka, bahkan di javadoc atau komentar
pemburu
@ pemburu saya tidak setuju. Benar-benar benar bahwa JavaDoc menyertakan daftar subclass yang dikenal . Tentu saja, "diketahui" mungkin tidak termasuk kelas yang Anda cari, tetapi untuk beberapa kasus penggunaan akan cukup.
Qw3ry
Dan Anda mungkin kehilangan beberapa kelas dalam hal apa pun: Saya dapat memuat tabung baru ke classpath (selama runtime) dan setiap deteksi yang terjadi sebelumnya akan gagal.
Qw3ry
10

Coba ClassGraph . (Penafian, saya penulisnya). ClassGraph mendukung pemindaian untuk subclass dari kelas yang diberikan, baik pada saat runtime atau saat membangun, tetapi juga banyak lagi. ClassGraph dapat membangun representasi abstrak dari seluruh grafik kelas (semua kelas, anotasi, metode, parameter metode, dan bidang) dalam memori, untuk semua kelas di classpath, atau untuk kelas dalam paket yang dimasukkan dalam daftar putih, dan Anda dapat meminta grafik kelas itu namun kamu ingin. ClassGraph mendukung lebih banyak mekanisme spesifikasi classpath dan classloader daripada pemindai lain, dan juga bekerja dengan mulus dengan sistem modul JPMS baru, jadi jika Anda mendasarkan kode Anda pada ClassGraph, kode Anda akan portabel secara maksimal. Lihat API di sini.

Luke Hutchison
sumber
9

Saya melakukan ini beberapa tahun yang lalu. Cara yang paling dapat diandalkan untuk melakukan ini (yaitu dengan Java API resmi dan tidak ada dependensi eksternal) adalah dengan menulis dokumen khusus untuk menghasilkan daftar yang dapat dibaca saat runtime.

Anda dapat menjalankannya dari baris perintah seperti ini:

javadoc -d build -doclet com.example.ObjectListDoclet -sourcepath java/src -subpackages com.example

atau jalankan dari semut seperti ini:

<javadoc sourcepath="${src}" packagenames="*" >
  <doclet name="com.example.ObjectListDoclet" path="${build}"/>
</javadoc>

Berikut kode dasarnya:

public final class ObjectListDoclet {
    public static final String TOP_CLASS_NAME =  "com.example.MyClass";        

    /** Doclet entry point. */
    public static boolean start(RootDoc root) throws Exception {
        try {
            ClassDoc topClassDoc = root.classNamed(TOP_CLASS_NAME);
            for (ClassDoc classDoc : root.classes()) {
                if (classDoc.subclassOf(topClassDoc)) {
                    System.out.println(classDoc);
                }
            }
            return true;
        }
        catch (Exception ex) {
            ex.printStackTrace();
            return false;
        }
    }
}

Untuk mempermudah, saya telah menghapus parsing argumen baris perintah dan saya menulis ke System.out daripada file.

David Leppik
sumber
Meskipun menggunakan ini secara pemrograman mungkin rumit, saya harus mengatakan - pendekatan yang cerdas !
Janaka Bandara
8

Saya tahu saya terlambat beberapa tahun ke pesta ini, tetapi saya menemukan pertanyaan ini mencoba menyelesaikan masalah yang sama. Anda dapat menggunakan pencarian internal Eclipse secara terprogram, jika Anda menulis Plugin Eclipse (dan dengan demikian mengambil keuntungan dari caching mereka, dll), untuk menemukan kelas yang mengimplementasikan antarmuka. Inilah potongan pertama saya (sangat kasar):

  protected void listImplementingClasses( String iface ) throws CoreException
  {
    final IJavaProject project = <get your project here>;
    try
    {
      final IType ifaceType = project.findType( iface );
      final SearchPattern ifacePattern = SearchPattern.createPattern( ifaceType, IJavaSearchConstants.IMPLEMENTORS );
      final IJavaSearchScope scope = SearchEngine.createWorkspaceScope();
      final SearchEngine searchEngine = new SearchEngine();
      final LinkedList<SearchMatch> results = new LinkedList<SearchMatch>();
      searchEngine.search( ifacePattern, 
      new SearchParticipant[]{ SearchEngine.getDefaultSearchParticipant() }, scope, new SearchRequestor() {

        @Override
        public void acceptSearchMatch( SearchMatch match ) throws CoreException
        {
          results.add( match );
        }

      }, new IProgressMonitor() {

        @Override
        public void beginTask( String name, int totalWork )
        {
        }

        @Override
        public void done()
        {
          System.out.println( results );
        }

        @Override
        public void internalWorked( double work )
        {
        }

        @Override
        public boolean isCanceled()
        {
          return false;
        }

        @Override
        public void setCanceled( boolean value )
        {
        }

        @Override
        public void setTaskName( String name )
        {
        }

        @Override
        public void subTask( String name )
        {
        }

        @Override
        public void worked( int work )
        {
        }

      });

    } catch( JavaModelException e )
    {
      e.printStackTrace();
    }
  }

Masalah pertama yang saya lihat sejauh ini adalah bahwa saya hanya menangkap kelas yang secara langsung mengimplementasikan antarmuka, tidak semua subclass mereka - tetapi sedikit rekursi tidak pernah menyakiti siapa pun.

Curtis
sumber
3
ATAU, ternyata Anda tidak perlu melakukan pencarian sendiri. Anda bisa mendapatkan ITypeHierarchy langsung dari IType Anda dengan memanggil .newTypeHierarchy () di atasnya: dev.eclipse.org/newslists/news.eclipse.tools.jdt/msg05036.html
Curtis
7

Dengan mengingat keterbatasan yang disebutkan dalam jawaban lain, Anda juga dapat menggunakan openpojoPojoClassFactory ( tersedia di Maven ) dengan cara berikut:

for(PojoClass pojoClass : PojoClassFactory.enumerateClassesByExtendingType(packageRoot, Superclass.class, null)) {
    System.out.println(pojoClass.getClazz());
}

Di mana packageRootString akar dari paket yang ingin Anda cari (misalnya "com.mycompany"atau bahkan hanya "com"), dan Superclassmerupakan supertype Anda (ini juga berfungsi pada antarmuka).

mikołak
sumber
Sejauh ini solusi tercepat dan paling elegan dari yang disarankan.
KidCrippler
4

Saya hanya menulis demo sederhana untuk menggunakan org.reflections.Reflectionsuntuk mendapatkan subclass dari kelas abstrak:

https://github.com/xmeng1/ReflectionsDemo

Xin Meng
sumber
akan lebih produktif jika Anda memposting sampel kode di sini daripada tautan dengan banyak kelas untuk digali.
JesseBoyd
4

Tergantung pada persyaratan khusus Anda, dalam beberapa kasus mekanisme loader layanan Java mungkin mencapai apa yang Anda cari.

Singkatnya, ini memungkinkan pengembang untuk secara eksplisit menyatakan bahwa suatu kelas mensubklasifikasikan beberapa kelas lain (atau mengimplementasikan beberapa antarmuka) dengan mendaftarkannya dalam file di direktori file JAR / WAR META-INF/services. Kemudian dapat ditemukan menggunakan java.util.ServiceLoaderkelas yang, ketika diberi Classobjek, akan menghasilkan instance dari semua subclass yang dideklarasikan dari kelas itu (atau, jika Classmewakili antarmuka, semua kelas yang mengimplementasikan antarmuka itu).

Keuntungan utama dari pendekatan ini adalah bahwa tidak perlu secara manual memindai seluruh classpath untuk subclass - semua logika penemuan terkandung dalam ServiceLoaderkelas, dan itu hanya memuat kelas yang secara eksplisit dinyatakan dalam META-INF/servicesdirektori (tidak setiap kelas di classpath) .

Namun, ada beberapa kelemahan:

  • Itu tidak akan menemukan semua subclass, hanya yang subclass dinyatakan secara eksplisit. Dengan demikian, jika Anda harus benar-benar menemukan semua subclass, pendekatan ini mungkin tidak cukup.
  • Ini mengharuskan pengembang untuk secara eksplisit mendeklarasikan kelas di bawah META-INF/servicesdirektori. Ini merupakan beban tambahan bagi pengembang, dan bisa rawan kesalahan.
  • The ServiceLoader.iterator()menghasilkan contoh subclass, bukan mereka Classobjek. Ini menyebabkan dua masalah:
    • Anda tidak bisa mengatakan bagaimana subkelas dikonstruksi - konstruktor no-arg digunakan untuk membuat instance.
    • Dengan demikian, subclass harus memiliki konstruktor default, atau harus eksplisit menyatakan konstruktor no-arg.

Rupanya Java 9 akan mengatasi beberapa kekurangan ini (khususnya, yang berkaitan dengan instantiasi subkelas).

Sebuah contoh

Misalkan Anda tertarik menemukan kelas yang mengimplementasikan antarmuka com.example.Example:

package com.example;

public interface Example {
    public String getStr();
}

Kelas com.example.ExampleImplmengimplementasikan antarmuka itu:

package com.example;

public class ExampleImpl implements Example {
    public String getStr() {
        return "ExampleImpl's string.";
    }
}

Anda akan mendeklarasikan kelas ExampleImpladalah implementasi Exampledengan membuat file yang META-INF/services/com.example.Exampleberisi teks com.example.ExampleImpl.

Kemudian, Anda dapat memperoleh instance dari setiap implementasi Example(termasuk instance dari ExampleImpl) sebagai berikut:

ServiceLoader<Example> loader = ServiceLoader.load(Example.class)
for (Example example : loader) {
    System.out.println(example.getStr());
}

// Prints "ExampleImpl's string.", plus whatever is returned
// by other declared implementations of com.example.Example.
Mac
sumber
3

Perlu dicatat juga bahwa ini tentu saja hanya akan menemukan semua subclass yang ada di classpath Anda saat ini. Agaknya ini tidak apa-apa untuk apa yang Anda lihat saat ini, dan kemungkinan Anda memang mempertimbangkan hal ini, tetapi jika Anda pernah melepaskan non- finalkelas ke alam bebas (untuk berbagai tingkat "liar") maka sepenuhnya layak bahwa orang lain telah menulis subkelas mereka sendiri yang tidak akan Anda ketahui.

Jadi jika Anda ingin melihat semua subclass karena Anda ingin membuat perubahan dan akan melihat bagaimana hal itu mempengaruhi perilaku subclass - maka ingatlah subclass yang tidak bisa Anda lihat. Idealnya semua metode non-pribadi Anda, dan kelas itu sendiri harus didokumentasikan dengan baik; buat perubahan sesuai dengan dokumentasi ini tanpa mengubah semantik metode / bidang non-pribadi dan perubahan Anda harus kompatibel-mundur, untuk setiap subkelas yang setidaknya mengikuti definisi Anda tentang superclass.

Andrzej Doyle
sumber
3

Alasan Anda melihat perbedaan antara implementasi Anda dan Eclipse adalah karena Anda memindai setiap kali, sementara Eclipse (dan alat lainnya) memindai hanya sekali (selama beban proyek sebagian besar waktu) dan membuat indeks. Lain kali Anda meminta data itu tidak memindai lagi, tetapi lihat indeks.

OscarRyz
sumber
3

Saya menggunakan lib refleksi, yang memindai classpath Anda untuk semua subclass: https://github.com/ronmamo/reflections

Beginilah caranya:

Reflections reflections = new Reflections("my.project");
Set<Class<? extends SomeType>> subTypes = reflections.getSubTypesOf(SomeType.class);
futcha
sumber
2

Tambahkan mereka ke peta statis di dalam (this.getClass (). GetName ()) konstruktor kelas induk (atau buat yang default) tetapi ini akan diperbarui dalam runtime. Jika inisialisasi malas adalah suatu pilihan, Anda dapat mencoba pendekatan ini.

Ravindranath Akila
sumber
0

Saya perlu melakukan ini sebagai test case, untuk melihat apakah kelas baru telah ditambahkan ke kode. Inilah yang saya lakukan

final static File rootFolder = new File(SuperClass.class.getProtectionDomain().getCodeSource().getLocation().getPath());
private static ArrayList<String> files = new ArrayList<String>();
listFilesForFolder(rootFolder); 

@Test(timeout = 1000)
public void testNumberOfSubclasses(){
    ArrayList<String> listSubclasses = new ArrayList<>(files);
    listSubclasses.removeIf(s -> !s.contains("Superclass.class"));
    for(String subclass : listSubclasses){
        System.out.println(subclass);
    }
    assertTrue("You did not create a new subclass!", listSubclasses.size() >1);     
}

public static void listFilesForFolder(final File folder) {
    for (final File fileEntry : folder.listFiles()) {
        if (fileEntry.isDirectory()) {
            listFilesForFolder(fileEntry);
        } else {
            files.add(fileEntry.getName().toString());
        }
    }
}
Cutetare
sumber