Compile-time annotations using the example of @Implement



We all love to catch errors at compile time, instead of runtime exceptions. The easiest way to eliminate them, the compiler itself shows all the places that need fixing. Although most problems can only be detected when the program is started, we still try to do it as soon as possible. In blocks of class initialization, in object constructors, on the first method call, etc. And sometimes we are lucky, and even at the compilation stage we know enough to check the program for certain errors.

In this article I want to share the experience of writing one such test. More precisely, the creation of annotations that can produce errors, as the compiler does. Judging by the fact that there is not so much information on this topic in Runet, then the situations described above, happy situations are not often.

I will describe the general verification algorithm, as well as all the steps and nuances for which I spent time and nerve cells.

Formulation of the problem


In this section, I will give an example of using this annotation. If you already know what check you want to do, feel free to skip it. I am sure it will not affect the completeness of the presentation.

Now we will talk more about improving the readability of the code rather than about eliminating errors. An example can be said from life, or rather from my hobby project.

Suppose there is a class UnitManager, which, in fact, is a collection of units. It has methods for adding, deleting, getting a unit, etc. When adding a new unit, the manager assigns it an id. The id generation is delegated to the RotateCounter class, which, returns a number in the specified range. And there is a tiny problem, RotateCounter cannot know whether the selected id is free. According to the principle of dependency inversion, you can create an interface, in my case, it is RotateCounter.IClient, which has a single isValueFree () method that receives an id and returns true if id is free. And UnitManager implements this interface, creates an instance of RotateCounter and transfers it to itself as a client.

That's exactly what I did. But, having opened the source code UnitManager a few days after writing, I entered into an easy stupor after seeing the isValueFree () method, which was not very logical for UnitManager. It would be much easier if it were possible to specify which interface this method implements. For example, in C #, from which I came to Java, an explicit interface implementation helps to cope with this problem. In this case, firstly, the method can be called only with an explicit caste to the interface. Secondly, and more importantly in this case, the signature of the method explicitly indicates the name of the interface (and without an access modifier), for example:

IClient.isValueFree(int value) { } 

One solution is to add annotations, with the name of the interface that implements this method. Something like @Override , only indicating the interface. I agree, you can use an anonymous inner class. In this case, as in C #, the method cannot simply be called on an object, and you can immediately see which interface it implements. But, it will increase the amount of code, therefore, degrade readability. Yes, and it must somehow get out of the class - create a getter or a public field (after all, there is no overload of caste operators in Java either). Not a bad option, but I don't like it.

At first, I thought that in Java, as in C #, annotations are full-fledged classes and can be inherited from them. In this case, you would simply need to create an annotation that inherits from @Override . But it turned out to be wrong, and I had to plunge into the amazing and frightening world of checks at the compilation stage.

Code example UnitManager
 public class Unit { private int id; } public class UnitManager implements RotateCounter.IClient { private final Unit[] units; private final RotateCounter idGenerator; public UnitManager(int size) { units = new Unit[size]; idGenerator = new RotateCounter(0, size, this); } public void addUnit(Unit unit) { int id = idGenerator.findFree(); units[id] = unit; } @Implement(RotateCounter.IClient.class) public boolean isValueFree(int value) { return units[value] == null; } public void removeUnit(int id) { units[id] = null; } } public class RotateCounter { private final IClient client; private int next; private int minValue; private int maxValue; public RotateCounter(int minValue, int maxValue, IClient client) { this.client = client; this.minValue = minValue; this.maxValue = maxValue; next = minValue; } public int incrementAndGet() { int current = next; if (next >= maxValue) { next = minValue; return current; } next++; return current; } public int range() { return maxValue - minValue + 1; } public int findFree() { int range = range(); int trysCounter = 0; int id; do { if (++trysCounter > range) { throw new IllegalStateException("No free values."); } id = incrementAndGet(); } while (!client.isValueFree(id)); return id; } public static interface IClient { boolean isValueFree(int value); } } 

Some theory


At once I will make a reservation, all the above methods are instance methods, so for brevity I will indicate the names of the methods with the type name and without parameters: <_>.<_>() .

The processing of elements at compile time is handled by special processor classes. These are classes which are inherited from javax.annotation.processing.AbstractProcessor (you can simply implement the javax.annotation.processing.Processor interface). You can read more about the processors here and here . The most important method in it is the process. In which we can get a list of all annotated elements and carry out the necessary checks.

 @Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment env) { return false; } 

At first, naively, I thought that working with types at the compilation stage is carried out in terms of reflection, but ... no. Everything is based on elements.

Element ( javax.lang.model.element.Element ) is the main interface for working with most of the structural elements of the language. An element has heirs that more accurately determine the properties of a particular element (for details, you can look here ):

 package ds.magic.example.implement; // PackageElement public class Unit // TypeElement { private int id; // VariableElement public void setId(int id) { // ExecutableElement this.id = id; } } 

TypeMirror ( javax.lang.model.type.TypeMirror ) is something like Class <?> Returned by the getClass () method. For example, they can be compared to find out whether the types of elements match. You can get it using the Element.asType() method. Also, this type is returned by some type operations, such as TypeElement.getSuperclass() or TypeElement.getInterfaces() .

Types ( javax.lang.model.util.Types ) - I advise you to take a closer look at this class. There you can find a lot of interesting things. In fact, this is a set of utilities for working with types. For example, it allows you to get TypeElement back from TypeMirror.

 private TypeElement asTypeElement(TypeMirror typeMirror) { return (TypeElement)processingEnv.getTypeUtils().asElement(typeMirror); } 

TypeKind ( javax.lang.model.type.TypeKind ) - enumeration, allows you to specify information about the type, check whether the type is an array (ARRAY), a custom type (DECLARED), a type variable (TYPEVAR), etc. You can get through TypeMirror.getKind()

ElementKind ( javax.lang.model.element.ElementKind ) - enumeration, it does not clarify the information about the element, check whether the element is a package (PACKAGE), class (CLASS), method (METHOD), interface (INTERFACE), etc.

Name ( javax.lang.model.element.Name ) is an interface for working with the name of an element; you can get it through Element.getSimpleName() .

Basically, these types were enough for me to write a verification algorithm.

I want to note another interesting feature. Implementations of Element interfaces in Eclipse are in org.eclipse ... packages, for example, elements that represent methods have type org.eclipse.jdt.internal.compiler.apt.model.ExecutableElementImpl . This gave me the idea that these interfaces are implemented by each IDE independently.

Check algorithm


First you need to create the annotation itself. There is already quite a lot written about this (for example, here ), so I will not dwell on this in detail. I can only say that for our example, we need to add two annotations @Target and @Retention . The first indicates that our annotation can be applied only to the method, and the second - that the annotation will exist only in the source code.

Annotations need to specify which interface implements the annotated method (the method to which the annotation is applied). This can be done in two ways: either specify the full name of the interface with a string, for example @Implement("com.ds.IInterface") , or pass the interface class directly: @Implement(IInterface.class) . The second method is clearly better. In this case, the compiler itself will monitor the correctness of the specified interface name. By the way, if you call this member value () then when you add annotations to the method, you will not need to explicitly specify the name of this parameter.

 @Target({ElementType.METHOD}) @Retention(RetentionPolicy.SOURCE) public @interface Implement { Class<?> value(); } 

Further the most interesting begins - creation of the processor. In the process method we get a list of all annotated elements. Then we get the annotation itself and its value - the specified interface. In general, the processor class framework looks like this:

 @SupportedAnnotationTypes({"ds.magic.annotations.compileTime.Implement"}) @SupportedSourceVersion(SourceVersion.RELEASE_8) public class ImplementProcessor extends AbstractProcessor { private Types typeUtils; @Override public void init(ProcessingEnvironment procEnv) { super.init(procEnv); typeUtils = this.processingEnv.getTypeUtils(); } @Override public boolean process(Set<? extends TypeElement> annos, RoundEnvironment env) { Set<? extends Element> annotatedElements = env.getElementsAnnotatedWith(Implement.class); for(Element annotated : annotatedElements) { Implement annotation = annotatedElement.getAnnotation(Implement.class); TypeMirror interfaceMirror = getValueMirror(annotation); TypeElement interfaceType = asTypeElement(interfaceMirror); //... } return false; } private TypeElement asTypeElement(TypeMirror typeMirror) { return (TypeElement)typeUtils.asElement(typeMirror); } } 

I want to note that you can not just take and get the value annotations. At attempt to cause annotation.value() exception of MirroredTypeException will be thrown, and here from it it is possible to receive TypeMirror. This cheat method, as well as the correct getting value, I found here :

 private TypeMirror getValueMirror(Implement annotation) { try { annotation.value(); } catch(MirroredTypeException e) { return e.getTypeMirror(); } return null; } 

The verification itself consists of three parts, if at least one of them is not passed, then you need to display an error message and proceed to the next annotation. By the way, you can display an error message using the following method:

 private void printError(String message, Element annotatedElement) { Messager messager = processingEnv.getMessager(); messager.printMessage(Kind.ERROR, message, annotatedElement); } 

First of all, you need to check whether the value annotation is an interface. Everything is simple:

 if (interfaceType.getKind() != ElementKind.INTERFACE) { String name = Implement.class.getSimpleName(); printError("Value of @" + name + " must be an interface", annotated); continue; } 

Next, you need to check whether the class in which the annotated method is located implements the specified interface. At first, I foolishly implemented this check with my hands. But then, taking advantage of good advice, I looked closely at Types and found there the Types.isSubtype() method, which checks the entire inheritance tree and returns true if the specified interface is there. What is important, can work with generic types, unlike the first option.

 TypeElement enclosingType = (TypeElement)annotatedElement.getEnclosingElement(); if (!typeUtils.isSubtype(enclosingType.asType(), interfaceMirror)) { Name className = enclosingType.getSimpleName(); Name interfaceName = interfaceType.getSimpleName(); printError(className + " must implemet " + interfaceName, annotated); continue; } 

Finally, you need to make sure that the interface has a method with the same signature as the annotated one. I would like to use the Types.isSubsignature() method, but unfortunately, it does not work correctly if the method has type parameters. So we roll up our sleeves and write all the checks with our hands. And we have three of them again. Well, more precisely, the method signature consists of three parts: the name of the method, the type of the return value, and the list of parameters. You need to go through all the methods of the interface and find the one that passed all three checks. It would be nice not to forget that the method can be inherited from another interface and recursively perform the same checks for the basic interfaces.

The call must be placed at the end of the loop in the process method, like this:

 if (!haveMethod(interfaceType, (ExecutableElement)annotatedElement)) { Name name = interfaceType.getSimpleName(); printError(name + " don't have \"" + annotated + "\" method", annotated); continue; } 

The haveMethod () method itself looks like this:

 private boolean haveMethod(TypeElement interfaceType, ExecutableElement method) { Name methodName = method.getSimpleName(); for (Element interfaceElement : interfaceType.getEnclosedElements()) { if (interfaceElement instanceof ExecutableElement) { ExecutableElement interfaceMethod = (ExecutableElement)interfaceElement; // Is names match? if (!interfaceMethod.getSimpleName().equals(methodName)) { continue; } // Is return types match (ignore type variable)? TypeMirror returnType = method.getReturnType(); TypeMirror interfaceReturnType = method.getReturnType(); if (!isTypeVariable(interfaceReturnType) && !returnType.equals(interfaceReturnType)) { continue; } // Is parameters match? if (!isParametersEquals(method.getParameters(), interfaceMethod.getParameters())) { continue; } return true; } } // Recursive search for (TypeMirror baseMirror : interfaceType.getInterfaces()) { TypeElement base = asTypeElement(baseMirror); if (haveMethod(base, method)) { return true; } } return false; } private boolean isParametersEquals(List<? extends VariableElement> methodParameters, List<? extends VariableElement> interfaceParameters) { if (methodParameters.size() != interfaceParameters.size()) { return false; } for (int i = 0; i < methodParameters.size(); i++) { TypeMirror interfaceParameterMirror = interfaceParameters.get(i).asType(); if (isTypeVariable(interfaceParameterMirror)) { continue; } if (!methodParameters.get(i).asType().equals(interfaceParameterMirror)) { return false; } } return true; } private boolean isTypeVariable(TypeMirror type) { return type.getKind() == TypeKind.TYPEVAR; } 

See the problem? Not? And she is there. The fact is that I could not find a way to get the actual type parameters for generic interfaces. For example, I have a class that implements the Predicate interface:
 MyPredicate implements Predicate&ltString&gt { @Implement(Predicate.class) boolean test(String t) { return false; } } 

When analyzing a method in a class, the type of the parameter is String , and in the interface it is T , and all attempts to get a String instead of it have failed. In the end, I did not come up with anything better than just ignoring the type parameters. The check will be passed for any actual type parameters, even if they do not match. Fortunately, the compiler itself will give an error if the method does not have a default implementation and is not implemented in the base class. But still, if anyone knows how to get around this, I will be extremely grateful for the hint.

Connect to Eclipse


Personally, I love Eclipce and only used it in my practice. Therefore, I will describe how to connect the processor to this IDE. In order for Eclipse to see the processor, you need to pack it in a separate .JAR, which will contain the annotation itself. At the same time, you need to create the META-INF / services folder in the project and create the javax.annotation.processing.Processor file and specify the full name of the processor class: ds.magic.annotations.compileTime.ImplementProcessor , in my case. Just in case, I will provide a screenshot, otherwise when nothing worked for me, I almost started to sin on the project structure.

image

Next we compile .JAR and connect it to our project, first as a regular library, so that the annotation would be visible in the code. Then we connect the processor (in more detail here ). To do this, open the project properties and select:

  1. Java Compiler -> Annotation Processing and put a tick in “Enable annotation processing”.
  2. Java Compiler -> Annotation Processing -> Factory Path put a tick in “Enable project specific settings”. Then click Add JARs ... and select the previously created JAR file.
  3. Agree to rebuild the project.

Total


All together and in the Eclipse project can be seen on GitHub . At the time of this writing, there are only two classes, if the annotation can be called: Implement.java and ImplementProcessor.java. I think you have already guessed their appointment.

Perhaps this annotation may seem useless to someone. Perhaps it is. But personally, I use it myself instead of @Override , when method names do not fit well with the class assignment. And so far, I have not had the desire to get rid of it. In general, I made an annotation for myself, and the purpose of the article was to show what rake I was attacking. I hope I did it. Thanks for attention.

Ps. Thanks to ohotNik_alex and Comdiv users for helping me fix the bugs.

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


All Articles