To do it generally would require rewriting the bytecode (probably with a Java Agent, or library using it) or the source code.
The way do it without hacking the code is to use an interface and a Proxy
. Interfaces are often suggested, but Java gets in the way with its old fashioned, super verbose syntax.
import java.io.*;
import java.lang.reflect.*;
import java.util.*;
import java.util.stream.*;
// Gratuitous use of new fangled record feature and streams.
record TraceInvocation(PrintStream out) {
public <T> T trace(Class<T> type, T target) {
Objects.requireNonNull(target);
return type.cast(Proxy.newProxyInstance(
type.getClassLoader(),
new Class<?>[] { type },
(proxy, method, args) -> {
// Apparently args can be null. Ffs.
out.println(
(target==null ? type.getSimpleName() : escape(target))+
"."+method.getName()+
// There's probably a better way without {}.
"("+(args==null ? "" : String.join(", ",
Stream.of(args)
.map(TraceInvocation::escape)
.toArray(String[]::new)
))+")"
);
return method.invoke(target, args);
}
));
}
// Don't even think about allowing log injection.
// (Okay, weird syntax.)
private static String escape(Object object) {
// I am not a fan of streams.
int[] escaped = String.valueOf(object).codePoints().flatMap(cp ->
(cp == '\' || cp == '.' || cp == ',') ?
IntStream.of('\', cp) :
(' ' <= cp && cp <= '~' ) ?
IntStream.of(cp) :
("\"+/*(int)*/cp+"\").codePoints()
).toArray();
return new String(escaped, 0, escaped.length);
}
}
Use as:
CharSequence cs = new TraceInvocation(System.err)
.trace(CharSequence.class, "Scunthorpe");
cs.subSequence(4, 10).length(); // No log for length
cs.charAt(2);
cs.length();
Possible variation include filtering which methods to display, logging return values/exceptions, alternative to toString
and tracing returned values.
I found this approach really useful when dealing with sending and receiving a stream in a proprietary format.
与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…