[Java] dynamically modify a property value of annotation through reflection

I saw one last night problem , the main idea is that the landlord wants to dynamically create multiple Spring scheduled tasks.

I'm not very familiar with this topic, but according to the topic description and related Spring creation timing task materials, I found that this may involve dynamic modification of annotation attribute values through Java code.

Today, I've tried this for a while and found that it is possible to dynamically modify the attribute values of annotations through reflection:

As we all know, the java/lang/reflect package is full of java reflection classes and tools.

Annotation annotation is also located in this package. Since Java 5.0 was introduced, annotations have become a very important part of the Java platform @Override@Deprecated.

There are a lot of materials on the Internet for more detailed information and usage of annotations, so we will not go into details here.

An annotation specifies its lifecycle through @ Retention. The dynamic modification of annotation attribute values discussed in this article is based on @ Retention (Retention policy. runtime). After all, this annotation can only operate at runtime through reflection mechanism.

Now let's define a @Foo Annotation, which has a value attribute of type String, is applied to the Field:


/**
 * Created by krun on 2017/9/18.
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Foo {
    String value();
}

Define a normal Java object Bar, which has a private String property val, and set the @ Foo annotation with the property value "fff" for it:


public class Bar {

    @Foo ("fff")
    private String val;
}

Next, in the main method, we try to change the attribute value of @ Foo annotation on Bar.val to "ddd".

First, get the annotation attribute value normally:


/**
 * Created by krun on 2017/9/18.
 */
public class Main {
    public static void main(String ...args) throws NoSuchFieldException {
        //Get Bar instance
        Bar bar = new Bar();
        //Get val field of Bar
        Field field = Bar.class.getDeclaredField("val");
        //Get Foo annotation instance on val field
        Foo foo = field.getAnnotation(Foo.class);
        //Get value attribute value of Foo annotation instance
        String value = foo.value();
        //Print this value
        System.out.println(value); // fff
    }
}

First, we need to know where the value of the annotation exists.

At the breakpoint of String value = foo.value(); let's run and find:

There are several variables in the current stack, but one of them is very special: foo is actually a Proxy instance.

Proxy is also under java/lang/reflect. Its function is to generate a proxy for a Java class, like this:


public interface A {
    String func1();
}

public class B implements A {
    
    @Override
    public String func1() { //do something ... }
    
    public String func2() { //do something ... };
}

public static void main(String ...args) {
    B bInstance = new B();
    
    B bProxy = Proxy.newProxyInstance(
        B.class.getClassLoader(),    // Class loader of class B
        B.class.getInterfaces(), // If you want to intercept a method of class B, you must declare the method in an interface and let class B implement the interface
        new InvocationHandler() { // Call handler, any call to the interface method implemented by class B will trigger this handler
            @Override
            public Object invoke (Object proxy, // This is an instance of the agent. This cannot be used when method.invoke is used, or it will loop
                                  Method method, // Triggered interface method
                                  Object[] args // The parameters of this call
                                  ) throws Throwable {
                System.out.println(String.format("call %s before", method.getName()));
                /**
                 * The instance of a specific implementation class of class B must be used here, because the method here is only a reference to an interface method when it is triggered,
                 * That is to say, it is empty. You need to specify a logical context (bInstance) for it.
                 */
                Object obj = method.invoke(bInstance, args); 
                System.out.println(String.format("call %s after", method.getName()));
                return obj; //Return call result
            }
        }
    );
}

In this way, you can block a method call of this Java class, but you can only block the call of func1. Think about why?

So notice:

ClassLoader this is a class, and annotations are no exception. So what does annotation have to do with interfaces?

Annotation is essentially an interface. Its essence is defined as interface someannotation extensions annotation.
The Annotation interface is located in the java/lang/annotation package. The first sentence in its Annotation is The common interface extended by all annotation types

So Foo annotation itself is just an interface, which means that it has no code logic, so where does its value attribute exist?

Expand foo to find:

This Proxy instance holds an AnnotationInvocationHandler. Remember how to create a Proxy instance? The third parameter is an InvocationHandler.
The name handler is unique to Annotation. Let's look at its code:


class AnnotationInvocationHandler implements InvocationHandler, Serializable {

    private final Class<? extends Annotation> type;
    private final Map<String, Object> memberValues;
    private transient volatile Method[] memberMethods = null;
    
    /* The subsequent irrelevant code is omitted. If you want to see it, you can see sun/reflect/annotation/AnnotationInvocationHandler */
   
}

At a glance, we can see an interesting name: memberValues. This is a Map. In the breakpoint, we can see that this is a LinknedHashMap. key is the attribute name of the annotation. Value is the attribute value of the annotation.

Now that we find out where the attribute value of the annotation exists, the next thing is easy:

 
/**
 * Created by krun on 2017/9/18.
 */
public class Main {
    public static void main(String ...args) throws NoSuchFieldException, IllegalAccessException {
        //Get Bar instance
        Bar bar = new Bar();
        //Get val field of Bar
        Field field = Bar.class.getDeclaredField("val");
        //Get Foo annotation instance on val field
        Foo foo = field.getAnnotation(Foo.class);
        //Get the InvocationHandler held by the agent instance foo
        InvocationHandler h = Proxy.getInvocationHandler(foo);
        // Get the memberValues field of AnnotationInvocationHandler
        Field hField = h.getClass().getDeclaredField("memberValues");
        // Because this field is modified by private final, you need to open the permission
        hField.setAccessible(true);
        // Get memberValues
        Map memberValues = (Map) hField.get(h);
        // Modify the value attribute value
        memberValues.put("value", "ddd");
        // Get the value attribute value of foo
        String value = foo.value();
        System.out.println(value); // ddd
    }
}

Tags: Programming Attribute Java Spring

Posted on Sat, 21 Mar 2020 07:07:33 -0400 by krysco