Προγραμματισμός

Ρίξτε μια ματιά στα μαθήματα Java

Καλώς ήλθατε στη δόση αυτού του μήνα "Java In Depth". Μία από τις πρώτες προκλήσεις για την Java ήταν αν μπορούσε ή όχι να είναι μια ικανή γλώσσα «συστημάτων». Η ρίζα της ερώτησης αφορούσε τα χαρακτηριστικά ασφαλείας της Java που εμποδίζουν μια κλάση Java να γνωρίζει άλλες τάξεις που εκτελούνται παράλληλα με αυτήν στην εικονική μηχανή. Αυτή η ικανότητα "ματιά μέσα" στα μαθήματα ονομάζεται ενδοσκόπηση. Στην πρώτη δημόσια έκδοση Java, γνωστή ως Alpha3, οι αυστηροί γλωσσικοί κανόνες σχετικά με την ορατότητα των εσωτερικών στοιχείων μιας τάξης θα μπορούσαν να παρακαμφθούν μέσω της ObjectScope τάξη. Στη συνέχεια, κατά τη διάρκεια της beta, όταν ObjectScope αφαιρέθηκε από τον χρόνο εκτέλεσης λόγω ανησυχιών για την ασφάλεια, πολλοί άνθρωποι δήλωσαν ότι η Java είναι ακατάλληλη για "σοβαρή" ανάπτυξη.

Γιατί είναι απαραίτητη η ενδοσκόπηση προκειμένου μια γλώσσα να θεωρηθεί γλώσσα «συστημάτων»; Ένα μέρος της απάντησης είναι αρκετά συνηθισμένο: Η μετάβαση από το "τίποτα" (δηλαδή ένα μη αρχικοποιημένο VM) στο "κάτι" (δηλαδή, μια τάξη 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 προσπαθεί να εντοπίσει μια κλάση με το όνομα Η τάξη μου σε έναν από τους καταλόγους που προσδιορίζονται στη διαδρομή τάξης. Εάν βρεθεί η τάξη, το τρίτο συμβάν είναι να εντοπίσετε μια μέθοδο μέσα στην κλάση που ονομάζεται κύριος, της οποίας η υπογραφή έχει τους τροποποιητές "δημόσιο" και "στατικό" και που παίρνει μια σειρά από Σειρά αντικείμενα ως επιχείρημά του. Εάν βρεθεί αυτή η μέθοδος, δημιουργείται ένα αρχέγονο νήμα και καλείται η μέθοδος. Στη συνέχεια, ο διερμηνέας Java μετατρέπει το "arg1 arg2" σε μια σειρά συμβολοσειρών. Μόλις χρησιμοποιηθεί αυτή η μέθοδος, όλα τα άλλα είναι καθαρή Java.

Όλα αυτά είναι καλά και καλά, εκτός από το ότι το κύριος Η μέθοδος πρέπει να είναι στατική επειδή ο χρόνος εκτέλεσης δεν μπορεί να τον καλέσει με περιβάλλον Java που δεν υπάρχει ακόμη. Επιπλέον, η πρώτη μέθοδος πρέπει να ονομάζεται κύριος επειδή δεν υπάρχει τρόπος να πείτε στον διερμηνέα το όνομα της μεθόδου στη γραμμή εντολών. Ακόμα κι αν είπατε στον διερμηνέα το όνομα της μεθόδου, δεν υπάρχει κανένας γενικός τρόπος για να μάθετε αν ήταν στην τάξη που είχατε ονομάσει αρχικά. Τέλος, επειδή το κύριος Η μέθοδος είναι στατική, δεν μπορείτε να την δηλώσετε σε μια διεπαφή και αυτό σημαίνει ότι δεν μπορείτε να καθορίσετε μια διεπαφή όπως αυτή:

δημόσια διεπαφή Εφαρμογή {public void main (String args []); } 

Εάν η παραπάνω διεπαφή ορίστηκε και οι κλάσεις την εφάρμοσαν, τότε τουλάχιστον θα μπορούσατε να χρησιμοποιήσετε το παράδειγμα τελεστής στην Java για να προσδιορίσει εάν είχατε μια εφαρμογή ή όχι και, επομένως, να προσδιορίσετε αν ήταν κατάλληλο για επίκληση από τη γραμμή εντολών Η κατώτατη γραμμή είναι ότι δεν μπορείτε (ορίστε τη διεπαφή), δεν ήταν (ενσωματωμένη στον διερμηνέα Java) και έτσι δεν μπορείτε (προσδιορίστε εάν ένα αρχείο κλάσης είναι μια εφαρμογή εύκολα). Τι μπορείτε λοιπόν να κάνετε;

Στην πραγματικότητα, μπορείτε να κάνετε αρκετά αν ξέρετε τι να ψάξετε και πώς να το χρησιμοποιήσετε.

Αποσύνθεση αρχείων τάξης

Το αρχείο κλάσης Java είναι αρχιτεκτονικής ουδέτερο, που σημαίνει ότι είναι το ίδιο σύνολο bit είτε φορτώνεται από υπολογιστή Windows 95 είτε από μηχανή Sun Solaris. Είναι επίσης πολύ καλά τεκμηριωμένο στο βιβλίο Η προδιαγραφή εικονικής μηχανής Java από τους Lindholm και Yellin. Η δομή του αρχείου κλάσης σχεδιάστηκε, εν μέρει, για εύκολη φόρτωση στο χώρο διευθύνσεων SPARC. Βασικά, το αρχείο τάξης θα μπορούσε να αντιστοιχιστεί στον εικονικό χώρο διευθύνσεων, στη συνέχεια, οι σχετικοί δείκτες μέσα στην τάξη σταθεροποιήθηκαν και το presto! Είχατε άμεση δομή τάξης. Αυτό ήταν λιγότερο χρήσιμο για τα μηχανήματα αρχιτεκτονικής Intel, αλλά η κληρονομιά άφησε τη μορφή αρχείου κατηγορίας εύκολη στην κατανόηση και ακόμη πιο εύκολη στην ανάλυση.

Το καλοκαίρι του 1994, δούλευα στην ομάδα Java και δημιούργησα αυτό που είναι γνωστό ως μοντέλο ασφαλείας "λιγότερο προνόμιο" για την Java. Μόλις τελείωσα να καταλάβω ότι αυτό που πραγματικά ήθελα να κάνω ήταν να κοιτάξω μέσα σε μια τάξη Java, να αποκοπή τα κομμάτια που δεν επιτρέπονται από το τρέχον επίπεδο προνομίων και, στη συνέχεια, να φορτώσω το αποτέλεσμα μέσω ενός προσαρμοσμένου φορτωτή κλάσης. Τότε ανακάλυψα ότι δεν υπήρχαν μαθήματα στην κύρια περίοδο εκτέλεσης που ήξεραν για την κατασκευή αρχείων τάξης. Υπήρχαν εκδόσεις στο δέντρο κλάσης μεταγλωττιστή (το οποίο έπρεπε να δημιουργήσει αρχεία κλάσης από τον μεταγλωττισμένο κώδικα), αλλά με ενδιέφερε περισσότερο να δημιουργήσω κάτι για το χειρισμό προϋπάρχοντων αρχείων τάξης.

Ξεκίνησα δημιουργώντας μια κλάση Java που θα μπορούσε να αποσυνθέσει ένα αρχείο κλάσης Java που του παρουσιάστηκε σε μια ροή εισόδου. Του έδωσα το λιγότερο από το αρχικό όνομα ClassFile. Η αρχή αυτής της τάξης φαίνεται παρακάτω.

δημόσια τάξη ClassFile {int magic; σύντομη majorVersion? σύντομη δευτερεύουσα έκδοση; ConstantPoolInfo stablePool []; σύντομη πρόσβασηFlags; ConstantPoolInfo thisClass; ConstantPoolInfo superClass; Διεπαφές ConstantPoolInfo []; Πεδία FieldInfo []; MethodInfo μέθοδοι []; AttributeInfo χαρακτηριστικά []; boolean 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; δημόσιο στατικό τελικό ACC_NATIVE = 0x100; δημόσιο στατικό τελικό int ACC_INTERFACE = 0x200; δημόσιο στατικό τελικό int ACC_ABSTRACT = 0x400; 

Όπως μπορείτε να δείτε, οι μεταβλητές παρουσίας για την τάξη ClassFile καθορίστε τα κύρια στοιχεία ενός αρχείου κλάσης Java. Συγκεκριμένα, η κεντρική δομή δεδομένων για ένα αρχείο κλάσης Java είναι γνωστή ως η σταθερή ομάδα. Άλλα ενδιαφέροντα κομμάτια του αρχείου τάξης έχουν δικά τους μαθήματα: Μέθοδος πληροφοριών για μεθόδους, Πληροφορίες πεδίου για πεδία (που είναι οι μεταβλητές δηλώσεις στην τάξη), Πληροφορίες για να διατηρήσετε χαρακτηριστικά αρχείου κλάσης και ένα σύνολο σταθερών που λήφθηκαν απευθείας από την προδιαγραφή σε αρχεία κλάσης για την αποκωδικοποίηση των διαφόρων τροποποιητών που ισχύουν για τις δηλώσεις πεδίου, μεθόδου και κλάσης.

Η κύρια μέθοδος αυτής της τάξης είναι ανάγνωση, το οποίο χρησιμοποιείται για την ανάγνωση ενός αρχείου τάξης από το δίσκο και τη δημιουργία ενός νέου ClassFile παρουσία από τα δεδομένα. Ο κωδικός για το ανάγνωση η μέθοδος φαίνεται παρακάτω. Έχω διαπεράσει την περιγραφή με τον κωδικό, καθώς η μέθοδος τείνει να είναι αρκετά μεγάλη.

1 δημόσια boolean read (InputStream in) 2 ρίχνει το IOException {3 DataInputStream di = new DataInputStream (σε); 4 int count; 5 6 magic = di.readInt (); 7 if (magic! = (Int) 0xCAFEBABE) {8 επιστροφή (false); 9} 10 11 majorVersion = di.readShort (); 12 minorVersion = di.readShort (); 13 count = di.readShort (); 14 stablePool = νέο ConstantPoolInfo [count]; 15 if (debug) 16 System.out.println ("read (): Read header ..."); 17 stablePool [0] = νέο ConstantPoolInfo (); 18 για (int i = 1; i <stablePool.length; i ++) {19 stablePool [i] = νέο ConstantPoolInfo (); 20 if (! StablePool [i] .read (di)) {21 return (false); 22} 23 // Αυτοί οι δύο τύποι καταλαμβάνουν "δύο" σημεία στον πίνακα 24 εάν ((stablePool [i] .type == ConstantPoolInfo.LONG) || 25 (stablePool [i] .type == ConstantPoolInfo.DOUBLE)) 26 i ++; 27} 

Όπως μπορείτε να δείτε, ο παραπάνω κώδικας ξεκινάει πρώτα τυλίγοντας ένα DataInputStream γύρω από τη ροή εισόδου που αναφέρεται από τη μεταβλητή σε. Περαιτέρω, στις γραμμές 6 έως 12, όλες οι απαραίτητες πληροφορίες για να προσδιοριστεί ότι ο κώδικας πράγματι εξετάζει ένα έγκυρο αρχείο κλάσης υπάρχει. Αυτές οι πληροφορίες αποτελούνται από το μαγικό "cookie" 0xCAFEBABE και τους αριθμούς έκδοσης 45 και 3 για τις κύριες και δευτερεύουσες τιμές αντίστοιχα. Στη συνέχεια, στις γραμμές 13 έως 27, η σταθερή δεξαμενή διαβάζεται σε μια σειρά από ConstantPoolInfo αντικείμενα. Ο πηγαίος κώδικας προς ConstantPoolInfo είναι αξιοσημείωτο - διαβάζει απλώς σε δεδομένα και τα αναγνωρίζει με βάση τον τύπο του. Αργότερα στοιχεία από τη σταθερή ομάδα χρησιμοποιούνται για την εμφάνιση πληροφοριών σχετικά με την τάξη.

Ακολουθώντας τον παραπάνω κωδικό, το ανάγνωση Η μέθοδος σαρώνει εκ νέου τη σταθερή ομάδα και "διορθώνει" τις αναφορές στη σταθερή ομάδα που αναφέρονται σε άλλα στοιχεία στη σταθερή ομάδα. Ο κωδικός επιδιόρθωσης εμφανίζεται παρακάτω. Αυτή η διόρθωση είναι απαραίτητη δεδομένου ότι οι αναφορές είναι συνήθως ευρετήρια στη σταθερή ομάδα και είναι χρήσιμο να έχουν ήδη επιλυθεί αυτά τα ευρετήρια. Αυτό παρέχει επίσης έλεγχο για τον αναγνώστη να γνωρίζει ότι το αρχείο τάξης δεν είναι κατεστραμμένο στο σταθερό επίπεδο συγκέντρωσης.

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

Στον παραπάνω κώδικα κάθε καταχώριση σταθερής συγκέντρωσης χρησιμοποιεί τις τιμές ευρετηρίου για να υπολογίσει την αναφορά σε μια άλλη καταχώριση σταθερής ομάδας. Όταν ολοκληρωθεί στη γραμμή 36, ολόκληρη η ομάδα απορρίπτεται προαιρετικά.

Μόλις ο κωδικός σαρωθεί πέρα ​​από τη σταθερή ομάδα, το αρχείο κλάσης ορίζει τις πληροφορίες της κύριας κλάσης: το όνομα της κλάσης, το όνομα του superclass και τις διεπαφές εφαρμογής. ο ανάγνωση ο κώδικας σαρώνει αυτές τις τιμές όπως φαίνεται παρακάτω.

32 accessFlags = di.readShort (); 33 34 thisClass = stablePool [di.readShort ()]; 35 superClass = stablePool [di.readShort ()]; 36 if (debug) 37 System.out.println ("read (): Διαβάστε πληροφορίες κλάσης ..."); 38 39 / * 30 * Προσδιορίστε όλες τις διεπαφές που εφαρμόζονται από αυτήν την κλάση 31 * / 32 count = di.readShort (); 33 if (count! = 0) {34 if (debug) 35 System.out.println ("Εφαρμογές κλάσης" + μετρήσεις + "διεπαφές."); 36 διεπαφές = νέο ConstantPoolInfo [count]; 37 για (int i = 0; i <count; i ++) {38 int iindex = di.readShort (); 39 if ((iindex stablePool.length - 1)) 40 επιστροφή (false); 41 διεπαφές [i] = stablePool [iindex]; 42 if (debug) 43 System.out.println ("I" + i + ":" + διεπαφές [i]); 44} 45} 46 if (debug) 47 System.out.println ("read (): Διαβάστε πληροφορίες διεπαφής ..."); 

Μόλις ολοκληρωθεί αυτός ο κωδικός, το ανάγνωση Η μέθοδος έχει δημιουργήσει μια αρκετά καλή ιδέα για τη δομή της τάξης. Το μόνο που μένει είναι να συλλέξουμε τους ορισμούς πεδίου, τους ορισμούς μεθόδου και, ίσως το πιο σημαντικό, τα χαρακτηριστικά αρχείου κλάσης.

Η μορφή αρχείου τάξης χωρίζει κάθε μία από αυτές τις τρεις ομάδες σε μια ενότητα που αποτελείται από έναν αριθμό, ακολουθούμενη από αυτόν τον αριθμό εμφανίσεων του αντικειμένου που ψάχνετε. Έτσι, για τα πεδία, το αρχείο κλάσης έχει τον αριθμό των καθορισμένων πεδίων και στη συνέχεια πολλούς ορισμούς πεδίων. Ο κωδικός για σάρωση στα πεδία φαίνεται παρακάτω.

48 count = di.readShort (); 49 if (debug) 50 System.out.println ("Αυτή η κλάση έχει πεδία" + count + "."); 51 if (count! = 0) {52 πεδία = νέο FieldInfo [count]; 53 για (int i = 0; i <count; i ++) {54 πεδία [i] = new FieldInfo (); 55 if (! Πεδία [i] .read (di, stablePool)) {56 return (false); 57} 58 if (debug) 59 System.out.println ("F" + i + ":" + 60 πεδία [i] .toString (stablePool)); 61} 62} 63 if (debug) 64 System.out.println ("read (): Διαβάστε πληροφορίες πεδίου ..."); 

Ο παραπάνω κωδικός ξεκινά διαβάζοντας μια καταμέτρηση στη γραμμή # 48 και, στη συνέχεια, ενώ ο αριθμός δεν είναι μηδέν, διαβάζεται σε νέα πεδία χρησιμοποιώντας το Πληροφορίες πεδίου τάξη. ο Πληροφορίες πεδίου Η τάξη συμπληρώνει απλά δεδομένα που ορίζουν ένα πεδίο στην εικονική μηχανή Java. Ο κώδικας για ανάγνωση μεθόδων και χαρακτηριστικών είναι ο ίδιος, αντικαθιστώντας απλώς τις αναφορές στο Πληροφορίες πεδίου με αναφορές σε Μέθοδος πληροφοριών ή Ιδιότητα ανάλογα με την περίπτωση. Αυτή η πηγή δεν περιλαμβάνεται εδώ, ωστόσο μπορείτε να δείτε την πηγή χρησιμοποιώντας τους συνδέσμους στην ενότητα Πόροι παρακάτω.