Koichi Sakata
PONOS Corporation
Bytecode Manipulation
with a Java Agent
and Byte Buddy
About Me
• Koichi Sakata (阪田 浩一)
• KanJava JUG Leader
• Java Champion
• PONOS Corporation
#oc1bcm2
Intended Audience
• Those who want to start manipulating
bytecode
• Not for experts
#oc1bcm3
javac
Java Bytecode
#oc1bcm
.java • Java Code
.class • Bytecode
Gets loaded by JVM
4
What is bytecode manipulation?
• Editing bytecode in the class files
–Add
–Delete
–Replace
#oc1bcm5
Why do we manipulate?
It's fun!
#oc1bcm6
Not Only Fun
• To change code at runtime
–Generate Entity objects in Hibernate
–Create mock objects in Mockito
#oc1bcm7
Bytecode Manipulation
• Hard to manipulate by hand...
– A class file is a binary file
– JVM has a verification process
#oc1bcm8
Bytecode Manipulation Libraries
• Byte Buddy
• Byteman
• Javassist
• cglib
• ASM
• Apache Commons BCEL
#oc1bcm9
Byte Buddy
• http://bytebuddy.net
• Easy-to-use API
• Java 11 Support
• Duke's Choice Award 2015 Winner
#oc1bcm10
Byte Buddy
• Usage in major libraries
–Hibernate
–Mockito
–Jackson
–etc.
#oc1bcm11
Class<?> subClass = new ByteBuddy()
.subclass(Object.class)
.method(ElementMatchers.named("toString"))
.intercept(FixedValue.value("Hello World!"))
.make()
.load(getClass().getClassLoader())
.getLoaded();
Hello World in Byte Buddy
#oc1bcm12
Class<?> subClass = new ByteBuddy()
.subclass(Object.class)
.method(ElementMatchers.named("toString"))
1. Create a subclass of Object
2. Choose the toString()
Hello World in Byte Buddy
#oc1bcm13
.method(ElementMatchers.named("toString"))
.intercept(FixedValue.value("Hello World!"))
1. Intercept toString() to return a fixed
string value "Hello World"
Hello World in Byte Buddy
#oc1bcm14
.make()
.load(getClass().getClassLoader())
.getLoaded();
1. Make a new type (unloaded class)
2. Load the type
3. Get the new loaded class
Hello World in Byte Buddy
#oc1bcm15
Class<?> subClass = new ByteBuddy()
.subclass(Object.class)
.method(ElementMatchers.named("toString"))
.intercept(FixedValue.value("Hello World!"))
.make()
.load(getClass().getClassLoader())
.getLoaded();
Hello World in Byte Buddy
#oc1bcm16
Demo 1
• Run Hello World
–See a generated class
#oc1bcm17
Demo code is available
• http://bit.ly/oc1bcm
–github.com/jyukutyo/SampleCodeForByt
eBuddySeeesion
#oc1bcm18
Usage in Mockito
• SubclassBytecodeGenerator
–mockClass()
#oc1bcm19
How to use BM?
1. Directly in your application
2. At build time via Maven/Gradle
3. With Java Agent
– No need for changing code
– Detachable
#oc1bcm20
Java Agent?
• Do espionage activities in
your application
– Profiling
– Logging
– Changing the target
application itself
#oc1bcm21
Java Agent
• Instrument programs running on the
JVM
–JSR 163: JavaTM Platform Profiling
Architecture
–JVM calls Java Agent method with
proper timing
#oc1bcm22
Create a Java Agent
• Just define methods
–premain() Method
–agentmain() Method
#oc1bcm23
Create a Java Agent
• premain() Method
–Gets called before main() method
• agentmain() Method
–Gets called when the agent is attached
after JVM startup
#oc1bcm24
With Java Agent
We can run bytecode
manipulation code
at runtime
#oc1bcm25
#oc1bcm26
premain()
public static void premain(String agentArgs,
Instrumentation inst)
• Provides opportunity to modify
classes before loading
–Instrumentation class has useful
methods
#oc1bcm27
Premain-Class: com.s.logging.LoggingAgent
Boot-Class-Path: byte-buddy-1.8.22.jar
MANIFEST.MF
#oc1bcm28
Demo 2
• Create a simple Java Agent
#oc1bcm29
Another Example
• Add outputting logs before executing
a particular method on a particular
class
#oc1bcm30
Example
1. Manipulate bytecode before loading
–Use premain()
2. Replace the class file
–Use ByteBuddy
–Not create a subclass
#oc1bcm31
public class Target {
public void foo() {
System.out.println("foo!");
}
public static void main(String[] args) {
new Target().foo();
}
}
Target Class
#oc1bcm32
TypePool pool = TypePool.Default.ofClassPath();
return new ByteBuddy()
.rebase(pool.describe("c.Target").resolve(),
ClassFileLocator.ForClassLoader
.ofClassPath()
)
Replace the class
#oc1bcm33
return new ByteBuddy()
.rebase(...)
• Replace the original implementation with
new one
• Save the original implementation under a
different method name
Replace the class
#oc1bcm34
Enhance Existing Method
• ByteBuddy#subclass()
–Override the method
• ByteBuddy#redefine()
–Replace the implementation
• ByteBuddy#rebase()
–Copy&Rename the method
–Replace the implementation
#oc1bcm35
TypePool pool = TypePool.Default.ofClassPath();
return new ByteBuddy()
.rebase(pool.describe("c.Target")
.resolve(),
• Can't use a class literal
– JVM loads class before changing the class
• Use the TypePool class and describe()
Replace the class
#oc1bcm36
.rebase(pool.describe(...).resolve(),
ClassFileLocator.ForClassLoader
.ofClassPath()
)
• Allow to locate a class file
• ofClassPath() - Scan the running
application's class path
Replace the class
#oc1bcm37
return new ByteBuddy()
...
.method(ElementMatchers.named("foo"))
.intercept(
MethodDelegation
.to(LoggingInterceptor.class)
.andThen(SuperMethodCall.INSTANCE)
)
Intercept the method
#oc1bcm38
Delegation
• MethodDelegation.to()
–Specify a Class to delegate
• A best match method is used
#oc1bcm39
return new ByteBuddy()
...
.method(ElementMatchers.named("foo"))
.intercept(
MethodDelegation
.to(LoggingInterceptor.class)
.andThen(SuperMethodCall.INSTANCE)
)
Intercept the method
#oc1bcm40
public static class LoggingInterceptor {
public static void intercept(
@Origin Method m) {
...println("Call " + m.getName()...);
}
}
Interceptor Class
#oc1bcm41
Delegated Method
• Annotations for Parameter
–@Origin
• Reference to the method/constructor
#oc1bcm42
Delegated Method
• Annotations for Parameter
–@This/@Super
• Dynamic/Super type's instance
–@Argument, @AllArguments
–@SuperCall
• Callable/Runnable instance of the super's
implementation of the method
#oc1bcm43
public static void premain(String args,
Instrumentation inst) {
inst.addTransformer(
new ClassFileTransformer() {
@Override
public byte[] transform(...) {
premain()
#oc1bcm44
public byte[] transform(ClassLoader loader,
String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer) {
// Call ByteBuddy
}
transform()
#oc1bcm45
public byte[] transform(ClassLoader loader,
String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer)
• className – Class name to be loaded
• classfileBuffer – Class file content
transform()
#oc1bcm46
public byte[] transform(ClassLoader loader,
String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer)
• Return the class file content
transform()
#oc1bcm47
Demo 3
• Add outputting logs before executing
a method
#oc1bcm48
#oc1bcm49
Features in ByteBuddy
• Add fields/methods
• Manipulate stack
• Generate CONDY bytecode
– Constant Dynamic – JEP 309
• Simplify Java Agent with AgentBuilder
• lots of other features
#oc1bcm50
Byte Buddy's AgentBuilder
• Simplify the Java Agent
implementation
#oc1bcm51
new AgentBuilder.Default()
.type(ElementMatchers.named("com.sakatakoic
hi.logging.Target"))
.transform(new AgentBuilder.Transformer() {
...
})
.installOn(inst);
AgentBuilder
#oc1bcm52
AgentBuilder
1. Use AgentBuilder
–instead of ByteBuddy class
2. Use AgentBuilder.Transformer
–instead of ClassFileTransformer
3. Call installOn()
#oc1bcm53
.transform(new AgentBuilder.Transformer() {
public DynamicType.Builder<?> transform(
DynamicType.Builder<?> builder,
TypeDescription typeDescription,
ClassLoader classLoader,
JavaModule javaModule) {
return builder
.method(ElementMatch...named("foo"))
.intercept(...);
}
})
Transformer
#oc1bcm54
Without -javaagent
• -javaagent option can be removed
–Call ByteBuddyAgent.install()
–Call AgentBuilder#installOnByteBuddyAgent()
• instead of installOn()
–Need to add byte-buddy-agent.jar
#oc1bcm55
ByteBuddyAgent.install();
new AgentBuilder.Default()
.type(ElementMatchers.named("com.sakatakoic
hi.logging.Target"))
.transform(new AgentBuilder.Transformer() {
...
})
.installOnByteBuddyAgent();
Without -javaagent
#oc1bcm56
Demo 4
• Add outputting logs without
-javaagent option
#oc1bcm57
Wrap Up
• BM is fun and close to us!
–Many libraries are using
• Application Examples
–Hot Swap
–Advanced Dynamic Proxy
#oc1bcm58

Bytecode Manipulation with a Java Agent and Byte Buddy

Editor's Notes

  • #2 Hello, everyone! Today, I'd like to talk about manipulating Java bytecode. I'll introduce Java Agent and a library called Byte Buddy.
  • #3 Let me briefly introduce myself. I'm Koichi Sakata. I'm a KanJava JUG leader. Kan means Kansai region in Japan. It includes Kyoto. Do you know Kyoto? Kyoto is visited by many tourists. I became a Java champion this year. I work at PONOS Corporation, that is a mobile game company.
  • #4 This session is for beginners, not for experts.
  • #5 Moving on to our main topic. Java bytecode is generated by the javac compiler. It's in a class file. It's an intermediate representation of Java programs. We can visualize bytecode with javap command.
  • #6 What is bytecode manipulation? It's to edit bytecode, such as add, delete and replace bytecode.
  • #7 Why do we manipulate bytecode? Because it's fun!
  • #8 But not only fun. It's also useful to change code at runtime. For example, Hibernate uses bytecode manipulation to generate entity objects, and Mockito uses it to create mock objects. Of course at runtime.
  • #9 It's hard to change bytecode by hand. Because a class file is a binary file and JVM has a verification process for a class file.
  • #10 So there are many libraries for bytecode manipulation. Byte Buddy, Byteman, Javassist, cglib, ASM and BCEL. Today I'll introduce Byte Buddy.
  • #11 Why Byte Buddy? Byte Buddy has a Easy-to-use API, supports Java 11 already and won a Duke's Choice Award.
  • #12 Many major libraries are using Byte Buddy, such as Hibernate, Mockito, Jackson, etc.
  • #13 This is a hello world program with Byte Buddy. It creates a new class that extends Object class, and overrides toString() method to return "Hello World". I'll explain the details of this code.
  • #14 First, we create a subclass of the Object class and choose the toString() method.
  • #15 Next, we intercept the metod to return a fixed string value "Hello World".
  • #16 Make method builds a new type. The new type isn't loaded by a classloader yet, so the type is an unloaded class. Then the classloader loads the type. Finally we can get the new loaded class object. It's easy to manipulate bytecode with Byte Buddy. That's the reason why Byte Buddy is so popular.
  • #17 OK, I explained all parts of this code.
  • #18 Let's run the hello world. This code is a bit different from the previous code. I'll save a generated class to a file and outputs the method return. (run HelloWorld) Let's see the generated file. It is in a temp directory. Byte Buddy names the dynamic class randomly.
  • #19 By the way, the demo code used today is available on GitHub. Just access "bitly/oc1bcm"
  • #20 Now I'd like to show code in Mockito. Mockito is a famous mocking framework. This method creates a mock class. As you can see, it's similar to our hello world example.
  • #21 How to use? You can run bytecode manipulation code directly in your application. But you need to change your code. When you choose the second option, you need to rebuild. So I'd like to introduce Java Agent. It doesn't require the change. It's also detachable.
  • #22 What is Java Agent? The word "Agent" invokes the "Mission: Impossible" spy series. Java Agent do espionage activities in your application, like profiling, logging and even changing the behavior of the application itself.
  • #23 Java Agent can instrument programs running on the JVM. It's defined as JSR 163. The JVM calls Java Agent methods with proper timing.
  • #24 To create a Java Agent, just define one of the these methods or both, premain() and agentmain().
  • #25 Premain() method is called by the JVM before the application's main method(). Agentmain() is called when the agent is attached after the JVM has started.
  • #26 So with Java Agent, we can run bytecode manipulation code at runtime.
  • #27 To create a Java Agent, first, define the Agent Class method. Second, write a MANIFEST.MF file. Third, package the Agent class to a JAR file. Finally, run java command with javaagent option.
  • #28 A signature of premain() is "public static void premain()". We can have an opportunity to modify classes before the class is loaded. Instrumentation class in method parameters provides useful methods to modify classes.
  • #29 In the manifest file, we specify the Agent class and set the path to the Byte Buddy's JAR file.
  • #30 Let's create a simple Java Agent. The Agent class is a Java agent class. It has the premain() method. This is the Main class. Run the main() method with javaagent option. As you can see, premain() executed before main method.
  • #31 Let's combine Java Agent with Byte Buddy. I'll show another example. It adds outputting logs before executing a particular method on a particular class.
  • #32 I'll do two things. First, define the premain() method in a class. Second, write code in premain() method to replace the class file to output a log. Note that I don't create a subclass.
  • #33 This is the Target class. We want to output a log when foo() method is called.
  • #34 Next, replace the class with Byte Buddy. Let me explain the details of this code.
  • #35 This code uses rebase() method, instead of previous subclass() method. Rebase() method doesn't create a subclass. It replaces the original implementation with new one and saves it under a different method name.
  • #36 Byte Buddy provides three method to enhance existing methods. Subclass() creates a subclass and overrides the method. Redefine() replaces the implementation with new one. Rebase() also replaces it and saves the original implementation.
  • #37 Note that we can't use a class literal. If we use a class literal for the Target class, the JVM will load the class before changing the class. We can use the TypePool class and its describe() method, to represent the class without the class literal.
  • #38 Also, note that when using unloaded classes, we need to specify a ClassFileLocator. It allows to locate a class file. In this example, we simply create a class file locator with ofClassPath() method. It scans the running application's class path for class files.
  • #39 This time I'd like to delegate outputting to another class, LoggingInterceptor class.
  • #40 To do so, We can use MethodDelegation.to() method. We simply specify a class to delegate. A best match method is chosen automatically by Byte Buddy.
  • #41 After the method is invoked, the original call will continue, thanks to andThen() method with SuperMethodCall.INSTANCE. SuperMethodCall.INSTANCE represents the original method.
  • #42 This is the interceptor class. This code outputs the intecepted method name.
  • #43 In delegated method, various annotations can be used. The Origin annotation is a reference to the original method or constructor. So we can get the method object of the original method.
  • #44 Other annotations are shown here. More details can be found in the Byte Buddy's website.
  • #45 Go back to the premain(). How do we run that code in Java Agent? Instrument API provides ClassFileTransformer class to change bytecode. To manipulate bytecode in Java Agent, We need to override transform(). Then pass the ClassFileTransformer instance to addTransformer() of the Instrumentation class. The transform() will be called by the JVM before loading.
  • #46 Let's go on to the transform() method. We can call the manipulation code in transform() method.
  • #47 In parameters, class name is the name to be loaded. Class file buffer is the content of the class file.
  • #48 We return the class file content from the transform().
  • #49 Finally we can run the demo. Let's see the complete code. (Show LoggingAgent class) (run Target class) OK, foo() is intercepted.
  • #50 So we covered the basics of Byte Buddy and Java Agent.
  • #51 Of course, there are many features in Byte Buddy. Add fields and methods, manipulate even stacks. Also, we can generate CONDY code with Byte Buddy. Have you ever heard about condy? CONDY means constant dynamic. It defines as JEP 309. It's included from Java 11, but the Java compiler doesn't use CONDY yet. We can try to use CONDY directly with Byte Buddy.
  • #52 Acutually Byte Buddy provides AgentBuilder class to simplify Java Agent. We can rewrite the previous Logging Agent with AgentBuilder.
  • #53 Just use AgentBuilder class instead of ByteBuddy class, and use AgentBuilder.Transformer instead of ClassFileTransformer. Call install() method at the end.
  • #54 Use AgentBuilder, Use AgentBuilder.Transformer and call installOn().
  • #55 Then We can use builder object to intercept the method. Let's see the complete code. (show LoggingAgent class) This code is more readable, isn't it? (mvn package & run Target class)
  • #56 Additionally, when we use AgentBuilder, we can remove javaagent option. Just call ByteBuddyAgent's install() and AgentBuilder's installOnByteBuddyAgent() instead of installOn() method. To use ByteBuddyAgent, we need to add byte-buddy-agent JAR file to the class path.
  • #57 Call ByteBuddyAgent.install() and AgentBuidler's installOnByteBuddyAgent().
  • #58 (show LoggingMainWithoutAgent class) Run this code without javaagent option. (run LoggingMainWithoutAgent)
  • #59 Let's wrap up. Bytecode manipulation is fun, and it's close to us. I'd like to stress this point. Many famous libraries are using bytecode manipulation. As application examples, there are How Swap, which swap classes at runtime, and advanced dynamic proxy.