Posso obter o nome do parâmetro do método usando a reflexão Java?

124

Se eu tiver uma classe como esta:

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

existe alguma maneira de saber que aMethodusa um parâmetro chamado aParam, que é do tipo int?

Geo
fonte
10
7 respostas e ninguém mencionou o termo "assinatura de método". Isso é basicamente o que você pode examinar com reflexão, o que significa: sem nomes de parâmetros.
ewernli
3
isso é possível, ver minha resposta abaixo
fly-by-wire
4
6 anos depois, é agora possível através da reflexão em Java 8 , consulte esta resposta SO
earcam

Respostas:

90

Para resumir:

  • obter nomes de parâmetros é possível se as informações de depuração forem incluídas durante a compilação. Veja esta resposta para mais detalhes
  • caso contrário, obter nomes de parâmetros não é possível
  • é possível obter o tipo de parâmetro, usando method.getParameterTypes()

Para escrever a funcionalidade de preenchimento automático para um editor (como você afirmou em um dos comentários), existem algumas opções:

  • utilização arg0, arg1, arg2etc.
  • utilização intParam, stringParam, objectTypeParam, etc.
  • use uma combinação dos itens acima - o primeiro para tipos não primitivos e o último para tipos primitivos.
  • não mostre os nomes dos argumentos - apenas os tipos.
Bozho
fonte
4
Isso é possível com interfaces? Ainda não consegui encontrar uma maneira de obter nomes de parâmetros de interface.
Cemo
@Cemo: você conseguiu encontrar uma maneira de obter nomes de parâmetros de interface?
Vaibhav Gupta
Apenas para adicionar, é por isso que o Spring precisa da anotação @ConstructorProperties para criar objetos de tipos primitivos sem ambiguidade.
Bharat
102

No Java 8, você pode fazer o seguinte:

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

Assim, para a sua turma Whatever, podemos fazer um teste 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 deve ser impresso [aParam]se você passou o -parametersargumento para o seu compilador Java 8.

Para usuários do 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 mais informações, consulte os seguintes links:

  1. Tutorial oficial de Java: Obtendo nomes de parâmetros de método
  2. JEP 118: Acesso aos nomes de parâmetros em tempo de execução
  3. Javadoc para a classe Parameter
lpandzic
fonte
2
Não sei se eles mudaram os argumentos para o plug-in do compilador, mas com o mais recente (3.5.1 a partir de agora) eu tive que fazer para usar os argumentos do compilador na seção de configuração: <configuration> <compilerArgs> <arg> - parâmetros </arg> </compilerArgs> </configuration>
max
15

A biblioteca Paranamer foi criada para resolver esse mesmo problema.

Ele tenta determinar os nomes dos métodos de algumas maneiras diferentes. Se a classe foi compilada com a depuração, ela pode extrair as informações lendo o bytecode da classe.

Outra maneira é injetar um membro estático privado no bytecode da classe após a compilação, mas antes de ser colocado em um jar. Em seguida, ele usa a reflexão para extrair essas informações da classe em tempo de execução.

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

Eu tive problemas ao usar esta biblioteca, mas consegui funcioná-la no final. Espero relatar os problemas ao mantenedor.

Sarel Botha
fonte
Estou tentando usar paranamer dentro de um apk android. Mas eu estou recebendoParameterNAmesNotFoundException
Rilwan
10

consulte a classe org.springframework.core.DefaultParameterNameDiscoverer

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

Sim.
O código deve ser compilado com o compilador compatível com Java 8, com a opção de armazenar nomes de parâmetros formais ativados ( opção -parameters ).
Então esse trecho de código deve 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
fonte
Tentei isso e funcionou! Uma dica, no entanto, tive que reconstruir seu projeto para que essas configurações do compilador entrassem em vigor.
IshmaelMakitla
5

Você pode recuperar o método com reflexão e detectar seus tipos de argumento. Verifique http://java.sun.com/j2se/1.4.2/docs/api/java/lang/reflect/Method.html#getParameterTypes%28%29

No entanto, você não pode dizer o nome do argumento usado.

Johnco
fonte
17
Verdadeiramente isso é tudo possível. Sua pergunta, porém, foi explicitamente sobre o nome do parâmetro . Verifique o topictitle.
BalusC
1
E cito: "No entanto, você não pode dizer o nome do argumento usado". acabei de ler minha resposta -_-
Johnco
3
A pergunta não foi formulada dessa maneira, ele não sabia sobre a obtenção do tipo.
BalusC
3

É possível e o Spring MVC 3 faz isso, mas não demorei muito para ver exatamente como.

A correspondência de nomes de parâmetros de métodos com nomes de variáveis ​​de modelo de URI só pode ser feita se seu código for compilado com a depuração ativada. Se você não tiver a depuração ativada, deverá especificar o nome do nome da variável do Modelo de URI na anotação @PathVariable para vincular o valor resolvido do nome da variável a um parâmetro de método. Por exemplo:

Retirado da documentação da primavera

flybywire
fonte
org.springframework.core.Conventions.getVariableName (), mas eu suponho que você deve ter cglib como dependência
Radu Toader
3

Embora não seja possível (como outros ilustraram), você pode usar uma anotação para substituir o nome do parâmetro e obtê-lo através de reflexão.

Não é a solução mais limpa, mas faz o trabalho. Alguns serviços da Web realmente fazem isso para manter os nomes dos parâmetros (por exemplo: implantar WSs com glassfish).

WhyNotHugo
fonte
3

Então você deve ser capaz de:

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

Mas você provavelmente obterá uma lista como esta:

["int : arg0"]

Eu acredito que isso será corrigido no Groovy 2.5+

Então, atualmente, a resposta é:

  • Se for uma classe Groovy, então não, você não pode obter o nome, mas deverá conseguir no futuro.
  • Se for uma classe Java compilada no Java 8, você poderá.

Veja também:


Para cada método, 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
fonte
Também não quero especificar o nome de um método aMethod. Eu quero obtê-lo para todos os métodos em uma classe.
snoop
@snoop adicionado um exemplo de obtenção de todos os métodos de não-sintético
tim_yates
Não podemos usar antlrpara obter nomes de parâmetros para isso?
snoop
2

se você usar o eclipse, consulte a imagem abaixo para permitir que o compilador armazene as informações sobre os parâmetros do método

insira a descrição da imagem aqui

Hazim
fonte
2

Como o @Bozho afirmou, é possível fazê-lo se as informações de depuração forem incluídas durante a compilação. Há uma boa resposta aqui ...

Como obter os nomes dos parâmetros dos construtores de um objeto (reflexão)? por @AdamPaynter

... usando a biblioteca ASM. Eu montei um exemplo mostrando como você pode alcançar seu objetivo.

Primeiro de tudo, comece com um pom.xml com essas dependências.

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

Então, essa classe deve fazer o que você deseja. Apenas invoque o 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;
    }
}

Aqui está um exemplo com um teste de unidade.

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

    }
}

Você pode encontrar o exemplo completo no GitHub

Ressalvas

  • Alterei levemente a solução original de @AdamPaynter para fazê-la funcionar nos métodos. Se bem entendi, sua solução funciona apenas com construtores.
  • Esta solução não funciona com staticmétodos. Nesse caso, o número de argumentos retornados pelo ASM é diferente, mas é algo que pode ser facilmente corrigido.
danidemi
fonte
0

Os nomes de parâmetros são úteis apenas para o compilador. Quando o compilador gera um arquivo de classe, os nomes dos parâmetros não são incluídos - a lista de argumentos de um método consiste apenas no número e nos tipos de argumentos. Portanto, seria impossível recuperar o nome do parâmetro usando reflexão (conforme marcado na sua pergunta) - ele não existe em nenhum lugar.

No entanto, se o uso da reflexão não for um requisito difícil, você poderá recuperar essas informações diretamente do código-fonte (supondo que você as tenha).

danben
fonte
2
O nome do parâmetro não é apenas útil para um compilador, mas também transmitem informações ao consumidor de uma biblioteca, suponha que eu escrevi uma classe StrangeDate que tinha o método add (int day, int hour, int minute) se você fosse usar isso e encontrou um método add (int arg0, int arg1, int arg2) quão útil isso seria?
David Waters
Sinto muito - você não entendeu completamente minha resposta. Releia-o no contexto da pergunta.
Danben
2
A aquisição dos nomes dos parâmetros é um grande benefício para chamar esse método de maneira reflexiva. Não é apenas útil para o compilador, mesmo no contexto da pergunta.
precisa saber é o seguinte
Parameter names are only useful to the compiler.Errado. Veja a biblioteca Retrofit. Ele usa interfaces dinâmicas para criar solicitações de API REST. Um de seus recursos é a capacidade de definir nomes de espaços reservados nos caminhos da URL e substituí-los pelos nomes de parâmetros correspondentes.
TheRealChx101 5/10/19
0

Para adicionar meus 2 centavos; as informações do parâmetro estão disponíveis em um arquivo de classe "para depuração" quando você usa o javac -g para compilar a fonte. E está disponível para o APT, mas você precisará de uma anotação para que não seja útil. (Alguém discutiu algo semelhante há 4-5 anos aqui: http://forums.java.net/jive/thread.jspa?messageID=13467&tstart=0 )

Em resumo, você não pode obtê-lo, a menos que trabalhe diretamente nos arquivos de origem (semelhante ao que o APT faz no momento da compilação).

Elister
fonte