Een instantie van een annotatie maken

Ik probeer wat Java-annotatiemagie te doen. Ik moet zeggen dat ik de annotatietrucs nog steeds aan het inhalen ben en dat bepaalde dingen me nog steeds niet helemaal duidelijk zijn.

Dus… ik heb een aantal geannoteerde klassen, methoden en velden. Ik heb een methode die reflectie gebruikt om enkele controles op de klassen uit te voeren en enkele waarden in een klasse te injecteren. Dit werkt allemaal prima.

Ik word nu echter geconfronteerd met een geval waarin ik een instantie (om zo te zeggen) van een annotatie nodig heb. Dus… annotaties zijn niet zoals gewone interfaces en je kunt geen anonieme implementatie van een klasse doen. Ik begrijp het. Ik heb hier wat berichten bekeken over soortgelijke problemen, maar ik kan het antwoord niet vinden op wat ik zoek.

Ik zou eigenlijk graag een annotatie willen krijgen en sommige velden ervan kunnen instellen met reflectie (denk ik). Is er een manier om dit te doen?


Antwoord 1, autoriteit 100%

Nou, het is blijkbaar niet zo ingewikkeld. Echt!

Zoals een collega heeft opgemerkt, kunt u eenvoudig een anonieme instantie van de annotatie (zoals elke interface) als volgt maken:

MijnAnnotatie:

public @interface MyAnnotation
{
    String foo();
}

Code oproepen:

class MyApp
{
    MyAnnotation getInstanceOfAnnotation(final String foo)
    {
        MyAnnotation annotation = new MyAnnotation()
        {
            @Override
            public String foo()
            {
                return foo;
            }
            @Override
            public Class<? extends Annotation> annotationType()
            {
                return MyAnnotation.class;
            }
        };
        return annotation;
    }
}

Credits aan Martin Grigorov.


Antwoord 2, autoriteit 24%

De proxy-aanpak, zoals voorgesteld in Gunnars antwoordis al geïmplementeerd in GeantyRef:

Map<String, Object> annotationParameters = new HashMap<>();
annotationParameters.put("name", "someName");
MyAnnotation myAnnotation = TypeFactory.annotation(MyAnnotation.class, annotationParameters);

Dit levert een annotatie op die gelijk is aan wat u zou krijgen van:

@MyAnnotation(name = "someName")

Annotatie-instanties die op deze manier worden geproduceerd, werken identiek aan degene die normaal door Java worden geproduceerd, en hun hashCodeen equalszijn correct geïmplementeerd voor compatibiliteit, dus geen bizarre voorbehouden zoals met het direct instantiëren van de annotatie zoals in het geaccepteerde antwoord. In feite gebruikt JDK intern dezelfde aanpak: sun.reflect.annotation.AnnotationParser#annotationForMap.

De bibliotheek zelf is klein en heeft geen afhankelijkheden (en is niet afhankelijk van interne JDK-API’s).

Onthulling:ik ben de ontwikkelaar achter GeantyRef.


Antwoord 3, autoriteit 10%

U kunt een annotatieproxy gebruiken, zoals dezevan het Hibernate Validator-project (disclaimer: ik ben een committer van dit project).


Antwoord 4, autoriteit 8%

U kunt sun.reflect.annotation.AnnotationParser.annotationForMap(Class, Map):

gebruiken

public @interface MyAnnotation {
    String foo();
}
public class MyApp {
    public MyAnnotation getInstanceOfAnnotation(final String foo) {
        MyAnnotation annotation = AnnotationParser.annotationForMap(
            MyAnnotation.class, Collections.singletonMap("foo", "myFooResult"));
    }
}

Nadeel: klassen van sun.*zijn onderhevig aan verandering in latere versies (hoewel deze methode bestaat sinds Java 5 met dezelfde handtekening) en zijn niet beschikbaar voor alle Java-implementaties, zie deze discussie.

Als dat een probleem is: je zou een generieke proxy kunnen maken met je eigen InvocationHandler– dit is precies wat AnnotationParserintern voor je doet. Of u gebruikt uw eigen implementatie van MyAnnotationzoals gedefinieerd hier. In beide gevallen moet u eraan denken om annotationType(), equals()en hashCode()te implementeren, aangezien het resultaat specifiek is gedocumenteerd voor java.lang.Annotation.


Antwoord 5, autoriteit 6%

Je kunt ook absoluut stom (maar eenvoudig) een dummy annotatiedoel maken en het daar vandaan halen

@MyAnnotation(foo="bar", baz=Blah.class)
private static class Dummy {}

En

final MyAnnotation annotation = Dummy.class.getAnnotation(MyAnnotation.class)

Het maken van op methoden/parameter gerichte annotatie-instanties is misschien wat uitgebreider, maar deze aanpak heeft het voordeel dat de annotatie-instantie wordt verkregen zoals de JVM normaal zou doen.
Onnodig te zeggen dat het zo eenvoudig mogelijk is.


Antwoord 6

Nogal ruwe manier met behulp van de proxy-aanpak met behulp van Apache Commons AnnotationUtils

public static <A extends Annotation> A mockAnnotation(Class<A> annotationClass, Map<String, Object> properties) {
    return (A) Proxy.newProxyInstance(annotationClass.getClassLoader(), new Class<?>[] { annotationClass }, (proxy, method, args) -> {
        Annotation annotation = (Annotation) proxy;
        String methodName = method.getName();
        switch (methodName) {
            case "toString":
                return AnnotationUtils.toString(annotation);
            case "hashCode":
                return AnnotationUtils.hashCode(annotation);
            case "equals":
                return AnnotationUtils.equals(annotation, (Annotation) args[0]);
            case "annotationType":
                return annotationClass;
            default:
                if (!properties.containsKey(methodName)) {
                    throw new NoSuchMethodException(String.format("Missing value for mocked annotation property '%s'. Pass the correct value in the 'properties' parameter", methodName));
                }
                return properties.get(methodName);
        }
    });
}

De typen doorgegeven eigenschappen worden niet gecontroleerd met het werkelijke type dat is gedeclareerd in de annotatie-interface en eventuele ontbrekende waarden worden alleen tijdens runtime ontdekt.

In functie vergelijkbaar met de code die wordt genoemd in het antwoord van Kaqqao(en waarschijnlijk Gunnar’s Answerook), zonder de nadelen van het gebruik van interne Java API zoals in Tobias Liefke’s antwoord.


Antwoord 7

Ik deed dit voor het toevoegen van annotatiereferentie aan mijn laseenheidtest:

@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ METHOD, FIELD, PARAMETER })
public @interface AuthenticatedUser {
    String value() default "foo";
    @SuppressWarnings("all")
    static class Literal extends AnnotationLiteral<AuthenticatedUser> implements AuthenticatedUser {
        private static final long serialVersionUID = 1L;
        public static final AuthenticatedUser INSTANCE = new Literal();
        private Literal() {
        }
        @Override
        public String value() {
            return "foo";
        }
    }
}

Gebruik:

Bean<?> createUserInfo() {
    return MockBean.builder()
            .types(UserInfo.class)
            .qualifiers(AuthenticatedUser.Literal.INSTANCE)
            .create((o) -> new UserInfo())
            .build();
}

Antwoord 8

Het antwoord van Gunnar is de eenvoudigste manier voor de meeste webservice zoals we al een overwinning hebben,
bijvoorbeeld
KafkaListener kafkaListener = new org.hibernate.validator.internal.util.annotation.AnnotationDescriptor.Builder<>(KafkaListener.class, ImmutableMap.of("topics", new String[]{"my-topic"})).build().getAnnotation();en alle andere eigenschappen zullen standaard blijven.


Antwoord 9

Bekijk annobuilder . Het mooie is dat het methode-referentie kan gebruiken in plaats van een naam van een attribuut

@interface Foo
{
    String value();
    int[] flags() default {0};
}
//test
    // @Foo(value="abc", flags={1})
    Foo foo1 = AnnoBuilder.of(Foo.class)
        .def(Foo::value, "abc")
        .def(Foo::flags, 1)
        .build();
    // @Foo(value="abc")
    Foo foo2 = AnnoBuilder.build(Foo.class, Foo::value, "abc");
    // @Foo("abc")
    Foo foo3 = AnnoBuilder.build(Foo.class, "abc");

Other episodes