Java
String getParamName(String param) throws Exception {
StackTraceElement[] strace = new Exception().getStackTrace();
String methodName = strace[0].getMethodName();
int lineNum = strace[1].getLineNumber();
String className = strace[1].getClassName().replaceAll(".{5}$", "");
String classPath = Class.forName(className).getProtectionDomain().getCodeSource().getLocation().getPath() + className + ".class";
StringWriter javapOut = new StringWriter();
com.sun.tools.javap.Main.run(new String[] {"-l", "-c", classPath}, new PrintWriter(javapOut));
List<String> javapLines = Arrays.asList(javapOut.toString().split("\\r?\\n"));
int byteCodeStart = -1;
Map<Integer, Integer> byteCodePointerToJavaPLine = new HashMap<Integer, Integer>();
Pattern byteCodeIndexPattern = Pattern.compile("^\\s*(\\d+): ");
for (int n = 0;n < javapLines.size();n++) {
String javapLine = javapLines.get(n);
if (byteCodeStart > -1 && (javapLine == null || "".equals(javapLine))) {
break;
}
Matcher byteCodeIndexMatcher = byteCodeIndexPattern.matcher(javapLine);
if (byteCodeIndexMatcher.find()) {
byteCodePointerToJavaPLine.put(Integer.parseInt(byteCodeIndexMatcher.group(1)), n);
} else if (javapLine.contains("line " + lineNum + ":")) {
byteCodeStart = Integer.parseInt(javapLine.substring(javapLine.indexOf(": ") + 2));
}
}
int varLoadIndex = -1;
int varTableIndex = -1;
for (int i = byteCodePointerToJavaPLine.get(byteCodeStart) + 1;i < javapLines.size();i++) {
if (varLoadIndex < 0 && javapLines.get(i).contains("Method " + methodName + ":")) {
varLoadIndex = i;
continue;
}
if (varLoadIndex > -1 && javapLines.get(i).contains("LocalVariableTable:")) {
varTableIndex = i;
break;
}
}
String loadLine = javapLines.get(varLoadIndex - 1).trim();
int varNumber;
try {
varNumber = Integer.parseInt(loadLine.substring(loadLine.indexOf("aload") + 6).trim());
} catch (NumberFormatException e) {
return null;
}
int j = varTableIndex + 2;
while(!"".equals(javapLines.get(j))) {
Matcher varName = Pattern.compile("\\s*" + varNumber + "\\s*([a-zA-Z_][a-zA-Z0-9_]*)").matcher(javapLines.get(j));
if (varName.find()) {
return varName.group(1);
}
j++;
}
return null;
}
Atualmente, isso funciona com algumas dicas:
- Se você usar um IDE para compilar isso, ele poderá não funcionar, a menos que seja executado como Admin (dependendo de onde os arquivos de classe temporários são salvos)
- Você deve compilar usando
javac
com a -g
bandeira. Isso gera todas as informações de depuração, incluindo nomes de variáveis locais no arquivo de classe compilado.
- Isso usa uma API Java interna
com.sun.tools.javap
que analisa o bytecode de um arquivo de classe e produz um resultado legível por humanos. Essa API é acessível apenas nas bibliotecas JDK, portanto, você deve usar o tempo de execução java JDK ou adicionar tools.jar ao seu caminho de classe.
Agora isso deve funcionar mesmo que o método seja chamado várias vezes no programa. Infelizmente ainda não funciona se você tiver várias chamadas em uma única linha. (Para quem faz, veja abaixo)
Experimente online!
Explicação
StackTraceElement[] strace = new Exception().getStackTrace();
String methodName = strace[0].getMethodName();
int lineNum = strace[1].getLineNumber();
String className = strace[1].getClassName().replaceAll(".{5}$", "");
String classPath = Class.forName(className).getProtectionDomain().getCodeSource().getLocation().getPath() + className + ".class";
Esta primeira parte obtém algumas informações gerais sobre em que classe estamos e qual é o nome da função. Isso é feito criando uma exceção e analisando as 2 primeiras entradas do rastreamento de pilha.
java.lang.Exception
at E.getParamName(E.java:28)
at E.main(E.java:17)
A primeira entrada é a linha na qual a exceção é lançada, da qual podemos obter o methodName e a segunda entrada é de onde a função foi chamada.
StringWriter javapOut = new StringWriter();
com.sun.tools.javap.Main.run(new String[] {"-l", "-c", classPath}, new PrintWriter(javapOut));
Nesta linha, estamos executando o executável javap que acompanha o JDK. Este programa analisa o arquivo de classe (bytecode) e apresenta um resultado legível por humanos. Usaremos isso para "análise" rudimentar.
List<String> javapLines = Arrays.asList(javapOut.toString().split("\\r?\\n"));
int byteCodeStart = -1;
Map<Integer, Integer> byteCodePointerToJavaPLine = new HashMap<Integer, Integer>();
Pattern byteCodeIndexPattern = Pattern.compile("^\\s*(\\d+): ");
for (int n = 0;n < javapLines.size();n++) {
String javapLine = javapLines.get(n);
if (byteCodeStart > -1 && (javapLine == null || "".equals(javapLine))) {
break;
}
Matcher byteCodeIndexMatcher = byteCodeIndexPattern.matcher(javapLine);
if (byteCodeIndexMatcher.find()) {
byteCodePointerToJavaPLine.put(Integer.parseInt(byteCodeIndexMatcher.group(1)), n);
} else if (javapLine.contains("line " + lineNum + ":")) {
byteCodeStart = Integer.parseInt(javapLine.substring(javapLine.indexOf(": ") + 2));
}
}
Estamos fazendo algumas coisas diferentes aqui. Primeiro, estamos lendo a saída javap linha por linha em uma lista. Segundo, estamos criando um mapa de índices de linha de bytecode para índices de linha javap. Isso nos ajuda mais tarde a determinar qual chamada de método queremos analisar. Finalmente, estamos usando o número de linha conhecido no rastreamento de pilha para determinar qual índice de linha de bytecode queremos ver.
int varLoadIndex = -1;
int varTableIndex = -1;
for (int i = byteCodePointerToJavaPLine.get(byteCodeStart) + 1;i < javapLines.size();i++) {
if (varLoadIndex < 0 && javapLines.get(i).contains("Method " + methodName + ":")) {
varLoadIndex = i;
continue;
}
if (varLoadIndex > -1 && javapLines.get(i).contains("LocalVariableTable:")) {
varTableIndex = i;
break;
}
}
Aqui, estamos repetindo as linhas javap mais uma vez para encontrar o local em que nosso método está sendo chamado e onde a Tabela Variável Local é iniciada. Precisamos da linha onde o método é chamado, porque a linha anterior a ele contém a chamada para carregar a variável e identifica qual variável (por índice) a ser carregada. A Tabela de Variáveis Locais nos ajuda a procurar o nome da variável com base no índice obtido.
String loadLine = javapLines.get(varLoadIndex - 1).trim();
int varNumber;
try {
varNumber = Integer.parseInt(loadLine.substring(loadLine.indexOf("aload") + 6).trim());
} catch (NumberFormatException e) {
return null;
}
Esta parte está realmente analisando a chamada de carregamento para obter o índice da variável. Isso pode gerar uma exceção se a função não for realmente chamada com uma variável, para que possamos retornar nulo aqui.
int j = varTableIndex + 2;
while(!"".equals(javapLines.get(j))) {
Matcher varName = Pattern.compile("\\s*" + varNumber + "\\s*([a-zA-Z_][a-zA-Z0-9_]*)").matcher(javapLines.get(j));
if (varName.find()) {
return varName.group(1);
}
j++;
}
return null;
Finalmente, analisamos o nome da variável na linha da Tabela de Variáveis Locais. Retorne nulo se não for encontrado, embora eu não tenha visto nenhuma razão para que isso aconteça.
Juntando tudo
public static void main(java.lang.String[]);
Code:
...
18: getstatic #19 // Field java/lang/System.out:Ljava/io/PrintStream;
21: aload_1
22: aload_2
23: invokevirtual #25 // Method getParamName:(Ljava/lang/String;)Ljava/lang/String;
...
LineNumberTable:
...
line 17: 18
line 18: 29
line 19: 40
...
LocalVariableTable:
Start Length Slot Name Signature
0 83 0 args [Ljava/lang/String;
8 75 1 e LE;
11 72 2 str Ljava/lang/String;
14 69 3 str2 Ljava/lang/String;
18 65 4 str4 Ljava/lang/String;
77 5 5 e1 Ljava/lang/Exception;
Isso é basicamente o que estamos vendo. No código de exemplo, a primeira chamada é a linha 17. a linha 17 na LineNumberTable mostra que o início dessa linha é o índice de linha de bytecode 18. Essa é a System.out
carga. Então, temos aload_2
um pouco antes da chamada do método, então procuramos a variável no slot 2 da LocalVariableTable, que é str
nesse caso.
Por diversão, aqui está uma que lida com várias chamadas de função na mesma linha. Isso faz com que a função não seja idempotente, mas esse é o ponto. Experimente online!