Java通过反射,如何动态修改注解的某个属性值
Java反射动态修改注解的某个属性值
昨晚看到⼀条问题,⼤意是楼主希望可以动态得建⽴多个Spring 的定时任务。
这个题⽬我并不是很熟悉,不过根据题⽬描述和查阅相关Spring 创建定时任务的资料,发现这也许涉及到通过Java代码动态修改注解的属性值。
今天对此尝试了⼀番,
发现通过反射来动态修改注解的属性值是可以做到的:
众所周知,java/lang/reflect这个包下⾯都是Java的反射类和⼯具。
Annotation注解,也是位于这个包⾥的。注解⾃从Java 5.0版本引⼊后,就成为了Java平台中⾮常重要的⼀部分,常见的如@Override、@Deprecated。
关于注解更详细的信息和使⽤⽅法,⽹上已经有很多资料,这⾥就不再赘述了。
⼀个注解通过@Retention指定其⽣命周期,本⽂所讨论的动态修改注解属性值,建⽴在@Retention(Ret
entionPolicy.RUNTIM)这种情况。毕竟这种注解才能在运⾏时(runtime)通过反射机制进⾏操作。那么现在我们定义⼀个@Foo注解,它有⼀个类型为String的value属性,该注解应⽤再Field上:
/**
* Created by krun on 2017/9/18.
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Foo {
String value();
}
再定义⼀个普通的Java对象Bar,它有⼀个私有的String属性val,并为它设置属性值为"fff"的@Foo注解:
public class Bar {
hanp@Foo ("fff")
private String val;
}
接下来在main⽅法中我们来尝试修改Bar.val上的@Foo注解的属性值为"ddd"。
先是正常的获取注解属性值:
/**
* 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 = DeclaredField("val");
//获取val字段上的Foo注解实例
Foo foo = Annotation(Foo.class);
//获取Foo注解实例的 value 属性值
String value = foo.value();
//打印该值
System.out.println(value); // fff
}
}
⾸先,我们要知道注解的值是存在哪⾥的。
在String value = foo.value();处下断点,我们跑⼀下可以发现:
当前栈中有这么⼏个变量,不过其中有⼀点很特别:foo,其实是个Proxy实例。
Proxy也是java/lang/reflect下的东西,它的作⽤是为⼀个Java类⽣成⼀个代理,就像这样:
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 = wProxyInstance(
ClassLoader(), // B 类的类加载器
Interfaces(), // 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 之前", Name()));
/**
* 这⾥必须使⽤B类的某个具体实现类的实例,因为触发时这⾥的method只是⼀个接⼝⽅法的引⽤,
* 也就是说它是空的,你需要为它指定具有逻辑的上下⽂(bInstance)。
*/
Object obj = method.invoke(bInstance, args);
System.out.println(String.format("调⽤ %s 之后", Name()));
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可以发现:
这个Proxy实例持有⼀个AnnotationInvocationHandler,还记得之前提到过如何创建⼀个Proxy实例么? 第三个参数就是⼀个InvocationHandler。
看名字这个handler即是Annotation所特有的,我们看⼀下它的代码:
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即为注解的属性值。
现在我们找到了注解的属性值存在哪⾥了,那么接下来的事就好办了:
/**
* 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 = DeclaredField("val");
//获取val字段上的Foo注解实例
Foo foo = Annotation(Foo.class);
//获取 foo 这个代理实例所持有的 InvocationHandler
InvocationHandler h = InvocationHandler(foo);
// 获取 AnnotationInvocationHandler 的 memberValues 字段
Field hField = h.getClass().getDeclaredField("memberValues");watermelon的意思
// 因为这个字段事 private final 修饰,所以要打开权限
hField.tAccessible(true);
// 获取 memberValues
Map memberValues = (Map) (h);
/
/ 修改 value 属性值
memberValues.put("value", "ddd");
// 获取 foo 的 value 属性值
String value = foo.value();
System.out.println(value); // ddd
lookover}
}
通过反射动态修改⾃定义注解属性值
java/lang/reflect 这个包下⾯都是Java的反射类和⼯具。
Annotation 注解,也是位于这个包⾥的。
注解⾃从Java 5.0版本引⼊后,就成为了Java平台中⾮常重要的⼀部分,常见的有 @Override、 @Deprecated
关于注解更详细的信息和使⽤⽅法,⽹上已经有很多资料,⾃⾏查看。
⼀个注解通过 @Retention 指定其⽣命周期,本⽂所讨论的动态修改注解属性值,建⽴在 @Retention(RetentionPolicy.RUNTIM) 这种情况。
这种注解才能在运⾏时(runtime)通过反射机制进⾏修改属性的操作。
我们先定义⼀个⾃定义注解 @TestAnno
它有⼀个类型为 String 的 name属性,该注解应⽤再Method上:
@Target({ ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Component
public @interface TestAnno {
String name() default "";
}
我⽤⾃定义注解⾸先得了解清楚注解的值存储在什么地⽅,我们可以写个main⽅法测试⼀下:
通过反射获取注解@TestAnno的值
我们定义了⼀个RetryTestService 在它的⽅法 retryTest() 上添加@TestAnno 注解,然后在main⽅法⾥⾯反射获取注解的name值
@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 rvice = new RetryTestService();
Method method = Class().getDeclaredMethod("retryTest",null);
TestAnno testAnno = DeclaredAnnotation(TestAnno.class);
System.out.println(testAnno.name());
}
}
当前栈中有这么⼏个变量,不过其中有⼀点很特别:@TestAnno,其实是个Proxy实例。
Proxy也是 java/lang/reflect下的东西,它的作⽤是为⼀个Java类⽣成⼀个代理,就像这样:
public interface A {
String func1();
}
pokerfacepublic 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 = wProxyInstance(
ClassLoader(), // B 类的类加载器
Interfaces(), // B 类所实现的接⼝,如果你想拦截B类的某个⽅法,必须让这个⽅法在某个接⼝中声明并让B类实现该接⼝
new InvocationHandler() { // 调⽤处理器,任何对 B类所实现的接⼝⽅法的调⽤都会触发此处理器
@Override
public Object invoke (Object proxy, // 这个是代理的实例,method.invoke时不能使⽤这个,否则会死循环
think aboutMethod method, // 触发的接⼝⽅法
Object[] args // 此次调⽤该⽅法的参数
) throws Throwable {
System.out.println(String.format("调⽤ %s 之前", Name()));
/**
* 这⾥必须使⽤B类的某个具体实现类的实例,因为触发时这⾥的method只是⼀个接⼝⽅法的引⽤,
* 也就是说它是空的,你需要为它指定具有逻辑的上下⽂(bInstance)。
*/
Object obj = method.invoke(bInstance, args);
System.out.println(String.format("调⽤ %s 之后", Name()));
return obj; //返回调⽤结果
}
}
);
}
注意了:
ClassLoader 这是个class就会有,注解也不例外。那么注解和interfaces有什么关系?
注解本质上就是⼀个接⼝,它的实质定义为: interface SomeAnnotation extends Annotation。
这个 Annotation 接⼝位于 java/lang/annotation 包,它的注释中第⼀句话就是 The common interface extended by all annotation types.
如此说来,@TestAnno 注解本⾝只是个接⼝,这就意味着它没有任何代码逻辑,那么它的 value 属性究竟是存在哪⾥的呢?
展开 @TestAnno 可以发现:
这个 Proxy 实例持有⼀个 AnnotationInvocationHandler,还记得之前提到过如何创建⼀个 Proxy 实例么? 第三个参数就是⼀个 InvocationHandler。看名字这个handler即是Annotation所特有的,我们看⼀下它的代码:
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:
@Aspect
@Component
@Order(1) //aop执⾏顺序1表⽰先执⾏此aop
public class AuthDemoAspect implements EnvironmentAware {
Environment environment;
@Override
public void tEnvironment(Environment environment) {
}
@Pointcut("@annotation(com.fig.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 = solvePlaceholders(testAnno.name());
//获取 foo 这个代理实例所持有的 InvocationHandler
InvocationHandler h = InvocationHandler(testAnno);
// 获取 AnnotationInvocationHandler 的 memberValues 字段
Field hField = h.getClass().getDeclaredField("memberValues");
// 因为这个字段事 private final 修饰,所以要打开权限
hField.tAccessible(true);
// 获取 memberValues
Map memberValues = (Map) (h);
/
/ 修改 value 属性值
climbmemberValues.put("name",s);
return joinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
return null;
}
}
第⼀个aop⾥⾯我改变注解的name值,由上⾯rvice⽅法上注解的${nba.kobe} 改成读取配置⽂件 nba.kobe的配置值
项⽬配置⽂件:application.properties增加⼀个值
nba.kobe=科⽐
String s = solvePlaceholders(testAnno.name());
这⾏代码其实就是通过原本注解值${nba.kobe}去配置⽂件取nba.kobe 对应的值。如果你只是修改原来注解的name值⽽不是去取配置⽂件⼤可以不⽤此⾏代码,直接给memberValues ⾥⾯的name put新的值就⾏。
注意:@Order(1) 可以控制aop的执⾏顺序
然后我再写第⼆个aop,打印出注解@TestAnno 的name值看看是不是第⼀个aop已经成功把值改掉了
第⼆个aop:
@Aspect
@Component
in the flesh@Order(2)
public class AuthDemoAspectTwo implements EnvironmentAware {
Environment environment;
@Override
public void tEnvironment(Environment environment) {
bought}
@Pointcut("@annotation(com.fig.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();
}
standoffreturn null;
}
然后我们只需要启动项⽬调⽤⼀下RetryTestService的 retryTest()⽅法就可以进⼊aop 看看打印出来的结果了
通过结果我们可以发现第⼀个aop的确把retryTest()⽅法上⾯注解@TestAnno的name值由原先的 @TestAnno(name = "${nba.kobe}") ${nba.kobe}值动态修改成了配置⽂件⾥⾯配置的“科⽐”了。以上为个⼈经验,希望能给⼤家⼀个参考,也希望⼤家多多⽀持。