为 Lombok 创建自定义注解

27

我在我的代码中使用了Lombok自动生成getter和setter代码。我想添加其他个人注释并使用它。

例如,我想添加一个@Exist方法来验证列表中是否存在键:

@Getter    @Setter
public class User {

    private String name;
    private List<Integer> keys;

    public boolean existKeys(Integer key) {
        boolean exist = keys.contains(key);
        return exist;
    }
}
创建注释后,我会执行以下操作:
@Getter    @Setter
public class User {

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

这应该在什么时候进行验证?在编译时吗? - JDC
编译时期 - Mesbah Gueffaf
6
如何在编译时验证列表中的密钥? - skadya
1个回答

77

总体考虑

如果您已经在使用Lombok,您可以添加自定义的Lombok转换注解和处理程序。

  1. Define Exists annotation with @Target(FIELD) and @Retention(SOURCE)

  2. Create a handler

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

    to process your annotation. Handler class package must start with the lombok. prefix. If you need to support Eclipse, etc. in addition to javac, you'll need to write more handlers extending appropriate framework classes.

  3. In the handler override/implement the handle() method to generate the required code through AST manipulation.


你可以以@Getter实现为样本:
(注:保留原文中的html标签)

Annotation: Getter.java

Handler: HandleGetter.java

你还可以查看其他注释的来源处理程序,以了解如何生成特定的代码。

您需要添加对lombok、JDK tools.jar的依赖。


一些资源:


注意,这里有几个需要考虑的要点:

  • 这是一堆不太容易编写和维护的代码。如果您打算使用注解5-6次,那就不值得了。
  • 您可能需要随着Lombok更新更改注解处理器实现。
  • Lombok依赖的编译器漏洞也可能被关闭(那么整个Lombok项目将发生重大变化或停止存在;在这种情况下,即使仅用于 @Getter,如果广泛使用Lombok,则无论如何都会遇到更严重的问题)。

没有Lombok的更复杂的替代方案是使用标准的注解处理进行代码生成,但据我所知,您不能更改原始类并必须生成/使用扩展它们的类(除非您将利用与Lombok相同的后门或诉诸于像CGLib或ASM这样的代码操作)。


Lombok示例

以下是一些工作代码,用于创建自定义的Lombok注释,我称之为@Contains

这只是javac实现,没有Eclipse等。我猜在Eclipse或其他IDE中创建类似的处理程序不会很难。

它将生成fieldNameContains()成员方法,该方法委托给fieldName.contains()。

请注意,此代码仅是快速而脏的(但有效)示例。对于生产级别的注释,您需要处理许多边界条件,检查正确的类型,处理Lombok配置等,就像在lombok或lombok-pg库源代码中观察到的那样。


使用示例


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

    }
}

注解实现


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

2
@WildGoat lombok似乎已经将内部文件隐藏起来以“清理命名空间”,祝你在这些被隐藏的文件中开发扩展好运。 - SnowyCoder
1
@WildGoat 请参考 https://dev59.com/v1sV5IYBdhLWcg3w-i-l。 - Florent Bayle
像安装了Lombok插件的IntelliJ这样的IDE能够支持自定义注释吗? - Ahmedie-m
1
据我了解,目前可用的Lombok插件仅处理预配置注释,因此添加自定义注释需要编写插件或分叉和修改现有插件 https://github.com/mplushnikov/lombok-intellij-plugin/issues/792 - Fedor Losev
谢谢@FedorLosev,看起来在编写自己的自定义注释时需要考虑一些额外的工作。 - Ahmedie-m
显示剩余5条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接