Compile-time annotations using @Implement as an example



We all love to catch errors at the compilation stage, instead of runtime exceptions. The easiest way to fix them is that the compiler itself shows all the places that need to be fixed. Although most problems can only be detected when the program starts, we are still trying to do this as soon as possible. In blocks of initialization of classes, in constructors of objects, at the first call of a method, 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, creating an annotation that can throw errors, as the compiler does. Judging by the fact that there is not so much information on this topic in RuNet, the happy situations described above 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, you can safely skip it. I am sure this will not affect the completeness of the presentation.

Now we will talk more about improving the readability of the code than about fixing bugs. An example, one might say, from life, or rather from my hobby project.

Suppose there is a UnitManager class, 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 generation of id is delegated to the RotateCounter class, which returns a number in the given range. And there is a tiny problem, RotateCounter cannot know if 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 method isValueFree (), which receives id and returns true if id is free. And UnitManager implements this interface, creates an instance of RotateCounter and passes it to itself as a client.

That's exactly what I did. But, having opened the source of UnitManager a few days after writing, I went into an easy stupor after seeing the isValueFree () method, which didn’t really fit the logic for UnitManager. It would be much simpler if it were possible to specify which interface implements this method. For example, in C #, from which I came to Java, an explicit interface implementation helps to cope with this problem. In this case, firstly, you can call the method only with an explicit cast to the interface. Secondly, and more importantly in this case, the interface name (and without the access modifier) ​​is explicitly indicated in the method signature, for example:

IClient.isValueFree(int value) {
}

One solution is adding an annotation with the name of the interface that implements this method. Something like @Overridethat, only with an indication of the interface. I agree, you can use an anonymous inner class. In this case, just like in C #, the method cannot just be called on the object, and you can immediately see which interface it implements. But, this will increase the amount of code, therefore, degrade readability. Yes, and you need to somehow get it from the class - create a getter or public field (after all, there is no overload of cast statements 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 complete classes and can be inherited from them. In this case, you just need to create an annotation that inherits from@Override. But this was not so, and I had to plunge into the amazing and frightening world of checks at the compilation stage.

UnitManager sample code
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);
  }
}

Bit of theory


Just specify all the above methods are the instance, this, for the sake of brevity, the method names will indicate the name of the type and with no parameters: <имя_типа>.<имя_метода>().

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

@Override
public boolean process(Set annotations, RoundEnvironment env) {
  return false;
}

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

Element ( javax.lang.model.element.Element ) - the main interface for working with most of the structural elements of the language. An element has descendants that more precisely determine the properties of a particular element (for details, see 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 if the types of elements match. You can get it using the method Element.asType(). Also this type returns some operations with types, such as TypeElement.getSuperclass()or TypeElement.getInterfaces().

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

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

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

ElementKind ( javax.lang.model.element.ElementKind ) - an enumeration, it clarifies the information about the element, checks whether the element is a packet (PACKAGE), class (CLASS), method (METHOD), interface (INTERFACE), etc. d.

Name ( javax.lang.model.element.Name ) - the interface for working with the name of the element, can be obtained through Element.getSimpleName().

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

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

Validation algorithm


First you need to create the annotation itself. A lot has already been written about it (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 @Targetand @Retention. The first indicates that our annotation can only be applied to the method, and the second that the annotation will exist only in the source code.

Annotations must be specified 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 way is clearly better. In this case, the compiler will monitor the correct interface name. By the way, if you call this member value (), then when adding 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();
}

Then the fun begins - the creation of the processor. In the process method, we get a list of all annotated elements. Then we get the annotation itself and its meaning - 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 annos, RoundEnvironment env)
  {
    Set 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’t just get and get value annotations just like that. When you try to call , a MirroredTypeExceptionannotation.value() will be thrown , but from it you can get a TypeMirror. This cheating method, as well as the correct receipt of value, I found here :

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

The check itself consists of three parts, if at least one of them fails, 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);
}

The first step is to check if value annotations are an interface. Everything is simple here:

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 actually implements the specified interface. At first I foolishly implemented this test with my hands. But then, using good advice, I looked at Types and found a method there Types.isSubtype()that checks the entire inheritance tree and returns true if the specified interface is there. Importantly, it 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 method Types.isSubsignature(), 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 underlying 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;
}

And 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 methodParameters, List 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<String>
{
  @Implement(Predicate.class)
  boolean test(String t) {
    return false;
  }
}

When analyzing the method in the class, the type of the parameter String, and in the interface - T, and all attempts to get instead of it Stringdid not lead to anything. In the end, I came up with nothing better than just ignoring type parameters. The check will be passed with any actual type parameters, even if they do not match. Fortunately, the compiler will throw an error if the method has no 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 in my practice I used only it. Therefore, I will describe how to connect the processor to this IDE. For Eclipse to see the processor, you need to pack it into a separate .JAR, in which the annotation itself will also be. In this case, you need to create the META-INF / services folder in the project and create the javax.annotation.processing.Processor file there and indicate the full name of the processor class:, ds.magic.annotations.compileTime.ImplementProcessorin my case. Just in case, I’ll give a screenshot, but when nothing worked for me, I almost started to sin on the structure of the project.

image

Next, collect .JAR and connect it to your project, first as a regular library, so that the annotation is visible in the code. Then we connect the processor ( here is more detailed). To do this, openproject properties and select:

  1. Java Compiler -> Annotation Processing and check the "Enable annotation processing" box.
  2. Java Compiler -> Annotation Processing -> Factory Path check the "Enable project specific settings" checkbox. Then click Add JARs ... and select the previously created JAR file.
  3. Agrees to rebuild the project.

Total


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

Perhaps this annotation may seem useless to some. Perhaps it is. But personally, I myself use it instead @Override, when the names of the methods do not fit well into the class's purpose. And so far, I have no desire to get rid of her. 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 users of ohotNik_alex and Comdiv for their help in fixing bugs.

Also popular now: