Джава
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;
}
В настоящее время это работает с несколькими ошибками:
- Если вы используете IDE для компиляции, она может не работать, если она не запущена с правами администратора (в зависимости от того, где хранятся временные файлы классов).
- Вы должны скомпилировать, используя
javac
с -g
флагом. Это генерирует всю отладочную информацию, включая имена локальных переменных в скомпилированном файле класса.
- При этом используется внутренний Java API,
com.sun.tools.javap
который анализирует байт-код файла класса и выдает читабельный результат. Этот API доступен только в библиотеках JDK, поэтому вы должны либо использовать Java-среду JDK, либо добавить tools.jar в ваш путь к классам.
Теперь это должно работать, даже если метод вызывается несколько раз в программе. К сожалению, это еще не работает, если у вас есть несколько вызовов в одной строке. (Для того, который делает, см. Ниже)
Попробуйте онлайн!
объяснение
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";
Эта первая часть получает некоторую общую информацию о том, в каком классе мы находимся и как называется функция. Это достигается созданием исключения и анализом первых двух записей трассировки стека.
java.lang.Exception
at E.getParamName(E.java:28)
at E.main(E.java:17)
Первая запись - это строка, в которую выдается исключение, из которой мы можем получить метод methodName, а вторая запись - это место, откуда была вызвана функция.
StringWriter javapOut = new StringWriter();
com.sun.tools.javap.Main.run(new String[] {"-l", "-c", classPath}, new PrintWriter(javapOut));
В этой строке мы выполняем исполняемый файл javap, который поставляется с JDK. Эта программа анализирует файл класса (байт-код) и выдает читабельный результат. Мы будем использовать это для элементарного "разбора".
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));
}
}
Мы делаем несколько разных вещей здесь. Сначала мы читаем вывод javap построчно в список. Во-вторых, мы создаем карту строковых индексов байт-кода и строковых индексов javap. Это поможет нам позже определить, какой вызов метода мы хотим проанализировать. Наконец, мы используем известный номер строки из трассировки стека, чтобы определить, какой индекс строки байт-кода мы хотим видеть.
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;
}
}
Здесь мы перебираем строки javap еще раз, чтобы найти точку, где вызывается наш метод и где начинается таблица локальных переменных. Нам нужна строка, где метод вызывается, потому что строка перед ним содержит вызов для загрузки переменной и определяет, какую переменную (по индексу) загрузить. Таблица локальных переменных помогает нам на самом деле искать имя переменной на основе индекса, который мы взяли.
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;
}
Эта часть фактически анализирует вызов load, чтобы получить индекс переменной. Это может вызвать исключение, если функция фактически не вызывается с переменной, поэтому мы можем вернуть здесь ноль.
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;
Наконец, мы анализируем имя переменной из строки в таблице локальных переменных. Возвратите ноль, если это не найдено, хотя я не видел причины, почему это должно случиться.
Собираем все вместе
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;
Это в основном то, на что мы смотрим. В примере кода первый вызов - строка 17. строка 17 в LineNumberTable показывает, что началом этой строки является индекс строки 18 байт-кода. Это System.out
нагрузка. Затем мы имеем aload_2
непосредственно перед вызовом метода, поэтому мы ищем переменную в слоте 2 LocalVariableTable, которая str
в этом случае.
Для забавы, вот один, который обрабатывает несколько вызовов функций на одной строке. Это приводит к тому, что функция не является идемпотентной, но в этом вся суть. Попробуйте онлайн!