Hacking A Maven Dependency with Javassist to Fix It
Have you ever wondered what to do when needing "just a small change" to a third-part library your project depended on? This post describes how to use Maven and Javassist to take a dependency of your project, instrument it to modify its behavior, re-pack it, and release it as an artifact with a different name (so that you me depend on my-customized-lib instead of on lib).
The process is as follows:
The explanation will follow the Maven lifecycle phases the individual operations are bound to.
An example is the jasper-el-customized-jsf12, which modifies org.apache.tomcat:jasper-el and releases it as jasper-el-customized-jsf12. Notice that I have added the jasper-el as an explicit dependency to the project - this is to make my IDE aware of it and to make it posssible for Maven to compile my instrumentation helper class (which accesses jasper-el classes). In theory it shouldn't be necessary as the classes will be made available in the process-sources phase but it would require some tweaking of the compiler's and IDE's classpaths though it would be a cleaner approach (for I would avoid having the dependency twice on the classpath: once explicitly as a .jar artifact and once as unpacked classes).
First you of course need to write the instrumentation code. I've decided to use Javassist because it doesn't introduce any new runtime dependencies to the modified artifact. (Read my recent introduction into Javassist if you feel the need.)
See JavassistTransformer.java for details of the implementation, the code is rather simple and straightforward. Few commnets:
(I have to specify Javassist as an explicit dependency because its scope as a project dependency is "provided" and thus the exec plugin ignores it.)
Testing doesn't require any special configuration as the instrumented classes to test are already in target/classes. If we wanted to have them somewhere else, we would just provide the surefire plugin's configuration with an additionalClasspathElements/additionalClasspathElement.
(You will notice that I have actually done that in the POM even though it is unnecessary given my direct usage of target/classes.)
BTW, if you wonder how the tests written in Groovy get compiled and executed, notice that I've configured that in the parent POM. (Which is the only piece of configuration there with any impact on this module.)
The process is as follows:
- Phase process-sources - maven-dependency-plugin unpacks the dependency to classes/
- Phase compile (implicit) - compile the bytecode manipulation code
- Phase process-classes - exec-maven-plugin executes the compiled Javassist instrumenter to modify the unpacked classes
- Phase test - run tests on the instrumented code
- Phase package - let maven-jar re-package the instrumented classes, excluding the instrumenter itself
Why: The Case Introduction
Modifying binaries of third-party libraries is certainly an ugly thing but sometimes it's the most feasible way to satisfy a need. So it was with my JSF EL Validator that reuses existing EL implementations with plugged-in custom variable and property resolvers (returning fake values of the expected types because real objects aren't available at the validation time). The problem was that the EL specification requires short-circuit evaluation of ?: branches and of /boolean expressions while to be able to validate the expressions I needed all parts of them to be evaluated. The only feasible solution proved to be the modification of the EL implementations to evaluate all children of a boolean/choice node.How: Javassist, Dependency and Exec Plugins
I've used common Maven plugins to fetch and unpack the dependency, to execute the instrumentation code, and to pack the classes again and release them as an artifact with a different name. The instrumentation is implemented with a little of Java code leveraging Javassist to perform the actual modification on the unpacked class files.The explanation will follow the Maven lifecycle phases the individual operations are bound to.
0. Set-up a Project to Do the Instrumentation
First we create a new project (or more likely a Maven module of the main project) to fetch, modify, pack, and release the dependency we need to tweak. The important thing in the project is only the POM that binds all the plugins and phases together and the Javassist code that performs the instrumentation.An example is the jasper-el-customized-jsf12, which modifies org.apache.tomcat:jasper-el and releases it as jasper-el-customized-jsf12. Notice that I have added the jasper-el as an explicit dependency to the project - this is to make my IDE aware of it and to make it posssible for Maven to compile my instrumentation helper class (which accesses jasper-el classes). In theory it shouldn't be necessary as the classes will be made available in the process-sources phase but it would require some tweaking of the compiler's and IDE's classpaths though it would be a cleaner approach (for I would avoid having the dependency twice on the classpath: once explicitly as a .jar artifact and once as unpacked classes).
1. Phase process-sources - maven-dependency-plugin unpacks the dependency
First we need to fetch and unpack the dependency so that Javassist can operate on it. The easiest thing is to unpack it into target/classes/ so that the tests and the JAR plugin have access to it without any configuration of their classpath.
(Notice that the instrumentation code could be compiled without dependening on the artifact being modified as the code to insert/modified can be represented just as String.)
This is how to do it:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>2.3</version>
<executions>
<execution>
<id>unpack</id>
<goals>
<goal>unpack</goal>
</goals>
<configuration>
<artifactItems>
<artifactItem>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-jasper-el</artifactId>
<version>${jasper.jsf20.version}</version>
</artifactItem>
</artifactItems>
<excludes>META-INF/**</excludes>
<outputDirectory>${project.build.outputDirectory}</outputDirectory>
<overWriteReleases>true</overWriteReleases>
</configuration>
</execution>
</executions>
</plugin>
2. Phase compile (implicit) - compile the bytecode manipulation code
No configuration necessary here, mvn compile will compile the Javassist instrumentation code as it would do with any other Java code.First you of course need to write the instrumentation code. I've decided to use Javassist because it doesn't introduce any new runtime dependencies to the modified artifact. (Read my recent introduction into Javassist if you feel the need.)
See JavassistTransformer.java for details of the implementation, the code is rather simple and straightforward. Few commnets:
- JavassistTransformer has a main() so it can be run by the exec plugin
- Javassist's ClassPool is instructed to search for the classes to instrument in the target/classes folder
- We tell Javassist what classes it should find and which of their methods it should modify (via insertBefore) and then we save the modified classes.
- I've minimized the code to inject in the transformer where it can only be represented as a string, and just insert a delegation of the processing to a helper class, GetValueFix.java, which is a regular Java code using the dependency's classes and compiled by Javac and added to the modified jar.
3. Phase process-classes - exec-maven-plugin executes the compiled Javassist instrumenter to modify the unpacked classes
The compiled instrumentation code has to be executed - that's why it has a main method - to actually modify the unpacked classes. That's easily achieved with the exec plugin:
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>1.2.1</version>
<executions>
<execution>
<goals>
<goal>java</goal>
</goals>
<phase>process-classes</phase>
</execution>
</executions>
<configuration>
<mainClass>net.jakubholy.jeeutils.jsfelcheck.jasperelcustomizer.instrumenter.JavassistTransformer</mainClass>
<arguments>
<argument>${project.build.outputDirectory}</argument>
</arguments>
<includePluginDependencies>true</includePluginDependencies>
</configuration>
<dependencies>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>${javassist.version}</version>
</dependency>
</dependencies>
</plugin>
(I have to specify Javassist as an explicit dependency because its scope as a project dependency is "provided" and thus the exec plugin ignores it.)
4. Phase test - run tests on the instrumented code
You wouldn't expect me not to test my code, right?Testing doesn't require any special configuration as the instrumented classes to test are already in target/classes. If we wanted to have them somewhere else, we would just provide the surefire plugin's configuration with an additionalClasspathElements/additionalClasspathElement.
(You will notice that I have actually done that in the POM even though it is unnecessary given my direct usage of target/classes.)
BTW, if you wonder how the tests written in Groovy get compiled and executed, notice that I've configured that in the parent POM. (Which is the only piece of configuration there with any impact on this module.)