如您所知,一个真正的程序员在生活中应该做三件事:创建自己的编程语言,编写自己的操作系统以及编写自己的ORM。 而且,如果我很久以前写过这种语言(也许我会再告诉你),而操作系统仍在继续,那么我现在想向您介绍ORM。 更确切地说,它甚至不关ORM本身,而是关乎一个小的,本地的以及看似完全简单的功能的实现。
我们将共同努力,从找到简单解决方案的喜悦,到意识到其脆弱性和不正确性的痛苦。 从使用专有的公共API到肮脏的黑客。 从“几乎没有反射”到“深入了解字节码解释器”。
谁在乎如何分析字节码,它遇到了什么困难以及最终可以得到什么惊人的结果,欢迎您的注意。
目录内容
1-一切如何开始。
2-4-在字节码的路上。
5-谁是字节码。
6-分析本身。 出于本章的考虑,一切都被构思出来,而其中正是胆量。
7-还有什么可以完成的。 梦想,梦想...
后记 -后记。
UPD:发布后立即丢失了第6-8部分(因此开始了所有工作)。 固定的。
第一部分 问题
想象我们有一个简单的方案。 有一个客户,他有几个帐户。 其中之一是默认设置。 此外,一个客户端可以具有多个SIM卡,并且可以显式设置每个SIM卡,或者可以使用默认客户端。

这是在我们的代码中描述此模型的方式(省略getters / setters /构造函数/ ...)。
@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; }
在ORM本身中,我们要求不存在代理(必须创建此特定类的实例)和单个请求。 因此,这是试图获取映射时将sql发送到数据库的内容。
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;
哎呀 客户和帐单重复。 没错,如果您考虑一下,这是可以理解的-毕竟,框架不知道卡客户端和卡帐户客户端是同一客户端。 而且,该请求仅需要静态生成,并且只能静态生成(请记住对请求唯一性的限制吗?)。
顺便说一句,出于完全相同的原因,这里根本没有Card.account.client.defaultAccount
和Card.client.defaultAccount.client
字段。 只有我们知道client
和client.defaultAccount.client
总是匹配。 而且框架不知道,对他来说这是一个任意链接。 在这种情况下该如何做还不清楚。 我知道3种选择:
- 在注解中明确描述不变式。
- 进行递归查询(
with recursive
/ connect by
)。 - 得分。
猜猜我们选择了哪个选项? 对啊 结果,所有递归字段现在都没有被填充,并且始终为空。
但是,如果仔细观察,您会发现复制背后的第二个问题,而且情况更糟。 我们想要什么? 卡号和余额。 你得到了什么? 4个连接和10个列。 而且这件事呈指数增长! 好吧 实际上,我们的情况是,首先,为了美观和完整性,我们在注释上充分描述了模型,然后, 为了5个字段 ,请求了15个连接和150列的请求。 而在这一刻,它变得非常可怕。
第二部分 一个可行但不方便的解决方案
一个简单的解决方案立即产生了作用。 仅将要使用的扬声器拖动! 说起来容易。 最明显的选择(用手写下选区)我们将立即掉落。 好吧,不是那时,我们描述了该模型以免使用它。 很久以前,就有了一种特殊的方法partialGet
。 与简单的get
不同,它接受List<String>
-要填充的字段的名称。 为此,您必须首先在表中注册别名
@JdbcJoinedObject(localColumn = "ACCOUNT", sqlTableAlias = "a") private Account account; @JdbcJoinedObject(localColumn = "CLIENT", sqlTableAlias = "c") private Client client;
然后享受结果。
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;
一切似乎都很好,但实际上不是。 这是在实际代码中的用法。
Card card = cardDAO.partialGet(cardId, "msisdn", "c_a_balance", "a_balance"); ... ... ... ... ... ... long clientId = card.getClient().getId();
事实证明,现在只有在partialGet和使用结果之间的距离只有几行的情况下,才可以使用partialGet。 但是,如果结果走得太远,或者,上帝禁止,是通过某种方法传递的,那么已经很难理解哪些字段已填充,哪些字段未填充。 而且,如果NPE发生在某个地方,那么您仍然需要了解它是否真正从空数据库返回,或者我们是否未填写此字段。 总而言之,非常不可靠。
当然,您可以使用映射专门针对请求编写另一个对象,甚至可以用手完全选择整个对象并将其组装成一些Tuple
。 实际上,实际上在大多数地方,我们只是这样做。 但是我还是不想用手书写选择内容,也不要重复映射。
第三部分。 一个方便但无效的解决方案。
如果您再想一想,那么很快就会想到答案-您需要使用接口。 然后声明
public interface MsisdnAndBalance { String getMsisdn(); long getBalance(); }
并使用
MsisdnAndBalance card = cardDAO.partialGet(cardId, ...);
仅此而已。 请勿多呼。 而且,随着过渡到Kotlin /十/龙,甚至可以消除这种可怕的类型。 但是这里最重要的一点仍然被省略。 什么参数应该传递给partialGet
? 像以前一样,丁字裤不再有感觉,因为冒着犯错误和写错字段的风险太大。 我希望你能够以某种方式
MsisdnAndBalance card = cardDAO.partialGet(cardId, MsisdnAndBalance.class);
甚至通过改良的泛型在Kotlin上更好
val card = cardDAO.paritalGet<MsisdnAndBalance>(cardId)
嗯,是个大错。 实际上,整个故事还只是此选项的实现。
第四部分 在字节码的路上
关键问题在于方法来自接口,并且注释位于字段上方。 我们需要通过方法找到这些相同的字段。 第一个也是最明显的想法是使用标准Java Bean约定。 对于微不足道的属性,这甚至可以工作。 但是事实证明非常不稳定。 例如,值得一提的是在界面中重命名方法(通过意识形态重构),因为一切都会立即崩溃。 这个想法足够聪明,可以重命名实现类中的方法,但不足以了解它是一个吸气剂,您需要重命名字段本身。 类似的解决方案导致字段重复。 例如,如果我在界面中需要getClientId()
方法,则无法以唯一正确的方式实现它
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(); } }
而且我必须重复字段。 然后在Client
拖动id
和clientId
,并在客户端旁边的地图中显式包含clientId
。 并确保所有这些都不会消失。 而且,例如,我还希望具有非平凡逻辑的吸气剂起作用
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(); } }
因此,不再需要使用按名称搜索的选项,您需要一些更棘手的事情。
下一个选择是完全疯了,并没有在我的脑海中长寿,但为了完整起见,我还将对其进行描述。 在解析阶段,我们可以创建一个空实体,然后简单地轮流在字段中写入一些值,然后我们获取吸气剂并查看其是否改变了它们返回的内容。 因此,我们将从name
字段中的记录中看到, getClientId
的值不会更改,但是从记录id
更改。 而且,这里自动支持不同类型的getter和字段(例如isActive() = i_active != 0
)的情况。 但是至少存在三个严重的问题(也许更多,但我没有进一步考虑)。
- 此算法本质的明显要求是,如果“相应”字段未更改,则从吸气剂返回“相同”值。 “相同” –从我们选择的比较运算符的角度来看。
==
显然不能(否则某些getAsInt() = Integer.parseInt(strField))
将停止工作getAsInt() = Integer.parseInt(strField))
。 保持相等。 因此,如果getter在每次调用时返回由字段生成的某种用户实体,则它必须具有equals
的覆盖。 - 压缩映射。 如上面的
int -> boolean
的示例。 如果我们检查值0和1,那么我们将看到一个变化。 但是,如果分别是40岁和42岁,那么我们两次都是正确的。 - getter中可能存在复杂的转换器,这些转换器依赖于字段中的某些不变量(例如,特殊的字符串格式)。 并且在我们生成的数据上,它们将引发异常。
因此,一般而言,该选项也不起作用。
在讨论整件事的过程中,我最初开玩笑地说了一个短语“好吧,nafig,更容易看到字节码,所有内容都写在这里”。 在那个时候,我什至没有意识到这个想法会吞噬我,一切都会走多远。
第五部分 什么是字节码,它如何工作
new #4, dup, invokespecial #5, areturn
如果您了解这里写的内容以及该代码的作用,那么可以跳到下一部分。
免责声明1.不幸的是,要了解进一步的情况,您至少需要对Java字节码的外观有基本的了解,因此我将写几段有关它的内容。 绝不假装是完整的。
免责声明2.它仅涉及方法主体。 我既不说常量池,也不涉及整个类的结构,甚至不涉及方法声明本身。
关于字节码,您需要了解的主要内容是Java堆栈虚拟机的汇编程序。 这意味着指令的参数是从堆栈中提取的,而指令的返回值则被推回堆栈中。 从这个角度来看,我们可以说字节码是用波兰语相反的符号表示的 。 除了堆栈之外,该方法还具有一组局部变量。 进入方法后, this
方法和该方法的所有参数将写入其中,并且在执行期间将本地变量存储在该方法中。 这是一个简单的例子。
public class Foo { private int bar; public int updateAndReturn(long baz, String str) { int result = (int) baz; result += str.length(); bar = result; return result; } }
我将以以下格式写评论
# [(<local_variable_index>:<actual_value>)*], [(<value_on_stack>)*]
堆栈顶部在左侧。
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 ,
有很多说明。 完整列表需要在JVMS的第六章中找到 ,在Wikipedia上有一个简短的复述 。 大量指令针对不同类型彼此重复(例如, lload
用于int, lload
用于长时间)。 另外,对于使用前4个局部变量的操作,突出显示了它们的指令(例如,有lload_1
并且它根本不接受参数,但是只有lload
,它将局部变量的编号作为参数。在上面的示例中,有一个类似的iload
)。
在全球范围内,我们将对以下说明组感兴趣:
*load*
, *store*
-读/写局部变量*aload
, *astore
按索引读取/写入数组元素getfield
, putfield
读/写字段getstatic
, putstatic
读取/写入静态字段checkcast
在对象类型之间进行转换。 需要,因为 输入的值位于堆栈和局部变量中。 例如,上面的参数为l2i,用于强制转换long-> int。invoke*
-方法调用*return
返回值并退出方法
第六部分 首页
对于那些错过这么长时间介绍的人,以及为了从图书馆的角度分散最初的问题和原因,我们更精确地提出了问题。
必须要有一个java.lang.reflect.Method
实例,以获取所有非静态字段(包括当前对象和所有嵌套对象)的列表,这些字段的读数(直接或传递)将位于此方法内。
例如,对于这种方法
public long getBalance() { Account acc; if (account != null) acc = account; else acc = client.getDefaultAccount(); return acc.getBalance(); }
您需要获取两个字段的列表: account.balance
和client.defaultAccount.balance
。
如果可能的话,我将写一个广义的解决方案。 但是在很多地方,您将不得不使用原始问题的知识来解决通常无法解决的问题。
首先,您需要获取方法主体本身的字节码,但是您不能直接通过Java来做到这一点。 但是因为 由于该方法最初存在于某个类中,因此更容易获取该类本身。 在全球范围内,我知道两个选择:进入类加载过程并在其中拦截已读取的byte[]
,或者只是在磁盘上找到ClassName.class
文件并读取它。 无法拦截常规库级别的加载。 您需要连接javaagent或使用自定义ClassLoader。 无论如何,都需要其他步骤来配置jvm /应用程序,这很不方便。 您可以轻松完成。 所有“普通”类始终与扩展名“ .class”在同一个文件中,该路径是类包。 是的,无法找到动态添加的类或由某些自定义类加载器加载的类,但是对于jdbc模型我们需要这样做,因此我们可以放心地说所有类都将以“默认方式”打包在jar中。 总计:
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); }
万岁,读取字节数组。 接下来我们将如何处理? 原则上,Java中有几个用于读取/写入字节码的库,但是ASM通常用于最底层的工作。 因为 它经过改进,可实现高性能和即时操作,其中的主要访问者API是-asm顺序读取该类并提取适当的方法
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); } } ... }
邀请用户重新定义他感兴趣的方法,并在那里编写自己的分析/转换逻辑。 另外,在MethodVisitor
的示例中,我想提请注意一个事实,即所有访问者都有通过委派实现的默认实现。
除了主要的api外,还有一个开箱即用的Tree API。 如果Core API是SAX解析器的类似物,则Tree API是DOM的类似物。 我们得到一个对象,该对象中存储了有关类/方法的所有信息,并可以根据需要进行分析,并跳转到任何地方。 实际上,此api是一个*Visitor
实现,它在visit*
方法内部仅存储信息。 几乎所有的方法如下所示:
public class MethodNode extends MethodVisitor { @Override public void visitJumpInsn(final int opcode, final Label label) { instructions.add(new JumpInsnNode(opcode, getLabelNode(label))); } ... }
现在,我们终于可以加载该方法进行分析了。
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) {
完整的代码读取方法。MethodNode
不是直接返回的,而是带有一对ext的包装器。 字段,因为 我们稍后也会需要它们。 入口点(也是唯一的公共方法)是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; } } } }
是时候直接进行分析了。 怎么做? 首先想到的是查看所有getfield
指令。 每个getfield
静态地说明它是哪个字段以及哪个类。 可以认为有必要访问我们班的所有字段。 但是,不幸的是,这不起作用。 这里的第一个问题是多余的被捕获。
class Foo { private int bar; private int baz; public int test() { return bar + new Foo().baz; } }
使用这种算法,我们认为需要baz字段,尽管实际上不需要。 但是这个问题仍然可以解决。 但是该方法怎么办?
public class Client implements HasClientId { private Long id; public Long getId() { HasClientId obj = this; return obj.getClientId(); } @Override public Long getClientId() { return id; } }
如果我们以与查找字段相同的方式查找方法调用,那么将找不到getClientId
。 因为没有对Client.getClientId
的调用,只有对HasClientId.getClientId
的调用。 当然,您可以考虑当前类上的所有方法,其所有超类和要使用的所有接口,但这已经太多了。 因此,您可能会意外捕获toString
,并且其中通常是所有字段的列表。
而且,我们也希望嵌套对象的getter调用也能正常工作
public class Account { private Client client; public long getClientId() { return client.getId(); } }
在这里,对Client.getId
方法的调用根本不适用于Account
类。
出于强烈的愿望,您仍然可以在一段时间内考虑特殊情况下的骇客攻击,但是很快就会了解到“事情并非如此”,您需要全面监视执行和数据移动的流程。 getfield
, this
, - this
. :
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
. 因为 , ( 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
. , , . 例如:
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); } }
, . 走吧
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); }
. ""? ? 不完全是 . , .. . , 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
.
万岁! . 接下来是什么? , ( , 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, .
后记
- , . - partialGet
. , . , , , , " " .
, .