In my lastblog entry I explained how proxy based mock frameworks work and created my own
demo mock framework based on CGLIB. Now I will explain how exactly the CGLIB proxy
works. Because then you will understand why these things doesn't work with a
proxy:
- intercept static method calls
- intercept private method calls
- intercept final method calls
- build a proxy for a final class
ASM
CGLIB is
based on ASM. ASM is a library to create class files, on bytecode level, on the
fly. So it's very low level and you need a good understanding of the bytecode
to use it. But it abstracts the bytecode and is much more comfortable to generate
bytecode with ASM than writing the bytecode itself. Because it takes care of
the common structures like classes, constructors, method definitions and so on.
One of the most helpful features is that it takes care about the constant pool.
Because the constant pool holds many elements and you need to adapt it if you
change anything. E.g. if a new method is added than you have to add the needed
elements to the constant pool and increase the constant pool size. Or the
labels in ASM are quite nice, too. Or that there are constants for the opcodes.
After the complete class is generated, you have the bytecode of the class. This
bytecode is passed into the ClassLoader which will load the class and then the
class can be used.
How the generated CGLIB looks
Now you
know how the generation of the proxy works. The question is how the
generated proxy looks like. Actually it's quite simple. The proxy class extends
the original class. All classes which should be intercepted by the proxy will
be overridden. These methods will call the MethodInterceptor:: intercept method
with the corresponding parameters. If the proxy callback is null than the super
method will be called. Here is the code snippet for a simple echo(String)
method:
public class at.rseiler.concept.mock.Foo$$EnhancerByCGLIB$$c0699eacextends at.rseiler.concept.mock.Fooimplements org.mockito.cglib.proxy.Factory{
private static final org.mockito.cglib.proxy.MethodProxy CGLIB$echo$1$Proxy;private static final java.lang.reflect.Method CGLIB$echo$1$Method;// ...private org.mockito.cglib.proxy.MethodInterceptor CGLIB$CALLBACK_0;// ...
public final String echo(String arg0) {if(CGLIB$CALLBACK_0 == null) {CGLIB$BIND_CALLBACKS(this);}
if(CGLIB$CALLBACK_0 != null) {// MethodInterceptor::intercept(Object obj, Method method, Object[] args, MethodProxy proxy)Object obj = CGLIB$CALLBACK_0.intercept(this, GLIB$echo$0$Method, new Object[] {arg0}, CGLIB$echo$0$Proxy);
if(!(obj instanceOf String)) {throw new ClassCastException();}
return obj;}
return super.echo(arg0);}
private static final void CGLIB$BIND_CALLBACKS(java.lang.Object object) {/* compiled code */}
// ...
}
Now you
should understand the limits of the proxy (see above). The limits caused
because the proxy is a subclass of the original class. Therefore it's not
possible to extend a final class or final methods and so on. That's just basic Java
limitations.
With code generation it's not possible to work around these limits. Sure it would be possible to generate a proxy class without the relationship to the original class. Than it would be possible to copy the whole class and insert the MethodInterceptor::intercept calls. This new class could remove all final keywords and make private methods public. The problem is that you can't call any method of this class without reflection. Because you don't have the type information of the "proxy" class at compile time - clearly because the class is generated at runtime. Further you can't pass the "proxy" object to any constructor or any method because it's not a subclass.
With code generation it's not possible to work around these limits. Sure it would be possible to generate a proxy class without the relationship to the original class. Than it would be possible to copy the whole class and insert the MethodInterceptor::intercept calls. This new class could remove all final keywords and make private methods public. The problem is that you can't call any method of this class without reflection. Because you don't have the type information of the "proxy" class at compile time - clearly because the class is generated at runtime. Further you can't pass the "proxy" object to any constructor or any method because it's not a subclass.
The only
way to break this limits is to do bytecode transformations of the original
class before the ClassLoader loads the class. Before the class is loaded
replace all private keywords with public keywords and to remove all final
keywords. To do so you need to create your own ClassLoader which will performs the
bytecode transformation. I don't know the code of PowerMock, but it must work
like this.
At last the
with javap --verbose decompiled bytecode of the echo(String) method from which
I created the Java code above:
public final java.lang.String echo(java.lang.String);flags: ACC_PUBLIC, ACC_FINALCode:stack=7, locals=2, args_size=20: aload_01: getfield #37 // Field CGLIB$CALLBACK_0:Lorg/mockito/cglib/proxy/MethodInterceptor;4: dup5: ifnonnull 178: pop9: aload_010: invokestatic #41 // Method CGLIB$BIND_CALLBACKS:(Ljava/lang/Object;)V13: aload_014: getfield #37 // Field CGLIB$CALLBACK_0:Lorg/mockito/cglib/proxy/MethodInterceptor;17: dup18: ifnull 4521: aload_022: getstatic #64 // Field CGLIB$echo$1$Method:Ljava/lang/reflect/Method;25: iconst_126: anewarray #66 // class java/lang/Object29: dup30: iconst_031: aload_132: aastore33: getstatic #68 // Field CGLIB$echo$1$Proxy:Lorg/mockito/cglib/proxy/MethodProxy;36: invokeinterface #53, 5 // InterfaceMethod org/mockito/cglib/proxy/MethodInterceptor.intercept:(Ljava/lang/Object;Ljava/lang/reflect/Method;[Ljava/lang/Object;Lorg/mockito/cglib/proxy/MethodProxy;)Ljava/lang/Object;41: checkcast #55 // class java/lang/String44: areturn45: aload_046: aload_147: invokespecial #62 // Method at/rseiler/concept/mock/Foo.echo:(Ljava/lang/String;)Ljava/lang/String;50: areturn