深入浅出Java注解

一、什么是注解?

注解对于开发人员来讲既熟悉又陌生,熟悉是因为只要你是做开发,都会用到注解(常见的@Override);陌生是因为即使不使用注解也照常能够进行开发;注解不是必须的,但了解注解有助于我们深入理解某些第三方框架(比如Android Support Annotations、ButterKnife、xUtils、ActiveAndroid等),提高工作效率。

Java注解又称为标注,是Java从1.5开始支持加入源码的特殊语法元数据;Java中的类、方法、变量、参数、包都可以被注解。这里提到的元数据是描述数据的数据,结合实例来说明:

<string name="app_name">AnnotationDemo</string>

这里的"app_name"就是描述数据"AnnotionDemo"的数据,这是在配置文件中写的,注解是在源码中写的,如下所示:

@Override
protected void onCreate(Bundle savedInstanceState){
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main_layout);
    new Thread(new Runnable(){
        @Override
        public void run(){
            setTextInOtherThread();
        }
    }).start();
}

在上面的代码中,在MainActivity.java中复写了父类Activity.java的onCreate方法,使用到了@Override注解。但即使不加上@Override注解标记代码,程序也能够正常运行。那这里的@Override注解有什么用呢?使用它有什么好处?事实上,@Override是告诉编译器这个方法是一个重写方法,如果父类中不存在该方法,编译器会报错,提示该方法不是父类中的方法。如果不小心拼写错误,将onCreate写成了onCreat,而且没有使用@Override注解,程序依然能够编译通过,但运行结果和期望的大不相同。而如果使用了@Override注解,拼写错误则会得到提示。从示例可以看出,注解有助于阅读代码。

使用注解很简单,根据注解类的@Target所修饰的对象范围,可以在类、方法、变量、参数、包中使用“@+注解类名+[属性值]”的方式使用注解。比如:

@UiThread
private void setTextInOtherThread(@StringRes int resId){
    TextView threadTxtView = (TextView)MainActivity.this.findViewById(R.id.threadTxtViewId);
    threadTxtView.setText(resId);
}

特别说明:

  • 注解仅仅是元数据,和业务逻辑无关,所以当你查看注解类时,发现里面没有任何逻辑处理;
  • javadoc中的@author、@version、@param、@return、@deprecated、@hide、@throws、@exception、@see是文档注释标记,并不是注解;

二、注解的作用

  • 格式检查:告诉编译器信息,比如被@Override标记的方法如果不是父类的某个方法,IDE会报错;

  • 减少配置:运行时动态处理,得到注解信息,实现代替配置文件的功能;

  • 减少重复工作:比如第三方框架xUtils,通过注解@ViewInject减少对findViewById的调用,类似的还有(ButterKnife、ActiveAndroid等);

三、注解是如何工作的?

注解仅仅是元数据,和业务逻辑无关,所以当你查看注解类时,发现里面没有任何逻辑处理,例如XUtils的ViewInject

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ViewInject {

    int value();

    /* parent view id */
    int parentId() default 0;
}

如果注解不包含业务逻辑处理,必然有人来实现这些逻辑。注解的逻辑实现是元数据的用户来处理的,注解仅仅提供它定义的属性(类/方法/变量/参数/包)的信息,注解的用户来读取这些信息并实现必要的逻辑。当使用java中的注解时(比如@Override、@Deprecated、@SuppressWarnings)JVM就是用户,它在字节码层面工作。如果是自定义的注解,比如第三方框架ActiveAndroid,它的用户是每个使用注解的类,所有使用注解的类都需要继承Model.java,在Model.java的构造方法中通过反射来获取注解类中的每个属性

四、注解和配置文件的区别

通过上面的描述可以发现,其实注解干的很多事情,通过配置文件也可以干,比如为类设置配置属性;但注解和配置文件是有很多区别的,在实际编程过程中,注解和配置文件配合使用在工作效率、低耦合、可拓展性方面才会达到权衡。

4.1 、配置文件:

使用场合:

  • 外部依赖的配置,比如build.gradle中的依赖配置;

  • 同一项目团队内部达成一致的时候;

  • 非代码类的资源文件(比如图片、布局、数据、签名文件等);

优点:

  • 降低耦合,配置集中,容易扩展,比如Android应用多语言支持;

  • 对象之间的关系一目了然,比如strings.xml;

  • xml配置文件比注解功能齐全,支持的类型更多,比如drawable、style等;

缺点:

  • 繁琐;

  • 类型不安全,比如R.java中的都是资源ID,用TextView的setText方法时传入int值时无法检测出该值是否为资源ID,但@StringRes可以;

4.2、注解:

使用场合:

  • 动态配置信息;

  • 代为实现程序逻辑(比如xUtils中的@ViewInject代为实现findViewById);

  • 代码格式检查,比如Override、Deprecated、NonNull、StringRes等,便于IDE能够检查出代码错误;

优点:

  • 在class文件中,提高程序的内聚性;

  • 减少重复工作,提高开发效率,比如findViewById。

缺点:

  • 如果对annotation进行修改,需要重新编译整个工程;

  • 业务类之间的关系不如XML配置那样一目了然;

  • 程序中过多的annotation,对于代码的简洁度有一定影响;

  • 扩展性较差;

五、常用注解库

  • ButterKnife
  • Dagger2
  • Retrofit
  • EventBus
  • Afinal

    开源的Android的orm和ioc应用开发框架,其特点是小巧灵活,代码入侵量少。在android应用开发中,通过Afinal的ioc框架,诸如ui绑定,事件绑定,通过注解可以自动绑定。通过Afinal的orm框架,无需任何配置信息,一行代码就可以对android的sqlite数据库进行增删改查操作。同时,Afinal内嵌了finalHttp等简单易用的工具,可以轻松的对http就行求情的操作。

六、Annotation 分类

6.1、 标准 Annotation

包括 Override, Deprecated, SuppressWarnings,标准 Annotation 是指 Java 自带的几个 Annotation,上面三个分别表示重写函数,不鼓励使用(有更好方式、使用有风险或已不在维护),忽略某项 Warning

6.2、元 Annotation

@Retention, @Target, @Inherited, @Documented,元 Annotation 是指用来定义 Annotation 的 Annotation

6.2.1、@Retention(英文:保留)

用于指定被修饰的Annotation可以保留多长时间,只能修饰Annotation定义。

@Retention包含一个RetentionPolicy类型的value成员变量,使用@Retention必须为该value成员变量指定值。value成员变量的值有3个选择:

  • RetentionPolicy.CLASS: 编译器将把Annotation记录在class文件中。当运行java程序时,JVM不可获取Annotation信息。(默认值)
  • RetentionPolicy.RUNTIME: 编译器将把Annotation记录在class文件中。当运行java程序时,JVM也可获取Annotation信息,程序可以通过反射获取该Annotation信息
  • RetentionPolicy.SOURCE: Annotation只保留在源代码中(.java文件中),编译器直接丢弃这种Annotation。

比如:

//定义下面的MyAnnotaion保留到运行时,也可以使用value=RetentionPolicy.RUNTIME
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotaion{}

6.2.2、@Target (目标)

用于指定被修饰的Annotation能用于修饰哪些程序单元,只能修饰Annotation定义。它包含一个名为value的成员变量,取值如下:

  • @Target(ElementType.ANNOTATION_TYPE): 指定该该策略的Annotation只能修饰Annotation.
  • @Target(ElementType.TYPE) : 接口、类、枚举、注解
  • @Target(ElementType.FIELD) : 成员变量(字段、枚举的常量)
  • @Target(ElementType.METHOD) : 方法
  • @Target(ElementType.PARAMETER): 方法参数
  • @Target(ElementType.CONSTRUCTOR): 构造函数
  • @Target(ElementType.LOCAL_VARIABLE): 局部变量
  • @Target(ElementType.PACKAGE): 修饰包定义
  • @Target(ElementType.TYPE_PARAMETER): java8新增,可以使用在方法参数上
  • @Target(ElementType.TYPE_USE): java8新增,修饰的注解称为Type Annotation(类型注解),Type Annotation可用在任何用到类型的地方。

比如:

@Target(ElementType.FIELD)
public @interface MyActionListener{}

6.2.3、@Documented

用于指定被修饰的Annotation将被javadoc工具提取成文档。即说明该注解将被包含在javadoc中。

6.2.4、@Inherited

用于指定被修饰的Annotation具有继承性。即子类可以继承父类中的该注解。比如:注解@TestAnnotation被元注解@Inherited修饰,把@TestAnnotation添加在类Base上,则Base的所有子类也将默认使用@TestAnnotation注解。

6.2.5、Repeatable(可重复)

Java SE8引入的注解,表示这个注解可以在同一处多次声明

6.3、 自定义 Annotation

自定义 Annotation 表示自己根据需要定义的 Annotation,定义时需要用到上面的元 Annotation
这里只是一种分类而已,也可以根据作用域分为源码时、编译时、运行时 Annotation

七、如何自定义注解

首先,我们需要先了解注解处理器Processor,注解处理器有什么作用呢?首先它会在编译期被调用,可以扫描特定注解的信息,你可以为你自己的的注解注册处理器,一个特定的注解处理器以java源码作为输入,然后生成一些文件作(通常为java)为输出,这些java文件同样会被编译。这意味着,你可以根据注解的信息和被注解类的信息生成你想生成的代码!

需求:

定义一个注解MyAnnotation,去注解MainActivity,然后处理器扫描生成一个java文件,这个java文件有个输出Hello MyAnnotation的方法,运行的我们的MainAcitivity,然后调用这个java文件的方法。

7.1、创建注解工程

同样我们先创建一个Java工程,编写一个注解类MyAnnotation

@Retention(RetentionPolicy.CLASS)
@Target(ElementType.TYPE)
public @interface MyAnnotation {
    String value() default "MyAnnotation";
}

7.2、创建Android工程

定义好我们注解的MyAnnotation,接下来,我们要用这个去注解MainActivity,现在我们是在Java工程,那么我们新创建一个Android工程,里面有个MainActivity,这个工程依赖我们MyAnnotation所在的工程。

@MyAnnotation
public class MainActivity extends AppCompatActivity {

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

接下来我们就要通过自己定义的注解处理器去扫描这个注解进而生成java文件,但是在此之前,我们需要先了解注解处理的工作流程和相关API。

7.3、创建Compiler工程

AbstractProcessor

AbstractProcessor就是系统抽象出来的处理器类,如果我们要处理自己定义的注解,就必须借助于它。
例如:

public class MyProcessor extends AbstractProcessor{

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

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        return super.getSupportedAnnotationTypes();
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return super.getSupportedSourceVersion();
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        return false;
    }

}

我们需要重写的方法有:

  • init(ProcessingEnvironment processingEnv) : 所有的注解处理器类都必须有一个无参构造函数。然而,有一个特殊的方法init(),它会被注解处理工具调用,以ProcessingEnvironment作为参数。ProcessingEnvironment 提供了一些实用的工具类Elements, Types和Filer。我们在后面将会使用到它们。

  • process(Set<? extends TypeElement> annoations, RoundEnvironment env) : 这类似于每个处理器的main()方法。你可以在这个方法里面编码实现扫描,处理注解,生成 java 文件。使用RoundEnvironment 参数,你可以查询被特定注解标注的元素。

  • getSupportedAnnotationTypes(): 在这个方法里面你必须指定哪些注解应该被注解处理器注册。注意,它的返回值是一个String集合,包含了你的注解处理器想要处理的注解类型的全称。换句话说,你在这里定义你的注解处理器要处理哪些注解。

  • getSupportedSourceVersion() : 用来指定你使用的 java 版本,建议使用SourceVersion.latestSupported()。

7.4、注册处理器

我们在编译好的META-INF/services添加我们的处理器路径,谷歌已经提供一个很方便的库,帮助我们做这些东西,我们只需要在处理器工程添加依赖

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

然后在Myprocessor中添加@AutoService(Processor.class)的注解,这样就完成了我们处理器的注册。


image

编译成生成的META-INF/services中就注册了我们的MyProcessor

image

接下来,我们编写一个我们自己的处理器,生成java文件,来讲解一下相关API,以及要注意的事项。


/**
 * 每一个注解处理器类都必须有一个空的构造函数,默认不写就行;
 */
@AutoService(Processor.class)
public class MyProcessor extends AbstractProcessor {

    //处理Element的工具类
    private Elements mElementUtils;
    //生成文件的工具
    private Filer mFiler;
    //日志信息的输出
    private Messager mMessager;


    /**
     * 这相当于每个处理器的主函数main(),你在这里写你的扫描、评估和处理注解的代码,以及生成Java文件。
     * 输入参数RoundEnviroment,可以让你查询出包含特定注解的被注解元素
     * @param annotations   请求处理的注解类型
     * @param roundEnvironment  有关当前和以前的信息环境
     * @return  如果返回 true,则这些注解已声明并且不要求后续 Processor 处理它们;
     *          如果返回 false,则这些注解未声明并且可能要求后续 Processor 处理它们
     */
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnvironment) {
        Set<? extends Element> set = roundEnvironment.getElementsAnnotatedWith(MyAnnotation.class);
        for (Element element : set){
            if(element.getKind() == ElementKind.CLASS){
                TypeElement typeElement = (TypeElement) element;
                brewJavaFile(typeElement);
            }
        }
        return true;
    }

    /**
     * init()方法会被注解处理工具调用,并输入ProcessingEnviroment参数。
     * ProcessingEnviroment提供很多有用的工具类Elements, Types 和 Filer
     * @param processingEnvironment 提供给 processor 用来访问工具框架的环境
     */
    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        mElementUtils = processingEnvironment.getElementUtils();
        mFiler = processingEnvironment.getFiler();
        mMessager = processingEnvironment.getMessager();
    }

    /**
     * 这里必须指定,这个注解处理器是注册给哪个注解的。注意,它的返回值是一个字符串的集合,包含本处理器想要处理的注解类型的合法全称
     * @return  注解器所支持的注解类型集合,如果没有这样的类型,则返回一个空集合
     */
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> set = new LinkedHashSet<>();
        set.add(MyAnnotation.class.getCanonicalName());
        return set;
    }

    /**
     * 指定使用的Java版本,通常这里返回SourceVersion.latestSupported(),默认返回SourceVersion.RELEASE_6
     * @return  使用的Java版本
     */
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    private void brewJavaFile(TypeElement pElement){
        //sayHello 方法
        MyAnnotation myAnnotation = pElement.getAnnotation(MyAnnotation.class);
        MethodSpec methodSpec = MethodSpec.methodBuilder("sayHello")
                .addModifiers(Modifier.PUBLIC, Modifier.STATIC).returns(void.class)
                .addStatement("$T.out.println($S)",System.class,"Hello"+myAnnotation.value()).build();

        // class
        TypeSpec typeSpec = TypeSpec.classBuilder(pElement.getSimpleName().toString()+"$$HelloWorld").addModifiers(Modifier.PUBLIC,Modifier.FINAL).addMethod(methodSpec).build();
        // 获取包路径,把我们的生成的源码放置在与被注解类中同一个包路径中
        JavaFile javaFile = JavaFile.builder(mElementUtils.getPackageOf(pElement).getQualifiedName().toString(),typeSpec).build();
        try {
            javaFile.writeTo(mFiler);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

7.5、测试

新建一个Android工程,该工程依赖注解工程,至于compiler处理器工程,我们要使用apt的方式依赖。
这里有人要问了,apt是什么?

它主要有两个作用:

  • 能在编译时期去依赖注解处理器并进行工作,但在生成 APK 时不会包含任何遗留的东西
  • 能够辅助 Android Studio 在项目的对应目录中存放注解处理器在编译期间生成的文件

了解完apt,那我们就先在项目目录下的build.gradle中添加
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'这个依赖

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.2.1'
        // apt 
        classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
    }
}

然后在Android 工程中,添加这个插件依赖

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

然后就可以使用apt依赖处理器工程了

apt project(':xs_compiler')

运行我们的Android工程,查看build生成文件

Paste_Image.png

顺利生成我们的文件了,剩下就是怎么去调用这个sayHello的方法,我们的思路是通过反射生成的类,调用该方法。

在注解工程中,新建AnnotationApi类,编码如下

public class MyAnnotationApi {

    public static void sayHelloAnnotation(Object pTarget){
        String name = pTarget.getClass().getCanonicalName();
        try {
            Class clazz = Class.forName(name+"$$HelloWorld");
            Method method = clazz.getMethod("sayHello");
            method.invoke(null);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }

}

然后在MainActivity中调用sayHelloAnnotation的方法


@MyAnnotation
public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main_layout);
        MyAnnotationApi.sayHelloAnnotation(this);
    }

}

查看输出:

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

推荐阅读更多精彩内容