Christophe Labouisse bio photo

Christophe Labouisse

Freelance Java expert, Docker enthusiast

Email Twitter Google+ LinkedIn Github Stackoverflow Hopwork

I’m a big fan of Spring Boot and I’m also a very big fan Javassist. I came on an interesting issue where everything was working OK from an IDE but not when launched on a test server.

The instrumentation is pretty simple as is consists in the following code:

public static void doBlackMagic() {
    ClassPool cp = ClassPool.getDefault();
    CtClass cc = cp.get("com.package.Class");
    // Do you stuff
    cc.toClass();
    cc.detach();
}

This code is called from the main just before calling SpringApplication.run in order to instrument the class before it gets any chance of being loaded during the context loading. I works fine from an IDE but once deployed on the test server it failed with a nice stack trace:

10:37:56.325 [main] ERROR calling.Class - Cannot perform instrumentation javassist.NotFoundException: com.package.Class
        at javassist.ClassPool.get(ClassPool.java:450) ~[javassist-3.18.1-GA.jar!/:na]

The diagnostic is pretty easy: from an IDE, the application runs with a normal classpath containing some classes directories and the jars for the dependencies. On the server, the application is running with Spring Boot’s Über-jar as classpath. As Javassist relies on this classpath to find the class to instrument he’ll only have access to the Spring Boot loader classes and the classes belonging directly to the application, but not the dependencies that are packed into the Über-jar under the lib directory.

Solution

The solution is based on the SpringApplicationRunListener interface which will allow us to call SpringApplication.run and run Javassist as soon as possible. The code above will be slightly changed:

public class Warlock implements SpringApplicationRunListener {
    private final SpringApplication application;

    public Warlock(SpringApplication application, String[] args) throws IOException {
        this.application = application;
    }

    @Override
    public void started() {
        doBlackMagic();
    }
}

Additionally, we need create (or update) the META-INF/spring.factories to declare this new listener:

# Run Listeners
org.springframework.boot.SpringApplicationRunListener=\
com.package.Warlock

At this point, this is not enough and the instrumentation is still failing with the same exception. The last tweak will be done in the doBlackMagic method:

public void doBlackMagic() {
    ClassPool cp = ClassPool.getDefault();
    cp.appendClassPath(new LoaderClassPath(application.getClassLoader()));
    CtClass cc = cp.get("com.package.Class");
    // Do you stuff
    cc.toClass();
    cc.detach();
}

And voilà.

Overview