Comme vous le savez, un vrai programmeur devrait faire 3 choses dans sa vie: créer son propre langage de programmation, écrire son propre système d'exploitation et créer son propre ORM. Et si j'ai écrit la langue il y a longtemps (peut-être que je vous le dirai une autre fois) et que le système d'exploitation est toujours en avance, alors je veux vous parler d'ORM en ce moment. Pour être plus précis, il ne s'agit même pas d'ORM lui-même, mais de la mise en œuvre d'une petite fonctionnalité locale et, semble-t-il, complètement simple.
Ensemble, nous irons de la joie de trouver une solution simple à l'amertume de la conscience de sa fragilité et de son inexactitude. De l'utilisation d'une API exclusivement publique aux hacks sales. De «presque sans réflexion» à «jusqu'aux genoux dans l'interpréteur de code octet».
Qui se soucie d'analyser le bytecode, quelles difficultés il présente et quel résultat incroyable vous pouvez obtenir à la fin, bienvenue chez cat.
Table des matières
1 - Comment tout a commencé.
2-4 - En route vers le bytecode.
5 - Qui est le bytecode.
6 - L'analyse elle-même. C'est pour le bien de ce chapitre que tout a été conçu et c'était en lui-même les tripes.
7 - Quoi d'autre peut être fini. Rêves, rêves ...
Afterword - Afterword.
UPD: Immédiatement après la publication, les parties 6-8 ont été perdues (pour le bien de quoi tout a commencé). Fixe.
Première partie Le problème
Imaginez que nous ayons un schéma simple. Il y a un client, il a plusieurs comptes. L'un d'eux est par défaut. En outre, un client peut avoir plusieurs cartes SIM et chaque carte SIM peut être définie explicitement, ou un client par défaut peut être utilisé.

Voici comment ce modèle est décrit dans notre code (en omettant les getters / setters / constructors / ...).
@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; }
Dans ORM lui-même, nous avons une exigence pour l'absence de procurations (nous devons créer une instance de cette classe particulière) et une seule demande. En conséquence, voici ce que sql est envoyé à la base de données lorsque vous essayez d'obtenir une carte.
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;
Oups. Le client et la facture sont dupliqués. Certes, si vous y réfléchissez, cela est compréhensible - après tout, le cadre ne sait pas que le client de la carte et le client du compte de carte sont le même client. Une requête doit être générée statiquement et une seule (rappelez-vous la limitation de l'unicité de la demande?)
Soit dit en passant, pour exactement la même raison, il n'y a aucun Card.account.client.defaultAccount
et Card.client.defaultAccount.client
. Nous savons seulement que client
et client.defaultAccount.client
correspondent toujours. Et le cadre ne sait pas, pour lui, c'est un lien arbitraire. Et que faire dans de tels cas n'est pas très clair. Je connais 3 options:
- Décrivez explicitement les invariants dans les annotations.
- Faire des requêtes récursives (
with recursive
/ se connect by
). - Pour marquer.
Devinez quelle option nous avons choisie? Oui. Par conséquent, tous les champs récursifs ne sont plus du tout remplis et il y a toujours null.
Mais si vous regardez attentivement, vous pouvez voir le deuxième problème derrière la duplication, et c'est bien pire. Que voulions-nous? Numéro de carte et solde. Qu'avez-vous obtenu? 4 jointures et 10 colonnes. Et cette chose grandit de façon exponentielle! Eh bien, c'est-à-dire nous avons vraiment une situation où, d'abord, pour des raisons de beauté et d'intégrité, nous décrivons pleinement le modèle sur les annotations, puis, pour 5 champs , une demande de 15 jointures et 150 colonnes est demandée. Et en ce moment ça devient vraiment effrayant.
Deuxième partie Une solution fonctionnelle mais peu pratique
Une solution simple demande immédiatement. Seuls les haut-parleurs qui seront utilisés doivent être traînés! Facile à dire. L'option la plus évidente (pour écrire la sélection avec vos mains) nous abandonnera immédiatement. Eh bien, pas à ce moment-là, nous avons décrit le modèle afin de ne pas l'utiliser. Il y a très longtemps, une méthode spéciale a été partialGet
- partialGet
. Contrairement à get
simple, il accepte List<String>
- les noms des champs à remplir. Pour ce faire, vous devez d'abord enregistrer des alias dans les tableaux
@JdbcJoinedObject(localColumn = "ACCOUNT", sqlTableAlias = "a") private Account account; @JdbcJoinedObject(localColumn = "CLIENT", sqlTableAlias = "c") private Client client;
Et puis profitez du résultat.
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;
Et tout semble aller bien, mais, en fait, non. Voici comment il sera utilisé dans du vrai code.
Card card = cardDAO.partialGet(cardId, "msisdn", "c_a_balance", "a_balance"); ... ... ... ... ... ... long clientId = card.getClient().getId();
Et il s'avère que vous ne pouvez désormais utiliser partialGet que si la distance entre celui-ci et l'utilisation du résultat n'est que de quelques lignes. Mais si le résultat va loin ou, Dieu nous en préserve, est transmis à l'intérieur d'une méthode, il est déjà extrêmement difficile de comprendre quels champs sont remplis et lesquels ne le sont pas. De plus, si NPE s'est produit quelque part, alors vous devez toujours savoir s'il est vraiment revenu de la base de données null ou si nous n'avons tout simplement pas rempli ce champ. Dans l'ensemble, très peu fiable.
Vous pouvez, bien sûr, simplement écrire un autre objet avec votre mappage spécifiquement pour la demande, ou même sélectionner complètement le tout avec vos mains et l'assembler en un Tuple
. En fait, en réalité, dans la plupart des endroits, nous faisons exactement cela. Mais je voudrais quand même ne pas écrire de sélections avec mes mains et ne pas dupliquer la cartographie.
Troisième partie. Une solution pratique mais inopérante.
Si vous réfléchissez un peu plus, la réponse me vient assez rapidement - vous devez utiliser des interfaces. Déclarez ensuite
public interface MsisdnAndBalance { String getMsisdn(); long getBalance(); }
Et utiliser
MsisdnAndBalance card = cardDAO.partialGet(cardId, ...);
Et c'est tout. N'appelez rien de plus. De plus, avec la transition vers Kotlin / ten / lomb, même ce type terrible peut être éliminé. Mais ici, le point le plus important est encore omis. Quels arguments doivent être transmis à partialGet
? Les tongs, comme avant, n'ont plus envie, car le risque est trop grand pour faire des erreurs et écrire les mauvais champs. Et je veux que vous puissiez en quelque sorte
MsisdnAndBalance card = cardDAO.partialGet(cardId, MsisdnAndBalance.class);
Ou encore mieux sur Kotlin grâce aux génériques réifiés
val card = cardDAO.paritalGet<MsisdnAndBalance>(cardId)
Ehh, une erreur. En fait, toute l'histoire supplémentaire est précisément la mise en œuvre de cette option.
Quatrième partie En route vers le bytecode
Le problème clé est que les méthodes proviennent de l'interface et que les annotations sont au-dessus des champs. Et nous devons trouver ces mêmes champs par des méthodes. La première et la plus évidente pensée est d'utiliser la convention Java Bean standard. Et pour les propriétés triviales, cela fonctionne même. Mais cela s'avère très instable. Par exemple, il vaut la peine de renommer une méthode dans une interface (par refactorisation idéologique), car tout s'écroule instantanément. L'idée est suffisamment intelligente pour renommer les méthodes dans les classes d'implémentation, mais pas assez pour comprendre qu'il s'agissait d'un getter et que vous devez renommer le champ lui-même. Et une solution similaire conduit à la duplication des champs. Par exemple, si j'ai besoin de la méthode getClientId()
dans mon interface, je ne peux pas l'implémenter de la seule manière correcte
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(); } }
Et je dois dupliquer les champs. Et dans Client
faites glisser id
et clientId
, et dans la carte à côté du client, explicitement clientId
. Et assurez-vous que tout cela ne part pas. De plus, je veux aussi que les getters avec une logique non triviale fonctionnent, par exemple
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(); } }
Ainsi, l'option de recherche par nom n'est plus nécessaire, vous avez besoin de quelque chose de plus délicat.
L'option suivante était complètement folle et ne vivait pas longtemps dans ma tête, mais dans un souci d'exhaustivité de l'histoire, je la décrirai également. Au stade de l'analyse, nous pouvons créer une entité vide et simplement écrire à tour de rôle des valeurs dans les champs, et après cela nous obtenons les getters et regardons que cela a changé ce qu'ils retournent ou non. Nous verrons donc qu'à partir de l'enregistrement dans le champ de name
, la valeur de getClientId
ne change pas, mais à partir de l' id
enregistrement - il change. De plus, la situation où des getters et des champs de différents types (tels que isActive() = i_active != 0
) est automatiquement prise en charge ici. Mais il y a au moins trois problèmes graves (peut-être plus, mais je n'ai pas réfléchi plus loin).
- L'exigence évidente pour l'essence avec cet algorithme est de renvoyer la "même" valeur du getter si le champ "correspondant" n'a pas changé. "Une seule et même chose" - du point de vue de l'opérateur de comparaison que nous avons choisi.
==
cela ne peut évidemment pas l'être (sinon certains getAsInt() = Integer.parseInt(strField))
cesseront de fonctionner getAsInt() = Integer.parseInt(strField))
. Reste égal. Donc, si le getter retourne une sorte d'entité utilisateur générée par des champs à chaque appel, il doit alors avoir un remplacement equals
. - Mappages de compression. Comme dans l'exemple avec
int -> boolean
ci-dessus. Si nous vérifions les valeurs 0 et 1, nous verrons un changement. Mais si à 40 et 42, alors les deux fois nous devenons vrais. - Il peut y avoir des convertisseurs complexes dans les getters qui dépendent de certains invariants dans les champs (par exemple, un format de chaîne spécial). Et sur nos données générées, ils lèveront des exceptions.
Donc, en général, l'option ne fonctionne pas non plus.
En discutant de tout cela, j'ai d'abord prononcé en plaisantant la phrase "eh bien, nafig, il est plus facile de voir le bytecode, tout y est écrit." À ce moment-là, je ne savais même pas que cette idée allait m'avaler et jusqu'où tout irait.
Cinquième partie Qu'est-ce que le bytecode et comment ça marche
new #4, dup, invokespecial #5, areturn
Si vous comprenez ce qui est écrit ici et ce que fait ce code, vous pouvez passer à la partie suivante .
Clause de non-responsabilité 1. Malheureusement, pour comprendre la suite de l'histoire, vous avez besoin d'au moins une compréhension de base de ce à quoi ressemble le bytecode Java, je vais donc écrire quelques paragraphes à ce sujet. Ne prétendez en aucun cas être complet.
Clause de non-responsabilité 2. Il s'agira exclusivement du corps des méthodes. Ni sur le pool constant, ni sur la structure de la classe dans son ensemble, ni même sur les déclarations de méthode elles-mêmes, je dirai un mot.
La principale chose que vous devez comprendre au sujet du bytecode est l'assembleur de la machine virtuelle de la pile Java. Cela signifie que les arguments des instructions sont extraits de la pile et que les valeurs de retour des instructions sont repoussées dans la pile. De ce point de vue, on peut dire que le bytecode est écrit en notation polonaise inversée . En plus de la pile, la méthode possède également un tableau de variables locales. En entrant dans la méthode, this
et tous les arguments de cette méthode y sont écrits et les variables locales y sont stockées pendant l'exécution. Voici un exemple simple.
public class Foo { private int bar; public int updateAndReturn(long baz, String str) { int result = (int) baz; result += str.length(); bar = result; return result; } }
J'écrirai des commentaires au format
# [(<local_variable_index>:<actual_value>)*], [(<value_on_stack>)*]
Haut de la pile à gauche.
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 ,
Il y a beaucoup d'instructions. La liste complète doit être consultée dans le sixième chapitre de JVMS , sur Wikipédia, il y a un bref récit . Un grand nombre d'instructions se dupliquent pour différents types (par exemple, iload
pour int et lload
pour long). De plus, pour travailler avec les 4 premières variables locales, leurs instructions sont mises en évidence (par exemple, il y a lload_1
et il n'accepte pas du tout d'arguments, mais il n'y a que lload
, il prendra le numéro de la variable locale comme argument. Dans l'exemple ci-dessus, il y a un iload
similaire).
Globalement, nous serons intéressés par les groupes d'instructions suivants:
*load*
, *store*
- lire / écrire une variable locale*aload
, *astore
- lecture / écriture d'un élément de tableau par indexgetfield
, putfield
- champ de lecture / écrituregetstatic
, putstatic
- champ statique en lecture / écriturecheckcast
- cast entre les types d'objets. Besoin car les valeurs saisies se trouvent sur la pile et dans les variables locales. Par exemple, ci-dessus était l2i pour le casting long -> int.invoke*
- appel de méthode*return
- retourne la valeur et quitte la méthode
Sixième partie Accueil
Pour ceux qui ont raté une si longue introduction, ainsi que pour détourner l'attention du problème et de la raison d'origine en termes de bibliothèque, nous formulons le problème plus précisément.
Il est nécessaire, ayant une instance java.lang.reflect.Method
sous la main, d'obtenir une liste de tous les champs non statiques (à la fois actuels et tous les objets imbriqués) dont les lectures (directement ou transitivement) seront à l'intérieur de cette méthode.
Par exemple, pour une telle méthode
public long getBalance() { Account acc; if (account != null) acc = account; else acc = client.getDefaultAccount(); return acc.getBalance(); }
Vous devez obtenir une liste de deux champs: account.balance
et client.defaultAccount.balance
.
J'écrirai, si possible, une solution généralisée. Mais dans quelques endroits, vous devrez utiliser la connaissance du problème d'origine pour résoudre des problèmes insolubles, dans le cas général.
Vous devez d'abord obtenir le bytecode du corps de la méthode lui-même, mais vous ne pouvez pas le faire directement via Java. Mais depuis Étant donné que la méthode existe à l'origine dans une classe, il est plus facile d'obtenir la classe elle-même. Globalement, je connais deux options: coincer dans le processus de chargement de classe et intercepter l' byte[]
déjà lu byte[]
, ou simplement trouver le fichier ClassName.class
sur le disque et le lire. L'interception du chargement au niveau de la bibliothèque habituelle ne peut pas être effectuée. Vous devez soit connecter javaagent, soit utiliser ClassLoader personnalisé. Dans tous les cas, des étapes supplémentaires sont nécessaires pour configurer jvm / application, ce qui n'est pas pratique. Vous pouvez le faire plus facilement. Toutes les classes "ordinaires" sont toujours dans le même fichier avec l'extension ".class", dont le chemin est le package de classe. Oui, cela ne fonctionnera pas pour trouver des classes ajoutées dynamiquement ou des classes chargées par un chargeur de classe personnalisé, mais nous en avons besoin pour le modèle jdbc, donc nous pouvons dire avec confiance que toutes les classes seront empaquetées de la "manière par défaut" dans des bocaux. Total:
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); }
Hourra, lisez le tableau d'octets. Qu'allons-nous en faire ensuite? En principe, il existe plusieurs bibliothèques en Java pour la lecture / écriture de bytecode, mais ASM est généralement utilisé pour les travaux de bas niveau. Parce que il est affiné pour des performances élevées et un fonctionnement à la volée, l'API visiteur est la principale - asm lit séquentiellement la classe et tire les méthodes appropriées
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); } } ... }
L'utilisateur est invité à redéfinir les méthodes qui l'intéressent et à y écrire sa propre logique d'analyse / transformation. Par ailleurs, sur l'exemple de MethodVisitor
, je voudrais attirer l'attention sur le fait que tous les visiteurs ont une implémentation par défaut par délégation.
En plus de l'API principale, il existe également une API Tree prête à l'emploi. Si l'API Core est un analogue de l'analyseur SAX, alors l'API Tree est un analogue du DOM. Nous obtenons un objet à l'intérieur duquel toutes les informations sur la classe / méthode sont stockées et nous pouvons l'analyser comme nous voulons avec des sauts à n'importe quel endroit. En fait, cette API est une implémentation *Visitor
qui, à l'intérieur des méthodes visit*
, stocke simplement des informations. Presque toutes les méthodes y ressemblent:
public class MethodNode extends MethodVisitor { @Override public void visitJumpInsn(final int opcode, final Label label) { instructions.add(new JumpInsnNode(opcode, getLabelNode(label))); } ... }
Maintenant, nous pouvons enfin charger la méthode pour l'analyse.
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) {
Méthode de lecture complète du code.MethodNode
pas retourné directement, mais un wrapper avec une paire d'ext. champs parce que nous en aurons également besoin plus tard. Le point d'entrée (et la seule méthode publique) est 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; } } } }
Il est temps de faire l'analyse directement. Comment le faire? La première pensée est de regarder toutes les instructions getfield
. Chaque getfield
indique statiquement de quel champ il s'agit et de quelle classe. On peut considérer comme nécessaire tous les domaines de notre classe auxquels il y avait accès. Mais, malheureusement, cela ne fonctionne pas. Le premier problème ici est que l'excédent est capturé.
class Foo { private int bar; private int baz; public int test() { return bar + new Foo().baz; } }
Avec cet algorithme, nous considérons que le champ baz est nécessaire, bien qu'en fait non. Mais ce problème pourrait encore être marqué. Mais que faire des méthodes?
public class Client implements HasClientId { private Long id; public Long getId() { HasClientId obj = this; return obj.getClientId(); } @Override public Long getClientId() { return id; } }
Si nous recherchons des appels de méthode de la même manière que nous recherchons des champs de lecture, nous ne trouverons pas getClientId
. Car il n'y a pas d'appel à Client.getClientId
, mais seulement un appel à HasClientId.getClientId
. Bien sûr, vous pouvez considérer toutes les méthodes de la classe actuelle, toutes ses superclasses et toutes les interfaces à utiliser, mais c'est déjà trop. Ainsi, vous pouvez accidentellement capturer toString
, et il contient une liste de tous les champs en général.
De plus, nous voulons que les appels getter sur les objets imbriqués fonctionnent aussi
public class Account { private Client client; public long getClientId() { return client.getId(); } }
Et ici, l'appel à la méthode Client.getId
ne s'applique pas du tout à la classe Account
.
Avec un fort désir, vous pouvez toujours penser à des hacks pour des cas spéciaux pendant un certain temps, mais assez rapidement, il s'agit de comprendre que «les choses ne se font pas comme ça» et vous devez surveiller pleinement le flux d'exécution et le mouvement des données. getfield
, this
, - this
. Voici un exemple:
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
. Parce que , ( 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
. , , . Par exemple:
public long getBalance() { Account acc; if (account != null) acc = account; else acc = client.getDefaultAccount(); return acc.getBalance(); }
Account.getBalance()
. - . . ? merge
.
— SuperInterpreter extends Interpreter<SuperValue>
? . 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
. ? ! — . , , , . , 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))
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); } }
, . C'est parti!
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); }
. . , , , — , . , .
@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); }
. ""? ? Pas vraiment. . , .. . , 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 :
invokespecial
— . , , ( super.call()
).invokevirtual
— . . , .invokeinterface
— , invokevirtual
, — .invokestatic
—invokedynamic
— , 7 JSR 292. JVM, invokedynamic
( dynamic). , (+ ), . , Invokedynamic: ? .
, , , . invokedynamic
, . , , , (, ), invokedynamic
. , "" . , invokedynamic
, .
. , . , . , 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
.
Hourra! . Et ensuite? , ( , 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 .
TOUT!
.
, 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, .
Postface
- , . - partialGet
. , . , , , , " " .
, .