Puis-je obtenir le nom du paramètre de méthode à l'aide de la réflexion Java?

124

Si j'ai un cours comme celui-ci:

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

y a-t-il un moyen de savoir qui aMethodutilise un paramètre nommé aParam, qui est de type int?

Géo
la source
10
7 réponses et personne n'a mentionné le terme «signature de méthode». C'est essentiellement ce que vous pouvez introspecter avec la réflexion, ce qui signifie: aucun nom de paramètre.
ewernli
3
il est possible, voir ma réponse ci - dessous
commande de vol électrique
4
6 ans plus tard, c'est désormais possible via la réflexion en Java 8 , voir cette réponse SO
earcam
Possible duplication de Y a
danidemi

Réponses:

90

Résumer:

  • l'obtention des noms de paramètres est possible si les informations de débogage sont incluses lors de la compilation. Voir cette réponse pour plus de détails
  • sinon, obtenir des noms de paramètres n'est pas possible
  • obtenir le type de paramètre est possible, en utilisant method.getParameterTypes()

Afin d'écrire une fonctionnalité de saisie semi-automatique pour un éditeur (comme vous l'avez indiqué dans l'un des commentaires), il existe quelques options:

  • utilisation arg0, arg1, arg2etc.
  • utilisation intParam, stringParam, objectTypeParam, etc.
  • utilisez une combinaison de ce qui précède - le premier pour les types non primitifs et le second pour les types primitifs.
  • ne montre pas du tout les noms d'argument - juste les types.
Bozho
la source
4
Est-ce possible avec les interfaces? Je n'ai toujours pas trouvé de moyen d'obtenir les noms des paramètres d'interface.
Cemo
@Cemo: avez-vous réussi à trouver un moyen d'obtenir les noms des paramètres d'interface?
Vaibhav Gupta
Juste pour ajouter, c'est pourquoi spring a besoin de l'annotation @ConstructorProperties pour créer sans ambiguïté des objets à partir de types primitifs.
Bharat
102

Dans Java 8, vous pouvez effectuer les opérations suivantes:

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(){}
}

Donc, pour votre classe, Whatevernous pouvons faire un test manuel:

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;
            }
        }
    }
}

qui devrait s'afficher [aParam]si vous avez passé l' -parametersargument à votre compilateur Java 8.

Pour les utilisateurs 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>

Pour plus d'informations, consultez les liens suivants:

  1. Tutoriel Java officiel: Obtention des noms des paramètres de méthode
  2. JEP 118: accès aux noms de paramètres lors de l'exécution
  3. Javadoc pour la classe Parameter
lpandzic
la source
2
Je ne sais pas s'ils ont changé les arguments pour le plugin du compilateur, mais avec le dernier (3.5.1 à partir de maintenant), j'ai dû utiliser les arguments du compilateur dans la section de configuration: <configuration> <compilerArgs> <arg> - paramètres </arg> </compilerArgs> </configuration>
max
15

La bibliothèque Paranamer a été créée pour résoudre ce même problème.

Il essaie de déterminer les noms de méthodes de différentes manières. Si la classe a été compilée avec le débogage, elle peut extraire les informations en lisant le bytecode de la classe.

Une autre façon est d'injecter un membre statique privé dans le bytecode de la classe après sa compilation, mais avant qu'il ne soit placé dans un fichier jar. Il utilise ensuite la réflexion pour extraire ces informations de la classe au moment de l'exécution.

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

J'ai eu des problèmes avec cette bibliothèque, mais je l'ai finalement fait fonctionner. J'espère signaler les problèmes au responsable.

Sarel Botha
la source
J'essaie d'utiliser le paranamer dans un apk Android. Mais je reçoisParameterNAmesNotFoundException
Rilwan
10

voir classe org.springframework.core.DefaultParameterNameDiscoverer

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

Oui.
Le code doit être compilé avec un compilateur compatible Java 8 avec l'option de stockage des noms de paramètres formels activée ( option -parameters ).
Ensuite, cet extrait de code devrait fonctionner:

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
la source
J'ai essayé cela et cela a fonctionné! Une astuce cependant, j'ai dû reconstruire votre projet pour que ces configurations de compilateur prennent effet.
ishmaelMakitla
5

Vous pouvez récupérer la méthode avec réflexion et détecter ses types d'arguments. Vérifiez http://java.sun.com/j2se/1.4.2/docs/api/java/lang/reflect/Method.html#getParameterTypes%28%29

Cependant, vous ne pouvez pas dire le nom de l'argument utilisé.

Johnco
la source
17
C'est vraiment possible. Sa question portait cependant explicitement sur le nom du paramètre . Vérifiez le titre.
BalusC
1
Et je cite: "Cependant, vous ne pouvez pas dire le nom de l'argument utilisé." il suffit de lire ma réponse -_-
Johnco
3
La question n'était pas formulée de cette façon, il ne savait pas comment obtenir le type.
BalusC
3

C'est possible et Spring MVC 3 le fait, mais je n'ai pas pris le temps de voir exactement comment.

La mise en correspondance des noms de paramètres de méthode avec les noms de variables de modèle d'URI ne peut être effectuée que si votre code est compilé avec le débogage activé. Si vous n'avez pas activé le débogage, vous devez spécifier le nom du nom de variable du modèle d'URI dans l'annotation @PathVariable afin de lier la valeur résolue du nom de la variable à un paramètre de méthode. Par exemple:

Tiré de la documentation du printemps

flybywire
la source
org.springframework.core.Conventions.getVariableName () mais je suppose que vous devez avoir cglib comme dépendance
Radu Toader
3

Bien que ce ne soit pas possible (comme d'autres l'ont illustré), vous pouvez utiliser une annotation pour reporter le nom du paramètre et l'obtenir par réflexion.

Ce n'est pas la solution la plus propre, mais elle fait le travail. Certains webservices font en fait cela pour conserver les noms de paramètres (par exemple: déployer des WS avec glassfish).

WhyNotHugo
la source
3

Vous devriez donc pouvoir faire:

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

Mais vous obtiendrez probablement une liste comme celle-ci:

["int : arg0"]

Je pense que cela sera corrigé dans Groovy 2.5+

Alors actuellement, la réponse est:

  • Si c'est une classe Groovy, alors non, vous ne pouvez pas obtenir le nom, mais vous devriez pouvoir le faire à l'avenir.
  • S'il s'agit d'une classe Java compilée sous Java 8, vous devriez pouvoir le faire.

Voir également:


Pour chaque méthode, alors quelque chose comme:

Whatever.declaredMethods
        .findAll { !it.synthetic }
        .collect { method -> 
            println method
            method.name + " -> " + method.parameters.collect { "[$it.type : $it.name]" }.join(';')
        }
        .each {
            println it
        }
tim_yates
la source
Je ne veux pas non plus spécifier le nom d'une méthode aMethod. Je veux l'obtenir pour toutes les méthodes d'une classe.
snoop
@snoop a ajouté un exemple d'obtention de chaque méthode non synthétique
tim_yates
Ne pouvons-nous pas utiliser antlrpour obtenir des noms de paramètres pour cela?
snoop
2

si vous utilisez l'éclipse, consultez l'image ci-dessous pour permettre au compilateur de stocker les informations sur les paramètres de méthode

entrez la description de l'image ici

Hazim
la source
2

Comme @Bozho l'a déclaré, il est possible de le faire si des informations de débogage sont incluses lors de la compilation. Il y a une bonne réponse ici ...

Comment obtenir les noms de paramètres des constructeurs d'un objet (réflexion)?par @AdamPaynter

... en utilisant la bibliothèque ASM. J'ai rassemblé un exemple montrant comment vous pouvez atteindre votre objectif.

Tout d'abord, commencez par un pom.xml avec ces dépendances.

<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>

Ensuite, cette classe devrait faire ce que vous voulez. Appelez simplement la méthode statique 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;
    }
}

Voici un exemple avec un test unitaire.

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) {
        }

    }
}

Vous pouvez trouver l'exemple complet sur GitHub

Mises en garde

  • J'ai légèrement changé la solution originale de @AdamPaynter pour qu'elle fonctionne pour les méthodes. Si j'ai bien compris, sa solution ne fonctionne qu'avec les constructeurs.
  • Cette solution ne fonctionne pas avec les staticméthodes. C'est parce que dans ce cas, le nombre d'arguments renvoyés par ASM est différent, mais c'est quelque chose qui peut être facilement corrigé.
Danidemi
la source
0

Les noms de paramètres ne sont utiles que pour le compilateur. Lorsque le compilateur génère un fichier de classe, les noms de paramètres ne sont pas inclus - la liste d'arguments d'une méthode se compose uniquement du nombre et des types de ses arguments. Il serait donc impossible de récupérer le nom du paramètre en utilisant la réflexion (comme indiqué dans votre question) - il n'existe nulle part.

Cependant, si l'utilisation de la réflexion n'est pas une exigence absolue, vous pouvez récupérer ces informations directement à partir du code source (en supposant que vous les ayez).

Danben
la source
2
Les noms de paramètres ne sont pas seulement utiles pour un compilateur, ils transmettent également des informations au consommateur d'une bibliothèque, supposons que j'ai écrit une classe StrangeDate qui avait la méthode add (int day, int hour, int minute) si vous alliez l'utiliser et avez trouvé une méthode add (int arg0, int arg1, int arg2) dans quelle mesure cela serait-il utile?
David Waters
Je suis désolé - vous avez entièrement mal compris ma réponse. Veuillez le relire dans le contexte de la question.
danben
2
L'acquisition des noms de paramètres est un grand avantage pour appeler cette méthode de manière réfléchie. Ce n'est pas seulement utile au compilateur, même dans le contexte de la question.
corsiKa
Parameter names are only useful to the compiler.Faux. Regardez la bibliothèque Retrofit. Il utilise des interfaces dynamiques pour créer des requêtes d'API REST. L'une de ses fonctionnalités est la possibilité de définir des noms d'espace réservé dans les chemins d'URL et de remplacer ces espaces réservés par les noms de paramètres correspondants.
TheRealChx101
0

Pour ajouter mes 2 cents; Les informations de paramètre sont disponibles dans un fichier de classe "pour le débogage" lorsque vous utilisez javac -g pour compiler la source. Et il est disponible pour APT mais vous aurez besoin d'une annotation donc inutile. (Quelqu'un a discuté de quelque chose de similaire il y a 4-5 ans ici: http://forums.java.net/jive/thread.jspa?messageID=13467&tstart=0 )

Dans l'ensemble, vous ne pouvez l'obtenir que si vous travaillez directement sur les fichiers source (similaire à ce que fait APT au moment de la compilation).

Elister
la source