I'll start with a very loud sorry... Would you have lost the chance to post this meme while talking of Java? Me neither.
Now, back to the serious stuff.
The problem
Although I have a love-hate relationship with Java, it is what pays my bills every single day. Yes, I am a Java dev.
I was porting a project from Java 17 to Java 20 (or Java 21) and consequently from Spring Boot 3.0.x to Spring Boot 3.2.0 (which at the time of writing is on Milestone stage). Boot 3.2.0 used Spring 6.1 under the hood, whereas 3.0.x used 6.0.
The update worked like a breeze, but when I ran the test suite, there it was an error. Something along the lines of:
org.springframework.data.mapping.MappingException: Parameter org.springframework.data.mapping.PreferredConstructor$Parameter@c3f657893 does not have a name!
Something inside Spring Data was not working correctly. Obviously I searched every corner of the web for a solution, but to no avail. I don't know if it was a real undocumented issue or something out of my ignorance, but either way the only way left was the debugger.
After some fiddling, I stumbled upon StandardReflectionParameterNameDiscoverer.java:
@Nullable
private String[] getParameterNames(Parameter[] parameters) {
String[] parameterNames = new String[parameters.length];
for (int i = 0; i < parameters.length; i++) {
Parameter param = parameters[i];
if (!param.isNamePresent()) {
return null; // the debugger takes this route
}
parameterNames[i] = param.getName();
}
return parameterNames;
}
I learned isNamePresent()
goes to some native code to understand if the parameter name is available in the compiled bytecode. Spoiler: it wasn't. Now it was clear it was a reflection issue.
The solution
Again, back to the search. What I found was that to have reflection metadata bundled inside the .class
files, the code has to be compiled with java -parameters
.
I was not using Gradle to run the tests, but IntelliJ. And guess what? Gradle does this automatically granted you have set your sourceCompatibility
above 11, while IntelliJ does not. Once I configured the IDE to use javac -parameters
everything was back to working order.
The documentation
Could it be that such a big change went unannounced? Or again was it due to my ignorance?
The answer can be found promptly on the wiki of the Spring Framework project at this page:
LocalVariableTableParameterNameDiscoverer
has been removed in 6.1. Compile your Java sources with the common Java 8+-parameters
flag for parameter name retention (instead of relying on the-debug
compiler flag) in order to be compatible withStandardReflectionParameterNameDiscoverer
. With the Kotlin compiler, we recommend the-java-parameters
flag.
It was indeed communicated in some way, although I would have preferred a somewhat louder announcement.
Final considerations
Why was this change done only on this minor version update? And why javac
does not put parameter names into the bytecode by default?
Well, as this very informative Stack Overflow answer states, nothing comes for free.
Summarizing the answer, there are three major concerns:
- File size
- Compatibility
- Exposure of sensible information
File size
As imaginable, filling the .class
files with reflection metadata even if not needed increases the size of the class file itself. I don't consider this a real-world problem: Java application tend to be chunky anyway, a couple of megs more are not the issue here.
Compatibility
Although changing the parameter names doesn't hurt the binary compatibility of bytecode in the JVM, Java embraces very strictly the segregation pattern and treats them as local variables. You shouldn't really depend on them.
But, sometimes reflection is convenient, think JSON to class mapping or ORMs. I take convenience over dependence every day, especially because Java is already too verbose. Adding other boilerplate to manually map Jackson POJOs is one step more in the direction of "too much".
Exposure of sensible information
This can be true. If an attacker gains knowledge of the .class
file source (looking at you Log4j), exposing the internal parameter names inherently broadens the attack surface area.
Hopefully my code artifacts will stay well hidden and protected behind some server and not reach any unwanted end user.