web-dev-qa-db-fra.com

Créer une annotation personnalisée pour Lombok

J'ai utilisé Lombok dans mon code pour générer automatiquement le code Getter et Setter, maintenant je veux ajouter d'autres Annotations personnels et les utiliser.

par exemple, je veux ajouter @Exist qui vérifie la clé existante dans la liste:

    @Getter    @Setter
public class User {

    private String name;
    private List<Integer> keys;

    public boolean existKeys(Integer key) {
        boolean exist = keys.contains(key);
        return exist;
    }

}

après avoir créé l'annotation, je devrai simplement faire quelque chose comme:

@Getter    @Setter
public class User {

    private String name;
    @Exist
    private List<Integer> keys;

} 
16
Mesbah Gueffaf

Considérations générales

Si vous utilisez déjà Lombok, vous pouvez ajouter une annotation et un gestionnaire de transformation Lombok personnalisés.

  1. Définir l'annotation Exists avec @Target(FIELD) et @Retention(SOURCE)
  2. Créez un gestionnaire

    @ProviderFor(JavacAnnotationHandler.class)
    public class HandleExists extends JavacAnnotationHandler<Exists>{ ...` 
    

    pour traiter votre annotation. Le package de classe de gestionnaire doit commencer par le préfixe lombok.. Si vous devez prendre en charge Eclipse, etc. en plus de javac, vous devrez écrire davantage de gestionnaires étendant les classes de framework appropriées.

  3. Dans le gestionnaire, remplacez/implémentez la méthode handle() pour générer le code requis via la manipulation AST.


Vous pouvez prendre comme exemple l'implémentation @ Getter :

Annotation: Getter.Java

Gestionnaire: HandleGetter.Java

Vous pouvez également consulter sources d'autres annotations et gestionnaires pour voir comment générer un code particulier.

Vous devrez ajouter des dépendances sur lombok, JDK tools.jar.


Quelques ressources:


Remarque, il y a quelques points à considérer ici

  • Il s'agit d'un tas de code non trivial à écrire et à maintenir. Si vous prévoyez d'utiliser l'annotation 5-6 fois, cela n'en vaut pas la peine.
  • Vous devrez peut-être modifier la mise en œuvre de votre processeur d'annotation avec les mises à niveau de lombok.
  • Le trou dans le compilateur sur lequel s'appuie lombok peut également être fermé (alors l'ensemble du projet Lombok changera radicalement ou cessera d'exister; dans ce cas, vous aurez de toute façon un problème plus grave si vous utilisez Lombok de manière intensive, même si ce n'est que pour @Getter ).

Une alternative plus complexe sans Lombok consiste à utiliser la norme traitement des annotations pour génération de code mais, AFAIK, vous ne pouvez pas modifier les classes d'origine et devez générer/utiliser des classes qui les étendent ( sauf si vous exploitez la même porte dérobée que Lombok ou recourrez à une manipulation de code comme CGLib ou ASM).


Exemple de Lombok

Vous trouverez ci-dessous du code de travail pour créer une annotation Lombok personnalisée que j'ai appelée @ Contient .

Il s'agit uniquement d'une implémentation javac, pas d'Eclipse, etc. Je suppose qu'il ne sera pas difficile de créer un gestionnaire similaire pour Eclipse ou un autre IDE.

Il générera fieldName Contains () la méthode membre qui est déléguée à fieldName. Contains ().

Remarque, le code est juste un échantillon rapide et sale (mais qui fonctionne). Pour l'annotation de qualité de production, vous devrez gérer de nombreuses conditions aux limites, vérifier les types corrects, gérer la configuration Lombok, etc., comme cela peut être observé dans les sources de bibliothèque lombok ou lombok-pg.


Exemple d'utilisation


SomeEnity.Java

@Getter
@Setter
public class SomeEntity {

    @NonNull
    @Contains
    private Collection<String> fieldOne = new ArrayList<>();

    @NonNull
    @Contains
    private Collection<String> fieldTwo = new ArrayList<>();

}

SomeEntityTest.Java

public class SomeEntityTest {

    @Test
    public void test() {
        SomeEntity entity = new SomeEntity();

        Collection<String> test1 = Arrays.asList(new String[] { "1", "2" });
        entity.setFieldOne(test1);
        assertSame(test1, entity.getFieldOne());

        Collection<String> test2 = new HashSet<String>(Arrays.asList(new String[] { "3", "4" }));
        entity.setFieldTwo(test2);
        assertSame(test2, entity.getFieldTwo());

        assertTrue(entity.fieldOneContains("1"));
        assertTrue(entity.fieldOneContains("2"));
        assertFalse(entity.fieldOneContains("3"));
        assertFalse(entity.fieldOneContains("4"));

        assertFalse(entity.fieldTwoContains("1"));
        assertFalse(entity.fieldTwoContains("2"));
        assertTrue(entity.fieldTwoContains("3"));
        assertTrue(entity.fieldTwoContains("4"));

        try {
            entity.setFieldOne(null);
            fail("exception expected");
        } catch (Exception ex) {
        }

        try {
            entity.setFieldTwo(null);
            fail("exception expected");
        } catch (Exception ex) {
        }

    }
}

Mise en œuvre d'annotation


Contient.Java

@Target({ElementType.FIELD})
@Retention(RetentionPolicy.SOURCE)
public @interface Contains {
    Class<?>[] types() default {};
    Class<?>[] excludes() default {};
}

HandleContains.Java

@ProviderFor(JavacAnnotationHandler.class) 
@HandlerPriority(65536) 
@ResolutionResetNeeded 
public class HandleContains extends JavacAnnotationHandler<Contains> {

    @Override 
    public void handle(AnnotationValues<Contains> annotation, JCAnnotation ast, JavacNode annotationNode) {

        try {
            JavacNode node = annotationNode.up();
            if (node.getKind() != Kind.FIELD) {
                annotationNode.addError("@Contains is allowed only on fields");
                return;
            }
            Name delegateName = annotationNode.toName(node.getName());
            JavacResolution reso = new JavacResolution(annotationNode.getContext());
            JCTree member = node.get();
            if (member.type == null) {
                reso.resolveClassMember(node);
            }
            Type delegateType = member.type;
            if (delegateType instanceof ClassType) {
                ClassType ct = (ClassType) delegateType;
                //TODO validate that this field is a collection type
                // if(!Collection)
                //   annotationNode.addError("@Contains can only be used on collections");
                final String methodName = "contains";
                MethodSig methodSig = getMethodBinding(methodName, ct, annotationNode.getTypesUtil());
                if (methodSig == null) throw new Exception("no method " + methodName + " in " + ct.tsym.name);
                JCMethodDecl methodDecl = createDelegateMethod(methodSig, annotationNode, delegateName);
                injectMethod(node.up(), methodDecl);
            } else {
                annotationNode.addError("@Contains can only use concrete class types");
                return;
            }
        } catch (Exception ex) {
            //ex.printStackTrace();
            annotationNode.addError("@Contains unexpected error: " + ex.getMessage());
        }

    }

    public JCMethodDecl createDelegateMethod(MethodSig sig, JavacNode annotation, Name delegateName) throws TypeNotConvertibleException {

        JavacTreeMaker maker = annotation.getTreeMaker();

        com.Sun.tools.javac.util.List<JCAnnotation> annotations;
        if (sig.isDeprecated) {
            annotations = com.Sun.tools.javac.util.List.of(maker.Annotation(genJavaLangTypeRef(annotation, "Deprecated"), com.Sun.tools.javac.util.List.<JCExpression>nil()));
        } else {
            annotations = com.Sun.tools.javac.util.List.nil();
        }

        JCModifiers mods = maker.Modifiers(PUBLIC, annotations);
        JCExpression returnType = JavacResolution.typeToJCTree((Type) sig.type.getReturnType(), annotation.getAst(), true);
        boolean useReturn = sig.type.getReturnType().getKind() != TypeKind.VOID;
        ListBuffer<JCVariableDecl> params = sig.type.getParameterTypes().isEmpty() ? null : new ListBuffer<JCVariableDecl>();
        ListBuffer<JCExpression> args = sig.type.getParameterTypes().isEmpty() ? null : new ListBuffer<JCExpression>();
        ListBuffer<JCExpression> thrown = sig.type.getThrownTypes().isEmpty() ? null : new ListBuffer<JCExpression>();
        ListBuffer<JCTypeParameter> typeParams = sig.type.getTypeVariables().isEmpty() ? null : new ListBuffer<JCTypeParameter>();
        ListBuffer<JCExpression> typeArgs = sig.type.getTypeVariables().isEmpty() ? null : new ListBuffer<JCExpression>();
        Types types = Types.instance(annotation.getContext());

        for (TypeMirror param : sig.type.getTypeVariables()) {
            Name name = ((TypeVar) param).tsym.name;

            ListBuffer<JCExpression> bounds = new ListBuffer<JCExpression>();
            for (Type type : types.getBounds((TypeVar) param)) {
                bounds.append(JavacResolution.typeToJCTree(type, annotation.getAst(), true));
            }

            typeParams.append(maker.TypeParameter(name, bounds.toList()));
            typeArgs.append(maker.Ident(name));
        }

        for (TypeMirror ex : sig.type.getThrownTypes()) {
            thrown.append(JavacResolution.typeToJCTree((Type) ex, annotation.getAst(), true));
        }

        int idx = 0;
        String[] paramNames = sig.getParameterNames();
        boolean varargs = sig.elem.isVarArgs();
        for (TypeMirror param : sig.type.getParameterTypes()) {
            long flags = JavacHandlerUtil.addFinalIfNeeded(Flags.PARAMETER, annotation.getContext());
            JCModifiers paramMods = maker.Modifiers(flags);
            Name name = annotation.toName(paramNames[idx++]);
            if (varargs && idx == paramNames.length) {
                paramMods.flags |= VARARGS;
            }
            params.append(maker.VarDef(paramMods, name, JavacResolution.typeToJCTree((Type) param, annotation.getAst(), true), null));
            args.append(maker.Ident(name));
        }

        JCExpression accessor = maker.Select(maker.Ident(annotation.toName("this")), delegateName);

        JCExpression delegateCall = maker.Apply(toList(typeArgs), maker.Select(accessor, sig.name), toList(args));
        JCStatement body = useReturn ? maker.Return(delegateCall) : maker.Exec(delegateCall);
        JCBlock bodyBlock = maker.Block(0, com.Sun.tools.javac.util.List.of(body));
        StringBuilder generatedMethodName = new StringBuilder(delegateName);
        generatedMethodName.append(sig.name.toString());
        generatedMethodName.setCharAt(delegateName.length(), Character.toUpperCase(generatedMethodName.charAt(delegateName.length())));
        return recursiveSetGeneratedBy(maker.MethodDef(mods, annotation.toName(generatedMethodName.toString()), returnType, toList(typeParams), toList(params), toList(thrown), bodyBlock, null), annotation.get(), annotation.getContext());
    }

    public static <T> com.Sun.tools.javac.util.List<T> toList(ListBuffer<T> collection) {
        return collection == null ? com.Sun.tools.javac.util.List.<T>nil() : collection.toList();
    }

    public static class MethodSig {
        final Name name;
        final ExecutableType type;
        final boolean isDeprecated;
        final ExecutableElement elem;

        MethodSig(Name name, ExecutableType type, boolean isDeprecated, ExecutableElement elem) {
            this.name = name;
            this.type = type;
            this.isDeprecated = isDeprecated;
            this.elem = elem;
        }

        String[] getParameterNames() {
            List<? extends VariableElement> paramList = elem.getParameters();
            String[] paramNames = new String[paramList.size()];
            for (int i = 0; i < paramNames.length; i++) {
                paramNames[i] = paramList.get(i).getSimpleName().toString();
            }
            return paramNames;
        }

        @Override public String toString() {
            return (isDeprecated ? "@Deprecated " : "") + name + " " + type;
        }
    }

    public MethodSig getMethodBinding(String name, ClassType ct, JavacTypes types) {
        MethodSig result = null;
        TypeSymbol tsym = ct.asElement();
        if (tsym == null) throw new IllegalArgumentException("no class");

        for (Symbol member : tsym.getEnclosedElements()) {
            if (member.getKind() != ElementKind.METHOD || !name.equals(member.name.toString())) {
                continue;
            }
            if (member.isStatic()) continue;
            if (member.isConstructor()) continue;
            ExecutableElement exElem = (ExecutableElement) member;
            if (!exElem.getModifiers().contains(Modifier.PUBLIC)) continue;
            ExecutableType methodType = (ExecutableType) types.asMemberOf(ct, member);
            boolean isDeprecated = (member.flags() & DEPRECATED) != 0;
            result = new MethodSig(member.name, methodType, isDeprecated, exElem);
        }
        if (result == null) {
            if (ct.supertype_field instanceof ClassType) {
                result = getMethodBinding(name, (ClassType) ct.supertype_field, types);
            }
            if (result == null) {
                if (ct.interfaces_field != null) {
                    for (Type iface : ct.interfaces_field) {
                        if (iface instanceof ClassType) {
                            result = getMethodBinding(name, (ClassType) iface, types);
                            if (result != null) {
                                break;
                            }
                        }
                    }
                }
            }
        }
        return result;
    }
}
57
Fedor Losev