-
-
Notifications
You must be signed in to change notification settings - Fork 5
Basics
The TransformerManager
is the main class of ClassTransform which is responsible for managing all transformers and applying them to classes.
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*/);
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 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
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*/);
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(...);
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 URL
s 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
.
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);
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 ...;
}
}