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;
}
Esto actualmente funciona con algunas trampas:
- Si usa un IDE para compilar esto, podría no funcionar a menos que se ejecute como administrador (dependiendo de dónde se guarden los archivos de clase temporales)
- Debe compilar usando
javac
con la -g
bandera. Esto genera toda la información de depuración, incluidos los nombres de variables locales en el archivo de clase compilado.
- Esto utiliza una API Java interna
com.sun.tools.javap
que analiza el código de bytes de un archivo de clase y produce un resultado legible para humanos. Solo se puede acceder a esta API en las bibliotecas JDK, por lo que debe usar el tiempo de ejecución JDK java o agregar tools.jar a su classpath.
Esto ahora debería funcionar incluso si el método se llama varias veces en el programa. Lamentablemente, todavía no funciona si tiene varias invocaciones en una sola línea. (Para uno que sí, ver abajo)
Pruébalo en línea!
Explicación
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";
Esta primera parte obtiene información general sobre en qué clase estamos y cuál es el nombre de la función. Esto se logra creando una excepción y analizando las 2 primeras entradas de la traza de la pila.
java.lang.Exception
at E.getParamName(E.java:28)
at E.main(E.java:17)
La primera entrada es la línea en la que se lanza la excepción en la que podemos tomar el methodName y la segunda entrada es desde donde se llamó a la función.
StringWriter javapOut = new StringWriter();
com.sun.tools.javap.Main.run(new String[] {"-l", "-c", classPath}, new PrintWriter(javapOut));
En esta línea, estamos ejecutando el ejecutable javap que viene con el JDK. Este programa analiza el archivo de clase (bytecode) y presenta un resultado legible para humanos. Usaremos esto para el "análisis" rudimentario.
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));
}
}
Estamos haciendo un par de cosas diferentes aquí. Primero, estamos leyendo la salida de javap línea por línea en una lista. En segundo lugar, estamos creando un mapa de índices de línea de bytecode a índices de línea javap. Esto nos ayuda más adelante a determinar qué método de invocación queremos analizar. Finalmente, estamos utilizando el número de línea conocido de la traza de la pila para determinar qué índice de línea de bytecode queremos ver.
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;
}
}
Aquí estamos iterando sobre las líneas javap una vez más para encontrar el lugar donde se invoca nuestro método y donde comienza la Tabla de variables locales. Necesitamos la línea donde se invoca el método porque la línea anterior contiene la llamada para cargar la variable e identifica qué variable (por índice) cargar. La tabla de variables locales nos ayuda a buscar el nombre de la variable según el índice que tomamos.
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;
}
Esta parte en realidad está analizando la llamada de carga para obtener el índice variable. Esto puede generar una excepción si la función no se llama realmente con una variable, por lo que podemos devolver nulo aquí.
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;
Finalmente analizamos el nombre de la variable de la línea en la Tabla de variables locales. Devuelva nulo si no se encuentra, aunque no he visto ninguna razón por la que esto debería suceder.
Poniendolo todo junto
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;
Esto es básicamente lo que estamos viendo. En el código de ejemplo, la primera invocación es la línea 17. la línea 17 en la LineNumberTable muestra que el comienzo de esa línea es el bytecode line index 18. Esa es la System.out
carga. Luego tenemos aload_2
justo antes de la llamada al método, por lo que buscamos la variable en la ranura 2 de LocalVariableTable, que es str
en este caso.
Por diversión, aquí hay uno que maneja múltiples llamadas a funciones en la misma línea. Esto hace que la función no sea idempotente, pero ese es el punto. Pruébalo en línea!