Погледнете задълбочено API за отразяване на Java

В „Дълбочината на Java за миналия месец“ говорих за самоанализи и начини, по които Java клас с достъп до сурови данни на класа може да изглежда „вътре“ в клас и да разбера как е конструиран класът. Освен това показах, че с добавянето на зареждащ клас, тези класове могат да бъдат заредени в работещата среда и изпълнени. Този пример е форма на статично самоанализиране. Този месец ще разгледам Java Reflection API, който дава на Java класовете способността да извършват динамична интроспекция: способността да се търсят вътре вече заредени класове.

Полезността на самоанализа

Една от силните страни на Java е, че тя е проектирана с предположението, че средата, в която работи, ще се променя динамично. Класовете се зареждат динамично, свързването се извършва динамично и обектите се създават динамично в движение, когато са необходими. Това, което не е било много динамично в исторически план, е способността да се манипулират „анонимни“ класове. В този контекст анонимен клас е този, който се зарежда или представя на Java клас по време на изпълнение и чийто тип преди е бил неизвестен за програмата Java.

Анонимни класове

Подкрепата за анонимни класове е трудна за обяснение и още по-трудна за проектиране в програма. Предизвикателството за поддръжка на анонимен клас може да се заяви по следния начин: „Напишете програма, която, когато получи Java обект, може да включи този обект в продължаващата си работа“. Общото решение е доста трудно, но чрез ограничаване на проблема могат да бъдат създадени някои специализирани решения. Има два примера за специализирани решения на този клас проблеми в 1.0 версия на Java: Java аплети и версия на командния ред на интерпретатора Java.

Java аплетите са Java класове, които се зареждат от работеща Java виртуална машина в контекста на уеб браузър и се извикват. Тези Java класове са анонимни, тъй като времето за изпълнение не знае предварително информацията, необходима за извикване на всеки отделен клас. Проблемът с извикването на определен клас обаче се решава с помощта на Java класа java.applet.Applet.

Общите суперкласове, като Applet, и Java интерфейси, като AppletContext, адресират проблема с анонимните класове чрез създаване на предварително договорен договор. По-конкретно, доставчик на среда за изпълнение рекламира, че може да използва всеки обект, който съответства на определен интерфейс, а потребителят на средата на изпълнение използва този интерфейс във всеки обект, който възнамерява да предостави на времето за изпълнение. В случай на аплети съществува точно определен интерфейс под формата на общ суперклас.

Недостатъкът на общо решение за суперклас, особено при липса на множествено наследяване, е, че обектите, изградени за изпълнение в околната среда, не могат да се използват и в друга система, освен ако тази система не изпълни целия договор. В случай на Appletинтерфейси, хостинг средата трябва да се внедри AppletContext. Това, което означава за решението на аплета, е, че решението работи само когато зареждате аплети. Ако поставите екземпляр на Hashtableобект на вашата уеб страница и насочите браузъра си към него, той няма да успее да се зареди, защото аплетната система не може да работи извън ограничения си обхват.

В допълнение към примера с аплет, самоанализът помага да се реши проблем, който споменах миналия месец: измисляне как да започне изпълнението в клас, който току-що е заредила версията на командния ред на виртуалната машина Java. В този пример виртуалната машина трябва да извика някакъв статичен метод в заредения клас. По споразумение този метод се именува mainи приема един аргумент - масив от Stringобекти.

Мотивацията за по-динамично решение

Предизвикателството със съществуващата архитектура на Java 1.0 е, че има проблеми, които могат да бъдат решени чрез по-динамична среда за самоанализ - като зареждащи се потребителски интерфейси, зареждащи се драйвери на устройства в базирана на Java операционна система и динамично конфигурируеми среди за редактиране. „Приложението убиец“ или проблемът, който е причинил създаването на API за отразяване на Java, е разработването на модел на обектни компоненти за Java. Този модел сега е известен като JavaBeans.

Компонентите на потребителския интерфейс са идеална точка за проектиране на система за самоанализ, тъй като имат два много различни потребители. От една страна, обектните компоненти са свързани заедно, за да образуват потребителски интерфейс като част от някакво приложение. Като алтернатива трябва да има интерфейс за инструменти, които манипулират потребителските компоненти, без да се налага да се знае кои са компонентите или, което е по-важно, без достъп до изходния код на компонентите.

API за отразяване на Java израства от нуждите на API на компонента на потребителския интерфейс JavaBeans.

Какво е отражение?

По същество API за отразяване се състои от два компонента: обекти, които представляват различните части на файл с клас, и средство за извличане на тези обекти по безопасен и сигурен начин. Последното е много важно, тъй като Java предоставя много защитни мерки и няма смисъл да се предлага набор от класове, които обезсилват тези предпазни мерки.

Първият компонент на API за отразяване е механизмът, използван за извличане на информация за клас. Този механизъм е вграден в клас с име Class. Специалният клас Classе универсалният тип за метаинформацията, която описва обекти в системата Java. Зареждащите клас в системата Java връщат обекти от тип Class. Досега трите най-интересни метода в този клас бяха:

  • forName, който би заредил клас с дадено име, използвайки текущия зареждащ клас

  • getName, което би върнало името на класа като Stringобект, което беше полезно за идентифициране на препратки към обекти по името на класа им

  • newInstance, който би извикал нулевия конструктор на класа (ако съществува) и ще ви върне екземпляр на обект на този клас обект

Към тези три полезни метода API за отразяване добавя някои допълнителни методи към класа Class. Те са както следва:

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

В допълнение към тези методи бяха добавени много нови класове, които представляват обектите, които тези методи ще върнат. Новите класове най-вече са част от java.lang.reflectпакета, но някои от новите основни класове тип ( Void, Byteи т.н.) са в java.langпакета. Взе се решение новите класове да бъдат поставени там, където са, като се поставят класове, които представляват метаданни в пакета за отражение и класове, които представляват типове в езиковия пакет.

По този начин API за отразяване представлява редица промени в класа, Classкоито ви позволяват да задавате въпроси относно вътрешните елементи на класа и куп класове, които представляват отговорите, които тези нови методи ви дават.

Как да използвам API за отразяване?

Въпросът "Как да използвам API?" е може би по-интересният въпрос от "Какво е размисъл?"

API за отразяване е симетричен , което означава, че ако държите Classобект, можете да попитате за вътрешните му елементи, а ако имате един от вътрешните, можете да го попитате кой клас го е декларирал. По този начин можете да се движите напред-назад от клас към метод към параметър към клас към метод и т.н. Едно интересно използване на тази технология е да се открият повечето взаимозависимости между даден клас и останалата част от системата.

Работен пример

На по-практично ниво обаче можете да използвате API за отразяване, за да изхвърлите клас, подобно на моя dumpclassклас в колоната от миналия месец.

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.

Последната стъпка, показана по-долу, е да се съберат референциите от всички методи. Този код трябва да получава препратки както от типа на метода (подобно на полетата по-горе), така и от параметрите (подобно на конструкторите по-горе).

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

В горния код има две извиквания tName- едно за събиране на типа връщане и едно за събиране на типа на всеки параметър.