Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
2.8k views
in Technique[技术] by (71.8m points)

Java 修改注解属性的工具类?

问题描述

想在Java运行期,基于MySQL来动态修改@注解上的属性值,想知道有没有好用的工具类?


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

起初一看,我印象中是不行的,因为我一直以为XXX.class.getAnnotation(XXXAnnotation.class)这个方法是一个native方法

直到刚自己去看了一眼,哎呀,不是native方法啊
image.png

好家伙,源来氏Class类中有一个私有内部静态类AnnotationData负责存储Class的注解里的值(前提你的注解是RetentionPolicy.RUNTIME的),当然存法也很简单就是用一个map存储了注解类型和注解实例
image.png
image.png

所以看起来直接去修改对应ClassAnnotationData属性中的annotations map里的注解实例就可以了

但是其实不然,因为我们反射是拿不到反射相关类的属性的,例如ClassField这些,不然那可不乱了套了(相当于搁这搁这这种套娃操作了...),因此呢只能再换个思路

我们可以注意到,虽然注解实例缓存在Class中,但是它确实可以通过getAnnotation获取到对应的实例,而这个实例恰好是一个代理对象,其实也就是用咱们jdk动态代理做的,只用debug一看就很清楚了
image.png

既然是代理对象,那肯定有对应的InvocationHandler啊,既然代理对象能够实现原注解的所有功能,那意味着其对应的InvocationHandler肯定也包含了该注解的所有功能。

果不其然,注解对应InvocationHandlerAnnotationInvocationHandler,这个类就很憨厚了,其中的memberValues属性就是我们需要的,怎么证实呢?
image.png

比如我们有个注解TestAnnotation

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface TestAnnotation {

    String value();
}

注解到一个叫Test的类上,value值就写成haha
image.png

我们直接通过getAnnotation拿到对应的注解,再用Proxy.getInvocationHandler拿到对应的InvocationHandler

TestAnnotation testAnnotation = Test.class.getDeclaredAnnotation(TestAnnotation.class);
InvocationHandler invocationHandler = Proxy.getInvocationHandler(testAnnotation);

这个debug一看就很明显啦

image.png

memberValues中就藏着我们的目标,所以我们可以直接通过反射修改它就可以了,比如这样

TestAnnotation annotation = Test.class.getAnnotation(TestAnnotation.class);
System.out.println("before value: " + annotation.value());

InvocationHandler invocationHandler = Proxy.getInvocationHandler(annotation);
Field value = invocationHandler.getClass().getDeclaredField("memberValues");
value.setAccessible(true);
Map<String, Object> memberValues = (Map<String, Object>) value.get(invocationHandler);
memberValues.put("value", "i am new value");

annotation = Test.class.getAnnotation(TestAnnotation.class);
System.out.println("after value: " + annotation.value());

最终打印效果:
image.png

但是这样有个问题,什么呢?就是注解不仅仅是可以注解在类上的,也可以注解在字段上,方法上,所以我后面看了一下字段和方法上是怎么实现这个getAnnotation,那肯定就不是用AnnotationData啦,毕竟人家是Class的内部类

Field呢是内部有个私有declaredAnnotations属性,是Map<Class<? extends Annotation>, Annotation>的类型,那这个就和AnnotationData里的annotations类似啦
image.png

Method是其父类Executable有个declaredAnnotations属性,还是Map<Class<? extends Annotation>, Annotation>类型

image.png

那说明无论是怎么获取注解,都是从某个地方的Map<Class<? extends Annotation>, Annotation>缓存中获取。那怎么把不同地方,例如类,方法,字段聚合在一个工具类中呢?

铛铛铛!恰好有个顶层的接口把它们聚在了一起AnnotatedElement。因此工具方法应运而生(其实他们的getAnnotation方法就是来自AnnotatedElement接口的方法)

public static void modify(AnnotatedElement element, 
                          Class<? extends Annotation> annotationClass, 
                          String key, Object value) throws NoSuchFieldException, IllegalAccessException {
     Annotation annotationToBeModified = element.getAnnotation(annotationClass);
     if (annotationToBeModified == null) return;

     InvocationHandler invocationHandler = Proxy.getInvocationHandler(annotationToBeModified);
     Field memberValuesField = invocationHandler.getClass().getDeclaredField("memberValues");
     memberValuesField.setAccessible(true);
     Map<String, Object> memberValues = (Map<String, Object>) memberValuesField.get(invocationHandler);
     memberValues.put(key, value);
}

参数也很好理解,AnnotatedElement就是被注解的地方啦,如果呢你是注解的类,就传Class,注解的方法,就传某个Method,注解的是字段,就传Field,就算注解的参数,那就传某个Parameter,总之理论上啥都够啦

最后题主提到的MySQL那就不是大问题啦,毕竟它只是一个数据库而已,如果真有某个工具可以实现题主的需求,那提供这个工具的人也太嘞了。。。竟然必须要MySQL,所以啊,这个问题最大的点应该还是如何实现运行时修改注解里的值,不过最后实现下来看起来也没多少代码,只是一个小工具,可以放到自己项目中进行一些组装成为其他业务组件的一个部分吧。

那就酱。。。o( ̄▽ ̄)d


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...