javaspring-bootgradlewaraspectj

How to get access to WEB-INF/lib/ jars?


I have Gradle project with Spring Boot and AspectJ.

Want to load aspectjweaver and spring-instrument javaagents dynamically and directly from WEB-INF/libs (where Spring Boot locate all dependencies)

Gradle dependencies:

enter image description here

AgentLoader:

public class AgentLoader {

private static final Logger LOGGER = LoggerFactory.getLogger(AgentLoader.class);

public static void loadJavaAgent() {
    if (!isAspectJAgentLoaded()) {
        LOGGER.warn("Aspect agent was not loaded!");
    }
}

public static boolean isAspectJAgentLoaded() {
    try {
        Agent.getInstrumentation();
    } catch (NoClassDefFoundError e) {
        return false;
    } catch (UnsupportedOperationException e) {
        LOGGER.info("Dynamically load AspectJAgent");
        return dynamicallyLoadAspectJAgent();
    }
    return true;
}

public static boolean dynamicallyLoadAspectJAgent() {
    String nameOfRunningVM = ManagementFactory.getRuntimeMXBean().getName();
    int p = nameOfRunningVM.indexOf('@');
    String pid = nameOfRunningVM.substring(0, p);
    try {
        VirtualMachine vm = VirtualMachine.attach(pid);
        String jarFilePath = AgentLoader.class.getClassLoader().getResource("WEB-INF/libs/aspectjweaver-1.9.6.jar").toString();
        vm.loadAgent(jarFilePath);
        jarFilePath = AgentLoader.class.getClassLoader().getResource("WEB-INF/libs/spring-instrument-5.3.2.jar").toString();
        vm.loadAgent(jarFilePath);
        vm.detach();
    } catch (Exception e) {
        LOGGER.error("Exception while attaching agent", e);
        return false;
    }
    return true;
 }
}

But found out that return value of getResource() in null

What is the best solution to handle this issue?


Solution

  • Update 2024-01-12: For another reason, I revisited this topic and changed the code a bit, utilising the tiny byte-buddy-agent library as a helper to determine the running JVM's process ID and either self-attach or, if on JDK 9+ that option is deactivated, spawn another helper JVM and attach the agent from there. I made sure that the solution still works on JDK 8, but also on 9+ (tested up to JDK 21).

    I removed the extra options listed below in the original answer from my Maven POM, i.e. no more includeSystemScope and no more system-scoped dependency on tools.jar from the old JDK 8. Instead, only this:

    <dependency>
      <groupId>net.bytebuddy</groupId>
      <artifactId>byte-buddy-agent</artifactId>
      <version>1.14.11</version>
    </dependency>
    

    The new code looks like this:

    public class AgentLoader {
      public static boolean dynamicallyLoadAspectJ() {
        try {
          ClassLoader classLoader = AgentLoader.class.getClassLoader();
          System.out.println("AgentLoader classloader = " + classLoader);
          final String pid = ByteBuddyAgent.ProcessProvider.ForCurrentVm.INSTANCE.resolve();
          try (InputStream nestedJar = Objects.requireNonNull(classLoader.getResourceAsStream("BOOT-INF/lib/aspectjweaver-1.9.7.jar"))) {
            File targetFile = new File("aspectjweaver.jar");
            Files.copy(nestedJar, targetFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
            ByteBuddyAgent.attach(targetFile, pid);
          }
        } catch (Exception e) {
          System.err.println("Exception while attaching agent");
          e.printStackTrace();
          return false;
        }
        return true;
      }
    }
    

    I also decided to use System.err directly instead of using a logger instance here, to avoid loading lots of classes before the aspect weaver is active. Once they are loaded, the weaver cannot change them anymore later.


    Nikita, today is your lucky day. I just had a moment and was curious how to make my code snippet from https://www.eclipse.org/aspectj/doc/released/README-187.html, which obviously you found before, work in the context of Spring Boot. I just used my Maven Spring Boot playground project. Depending on which Java version you are using, you either need to make sure that tools.jar from JDK 8 is defined as a system-scoped dependency and also copied into the executable Spring uber JAR, or you need to make sure that the Java attach API is activated in Java 9+. Here is what I did for Java 8:

    Maven:

    <dependency>
      <groupId>com.sun</groupId>
      <artifactId>tools</artifactId>
      <version>1.8</version>
      <scope>system</scope>
      <systemPath>${java.home}/../lib/tools.jar</systemPath>
    </dependency>
    <!-- (...) -->
    <plugin>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-maven-plugin</artifactId>
      <configuration>
        <mainClass>spring.aop.DemoApplication</mainClass>
        <!-- Important for tools.jar on Java 8 -->
        <includeSystemScope>true</includeSystemScope>
      </configuration>
    </plugin>
    

    The <includeSystemScope> option is necessary because otherwise Boot does not know how to find the attach API classes. Just do something equivalent in Gradle and you should be fine.

    Java:

    You need to know that in order to attach an agent, it must be a file on the file system, not just any resource or input stream. This is how the attach API works. So unfortunately, you have to copy it from the uber JAR to the file system first. Here is how you do it:

    public static boolean dynamicallyLoadAspectJAgent() {
      String nameOfRunningVM = ManagementFactory.getRuntimeMXBean().getName();
      int p = nameOfRunningVM.indexOf('@');
      String pid = nameOfRunningVM.substring(0, p);
      try {
        VirtualMachine vm = VirtualMachine.attach(pid);
        ClassLoader classLoader = AgentLoader.class.getClassLoader();
    
        try (InputStream nestedJar = Objects.requireNonNull(classLoader.getResourceAsStream("BOOT-INF/lib/aspectjweaver-1.9.4.jar"))) {
          File targetFile = new File("aspectjweaver.jar");
          java.nio.file.Files.copy(nestedJar, targetFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
          vm.loadAgent(targetFile.getAbsolutePath());
        }
    
        vm.detach();
      }
      catch (Exception e) {
        LOGGER.error("Exception while attaching agent", e);
        return false;
      }
      return true;
    }
    

    Besides, in my case the files were unter BOOT-INF/lib, not WEB-INF/lib.


    Update: You said you have this follow-up problem somewhere along the line (reformatted for readability):

    failed to access class
      org.aspectj.weaver.loadtime.Aj$WeaverContainer
    from class
      org.aspectj.weaver.loadtime.Aj
    (
      org.aspectj.weaver.loadtime.Aj$WeaverContainer is in
        unnamed module of
        loader 'app';
      org.aspectj.weaver.loadtime.Aj is in
        unnamed module of
        loader org.springframework.boot.loader.LaunchedURLClassLoader @3e9b1010
    )
    at org.aspectj.weaver.loadtime.Aj.preProcess(Aj.java:108)
    

    This means that Aj is unable to find its own inner class Aj.WeaverContainer. This indicates that they are loaded at different points in time and in different classloaders. When remote-debugging into my sample Boot application starting from an executable JAR, I see that the application classloader is actually the LaunchedURLClassLoader's parent, i.e. the class loaded in the parent is trying to access another class only available to its child classloader, which is impossible in Java. It only works the other way around.

    Maybe it helps not to import and reference AspectJ weaver classes from inside the agent loader. Try commenting out the loadJavaAgent() and isAspectJAgentLoaded() methods and also remove import org.aspectj.weaver.loadtime.Agent;. Then in your application just directly call AgentLoader.dynamicallyLoadAspectJAgent() and see if this helps. I have some more aces up my sleeves with regard to agent loading, but let's keep it as simple as possible first.