Przyjrzyj się dokładniej interfejsowi API Java Reflection

W zeszłomiesięcznym „Java In-Depth” mówiłem o introspekcji i sposobach, w jakie klasa Java z dostępem do surowych danych klasy mogłaby zajrzeć „do wnętrza” klasy i dowiedzieć się, jak została skonstruowana. Ponadto pokazałem, że po dodaniu programu ładującego klasy te klasy mogą być ładowane do działającego środowiska i wykonywane. Ten przykład jest formą statycznej introspekcji. W tym miesiącu przyjrzę się Java Reflection API, które daje klasom Java możliwość przeprowadzania dynamicznej introspekcji: możliwość zajrzenia do już załadowanych klas.

Użyteczność introspekcji

Jedną z mocnych stron Javy jest to, że została zaprojektowana przy założeniu, że środowisko, w którym działa, będzie się dynamicznie zmieniać. Klasy są ładowane dynamicznie, wiązanie jest wykonywane dynamicznie, a instancje obiektów są tworzone dynamicznie w locie, gdy są potrzebne. To, co historycznie nie było zbyt dynamiczne, to możliwość manipulowania „anonimowymi” klasami. W tym kontekście klasa anonimowa to taka, która jest ładowana lub prezentowana klasie Java w czasie wykonywania i której typ był wcześniej nieznany programowi Java.

Zajęcia anonimowe

Obsługa anonimowych klas jest trudna do wyjaśnienia, a jeszcze trudniejsza do zaprojektowania w programie. Wyzwanie związane z obsługą klasy anonimowej można określić w następujący sposób: „Napisz program, który po otrzymaniu obiektu Java może włączyć ten obiekt do swojej ciągłej operacji”. Ogólne rozwiązanie jest raczej trudne, ale ograniczając problem, można stworzyć pewne specjalistyczne rozwiązania. Istnieją dwa przykłady wyspecjalizowanych rozwiązań tej klasy problemów w wersji 1.0 języka Java: aplety Java i wersja interpretera języka Java z wiersza poleceń.

Aplety Java to klasy języka Java, które są ładowane przez uruchomioną maszynę wirtualną Java w kontekście przeglądarki internetowej i wywoływane. Te klasy Java są anonimowe, ponieważ środowisko wykonawcze nie zna z wyprzedzeniem informacji niezbędnych do wywołania poszczególnych klas. Jednak problem wywoływania określonej klasy jest rozwiązywany za pomocą klasy Java java.applet.Applet.

Typowe nadklasy, takie jak Appleti interfejsy Java AppletContext, rozwiązują problem klas anonimowych, tworząc wcześniej uzgodniony kontrakt. W szczególności dostawca środowiska wykonawczego ogłasza, że ​​może używać dowolnego obiektu zgodnego z określonym interfejsem, a konsument środowiska wykonawczego używa tego określonego interfejsu w dowolnym obiekcie, który zamierza dostarczyć do środowiska wykonawczego. W przypadku apletów istnieje dobrze określony interfejs w postaci wspólnej nadklasy.

Wadą typowego rozwiązania dla nadklasy, zwłaszcza przy braku dziedziczenia wielokrotnego, jest to, że obiekty zbudowane do działania w środowisku nie mogą być również używane w jakimś innym systemie, chyba że ten system implementuje całą umowę. W przypadku Appletinterfejsów musi zaimplementować środowisko hostingowe AppletContext. W przypadku rozwiązania w postaci apletów oznacza to, że rozwiązanie działa tylko podczas ładowania apletów. Jeśli umieścisz instancję Hashtableobiektu na swojej stronie internetowej i skierujesz do niej przeglądarkę, załadowanie się nie powiedzie, ponieważ system apletów nie może działać poza swoim ograniczonym zakresem.

Oprócz przykładu apletu, introspekcja pomaga rozwiązać problem, o którym wspomniałem w zeszłym miesiącu: zastanawianie się, jak rozpocząć wykonywanie w klasie, którą właśnie załadowała wersja maszyny wirtualnej Javy z wiersza poleceń. W tym przykładzie maszyna wirtualna musi wywołać pewną statyczną metodę w załadowanej klasie. Zgodnie z konwencją ta metoda ma nazwę maini przyjmuje pojedynczy argument - tablicę Stringobiektów.

Motywacja do bardziej dynamicznego rozwiązania

Wyzwanie z istniejącą architekturą Java 1.0 polega na tym, że istnieją problemy, które można rozwiązać za pomocą bardziej dynamicznego środowiska introspekcji - takich jak ładowalne komponenty interfejsu użytkownika, ładowalne sterowniki urządzeń w systemie operacyjnym opartym na Javie oraz dynamicznie konfigurowalne środowiska edycji. „Zabójcza aplikacja”, czyli problem, który spowodował utworzenie interfejsu API Java Reflection, polegał na opracowaniu modelu komponentów obiektowych dla języka Java. Ten model jest teraz znany jako JavaBeans.

Komponenty interfejsu użytkownika są idealnym punktem projektowym dla systemu introspekcji, ponieważ mają dwóch bardzo różnych konsumentów. Z jednej strony obiekty składowe są ze sobą połączone, tworząc interfejs użytkownika jako część aplikacji. Alternatywnie, musi istnieć interfejs dla narzędzi, które manipulują komponentami użytkownika bez konieczności znajomości tych komponentów lub, co ważniejsze, bez dostępu do kodu źródłowego komponentów.

Java Reflection API wyrosła z potrzeb API komponentu interfejsu użytkownika JavaBeans.

Co to jest odbicie?

Zasadniczo interfejs API Reflection składa się z dwóch komponentów: obiektów, które reprezentują różne części pliku klasy, oraz środków do wyodrębniania tych obiektów w bezpieczny sposób. To ostatnie jest bardzo ważne, ponieważ Java zapewnia wiele zabezpieczeń i nie miałoby sensu dostarczanie zestawu klas, które unieważniają te zabezpieczenia.

Pierwszym składnikiem API Reflection jest mechanizm służący do pobierania informacji o klasie. Ten mechanizm jest wbudowany w klasę o nazwie Class. Specjalna klasa Classto uniwersalny typ metainformacji opisujących obiekty w systemie Java. Programy ładujące klasy w systemie Java zwracają obiekty typu Class. Do tej pory trzy najciekawsze metody w tej klasie to:

  • forName, który załadowałby klasę o danej nazwie przy użyciu bieżącego programu ładującego klasy

  • getName, który zwróciłby nazwę klasy jako Stringobiekt, co było przydatne do identyfikowania odniesień do obiektów na podstawie ich nazwy klasy

  • newInstance, który wywołałby konstruktor o wartości null w klasie (jeśli istnieje) i zwróciłby instancję obiektu tej klasy obiektu

Do tych trzech użytecznych metod API Reflection dodaje kilka dodatkowych metod do klasy Class. Są to następujące:

  • getConstructor, getConstructors,getDeclaredConstructor
  • getMethod, getMethods,getDeclaredMethods
  • getField, getFields,getDeclaredFields
  • getSuperclass
  • getInterfaces
  • getDeclaredClasses

Oprócz tych metod dodano wiele nowych klas reprezentujących obiekty zwracane przez te metody. Nowe klasy są przeważnie częścią java.lang.reflectpakietu, ale niektóre z nowych klas typu podstawowego ( Void, Byteitd.) Znajdują się w java.langpakiecie. Podjęto decyzję o umieszczeniu nowych klas tam, gdzie są, umieszczając klasy reprezentujące metadane w pakiecie refleksji oraz klasy reprezentujące typy w pakiecie językowym.

W związku z tym interfejs API Reflection reprezentuje szereg zmian w klasie, Classktóre pozwalają zadawać pytania dotyczące wewnętrznych elementów klasy, a także kilka klas, które reprezentują odpowiedzi, które te nowe metody dają.

Jak używać interfejsu API Reflection?

Pytanie „Jak używać interfejsu API?” jest chyba bardziej interesującym pytaniem niż „Co to jest odbicie?”

Interfejs API Reflection jest symetryczny , co oznacza, że ​​jeśli trzymasz Classobiekt, możesz zapytać o jego elementy wewnętrzne, a jeśli masz jeden z elementów wewnętrznych, możesz zapytać, która klasa go zadeklarowała. W ten sposób możesz przechodzić od klasy do metody, parametru do klasy do metody i tak dalej. Ciekawym zastosowaniem tej technologii jest poznanie większości współzależności między daną klasą a resztą systemu.

Działający przykład

Jednak na bardziej praktycznym poziomie możesz użyć interfejsu API Reflection do zrzucenia klasy, podobnie jak moja dumpclassklasa zrobiła to w kolumnie z zeszłego miesiąca.

To demonstrate the Reflection API, I wrote a class called ReflectClass that would take a class known to the Java run time (meaning it is in your class path somewhere) and, through the Reflection API, dump out its structure to the terminal window. To experiment with this class, you will need to have a 1.1 version of the JDK available.

Note: Do not try to use a 1.0 run time as it gets all confused, usually resulting in an incompatible class change exception.

The class ReflectClass begins as follows:

import java.lang.reflect.*; import java.util.*; public class ReflectClass { 

As you can see above, the first thing the code does is import the Reflection API classes. Next, it jumps right into the main method, which starts out as shown below.

 public static void main(String args[]) { Constructor cn[]; Class cc[]; Method mm[]; Field ff[]; Class c = null; Class supClass; String x, y, s1, s2, s3; Hashtable classRef = new Hashtable(); if (args.length == 0) { System.out.println("Please specify a class name on the command line."); System.exit(1); } try { c = Class.forName(args[0]); } catch (ClassNotFoundException ee) { System.out.println("Couldn't find class '"+args[0]+"'"); System.exit(1); } 

The method main declares arrays of constructors, fields, and methods. If you recall, these are three of the four fundamental parts of the class file. The fourth part is the attributes, which the Reflection API unfortunately does not give you access to. After the arrays, I've done some command-line processing. If the user has typed a class name, the code attempts to load it using the forName method of class Class. The forName method takes Java class names, not file names, so to look inside the java.math.BigInteger class, you simply type "java ReflectClass java.math.BigInteger," rather than point out where the class file actually is stored.

Identifying the class's package

Assuming the class file is found, the code proceeds into Step 0, which is shown below.

 /* * Step 0: If our name contains dots we're in a package so put * that out first. */ x = c.getName(); y = x.substring(0, x.lastIndexOf(".")); if (y.length() > 0) { System.out.println("package "+y+";\n\r"); } 

In this step, the name of the class is retrieved using the getName method in class Class. This method returns the fully qualified name, and if the name contains dots, we can presume that the class was defined as part of a package. So Step 0 is to separate the package name part from the class name part, and print out the package name part on a line that starts with "package...."

Collecting class references from declarations and parameters

With the package statement taken care of, we proceed to Step 1, which is to collect all of the other class names that are referenced by this class. This collection process is shown in the code below. Remember that the three most common places where class names are referenced are as types for fields (instance variables), return types for methods, and as the types of the parameters passed to methods and constructors.

 ff = c.getDeclaredFields(); for (int i = 0; i < ff.length; i++) { x = tName(ff[i].getType().getName(), classRef); } 

In the above code, the array ff is initialized to be an array of Field objects. The loop collects the type name from each field and process it through the tName method. The tName method is a simple helper that returns the shorthand name for a type. So java.lang.String becomes String. And it notes in a hashtable which objects have been seen. At this stage, the code is more interested in collecting class references than in printing.

The next source of class references are the parameters supplied to constructors. The next piece of code, shown below, processes each declared constructor and collects the references from the parameter lists.

 cn = c.getDeclaredConstructors(); for (int i = 0; i  0) { for (int j = 0; j < cx.length; j++) { x = tName(cx[j].getName(), classRef); } } } 

As you can see, I've used the getParameterTypes method in the Constructor class to feed me all of the parameters that a particular constructor takes. These are then processed through the tName method.

An interesting thing to note here is the difference between the method getDeclaredConstructors and the method getConstructors. Both methods return an array of constructors, but the getConstructors method only returns those constructors that are accessible to your class. This is useful if you want to know if you actually can invoke the constructor you've found, but it isn't useful for this application because I want to print out all of the constructors in the class, public or not. The field and method reflectors also have similar versions, one for all members and one only for public members.

Ostatnim krokiem, pokazanym poniżej, jest zebranie odniesień ze wszystkich metod. Ten kod musi pobierać odwołania zarówno z typu metody (podobnie jak w powyższych polach), jak iz parametrów (podobnie jak w konstruktorach powyżej).

mm = c.getDeclaredMethods (); for (int i = 0; i 0) {for (int j = 0; j <cx.length; j ++) {x = tName (cx [j] .getName (), classRef); }}}

W powyższym kodzie istnieją dwa wywołania tName- jedno do zbierania typu zwracanego i jedno do zbierania typu każdego parametru.