ฉันสามารถรับชื่อพารามิเตอร์เมธอดโดยใช้การสะท้อนของ Java ได้หรือไม่


124

ถ้าฉันมีชั้นเรียนเช่นนี้:

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

จะมีวิธีใดที่จะรู้ว่าaMethodใช้พารามิเตอร์การตั้งชื่อaParamที่เป็นประเภทint?


10
7 คำตอบและไม่มีใครพูดถึงคำว่า "method signature" นี่คือสิ่งที่คุณสามารถไตร่ตรองด้วยการไตร่ตรองซึ่งหมายความว่า: ไม่มีชื่อพารามิเตอร์
ewernli

3
มันเป็นไปได้ดูคำตอบของฉันด้านล่าง
flybywire

4
6 ปีต่อมาสามารถทำได้ผ่านการสะท้อนในJava 8ดูคำตอบ SO นี้
earcam

คำตอบ:


90

สรุป:

  • การรับชื่อพารามิเตอร์เป็นไปได้หากรวมข้อมูลการดีบักระหว่างการคอมไพล์ ดูคำตอบนี้สำหรับรายละเอียดเพิ่มเติม
  • มิฉะนั้นจะไม่สามารถรับชื่อพารามิเตอร์ได้
  • การรับประเภทพารามิเตอร์เป็นไปได้โดยใช้ method.getParameterTypes()

เพื่อประโยชน์ในการเขียนฟังก์ชันการเติมข้อความอัตโนมัติสำหรับตัวแก้ไข (ตามที่คุณระบุไว้ในความคิดเห็น) มีตัวเลือกสองสามตัวเลือก:

  • การใช้งานarg0, arg1, arg2ฯลฯ
  • การใช้งานintParam, stringParam, objectTypeParamฯลฯ
  • ใช้ชุดค่าผสมข้างต้น - ก่อนหน้านี้สำหรับประเภทที่ไม่ใช่ดั้งเดิมและหลังสำหรับประเภทดั้งเดิม
  • อย่าแสดงชื่ออาร์กิวเมนต์เลย - เฉพาะประเภท

4
เป็นไปได้กับอินเทอร์เฟซหรือไม่? ฉันยังไม่พบวิธีรับชื่อพารามิเตอร์อินเทอร์เฟซ
Cemo

@Cemo: คุณหาวิธีรับชื่อพารามิเตอร์อินเทอร์เฟซได้หรือไม่?
Vaibhav Gupta

เพื่อเพิ่มนั่นคือเหตุผลที่ spring ต้องการคำอธิบายประกอบ @ConstructorProperties สำหรับการสร้างวัตถุจากชนิดดั้งเดิมที่ไม่คลุมเครือ
Bharat

102

ใน Java 8 คุณสามารถทำสิ่งต่อไปนี้:

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

ดังนั้นสำหรับชั้นเรียนของคุณWhateverเราสามารถทำการทดสอบด้วยตนเองได้:

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

ซึ่งควรพิมพ์[aParam]หากคุณส่ง-parametersอาร์กิวเมนต์ไปยังคอมไพเลอร์ Java 8 ของคุณ

สำหรับผู้ใช้ 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>

สำหรับข้อมูลเพิ่มเติมโปรดดูลิงค์ต่อไปนี้:

  1. สอน Java อย่างเป็นทางการ: การรับชื่อของพารามิเตอร์วิธีการ
  2. JEP 118: เข้าถึงชื่อพารามิเตอร์ที่รันไทม์
  3. Javadoc สำหรับคลาส Parameter

2
ฉันไม่รู้ว่าพวกเขาเปลี่ยนอาร์กิวเมนต์สำหรับปลั๊กอินคอมไพเลอร์หรือไม่ แต่ด้วยล่าสุด (3.5.1 ณ ตอนนี้) ฉันต้องทำเพื่อใช้ compiler args ในส่วน configuration: <configuration> <compilerArgs> <arg> - พารามิเตอร์ </arg> </compilerArgs> </configuration>
สูงสุด

15

ไลบรารี Paranamer ถูกสร้างขึ้นเพื่อแก้ปัญหาเดียวกันนี้

จะพยายามกำหนดชื่อวิธีการด้วยวิธีต่างๆ หากคลาสถูกคอมไพล์ด้วยการดีบักก็สามารถดึงข้อมูลโดยการอ่าน bytecode ของคลาส

อีกวิธีหนึ่งคือฉีดสมาชิกแบบคงที่ส่วนตัวลงใน bytecode ของคลาสหลังจากที่คอมไพล์แล้ว แต่ก่อนที่มันจะถูกใส่ลงในโถ จากนั้นใช้การสะท้อนเพื่อดึงข้อมูลนี้จากคลาสที่รันไทม์

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

ฉันมีปัญหาในการใช้ไลบรารีนี้ แต่สุดท้ายก็ใช้งานได้ ฉันหวังว่าจะรายงานปัญหาไปยังผู้ดูแล


ฉันพยายามใช้ paranamer ใน android apk แต่ฉันกำลังได้รับParameterNAmesNotFoundException
Rilwan


10

ใช่.
โค้ดต้องคอมไพเลอร์ด้วยคอมไพเลอร์ที่เข้ากันได้กับ Java 8พร้อมอ็อพชันในการจัดเก็บชื่อพารามิเตอร์ที่เป็นทางการที่เปิดอยู่ ( อ็อพชัน-parameters )
จากนั้นข้อมูลโค้ดนี้ควรใช้งานได้:

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

ลองแล้วได้ผล! เคล็ดลับอย่างหนึ่งฉันต้องสร้างโครงการของคุณใหม่เพื่อให้การกำหนดค่าคอมไพเลอร์เหล่านี้มีผล
ishmaelMakitla

5

คุณสามารถดึงวิธีการด้วยการสะท้อนและตรวจจับประเภทอาร์กิวเมนต์ได้ ตรวจสอบhttp://java.sun.com/j2se/1.4.2/docs/api/java/lang/reflect/Method.html#getParameterTypes%28%29

อย่างไรก็ตามคุณไม่สามารถบอกชื่อของอาร์กิวเมนต์ที่ใช้


17
นั่นคือทั้งหมดที่เป็นไปได้อย่างแท้จริง คำถามของเขาเป็นอย่างไรอย่างชัดเจนเกี่ยวกับชื่อพารามิเตอร์ ตรวจสอบ topictitle
BalusC

1
และฉันอ้างว่า: "อย่างไรก็ตามคุณไม่สามารถบอกชื่อของอาร์กิวเมนต์ที่ใช้" เพิ่งอ่านคำตอบของฉัน -_-
Johnco

3
คำถามไม่ได้ถูกกำหนดขึ้นโดยที่เขาไม่รู้เกี่ยวกับการได้รับประเภท
BalusC

3

เป็นไปได้และ Spring MVC 3 ก็ทำได้ แต่ฉันไม่ได้ใช้เวลาเพื่อดูว่าเป็นอย่างไร

การจับคู่ชื่อพารามิเตอร์เมธอดกับชื่อตัวแปรเทมเพลต URI สามารถทำได้ก็ต่อเมื่อโค้ดของคุณถูกคอมไพล์โดยเปิดใช้งานการดีบัก หากคุณไม่ได้เปิดใช้งานการดีบักคุณต้องระบุชื่อของชื่อตัวแปร URI Template ในคำอธิบายประกอบ @PathVariable เพื่อผูกค่าที่แก้ไขแล้วของชื่อตัวแปรกับพารามิเตอร์เมธอด ตัวอย่างเช่น:

นำมาจากเอกสารสปริง


org.springframework.core.Conventions.getVariableName () แต่ฉันคิดว่าคุณต้องมี cglib เป็นที่พึ่ง
Radu Toader

3

แม้ว่าจะเป็นไปไม่ได้ (ตามที่คนอื่นแสดงให้เห็น) แต่คุณสามารถใช้คำอธิบายประกอบเพื่อนำไปใช้กับชื่อพารามิเตอร์และรับสิ่งนั้นแม้ว่าจะสะท้อนกลับ

ไม่ใช่วิธีการแก้ปัญหาที่สะอาดที่สุด แต่จะทำให้งานลุล่วง เว็บไซต์บริการบางอย่างทำเช่นนี้เพื่อเก็บชื่อพารามิเตอร์ไว้ (เช่นการปรับใช้ WSs กับ glassfish)



3

ดังนั้นคุณควรจะทำได้:

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

แต่คุณอาจได้รับรายชื่อดังนี้:

["int : arg0"]

ฉันเชื่อว่าสิ่งนี้จะได้รับการแก้ไขใน Groovy 2.5+

ดังนั้นในปัจจุบันคำตอบคือ:

  • ถ้าเป็นคลาส Groovy ก็ไม่ได้ชื่อนี้ แต่น่าจะทำได้ในอนาคต
  • หากเป็นคลาส Java ที่คอมไพล์ภายใต้ Java 8 คุณควรจะทำได้

ดูสิ่งนี้ด้วย:


สำหรับทุกวิธีแล้วสิ่งที่ต้องการ:

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

aMethodฉันยังไม่ต้องการที่จะระบุชื่อของวิธีการที่ ฉันต้องการรับมันสำหรับทุกวิธีในชั้นเรียน
สอดแนม

@snoop เพิ่มตัวอย่างของการได้รับทุกวิธีที่ไม่สังเคราะห์
tim_yates

เราไม่สามารถใช้antlrชื่อพารามิเตอร์สำหรับสิ่งนี้ได้หรือไม่?
สอดแนม

2

หากคุณใช้ eclipse โปรดดูภาพต่อไปนี้เพื่ออนุญาตให้คอมไพลเลอร์เก็บข้อมูลเกี่ยวกับพารามิเตอร์วิธีการ

ใส่คำอธิบายภาพที่นี่


2

ตามที่ @Bozho ระบุไว้เป็นไปได้ที่จะดำเนินการดังกล่าวหากมีการรวมข้อมูลการแก้ปัญหาในระหว่างการรวบรวม มีคำตอบที่ดีที่นี่ ...

จะรับชื่อพารามิเตอร์ของตัวสร้างวัตถุ (การสะท้อน) ได้อย่างไร? โดย @AdamPaynter

... โดยใช้ไลบรารี ASM ฉันรวบรวมตัวอย่างที่แสดงให้เห็นว่าคุณจะบรรลุเป้าหมายได้อย่างไร

ก่อนอื่นเริ่มต้นด้วย pom.xml ด้วยการอ้างอิงเหล่านี้

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

จากนั้นชั้นเรียนนี้ควรทำในสิ่งที่คุณต้องการ 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;
    }
}

นี่คือตัวอย่างของการทดสอบหน่วย

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

    }
}

คุณสามารถดูตัวอย่างทั้งหมดได้ในGitHub

คำเตือน

  • ฉันเปลี่ยนโซลูชันดั้งเดิมเล็กน้อยจาก @AdamPaynter เพื่อให้ใช้งานได้กับ Methods หากฉันเข้าใจอย่างถูกต้องวิธีการแก้ปัญหาของเขาใช้ได้กับผู้สร้างเท่านั้น
  • วิธีนี้ใช้ไม่ได้กับstaticเมธอด เนื่องจากในกรณีนี้จำนวนอาร์กิวเมนต์ที่ ASM ส่งคืนแตกต่างกัน แต่เป็นสิ่งที่แก้ไขได้ง่าย

0

ชื่อพารามิเตอร์มีประโยชน์กับคอมไพเลอร์เท่านั้น เมื่อคอมไพลเลอร์สร้างไฟล์คลาสจะไม่มีการรวมชื่อพารามิเตอร์ - รายการอาร์กิวเมนต์ของเมธอดประกอบด้วยจำนวนและประเภทของอาร์กิวเมนต์เท่านั้น ดังนั้นจึงเป็นไปไม่ได้ที่จะดึงชื่อพารามิเตอร์โดยใช้การสะท้อนกลับ (ตามที่ติดแท็กในคำถามของคุณ) - ไม่มีอยู่ที่ใดเลย

อย่างไรก็ตามหากการใช้การสะท้อนกลับไม่ใช่ข้อกำหนดที่ยากคุณสามารถดึงข้อมูลนี้ได้โดยตรงจากซอร์สโค้ด (สมมติว่าคุณมี)


2
ชื่อพารามิเตอร์ไม่เพียง แต่มีประโยชน์กับคอมไพเลอร์เท่านั้น แต่ยังถ่ายทอดข้อมูลไปยังผู้บริโภคของ libary ด้วยสมมติว่าฉันเขียนคลาส StrangeDate ซึ่งมี method add (int day, int hour, int minute) หากคุณไปใช้สิ่งนี้และพบวิธีการ เพิ่ม (int arg0, int arg1, int arg2) มันจะมีประโยชน์แค่ไหน?
David Waters

ฉันขอโทษ - คุณเข้าใจคำตอบของฉันผิดทั้งหมด โปรดอ่านซ้ำในบริบทของคำถาม
danben

2
การได้มาซึ่งชื่อของพารามิเตอร์เป็นประโยชน์อย่างยิ่งในการเรียกวิธีการนั้นอย่างไตร่ตรอง ไม่เพียง แต่มีประโยชน์ต่อผู้เรียบเรียงแม้ในบริบทของคำถาม
corsiKa

Parameter names are only useful to the compiler.ไม่ถูกต้อง. ดูที่ Retrofit library ใช้อินเทอร์เฟซแบบไดนามิกเพื่อสร้างคำขอ REST API คุณสมบัติอย่างหนึ่งคือความสามารถในการกำหนดชื่อตัวยึดตำแหน่งในเส้นทาง URL และแทนที่ตัวยึดตำแหน่งเหล่านั้นด้วยชื่อพารามิเตอร์ที่เกี่ยวข้อง
TheRealChx101

0

เพื่อเพิ่ม 2 เซ็นต์ของฉัน ข้อมูลพารามิเตอร์มีอยู่ในไฟล์คลาส "สำหรับการดีบัก" เมื่อคุณใช้ javac -g เพื่อคอมไพล์ซอร์ส และสามารถใช้ได้กับ APT แต่คุณจะต้องมีคำอธิบายประกอบจึงไม่มีประโยชน์สำหรับคุณ (มีคนพูดถึงสิ่งที่คล้ายกันเมื่อ 4-5 ปีก่อนที่นี่: http://forums.java.net/jive/thread.jspa?messageID=13467&tstart=0 )

โดยรวมแล้วคุณจะไม่สามารถรับมันได้เว้นแต่คุณจะทำงานกับไฟล์ต้นฉบับโดยตรง (คล้ายกับที่ APT ทำในเวลาคอมไพล์)

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.