Czy mogę uzyskać nazwę parametru metody za pomocą odbicia Java?


124

Jeśli mam taką klasę:

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

czy istnieje sposób, aby dowiedzieć się, że aMethodużywa parametru o nazwie aParam, czyli typu int?


10
7 odpowiedzi i nikt nie wspomniał o pojęciu „sygnatura metody”. To jest w zasadzie to, co można introspekować z refleksją, co oznacza: brak nazw parametrów.
ewernli

3
jest to możliwe, zobacz moją odpowiedź poniżej
flybywire

4
6 lat później jest to teraz możliwe dzięki refleksji w Javie 8 , zobacz odpowiedź SO
kamera douszna

Odpowiedzi:


90

Podsumowując:

  • pobieranie nazw parametrów jest możliwe, jeśli podczas kompilacji uwzględniono informacje debugowania. Zobacz tę odpowiedź, aby uzyskać więcej informacji
  • w przeciwnym razie pobranie nazw parametrów nie jest możliwe
  • pobranie typu parametru jest możliwe przy użyciu method.getParameterTypes()

Ze względu na pisanie funkcji autouzupełniania dla edytora (jak wspomniałeś w jednym z komentarzy) jest kilka opcji:

  • Zastosowanie arg0, arg1, arg2itd.
  • Zastosowanie intParam, stringParam, objectTypeParam, itd.
  • użyj kombinacji powyższych - pierwsza dla typów niebędących prymitywami, a druga dla typów pierwotnych.
  • nie pokazuj w ogóle nazw argumentów - tylko typy.

4
Czy jest to możliwe w przypadku interfejsów? Nadal nie mogłem znaleźć sposobu na uzyskanie nazw parametrów interfejsu.
Cemo

@Cemo: czy udało Ci się znaleźć sposób na uzyskanie nazw parametrów interfejsu?
Vaibhav Gupta

Dodam tylko, że dlatego Spring potrzebuje adnotacji @ConstructorProperties do jednoznacznego tworzenia obiektów z typów pierwotnych.
Bharat,

102

W Javie 8 możesz wykonać następujące czynności:

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

Więc dla Twojej klasy Whatevermożemy wykonać test manualny:

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

który powinien zostać wydrukowany, [aParam]jeśli przekazałeś -parametersargument do kompilatora Java 8.

Dla użytkowników 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>

Aby uzyskać więcej informacji, zobacz następujące łącza:

  1. Oficjalny samouczek Java: uzyskiwanie nazw parametrów metod
  2. JEP 118: Dostęp do nazw parametrów w czasie wykonywania
  3. Javadoc dla klasy Parameter

2
Nie wiem, czy zmienili argumenty dla wtyczki kompilatora, ale z najnowszą (3.5.1 na razie) musiałem zrobić, aby użyć argumentów kompilatora w sekcji konfiguracji: <configuration> <compilerArgs> <arg> - parametry </arg> </compilerArgs> </configuration>
maks.

15

Biblioteka Paranamer została stworzona, aby rozwiązać ten sam problem.

Próbuje określić nazwy metod na kilka różnych sposobów. Jeśli klasa została skompilowana z debugowaniem, może wyodrębnić informacje, odczytując kod bajtowy klasy.

Innym sposobem jest wstrzyknięcie prywatnego statycznego elementu członkowskiego do kodu bajtowego klasy po jej skompilowaniu, ale przed umieszczeniem go w słoiku. Następnie używa odbicia, aby wyodrębnić te informacje z klasy w czasie wykonywania.

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

Miałem problemy z używaniem tej biblioteki, ale w końcu udało mi się ją uruchomić. Mam nadzieję zgłosić ten problem opiekunowi.


Próbuję użyć paranamer wewnątrz aplikacji dla Androida. Ale dostajęParameterNAmesNotFoundException
Rilwan,

10

zobacz klasę org.springframework.core.DefaultParameterNameDiscoverer

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

10

Tak.
Kod musi być skompilowany za pomocą kompilatora zgodnego z Javą 8 z włączoną opcją przechowywania formalnych nazw parametrów ( opcja -parameters ).
Wtedy ten fragment kodu powinien działać:

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

Próbowałem i zadziałało! Jedna wskazówka: musiałem przebudować twój projekt, aby te konfiguracje kompilatora zaczęły obowiązywać.
ishmaelMakitla

5

Możesz pobrać metodę z odbiciem i wykryć jej typy argumentów. Sprawdź http://java.sun.com/j2se/1.4.2/docs/api/java/lang/reflect/Method.html#getParameterTypes%28%29

Jednak nie możesz podać nazwy użytego argumentu.


17
Naprawdę to wszystko jest możliwe. Jego pytanie było jednak wyraźnie o nazwie parametru . Sprawdź tytuł.
BalusC,

1
Cytuję: „Jednak nie możesz podać nazwy użytego argumentu”. po prostu przeczytaj moją odpowiedź -_-
Johnco,

3
Pytanie nie zostało sformułowane w ten sposób, że nie wiedział o uzyskaniu typu.
BalusC,

3

Jest to możliwe i Spring MVC 3 to robi, ale nie poświęciłem czasu, aby zobaczyć, jak to zrobić.

Dopasowanie nazw parametrów metod do nazw zmiennych szablonu URI można wykonać tylko wtedy, gdy kod jest kompilowany z włączonym debugowaniem. Jeśli debugowanie nie jest włączone, należy określić nazwę zmiennej szablonu URI w adnotacji @PathVariable, aby powiązać rozstrzygniętą wartość nazwy zmiennej z parametrem metody. Na przykład:

Zaczerpnięte z dokumentacji wiosennej


org.springframework.core.Conventions.getVariableName (), ale przypuszczam, że musisz mieć cglib jako zależność
Radu Toader

3

Chociaż nie jest to możliwe (jak inni rysunku), to mógłby użyć adnotacji o przeniesienie nazwę parametru, a uzyskanie że chociaż refleksji.

Nie jest to najczystsze rozwiązanie, ale spełnia swoje zadanie. Niektóre usługi sieciowe faktycznie robią to, aby zachować nazwy parametrów (np. Wdrażanie WS z Glassfish).



3

Więc powinieneś być w stanie zrobić:

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

Ale prawdopodobnie otrzymasz taką listę:

["int : arg0"]

Wierzę, że zostanie to naprawione w Groovy 2.5+

Tak więc obecnie odpowiedź brzmi:

  • Jeśli to klasa Groovy, to nie, nie możesz uzyskać nazwy, ale powinieneś być w stanie to zrobić w przyszłości.
  • Jeśli jest to klasa Java skompilowana pod Javą 8, powinieneś być w stanie.

Zobacz też:


Dla każdej metody wtedy coś takiego:

Whatever.declaredMethods
        .findAll { !it.synthetic }
        .collect { method -> 
            println method
            method.name + " -> " + method.parameters.collect { "[$it.type : $it.name]" }.join(';')
        }
        .each {
            println it
        }

Nie chcę też określać nazwy metody aMethod. Chcę to uzyskać dla wszystkich metod w klasie.
snoop

@snoop dodał przykład pobierania każdej metody niesyntetycznej
tim_yates

Nie możemy użyć antlrdo uzyskania nazw parametrów dla tego?
snoop

2

jeśli używasz eclipse, zobacz poniższy obrazek, aby umożliwić kompilatorowi przechowywanie informacji o parametrach metody

wprowadź opis obrazu tutaj


2

Jak stwierdził @Bozho, można to zrobić, jeśli podczas kompilacji zostaną uwzględnione informacje o debugowaniu. Tutaj jest dobra odpowiedź ...

Jak uzyskać nazwy parametrów konstruktorów obiektu (odbicie)? przez @AdamPaynter

... używając biblioteki ASM. Przygotowałem przykład pokazujący, jak możesz osiągnąć swój cel.

Przede wszystkim zacznij od pom.xml z tymi zależnościami.

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

Następnie ta klasa powinna robić, co chcesz. Po prostu wywołaj metodę statyczną 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;
    }
}

Oto przykład z testem jednostkowym.

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

    }
}

Kompletny przykład można znaleźć na GitHub

Ostrzeżenia

  • Nieznacznie zmieniłem oryginalne rozwiązanie z @AdamPaynter, aby działało dla metod. Jeśli dobrze zrozumiałem, jego rozwiązanie działa tylko z konstruktorami.
  • To rozwiązanie nie działa z staticmetodami. Dzieje się tak dlatego, że w tym przypadku liczba argumentów zwracanych przez ASM jest różna, ale jest to coś, co można łatwo naprawić.

0

Nazwy parametrów są przydatne tylko dla kompilatora. Gdy kompilator generuje plik klasy, nazwy parametrów nie są uwzględniane - lista argumentów metody składa się tylko z liczby i typów jej argumentów. Dlatego nie byłoby możliwe pobranie nazwy parametru za pomocą refleksji (tak jak zostało to zaznaczone w Twoim pytaniu) - nigdzie nie istnieje.

Jeśli jednak użycie odbicia nie jest trudnym wymaganiem, możesz pobrać te informacje bezpośrednio z kodu źródłowego (zakładając, że je masz).


2
Nazwa parametru jest przydatna nie tylko dla kompilatora, ale również przekazuje informacje konsumentowi biblioteki, załóżmy, że napisałem klasę StrangeDate, która miała metodę add (int day, int hour, int minute), jeśli użyłeś tego i znalazłeś metodę add (int arg0, int arg1, int arg2), jak przydatne byłoby to?
David Waters

Przepraszam - całkowicie źle zrozumiałeś moją odpowiedź. Przeczytaj go ponownie w kontekście pytania.
danben

2
Uzyskanie nazw parametrów jest wielką korzyścią dla wywoływania tej metody w sposób refleksyjny. Jest to przydatne nie tylko dla kompilatora, nawet w kontekście pytania.
corsiKa

Parameter names are only useful to the compiler.Źle. Spójrz na bibliotekę Retrofit. Wykorzystuje dynamiczne interfejsy do tworzenia żądań REST API. Jedną z jego funkcji jest możliwość definiowania nazw symboli zastępczych w ścieżkach adresów URL i zastępowania tych symboli zastępczych odpowiadającymi im nazwami parametrów.
TheRealChx101

0

Aby dodać moje 2 centy; Informacje o parametrach są dostępne w pliku klasy „do debugowania”, gdy używasz javac -g do kompilowania źródła. Jest dostępny dla APT, ale będziesz potrzebować adnotacji, więc nie będzie dla ciebie pożytku. (Ktoś omawiał coś podobnego 4-5 lat temu tutaj: http://forums.java.net/jive/thread.jspa?messageID=13467&tstart=0 )

Krótko mówiąc, nie możesz tego dostać, chyba że pracujesz bezpośrednio na plikach źródłowych (podobnie do tego, co robi APT w czasie kompilacji).

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.