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
- Review API Reference
- Check out Examples
- Join the community and share your patterns