How to get Method Parameter names in Java 8 using reflection? - java

Java 8 has the ability to acquire method parameter names using Reflection API.
How can I get these method parameter names?
As per my knowledge, class files do not store formal parameter names. How can I get these using reflection?

How can i get these method parameter names?
Basically, you need to:
get a reference to a Class
From the Class, get a reference to a Method by calling getDeclaredMethod() or getDeclaredMethods() which returns references to Method objects
From the Method object, call (new as of Java 8) getParameters() which returns an array of Parameter objects
On the Parameter object, call getName()
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());
}
}
Output:
...
indexOf
arg0
indexOf
arg0
arg1
...
Also as per my knowledge .class files do not store formal parameter. Then how can i get them using reflection?
See the javadoc for Parameter.getName():
... If the parameter's name is present, then this method returns the name provided by the class file. Otherwise, this method synthesizes a name of the form argN, where N is the index of the parameter in the descriptor of the method which declares the parameter.
Whether a JDK supports this, is implementation specific (as you can see form the above output, build 125 of JDK 8 does not support it). The class file format supports optional attributes which can be used by a specific JVM/javac implementation and which are ignored by other implementations which do not support it.
Note that you could even generate the above output with arg0, arg1, ... with pre Java 8 JVMs - all you need to know is the parameter count which is accessible through Method.getParameterTypes():
Class<String> clz = String.class;
for (Method m : clz.getDeclaredMethods()) {
System.err.println(m.getName());
int paramCount = m.getParameterTypes().length;
for (int i = 0; i < paramCount; i++) {
System.err.println(" arg" + i);
}
}
What is new with JDK 8 is that there is an extended API and the possibility for JVMs to provide the real parameter names instead of arg0, arg1, ...
Supporting such optional features is possible through optional attributes which can be attached to the various class file structures. See 4.6. Methods for the method_info structure within a class file. See also 4.7.1. Defining and Naming New Attributes in the JVM spec.
Since with JDK 8, the class file version will be incremented to 52, it would also be possible to change the file format itself to support this feature.
See also JEP 118: Access to Parameter Names at Runtime for more information and implementation alternatives. The proposed implementation model is to add an optional attribute which stores the parameter names. Since the class file format already supports these optional attributes, this would even be possible in a way so that the class files can still be used by older JVMs, where they are simply ignored as demanded by the spec:
Java Virtual Machine implementations are required to silently ignore attributes they do not recognize.
Update
As suggested by #assylias, the source needs to be compiled with the javac command line option -parameters in order to add the meta data for parameter name reflection to the class file. However, this will of course only affect code compiled with this option - the code above will still print arg0, arg1 etc. since the runtime libraries are not be compiled with this flag and hence do not contain the necessary entries in the class files.

Thanks Andreas, but finally i got the complete solution from oracle Tutorials on Method Parameters
It says,
You can obtain the names of the formal parameters of any method or
constructor with the method
java.lang.reflect.Executable.getParameters. (The classes Method and
Constructor extend the class Executable and therefore inherit the
method Executable.getParameters.) However, .class files do not store
formal parameter names by default. This is because many tools that
produce and consume class files may not expect the larger static and
dynamic footprint of .class files that contain parameter names. In
particular, these tools would have to handle larger .class files, and
the Java Virtual Machine (JVM) would use more memory. In addition,
some parameter names, such as secret or password, may expose
information about security-sensitive methods.
To store formal parameter names in a particular .class file, and thus
enable the Reflection API to retrieve formal parameter names, compile
the source file with the -parameters option to the javac compiler.
How to Compile
Remember to compile the with the -parameters compiler option
Expected Output(For complete example visit the link mentioned above)
java MethodParameterSpy ExampleMethods
This command prints the following:
Number of constructors: 1
Constructor #1
public ExampleMethods()
Number of declared constructors: 1
Declared constructor #1
public ExampleMethods()
Number of methods: 4
Method #1
public boolean ExampleMethods.simpleMethod(java.lang.String,int)
Return type: boolean
Generic return type: boolean
Parameter class: class java.lang.String
Parameter name: stringParam
Modifiers: 0
Is implicit?: false
Is name present?: true
Is synthetic?: false
Parameter class: int
Parameter name: intParam
Modifiers: 0
Is implicit?: false
Is name present?: true
Is synthetic?: false
Method #2
public int ExampleMethods.varArgsMethod(java.lang.String...)
Return type: int
Generic return type: int
Parameter class: class [Ljava.lang.String;
Parameter name: manyStrings
Modifiers: 0
Is implicit?: false
Is name present?: true
Is synthetic?: false
Method #3
public boolean ExampleMethods.methodWithList(java.util.List<java.lang.String>)
Return type: boolean
Generic return type: boolean
Parameter class: interface java.util.List
Parameter name: listParam
Modifiers: 0
Is implicit?: false
Is name present?: true
Is synthetic?: false
Method #4
public <T> void ExampleMethods.genericMethod(T[],java.util.Collection<T>)
Return type: void
Generic return type: void
Parameter class: class [Ljava.lang.Object;
Parameter name: a
Modifiers: 0
Is implicit?: false
Is name present?: true
Is synthetic?: false
Parameter class: interface java.util.Collection
Parameter name: c
Modifiers: 0
Is implicit?: false
Is name present?: true
Is synthetic?: false

as per Store information about method parameters (usable via reflection) in intellij 13, the equivalent of "javac -parameters" within the Eclipse IDE is 'Store information about method parameters (usable via reflection)' in Window -> Preferences -> Java -> Compiler.

You can use Paranamer lib (https://github.com/paul-hammant/paranamer)
Sample code that works for me:
import com.thoughtworks.paranamer.AnnotationParanamer;
import com.thoughtworks.paranamer.BytecodeReadingParanamer;
import com.thoughtworks.paranamer.CachingParanamer;
import com.thoughtworks.paranamer.Paranamer;
Paranamer info = new CachingParanamer(new AnnotationParanamer(new BytecodeReadingParanamer()));
Method method = Foo.class.getMethod(...);
String[] parameterNames = info.lookupParameterNames(method);
If you use Maven then put this dependency in your pom.xml:
<dependency>
<groupId>com.thoughtworks.paranamer</groupId>
<artifactId>paranamer</artifactId>
<version>2.8</version>
</dependency>

Related

How to visit annotation inside method in ASM

guys, I can not visit local variable annotation use asm MethodVisitor, I do not know how to do, please help me, I want get the value "in method", but the MethodVisitor visitLocalVariableAnnotation() method does not called by the ASM logic, here is my code:
#Retention(RetentionPolicy.CLASS)
#Target({ElementType.LOCAL_VARIABLE})
public #interface SendEvent {
String value() default "hello every";
}
public class AnnotationTest {
public void test() {
#SendEvent(value = "in method")
EventBase base = new EventBase();
}
}
public class AsmMethodVisitor extends MethodVisitor implements Opcodes {
public AsmMethodVisitor(MethodVisitor methodVisitor) {
super(ASM7, methodVisitor);
System.out.println("== AsmMethodVisitor");
}
#Override
public AnnotationVisitor visitLocalVariableAnnotation(int typeRef, TypePath typePath, Label[] start, Label[] end, int[] index, String descriptor, boolean visible) {
System.out.println("== visitLocalVariableAnnotation");
return super.visitLocalVariableAnnotation(typeRef, typePath, start, end, index, descriptor, visible);
}
}
This is expected, as per JLS 9.6.4.2 this information is simply not retained in the class file:
9.6.4.2. #Retention
Annotations may be present only in source code, or they may be present in the binary form of a class or interface. An annotation that is present in the binary form may or may not be available at run time via the reflection libraries of the Java SE Platform. The annotation type java.lang.annotation.Retention is used to choose among these possibilities.
If an annotation a corresponds to a type T, and T has a (meta-)annotation m that corresponds to java.lang.annotation.Retention, then:
If m has an element whose value is java.lang.annotation.RetentionPolicy.SOURCE, then a Java compiler must ensure that a is not present in the binary representation of the class or interface in which a appears.
If m has an element whose value is java.lang.annotation.RetentionPolicy.CLASS or java.lang.annotation.RetentionPolicy.RUNTIME, then a Java compiler must ensure that a is represented in the binary representation of the class or interface in which a appears, unless a annotates a local variable declaration or a annotates a formal parameter declaration of a lambda expression.
An annotation on the declaration of a local variable, or on the declaration of a formal parameter of a lambda expression, is never retained in the binary representation. In contrast, an annotation on the type of a local variable, or on the type of a formal parameter of a lambda expression, is retained in the binary representation if the annotation type specifies a suitable retention policy.
Note that it is not illegal for an annotation type to be meta-annotated with #Target(java.lang.annotation.ElementType.LOCAL_VARIABLE) and #Retention(java.lang.annotation.RetentionPolicy.CLASS) or #Retention(java.lang.annotation.RetentionPolicy.RUNTIME).
If m has an element whose value is java.lang.annotation.RetentionPolicy.RUNTIME, the reflection libraries of the Java SE Platform must make a available at run time.
If T does not have a (meta-)annotation m that corresponds to java.lang.annotation.Retention, then a Java compiler must treat T as if it does have such a meta-annotation m with an element whose value is java.lang.annotation.RetentionPolicy.CLASS.
(emphasis mine)
ASM can simply not emit events for information that is not present.

Accessing a Scala object from Java code

I am trying to use a Scala class that has a default argument:
object SimpleCredStashClient {
def apply(kms: AWSKMSClient, dynamo: AmazonDynamoDBClient, aes: AESEncryption = DefaultAESEncryption)
...
}
When I try to instantiate an instance of this class from Java, I get the error:
Error:(489, 43) java: cannot find symbol
symbol: method SimpleCredStashClient(com.amazonaws.services.kms.AWSKMSClient,com.amazonaws.services.dynamodbv2.AmazonDynamoDBClient)
location: class com.engineersgate.build.util.CredentialsUtil
DefaultAESEncryption is a Scala object. How do I access the Scala object in Java?
Default arguments become synthetic methods of the form <meth>$default$<idx>(). Further, instances of an object A may be found at A$.MODULE$ (if A is a top-level object), or at outer.A() (if A is defined as something like class O { object A }).Therefore, there are two ways to do this:
Direct usage of object:
SimpleCredStashClient.apply(
kms,
dynamo,
DefaultAESEncryption$.MODULE$
);
Default argument:
SimpleCredStashClient.apply(
kms,
dynamo,
SimpleCredStashClient.apply$default$3()
);
The first one certainly looks better, but if the default argument ever changes, you'll have to update this code too. In the second one, the argument is whatever the default argument is, and will only break if the argument stops having a default, or changes its index. Scala uses the second method when compiled.

How to prune variations of same method obtained by use of Java reflection?

I'm using reflection discover a method satisfying some conditions and to invoke the found method.
Check following code. Using Groovy..
class TestClass<T>{
T hello(){
return null
}
}
class TestSubClass extends TestClass<List<String>>{
List<String> hello(){
return null
}
}
TestSubClass.methods.each{
if(it.name.contains("hello")){
println it.toGenericString()
}
}
which prints out
public java.util.List<java.lang.String> TestSubClass.hello() // <-- most relevant method for a user of this class
public java.lang.Object TestSubClass.hello()
public java.lang.Object TestSubClass.super$2$hello()
Java reflection is returning multiple declarations of same method based on inheritance/generics, which is understandable.
In my case, I'd like to discover the method with most appropriate signature, including exact type of returnTypes. For example, in the above example, the 1st method in the output has full signature and that's the one we'd usually invoke (without reflection).
Note: above is a simplified example. The real logic is not about finding methods based on naming.
The compiler generates the other 2 methods. Luckily, there is a property that you can check to see this: synthetic:
TestSubClass.declaredMethods.each{
if(it.name.contains("hello") && !it.synthetic) {
println it.toGenericString()
}
}
Which now prints just:
public java.util.List<java.lang.String> test.TestSubClass.hello()
The Java specifications require a method to marked synthetic if it is not explicitly in the source code.
A construct emitted by a Java compiler must be marked as synthetic if
it does not correspond to a construct declared explicitly or
implicitly in source code, unless the emitted construct is a class
initialization method (JVMS ยง2.9).
JAVA specifications
You can try:
TestSubClass.methods.each{
if(it.name.contains("hello") && !m.isSynthetic()){
println it
}
}
You can also check against if the method is bridged. Which is a similar concept:
https://stackoverflow.com/a/5007394/1754020
In my case, I'd like to discover the method with most appropriate
signature, including exact type of return Types.
If it's the Java API that you're wondering about, then you'll want to look at class Class. It contains a large number of reflective methods that allow you to interrogate types.
For example, the following code fragment searches all the methods declared on a supplied type for one method which: takes no arguments, is public and static, and has a return type of DateSerial.Config...
public static <D extends DateSerial<?>> DateSerial.Config<D> obtainMetadata(Class<D> cls) {
Method exe = Stream.of(cls.getDeclaredMethods())
.filter(m -> m.getParameterCount() == 0 &&
m.getReturnType() == DateSerial.Config.class)
.filter(m -> {
int mod = m.getModifiers();
return Modifier.isStatic(mod) && Modifier.isPublic(mod);
})
.findFirst()
.orElseThrow(() -> new IllegalArgumentException(
"No metadata accessor for " + cls.getName()));
:
:
}
You can get as precise with your interrogations as you need. For example, you can filter methods based on those with a certain number of arguments, the last of which is a String[] array, etc. etc. Caveat emptor: Java reflective code is verbose, ugly, and can be hard to read.

Ambiguous constructor due to type erasure

I have a fragment of legacy source code which looks like this:
import javax.management.MBeanParameterInfo;
import javax.management.openmbean.OpenMBeanParameterInfoSupport;
import javax.management.openmbean.OpenType;
class C {
void f() {
final MBeanParameterInfo parameter = ...;
final OpenType openType = ...;
new OpenMBeanParameterInfoSupport("...", "...", openType, parameter.getDescriptor());
}
}
The OpenMBeanParameterInfoSupport constructor used in the code was introduced in 1.6. Whenever the code is compiled with any 1.6+ javac, I receive the following error message:
reference to OpenMBeanParameterInfoSupport is ambiguous, both method OpenMBeanParameterInfoSupport(java.lang.String,java.lang.String,javax.management.openmbean.OpenType<?>,javax.management.Descriptor) in javax.management.openmbean.OpenMBeanParameterInfoSupport and method <T>OpenMBeanParameterInfoSupport(java.lang.String,java.lang.String,javax.management.openmbean.OpenType<T>,T) in javax.management.openmbean.OpenMBeanParameterInfoSupport match
new OpenMBeanParameterInfoSupport("...", "...", openType, parameter.getDescriptor());
^
2 questions:
I understand using raw types is a malpractice (openType should be declared as OpenType<?>, not OpenType), but how comes the ctor signatures are ambiguous? In the first case, the signature erasure is OpenMBeanParameterInfoSupport(String, String, OpenType, Descriptor), and in the second one OpenMBeanParameterInfoSupport(String, String, OpenType, Object), so javac should just pick the signature with the most specific type (i. e. Descriptor), shouldn't it?
My colleagues claim they can successfully build the project with any 1.7 JDK, specifying -source 1.6 -target 1.6, while I'm the only one facing the compiler error. Is there any way to compile the code w/o changing it? The only workaround I sound is setting source level to 1.4, which is definitely not what our build server uses.
both method
OpenMBeanParameterInfoSupport(
java.lang.String,
java.lang.String,
javax.management.openmbean.OpenType<?>,
javax.management.Descriptor)
in javax.management.openmbean.OpenMBeanParameterInfoSupport
and method
<T> OpenMBeanParameterInfoSupport(
java.lang.String,
java.lang.String,
javax.management.openmbean.OpenType<T>,
T)
in javax.management.openmbean.OpenMBeanParameterInfoSupport
match.
Indeed they do, the second method can be auto-resolved as <javax.management.Descriptor> while the first also matches because <?> is basically ? extends Object which could be anything including javax.management.Descriptor.
You would need to somehow change the signature so that they can't match when you call them. Even just switching the order on one of the parameters (the String with the OpenType for example) would fix the error.

Spring AOP: get access to argument names

I'm using Spring 3.x, Java 6.
I have an #Around aspect with the following joinpoint:
#Around("execution(public * my.service.*.*Connector.*(..))")
So, I'm basically interested in intercepting all calls to public methods of classes with the class name ending with "Connector". So far so good.
Now, in my aspect I would like to access the actual argument names of the methods:
public doStuff(String myarg, Long anotherArg)
myarg and anotherArg
I understand that using:
CodeSignature signature = (CodeSignature)jointPoint.getSignature();
return signature.getParameterNames();
will actually work but only if I compile the code with the "-g" flag (full debug) and I would rather not do it.
Is there any other way to get access to that kind of runtime information.
Thanks
L
Unfortunately you can't do this :-(. It is a well known limitation of JVM/bytecode - argument names can't be obtained using reflection, as they are not always stored in bytecode (in the contrary to method/class names).
As a workaround several frameworks/specification introduce custom annotations over arguments like WebParam (name property) or PathParam.
For the time being all you can get without annotations is an array of values.
Check the implementations of org.springframework.core.ParameterNameDiscoverer.
Annotations like #RequestParam used by spring inspect the parameter name if no value is set. So #RequestParam String foo will in fact fetch the request parameter named "foo". It uses the ParameterNameDiscoverer mechanism. I'm just not sure which of the implementations are used, by try each of them.
The LocalVariableTableParameterNameDiscoverer reads the .class and uses asm to inspect the names.
So, it is possible. But make sure to cache this information (for example - store a parameter name in a map, with key = class+method+parameter index).
But, as it is noted in the docs, you need the debug information. From the docs of #PathVariable:
The matching of method parameter names to URI Template variable names can only be done if your code is compiled with debugging enabled. If you do not have debugging enabled, you must specify the name of the URI Template variable name in the #PathVariable annotation in order to bind the resolved value of the variable name to a method parameter
So, if you really don't want to include that information, Tomasz Nurkiewicz's answer explains the workaround.
In Java 8 there is a new compiler flag that allows additional metadata to be stored with byte code and these parameter names can be extracted using the Parameter object in reflection. See JDK 8 spec. In newer versions of hibernate org.springframework.core.ParameterNameDiscoverer uses this feature. To use it compile using javac with this flag:
-parameters Generate metadata for reflection on method parameters
Access parameters using reflection's Parameter class.
I am not sure if its a best way, but I added a Annotation on my method:
My Annotation:
#Retention (RetentionPolicy.RUNTIME)
#Target (ElementType.METHOD)
public #interface ReadApi
{
String[] paramNames() default "";
}
#ReadApi (paramNames={"name","id","phone"})
public Address findCustomerInfo(String name, String id, String phone)
{ ..... }
And in the Aspect:
#Around("aspect param && #annotation(readApi)")
public Object logParams(ProceedingJoinPoint pjp,
ReadApi readApi)
{
//use pjp.getArgs() and readApi.paramNames();
}
This is probably a hack but i did not want to compile with more options to get information. Anyways, its working well for me. Only downside is that i need to keep the names in annotation and method in sync.

Categories

Resources