自定义编译期注解

作者: 飘飘然的影子 | 来源:发表于2022-09-04 20:42 被阅读0次

    什么是注解

    注解(Annotation),也叫元数据(即描述数据的数据),一种代码级别的说明。

    它是JDK1.5及以后版本引入的一个特性,与类、接口、枚举是在同一个层次。它可以声明在包、类、字段、方法、局部变量、方法参数等的前面,用来对这些元素进行说明,注释。

    起初作用基本类似是加强型的注释语言,为了编译检查,增强可阅读性,后续也可以用来生成代码,起到辅助项目的作用。

    基本注解

    内置注解

    java内置注解,举个🌰:

    @Override:让编译器检查被标记的方法,保证其重写了父类的某一个方法。此注解只能标记方法。源码如下:

    
    @Target(ElementType.METHOD)
    
    @Retention(RetentionPolicy.SOURCE)
    
    public @interface Override {
    
    }
    

    元注解

    用来给其他注解打标签的注解,即用来解释其他注解的注解,举两个🌰:

    @Retention:用于指定被此元注解标注的注解的保留时长,即作用域

    RetentionPolicy.SOURCE: :在源文件中有效(即源文件保留),例如@Override

    RetentionPolicy.CLASS::注解信息保留在class文件中,但是虚拟机不会持有其信息,可用来编译期注解

    RetentionPolicy.RUNTIME::注解信息保留在class文件中,而且虚拟机也会持有此注解信息,所以可以通过反射的方式获得注解信息,用来运行时注解

    编译期注解: 在class文件中可用,能够存于编译之后的字节码之中,该注解的注册信息会保留在.java源码里和.class文件里,在执行的时候,会被Java虚拟机丢弃,不会加载到虚拟机中。

    运行期注解:Java虚拟机在运行期保留注解信息,可以通过反射机制读取注解的信息(.java源码,.class文件和执行的时候都有注解的信息)

    @Target:注解对象的作用范围

    CONSTRUCTOR:用于描述构造器

    FIELD:用于描述字段

    LOCAL_VARIABLE:用于描述局部变量

    METHOD:用于描述方法

    PACKAGE:用于描述包

    PARAMETER:用于描述参数

    TYPE:用于描述类、接口(包括注解类型) 或enum声明

    自定义注解

    编译期注解总体结构

    编译时注解 + APT (Annotation Processing Tool)+ JavaPoet(自定义Java源文件) + auto-service(处理器注册)

    APT(Annotation Processing Tool) 是一种处理注释的工具, 它对源代码文件进行检测找出其中的 Annotation,使用 Annotation 进行额外的处理。处理器在处理 Annotation 时可以根据源文件中的 Annotation 生成额外的源文件和其它的文件 (文件具体内容由 Annotation 处理器的编写者决定),APT 还会编译生成的源文件和原来的源文件,将它们一起生成 class 文件。

    JavaPoet是一款可以自动生成Java文件的第三方库,简洁易懂的API,让繁杂、重复的Java文件,自动化生成,简化流程。

    auto-service:编译时注解处理器AbstractProcessor需要注册到JVM中。使用@Autoservice注解就完成了注册,替代以前的手动配置(🌰:在Java Library项目中,在resources资源文件夹下创建META-INF.services,然后在该路径下创建名为javax.annotation.processing.Processor的文件,在该文件中配置(处理器的完整路径,每行一个)需要启用的注解处理器)。

    编译期注解处理流程

    screenshot-20220904-203719.png

    编译期注解实践

    写了ShadowAptDemo,实现类似butterknife @BindView方法,根据注解自动获取view对象

    c7101025-1714-43cc-bf29-2d232875ccbd.png

    app:主项目

    apt_knife_api:功能API,给主项目依赖调用,Android lib

    apt_complier:注解处理器,AbstractProcessor是javax.annotation.processing包下的一个抽象类,Android平台是基于OpenJDK的,而OpenJDK中不包含AbstractProcessor的相关代码,所以是一个Java lib

    apt_annotations:注解的定义,需要给到注解处理器以及app主项目使用,所以也是Java lib

    JDK的版本要一致,不然编译出错

    注解的定义

    apt_annotations:

    
    @Retention(RetentionPolicy.CLASS)
    
    @Target(ElementType.FIELD)
    
    public @interface ShadowBindView {
    
        int value();
    
    }
    

    定义一个编译期作用域的注解 ShadowBindView,用于描述字段

    里面包含一个方法用于接收ResID

    注解处理器

    apt_complier:

    需要配置相关三方库的依赖

    dependencies{
    
        //处理器注册
    
        implementation 'com.google.auto.service:auto-service:1.0-rc6'
    
        annotationProcessor 'com.google.auto.service:auto-service:1.0-rc6'
    
        implementation 'com.google.auto:auto-common:0.10'
    
        //java文件生成
    
        implementation 'com.squareup:javapoet:1.11.1'
    
        //注解依赖
    
        implementation project(':apt_annotations')
    
    }
    

    自定义注解处理器

    进行相关变量的初始化

    
    @AutoService(Processor.class) //使用auto进行注册注解处理器,只需要在这里声明即可
    
    @SupportedSourceVersion(SourceVersion.RELEASE_7) //指定jdk版本
    
    //声明需要处理的注解
    
    @SupportedAnnotationTypes("com.example.apt_annotations.ShadowBindView")
    
    public class ShadowProcessor extends AbstractProcessor {
    
    //Processor工具元素
    
    private Elements processorElements;
    
    //生成文件
    
    private Filer filer;
    
    //自定义注解类集合
    
    private Map<String, AnnotationClass> mAnnotatedClassMap;
    
    //注解处理器初始化的时候回调
    
    @Override
    
    public synchronized void init(ProcessingEnvironment processingEnv) {
    
        super.init(processingEnv);
    
        //返回包含用于操作Element的工具方法元素
    
        processorElements = processingEnv.getElementUtils();
    
        filer = processingEnv.getFiler();
    
        mAnnotatedClassMap = new HashMap<>();
    
    }
    
    ....
    

    AbstractProcessor是抽象类,有一个必须实现的抽象方法process方法,是注解处理器处理注解时候进行回调,需要在这里进行扫描和处理注解。

    返回值逻辑

    true,则这些注解已声明,且不要求后续Processor处理它们

    false,则这些注解未声明,且可能要求后续Processor处理它们

    RoundEnvironment,是指这轮处理注解所需要的元素信息,常用方法有:

    getElementsAnnotatedWith,返回包含指定注解类型的元素的集合

    errorRaised,上一轮注解处理器是否产生错误

    Element,基础元素, 以下是继承它的元素

    VariableElement ,代表成员变量

    ExecutableElement ,代表类中的方法

    TypeElement ,代表类

    PackageElement ,代表Package

    
    ShadowProcessor
    
    @Override
    
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
    
    System.out.println("process: " + roundEnv.toString());
    
        mAnnotatedClassMap.clear();
    
        //创建自定义注解处理类
    
        try {
    
            processBindView(roundEnv);
    
        } catch (Exception e) {
    
            e.printStackTrace();
    
        }
    
        //将自定义注解处理类,写入文件
    
        for (AnnotationClass annotationClass : mAnnotatedClassMap.values()) {
    
            try {
    
                annotationClass.generateFiler().writeTo(filer);
    
            } catch (IOException e) {
    
                e.printStackTrace();
    
            }
    
        }
    
        return true;
    
    }
    
    private void processBindView(RoundEnvironment roundEnv) {
    
        //遍历包含ShadowBindView注解类型的元素的集合
    
        for (Element element : roundEnv.getElementsAnnotatedWith(ShadowBindView.class)) {
    
            //注解类,AnnotationClass后续会详细讲到
    
            AnnotationClass annotationClass = createAnnotationClass(element);
    
            //构建注解变量类,后续会详细讲到
    
            BindViewField bindViewField = new BindViewField(element);
    
            //添加对应的变量
    
            annotationClass.addField(bindViewField);
    
    System.out.println("processBindView annotatedClass: " + annotationClass);
    
    System.out.println("processBindView bindViewField: " + bindViewField);
    
        }
    
    }
    
    //组建注解类
    
    private AnnotationClass createAnnotationClass(Element element) {
    
        //获取包含ShadowBindView注解的类元素
    
        TypeElement typeElement = (TypeElement) element.getEnclosingElement();
    
        String fullName = typeElement.getQualifiedName().toString();
    
    System.out.println("createAnnotationClass typeElement: " + typeElement);
    
        AnnotationClass annotationClass = mAnnotatedClassMap.get(fullName);
    
        //集合中不存在,才添加到集合中,去重
    
        if (annotationClass == null) {
    
            //创建注解类 将类元素typeElement,工具元素processorElements传入
    
            annotationClass = new AnnotationClass(typeElement, processorElements);
    
            mAnnotatedClassMap.put(fullName, annotationClass);
    
        }
    
        return annotationClass;
    
    }
    

    创建注解变量类,用来存储注解变量数据

    
    public class BindViewField {
    
        //变量元素
    
        private VariableElement mVariableElement;
    
        private int mResId;
    
        public BindViewField(Element element) {
    
            //判断类型,是否是变量元素
    
    if (element.getKind() != ElementKind.FIELD) {
    
                return;
    
            }
    
            mVariableElement = (VariableElement) element;
    
            //获取自定义注解类型的变量
    
            ShadowBindView bindView = mVariableElement.getAnnotation(ShadowBindView.class);
    
            //获取view对象Rid
    
            mResId = bindView.value();
    
        }
    
        public int getResId() {
    
            return mResId;
    
        }
    
        public Name getFieldName() {
    
            return mVariableElement.getSimpleName();
    
        }
    
      //变量对应的类型
    
        public TypeMirror getFieldType() {
    
            return mVariableElement.asType();
    
        }
    
    }
    

    在注解类之前,先创建一个接口IViewBinder,它是用来衔接api和生成类文件的通信

    就定义了两个需要实际使用的方法,后面构建类的时候会实现这个接口

    apt_knife_api

    public interface IViewBinder<T> {
    
        void bindView(T host);
    
        void unbindView(T host);
    
    }
    

    创建注解类,用来存储类信息和构建类文件

    
    public class AnnotationClass {
    
    static final ClassNameINTERFACE = ClassName.get("com.example.apt_knife_api", "IViewBinder");
    
        //注解类的BindView变量集合
    
        private final ArrayList<BindViewField> mFields;
    
        //注解类的元素
    
        private final TypeElement mTypeElement;
    
        //Processor元素
    
        private final Elements mProcessorElements;
    
        public AnnotationClass(TypeElement typeElement, Elements elements) {
    
            mFields = new ArrayList<>();
    
            mTypeElement = typeElement;
    
            mProcessorElements = elements;
    
        }
    
    /**
    
        * 添加BindView变量
    
        *
    
        * @param field
    
        */
    
        public void addField(BindViewField field) {
    
            mFields.add(field);
    
        }
    
    /**
    
        * 利用javaPoet生成对应的.java代码
    
        *
    
        * @return
    
        */
    
        public JavaFile generateFiler() {
    
            //生成java方法bindView
    
    MethodSpec.Builder bindViewBuidler = MethodSpec.methodBuilder("bindView")
    
    .addModifiers(Modifier.PUBLIC)//public
    
                    .addAnnotation(Override.class)//接口的复写方法
    
    .addParameter(TypeName.get(mTypeElement.asType()), "host");//参数
    
            //添加bindView方法的处理解析
    
            for (BindViewField field : mFields) {
    
                //方法参数
    
              bindViewBuidler.addStatement("host.$N = ($T)(host.findViewById($L))"
    
                        , field.getFieldName()
    
    , ClassName.get(field.getFieldType())
    
                        , field.getResId());
    
            }
    
            //生成java方法unbindView:
    
    MethodSpec.Builder unbindViewBuilder = MethodSpec.methodBuilder("unbindView")
    
    .addModifiers(Modifier.PUBLIC)//public
    
                    .addAnnotation(Override.class)//接口的复写方法
    
    .addParameter(TypeName.get(mTypeElement.asType()), "host");//添加参数
    
            //添加unbindView方法的处理解析
    
            for (BindViewField field : mFields) {
    
          unbindViewBuilder.addStatement("host.$N = null", field.getFieldName());
    
            }
    
            //生成java的类文件(.java的文件),
    
    TypeSpec injectClass = TypeSpec.classBuilder(mTypeElement.getSimpleName() + "$ShadowViewBinder")
    
    .addModifiers(Modifier.PUBLIC)
    
    .addSuperinterface(ParameterizedTypeName.get(INTERFACE,
    
    TypeName.get(mTypeElement.asType())))//类实现的接口名IViewBinder
    
                    .addMethod(bindViewBuidler.build())//添加bindView方法
    
                    .addMethod(unbindViewBuilder.build())//添加unbindView方法
    
                    .build();
    
            //添加包名 使用传入的工具类元素获取包名
    
            String packageName = mProcessorElements.getPackageOf(mTypeElement).getQualifiedName().toString();
    
            //生成JavaFile 然后会用JavaFile.writeTo(filer)写入文件
    
    return JavaFile.builder(packageName, injectClass).build();
    
        }
    
    }
    

    主要有两个方法,一个添加构建好的变量类,用于生成类文件时候生成变量,最后一个是使用javaPoet生成类文件

    使用javaPoet生成类文件的关键步骤注释都有,有个特别值得关注的点是构建方法代码的时候

    //添加方法代码
    bindViewBuidler.addStatement("host.$N = ($T)(host.findViewById($L))", field.getFieldName() ,
     ClassName.get(field.getFieldType()) , field.getResId());
    

    可以看见在写配置代码的时候有多个不同的通配符,分别代表不同的含义,可以简单看一下源码

    private void addArgument(String format, char c, Object arg) {
    
          switch (c) {
    
          //$N表示获取参数的name
    
            case 'N':
    
              this.args.add(argToName(arg));
    
              break;
    
          //$L表示字面意义,原样输出
    
            case 'L':
    
              this.args.add(argToLiteral(arg));
    
              break;
    
          //$S表示转成字符串
    
            case 'S':
    
              this.args.add(argToString(arg));
    
              break;
    
          //$T表示转成类型,并自动import
    
            case 'T':
    
              this.args.add(argToType(arg));
    
              break;
    
            default:
    
              throw new IllegalArgumentException(
    
                  String.format("invalid format string: '%s'", format));
    
          }
    
        }
    
    private String argToName(Object o) {
    
          if (o instanceof CharSequence) return o.toString();
    
          if (o instanceof ParameterSpec) return ((ParameterSpec) o).name;
    
          if (o instanceof FieldSpec) return ((FieldSpec) o).name;
    
          if (o instanceof MethodSpec) return ((MethodSpec) o).name;
    
          if (o instanceof TypeSpec) return ((TypeSpec) o).name;
    
          throw new IllegalArgumentException("expected name but was " + o);
    
        }
    

    调用API

    apt_knife_api:

    创建主项目可以调用的api类ShadowKnife

    public class ShadowKnife {
    
      //缓存类,避免重复反射创建
    
    private static final MapbinderMap = new LinkedHashMap<>();
    
    /**
    
        * 注解绑定
    
        *
    
        * @param host 表示注解 View 变量所在的类,也就是注解类 进行绑定的目标对象
    
        */
    
        public static void bind(Object host) {
    
            String className = host.getClass().getName();
    
            try {
    
                //看下对应的ViewBinder是否存在
    
    IViewBinder binder =binderMap.get(className);
    
                if (binder == null) {
    
                    //不存在则通过 反射 创建一个 然后存入缓存 这个类是通过javapoet生成的
    
    Class aClass = Class.forName(className + "$ShadowViewBinder");
    
                    binder = (IViewBinder) aClass.newInstance();
    
    binderMap.put(className, binder);
    
                }
    
                //使用注解类的进行绑定
    
                if (binder != null) {
    
                    binder.bindView(host);
    
                }
    
            } catch (Throwable e) {
    
                e.printStackTrace();
    
            }
    
        }
    
    /**
    
        * 解除注解绑定
    
        *
    
        * @param host
    
        */
    
        public static void unBind(Object host) {
    
            String className = host.getClass().getName();
    
    IViewBinder binder =binderMap.get(className);
    
            if (binder != null) {
    
                binder.unbindView(host);
    
            }
    
    binderMap.remove(className);
    
        }
    
    }
    

    主项目使用

    先进行模块依赖

    dependencies{
    
        implementation fileTree(include: ['*.jar'], dir: 'libs')
    
        implementation 'com.android.support:appcompat-v7:28.0.0'
    
        //注解处理器依赖
    
        annotationProcessor project(':apt_complier')
    
        implementation project(':apt_annotations')
    
        implementation project(':apt_knife_api')
    
    }
    

    测试api代码

    public class MainActivity extends AppCompatActivity {
    
    @ShadowBindView(R.id.text1)
    
        TextView textView;
    
        @Override
    
        protected void onCreate(Bundle savedInstanceState) {
    
            super.onCreate(savedInstanceState);
    
            setContentView(R.layout.activity_main);
    
            ShadowKnife.bind(this);
    
            textView.setOnClickListener(new View.OnClickListener() {
    
                @Override
    
                public void onClick(View v) {
    
                Toast.makeText(MainActivity.this, "test", Toast.LENGTH_LONG).show();
    
                }
    
            });
    
        }
    
        @Override
    
        protected void onDestroy() {
    
            super.onDestroy();
    
            ShadowKnife.unBind(this);
    
        }
    
    }
    

    然后进行clean,编译,看一下是否生成预期所想的代码

    编译过程打印的日志

    7bd686bd-0a44-4358-bab9-1cf8a6d14078.png

    查看所生成的代码

    695aaaba-22db-47ef-9c33-c05ced0e732f.png

    大功告成!

    相关文章

      网友评论

        本文标题:自定义编译期注解

        本文链接:https://www.haomeiwen.com/subject/gvvmnrtx.html