Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction to bytekin

What is bytekin?

bytekin is a lightweight, easy-to-use Java bytecode transformation framework built on top of ASM. It allows developers to programmatically modify Java classes at the bytecode level, enabling powerful code injection, method interception, and transformation capabilities.

bytekin is designed to be:

  • Simple: Intuitive annotation-based API
  • Lightweight: Minimal dependencies (only ASM)
  • Flexible: Multiple transformation strategies
  • Powerful: Support for complex bytecode manipulations

Key Features

  • Inject: Insert custom code at specific points in methods (head, return, etc.)
  • Invoke: Intercept method calls and modify their behavior
  • Redirect: Change method call targets
  • Constant Modification: Modify constant values in bytecode
  • Variable Modification: Manipulate local variables
  • Mapping Support: Transform class and method names automatically
  • Builder Pattern: Fluent API for constructing transformers

Use Cases

bytekin is ideal for scenarios where you need to:

  1. Add logging or monitoring to existing classes without modifying source code
  2. Implement AOP (Aspect-Oriented Programming) without framework overhead
  3. Add security checks at runtime
  4. Modify third-party library behavior at the bytecode level
  5. Implement mocking or stubbing for testing
  6. Instrument code for profiling or analytics
  7. Apply cross-cutting concerns to multiple classes

Why bytekin?

Unlike full-featured frameworks, bytekin is:

  • Minimal: Only depends on ASM, no heavy dependencies
  • Direct: Works with bytecode directly without reflection overhead
  • Flexible: Supports both annotation-based and programmatic approaches
  • Fast: Efficient bytecode manipulation at JVM load time

Project Structure

The bytekin project is organized into several modules:

  • Transformation Engine: Core bytecode manipulation logic
  • Injection System: Method injection capabilities
  • Mapping System: Class and method name mapping support
  • Utilities: Helper classes for bytecode manipulation

Next Steps

Getting Started with bytekin

This section will guide you through setting up bytekin and creating your first bytecode transformation.

Prerequisites

  • Java 8 or higher
  • Basic understanding of Java
  • Maven or Gradle (for dependency management)

Installation

Maven

Add the following to your pom.xml:

<dependency>
    <groupId>io.github.brqnko.bytekin</groupId>
    <artifactId>bytekin</artifactId>
    <version>1.0</version>
</dependency>

Gradle

Add the following to your build.gradle:

dependencies {
    implementation 'io.github.brqnko.bytekin:bytekin:1.0'
}

Your First Transformation

Step 1: Create a Hook Class

Create a class with the @ModifyClass annotation that defines how you want to transform a target class:

import io.github.brqnko.bytekin.injection.ModifyClass;
import io.github.brqnko.bytekin.injection.Inject;
import io.github.brqnko.bytekin.injection.At;
import io.github.brqnko.bytekin.data.CallbackInfo;

@ModifyClass("com.example.Calculator")
public class CalculatorHooks {

    @Inject(methodName = "add", methodDesc = "(II)I", at = At.HEAD)
    public static CallbackInfo onAddHead(int a, int b) {
        System.out.println("Adding " + a + " + " + b);
        return CallbackInfo.empty();
    }
}

Step 2: Create the Transformer

Instantiate a BytekinTransformer with your hook class:

BytekinTransformer transformer = new BytekinTransformer.Builder(CalculatorHooks.class)
    .build();

Step 3: Transform Your Classes

Apply the transformation to the target class bytecode:

byte[] originalBytecode = loadClassBytecode("com.example.Calculator");
byte[] transformedBytecode = transformer.transform("com.example.Calculator", originalBytecode);

Step 4: Use the Transformed Class

Load the transformed bytecode into your JVM using a custom ClassLoader:

ClassLoader loader = new ByteArrayClassLoader(transformedBytecode);
Class<?> transformedClass = loader.loadClass("com.example.Calculator");

Result

The transformed class will have logging added to the add method:

// Original code
public class Calculator {
    public int add(int a, int b) {
        return a + b;
    }
}

// Transformed code
public class Calculator {
    public int add(int a, int b) {
        System.out.println("Adding " + a + " + " + b);  // Injected!
        return a + b;
    }
}

Next Steps

Installation Guide

Using Maven

1. Add Dependency

Edit your pom.xml file and add the following dependency:

<dependency>
    <groupId>io.github.brqnko.bytekin</groupId>
    <artifactId>bytekin</artifactId>
    <version>1.0</version>
</dependency>

2. Update Your Project

Run Maven to download dependencies:

mvn clean install

Using Gradle

1. Add Dependency

Edit your build.gradle file and add:

dependencies {
    implementation 'io.github.brqnko.bytekin:bytekin:1.0'
}

2. Sync Your Project

For Android Studio or IntelliJ IDEA, you can sync Gradle manually. For command line:

./gradlew build

Dependency Requirements

bytekin has minimal dependencies:

DependencyVersionPurpose
ASM9.7.1+Bytecode manipulation library
Java8+Runtime environment

Verification

After installation, verify that bytekin is correctly set up by running a simple test:

import io.github.brqnko.bytekin.transformer.BytekinTransformer;

public class ByitekVer {
    public static void main(String[] args) {
        BytekinTransformer transformer = new BytekinTransformer.Builder()
            .build();
        System.out.println("bytekin is ready to use!");
    }
}

Troubleshooting Installation

Maven: Dependency not found

  • Ensure you're connected to the internet
  • Try running mvn clean and then mvn install again
  • Check if the repository is accessible

Gradle: Build fails

  • Run ./gradlew clean first
  • Check your Gradle wrapper version
  • Verify Java version compatibility

Next Steps

Your First Transformation

In this guide, we'll create a complete example demonstrating a simple bytecode transformation.

Example: Adding Logging to a Calculator

Step 1: Create the Target Class

First, let's create a simple calculator class that we'll transform:

package com.example;

public class Calculator {
    public int add(int a, int b) {
        return a + b;
    }

    public int subtract(int a, int b) {
        return a - b;
    }

    public int multiply(int a, int b) {
        return a * b;
    }
}

Step 2: Create Hook Methods

Create a class annotated with @ModifyClass that defines what you want to inject:

package com.example;

import io.github.brqnko.bytekin.injection.ModifyClass;
import io.github.brqnko.bytekin.injection.Inject;
import io.github.brqnko.bytekin.injection.At;
import io.github.brqnko.bytekin.data.CallbackInfo;

@ModifyClass("com.example.Calculator")
public class CalculatorHooks {

    @Inject(
        methodName = "add",
        methodDesc = "(II)I",
        at = At.HEAD
    )
    public static CallbackInfo logAdd(int a, int b) {
        System.out.println("Computing: " + a + " + " + b);
        return CallbackInfo.empty();
    }

    @Inject(
        methodName = "multiply",
        methodDesc = "(II)I",
        at = At.HEAD
    )
    public static CallbackInfo logMultiply(int a, int b) {
        System.out.println("Computing: " + a + " * " + b);
        return CallbackInfo.empty();
    }
}

Step 3: Build the Transformer

package com.example;

import io.github.brqnko.bytekin.transformer.BytekinTransformer;

public class TransformerSetup {
    public static BytekinTransformer createTransformer() {
        return new BytekinTransformer.Builder(CalculatorHooks.class)
            .build();
    }
}

Step 4: Apply the Transformation

package com.example;

import io.github.brqnko.bytekin.transformer.BytekinTransformer;

public class Main {
    public static void main(String[] args) {
        // Get original bytecode
        byte[] originalBytecode = getClassBytecode("com.example.Calculator");

        // Create transformer
        BytekinTransformer transformer = TransformerSetup.createTransformer();

        // Apply transformation
        byte[] transformedBytecode = transformer.transform(
            "com.example.Calculator",
            originalBytecode
        );

        // Load transformed class
        Calculator calc = loadTransformedClass(transformedBytecode);

        // Use the transformed class
        int result = calc.add(5, 3);
        // Output: "Computing: 5 + 3" then "8"

        result = calc.multiply(4, 7);
        // Output: "Computing: 4 * 7" then "28"
    }

    // Helper to get class bytecode (pseudo code)
    static byte[] getClassBytecode(String className) {
        // Implementation depends on your classloader setup
        return new byte[]{};
    }

    // Helper to load transformed class (pseudo code)
    static Calculator loadTransformedClass(byte[] bytecode) {
        // Load using custom ClassLoader
        return null;
    }
}

What Happened?

The transformation process:

  1. Scanned CalculatorHooks for methods with @Inject annotation
  2. Found injections targeting com.example.Calculator
  3. Modified the Calculator class bytecode to call our hook methods
  4. Inserted the logging code at the head of specified methods

Before and After

Before Transformation:

public int add(int a, int b) {
    return a + b;
}

After Transformation:

public int add(int a, int b) {
    // Injected code
    com.example.CalculatorHooks.logAdd(a, b);
    // Original code
    return a + b;
}

Next Steps

Core Concepts

This section explains the fundamental concepts behind bytekin and bytecode transformation.

What is Bytecode?

Java source code is compiled into bytecode - a platform-independent intermediate representation that runs on the Java Virtual Machine (JVM). Unlike source code, bytecode is not human-readable but is standardized and can be manipulated programmatically.

Bytecode Structure

Java bytecode consists of:

  • Constant Pool: String literals, method references, field references
  • Methods: Compiled methods with instruction sequences
  • Fields: Class properties
  • Attributes: Metadata like line numbers, local variables, exceptions

What is Bytecode Transformation?

Bytecode transformation is the process of reading, analyzing, and modifying bytecode after compilation but before class loading. This allows you to alter class behavior without modifying source code.

Use Cases

  1. Runtime Monitoring: Add logging without changing source
  2. Cross-Cutting Concerns: Apply behavior to multiple classes
  3. Testing: Mock or stub methods
  4. Security: Inject security checks
  5. Performance: Add profiling instrumentation

How bytekin Fits In

bytekin simplifies bytecode transformation by:

  1. Abstracting ASM Complexity: Provides a clean API over raw ASM
  2. Annotation-Based Configuration: Use Java annotations to define transformations
  3. Multiple Strategies: Support for injections, method interception, and redirects
  4. Mapping Support: Handle obfuscated or renamed classes
  5. Flexible Builder Pattern: Programmatically compose transformations

The Transformation Pipeline

Target Class Bytecode
        ↓
   BytekinTransformer
        ↓
  Scan Hook Classes
        ↓
  Apply Transformations
        ↓
   Modified Bytecode

Key Components

Hook Classes

Classes annotated with @ModifyClass that define how to transform target classes. They contain methods with transformation annotations.

Transformers

Objects that apply transformations to bytecode. bytekin provides BytekinTransformer which handles the entire transformation process.

Annotations

Special markers in Java code that define transformation behavior:

  • @ModifyClass: Mark the target class
  • @Inject: Insert code at specific points
  • @Invoke: Intercept method calls
  • And more...

CallbackInfo

A data structure that controls transformation behavior:

  • cancelled: Whether to skip original code
  • returnValue: Custom return value
  • modifyArgs: Modified method arguments

Important Concepts

Method Descriptors

Method descriptors describe method signatures in JVM format:

(ParameterTypes)ReturnType

Examples:

  • (II)I - Takes two ints, returns int
  • (Ljava/lang/String;)V - Takes String, returns void
  • ()Ljava/lang/String; - Takes nothing, returns String

Class Names

In bytekin, class names use dot notation:

  • Java notation: java.lang.String
  • Internal notation: java/lang/String
  • bytekin uses Java notation

Next Steps

Bytecode Basics

What is Java Bytecode?

Java source code (.java files) is compiled into Java bytecode (contained in .class files). This bytecode is a platform-independent intermediate language that the Java Virtual Machine (JVM) understands.

Source Code vs Bytecode

Java Source Code:

public class Example {
    public void greet(String name) {
        System.out.println("Hello, " + name);
    }
}

Compiled Bytecode (Conceptual):

aload_0
aload_1
invokedynamic <concat>
getstatic System.out
swap
invokevirtual println
return

The bytecode version is more verbose and operating-system independent.

Why is Bytecode Important?

  1. Platform Independence: Works on any system with a JVM
  2. Runtime Flexibility: Can be transformed before loading
  3. Security: JVM can verify bytecode correctness
  4. Optimization: JVM can JIT-compile to native code
  5. Introspection: Tools can analyze bytecode without source code

Reading Bytecode

Tools for Inspection

  • javap: Built-in Java disassembler
  • Bytecode Viewer: GUI tool for inspecting bytecode
  • ASM Tree View: Plugin for IDE visualization

Example: Using javap

javap -c Example.class

Output shows the bytecode instructions for each method.

Common Bytecode Instructions

Some frequently encountered bytecode instructions:

InstructionPurpose
aloadLoad object reference
iloadLoad integer
invoke*Call methods (static, virtual, etc)
returnReturn from method
getstaticRead static field
putstaticWrite static field
newCreate new object

Method Descriptors (Signatures)

Bytecode uses a special notation for method signatures:

(ParameterTypes)ReturnType

Primitive Types

  • Z - boolean
  • B - byte
  • C - char
  • S - short
  • I - int
  • J - long
  • F - float
  • D - double
  • V - void

Object Types

  • Ljava/lang/String; - String class
  • L...classname...; - Any class

Array Types

  • [I - int array
  • [Ljava/lang/String; - String array

Examples

  • (II)I - Adds two integers: int method(int a, int b) { return ...; }
  • (Ljava/lang/String;)V - Takes string, returns nothing: void method(String s) { ... }
  • ()Ljava/lang/String; - No parameters, returns string: String method() { ... }
  • ([Ljava/lang/String;)V - Takes string array: void method(String[] args) { ... }

Class References

Classes are referenced using their fully qualified names with / separators:

  • java/lang/String
  • java/util/ArrayList
  • com/mycompany/MyClass

In bytekin, we typically use the standard Java notation with dots:

  • java.lang.String
  • java.util.ArrayList
  • com.mycompany.MyClass

The Class File Format

A compiled .class file contains:

  1. Magic Number: Identifies it as a class file (0xCAFEBABE)
  2. Version: Java version information
  3. Constant Pool: Strings, method names, field names, type information
  4. Access Flags: public, final, abstract, etc.
  5. This Class: Class name
  6. Super Class: Parent class
  7. Interfaces: Implemented interfaces
  8. Fields: Class member variables
  9. Methods: Methods with their bytecode
  10. Attributes: Additional metadata

Important Notes

  • Bytecode is not human-readable but is systematic and analyzable
  • Every Java source construct maps to bytecode
  • Bytecode is verifiable - JVM checks correctness before execution
  • Bytecode can be manipulated programmatically without source code

Next Steps

How bytekin Works

This document explains the internal mechanisms of bytekin and how it transforms bytecode.

The Transformation Process

Step 1: Initialization

Define Hook Classes
        ↓
Create BytekinTransformer.Builder
        ↓
Pass hook classes to Builder

Example:

BytekinTransformer transformer = new BytekinTransformer.Builder(
    CalculatorHooks.class,
    StringHooks.class
).build();

Step 2: Analysis

When build() is called, bytekin:

  1. Scans hook classes for annotations
  2. Extracts transformation rules
  3. Validates method signatures
  4. Prepares transformation strategy
  5. Creates BytekinClassTransformer instances
Builder.build()
    ↓
Scan @ModifyClass annotations
    ↓
Extract @Inject, @Invoke, etc.
    ↓
Create transformers map
    ↓
Return BytekinTransformer

Step 3: Transformation

When transform() is called:

byte[] transformed = transformer.transform("com.example.Calculator", bytecode);

bytekin:

  1. Looks up the target class
  2. Finds matching transformer
  3. Uses ASM to parse bytecode
  4. Applies all registered transformations
  5. Returns modified bytecode
Target Bytecode
    ↓
ASM ClassReader
    ↓
BytekinClassVisitor
    ↓
Apply Injections
    ↓
Apply Invocations
    ↓
Apply Redirects
    ↓
Apply Constant Modifications
    ↓
Apply Variable Modifications
    ↓
ASM ClassWriter
    ↓
Modified Bytecode

Architecture Overview

Core Components

┌─────────────────────────────────────┐
│   BytekinTransformer (Main API)     │
└──────────────┬──────────────────────┘
               │
        ┌──────┴──────┐
        ↓             ↓
    Builder      transform()
        │             │
        └──────┬──────┘
               ↓
   ┌───────────────────────────┐
   │  BytekinClassTransformer  │
   └───────────┬───────────────┘
               ↓
   ┌───────────────────────────┐
   │   BytekinClassVisitor     │
   │  (ASM ClassVisitor)       │
   └───────────┬───────────────┘
               ↓
   ┌───────────────────────────┐
   │  BytekinMethodVisitor     │
   │  (ASM MethodVisitor)      │
   └───────────────────────────┘

Visitor Pattern

bytekin uses the Visitor Pattern (from ASM):

┌─ ClassVisitor
│   └─ Method Visitor
│       └─ Code Visitor
│           └─ Instruction Handlers

As ASM parses bytecode, it calls methods on visitors to notify them of each element (class, method, field, instruction, etc.).

Transformation Types

1. Injection (Code Insertion)

Goal: Insert code at specific method points

Method Bytecode
    ↓
Find injection point
    ↓
Insert call to hook method
    ↓
Continue with original code

Example locations:

  • At.HEAD: Before method body
  • At.RETURN: Before return statements
  • At.TAIL: At method end

2. Invocation (Method Call Interception)

Goal: Intercept method calls within a method

Method Bytecode
    ↓
Find target method invocation
    ↓
Call hook method with same arguments
    ↓
Optionally modify arguments
    ↓
Make method call (or skip it)

3. Redirect (Call Target Change)

Goal: Change which method is called

Method Call to A()
    ↓
Intercept call
    ↓
Redirect to B()

4. Constant Modification

Goal: Change constant values

Load Constant X
    ↓
Replace with Constant Y

5. Variable Modification

Goal: Modify local variable values

Local Variable at index N
    ↓
Load from slot
    ↓
Apply modification
    ↓
Store back

Key Data Structures

Injection

Represents a code injection:

  • Target Method: Which method to inject into
  • Hook Method: Which hook method to call
  • Location: Where to inject (HEAD, RETURN, etc.)
  • Parameters: What parameters to pass

Invocation

Represents a method call interception:

  • Target Method: Which method calls the target
  • Target Call: Which call to intercept
  • Hook Method: Which hook to call
  • Shift: Before or after the call

CallbackInfo

Controls injection behavior:

public class CallbackInfo {
    public boolean cancelled;      // Cancel execution?
    public Object returnValue;     // Custom return?
    public Object[] modifyArgs;    // Modified arguments?
}

Mapping System

bytekin supports class/method name mappings for obfuscated code:

Original Name     Mapped Name
     ↓                  ↓
  a.class  ────→  com.example.Calculator
  b(II)I   ────→  add(II)I

Mappings are applied during transformation:

Hook class references "com.example.Calculator"
    ↓
Apply mapping
    ↓
Look for mapped name in bytecode
    ↓
Transform accordingly

Thread Safety

  • BytekinTransformer: Thread-safe after build()
  • Builder: Not thread-safe during configuration
  • transform(): Can be called concurrently from multiple threads

Performance Considerations

Efficiency

  • One-time cost: Building transformers
  • Transform time: Proportional to bytecode size
  • Runtime overhead: Only injected code is executed

Optimization Tips

  1. Build transformers once, reuse them
  2. Use transformation early in classloading
  3. Minimize hook method complexity
  4. Profile to identify bottlenecks

Next Steps

Features Overview

bytekin provides several powerful transformation features to manipulate Java bytecode. This section provides an overview of each feature.

Available Features

1. Inject - Code Insertion

Insert custom code at specific points in methods without modifying source code.

Use Cases:

  • Add logging statements
  • Implement cross-cutting concerns
  • Add security checks
  • Validate parameters

Example:

@Inject(methodName = "calculate", methodDesc = "(II)I", at = At.HEAD)
public static CallbackInfo logStart(int a, int b) {
    System.out.println("Starting calculation");
    return CallbackInfo.empty();
}

Learn More: Inject Transformation

2. Invoke - Method Call Interception

Intercept method calls and optionally modify arguments or return values.

Use Cases:

  • Intercept specific method calls
  • Modify method arguments
  • Mock or stub methods
  • Add pre/post processing

Example:

@Invoke(
    targetMethodName = "process",
    targetMethodDesc = "(Ljava/lang/String;)V",
    invokeMethodName = "validate",
    invokeMethodDesc = "(Ljava/lang/String;)V",
    shift = Shift.BEFORE
)
public static CallbackInfo validateBefore(String input) {
    return new CallbackInfo(false, null, new Object[]{input.trim()});
}

Learn More: Invoke Transformation

3. Redirect - Method Call Redirection

Change which method is called at runtime.

Use Cases:

  • Redirect calls to alternative implementations
  • Mock method behavior
  • Implement method forwarding
  • Change behavior based on conditions

Example:

@Redirect(
    targetMethodName = "oldMethod",
    targetMethodDesc = "(I)V",
    redirectMethodName = "newMethod",
    redirectMethodDesc = "(I)V"
)
public static void redirectCall(int value) {
    System.out.println("Redirected to new method: " + value);
}

Learn More: Redirect Transformation

4. Constant Modification

Modify constant values embedded in bytecode.

Use Cases:

  • Change hardcoded configuration values
  • Modify string literals
  • Change numeric constants
  • Patch constants at runtime

Example:

@ModifyConstant(
    methodName = "getVersion",
    oldValue = "1.0",
    newValue = "2.0"
)
public static CallbackInfo updateVersion() {
    return CallbackInfo.empty();
}

Learn More: Constant Modification

5. Variable Modification

Modify local variable values within methods.

Use Cases:

  • Sanitize inputs
  • Transform data
  • Debug variable values
  • Implement custom logic

Example:

@ModifyVariable(
    methodName = "process",
    variableIndex = 1
)
public static void transformVariable(int original) {
    // Transformation logic
}

Learn More: Variable Modification

Combining Features

You can use multiple features together for complex transformations:

@ModifyClass("com.example.Service")
public class ServiceHooks {
    
    // Inject logging
    @Inject(methodName = "handle", methodDesc = "(Ljava/lang/String;)V", at = At.HEAD)
    public static CallbackInfo logStart(String input) {
        System.out.println("Processing: " + input);
        return CallbackInfo.empty();
    }
    
    // Intercept internal calls
    @Invoke(
        targetMethodName = "handle",
        targetMethodDesc = "(Ljava/lang/String;)V",
        invokeMethodName = "validate",
        invokeMethodDesc = "(Ljava/lang/String;)V",
        shift = Shift.BEFORE
    )
    public static CallbackInfo validateInput(String input) {
        return new CallbackInfo(false, null, new Object[]{sanitize(input)});
    }
    
    private static String sanitize(String input) {
        return input.trim().toLowerCase();
    }
}

Choosing the Right Feature

FeaturePurposeComplexity
InjectInsert code at method pointsLow
InvokeIntercept specific callsMedium
RedirectChange call targetMedium
Constant ModificationChange hardcoded valuesLow
Variable ModificationTransform local variablesHigh

Next Steps

Inject Transformation

The @Inject annotation allows you to insert custom code at specific points in target methods.

Basic Usage

@ModifyClass("com.example.Calculator")
public class CalculatorHooks {

    @Inject(
        methodName = "add",
        methodDesc = "(II)I",
        at = At.HEAD
    )
    public static CallbackInfo onAddStart(int a, int b) {
        System.out.println("Adding: " + a + " + " + b);
        return CallbackInfo.empty();
    }
}

Annotation Parameters

methodName (required)

The name of the target method to inject into.

methodName = "add"

methodDesc (required)

The JVM descriptor of the target method signature.

methodDesc = "(II)I"  // int add(int a, int b)

See Method Descriptors for details.

at (required)

Where to inject the code within the method.

At Enum - Injection Points

At.HEAD

Inject at the very beginning of the method, before any existing code.

@Inject(methodName = "calculate", methodDesc = "()I", at = At.HEAD)
public static CallbackInfo atMethodStart() {
    System.out.println("Method started");
    return CallbackInfo.empty();
}

Result:

public int calculate() {
    System.out.println("Method started");  // Injected
    // Original code here
}

At.RETURN

Inject before every return statement in the method.

@Inject(methodName = "getValue", methodDesc = "()I", at = At.RETURN)
public static CallbackInfo beforeReturn(CallbackInfo ci) {
    System.out.println("Returning: " + ci.returnValue);
    return CallbackInfo.empty();
}

Result:

public int getValue() {
    if (condition) {
        System.out.println("Returning: " + value);  // Injected
        return value;
    }
    
    System.out.println("Returning: " + defaultValue);  // Injected
    return defaultValue;
}

At.TAIL

Inject at the very end of the method, after all code but before implicit return.

@Inject(methodName = "cleanup", methodDesc = "()V", at = At.TAIL)
public static CallbackInfo atMethodEnd() {
    System.out.println("Cleanup complete");
    return CallbackInfo.empty();
}

Hook Method Parameters

Hook methods receive the same parameters as the target method, plus CallbackInfo:

// Target method:
public String process(String input, int count) { ... }

// Hook method:
@Inject(methodName = "process", methodDesc = "(Ljava/lang/String;I)Ljava/lang/String;", at = At.HEAD)
public static CallbackInfo processHook(String input, int count, CallbackInfo ci) {
    // Can access parameters
    return CallbackInfo.empty();
}

CallbackInfo - Controlling Behavior

The CallbackInfo object allows you to control how the injection behaves:

public class CallbackInfo {
    public boolean cancelled;      // Skip original code?
    public Object returnValue;     // Return custom value?
}

Cancelling Execution

Skip the original method and return early:

@Inject(methodName = "authenticate", methodDesc = "(Ljava/lang/String;)Z", at = At.HEAD)
public static CallbackInfo checkPermission(String user, CallbackInfo ci) {
    if (!user.equals("admin")) {
        ci.cancelled = true;
        ci.returnValue = false;  // Return false without running original code
    }
    return ci;
}

Custom Return Values

Return a custom value instead of the original result:

@Inject(methodName = "getCached", methodDesc = "()Ljava/lang/Object;", at = At.HEAD)
public static CallbackInfo useCachedValue(CallbackInfo ci) {
    Object cached = getFromCache();
    if (cached != null) {
        ci.cancelled = true;
        ci.returnValue = cached;
    }
    return ci;
}

Multiple Injections

You can inject into the same method multiple times:

@ModifyClass("com.example.Service")
public class ServiceHooks {

    @Inject(methodName = "handle", methodDesc = "(Ljava/lang/String;)V", at = At.HEAD)
    public static CallbackInfo logStart(String input) {
        System.out.println("Start: " + input);
        return CallbackInfo.empty();
    }

    @Inject(methodName = "handle", methodDesc = "(Ljava/lang/String;)V", at = At.RETURN)
    public static CallbackInfo logEnd(String input) {
        System.out.println("End: " + input);
        return CallbackInfo.empty();
    }
}

Both injections will be applied.

Instance Methods vs Static Methods

For instance methods, the first parameter is usually this (or the object instance):

// Target instance method:
public class Calculator {
    public int add(int a, int b) { return a + b; }
}

// Hook can receive 'this':
@Inject(methodName = "add", methodDesc = "(II)I", at = At.HEAD)
public static CallbackInfo onAdd(Calculator self, int a, int b) {
    System.out.println("Calculator instance: " + self);
    return CallbackInfo.empty();
}

Best Practices

  1. Keep hooks simple: Complex logic should be in separate methods
  2. Avoid exceptions: Handle exceptions within hook methods
  3. Use At.HEAD for guards: Check conditions early
  4. Be careful with At.RETURN: Multiple returns need handling
  5. Test thoroughly: Verify injections work correctly

Examples

See Examples - Inject for complete working examples.

Next Steps

Invoke Transformation

The @Invoke annotation allows you to intercept method calls and optionally modify their arguments before execution.

Basic Usage

@ModifyClass("com.example.DataProcessor")
public class ProcessorHooks {

    @Invoke(
        targetMethodName = "process",
        targetMethodDesc = "(Ljava/lang/String;)V",
        invokeMethodName = "validate",
        invokeMethodDesc = "(Ljava/lang/String;)V",
        shift = Shift.BEFORE
    )
    public static CallbackInfo validateBeforeProcess(String data) {
        if (data == null || data.isEmpty()) {
            return new CallbackInfo(true, null, new Object[]{"default"});
        }
        return CallbackInfo.empty();
    }
}

Annotation Parameters

targetMethodName (required)

The method name that contains the call to intercept.

targetMethodName = "process"

targetMethodDesc (required)

The JVM descriptor of the method that contains the call.

targetMethodDesc = "(Ljava/lang/String;)V"

invokeMethodName (required)

The name of the method being called (the one you want to intercept).

invokeMethodName = "helper"

invokeMethodDesc (required)

The JVM descriptor of the method being called.

invokeMethodDesc = "(I)Ljava/lang/String;"

shift (required)

When to execute the hook relative to the method call.

Shift Enum - Timing

Shift.BEFORE

Execute the hook before the method is called.

@Invoke(
    targetMethodName = "process",
    targetMethodDesc = "(Ljava/lang/String;)V",
    invokeMethodName = "validate",
    invokeMethodDesc = "(Ljava/lang/String;)V",
    shift = Shift.BEFORE
)
public static CallbackInfo beforeCall(String input) {
    System.out.println("Before calling validate");
    return CallbackInfo.empty();
}

Result:

public void process(String input) {
    System.out.println("Before calling validate");  // Injected
    validate(input);
    // Rest of code
}

Shift.AFTER

Execute the hook after the method is called.

@Invoke(
    targetMethodName = "process",
    targetMethodDesc = "(Ljava/lang/String;)V",
    invokeMethodName = "save",
    invokeMethodDesc = "()V",
    shift = Shift.AFTER
)
public static CallbackInfo afterCall() {
    System.out.println("After calling save");
    return CallbackInfo.empty();
}

Result:

public void process(String input) {
    // Some code
    save();
    System.out.println("After calling save");  // Injected
}

Modifying Arguments

Use CallbackInfo to change the arguments passed to the intercepted call:

@Invoke(
    targetMethodName = "processData",
    targetMethodDesc = "(Ljava/lang/String;I)V",
    invokeMethodName = "transform",
    invokeMethodDesc = "(Ljava/lang/String;I)V",
    shift = Shift.BEFORE
)
public static CallbackInfo sanitizeInput(String data, int count, CallbackInfo ci) {
    // Modify arguments
    String sanitized = data.trim().toLowerCase();
    int newCount = Math.max(0, count);
    
    ci.modifyArgs = new Object[]{sanitized, newCount};
    return ci;
}

Result:

public void processData(String data, int count) {
    // Original: transform(data, count);
    // After hook: transform(data.trim().toLowerCase(), max(0, count));
    transform(data, count);
}

Cancelling Method Calls

Prevent the method from being called:

@Invoke(
    targetMethodName = "deleteFile",
    targetMethodDesc = "(Ljava/lang/String;)Z",
    invokeMethodName = "delete",
    invokeMethodDesc = "(Ljava/io/File;)Z",
    shift = Shift.BEFORE
)
public static CallbackInfo preventDeletion(File file, CallbackInfo ci) {
    if (isSystemFile(file)) {
        // Don't call delete(), return false
        ci.cancelled = true;
        ci.returnValue = false;
    }
    return ci;
}

Handling Return Values

Access and modify return values (with Shift.AFTER):

@Invoke(
    targetMethodName = "getValue",
    targetMethodDesc = "()I",
    invokeMethodName = "compute",
    invokeMethodDesc = "()I",
    shift = Shift.AFTER
)
public static CallbackInfo modifyReturnValue(CallbackInfo ci) {
    // Access the return value
    int original = (int) ci.returnValue;
    
    // Modify it
    ci.returnValue = original * 2;
    
    return ci;
}

Complex Example

@ModifyClass("com.example.UserService")
public class UserServiceHooks {

    // Intercept login attempts
    @Invoke(
        targetMethodName = "authenticate",
        targetMethodDesc = "(Ljava/lang/String;Ljava/lang/String;)Z",
        invokeMethodName = "validateCredentials",
        invokeMethodDesc = "(Ljava/lang/String;Ljava/lang/String;)Z",
        shift = Shift.BEFORE
    )
    public static CallbackInfo logAuthAttempt(
        String username, String password, CallbackInfo ci
    ) {
        // Log the attempt
        System.out.println("Auth attempt for: " + username);
        
        // Block certain usernames
        if (username.equals("blocked")) {
            ci.cancelled = true;
            ci.returnValue = false;
        }
        
        return ci;
    }
}

Best Practices

  1. Understand call flow: Know where the method is called
  2. Consider timing: BEFORE for input validation, AFTER for output transformation
  3. Test argument modification: Ensure types match
  4. Handle cancellation carefully: Ensure calling code handles cancelled calls
  5. Profile performance: Hooks are executed on every call

Limitations

  • Can only intercept explicit method calls, not virtual method invocations from bytecode
  • Cannot intercept calls to constructors directly
  • Performance impact is incurred for every call

Examples

See Examples - Invoke for complete working examples.

Next Steps

Redirect Transformation

The @Redirect annotation allows you to change which method is actually called at runtime.

Basic Usage

@ModifyClass("com.example.LegacyService")
public class LegacyServiceHooks {

    @Redirect(
        targetMethodName = "oldMethod",
        targetMethodDesc = "(I)V",
        redirectMethodName = "newMethod",
        redirectMethodDesc = "(I)V"
    )
    public static void redirectCall(int value) {
        System.out.println("Redirecting call with value: " + value);
    }
}

Annotation Parameters

targetMethodName (required)

The name of the method that contains the call to redirect.

targetMethodName = "process"

targetMethodDesc (required)

The JVM descriptor of the target method.

targetMethodDesc = "(Ljava/lang/String;)V"

redirectMethodName (required)

The name of the new method to call instead.

redirectMethodName = "newImplementation"

redirectMethodDesc (required)

The JVM descriptor of the redirect method.

redirectMethodDesc = "(Ljava/lang/String;)V"

How Redirect Works

Before:

public class LegacyAPI {
    public void oldMethod(int value) {
        // Old implementation
    }
}

public class Client {
    public void use() {
        api.oldMethod(42);  // Calls oldMethod
    }
}

After Redirect:

public class Client {
    public void use() {
        api.newMethod(42);  // Redirected to newMethod!
    }
}

Practical Examples

Migration Strategy

Gradually migrate from old API to new API:

@ModifyClass("com.example.Application")
public class APIRedirection {

    @Redirect(
        targetMethodName = "main",
        targetMethodDesc = "([Ljava/lang/String;)V",
        redirectMethodName = "legacySearch",
        redirectMethodDesc = "(Ljava/lang/String;)Ljava/util/List;",
        from = "oldSearch",
        to = "modernSearch"
    )
    public static void upgradeSearch() {
        // Search calls are now routed to the modern implementation
    }
}

Mocking for Tests

Replace real implementations with test doubles:

@ModifyClass("com.example.DataAccess")
public class TestRedirection {

    @Redirect(
        targetMethodName = "query",
        targetMethodDesc = "(Ljava/lang/String;)Ljava/util/List;",
        redirectMethodName = "fetchFromDatabase",
        redirectMethodDesc = "(Ljava/lang/String;)Ljava/util/List;",
        from = "realDB",
        to = "mockDB"
    )
    public static void useMockDatabase() {
        // All database calls use mocked implementation
    }
}

Performance Optimization

Route to optimized implementations:

@ModifyClass("com.example.Processing")
public class PerformanceOptimization {

    @Redirect(
        targetMethodName = "processLargeList",
        targetMethodDesc = "(Ljava/util/List;)Ljava/util/List;",
        redirectMethodName = "slowImplementation",
        redirectMethodDesc = "(Ljava/util/List;)Ljava/util/List;",
        from = "bruteForce",
        to = "optimized"
    )
    public static void useOptimizedAlgorithm() {
        // Uses fast algorithm instead of slow one
    }
}

Differences from Other Transformations

FeatureInjectInvokeRedirect
What it doesInsert codeIntercept callsChange target
Call happensYesYesYes, but different target
Can skip executionYesYesYes
Use caseAdd loggingModify behaviorAPI migration

Type Compatibility

The redirect method must have compatible signature:

// Original call
search(String query);  // (Ljava/lang/String;)Ljava/util/List;

// Must redirect to compatible signature
newSearch(String query);  // (Ljava/lang/String;)Ljava/util/List;

Type mismatch will cause issues:

// WRONG - Different parameter types
@Redirect(..., from = "process(int)", to = "process(String)")

Performance Considerations

Redirect has minimal overhead compared to normal method calls since:

  1. It's a direct bytecode substitution
  2. No wrapper or proxy is created
  3. The JVM can inline and optimize as normal

Limitations

  • Both methods must have compatible signatures
  • Cannot redirect to final methods
  • Cannot redirect constructor calls (use @Invoke instead)
  • Redirects are static - same target for all calls

Best Practices

  1. Ensure compatibility: Verify method signatures match exactly
  2. Document redirects: Leave comments explaining why
  3. Test redirects: Verify behavior after redirection
  4. Use for migration: Great for moving from old to new APIs
  5. Be cautious: Track all redirects to avoid confusion

Advanced Pattern: Conditional Redirect

While @Redirect is static, you can combine it with @Invoke for conditional behavior:

@Invoke(
    targetMethodName = "search",
    targetMethodDesc = "(Ljava/lang/String;)Ljava/util/List;",
    invokeMethodName = "getResults",
    invokeMethodDesc = "(Ljava/lang/String;)Ljava/util/List;",
    shift = Shift.BEFORE
)
public static CallbackInfo selectImplementation(String query, CallbackInfo ci) {
    if (query.length() > 100) {
        // Use optimized search for large queries
        ci.returnValue = optimizedSearch(query);
        ci.cancelled = true;
    }
    return ci;
}

Next Steps

Constant Modification

Modify hardcoded constant values in bytecode without recompiling source code.

Basic Usage

@ModifyClass("com.example.Config")
public class ConfigHooks {

    @ModifyConstant(
        methodName = "getVersion",
        oldValue = "1.0.0",
        newValue = "2.0.0"
    )
    public static CallbackInfo updateVersion() {
        return CallbackInfo.empty();
    }
}

What Constants Can Be Modified?

  • String literals
  • Numeric constants (int, long, float, double)
  • Boolean constants
  • Constants in the constant pool

String Constants

@ModifyClass("com.example.App")
public class AppHooks {

    @ModifyConstant(
        methodName = "getAPIEndpoint",
        oldValue = "http://localhost:8080",
        newValue = "https://api.production.com"
    )
    public static CallbackInfo updateEndpoint() {
        return CallbackInfo.empty();
    }
}

Before:

public String getAPIEndpoint() {
    return "http://localhost:8080";
}

After:

public String getAPIEndpoint() {
    return "https://api.production.com";
}

Numeric Constants

@ModifyClass("com.example.Limits")
public class LimitsHooks {

    @ModifyConstant(
        methodName = "getMaxConnections",
        oldValue = 10,
        newValue = 100
    )
    public static CallbackInfo increaseLimit() {
        return CallbackInfo.empty();
    }
}

Multiple Constants in Same Method

@ModifyClass("com.example.Config")
public class ConfigHooks {

    @ModifyConstant(
        methodName = "initialize",
        oldValue = "DEBUG",
        newValue = "PRODUCTION"
    )
    public static CallbackInfo updateMode() {
        return CallbackInfo.empty();
    }

    @ModifyConstant(
        methodName = "initialize",
        oldValue = "localhost",
        newValue = "example.com"
    )
    public static CallbackInfo updateHost() {
        return CallbackInfo.empty();
    }
}

Both modifications will be applied to the same method.

Use Cases

Environment Configuration

Change environment-specific values:

@ModifyClass("com.example.Environment")
public class EnvironmentHooks {

    @ModifyConstant(
        methodName = "getDatabaseURL",
        oldValue = "jdbc:mysql://dev.local:3306/db",
        newValue = "jdbc:mysql://prod.remote:3306/db"
    )
    public static CallbackInfo updateDatabase() {
        return CallbackInfo.empty();
    }
}

Feature Flags

Enable/disable features without recompilation:

@ModifyClass("com.example.Features")
public class FeatureHooks {

    @ModifyConstant(
        methodName = "isNewFeatureEnabled",
        oldValue = false,
        newValue = true
    )
    public static CallbackInfo enableFeature() {
        return CallbackInfo.empty();
    }
}

API Versioning

Update API endpoints:

@ModifyClass("com.example.API")
public class APIHooks {

    @ModifyConstant(
        methodName = "getAPIVersion",
        oldValue = "v1",
        newValue = "v3"
    )
    public static CallbackInfo updateAPIVersion() {
        return CallbackInfo.empty();
    }
}

Performance Impact

Constant modification has minimal runtime overhead:

  1. Changes occur during bytecode transformation (one-time)
  2. Runtime performance is identical to recompiled code
  3. JVM can optimize modified constants

Limitations

Cannot Modify

  • Local variable initializations (in some cases)
  • Constants created at runtime
  • Constants already optimized by JVM

Type Matching

The old value and new value must have compatible types:

// CORRECT
@ModifyConstant(
    methodName = "getCount",
    oldValue = 10,        // int
    newValue = 20         // int - compatible
)

// WRONG
@ModifyConstant(
    methodName = "getCount",
    oldValue = 10,        // int
    newValue = "20"       // String - incompatible!
)

Best Practices

  1. Use for configuration: Not for logic changes
  2. Document clearly: Explain why constants are modified
  3. Keep values consistent: Use the exact old value
  4. Test all paths: Verify code works with new values
  5. Avoid type changes: Keep types compatible

Advanced: Conditional Modification

Combine with other transformations for more control:

@ModifyClass("com.example.Service")
public class ServiceHooks {

    @Inject(
        methodName = "initialize",
        methodDesc = "()V",
        at = At.HEAD
    )
    public static CallbackInfo checkEnvironment() {
        String env = System.getProperty("environment");
        if ("production".equals(env)) {
            // Additional setup for production
        }
        return CallbackInfo.empty();
    }

    @ModifyConstant(
        methodName = "getTimeout",
        oldValue = 5000,
        newValue = 30000
    )
    public static CallbackInfo productionTimeout() {
        return CallbackInfo.empty();
    }
}

Next Steps

Variable Modification

Modify local variable values within methods during bytecode transformation.

Basic Usage

@ModifyClass("com.example.Processor")
public class ProcessorHooks {

    @ModifyVariable(
        methodName = "process",
        variableIndex = 1
    )
    public static void sanitizeInput(String original) {
        // Transformation logic
    }
}

Understanding Variable Indices

Local variables in a method are stored in slots, indexed starting from 0:

Instance Methods

public void process(String name, int count) {
    String result;
    // ...
}

Variable indices:

  • 0: this (implicit)
  • 1: name (first parameter)
  • 2: count (second parameter)
  • 3: result (local variable)

Static Methods

public static void process(String name, int count) {
    String result;
    // ...
}

Variable indices:

  • 0: name (first parameter)
  • 1: count (second parameter)
  • 2: result (local variable)

Annotation Parameters

methodName (required)

The name of the target method.

methodName = "process"

variableIndex (required)

The index of the local variable to modify.

variableIndex = 1

Modifying Parameters

Transform method parameters:

@ModifyClass("com.example.UserService")
public class UserServiceHooks {

    @ModifyVariable(
        methodName = "createUser",
        variableIndex = 1  // First parameter: email
    )
    public static void normalizeEmail(String original) {
        // The original email will be normalized
        // e.g., "USER@EXAMPLE.COM" becomes "user@example.com"
    }
}

Before:

public void createUser(String email) {
    // email = "USER@EXAMPLE.COM"
    // ...
}

After:

public void createUser(String email) {
    // email = "user@example.com" (normalized)
    // ...
}

Modifying Local Variables

Transform variables created inside methods:

@ModifyClass("com.example.Calculator")
public class CalculatorHooks {

    @ModifyVariable(
        methodName = "calculateTotal",
        variableIndex = 2  // Local variable: total
    )
    public static void applyTaxToTotal(int original) {
        // total will be multiplied by 1.1 to apply tax
    }
}

Use Cases

Input Sanitization

Clean up method inputs:

@ModifyClass("com.example.WebService")
public class WebServiceHooks {

    @ModifyVariable(
        methodName = "handleRequest",
        variableIndex = 1  // request parameter
    )
    public static void sanitizeRequest(String original) {
        // Removes malicious characters
    }

    @ModifyVariable(
        methodName = "handleRequest",
        variableIndex = 2  // path parameter
    )
    public static void validatePath(String original) {
        // Ensures path doesn't escape root directory
    }
}

Data Transformation

Convert data format:

@ModifyClass("com.example.DateProcessor")
public class DateProcessorHooks {

    @ModifyVariable(
        methodName = "processDate",
        variableIndex = 1  // date parameter
    )
    public static void convertToUTC(String original) {
        // Converts from local time to UTC
    }
}

Type Conversion

Change data types:

@ModifyClass("com.example.Converter")
public class ConverterHooks {

    @ModifyVariable(
        methodName = "process",
        variableIndex = 1  // number parameter
    )
    public static void convertToPercentage(int original) {
        // Converts raw number to percentage
    }
}

Advanced Patterns

Multiple Variables

Modify multiple variables in same method:

@ModifyClass("com.example.Transfer")
public class TransferHooks {

    @ModifyVariable(
        methodName = "transfer",
        variableIndex = 1  // from account
    )
    public static void validateFromAccount(String original) {
        // Validate source account
    }

    @ModifyVariable(
        methodName = "transfer",
        variableIndex = 2  // to account
    )
    public static void validateToAccount(String original) {
        // Validate destination account
    }

    @ModifyVariable(
        methodName = "transfer",
        variableIndex = 3  // amount
    )
    public static void validateAmount(long original) {
        // Ensure amount is positive
    }
}

All three modifications are applied to the same method.

Type Preservation

Variable type is preserved during modification:

@ModifyClass("com.example.Data")
public class DataHooks {

    // Modifying String parameter
    @ModifyVariable(methodName = "processName", variableIndex = 1)
    public static void transformName(String original) { }

    // Modifying int parameter
    @ModifyVariable(methodName = "processCount", variableIndex = 1)
    public static void transformCount(int original) { }

    // Modifying List parameter
    @ModifyVariable(methodName = "processItems", variableIndex = 1)
    public static void transformItems(List<?> original) { }
}

Each hook receives the correct type automatically.

Limitations

Cannot Modify

  • Variables that are never used
  • Variables whose values are optimized away by JVM
  • Variables modified after initialization in complex ways

Challenges

  1. Index calculation: Must correctly identify variable indices
  2. Type safety: Parameter types must match
  3. Scope: Changes only within that method
  4. Debugging: Can be hard to trace modifications

Finding Correct Variable Indices

Use javap to inspect variable layout:

javap -c -private MyClass.class | grep -A 50 "methodName"

Look for LocalVariableTable which shows variable positions.

Best Practices

  1. Document indices: Clearly comment which variable at which index
  2. Keep transformations simple: Complex logic should be separate
  3. Preserve semantics: Ensure modified values make sense
  4. Test thoroughly: Verify behavior with modified variables
  5. Use inspectors: Verify indices are correct before applying

Combining with Other Features

Use variable modification with injections:

@ModifyClass("com.example.Service")
public class ServiceHooks {

    @Inject(
        methodName = "handle",
        methodDesc = "(Ljava/lang/String;)V",
        at = At.HEAD
    )
    public static CallbackInfo validateInput(String input) {
        if (input == null || input.isEmpty()) {
            return new CallbackInfo(true, null, null);
        }
        return CallbackInfo.empty();
    }

    @ModifyVariable(
        methodName = "handle",
        variableIndex = 1  // input parameter
    )
    public static void normalizeInput(String original) {
        // Also normalize the input
    }
}

Next Steps

Advanced Usage

This section covers advanced patterns and techniques for using bytekin effectively.

Programmatic API (Non-Annotation Based)

While annotations are convenient, you can also use the programmatic API:

BytekinTransformer transformer = new BytekinTransformer.Builder()
    .inject("com.example.Calculator", new Injection(
        "add",
        "(II)I",
        At.HEAD,
        Arrays.asList(Parameter.THIS, Parameter.INT, Parameter.INT)
    ))
    .build();

Multiple Hook Classes

Organize hooks into multiple classes and pass them all:

BytekinTransformer transformer = new BytekinTransformer.Builder(
    LoggingHooks.class,
    AuthenticationHooks.class,
    PerformanceHooks.class,
    SecurityHooks.class
).build();

Class Remapping

Handle obfuscated code using mappings:

class MyMappingProvider implements IMappingProvider {
    @Override
    public String getClassName(String name) {
        // Map a.class to com.example.Calculator
        if ("a".equals(name)) return "com.example.Calculator";
        return name;
    }
    
    @Override
    public String getMethodName(String className, String methodName, String descriptor) {
        // Map b to add
        if ("com.example.Calculator".equals(className) && "b".equals(methodName)) {
            return "add";
        }
        return methodName;
    }
    
    @Override
    public String getFieldName(String className, String fieldName) {
        return fieldName;
    }
}

BytekinTransformer transformer = new BytekinTransformer.Builder(MyHooks.class)
    .mapping(new MyMappingProvider())
    .build();

Chaining Transformations

Apply multiple transformations to the same class:

byte[] original = getClassBytecode("com.example.Service");

// First transformation
byte[] step1 = transformer1.transform("com.example.Service", original);

// Second transformation
byte[] step2 = transformer2.transform("com.example.Service", step1);

// Load final result
Class<?> clazz = loadFromBytecode(step2);

Conditional Hook Logic

Execute hooks based on conditions:

@Inject(methodName = "process", methodDesc = "(Ljava/lang/String;)V", at = At.HEAD)
public static CallbackInfo conditionalHook(String input, CallbackInfo ci) {
    // Only inject for certain inputs
    if (input.startsWith("test_")) {
        System.out.println("Test mode: " + input);
    }
    
    // Only inject for certain environments
    String env = System.getProperty("app.env", "dev");
    if ("prod".equals(env)) {
        // Different behavior for production
    }
    
    return ci;
}

Stateful Hooks

Maintain state across hook invocations:

@ModifyClass("com.example.RequestHandler")
public class RequestHooks {
    private static final Map<String, Integer> callCounts = new ConcurrentHashMap<>();
    
    @Inject(methodName = "handle", methodDesc = "(Ljava/lang/String;)V", at = At.HEAD)
    public static CallbackInfo trackCalls(String id, CallbackInfo ci) {
        int count = callCounts.getOrDefault(id, 0);
        callCounts.put(id, count + 1);
        
        if (count > 100) {
            System.out.println("High call count for: " + id);
        }
        
        return ci;
    }
}

Combining Multiple Transformations

Use different transformation types on the same method:

@ModifyClass("com.example.DataService")
public class ServiceHooks {
    
    // Log entry
    @Inject(methodName = "query", methodDesc = "(Ljava/lang/String;)Ljava/util/List;", 
            at = At.HEAD)
    public static CallbackInfo logEntry(String sql) {
        System.out.println("Query: " + sql);
        return CallbackInfo.empty();
    }
    
    // Intercept database calls
    @Invoke(
        targetMethodName = "query",
        targetMethodDesc = "(Ljava/lang/String;)Ljava/util/List;",
        invokeMethodName = "execute",
        invokeMethodDesc = "(Ljava/lang/String;)Ljava/util/List;",
        shift = Shift.BEFORE
    )
    public static CallbackInfo cacheLookup(String sql, CallbackInfo ci) {
        List<?> cached = getFromCache(sql);
        if (cached != null) {
            ci.cancelled = true;
            ci.returnValue = cached;
        }
        return ci;
    }
    
    // Modify constant database URL
    @ModifyConstant(methodName = "getConnection", oldValue = "localhost", 
                    newValue = "db.production.com")
    public static CallbackInfo updateDbLocation() {
        return CallbackInfo.empty();
    }
}

Performance Optimization Pattern

Use hooks for efficient performance monitoring:

@ModifyClass("com.example.CriticalPath")
public class PerformanceHooks {
    private static final int SLOW_THRESHOLD = 1000; // ms
    
    @Inject(methodName = "criticalOperation", methodDesc = "()V", at = At.HEAD)
    public static CallbackInfo startTimer() {
        TIMER.set(System.currentTimeMillis());
        return CallbackInfo.empty();
    }
    
    @Inject(methodName = "criticalOperation", methodDesc = "()V", at = At.RETURN)
    public static CallbackInfo checkTimer() {
        long duration = System.currentTimeMillis() - TIMER.get();
        if (duration > SLOW_THRESHOLD) {
            System.out.println("Slow operation: " + duration + "ms");
        }
        return CallbackInfo.empty();
    }
    
    private static final ThreadLocal<Long> TIMER = ThreadLocal.withInitial(() -> 0L);
}

Security Pattern - Input Validation

Validate all inputs at entry points:

@ModifyClass("com.example.WebController")
public class SecurityHooks {
    
    @Inject(methodName = "handleRequest", methodDesc = "(Ljava/lang/String;)V", 
            at = At.HEAD)
    public static CallbackInfo validateRequest(String request, CallbackInfo ci) {
        if (request == null || isMalicious(request)) {
            ci.cancelled = true;  // Block request
            return ci;
        }
        return ci;
    }
    
    private static boolean isMalicious(String request) {
        // Check for SQL injection, XSS, etc.
        return request.contains("DROP") || request.contains("<script>");
    }
}

Testing Pattern - Mock Objects

Use hooks for dependency injection in tests:

@ModifyClass("com.example.UserService")
public class TestHooks {
    private static UserRepository mockRepository = new MockUserRepository();
    
    @Inject(methodName = "getUserById", methodDesc = "(I)Lcom/example/User;", 
            at = At.HEAD)
    public static CallbackInfo useMockRepository() {
        // Inject mock repository
        return CallbackInfo.empty();
    }
}

A/B Testing Pattern

Route to different implementations based on user:

@ModifyClass("com.example.Algorithm")
public class ABTestingHooks {
    
    @Invoke(
        targetMethodName = "process",
        targetMethodDesc = "(Ljava/lang/Object;)Ljava/lang/Object;",
        invokeMethodName = "compute",
        invokeMethodDesc = "(Ljava/lang/Object;)Ljava/lang/Object;",
        shift = Shift.BEFORE
    )
    public static CallbackInfo selectImplementation(Object data, CallbackInfo ci) {
        // Route to new or old implementation based on user
        if (useNewImplementation(data)) {
            ci.returnValue = computeNew(data);
            ci.cancelled = true;
        }
        return ci;
    }
    
    private static boolean useNewImplementation(Object data) {
        String userId = extractUserId(data);
        int hash = userId.hashCode();
        return hash % 2 == 0;  // 50/50 split
    }
}

Feature Flag Pattern

Enable/disable features without deployment:

@ModifyClass("com.example.Features")
public class FeatureFlagHooks {
    private static final Map<String, Boolean> flags = new ConcurrentHashMap<>();
    
    @Inject(methodName = "newFeature", methodDesc = "()V", at = At.HEAD)
    public static CallbackInfo checkFeatureFlag(CallbackInfo ci) {
        if (!isFeatureEnabled("newFeature")) {
            ci.cancelled = true;  // Skip this method
        }
        return ci;
    }
    
    private static boolean isFeatureEnabled(String feature) {
        return flags.getOrDefault(feature, false);
    }
    
    public static void setFeatureFlag(String feature, boolean enabled) {
        flags.put(feature, enabled);
    }
}

Lazy Initialization Pattern

Defer expensive initialization:

@ModifyClass("com.example.Config")
public class ConfigHooks {
    private static volatile Configuration config;
    
    @Inject(methodName = "getConfig", methodDesc = "()Lcom/example/Configuration;", 
            at = At.HEAD)
    public static CallbackInfo lazyInitialize(CallbackInfo ci) {
        if (config == null) {
            synchronized (ConfigHooks.class) {
                if (config == null) {
                    config = loadConfiguration();  // Expensive operation
                }
            }
        }
        ci.cancelled = true;
        ci.returnValue = config;
        return ci;
    }
}

Next Steps

Mappings

bytekin supports class and method name mappings for working with obfuscated or renamed code.

What Are Mappings?

Mappings translate between human-readable names and bytecode names. This is useful when:

  • Working with obfuscated code
  • Applying transformations to renamed classes
  • Handling version differences
  • Supporting multiple naming conventions

Creating a Mapping Provider

Implement IMappingProvider interface:

public class MyMappingProvider implements IMappingProvider {
    
    @Override
    public String getClassName(String name) {
        // Map class names
        if ("OriginalName".equals(name)) {
            return "MappedName";
        }
        return name;
    }
    
    @Override
    public String getMethodName(String className, String methodName, String descriptor) {
        // Map method names based on class and signature
        if ("MyClass".equals(className) && "oldMethod".equals(methodName)) {
            return "newMethod";
        }
        return methodName;
    }
    
    @Override
    public String getFieldName(String className, String fieldName) {
        // Map field names
        if ("MyClass".equals(className) && "oldField".equals(fieldName)) {
            return "newField";
        }
        return fieldName;
    }
}

Using Mappings

Pass mapping provider to builder:

BytekinTransformer transformer = new BytekinTransformer.Builder(MyHooks.class)
    .mapping(new MyMappingProvider())
    .build();

Common Mapping Patterns

Simple Rename

public String getClassName(String name) {
    return name.replace("OldPrefix", "NewPrefix");
}

Lookup Table

private static final Map<String, String> classMap = new HashMap<>();

static {
    classMap.put("a", "com.example.ClassA");
    classMap.put("b", "com.example.ClassB");
}

public String getClassName(String name) {
    return classMap.getOrDefault(name, name);
}

File-Based Mappings

public String getClassName(String name) {
    // Load from configuration file
    Properties props = loadMappings("mappings.properties");
    return props.getProperty(name, name);
}

Mapping Obfuscated Code

When working with obfuscated code:

public class ObfuscationMapping implements IMappingProvider {
    
    @Override
    public String getClassName(String name) {
        // a.class -> com.example.MyClass
        switch (name) {
            case "a": return "com.example.MyClass";
            case "b": return "com.example.OtherClass";
            default: return name;
        }
    }
    
    @Override
    public String getMethodName(String className, String methodName, String descriptor) {
        // a.b() -> MyClass.process()
        if ("com.example.MyClass".equals(className)) {
            switch (methodName) {
                case "b": return "process";
                case "c": return "validate";
                default: return methodName;
            }
        }
        return methodName;
    }
}

Hook Configuration with Mappings

Write hooks using human-readable names:

@ModifyClass("com.example.UserService")  // Use readable name
public class UserServiceHooks {
    @Inject(methodName = "getUser", methodDesc = "(I)Lcom/example/User;", at = At.HEAD)
    public static CallbackInfo hook() { }
}

The mapping provider will translate to actual class names in bytecode.

Default (No-Op) Mapping

Use empty mapping for unchanged names:

public class EmptyMappingProvider implements IMappingProvider {
    
    @Override
    public String getClassName(String name) {
        return name;  // No change
    }
    
    @Override
    public String getMethodName(String className, String methodName, String descriptor) {
        return methodName;  // No change
    }
    
    @Override
    public String getFieldName(String className, String fieldName) {
        return fieldName;  // No change
    }
}

Advanced: Version-Specific Mappings

Support multiple versions:

public class VersionAwareMappingProvider implements IMappingProvider {
    private final String version;
    
    public VersionAwareMappingProvider(String version) {
        this.version = version;
    }
    
    @Override
    public String getClassName(String name) {
        if ("1.0".equals(version)) {
            return mapToV1(name);
        } else if ("2.0".equals(version)) {
            return mapToV2(name);
        }
        return name;
    }
    
    private String mapToV1(String name) {
        // Version 1 mappings
        return name;
    }
    
    private String mapToV2(String name) {
        // Version 2 mappings
        return name;
    }
}

Next Steps

Builder Pattern

bytekin provides a fluent Builder API for constructing transformers programmatically.

Basic Usage

BytekinTransformer transformer = new BytekinTransformer.Builder(MyHooks.class)
    .build();

Using Mappings

Apply name mappings during builder construction:

BytekinTransformer transformer = new BytekinTransformer.Builder(MyHooks.class)
    .mapping(new CustomMappingProvider())
    .build();

Adding Programmatic Transformations

Mix annotation-based and programmatic configurations:

BytekinTransformer transformer = new BytekinTransformer.Builder(AnnotationHooks.class)
    .inject("com.example.Extra", new Injection(...))
    .invoke("com.example.Another", new Invocation(...))
    .build();

Multiple Hook Classes

BytekinTransformer transformer = new BytekinTransformer.Builder(
    LoggingHooks.class,
    SecurityHooks.class,
    PerformanceHooks.class
)
.mapping(myMappings)
.build();

Builder Methods

mapping(IMappingProvider)

Set a mapping provider for class/method name translation.

inject(String, Injection)

Add injection transformation programmatically.

invoke(String, Invocation)

Add invocation transformation programmatically.

redirect(String, RedirectData)

Add redirect transformation programmatically.

modifyConstant(String, ConstantModification)

Add constant modification programmatically.

modifyVariable(String, VariableModification)

Add variable modification programmatically.

build()

Build and return the transformer. This method:

  1. Scans all hook classes
  2. Extracts annotations
  3. Adds programmatic transformations
  4. Creates internal transformer map
  5. Returns ready-to-use transformer

Best Practices

  1. Build once: Create transformers during initialization
  2. Reuse: Use the same transformer for multiple transformations
  3. Combine patterns: Mix annotations and programmatic API
  4. Document configuration: Comment why specific transformations are applied

Performance Tips

// Good: Build once
BytekinTransformer transformer = new BytekinTransformer.Builder(Hooks.class).build();

for (String className : classes) {
    byte[] transformed = transformer.transform(className, bytecode);
}

// Bad: Building multiple times
for (String className : classes) {
    BytekinTransformer transformer = new BytekinTransformer.Builder(Hooks.class).build();
    byte[] transformed = transformer.transform(className, bytecode);
}

Next Steps

Custom Transformers

Beyond annotations, bytekin allows you to create custom transformers for advanced use cases.

Creating Custom Transformers

You can extend the transformation system by implementing custom logic:

public class CustomTransformer implements IBytekinMethodTransformer {
    @Override
    public byte[] transform(byte[] bytecode) {
        // Custom transformation logic
        return bytecode;
    }
}

Advanced Customization

For more complex scenarios, work directly with ASM visitor pattern:

public class AdvancedCustomTransformer extends ClassVisitor {
    public AdvancedCustomTransformer(ClassVisitor cv) {
        super(ASM9, cv);
    }
    
    @Override
    public MethodVisitor visitMethod(int access, String name, String descriptor, 
                                     String signature, String[] exceptions) {
        MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
        
        // Return custom method visitor
        return new MethodVisitor(ASM9, mv) {
            @Override
            public void visitCode() {
                // Custom method instrumentation
                super.visitCode();
            }
        };
    }
}

Combining Custom and Built-in Transformations

Mix custom transformers with bytekin's built-in features:

BytekinTransformer transformer = new BytekinTransformer.Builder(BuiltInHooks.class)
    .build();

// Apply custom transformation after
byte[] original = getClassBytecode("com.example.MyClass");
byte[] withBuiltIn = transformer.transform("com.example.MyClass", original);
byte[] withCustom = applyCustom(withBuiltIn);

Performance Considerations

  • Keep custom transformers efficient
  • Cache transformation results when possible
  • Profile custom code for hotspots

Next Steps

API Reference

This section provides detailed API documentation for bytekin.

Core Classes

BytekinTransformer

The main entry point for bytecode transformation.

public class BytekinTransformer {
    public byte[] transform(String className, byte[] bytes, int api);
    
    public static class Builder {
        public Builder(Class<?>... classes);
        public Builder mapping(IMappingProvider mapping);
        public Builder inject(String className, Injection injection);
        public Builder invoke(String className, Invocation invocation);
        public Builder redirect(String className, RedirectData redirect);
        public Builder modifyConstant(String className, ConstantModification modification);
        public Builder modifyVariable(String className, VariableModification modification);
        public BytekinTransformer build();
    }
}

CallbackInfo

Controls transformation behavior within hook methods.

public class CallbackInfo {
    public boolean cancelled;
    public Object returnValue;
    public Object[] modifyArgs;
    
    public static CallbackInfo empty();
}

Annotations

@ModifyClass

Marks a class as a hook container for bytecode transformations.

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ModifyClass {
    String className();
}

@Inject

Injects code at specific points in methods.

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Inject {
    String methodName();
    String methodDesc();
    At at();
}

@Invoke

Intercepts method calls.

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Invoke {
    String targetMethodName();
    String targetMethodDesc();
    String invokeMethodName();
    String invokeMethodDesc();
    Shift shift();
}

@Redirect

Redirects method calls to different target.

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Redirect {
    String targetMethodName();
    String targetMethodDesc();
    String redirectMethodName();
    String redirectMethodDesc();
}

@ModifyConstant

Modifies constant values in bytecode.

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ModifyConstant {
    String methodName();
    Object oldValue();
    Object newValue();
}

@ModifyVariable

Modifies local variable values.

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ModifyVariable {
    String methodName();
    int variableIndex();
}

Enums

At

Specifies where to inject code.

public enum At {
    HEAD,      // Before method body
    RETURN,    // Before return statements
    TAIL       // At method end
}

Shift

Specifies timing relative to method invocation.

public enum Shift {
    BEFORE,    // Before the call
    AFTER      // After the call
}

Interfaces

IMappingProvider

Maps class and method names.

public interface IMappingProvider {
    String getClassName(String name);
    String getMethodName(String className, String methodName, String descriptor);
    String getFieldName(String className, String fieldName);
}

Data Classes

Injection

Represents an injection transformation.

public class Injection {
    // Constructor and methods
}

Invocation

Represents an invocation transformation.

public class Invocation {
    // Constructor and methods
}

RedirectData

Represents a redirect transformation.

public class RedirectData {
    // Constructor and methods
}

ConstantModification

Represents a constant modification transformation.

public class ConstantModification {
    // Constructor and methods
}

VariableModification

Represents a variable modification transformation.

public class VariableModification {
    // Constructor and methods
}

Common Exceptions

VerifyError

Thrown when transformed bytecode is invalid.

ClassNotFoundException

Thrown when target class cannot be found.

ClassFormatException

Thrown when bytecode format is invalid.

Utility Classes

DescriptorParser

Utility for parsing and validating method descriptors.

BytecodeManipulator

Low-level bytecode manipulation utilities.

Threading

All public methods are thread-safe after initialization:

  • BytekinTransformer.transform() can be called from multiple threads
  • Builder is not thread-safe during configuration
  • CallbackInfo is local to each hook invocation

Performance Characteristics

OperationComplexity
Builder.build()O(n) where n = number of hook methods
transform()O(m) where m = bytecode size
Hook executionO(1) average case

Next Steps

Annotations Reference

Complete reference for bytekin annotations.

@ModifyClass

Purpose

Marks a class as containing hook methods for bytecode transformation.

Usage

@ModifyClass("com.example.TargetClass")
public class MyHooks {
    // Hook methods here
}

Parameters

ParameterTypeRequiredDescription
classNameStringYesFully qualified name of the target class

Scope

Applied to class types only.

@Inject

Purpose

Inject code at specific points in methods.

Usage

@Inject(
    methodName = "myMethod",
    methodDesc = "(I)Ljava/lang/String;",
    at = At.HEAD
)
public static CallbackInfo hook(int param) { }

Parameters

ParameterTypeRequiredDescription
methodNameStringYesTarget method name
methodDescStringYesMethod descriptor (JVM format)
atAtYesWhere to inject code

Scope

Applied to methods only.

Return Type

Must return CallbackInfo.

@Invoke

Purpose

Intercept method calls.

Usage

@Invoke(
    targetMethodName = "parentMethod",
    targetMethodDesc = "()V",
    invokeMethodName = "childMethod",
    invokeMethodDesc = "(I)V",
    shift = Shift.BEFORE
)
public static CallbackInfo hook() { }

Parameters

ParameterTypeRequiredDescription
targetMethodNameStringYesMethod containing the call
targetMethodDescStringYesDescriptor of target method
invokeMethodNameStringYesName of method being called
invokeMethodDescStringYesDescriptor of called method
shiftShiftYesBEFORE or AFTER the call

Scope

Applied to methods only.

@Redirect

Purpose

Redirect method calls to different target.

Usage

@Redirect(
    targetMethodName = "oldMethod",
    targetMethodDesc = "()V",
    redirectMethodName = "newMethod",
    redirectMethodDesc = "()V"
)
public static void hook() { }

Parameters

ParameterTypeRequiredDescription
targetMethodNameStringYesMethod containing the call
targetMethodDescStringYesDescriptor of target method
redirectMethodNameStringYesName of redirect method
redirectMethodDescStringYesDescriptor of redirect method

@ModifyConstant

Purpose

Modify constant values in bytecode.

Usage

@ModifyConstant(
    methodName = "getConfig",
    oldValue = "dev",
    newValue = "prod"
)
public static CallbackInfo hook() { }

Parameters

ParameterTypeRequiredDescription
methodNameStringYesMethod containing the constant
oldValueObjectYesOriginal constant value
newValueObjectYesNew constant value

@ModifyVariable

Purpose

Modify local variable values.

Usage

@ModifyVariable(
    methodName = "process",
    variableIndex = 1
)
public static void hook(String param) { }

Parameters

ParameterTypeRequiredDescription
methodNameStringYesTarget method name
variableIndexintYesLocal variable slot index

Enum: At

Values

ValueDescription
HEADAt start of method, before all code
RETURNBefore each return statement
TAILAt end of method

Enum: Shift

Values

ValueDescription
BEFOREExecute hook before method call
AFTERExecute hook after method call

Next Steps

Classes and Interfaces

Reference documentation for bytekin classes and interfaces.

Core Classes

BytekinTransformer

Main transformer class for bytecode manipulation.

Methods:

  • byte[] transform(String className, byte[] bytes, int api) - Transform class bytecode
  • byte[] transform(String className, byte[] bytes) - Transform (default API)

Builder:

  • new BytekinTransformer.Builder(Class<?>... classes) - Create builder

CallbackInfo

Data structure for controlling transformation behavior.

Fields:

  • boolean cancelled - Skip original code execution
  • Object returnValue - Custom return value
  • Object[] modifyArgs - Modified method arguments

Methods:

  • static CallbackInfo empty() - Create empty callback
  • CallbackInfo(boolean cancelled, Object returnValue, Object[] modifyArgs) - Constructor

Builder Class

BytekinTransformer.Builder

Fluent builder for constructing transformers.

Constructors:

  • Builder(Class<?>... classes) - Initialize with hook classes

Methods:

  • Builder mapping(IMappingProvider) - Set mapping provider
  • Builder inject(String, Injection) - Add injection
  • Builder invoke(String, Invocation) - Add invocation
  • Builder redirect(String, RedirectData) - Add redirect
  • Builder modifyConstant(String, ConstantModification) - Add constant modification
  • Builder modifyVariable(String, VariableModification) - Add variable modification
  • BytekinTransformer build() - Build transformer

Data Classes

Injection

Represents an injection point.

Purpose: Store injection configuration data.

Invocation

Represents an invocation point.

Purpose: Store invocation configuration data.

RedirectData

Represents a redirect target.

Purpose: Store redirect configuration data.

ConstantModification

Represents a constant modification.

Purpose: Store constant modification data.

VariableModification

Represents a variable modification.

Purpose: Store variable modification data.

Interfaces

IMappingProvider

Interface for name mapping.

Methods:

  • String getClassName(String name) - Map class name
  • String getMethodName(String className, String methodName, String descriptor) - Map method name
  • String getFieldName(String className, String fieldName) - Map field name

Implementation Examples

EmptyMappingProvider - No-op mapping (returns unchanged names)

Custom Mapping:

public class CustomMapping implements IMappingProvider {
    @Override
    public String getClassName(String name) {
        // Custom mapping logic
        return name;
    }
    
    @Override
    public String getMethodName(String className, String methodName, String descriptor) {
        // Custom mapping logic
        return methodName;
    }
    
    @Override
    public String getFieldName(String className, String fieldName) {
        // Custom mapping logic
        return fieldName;
    }
}

Utility Classes

DescriptorParser

Parse and validate method descriptors.

Methods:

  • static String parseDescriptor(String desc) - Parse descriptor format

BytecodeManipulator

Low-level bytecode utilities.

Purpose: Internal utilities for bytecode manipulation.

Inheritance Hierarchy

Object
├── BytekinTransformer
│   └── BytekinTransformer.Builder
├── CallbackInfo
├── Injection
├── Invocation
├── RedirectData
├── ConstantModification
└── VariableModification

Interface Implementations

IMappingProvider
├── EmptyMappingProvider
└── (Custom implementations)

Next Steps

Examples

Examples - Basic Usage

This section contains complete, working examples for common bytekin use cases.

Example 1: Adding Logging

Problem

Add logging to a method without modifying source code.

Solution

Target Class:

package com.example;

public class Calculator {
    public int add(int a, int b) {
        return a + b;
    }
}

Hook Class:

package com.example;

import io.github.brqnko.bytekin.injection.*;
import io.github.brqnko.bytekin.data.CallbackInfo;

@ModifyClass("com.example.Calculator")
public class CalculatorLoggingHooks {

    @Inject(
        methodName = "add",
        methodDesc = "(II)I",
        at = At.HEAD
    )
    public static CallbackInfo logAddition(int a, int b) {
        System.out.println("Adding: " + a + " + " + b);
        return CallbackInfo.empty();
    }
}

Usage:

public class Main {
    public static void main(String[] args) {
        BytekinTransformer transformer = new BytekinTransformer.Builder(
            CalculatorLoggingHooks.class
        ).build();

        byte[] original = getClassBytecode("com.example.Calculator");
        byte[] transformed = transformer.transform("com.example.Calculator", original);
        
        Calculator calc = loadTransformed(transformed);
        int result = calc.add(5, 3);
        // Output:
        // Adding: 5 + 3
        // 8
    }
}

Example 2: Parameter Validation

Problem

Validate method parameters before execution.

Solution

Hook Class:

@ModifyClass("com.example.UserService")
public class UserValidationHooks {

    @Inject(
        methodName = "createUser",
        methodDesc = "(Ljava/lang/String;I)V",
        at = At.HEAD
    )
    public static CallbackInfo validateUser(String name, int age, CallbackInfo ci) {
        if (name == null || name.isEmpty()) {
            System.out.println("ERROR: Name cannot be empty");
            ci.cancelled = true;
            return ci;
        }
        
        if (age < 18) {
            System.out.println("ERROR: User must be 18 or older");
            ci.cancelled = true;
            return ci;
        }
        
        System.out.println("Valid user: " + name + ", age " + age);
        return CallbackInfo.empty();
    }
}

Example 3: Caching

Problem

Intercept method calls to implement caching.

Solution

Hook Class:

@ModifyClass("com.example.DataRepository")
public class CachingHooks {
    private static final Map<String, Object> cache = new ConcurrentHashMap<>();

    @Invoke(
        targetMethodName = "fetch",
        targetMethodDesc = "(Ljava/lang/String;)Ljava/lang/Object;",
        invokeMethodName = "queryDatabase",
        invokeMethodDesc = "(Ljava/lang/String;)Ljava/lang/Object;",
        shift = Shift.BEFORE
    )
    public static CallbackInfo checkCache(String key, CallbackInfo ci) {
        Object cached = cache.get(key);
        if (cached != null) {
            System.out.println("Cache hit for: " + key);
            ci.cancelled = true;
            ci.returnValue = cached;
        } else {
            System.out.println("Cache miss for: " + key);
        }
        return ci;
    }
}

Example 4: Security - Authentication Check

Problem

Ensure all sensitive methods require authentication.

Solution

Hook Class:

@ModifyClass("com.example.PaymentService")
public class AuthenticationHooks {

    @Inject(
        methodName = "transfer",
        methodDesc = "(Ljava/lang/String;J)Z",
        at = At.HEAD
    )
    public static CallbackInfo checkAuthentication(String account, long amount, CallbackInfo ci) {
        if (!isUserAuthenticated()) {
            System.out.println("ERROR: Authentication required");
            ci.cancelled = true;
            ci.returnValue = false;
            return ci;
        }
        
        System.out.println("Authenticated transfer: " + amount);
        return CallbackInfo.empty();
    }

    private static boolean isUserAuthenticated() {
        // Check authentication status
        return true;
    }
}

Example 5: Monitoring - Method Call Counter

Problem

Count how many times specific methods are called.

Solution

Hook Class:

@ModifyClass("com.example.UserService")
public class MonitoringHooks {
    private static final AtomicInteger callCount = new AtomicInteger(0);

    @Inject(
        methodName = "getUser",
        methodDesc = "(I)Lcom/example/User;",
        at = At.HEAD
    )
    public static CallbackInfo countCalls(int userId) {
        int count = callCount.incrementAndGet();
        if (count % 100 == 0) {
            System.out.println("getUser() called " + count + " times");
        }
        return CallbackInfo.empty();
    }
}

Example 6: Transforming Return Values

Problem

Modify the return value of a method.

Solution

Hook Class:

@ModifyClass("com.example.PriceCalculator")
public class PriceHooks {

    @Inject(
        methodName = "getPrice",
        methodDesc = "()D",
        at = At.RETURN
    )
    public static CallbackInfo applyDiscount(CallbackInfo ci) {
        double originalPrice = (double) ci.returnValue;
        double discounted = originalPrice * 0.9;  // 10% discount
        ci.returnValue = discounted;
        return ci;
    }
}

Invoke Examples

Example: Method Call Interception

Hook Class:

@ModifyClass("com.example.DataProcessor")
public class ProcessorHooks {

    @Invoke(
        targetMethodName = "process",
        targetMethodDesc = "(Ljava/lang/String;)Ljava/lang/String;",
        invokeMethodName = "validate",
        invokeMethodDesc = "(Ljava/lang/String;)Ljava/lang/String;",
        shift = Shift.BEFORE
    )
    public static CallbackInfo sanitizeBeforeValidation(String data, CallbackInfo ci) {
        String sanitized = data.trim().toLowerCase();
        ci.modifyArgs = new Object[]{sanitized};
        return ci;
    }
}

Combined Example: Comprehensive Transformation

Complete Hook Class:

@ModifyClass("com.example.UserRepository")
public class ComprehensiveHooks {

    @Inject(
        methodName = "save",
        methodDesc = "(Lcom/example/User;)V",
        at = At.HEAD
    )
    public static CallbackInfo validateBeforeSave(Object user, CallbackInfo ci) {
        // Validate input
        if (user == null) {
            System.out.println("ERROR: Cannot save null user");
            ci.cancelled = true;
        }
        return ci;
    }

    @Invoke(
        targetMethodName = "save",
        targetMethodDesc = "(Lcom/example/User;)V",
        invokeMethodName = "validateUser",
        invokeMethodDesc = "(Lcom/example/User;)Z",
        shift = Shift.BEFORE
    )
    public static CallbackInfo modifyValidation(Object user, CallbackInfo ci) {
        // Enhance validation
        System.out.println("Validating user...");
        return ci;
    }

    @Inject(
        methodName = "save",
        methodDesc = "(Lcom/example/User;)V",
        at = At.RETURN
    )
    public static CallbackInfo logSuccess(Object user) {
        System.out.println("User saved successfully");
        return CallbackInfo.empty();
    }
}

Next Steps

Advanced Examples

Advanced use cases and patterns for bytekin.

Example 1: Custom ClassLoader

Implement a custom ClassLoader that applies transformations:

public class TransformingClassLoader extends ClassLoader {
    private final BytekinTransformer transformer;
    private final ClassLoader parent;
    
    public TransformingClassLoader(BytekinTransformer transformer, ClassLoader parent) {
        super(parent);
        this.transformer = transformer;
        this.parent = parent;
    }
    
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            byte[] classBytes = loadBytesFromClasspath(name);
            byte[] transformed = transformer.transform(name, classBytes);
            return defineClass(name, transformed, 0, transformed.length);
        } catch (IOException e) {
            throw new ClassNotFoundException("Cannot find " + name, e);
        }
    }
    
    private byte[] loadBytesFromClasspath(String className) throws IOException {
        String path = className.replace('.', '/') + ".class";
        try (InputStream is = parent.getResourceAsStream(path)) {
            return is.readAllBytes();
        }
    }
}

// Usage
BytekinTransformer transformer = new BytekinTransformer.Builder(MyHooks.class).build();
ClassLoader loader = new TransformingClassLoader(transformer, ClassLoader.getSystemClassLoader());
Class<?> clazz = loader.loadClass("com.example.MyClass");

Example 2: Java Agent

Create a Java agent for bytecode transformation:

public class BytekinAgent {
    public static void premain(String agentArgs, Instrumentation inst) {
        BytekinTransformer transformer = new BytekinTransformer.Builder(MyHooks.class).build();
        inst.addTransformer((loader, className, klass, pd, bytecode) -> {
            return transformer.transform(className, bytecode);
        });
    }
}

Launch with: java -javaagent:bytekin-agent.jar MyApplication

Example 3: Aspect-Oriented Programming (AOP)

Implement cross-cutting concerns:

@ModifyClass("com.example.UserService")
public class AuditingAspect {
    
    @Inject(methodName = "save", methodDesc = "(Lcom/example/User;)V", at = At.HEAD)
    public static CallbackInfo auditBefore(Object user) {
        System.out.println("Audit: save() started");
        return CallbackInfo.empty();
    }
    
    @Inject(methodName = "delete", methodDesc = "(I)V", at = At.HEAD)
    public static CallbackInfo auditDelete(int id) {
        System.out.println("Audit: delete(" + id + ") started");
        return CallbackInfo.empty();
    }
}

Example 4: Lazy Initialization

Implement lazy loading pattern:

@ModifyClass("com.example.Repository")
public class LazyLoadingHooks {
    private static Object resource;
    
    @Inject(methodName = "initialize", methodDesc = "()V", at = At.HEAD)
    public static CallbackInfo lazyInit(CallbackInfo ci) {
        if (resource == null) {
            synchronized (LazyLoadingHooks.class) {
                if (resource == null) {
                    resource = loadExpensiveResource();
                }
            }
        }
        ci.cancelled = true;
        return ci;
    }
    
    private static Object loadExpensiveResource() {
        // Expensive initialization
        return new Object();
    }
}

Example 5: Dynamic Configuration

Change behavior based on configuration:

@ModifyClass("com.example.Service")
public class DynamicConfigHooks {
    private static final Properties config = new Properties();
    
    static {
        try {
            config.load(new FileInputStream("config.properties"));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    
    @Inject(methodName = "process", methodDesc = "(Ljava/lang/String;)V", at = At.HEAD)
    public static CallbackInfo checkConfig(String input, CallbackInfo ci) {
        boolean enabled = Boolean.parseBoolean(config.getProperty("feature.enabled", "false"));
        if (!enabled) {
            ci.cancelled = true;
        }
        return ci;
    }
}

Example 6: Multi-Layer Transformation

Apply multiple transformers sequentially:

public class MultiLayerTransformation {
    public static void main(String[] args) {
        BytekinTransformer logging = new BytekinTransformer.Builder(LoggingHooks.class).build();
        BytekinTransformer security = new BytekinTransformer.Builder(SecurityHooks.class).build();
        BytekinTransformer caching = new BytekinTransformer.Builder(CachingHooks.class).build();
        
        byte[] original = getClassBytecode("com.example.Service");
        
        // Apply layer by layer
        byte[] withLogging = logging.transform("com.example.Service", original);
        byte[] withSecurity = security.transform("com.example.Service", withLogging);
        byte[] withCaching = caching.transform("com.example.Service", withSecurity);
        
        Class<?> transformed = loadClass(withCaching);
    }
}

Example 7: Performance Profiling

Add profiling without source changes:

@ModifyClass("com.example.CriticalPath")
public class ProfilingHooks {
    private static final ThreadLocal<Long> timer = ThreadLocal.withInitial(() -> 0L);
    
    @Inject(methodName = "compute", methodDesc = "()Ljava/lang/Object;", at = At.HEAD)
    public static CallbackInfo startProfiling() {
        timer.set(System.nanoTime());
        return CallbackInfo.empty();
    }
    
    @Inject(methodName = "compute", methodDesc = "()Ljava/lang/Object;", at = At.RETURN)
    public static CallbackInfo endProfiling() {
        long duration = System.nanoTime() - timer.get();
        System.out.println("Duration: " + (duration / 1_000_000.0) + "ms");
        return CallbackInfo.empty();
    }
}

Example 8: Resilience Pattern

Add retry logic:

@ModifyClass("com.example.HttpClient")
public class ResilienceHooks {
    private static final int MAX_RETRIES = 3;
    
    @Inject(methodName = "request", methodDesc = "(Ljava/lang/String;)Ljava/lang/String;", 
            at = At.HEAD)
    public static CallbackInfo addRetry(String url, CallbackInfo ci) {
        String result = null;
        int attempt = 0;
        
        while (attempt < MAX_RETRIES) {
            try {
                result = executeRequest(url);
                break;
            } catch (Exception e) {
                attempt++;
                if (attempt >= MAX_RETRIES) throw e;
            }
        }
        
        ci.cancelled = true;
        ci.returnValue = result;
        return ci;
    }
    
    private static String executeRequest(String url) throws Exception {
        // Make HTTP request
        return "";
    }
}

Example 9: Observability

Collect metrics:

@ModifyClass("com.example.DataStore")
public class ObservabilityHooks {
    private static final AtomicLong callCount = new AtomicLong(0);
    private static final AtomicLong errorCount = new AtomicLong(0);
    
    @Inject(methodName = "query", methodDesc = "(Ljava/lang/String;)Ljava/util/List;", 
            at = At.HEAD)
    public static CallbackInfo trackCall(String query) {
        callCount.incrementAndGet();
        return CallbackInfo.empty();
    }
    
    @Invoke(
        targetMethodName = "query",
        targetMethodDesc = "(Ljava/lang/String;)Ljava/util/List;",
        invokeMethodName = "throwException",
        invokeMethodDesc = "()V",
        shift = Shift.BEFORE
    )
    public static CallbackInfo trackError() {
        errorCount.incrementAndGet();
        return CallbackInfo.empty();
    }
    
    public static void printMetrics() {
        System.out.println("Calls: " + callCount.get());
        System.out.println("Errors: " + errorCount.get());
    }
}

Example 10: Migration Strategy

Gradually migrate from old to new API:

@ModifyClass("com.example.Application")
public class MigrationHooks {
    private static final boolean USE_NEW_API = true;
    
    @Redirect(
        targetMethodName = "main",
        targetMethodDesc = "([Ljava/lang/String;)V",
        redirectMethodName = "oldSearch",
        redirectMethodDesc = "(Ljava/lang/String;)Ljava/util/List;",
        from = "search",
        to = USE_NEW_API ? "newSearch" : "oldSearch"
    )
    public static void migrateAPI() {
        // Gradually route to new implementation
    }
}

Next Steps

Best Practices

This guide covers best practices for using bytekin effectively and safely.

Design Principles

1. Keep Hooks Simple

Keep hook methods focused and simple:

Good:

@Inject(methodName = "process", methodDesc = "()V", at = At.HEAD)
public static CallbackInfo log() {
    System.out.println("Starting process");
    return CallbackInfo.empty();
}

Avoid:

@Inject(methodName = "process", methodDesc = "()V", at = At.HEAD)
public static CallbackInfo complexLogic() {
    // Multiple database calls
    // Complex calculations
    // File I/O operations
    // This is too much for a hook!
    return CallbackInfo.empty();
}

2. Extract Complex Logic

Move complex logic to separate methods:

@Inject(methodName = "validate", methodDesc = "(Ljava/lang/String;)Z", at = At.HEAD)
public static CallbackInfo onValidate(String input, CallbackInfo ci) {
    if (!isValidInput(input)) {
        ci.cancelled = true;
        ci.returnValue = false;
    }
    return ci;
}

private static boolean isValidInput(String input) {
    // Complex validation logic here
    return !input.isEmpty() && input.length() < 256;
}

Performance Guidelines

1. Minimize Hook Overhead

Hooks are executed frequently. Keep them fast:

Good:

@Inject(methodName = "getData", methodDesc = "()Ljava/lang/Object;", at = At.HEAD)
public static CallbackInfo checkCache() {
    if (cacheHit()) {
        // Quick cache lookup
        return new CallbackInfo(true, getFromCache(), null);
    }
    return CallbackInfo.empty();
}

Avoid:

@Inject(methodName = "getData", methodDesc = "()Ljava/lang/Object;", at = At.HEAD)
public static CallbackInfo expensiveCheck() {
    // Scanning entire database
    List<Item> results = database.queryAll();
    // Processing results
    // ...this is too slow!
    return CallbackInfo.empty();
}

2. Reuse Builder

Build transformers once and reuse:

Good:

// In initialization code
BytekinTransformer transformer = new BytekinTransformer.Builder(MyHooks.class)
    .build();

// Use transformer multiple times
byte[] transformed1 = transformer.transform("com.example.Class1", bytes1);
byte[] transformed2 = transformer.transform("com.example.Class2", bytes2);

Avoid:

// DON'T do this in a loop!
for (String className : classNames) {
    // Creating transformer for each class is wasteful
    BytekinTransformer transformer = new BytekinTransformer.Builder(MyHooks.class)
        .build();
    byte[] transformed = transformer.transform(className, bytes);
}

Error Handling

1. Handle Exceptions in Hooks

Exceptions in hooks can break transformations:

Good:

@Inject(methodName = "process", methodDesc = "()V", at = At.HEAD)
public static CallbackInfo safeLogging() {
    try {
        System.out.println("Processing started");
    } catch (Exception e) {
        // Handle gracefully, don't let it propagate
        e.printStackTrace();
    }
    return CallbackInfo.empty();
}

Avoid:

@Inject(methodName = "process", methodDesc = "()V", at = At.HEAD)
public static CallbackInfo unsafeLogging() {
    // If this throws, it breaks the transformation!
    Path path = Paths.get("/invalid/path");
    Files.writeString(path, "log");
    return CallbackInfo.empty();
}

2. Validate Return Values

When modifying CallbackInfo, ensure types are correct:

Good:

@Inject(methodName = "getValue", methodDesc = "()I", at = At.HEAD)
public static CallbackInfo returnCustomValue() {
    CallbackInfo ci = new CallbackInfo();
    ci.cancelled = true;
    ci.returnValue = 42;  // Integer matches return type
    return ci;
}

Avoid:

@Inject(methodName = "getValue", methodDesc = "()I", at = At.HEAD)
public static CallbackInfo wrongType() {
    CallbackInfo ci = new CallbackInfo();
    ci.cancelled = true;
    ci.returnValue = "42";  // String doesn't match int return type!
    return ci;
}

Documentation

1. Document Transformations

Clearly document what each hook does:

/**
 * Adds authentication check to all data access methods.
 * If user is not authenticated, cancels the method and returns false.
 */
@ModifyClass("com.example.DataStore")
public class DataStoreHooks {

    /**
     * Injects authentication check at the start of read operations.
     * 
     * @param ci Callback info - set cancelled=true if not authenticated
     */
    @Inject(methodName = "read", methodDesc = "()Ljava/lang/Object;", at = At.HEAD)
    public static CallbackInfo ensureAuthenticated(CallbackInfo ci) {
        if (!isAuthenticated()) {
            ci.cancelled = true;
            ci.returnValue = null;
        }
        return ci;
    }
}

2. Document Parameters

Clearly indicate which parameters correspond to method arguments:

/**
 * Sanitizes user input before processing.
 * 
 * @param userId the user ID (first parameter of target method)
 * @param action the requested action (second parameter)
 */
@Inject(methodName = "execute", methodDesc = "(Ljava/lang/String;Ljava/lang/String;)V", 
        at = At.HEAD)
public static CallbackInfo sanitizeInput(String userId, String action) {
    // userId and action are from the target method's parameters
    return CallbackInfo.empty();
}

Testing

1. Test Transformations

Always test your transformations:

public class TransformationTest {
    @Test
    public void testInjectionWorks() {
        BytekinTransformer transformer = new BytekinTransformer.Builder(MyHooks.class)
            .build();
        
        byte[] original = getClassBytecode("com.example.Target");
        byte[] transformed = transformer.transform("com.example.Target", original);
        
        // Load and test transformed class
        Class<?> clazz = loadFromBytecode(transformed);
        Object instance = clazz.newInstance();
        
        // Verify transformation was applied
        assertNotNull(instance);
    }
}

2. Verify No Regression

Ensure original behavior is preserved:

@Test
public void testOriginalBehaviorPreserved() {
    // Test without transformation
    Calculator calc1 = new Calculator();
    int result1 = calc1.add(3, 4);
    
    // Test with transformation
    byte[] transformed = applyTransformation(Calculator.class);
    Calculator calc2 = loadTransformed(transformed);
    int result2 = calc2.add(3, 4);
    
    // Results should be the same
    assertEquals(result1, result2);
}

Compatibility

1. Version Compatibility

Document supported Java versions:

/**
 * These hooks work with Java 8+
 * Uses standard method descriptors compatible across versions
 */
@ModifyClass("com.example.Service")
public class CompatibleHooks {
    // ...
}

2. Library Compatibility

Check for incompatibilities with other bytecode tools:

// Document conflicts with other bytecode manipulation
// For example: Spring, Mockito, AspectJ, etc.

Security

1. Input Validation

Always validate inputs in hooks:

@Inject(methodName = "processFile", methodDesc = "(Ljava/lang/String;)V", 
        at = At.HEAD)
public static CallbackInfo validatePath(String path, CallbackInfo ci) {
    if (path != null && isPathTraversal(path)) {
        // Prevent directory traversal attacks
        ci.cancelled = true;
    }
    return ci;
}

private static boolean isPathTraversal(String path) {
    return path.contains("..") || path.startsWith("/");
}

2. Avoid Sensitive Data Exposure

Don't log or expose sensitive information:

Good:

@Inject(methodName = "login", methodDesc = "(Ljava/lang/String;Ljava/lang/String;)Z", 
        at = At.HEAD)
public static CallbackInfo logAttempt(String user) {
    System.out.println("Login attempt by: " + user);
    return CallbackInfo.empty();
}

Avoid:

@Inject(methodName = "login", methodDesc = "(Ljava/lang/String;Ljava/lang/String;)Z", 
        at = At.HEAD)
public static CallbackInfo logAttempt(String user, String password) {
    // Don't log passwords!
    System.out.println("Login attempt: " + user + " / " + password);
    return CallbackInfo.empty();
}

Debugging Tips

1. Bytecode Inspection

Inspect generated bytecode to verify transformations:

# Use javap to inspect the transformed class
javap -c TransformedClass.class

# Look for your injected method calls

2. Add Logging

Use logging to track transformation execution:

@Inject(methodName = "critical", methodDesc = "()V", at = At.HEAD)
public static CallbackInfo logEntry() {
    System.out.println("[DEBUG] Entering critical method");
    System.out.println("[DEBUG] Stack trace: " + Arrays.toString(Thread.currentThread().getStackTrace()));
    return CallbackInfo.empty();
}

Maintenance

1. Version Your Hooks

Keep track of hook versions:

/**
 * Transformation hooks for version 2.0
 * 
 * Changes from v1.0:
 * - Added authentication checks
 * - Optimized caching strategy
 * - Fixed null pointer issue in legacy code
 */
@ModifyClass("com.example.Service")
public class ServiceHooksV2 {
    // ...
}

2. Keep Records

Document why each transformation exists:

Transform: Calculator.add() logging
Created: 2025-01-15
Reason: Performance monitoring for debug builds
Status: Active
Notes: Can be removed after profiling phase

Common Pitfalls

1. Wrong Method Descriptors

Wrong:

@Inject(methodName = "add", methodDesc = "(I I)I", at = At.HEAD)  // Spaces in descriptor!

Right:

@Inject(methodName = "add", methodDesc = "(II)I", at = At.HEAD)

2. Type Mismatches

Wrong:

@Invoke(..., invokeMethodDesc = "(I)V", shift = Shift.BEFORE)
public static CallbackInfo hook(String param) {  // Type mismatch!
}

Right:

@Invoke(..., invokeMethodDesc = "(I)V", shift = Shift.BEFORE)
public static CallbackInfo hook(int param) {  // Correct type
}

3. Modifying Immutable Data

Wrong:

@ModifyVariable(methodName = "process", variableIndex = 1)
public static void modify(String str) {
    str = str.toUpperCase();  // Strings are immutable, won't work!
}

Right:

@Inject(methodName = "process", methodDesc = "(Ljava/lang/String;)V", at = At.HEAD)
public static CallbackInfo modifyByReplacing(String str, CallbackInfo ci) {
    ci.modifyArgs = new Object[]{str.toUpperCase()};
    return ci;
}

Next Steps

FAQ - Frequently Asked Questions

General Questions

What is bytekin?

bytekin is a lightweight Java bytecode transformation framework built on ASM. It allows you to modify Java classes at the bytecode level without touching the source code.

Why would I need bytecode transformation?

Common use cases include:

  • Adding logging without modifying source code
  • Implementing cross-cutting concerns
  • Testing and mocking
  • Performance profiling
  • Security enhancements

How does bytekin compare to other tools?

ToolSizeComplexityUse Case
bytekinSmallSimpleDirect bytecode manipulation
Spring AOPLargeComplexEnterprise framework
MockitoMediumMediumTesting/Mocking
AspectMediumComplexAspect-oriented programming

Is bytekin production-ready?

Yes, bytekin is designed for production use. It has minimal dependencies (only ASM) and has been tested thoroughly.

Technical Questions

What Java versions does bytekin support?

bytekin requires Java 8 or higher.

Can I use bytekin with Spring Boot?

Yes! bytekin can work alongside Spring Boot. You would typically apply transformations during a custom ClassLoader setup or at build time.

Does bytekin work with obfuscated code?

Yes, with mappings! Use the mapping system to handle obfuscated class and method names.

Can I combine multiple transformations?

Yes! You can use multiple @Inject, @Invoke, and other annotations on the same class. They all get applied.

Usage Questions

How do I find the method descriptor for a method?

Use javap:

javap -c MyClass.class

Look at the method signature and convert it to JVM descriptor format:

  • int add(int a, int b)(II)I
  • String process(String s)(Ljava/lang/String;)Ljava/lang/String;

What's the difference between Inject and Invoke?

  • Inject: Insert your code at a specific point in a method
  • Invoke: Intercept a method call within a method and possibly modify arguments

Can I cancel a method execution?

Yes, set ci.cancelled = true in your hook method. However, this only works for certain transformation types.

How do I modify method arguments?

Use CallbackInfo.modifyArgs:

ci.modifyArgs = new Object[]{ modifiedArg1, modifiedArg2 };

Can I access static fields from hook methods?

Yes, you can reference static fields from your hook class:

@Inject(...)
public static CallbackInfo hook() {
    // Access static fields
    if (cacheEnabled) {
        // ...
    }
}

Performance Questions

What's the overhead of using bytekin?

  • Transformation time: Minimal, happens once at class load
  • Runtime overhead: Zero! Transformed bytecode runs at same speed as hand-written code

Should I rebuild transformers for each transform?

No! Build once and reuse:

// Good
BytekinTransformer transformer = new BytekinTransformer.Builder(MyHooks.class).build();
for (String className : classNames) {
    byte[] transformed = transformer.transform(className, bytecode);
}

// Bad
for (String className : classNames) {
    BytekinTransformer transformer = new BytekinTransformer.Builder(MyHooks.class).build();
    byte[] transformed = transformer.transform(className, bytecode);
}

How much does bytecode transformation impact startup time?

Impact is minimal when transformations are simple and applied only to necessary classes.

Troubleshooting Questions

My transformations aren't being applied

Common causes:

  1. Wrong class name: Check the @ModifyClass value matches exactly
  2. Wrong method descriptor: Verify the methodDesc parameter
  3. Class not loaded: Ensure the class is loaded before transformation

I'm getting ClassCastException

This usually means:

  1. Type mismatch in CallbackInfo.returnValue
  2. Wrong type in hook method signature
  3. Modifying arguments to incompatible types

Hook method is not being called

Check:

  1. Is the hook class passed to the Builder?
  2. Are method name and descriptor correct?
  3. Is the target class name correct?

java.lang.VerifyError

This means the transformed bytecode is invalid. Common causes:

  1. Incorrect bytecode modification
  2. Type mismatches
  3. Invalid method signatures

Performance degradation after transformation

If transformations are slow:

  1. Simplify hook methods
  2. Avoid expensive operations in hooks
  3. Use conditional logic to skip unnecessary work
  4. Profile with a JVM profiler

Advanced Questions

Can I create custom transformers?

Yes! You can extend the transformer classes or use the programmatic API instead of annotations.

Does bytekin support method overloading?

Yes, by using the complete method descriptor which includes parameter types and return type.

Can I transform the same class multiple times?

Yes, you can apply different transformations sequentially.

Is bytekin thread-safe?

After building, BytekinTransformer.transform() is thread-safe and can be called from multiple threads concurrently.

Can I use bytekin with Java agents?

Yes! bytekin works well with Java agents. Use it within your agent's transform() method.

Migration and Upgrade Questions

How do I migrate from another bytecode tool?

The concepts are similar:

  1. Define target classes
  2. Create hook methods with transformation annotations
  3. Build transformers
  4. Apply transformations

Can I upgrade bytekin without changing my code?

Yes, bytekin maintains backward compatibility. Always check release notes before upgrading.

What license is bytekin under?

bytekin is licensed under the Apache License 2.0.

Can I use bytekin in commercial projects?

Yes! Apache 2.0 allows commercial use.

Do I need to open-source my code if I use bytekin?

No, Apache 2.0 does not require you to open-source your code. Just include the license notice.

Community Questions

How do I report bugs?

Report bugs on the GitHub Issues page.

How can I contribute?

Contributions are welcome! See the GitHub repository for contribution guidelines.

Where can I get help?

Still Have Questions?

If your question isn't answered here:

  1. Check the API Reference
  2. Review Best Practices
  3. Look at Examples
  4. Open an issue on GitHub

Next Steps

Troubleshooting Guide

This guide helps you resolve common issues when using bytekin.

Transformation Not Applied

Symptoms

  • Hook methods are never called
  • Original code runs without modifications
  • Breakpoints in hooks are never hit

Causes and Solutions

1. Incorrect Class Name

The @ModifyClass value must exactly match the bytecode class name.

Problem:

@ModifyClass("Calculator")  // Wrong!
public class CalcHooks { }

Solution:

@ModifyClass("com.example.Calculator")  // Correct
public class CalcHooks { }

How to verify:

# List all classes in JAR
jar tf myapp.jar | grep -i calculator

2. Wrong Method Descriptor

The methodDesc must exactly match the method signature in bytecode.

Problem:

// Method in bytecode: public int add(int a, int b)
@Inject(methodName = "add", methodDesc = "(int, int)int", at = At.HEAD)  // Wrong!
public static CallbackInfo hook() { }

Solution:

@Inject(methodName = "add", methodDesc = "(II)I", at = At.HEAD)  // Correct
public static CallbackInfo hook() { }

How to find correct descriptor:

# Use javap to see method signatures
javap -c com.example.Calculator | grep -A 5 "public int add"

3. Hook Class Not Passed to Builder

The hook class must be passed to the Builder.

Problem:

BytekinTransformer transformer = new BytekinTransformer.Builder()
    .build();  // Where are the hooks?

Solution:

BytekinTransformer transformer = new BytekinTransformer.Builder(MyHooks.class)
    .build();  // Pass hook class

4. Class Not Yet Loaded

Transformations must be applied before the class is loaded by the JVM.

Problem:

// Class already loaded
Class<?> clazz = Class.forName("com.example.MyClass");

// Now trying to transform - too late!
byte[] transformed = transformer.transform("com.example.MyClass", bytecode);

Solution:

  • Use a custom ClassLoader that applies transformations during loading
  • Or use Java instrumentation/agents to intercept class loading

Type Mismatch Errors

Symptoms

  • java.lang.ClassCastException
  • Wrong values returned from methods
  • Type incompatibility errors

Common Causes

1. Wrong Return Type in CallbackInfo

Problem:

@Inject(methodName = "getCount", methodDesc = "()I", at = At.HEAD)
public static CallbackInfo wrongReturn() {
    CallbackInfo ci = new CallbackInfo();
    ci.cancelled = true;
    ci.returnValue = "42";  // String instead of int!
    return ci;
}

Solution:

@Inject(methodName = "getCount", methodDesc = "()I", at = At.HEAD)
public static CallbackInfo correctReturn() {
    CallbackInfo ci = new CallbackInfo();
    ci.cancelled = true;
    ci.returnValue = 42;  // Correct: int
    return ci;
}

2. Wrong Parameter Types in Hook Method

Problem:

// Target method: void process(int count, String name)
@Inject(methodName = "process", methodDesc = "(ILjava/lang/String;)V", at = At.HEAD)
public static CallbackInfo wrongParams(String name, int count) {  // Reversed!
    return CallbackInfo.empty();
}

Solution:

@Inject(methodName = "process", methodDesc = "(ILjava/lang/String;)V", at = At.HEAD)
public static CallbackInfo correctParams(int count, String name) {  // Correct order
    return CallbackInfo.empty();
}

3. Modifying Arguments to Wrong Type

Problem:

@Invoke(..., shift = Shift.BEFORE)
public static CallbackInfo wrongArgType() {
    CallbackInfo ci = new CallbackInfo();
    ci.modifyArgs = new Object[]{"100"};  // String instead of int
    return ci;
}

Solution:

@Invoke(..., shift = Shift.BEFORE)
public static CallbackInfo correctArgType() {
    CallbackInfo ci = new CallbackInfo();
    ci.modifyArgs = new Object[]{100};  // Correct: int
    return ci;
}

Null Pointer Exceptions

Symptoms

  • NPE during transformation
  • NPE when calling transformed methods
  • Stack trace originates from bytecode

Causes and Solutions

1. Returning null from Injection

Problem:

@Inject(methodName = "getValue", methodDesc = "()Ljava/lang/String;", at = At.HEAD)
public static CallbackInfo returnNull() {
    CallbackInfo ci = new CallbackInfo();
    ci.cancelled = true;
    ci.returnValue = null;  // Valid for objects, but may not be expected
    return ci;
}

Solution:

  • Document that null can be returned
  • Or return a default value instead:
ci.returnValue = "";  // Empty string instead of null

2. Accessing null Parameters in Hooks

Problem:

@Inject(methodName = "process", methodDesc = "(Ljava/lang/String;)V", at = At.HEAD)
public static CallbackInfo unsafeAccess(String input) {
    System.out.println(input.length());  // NPE if input is null!
    return CallbackInfo.empty();
}

Solution:

@Inject(methodName = "process", methodDesc = "(Ljava/lang/String;)V", at = At.HEAD)
public static CallbackInfo safeAccess(String input) {
    if (input != null) {
        System.out.println(input.length());
    }
    return CallbackInfo.empty();
}

Performance Issues

Symptoms

  • Application startup is slow
  • Memory usage is high
  • Response times are degraded

Causes and Solutions

1. Complex Hook Methods

Problem:

@Inject(methodName = "process", methodDesc = "()V", at = At.HEAD)
public static CallbackInfo slowHook() {
    // Database queries
    List<Item> items = database.queryAll();
    // File I/O
    Files.write(Paths.get("log.txt"), data);
    // Expensive computations
    // ...
    return CallbackInfo.empty();
}

Solution:

  • Keep hooks simple and fast
  • Defer expensive work to background threads
  • Use lazy initialization for resources

2. Rebuilding Transformers Repeatedly

Problem:

for (String className : classNames) {
    // Creating new transformer for each class!
    BytekinTransformer transformer = new BytekinTransformer.Builder(Hooks.class)
        .build();
    transformer.transform(className, bytecode);
}

Solution:

// Build once, reuse many times
BytekinTransformer transformer = new BytekinTransformer.Builder(Hooks.class)
    .build();

for (String className : classNames) {
    transformer.transform(className, bytecode);
}

3. Transforming Unnecessary Classes

Problem:

// Applying transformation to all classes, even ones that don't need it
for (String className : allClasses) {
    byte[] transformed = transformer.transform(className, bytecode);
}

Solution:

  • Transform only specific classes that need it
  • Use filtering/naming patterns
  • Profile to identify hotspots

Bytecode Verification Errors

Symptoms

  • java.lang.VerifyError when loading class
  • "Illegal type at offset X" errors
  • Stack trace is hard to interpret

Common Causes

1. Invalid Bytecode Modifications

This usually means the transformation created invalid bytecode.

How to Debug:

  1. Use javap to inspect the transformed bytecode
  2. Look for unusual instruction sequences
  3. Verify return types match

2. Incorrect Method Descriptors

An incorrect descriptor can cause verification failures.

Solution:

  • Double-check all method descriptors
  • Use online descriptor converters to verify
  • Compare with javap output

Methods Not Found

Symptoms

  • Specific methods aren't being transformed
  • Overloaded methods cause issues
  • Constructor transformations fail

Causes and Solutions

1. Overloaded Methods

Overloaded methods must be distinguished by their full descriptor.

Problem:

// Class has multiple add() methods
// add(int, int) and add(double, double)

@Inject(methodName = "add", methodDesc = "(II)I", at = At.HEAD)  // Only matches int version
public static CallbackInfo hook() { }

Solution:

  • Use complete descriptor with parameter and return types
  • The descriptor automatically distinguishes overloads

2. Private or Internal Methods

Some private methods might not be accessible.

Problem:

@Inject(methodName = "internalMethod", methodDesc = "()V", at = At.HEAD)  // Private method
public static CallbackInfo hook() { }

Solution:

  • Verify the method is not synthetic or bridge method
  • Check that method name and descriptor are exactly correct

Cannot Load Transformed Classes

Symptoms

  • ClassNotFoundException after transformation
  • Class appears to be missing
  • Custom ClassLoader issues

Causes and Solutions

1. Incorrect ClassLoader Setup

Problem:

// Trying to use transformed bytecode with default classloader
byte[] transformed = transformer.transform("com.example.MyClass", bytecode);
Class<?> clazz = Class.forName("com.example.MyClass");  // Won't use transformed bytecode!

Solution:

  • Create custom ClassLoader to use transformed bytecode
  • Or use instrumentation/agents to intercept loading

2. Bytecode Corruption

The transformation might have produced invalid bytecode.

Solution:

  • Verify transformation didn't corrupt bytecode
  • Check bytecode size/integrity
  • Use bytecode inspection tools

Debugging Tips

1. Enable Verbose Output

// Add debug logging in hooks
@Inject(methodName = "process", methodDesc = "()V", at = At.HEAD)
public static CallbackInfo debug() {
    System.out.println("[DEBUG] Hook executed");
    System.out.println("[DEBUG] Stack: " + Arrays.toString(Thread.currentThread().getStackTrace()));
    return CallbackInfo.empty();
}

2. Inspect Bytecode

# View transformed bytecode
javap -c -private TransformedClass.class

# Look for your injected calls

3. Use a Bytecode Viewer

Tools like Bytecode Viewer or IDEA plugins help visualize bytecode.

4. Profile Performance

# Use JProfiler or YourKit to identify bottlenecks
# Monitor memory usage and CPU time

Common Questions

Q: Can I transform bootstrap classes? A: Not easily with standard classloaders. Use Java agents with instrumentation API.

Q: Do transformations affect serialization? A: Transformed classes will have different bytecode but same serialization format if you don't change fields.

Q: Can I use bytekin in Spring Boot? A: Yes, but you need to configure custom class loading or use agents.

Getting Help

  1. Check this troubleshooting guide
  2. Review Best Practices
  3. Look at Examples
  4. Open an issue on GitHub

Next Steps