在Android中使用注解生成Java代碼 AbstractProcessor

前段時間在學(xué)習(xí)Dagger2,對它生成代碼的原理充滿了好奇。google了之后發(fā)現(xiàn)原來java原生就是支持代碼生成的。

通過Annotation Processor可以在編譯的時候處理注解,生成我們自定義的代碼,這些生成的代碼會和其他手寫的代碼一樣被javac編譯。注意Annotation Processor只能用來生成代碼,而不能對原來的代碼進行修改。

實現(xiàn)的原理是通過繼承AbstractProcessor,實現(xiàn)我們自己的Processor,然后把它注冊給java編譯器,編譯器在編譯之前使用我們定義的Processor去處理注解。

AbstractProcessor

AbstractProcessor是一個抽象類,我們繼承它需要實現(xiàn)一個抽象方法process,在這個方法里面去處理注解。然后它還有幾個方法需要我們?nèi)ブ貙憽?/p>

public class MyProcessor extends AbstractProcessor {
    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {...}
    
    @Override
    public Set<String> getSupportedAnnotationTypes() {...}
    
    
    @Override
    public SourceVersion getSupportedSourceVersion() {...}
    
    
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {...}
}
  • init方法是初始化的地方,我們可以通過ProcessingEnvironment獲取到很多有用的工具類

  • getSupportedAnnotationTypes 這個方法指定處理的注解,需要將要處理的注解的全名放到Set中返回

  • getSupportedSourceVersion 這個方法用來指定支持的java版本

  • process 是實際處理注解的地方

在Java 7后多了 SupportedAnnotationTypes 和 SupportedSourceVersion 這個兩個注解用來簡化指定注解和java版本的操作:

@SupportedAnnotationTypes({"linjw.demo.injector.InjectView"})
@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class InjectorProcessor extends AbstractProcessor {
    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {...}
        
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {...}

注冊Processor

編寫完我們的Processor之后需要將它注冊給java編譯器

  1. 在src/main目錄下創(chuàng)建resources/META-INF/services/javax.annotation.processing.Processor文件(即創(chuàng)建resources目錄,在resources目錄下創(chuàng)建META-INF目錄,繼續(xù)在META-INF目錄下創(chuàng)建services目錄,最后在services目錄下創(chuàng)建javax.annotation.processing.Processor文件)。

  2. 在javax.annotation.processing.Processor中寫入自定義的Processor的全名,如果有多個Processor的話,每一行寫一個。

完成后 javax.annotation.processing.Processor 內(nèi)容如下

$ cat javax.annotation.processing.Processor
linjw.demo.injector.InjectorProcessor

在安卓中自定義Processor

我以前在學(xué)習(xí)Java自定義注解的時候?qū)戇^一個小例子,它是用運行時注解通過反射簡化findViewById操作的。但是這種使用運行時注解的方法在效率上是有缺陷的,因為反射的效率很低。

基本上學(xué)安卓的人都知道有個很火的開源庫ButterKnife,它也能簡化findViewById操作,但它是通過編譯時注解生成代碼去實現(xiàn)的,效率比我們使用反射實現(xiàn)要高很多很多。

其實我對ButterKnife的原理也一直很好奇,下面就讓我們也用生成代碼的方式高效的簡化findViewById操作。

創(chuàng)建配置工程

首先在android項目中是找不到AbstractProcessor的,需要新建一個Java Library Module。

Android Studio中按File -> New -> New Module... 然后選擇新建Java Library, Module的名字改為libinjector。

同時在安卓中使用AbstractProcessor需要apt的支持,所以需要配置一下gradle:

1.在 project 的 build.gradle 的 dependencies 下加上 android-apt 支持

...
dependencies {
        classpath 'com.android.tools.build:gradle:2.2.2'
        classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
}
...

2.在 app 的 build.gradle 的開頭加上 "apply plugin: 'com.neenbedankt.android-apt'"

apply plugin: 'com.android.application'
apply plugin: 'com.neenbedankt.android-apt'
...

創(chuàng)建注解

我們在libinjector中創(chuàng)建注解InjectView

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.SOURCE)
public @interface InjectView {
    int value();
}

這個是個修飾Field且作用于源碼的自定義注解。關(guān)于自定義注解的知識可以看看我以前寫的一篇文章《Java自定義注解和動態(tài)代理》。我們用它來修飾View成員變量并保持View的resource id,生成的代碼通過resource id使用findViewById注入成員變量。

創(chuàng)建InjectorProcessor

在libinjector中創(chuàng)建InjectorProcessor實現(xiàn)代碼的生成

@SupportedAnnotationTypes({"linjw.demo.injector.InjectView"})
@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class InjectorProcessor extends AbstractProcessor {
    private static final String GEN_CLASS_SUFFIX = "Injector";
    private static final String INJECTOR_NAME = "ViewInjector";

    private Types mTypeUtils;
    private Elements mElementUtils;
    private Filer mFiler;
    private Messager mMessager;

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

        mTypeUtils = processingEnv.getTypeUtils();
        mElementUtils = processingEnv.getElementUtils();
        mFiler = processingEnv.getFiler();
        mMessager = processingEnv.getMessager();
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(InjectView.class);

        //process會被調(diào)用三次,只有一次是可以處理InjectView注解的,原因不明
        if (elements.size() == 0) {
            return true;
        }

        Map<Element, List<Element>> elementMap = new HashMap<>();

        StringBuffer buffer = new StringBuffer();
        buffer.append("package linjw.demo.injector;\n")
                .append("public class " + INJECTOR_NAME + " {\n");

        //遍歷所有被InjectView注釋的元素
        for (Element element : elements) {
            //如果標(biāo)注的對象不是FIELD則報錯,這個錯誤其實不會發(fā)生因為InjectView的Target已經(jīng)聲明為ElementType.FIELD了
            if (element.getKind()!= ElementKind.FIELD) {
                mMessager.printMessage(Diagnostic.Kind.ERROR, "is not a FIELD", element);
            }

            //這里可以先將element轉(zhuǎn)換為VariableElement,但我們這里不需要
            //VariableElement variableElement = (VariableElement) element;

            //如果不是View的子類則報錯
            if (!isView(element.asType())){
                mMessager.printMessage(Diagnostic.Kind.ERROR, "is not a View", element);
            }

            //獲取所在類的信息
            Element clazz = element.getEnclosingElement();

            //按類存入map中
            addElement(elementMap, clazz, element);
        }

        for (Map.Entry<Element, List<Element>> entry : elementMap.entrySet()) {
            Element clazz = entry.getKey();

            //獲取類名
            String className = clazz.getSimpleName().toString();

            //獲取所在的包名
            String packageName = mElementUtils.getPackageOf(clazz).asType().toString();

            //生成注入代碼
            generateInjectorCode(packageName, className, entry.getValue());

            //完整類名
            String fullName = clazz.asType().toString();

            buffer.append("\tpublic static void inject(" + fullName + " arg) {\n")
                    .append("\t\t" + fullName + GEN_CLASS_SUFFIX + ".inject(arg);\n")
                    .append("\t}\n");
        }

        buffer.append("}");

        generateCode(INJECTOR_NAME, buffer.toString());

        return true;
    }

    //遞歸判斷android.view.View是不是其父類
    private boolean isView(TypeMirror type) {
        List<? extends TypeMirror> supers = mTypeUtils.directSupertypes(type);
        if (supers.size() == 0) {
            return false;
        }
        for (TypeMirror superType : supers) {
            if (superType.toString().equals("android.view.View") || isView(superType)) {
                return true;
            }
        }
        return false;
    }

    private void addElement(Map<Element, List<Element>> map, Element clazz, Element field) {
        List<Element> list = map.get(clazz);
        if (list == null) {
            list = new ArrayList<>();
            map.put(clazz, list);
        }
        list.add(field);
    }

    private void generateCode(String className, String code) {
        try {
            JavaFileObject file = mFiler.createSourceFile(className);
            Writer writer = file.openWriter();
            writer.write(code);
            writer.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 生成注入代碼
     *
     * @param packageName 包名
     * @param className   類名
     * @param views       需要注入的成員變量
     */
    private void generateInjectorCode(String packageName, String className, List<Element> views) {
        StringBuilder builder = new StringBuilder();
        builder.append("package " + packageName + ";\n\n")
                .append("public class " + className + GEN_CLASS_SUFFIX + " {\n")
                .append("\tpublic static void inject(" + className + " arg) {\n");

        for (Element element : views) {
            //獲取變量類型
            String type = element.asType().toString();

            //獲取變量名
            String name = element.getSimpleName().toString();

            //id
            int resourceId = element.getAnnotation(InjectView.class).value();

            builder.append("\t\targ." + name + "=(" + type + ")arg.findViewById(" + resourceId + ");\n");
        }

        builder.append("\t}\n")
                .append("}");

        //生成代碼
        generateCode(className + GEN_CLASS_SUFFIX, builder.toString());
    }
}

注冊InjectorProcessor

在libinjector的src/main目錄下創(chuàng)建resources/META-INF/services/javax.annotation.processing.Processor文件注冊InjectorProcessor:

# 注冊InjectorProcessor
linjw.demo.injector.InjectorProcessor

使用InjectView注解

我們在Activity中使用InjectView修飾需要賦值的View變量并且用ViewInjector.inject(this);調(diào)用生成的掉初始化修飾的成員變量。這里有兩個Activity都使用了InjectView去簡化findViewById操作:

public class MainActivity extends AppCompatActivity {
    @InjectView(R.id.label)
    TextView mLabel;

    @InjectView(R.id.button)
    Button mButton;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        //使用findViewById注入被InjectView修飾的成員變量
        ViewInjector.inject(this);

        // ViewInjector.inject(this) 已經(jīng)將mLabel和mButton賦值了,可以直接使用
        mLabel.setText("MainActivity");

        mButton.setText("jump to SecondActivity");
        mButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent  = new Intent(MainActivity.this, SecondActivity.class);
                startActivity(intent);
            }
        });
    }
}
public class SecondActivity extends Activity {
    @InjectView(R.id.label)
    TextView mLabel;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_second);

        //使用findViewById注入被InjectView修飾的成員變量
        ViewInjector.inject(this);

        // ViewInjector.inject(this) 已經(jīng)將mLabel賦值了,可以直接使用
        mLabel.setText("SecondActivity");
    }
}

工具類

在 AbstractProcessor.init 方法中我們可以獲得幾個很有用的工具類:

mTypeUtils = processingEnv.getTypeUtils();
mElementUtils = processingEnv.getElementUtils();
mFiler = processingEnv.getFiler();
mMessager = processingEnv.getMessager();

它們的作用如下:

Types

Types提供了和類型相關(guān)的一些操作,如獲取父類、判斷兩個類是不是父子關(guān)系等,我們在isView中就用它去獲取父類

    //遞歸判斷android.view.View是不是其父類   
    private boolean isView(TypeMirror type) {
        List<? extends TypeMirror> supers = mTypeUtils.directSupertypes(type);
        if (supers.size() == 0) {
            return false;
        }
        for (TypeMirror superType : supers) {
            if (superType.toString().equals("android.view.View") || isView(superType)) {
                return true;
            }
        }
        return false;
    }

Elements

Elements提供了一些和元素相關(guān)的操作,如獲取所在包的包名等:

//獲取所在的包名
String packageName = mElementUtils.getPackageOf(clazz).asType().toString();

Filer

Filer用于文件操作,我們用它去創(chuàng)建生成的代碼文件

    private void generateCode(String className, String code) {
        try {
            JavaFileObject file = mFiler.createSourceFile(className);
            Writer writer = file.openWriter();
            writer.write(code);
            writer.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

Messager

Messager 顧名思義就是用于打印的,它會打印出Element所在的源代碼,它還會拋出異常。靠默認的錯誤打印有時很難找出錯誤的地方,我們可以用它去添加更直觀的日志打印

當(dāng)用InjectView標(biāo)注了非View的成員變量我們就會打印錯誤并拋出異常(這里我們使用Diagnostic.Kind.ERROR,這個打印會拋出異常終止Processor):

//如果不是View的子類則報錯
if (!isView(element.asType())){
    mMessager.printMessage(Diagnostic.Kind.ERROR, "is not a View", element);
}

例如我們?nèi)绻贛ainActivity中為一個String變量標(biāo)注InjectView:

//在非View上使用InjectView就會報錯
@InjectView(R.id.button)
String x;

則會報錯:

  符號:   類 ViewInjector
  位置: 程序包 linjw.demo.injector
/Users/linjw/workspace/ProcessorDemo/app/src/main/java/linjw/demo/processordemo/MainActivity.java:22: 錯誤: is not a View
    String x;
           ^

如果我們不用Messager去打印,生成的代碼之后也會有打印,但是就不是那么清晰了:

/Users/linjw/workspace/ProcessorDemo/app/build/generated/source/apt/debug/MainActivityInjector.java:7: 錯誤: 不兼容的類型: View無法轉(zhuǎn)換為String
                arg.x=(java.lang.String)arg.findViewById(2131427415);

Element的子接口

我們在process方法中使用getElementsAnnotatedWith獲取到的都是Element接口,其實我們用Element.getKind獲取到類型之后可以將他們強轉(zhuǎn)成對應(yīng)的子接口,這些子接口提供了一些針對性的操作。

這些子接口有:

  • TypeElement:表示一個類或接口程序元素。
  • PackageElement:表示一個包程序元素。
  • VariableElement:表示一個屬性、enum 常量、方法或構(gòu)造方法參數(shù)、局部變量或異常參數(shù)。
  • ExecutableElement:表示某個類或接口的方法、構(gòu)造方法或初始化程序(靜態(tài)或?qū)嵗?,包括注釋類型元素?/li>

對應(yīng)關(guān)系如下

package linjw.demo;  // PackageElement
public class Person {  // TypeElement
    private String mName;  // VariableElement
    public Person () {}  // ExecutableElement
    public void setName (String name) {mName=name;}  // ExecutableElement
}

Element的一些常用操作

獲取類名:

  • Element.getSimpleName().toString(); // 獲取類名
  • Element.asType().toString(); //獲取類的全名

獲取所在的包名:

  • Elements.getPackageOf(Element).asType().toString();

獲取所在的類:

  • Element.getEnclosingElement();

獲取父類:

  • Types.directSupertypes(Element.asType())

獲取標(biāo)注對象的類型:

  • Element.getKind()

Demo地址

可以在這里查看完整代碼

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

相關(guān)閱讀更多精彩內(nèi)容

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