以@Implement为例的编译时注释



我们都喜欢在编译阶段捕获错误,而不是运行时异常。 修复它们的最简单方法是,编译器本身会显示所有需要修复的位置。 尽管大多数问题只能在程序启动时才能检测到,但我们仍在尝试尽快做到这一点。 在类的初始化块中,在对象的构造函数中,在方法的第一次调用中,等等。 有时我们很幸运,即使在编译阶段,我们也知道足以检查程序是否存在某些错误。

在本文中,我想分享编写这样一个测试的经验。 更准确地说,就像编译器一样,创建可以引发错误的注释。 通过在RuNet中没有太多关于此主题的信息来判断,上述的快乐情况并不常见。

我将描述通用的验证算法,以及我花费时间和神经细胞的所有步骤和细微差别。

问题陈述


在本节中,我将给出一个使用此注释的示例。 如果您已经知道要执行的检查,则可以安全地跳过它。 我相信这不会影响演示文稿的完整性。

现在,我们将谈论更多关于提高代码的可读性,而不是修复错误。 可以说一个例子,来自生活,或者来自我的业余爱好项目。

假设有一个UnitManager类,它实际上是单元的集合。 它具有添加,删除,获取单位等的方法。 添加新单元时,管理员将为其分配一个ID。 id的生成委托给RotateCounter类,该类返回给定范围内的数字。 而且有一个小问题,RotateCounter无法知道所选ID是否可用。 根据依赖关系反转的原理,您可以创建一个接口,在我的例子中是RotateCounter.IClient,它具有单个方法isValueFree(),该方法接收id,如果id可用则返回true。 然后,UnitManager实现此接口,创建RotateCounter的实例,并将其作为客户端传递给自身。

我就是这样做的。 但是,在写完几天后打开了UnitManager的源代码后,当我看到isValueFree()方法不符合UnitManager的逻辑时,我陷入了混乱。 如果可以指定哪个接口实现此方法,则将更加简单。 例如,在我来自Java的C#中,显式接口实现有助于解决此问题。 在这种情况下,首先,您只能在显式转换为接口的情况下调用该方法。 其次,在这种情况下更重要的是,在方法签名中明确指出了接口名称(并且没有访问修饰符),例如:

IClient.isValueFree(int value) { } 

一种解决方案是添加带有实现此方法的接口名称的注释。 类似于@Override ,仅具有接口。 我同意,您可以使用匿名内部类。 在这种情况下,就像在C#中一样,不能仅在对象上调用该方法,您可以立即看到其实现的接口。 但是,这将增加代码量,因此会降低可读性。 是的,您需要以某种方式从类中获取它-创建一个getter或public字段(毕竟,Java中也没有重载强制转换语句)。 不错的选择,但我不喜欢它。

起初,我认为在Java中,就像在C#中一样,注释是完整的类,可以从它们继承。 在这种情况下,您只需要创建一个从@Override继承的注释即可。 但是事实并非如此,在编译阶段,我不得不涉足令人惊奇而又令人恐惧的检查世界。

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

一点理论


我将立即进行保留,以上所有方法都是实例,因此,为了简便起见,我将使用类型名称和不带参数的方法名称来表示方法名称: <_>.<_>()

在编译阶段对元素的处理涉及特殊的处理器类。 这些是继承自javax.annotation.processing.AbstractProcessor类(您可以简单地实现javax.annotation.processing.Processor接口)。 您可以在此处此处阅读有关处理器的更多信息。 其中最重要的方法是过程。 在其中我们可以获取所有带注释元素的列表并进行必要的检查。

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

起初,我非常天真,我认为在编译阶段使用类型的工作是从反射的角度进行的,但是……没有。 一切都基于那里的元素。

元素javax.lang.model.element.Element )-使用该语言的大多数结构元素的主界面。 元素的后代可以更精确地确定特定元素的属性(有关详细信息,请参见此处 ):

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

TypeMirrorjavax.lang.model.type.TypeMirror )与类<?>类似,由getClass()方法返回。 例如,可以将它们进行比较以找出元素类型是否匹配。 您可以使用Element.asType()方法获取它。 此类型还返回一些类型操作,例如TypeElement.getSuperclass()TypeElement.getInterfaces()

类型javax.lang.model.util.Types )-我建议您仔细看一下此类。 您可以在那找到很多有趣的东西。 本质上,这是一组用于处理类型的实用程序。 例如,它允许您从TypeMirror检索TypeElement。

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

TypeKindjavax.lang.model.type.TypeKind )-一个枚举,使您可以澄清类型信息,检查类型是否为数组(ARRAY),自定义类型(DECLARED),类型变量(TYPEVAR)等。 您可以通过TypeMirror.getKind()获得它

ElementKindjavax.lang.model.element.ElementKind )-枚举,使您可以澄清有关元素的信息,检查元素是否是数据包(PACKAGE),类(CLASS),方法(METHOD),接口(INTERFACE)等。

Namejavax.lang.model.element.Name )-使用元素名称的接口,可以通过Element.getSimpleName()获得。

基本上,这些类型足以让我编写验证算法。

我想指出另一个有趣的功能。 Eclipse中Element接口的实现位于org.eclipse ...包中,例如,表示方法的元素的类型为org.eclipse.jdt.internal.compiler.apt.model.ExecutableElementImpl 。 这使我想到了这些接口是由每个IDE独立实现的。

验证算法


首先,您需要创建注释本身。 关于它的文章已经很多了(例如, 在此处 ),因此我将不对其进行详细介绍。 我只能说对于我们的示例,我们需要添加两个注释@Target@Retention 。 第一个指示我们的注释只能应用于该方法,第二个指示该注释将仅存在于源代码中。

必须指定注释,该接口实现注释方法(注释所应用的方法)。 这可以通过两种方式完成:要么使用字符串指定接口的全名,例如@Implement("com.ds.IInterface") ,要么直接传递接口类: @Implement(IInterface.class) 。 第二种方法显然更好。 在这种情况下,编译器将监视正确的接口名称。 顺便说一句,如果调用此成员值(),则在向方法中添加注释时,无需显式指定此参数的名称。

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

然后,乐趣开始了-创建处理器。 在处理方法中,我们获得所有带注释元素的列表。 然后,我们获得注释本身及其含义-指定的接口。 通常,处理器类框架如下所示:

 @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); } } 

我想指出的是,您不能只获得那样的价值注释。 当您尝试调用annotation.value() ,将抛出MirroredTypeException ,但是从中可以获得TypeMirror。 我在这里找到这种作弊方法以及正确的价值收据:

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

检查本身包括三个部分,如果其中至少一个失败,则需要显示错误消息并继续下一个注释。 顺便说一句,您可以使用以下方法显示错误消息:

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

第一步是检查值注释是否为接口。 这里的一切都很简单:

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

接下来,您需要检查带注释的方法所在的类是否实际上实现了指定的接口。 一开始,我愚蠢地用手进行了这项测试。 但是,然后,根据好的建议,我查看了Types,并在其中找到Types.isSubtype()方法,该方法将检查整个继承树,如果存在指定的接口,则返回true。 重要的是,与第一个选项不同,它可以与泛型类型一起使用。

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

最后,您需要确保该接口具有与带注释的方法具有相同签名的方法。 我想使用Types.isSubsignature()方法,但是,不幸的是,如果该方法具有类型参数,它将无法正常工作。 因此,我们卷起袖子,用手写下所有支票。 我们又有三个。 好吧,更准确地说,方法签名由三部分组成:方法的名称,返回值的类型和参数列表。 您需要遍历接口的所有方法,并找到通过所有三项检查的方法。 最好不要忘记该方法可以从另一个接口继承并递归地对基础接口执行相同的检查。

调用必须放在process方法的循环末尾,如下所示:

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

HaveMethod()方法本身看起来像这样:

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

看到问题了吗? 不行吗 她在那里。 事实是,我找不到一种方法来获取通用接口的实际类型参数。 例如,我有一个实现Predicate接口的类:
 MyPredicate implements Predicate&ltString&gt { @Implement(Predicate.class) boolean test(String t) { return false; } } 

分析类中的方法时,参数的类型为String ,而在接口中为T ,并且所有尝试获取String而不是String尝试都不会导致任何结果。 最后,我没有想到的就是忽略类型参数。 该检查将通过任何实际的类型参数传递,即使它们不匹配。 幸运的是,如果该方法没有默认实现,并且未在基类中实现,则编译器将引发错误。 但是,如果有人知道如何解决这个问题,我将非常感谢您的提示。

连接到Eclipse


就个人而言,我爱Eclipce,在我的实践中我只使用它。 因此,我将描述如何将处理器连接到此IDE。 为了使Eclipse能够看到处理器,您需要将其打包到单独的.JAR中,批注本身也将包含在其中。 在这种情况下,您需要在项目中创建META-INF / services文件夹,并在其中创建javax.annotation.processing.Processor文件并指定处理器类的全名: ds.magic.annotations.compileTime.ImplementProcessor ,在我的示例中。 为了以防万一,我将提供一个屏幕截图,但是当对我没有任何帮助时,我几乎开始对项目的结构感到不快。

图片

接下来,收集.JAR并将其连接到您的项目,首先将其作为常规库,以使注释在代码中可见。 然后,我们连接处理器( 此处有更多详细信息)。 为此,请打开项目属性,然后选择:

  1. Java编译器->注释处理,然后选中“启用注释处理”框。
  2. Java编译器->注释处理->工厂路径,选中“启用项目特定设置”复选框。 然后单击“添加JAR ...”,然后选择以前创建的JAR文件。
  3. 同意重建项目。

总结


所有这些以及在Eclipse项目中都可以在GitHub上看到。 在撰写本文时,如果可以将注释称为该类,则只有两个类:Implement.java和ImplementProcessor.java。 我想您已经猜到了他们的目的。

也许这个注释对某些人似乎没有用。 也许是。 但就我个人而言,当方法名称不太适合该类目的时,我自己使用它而不是@Override 。 到目前为止,我还没有摆脱她的愿望。 总的来说,我为自己做了一个注释,这篇文章的目的是展示我正在攻击的耙子。 我希望我做到了。 谢谢您的关注。

PS。 感谢ohotNik_alexComdiv的用户在修复错误方面的帮助。

Source: https://habr.com/ru/post/zh-CN414715/


All Articles