Crear anotación personalizada para Lombok

8 minutos de lectura

avatar de usuario
Mesbah Gueffaf

He usado Lombok en mi código para generar automáticamente código getter y setter. Quiero agregar otras anotaciones personales y usarlas.

Por ejemplo, quiero agregar un @Exist método que verifica la existencia de una clave en una lista:

@Getter    @Setter
public class User {

    private String name;
    private List<Integer> keys;

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

Después de crear la anotación, haría algo como:

@Getter    @Setter
public class User {

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

  • ¿Cuándo se debe verificar esto? ¿Durante el tiempo de compilación?

    – JDC

    20 de diciembre de 2016 a las 13:05

  • sí Durante el tiempo de compilación

    – Mesbah Gueffaf

    21 de diciembre de 2016 a las 9:42

  • ¿Cómo se puede verificar una clave en la lista durante el tiempo de compilación?

    – Skadya

    27 dic 2016 a las 20:35

avatar de usuario
Fedor Losev

Consideraciones Generales

Si ya está utilizando Lombok, puede agregar una anotación y un controlador de transformación de Lombok personalizados.

  1. Definir anotación Existe con @Target(FIELD) y @Retention(SOURCE)

  2. Crear un manipulador

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

    para procesar su anotación. El paquete de clase de controlador debe comenzar con el lombok. prefijo. Si necesita admitir Eclipse, etc. además de javac, deberá escribir más controladores que extiendan las clases de marco apropiadas.

  3. En el controlador anula/implementa el handle() método para generar el código requerido a través de la manipulación de AST.


Puedes tomar como muestra el @Adquiridor implementación:

Anotación:
Getter.java

Manipulador:
HandleGetter.java

También puedes investigar fuentes de otras anotaciones y manipuladores para ver cómo generar un código particular.

Deberá agregar dependencias en lombok, JDK tools.jar.


Algunos recursos:


Tenga en cuenta que hay algunos puntos a considerar aquí

  • Este es un montón de código no trivial para escribir y mantener. Si planea usar la anotación 5-6 veces, simplemente no vale la pena.
  • Es posible que deba cambiar la implementación de su procesador de anotaciones con las actualizaciones de lombok.
  • El agujero en el compilador en el que se basa lombok también puede cerrarse (entonces todo el proyecto de Lombok cambiará drásticamente o dejará de existir; en este caso, tendrá un problema más grave de todos modos si usa Lombok de forma extensiva, aunque solo sea para @Getter ).

Una alternativa más compleja sin Lombok es usar estándar procesamiento de anotaciones por codigo de GENERACION pero, AFAIK, no puede cambiar las clases originales y debe generar/usar clases que las amplíen (a menos que quiera explotar la misma puerta trasera como Lombok o recurrir a una manipulación de código como CGLib o ASM).


Ejemplo de Lombok

A continuación se muestra un código de trabajo para crear una anotación de Lombok personalizada que he llamado @Contiene.

Es solo implementación de javac, no Eclipse, etc. Supongo que no será difícil crear un controlador similar para Eclipse u otro IDE.

generará nombre del campoContiene () método miembro que se delega al nombre del campo.contiene().

Tenga en cuenta que el código es solo una muestra rápida y sucia (pero funcional). Para la anotación de grado de producción, deberá manejar muchas condiciones de contorno, verificar los tipos correctos, manejar la configuración de Lombok, etc., como se puede observar en las fuentes de la biblioteca lombok o lombok-pg.


Ejemplo de uso


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) {
        }

    }
}

Implementación de anotaciones


Contiene.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;
    }
}

  • gracias ese trabajo! Ahora quiero agregar una línea antes del método de retorno; System.out.println(“prueba”); por ejemplo: public boolean fieldOneContains(final java.lang.Object contains) { System.out.println(“test”); return this.fieldOne.contains(contains); }

    – Mesbah Gueffaf

    8 de enero de 2017 a las 14:02


  • github.com/eldest/hello-lombok/blob/master/src/main/java/lombok/…

    – Fyodor Losev

    15 de enero de 2017 a las 18:37

  • Tengo problemas con las importaciones. No puedo encontrar muchas clases dentro del paquete lombok que he inicializado a través de maven, por ejemplo, JavacAnnotationHandler. ¿Alguna idea de por qué?

    – Cabra Salvaje

    19 oct 2017 a las 16:07

  • @WildGoat lombok parece haber sombreado los archivos internos para “limpiar el espacio de nombres”, buena suerte haciendo una extensión con los archivos sombreados

    – SnowyCoder

    25 de febrero de 2018 a las 10:37

  • @WildGoat Ver stackoverflow.com/questions/35550460/…

    – Florent Bayle

    17 de julio de 2018 a las 13:37

¿Ha sido útil esta solución?

Esta web utiliza cookies propias y de terceros para su correcto funcionamiento y para fines analíticos y para mostrarte publicidad relacionada con sus preferencias en base a un perfil elaborado a partir de tus hábitos de navegación. Al hacer clic en el botón Aceptar, acepta el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Configurar y más información
Privacidad