Como você sabe, um programador real deve fazer três coisas em sua vida: criar sua própria linguagem de programação, escrever seu próprio sistema operacional e criar seu próprio ORM. E se eu escrevi o idioma há muito tempo (talvez eu lhe diga outra hora) e o sistema operacional ainda esteja à frente, quero falar sobre o ORM agora. Para ser mais preciso, não se trata nem da própria ORM, mas da implementação de um recurso pequeno, local e, ao que parece, completamente simples.
Juntos, percorreremos todo o caminho, desde a alegria de encontrar uma solução simples até a amargura da consciência de sua fragilidade e incorreção. Do uso de uma API exclusivamente pública a hacks sujos. De "quase sem reflexão" a "até os joelhos no interpretador de código de bytes".
Quem se importa com a análise do bytecode, com quais dificuldades ele está enfrentando e com que resultado incrível você pode obter no final das contas, bem-vindo ao gato.
Conteúdo
1 - Como tudo começou.
2-4 - No caminho para o bytecode.
5 - Quem é o bytecode.
6 - A própria análise. Foi por causa deste capítulo que tudo foi concebido e estava nele a própria coragem.
7 - O que mais pode ser terminado. Sonhos, sonhos ...
Posfácio - Posfácio.
UPD: Imediatamente após a publicação, as partes 6-8 foram perdidas (por uma questão de que tudo foi iniciado). Fixed.
Parte I O problema
Imagine que temos um esquema simples. Há um cliente, ele tem várias contas. Um deles é o padrão. Além disso, um cliente pode ter vários cartões SIM e cada cartão SIM pode ser definido explicitamente ou um cliente padrão pode ser usado.

Aqui está como este modelo é descrito em nosso código (omitindo getters / setters / construtores / ...).
@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; }
No próprio ORM, temos um requisito para a ausência de proxies (precisamos criar uma instância dessa classe específica) e uma única solicitação. Assim, aqui está o que o sql é enviado ao banco de dados ao tentar obter um 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;
Opa O cliente e a conta são duplicados. É verdade que, se você pensar bem, isso é compreensível - afinal, a estrutura não sabe que o cliente do cartão e o cliente da conta do cartão são o mesmo cliente. E a solicitação precisa ser gerada estaticamente e apenas uma (lembre-se da restrição à exclusividade da solicitação?).
A propósito, exatamente pelo mesmo motivo, não existem campos Card.client.defaultAccount.client
e Card.client.defaultAccount.client
aqui. Só sabemos que client
e client.defaultAccount.client
sempre correspondem. E a estrutura não sabe, para ele, esse é um elo arbitrário. E o que fazer nesses casos não é muito claro. Eu conheço 3 opções:
- Descreva explicitamente invariantes em anotações.
- Faça consultas recursivas (
with recursive
/ connect by
). - Para marcar.
Adivinhe qual opção escolhemos? Certo. Como resultado, todos os campos recursivos não são preenchidos agora e sempre há nulos.
Mas se você olhar de perto, poderá ver o segundo problema por trás da duplicação, e é muito pior. O que nós queríamos? Número do cartão e saldo. O que você recebeu? 4 junções e 10 colunas. E essa coisa está crescendo exponencialmente! Bem, isto é realmente temos uma situação em que, primeiro, por questões de beleza e integridade, descrevemos completamente o modelo em anotações e, em seguida, por 5 campos , uma solicitação de 15 junções e 150 colunas . E, neste momento, torna-se realmente assustador.
Parte Dois Uma solução funcional, mas inconveniente
Uma solução simples implora imediatamente. Somente os alto-falantes que serão usados devem ser arrastados! Fácil de dizer. A opção mais óbvia (escrever a seleção com as mãos), cairemos imediatamente. Bem, não então, descrevemos o modelo para não usá-lo. Há muito tempo, um método especial foi criado - o partialGet
. Diferentemente de get
simples, aceita List<String>
- os nomes dos campos a serem preenchidos. Para fazer isso, você deve primeiro registrar aliases nas tabelas
@JdbcJoinedObject(localColumn = "ACCOUNT", sqlTableAlias = "a") private Account account; @JdbcJoinedObject(localColumn = "CLIENT", sqlTableAlias = "c") private Client client;
E depois aproveite o 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;
E tudo parece estar bem, mas, de fato, não. Veja como será usado no código real.
Card card = cardDAO.partialGet(cardId, "msisdn", "c_a_balance", "a_balance"); ... ... ... ... ... ... long clientId = card.getClient().getId();
E agora você pode usar o parcialGet apenas se a distância entre ele e o resultado for de apenas algumas linhas. Mas se o resultado for longe ou, se Deus o permitir, for passado dentro de algum método, já é extremamente difícil entender quais campos são preenchidos e quais não são. Além disso, se o NPE aconteceu em algum lugar, você ainda precisa entender se ele realmente retornou do banco de dados nulo ou se simplesmente não preenchemos esse campo. Em suma, muito pouco confiável.
Você pode, é claro, apenas escrever outro objeto com seu mapeamento especificamente para a solicitação ou até selecionar completamente a coisa toda com as mãos e montá-la em alguma Tuple
. Na verdade, na realidade agora, na maioria dos lugares, fazemos exatamente isso. Mas ainda assim eu gostaria de não escrever seleções com as mãos e não duplicar o mapeamento.
Parte três. Uma solução conveniente, mas inoperante.
Se você pensa um pouco mais, logo a resposta vem à minha mente - você precisa usar interfaces. Então apenas declare
public interface MsisdnAndBalance { String getMsisdn(); long getBalance(); }
E use
MsisdnAndBalance card = cardDAO.partialGet(cardId, ...);
E isso é tudo. Não chame nada extra. Além disso, com a transição para Kotlin / ten / lomb, mesmo esse tipo terrível pode ser eliminado. Mas aqui o ponto mais importante ainda é omitido. Quais argumentos devem ser passados para o partialGet
? Tangas, como antes, não parecem mais, porque o risco é grande demais para cometer erros e escrever os campos errados. E eu quero que você seja capaz de alguma forma
MsisdnAndBalance card = cardDAO.partialGet(cardId, MsisdnAndBalance.class);
Ou ainda melhor no Kotlin através de genéricos reificados
val card = cardDAO.paritalGet<MsisdnAndBalance>(cardId)
Ehh, um erro. Na verdade, toda a história adicional é precisamente a implementação dessa opção.
Parte Quatro No caminho para o bytecode
O principal problema é que os métodos vêm da interface e as anotações estão acima dos campos. E precisamos encontrar esses mesmos campos por métodos. O primeiro e mais óbvio pensamento é usar a convenção padrão do Java Bean. E para propriedades triviais, isso até funciona. Mas acontece muito instável. Por exemplo, vale a pena renomear um método em uma interface (através da refatoração ideológica), pois tudo instantaneamente desmorona. A idéia é inteligente o suficiente para renomear métodos nas classes de implementação, mas não o suficiente para entender que era um getter e você precisa renomear o próprio campo. E uma solução semelhante leva à duplicação de campos. Por exemplo, se eu precisar do getClientId()
na minha interface, não será possível implementá-lo da única maneira correta
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(); } }
E eu tenho que duplicar campos. E no Client
arraste id
e clientId
, e no mapa ao lado do cliente explicitamente clientId
. E certifique-se de que tudo isso não saia. Além disso, também quero que getters com lógica não trivial funcionem, por exemplo
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(); } }
Portanto, a opção de pesquisar por nome não é mais necessária, você precisa de algo mais complicado.
A próxima opção foi completamente insana e não me deixou muito tempo na cabeça, mas, para completar a história, também a descreverei. No estágio de análise, podemos criar uma entidade vazia e simplesmente revezar na escrita de alguns valores nos campos; depois disso, obtemos os getters e observamos que ele mudou o que eles retornam ou não. Portanto, veremos que, a partir do registro no campo de name
, o valor de getClientId
não muda, mas a partir da id
do registro - ele muda. Além disso, a situação em que getters e campos de diferentes tipos (como isActive() = i_active != 0
) são automaticamente suportados aqui. Mas há pelo menos três problemas sérios (talvez mais, mas não pensei mais).
- O requisito óbvio para a essência desse algoritmo é retornar o valor "mesmo" do getter se o campo "correspondente" não tiver sido alterado. "Um e o mesmo" - do ponto de vista do operador de comparação que escolhemos.
==
obviamente não pode ser (caso contrário, getAsInt() = Integer.parseInt(strField))
deixará de funcionar getAsInt() = Integer.parseInt(strField))
. Permanece igual. Portanto, se o getter retornar algum tipo de entidade do usuário gerada pelos campos em cada chamada, ele deverá ter uma substituição de equals
. - Mapeamentos de compactação. Como no exemplo com
int -> boolean
acima. Se verificarmos os valores 0 e 1, veremos uma mudança. Mas se aos 40 e 42, ambas as vezes nos tornamos realidade. - Pode haver conversores complexos em getters que dependem de certos invariantes nos campos (por exemplo, um formato de string especial). E em nossos dados gerados eles lançam exceções.
Portanto, em geral, a opção também não está funcionando.
No processo de discutir a coisa toda, eu jocosamente pronunciei a frase "bem, nafig, é mais fácil ver o bytecode, tudo está escrito lá". Naquela época, eu nem percebi que essa idéia me engoliria e até onde tudo iria.
Parte Cinco O que é bytecode e como funciona
new #4, dup, invokespecial #5, areturn
Se você entender o que está escrito aqui e o que esse código faz, poderá pular para a próxima parte.
Isenção de responsabilidade 1. Infelizmente, para entender a história adicional, você precisa de pelo menos um entendimento básico da aparência do bytecode Java, então escreverei alguns parágrafos sobre isso. De forma alguma finja estar completo.
Isenção 2. Será exclusivamente sobre o corpo dos métodos. Nem sobre o pool constante, nem sobre a estrutura da classe como um todo, nem sobre as próprias declarações do método, direi uma palavra.
A principal coisa que você precisa entender sobre o bytecode é o montador da máquina virtual de pilha Java. Isso significa que os argumentos para as instruções são retirados da pilha e os valores de retorno das instruções são empurrados de volta para a pilha. Deste ponto de vista, podemos dizer que o bytecode está escrito em notação polonesa reversa . Além da pilha, o método também possui uma matriz de variáveis locais. Ao inserir o método, this
e todos os argumentos deste método são gravados nele e as variáveis locais são armazenadas lá durante a execução. Aqui está um exemplo simples.
public class Foo { private int bar; public int updateAndReturn(long baz, String str) { int result = (int) baz; result += str.length(); bar = result; return result; } }
Vou escrever comentários no formato
# [(<local_variable_index>:<actual_value>)*], [(<value_on_stack>)*]
Topo da pilha à esquerda.
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 ,
Há muitas instruções. A lista completa precisa ser consultada no sexto capítulo da JVMS , na Wikipedia há uma breve recontagem . Um grande número de instruções se duplicam para tipos diferentes (por exemplo, iload
para int e lload
por muito tempo). Além disso, para trabalhar com as 4 primeiras variáveis locais, suas instruções são destacadas (por exemplo, existe lload_1
e não leva argumentos, mas apenas lload
, o número da variável local será lload
como argumento. No exemplo acima, há um iload
semelhante).
Globalmente, estaremos interessados nos seguintes grupos de instruções:
*load*
, *store*
- lê / grava uma variável local*aload
, *astore
- lê / escreve um elemento da matriz por índicegetfield
, putfield
- campo de leitura / gravaçãogetstatic
, putstatic
- campo estático de leitura / gravaçãocheckcast
- convertido entre tipos de objetos. Precisa porque valores digitados estão na pilha e em variáveis locais. Por exemplo, acima foi l2i para o elenco long -> int.invoke*
- chamada de método*return
- retorna o valor e sai do método
Parte Seis Página inicial
Para aqueles que perderam uma introdução tão longa, assim como para se distrair do problema original e da razão em termos da biblioteca, formulamos o problema com mais precisão.
É necessário ter uma instância java.lang.reflect.Method
disponível para obter uma lista de todos os campos não estáticos (objetos atuais e todos os objetos aninhados) cujas leituras (direta ou transitivamente) estarão dentro desse método.
Por exemplo, para esse método
public long getBalance() { Account acc; if (account != null) acc = account; else acc = client.getDefaultAccount(); return acc.getBalance(); }
Você precisa obter uma lista de dois campos: account.balance
e client.defaultAccount.balance
.
Escreverei, se possível, uma solução generalizada. Mas em alguns lugares você terá que usar o conhecimento do problema original para resolver problemas insolúveis, no caso geral.
Primeiro, você precisa obter o bytecode do corpo do método, mas não pode fazer isso diretamente através do Java. Mas desde Como o método existe originalmente em alguma classe, é mais fácil obter a própria classe. Globalmente, conheço duas opções: entrar no processo de carregamento de classe e interceptar o byte[]
já lido byte[]
lá, ou simplesmente encontrar o arquivo ClassName.class
no disco e lê-lo. A interceptação de carregamento no nível da biblioteca usual não pode ser feita. Você precisa conectar o javaagent ou usar o ClassLoader personalizado. Em qualquer caso, são necessárias etapas adicionais para configurar o jvm / application, e isso é inconveniente. Você pode fazer isso mais fácil. Todas as classes "comuns" estão sempre no mesmo arquivo com a extensão ".class", cujo caminho é o pacote de classes. Sim, não funcionará para encontrar classes adicionadas dinamicamente ou carregadas por algum carregador de classes personalizado, mas precisamos disso para o modelo jdbc, para que possamos dizer com confiança que todas as classes serão empacotadas da "maneira padrão" em jars. 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); }
Viva, leia a matriz de bytes. O que faremos com isso a seguir? Em princípio, existem várias bibliotecas em Java para leitura / gravação de código de código, mas o ASM é geralmente usado para o trabalho de nível mais baixo. Porque ela é aprimorada para alto desempenho e operação imediata, a API do visitante é a principal - a ASM lê sequencialmente a classe e usa os métodos apropriados
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); } } ... }
O usuário é convidado a redefinir os métodos de interesse para ele e a escrever sua própria lógica de análise / transformação. Separadamente, no exemplo do MethodVisitor
, gostaria de chamar a atenção para o fato de que todos os visitantes têm uma implementação padrão por meio da delegação.
Além da API principal, também há uma API de árvore pronta para uso. Se a API principal for analógica do analisador SAX, a API da árvore será analógica do DOM. Nós temos um objeto dentro do qual todas as informações sobre a classe / método são armazenadas e podemos analisá-lo como quisermos com saltos para qualquer lugar. De fato, essa API é uma implementação *Visitor
que, dentro dos métodos visit*
, simplesmente armazena informações. Quase todos os métodos são parecidos com este:
public class MethodNode extends MethodVisitor { @Override public void visitJumpInsn(final int opcode, final Label label) { instructions.add(new JumpInsnNode(opcode, getLabelNode(label))); } ... }
Agora, finalmente, podemos carregar o método para análise.
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étodo de leitura de código completo.MethodNode
não MethodNode
retornado diretamente, mas um wrapper com um par de ext. campos porque também precisaremos deles mais tarde. O ponto de entrada (e o único método público) é 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; } } } }
É hora de fazer a análise diretamente. Como fazer isso? O primeiro pensamento é examinar todas getfield
instruções getfield
. Cada getfield
diz estaticamente qual é o campo e qual classe. Pode ser considerado necessário todos os campos de nossa classe aos quais houve acesso. Mas, infelizmente, isso não funciona. O primeiro problema aqui é que o excesso está sendo capturado.
class Foo { private int bar; private int baz; public int test() { return bar + new Foo().baz; } }
Com esse algoritmo, consideramos que o campo baz é necessário, embora, de fato, não. Mas esse problema ainda pode ser marcado. Mas o que fazer com os 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; } }
Se procurarmos por chamadas de método da mesma maneira que procuramos por campos de leitura, não encontraremos getClientId
. Pois não há chamada para Client.getClientId
, mas apenas uma chamada para HasClientId.getClientId
. Obviamente, você pode considerar todos os métodos da classe atual, todas as suas superclasses e todas as interfaces a serem usadas, mas isso já é demais. Assim, você pode capturar acidentalmente o toString
, e nele está uma lista de todos os campos em geral.
Além disso, queremos que as chamadas getter em objetos aninhados funcionem também
public class Account { private Client client; public long getClientId() { return client.getId(); } }
E aqui a chamada para o método Client.getId
não se aplica à classe Account
.
Com um forte desejo, você ainda pode pensar em hacks para casos especiais por algum tempo, mas rapidamente entende-se que "as coisas não são feitas dessa maneira" e você precisa monitorar completamente o fluxo de execução e movimentação de dados. getfield
, this
, - this
. Aqui está um exemplo:
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 exemplo:
public long getBalance() { Account acc; if (account != null) acc = account; else acc = client.getDefaultAccount(); return acc.getBalance(); }
Account.getBalance()
. - . . ? merge
.
— SuperInterpreter extends Interpreter<SuperValue>
? Certo. 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
. ? Não! — . , , , . , 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); } }
, . Vamos lá!
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); }
. ""? ? Na verdade não. . , .. . , 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
.
Viva! . O que vem a seguir? , ( , 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, .
Posfácio
- , . - partialGet
. , . , , , , " " .
, .