Once in a while you may find yourself in a situation, where you need to debug a third party library on a production server or other environment where it is not possible or impractical to attach a debugger. It may also be the case that modifying the sources of that library and compiling a custom version is not an option (due to compilation dependencies, licenses, source not available or whatnot).
I found myself in such a situation a while back when I needed to understand memory leaks in OpenOffice JURT. I achieved this by injecting debug logging into the library with AspectJ, and in this post I will describe how.
In my situation the third party library was a transitive Maven dependency. In order to weave this library with my own aspects, I added this to the pom.xml:
<build> <plugins> <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>aspectj-maven-plugin</artifactId> <version>1.4</version> <configuration> <!-- Weave third party dependency --> <weaveDependencies> <weaveDependency> <groupId>org.openoffice</groupId> <artifactId>jurt</artifactId> </weaveDependency> </weaveDependencies> </configuration> <!-- Weave on compile --> <executions> <execution> <id>compile</id> <goals> <goal>compile</goal> </goals> </execution> </executions> </plugin> </plugins> </build>
Note! In order for the weaved version of the library to be included in your .war, the above should be located in the web module if your Maven project has multiple modules. If your aspect is in another module you also need to add
<aspectLibraries> <aspectLibrary> <groupId>your.project</groupId> <artifactId>module-with-aspect</artifactId> </aspectLibrary> </aspectLibraries>
to the configuration.
You’ll likely also want to add the AspectJ runtime dependency, for the aspect implementation.
<dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjrt</artifactId> <version>1.6.12</version> </dependency>
Now lets move on with the actual aspect. If you need an introduction to AspectJ, you may find this post useful. First we have our aspect class:
import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * AspectJ aspect used for debugging memory leaks caused by OpenOffice JURT * @author Mattias Jiderhamn */ @Aspect public class JURTDebugAspect { private static final Logger LOG = LoggerFactory.getLogger(JURTDebugAspect.class); /** Utility method to get StackTraceElement of caller */ private static StackTraceElement getCaller(int levels) { StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace(); return stackTrace[levels + 1]; } }
Inside that aspect class, we can add different joinpoints and advice to inject logging.
@Before("execution(* com.sun.star.lib.util.AsynchronousFinalizer.add(..))") public void addCalled() { LOG.warn("JURT: add() called by " + getCaller(3)); }
This will log calls to the third party API add()
method, including calls from other third party libraries. Note that “execution” pointcut is used rather than “call” pointcut, since we are weaving the callee, not the caller. The getCaller()
utility method also allows us to see what code called the add()
method.
@Before("call(* java.lang.Thread.start(..)) && within(com.sun.star.lib.util.*) && target(thread)") public void threadStarted(Thread thread) { LOG.warn("JURT: Thread.start() called by " + getCaller(2) + " on thread named " + thread.getName() + " of type " + thread.getClass().getName()); }
Here we add some logging for when the third party library calls the start()
method on java.lang.Thread
, and include the name and type of the started thread.
@Around("call(* com.sun.star.lib.util.AsynchronousFinalizer$Job.run(..)) && within(com.sun.star.lib.util.*) && target(job)") public Object jobRun(ProceedingJoinPoint proceedingJoinPoint, Object job) throws Throwable { LOG.info("JURT: Job.run() starting: " + job.getClass().getName() + " in thread " + Thread.currentThread().getName()); final long start = System.currentTimeMillis(); try { return proceedingJoinPoint.proceed(); } finally { LOG.info("JURT: Job.run() done: " + job.getClass().getName() + " in thread " + Thread.currentThread().getName() + "; took " + (System.currentTimeMillis() - start) + " ms"); } }
Here we log internal calls to Job.run()
inside the API, and measure the time that those calls take. The name of the Job
subclass is included.
Good luck debugging third party libraries!