0

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:

  1. Create a subclass of the failing class using ByteBuddy - fails with NoClassDefFoundError, probably because the superclass is in an error state.
  2. 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.
  3. 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)
Saar
  • 1
  • 1

1 Answers1

0

If a class fails to load, it is broken forever. What you can do is that you'd instrument the classes coming in before they are loaded by manipulating their byte code. For example, you could use Byte Buddy to redefine the classes to entirely remove the static initializer. If you additionally mock those classes using Mockito, they cannot execute any code if you choose to mock them.

You can instrument classes by either adding a Java agent or by implementing a custom ClassLoader where you explicitly transform any class prior to its definition.

Rafael Winterhalter
  • 42,759
  • 13
  • 108
  • 192
  • In that case, I think option #2 would be the most feasible. I followed [this](https://stackoverflow.com/questions/23491902/adding-try-catch-block-in-bytecode-through-asm) and implemented it with Byte Buddy's `AsmVisitorWrapper`, however I encounter `java.lang.IllegalArgumentException: JSR/RET are not supported with computeFrames option` on some of the classes coming from Byte Buddy's `ClassReader`. Do you have any idea of how to handle this? – Saar Feb 21 '21 at 09:09
  • Do you reqlly need to conpute the franes? Do you have an example for your setup? – Rafael Winterhalter Feb 22 '21 at 07:08
  • I'm getting an error if I don't compute the frames. I added my setup to the question. – Saar Feb 23 '21 at 08:40
  • It might be, can you create a reproduction so I can look into it? – Rafael Winterhalter Feb 24 '21 at 09:58