1. 程式人生 > >利用編譯時註解生成Java原始碼

利用編譯時註解生成Java原始碼

我們在編寫註解的時候,需要指定@Retention,有三個可選值,表示註解會被保留到那個階段。

RetentionPolicy.SOURCE 
     這種型別的Annotations只在原始碼級別保留,編譯時就會被忽略,因此一般用來為編譯器提供額外資訊,以便於檢測錯誤,抑制警告等. 比如@Override @SuppressWarnings

RetentionPolicy.CLASS 
    這種型別的Annotations編譯時被保留,在class檔案中存在,但JVM將會忽略,一般用來生成原始碼,xml檔案等
RetentionPolicy.RUNTIME 
    這種型別的Annotations將被JVM保留,所以它們能在執行時被JVM或其他使用反射機制的程式碼所讀取和使用.

編譯時註解就是針對Retention=RetentionPolicy.CLASS的情況,目前android上有很多框架都使用了編譯時註解,比如Dagger、ButterKnife。利用編譯時註解,實現動態的生成程式碼,大大提升了執行效率。

下面就做個簡單的案例:

    通過定義一個BindView註解,用來幫助我們獲取view,類似ButterKnife中的BindView註解功能一樣。

開發工具使用主流的AndroidStudio。

開發前我們先設計一下module依賴關係:

——InjecterAnnotation

java工程,用來存放註解

——InjecterProcessor

java工程,用來處理我們定義的編譯時註解,實現動態生成程式碼。依賴於InjecterAnnotation

——Injecter

Android library工程,對外提供的註解api,依賴於InjecterAnnotation

——app

Android測試工程,用來測試我們的編譯時註解,依賴於Injecter。

一、建立InjecterAnnotation 工程

建立一個java module,定義我們的註解,並指定Retention=RetentionPolicy.CLASS

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface BindView {
    int value();
}
build.gradle
apply plugin: 'java'

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    sourceCompatibility = "1.7"
    targetCompatibility = "1.7"
}

二、建立InjecterProcessor工程

建立一個java工程,編寫我們的註解處理器

我們編寫的註解處理器,需要繼承javax.annotation.processing.AbstractProcessor

@AutoService(Processor.class)
public final class InjecterProcessor extends AbstractProcessor{

    private Elements elementUtils;
    private Types typeUtils;
    private Filer filer; //生成原始碼
    private Messager messager; //列印資訊
    private static final ClassName VIEW_BINDER = ClassName.get("injecter.api", "ViewBinder");//實現的介面

    private static final String BINDING_CLASS_SUFFIX = "$$ViewBinder";//生成類的字尾 以後會用反射去取

    @Override public synchronized void init(ProcessingEnvironment env) {
        super.init(env);

        elementUtils = env.getElementUtils();
        typeUtils = env.getTypeUtils();
        filer = env.getFiler();
        messager = env.getMessager();
    }

    /**
     * 需要處理的註解,有多少個就新增多少個
     * @return
     */
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> types = new LinkedHashSet<>();
        types.add(BindView.class.getCanonicalName());
        return types;
    }

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

    /**
     * 獲取註解資訊,動態生成程式碼
     * @param annotations
     * @param roundEnv
     * @return
     */
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
       
        //程式碼放到後面來講
        return true;
    }
}	
如上所示,一般需要重寫getSupportedAnnotationTypes,getSupportedSourceVersion,process

getSupportedAnnotationTypes

  新增我們需要處理的註解

getSupportedSourceVersion

 支援的原始碼版本,一般返回SourceVersion.latestSupported()即可。

process

處理註解,動態生成程式碼,這是重點。

該java module的build.gradle檔案定義如下

apply plugin: 'java'

dependencies {
    compile fileTree(include: ['*.jar'], dir: 'libs')
    compile project(':InjecterAnnotation')
    compile 'com.google.auto:auto-common:0.6'
    compile 'com.google.auto.service:auto-service:1.0-rc2'
    compile 'com.squareup:javapoet:1.7.0'

    sourceCompatibility = "1.7"
    targetCompatibility = "1.7"
}
1.com.google.auto.service:auto-service:用來幫助我們自動生成META-INF,該目錄結構如下

META-INF

     --services

         --javax.annotation.processing.Processor

檔案中新增我們的injecter.processor.InjecterProcessor

2.com.squareup:javapoet:java原始碼生成工具

三、建立Injecter工程

提供api

public class Injecter {
    public static void bind(Activity activity){
        String clsName = activity.getClass().getName();
        try {
            Class<?> viewBindingClass = Class.forName(clsName + "$$ViewBinder");
            ViewBinder viewBinder = (ViewBinder)viewBindingClass.newInstance();
            viewBinder.bind(activity);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}

通過Injecter.bind(activity)例項化動態建立的註解類,完成View的查詢

四、Demo

首先在工程根目錄中的build.gradle中新增apt支援

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.2.0'

        classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'

    }
}

allprojects {
    repositories {
        jcenter()
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

在demo module下的build.gradle中新增apt 依賴工程
apply plugin: 'com.android.application'
apply plugin: 'android-apt'

android {
    compileSdkVersion 24
    buildToolsVersion "24.0.3"
    defaultConfig {
        applicationId "com.annotation"
        minSdkVersion 15
        targetSdkVersion 24
        versionCode 1
        versionName "1.0"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    compile fileTree(include: ['*.jar'], dir: 'libs')
    compile project(':Injecter')
    //apt
    apt project(':InjecterProcessor')
}

編寫測試類

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Injecter.bind(this);
    }
    @BindView(R.id.textview)
    TextView textView;
}

終於完成了所有步驟,下面我們看看編譯器是否為我們生存了想要的程式碼,clean...rebuild project


可以看到build...generated....source目錄下多了一個apt目錄,且幫我們生存了一個java類 MainActivity$$ViewBinder.

接下來看看MainActivity$$ViewBinder類中都有什麼

import android.widget.TextView;

import injecter.api.ViewBinder;

public class MainActivity$$ViewBinder<T extends MainActivity> implements ViewBinder<MainActivity> {
  @Override
  public void bind(final MainActivity target) {
    target.textView=(TextView)target.findViewById(2131165185);
  }
}
可以看到我們呼叫Injecter.bind(activity)時,會通過反射建立MainActivity$$ViewBinder 例項,然後呼叫findViewById生成我們需要的View物件。

到此,就完成了編譯時註解的解析,程式碼生成,註解使用。重點還是在於註解處理器,由於篇幅已經很長,大家直接看程式碼吧

程式碼下載