javac 编译期拓展之 实现 CallSuper 注解功能
背景:
元旦之前,就和朋友探讨了这么一个问题。比如我在一个父类的 a 方法里做了一些逻辑,这个逻辑是必须存在的,假如现在子类要重写这个 a 方法,
那么他就需要先调用父类的 a 方法。如果他不调用那么就应该编译报错。
解决这个问题其实有两种方法:
- 将这个问题绕过去,比如将父类的 a 方法直接设置为final 不让子类重写,然后再定义一个额外的方法 b 让子类重写。
在b 之前父类的必要逻辑已经做了
public class Parent{
final void a(){
//做必要的操作,然后调用b
b();
}
void b(){
}
}
这样的好处是简单。不好的地方是,如果是一些很通用的方法。大家都约定俗成的用的 a 方法,就你这必须得要我重写 b 方法,就破坏了程序一致性,也需要一些额外的文档和沟通成本。
- 第二种方法那就直面问题,比如我在父类的方法上加上一个注解,这个注解就表示你必须在重写的时候先调用父类的方法。比如安卓里面就有 CallSuper 注解。子类重写时没调用父类方法编译就通不过。
-
从简单解决问题来讲,我更推荐方法1,能跑就行了
-
如果要优雅的解决问题,我觉得应该使用方法2。就像 CallSuper 注解一样,既能保证程序的一致性也能保证正确性
平时的一些知识积累,以及后面不断捣鼓,勉强实现了方法2 这么个功能,当然也只是简单实现了,问题还有不少。
查项目代码
接下来简单介绍一下,如何实现一个 javac 编译期间校验 CallSuper 注解的程序
功能介绍: 实现一个程序,提供一个注解 MustCallSuper 注解,其它项目的父类方法加上了此注解后,如果子类重写了此方法但是没在最开始调用super.这个方法,就编译报错。
如下编译就应该报错:
public class Parent{ @MustCallSuper protected void test(String name, int age, Integer lo){ } } public class Son extends Parent { @Override protected void test(String name, int age, Integer lo) { //super.test(name, age, lo); 没调用父类方法编译报错 System.out.println(123); } }
基础条件: 可以基于java自带的 注解处理器(Annotation Processing Tool),在此拓展功能
其它:为了使用方便使用,创建一个maven项目,然后打包,写入 spi 等信息,借助 spi 自动完成 注解处理器调用。
问题注意:当前只支持标准的maven 项目 src/main/java 这种目录结构的项目,只支持 java8
开发步骤简介:
- 创建 MustCallSuper 注解和 注解处理器
- 在注解处理器中,拿到当前类的父类的信息,然后遍历父类的方法,找到有 MustCallSuper 注解的方法。(如果需要支持多层继承的情况,可以递归一直向上找,直到找到java.lang.Object)
- 遍历当前类的方法,判断是否有重写1中的有 MustCallSuper 注解的方法,如果没有,流程结束,如果有找到重写的方法
- 利用 javaparser 解析源码,对重写的方法进行判断,判断第一行是否是super.这个方法。
- 校验不通过,抛异常
抽象实现(简单介绍下过程,具体细节可以查看项目源码):
- 新建一个项目,创建 MustCallSuper 注解
/**
* @author rxf113
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface MustCallSuper {
}
- 创建注解处理器,继承
javax.annotation.processing.AbstractProcessor
重写 process 方法,这个方法包含了所有步骤,具体代码看源码。
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
if (!roundEnv.processingOver()) {
for (Element rootElement : roundEnv.getRootElements()) {
TypeElement typeElement = (TypeElement) rootElement;
//1. 获取父类的有自定义CallSuper注解的方法
List<ExecutableElement> methodWithCallSuperList = getSupClassMethodsWithCusAnnotation(typeElement);
if (!methodWithCallSuperList.isEmpty()) {
//2. 获取当前类重写了父类方法的当前类方法
List<ExecutableElement> overrideMethodBySuperMethod = getOverrideMethodBySuperMethod(methodWithCallSuperList, typeElement);
if (!overrideMethodBySuperMethod.isEmpty()) {
String classSourceCode = getClassSourceCode(typeElement);
for (ExecutableElement executableElement : overrideMethodBySuperMethod) {
//3. 判断源码中, 方法第一行是否有调用 super.xxxx
boolean b = checkFirstStatementCallSuper(classSourceCode, executableElement);
if (!b) {
//4. 校验不通过
throw new MustCallSuperException("class: " + typeElement.getQualifiedName().toString()
+ " method: " + executableElement.getSimpleName().toString()
+ " 第一行没调用父类的此方法");
}
}
}
}
}
}
return false;
}
- 加入 spi 文件,方便在 javac 的时候,自动执行此 processor
-
打包项目:先清空 spi文件javax.annotation.processing.Processor 中的内容,然后
mvn compile
编译,然后写入
com.rxf113.MustCallSuperProcessor 再mvn package install
。(这里一定得先清空,编译完了再写入内容再打包) -
测试,新建一个maven项目,引入依赖,然后写两个类,发现编译是可以正常判断的。
public class Parent{
@MustCallSuper
protected void test(String name, int age, Integer lo){
}
}
public class Son extends Parent {
@Override
protected void test(String name, int age, Integer lo) {
//super.test(name, age, lo);
System.out.println(123);
}
}
MustCallSuper annotation process exception, msg: class: test.reader.Son method: test 第一行没调用父类的此方法
但是对于引入了 maven-compile-plugin 的项目,没生效