爪哇
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进行编译,除非以Admin身份运行(取决于保存临时类文件的位置),否则它可能无法工作。
- 您必须使用
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)
第一个条目是引发异常的行,我们可以从中捕获methodName,第二个条目是调用函数的位置。
StringWriter javapOut = new StringWriter();
com.sun.tools.javap.Main.run(new String[] {"-l", "-c", classPath}, new PrintWriter(javapOut));
在这一行中,我们正在执行JDK随附的javap可执行文件。该程序解析类文件(字节码),并提供人类可读的结果。我们将其用于基本的“解析”。
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调用以获取变量索引。如果实际上未使用变量调用该函数,则可能引发异常,因此我们可以在此处返回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;
最后,我们从局部变量表中的行中解析出变量的名称。如果没有找到它,则返回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行。LineNumberTable中的第17行显示该行的开头是字节码行索引18。这就是System.out
负载。然后我们就aload_2
在方法调用之前,所以我们str
在这种情况下在LocalVariableTable的插槽2中查找变量。
有趣的是,这里处理在同一行上的多个函数调用。这导致函数不是幂等的,但是这很重要。在线尝试!