Wir streben das Maximum an: vom ORM bis zur Bytecode-Analyse

Wie Sie wissen, sollte ein echter Programmierer drei Dinge in seinem Leben tun: seine eigene Programmiersprache erstellen, sein eigenes Betriebssystem schreiben und sein eigenes ORM erstellen. Und wenn ich die Sprache vor langer Zeit geschrieben habe (vielleicht erzähle ich es Ihnen ein andermal) und das Betriebssystem noch in Betrieb ist, dann möchte ich Ihnen jetzt etwas über ORM erzählen. Genauer gesagt geht es nicht einmal um ORM selbst, sondern um die Implementierung einer kleinen, lokalen und scheinbar völlig einfachen Funktion.


Gemeinsam werden wir den ganzen Weg von der Freude, eine einfache Lösung zu finden, bis zur Bitterkeit des Bewusstseins für seine Zerbrechlichkeit und Unrichtigkeit gehen. Von der Verwendung einer ausschließlich öffentlichen API bis hin zu schmutzigen Hacks. Von "fast ohne Reflexion" bis "knietief im Bytecode-Interpreter".


Wen interessiert es, wie der Bytecode zu analysieren ist, mit welchen Schwierigkeiten er behaftet ist und was für ein erstaunliches Ergebnis Sie am Ende erzielen können? Willkommen bei cat.


Inhalt


1 - Wie alles begann.
2-4 - Auf dem Weg zum Bytecode.
5 - Wer ist der Bytecode?
6 - Die Analyse selbst. Um dieses Kapitels willen wurde alles konzipiert und es war der Mut darin.
7 - Was kann noch beendet werden. Träume, Träume ...
Nachwort - Nachwort.


UPD: Unmittelbar nach der Veröffentlichung gingen die Teile 6-8 verloren (für die alles gestartet wurde). Behoben.



Teil Eins Das Problem


Stellen Sie sich vor, wir haben ein einfaches Schema. Es gibt einen Kunden, er hat mehrere Konten. Einer von ihnen ist voreingestellt. Ein Client kann auch mehrere SIM-Karten haben und jede SIM-Karte kann explizit festgelegt oder ein Standard-Client verwendet werden.



So wird dieses Modell in unserem Code beschrieben (ohne Getter / Setter / Konstruktoren / ...).


@JdbcEntity(table = "CLIENT") public class Client { @JdbcId private Long id; @JdbcColumn private String name; @JdbcJoinedObject(localColumn = "DEFAULTACCOUNT") private Account defaultAccount; } @JdbcEntity(table = "ACCOUNT") public class Account { @JdbcId private Long id; @JdbcColumn private Long balance; @JdbcJoinedObject(localColumn = "CLIENT") private Client client; } @JdbcEntity(table = "CARD") public class Card { @JdbcId private Long id; @JdbcColumn private String msisdn; @JdbcJoinedObject(localColumn = "ACCOUNT") private Account account; @JdbcJoinedObject(localColumn = "CLIENT") private Client client; } 

In ORM selbst müssen keine Proxys vorhanden sein (wir müssen eine Instanz dieser bestimmten Klasse erstellen) und eine einzelne Anforderung. Dementsprechend wird hier Folgendes an SQL gesendet, wenn versucht wird, eine Karte abzurufen.


 select CARD.id id, CARD.msisdn msisdn, ACCOUNT_2.id ACCOUNT_2_id, ACCOUNT_2.balance ACCOUNT_2_balance, CLIENT_3.id CLIENT_3_id, CLIENT_3.name CLIENT_3_name, CLIENT_1.id CLIENT_1_id, CLIENT_1.name CLIENT_1_name, ACCOUNT_4.id ACCOUNT_4_id, ACCOUNT_4.balance ACCOUNT_4_balance from CARD left outer join CLIENT CLIENT_1 on CARD.CLIENT = CLIENT_1.id left outer join ACCOUNT ACCOUNT_2 on CARD.ACCOUNT = ACCOUNT_2.id left outer join CLIENT CLIENT_3 on ACCOUNT_2.CLIENT = CLIENT_3.id left outer join ACCOUNT ACCOUNT_4 on CLIENT_1.DEFAULTACCOUNT = ACCOUNT_4.id; 

Ups. Der Kunde und die Rechnung werden dupliziert. Wenn Sie darüber nachdenken, ist dies zwar verständlich - schließlich weiß das Framework nicht, dass der Kartenclient und der Kartenkonto-Client derselbe Client sind. Und die Anfrage muss statisch und nur eine generiert werden (erinnern Sie sich an die Einschränkung der Eindeutigkeit der Anfrage?).


Aus genau dem gleichen Grund gibt es hier überhaupt keine Felder für Card.account.client.defaultAccount und Card.client.defaultAccount.client . Nur wir wissen, dass client und client.defaultAccount.client immer übereinstimmen. Und der Rahmen weiß es nicht, für ihn ist dies eine willkürliche Verbindung. Und was in solchen Fällen zu tun ist, ist nicht sehr klar. Ich kenne 3 Optionen:


  1. Beschreiben Sie Invarianten explizit in Anmerkungen.
  2. Stellen Sie rekursive Abfragen ( with recursive / connect by ).
  3. Um zu punkten.

Ratet mal, welche Option wir gewählt haben? Richtig. Infolgedessen werden jetzt nicht alle rekursiven Felder gefüllt und es gibt immer null.


Wenn Sie genau hinschauen, können Sie das zweite Problem hinter der Vervielfältigung erkennen, und es ist viel schlimmer. Was wollten wir? Kartennummer und Guthaben. Was hast du bekommen 4 Verknüpfungen und 10 Spalten. Und dieses Ding wächst exponentiell! Nun, d.h. Wir haben wirklich eine Situation, in der wir zunächst aus Gründen der Schönheit und Integrität das Modell für Anmerkungen vollständig beschreiben und dann aus Gründen von 5 Feldern eine Anforderung für 15 Verknüpfungen und 150 Spalten angefordert wird. Und in diesem Moment wird es wirklich beängstigend.



Teil Zwei Eine funktionierende, aber unpraktische Lösung


Eine einfache Lösung bittet sofort. Es dürfen nur die Lautsprecher gezogen werden, die verwendet werden! Einfach zu sagen. Die naheliegendste Option (um die Auswahl mit Ihren Händen zu schreiben) werden wir sofort fallen lassen. Nun, nicht dann haben wir das Modell beschrieben, um es nicht zu verwenden. Vor langer Zeit wurde eine spezielle Methode entwickelt - partialGet . Im Gegensatz zu simple get akzeptiert es List<String> - die Namen der zu füllenden Felder. Dazu müssen Sie zuerst Aliase in den Tabellen registrieren


 @JdbcJoinedObject(localColumn = "ACCOUNT", sqlTableAlias = "a") private Account account; @JdbcJoinedObject(localColumn = "CLIENT", sqlTableAlias = "c") private Client client; 

Und dann genießen Sie das Ergebnis.


 List<String> requiredColumns = asList("msisdn", "c_a_balance", "a_balance"); String query = cardMapper.getSelectSQL(requiredColumns, DatabaseType.ORACLE); System.out.println(query); 

 select CARD.msisdn msisdn, c_a.balance c_a_balance, a.balance a_balance from CARD left outer join ACCOUNT a on CARD.ACCOUNT = a.id left outer join CLIENT c on CARD.CLIENT = c.id left outer join ACCOUNT c_a on c.DEFAULTACCOUNT = c_a.id; 

Und alles scheint in Ordnung zu sein, aber tatsächlich nein. Hier ist, wie es in echtem Code verwendet wird.


 Card card = cardDAO.partialGet(cardId, "msisdn", "c_a_balance", "a_balance"); ... ... ...    ... ... ... long clientId = card.getClient().getId();//, NPE.  , id    ?! 

Und es stellt sich heraus, dass Sie PartialGet jetzt nur verwenden können, wenn der Abstand zwischen ihm und der Verwendung des Ergebnisses nur wenige Zeilen beträgt. Aber wenn das Ergebnis weit geht oder, Gott bewahre, innerhalb einer Methode weitergegeben wird, ist es bereits äußerst schwierig zu verstehen, welche Felder gefüllt sind und welche nicht. Wenn NPE irgendwo passiert ist, müssen Sie außerdem noch verstehen, ob es wirklich aus der Nulldatenbank zurückgegeben wurde oder ob wir dieses Feld einfach nicht ausgefüllt haben. Alles in allem sehr unzuverlässig.


Sie können natürlich einfach ein anderes Objekt mit Ihrer Zuordnung speziell für die Anforderung schreiben oder das Ganze sogar vollständig mit Ihren Händen auswählen und zu einem Tuple . Tatsächlich tun wir in der Realität an den meisten Orten genau das. Trotzdem möchte ich keine Auswahl mit meinen Händen schreiben und keine Zuordnung duplizieren.



Teil drei. Eine bequeme, aber nicht funktionsfähige Lösung.


Wenn Sie ein bisschen mehr nachdenken, fällt mir ziemlich schnell die Antwort ein - Sie müssen Schnittstellen verwenden. Dann einfach deklarieren


 public interface MsisdnAndBalance { String getMsisdn(); long getBalance(); } 

Und verwenden


 MsisdnAndBalance card = cardDAO.partialGet(cardId, ...); 

Und alle. Rufen Sie nichts extra an. Darüber hinaus kann mit dem Übergang zu Kotlin / ten / lomb auch dieser schreckliche Typ beseitigt werden. Aber hier wird der wichtigste Punkt immer noch weggelassen. Welche Argumente sollten an partialGet ? Riemen fühlen sich nach wie vor nicht mehr so ​​an, weil das Risiko zu groß ist, um Fehler zu machen und die falschen Felder zu schreiben. Und ich möchte, dass du es irgendwie kannst


 MsisdnAndBalance card = cardDAO.partialGet(cardId, MsisdnAndBalance.class); 

Oder noch besser auf Kotlin durch reifizierte Generika


 val card = cardDAO.paritalGet<MsisdnAndBalance>(cardId) 

Ehh, ein Fehler. Tatsächlich ist die ganze weitere Geschichte genau die Implementierung dieser Option.



Teil vier Auf dem Weg zum Bytecode


Das Hauptproblem besteht darin, dass Methoden von der Schnittstelle stammen und Anmerkungen über den Feldern liegen. Und wir müssen dieselben Felder mit Methoden finden. Der erste und naheliegendste Gedanke ist die Verwendung der Standard-Java-Bean-Konvention. Und für triviale Eigenschaften funktioniert dies sogar. Aber es stellt sich als sehr instabil heraus. Zum Beispiel lohnt es sich, eine Methode in einer Schnittstelle umzubenennen (durch ideologisches Refactoring), da alles sofort auseinander fällt. Die Idee ist klug genug, um Methoden in Implementierungsklassen umzubenennen, aber nicht genug, um zu verstehen, dass es sich um einen Getter handelt und Sie das Feld selbst umbenennen müssen. Eine ähnliche Lösung führt zu doppelten Feldern. Wenn ich beispielsweise die Methode getClientId() in meiner Schnittstelle benötige, kann ich sie nicht auf die einzig richtige Weise implementieren


 public class Client implements HasClientId { private Long id; @Override public Long getClientId() { return id; } } 

 public class Card implements HasClientId { private Client client; @Override public Long getClientId() { return client.getId(); } } 

Und ich muss Felder duplizieren. Ziehen Sie im Client sowohl die id als auch die Client- clientId , und in der Zuordnung neben dem Client befindet sich explizit die Client- clientId . Und stellen Sie sicher, dass dies alles nicht verlässt. Darüber hinaus möchte ich zum Beispiel, dass Getter mit nicht trivialer Logik funktionieren


 public class Card implements HasBalance { private Account account; private Client client; public long getBalance() { if (account != null) return account.getBalance(); else return client.getDefaultAccount().getBalance(); } } 

Die Option zum Suchen nach Namen wird also nicht mehr benötigt, Sie benötigen etwas Schwierigeres.


Die nächste Option war völlig verrückt und lebte nicht lange in meinem Kopf, aber der Vollständigkeit halber werde ich sie auch beschreiben. In der Analysephase können wir eine leere Entität erstellen und einfach abwechselnd einige Werte in die Felder schreiben. Danach erhalten wir die Getter und sehen, dass sich geändert hat, was sie zurückgeben oder nicht. Wir werden also sehen, dass sich der Wert von getClientId aus dem Datensatz im getClientId nicht ändert, sondern aus der Datensatz- id - er ändert sich. Darüber hinaus wird hier automatisch die Situation unterstützt, in der Getter und Felder unterschiedlichen Typs (z. B. isActive() = i_active != 0 ) automatisch unterstützt werden. Aber es gibt mindestens drei schwerwiegende Probleme (vielleicht mehr, aber ich habe nicht weiter darüber nachgedacht).


  1. Die offensichtliche Voraussetzung für das Wesentliche bei diesem Algorithmus besteht darin, den "gleichen" Wert vom Getter zurückzugeben, wenn sich das "entsprechende" Feld nicht geändert hat. "Ein und dasselbe" - aus Sicht des von uns gewählten Vergleichsoperators. == es kann offensichtlich nicht sein (andernfalls getAsInt() = Integer.parseInt(strField)) nicht mehr getAsInt() = Integer.parseInt(strField)) . Bleibt gleich. Wenn der Getter also eine Art Benutzerentität zurückgibt, die bei jedem Aufruf von Feldern generiert wird, muss er eine Überschreibung von equals .
  2. Komprimierungszuordnungen. Wie im obigen Beispiel mit int -> boolean . Wenn wir die Werte 0 und 1 überprüfen, sehen wir eine Änderung. Aber wenn wir 40 und 42 sind, dann werden wir beide Male wahr.
  3. Es kann komplexe Konverter in Gettern geben, die auf bestimmten Invarianten in Feldern beruhen (z. B. ein spezielles Zeichenfolgenformat). Und auf unsere generierten Daten werden sie Ausnahmen werfen.

Im Allgemeinen funktioniert die Option also auch nicht.


Während ich über das Ganze diskutierte, sprach ich zunächst scherzhaft den Satz aus: "Nun, nafig, es ist einfacher, Bytecode zu sehen, alles ist dort geschrieben." Zu dieser Zeit war mir nicht einmal klar, dass diese Idee mich verschlucken würde und wie weit alles gehen würde.



Fünfter Teil Was ist Bytecode und wie funktioniert es?


new #4, dup, invokespecial #5, areturn
Wenn Sie verstehen, was hier geschrieben steht und was dieser Code bewirkt, können Sie mit dem nächsten Teil fortfahren.


Haftungsausschluss 1. Um die weitere Geschichte zu verstehen, benötigen Sie leider mindestens ein grundlegendes Verständnis des Aussehens von Java-Bytecode. Daher schreibe ich ein paar Absätze darüber. In keiner Weise vorgeben, vollständig zu sein.


Haftungsausschluss 2. Es geht ausschließlich um die Methoden. Weder über den konstanten Pool noch über die Struktur der Klasse als Ganzes noch über die Methodendeklarationen selbst werde ich ein Wort sagen.


Das Wichtigste, was Sie über Bytecode wissen müssen, ist der Assembler für die virtuelle Java-Stack-Maschine. Dies bedeutet, dass die Argumente für die Anweisungen aus dem Stapel übernommen werden und die Rückgabewerte aus den Anweisungen auf den Stapel zurückgeschoben werden. Unter diesem Gesichtspunkt können wir sagen, dass der Bytecode in umgekehrter polnischer Notation geschrieben ist . Zusätzlich zum Stapel verfügt die Methode über ein Array lokaler Variablen. Bei der Eingabe der Methode werden this und alle Argumente dieser Methode in die Methode geschrieben und dort während der Ausführung lokale Variablen gespeichert. Hier ist ein einfaches Beispiel.


 public class Foo { private int bar; public int updateAndReturn(long baz, String str) { int result = (int) baz; result += str.length(); bar = result; return result; } } 

Ich werde Kommentare im Format schreiben


 # [(<local_variable_index>:<actual_value>)*], [(<value_on_stack>)*] 

Oben auf dem Stapel links.


 public int updateAndReturn(long, java.lang.String); Code: # [0:this, 1:long baz, 3:str], () 0: lload_1 # [0:this, 1:long baz, 3:str], (long baz) 1: l2i # [0:this, 1:long baz, 3:str], (int baz) 2: istore 4 # [0:this, 1:long baz, 3:str, 4:int baz], () 4: iload 4 # [0:this, 1:long baz, 3:str, 4:int baz], (int baz) 6: aload_3 # [0:this, 1:long baz, 3:str, 4:int baz], (str, int baz) 7: invokevirtual #2 // Method java/lang/String.length:()I # [0:this, 1:long baz, 3:str, 4:int baz], (length(str), int baz) 10: iadd # [0:this, 1:long baz, 3:str, 4:int baz], (length(str) + int baz) 11: istore 4 # [0:this, 1:long baz, 3:str, 4:length(str) + int baz], () 13: aload_0 # [0:this, 1:long baz, 3:str, 4:length(str) + int baz], (this) 14: iload 4 # [0:this, 1:long baz, 3:str, 4:length(str) + int baz], (length(str) + int baz, this) 16: putfield #3 // Field bar:I # [0:this, 1:long baz, 3:str, 4:length(str) + int baz], (),     bar 19: iload 4 # [0:this, 1:long baz, 3:str, 4:length(str) + int baz], (length(str) + int baz) 21: ireturn #  int   ,       

Es gibt viele Anweisungen. Die vollständige Liste muss im sechsten Kapitel von JVMS nachgeschlagen werden , auf Wikipedia gibt es eine kurze Nacherzählung . Eine große Anzahl von Anweisungen dupliziert sich für verschiedene Typen (z. B. iload für int und lload für long). lload_1 Sie mit den ersten 4 lokalen Variablen arbeiten, werden deren Anweisungen hervorgehoben (zum Beispiel gibt es lload_1 und es werden überhaupt keine Argumente verwendet, aber es gibt nur lload , es wird die Nummer der lokalen Variablen als Argument verwendet. Im obigen Beispiel gibt es eine ähnliche iload ).


Weltweit werden wir an folgenden Anweisungsgruppen interessiert sein:


  1. *load* , *store* - lokale Variable lesen / schreiben
  2. *aload , *astore - Lesen / Schreiben eines Array-Elements nach Index
  3. getfield , putfield - Lese- / Schreibfeld
  4. getstatic , putstatic - statisches Feld lesen / schreiben
  5. checkcast - zwischen Objekttypen checkcast . Brauche da Typisierte Werte liegen auf dem Stapel und in lokalen Variablen. Zum Beispiel war oben l2i für die Besetzung long -> int.
  6. invoke* - Methodenaufruf aufrufen
  7. *return - gibt den Wert zurück und beendet die Methode


Teil Sechs Zuhause


Für diejenigen, die eine so lange Einführung verpasst haben und um vom ursprünglichen Problem und der Vernunft in Bezug auf die Bibliothek abzulenken, formulieren wir das Problem genauer.


Mit einer java.lang.reflect.Method Instanz muss eine Liste aller nicht statischen Felder (sowohl aktuelle als auch alle verschachtelten Objekte) abgerufen werden, deren Messwerte (direkt oder transitiv) innerhalb dieser Methode liegen.

Zum Beispiel für eine solche Methode


 public long getBalance() { Account acc; if (account != null) acc = account; else acc = client.getDefaultAccount(); return acc.getBalance(); } 

Sie müssen eine Liste mit zwei Feldern account.balance : account.balance und client.defaultAccount.balance .


Ich werde, wenn möglich, eine allgemeine Lösung schreiben. An einigen Stellen müssen Sie jedoch das Wissen über das ursprüngliche Problem nutzen, um unlösbare, im allgemeinen Fall auftretende Probleme zu lösen.


Zuerst müssen Sie den Bytecode des Methodenkörpers selbst abrufen, dies kann jedoch nicht direkt über Java erfolgen. Aber seitdem Da die Methode ursprünglich in einer Klasse vorhanden war, ist es einfacher, die Klasse selbst abzurufen. Weltweit kenne ich zwei Möglichkeiten: Keilen Sie sich in den Klassenladevorgang ein und fangen Sie das bereits gelesene byte[] dort ab, oder suchen ClassName.class einfach die Datei ClassName.class auf der Festplatte und lesen Sie sie. Das Abfangen des Ladens auf der Ebene der üblichen Bibliothek kann nicht durchgeführt werden. Sie müssen entweder Javaagent verbinden oder einen benutzerdefinierten ClassLoader verwenden. In jedem Fall sind zusätzliche Schritte erforderlich, um die JVM / Anwendung zu konfigurieren, und dies ist unpraktisch. Sie können es einfacher machen. Alle "normalen" Klassen befinden sich immer in derselben Datei mit der Erweiterung ".class", deren Pfad das Klassenpaket ist. Ja, es wird nicht funktionieren, dynamisch hinzugefügte Klassen oder Klassen zu finden, die von einem benutzerdefinierten Klassenladeprogramm geladen wurden, aber wir benötigen dies für das jdbc-Modell, sodass wir mit Sicherheit sagen können, dass alle Klassen auf die "Standardmethode" in Jars gepackt werden. Gesamt:


 private static InputStream getClassFile(Class<?> clazz) { String file = clazz.getName().replace('.', '/') + ".class"; ClassLoader cl = clazz.getClassLoader(); if (cl == null) return ClassLoader.getSystemResourceAsStream(file); else return cl.getResourceAsStream(file); } 

Hurra, lies das Byte-Array. Was machen wir als nächstes damit? Im Prinzip gibt es in Java mehrere Bibliotheken zum Lesen / Schreiben von Bytecode, aber ASM wird normalerweise für Arbeiten auf niedriger Ebene verwendet. Weil Es ist für eine hohe Leistung und einen schnellen Betrieb geschärft. Die Besucher-API ist die wichtigste dort - asm liest die Klasse nacheinander und ruft die entsprechenden Methoden ab


 public abstract class ClassVisitor { public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {...} public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {...} public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {...} ... } public abstract class MethodVisitor { protected MethodVisitor mv; public MethodVisitor(final int api, final MethodVisitor mv) { ... this.mv = mv; } public void visitJumpInsn(int opcode, Label label) { if (mv != null) { mv.visitJumpInsn(opcode, label); } } ... } 

Der Benutzer wird aufgefordert, die für ihn interessanten Methoden neu zu definieren und dort seine eigene Analyse- / Transformationslogik zu schreiben. MethodVisitor möchte ich am Beispiel von MethodVisitor , dass alle Besucher eine Standardimplementierung durch Delegierung haben.


Neben der Haupt-API gibt es auch eine sofort einsatzbereite Tree-API. Wenn die Core-API ein Analogon zum SAX-Parser ist, ist die Tree-API ein Analogon zum DOM. Wir erhalten ein Objekt, in dem alle Informationen über die Klasse / Methode gespeichert sind, und können sie nach Belieben mit Sprüngen an einen beliebigen Ort analysieren. Tatsächlich handelt es sich bei dieser API um eine *Visitor , bei der in den Besuchsmethoden einfach Informationen gespeichert werden. Fast alle Methoden dort sehen so aus:


 public class MethodNode extends MethodVisitor { @Override public void visitJumpInsn(final int opcode, final Label label) { instructions.add(new JumpInsnNode(opcode, getLabelNode(label))); } ... } 

Jetzt können wir endlich die Methode zur Analyse laden.


 private static class AnalyzerClassVisitor extends ClassVisitor { private final String getterName; private final String getterDesc; private MethodNode methodNode; public AnalyzerClassVisitor(Method getter) { super(ASM6); this.getterName = getter.getName(); this.getterDesc = getMethodDescriptor(getter); } public MethodNode getMethodNode() { if (methodNode == null) throw new IllegalStateException(); return methodNode; } @Override public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { //      if (!name.equals(getterName) || !desc.equals(getterDesc)) return null; return new AnalyzerMethodVisitor(access, name, desc, signature, exceptions); } private class AnalyzerMethodVisitor extends MethodVisitor { public AnalyzerMethodVisitor(int access, String name, String desc, String signature, String[] exceptions) { super(ASM6, new MethodNode(ASM6, access, name, desc, signature, exceptions)); } @Override public void visitEnd() { //     ,     MethodVisitor    if (methodNode != null) throw new IllegalStateException(); methodNode = (MethodNode) mv; } } } 

Vollständige Code-Lesemethode.

MethodNode nicht direkt zurückgegeben, sondern ein Wrapper mit einem Paar ext. Felder weil wir werden sie auch später brauchen. Der Einstiegspunkt (und die einzige öffentliche Methode) ist readMethod(Method): MethodInfo .


 public class MethodReader { public static class MethodInfo { private final String internalDeclaringClassName; private final int classAccess; private final MethodNode methodNode; public MethodInfo(String internalDeclaringClassName, int classAccess, MethodNode methodNode) { this.internalDeclaringClassName = internalDeclaringClassName; this.classAccess = classAccess; this.methodNode = methodNode; } public String getInternalDeclaringClassName() { return internalDeclaringClassName; } public int getClassAccess() { return classAccess; } public MethodNode getMethodNode() { return methodNode; } } public static MethodInfo readMethod(Method method) { Class<?> clazz = method.getDeclaringClass(); String internalClassName = getInternalName(clazz); try (InputStream is = getClassFile(clazz)) { ClassReader cr = new ClassReader(is); AnalyzerClassVisitor cv = new AnalyzerClassVisitor(internalClassName, method); cr.accept(cv, SKIP_DEBUG | SKIP_FRAMES); return new MethodInfo(internalClassName, cv.getAccess(), cv.getMethodNode()); } catch (IOException e) { throw new RuntimeException(e); } } private static InputStream getClassFile(Class<?> clazz) { String file = clazz.getName().replace('.', '/') + ".class"; ClassLoader cl = clazz.getClassLoader(); if (cl == null) return ClassLoader.getSystemResourceAsStream(file); else return cl.getResourceAsStream(file); } private static class AnalyzerClassVisitor extends ClassVisitor { private final String className; private final String getterName; private final String getterDesc; private MethodNode methodNode; private int access; public AnalyzerClassVisitor(String internalClassName, Method getter) { super(ASM6); this.className = internalClassName; this.getterName = getter.getName(); this.getterDesc = getMethodDescriptor(getter); } public MethodNode getMethodNode() { if (methodNode == null) throw new IllegalStateException(); return methodNode; } public int getAccess() { return access; } @Override public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { if (!name.equals(className)) throw new IllegalStateException(); this.access = access; } @Override public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { if (!name.equals(getterName) || !desc.equals(getterDesc)) return null; return new AnalyzerMethodVisitor(access, name, desc, signature, exceptions); } private class AnalyzerMethodVisitor extends MethodVisitor { public AnalyzerMethodVisitor(int access, String name, String desc, String signature, String[] exceptions) { super(ASM6, new MethodNode(ASM6, access, name, desc, signature, exceptions)); } @Override public void visitEnd() { if (methodNode != null) throw new IllegalStateException(); methodNode = (MethodNode) mv; } } } } 

Es ist Zeit, die Analyse direkt durchzuführen. Wie macht man das? Der erste Gedanke ist, alle getfield Anweisungen zu getfield . Jedes getfield statisch an, um welches Feld es sich handelt und welche Klasse. Es können alle Felder unserer Klasse als notwendig angesehen werden, zu denen Zugang bestand. Dies funktioniert aber leider nicht. Das erste Problem hierbei ist, dass der Überschuss erfasst wird.


 class Foo { private int bar; private int baz; public int test() { return bar + new Foo().baz; } } 

Bei diesem Algorithmus berücksichtigen wir, dass das Baz-Feld benötigt wird, obwohl tatsächlich nein. Dieses Problem konnte jedoch noch behoben werden. Aber was tun mit den Methoden?


 public class Client implements HasClientId { private Long id; public Long getId() { HasClientId obj = this; return obj.getClientId(); } @Override public Long getClientId() { return id; } } 

Wenn wir nach Methodenaufrufen genauso suchen wie nach getClientId finden wir getClientId . Denn es gibt keinen Aufruf von Client.getClientId , sondern nur einen Aufruf von HasClientId.getClientId . Natürlich können Sie alle Methoden der aktuellen Klasse, alle ihre Oberklassen und alle zu verwendenden Schnittstellen berücksichtigen, aber das ist bereits zu viel. Sie können also versehentlich toString erfassen und darin finden Sie eine Auflistung aller Felder im Allgemeinen.


Darüber hinaus möchten wir, dass Getter-Aufrufe für verschachtelte Objekte auch funktionieren


 public class Account { private Client client; public long getClientId() { return client.getId(); } } 

Und hier gilt der Aufruf der Client.getId Methode überhaupt nicht für die Account Klasse.


Mit einem starken Wunsch können Sie noch einige Zeit an Hacks für Sonderfälle denken, aber ziemlich schnell wird klar, dass „Dinge nicht so gemacht werden“ und Sie den Ablauf der Ausführung und der Datenbewegung vollständig überwachen müssen. getfield , this , - this . Hier ist ein Beispiel:


 class Client { public long id; } class Account { public long id; public Client client; public long test() { return client.id + new Account().id; } } 

 class Account { public Client client; public long test(); Code: 0: aload_0 1: getfield #2 // Field client:LClient; 4: getfield #3 // Field Client.id:J 7: new #4 // class Account 10: dup 11: invokespecial #5 // Method "<init>":()V 14: getfield #6 // Field id:J 17: ladd 18: lreturn } 

  • 1: getfield this , aload_0 .
  • 4: getfield — , 1: getfield , , , this .
  • 14: getfield . Weil , ( Account ), this , , 7: new .

, Account.client.id , Account.id — . , , .


— , , aload_0 getfield this , , . , . . — ! -, . MethodNode , ( ). , .. (//) .


:


 public class Analyzer<V extends Value> { public Analyzer(final Interpreter<V> interpreter) {...} public Frame<V>[] analyze(final String owner, final MethodNode m) {...} } 

Analyzer ( Frame , ) . , , , , //etc.


 public abstract class Interpreter<V extends Value> { public abstract V newValue(Type type); public abstract V newOperation(AbstractInsnNode insn) throws AnalyzerException; public abstract V copyOperation(AbstractInsnNode insn, V value) throws AnalyzerException; public abstract V unaryOperation(AbstractInsnNode insn, V value) throws AnalyzerException; public abstract V binaryOperation(AbstractInsnNode insn, V value1, V value2) throws AnalyzerException; public abstract V ternaryOperation(AbstractInsnNode insn, V value1, V value2, V value3) throws AnalyzerException; public abstract V naryOperation(AbstractInsnNode insn, List<? extends V> values) throws AnalyzerException; public abstract void returnOperation(AbstractInsnNode insn, V value, V expected) throws AnalyzerException; public abstract V merge(V v, V w); } 

V — , , . Analyzer , , , . , getfield — , , . , unaryOperation(AbstractInsnNode insn, V value): V , . 1: getfield Value , " client , Client ", 14: getfield " — - , ".


merge(V v, V w): V . , , . Zum Beispiel:


 public long getBalance() { Account acc; if (account != null) acc = account; else acc = client.getDefaultAccount(); return acc.getBalance(); } 

Account.getBalance() . - . . ? merge .


SuperInterpreter extends Interpreter<SuperValue> ? Richtig. SuperValue . — , . , .


 public class Value extends BasicValue { private final Set<Ref> refs; private Value(Type type, Set<Ref> refs) { super(type); this.refs = refs; } } public class Ref { private final List<Field> path; private final boolean composite; public Ref(List<Field> path, boolean composite) { this.path = path; this.composite = composite; } } 

composite . , . , String . String.length() , , name , name.value.length . , length — , , arraylength . ? Nein! — . , , , . , Date , String , Long , . , , .


 class Persion { @JdbcColumn(converter = CustomJsonConverter.class) private PassportInfo passportInfo; } 

PassportInfo . , . , composite . .


 public class Ref { private final List<Field> path; private final boolean composite; public Ref(List<Field> path, boolean composite) { this.path = path; this.composite = composite; } public List<Field> getPath() { return path; } public boolean isComposite() { return composite; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Ref ref = (Ref) o; return Objects.equals(path, ref.path); } @Override public int hashCode() { return Objects.hash(path); } @Override public String toString() { if (path.isEmpty()) return "<[this]>"; else return "<" + path.stream().map(Field::getName).collect(joining(".")) + ">"; } public static Ref thisRef() { return new Ref(emptyList(), true); } public static Optional<Ref> childRef(Ref parent, Field field, Configuration configuration) { if (!parent.isComposite()) return empty(); if (parent.path.contains(field))//    ,   return empty(); List<Field> path = new ArrayList<>(parent.path); path.add(field); return of(new Ref(path, configuration.isCompositeField(field))); } public static Optional<Ref> childRef(Ref parent, Ref child) { if (!parent.isComposite()) return empty(); if (child.path.stream().anyMatch(parent.path::contains))// ,   return empty(); List<Field> path = new ArrayList<>(parent.path); path.addAll(child.path); return of(new Ref(path, child.composite)); } } 

 public class Value extends BasicValue { private final Set<Ref> refs; private Value(Type type, Set<Ref> refs) { super(type); this.refs = refs; } public Set<Ref> getRefs() { return refs; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; if (!super.equals(o)) return false; Value value = (Value) o; return Objects.equals(refs, value.refs); } @Override public int hashCode() { return Objects.hash(super.hashCode(), refs); } @Override public String toString() { return "(" + refs.stream().map(Object::toString).collect(joining(",")) + ")"; } public static Value typedValue(Type type, Ref ref) { return new Value(type, singleton(ref)); } public static Optional<Value> childValue(Value parent, Value child) { Type type = child.getType(); Set<Ref> fields = parent.refs.stream() .flatMap(p -> child.refs.stream().map(c -> childRef(p, c))) .filter(Optional::isPresent) .map(Optional::get) .collect(toSet()); if (fields.isEmpty()) return empty(); return of(new Value(type, fields)); } public static Optional<Value> childValue(Value parent, FieldInsnNode childInsn, Configuration configuration) { Type type = Type.getType(childInsn.desc); Field child = resolveField(childInsn); Set<Ref> fields = parent.refs.stream() .map(p -> childRef(p, child, configuration)) .filter(Optional::isPresent) .map(Optional::get) .collect(toSet()); if (fields.isEmpty()) return empty(); return of(new Value(type, fields)); } public static Value mergeValues(Collection<Value> values) { List<Type> types = values.stream().map(BasicValue::getType).distinct().collect(toList()); if (types.size() != 1) { String typesAsString = types.stream().map(Type::toString).collect(joining(", ", "(", ")")); throw new IllegalStateException("could not merge " + typesAsString); } Set<Ref> fields = values.stream().flatMap(v -> v.refs.stream()).distinct().collect(toSet()); return new Value(types.get(0), fields); } public static boolean isComposite(BasicValue value) { return value instanceof Value && value.getType().getSort() == Type.OBJECT && ((Value) value).refs.stream().anyMatch(Ref::isComposite); } } 

, . Lass uns gehen!


 public class FieldsInterpreter extends BasicInterpreter { 

, BasicInterpreter . BasicValue ( , Value extends BasicValue ) .


 public class BasicValue implements Value { public static final BasicValue UNINITIALIZED_VALUE = new BasicValue(null); public static final BasicValue INT_VALUE = new BasicValue(Type.INT_TYPE); public static final BasicValue FLOAT_VALUE = new BasicValue(Type.FLOAT_TYPE); public static final BasicValue LONG_VALUE = new BasicValue(Type.LONG_TYPE); public static final BasicValue DOUBLE_VALUE = new BasicValue(Type.DOUBLE_TYPE); public static final BasicValue REFERENCE_VALUE = new BasicValue(Type.getObjectType("java/lang/Object")); public static final BasicValue RETURNADDRESS_VALUE = new BasicValue(Type.VOID_TYPE); private final Type type; public BasicValue(final Type type) { this.type = type; } } 

( (Value)basicValue ) , , ( " iconst ") .


newValue . , , " ". , this catch . , , . BasicInterpreter BasicValue(actualType) BasicValue.REFERENCE_VALUE . .


 @Override public BasicValue newValue(Type type) { if (type != null && type.getSort() == OBJECT) return new BasicValue(type); return super.newValue(type); } 

entry point. this . , - , , this , BasicValue(actualType) , Value.typedValue(actualType, Ref.thisRef()) . , , this newValue , . , .. , this . this . , . , this 0. , . , , . .


 @Override public BasicValue copyOperation(AbstractInsnNode insn, BasicValue value) throws AnalyzerException { if (wasUpdated || insn.getType() != VAR_INSN || ((VarInsnNode) insn).var != 0) { return super.copyOperation(insn, value); } switch (insn.getOpcode()) { case ALOAD: return typedValue(value.getType(), thisRef()); case ISTORE: case LSTORE: case FSTORE: case DSTORE: case ASTORE: wasUpdated = true; } return super.copyOperation(insn, value); } 

Mach weiter. . , , , — , . , .


 @Override public BasicValue merge(BasicValue v, BasicValue w) { if (v.equals(w)) return v; if (v instanceof Value || w instanceof Value) { if (!Objects.equals(v.getType(), w.getType())) { if (v == UNINITIALIZED_VALUE || w == UNINITIALIZED_VALUE) return UNINITIALIZED_VALUE; throw new IllegalStateException("could not merge " + v + " and " + w); } if (v instanceof Value != w instanceof Value) { if (v instanceof Value) return v; else return w; } return mergeValues(asList((Value) v, (Value) w)); } return super.merge(v, w); } 

. ""? ? Nicht wirklich. . , .. . , 3 ( ): putfield , putstatic , aastore . . putstatic ( ) . , . putfield aastore . , , . ( ) . , . , — .


 public class Account { private Client client; public Long getClientId() { return Optional.ofNullable(client).map(Client::getId).orElse(null); } } 

, ( ofNullable Optional client value ), . . . , - ofNullable(client) , - map(Client::getId) , .


putfield , putstatic aastore .


 @Override public BasicValue binaryOperation(AbstractInsnNode insn, BasicValue value1, BasicValue value2) throws AnalyzerException { if (insn.getOpcode() == PUTFIELD && Value.isComposite(value2)) { throw new IllegalStateException("could not trace " + value2 + " over putfield"); } return super.binaryOperation(insn, value1, value2); } @Override public BasicValue ternaryOperation(AbstractInsnNode insn, BasicValue value1, BasicValue value2, BasicValue value3) throws AnalyzerException { if (insn.getOpcode() == AASTORE && Value.isComposite(value3)) { throw new IllegalStateException("could not trace " + value3 + " over aastore"); } return super.ternaryOperation(insn, value1, value2, value3); } @Override public BasicValue unaryOperation(AbstractInsnNode insn, BasicValue value) throws AnalyzerException { if (Value.isComposite(value)) { switch (insn.getOpcode()) { case PUTSTATIC: { throw new IllegalStateException("could not trace " + value + " over putstatic"); } ... } } return super.unaryOperation(insn, value); } 

. checkcast . : . —


 Client client1 = ...; Object objClient = client1; Client client2 = (Client) objClient; 

, . , , client1 objClient , . , checkcast .


.


 class Foo { private List<?> list; public void trimToSize() { ((ArrayList<?>) list).trimToSize(); } } 

. , , , . , , , , , . ? , ! . , , , null/0/false. . —


 @JdbcJoinedObject(localColumn = "CLIENT") private Client client; 

, , ORM , . checkcast


 @Override public BasicValue unaryOperation(AbstractInsnNode insn, BasicValue value) throws AnalyzerException { if (Value.isComposite(value)) { switch (insn.getOpcode()) { ... case CHECKCAST: { Class<?> original = reflectClass(value.getType()); Type targetType = getObjectType(((TypeInsnNode) insn).desc); Class<?> afterCast = reflectClass(targetType); if (afterCast.isAssignableFrom(original)) { return value; } else { throw new IllegalStateException("type specification not supported"); } } } } return super.unaryOperation(insn, value); } 

getfield . — ?


 class Foo { private Foo child; public Foo test() { Foo loopedRef = this; while (ThreadLocalRandom.current().nextBoolean()) { loopedRef = loopedRef.child; } return loopedRef; } } 

, . ? child , child.child , child.child.child ? ? , . , . ,


null.

child, null, , , . Ref.childRef


 if (parent.path.contains(field)) return empty(); 

. , .


" ". . , . , , ( @JdbcJoinedObject , @JdbcColumn ), , . ORM .


, getfield , . , , , . — .


 @Override public BasicValue unaryOperation(AbstractInsnNode insn, BasicValue value) throws AnalyzerException { if (Value.isComposite(value)) { switch (insn.getOpcode()) { ... case GETFIELD: { Optional<Value> optionalFieldValue = childValue((Value) value, (FieldInsnNode) insn, configuration); if (!optionalFieldValue.isPresent()) break; Value fieldValue = optionalFieldValue.get(); if (configuration.isInterestingField(resolveField((FieldInsnNode) insn))) { context.addUsedField(fieldValue); } if (Value.isComposite(fieldValue)) { return fieldValue; } break; } ... } } return super.unaryOperation(insn, value); } 

. , , . , invoke* . , , , . , :


 public long getClientId() { return getClient().getId(); } 

, , . , . . , . ? . . .


 class Account implements HasClient { @JdbcJoinedObject private Client client; public Client getClient() { return client; } } 

Account.client . , . . — , .


 public static class Result { private final Set<Value> usedFields; private final Value returnedCompositeValue; } 

? , . . , .. ( , — ), , areturn , , , *return . MethodNode ( , Tree API) . . — . , ? . .


 private static Value getReturnedCompositeValue(Frame<BasicValue>[] frames, AbstractInsnNode[] insns) { Set<Value> resultValues = new HashSet<>(); for (int i = 0; i < insns.length; i++) { AbstractInsnNode insn = insns[i]; switch (insn.getOpcode()) { case IRETURN: case LRETURN: case FRETURN: case DRETURN: case ARETURN: BasicValue value = frames[i].getStack(0); if (Value.isComposite(value)) { resultValues.add((Value) value); } break; } } if (resultValues.isEmpty()) return null; return mergeValues(resultValues); } 

analyzeField


 public static Result analyzeField(Method method, Configuration configuration) { if (Modifier.isNative(method.getModifiers())) throw new IllegalStateException("could not analyze native method " + method); MethodInfo methodInfo = readMethod(method); MethodNode mn = methodInfo.getMethodNode(); String internalClassName = methodInfo.getInternalDeclaringClassName(); int classAccess = methodInfo.getClassAccess(); Context context = new Context(method, classAccess); FieldsInterpreter interpreter = new FieldsInterpreter(context, configuration); Analyzer<BasicValue> analyzer = new Analyzer<>(interpreter); try { analyzer.analyze(internalClassName, mn); } catch (AnalyzerException e) { throw new RuntimeException(e); } Frame<BasicValue>[] frames = analyzer.getFrames(); AbstractInsnNode[] insns = mn.instructions.toArray(); Value returnedCompositeValue = getReturnedCompositeValue(frames, insns); return new Result(context.getUsedFields(), returnedCompositeValue); } 

, -, . invoke* . 5 :


  1. invokespecial — . , , ( super.call() ).
  2. invokevirtual — . . , .
  3. invokeinterface — , invokevirtual , — .
  4. invokestatic
  5. invokedynamic — , 7 JSR 292. JVM, invokedynamic ( dynamic). , (+ ), . , Invokedynamic: ? .

, , , . invokedynamic , . , , , (, ), invokedynamic . , "" . , invokedynamic , .


Mach weiter. , . , . , this , 0? , - , FieldsInterpreter copyOperation . , MethodAnalyzer.analyzeFields " this " " " ( this — ). , . , , . , - . , (- Optional.ofNullable(client) ). .


, invokestatic (.. , this ). invokespecial , invokevirtual invokeinterface . , . , , jvm. invokespecial , , . invokevirtual invokeinterface . , .


 public String objectToString(Object obj) { return obj.toString(); } 

 public static java.lang.String objectToString(java.lang.Object); Code: 0: aload_0 1: invokevirtual #104 // Method java/lang/Object.toString:()Ljava/lang/String; 4: areturn 

, , ( ) . , , . ? ORM . ORM , , . invokevirtual invokeinterface .


Hurra! . Was weiter? , ( , this ), ( , ) . !


  @Override public BasicValue naryOperation(AbstractInsnNode insn, List<? extends BasicValue> values) throws AnalyzerException { Method method = null; Value methodThis = null; switch (insn.getOpcode()) { case INVOKESPECIAL: {...} case INVOKEVIRTUAL: {...} case INVOKEINTERFACE: { if (Value.isComposite(values.get(0))) { MethodInsnNode methodNode = (MethodInsnNode) insn; Class<?> objectClass = reflectClass(values.get(0).getType()); Method interfaceMethod = resolveInterfaceMethod(reflectClass(methodNode.owner), methodNode.name, getMethodType(methodNode.desc)); method = lookupInterfaceMethod(objectClass, interfaceMethod); methodThis = (Value) values.get(0); } List<?> badValues = values.stream().skip(1).filter(Value::isComposite).collect(toList()); if (!badValues.isEmpty()) throw new IllegalStateException("could not pass " + badValues + " as parameter"); break; } case INVOKESTATIC: case INVOKEDYNAMIC: { List<?> badValues = values.stream().filter(Value::isComposite).collect(toList()); if (!badValues.isEmpty()) throw new IllegalStateException("could not pass " + badValues + " as parameter"); break; } } if (method != null) { MethodAnalyzer.Result methodResult = analyzeFields(method, configuration); for (Value usedField : methodResult.getUsedFields()) { childValue(methodThis, usedField).ifPresent(context::addUsedField); } if (methodResult.getReturnedCompositeValue() != null) { Optional<Value> returnedValue = childValue(methodThis, methodResult.getReturnedCompositeValue()); if (returnedValue.isPresent()) { return returnedValue.get(); } } } return super.naryOperation(insn, values); } 

. , . , JVMS 1 1. . — . , , . , .. , - , 2 — . , , . , — . , ResolutionUtil LookupUtil .


!



.


, 80% 20% 20% 80% . , , , ?


  • . .


  • . (.. ), . , . , .


     public class Account { private Client client; public Long getClientId() { return Optional.ofNullable(client).map(Client::getId).orElse(null); } } 

    Optional , ofNullable getClientId , , value . , returnedCompositeValue — , , . , , ( ) , . -. , , , " value Optional@1234 Client@5678 " .


  • invokedynamic , . indy , . , . , . , invokedynamic. . . , java.lang.invoke.LambdaMetafactory.metafactory . , , , . java.lang.invoke.StringConcatFactory.makeConcat/makeConcatWithConstants . . toString() . , , , , . , , , /- . jvm, . , , . . . , , . indy . ? indy , — CallSite . . , , LambdaMetafactory.metafactory getValue , . getValue . ( ) . , , , stateless. , ! - , . CallSite ConstantCallSite , MutableCallSite VolatileCallSite . mutable volatile , , ConstantCallSite . "- ". , , . , VM, .




Nachwort


- , . - partialGet . , . , , , , " " .


, .

Source: https://habr.com/ru/post/de409043/


All Articles