[78→100]编译时Annotation的处理流程

神奇的说明——Java Annotation(注解)中介绍了如何在运行时透过反射的方式获取注解信息。
那么编译时如何获取注解信息呢?
其实,编译时 Annotation 指 @Retention 为 CLASS 的 Annotation,是由 APT(Annotation Processing Tool) 自动解析的。APT在编译时根据resources资源文件夹下的META-INF/services/javax.annotation.processing.Processor自动查找所有继承自 AbstractProcessor 的类,然后调用他们的 process 方法去处理。

核心步骤如下:

  1. 新建两个注解PrintMePrintMe2
package panda.annotation;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Retention(RetentionPolicy.SOURCE)
public @interface PrintMe {
}
package panda.annotation;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Retention(RetentionPolicy.SOURCE)
public @interface PrintMe2 {
}
  1. 新建编译时处理类MyProcessor
package panda.annotation;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Messager;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic;
@SupportedAnnotationTypes({"panda.annotation.PrintMe","panda.annotation.PrintMe2"})
public class MyProcessor extends AbstractProcessor {
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment env) {
        Messager messager = processingEnv.getMessager();
        for (TypeElement te : annotations) {
            messager.printMessage(Diagnostic.Kind.NOTE, "Printing: " + te);
            if (te instanceof PrintMe){
                for (Element e : env.getElementsAnnotatedWith(te)) {
                    messager.printMessage(Diagnostic.Kind.NOTE, "\t**Printing: " + e.toString());
                }
            }
            else{
                for (Element e : env.getElementsAnnotatedWith(te)) {
                    messager.printMessage(Diagnostic.Kind.NOTE, "\t--Printing: " + e.toString());
                }
            }

        }
        return true;
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }
}
  1. 为了这个处理器在编译时被调用,需要在META-INF中显示标识。在resources资源文件夹下新建META-INF/services/
    javax.annotation.processing.Processor,把MyProcessor放在里面
panda.annotation.MyProcessor
  1. 新建测试类,进行注解测试
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import panda.annotation.PrintMe;
import panda.annotation.PrintMe2;
public class MainActivity extends AppCompatActivity {
    @PrintMe
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
    @PrintMe
    @Override
    protected void onStart() {
        super.onStart();
    }
    @PrintMe
    @Override
    protected void onResume() {
        super.onResume();
    }
    @PrintMe2
    @Override
    protected void onPause() {
        super.onPause();
    }
    @PrintMe2
    @Override
    protected void onStop() {
        super.onStop();
    }
    @PrintMe2
    @Override
    protected void onDestroy() {
        super.onDestroy();
    }
}

编译后,输入的编译日志为

注: Printing: panda.annotation.PrintMe
注:  --Printing: onCreate(android.os.Bundle)
注:  --Printing: onStart()
注:  --Printing: onResume()
注: Printing: panda.annotation.PrintMe2
注:  --Printing: onPause()
注:  --Printing: onStop()
注:  --Printing: onDestroy()

解析JsonAnnotation

JsonAnnotation是一个典型的编译时注解库。

package kale.net.json.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @author Jack Tony
 * @date 2015/8/13
 */
@Documented
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface Json2Model {

    String modelName();
    
    String jsonStr();

    // custom the model's package name
    String packageName() default "";
}

其处理流程为:

  1. 根据注解,获得包名类名json字符串
  2. 类名json字符串 送入 jsonformat 包,获得对应的Model类字符串
  3. 根据包名类名Model类字符串,写入java文件。

核心代码如下:

package kale.net.json.processor;

import com.jsonformat.JsonParserHelper;

import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.nio.charset.Charset;
import java.util.Set;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.util.Elements;
import javax.tools.Diagnostic;
import javax.tools.JavaFileObject;

import kale.net.json.annotation.Json2Model;

/**
 * @author Jack Tony
 * @date 2015/8/13
 */
@SupportedAnnotationTypes({"kale.net.json.annotation.Json2Model"})
@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class Json2ModelProcessor extends AbstractProcessor {

    private static final String TAG = "[ " + Json2Model.class.getSimpleName() + " ]:";

    private Elements elementUtils;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        elementUtils = processingEnv.getElementUtils();
    }

    private String packageName;

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        for (TypeElement te : annotations) {
            for (Element e : roundEnv.getElementsAnnotatedWith(te)) {
                log("Working on: " + e.toString());
                VariableElement varE = (VariableElement) e;

                Json2Model json2Model = e.getAnnotation(Json2Model.class);
                if (json2Model.packageName().equals("")) {
                    // no custom package name
                    /**
                     *  example:
                     *  String GET_USER_INFO = "create/info/user/info"; 
                     *  result:create/info/user/info
                     */
                    if (varE.getConstantValue() == null) {
                        fatalError("jsonStr couldn't be final");
                    }
                    String url = varE.getConstantValue().toString();
                    packageName = url2packageName(url);
                } else {
                    // has custom package name
                    packageName = json2Model.packageName();
                }
                if (json2Model.jsonStr() == null || json2Model.jsonStr().equals("")) {
                    fatalError("json string is null");
                }

                final String clsName = json2Model.modelName();

                JsonParserHelper helper = new JsonParserHelper();
                helper.parse(json2Model.jsonStr(), clsName, new JsonParserHelper.ParseListener() {
                    public void onParseComplete(String str) {
                        createModelClass(packageName, clsName, "package " + packageName + ";\n" + str);
                    }

                    public void onParseError(Exception e) {
                        e.printStackTrace();
                        fatalError(e.getMessage());
                    }
                });
                log("Complete on: " + e.toString());
            }
        }
        return true;
    }

    private void createModelClass(String packageName, String clsName, String content) {
        //PackageElement pkgElement = elementUtils.getPackageElement("");
        TypeElement pkgElement = elementUtils.getTypeElement(packageName);

        OutputStreamWriter osw = null;
        try {
            // create a model file
            JavaFileObject fileObject = processingEnv.getFiler().createSourceFile(packageName + "." + clsName, pkgElement);
            OutputStream os = fileObject.openOutputStream();
            osw = new OutputStreamWriter(os, Charset.forName("UTF-8"));
            osw.write(content, 0, content.length());

        } catch (IOException e) {
            e.printStackTrace();
            fatalError(e.getMessage());
        } finally {
            try {
                if (osw != null) {
                    osw.flush();
                    osw.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
                fatalError(e.getMessage());
            }
        }
    }

    private void log(String msg) {
        processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, TAG + msg);
    }

    private void fatalError(String msg) {
        processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, TAG + " FATAL ERROR: " + msg);
    }

    /**
     * /user/test/ - > user.test
     */
    public static String url2packageName(String url) {
        String packageName = url.replaceAll("/", ".");
        if (packageName.startsWith(".")) {
            packageName = packageName.substring(1);
        }
        if (packageName.substring(packageName.length() - 1).equals(".")) {
            packageName = packageName.substring(0, packageName.length() - 1);
        }
        return packageName;
    }

}

思考

编译时注解可以将一些固定的代码隐藏起来,只保留核心逻辑,最典型的应用是butterknife
透过注解BindView把冗余常见的findViewById替换掉了,最后的代码结构简单清晰,便于理解。

class ExampleActivity extends Activity {
  @BindView(R.id.title) TextView title;
  @BindView(R.id.subtitle) TextView subtitle;
  @BindView(R.id.footer) TextView footer;

  @Override public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.simple_activity);
    ButterKnife.bind(this);
    // TODO Use fields...
  }
}

而json字符串放在java里面,并不是一个可读性很强的结构,以后字段增减看起来也不直观。

     String simpleStr = "{\n"
            + "    \"id\": 100,\n"
            + "    \"body\": \"It is my post\",\n"
            + "    \"number\": 0.13,\n"
            + "    \"created_at\": \"2014-05-22 19:12:38\"\n"
            + "}";
    // 简单格式
    @Json2Model(modelName = "Simple", jsonStr = simpleStr)
    String TEST_SIMPLE = "test/simple"; // api url

所以这时候采用编译时注解并不太合适。还是提取一个jar包,批量将json转成model,以后的增减由GsonFormat单个操作吧。

参考

  1. Java 注解
  2. Annotation实战【自定义AbstractProcessor】
  3. Java Annotation 及几个常用开源项目注解原理简析

Panda
2016-07-13

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容