2

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?

dreamcrash
  • 47,137
  • 25
  • 94
  • 117
  • 1
    Welcome to SO. Interesting question. Please help me a little bit, so I can help you. Just post a little [MCVE](https://stackoverflow.com/help/mcve) on GitHub, so I can reproduce the problem. I am not a Spring Boot or Gradle user, I am a Maven guy. But if I have a working project setup, I can look into your class-loader problem with AspectJ. I need to see how the uber JAR is packaged, which class loader is used while executing your agent loader class and how/when that one is started. – kriegaex Apr 04 '21 at 09:07

1 Answers1

1

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());
    }
    try (InputStream nestedJar = Objects.requireNonNull(classLoader.getResourceAsStream("BOOT-INF/lib/spring-instrument-5.1.9.RELEASE.jar"))) {
      File targetFile = new File("spring-instrument.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.

kriegaex
  • 63,017
  • 15
  • 111
  • 202
  • Thanks kriegaex a lot. It's wonderful that I solved my problem in same way you presented. We think lonely :) – Никита Apr 06 '21 at 10:53
  • It works for now, but I started receiving strange error: `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)` – Никита Apr 06 '21 at 10:58
  • 1
    That is a typical classloader problem. The AspectJ weaver is loaded in classloader A, but referenced by classes in loader B. I might be able to help you, but not without an [MCVE](https://stackoverflow.com/help/mcve). So now it is your turn. Extract a minimal reproducer project and publish it on GitHub, so I can take a look. I do not wish to speculate or guess. Thanks. – kriegaex Apr 06 '21 at 15:35
  • I happened to click on this question again and just noticed 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. See the answer update I am going to write in a few minutes. – kriegaex Apr 07 '21 at 01:11
  • Removing `import org.aspectj.weaver.loadtime.Agent` really works, thank you for the help. But which aces did you mean? :) – Никита Apr 07 '21 at 13:08
  • Please let us not make my answer and this discussion any longer if your problem is solved. SO is a Q/A site, not a discussion forum. I am happy it works for you now. – kriegaex Apr 07 '21 at 13:10
  • @kriegaex you "should" create a git repo with all this nice examples of yours – dreamcrash Apr 07 '21 at 13:28