0
点赞
收藏
分享

微信扫一扫

Java高级语言特性----注解

正义的杰克船长 2022-04-24 阅读 69
java

        Java注解是JDK5.0引入的。它是元数据的一种形式,提供有关程序而不属于程序本身的数据。注解默认实现Annotation接口(这一点类似于java类默认都继承自Object)。声明一个注解用@Interface关键字。

// Java lang包下的 Annotation.java
package java.lang.annotation;

public interface Annotation {
    boolean equals(Object var1);

    int hashCode();

    String toString();

    Class<? extends Annotation> annotationType();
}

// 定义一个自己的注解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface AnnoTest {
    String strValue();
    int intValue() default 1;
}

// 使用自定义注解
@AnnoTest(strValue = "test", intValue = 2)
public class MyAnnoTest {
    
}

元注解

        元注解,是注解的注解。在自定义注解时,用元注解来对注解进行修饰。需要用到的元注解有两个@Target@Retention。另外还有@Documented与@Inherited,@Documented前者被用于被javadoc工具提取成文档;@Inherited表示允许子类继承父类中定义的注解。

        @Target用于修饰注解作用的对象。

ElementType.ANNOTATION_TYPE //应用于注解类型
ElementType.CONSTRUCTOR //应用于构造函数
ElementType.FIELD // 应用于字段或属性
ElementType.LOCAL_VARIABLE // 应用于局部变量
ElementType.METHOD // 应用于方法级注解
ElementType.PACKAGE // 应用于包声明
ElementType.PARAMETER // 应用于方法的参数
ElementType.TYPE //可以应用于类的任何元素

        @Retention用于表明注解的保留级别

RetentionPolicy.SOURCE // 保留到源级别,在编译时会被忽略
RetentionPolicy.CLASS // 保留到编译级别,在编译时由编译器保留,运行时会忽略
RetentionPolicy.RUNTIME // 保留到运行级别,在JVM运行时可以使用

        注解保留的级别后者会包含前者,既CLASS级包含了SOURCE级,RUNTIME级包含CLASS级。SOURCE < CLASS < RUNTIME。

注解使用场景

Source保留级别      

        RetentionPolicy.SOURCE,是源码级别的注解,通常用于提供IDE语法检查、APT技术等场景。

IDE语法检查

        例如,自定义一个注解,对方法的入参进行限制,替代枚举的作用。

// 定义一个AWeekEnd注解,声明作用于函数的入参,保留到源码级别
@IntDef(value = {WeekEnd.Saturday, WeekEnd.Sunday})
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.SOURCE)
public @interface AWeekEnd {
    int Saturday = 1;
    int Sunday = 2;
}

// 方法的参数使用@AWeekEnd注解修饰
public void playOnWeekEnd(@AWeekEnd int weekEnd) {
        // ...
}

public void test() {
    // 调用该方法的参数需要传AWeekEnd.Saturday或AWeekEnd.Sunday
    // 否则语法检查会报错
    playOnWeekEnd(AWeekEnd.Saturday); 
    // playOnWeekEnd(AWeekEnd.Sunday);
}

        调用上面的playOnWeekEnd方法,如果传入的不是AWeekEnd.Saturday或AWeekEnd.Sunday的话,IDE语法检查会提示错误:

        这样替代枚举的意义是会比枚举更加节省空间。一个int类型数占4个字节。而枚举类型每一个都是对象,占据空间要大的多。前面在博客“Jvm对象初始化过程”中提到对象的组成部分,包括对象头、实例数据和填充数据。对象头占12字节,另外需要进行8字节对齐(填充数据),一个对象最少需要占16位。

        另外,这里要用到androidx.annotation提供的@IntDef注解。这是一个元注解,定义如下。

// androidx.annotation IntDef注解
@Retention(SOURCE)
@Target({ANNOTATION_TYPE})
public @interface IntDef {
    int[] value() default {};

    boolean flag() default false;

    boolean open() default false;
}

        另外,AS可以修改语法检查的级别:

 APT技术 

        APT全称为:"Anotation Processor Tools",即注解处理器。APT是javac自带的一个工具,用于在将Java源文件编译成Class文件时,扫描源文件中的注解信息,将其传递给注解处理器进行处理。我们可以自定义注解处理器,针对特定的注解进行处理。当前许多著名的开源框架都采用了APT的技术。如Glide、EventBus3.0、ARouter等。APT技术的实现,在后面博客再专门讨论。

Class保留级别

        此类注解会保留到经过javac编译的到的Class文件中。应用场景通常为需要进行字节码插桩的场景。如,Roubust热修复技术,用与解决类被带上CLASS_ISPREVERIFIED标签的问题。字节码插桩,既是修改字节码(Class)文件,进行代码逻辑的修改。

        Android中插桩的点,一般选择在dex工具将Class文件转换为dex文件时。gladle编译过程中"transformClassesWithDexBuilderForDebug" task既是将class文件转换为dex文件,输入是.class文件,输出是.dex文件。在gradle中,增加自定义的编译插件,拦截该task,对class文件进行修改(可以用ClassVisitor工具)。关于字节码插桩、gradle插件在后面博客再单独讨论。

        举一个例子,简单说明结合注解进行字节码插桩后的效果。自定义Login注解,标识调用该方法需要先进行登录。在编译后的字节码文件,插入是否登录和跳转到登录页的功能:

// 定义Login注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface Login {
}

// Java源文件
@Login
public void jump(){
    startActivity(new Intent(this,AActivity.class));
}

// 编译后字节码文件
@Login
public void jump() {
    this.startActivity(new Intent(this, AActivity.class));
}

// 插桩修改后字节码文件
@Login
public void jump() {
    if (this.isLogin) {
        this.startActivity(new Intent(this, LoginActivity.class));
    } else {
        this.startActivity(new Intent(this, AActivity.class));
    }
}

Runtime保留级别

        此类注解,能够保留到运行期。通常是结合反射技术获取注解的信息,并根据信息做对应的处理。

        举一个基于注解,在运行时进行控件自动注入的例子:

// 定义一个注解,作用在成员变量上,需要一个资源id号的参数
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface InjectView {
    @IdRes int value();
}

// 在MainActivity.java中使用注解
public class MainActivity extends Activity {

    // 用注解修饰需要注入的控件,将控件的id作为注解的参数
    @InjectView(R.id.tv)
    private TextView tv;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        // 调用方法进行动态注入
        InjectUtils.injectView(this);
    }
    ...
}

// InjectUtils.java中进行动态注入
public class InjectUtils {
    public static void injectView(Activity activity) {
        Class<? extends Activity> cls = activity.getClass();
        
        // 获得此类所有的成员
        Field[] declaredFields = cls.getDeclaredFields();
        // 对所有成员进行遍历,找到被InjectView修饰的成员
        for (Field filed : declaredFields) {
            // 判断属性是否被InjectView注解声明
            if (filed.isAnnotationPresent(InjectView.class)) {
                InjectView injectView = filed.getAnnotation(InjectView.class);
                // 获得了注解中传递的参数,控件的id
                int id = injectView.value();
                View view = activity.findViewById(id);
                // 设置访问权限,允许操作private的属性
                filed.setAccessible(true); 
                try {
                    // 通过反射对属性进行赋值,完成对控件注入
                    filed.set(activity, view);
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
举报

相关推荐

0 条评论