gpt4 book ai didi

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

转载 作者:qq735679552 更新时间:2022-09-29 22:32:09 40 4
gpt4 key购买 nike

CFSDN坚持开源创造价值,我们致力于搭建一个资源共享平台,让每一个IT人在这里找到属于你的精彩世界.

这篇CFSDN的博客文章Java通过反射,如何动态修改注解的某个属性值由作者收集整理,如果你对这篇文章有兴趣,记得点赞哟.

Java反射动态修改注解的某个属性值

昨晚看到一条问题,大意是楼主希望可以动态得建立多个Spring 的定时任务.

这个题目我并不是很熟悉,不过根据题目描述和查阅相关Spring 创建定时任务的资料,发现这也许涉及到通过Java代码动态修改注解的属性值.

今天对此尝试了一番, 。

发现通过反射来动态修改注解的属性值是可以做到的:

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

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

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

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

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

?
1
2
3
4
5
6
7
8
/**
  * Created by krun on 2017/9/18.
  */
@Target (ElementType.FIELD)
@Retention (RetentionPolicy.RUNTIME)
public @interface Foo {
     String value();
}

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

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

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

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

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

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

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

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

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

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

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

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

那么注意了:

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

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

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

展开foo可以发现:

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

这个Proxy实例持有一个AnnotationInvocationHandler,还记得之前提到过如何创建一个Proxy实例么? 第三个参数就是一个InvocationHandler.

看名字这个handler即是Annotation所特有的,我们看一下它的代码:

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

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

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

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

通过反射动态修改自定义注解属性值

java/lang/reflect 这个包下面都是Java的反射类和工具.

Annotation 注解,也是位于这个包里的.

注解自从Java 5.0版本引入后,就成为了Java平台中非常重要的一部分,常见的有 @Override、 @Deprecated 。

关于注解更详细的信息和使用方法,网上已经有很多资料,自行查看.

一个注解通过 @Retention 指定其生命周期,本文所讨论的动态修改注解属性值,建立在 @Retention(RetentionPolicy.RUNTIM) 这种情况.

这种注解才能在运行时(runtime)通过反射机制进行修改属性的操作.

我们先定义一个自定义注解 @TestAnno

它有一个类型为 String 的 name属性,该注解应用再Method上

?
1
2
3
4
5
6
@Target ({ ElementType.METHOD })
@Retention (RetentionPolicy.RUNTIME)
@Component
public @interface TestAnno {
    String name() default "" ;
}

我用自定义注解首先得了解清楚注解的值存储在什么地方,我们可以写个main方法测试一下:

通过反射获取注解@TestAnno的值

我们定义了一个RetryTestService 在它的方法 retryTest() 上添加@TestAnno 注解,然后在main方法里面反射获取注解的name值 。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Service
public class RetryTestService {
     @TimeLog
     @TestAnno (name = "${nba.kobe}" )
     public String retryTest(){
         System.out.println( "---进行了接口请求...." );
         return "success" ;
     }
     public static void main(String[] args) throws NoSuchMethodException {
         RetryTestService service = new RetryTestService();
         Method method = service.getClass().getDeclaredMethod( "retryTest" , null );
         TestAnno testAnno = method.getDeclaredAnnotation(TestAnno. class );
         System.out.println(testAnno.name());
     }
}

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

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

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

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

注意了:

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

注解本质上就是一个接口,它的实质定义为: interface SomeAnnotation extends Annotation.

这个 Annotation 接口位于 java/lang/annotation 包,它的注释中第一句话就是 The common interface extended by all annotation types. 。

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

展开 @TestAnno 可以发现:

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

这个 Proxy 实例持有一个 AnnotationInvocationHandler,还记得之前提到过如何创建一个 Proxy 实例么? 第三个参数就是一个 InvocationHandler.

看名字这个handler即是Annotation所特有的,我们看一下它的代码:

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

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

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

我这里写两个aop。第一个aop拦截带@TestAnno注解的方法,然后改变注解的name值,第二个aop我们再把注解的name值打印出来,看看是不是真被改了 。

第一个aop:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
@Aspect
@Component
@Order ( 1 ) //aop执行顺序1表示先执行此aop
public class AuthDemoAspect implements EnvironmentAware {
     Environment environment;
     @Override
     public void setEnvironment(Environment environment) {
         this .environment = environment;
     }
 
     @Pointcut ( "@annotation(com.ali.hangz.tooltest.config.TestAnno)" )
     public void myPointCut() {
     }
 
     @Before (value = "myPointCut()" )
     public void check(){
     }
 
     @After (value = "myPointCut()" )
     public void bye(){
     }
  
     /**
      *配置文件配置
      * @return
      */
     @Around ( "myPointCut() && @annotation(testAnno)" )
     public Object around(ProceedingJoinPoint joinPoint, TestAnno testAnno){
         try {
             System.out.println( "---修改前注解@TestAnno的name指为:" + testAnno.name());
             String s = environment.resolvePlaceholders(testAnno.name());
             //获取 foo 这个代理实例所持有的 InvocationHandler
             InvocationHandler h = Proxy.getInvocationHandler(testAnno);
             // 获取 AnnotationInvocationHandler 的 memberValues 字段
             Field hField = h.getClass().getDeclaredField( "memberValues" );
             // 因为这个字段事 private final 修饰,所以要打开权限
             hField.setAccessible( true );
             // 获取 memberValues
             Map memberValues = (Map) hField.get(h);
             // 修改 value 属性值
             memberValues.put( "name" ,s);
             return joinPoint.proceed();
         } catch (Throwable throwable) {
             throwable.printStackTrace();
         }
         return null ;
     }
}

第一个aop里面我改变注解的name值,由上面service方法上注解的${nba.kobe} 改成读取配置文件 nba.kobe的配置值 。

项目配置文件:application.properties增加一个值 。

?
1
2
nba.kobe=科比
String s = environment.resolvePlaceholders(testAnno.name());

这行代码其实就是通过原本注解值${nba.kobe}去配置文件取nba.kobe 对应的值。如果你只是修改原来注解的name值而不是去取配置文件大可以不用此行代码,直接给memberValues 里面的name put新的值就行.

注意:@Order(1) 可以控制aop的执行顺序 。

然后我再写第二个aop,打印出注解@TestAnno 的name值看看是不是第一个aop已经成功把值改掉了 。

第二个aop:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@Aspect
@Component
@Order ( 2 )
public class AuthDemoAspectTwo implements EnvironmentAware { 
     Environment environment; 
     @Override
     public void setEnvironment(Environment environment) {
         this .environment = environment;
     }
 
     @Pointcut ( "@annotation(com.ali.hangz.tooltest.config.TestAnno)" )
     public void myPointCut() {
     }
 
     @Before (value = "myPointCut()" )
     public void check(){
     }
 
     @After (value = "myPointCut()" )
     public void bye(){
     }
 
     /**
      *配置文件配置
      * @return
      */
     @Around ( "myPointCut() && @annotation(testAnno)" )
     public Object around(ProceedingJoinPoint joinPoint, TestAnno testAnno){
         try {
             System.out.println( "---修改后的注解名称:" + testAnno.name());
             return joinPoint.proceed();
         } catch (Throwable throwable) {
             throwable.printStackTrace();
         }
         return null ;
     }

然后我们只需要启动项目调用一下RetryTestService的 retryTest()方法 就可以进入aop 看看打印出来的结果了 。

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

通过结果我们可以发现第一个aop的确把retryTest()方法上面注解@TestAnno的name值由原先的 @TestAnno(name = "${nba.kobe}") ${nba.kobe}值动态修改成了配置文件里面配置的“科比”了.

以上为个人经验,希望能给大家一个参考,也希望大家多多支持我.

原文链接:https://segmentfault.com/a/1190000011213222 。

最后此篇关于Java通过反射,如何动态修改注解的某个属性值的文章就讲到这里了,如果你想了解更多关于Java通过反射,如何动态修改注解的某个属性值的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。

40 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com