This blog is about Java (advanced Java topics like Reflection, Byte Code transformation, Code Generation), Maven, Web technologies, Raspberry Pi and IT in general.

Sonntag, 29. Juni 2014

Explanation how CGLIB proxies work


 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$$c0699eac
  extends at.rseiler.concept.mock.Foo
  implements 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.
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_FINAL

Code:
stack=7, locals=2, args_size=2
0: aload_0
1: getfield      #37                 // Field CGLIB$CALLBACK_0:Lorg/mockito/cglib/proxy/MethodInterceptor;
4: dup
5: ifnonnull     17
8: pop
9: aload_0
10: invokestatic  #41                 // Method CGLIB$BIND_CALLBACKS:(Ljava/lang/Object;)V
13: aload_0
14: getfield      #37                 // Field CGLIB$CALLBACK_0:Lorg/mockito/cglib/proxy/MethodInterceptor;
17: dup
18: ifnull        45
21: aload_0
22: getstatic     #64                 // Field CGLIB$echo$1$Method:Ljava/lang/reflect/Method;
25: iconst_1
26: anewarray     #66                 // class java/lang/Object
29: dup
30: iconst_0
31: aload_1
32: aastore
33: 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/String
44: areturn
45: aload_0
46: aload_1
47: invokespecial #62                 // Method at/rseiler/concept/mock/Foo.echo:(Ljava/lang/String;)Ljava/lang/String;
50: areturn

Kommentare:

  1. Danke fur die Information! This really helps with a project I'm working on... tightly integrated 3rd party library which was not built with any useful interfaces!

    AntwortenLöschen
  2. I have read your blog its very attractive and impressive. I like it your blog.

    Java Training in Chennai Java Training in Chennai

    AntwortenLöschen