Search code examples
aspectjjava-17jep-396

Instrument Java 17 with AspectJ


When I try to run an AspectJ instrumentation with Java 17, I always get errors like the following:

java.lang.reflect.InvocationTargetException
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
        at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.base/java.lang.reflect.Method.invoke(Method.java:568)
        at org.aspectj.weaver.loadtime.ClassLoaderWeavingAdaptor.initializeForJava11(ClassLoaderWeavingAdaptor.java:1069)
        at org.aspectj.weaver.loadtime.ClassLoaderWeavingAdaptor.defineClass(ClassLoaderWeavingAdaptor.java:1096)
        at org.aspectj.weaver.loadtime.ClassLoaderWeavingAdaptor.access$300(ClassLoaderWeavingAdaptor.java:66)
        at org.aspectj.weaver.loadtime.ClassLoaderWeavingAdaptor$SimpleGeneratedClassHandler.acceptClass(ClassLoaderWeavingAdaptor.java:150)
        at org.aspectj.weaver.tools.WeavingAdaptor$WeavingClassFileProvider$1.acceptResult(WeavingAdaptor.java:917)
        at org.aspectj.weaver.bcel.BcelWeaver.weaveAndNotify(BcelWeaver.java:1431)
        at org.aspectj.weaver.bcel.BcelWeaver.weave(BcelWeaver.java:1192)
        at org.aspectj.weaver.tools.WeavingAdaptor.getWovenBytes(WeavingAdaptor.java:549)
        at org.aspectj.weaver.tools.WeavingAdaptor.weaveClass(WeavingAdaptor.java:385)
        at org.aspectj.weaver.loadtime.Aj.preProcess(Aj.java:115)
        at org.aspectj.weaver.loadtime.ClassPreProcessorAgentAdapter.transform(ClassPreProcessorAgentAdapter.java:51)
        at java.instrument/java.lang.instrument.ClassFileTransformer.transform(ClassFileTransformer.java:244)
        at java.instrument/sun.instrument.TransformerManager.transform(TransformerManager.java:188)
        at java.instrument/sun.instrument.InstrumentationImpl.transform(InstrumentationImpl.java:541)
        at java.base/java.lang.ClassLoader.defineClass1(Native Method)
        at java.base/java.lang.ClassLoader.defineClass(ClassLoader.java:1012)
        at java.base/java.security.SecureClassLoader.defineClass(SecureClassLoader.java:150)
        at java.base/jdk.internal.loader.BuiltinClassLoader.defineClass(BuiltinClassLoader.java:862)
        at java.base/jdk.internal.loader.BuiltinClassLoader.findClassOnClassPathOrNull(BuiltinClassLoader.java:760)
        at java.base/jdk.internal.loader.BuiltinClassLoader.loadClassOrNull(BuiltinClassLoader.java:681)
        at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:639)
        at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:188)
        at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:520)
        at java.base/java.lang.Class.forName0(Native Method)
        at java.base/java.lang.Class.forName(Class.java:467)
        at java.base/sun.launcher.LauncherHelper.loadMainClass(LauncherHelper.java:780)
        at java.base/sun.launcher.LauncherHelper.checkAndLoadMain(LauncherHelper.java:675)
Caused by: java.lang.IllegalAccessException: module java.base does not open java.lang to unnamed module @6acbcfc0
        at java.base/java.lang.invoke.MethodHandles.privateLookupIn(MethodHandles.java:259)
        ... 31 more

You can reproduce this by running the example from https://github.com/DaGeRe/aspect-final-example (runMainWithoutError.sh, since the MWE was originally to demonstrate a Java 11 problem). If you run this with Java 11, everything runs fine, if you run this with Java 17 you get the above error.

Is there a known workaround for this, or any way to use AspectJ with Java 17?

EDIT The exact failing versions where:

reichelt@reichelt-desktop:~/nvme/workspaces/dissworkspace/aspect-final-example/woven-project$ java -version
openjdk version "17.0.1" 2021-10-19
OpenJDK Runtime Environment (build 17.0.1+12-Ubuntu-120.04)
OpenJDK 64-Bit Server VM (build 17.0.1+12-Ubuntu-120.04, mixed mode, sharing)
reichelt@reichelt-desktop:~/nvme/workspaces/dissworkspace/aspect-final-example/woven-project$ javac -version
javac 17.0.1

The same happens with GraalVM CE:

reichelt@reichelt-desktop:~/nvme/workspaces/dissworkspace/aspect-final-example/woven-project$ java -version
openjdk version "17.0.1" 2021-10-19
OpenJDK Runtime Environment GraalVM CE 21.3.0 (build 17.0.1+12-jvmci-21.3-b05)
OpenJDK 64-Bit Server VM GraalVM CE 21.3.0 (build 17.0.1+12-jvmci-21.3-b05, mixed mode, sharing)

Due to the error message, I would assume that this somehow is caused by AspectJ not (correctly?) using the modules.


Solution

  • Running AspectJ LTW on JDK 16+

    As described in the AspectJ 1.9.7 release notes, due to JEP 396 you need to add --add-opens java.base/java.lang=ALL-UNNAMED to your Java command line.

    IllegalAccessError in your aspect

    The problem you have when running your runMainWithError.sh script is documented in AspectJ issue #563710. There is an advice which ought to be inlined, but is not.

    Looking at your around advice, I see that it proceeds, but it does not actually change any parameters or return values. So you can simply split it into a before/after-advice pair. Actually, you are not even doing anything after proceeding, so even a simple before-advice would suffice:

    @Before("notWithinAspect() && noSet()")
    public void afterStuff(JoinPoint thisJoinPoint) {
      System.out.println("=== Call: " + thisJoinPoint.getSignature() + " " + thisJoinPoint.getKind());
      System.out.println(thisJoinPoint.getSourceLocation() + " " + thisJoinPoint.getStaticPart());
      System.out.println(thisJoinPoint.toLongString());
    }
    

    If for whatever reason you do need an around advice, you better explicitly only weave the joinpoint types you need instead of excluding set(private * *). The latter does not work because it seems to be evaluated dynamically rather than statically, i.e. joinpoint matching happens during runtime where you expect it to happen during weaving time. I have not fully analysed that, but the suggested workarounds should fix your aspect.


    Incidentally, I was surprised to see your approach to shade the aspect library into the AspectJ weaver instead of simply putting it on the classpath and using the weaver as a Java agent separately. That seems over-engineered and somewhat contrived, forcing you to generate a new manifest in order to emulate the weaver's original one. It also makes the build slower and the weaving process no quicker. Is there any specific reason you did this?


    Update: I analysed the situation a bit, simply configuring the load-time weaver to dump the woven class files:

    <!DOCTYPE aspectj PUBLIC "-//AspectJ//DTD//EN" "http://www.aspectj.org/dtd/aspectj_1_5_0.dtd">
    <aspectj>
      <weaver options="-verbose -showWeaveInfo">
        <include within="de.test..*"/>
        <dump within="de.test..*"/>
      </weaver>
      <aspects>
        <aspect name="de.aspectjtest.ExampleAspect"/>
      </aspects>
    </aspectj>
    

    When decompiling FinalFieldConstructorExample, I see this:

    class FinalFieldConstructorExample {
      private final Integer parameters;
    
      FinalFieldConstructorExample() {
        JoinPoint var5 = Factory.makeJP(ajc$tjp_0, ajc$this, ajc$this);
        ExampleAspect var10000 = ExampleAspect.aspectOf();
        Object[] var6 = new Object[]{ajc$this, var5};
        var10000.aroundStuff((new AjcClosure3(var6)).linkClosureAndJoinPoint(69648), ajc$tjp_0);
      }
    
      // (...)
    }
    

    So you see that the around-advice is executed in ExampleAspect, and from there it is simply forbidden to initialise a final field in FinalFieldConstructorExample. You also see that other than it looks like in the source code, the final field's initialisation is not somehow done "before" or "outside of" the constructor, but actually inside the constructor. This becomes more evident if we decompile the version with the before-advice where there is no more call to an external around-advice:

    class FinalFieldConstructorExample {
      private final Integer parameters;
    
      FinalFieldConstructorExample() {
        JoinPoint var8 = Factory.makeJP(ajc$tjp_7, (Object)null, (Object)null);
        ExampleAspect.aspectOf().beforeStuff(var8);
        super();
        JoinPoint var7 = Factory.makeJP(ajc$tjp_6, this, this);
        ExampleAspect.aspectOf().beforeStuff(var7);
        JoinPoint var6 = Factory.makeJP(ajc$tjp_2, this, this);
        ExampleAspect.aspectOf().beforeStuff(var6);
        byte var2 = 5;
        JoinPoint var1 = Factory.makeJP(ajc$tjp_0, this, (Object)null, Conversions.intObject(var2));
        ExampleAspect.aspectOf().beforeStuff(var1);
        // Convert int to Integer
        Integer var4 = Integer.valueOf(var2);
        JoinPoint var3 = Factory.makeJP(ajc$tjp_1, this, this, var4);
        ExampleAspect.aspectOf().beforeStuff(var3);
        // Write Integer to final field
        this.parameters = var4;
      }
    
      // (...)
    }
    

    We can simplify the situation more by changing the field type from Integer to int (private final int parameters = 5;) in order to avoid the conversion step:

    class FinalFieldConstructorExample {
      private final int parameters;
    
      FinalFieldConstructorExample() {
        JoinPoint var3 = Factory.makeJP(ajc$tjp_5, (Object)null, (Object)null);
        ExampleAspect.aspectOf().beforeStuff(var3);
        super();
        JoinPoint var2 = Factory.makeJP(ajc$tjp_4, this, this);
        ExampleAspect.aspectOf().beforeStuff(var2);
        JoinPoint var1 = Factory.makeJP(ajc$tjp_0, this, this);
        ExampleAspect.aspectOf().beforeStuff(var1);
        this.parameters = 5;
      }
    
      // (...)
    }