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;
}
Наразі це працює з кількома дітками:
- Якщо ви використовуєте IDE для компіляції, це може не працювати, якщо він не запускається як адміністратор (залежно від того, де зберігаються тимчасові файли класу)
- Ви повинні компілювати за
javac
допомогою -g
прапора. Це генерує всю інформацію про налагодження, включаючи імена локальних змінних у складеному файлі класу.
- Для цього використовується внутрішній Java API,
com.sun.tools.javap
який аналізує байт-код класового файлу та дає результат, прочитаний людиною. Цей API доступний лише в бібліотеках JDK, тому вам потрібно або використовувати час виконання програми JDK java, або додати 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";
Ця перша частина отримує загальну інформацію про те, в якому класі ми перебуваємо і як називається функція. Це здійснюється шляхом створення винятку та розбору перших 2 записів трасування стека.
java.lang.Exception
at E.getParamName(E.java:28)
at E.main(E.java:17)
Перший запис - це рядок, з якого викинуто виняток, з якого ми можемо схопити методName, а другий запис - звідки була викликана функція.
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 в список. По-друге, ми створюємо карту індексів рядок байт-кодів до індексів рядків Javavap. Це допомагає нам пізніше визначити, який виклик методу ми хочемо проаналізувати. Нарешті, ми використовуємо відомий номер рядка зі сліду стека, щоб визначити, який індекс рядка байт-коду ми хочемо дивитись.
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;
}
Ця частина насправді аналізує виклик завантаження, щоб отримати індекс змінної. Це може кинути виняток, якщо функція насправді не викликається змінною, тому ми можемо повернути нуль сюди.
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
в цьому випадку.
Для задоволення, ось один, який обробляє декілька функціональних дзвінків на одній лінії. Це призводить до того, що функція не є ідентичною, але в цьому полягає суть. Спробуйте в Інтернеті!