在运行时修改类定义的注释字符串参数
Posted
技术标签:
【中文标题】在运行时修改类定义的注释字符串参数【英文标题】:Modify a class definition's annotation string parameter at runtime 【发布时间】:2012-12-25 11:50:11 【问题描述】:假设有一个类:
@Something(someProperty = "some value")
public class Foobar
//...
已经编译(我无法控制源代码),并且是 jvm 启动时类路径的一部分。我希望能够在运行时将“某个值”更改为其他值,这样之后的任何反射都会有我的新值,而不是默认的“某个值”。
这可能吗?如果有,怎么做?
【问题讨论】:
Class 有一个annotations
和一个 declaredAnnotations
映射字段,您可以尝试使用反射进行修改...
grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/… 在第 3086 行附近。这非常脆弱,因为可能会有副作用,如果 Class.java 的实现发生更改,它可能会停止工作......
哦,那太酷了,所以你说的基本上只是用我想要的值换出(或预先插入)我自己的注释实例?
@assylias 由于使用了SoftReference
,即使不更改实现也很脆弱,这使得您可能在任意点丢失所有更改。
【参考方案1】:
警告:未在 OSX 上测试 - 请参阅 @Marcel 的评论
在 OSX 上测试。工作正常。
由于我还需要在运行时更改注释值,所以我重新审视了这个问题。
这是 @assylias 方法的修改版本(非常感谢您的启发)。
/**
* Changes the annotation value for the given key of the given annotation to newValue and returns
* the previous value.
*/
@SuppressWarnings("unchecked")
public static Object changeAnnotationValue(Annotation annotation, String key, Object newValue)
Object handler = Proxy.getInvocationHandler(annotation);
Field f;
try
f = handler.getClass().getDeclaredField("memberValues");
catch (NoSuchFieldException | SecurityException e)
throw new IllegalStateException(e);
f.setAccessible(true);
Map<String, Object> memberValues;
try
memberValues = (Map<String, Object>) f.get(handler);
catch (IllegalArgumentException | IllegalAccessException e)
throw new IllegalStateException(e);
Object oldValue = memberValues.get(key);
if (oldValue == null || oldValue.getClass() != newValue.getClass())
throw new IllegalArgumentException();
memberValues.put(key,newValue);
return oldValue;
使用示例:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface ClassAnnotation
String value() default "";
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface FieldAnnotation
String value() default "";
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MethodAnnotation
String value() default "";
@ClassAnnotation("class test")
public static class TestClass
@FieldAnnotation("field test")
public Object field;
@MethodAnnotation("method test")
public void method()
public static void main(String[] args) throws Exception
final ClassAnnotation classAnnotation = TestClass.class.getAnnotation(ClassAnnotation.class);
System.out.println("old ClassAnnotation = " + classAnnotation.value());
changeAnnotationValue(classAnnotation, "value", "another class annotation value");
System.out.println("modified ClassAnnotation = " + classAnnotation.value());
Field field = TestClass.class.getField("field");
final FieldAnnotation fieldAnnotation = field.getAnnotation(FieldAnnotation.class);
System.out.println("old FieldAnnotation = " + fieldAnnotation.value());
changeAnnotationValue(fieldAnnotation, "value", "another field annotation value");
System.out.println("modified FieldAnnotation = " + fieldAnnotation.value());
Method method = TestClass.class.getMethod("method");
final MethodAnnotation methodAnnotation = method.getAnnotation(MethodAnnotation.class);
System.out.println("old MethodAnnotation = " + methodAnnotation.value());
changeAnnotationValue(methodAnnotation, "value", "another method annotation value");
System.out.println("modified MethodAnnotation = " + methodAnnotation.value());
这种方法的优点是不需要创建新的注解实例。因此不需要事先知道具体的注解类。此外,由于原始注释实例保持不变,因此副作用应该是最小的。
使用 Java 8 测试。
【讨论】:
谢谢Balder,你能解释一下f = handler.getClass().getDeclaredField("memberValues");
这里的memberValues是什么吗?
"memberValues" 是 Java 注释的私有映射,它的成员值对存储在其中 - 即,注释的任何值都存储为其名称/键及其实际值对价值。上面的方法简单地使用反射访问这个字段,方法是设置它可访问,然后用给定的新值替换现有值。
有人可以告诉我如何更改字段上的注释吗?此解决方案适用于类注释,但不适用于字段:(
@Marcel:当查询一个Method
(或任何其他成员)的类时,您会得到一个副本,但如果存在缓存信息,则该副本会使用缓存信息进行初始化。但是,此缓存信息是软引用的,可以随时删除。所以这基本上是一个运气问题,后续的反射查询是否会获得被操纵的注释或是否会重建原始注释。
我试过这个解决方案。但是它改变了类的所有实例的注释值。是否可以仅针对特定的类实例进行更改?【参考方案2】:
此代码或多或少地满足您的要求 - 它是一个简单的概念证明:
正确的实现还需要处理declaredAnnotations
如果 Class.java 中注解的实现发生变化,代码将会中断(即它可以在未来的任何时间中断)
不知道有没有副作用...
输出:
oldAnnotation = 一些值 modifiedAnnotation = 另一个值
public static void main(String[] args) throws Exception
final Something oldAnnotation = (Something) Foobar.class.getAnnotations()[0];
System.out.println("oldAnnotation = " + oldAnnotation.someProperty());
Annotation newAnnotation = new Something()
@Override
public String someProperty()
return "another value";
@Override
public Class<? extends Annotation> annotationType()
return oldAnnotation.annotationType();
;
Field field = Class.class.getDeclaredField("annotations");
field.setAccessible(true);
Map<Class<? extends Annotation>, Annotation> annotations = (Map<Class<? extends Annotation>, Annotation>) field.get(Foobar.class);
annotations.put(Something.class, newAnnotation);
Something modifiedAnnotation = (Something) Foobar.class.getAnnotations()[0];
System.out.println("modifiedAnnotation = " + modifiedAnnotation.someProperty());
@Something(someProperty = "some value")
public static class Foobar
@Retention(RetentionPolicy.RUNTIME)
@interface Something
String someProperty();
【讨论】:
很抱歉从死里复活,但我有一个问题。我正在基于夹具构建器类型模式修改测试模拟中的一些注释值。有时测试无法更新注释值并以“Class cast exception”轰炸,而在一切运行正常(绿色)之后立即。有什么建议吗?我在 Robolectric-android 环境下使用 JUnit4... @OceanLife 没有看到有问题的测试很难说 - 这可能是由于与本身使用反射或更改字节码的模拟框架的交互。您可能应该发布一个包含更多详细信息的单独问题。 @assylias 感谢您的及时回复。关于影响这种方法的 CGLIB 和模拟框架存在一些共识。我得出的结论是,这种方法并不是我测试的最佳途径,但对于酷代码来说却是 +1。谢谢。 这不适用于 java 8。getDeclaredField("annotations")
似乎不再适用
这篇文章展示了如何在 Java 8 中操作注解:rationaleemotions.wordpress.com/2016/05/27/…。在页面中间寻找“public class AnnotationHelper”。【参考方案3】:
这可以在我的 Java 8 机器上运行。它将注释 @JsonIgnoreProperties(ignoreUnknown = true)
中的 ignoreUnknown
的值从 true 更改为 false。
final List<Annotation> matchedAnnotation = Arrays.stream(SomeClass.class.getAnnotations()).filter(annotation -> annotation.annotationType().equals(JsonIgnoreProperties.class)).collect(Collectors.toList());
final Annotation modifiedAnnotation = new JsonIgnoreProperties()
@Override public Class<? extends Annotation> annotationType()
return matchedAnnotation.get(0).annotationType();
@Override public String[] value()
return new String[0];
@Override public boolean ignoreUnknown()
return false;
@Override public boolean allowGetters()
return false;
@Override public boolean allowSetters()
return false;
;
final Method method = Class.class.getDeclaredMethod("getDeclaredAnnotationMap", null);
method.setAccessible(true);
final Map<Class<? extends Annotation>, Annotation> annotations = (Map<Class<? extends Annotation>, Annotation>) method.invoke(SomeClass.class, null);
annotations.put(JsonIgnoreProperties.class, modifiedAnnotation);
【讨论】:
matchedAnnotation
在哪里使用?【参考方案4】:
SPRING 可以很容易地完成这项工作,可能对 spring 开发人员有用。 请按照以下步骤操作:-
第一个解决方案:- 1) 创建一个返回 someProperty 值的 Bean。在这里,我从 DB 或属性文件中注入了带有 @Value 注释的 somePropertyValue :-
@Value("$config.somePropertyValue")
private String somePropertyValue;
@Bean
public String somePropertyValue()
return somePropertyValue;
2) 在此之后,可以像这样将 somePropertyValue 注入到 @Something 注释中:-
@Something(someProperty = "#@somePropertyValue")
public class Foobar
//...
第二种解决方案:-
1) 在 bean 中创建 getter setter :-
@Component
public class config
@Value("$config.somePropertyValue")
private String somePropertyValue;
public String getSomePropertyValue()
return somePropertyValue;
public void setSomePropertyValue(String somePropertyValue)
this.somePropertyValue = somePropertyValue;
2) 在此之后,可以像这样将 somePropertyValue 注入到 @Something 注释中:-
@Something(someProperty = "#config.somePropertyValue")
public class Foobar
//...
【讨论】:
还需要在@Componenet("...") 和@Scope 中为SCOPE_PROTOTYPE 添加bean 名称。 这有完整的例子吗?这实际上是我当前用例中所需要的。但我似乎无法让它工作。 这也可以与 JPA 注释一起使用吗?还是只是 spring 注释?【参考方案5】:试试这个 Java 8 解决方案
public static void main(String[] args) throws Exception
final Something oldAnnotation = (Something) Foobar.class.getAnnotations()[0];
System.out.println("oldAnnotation = " + oldAnnotation.someProperty());
Annotation newAnnotation = new Something()
@Override
public String someProperty()
return "another value";
@Override
public Class<? extends Annotation> annotationType()
return oldAnnotation.annotationType();
;
Method method = Class.class.getDeclaredMethod("annotationData", null);
method.setAccessible(true);
Object annotationData = method.invoke(getClass(), null);
Field declaredAnnotations = annotationData.getClass().getDeclaredField("declaredAnnotations");
declaredAnnotations.setAccessible(true);
Map<Class<? extends Annotation>, Annotation> annotations = (Map<Class<? extends Annotation>, Annotation>) declaredAnnotations.get(annotationData);
annotations.put(Something.class, newAnnotation);
Something modifiedAnnotation = (Something) Foobar.class.getAnnotations()[0];
System.out.println("modifiedAnnotation = " + modifiedAnnotation.someProperty());
@Something(someProperty = "some value")
public static class Foobar
@Retention(RetentionPolicy.RUNTIME)
@interface Something
String someProperty();
【讨论】:
java.util.AbstractMap.put 处的 java.lang.UnsupportedOperationException(未知来源)【参考方案6】:我在jdk1.8中可以通过这种方式访问和修改注释,但不知道为什么没有效果,
try
Field annotationDataField = myObject.getClass().getClass().getDeclaredField("annotationData");
annotationDataField.setAccessible(true);
Field annotationsField = annotationDataField.get(myObject.getClass()).getClass().getDeclaredField("annotations");
annotationsField.setAccessible(true);
Map<Class<? extends Annotation>, Annotation> annotations = (Map<Class<? extends Annotation>, Annotation>) annotationsField.get(annotationDataField.get(myObject.getClass()));
annotations.put(Something.class, newSomethingValue);
catch (IllegalArgumentException | IllegalAccessException e)
e.printStackTrace();
catch (NoSuchFieldException e)
e.printStackTrace();
catch (SecurityException e)
e.printStackTrace();
【讨论】:
【参考方案7】:注解属性值必须是常量——所以除非你想做一些严肃的字节码操作,否则这是不可能的。有没有更简洁的方法,比如用你想要的注解创建一个包装类?
【讨论】:
不,不幸的是它必须是这个。在运行时修改常量有多困难/疯狂? jvm 是在堆中做某种字符串实习,还是更像是内联,字面上是在字节码的程序文本部分? 上述至少一种解决方案(Balder 的)有效,并且它没有进行严重的字节码操作。以上是关于在运行时修改类定义的注释字符串参数的主要内容,如果未能解决你的问题,请参考以下文章
“TypeError:参数“url”必须是字符串,而不是未定义“在本地运行heroku应用程序时