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

注解處理器,顧名思義:就是專門處理注解的處理器.

上一篇 我們對java的注解進行了比較詳細的認識和了解,系統(tǒng)提供的注解是由系統(tǒng)的注解處理器來執(zhí)行處理,而我們自定義的注解系統(tǒng)是沒法幫我們直接處理的,這樣我們自定義的注解就失去了本身存在的意義了,實則不然,我們自定義的注解需要我們自己來處理,比如通過APT技術、反射就可以來處理我們自己的注解。

今天我們從APT技術的基礎來進行了解。

APT ( Annotation Processing Tool),他是javac的一個工具,中文意思為注解處理器。他可以用>來在 編譯時 掃描和處理注解。通過APT我們可以獲取到注解和被注解對象的相關信息,在拿>到這些信息后我們可以根據(jù)需求來自動生成一些代碼,省去了不少手動編寫代碼。

需要強調(diào)的一點:<u>獲取注解和生成一些代碼都是在編譯時完成的。</u>

我們先來看看JDK提供給我們的注解處理器:

注解處理器類分析思維導圖.png

手把手教你自定義注解處理器:

背景:Android Studio新建工程 CustomAnnotation

1. 注解Annotation Module :CusAnnotation

<!--Java Library-->

自定義注解  BindView,保留級別是Class ,作用域是 Field ,  參數(shù)是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<>();//存放解析的注解節(jié)點信息   注解名-包名-類名  作為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來區(qū)分
                    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]);

            //用來存放創(chuàng)建的方法,因為如果同時存在多個注解,可以穿記得方法會比較多
            List<MethodSpec> methods = new ArrayList<>();
            if (subKeys[0].equals(BindView.class.getTypeName())) {
                //創(chuàng)建綁定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())) {
                //創(chuàng)建綁定點擊事件的方法
                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 根據(jù)類名和包名進行分組存放,便于在不各自不同的文件中存放對應的代碼
             */
            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文件,文件內(nèi)容為自定義注解的包名+注解處理器名。如:

    com.com.leon.processor.AnnotationProcessor

? 注:

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

  2. 網(wǎng)上說可以使用com.google.auto.service:auto-service:1.0-rc6 ,參考:http://m.itdecent.cn/p/402788e70b39

3. 注解處理器綁定

? 截止目前為止,注解和注解處理器都已經(jīng)完成了,那么怎么樣才可以讓我們得注解處理器工作起來呢。我們新建一個module 來專門完成注解與注解處理器的綁定工作。

?

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

api project(':CusAnnotation')

3.2 ) 創(chuàng)建綁定類:CustomButterKnife

public class CustomButterKnife {

     public static void bind(Activity activity) {
        String activityName = activity.getClass().getName();
        String genrateClass = activityName + "_customButterKnife";
        try {
            //調(diào)用構造器來實現(xiàn)bind
            Object inject = Class.forName(genrateClass).getConstructor().newInstance();

            /*
             * TODO 調(diào)用生成的java類進行onclick事件的注解綁定
             */
            if (null != inject) {
                Method bindViewByIdMethond = inject.getClass().getDeclaredMethod("BindViewById", activity.getClass());
                bindViewByIdMethond.invoke(inject, activity);
            }

            /*
             * TODO 調(diào)用生成的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 有無法調(diào)試注解處理器。不要慌,坑我已經(jīng)踩過了,接著往下走。。。。

5. Debug調(diào)試注解處理器

注解處理器不是不可以調(diào)試,

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調(diào)試注解處理器

? 選擇目標設備APT ,在自定義的注解處理器中打斷點,然后 Clean Project , 然后Make Project 即可進入斷點。

5.4調(diào)試注解處理器信息

Debug調(diào)試信息

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

生成的java文件

以上測試代碼的demo地址:CustomAnnotation 更新中。。。

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內(nèi)容