Giava
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;
}
Questo attualmente funziona con alcuni gotcha:
- Se si utilizza un IDE per compilarlo, potrebbe non funzionare a meno che non venga eseguito come amministratore (a seconda della posizione in cui vengono salvati i file di classe temporanei)
- È necessario compilare utilizzando
javac
con il -g
flag. Ciò genera tutte le informazioni di debug inclusi i nomi delle variabili locali nel file di classe compilato.
- Questo utilizza un'API Java interna
com.sun.tools.javap
che analizza il bytecode di un file di classe e produce un risultato leggibile dall'uomo. Questa API è accessibile solo nelle librerie JDK, quindi è necessario utilizzare il runtime java JDK o aggiungere tools.jar al percorso di classe.
Ora dovrebbe funzionare anche se il metodo viene chiamato più volte nel programma. Sfortunatamente non funziona ancora se hai più invocazioni su una sola riga. (Per uno che lo fa, vedi sotto)
Provalo online!
Spiegazione
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";
Questa prima parte ottiene alcune informazioni generali su quale classe ci troviamo e qual è il nome della funzione. Ciò si ottiene creando un'eccezione e analizzando le prime 2 voci della traccia dello stack.
java.lang.Exception
at E.getParamName(E.java:28)
at E.main(E.java:17)
La prima voce è la riga da cui viene generata l'eccezione da cui è possibile catturare methodName e la seconda voce è da dove è stata chiamata la funzione.
StringWriter javapOut = new StringWriter();
com.sun.tools.javap.Main.run(new String[] {"-l", "-c", classPath}, new PrintWriter(javapOut));
In questa linea stiamo eseguendo l'eseguibile javap fornito con JDK. Questo programma analizza il file di classe (bytecode) e presenta un risultato leggibile dall'uomo. Lo useremo per un "analisi" rudimentale.
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));
}
}
Stiamo facendo un paio di cose diverse qui. Innanzitutto, stiamo leggendo l'output javap riga per riga in un elenco. In secondo luogo stiamo creando una mappa di indici di linea bytecode per gli indici di linea javap. Questo ci aiuta in seguito a determinare quale metodo di chiamata vogliamo analizzare. Finalmente stiamo usando il numero di riga noto dalla traccia dello stack per determinare quale indice di linea bytecode vogliamo vedere.
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;
}
}
Qui stiamo ripetendo le righe javap ancora una volta per trovare il punto in cui viene invocato il nostro metodo e dove inizia la tabella delle variabili locali. Abbiamo bisogno della linea in cui viene invocato il metodo perché la riga prima contiene la chiamata per caricare la variabile e identifica quale variabile (per indice) caricare. La tabella delle variabili locali ci aiuta effettivamente a cercare il nome della variabile in base all'indice che abbiamo acquisito.
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;
}
Questa parte sta effettivamente analizzando la chiamata di caricamento per ottenere l'indice variabile. Questo può generare un'eccezione se la funzione non è effettivamente chiamata con una variabile, quindi possiamo restituire null qui.
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;
Infine analizziamo il nome della variabile dalla riga nella tabella delle variabili locali. Restituisce null se non viene trovato, anche se non ho visto alcun motivo per cui ciò dovrebbe accadere.
Mettere tutto insieme
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;
Questo è fondamentalmente quello che stiamo guardando. Nel codice di esempio la prima chiamata è la riga 17. la riga 17 nella tabella LineNumber mostra che l'inizio di quella riga è l'indice della riga bytecode 18. Questo è il System.out
carico. Quindi abbiamo aload_2
subito prima della chiamata del metodo, quindi cerchiamo la variabile nello slot 2 della LocalVariableTable che è str
in questo caso.
Per divertimento, eccone uno che gestisce più chiamate di funzione sulla stessa linea. Questo fa sì che la funzione non sia idempotente, ma questo è il punto. Provalo online!