Search code examples
javaaopaspectjjvm-bytecodejd-gui

Why does AspectJ generate an empty Annotation check?


I'm using AspectJ 1.8.8 compile-time weaving and I have a block like this

@SomeAnnotation(value="someValue")
public List doSomething(String someArg) {
    ...
}

where @SomeAnnotation is implemented with an "Around" advice.

Looking at the bytecode with JD-GUI, I see the following generated code (slightly formatted):

public class SomeClass {
  private static Annotation ajc$anno$5;

  ...

  @SomeAnnotation(value="someValue")
  public List doSomething(String someArg)
  {
    String str = someArg;
    JoinPoint localJoinPoint = Factory.makeJP(ajc$tjp_5, this, this, str);
    Object[] arrayOfObject = new Object[3];
    arrayOfObject[0] = this;
    arrayOfObject[1] = str;
    arrayOfObject[2] = localJoinPoint;
    Annotation tmp56_53 = ajc$anno$5;
    if (tmp56_53 == null) {
      tmp56_53;
    }
    return (List)new SomeClass.AjcClosure11(arrayOfObject).linkClosureAndJoinPoint(69648).around(tmp56_53, (SomeAnnotation)(ajc$anno$5 = SomeClass.class.getDeclaredMethod("doSomething", new Class[] { String.class }).getAnnotation(SomeAnnotation.class)));
  }
}

I was wondering why that condition (if (tmp56_53...)) even exists as it appears to be doing nothing (And is also syntactically incorrect Java? Maybe because this was generated by ajc?). I'm curious about this because it's causing "branch misses" in a coverage tool (JaCoCo).


Edit 1

Here is the raw Java machine code from javap:

       0: aload_1
       1: astore_2
       2: getstatic     #480                // Field ajc$tjp_10:Lorg/aspectj/lang/JoinPoint$StaticPart;
       5: aload_0
       6: aload_0
       7: aload_2
       8: invokestatic  #312                // Method org/aspectj/runtime/reflect/Factory.makeJP:(Lorg/aspectj/lang/JoinPoint$StaticPart;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Lorg/aspectj/lang/JoinPoint;
      11: astore_3
      12: invokestatic  #339                // Method com/foo/SomeAspect.aspectOf:()Lcom/foo/SomeAspect;
      15: iconst_3
      16: anewarray     #2                  // class java/lang/Object
      19: astore        4
      21: aload         4
      23: iconst_0
      24: aload_0
      25: aastore
      26: aload         4
      28: iconst_1
      29: aload_2
      30: aastore
      31: aload         4
      33: iconst_2
      34: aload_3
      35: aastore
      36: new           #484                // class com/foo/SomeClass$AjcClosure21
      39: dup
      40: aload         4
      42: invokespecial #485                // Method com/foo/SomeClass$AjcClosure21."<init>":([Ljava/lang/Object;)V
      45: ldc_w         #327                // int 69648
      48: invokevirtual #333                // Method org/aspectj/runtime/internal/AroundClosure.linkClosureAndJoinPoint:(I)Lorg/aspectj/lang/ProceedingJoinPoint;
      51: getstatic     #488                // Field ajc$anno$10:Ljava/lang/annotation/Annotation;
      54: dup
      55: ifnonnull     86
      58: pop
      59: ldc           #75                 // class com/foo/SomeClass
      61: ldc_w         #489                // String someArg
      64: iconst_1
      65: anewarray     #348                // class java/lang/Class
      68: dup
      69: iconst_0
      70: ldc           #171                // class java/lang/String
      72: aastore
      73: invokevirtual #352                // Method java/lang/Class.getDeclaredMethod:(Ljava/lang/String;[Ljava/lang/Class;)Ljava/lang/reflect/Method;
      76: ldc_w         #341                // class com/foo/SomeAnnotation
      79: invokevirtual #358                // Method java/lang/reflect/Method.getAnnotation:(Ljava/lang/Class;)Ljava/lang/annotation/Annotation;
      82: dup
      83: putstatic     #488                // Field ajc$anno$10:Ljava/lang/annotation/Annotation;
      86: nop
      87: checkcast     #341                // class com/foo/SomeAnnotation
      90: invokevirtual #362                // Method com/foo/SomeAspect.around:(Lorg/aspectj/lang/ProceedingJoinPoint;Lcom/foo/SomeAnnotation;)Ljava/lang/Object;
      93: pop
      94: return

Looks like ifnonnull could be conditional in question, but I am not familiar at all with JVM instructions, and I still don't know why AspectJ would generate logic like this.


Solution

  • tl;dr: It's a normal lazy initialization, and jd is just confused.

    Byte 16 is where it creates that new Object[3]:

    16: anewarray     #2                  // class java/lang/Object
    

    You can see after that in 19-35 that it's just immediately copying the local variables onto the stack (iconst for the index, aload for the reference) and then writing them to the array (aastore). The immediate next byte is 36, which is the new operator (just the allocation, then immediately followed by the invokespecial to run the constructor).

    That brings us to byte 48, which calls linkClosureAndJoinPoint. You didn't include your constants table, but at 45 ldc_w #327 loads the constant value 69648, so that brings us up to the point of .around.

    Now something interesting happens at byte 51. The single chained call that jd reconstructed is now interrupted. The bytecode loads the static annotation field ajc$anno$10 (not 5, as jd says) onto the stack. If that annotation field is not null (55), then execution jumps to 86 (a no-op, used as a "landing point" for the jump), which performs that cast check ((SomeAnnotation)) , then finally actually invokes the advice.

    The code that's skipped over (58-82) says this, which you'll recognize from the decompilation:

    SomeClass.class
        .getDeclaredMethod("doSomething", new Class[] { String.class })
        .getAnnotation(SomeAnnotation.class)
    

    Byte 83 then stores the result into the static field, and execution proceeds from there.

    In Java terms, this is exactly what's happening:

    if (cachedAnnotation == null) {
        cachedAnnotation = getAnnotationOnMethodUsingReflection();
    }
    

    AspectJ's bytecode is very tight and clean here (likely hand-optimized because this is likely to be very hot code). Either because of that or because that logic interrupts the chained method call, jd is getting confused and splitting the null check and the assignment.