Bisakah saya mendapatkan nama parameter metode menggunakan refleksi Java?

124

Jika saya memiliki kelas seperti ini:

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

apakah ada cara untuk mengetahui yang aMethodmenggunakan parameter bernama aParam, yaitu tipe int?

Geo
sumber
10
7 jawaban dan tidak ada yang menyebutkan istilah "metode tanda tangan". Ini pada dasarnya adalah apa yang dapat Anda introspeksi dengan refleksi, yang artinya: tidak ada nama parameter.
ewernli
3
itu adalah mungkin, lihat jawaban saya di bawah ini
fly-by-wire
4
6 tahun kemudian, sekarang dimungkinkan melalui refleksi di Jawa 8 , lihat jawaban SO ini
earcam

Jawaban:

90

Untuk meringkas:

  • mendapatkan nama parameter adalah mungkin jika informasi debug disertakan selama kompilasi. Lihat jawaban ini untuk lebih jelasnya
  • jika tidak, mendapatkan nama parameter tidak dimungkinkan
  • mendapatkan jenis parameter dimungkinkan, menggunakan method.getParameterTypes()

Demi menulis fungsionalitas pelengkapan otomatis untuk editor (seperti yang Anda nyatakan di salah satu komentar) ada beberapa opsi:

  • penggunaan arg0, arg1, arg2dll
  • penggunaan intParam, stringParam, objectTypeParam, dll
  • menggunakan kombinasi di atas - yang pertama untuk tipe non-primitif, dan yang terakhir untuk tipe primitif.
  • jangan tampilkan nama argumen sama sekali - cukup jenisnya saja.
Bozho
sumber
4
Apakah ini mungkin dengan Interfaces? Saya masih tidak dapat menemukan cara untuk mendapatkan nama parameter antarmuka.
Cemo
@Cemo: apakah Anda dapat menemukan cara untuk mendapatkan nama parameter antarmuka?
Vaibhav Gupta
Sekadar menambahkan, itulah mengapa spring membutuhkan anotasi @ConstructorProperties untuk membuat objek dari tipe primitif secara jelas.
Bharat
102

Di Java 8 Anda dapat melakukan hal berikut:

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

Jadi untuk kelas Anda, Whateverkami dapat melakukan tes 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;
            }
        }
    }
}

yang akan dicetak [aParam]jika Anda telah memberikan -parametersargumen ke compiler Java 8 Anda.

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

Untuk informasi lebih lanjut lihat tautan berikut:

  1. Tutorial Resmi Java: Mendapatkan Nama Parameter Metode
  2. JEP 118: Akses ke Nama Parameter pada Waktu Proses
  3. Javadoc untuk kelas Parameter
lpandzic.dll
sumber
2
Saya tidak tahu apakah mereka mengubah argumen untuk plugin kompilator, tetapi dengan yang terbaru (3.5.1 seperti yang sekarang) saya harus lakukan untuk menggunakan argumen kompilator di bagian konfigurasi: <configuration> <compilerArgs> <arg> - parameter </arg> </compilerArgs> </configuration>
maks
15

Perpustakaan Paranamer dibuat untuk mengatasi masalah yang sama ini.

Ia mencoba untuk menentukan nama metode dengan beberapa cara berbeda. Jika kelas dikompilasi dengan debugging, ia dapat mengekstrak informasi dengan membaca bytecode kelas.

Cara lain adalah dengan menginjeksi anggota statis pribadi ke dalam bytecode kelas setelah dikompilasi, tetapi sebelum ditempatkan di dalam toples. Kemudian menggunakan refleksi untuk mengekstrak informasi ini dari kelas pada saat runtime.

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

Saya mengalami masalah saat menggunakan perpustakaan ini, tetapi pada akhirnya saya berhasil membuatnya berfungsi. Saya berharap untuk melaporkan masalah ini ke pengelola.

Sarel Botha
sumber
Saya mencoba menggunakan paranamer di dalam apk android. Tapi saya mendapatkanParameterNAmesNotFoundException
Rilwan
10

lihat kelas org.springframework.core.DefaultParameterNameDiscoverer

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

Iya.
Kode harus dikompilasi dengan compiler yang sesuai dengan Java 8 dengan opsi untuk menyimpan nama parameter formal yang diaktifkan ( opsi -parameters ).
Maka cuplikan kode ini akan berfungsi:

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
sumber
Sudah mencoba ini dan berhasil! Namun satu tip, saya harus membangun kembali proyek Anda agar konfigurasi kompilator ini berlaku.
ishmaelMakitla
5

Anda dapat mengambil metode dengan refleksi dan mendeteksi tipe argumennya. Periksa http://java.sun.com/j2se/1.4.2/docs/api/java/lang/reflect/Method.html#getParameterTypes%28%29

Namun, Anda tidak dapat memberi tahu nama argumen yang digunakan.

Johnco
sumber
17
Sungguh itu semua mungkin. Namun pertanyaannya secara eksplisit tentang nama parameter . Periksa judulnya.
BalusC
1
Dan saya mengutip: "Namun, Anda tidak bisa menyebutkan nama argumen yang digunakan." baca saja jawaban saya -_-
Johnco
3
Pertanyaannya tidak dirumuskan seperti itu, dia tidak tahu tentang mendapatkan tipe tersebut.
BalusC
3

Itu mungkin dan Spring MVC 3 melakukannya, tetapi saya tidak meluangkan waktu untuk melihat persis bagaimana caranya.

Pencocokan nama parameter metode ke nama variabel Template URI hanya dapat dilakukan jika kode Anda dikompilasi dengan debugging diaktifkan. Jika Anda belum mengaktifkan debugging, Anda harus menentukan nama nama variabel Template URI dalam anotasi @PathVariable untuk mengikat nilai yang diselesaikan dari nama variabel ke parameter metode. Sebagai contoh:

Diambil dari dokumentasi musim semi

flybywire
sumber
org.springframework.core.Conventions.getVariableName () tetapi saya kira Anda harus memiliki cglib sebagai dependensi
Radu Toader
3

Meskipun tidak mungkin (seperti yang digambarkan orang lain), Anda bisa menggunakan anotasi untuk membawa nama parameter, dan mendapatkannya melalui refleksi.

Bukan solusi terbersih, tetapi menyelesaikan pekerjaan. Beberapa layanan web sebenarnya melakukan ini untuk menyimpan nama parameter (yaitu: menyebarkan WS dengan glassfish).

WhyNotHugo
sumber
3

Jadi, Anda harus bisa melakukan:

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

Tetapi Anda mungkin akan mendapatkan daftar seperti ini:

["int : arg0"]

Saya yakin ini akan diperbaiki di Groovy 2.5+

Jadi saat ini jawabannya adalah:

  • Jika itu kelas Groovy, maka tidak, Anda tidak bisa mendapatkan namanya, tetapi Anda harus bisa mendapatkannya di masa mendatang.
  • Jika itu adalah kelas Java yang dikompilasi di bawah Java 8, Anda seharusnya bisa.

Lihat juga:


Untuk setiap metode, maka sesuatu seperti:

Whatever.declaredMethods
        .findAll { !it.synthetic }
        .collect { method -> 
            println method
            method.name + " -> " + method.parameters.collect { "[$it.type : $it.name]" }.join(';')
        }
        .each {
            println it
        }
tim_yates
sumber
Saya juga tidak ingin menentukan nama metode aMethod. Saya ingin mendapatkannya untuk semua metode di kelas.
mengintip
@snoop menambahkan contoh untuk mendapatkan setiap metode non-sintetis
tim_yates
Bisakah kita menggunakan antlruntuk mendapatkan nama parameter untuk ini?
mengintip
2

jika Anda menggunakan gerhana, lihat gambar di bawah ini untuk memungkinkan kompiler menyimpan informasi tentang parameter metode

masukkan deskripsi gambar di sini

Hazim
sumber
2

Seperti yang dinyatakan @Bozho, dimungkinkan untuk melakukannya jika informasi debug disertakan selama kompilasi. Ada jawaban yang bagus di sini ...

Bagaimana cara mendapatkan nama parameter konstruktor objek (refleksi)? oleh @AdamPaynter

... menggunakan perpustakaan ASM. Saya mengumpulkan contoh yang menunjukkan bagaimana Anda dapat mencapai tujuan Anda.

Pertama-tama, mulailah dengan pom.xml dengan dependensi ini.

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

Kemudian, kelas ini harus melakukan apa yang Anda inginkan. Panggil saja metode statis 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;
    }
}

Berikut adalah contoh dengan unit test.

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

    }
}

Anda dapat menemukan contoh lengkapnya di GitHub

Peringatan

  • Saya sedikit mengubah solusi asli dari @AdamPaynter agar berfungsi untuk Metode. Jika saya mengerti dengan benar, solusinya hanya berfungsi dengan konstruktor.
  • Solusi ini tidak bekerja dengan staticmetode. Ini karena dalam hal ini jumlah argumen yang dikembalikan oleh ASM berbeda, tetapi itu adalah sesuatu yang dapat diperbaiki dengan mudah.
danidemi
sumber
0

Nama parameter hanya berguna untuk kompilator. Ketika kompilator membuat file kelas, nama parameter tidak disertakan - daftar argumen metode hanya terdiri dari jumlah dan jenis argumennya. Jadi tidak mungkin untuk mengambil nama parameter menggunakan refleksi (seperti yang ditandai dalam pertanyaan Anda) - tidak ada di mana pun.

Namun, jika penggunaan refleksi bukanlah persyaratan yang sulit, Anda dapat mengambil informasi ini langsung dari kode sumber (dengan asumsi Anda memilikinya).

danben
sumber
2
Nama parameter tidak hanya berguna bagi kompiler, mereka juga menyampaikan informasi kepada pengguna perpustakaan, asumsikan saya menulis kelas StrangeDate yang memiliki metode add (int day, int hour, int minute) jika Anda menggunakan ini dan menemukan metode tambahkan (int arg0, int arg1, int arg2) seberapa berguna itu?
David Waters
Maaf - Anda salah memahami jawaban saya sepenuhnya. Harap baca ulang dalam konteks pertanyaan.
danben
2
Mendapatkan nama-nama parameter adalah manfaat besar untuk memanggil metode itu secara reflektif. Ini tidak hanya berguna bagi kompiler, bahkan dalam konteks pertanyaannya.
corsiKa
Parameter names are only useful to the compiler.Salah. Lihatlah perpustakaan Retrofit. Ini menggunakan Antarmuka dinamis untuk membuat permintaan REST API. Salah satu fiturnya adalah kemampuan untuk menentukan nama placeholder di jalur URL dan mengganti placeholder tersebut dengan nama parameter yang sesuai.
TheRealChx101
0

Untuk menambahkan 2 sen saya; info parameter tersedia dalam file kelas "untuk debugging" saat Anda menggunakan javac -g untuk mengompilasi sumber. Dan itu tersedia untuk APT tetapi Anda memerlukan anotasi sehingga tidak berguna bagi Anda. (Seseorang mendiskusikan hal serupa 4-5 tahun yang lalu di sini: http://forums.java.net/jive/thread.jspa?messageID=13467&tstart=0 )

Secara keseluruhan singkatnya Anda tidak bisa mendapatkannya kecuali Anda mengerjakan file Sumber secara langsung (mirip dengan apa yang dilakukan APT pada waktu kompilasi).

Elister
sumber