Jawa
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;
}
Obecnie działa to z kilkoma gotchas:
- Jeśli używasz IDE do skompilowania tego, może nie działać, chyba że zostanie uruchomione jako Administrator (w zależności od tego, gdzie są zapisywane tymczasowe pliki klas)
- Musisz skompilować korzystając
javacz-g flagą. Generuje to wszystkie informacje debugowania, w tym nazwy zmiennych lokalnych w skompilowanym pliku klasy.
- Wykorzystuje to wewnętrzny interfejs API języka Java,
com.sun.tools.javapktóry analizuje kod bajtowy pliku klasy i daje wynik czytelny dla człowieka. Ten interfejs API jest dostępny tylko w bibliotekach JDK, więc musisz użyć środowiska wykonawczego JDK java lub dodać tools.jar do ścieżki klasy.
To powinno teraz działać, nawet jeśli metoda jest wywoływana wielokrotnie w programie. Niestety to nie działa, jeśli masz wiele wywołań w jednym wierszu. (Dla takiego, patrz poniżej)
Wypróbuj online!
Wyjaśnienie
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";
Ta pierwsza część zawiera ogólne informacje o tym, w jakiej klasie się znajdujemy i jak nazywa się ta funkcja. Można to osiągnąć, tworząc wyjątek i analizując pierwsze 2 wpisy śledzenia stosu.
java.lang.Exception
at E.getParamName(E.java:28)
at E.main(E.java:17)
Pierwszy wpis to wiersz, w którym zgłaszany jest wyjątek, z którego możemy pobrać metodę methodName, a drugi wpis to miejsce, z którego wywołano funkcję.
StringWriter javapOut = new StringWriter();
com.sun.tools.javap.Main.run(new String[] {"-l", "-c", classPath}, new PrintWriter(javapOut));
W tym wierszu wykonujemy plik wykonywalny javap, który jest dostarczany z JDK. Ten program analizuje plik klasy (kod bajtowy) i przedstawia wynik czytelny dla człowieka. Wykorzystamy to do podstawowego „parsowania”.
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));
}
}
Robimy tutaj kilka różnych rzeczy. Po pierwsze, czytamy wynik wyjściowy javap linia po linii do listy. Po drugie, tworzymy mapę indeksów linii kodu bajtowego na indeksy linii javap. To pomaga nam później ustalić, które wywołanie metody chcemy przeanalizować. Na koniec używamy znanego numeru linii ze śladu stosu, aby określić, który indeks linii kodu bajtowego chcemy oglądać.
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;
}
}
Tutaj iterujemy jeszcze raz po wierszach javap, aby znaleźć miejsce, w którym wywoływana jest nasza metoda i gdzie zaczyna się lokalna tabela zmiennych. Potrzebujemy linii, w której metoda jest wywoływana, ponieważ linia przed nią zawiera wywołanie do załadowania zmiennej i identyfikuje, którą zmienną (według indeksu) załadować. Tabela zmiennych lokalnych pomaga nam właściwie wyszukać nazwę zmiennej na podstawie przechwyconego przez nas indeksu.
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;
}
Ta część analizuje wywołanie load, aby uzyskać indeks zmiennej. Może to spowodować wyjątek, jeśli funkcja nie jest faktycznie wywoływana ze zmienną, dzięki czemu możemy tutaj zwrócić wartość 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;
Na koniec analizujemy nazwę zmiennej z wiersza w lokalnej tabeli zmiennych. Zwróć null, jeśli nie zostanie znaleziony, chociaż nie widziałem powodu, dla którego miałoby się tak stać.
Kładąc wszystko razem
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;
Właśnie na to patrzymy. W przykładowym kodzie pierwsze wywołanie to linia 17. linia 17 w LineNumberTable pokazuje, że początek tej linii to indeks kodu linii bajtowej 18. To jest System.outobciążenie. Mamy więc aload_2tuż przed wywołaniem metody, więc szukamy zmiennej w gnieździe 2 tabeli LocalVariableTable, która jest strw tym przypadku.
Dla zabawy, oto jedno, które obsługuje wiele wywołań funkcji na tej samej linii. To powoduje, że funkcja nie jest idempotentna, ale o to właśnie chodzi. Wypróbuj online!