Погледнете вътре в Java класовете

Добре дошли в този месец на „Java In Depth“. Едно от най-ранните предизвикателства пред Java беше дали тя може да се представи като способен „системен“ език. Коренът на въпроса включваше функциите за безопасност на Java, които пречат на клас Java да познава други класове, които работят заедно с него във виртуалната машина. Тази способност за „вглеждане“ в класовете се нарича самоанализ . В първата публична версия на Java, известна като Alpha3, стриктните езикови правила относно видимостта на вътрешните компоненти на клас могат да бъдат заобиколени чрез използването на ObjectScopeкласа. След това, по време на бета версия, когато ObjectScopeбеше отстранена от времето за изпълнение поради опасения за сигурността, много хора обявиха Java за негодна за „сериозно“ развитие.

Защо е необходима самоанализ, за ​​да може даден език да се счита за "системен" език? Една част от отговорите са доста светски: Преминаването от „нищо“ (т.е. неинициализирана виртуална машина) до „нещо“ (т.е. работещ клас Java) изисква част от системата да може да проверява класовете, които трябва да бъдат бягайте, за да разберете какво да правите с тях. Каноничният пример за този проблем е просто следният: „Как една програма, написана на език, който не може да погледне„ вътре “на друг езиков компонент, започва да изпълнява първия езиков компонент, който е началната точка на изпълнение за всички останали компоненти? "

Има два начина да се справите с интроспекцията в Java: проверка на файл с клас и новия API за отражение, който е част от Java 1.1.x. Ще разгледам и двете техники, но в тази колона ще се съсредоточа върху първокласната проверка на файлове. В следваща колона ще разгледам как API за отражение решава този проблем. (Връзките към пълния изходен код за тази колона са налични в раздела Ресурси.)

Погледнете дълбоко в моите файлове ...

В версиите на 1.0.x на Java, една от най-големите брадавици по време на изпълнение на Java е начинът, по който изпълнимият файл на Java стартира програма. Какъв е проблемът? Изпълнението е преход от домейна на хост операционната система (Win 95, SunOS и т.н.) в домейна на Java виртуалната машина. Въвеждането на реда " java MyClass arg1 arg2" задейства поредица от събития, които са напълно кодирани от интерпретатора на Java.

Като първото събитие командната обвивка на операционната система зарежда интерпретатора на Java и му предава низа "MyClass arg1 arg2" като свой аргумент. Следващото събитие се случва, когато интерпретаторът на Java се опитва да намери клас, посочен MyClassв една от директориите, идентифицирани в пътя на класа. Ако класът бъде намерен, третото събитие е да се намери метод вътре в имената на класа main, чийто подпис има модификаторите „public“ и „static“ и който приема масив от Stringобекти като свой аргумент. Ако този метод бъде намерен, се конструира първична нишка и методът се извиква. След това интерпретаторът на Java преобразува "arg1 arg2" в масив от низове. След като този метод бъде извикан, всичко останало е чиста Java.

Всичко това е добре, с изключение на това, че mainметодът трябва да бъде статичен, тъй като времето за изпълнение не може да го извика с Java среда, която все още не съществува. Освен това, първият метод трябва да бъде именуван, mainтъй като няма начин да се каже на интерпретатора името на метода в командния ред. Дори ако сте казали на интерпретатора името на метода, няма някакъв общ начин, по който да разберете дали той е бил в класа, който първо сте посочили. И накрая, тъй като mainметодът е статичен, не можете да го декларирате в интерфейс и това означава, че не можете да посочите интерфейс като този:

публичен интерфейс Приложение {public void main (String args []); }

Ако горният интерфейс беше дефиниран и класовете го внедриха, тогава поне можете да използвате instanceofоператора в Java, за да определите дали имате приложение или не и по този начин да определите дали той е подходящ за извикване от командния ред. Изводът е, че не можете (да дефинирате интерфейса), той не е бил (вграден в интерпретатора на Java) и затова не можете (да определите дали файлът с клас е приложение лесно). И така, какво можете да направите?

Всъщност можете да направите доста, ако знаете какво да търсите и как да го използвате.

Декомпилиране на файлове от клас

Файлът на класа Java е архитектурно неутрален, което означава, че е същият набор от битове, независимо дали е зареден от машина с Windows 95 или машина Sun Solaris. Той е много добре документиран и в книгата „Спецификация на виртуалната машина Java“ от Линдхолм и Йелин. Файловата структура на класа е проектирана отчасти за лесно зареждане в адресното пространство на SPARC. По принцип файлът на класа може да бъде картографиран във виртуалното адресно пространство, след това относителните указатели вътре в класа са фиксирани и presto! Имахте незабавна структура на класа. Това беше по-малко полезно за машините на архитектурата на Intel, но наследството остави формата на файла на класа лесен за разбиране и дори по-лесен за разбиване.

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

Започнах с изграждането на клас Java, който може да декомпозира файл на клас Java, който му беше представен във входния поток. Дадох му по-малко от оригиналното име ClassFile. Началото на този клас е показано по-долу.

публичен клас ClassFile {int magic; къса главна версия; къс минор Версия; ConstantPoolInfo constantPool []; кратък достъпFlags; ConstantPoolInfo thisClass; ConstantPoolInfo superClass; Интерфейси на ConstantPoolInfo []; FieldInfo полета []; MethodInfo методи []; AttributeInfo атрибути []; булева isValidClass = false; публичен статичен финал int ACC_PUBLIC = 0x1; публичен статичен финал int ACC_PRIVATE = 0x2; публичен статичен финал int ACC_PROTECTED = 0x4; публичен статичен финал int ACC_STATIC = 0x8; публичен статичен финал int ACC_FINAL = 0x10; публичен статичен финал int ACC_SYNCHRONIZED = 0x20; публичен статичен финал int ACC_THREADSAFE = 0x40; публичен статичен финал int ACC_TRANSIENT = 0x80; публичен статичен финал int ACC_NATIVE = 0x100; публичен статичен финал int ACC_INTERFACE = 0x200; публичен статичен финал int ACC_ABSTRACT = 0x400;

Както можете да видите, променливите на инстанцията за клас ClassFileдефинират основните компоненти на файл с клас на Java. По-специално, централната структура на данни за файл с клас Java е известна като константния пул. Други интересни парчета от файла на класа получават собствени класове: MethodInfoза методи, FieldInfoза полета (които са декларациите на променливи в класа), за AttributeInfoда съдържат атрибутите на файла на класа и набор от константи, който е взет директно от спецификацията на файловете на класа към декодирайте различните модификатори, които се прилагат за декларации на поле, метод и клас.

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

1 публично булево четене (InputStream in) 2 хвърля IOException {3 DataInputStream di = new DataInputStream (in); 4 броя инт; 5 6 магия = di.readInt (); 7 if (magic! = (Int) 0xCAFEBABE) {8 return (false); 9} 10 11 majorVersion = di.readShort (); 12 minorVersion = di.readShort (); 13 броя = di.readShort (); 14 constantPool = нов ConstantPoolInfo [брой]; 15 if (отстраняване на грешки) 16 System.out.println ("read (): Read header ..."); 17 constantPool [0] = нов ConstantPoolInfo (); 18 за (int i = 1; i <constantPool.length; i ++) {19 constantPool [i] = new ConstantPoolInfo (); 20 if (! ConstantPool [i] .read (di)) {21 return (false); 22} 23 // Тези два типа заемат "две" места в таблицата 24 if ((constantPool [i] .type == ConstantPoolInfo.LONG) || 25 (constantPool [i] .type == ConstantPoolInfo.DOUBLE)) 26 i ++; 27}

Както можете да видите, горният код започва с първо увиване на DataInputStreamоколо входния поток, посочен от променливата в . Освен това в редове 6 до 12 присъства цялата информация, необходима, за да се определи дали кодът наистина разглежда валиден файл на класа. Тази информация се състои от вълшебната "бисквитка" 0xCAFEBABE и версиите с номера 45 и 3 съответно за главните и второстепенните стойности. На следващо място, в редове 13 до 27, константният пул се чете в масив от ConstantPoolInfoобекти. Изходният код до не ConstantPoolInfoе забележителен - той просто чете данни и ги идентифицира въз основа на техния тип. По-късно елементи от константния пул се използват за показване на информация за класа.

Следвайки горния код, readметодът повторно сканира константния пул и "поправя" препратки в постоянния пул, които се отнасят до други елементи в константния пул. Кодът за корекция е показан по-долу. Това коригиране е необходимо, тъй като референциите обикновено са индекси в константния пул и е полезно тези индекси да са вече разрешени. Това също така осигурява проверка на читателя да знае, че файлът на класа не е повреден на нивото на постоянен пул.

28 за (int i = 1; i 0) 32 constantPool [i] .arg1 = constantPool [constantPool [i] .index1]; 33 if (constantPool [i] .index2> 0) 34 constantPool [i] .arg2 = constantPool [constantPool [i] .index2]; 35} 36 37 if (dumpConstants) {38 for (int i = 1; i <constantPool.length; i ++) {39 System.out.println ("C" + i + "-" + constantPool [i]); 30} 31}

В горния код всеки запис на постоянен пул използва стойностите на индекса, за да разбере препратката към друг запис на постоянен пул. Когато се попълни в ред 36, целият пул по желание се изхвърля.

След като кодът е сканиран покрай постоянния пул, файлът на класа определя основната информация за класа: името на класа, името на суперкласа и интерфейсите за внедряване. Най- четените код сканира за тези стойности, както е показано по-долу.

32 accessFlags = di.readShort (); 33 34 thisClass = constantPool [di.readShort ()]; 35 superClass = constantPool [di.readShort ()]; 36 if (отстраняване на грешки) 37 System.out.println ("read (): Прочетете информация за класа ..."); 38 39 / * 30 * Идентифицирайте всички интерфейси, внедрени от този клас 31 * / 32 count = di.readShort (); 33 if (count! = 0) {34 if (debug) 35 System.out.println ("Class implements" + count + "interfaces."); 36 интерфейса = нов ConstantPoolInfo [брой]; 37 за (int i = 0; i <count; i ++) {38 int iindex = di.readShort (); 39 if ((iindex constantPool.length - 1)) 40 return (false); 41 интерфейса [i] = constantPool [iindex]; 42 if (отстраняване на грешки) 43 System.out.println ("I" + i + ":" + интерфейси [i]); 44} 45} 46 if (отстраняване на грешки) 47 System.out.println ("read (): Прочетете информация за интерфейса ...");

След като този код е завършен, readметодът е изградил доста добра представа за структурата на класа. Остава само да се съберат дефинициите на полета, дефинициите на метода и, може би най-важното, атрибутите на файла на класа.

Файловият формат на класа разделя всяка от тези три групи на секция, състояща се от число, последвано от този брой копия на нещото, което търсите. И така, за полетата файлът на класа има броя на дефинираните полета и след това толкова много дефиниции на полета. Кодът за сканиране в полетата е показан по-долу.

48 count = di.readShort(); 49 if (debug) 50 System.out.println("This class has "+count+" fields."); 51 if (count != 0) { 52 fields = new FieldInfo[count]; 53 for (int i = 0; i < count; i++) { 54 fields[i] = new FieldInfo(); 55 if (! fields[i].read(di, constantPool)) { 56 return (false); 57 } 58 if (debug) 59 System.out.println("F"+i+": "+ 60 fields[i].toString(constantPool)); 61 } 62 } 63 if (debug) 64 System.out.println("read(): Read field info..."); 

Горният код започва с четене на брой в ред # 48, след което, докато броят не е нула, той чете в нови полета, използвайки FieldInfoкласа. В FieldInfoкласа просто попълва данните, които определят полето на виртуалната машина на Java. Кодът за четене на методи и атрибути е един и същ, просто замествайки препратките към FieldInfoпрепратки към MethodInfoили AttributeInfoспоред случая. Този източник не е включен тук, но можете да го погледнете, като използвате връзките в раздела Ресурси по-долу.