Life Codecs @ NamingCrisis.net

Ruminations. Reflections. Refractions. Code.

Apr 5, 2010 - software dev

Generics and Method Selection: A Backwards Compatibility Gotcha

If you managed to read through the rather long post title… let’s move on to a much longer post.

I recently came across an interesting bug at work which was rather eye-opening on just how nasty backwards-compatibility (which prompted type erasure!) can be. Credits are due to my friend and colleague Marcus Hasslinger who reasoned through the issue with me from a backwards compatibility viewpoint.

Some background: we use Java 5 and EJB 3, we also make use of annotated methods that are inspected by custom EJB 3 interceptor(s). A sample EJB might implementation might be:

1
2
3
4
5
6
7
8
9
10
11
@Local
public interface Foo {
    void doSomething();
}

@Stateless
@Interceptors({TransactionHandlerInterceptor.class})
public class FooServiceImpl implements Foo {
    @RollbackWhen(...)
    public void doSomething() { /* ... */ }    
}

In the above, the interceptor (TransactionHandlerInterceptor) is an around-invoke type interceptor which intercepts doSomething(), reads the annotation on it (RollbackWhen) and decides some rollback logic.

The defect was that in a particular EJB, it would not detect the method as having this annotation, and thus the necessary rollback logic was never executed. Thus begins our saga. Rather than going through work artifacts (which I probably can’t show anyway) and bore you with our business domain, I’ll create an essentially similar example using the classic ‘Shape’ set of objects (I know.. I know…).

This particular EJB had the following structure:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// package foo
// Domain objects
public interface Shape {
}

public class Circle implements Shape {
}

// Services
public interface ShapeService<T extends Shape> {
    void draw(T shape);
}

@Local
public interface CircleService extends ShapeService<Circle> {
}

@Stateless
@Interceptors({TransactionHandlerInterceptor.class})
public class CircleServiceImpl implements CircleService {
    @Override
    @RollbackWhen(...)
    public void draw(final Circle shape) {
        System.out.println("Drawing circle");
    }
}

// Sample dumb, non-EJB client
public class Client {
    public static void main(final String[] args) {
        final CircleService circleService = new CircleServiceImpl();
        circleService.draw(new Circle());
    }
}

The issue was that whenever the interceptor intercepted CircleServiceImpl.draw(), it would not detect that it was annotated with @RollbackWhen. So I printed out the actual method.toString() output from within the interceptor – this turned out to be:

1
public void draw(Shape)

instead of the expected:

1
public void draw(Circle)

Obviously draw(Shape) is not annotated with @RollbackWhen, draw(Circle) is. Hmm. So where exactly does draw(Shape) come from though, we only ever implemented draw(Circle) in CircleServiceImpl. It’s time to look at type-erasure in action under the hood using ‘javap’ – a useful little bytecode disassembler that comes as part of the stock JDK. The following output was obtained by running ‘javap -c ’ (without the .class).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
// javap -c Shape Circle ShapeService CircleService CircleServiceImpl Client
Compiled from "Shape.java"
public interface foo.Shape{
}

Compiled from "Circle.java"
public class foo.Circle extends java.lang.Object implements foo.Shape{
public foo.Circle();
  Code:
   :   aload_0
   1:   invokespecial   #10; //Method java/lang/Object."":()V
   4:   return

}

Compiled from "ShapeService.java"
public interface foo.ShapeService{
public abstract void draw(foo.Shape);

}

Compiled from "CircleService.java"
public interface foo.CircleService extends foo.ShapeService{
}

Compiled from "CircleServiceImpl.java"
public class foo.CircleServiceImpl extends java.lang.Object implements foo.CircleService{
public foo.CircleServiceImpl();
  Code:
   :   aload_0
   1:   invokespecial   #10; //Method java/lang/Object."":()V
   4:   return

public void draw(foo.Circle);
  Code:
   :   getstatic       #18; //Field java/lang/System.out:Ljava/io/PrintStream;
   3:   ldc     #24; //String Drawing circle
   5:   invokevirtual   #26; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
   8:   return

public void draw(foo.Shape);
  Code:
   :   aload_0
   1:   aload_1
   2:   checkcast       #35; //class foo/Circle
   5:   invokevirtual   #37; //Method draw:(Lfoo/Circle;)V
   8:   return

}

Compiled from "Client.java"
public class foo.Client extends java.lang.Object{
public foo.Client();
  Code:
   :   aload_0
   1:   invokespecial   #8; //Method java/lang/Object."":()V
   4:   return

public static void main(java.lang.String[]);
  Code:
   :   new     #16; //class foo/CircleServiceImpl
   3:   dup
   4:   invokespecial   #18; //Method foo/CircleServiceImpl."":()V
   7:   astore_1
   8:   aload_1
   9:   new     #19; //class foo/Circle
   12:  dup
   13:  invokespecial   #21; //Method foo/Circle."":()V
   16:  invokeinterface #22,  2; //InterfaceMethod foo/CircleService.draw:(Lfoo/Shape;)V
   21:  return

Let’s start with the simple cases: Shape and Circle – Shape is a simple interface, all fine and dandy. Circle is a class implementing Shape, we can see the default constructor generated since we didn’t provide one of our own.

Next we look at ShapeService – a bit more interesting – it has an abstract method draw(Shape). Generics information is erased, there’s no ‘T’ anywhere. T is replaced by its upperbound, i.e. Shape. Thus the generated method too is draw(Shape). So far so good.

Moving on to CircleService – once more, the parameterisation is nowhere to be seen, there’s therefore no notion of Circle in CircleService, it simply extends ShapeService, and thus inherits the same method signature: draw(Shape). This is what got me – it of course, needs to be this way due to type erasure. You get the illusion of full type-safety, as if a new method draw(Circle) is automatically generated in CircleService – since after all, compile-time code completion, etc. in your IDE works just dandy on a reference of type CircleService – CircleService.draw(Circle) – never does it show CircleService.draw(Shape)!

Let’s move on further – things are getting more interesting. CircleServiceImpl has 2 overloaded methods, draw(Circle) which is our code converted to bytecode, and a compiler generated draw(Shape) – which casts (or at least does a cast-check – haven’t read the bytecode description) to ensure the supplied Shape is indeed a Circle, and then delegates on to our draw(Circle). Think about this for a moment – by itself, it’s just madness – but from a backwards compatibility viewpoint — 2 methods need to be there, our own draw(Circle) which as you can see, did not override any method in the interface as such(!), and draw(Shape) the compiler generated one – which is needed to satisfy the type-erased interfaces we implemented!

One final piece of the puzzle — client code: once more we see a default constructor is generated since we didn’t create one. And then the main() method which in fact calls draw(Shape), not draw(Circle). This is once more correct in a backwards-compatible-whacky way — as we saw above CircleService only ever had draw(Shape) — since we used the CircleService reference, that’s the method we got. And thus the implementation class needs to generate draw(Shape) and delegate to our draw(Circle).

In as far as our defect goes — this is exactly why it did not detect the annotation — it was intercepting the client-invoked, compiler-generated draw(Shape) which in turn invoked draw(Circle). draw(Circle) is of course an internal invocation, not accessed via the EJB proxy anymore, and hence never intercepted again.

Ah backwards compatibility.

That aside, there are a few observations to be made:

  • A generics call in the above fashion will always incur an additional cast check and method call, not a big deal for most code, but within a loop, etc. best to measure it. On the bright side, the checkcast is a single JVM instruction. Another optimisation – if you can even call it that! – is to explicitly declare a method draw(Circle) in CircleService – then it is generated as per normal, and client code goes directly to CircleServiceImpl.draw(Circle) – at this time, this is our workaround for the defect. I’ll leave the discussion of where the annotation should belong for another time (i.e. implementation class or interface – this is more a design principle/responsibility issue)
  • It’s interesting that specifically from an EJB 3 interception and transactional semantics viewpoint – this is quite nasty since you do not intercept a method that you think is part of your EJB interface, nor does the transactional semantics get applied (apart from the default ‘required’ of course)! I wonder if it might be better, if in an EJB 3 class, the generated draw(Shape) method did not do a plain invocation of draw(Circle), but accessed it via an internal EJB proxy reference (circleServiceEjbRef.draw(Circle)) – this would ensure proper transactional semantics and interception. Wait – the generated method in fact should not enforce any transaction type either — it should just be (if I recall correctly) using ‘supports’ semantics. As it stands, if draw(Circle) were annotated with a transaction attribute other than ‘required’ – this would not be picked up at all since it was a direct invocation, not via the EJB transactional proxy! Nasty, nasty! It could lead to some very obscure bugs!

Hmm.. I never remember reading about this in any generics book :P. Anyway, if you got this far, hope it was an informative read.