A Tangled Web We Weave
An alternative to modifying the JRE is to modify a Java applications bytecode. If we could effectively modify the bytecode, we wouldnt need to recompile the original source code. Because we are modifying selected portions of bytecode in an application, the changes would be localized and contained -- unlike the previous JRE modification approach. There are several useful and powerful tools available to modify Java bytecode such as Javassist. We chose to leverage AspectJ, the popular aspect-oriented programming framework for Java, for our bytecode modifications. AspectJ is less powerful than a tool like Javassist that lets you directly manipulate bytecode, but it was sufficient for our needs and easy to integrate with our workflow.
Aspect-oriented programming (AOP) evolved from OOP as a means to separate common code fragments that cross-cut a number of classes. Proponents of AOP maintain that encapsulating this common functionality in a single aspect enhances the maintainability of the code. For our purposes, we were less concerned with the pros and cons of aspects and more interested in leveraging tools that weave these aspects across the objects. AspectJ supports the weaving of aspects at load-time, so we dont need to rebuild the application. AspectJ relies upon text pattern matching to identify join points in the application where advice from the aspect can be inserted.
Using AspectJ, we can take an approach that is less like an evil twin and more like a parasite that can be attached when needed. To selectively replace Sockets with our own customized Socket (e.g., CustomSocket), we can use AspectJ to create an aspect that wraps around calls to the Socket constructor. This aspect takes the Socket that is being created in that constructor call and passes it to the CustomSocket constructor. The CustomSocket will delegate to the Socket that has been passed in, injecting behavior to mimic certain properties. For example, to mimic low bandwidth, the CustomSocket will return a modified CustomInputStream that wraps the original InputStream. This CustomInputStream will sleep for a set period of time on each "read" operation, resulting in net data transfer at the desired bandwidth.
A simple AspectJ aspect is shown in Example 2. Its job is to find calls to two kinds of Socket constructors, one with no arguments and one with two arguments, a String and an int. Notice that we are matching against calls to the Socket constructor and not the execution of the Socket constructor. This distinction means that our pointcut will insert bytecode into any class that instantiates a Socket with one of these constructors, rather than inside the Socket class itself. The advantage of not modifying the Socket is that the original vanilla Socket is still available for those classes that need it. This is a useful benefit of our aspect weaving approach, but the dirty little secret is that this was done by necessity rather than by choice.
public aspect SocketSwap { pointcut onCreateSocketNoArgs(): call (Socket.new()) && !within(SocketSwap); pointcut onCreateSocketTwoArgs(String host, int port): args(host, port) && call(Socket.new(..)) && !within(SocketSwap); Socket around(): onCreateSocketNoArgs() { return new CustomSocket(new Socket()); } Socket around(String host, int port) throws UnknownHostException, IOException: onCreateSocketTwoArgs(host, port) { return new CustomSocket(new Socket(host, port)); } }
The downside of this bytecode manipulation approach is that we are modifying the application codes direct access to Sockets only, and indirect invocations will not be modified. Because AspectJ uses a custom classloader to insert aspects, classes loaded via other classloaders (e.g., the bootstrap classloader) cannot be woven. Although some other bytecode manipulation tools are more flexible, similar constraints apply. This is why we could not modify Socket bytecode directly. Leaving Socket unmodified is fine for our purposes, but there is a more ominous implication. The inability to weave into classes on the bootstrap classloader means that we cant modify JRE classes that may internally invoke Socket constructors, such as several classes within the java.rmi package. You could potentially implement customized RMI classes and apply these aspects in the same way that we applied our CustomSocket, but not only would you need to understand and duplicate the non-Socket related internals of the RMI classes, but you would also need to carefully review the implementation of the JRE to capture all Socket invocations. Similar to the modified JRE approach, this would be highly version-specific and similarly brittle.
Conclusion
We have demonstrated two approaches to modifying the behavior of third-party Java applications without rebuilding or otherwise directly modifying the original source code. They both have their pros and cons, but under the right circumstances can provide an interesting means to modify behavior. While not all applications can be successfully modified by these two approaches, there will be many Java applications that can benefit from this post-build modification of behavior.