nameOfWhen validating arguments, it is usually a good idea to include the respective parameter names in the exception message to make it immediately clear which parameter received a bad value:
public static MyObject create(String name) {
if (name.isBlank()) throw new IllegalArgumentException("name must not be blank");
// ...
}
However, there is one crucial duplication here: The parameter name is hardcoded into the exception message. To avoid
this duplication (and for a few more niche use cases), some programming languages (e.g., C#) implement a nameof
expression (or similar) which allows retrieving the textual name of an identifier as a string. This prevents incomplete
refactorings from silently introducing inconsistencies.
Java does not have such a construct...
...but we can build one!
By introspecting the bytecode at runtime, it is even possible to implement a pure library solution to this "problem"
without the need to resort to instrumentation or annotation processing. This is admittedly solving a tiny problem in a
spectacularly over-engineered way, but it turns out to be an interesting exercise in understanding how Java bytecode
works. So, let's hack in a nameOf!
First, it is essential to understand how the JVM operates. When the Java compiler compiles Java source code, it emits
Java bytecode. The bytecode usually contains quite a bit of debug information that can be used to match bytecode
instructions to their sources, which is why debugging Java works nicely in your IDEs. Another useful bit of information
that is usually stored in bytecode is the names of local variables and — when compiling with -parameters — parameters.
The JVM is a stack machine that essentially reads instructions from the bytecode representation of a class. It executes said instructions by pushing arguments onto the operand stack, popping them off to perform operations, and pushing results back onto the stack for subsequent instructions.
nameOfTo actually implement a nameOf method in Java, we will use two key APIs: The StackWalker API
and the relatively recent ClassFile API.
We can use the former to determine the frame from which our nameOf method was called. This frame contains multiple
important bits of information: The calling class, the calling method's name and descriptor, and even the bytecode index
within the calling method.
Optional<StackWalker.StackFrame> optCallerFrame = STACK_WALKER.walk(frames -> frames.skip(1).findFirst());
if (optCallerFrame.isEmpty()) throw new IllegalStateException("Could not find caller frame");
StackWalker.StackFrame callerFrame = optCallerFrame.get();
Class<?> callerClass = callerFrame.getDeclaringClass();
int currentBci = callerFrame.getByteCodeIndex();
if (currentBci < 0) throw new IllegalArgumentException("Cannot determine bytecode index of caller. Ensure the JVM is not configured to strip bytecode indices.");
Using this information, we can locate the .class resource for the calling class and parse it into a ClassModel:
String classResourceName = callerClass.getName().replace('.', '/') + ".class";
byte[] classBytes;
try (InputStream is = callerClass.getClassLoader().getResourceAsStream(classResourceName)) {
classBytes = is.readAllBytes();
} catch (IOException e) {
throw new RuntimeException(e);
}
ClassModel callerClassModel = ClassFile.of().parse(classBytes);
And this is where the fun begins! While it is fairly straightforward to find the correct method by comparing name and descriptor, we still need to find what happens at the bytecode index inside the method. To achieve this, we essentially have to implement a stack machine ourselves. We can iterate over the instructions that make up the body of the calling method and simulate the operand stack.
Rather than storing actual runtime values, our simulated operand stack stores symbolic names (such as local variables or
field names). As instructions execute, these symbolic values are propagated through the simulated stack until we reach
the nameOf call.
Once we reach the desired instruction (which will always be a method invocation), we can recover the symbolic name of the argument from our simulated stack.
int byteCodeOffset = 0;
for (CodeElement element : codeModel.elementList()) {
if (element instanceof Instruction instruction) {
switch (instruction) {
case FieldInstruction field -> {
String name = field.name().stringValue();
int i = name.indexOf('$');
if (i != -1) name = name.substring(i + 1);
switch (field.opcode()) {
case GETSTATIC -> stack.push(new StackValue(name));
case GETFIELD -> {
stack.pop();
stack.push(new StackValue(name));
}
case PUTSTATIC -> stack.pop();
case PUTFIELD -> {
stack.pop();
stack.pop();
}
default -> throw new RuntimeException("Unknown FIELD_ACCESS opcode: " + field.opcode());
}
}
case InvokeInstruction invoke -> {
MethodTypeDesc desc = invoke.typeSymbol();
/*
* Pop the arguments in reverse order from the operand stack, to that args.getFirst() ends up
* being the *first* parameter passed to the invoked method.
*/
int paramCount = desc.parameterCount();
List<StackValue> args = new ArrayList<>(Collections.nCopies(paramCount, null));
for (int i = paramCount - 1; i >= 0; i--) {
args.set(i, stack.pop());
}
String methodName = invoke.method().name().stringValue();
if (invoke.method().owner().name().equalsString("com/example/nameof/Name")
&& methodName.equals("nameOf")
&& byteCodeOffset == currentBci
) {
if (args.isEmpty()) {
throw new IllegalArgumentException("nameOf() call has no argument");
}
return args.getFirst().s;
}
if (!desc.returnType().equals(ConstantDescs.CD_void)) {
stack.push(new StackValue("<result:" + methodName + ">"));
}
}
case LoadInstruction load -> stack.push(new StackValue(localVariables.get(load.slot())));
case StoreInstruction _ -> stack.pop();
// ... Handle all other instructions
default -> throw new IllegalStateException("Unexpected instruction: " + instruction);
}
byteCodeOffset += instruction.sizeInBytes();
}
}
After putting everything together, we end up with a nameOf method that allows us to remove a bit of duplication at the
small cost of an extremely unstable and potentially forward-incompatible implementation!
public static MyObject create(String name) {
if (name.isBlank()) throw new IllegalArgumentException(nameOf(name) + " must not be blank");
// ...
}
jshell> create("");
java.lang.IllegalArgumentException: name must not be blank
No!
Not convinced yet? While the code seems to work fine, it is mostly untested. Even if it were tested, it would be prone to breaking. Every new opcode that affects the operand stack could subtly break this implementation in unpredictable ways. Possible failures range from exceptions to wrong results. This implementation will not work reliably across JVM configurations and compiler implementations, as these could strip optional debug symbols that this implementation relies on. In short, don't use this.