¿Puedo obtener el nombre del parámetro del método usando la reflexión de Java?

124

Si tengo una clase como esta:

public class Whatever
{
  public void aMethod(int aParam);
}

¿Hay alguna manera de saber que aMethodusa un parámetro llamado aParam, que es de tipo int?

Geo
fuente
10
7 respuestas y nadie mencionó el término "firma del método". Esto es básicamente lo que puede introspectar con la reflexión, lo que significa: sin nombres de parámetros.
ewernli
3
que es posible, ver mi respuesta a continuación
fly-by-wire
44
6 años después, ahora es posible a través de la reflexión en Java 8 , vea esta respuesta SO
earcam

Respuestas:

90

Para resumir:

  • es posible obtener nombres de parámetros si se incluye información de depuración durante la compilación. Vea esta respuesta para más detalles.
  • de lo contrario no es posible obtener nombres de parámetros
  • es posible obtener el tipo de parámetro, usando method.getParameterTypes()

En aras de escribir la funcionalidad de autocompletar para un editor (como mencionó en uno de los comentarios), hay algunas opciones:

  • uso arg0, arg1, arg2etc.
  • uso intParam, stringParam, objectTypeParam, etc.
  • use una combinación de lo anterior: el primero para los tipos no primitivos y el último para los tipos primitivos.
  • no muestre nombres de argumentos en absoluto, solo los tipos.
Bozho
fuente
44
¿Es esto posible con las interfaces? Todavía no pude encontrar una manera de obtener los nombres de los parámetros de la interfaz.
Cemo
@ Cemo: ¿pudiste encontrar una manera de obtener los nombres de los parámetros de la interfaz?
Vaibhav Gupta
Solo para agregar, es por eso que spring necesita la anotación @ConstructorProperties para crear objetos sin ambigüedad a partir de tipos primitivos.
Bharat
102

En Java 8 puede hacer lo siguiente:

import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.ArrayList;
import java.util.List;

public final class Methods {

    public static List<String> getParameterNames(Method method) {
        Parameter[] parameters = method.getParameters();
        List<String> parameterNames = new ArrayList<>();

        for (Parameter parameter : parameters) {
            if(!parameter.isNamePresent()) {
                throw new IllegalArgumentException("Parameter names are not present!");
            }

            String parameterName = parameter.getName();
            parameterNames.add(parameterName);
        }

        return parameterNames;
    }

    private Methods(){}
}

Entonces, para su clase Whatever, podemos hacer una prueba manual:

import java.lang.reflect.Method;

public class ManualTest {
    public static void main(String[] args) {
        Method[] declaredMethods = Whatever.class.getDeclaredMethods();

        for (Method declaredMethod : declaredMethods) {
            if (declaredMethod.getName().equals("aMethod")) {
                System.out.println(Methods.getParameterNames(declaredMethod));
                break;
            }
        }
    }
}

que debería imprimirse [aParam]si ha pasado el -parametersargumento a su compilador Java 8.

Para usuarios de Maven:

<properties>
    <!-- PLUGIN VERSIONS -->
    <maven-compiler-plugin.version>3.1</maven-compiler-plugin.version>

    <!-- OTHER PROPERTIES -->
    <java.version>1.8</java.version>
</properties>

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>${maven-compiler-plugin.version}</version>
            <configuration>
                <!-- Original answer -->
                <compilerArgument>-parameters</compilerArgument>
                <!-- Or, if you use the plugin version >= 3.6.2 -->
                <parameters>true</parameters>
                <testCompilerArgument>-parameters</testCompilerArgument>
                <source>${java.version}</source>
                <target>${java.version}</target>
            </configuration>
        </plugin>
    </plugins>
</build>

Para obtener más información, consulte los siguientes enlaces:

  1. Tutorial oficial de Java: obtención de nombres de parámetros de métodos
  2. JEP 118: Acceso a nombres de parámetros en tiempo de ejecución
  3. Javadoc para la clase de parámetro
lpandzic
fuente
2
No sé si cambiaron los argumentos para el complemento del compilador, pero con la última versión (3.5.1 a partir de ahora) tuve que hacer para usar los argumentos del compilador en la sección de configuración: <configuración> <compilerArgs> <arg> - parámetros </arg> </compilerArgs> </configuration>
máximo
15

La biblioteca Paranamer fue creada para resolver este mismo problema.

Intenta determinar los nombres de los métodos de diferentes maneras. Si la clase se compiló con depuración, puede extraer la información leyendo el código de bytes de la clase.

Otra forma es inyectar un miembro estático privado en el código de bytes de la clase después de compilarlo, pero antes de colocarlo en un jar. Luego usa la reflexión para extraer esta información de la clase en tiempo de ejecución.

https://github.com/paul-hammant/paranamer

Tuve problemas al usar esta biblioteca, pero al final lo hice funcionar. Espero informar los problemas al mantenedor.

Sarel Botha
fuente
Estoy tratando de usar paranamer dentro de un apk de Android. Pero me estoy poniendoParameterNAmesNotFoundException
Rilwan
10

vea la clase org.springframework.core.DefaultParameterNameDiscoverer

DefaultParameterNameDiscoverer discoverer = new DefaultParameterNameDiscoverer();
String[] params = discoverer.getParameterNames(MathUtils.class.getMethod("isPrime", Integer.class));
Denis Danilkovich
fuente
10

Si.
Código debe ser compilado con Java 8 compilador compatible con la opción de almacenar nombres de parámetros formales activada ( -parámetros opción).
Entonces este fragmento de código debería funcionar:

Class<String> clz = String.class;
for (Method m : clz.getDeclaredMethods()) {
   System.err.println(m.getName());
   for (Parameter p : m.getParameters()) {
    System.err.println("  " + p.getName());
   }
}
Karol Król
fuente
Intenté esto y funcionó! Sin embargo, un consejo, tuve que reconstruir su proyecto para que estas configuraciones del compilador surtan efecto.
ishmaelMakitla
5

Puede recuperar el método con reflexión y detectar sus tipos de argumento. Verifique http://java.sun.com/j2se/1.4.2/docs/api/java/lang/reflect/Method.html#getParameterTypes%28%29

Sin embargo, no puede decir el nombre del argumento utilizado.

Johnco
fuente
17
En verdad eso es todo posible. Sin embargo, su pregunta era explícitamente sobre el nombre del parámetro . Revisa el título.
BalusC
1
Y cito: "Sin embargo, no se puede decir el nombre del argumento utilizado". Acabo de leer mi respuesta -_-
Johnco
3
La pregunta no fue formulada de esa manera, no sabía cómo obtener el tipo.
BalusC
3

Es posible y Spring MVC 3 lo hace, pero no me tomé el tiempo para ver exactamente cómo.

La coincidencia de los nombres de los parámetros del método con los nombres de las variables de plantilla URI solo se puede hacer si su código se compila con la depuración habilitada. Si no tiene habilitada la depuración, debe especificar el nombre del nombre de la variable de plantilla URI en la anotación @PathVariable para vincular el valor resuelto del nombre de la variable a un parámetro de método. Por ejemplo:

Tomado de la documentación de primavera

flybywire
fuente
org.springframework.core.Conventions.getVariableName () pero supongo que debe tener cglib como dependencia
Radu Toader
3

Si bien no es posible (como lo han ilustrado otros), puede usar una anotación para transferir el nombre del parámetro y obtener eso a través de la reflexión.

No es la solución más limpia, pero hace el trabajo. Algunos servicios web realmente hacen esto para mantener los nombres de los parámetros (es decir, implementar WS con glassfish).

WhyNotHugo
fuente
3

Entonces deberías poder hacer:

Whatever.declaredMethods
        .find { it.name == 'aMethod' }
        .parameters
        .collect { "$it.type : $it.name" }

Pero probablemente obtendrá una lista como esta:

["int : arg0"]

Creo que esto se solucionará en Groovy 2.5+

Entonces, actualmente, la respuesta es:

  • Si es una clase Groovy, entonces no, no puedes obtener el nombre, pero deberías poder hacerlo en el futuro.
  • Si se trata de una clase Java compilada en Java 8, debería poder hacerlo.

Ver también:


Para cada método, entonces algo como:

Whatever.declaredMethods
        .findAll { !it.synthetic }
        .collect { method -> 
            println method
            method.name + " -> " + method.parameters.collect { "[$it.type : $it.name]" }.join(';')
        }
        .each {
            println it
        }
tim_yates
fuente
Tampoco quiero especificar el nombre de un método aMethod. Quiero obtenerlo para todos los métodos en una clase.
fisgonea el
@snoop agregó un ejemplo de cómo obtener todos los métodos no sintéticos
tim_yates
¿No podemos usar antlrpara obtener nombres de parámetros para esto?
fisgonea el
2

si usa el eclipse, vea la imagen siguiente para permitir que el compilador almacene la información sobre los parámetros del método

ingrese la descripción de la imagen aquí

Hazim
fuente
2

Como dijo @Bozho, es posible hacerlo si se incluye información de depuración durante la compilación. Hay una buena respuesta aquí ...

¿Cómo obtener los nombres de los parámetros de los constructores de un objeto (reflexión)? por @AdamPaynter

... usando la biblioteca ASM. He preparado un ejemplo que muestra cómo puedes lograr tu objetivo.

En primer lugar, comience con un pom.xml con estas dependencias.

<dependency>
    <groupId>org.ow2.asm</groupId>
    <artifactId>asm-all</artifactId>
    <version>5.2</version>
</dependency>
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
    <scope>test</scope>
</dependency>

Entonces, esta clase debe hacer lo que quieras. Solo invoca el método estático getParameterNames().

import org.objectweb.asm.ClassReader;
import org.objectweb.asm.Type;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.LocalVariableNode;
import org.objectweb.asm.tree.MethodNode;

public class ArgumentReflection {
    /**
     * Returns a list containing one parameter name for each argument accepted
     * by the given constructor. If the class was compiled with debugging
     * symbols, the parameter names will match those provided in the Java source
     * code. Otherwise, a generic "arg" parameter name is generated ("arg0" for
     * the first argument, "arg1" for the second...).
     * 
     * This method relies on the constructor's class loader to locate the
     * bytecode resource that defined its class.
     * 
     * @param theMethod
     * @return
     * @throws IOException
     */
    public static List<String> getParameterNames(Method theMethod) throws IOException {
        Class<?> declaringClass = theMethod.getDeclaringClass();
        ClassLoader declaringClassLoader = declaringClass.getClassLoader();

        Type declaringType = Type.getType(declaringClass);
        String constructorDescriptor = Type.getMethodDescriptor(theMethod);
        String url = declaringType.getInternalName() + ".class";

        InputStream classFileInputStream = declaringClassLoader.getResourceAsStream(url);
        if (classFileInputStream == null) {
            throw new IllegalArgumentException(
                    "The constructor's class loader cannot find the bytecode that defined the constructor's class (URL: "
                            + url + ")");
        }

        ClassNode classNode;
        try {
            classNode = new ClassNode();
            ClassReader classReader = new ClassReader(classFileInputStream);
            classReader.accept(classNode, 0);
        } finally {
            classFileInputStream.close();
        }

        @SuppressWarnings("unchecked")
        List<MethodNode> methods = classNode.methods;
        for (MethodNode method : methods) {
            if (method.name.equals(theMethod.getName()) && method.desc.equals(constructorDescriptor)) {
                Type[] argumentTypes = Type.getArgumentTypes(method.desc);
                List<String> parameterNames = new ArrayList<String>(argumentTypes.length);

                @SuppressWarnings("unchecked")
                List<LocalVariableNode> localVariables = method.localVariables;
                for (int i = 1; i <= argumentTypes.length; i++) {
                    // The first local variable actually represents the "this"
                    // object if the method is not static!
                    parameterNames.add(localVariables.get(i).name);
                }

                return parameterNames;
            }
        }

        return null;
    }
}

Aquí hay un ejemplo con una prueba unitaria.

public class ArgumentReflectionTest {

    @Test
    public void shouldExtractTheNamesOfTheParameters3() throws NoSuchMethodException, SecurityException, IOException {

        List<String> parameterNames = ArgumentReflection
                .getParameterNames(Clazz.class.getMethod("callMe", String.class, String.class));
        assertEquals("firstName", parameterNames.get(0));
        assertEquals("lastName", parameterNames.get(1));
        assertEquals(2, parameterNames.size());

    }

    public static final class Clazz {

        public void callMe(String firstName, String lastName) {
        }

    }
}

Puedes encontrar el ejemplo completo en GitHub

Advertencias

  • Cambié ligeramente la solución original de @AdamPaynter para que funcione para Métodos. Si entendí bien, su solución solo funciona con constructores.
  • Esta solución no funciona con staticmétodos. Esto se debe a que en este caso el número de argumentos devueltos por ASM es diferente, pero es algo que se puede solucionar fácilmente.
danidemi
fuente
0

Los nombres de los parámetros solo son útiles para el compilador. Cuando el compilador genera un archivo de clase, los nombres de los parámetros no se incluyen: la lista de argumentos de un método solo consta del número y los tipos de sus argumentos. Por lo tanto, sería imposible recuperar el nombre del parámetro utilizando la reflexión (como está etiquetado en su pregunta); no existe en ningún lado.

Sin embargo, si el uso de la reflexión no es un requisito difícil, puede recuperar esta información directamente del código fuente (suponiendo que la tenga).

danben
fuente
2
El nombre del parámetro no solo es útil para un compilador, sino que también transmite información al consumidor de una biblioteca, suponga que escribí una clase StrangeDate que tenía el método add (int day, int hour, int minute) si fue a usar esto y encontró un método agregue (int arg0, int arg1, int arg2) ¿qué tan útil sería eso?
David Waters
Lo siento, entendiste mal mi respuesta por completo. Vuelva a leerlo en el contexto de la pregunta.
danben
2
Adquirir los nombres de los parámetros es un gran beneficio para llamar a ese método reflexivamente. No solo es útil para el compilador, incluso en el contexto de la pregunta.
corsiKa
Parameter names are only useful to the compiler.Incorrecto. Mire la biblioteca Retrofit. Utiliza interfaces dinámicas para crear solicitudes de API REST. Una de sus características es la capacidad de definir nombres de marcadores de posición en las rutas URL y reemplazar esos marcadores de posición con sus correspondientes nombres de parámetros.
TheRealChx101
0

Para agregar mis 2 centavos; la información de parámetros está disponible en un archivo de clase "para depuración" cuando usa javac -g para compilar la fuente. Y está disponible para APT, pero necesitarás una anotación, así que no te servirá. (Alguien discutió algo similar hace 4-5 años aquí: http://forums.java.net/jive/thread.jspa?messageID=13467&tstart=0 )

En resumen, no puede obtenerlo a menos que trabaje directamente en los archivos de origen (similar a lo que hace APT en tiempo de compilación).

Elister
fuente