Create custom annotation for Lombok

I used Lombok in my code to automatically create Getter and Setter code. Now I want to add another personalized Annotations and use it.

for example, I want to add @Exist to check for an existing list of keys:

  @Getter @Setter public class User { private String name; private List<Integer> keys; public boolean existKeys(Integer key) { boolean exist = keys.contains(key); return exist; } } 

after creating the annotation, I just need to do something like:

 @Getter @Setter public class User { private String name; @Exist private List<Integer> keys; } 
+5
source share
1 answer

General considerations

If you are already using Lombok, you can add an annotation and Lombok transform handler.

  • Define Exists annotation with @Target(FIELD) and @Retention(SOURCE)
  • Create handler

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

    for processing annotations. The package of the Handler class must begin with the lombok. prefix lombok. . If you need to support Eclipse, etc. In addition to javac, you will need to write more handlers to extend the corresponding class classes.

  • In the handler, override / implement the handle() method to generate the required code using AST processing.


As an example, you can take @Getter :

Summary: Getter.java

Handler: HandleGetter.java

You can also look at other annotation sources and handlers to find out how to generate specific code.

You need to add dependencies on lombok, JDK tools.jar.


Some resources:


Note that there are several points

  • This is a bunch of nontrivial code to write and maintain. If you plan to use the annotation 5-6 times, it's just not worth it.
  • You may need to change the annotation processor implementation with the lombok update.
  • The hole in the compiler on which the reliance relies can also be closed (then the entire Lombok project will change dramatically or cease to exist; in this case you will have a more serious problem if you use Lombok extensively, even if it is just for @Getter).

A more complex alternative without Lombok is to use standard annotation processing to generate code , but AFAIK you cannot modify the source classes and you must generate / use classes that extend them (unless you use the same -door back as Lombok or resort to code manipulation, for example CGLib or ASM).


Lombok example

Below is some working code for creating a custom Lombok annotation, which I called @Contains .

This is a javac implementation only, no Eclipse, etc. I think it will not be easy to create a similar handler for Eclipse or another IDE.

It will generate the memberNameContains () method, which is delegated to the Name.contains () field.

Please note that the code is just a quick and dirty (but working) sample. To annotate the production brand, you will need to process many boundary conditions, check the correct types, process the Lombok configuration, etc., As can be seen in the library sources Lombok or Lombok-pg.


Sample use


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

Execution Summary


Containers.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; } } 
+17
source

Source: https://habr.com/ru/post/1261571/


All Articles