java personnalisé - Modifier le paramètre de chaîne d'annotation d'une définition de classe à l'exécution




@retention @deprecated (7)

Imaginez qu'il y a une classe:

@Something(someProperty = "some value")
public class Foobar {
    //...
}

Ce qui est déjà compilé (je ne peux pas contrôler la source), et fait partie du classpath quand jvm démarre. Je voudrais être en mesure de changer "une certaine valeur" à quelque chose d'autre à l'exécution, de sorte que toute réflexion par la suite aurait ma nouvelle valeur au lieu de la "valeur" par défaut.

Est-ce possible? Si c'est le cas, comment?


Answers

Ce code fait plus ou moins ce que vous demandez - c'est une simple preuve de concept:

  • une mise en œuvre correcte doit également traiter les declaredAnnotations
  • si l'implémentation des annotations dans Class.java change, le code va se casser (c'est à dire qu'il peut se casser à tout moment dans le futur)
  • Je n'ai aucune idée s'il y a des effets secondaires ...

Sortie:

oldAnnotation = une certaine valeur
modifiedAnnotation = une autre valeur

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();
}

Attention: Non testé sur OSX - voir commentaire de @Marcel

Testé sur OSX. Fonctionne bien.

Comme j'avais aussi besoin de changer les valeurs d'annotation lors de l'exécution, j'ai réexaminé cette question.

Voici une version modifiée de l'approche @assylias (merci beaucoup pour l'inspiration).

/**
 * 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;
}

Exemple d'utilisation:

@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());
}

L'avantage de cette approche est qu'il n'est pas nécessaire de créer une nouvelle instance d'annotation. Il n'est donc pas nécessaire de connaître à l'avance la classe d'annotation concrète. Les effets secondaires doivent également être minimes puisque l'instance d'annotation originale reste intacte.

Testé avec Java 8.


Celui-ci fonctionne sur ma machine avec Java 8. Il change la valeur de ignoreUnknown dans l'annotation @JsonIgnoreProperties(ignoreUnknown = true) de 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);

Essayez cette solution pour 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();
}

SPRING peut très facilement faire ce travail, ce qui peut être utile pour les développeurs de printemps. Suivez ces étapes :-

Première solution: - 1) créer un Bean retournant une valeur pour someProperty. Ici, j'ai injecté le somePropertyValue avec l'annotation @Value à partir du fichier DB ou de la propriété: -

    @Value("${config.somePropertyValue}")
    private String somePropertyValue;

    @Bean
    public String somePropertyValue(){
        return somePropertyValue;
    }

2) Après cela, il est possible d'injecter le somePropertyValue dans l'annotation @Something comme ceci: -

@Something(someProperty = "#{@somePropertyValue}")
public class Foobar {
    //...
}

Deuxième solution: -

1) créer getter setter en haricot: -

 @Component
    public class config{
         @Value("${config.somePropertyValue}")
         private String somePropertyValue;

         public String getSomePropertyValue() {
           return somePropertyValue;
         }
        public void setSomePropertyValue(String somePropertyValue) {
           this.somePropertyValue = somePropertyValue;
        }
    }

2) Après cela, il est possible d'injecter le somePropertyValue dans l'annotation @Something comme ceci: -

@Something(someProperty = "#{config.somePropertyValue}")
public class Foobar {
    //...
}

je suis capable d'accéder et de modifier des annotaions de cette manière en jdk1.8, mais je ne sais pas pourquoi cela n'a aucun effet,

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();
}

Dans une application à plusieurs niveaux, nous aurons différentes couches comme la présentation, le service, l'entreprise, l'accès aux données, etc. Lorsqu'une classe doit être annotée pour la détection automatique par Spring, nous devrions utiliser le stéréotype respectif comme ci-dessous.

  • @Component - générique et peut être utilisé à travers l'application.
  • @Service - annote les classes au niveau de la couche de service.
  • @Controller - annote les classes au niveau des couches de présentation, principalement utilisées dans Spring MVC.
  • @Repository - annote des classes à la couche de persistance, qui agira comme référentiel de base de données.




java reflection