For a monitoring project I try to instrument the OutputStream
class with Javassist. I would like to make the OutputStream
write()
method print whatever byte[]
is given to it (encoding is a different problem and does not matter here). My problem is that replacing the OutputStream
class during runtime with an agent crashes the JVM. Using Javassist to just manipulate the Bytecode fails as somehow Javassist ClassPool.get("java.io.OutputStream")
also crashes the JVM.
Is that class so untouchable? The same approach worked fine for the URI
class.
Is there another way of doing what I would like or is my only option to replace the rt.jar in the JRE?
@Override
public byte[] transform(ClassLoader loader, String className,
Class<?> classBeingRedefined, ProtectionDomain protectionDomain,
byte[] classfileBuffer) {
ClassPool pool = ClassPool.getDefault();
try {
CtClass cc = pool.get("java.io.OutputStream");
CtMethod method =
Arrays.stream(cc.getDeclaredMethods()).filter(ctMethod -> ctMethod.getLongName().equals("java.io.OutputStream.write(byte[],int,int)"))
.findFirst().get();
method.insertBefore("System.out.println(new String($1, java.nio.charset.StandardCharsets.UTF_8));");
return cc.toBytecode();
} catch (NotFoundException | CannotCompileException | IOException e) {
e.printStackTrace();
}
return null;
}
produces the output:
Exception in thread "main" java.lang.NoClassDefFoundError: org/springframework/boot/SpringApplication (wrong name: java/io/OutputStream)
because I overwrite the very first loaded class with my "new" OutputStream
already. The OutputStream
class however is never called into the transform method...
The transform
method is a callback function that may get invoked for every loaded class. You receive the class name in the className
parameter and the original definition in the classfileBuffer
.
So the first thing you have to do, is to check whether current invocation really corresponds to a class you want to transform. If not, just return null
. You don’t do this, but return a transformed version of java.io.OutputStream
regardless of which class you were requested to transform. That’s why you get the error. You were requested to transform the class org/springframework/boot/SpringApplication
but return a class named java/io/OutputStream
. Note the “wrong name” in the message. Of course, this mismatching class would cause even more problems without that name verification.
After checking that the current invocation is responsible for java.io.OutputStream
, you should use the provided classfileBuffer
for reading the original class version, instead of using ClassPool.getDefault().get(…)
, as the ClassPool
does not know the version of the class you received in the parameter and might find a wrong version of it or be unable to access the class al all.
But note that instrumenting the method OutputStream.write(byte[],int,int)
alone is unlikely to be sufficient. As its documentation says:
Subclasses are encouraged to override this method and provide a more efficient implementation.
This happens in practice in all relevant implementation classes. To record the invocations of the overriding methods you have to instrument them as well. Also mind that subclasses may override write(byte[])
with an implementation that does not delegate to write(byte[],int,int)
.