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;
}
Điều này hiện đang hoạt động với một vài vấn đề:
- Nếu bạn sử dụng IDE để biên dịch thì nó có thể không hoạt động trừ khi nó được chạy với tư cách Quản trị viên (tùy thuộc vào nơi lưu các tệp lớp tạm thời)
- Bạn phải biên dịch sử dụng
javac
với -g
lá cờ. Điều này tạo ra tất cả thông tin gỡ lỗi bao gồm tên biến cục bộ trong tệp lớp đã biên dịch.
- Điều này sử dụng API Java nội bộ
com.sun.tools.javap
để phân tích mã byte của tệp lớp và tạo ra kết quả có thể đọc được của con người. API này chỉ có thể truy cập được trong các thư viện JDK, do đó bạn phải sử dụng thời gian chạy java JDK hoặc thêm tools.jar vào đường dẫn lớp của bạn.
Điều này bây giờ sẽ hoạt động ngay cả khi phương thức được gọi nhiều lần trong chương trình. Thật không may, nó không hoạt động nếu bạn có nhiều lời mời trên một dòng. (Đối với một trong đó, xem bên dưới)
Hãy thử trực tuyến!
Giải trình
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";
Phần đầu tiên này có một số thông tin chung về lớp chúng ta đang ở và tên của hàm là gì. Điều này được thực hiện bằng cách tạo một ngoại lệ và phân tích cú pháp 2 mục đầu tiên của theo dõi ngăn xếp.
java.lang.Exception
at E.getParamName(E.java:28)
at E.main(E.java:17)
Mục nhập đầu tiên là dòng mà ngoại lệ được ném vào mà chúng ta có thể lấy tên phương thức từ đó và mục nhập thứ hai là nơi hàm được gọi từ đó.
StringWriter javapOut = new StringWriter();
com.sun.tools.javap.Main.run(new String[] {"-l", "-c", classPath}, new PrintWriter(javapOut));
Trong dòng này, chúng tôi đang thực thi javap thực thi đi kèm với JDK. Chương trình này phân tích tệp lớp (mã byte) và trình bày kết quả có thể đọc được của con người. Chúng tôi sẽ sử dụng điều này cho "phân tích cú pháp" thô sơ.
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));
}
}
Chúng tôi đang làm một vài điều khác nhau ở đây. Đầu tiên, chúng tôi đang đọc dòng đầu ra javap theo dòng vào một danh sách. Thứ hai, chúng tôi đang tạo một bản đồ các chỉ mục dòng bytecode thành các chỉ mục dòng javap. Điều này giúp chúng tôi sau này xác định cách gọi phương thức nào chúng tôi muốn phân tích. Cuối cùng, chúng tôi đang sử dụng số dòng đã biết từ theo dõi ngăn xếp để xác định chỉ mục dòng mã byte nào mà chúng tôi muốn xem xét.
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;
}
}
Ở đây chúng tôi đang lặp lại các dòng javap một lần nữa để tìm vị trí mà phương thức của chúng tôi đang được gọi và nơi Bảng biến cục bộ bắt đầu. Chúng ta cần dòng nơi phương thức được gọi bởi vì dòng trước nó chứa lệnh gọi để tải biến và xác định biến nào (theo chỉ mục) sẽ tải. Bảng biến cục bộ giúp chúng ta thực sự tra cứu tên của biến dựa trên chỉ mục chúng ta đã lấy.
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;
}
Phần này thực sự phân tích cú pháp tải để lấy chỉ số biến. Điều này có thể đưa ra một ngoại lệ nếu hàm không thực sự được gọi với một biến để chúng ta có thể trả về null ở đây.
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;
Cuối cùng, chúng tôi phân tích tên của biến từ dòng trong Bảng biến cục bộ. Trả về null nếu không tìm thấy mặc dù tôi không thấy lý do tại sao điều này sẽ xảy ra.
Để tất cả chúng cùng nhau
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;
Đây là những gì chúng ta đang xem xét. Trong mã ví dụ, lệnh gọi đầu tiên là dòng 17. dòng 17 trong LineNumberTable cho thấy điểm bắt đầu của dòng đó là chỉ số dòng bytecode 18. Đó là System.out
tải. Sau đó, chúng ta có aload_2
quyền trước lệnh gọi phương thức để chúng ta tìm biến trong khe 2 của LocalVariableTable str
trong trường hợp này.
Để giải trí, đây là một trong đó xử lý nhiều cuộc gọi chức năng trên cùng một dòng. Điều này làm cho hàm không phải là idempotent nhưng đó là loại điểm. Hãy thử trực tuyến!