Android算法Android知识

深入浅出Java注解

2017-07-20  本文已影响237人  _Justin

一、什么是注解?

注解对于开发人员来讲既熟悉又陌生,熟悉是因为只要你是做开发,都会用到注解(常见的@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);
}

特别说明:

二、注解的作用

三、注解是如何工作的?

注解仅仅是元数据,和业务逻辑无关,所以当你查看注解类时,发现里面没有任何逻辑处理,例如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 、配置文件:

使用场合:

优点:

缺点:

4.2、注解:

使用场合:

优点:

缺点:

五、常用注解库

六、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个选择:

比如:

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

6.2.2、@Target (目标)

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

比如:

@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;
    }

}

我们需要重写的方法有:

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是什么?

它主要有两个作用:

了解完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
上一篇下一篇

猜你喜欢

热点阅读