I have a scenario where I need to load classes from unknown sources and instantiate them for mocking - I don't need the code to run, but methods and properties must be in the resulting instance. I also need the class's name to remain unchanged, so its instance can be assigned to fields from that type of other already loaded classes.
Sometimes a class instantiation fails due to an ExceptionInInitializerError
, leaving the class in an invalid state which is impossible to recover. I do not know which class will fail beforehand.
Consider this:
class A {
static {
// Throws exception, resulting in 'A' changing to an error state
}
}
class B {
// In case 'A' could not be instantiated properly, I wish to mock
// it so it can be assigned to this field
private A someField;
}
The following is what I came up with:
- Create a subclass of the failing class using ByteBuddy - fails with
NoClassDefFoundError
, probably because the superclass is in an error state. - Modify the class's static initializer and wrap it in try-catch statements while it is loaded using a ByteBuddy's agent - this seems rather complicated to accomplish in a portable manner.
- Load a class in a separate temporary class loader and identify the initialization failure; if an
ExceptionInInitializerError
has been thrown, redefine that class and remove its static initializer. This also appears very complex to achieve and results in various linkage and circularity errors.
Am I missing something? Is there a simpler way to achieve what I'm looking for?
Edit:
I eventually got some kind of solution working, however I was unable to get Byte Buddy to filter out classes without a static initializer (It will compute the frames for every class)
agentBuilder = agentBuilder.type(ElementMatchers.any())
.transform((builder, type, classLoader, module) -> builder
.visit(new AsmVisitorWrapper.AbstractBase() {
@Override
public int mergeWriter(int flags) {
return flags | ClassWriter.COMPUTE_FRAMES;
}
@Override
public ClassVisitor wrap(TypeDescription instrumentedType, ClassVisitor classVisitor,
Implementation.Context implementationContext, TypePool typePool,
FieldList<FieldDescription.InDefinedShape> fields,
MethodList<?> methods, int writerFlags, int readerFlags) {
if (methods.stream().noneMatch(MethodDescription::isTypeInitializer)) {
return classVisitor;
}
return new ClassVisitor(Opcodes.ASM9, classVisitor) {
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor,
String signature, String[] exceptions) {
MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor,
signature, exceptions);
methodVisitor = new JSRInlinerAdapter(methodVisitor, access,
name, descriptor,
signature, exceptions);
if (name.equals("<clinit>")) {
methodVisitor = new TryCatchBlockSorter(methodVisitor, access, name,
descriptor, signature, exceptions);
methodVisitor = new MethodVisitor(Opcodes.ASM7, methodVisitor) {
private final Label start = new Label();
private final Label end = new Label();
private final Label handler = new Label();
@Override
public void visitCode() {
super.visitCode();
visitTryCatchBlock(start,
end,
handler,
"java/lang/RuntimeException");
visitLabel(start);
}
@Override
public void visitMaxs(int maxStack, int maxLocals) {
visitJumpInsn(Opcodes.GOTO, end);
visitLabel(handler);
visitInsn(Opcodes.RETURN);
visitLabel(end);
super.visitMaxs(maxStack, maxLocals);
}
};
}
return methodVisitor;
}
};
}
}));
In addition, I'm hitting the following exception on certain classes, might be a Byte Buddy bug?
[Byte Buddy] ERROR ch.qos.logback.core.util.COWArrayList [ClassLoader@3b00856b, unnamed module @4189d70b, loaded=false]
java.lang.NullPointerException
at net.bytebuddy.description.type.TypeDescription$Generic$Visitor$Reducing.onGenericArray(TypeDescription.java:2326)
at net.bytebuddy.description.type.TypeDescription$Generic$Visitor$Reducing.onGenericArray(TypeDescription.java:2281)
at net.bytebuddy.description.type.TypeDescription$Generic$OfGenericArray.accept(TypeDescription.java:4415)
at net.bytebuddy.description.method.MethodDescription$Token.asSignatureToken(MethodDescription.java:1915)
at net.bytebuddy.description.method.MethodList$AbstractBase.asSignatureTokenList(MethodList.java:109)
at net.bytebuddy.dynamic.scaffold.inline.RebaseDynamicTypeBuilder.make(RebaseDynamicTypeBuilder.java:227)
at net.bytebuddy.agent.builder.AgentBuilder$Default$ExecutingTransformer.doTransform(AgentBuilder.java:10438)
at net.bytebuddy.agent.builder.AgentBuilder$Default$ExecutingTransformer.transform(AgentBuilder.java:10374)
at net.bytebuddy.agent.builder.AgentBuilder$Default$ExecutingTransformer.access$1600(AgentBuilder.java:10140)
at net.bytebuddy.agent.builder.AgentBuilder$Default$ExecutingTransformer$Java9CapableVmDispatcher.run(AgentBuilder.java:10833)
at net.bytebuddy.agent.builder.AgentBuilder$Default$ExecutingTransformer$Java9CapableVmDispatcher.run(AgentBuilder.java:10771)
at java.base/java.security.AccessController.doPrivileged(AccessController.java:391)
at net.bytebuddy.agent.builder.AgentBuilder$Default$ExecutingTransformer.transform(AgentBuilder.java:10330)
at net.bytebuddy.agent.builder.AgentBuilder$Default$ExecutingTransformer$ByteBuddy$ModuleSupport.transform(Unknown Source)
at java.instrument/sun.instrument.TransformerManager.transform(TransformerManager.java:188)
at java.instrument/sun.instrument.InstrumentationImpl.transform(InstrumentationImpl.java:563)