在运行时修改类定义的注释字符串参数

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 的)有效,并且它没有进行严重的字节码操作。

以上是关于在运行时修改类定义的注释字符串参数的主要内容,如果未能解决你的问题,请参考以下文章

查找对象的参数化注释的运行时类类型

Java运行时动态生成类几种方式

“TypeError:参数“url”必须是字符串,而不是未定义“在本地运行heroku应用程序时

如何解决:“路径”参数必须是字符串类型。运行'vue add vuetify'时收到类型未定义'

如何运行一个ruby类 中的方法

IDEA自定义类注释和方法注释(自定义groovyScript方法实现多行参数注释)