昨晚看到一条问题,大意是楼主但愿能够动态得创建多个Spring 的定时任务。java
这个题目我并非很熟悉,不过根据题目描述和查阅相关 Spring 建立定时任务 的资料,发现这也许涉及到经过Java代码动态修改注解的属性值。segmentfault
今天对此尝试了一番,发现经过反射来动态修改注解的属性值是能够作到的:ide
众所周知,java/lang/reflect
这个包下面都是Java的反射类和工具。工具
Annotation
注解,也是位于这个包里的。注解自从Java 5.0版本引入后,就成为了Java平台中很是重要的一部分,常见的如 @Override
、 @Deprecated
。spa
关于注解更详细的信息和使用方法,网上已经有不少资料,这里就再也不赘述了。代理
一个注解经过 @Retention
指定其生命周期,本文所讨论的动态修改注解属性值,创建在 @Retention(RetentionPolicy.RUNTIM)
这种状况。毕竟这种注解才能在运行时(runtime)经过反射机制进行操做。code
那么如今咱们定义一个 @Foo
注解,它有一个类型为 String
的 value
属性,该注解应用再Field
上:orm
/** * 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 { @Foo ("fff") private String val; }
接下来在 main
方法中咱们来尝试修改 Bar.val
上的 @Foo
注解的属性值为 "ddd"
。blog
先是正常的获取注解属性值:
/** * 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();
处下断点,咱们跑一下能够发现:
当前栈中有这么几个变量,不过其中有一点很特别: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 = 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
能够发现:
这个 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 = 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 } }