美文网首页Java高级
Java--Annotation之旅(手撕注解处理器)

Java--Annotation之旅(手撕注解处理器)

作者: 初夏的雪 | 来源:发表于2020-11-06 18:17 被阅读0次

    注解处理器,顾名思义:就是专门处理注解的处理器.

    上一篇 我们对java的注解进行了比较详细的认识和了解,系统提供的注解是由系统的注解处理器来执行处理,而我们自定义的注解系统是没法帮我们直接处理的,这样我们自定义的注解就失去了本身存在的意义了,实则不然,我们自定义的注解需要我们自己来处理,比如通过APT技术、反射就可以来处理我们自己的注解。

    今天我们从APT技术的基础来进行了解。

    APT ( Annotation Processing Tool),他是javac的一个工具,中文意思为注解处理器。他可以用>来在 编译时 扫描和处理注解。通过APT我们可以获取到注解和被注解对象的相关信息,在拿>到这些信息后我们可以根据需求来自动生成一些代码,省去了不少手动编写代码。

    需要强调的一点:<u>获取注解和生成一些代码都是在编译时完成的。</u>

    我们先来看看JDK提供给我们的注解处理器:

    注解处理器类分析思维导图.png

    手把手教你自定义注解处理器:

    背景:Android Studio新建工程 CustomAnnotation

    1. 注解Annotation Module :CusAnnotation

    <!--Java Library-->
    
    自定义注解  BindView,保留级别是Class ,作用域是 Field ,  参数是viewId, 
    

    ​ 如下代码:

    @Documented
    @Retention(RetentionPolicy.CLASS)
    @Target(ElementType.FIELD)
    public @interface BindView {
        int viewId() ;
    }
    
    
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.CLASS)
    public @interface ViewOnClick {
        int[] viewId();
    }
    

    2. 注解处理器Annotation Processor Module: CusProcessor

    2.1 ) 在该module的build.gradle 中添加:

    api 'com.squareup:javapoet:1.7.0'  //生成java文件   可以去查一下该库的作用
    api project(':CusAnnotation')  //应用自定义的注解module
    

    2.2 ) 新建AnnotationProcessor.java 文件

    @SupportedAnnotationTypes({"com.leon.annotation.BindView", "com.leon.annotation.ViewOnClick"})
    @SupportedSourceVersion(SourceVersion.RELEASE_8)
    public class AnnotationProcessor extends AbstractProcessor {
    
        private static final String TAG = AnnotationProcessor.class.getSimpleName();
        private Filer filer;
        private Elements elementUtils;
    
        private HashMap<String, List<Element>> mPackageClassSet = new HashMap<>();//存放解析的注解节点信息   注解名-包名-类名  作为key
    
        private HashMap<String, TypeSpec> mFilerTypeSpec = new HashMap<>();//存放生成对应文件的type info    包名-类名  作为key
    
        @Override
        public synchronized void init(ProcessingEnvironment processingEnvironment) {
            super.init(processingEnvironment);
            elementUtils = processingEnv.getElementUtils();
            filer = processingEnv.getFiler();
        }
    
        @Override
        public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
            if (set != null && set.size() > 0) {
                //set中存放的是注解处理器支持并解析的注解类型
                for (TypeElement typeElement : set) {
                    Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(typeElement);
                    for (Element element : elements) {
                        //获取注解所属的类名
                        String className = element.getEnclosingElement().getSimpleName().toString();
                        String packageName = elementUtils.getPackageOf(element).getQualifiedName().toString();
    
                        //使用annotationTypeName , packageName ,ClassName 拼接为mapkey ,中间用-连接,方便split来区分
                        if (!mPackageClassSet.containsKey(typeElement.toString() + "-" + packageName + "-" + className)) {
                            List<Element> subelements = new ArrayList<>();
                            subelements.add(element);
                            /*
                            * TODO 相同注解的存在在一起,便于做方法体时使用
                            */
                            mPackageClassSet.put(typeElement.toString() + "-" + packageName + "-" + className, subelements);
                        } else {
                            mPackageClassSet.get(typeElement.toString() + "-" + packageName + "-" + className).add(element);
                        }
                    }
                }
    
                generateCodeByJavaPoet();
            }
            return false;
        }
    
        private void generateCodeByJavaPoet() {
    
            Iterator entries = mPackageClassSet.entrySet().iterator();
            while (entries.hasNext()) {
                //取出hashmap的元素
                Map.Entry<String, ArrayList<Element>> entry = (Map.Entry<String, ArrayList<Element>>) entries.next();
    
                //解析key ,包名和类名
                String[] subKeys = entry.getKey().split("-");
    
                ClassName t = ClassName.bestGuess(subKeys[2]);
    
                //用来存放创建的方法,因为如果同时存在多个注解,可以穿记得方法会比较多
                List<MethodSpec> methods = new ArrayList<>();
                if (subKeys[0].equals(BindView.class.getTypeName())) {
                    //创建绑定view的方法
                    MethodSpec.Builder bindMethodBuilder = MethodSpec.methodBuilder("BindViewById")
                            .addModifiers(Modifier.PUBLIC)
                            .addParameter(t, "activity");
    
    
                    for (Element element : entry.getValue()) {
                        //注解上的额值
                        int resId = element.getAnnotation(BindView.class).viewId();
                        //添加方法体
                        bindMethodBuilder.addStatement("activity.$L = activity.findViewById($L)", element.getSimpleName(), resId);
                    }
                    methods.add(bindMethodBuilder.build());
                }
                if (subKeys[0].equals(ViewOnClick.class.getTypeName())) {
                    //创建绑定点击事件的方法
                    MethodSpec.Builder onClickMethodSpecBuilder = MethodSpec.methodBuilder("BindViewOnClick")
                            .addModifiers(Modifier.PUBLIC)
                            .addParameter(t, "activity", Modifier.FINAL)
                            .returns(void.class);
    
                    for (Element element : entry.getValue()) {
                        int[] resId = element.getAnnotation(ViewOnClick.class).viewId();
                        for (int i = 0; i < resId.length; i++) {
                            String stateMent = String.format("activity.findViewById(%d).setOnClickListener(new android.view.View.OnClickListener() {" +
                                    "\n @Override" +
                                    "\n public void onClick(android.view.View v) {" +
                                    "\n activity.onClick(%d);" +
                                    "\n }" +
                                    "\n })", resId[i], resId[i]);
                            onClickMethodSpecBuilder.addStatement(stateMent);
                        }
                    }
                    methods.add(onClickMethodSpecBuilder.build());
                }
    
    
                /*
                 * TODO 根据类名和包名进行分组存放,便于在不各自不同的文件中存放对应的代码
                 */
                String filerTypeKey = entry.getKey().substring(entry.getKey().indexOf("-") + 1);
    
                if (mFilerTypeSpec.containsKey(filerTypeKey)) {
                    TypeSpec typeSpec = mFilerTypeSpec.get(filerTypeKey).toBuilder().addMethods(methods).build();
                    mFilerTypeSpec.put(filerTypeKey, typeSpec);
                } else {
                    TypeSpec typeSpec = TypeSpec.classBuilder(String.format(subKeys[2] + "_customButterKnife"))//设置类名
                            .addModifiers(Modifier.PUBLIC).build();//添加修饰符
                    typeSpec = typeSpec.toBuilder().addMethods(methods).build();
                    mFilerTypeSpec.put(filerTypeKey, typeSpec);
                }
    
                if (!entries.hasNext()) {
                    //通过包名-类名和TypeSpec(类)生成一个java文件
                    Set<String> keySet = mFilerTypeSpec.keySet();
                    for (String key : keySet) {
                        String[] split = key.split("-");
                        JavaFile build = JavaFile.builder(split[0], mFilerTypeSpec.get(key)).build();
                        try {
                            //写入到filer中
                            build.writeTo(filer);
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
    }
    

    2.3) 注册自定义的注解处理器

    ​ a. 在main目录下面新建resources 文件夹,

    ​ b. 在resources中新建 'META-INF/services'的文件夹,

    ​ c. 新建命名为:javax.annotation.processing.Processor文件,文件内容为自定义注解的包名+注解处理器名。如:

        com.com.leon.processor.AnnotationProcessor
    

    ​ 注:

    1. 上述每一步的名字都不能错,包括大小写

    2. 网上说可以使用com.google.auto.service:auto-service:1.0-rc6 ,参考:https://www.jianshu.com/p/402788e70b39

    3. 注解处理器绑定

    ​ 截止目前为止,注解和注解处理器都已经完成了,那么怎么样才可以让我们得注解处理器工作起来呢。我们新建一个module 来专门完成注解与注解处理器的绑定工作。

    3.1 ) 引用自定义的注解 build.gradle

    api project(':CusAnnotation')
    

    3.2 ) 创建绑定类:CustomButterKnife

    public class CustomButterKnife {
    
         public static void bind(Activity activity) {
            String activityName = activity.getClass().getName();
            String genrateClass = activityName + "_customButterKnife";
            try {
                //调用构造器来实现bind
                Object inject = Class.forName(genrateClass).getConstructor().newInstance();
    
                /*
                 * TODO 调用生成的java类进行onclick事件的注解绑定
                 */
                if (null != inject) {
                    Method bindViewByIdMethond = inject.getClass().getDeclaredMethod("BindViewById", activity.getClass());
                    bindViewByIdMethond.invoke(inject, activity);
                }
    
                /*
                 * TODO 调用生成的java类进行onclick事件的注解绑定
                 */
                if (null != inject) {
                    Method bindViewOnClickMethod = inject.getClass().getDeclaredMethod("BindViewOnClick", activity.getClass());
                    bindViewOnClickMethod.invoke(inject, activity);
                }
            } catch (InstantiationException | ClassNotFoundException | NoSuchMethodException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            }
        }
    }
    

    4.测试module

    ​ 这一步就是在app module 中使用我们自己的注解。

    public class MainActivity extends AppCompatActivity {
    
       @BindView(viewId = R.id.name)
        TextView mName;
    
        @BindView(viewId = R.id.school)
        TextView mSchool;
    
    
        @Override
        protected int getLayoutId() {
            return R.layout.activity_main;
        }
    
        @Override
        protected void onResume() {
            super.onResume();
            if (mName != null) {
                mName.setText(mName.getText().toString() + "张三");
            }
    
            if (mSchool != null) {
                mSchool.setText(mSchool.getText().toString() + "学校");
            }
        }
    
        @ViewOnClick(viewId = {R.id.name, R.id.school})
        public void onClick(int viewId) {
            if (viewId == R.id.name) {
                Toast.makeText(this, mName.getText().toString(), Toast.LENGTH_SHORT).show();
            }
            if (viewId == R.id.school) {
                Intent intent = new Intent(this, SecondActivity.class);
                startActivity(intent);
            }
        }
    }
    

    记得要在build.gradle中添加注解和注解处理的引用

    implementation 'com.squareup:javapoet:1.7.0'
    annotationProcessor project(":CusProcessor")
    api project (':CustomButterKnife')
    

    ​ 至此,你可以将运行你的程序,自定义的注解处理器可以完成你安排的事情了。

    ​ 笔者刚开始遇到注解无效的情况,那么debug 有无法调试注解处理器。不要慌,坑我已经踩过了,接着往下走。。。。

    5. Debug调试注解处理器

    注解处理器不是不可以调试,

    5.1 修改根目录中的gradle.properties文件

    - org.gradle.jvmargs= -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5006
    - org.gradle.parallel=true
    

    5.2 修改Edit configuation

    EditConfiguation

    ​ 新建Remote ,可以修改名字为APT ,其他配置和我们gradle.properties一致

    Remote新建

    点击OK ,

    5.3调试注解处理器

    ​ 选择目标设备APT ,在自定义的注解处理器中打断点,然后 Clean Project , 然后Make Project 即可进入断点。

    5.4调试注解处理器信息

    Debug调试信息

    d. 查看注解处理器生成的java文件

    生成的java文件

    以上测试代码的demo地址:CustomAnnotation 更新中。。。

    相关文章

      网友评论

        本文标题:Java--Annotation之旅(手撕注解处理器)

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