美文网首页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