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

前段時間在學習Dagger2,對它生成代碼的原理充滿了好奇。google了之后發現原來java原生就是支持代碼生成的。

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

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

AbstractProcessor

AbstractProcessor是一個抽象類,我們繼承它需要實現一個抽象方法process,在這個方法里面去處理注解。然后它還有幾個方法需要我們去重寫。

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目錄下創建resources/META-INF/services/javax.annotation.processing.Processor文件(即創建resources目錄,在resources目錄下創建META-INF目錄,繼續在META-INF目錄下創建services目錄,最后在services目錄下創建javax.annotation.processing.Processor文件)。

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

完成后 javax.annotation.processing.Processor 內容如下

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

在安卓中自定義Processor

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

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

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

創建配置工程

首先在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'
...

創建注解

我們在libinjector中創建注解InjectView

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

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

創建InjectorProcessor

在libinjector中創建InjectorProcessor實現代碼的生成

@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會被調用三次,只有一次是可以處理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) {
            //如果標注的對象不是FIELD則報錯,這個錯誤其實不會發生因為InjectView的Target已經聲明為ElementType.FIELD了
            if (element.getKind()!= ElementKind.FIELD) {
                mMessager.printMessage(Diagnostic.Kind.ERROR, "is not a FIELD", element);
            }

            //這里可以先將element轉換為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目錄下創建resources/META-INF/services/javax.annotation.processing.Processor文件注冊InjectorProcessor:

# 注冊InjectorProcessor
linjw.demo.injector.InjectorProcessor

使用InjectView注解

我們在Activity中使用InjectView修飾需要賦值的View變量并且用ViewInjector.inject(this);調用生成的掉初始化修飾的成員變量。這里有兩個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) 已經將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) 已經將mLabel賦值了,可以直接使用
        mLabel.setText("SecondActivity");
    }
}

工具類

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

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

它們的作用如下:

Types

Types提供了和類型相關的一些操作,如獲取父類、判斷兩個類是不是父子關系等,我們在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提供了一些和元素相關的操作,如獲取所在包的包名等:

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

Filer

Filer用于文件操作,我們用它去創建生成的代碼文件

    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所在的源代碼,它還會拋出異常。靠默認的錯誤打印有時很難找出錯誤的地方,我們可以用它去添加更直觀的日志打印

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

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

例如我們如果在MainActivity中為一個String變量標注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無法轉換為String
                arg.x=(java.lang.String)arg.findViewById(2131427415);

Element的子接口

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

這些子接口有:

  • TypeElement:表示一個類或接口程序元素。
  • PackageElement:表示一個包程序元素。
  • VariableElement:表示一個屬性、enum 常量、方法或構造方法參數、局部變量或異常參數。
  • ExecutableElement:表示某個類或接口的方法、構造方法或初始化程序(靜態或實例),包括注釋類型元素。

對應關系如下

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())

獲取標注對象的類型:

  • Element.getKind()

Demo地址

可以在這里查看完整代碼

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容