1. 程式人生 > >java註解之編譯時註解RetentionPolicy.CLASS 基本用法

java註解之編譯時註解RetentionPolicy.CLASS 基本用法

1 前言

我們知道,在日常開發中我們常用的兩種註解是執行時註解和編譯時註解,執行時註解是通過反射來實現註解處理器的,對效能稍微有一點損耗,而編譯時註解是在程式編譯期間生成相應的代理類,替我們完成某些功能。今天我們來講解一下編譯時註解以及寫一個小例子,以便加深對編譯時註解的理解。

2 編譯時註解

編譯時註解(RetentionPolicy.CLASS),指@Retention(RetentionPolicy.CLASS)作用域class位元組碼上,生命週期只有在編譯器間有效。編譯時註解註解處理器的實現主要依賴於AbstractProcessor來實現,這個類是在javax.annotation.processing包中,同時為了我們自己生成java原始檔方便,我們還需要引入一些第三方庫,主要包括
javapoet 用於生成java原始檔,可參考

https://github.com/square/javapoet
auto-service 主要用於生成一些輔助資訊,例如META-INF/services 一些資訊等

編譯時註解的核心就是實現AbstractProcessor的process()方法,一般來說主要有以下兩個步驟
1 蒐集資訊,包括被註解的類的類資訊,方法,欄位等資訊,還有註解的值
2 生成對應的java原始碼,主要根據上一步的資訊,生成響應的程式碼

下面我們來實際的寫一個編譯時註解的例子

3 編譯時註解例子

假設我們寫這樣一個類註解

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

這個Bind註解只能作用與類上面,一般我們作用於某個類上面了,我們做什麼工作呢,我們新建一個類,這個類的內容大體如下:

public class XXX$$value {
  public int sayHello(int n) {
    return value;
  }
}

其中,XXX是被註解的類名,value是註解的值。
看起來很簡單,對吧?下面我們就開始吧。

1 新建兩個java model,注意不是Android library model
這裡寫圖片描述
model的gradle配置如下

apply plugin: 'java'

為什麼要新建兩個module呢,
(1) 是因為後面再引用的時候,兩者有些不一樣,可以看一下

    api project(':ioc-annotation')
    annotationProcessor project(':ioc-compiler')

(2) 因為兩者的引用方式不同,導致最好把註解處理器和註解分開,這樣我們也能更好的解耦

注意,ioc-compiler需要引用以下兩個開源庫

apply plugin: 'java'

//解決編譯中文亂碼問題
tasks.withType(JavaCompile) {
    options.encoding = "UTF-8"
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])

    compile 'com.google.auto.service:auto-service:1.0-rc4'
    compile 'com.squareup:javapoet:1.7.0'
    compile project(':ioc-annotation')
}

sourceCompatibility = "1.8"
targetCompatibility = "1.8"

2 新建Bind註解,指定註解型別為class

/**
 * @author Created by qiyei2015 on 2018/4/14.
 * @version: 1.0
 * @email: [email protected]
 * @description: Bind註解 指明註解作用域為類
 */
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.CLASS)
public @interface Bind {

    int value();

}

這一步挺好理解的。

3 新建BindProcessor繼承與AbstractProcessor並實現其process方法

/**
 * @author Created by qiyei2015 on 2018/4/15.
 * @version: 1.0
 * @email: [email protected]
 * @description: Bind註解處理器
 */
@AutoService(Processor.class)
public class BindProcessor extends AbstractProcessor{

    /**
     * java原始檔操作相關類,主要用於生成java原始檔
     */
    private Filer mFiler;
    /**
     * 註解型別工具類,主要用於後續生成java原始檔使用
     * 類為TypeElement,變數為VariableElement,方法為ExecuteableElement
     */
    private Elements mElementsUtils;
    /**
     * 日誌列印,類似於log,可用於輸出錯誤資訊
     */
    private Messager mMessager;

    private static final ClassName sClassName = ClassName.get("com.qiyei.ioc.api", "Test");

    /**
     * 初始化,主要用於初始化各個變數
     * @param processingEnv
     */
    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);

        mFiler = processingEnv.getFiler();
        mElementsUtils = processingEnv.getElementUtils();
        mMessager = processingEnv.getMessager();
    }

    /**
     * 支援的註解型別
     * @return
     */
    @Override
    public Set<String> getSupportedAnnotationTypes() {

        Set<String> typeSet = new LinkedHashSet<>();

        typeSet.add(Bind.class.getCanonicalName());

        return typeSet;
    }

    /**
     * 支援的版本
     * @return
     */
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    /**
     *
     * 1.蒐集資訊
     * 2.生成java原始檔
     * @param annotations
     * @param roundEnv
     * @return
     */
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {

        if (!annotations.isEmpty()){
            //獲取Bind註解型別的元素,這裡是類型別TypeElement
            Set<? extends Element> bindElement = roundEnv.getElementsAnnotatedWith(Bind.class);

            try {
                generateCode(bindElement);
            } catch (IOException e) {
                e.printStackTrace();
            }
            return true;
        }

        return false;
    }

    /**
     *
     * @param elements
     */
    private void generateCode(Set<? extends Element> elements) throws IOException{

        for (Element element : elements){

            //由於是在類上註解,那麼獲取TypeElement
            TypeElement typeElement = (TypeElement) element;

            //獲取全限定類名
            String className = typeElement.getQualifiedName().toString();

            mMessager.printMessage(Diagnostic.Kind.WARNING,"className:" + className);

            //獲取包路徑
            PackageElement packageElement = mElementsUtils.getPackageOf(typeElement);
            String packageName = packageElement.getQualifiedName().toString();

            mMessager.printMessage(Diagnostic.Kind.WARNING,"packageName:" + packageName);

            //獲取用於生成的類名
            className = getClassName(typeElement,packageName);

            //獲取註解值
            Bind bindAnnotation = typeElement.getAnnotation(Bind.class);
            int value = bindAnnotation.value();
            System.out.println("value:" + value);

            //生成方法
            MethodSpec.Builder methodBuilder = MethodSpec
                    .methodBuilder("sayHello")
                    .addModifiers(Modifier.PUBLIC)
                    .addParameter(TypeName.INT,"n")
                    .returns(TypeName.INT);

            //$L表示字面量 $T表示型別
            methodBuilder.addStatement("return $L",value);

            //生成的類
            TypeSpec type = TypeSpec
                    .classBuilder(className + "$$" + value)
                    .addModifiers(Modifier.PUBLIC)
                    .addMethod(methodBuilder.build())
                    .build();

            //建立javaFile檔案物件
            JavaFile javaFile = JavaFile.builder(packageName,type).build();
            //寫入原始檔
            javaFile.writeTo(mFiler);

        }
    }

    /**
     * 根據type和package獲取類名
     * @param type
     * @param packageName
     * @return
     */
    private static String getClassName(TypeElement type, String packageName) {
        int packageLen = packageName.length() + 1;
        return type.getQualifiedName().toString().substring(packageLen)
                .replace('.', '$');
    }
}

其他方法暫時不做講解,邏輯都很簡單,主要講解generateCode(Set

            //獲取全限定類名
            String className = typeElement.getQualifiedName().toString();
            //獲取包路徑
            PackageElement packageElement = mElementsUtils.getPackageOf(typeElement);
            String packageName = packageElement.getQualifiedName().toString();

(2) 構造如上所示的java原始檔

            //生成方法
            MethodSpec.Builder methodBuilder = MethodSpec
                    .methodBuilder("sayHello")
                    .addModifiers(Modifier.PUBLIC)
                    .addParameter(TypeName.INT,"n")
                    .returns(TypeName.INT);

            //$L表示字面量 $T表示型別
            methodBuilder.addStatement("return $L",value);

            //生成的類
            TypeSpec type = TypeSpec
                    .classBuilder(className + "$$" + value)
                    .addModifiers(Modifier.PUBLIC)
                    .addMethod(methodBuilder.build())
                    .build();

這裡主要是用到javapoet,關於為什麼用這個開源庫,首先是用法比較簡單,其次很多開源框架都在用,包括ButterKnife等。關於生成java程式碼請一定參考https://github.com/square/javapoet 裡面有很詳細的說明。

(3) 將java原始碼寫入檔案

            //建立javaFile檔案物件
            JavaFile javaFile = JavaFile.builder(packageName,type).build();
            //寫入原始檔
            javaFile.writeTo(mFiler);

(4) 使用Bind註解,然後編譯生成程式碼
我們在我們的一個Activity上使用Bind註解,如下:

/**
 * @author Created by qiyei2015 on 2017/8/28.
 * @version: 1.0
 * @email: [email protected]
 * @description:
 */
@Bind(10)
public class MainActivity extends BaseSkinActivity {

    private RecyclerView mRecyclerView;

    private static final int MY_PERMISSIONS_REQUEST_WRITE_STORE = 1;

    /**
     * ViewModel
     */
    private MainMenuViewModel mMenuViewModel;

    private MainMenuAdapter mMenuAdapter;

    /**
     * 標題欄
     */
    private CommonTitleBar mTitleBar = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
//        PermissionManager.requestAllDangerousPermission(this);
        initData();
        initView();
        LogManager.i(TAG,"onCreate");
    }
    .......

}

然後編譯,生成的程式碼如下:
程式碼路徑:\build\generated\source\apt\baidu\debug\com\qiyei\appdemo\ui\activity

package com.qiyei.appdemo.ui.activity;

public class MainActivity$$10 {
  public int sayHello(int n) {
    return 10;
  }
}

可以看到,符合我們的預期,這樣我們一個簡單的編譯時註解就已經完成了

注:雖然我們的程式碼如約生成了,但是我們並沒有用生成的程式碼,所以我們還應該寫一個api來使用生成的java原始碼檔案,不過這都是後話了,下次再介紹。