Java Bytecode: Journey to the Wonderland (Part 3)
Examine the tools and methods used to change and work with Java bytecode, the Java Virtual Machine's (JVM) intermediate representation of Java code.
Join the DZone community and get the full member experience.
Join For FreeOur previous article unpacked bytecode further and discussed ConstantPool
. Today, I'll go through several resources for working with it now.
Java bytecode is the Java Virtual Machine's (JVM) intermediate representation of Java code. While Java bytecode is not meant to be human-readable, it may be edited and manipulated for several reasons. This article examines the tools and methods used to change and work with Java bytecode.
Changing Java bytecode is often done to add new features to a Java program that already exists. This can be done with a bytecode injector, a tool that lets you add bytecode to a Java class file that has already been compiled. Bytecode injectors are often used to log or debug information and to allow updates like A/B testing or feature flags while the program is running.
Javaassist is one of the tools that can be leveraged to inject bytecode. Look at the following class.
package ca.bazlur;
public class Greetings {
public void sayHello(String name) {
System.out.println("Hello " + name + "!");
}
}
Let's say we have this class and would like to add a method to it but through bytecode manipulation.
package ca.bazlur;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
public class BytecodeInjector {
public static void main(String[] args) throws IOException {
try (var resource = BytecodeInjector.class.getResourceAsStream("Greetings.class")) {
final var classBytes = resource.readAllBytes();
// Create a ClassPool and import the original class
ClassPool classPool = ClassPool.getDefault();
CtClass ctClass = classPool.makeClass(new java.io.ByteArrayInputStream(classBytes));
// Create a new method and add it to the class
CtMethod newMethod = CtMethod.make("""
public void printHelloWorld() {
System.out.println("Hello, world!");
}
""", ctClass);
ctClass.addMethod(newMethod);
// Write the modified class back to a byte array
byte[] modifiedClassBytes = ctClass.toBytecode();
// Load the modified class bytes into the JVM
MyClassLoader classLoader = new MyClassLoader();
Class<?> modifiedClass = classLoader.defineClass("ca.bazlur.Greetings", modifiedClassBytes);
// Invoke the new method on an instance of the modified class
Object obj = modifiedClass.newInstance();
Method method = modifiedClass.getMethod("printHelloWorld");
method.invoke(obj);
} catch (CannotCompileException | InvocationTargetException | InstantiationException |
IllegalAccessException | NoSuchMethodException e) {
throw new RuntimeException(e);
}
}
}
In this code, we had a class called Greetings
. We wanted to add a new method. To do that, we had to read the original class into a byte array, import it into a ClassPool
, and then modify it by adding a new method. Then, the modified class is written back to a byte array and loaded into the JVM using a custom ClassLoader
. Finally, the new method is invoked on an instance of the modified class.
package ca.bazlur;
public class MyClassLoader extends ClassLoader {
public Class<?> defineClass(String name, byte[] bytes) {
return super.defineClass(name, bytes, 0, bytes.length);
}
}
If we run the above class, we will see that the functionality has been added to the Greetings
class and also executed. It will print:
Hello, world!
There is also a program known as "Byte Buddy," and we can make use of it to do a similar thing.
Let's assume we want to know how much time a method takes to execute. We can make use of bytecode instrumentation. Bytecode instrumentation is another method for modifying Java bytecode. This is done by using a library or tool to change the bytecode of a Java class before the JVM loads it. This might be beneficial for adding performance monitoring or code profiling to an application.
Let's use Byte Buddy to build a simple agent that will instrument every class and calculate the time it takes for each method to execute.
package ca.bazlur;
import java.lang.instrument.Instrumentation;
import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.matcher.ElementMatchers;
public class MyAgent {
public static void premain(String agentArgs, Instrumentation inst) {
new AgentBuilder.Default()
.type(ElementMatchers.any())
.transform((builder, typeDescription, classLoader, module) -> builder
.method(ElementMatchers.any())
.intercept(Advice.to(TimerAdvice.class)))
.installOn(inst);
}
}
The TimerAdvice
class is here:
package ca.bazlur;
import net.bytebuddy.asm.Advice;
public class TimerAdvice {
@Advice.OnMethodEnter
static long invokeBeforeEachMethod(
@Advice.Origin String method) {
System.out.println("Entering to invoke : " + method);
return System.currentTimeMillis();
}
@Advice.OnMethodExit
static void invokeWhileExitingEachMethod(@Advice.Origin String method,
@Advice.Enter long startTime) {
System.out.println(
"Method " + method + " took " + (System.currentTimeMillis() - startTime) + "ms");
}
}
The full source code is available at the Byte Code tutorial on GitHub.
Once we've built it and generated a jar, we can use it in the CLI by issuing the following command:
java -javaagent:myagent-1.0-SNAPSHOT.jar MyAwesomeJavaProgram
The MyAwesomeJavaProgram
looks like this:
public class MyAwesomeJavaProgram {
public static void main(String[] args) {
System.out.println(doCalculation());
}
public static int doCalculation() {
int result = 0;
for (int i = 0; i < 100000000; i++) {
result += i;
}
return result;
}
}
Once we run it in the CLI, we will get the output as follows:
Entering to invoke : public static void MyAwesomeJavaProgram.main(java.lang.String[]) Entering to invoke : public static int MyAwesomeJavaProgram.doCalculation() Method public static int MyAwesomeJavaProgram.doCalculation() took 41ms 887459712 Method public static void MyAwesomeJavaProgram.main(java.lang.String[]) took 45ms
Here are some libraries for manipulating Java bytecode:
- ASM: A fast, small, and efficient Java bytecode manipulation framework
- BCEL: A library for manipulating Java bytecode in the Apache Commons project
- Javassist: A bytecode manipulation library for Java
- Byte Buddy: A library for generating and modifying Java bytecode
- CFR: A bytecode decompiler for Java, written in Java
Changes can be made to Java bytecode for obfuscation and other reasons. "Obfuscation" is the process of making code harder to understand and figure out how it works. This may be beneficial for preventing a program's illegal usage or safeguarding intellectual property.
To make it harder to understand, Java bytecode can be obfuscated by changing the names of classes and methods or adding extra code.
There are various tools for obfuscating Java code available:
- ProGuard: This is a free and open-source program for optimizing and obscuring Java code. It can get rid of code that isn't needed, speed up code, and change the names of classes, fields, and functions to make them harder to understand.
- DashO: A commercial obfuscation and optimization program that includes control flow obfuscation, string encryption, and watermarking
- Zelix KlassMaster: A paid program that provides extensive obfuscation and protection capabilities, such as control flow obfuscation, string encryption, and class and member renaming
- Allatori: A commercial product that provides extensive obfuscation and security features such as control flow obfuscation, string encryption, and class and member renaming.
- yGuard: An open-source tool for optimizing and obscuring Java code, it can get rid of code that isn't needed, speed up code, and change the names of classes, fields, and functions to make them harder to understand.
In conclusion, Java bytecode can be updated and controlled for many reasons, like adding new features, instrumenting code for performance monitoring or profiling, or obfuscating code to protect intellectual property.
But even though these methods may work sometimes, they must be used correctly and in accordance with the terms of any license applications.
Published at DZone with permission of A N M Bazlur Rahman, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments