DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports Events Over 2 million developers have joined DZone. Join Today! Thanks for visiting DZone today,
Edit Profile Manage Email Subscriptions Moderation Admin Console How to Post to DZone Article Submission Guidelines
View Profile
Sign Out
Refcards
Trend Reports
Events
Zones
Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Partner Zones AWS Cloud
by AWS Developer Relations
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Partner Zones
AWS Cloud
by AWS Developer Relations
The Latest "Software Integration: The Intersection of APIs, Microservices, and Cloud-Based Systems" Trend Report
Get the report
  1. DZone
  2. Coding
  3. Java
  4. Java Bytecode: Journey to the Wonderland (Part 3)

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.

A N M Bazlur Rahman user avatar by
A N M Bazlur Rahman
CORE ·
Mar. 06, 23 · Tutorial
Like (2)
Save
Tweet
Share
3.19K Views

Join the DZone community and get the full member experience.

Join For Free

Our 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.

Java
 
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.

Java
 
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.

Java
 
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.

Java
 
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:

Java
 
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:

Java
 
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:

  1. ASM: A fast, small, and efficient Java bytecode manipulation framework
  2. BCEL: A library for manipulating Java bytecode in the Apache Commons project
  3. Javassist: A bytecode manipulation library for Java
  4. Byte Buddy: A library for generating and modifying Java bytecode
  5. 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:

  1. 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.
  2. DashO: A commercial obfuscation and optimization program that includes control flow obfuscation, string encryption, and watermarking
  3. Zelix KlassMaster: A paid program that provides extensive obfuscation and protection capabilities, such as control flow obfuscation, string encryption, and class and member renaming
  4. Allatori: A commercial product that provides extensive obfuscation and security features such as control flow obfuscation, string encryption, and class and member renaming. 
  5. 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.

Java bytecode Java (programming language) Obfuscation (software)

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.

Popular on DZone

  • gRPC on the Client Side
  • 11 Observability Tools You Should Know
  • Chaos Engineering Tutorial: Comprehensive Guide With Best Practices
  • Cloud Performance Engineering

Comments

Partner Resources

X

ABOUT US

  • About DZone
  • Send feedback
  • Careers
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 600 Park Offices Drive
  • Suite 300
  • Durham, NC 27709
  • support@dzone.com
  • +1 (919) 678-0300

Let's be friends: