Skip to content
Lenni0451 edited this page Dec 20, 2023 · 4 revisions

TransformerManager

The TransformerManager is the main class of ClassTransform which is responsible for managing all transformers and applying them to classes.

Creating a TransformManager

There are two constructors to choose from:

  • TransformerManager(IClassProvider)
  • TransformerManager(IClassProvider, AMapper)

An IClassProvider is always required. It is used to query classes that are required for the transformation (e.g. stack map frame calculation).
The AMapper is optional and can be used to remap transformers when injecting into obfuscated code. Check out the Mappings page for more information.

Example:

//A BasicClassProvider is used in this example
IClassProvider classProvider = new BasicClassProvider();
TransformerManager transformerManager = new TransformerManager(classProvider/*, mapper*/);

Registering Transformers

Before registering a transformer you need to decide which transformer type you want to use. Check out the Transformer Types page for a list of all available transformer types and how to use them.

Transformer Type Register Method Additional Information
IBytecodeTransformer addBytecodeTransformer(bytecodeTransformer)
IRawTransformer addRawTransformer(className, rawTransformer) The className parameter has to be with the . separator (e.g. net.lenni0451.example.ExampleClass)
CTransformer addTransformer(name), addTransformer(classNode) or addTransformer(classNode, requireAnnotation) The name parameter is the name of the transformer (e.g. net.lenni0451.example.ExampleTransformer). Wildcard imports (net.lenni0451.example.* use ** to match all subpackages) are supported if the used IClassProvider supports them.
IPostTransformer addPostTransformConsumer(postTransformer)
IAnnotationHandlerPreprocessor addTransformerPreprocessor(annotationHandlerPreprocessor)
IAnnotationCoprocessor addCoprocessor(coprocessorSupplier) A supplier is used to create a new instance of the coprocessor for every transformer.

Manually transforming class bytecode

Manually transforming class bytecode is required when using a custom ClassLoader or when transforming classes that are not loaded into the runtime (e.g. transforming .jar files).
The TransformerManager provides the byte[] transform(final String name, byte[] bytecode) method for this purpose. There is also an overload that takes an additional boolean parameter to calculate the stack map frames (default is true).

The name parameter is the name of the class (e.g. net.lenni0451.example.ExampleClass) separated with ..
The bytecode parameter is the bytecode of the class.
The return value is the transformed bytecode of the class. If the bytecode should not be changed null is returned.

Example:

TransformerManager transformerManager = ...;
String className = ...;
byte[] bytecode = ...;

byte[] transformedBytecode = transformerManager.transform(className, bytecode);
if (transformedBytecode != null) {
    //The bytecode was changed by a transformer
    bytecode = transformedBytecode;
}
//Load the class or write it to a file

Using a Java Agent

ClassTransform was designed to be used together with a Java Agent which is the recommended way to use ClassTransform as it allows for the most flexibility and is the easiest to use.
To use ClassTransform as a Java Agent you can call the hookInstrumentation(instrumentation) method with your Instrumentation instance. There is also an overload that takes an additional boolean parameter to enable hot-swapping support (default is false as it causes an overhead).
When calling the hookInstrumentation method all loaded classes are automatically retransformed.

Important note when transforming already loaded classes:
Due to the way ClassTransform works, additional fields and methods are added to the target class when using a CTransformer. This causes the JVM to throw an exception when trying to retransform the target class.
This can be circumvented by using the @CInline annotation on the transformer methods in a CTransformer.

Example:

TransformerManager transformerManager = ...;
Instrumentation instrumentation = ...;

//Hook the instrumentation - add true as the second parameter to enable hot-swapping support
transformerManager.hookInstrumentation(instrumentation/*, true*/);

Manually using the Java Agent

The TransformerManager class implements the ClassFileTransformer interface which is used by the Java Agent API.
This allows you to manually add the TransformerManager as a transformer to the Instrumentation instance. You need to retransform all loaded classes yourself if required.

Example:

TransformerManager transformerManager = ...;
Instrumentation instrumentation = ...;

//Add the transformer manager as a transformer
instrumentation.addTransformer(transformerManager/*, true*/);

//Retransform required classes
instrumentation.retransformClasses(...);

Using a ClassLoader

ClassTransform also supports using a custom ClassLoader to transform classes. The InjectionClassLoader is an implementation that can be used for this purpose.

To use the InjectionClassLoader you need to create a new instance and pass the TransformerManager and an array of URLs to the constructor. Optionally you can also pass a parent ClassLoader to the constructor.
After creating the InjectionClassLoader you can call the executeMain(className, methodName, args) method to execute the main method of the specified class. The main method must be static and take a String[] as the parameter.
The className parameter is the name of the class (e.g. net.lenni0451.example.ExampleClass) separated with ..
If the main method has a different signature you need to call the method manually:

//Set the context class loader
TransformerManager transformerManager = ...;
InjectionClassLoader injectionClassLoader = new InjectionClassLoader(transformerManager, ClassLoader.getSystemClassLoader(), urls...);

Thread.currentThread().setContextClassLoader(injectionClassLoader);
Class<?> mainClass = injectionClassLoader.loadClass("net.lenni0451.example.ExampleClass");
Method method = mainClass.getDeclaredMethod("main", String[].class, int.class /* Example for a different signature */);
method.setAccessible(true);
method.invoke(null, args, 0);

The main method should not be called directly (e.g. MainClass.main(args, 0)). This can cause issues with the ClassLoader.

Depending on your project setup you might need to change the loader priority. This can be done by calling the setPriority(priority) method. The default priority is CUSTOM_FIRST.

Full Example

IClassProvider classProvider = new BasicClassProvider();
TransformerManager transformerManager = new TransformerManager(classProvider);
transformerManager.addTransformerPreprocessor(new MixinsTranslator());

transformerManager.addTransformer("net.lenni0451.example.ExampleTransformer1");
transformerManager.addTransformer("net.lenni0451.example.ExampleTransformer2");
transformerManager.addTransformer("net.lenni0451.example.ExampleTransformer3");

transformerManager.hookInstrumentation(instrumentation);

Class Provider

ClassTransform requires an IClassProvider to query classes that are required for the transformation (e.g. stack map frame calculation).
In the core module, there is only one implementation available: BasicClassProvider. It uses the current class loader to query classes and does not support wildcard imports.
The AdditionalClassProvider submodule provides a few more implementations that may be useful in some cases.

A class provider can be created by implementing the IClassProvider interface.
The byte[] getClass(final String name) method is used to query classes.
The Map<String, Supplier<byte[]>> getAllClasses() method is used to get all available classes. This is only used when using a CTransformer with a wildcard import.

Example:

public class ExampleClassProvider implements IClassProvider {

    @Override
    public byte[] getClass(final String name) throws ClassNotFoundException {
        //Query the class
        return ...;
    }

    @Override
    public Map<String, Supplier<byte[]>> getAllClasses() {
        //Query all classes
        return ...;
    }

}