自定义注解

java annotation基础

java注解分为标准注解和元注解。

标准注解是java为我们提供的预定义的注解,@override,@deprecated,@suppresswarnnings,@safevarargs

元注解是给我们自定义注解用的共有5种,@target,@retention,@documented,@inherited,@repeatable

@Target:用来修饰注解能够修饰的对象的类型,接收一个elementtype类型的数组。

@Remention:用来指定注解的保留策略。可以指定如下值
- SOURCE 注解只保留在源码层面,编译时即被丢弃
- CLASS 注解可以保留在class文件中,但会被jvm丢弃
- RUNTIME 注解也会在jvm运行事件保留,可以通过反射读取注解信息

@Documented:此注解将被包含在javadoc中

@Inherited:此注解可以被继承

@Repeatable:指定的注解可以重复应用到指定的对象上边

标准注解也是用元注解创建的

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override{}

如上所示:

@Target(ElementType.METHOD)表示这个注解能够修饰方法

@Retention(RetentionPolicy.SOURCE)表示这个注解只存在于源码

解析注解的两种方式

1. 运行时注解可以使用反射解析

下边的使用反射解析注解的实例:创建一个类,然后在类中对字段做注解,最后通过反射的方法解析注解的内容

首先需要创建注解类

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UseCase {
    String name();
    int id() default -1;
}

然后定义一个类,使用上边的注解

public class Persion {
    @UseCase(id = 0, name = "nancy")
    int number;

    @UseCase(id = 2, name = "lucky")
    String name;
}

最后通过反射解析注解

try {
            Class clz =Class.forName("com.dou.demo.knife_annotation.Persion");
            Field[] fields = clz.getDeclaredFields();

            for (Field field : fields) {
                UseCase annotation = field.getAnnotation(UseCase.class);

                if (annotation != null) {
                    System.out.println(annotation.name() + annotation.id());
                }
            }

        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }

2. 编译时注解可以使用annotationprocessor解析

编译时注解的使用场景很多,包括很多的第三方库例如Butterknife, Retrofit, GreenDao等,下边我们就做一个简单的demo,类似于butterknife的例子来讲解如何使用annotationprocessor。

首先来讲一下butterknife的大致的注入流程

首先我们需要在activity中使用@BindView(R.id.xx)注解对应的控件,然后使用Butterknife.bind(this)方法完成对控件的绑定,实际上底层还是调用findviewbyid方法。大概的思路就是在编译期生成一个类似MainActivity_ViewBinding.java文件,在这个类的构造方法中最终会调用findViewById()方法。而调用Butterknife.bind(this)方法,首先会找到MainActivity_ViewBinding这个类的构造方法对象,然后实例化该构造方法(执行类的构造方法),也就是执行findviewbyid动作

基于annotationprocessor使用注解的步骤

原理为:先在activity中使用注解@BindView或者@OnClick标注控件对象,然后调用knife.bind(this)方法进行绑定,编译注解时生成一个Activity$$Injector类,bind方法主要就是调用这个类的inject方法,这个方法底层调用findviewbyid

因为自己学习时已经定义了@BindView,下边主要定义另外一个注解@OnClick

定义注解类

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface OnClick {
    int[] id() default -1;
}

封装绑定接口

在activity中使用了注解之后,在编译的时候自动生成对应的XXActivity$$Inject类,这个类中有一个inject方法,这个方法提供findviewbyid和setonclicklistener方法。这里的我们首先定义一个Injector接口,在接口中定义一个inject方法,自动生成的xxactivity$$injector类实现了这个方法。这个接口封装绑定接口指的就是Knife.bind(this)的这个过程,我们使用多态的特性调用Injector.inject()方法完成绑定。具体代码如下:

Knife类:

public class Knife {

    static Finder ACTIVITY_FINDER = new ActivityFinder();
    static Finder VIEW_FINDER = new ViewFinder();

    public Knife() {
        throw new AssertionError("not available for instance.");
    }

    public static void bind(Context context){
        bind(context, ACTIVITY_FINDER);
    }

    public static void bind(View view){
        bind(view, VIEW_FINDER);
    }

    public static void bind(Object host, Finder finder){
        bind(host, host, finder);
    }

    public static void bind(Object host, Object source, Finder finder){
        String className = host.getClass().getName();

        try {
            Class findClass = Class.forName(className + "$$Injector");
            Injector injector = null;
            try {
                injector = (Injector) findClass.newInstance();
            } catch (InstantiationException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }

            if (host == null) {
                Log.d("xxx", "bind: host");
                return;
            } else if (finder == null) {
                Log.d("xxx", "bind: finder");
                return;
            }

            injector.inject(host, source, finder);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

Injector接口:

public interface Injector<T> {
    void inject(T host, Object source, Finder finder);
}

其他的类:

public interface Finder {
    Context getContext();
    View findView(Object source, int resId);
}

public class ActivityFinder implements Finder {

    Context context;

    @Override
    public Context getContext() {
        return context;
    }

    @Override
    public View findView(Object activity, int resId) {
        context = ((Activity) activity);
        return ((Activity) activity).findViewById(resId);
    }
}

public class ViewFinder implements Finder {

    Context context;

    @Override
    public Context getContext() {
        return context;
    }

    @Override
    public View findView(Object view, int resId) {
        context = ((View) view).getContext();
        return ((View) view).findViewById(resId);
    }
}

定义annotationprocessor

这里是对编译时注解进行解析并生成对应类的地方,我们在这里使用annotationprocessor和javapoet。annotationprocessor会在注解编译时期提供回调,我们的主要工作都是在它的process()中进行的。javapoet是square开源的生成java类的开源库。

首先我们需要定义一个类集成AbstractProcessor,他是annotationprocessor的核心类,我们需要实现它的4个方法:

  • init() 初始化代码,一般会去获取Elements,messager,filer
  • process() 处理方法
  • getSupportedAnnotationTypes() 用来指定该处理器适用的注解
  • getSupportedSourceVersion() 用来指定你的编译器的java版本

之后还需要对处理器进行注册,第一种方法是在java同级目录下创建一个resources/META-INF/service文件夹,然后在文件夹中创建名为javax.annotation.processing.Processor的文件,文件内容为 我们的处理器的目录。
另一种方法是使用谷歌的@AutoService注解,你需要添加依赖

    compile 'com.google.auto.service:auto-service:1.0-rc2'

然后在自己的处理器上面加上@AutoService(Processor.class)注解即可。

代码的生成过程

首先做一些初始化操作,指定要注解类型,java编译器版本

@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
    super.init(processingEnvironment);
    System.out.println("processor: init");
    filer = processingEnvironment.getFiler();
    messager = processingEnvironment.getMessager();
    elements = processingEnvironment.getElementUtils();
}

@Override
public Set<String> getSupportedAnnotationTypes() {
    System.out.println("processor: getSupportedAnnotationTypes");
    Set<String> types = new LinkedHashSet<>();
    types.add(BindView.class.getCanonicalName());
    return types;
}
    
@Override
public SourceVersion getSupportedSourceVersion() {
    System.out.println("processor: getSupportedSourceVersion");
    return SourceVersion.latestSupported();
}

在process中进行代码生成的工作,从参数roundEnvironment中,我们可以获得注解对应的Element信息。

@Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        System.out.println("processor: process");
        processBindview(roundEnvironment);
        processOnClick(roundEnvironment);

        System.out.println("processor: annotationCount = " + annotationClassMap.size());

        for (AnnotationClass annotation : annotationClassMap.values()) {
            try {
                annotation.generateInjector().writeTo(filer);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return true;
    }

private void processOnClick(RoundEnvironment roundEnvironment) {
        System.out.println("processor: processOnClick");

        for (Element element : roundEnvironment.getElementsAnnotatedWith(OnClick.class)){

            AnnotationClass annotationClass = getAnnotationClass(element);
            OnClickMethod field = new OnClickMethod(element);
            annotationClass.addField(field);
        }
    }

    private void processBindview(RoundEnvironment roundEnvironment) {

        System.out.println("processor: processBindview");

        for (Element element : roundEnvironment.getElementsAnnotatedWith(BindView.class)){
            AnnotationClass annotationClass = getAnnotationClass(element);
            BindViewField bindViewField = new BindViewField(element);
            annotationClass.addField(bindViewField);
        }
    }

    private AnnotationClass getAnnotationClass(Element element){
        String className = ((TypeElement) element.getEnclosingElement()).getQualifiedName().toString();
        AnnotationClass annotationClass = annotationClassMap.get(className);
        if (annotationClass == null) {
            annotationClass = new AnnotationClass((TypeElement) element.getEnclosingElement(), elements);
            annotationClassMap.put(className, annotationClass);
        }
        return annotationClass;
    }

上边的逻辑主要是根据roundEnvironment分别处理@BindView和@OnClick注解,这些注解分别被保存到AnnotaionClass对象的List<BindViewField>和List<OnClickMethod>集合中,最后调用annotationClass.generateInjector()来生成java类。

public JavaFile generateInjector(){
        System.out.println("processor: generateInjector");
        // to create method declear
        // @Override
        // public void inject(MainActivity host, Object source, Finder finder) {
        // }
        MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("inject")
                .addModifiers(Modifier.PUBLIC)
                .addAnnotation(Override.class)
                .addParameter(TypeName.get(typeElement.asType()), "host", Modifier.FINAL)
                .addParameter(TypeName.OBJECT, "source")
                .addParameter(ClassName.get("com.dou.demo.knife_api.finder", "Finder"), "finder");

        // to create method body
        // host.targetview = (TextView) finder.findView)(source, R.id.xx)
        for (BindViewField field : bindViewFields) {
            methodBuilder.addStatement("host.$N=($T)finder.findView(source,$L)",
                    field.getFieldName(),
                    ClassName.get(field.getFieldType()),
                    field.getViewId());
        }

        // to create variable declear
        // OnClickListener listener;
        if (onClickMethods.size() > 0) {
            methodBuilder.addStatement("$T listener",
                    ClassName.get("android.view", "View", "OnClickListener"));
        }

        // to create varible define
        // listener = new OnClickListener(){
        //      @Override
        //      public void onClick(View v ){
        //          host.onClick();
        //      }
        // }
        for (OnClickMethod onClickMethod : onClickMethods) {
            TypeSpec listener = TypeSpec.anonymousClassBuilder("")
                    .addSuperinterface(ClassName.get("android.view", "View", "OnClickListener"))
                    .addMethod(MethodSpec.methodBuilder("onClick")
                            .addAnnotation(Override.class)
                            .addModifiers(Modifier.PUBLIC)
                            .returns(TypeName.VOID)
                            .addParameter(ClassName.get("android.view", "View"), "view")
                            .addStatement("host.$N()", onClickMethod.methodName)
                            .build())
                    .build();

            methodBuilder.addStatement("listener=$L", listener);

            for (int id : onClickMethod.viewIds) {
                methodBuilder.addStatement("finder.findView(source, $L).setOnClickListener(listener)", id);
            }
        }

        String packagename = getPackageName(typeElement);
        String classname = getBinderClassName(packagename, typeElement);

        ClassName binderClassname = ClassName.get(packagename, classname);
        TypeSpec injectorClass = TypeSpec.classBuilder(binderClassname.simpleName() + "$$Injector")
                .addModifiers(Modifier.PUBLIC)
                .addSuperinterface(ParameterizedTypeName.get(ClassName.get("com.dou.demo.knife_api", "Injector"), TypeName.get(typeElement.asType())))
                .addMethod(methodBuilder.build())
                .build();

        return JavaFile.builder(packagename, injectorClass).build();
    }

使用方式

implementation project(":knife-annotation")
    implementation project(":knife-api")
    annotationProcessor project(":knife-compiler")


public class MainActivity extends AppCompatActivity {

    @BindView(id = R.id.tv_content)
    TextView tv_content;

    @OnClick(id = R.id.tv_content)
    public void onclick(){
        Toast.makeText(this, "ggggggg", Toast.LENGTH_SHORT).show();
    }

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

        Knife.bind(this);
        tv_content.setText("hello knife");
    }
}

总结

上边可能有一些需要注意的地方:

  1. 定义processor时最好创建java library类型,不然会提示AbstractProcessor类找不到
  2. 可能会出现注解编译的时候没有生成对应的注解的情况,需要配合gradle console的log查看哪里出错了

源码:

douyn/annotation-demo

参考:

Java 注解及其在 Android 中的应用

Shouheng88/Android-references

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

推荐阅读更多精彩内容