Nos esforzamos al máximo: desde ORM hasta análisis de bytecode

Como saben, un verdadero programador debe hacer 3 cosas en su vida: crear su propio lenguaje de programación, escribir su propio sistema operativo y crear su propio ORM. Y si escribí el idioma hace mucho tiempo (tal vez te lo cuente en otro momento), y el sistema operativo todavía está por delante, entonces quiero contarte sobre ORM en este momento. Para ser más precisos, ni siquiera se trata de ORM en sí, sino de la implementación de una característica pequeña, local y, como parecía, completamente simple.


Juntos iremos desde la alegría de encontrar una solución simple hasta la amargura de la conciencia de su fragilidad e incorrección. Desde el uso de una API exclusivamente pública hasta hacks sucios. De "casi sin reflexión" a "hasta las rodillas en el intérprete de código de bytes".


A quién le importa cómo analizar el código de bytes, qué dificultades tiene y qué resultado sorprendente puede obtener al final, bienvenido a cat.


Contenido


1 - Cómo comenzó todo.
2-4 - En camino a bytecode.
5 - ¿Quién es el bytecode?
6 - El análisis en sí. Fue por el bien de este capítulo que todo fue concebido y fue en sí mismo las entrañas.
7 - ¿Qué más se puede terminar? Sueños, sueños ...
Epílogo - Epílogo.


UPD: Inmediatamente después de la publicación, se perdieron las partes 6-8 (por lo que todo comenzó). Fijo



Primera parte El problema


Imagina que tenemos un esquema simple. Hay un cliente, tiene varias cuentas. Uno de ellos está en incumplimiento. Además, un cliente puede tener varias tarjetas SIM y cada tarjeta SIM se puede configurar explícitamente, o se puede usar un cliente predeterminado.



Así es como se describe este modelo en nuestro código (omitiendo 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; } 

En ORM, tenemos un requisito para la ausencia de proxys (debemos crear una instancia de esta clase en particular) y una sola solicitud. En consecuencia, esto es lo que sql se envía a la base de datos cuando se intenta obtener un mapa.


 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 El cliente y la factura están duplicados. Es cierto, si lo piensa, esto es comprensible: después de todo, el marco no sabe que el cliente de la tarjeta y el cliente de la cuenta de la tarjeta son el mismo cliente. Y la solicitud debe generarse estáticamente y solo una (¿recuerda la restricción sobre la unicidad de la solicitud?).


Por cierto, por exactamente la misma razón, aquí no Card.client.defaultAccount.client campos Card.account.client.defaultAccount y Card.client.defaultAccount.client . Solo sabemos que client y client siempre coinciden. Y el marco no lo sabe, para él este es un enlace arbitrario. Y qué hacer en tales casos no está muy claro. Sé 3 opciones:


  1. Describe explícitamente invariantes en anotaciones.
  2. Realiza consultas recursivas ( with recursive / connect by ).
  3. Para anotar.

Adivina qué opción elegimos? Derecho Como resultado, todos los campos recursivos no se rellenan en absoluto ahora y siempre hay un valor nulo.


Pero si observa de cerca, puede ver el segundo problema detrás de la duplicación, y es mucho peor. Que queriamos Número de tarjeta y saldo. Que recibiste 4 uniones y 10 columnas. ¡Y esto está creciendo exponencialmente! Bueno, es decir Realmente tenemos una situación en la que, primero, en aras de la belleza y la integridad, describimos completamente el modelo en anotaciones, y luego, en aras de 5 campos , se solicita una solicitud de 15 uniones y 150 columnas . Y en este momento se vuelve realmente aterrador.



Segunda parte Una solución funcional pero inconveniente


Una solución simple suplica de inmediato. ¡Solo se deben arrastrar los altavoces que se utilizarán! Fácil de decir La opción más obvia (para escribir la selección con las manos) la eliminaremos de inmediato. Bueno, no entonces, describimos el modelo para no usarlo. Hace mucho tiempo se creó un método especial: partialGet . A diferencia de get simple, acepta List<String> : los nombres de los campos que se deben completar. Para hacer esto, primero debe registrar los alias en las tablas


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

Y luego disfruta el resultado.


 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; 

Y todo parece estar bien, pero, de hecho, no. Así es como se usará en código real.


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

Y resulta que ahora puede usar parcialGet solo si la distancia entre él y el resultado es solo unas pocas líneas. Pero si el resultado llega lejos o, Dios no lo quiera, se pasa dentro de algún método, entonces ya es extremadamente difícil entender qué campos están llenos y cuáles no. Además, si NPE sucedió en algún lugar, aún debe comprender si realmente regresó de la base de datos nula o si simplemente no completamos este campo. En general, muy poco confiable.


Por supuesto, puede escribir otro objeto con su mapeo específicamente para la solicitud, o incluso seleccionar completamente todo con sus manos y ensamblarlo en una Tuple . En realidad, en realidad ahora en la mayoría de los lugares hacemos exactamente eso. Pero aún así me gustaría no escribir selecciones con mis manos, y no duplicar el mapeo.



Tercera parte Una solución conveniente pero inoperante.


Si piensas un poco más, entonces la respuesta viene a mi mente bastante rápido: necesitas usar interfaces. Entonces solo declara


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

Y usar


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

Y eso es todo. No llame a nada extra. Además, con la transición a Kotlin / ten / lomb, incluso este tipo terrible puede ser eliminado. Pero aquí todavía se omite el punto más importante. ¿Qué argumentos deben pasarse a partialGet ? Las tangas, como antes, ya no se sienten, porque el riesgo es demasiado grande para cometer errores y escribir los campos incorrectos. Y quiero que puedas de alguna manera


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

O incluso mejor en Kotlin a través de genéricos reificados


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

Ehh, un error. En realidad, toda la historia adicional es precisamente la implementación de esta opción.



Parte cuatro En camino a bytecode


El problema clave es que los métodos provienen de la interfaz y las anotaciones están por encima de los campos. Y necesitamos encontrar estos mismos campos por métodos. El primer y más obvio pensamiento es utilizar la convención estándar de Java Bean. Y para propiedades triviales, esto incluso funciona. Pero resulta muy inestable. Por ejemplo, vale la pena renombrar un método en una interfaz (a través de la refactorización ideológica), ya que todo se desmorona instantáneamente. La idea es lo suficientemente inteligente como para cambiar el nombre de los métodos en las clases de implementación, pero no lo suficiente como para comprender que fue un captador y que debe cambiar el nombre del campo. Y una solución similar conduce a la duplicación de campos. Por ejemplo, si necesito el método getClientId() en mi interfaz, entonces no puedo implementarlo de la única manera correcta


 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(); } } 

Y tengo que duplicar campos. Y en Client arrastre tanto id como clientId , y en el mapa al lado del cliente tiene explícitamente clientId . Y asegúrese de que todo esto no se vaya. Además, también quiero que los getters con lógica no trivial funcionen, por ejemplo


 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(); } } 

Por lo tanto, la opción de buscar por nombre ya no es necesaria, necesita algo más complicado.


La siguiente opción era completamente loca y no viví mucho tiempo en mi cabeza, pero por el bien de la historia también la describiré. En la etapa de análisis, podemos crear una entidad vacía y simplemente turnarnos para escribir algunos valores en los campos, y después de eso obtenemos los captadores y vemos que ha cambiado lo que devuelven o no. Entonces veremos que desde el registro en el campo de name , el valor de getClientId no cambia, pero desde la id del registro, cambia. Además, la situación cuando los captadores y los campos de diferentes tipos (como isActive() = i_active != 0 ) se admite automáticamente aquí. Pero hay al menos tres problemas graves (tal vez más, pero no lo pensé más).


  1. El requisito obvio para la esencia con este algoritmo es devolver el "mismo" valor del captador si el campo "correspondiente" no ha cambiado. "Uno y el mismo" - desde el punto de vista del operador de comparación que hemos elegido. == obviamente no puede ser (de lo contrario, algunos getAsInt() = Integer.parseInt(strField)) dejarán de funcionar getAsInt() = Integer.parseInt(strField)) . Sigue siendo igual. Entonces, si el getter devuelve algún tipo de entidad de usuario generada por los campos en cada llamada, entonces debe tener una anulación de equals .
  2. Mapeos de compresión. Como en el ejemplo con int -> boolean arriba. Si verificamos los valores 0 y 1, veremos un cambio. Pero si a los 40 y 42 años, ambas veces nos hacemos realidad.
  3. Puede haber convertidores complejos en captadores que dependen de ciertos invariantes en los campos (por ejemplo, un formato de cadena especial). Y en nuestros datos generados arrojarán excepciones.

Entonces, en general, la opción tampoco funciona.


En el proceso de discutir todo el asunto, inicialmente bromeé con la frase "bueno, nafig, es más fácil ver el código de bytes, todo está escrito allí". En ese momento, ni siquiera me di cuenta de que esta idea me tragaría y hasta dónde llegaría todo.



Quinta parte ¿Qué es el bytecode y cómo funciona?


new #4, dup, invokespecial #5, areturn
Si comprende lo que está escrito aquí y lo que hace este código, puede pasar a la siguiente parte.


Descargo de responsabilidad 1. Desafortunadamente, para comprender la historia adicional, necesita al menos una comprensión básica de cómo se ve el código de bytes de Java, por lo que escribiré un par de párrafos al respecto. De ninguna manera finge estar completo.


Descargo de responsabilidad 2. Será exclusivamente sobre el conjunto de métodos. Ni sobre el grupo constante, ni sobre la estructura de la clase en su conjunto, ni siquiera sobre las declaraciones de método en sí, diré una palabra.


Lo principal que debe comprender sobre el bytecode es el ensamblador de la máquina virtual de pila Java. Esto significa que los argumentos para las instrucciones se toman de la pila y los valores de retorno de las instrucciones se devuelven a la pila. Desde este punto de vista, podemos decir que el bytecode está escrito en notación polaca inversa . Además de la pila, el método también tiene una matriz de variables locales. Al ingresar al método, this y todos los argumentos de este método se escriben en él, y las variables locales se almacenan allí durante la ejecución. Aquí hay un ejemplo 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; } } 

Escribiré comentarios en el formato


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

Parte superior de la pila a la izquierda.


 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   ,       

Hay muchas instrucciones La lista completa debe verse en el sexto capítulo de JVMS , en Wikipedia hay un breve recuento . Una gran cantidad de instrucciones se duplican entre sí para diferentes tipos (por ejemplo, iload para int y lload durante mucho tiempo). Además, para trabajar con las primeras 4 variables locales, se resaltan sus instrucciones (por ejemplo, hay lload_1 y no acepta argumentos en absoluto, pero solo hay lload , tomará el número de la variable local como argumento. En el ejemplo anterior, hay una iload similar).


A nivel mundial, nos interesarán los siguientes grupos de instrucciones:


  1. *load* , *store* - lee / escribe una variable local
  2. *aload , *astore - lee / escribe un elemento de matriz por índice
  3. getfield , putfield - campo de lectura / escritura
  4. getstatic , putstatic - campo estático de lectura / escritura
  5. checkcast : checkcast entre tipos de objetos. Necesito porque los valores escritos se encuentran en la pila y en las variables locales. Por ejemplo, arriba estaba l2i para el elenco largo -> int.
  6. invoke* - llamada al método
  7. *return : devuelve el valor y sale del método


Sexta parte Inicio


Para aquellos que se perdieron una introducción tan larga, así como para distraerse del problema original y la razón en términos de la biblioteca, formulamos el problema con mayor precisión.


Es necesario, tener una instancia de java.lang.reflect.Method a mano, para obtener una lista de todos los campos no estáticos (actuales y todos los objetos anidados) cuyas lecturas (directa o transitivamente) estarán dentro de este método.

Por ejemplo, para tal método


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

Debe obtener una lista de dos campos: account.balance y client.defaultAccount.balance .


Escribiré, si es posible, una solución generalizada. Pero en un par de lugares tendrá que usar el conocimiento del problema original para resolver problemas irresolubles, en el caso general.


Primero debe obtener el código de bytes del cuerpo del método, pero no puede hacerlo directamente a través de Java. Pero desde Como el método existe originalmente dentro de alguna clase, es más fácil obtener la clase en sí. A nivel mundial, conozco dos opciones: entrar en el proceso de carga de clases e interceptar el byte[] ya leído byte[] allí, o simplemente encontrar el archivo ClassName.class en el disco y leerlo. No se puede interceptar la carga a nivel de la biblioteca habitual. Debe conectar javaagent o usar ClassLoader personalizado. En cualquier caso, se requieren pasos adicionales para configurar jvm / application, y esto es inconveniente. Puedes hacerlo más fácil. Todas las clases "ordinarias" están siempre en el mismo archivo con la extensión ".class", la ruta a la que se encuentra el paquete de la clase. Sí, no funcionará para encontrar clases agregadas dinámicamente o clases cargadas por algún cargador de clases personalizado, pero necesitamos esto para el modelo jdbc, por lo que podemos decir con confianza que todas las clases se empaquetarán en la "forma predeterminada" en frascos. 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); } 

Hurra, lea la matriz de bytes. ¿Qué haremos con él después? En principio, hay varias bibliotecas en Java para leer / escribir bytecode, pero ASM se usa generalmente para el trabajo de más bajo nivel. Porque está afilado para un alto rendimiento y operación sobre la marcha, la API de visitante es la principal allí: asm lee secuencialmente la clase y extrae los métodos adecuados


 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); } } ... } 

Se invita al usuario a redefinir los métodos que le interesan y escribir allí su propia lógica de análisis / transformación. Por separado, en el ejemplo de MethodVisitor , me gustaría llamar la atención sobre el hecho de que todos los visitantes tienen una implementación predeterminada a través de la delegación.


Además de la API principal, también hay una API de árbol lista para usar. Si la API Core es un análogo del analizador SAX, entonces la API Tree es un análogo del DOM. Obtenemos un objeto dentro del cual se almacena toda la información sobre la clase / método y podemos analizarlo como queramos con saltos a cualquier lugar. De hecho, esta API es una implementación de *Visitor que dentro de los métodos de visit* simplemente almacena información. Casi todos los métodos tienen este aspecto:


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

Ahora finalmente podemos cargar el método para el análisis.


 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; } } } 

Método de lectura de código completo.

MethodNode no se devuelve directamente, sino un contenedor con un par de ext. campos porque los necesitaremos más tarde también. El punto de entrada (y el único método público) es 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 hora de hacer el análisis directamente. ¿Cómo hacerlo? El primer pensamiento es mirar todas getfield instrucciones de getfield . Cada getfield dice estáticamente qué campo es y qué clase. Se puede considerar como necesario todos los campos de nuestra clase a los que había acceso. Pero, desafortunadamente, esto no funciona. El primer problema aquí es que se está capturando el exceso.


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

Con este algoritmo, consideramos que se necesita el campo baz, aunque, de hecho, no. Pero este problema aún podría puntuarse. ¿Pero qué hacer con los métodos?


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

Si buscamos llamadas a métodos de la misma manera que buscamos campos de lectura, entonces no encontraremos getClientId . Porque no hay una llamada a Client.getClientId , sino solo una llamada a HasClientId.getClientId . Por supuesto, puede considerar todos los métodos en la clase actual, todas sus superclases y todas las interfaces que se utilizarán, pero esto ya es demasiado. Por lo tanto, puede capturar accidentalmente toString , y en él hay una lista de todos los campos en general.


Además, queremos que las llamadas getter en objetos anidados también funcionen


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

Y aquí la llamada al método Client.getId no se aplica a la clase Account en absoluto.


Con un fuerte deseo, aún puede pensar en hacks para casos especiales durante algún tiempo, pero rápidamente se da cuenta de que "las cosas no se hacen así" y necesita monitorear completamente el flujo de ejecución y el movimiento de datos. getfield , this , - this . Aquí hay un ejemplo:


 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 . Porque , ( 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 . , , . Por ejemplo:


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

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


SuperInterpreter extends Interpreter<SuperValue> ? Derecho 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 . ? No! — . , , , . , 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); } } 

, . Vamos!


 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); } 

. ""? ? En realidad no . , .. . , 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 , .


. , . , . , 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! . Que sigue , ( , this ), ( , ) . ¡Y eso es todo!


  @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, .




Epílogo


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


, .

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


All Articles