通过反射,动态修改注解的某个属性值

/

2019-11-12

众所周知,java/lang/reflect 这个包下面都是Java的反射类和工具。

Annotation 注解,也是位于这个包里的。注解自从Java 5.0版本引入后,就成为了Java平台中非常重要的一部分,常见的如 @Override@Deprecated

关于注解更详细的信息和使用方法,网上已经有很多资料,这里就不再赘述了。

一个注解通过 @Retention 指定其生命周期,本文所讨论的动态修改注解属性值,建立在 @Retention(RetentionPolicy.RUNTIM) 这种情况。毕竟这种注解才能在运行时(runtime)通过反射机制进行操作。

那么现在我们定义一个 @Foo 注解,它有一个类型为 String 的 value 属性,该注解应用再Field上:

  1. @Target(ElementType.FIELD)
  2. @Retention(RetentionPolicy.RUNTIME)
  3. public @interface Foo {
  4. String value();
  5. }

再定义一个普通的Java对象 Bar,它有一个私有的String属性 val,并为它设置属性值为”fff” 的 @Foo 注解:

  1. public class Bar {
  2. @Foo ("fff")
  3. private String val;
  4. }

接下来在 main 方法中我们来尝试修改 Bar.val 上的 @Foo注解的属性值为 “ddd”。

先是正常的获取注解属性值:

  1. public class Main {
  2. public static void main(String ...args) throws NoSuchFieldException {
  3. //获取Bar实例
  4. Bar bar = new Bar();
  5. //获取Bar的val字段
  6. Field field = Bar.class.getDeclaredField("val");
  7. //获取val字段上的Foo注解实例
  8. Foo foo = field.getAnnotation(Foo.class);
  9. //获取Foo注解实例的 value 属性值
  10. String value = foo.value();
  11. //打印该值
  12. System.out.println(value); // fff
  13. }
  14. }

首先,我们要知道注解的值是存在哪里的。

在 String value = foo.value(); 处下断点,我们跑一下可以发现:

当前栈中有这么几个变量,不过其中有一点很特别:foo,其实是个Proxy实例。

Proxy也是 java/lang/reflect下的东西,它的作用是为一个Java类生成一个代理,就像这样:

  1. public interface A {
  2. String func1();
  3. }
  4. public class B implements A {
  5. @Override
  6. public String func1() { //do something ... }
  7. public String func2() { //do something ... };
  8. }
  9. public static void main(String ...args) {
  10. B bInstance = new B();
  11. B bProxy = Proxy.newProxyInstance(
  12. B.class.getClassLoader(), // B 类的类加载器
  13. B.class.getInterfaces(), // B 类所实现的接口,如果你想拦截B类的某个方法,必须让这个方法在某个接口中声明并让B类实现该接口
  14. new InvocationHandler() { // 调用处理器,任何对 B类所实现的接口方法的调用都会触发此处理器
  15. @Override
  16. public Object invoke (Object proxy, // 这个是代理的实例,method.invoke时不能使用这个,否则会死循环
  17. Method method, // 触发的接口方法
  18. Object[] args // 此次调用该方法的参数
  19. ) throws Throwable {
  20. System.out.println(String.format("调用 %s 之前", method.getName()));
  21. /**
  22. * 这里必须使用B类的某个具体实现类的实例,因为触发时这里的method只是一个接口方法的引用,
  23. * 也就是说它是空的,你需要为它指定具有逻辑的上下文(bInstance)。
  24. */
  25. Object obj = method.invoke(bInstance, args);
  26. System.out.println(String.format("调用 %s 之后", method.getName()));
  27. return obj; //返回调用结果
  28. }
  29. }
  30. );
  31. }

这样你就可以拦截这个Java类的某个方法调用,但是你只能拦截到 func1的调用,想想为什么?

那么注意了:

ClassLoader 这是个class就会有,注解也不例外。那么注解和interfaces有什么关系?

注解本质上就是一个接口,它的实质定义为: interface SomeAnnotation extends Annotation。
这个 Annotation 接口位于 java/lang/annotation 包,它的注释中第一句话就是 The common interface extended by all annotation types.

如此说来,Foo 注解本身只是个接口,这就意味着它没有任何代码逻辑,那么它的 value 属性究竟是存在哪里的呢?

展开 foo 可以发现:

这个 Proxy 实例持有一个 AnnotationInvocationHandler,还记得之前提到过如何创建一个 Proxy 实例么? 第三个参数就是一个 InvocationHandler。
看名字这个handler即是Annotation所特有的,我们看一下它的代码:

  1. class AnnotationInvocationHandler implements InvocationHandler, Serializable {
  2. private final Class<? extends Annotation> type;
  3. private final Map<String, Object> memberValues;
  4. private transient volatile Method[] memberMethods = null;
  5. /* 后续无关代码就省略了,想看的话可以查看 sun/reflect/annotation/AnnotationInvocationHandler */
  6. }

我们一眼就可以看到一个有意思的名字: memberValues,这是一个Map,而断点中可以看到这是一个 LinknedHashMap,key为注解的属性名称,value即为注解的属性值。

现在我们找到了注解的属性值存在哪里了,那么接下来的事就好办了:

  1. public class Main {
  2. public static void main(String ...args) throws NoSuchFieldException, IllegalAccessException {
  3. //获取Bar实例
  4. Bar bar = new Bar();
  5. //获取Bar的val字段
  6. Field field = Bar.class.getDeclaredField("val");
  7. //获取val字段上的Foo注解实例
  8. Foo foo = field.getAnnotation(Foo.class);
  9. //获取 foo 这个代理实例所持有的 InvocationHandler
  10. InvocationHandler h = Proxy.getInvocationHandler(foo);
  11. // 获取 AnnotationInvocationHandler 的 memberValues 字段
  12. Field hField = h.getClass().getDeclaredField("memberValues");
  13. // 因为这个字段事 private final 修饰,所以要打开权限
  14. hField.setAccessible(true);
  15. // 获取 memberValues
  16. Map memberValues = (Map) hField.get(h);
  17. // 修改 value 属性值
  18. memberValues.put("value", "ddd");
  19. // 获取 foo 的 value 属性值
  20. String value = foo.value();
  21. System.out.println(value); // ddd
  22. }

Reproduced please indicate the author and the source, and error a link to this page.
text link: /26.html

君子坐而论道,少年起而行之!
2019/7/29上线