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;
}
Dies funktioniert derzeit mit ein paar Fallstricken:
- Wenn Sie zum Kompilieren eine IDE verwenden, funktioniert diese möglicherweise nur, wenn sie als Administrator ausgeführt wird (je nachdem, wo die temporären Klassendateien gespeichert sind).
- Sie müssen
javac
mit dem -g
Flag kompilieren . Dadurch werden alle Debugging-Informationen einschließlich der lokalen Variablennamen in der kompilierten Klassendatei generiert.
- Hierbei wird eine interne Java-API verwendet,
com.sun.tools.javap
die den Bytecode einer Klassendatei analysiert und ein für den Menschen lesbares Ergebnis liefert. Auf diese API kann nur in den JDK-Bibliotheken zugegriffen werden. Sie müssen daher entweder die JDK-Java-Laufzeit verwenden oder tools.jar zu Ihrem Klassenpfad hinzufügen.
Dies sollte nun auch dann funktionieren, wenn die Methode im Programm mehrfach aufgerufen wird. Leider funktioniert es noch nicht, wenn Sie mehrere Aufrufe in einer einzigen Zeile haben. (Für eine, die das tut, siehe unten)
Probieren Sie es online!
Erläuterung
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";
In diesem ersten Teil erhalten Sie allgemeine Informationen darüber, in welcher Klasse wir uns befinden und wie die Funktion heißt. Dies wird erreicht, indem eine Ausnahme erstellt und die ersten 2 Einträge des Stack-Trace analysiert werden.
java.lang.Exception
at E.getParamName(E.java:28)
at E.main(E.java:17)
Der erste Eintrag ist die Zeile, in der die Ausnahme ausgelöst wird, von der wir den Methodennamen abrufen können, und der zweite Eintrag ist die Zeile, von der aus die Funktion aufgerufen wurde.
StringWriter javapOut = new StringWriter();
com.sun.tools.javap.Main.run(new String[] {"-l", "-c", classPath}, new PrintWriter(javapOut));
In dieser Zeile führen wir die im JDK enthaltene ausführbare Java-Datei aus. Dieses Programm analysiert die Klassendatei (Bytecode) und zeigt ein für Menschen lesbares Ergebnis an. Wir werden dies zum rudimentären "Parsen" verwenden.
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));
}
}
Wir machen hier ein paar verschiedene Dinge. Zunächst lesen wir die Ausgabe von javap zeilenweise in eine Liste ein. Zweitens erstellen wir eine Zuordnung von Bytecode-Zeilenindizes zu Java-Zeilenindizes. Dies hilft uns später zu bestimmen, welchen Methodenaufruf wir analysieren möchten. Schließlich verwenden wir die bekannte Zeilennummer aus dem Stack-Trace, um zu bestimmen, welchen Bytecode-Zeilenindex wir betrachten möchten.
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;
}
}
Hier iterieren wir noch einmal über die Java-Zeilen, um die Stelle zu finden, an der unsere Methode aufgerufen wird und an der die lokale Variablentabelle beginnt. Wir benötigen die Zeile, in der die Methode aufgerufen wird, da die Zeile vor ihr den Aufruf zum Laden der Variablen enthält und angibt, welche Variable (nach Index) geladen werden soll. Mithilfe der lokalen Variablentabelle können wir den Namen der Variablen anhand des von uns erfassten Index nachschlagen.
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;
}
Dieser Teil analysiert tatsächlich den Ladeaufruf, um den Variablenindex zu erhalten. Dies kann eine Ausnahme auslösen, wenn die Funktion nicht tatsächlich mit einer Variablen aufgerufen wird, sodass hier null zurückgegeben werden kann.
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;
Schließlich analysieren wir den Namen der Variablen aus der Zeile in der lokalen Variablentabelle. Gibt null zurück, wenn es nicht gefunden wird, obwohl ich keinen Grund gesehen habe, warum dies passieren sollte.
Alles zusammen
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;
Dies ist im Grunde, was wir suchen. Im Beispielcode lautet der erste Aufruf Zeile 17. Zeile 17 in der LineNumberTable zeigt, dass der Anfang dieser Zeile der Bytecode-Zeilenindex 18 ist. Das ist die System.out
Last. Dann müssen wir aload_2
direkt vor dem Methodenaufruf nach der Variablen in Slot 2 der LocalVariableTable suchen, was str
in diesem Fall der Fall ist.
Hier ist zum Spaß einer, der mehrere Funktionsaufrufe auf derselben Leitung verarbeitet. Dies führt dazu, dass die Funktion nicht idempotent ist, aber genau darum geht es. Probieren Sie es online!