How to bypass static initializer block of redefined or subclassed super class
I feel like there's a simple answer to this, and if the answer is it's not feasible - I'm OK with that.
I'm looking to bypass the static initializer block (<clinit>??) of a class that I'm trying to either .redefine or .subclass.
I've tried with
.initializer(LoadedTypeInitializer.NoOp.INSTANCE)
and
.invokable(ElementMatchers.isTypeInitializer())
.intercept(StubMethod.INSTANCE)
To go one step further, I even added
.make(new TypeResolutionStrategy.Active())
.load(siblingClass.getClassLoader(), ClassReloadingStrategy.fromInstalledAgent())
But when I call:
.getLoaded().newInstance();
I'm still seeing the parent class static initializer block get called.
Am I missing something?
Thanks in advance!
You would need to register a ClassVisitor via AsmVisitorWrapper in the builder's visit method. From there, you need to return null for the <clinit> method, this way it will be removed. There is no mechanism in Byte Buddy today.
Thanks @raphw. When you say "There is no mechanism in Byte Buddy today." did you mean that those suggestions won't work based on how Byte Buddy works today?
Here's my use of Byte Buddy:
Class<?> uninitializedClassWithBadClinit =
Class.forName(CLASS_WITH_BAD_CLINIT_CLASSNAME, false, siblingClass.getClassLoader());
ByteBuddyAgent.install();
Class<?> initializedClassWithBadClinitRemoved =
new ByteBuddy()
.redefine(uninitializedClassWithBadClinit)
.visit(ClinitRemovingAsmVisitorWrapper.INSTANCE)
.make()
.load(siblingClass.getClassLoader(), ClassReloadingStrategy.fromInstalledAgent())
.getLoaded();
Object instance = initializedClassWithBadClinitRemoved.newInstance();
And here's my initial ClinitRemovingAsmVisitorWrapper implementation:
/**
* This class visitor wrapper ensures that class files don't include a {@code <clinit>} static
* initializer block, removing them if they do exist.
*/
public enum ClinitRemovingAsmVisitorWrapper implements AsmVisitorWrapper {
/** The singleton instance. */
INSTANCE;
/** The {@code <clinit>} method name. */
protected static final String CLINIT = "<clinit>";
/** {@inheritDoc} */
@Override
public int mergeWriter(int flags) {
return flags;
}
/** {@inheritDoc} */
@Override
public int mergeReader(int flags) {
return flags;
}
/** {@inheritDoc} */
@Override
public ClassVisitor wrap(
TypeDescription instrumentedType,
net.bytebuddy.jar.asm.ClassVisitor classVisitor,
Context implementationContext,
TypePool typePool,
FieldList<InDefinedShape> fields,
MethodList<?> methods,
int writerFlags,
int readerFlags) {
return new ClinitRemovingClassVisitor(classVisitor);
}
/** A class visitor that attempts to remove any logic executed in a {@code <clinit>} method. */
protected static class ClinitRemovingClassVisitor extends ClassVisitor {
/**
* Creates a new {@code <clinit>} removing class visitor.
*
* @param classVisitor The underlying class visitor.
*/
protected ClinitRemovingClassVisitor(ClassVisitor classVisitor) {
super(OpenedClassReader.ASM_API, classVisitor);
}
/** {@inheritDoc} */
@Override
public MethodVisitor visitMethod(
int modifiers, String name, String descriptor, String signature, String[] exception) {
MethodVisitor methodVisitor =
super.visitMethod(modifiers, name, descriptor, signature, exception);
return name.equals(CLINIT)
? null
: methodVisitor;
}
}
}
Trying to return null, in visitMethod I end up getting:
java.lang.UnsupportedOperationException: class redefinition failed: attempted to delete a method
at sun.instrument.InstrumentationImpl.retransformClasses0(Native Method)
at sun.instrument.InstrumentationImpl.retransformClasses(InstrumentationImpl.java:144)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at net.bytebuddy.utility.Invoker$Dispatcher.invoke(Unknown Source)
at net.bytebuddy.utility.dispatcher.JavaDispatcher$Dispatcher$ForNonStaticMethod.invoke(JavaDispatcher.java:1013)
at net.bytebuddy.utility.dispatcher.JavaDispatcher$ProxiedInvocationHandler.invoke(JavaDispatcher.java:1142)
at net.bytebuddy.dynamic.loading.$Proxy26.retransformClasses(Unknown Source)
at net.bytebuddy.dynamic.loading.ClassReloadingStrategy$Strategy$2.apply(ClassReloadingStrategy.java:408)
at net.bytebuddy.dynamic.loading.ClassReloadingStrategy.load(ClassReloadingStrategy.java:236)
at net.bytebuddy.dynamic.TypeResolutionStrategy$Passive.initialize(TypeResolutionStrategy.java:100)
at net.bytebuddy.dynamic.DynamicType$Default$Unloaded.load(DynamicType.java:6154)
...
I also tried implementing an explicit MethodVisitor, but still no luck:
/** A method visitor that skips the code-block. */
protected static class CodeBlockSkippingMethodVisitor extends MethodVisitor {
private static final Logger log = LoggerFactory.getLogger(CodeBlockSkippingMethodVisitor.class);
/**
* Creates a new code-block skipping method visitor.
*
* @param methodVisitor The underlying method visitor.
*/
protected CodeBlockSkippingMethodVisitor(MethodVisitor methodVisitor) {
super(OpenedClassReader.ASM_API, methodVisitor);
}
@Override
public void visitCode() {
log.debug("Bypassing {}", CLINIT);
}
}
If instead of redefine I decide to subclass, I don't end up seeing the <clinit> method called in the visitor, but do see the repercussions of it being called in the super class.
If the class is already loaded, it cannot be redefined anymore. This is a general restriction. If you wanted to avoid this, you would need to register a Java agent via AgentBuilder and match the class by its name before it is loaded for the first time.