I'm trying to investigate whether dynamic weaving is being applied to my classes.
First, here's some context…
It is very easy to prove whether static weaving is occurring. You use javap -c MyCoolEntity
to see the disassembly of some Java class MyCoolEntity.class
.
In this example: MyCoolEntity
is a class describing a JPA entity. It is possible to use a plugin in our build process to "weave" its bytecode.
A getter which represents a JPA entity relationship will — in non-weaved bytecode — look like a simple field lookup:
public java.util.List<com.stackoverflow.Friend> getFriends();
Code:
0: aload_0
1: getfield #222 // Field friends:Ljava/util/List;
4: areturn
But weaved bytecode delegates the call to a virtual method, _persistence_get_*()
:
public java.util.List<com.stackoverflow.Friend> getFriends();
Code:
0: aload_0
1: invokevirtual #640 // Method _persistence_get_friends:()Ljava/util/List;
4: areturn
This weaved bytecode can be used to achieve a lazy-lookup (fetch the related entity on-demand from the database).
I expect that I can apply the same logic to ascertain whether dynamic weaving is occurring in my application.
My understanding is this: a Java agent transforms the class bytecode at runtime.
Is there a way I can access the .class
file that my JPA implementation's Java agent produces at run-time? Otherwise: is there a way that I can connect to the running JVM and perform a disassembly on the class that it holds in-memory?
Disclaimer: my question is primarily concerned with "how do you grab & disassemble a .class
file that is generated at runtime". There are likely other ways to answer the sub-question "is dynamic weaving occurring?", but that part is not the primary focus of my question. :)
For what it's worth: I am using EclipseLink 2.5.1 and TomCat 8.5.
You can use Apache Commons BCEL and do something like this:
package de.scrum_master.stackoverflow;
import java.io.IOException;
import java.io.InputStream;
import org.apache.bcel.classfile.ClassParser;
import org.apache.bcel.classfile.JavaClass;
import org.apache.bcel.classfile.Method;
public class MethodDisassembler {
public static void main(String[] args) throws IOException {
Class<?> clazz = MethodDisassembler.class;
String classAsPath = clazz.getName().replace('.', '/') + ".class";
try (InputStream classStream = clazz.getClassLoader().getResourceAsStream(classAsPath)) {
ClassParser classParser = new ClassParser(classStream, classAsPath);
JavaClass javaClass = classParser.parse();
for (Method method : javaClass.getMethods())
System.out.println(method.getCode());
}
}
public void doSomething() {
sendEmail();
}
public void sendEmail() {
System.out.println("Sending e-mail");
}
}
Console log:
Code(max_stack = 1, max_locals = 1, code_length = 5)
0: aload_0
1: invokespecial java.lang.Object.<init>:()V (8)
4: return
Attribute(s) =
LineNumber(0, 10)
LocalVariable(start_pc = 0, length = 5, index = 0:de.scrum_master.stackoverflow.MethodDisassembler this)
Code(max_stack = 5, max_locals = 12, code_length = 165)
0: ldc de.scrum_master.stackoverflow.MethodDisassembler (1)
2: astore_1
3: new <java.lang.StringBuilder> (19)
6: dup
7: aload_1
8: invokevirtual java.lang.Class.getName:()Ljava/lang/String; (21)
11: bipush 46
13: bipush 47
15: invokevirtual java.lang.String.replace:(CC)Ljava/lang/String; (27)
18: invokestatic java.lang.String.valueOf:(Ljava/lang/Object;)Ljava/lang/String; (33)
21: invokespecial java.lang.StringBuilder.<init>:(Ljava/lang/String;)V (37)
24: ldc ".class" (40)
26: invokevirtual java.lang.StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; (42)
29: invokevirtual java.lang.StringBuilder.toString:()Ljava/lang/String; (46)
32: astore_2
33: aconst_null
34: astore_3
35: aconst_null
36: astore %4
38: aload_1
39: invokevirtual java.lang.Class.getClassLoader:()Ljava/lang/ClassLoader; (49)
42: aload_2
43: invokevirtual java.lang.ClassLoader.getResourceAsStream:(Ljava/lang/String;)Ljava/io/InputStream; (53)
46: astore %5
48: new <org.apache.bcel.classfile.ClassParser> (59)
51: dup
52: aload %5
54: aload_2
55: invokespecial org.apache.bcel.classfile.ClassParser.<init>:(Ljava/io/InputStream;Ljava/lang/String;)V (61)
58: astore %6
60: aload %6
62: invokevirtual org.apache.bcel.classfile.ClassParser.parse:()Lorg/apache/bcel/classfile/JavaClass; (64)
65: astore %7
67: aload %7
69: invokevirtual org.apache.bcel.classfile.JavaClass.getMethods:()[Lorg/apache/bcel/classfile/Method; (68)
72: dup
73: astore %11
75: arraylength
76: istore %10
78: iconst_0
79: istore %9
81: goto #105
84: aload %11
86: iload %9
88: aaload
89: astore %8
91: getstatic java.lang.System.out:Ljava/io/PrintStream; (74)
94: aload %8
96: invokevirtual org.apache.bcel.classfile.Method.getCode:()Lorg/apache/bcel/classfile/Code; (80)
99: invokevirtual java.io.PrintStream.println:(Ljava/lang/Object;)V (86)
102: iinc %9 1
105: iload %9
107: iload %10
109: if_icmplt #84
112: aload %5
114: ifnull #164
117: aload %5
119: invokevirtual java.io.InputStream.close:()V (92)
122: goto #164
125: astore_3
126: aload %5
128: ifnull #136
131: aload %5
133: invokevirtual java.io.InputStream.close:()V (92)
136: aload_3
137: athrow
138: astore %4
140: aload_3
141: ifnonnull #150
144: aload %4
146: astore_3
147: goto #162
150: aload_3
151: aload %4
153: if_acmpeq #162
156: aload_3
157: aload %4
159: invokevirtual java.lang.Throwable.addSuppressed:(Ljava/lang/Throwable;)V (97)
162: aload_3
163: athrow
164: return
Exception handler(s) =
From To Handler Type
48 112 125 <Any exception>(0)
38 138 138 <Any exception>(0)
Attribute(s) =
LineNumber(0, 12), LineNumber(3, 13), LineNumber(33, 14), LineNumber(38, 14),
LineNumber(48, 15), LineNumber(60, 16), LineNumber(67, 17), LineNumber(91, 18),
LineNumber(102, 17), LineNumber(112, 19), LineNumber(164, 20)
LocalVariable(start_pc = 0, length = 165, index = 0:java.lang.String[] args)
LocalVariable(start_pc = 3, length = 162, index = 1:java.lang.Class clazz)
LocalVariable(start_pc = 33, length = 132, index = 2:java.lang.String classAsPath)
LocalVariable(start_pc = 48, length = 88, index = 5:java.io.InputStream classStream)
LocalVariable(start_pc = 60, length = 52, index = 6:org.apache.bcel.classfile.ClassParser classParser)
LocalVariable(start_pc = 67, length = 45, index = 7:org.apache.bcel.classfile.JavaClass javaClass)
LocalVariable(start_pc = 91, length = 11, index = 8:org.apache.bcel.classfile.Method method)
LocalVariableTypes(start_pc = 3, length = 162, index = 1:java.lang.Class<?>... clazz)
StackMap((FULL, offset delta=84, locals={(type=Object, class=[Ljava.lang.String;), (type=Object, class=java.lang.Class), (type=Object, class=java.lang.String), (type=Object, class=java.lang.Throwable), (type=Object, class=java.lang.Throwable), (type=Object, class=java.io.InputStream), (type=Object, class=org.apache.bcel.classfile.ClassParser), (type=Object, class=org.apache.bcel.classfile.JavaClass), (type=Bogus), (type=Integer), (type=Integer), (type=Object, class=[Lorg.apache.bcel.classfile.Method;)}), (SAME, offset delta=20), (FULL, offset delta=19, locals={(type=Object, class=[Ljava.lang.String;), (type=Object, class=java.lang.Class), (type=Object, class=java.lang.String), (type=Object, class=java.lang.Throwable), (type=Object, class=java.lang.Throwable), (type=Object, class=java.io.InputStream)}, stack items={(type=Object, class=java.lang.Throwable)}), (CHOP 1, offset delta=10), (SAME_LOCALS_1_STACK, offset delta=1, stack items={(type=Object, class=java.lang.Throwable)}), (SAME, offset delta=11), (SAME, offset delta=11), (CHOP 2, offset delta=1))
Code(max_stack = 1, max_locals = 1, code_length = 5)
0: aload_0
1: invokevirtual de.scrum_master.stackoverflow.MethodDisassembler.sendEmail:()V (124)
4: return
Attribute(s) =
LineNumber(0, 23), LineNumber(4, 24)
LocalVariable(start_pc = 0, length = 5, index = 0:de.scrum_master.stackoverflow.MethodDisassembler this)
Code(max_stack = 2, max_locals = 1, code_length = 9)
0: getstatic java.lang.System.out:Ljava/io/PrintStream; (74)
3: ldc "Sending e-mail" (127)
5: invokevirtual java.io.PrintStream.println:(Ljava/lang/String;)V (129)
8: return
Attribute(s) =
LineNumber(0, 27), LineNumber(8, 28)
LocalVariable(start_pc = 0, length = 9, index = 0:de.scrum_master.stackoverflow.MethodDisassembler this)
I guess you can figure out the rest by yourself.
Update & disclaimer: Sorry, I have ovelooked the requirement that this should be done for instrumented classes, i.e. classes which have changed during classloading as compared to the original class files on disk. In this case I suggest to write your own little Java agent and put it after your weaving agent (whatever it may be, AspectJ or something else) in the chain of agents, then use the instrumentation API itself or BCEL again in order to find out if the class was correctly instrumented before.
Update 2: Here you see how you can dump the binary class from an instrumentation agent as suggested above. You can still decide whether you want to store the bytes on disk or want to buffer them in a byte array in memory and read them from there via BCEL via a ByteArrayInputStream
. But this seems pretty contrived if you need it outside of integration tests. During runtime in production maybe you do not want to use it, or at least only sparingly as a quick smoke test when booting up your application.