Advanced patching
Everybody writes code that depends on other code. This code is called libraries and it’s a good thing because otherwise we would constantly reinvent the wheel. Unfortunately, even library developers make mistakes and the libraries contain bugs. The happy path to getting such a problem solved looks like this:
- The bug comes from an open source library,
- you open a bug report,
- the author fixes the bug,
- a new version of the library is released,
- you use the new version in your project.
Unfortunately, every step can go wrong and then we are stuck with the broken library. Sometimes, a workaround is possible, sometimes, we can keep the bug and blame the library. But most of the time, there’s a customer waiting for the bug to be fixed, no matter what’s the reason and who to blame for it.
In this situation, the library can be patched. Maybe in the Ruby or Javascript universe, this is pretty common due to the dynamic nature of these languages. But in Java land, it’s really bad practice. And for good reasons.
You copy whole class(es) from the library into your project and change the code. If the code is not open source, you have the first problem here. If you have the code, you have to maintain it. This means to document the changes applied to it, and make sure it still works when the original library is updated.
Lately, I had this problem, and was not happy about the perspectives. But I remembered a talk I saw about runtime byte code manipulation and gave it try.
The problem at hand had to do with an event bus. It allows to send and receive any Java Object
. It also allows to attach Javascript clients running in browsers to it. But as soon as the first browser is attached, only JSON objects are allowed to be sent. Otherwise, exceptions are thrown all over you. A very nice design: Attaching an external client to the running system can cause failures in totally distinct parts of the system. No comment.
It all boils down to this method:
private void deliverMessage(SockJSSocket sock, String address, Message message) {...}
If I could access the Message
and transform its payload into a JSON string before it is sent, the problem would be solved.
Using Byte Buddy, this can be done with these few lines:
public class JsonEventBridgeInterceptor {
public patchEventBus() {
ByteBuddyAgent.install();
new AgentBuilder.Default()
.type(named("io.vertx.ext.web.handler.sockjs.impl.EventBusBridgeImpl"))
.transform((builder, typeDesc, classLoader) -> builder
.method(named("deliverMessage"))
.intercept(MethodDelegation
.to(JsonEventBridgeInterceptor.class)
.appendParameterBinder(Morph.Binder.install(Morpher.class))))
.installOn(ByteBuddyAgent.install());
}
public interface Morpher {
Object invoke(Object[] args);
}
public static void intercept(@AllArguments Object[] args, @Morph Morpher morpher) {
final Message message = (Message) args[2];
args[2] = transformIntoJsonMessage(message);
morpher.invoke(args);
}
}
Byte Buddy does some magic to install a Java agent into the running JVM. The agent can manipulate the byte code of every class that is loaded. In this case, a transformer is registered for the deliverMessage
method which delegates all calls to the intercept
method. Here, the message payload is transformed into a JSON object, and the original method is invoked with the new message.
Voilà. Problem solved without copying any code. It works without a separate compiling step or running the JVM with some special arguments. It’s still patching and I still feel dirty, but it works, I learned something new, and I think it’s as clean as it can get.
By the way, this is not the only use case for Byte Buddy. A lot more cool things can be achieved with a little fantasy…