I want to transform a field of a class to be effectively a constant. I'm using ASM 5.0.3.
Here is the test class I have:
public class BytecodeUtilsTest {
#BeforeClass
public static void beforeClass(){
String replaceFieldClassName = "com.mypackage.ClassWithFieldToReplaceWithConstant";
String replaceFieldClassNameAsPath = replaceFieldClassName.replace('.', '/') + ".class";
// standard code to redefine class (inspired by ASM FAQ http://asm.ow2.org/doc/faq.html, sec. 5)
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
InputStream stream = contextClassLoader.getResourceAsStream(replaceFieldClassNameAsPath);
byte[] classBytes;
try {
classBytes = IOUtils.toByteArray(stream);
// here is the interesting part
byte[] patchedClassBytes = BytecodeUtils.patch(classBytes, "_fieldToReplace", true);
Reflection.invoke(contextClassLoader, "defineClass", Class.class,
new Class[]{String.class, byte[].class, int.class, int.class},
new Object[]{replaceFieldClassName, patchedClassBytes, 0, patchedClassBytes.length});
} catch (IOException e) {
throw new RuntimeException(e);
}
}
#Test
public void testFieldReplace(){
Assert.assertTrue(new ClassWithFieldToReplaceWithConstant().getFieldToReplace());
Assert.assertTrue(new ClassWithFieldToReplaceWithConstant()._fieldToReplace);
}
}
Here is the test class to update:
public class ClassWithFieldToReplaceWithConstant {
boolean _fieldToReplace;
public boolean getFieldToReplace() {
return _fieldToReplace;
}
}
And here is the patcher:
public class BytecodeUtils {
public static byte[] patch(byte[] bytecode, final String fieldToReplace, final boolean value) {
ClassReader classReader = new ClassReader(bytecode);
final ClassWriter classWriter = new ClassWriter(classReader, 0);
ClassVisitor classVisitor = new ClassVisitor(Opcodes.ASM4, classWriter) {
#Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
return new MethodVisitor(Opcodes.ASM4, super.visitMethod(access, name, desc, signature, exceptions)) {
#Override
public void visitFieldInsn(int opcode, String owner, String name, String desc) {
if (opcode == Opcodes.GETFIELD && name.equals(fieldToReplace)) {
mv.visitInsn(Opcodes.POP);
mv.visitInsn(value ? Opcodes.ICONST_1 : Opcodes.ICONST_0);
} else {
super.visitFieldInsn(opcode, owner, name, desc);
}
}
};
}
};
classReader.accept(classVisitor, 0);
return classWriter.toByteArray();
}
}
The problem is test fails at the second assert. So if I use getter, it returns trueas expected, but if I read field directly it returns false. That's quite unexpected considering the fact that getter yields INVOKEVIRTUAL instruction and field read yields GETFIELD which is updated by invocation of visitInsn method.
What am I doing wrong and how to make direct field access return true?
For the second assert to work you need to patch BytecodeUtilsTest, not ClassWithFieldToReplaceWithConstant, since bytecode instruction to read field for the second case is actually in the BytecodeUtilsTest#testFieldReplace method.
Getter case works fine since instruction to read the field is inside the getter body, i.e, inside the ClassWithFieldToReplaceWithConstant class.
If in real scenario field is private, this code should be fine (since there will be no access to the field from outside of the class field is declared in). Otherwise, you will have to patch every class which reads or writes to this field.
Related
This question already has answers here:
Printing debug info on errors with java 8 lambda expressions
(2 answers)
How can I find the target of a Java8 method reference?
(2 answers)
Closed 3 years ago.
If a method in a library is called with a Java Lambda expression, these are often just wrapped method calls. Is it possible to find out what method that originally was, just for logging purposes? (Another question is about what object it applies to - this is specifically about the called method.)
class Foo {
private void doSomething() { ... }
public void doSomethingInTransaction() {
doInTransaction(this::doSomething);
}
private void doInTransaction(Runnable run) { ... }
}
When calling doSomethingInTransaction() the method doInTransaction is actually called with an Object of type Runnable. It would sometimes be nice to log the name and class of the method that was passed here (that is, Foo.doSomething), as well as the object. Is it somehow possible to find out what that was via reflection or whatnot? If that requires specific Java versions, that'd be an interesting answer, too.
(UPDATE: please note that this is not a duplicate of the related question Java 8 - how to access object and method encapsulated as lambda since I'm mostly asking for the method that is encapsulated there. That wasn't asked there.)
The following example shows how to get the method reference name from the runnable. As explained in the comments, the code may be unnecesserarily complex and only works for certain cases (including the one in the question). Also, it makes certain assumptions that don't work in the general case.
Example class:
public class Test {
public void callingMethod() {
this.acceptingMethod(this::methodReferenceMethod);
}
public void acceptingMethod(final Runnable runnable) {
final String name = Util.getRunnableName(runnable, "acceptingMethod");
System.out.println("Name is " + name);
}
public void methodReferenceMethod() {
}
public static void main(final String[] args) {
new Test().callingMethod();
}
}
Now the actual magic here:
class Util {
public static String getRunnableName(final Runnable runnable, final String calledMethodName) {
final String callSiteMethodName = getCallSiteMethodNameNotThreadSafe();
final Class<?> callSiteClass = getDeclaringClass(runnable);
final String runnableName = extractRunnableName(callSiteClass, callSiteMethodName, calledMethodName);
return runnableName;
}
private static String extractRunnableName(
final Class<?> callSiteClass,
final String callSiteMethodName,
final String calledMethodName) {
try {
final AtomicReference<String> result = new AtomicReference<>(null);
final ClassReader cr = new ClassReader(callSiteClass.getName());
final TraceClassVisitor traceVisitor = new TraceClassVisitor(new PrintWriter(System.out));
cr.accept(new CheckClassAdapter(Opcodes.ASM7, traceVisitor, false) {
#Override
public MethodVisitor visitMethod(final int access, final String name, final String descriptor, final String signature, final String[] exceptions) {
if (!name.equals(callSiteMethodName)) {
return super.visitMethod(access, calledMethodName, descriptor, signature, exceptions);
}
return new CheckMethodAdapter(Opcodes.ASM7, super.visitMethod(access, name, descriptor, signature, exceptions), new HashMap<>()) {
#Override
public void visitInvokeDynamicInsn(final String name, final String descriptor, final Handle bootstrapMethodHandle, final Object... bootstrapMethodArguments) {
final String invokeDynamic = ((Handle) bootstrapMethodArguments[1]).getName();
result.set(invokeDynamic);
}
};
}
}, 0);
return result.get();
} catch (final IOException e) {
throw new RuntimeException(e);
}
}
public static String getCallSiteMethodNameNotThreadSafe() {
final int depth = 4;
return Thread.currentThread().getStackTrace()[depth].getMethodName();
}
public static Class<?> getDeclaringClass(final Runnable runnable) {
return Arrays.stream(runnable.getClass().getDeclaredFields())
.filter(f -> f.getName().equals("arg$1"))
.map(f -> {
f.setAccessible(true);
try {
return f.get(runnable).getClass();
} catch (IllegalArgumentException | IllegalAccessException e) {
throw new RuntimeException(e);
}
})
.findFirst()
.orElseThrow(IllegalStateException::new);
}
}
The output is as expected "Name is methodReferenceMethod". I would probably never use this in any project, but I guess it is possible. Also, this only works for the given example, as there is only one INVOKEVIRTUAL in the calling method. For the general case, one would need to adjust the checkMethodVisitor and filter the calls to the "calledMethodName" only. Lastly, the code to get the calling method uses a fixed index for the stack trace element, which also does not generalize well.
I am extending the class ClassVisitor and overriding the method visitMethod. Then I extend the MethodVisitor and override the visitMethodInsn. When I override the visitMethod I create a new instance of the extended MethodVisitor.
Please see the code below to understand. Keep in mind knowledge of the ASM library is required in order to understand it properly.
GraphClass.java:
public class GraphClass extends ClassVisitor {
public GraphClass() {
super(ASM5);
}
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
System.out.println("testing " + name + desc);
GraphMethod newVisitor = new GraphMethod();
return newVisitor;
}
}
GraphMethod.java:
public class GraphMethod extends MethodVisitor{
public GraphMethod() {
super(ASM5);
}
public void visitMethodInsn(int opcode, java.lang.String owner, java.lang.String name, java.lang.String descriptor, boolean isInterface) {
System.out.println(owner + name);
}
}
What I am trying to do is pass the name variable from visitMethod to be printed along with the other variables in the visitMethodInsn.
I am new to Java, so any tips would be very helpful.
Based on the comments I will assume that you want to know which methods call by methods of the visited class, all right?
It takes solved easy by using tree-api of objectweb asm.
ClassReader cr = new ClassReader(bytesOfSomeClass);
//Used class node instead of visiter
ClasaNode cn = new ClassNode(<asm version>);
cr.accept(cn, 0);
//Iterate all methods of class
cn.methods.forEach( (MethodNode mn) -> {
String callerName = mn.name;
//Iterate all instructions of current method
Stream.iterate(mn.instructions.getFirst(), AbstractInsnNode::getNext).limit(instructions.size())
.filter(node -> node instanceof MethodInsnNode) //take only method calls
.forEach(node -> {
String calledName = ((MethodInsnNode) node).name;
//Print results
System.out.println(calledName + " is called by " + callerName);
});
});
I'm writing a gradle plugin for my lib. https://github.com/shehabic/sherlock, I need to inject a network interceptor at compilation time in the byte code of OkHttp Client (https://github.com/square/okhttp/blob/master/okhttp/src/main/java/okhttp3/OkHttpClient.java)
to specific I would like to inject the following line in Java:
this.interceptors.add(new com.shehabic.sherlock.interceptors(new SherlockOkHttpInterceptor())
https://github.com/square/okhttp/blob/master/okhttp/src/main/java/okhttp3/OkHttpClient.java#L1068
I have written the plugin the transformer already and here's my class writer:
public class SherlockClassWriter {
ClassReader reader;
ClassWriter writer;
PublicizeMethodAdapter pubMethAdapter;
final static String CLASSNAME = "okhttp3.OkHttpClient";
public SherlockClassWriter() {
try {
reader = new ClassReader(CLASSNAME);
writer = new ClassWriter(reader, 0);
} catch (IOException ex) {
ex.printStackTrace();
}
}
public SherlockClassWriter(byte[] contents) {
reader = new ClassReader(contents);
writer = new ClassWriter(reader, 0);
}
public static void main(String[] args) {
SherlockClassWriter ccw = new SherlockClassWriter();
ccw.publicizeMethod();
}
public byte[] publicizeMethod() {
pubMethAdapter = new PublicizeMethodAdapter(writer);
reader.accept(pubMethAdapter, 0);
return writer.toByteArray();
}
public class PublicizeMethodAdapter extends ClassVisitor {
TraceClassVisitor tracer;
PrintWriter pw = new PrintWriter(System.out);
public PublicizeMethodAdapter(ClassVisitor cv) {
super(ASM4, cv);
this.cv = cv;
tracer = new TraceClassVisitor(cv, pw);
}
#Override
public MethodVisitor visitMethod(
int access,
String name,
String desc,
String signature,
String[] exceptions
) {
if (name.equals("build")) {
MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
// call method in java:
// this.interceptors.add(new com.shehabic.sherlock.interceptors(new SherlockOkHttpInterceptor())
}
return tracer.visitMethod(access, name, desc, signature, exceptions);
}
}
}
a similar method that adds interceptors has a bytecode as follows:
aload_0
getfield #4 <okhttp3/OkHttpClient$Builder.interceptors>
aload_1
invokeinterface #117 <java/util/List.add> count 2
pop
aload_0
My questions are:
1.How do I inject more code into a method? even if Bytecode.
Update
Here is my working solution, based on the answer:
https://github.com/shehabic/sherlock/blob/creating-plugin-to-intercept-all-okhttp-connections/sherlock-plugin/src/main/java/com/shehabic/sherlock/plugin/SherlockClassWriter.java
There is example code to insert your line at the beggining of the function
public class YourClassVisitor extends ClassVisitor {
public YourClassVisitor(ClassVisitor cv) {
super(Opcodes.ASM5, cv);
}
#Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
if (name.equals("targetName")) {
return new YourMethodVisitor(super.visitMethod(access, name, desc, signature, exceptions));
}
return super.visitMethod(access, name, desc, signature, exceptions);
}
private static class YourMethodVisitor extends MethodVisitor {
public YourMethodVisitor(MethodVisitor mv) {
super(Opcodes.ASM5, mv);
}
// This method will be called before almost all instructions
#Override
public void visitCode() {
// Default implementation is empty. So we haven't to call super method
// Puts 'this' on top of the stack. If your method is static just delete it
visitVarInsn(Opcodes.ALOAD, 0);
// Takes instance of class "the/full/name/of/your/Class" from top of the stack and put value of field interceptors
// "Ljava/util/List;" is just internal name of java.util.List
// If your field is static just replace GETFIELD with GETSTATIC
visitFieldInsn(Opcodes.GETFIELD, "the/full/name/of/your/Class", "interceptors", "Ljava/util/List;");
// Before we call add method of list we have to put target value on top of the stack
// New object creation starts with creating not initialized instance of it
visitTypeInsn(Opcodes.NEW, "com/shehabic/sherlock/interceptors");
// Than we just copy it
visitInsn(Opcodes.DUP);
visitTypeInsn(Opcodes.NEW, "example/path/to/class/SherlockOkHttpInterceptor");
visitInsn(Opcodes.DUP);
// We have to call classes constructor
// Internal name of constructor - <init>
// ()V - signature of method. () - method doesn't have parameters. V - method returns void
visitMethodInsn(Opcodes.INVOKESPECIAL, "example/path/to/class/SherlockOkHttpInterceptor", "<init>", "()V", false);
// So on top of the stack we have initialized instance of example/path/to/class/SherlockOkHttpInterceptor
// Now we can call constructor of com/shehabic/sherlock/interceptors
visitMethodInsn(Opcodes.INVOKESPECIAL, "com/shehabic/sherlock/interceptors", "<init>", "(Lexample/path/to/class/SherlockOkHttpInterceptor;)V", false);
// So on top of the stack we have initialized instance of com/shehabic/sherlock/interceptors
// Now we can put it into list
visitMethodInsn(Opcodes.INVOKEINTERFACE, "java/util/List", "add", "(Ljava/lang/Object;)Z", true);
}
}
}
There is example of using class visitor
byte[] cache = null;
try (FileInputStream in = new FileInputStream("C:\\Users\\JustAGod\\Projects\\gloomymods\\BuildTools\\BytecodeTools\\out\\production\\classes\\gloomyfolken\\Kek.class")) {
ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS);
ClassReader reader = new ClassReader(in);
reader.accept(new YourClassVisitor(writer), ClassReader.EXPAND_FRAMES);
cache = writer.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
try(FileOutputStream out = new FileOutputStream("C:\\Users\\JustAGod\\Projects\\gloomymods\\BuildTools\\BytecodeTools\\out\\production\\classes\\gloomyfolken\\Kek.class")) {
out.write(cache);
} catch (IOException e) {
e.printStackTrace();
}
I am really sorry for my English.
I'll add my code to the end of this post.
I'm using byteBuddy 1.7.9 and whatever ASM version comes with that.
In a nutshell
I have
byte[] rawClass = ...;
ClassReader cr = new ClassReader(rawClass);
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
MethodAdder ma = new MethodAdder(Opcodes.ASM5,cw);
cr.accept(ma,ClassReader.EXPAND_FRAMES);
Where in MethodAdder, I want to add a static initialiser:
#Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
if(mv != null){
if(!name.equals(CLINIT_NAME)) return mv;
else{
hasStaticInitialiser = true;
return new ClinitReplacer(api,mv,classname);
}
}else return null;
}
hasStaticInitialiser = true is reached, but ClinitReplacer.visitCode is never executed.
Why?
the whole story
Let's say I want to generate class B from this example using byteBuddy.
Why bytebuddy? Well, for one it's supposedly convenient, and for another, I need its class reloading functionality.
But as you can see in the tutorial, there are some inconveniences with using "pure" byte buddy code. Most importantly,
if you really need to create byte code with jump instructions, make sure to add the correct stack map frames using ASM since Byte Buddy will not automatically include them for you.
I don't want to do that.
And even if I wanted to, I tried
builder = builder
.defineMethod("<clinit>",void.class, Modifier.STATIC)
.withParameters(new LinkedList<>())
.withoutCode()
;
and all it got me was an
Exception in thread "main" java.lang.IllegalStateException: Illegal explicit declaration of a type initializer by class B
at net.bytebuddy.dynamic.scaffold.InstrumentedType$Default.validated(InstrumentedType.java:901)
at net.bytebuddy.dynamic.scaffold.MethodRegistry$Default.prepare(MethodRegistry.java:465)
at net.bytebuddy.dynamic.scaffold.subclass.SubclassDynamicTypeBuilder.make(SubclassDynamicTypeBuilder.java:162)
at net.bytebuddy.dynamic.scaffold.subclass.SubclassDynamicTypeBuilder.make(SubclassDynamicTypeBuilder.java:155)
at net.bytebuddy.dynamic.DynamicType$Builder$AbstractBase.make(DynamicType.java:2639)
at net.bytebuddy.dynamic.DynamicType$Builder$AbstractBase$Delegator.make(DynamicType.java:2741)
at Main.main(Main.java)
So what I do instead is, I stop after I've added all my fields, get the bytecode for that and load the class.
Then I have ASM add the methods for me. ( In the actual application, I also need to run the bytecode through some other ASM visitors anyway.)
And then reload the re-instrumented bytecode using ByteBuddy.
The reloading fails with
Exception in thread "main" java.lang.ClassFormatError
at sun.instrument.InstrumentationImpl.redefineClasses0(Native Method)
at sun.instrument.InstrumentationImpl.redefineClasses(InstrumentationImpl.java:170)
at net.bytebuddy.dynamic.loading.ClassReloadingStrategy$Strategy$1.apply(ClassReloadingStrategy.java:261)
at net.bytebuddy.dynamic.loading.ClassReloadingStrategy.load(ClassReloadingStrategy.java:171)
at Main.main(Main.java)
And the reason for that seems to be that B looks like this when disassembled:
super public class B
extends A
version 51:0
{
public static final Field foo:"Ljava/util/Set;";
public Method "<init>":"()V"
stack 1 locals 1
{
aload_0;
invokespecial Method A."<init>":"()V";
return;
}
static Method "<clinit>":"()V";
} // end Class B
Comparing that to the rawClass bytecode, we notice that
static Method "<clinit>":"()V";
didn't exist and was indeed added by the MethodAdder.
However, the Visitor returned in
return new ClinitReplacer(api,mv,classname);
is never used. And therefore the static initialiser body remains empty resulting in the wrongful classification as native.
Code
A.java
import java.util.HashSet;
import java.util.Set;
public class A{
public static final Set foo;
static{
foo = new HashSet<String>();
foo.add("A");
}
}
Main.java
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.agent.ByteBuddyAgent;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.dynamic.loading.ClassLoadingStrategy;
import net.bytebuddy.dynamic.loading.ClassReloadingStrategy;
import net.bytebuddy.jar.asm.*;
import net.bytebuddy.jar.asm.commons.InstructionAdapter;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.Field;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
public class Main {
public static void main(String[] args) {
ByteBuddyAgent.install();
String targetClassname = "B";
Class superclass = A.class;
ByteBuddy byteBuddy = new ByteBuddy();
DynamicType.Builder builder = byteBuddy
.subclass(superclass)
.name(targetClassname)
;
for(Field f : superclass.getFields()){
builder = builder.defineField(f.getName(),f.getType(),f.getModifiers());
}
DynamicType.Unloaded<?> loadable = builder.make();
byte[] rawClass = loadable.getBytes();
loadable.load(A.class.getClassLoader(), ClassLoadingStrategy.Default.INJECTION);
ClassReader cr = new ClassReader(rawClass);
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
MethodAdder ma = new MethodAdder(Opcodes.ASM5,cw);
cr.accept(ma,ClassReader.EXPAND_FRAMES);
byte[] finishedClass = cw.toByteArray();
Class unfinishedClass;
try {
unfinishedClass = Class.forName(targetClassname);
}catch(ClassNotFoundException e){
throw new RuntimeException(e);
}
ClassReloadingStrategy.fromInstalledAgent()
.load(
A.class.getClassLoader(),
Collections.singletonMap((TypeDescription)new TypeDescription.ForLoadedType(unfinishedClass), finishedClass)
);
Set<String> result;
try {
result = (Set<String>)Class.forName("B").getField("foo").get(null);
}catch(ClassNotFoundException | NoSuchFieldException | IllegalAccessException e){
throw new RuntimeException(e);
}
System.out.println(result);
}
private static void store(String name, byte[] finishedClass) {
Path path = Paths.get(name + ".class");
try {
FileChannel fc = null;
try {
Files.deleteIfExists(path);
fc = new FileOutputStream(path.toFile()).getChannel();
fc.write(ByteBuffer.wrap(finishedClass));
} finally {
if (fc != null) {
fc.close();
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
static class MethodAdder extends ClassVisitor implements Opcodes{
private static final String INIT_NAME = "<init>";
private static final String INIT_DESC = "()V";
private static final int CLINIT_ACCESS = ACC_STATIC;
private static final String CLINIT_NAME = "<clinit>";
private static final String CLINIT_DESC = "()V";
private static final String CLINIT_SIG = null;
private static final String[] CLINIT_EXCEPT = null;
public MethodAdder(int api, ClassVisitor cv) {
super(api, cv);
}
private String classname = null;
private boolean hasStaticInitialiser = false;
#Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
classname = name;
hasStaticInitialiser = false;
cv.visit(version, access, name, signature, superName, interfaces);
}
#Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
if(mv != null){
if(!name.equals(CLINIT_NAME)) return mv;
else{
hasStaticInitialiser = true;
return new ClinitReplacer(api,mv,classname);
}
}else return null;
}
#Override
public void visitEnd() {
if(!hasStaticInitialiser) visitMethod(CLINIT_ACCESS,CLINIT_NAME,CLINIT_DESC,CLINIT_SIG,CLINIT_EXCEPT);
if(!hasStaticInitialiser) throw new IllegalStateException("ClinitReplacer not created");
super.visitEnd();
}
private static class ClinitReplacer extends InstructionAdapter implements Opcodes{
private final String classname;
public ClinitReplacer(int api, MethodVisitor mv, String classname) {
super(api, mv);
this.classname = classname;
}
#Override
public void visitCode() {
mv.visitCode();
InstructionAdapter mv = new InstructionAdapter(this.mv);
mv.anew(Type.getType(HashSet.class));
mv.dup();
mv.dup();
mv.invokespecial(Type.getInternalName(HashSet.class),INIT_NAME,INIT_DESC,false);
mv.putstatic(classname,"foo",Type.getDescriptor(Set.class));
mv.visitLdcInsn(classname);
mv.invokevirtual(Type.getInternalName(HashSet.class),"add","(Ljava/lang/Object;)Z",false);
mv.visitInsn(RETURN);
}
}
}
}
The problem is that your source class file doesn’t have a <clinit> method, hence, ASM doesn’t invoke visitMethod at all; it is you who does in
#Override
public void visitEnd() {
if(!hasStaticInitialiser) visitMethod(CLINIT_ACCESS,CLINIT_NAME,CLINIT_DESC,CLINIT_SIG,CLINIT_EXCEPT);
if(!hasStaticInitialiser) throw new IllegalStateException("ClinitReplacer not created");
super.visitEnd();
}
Here, you are invoking visitMethod for <clinit> if you didn’t encounter it before, but you’re not doing anything with the returned MethodVisitor, hence, no-one is doing anything with it.
If you want to treat an absent <clinit> like visiting an empty initializer, to be transformed, you have to perform the appropriate method calls yourself, i.e.
#Override
public void visitEnd() {
if(!hasStaticInitialiser) {
MethodVisitor mv = visitMethod(CLINIT_ACCESS,CLINIT_NAME,CLINIT_DESC,CLINIT_SIG,CLINIT_EXCEPT);
mv.visitCode();
mv.visitInsn(RETURN);
mv.visitMaxs(0, 0);
mv.visitEnd();
}
if(!hasStaticInitialiser) throw new IllegalStateException("ClinitReplacer not created");
super.visitEnd();
}
But note that then, you can’t do hot code replacement, as it doesn’t support adding any methods, including <clinit>. Further, hot code replacement won’t (re-)execute class initializers anyway.
But in your code, there is no need to load the type before performing your ASM transformation. You may remove the line
loadable.load(A.class.getClassLoader(), ClassLoadingStrategy.Default.INJECTION);
and then just use the resulting finishedClass bytecode, e.g.
ClassLoadingStrategy.Default.INJECTION.load(A.class.getClassLoader(),
Collections.singletonMap(loadable.getTypeDescription(), finishedClass));
Note that you won’t see much effect, as you are injecting code creating a HashMap, but not doing anything useful with it. You likely want to assign it to a field…
And, by the way, your code for writing a byte array is unnecessarily complicated:
private static void store(String name, byte[] finishedClass) {
Path path = Paths.get(name + ".class");
try {
FileChannel fc = null;
try {
Files.deleteIfExists(path);
fc = new FileOutputStream(path.toFile()).getChannel();
fc.write(ByteBuffer.wrap(finishedClass));
} finally {
if (fc != null) {
fc.close();
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
Just use
private static void store(String name, byte[] finishedClass) {
Path path = Paths.get(name + ".class");
try {
Files.write(path, finishedClass);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
Both, “create if it doesn’t exist” and “overwrite/truncate if it exists” are the default behavior.
To answer the part about defining a type initializer in Byte Buddy, this can be done using:
builder = builder.invokable(isTypeInitializer()).intercept(...);
You cannot explicitly define a type initializer as those initializers are for example never exposed by the reflection API and this helps to keep Byte Buddy's type description model coherent. Instead, you match the type initializer and Byte Buddy makes sure that an intializer is added as it seems appropriate.
I am new to ASM framework. I have been working around this ASM framework for a week. I saw tutorials in net regarding parsing a class and Generating a .class file from scratch.
But am unable to follow how to modify a existing class in ASM.
I am unable to follow the flow of execution between the ClassVisitor, ClassWriter and ClassReader.
Kindly solve my issue by giving me a ASM example for the following code.
public class ClassName {
public void showOne() {
System.out.println("Show One Method");
}
public static void main(String[] args) {
ClassName c = new ClassName();
c.showOne();
}
}
The above class should be modified as:
public class ClassName {
public void showOne() {
System.out.println("Show One Method");
}
public void showTwo() { // <- Newly added method
System.out.println("Show Two Method");
}
public static void main(String[] args) {
ClassName c = new ClassName();
c.showOne();
c.showTwo(); // <- Newly inserted method call
}
}
What should be the ASM code to modify it?
I used the ASMifier tool to generate the code. But I don't know where to apply it.
Your requirements are a bit underspecified. Below is an example program which uses ASM’s visitor API for transforming a class assumed to have the structure of your question to the resulting class. I added a convenience method taking a byte array and returning a byte array. Such a method can be used in both cases, a static transformation applied to class files on disk as well as in an Instrumentation agent.
When combining a ClassWriter with a ClassVisitor passed to a ClassReader as below, it will automatically replicate every feature of the source class so you have to override only these methods where you want to apply changes.
Here, visitMethod is overridden to intercept when encountering the main method to modify it and visitEnd is overridden to append the entirely new showTwo method. The MainTransformer will intercept RETURN instructions (there should be only one in your example) to insert the call to showTwo before it.
import org.objectweb.asm.*;
import org.objectweb.asm.commons.GeneratorAdapter;
public class MyTransformer extends ClassVisitor {
public static byte[] transform(byte[] b) {
final ClassReader classReader = new ClassReader(b);
final ClassWriter cw = new ClassWriter(classReader,
ClassWriter.COMPUTE_FRAMES|ClassWriter.COMPUTE_MAXS);
classReader.accept(new MyTransformer(cw), ClassReader.EXPAND_FRAMES);
return cw.toByteArray();
}
public MyTransformer(ClassVisitor cv) {
super(Opcodes.ASM5, cv);
}
#Override
public MethodVisitor visitMethod(int access, String name, String desc,
String signature, String[] exceptions) {
MethodVisitor v=super.visitMethod(access, name, desc, signature, exceptions);
if(name.equals("main") && desc.equals("([Ljava/lang/String;)V"))
v=new MainTransformer(v, access, name, desc, signature, exceptions);
return v;
}
#Override
public void visitEnd() {
appendShowTwo();
super.visitEnd();
}
private void appendShowTwo() {
final MethodVisitor defVisitor = super.visitMethod(
Opcodes.ACC_PUBLIC, "showTwo", "()V", null, null);
defVisitor.visitCode();
defVisitor.visitFieldInsn(Opcodes.GETSTATIC,
"java/lang/System", "out", "Ljava/io/PrintStream;");
defVisitor.visitLdcInsn("Show Two Method");
defVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
"java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
defVisitor.visitInsn(Opcodes.RETURN);
defVisitor.visitMaxs(0, 0);
defVisitor.visitEnd();
}
class MainTransformer extends GeneratorAdapter
{
MainTransformer(MethodVisitor delegate, int access, String name, String desc,
String signature, String[] exceptions) {
super(Opcodes.ASM5, delegate, access, name, desc);
}
#Override
public void visitInsn(int opcode) {
if(opcode==Opcodes.RETURN) {
// before return insert c.showTwo();
super.visitVarInsn(Opcodes.ALOAD, 1); // variable c
super.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
"ClassName", "showTwo", "()V", false);
}
super.visitInsn(opcode);
}
}
}