This explained why I was seeing problems with my debug code. The args field of Debug was being set in a static initialization block, when the class was first loaded into the JVM. Due to some race conditions, my code could call System.setProperty("java.net.debug", options) before Debug was loaded, but there was no way of ensuring that. And of course, the args file was marked as private static final so there was no way to modify it outside of the class after that point.
But it was even worse than that. The static methods isOn and getInstance were used by the JSSE internal classes to determine whether debug information should be logged or not, and those fields were also marked private static final, i.e. in sun.security.ssl.SSLContextImpl:
In order to turn debugging on, I not only needed to change the args field in sun.security.ssl.Debug after it had been initialized, but I also needed to change every class that used private static final Debug debug = Debug.getInstance("ssl"); so that it would no longer be null.
The first idea I came up with was to change Debug.getInstance() and Debug.isOn. Now, there are ways to change method definitions in the JVM. The package java.lang.instrument defines java agents that can swap out code at run time ("hot reloading"), there's "HotSwap" the JVM debug option, and there's the disposable classloader option. None of them are really applicable in this case – JSSE is a system level package, Play does not require a java agent, and HotSwap is… well, it would probably work fine. But we don't actually need to change the method definition. We just need to swap out private static final field references at run time.
The language tells you that you can't touch final fields and it tells you that you absolutely can't touch private fields from outside the class. It's lying. If you own the JVM, you can monkey patch any field reference.
So, we've got the means to swap out any given field. Now we need to find all the classes that have a Debug field defined. This is a little trickier, as the class loader only knows about the fields in classes that have already been loaded. We need to go through the JAR file, load all the classes in that JAR into memory, and then quiz them about whether they have that field or not.
So, we add a class finder trait:
And then we're going to set up something that will do Debug and args swapping specifically. There are a couple of wrinkles to this: we need to have AccessController give us privileged conditions, and between 1.6 and 1.7 the package name of Debug changed from com.sun.net.ssl.internal.ssl.Debug to sun.security.ssl.Debug, so we have to get the class we want at runtime as well.
That's it! Just call FixInternalDebugLogging("ssl") and you can turn on debug information in TLS dynamically, at runtime.
I tried a couple of different things to swap out the Debug class to use a logger instead of System.out.println – this workd in some cases where debug.println was used, but not in others, where System.out.println is used explicitly. There was a patch at one point to fix this, but as of 2014, the only way to do this reliably is to change System.out to a logger. Still, this is tremendously useful when you are in the Play console or running a couple of tests to a specific client.
This is a technique that works best on initialization code where the original codebase just needs to be tweaked a bit, but monkey patching can be applied to any accessible field. It's obviously unsafe, but it's effective. Evil, sure. But effective.