【Java基础】学java注解,看这一篇文章就够了

一、注解的概念

Annotation(注解)是一种标记,使类或接口附加额外信息,帮助编译器和 JVM 完成一些特定功能。

Annotation(注解)也被称为元数据(Metadata)是JDK1.5及以后版本引入的,用于修饰包、类、接口、字段、方法参数、局部变量等。

常见的注解如:@Override、@Deprecated和@SuppressWarnings

二、注解使用步骤及场景

2.1 使用步骤

步骤:定义注解 -> 获取注解 -> 创建注解实例 -> 解析注解 ->使用。

定义如下:

public @interface Persions{
    Person[]  value();
}

@Repeatable(Persons.class)
public @interface Person{
	String role default "";
}

一个人他既是程序员又是产品经理,同时他还是个画家

@Person(role="artist")
@Person(role="coder")
@Person(role="PM")
public class SuperMan{
	
}

注解@interface 是一个实现了Annotation接口的 接口, 然后在调用getDeclaredAnnotations()方法的时候,返回一个代理$Proxy对象,这个是使用jdk动态代理创建,使用Proxy的newProxyInstance方法时候,传入接口 和InvocationHandler的一个实例(也就是 AnotationInvocationHandler ) ,最后返回一个代理实例。

期间,在创建代理对象之前,解析注解时候 从该注解类的常量池中取出注解的信息,包括之前写到注解中的参数,然后将这些信息在创建 AnnotationInvocationHandler时候 ,传入进去 作为构造函数的参数,当调用该代理实例的获取值的方法时,就会调用执行AnotationInvocationHandler里面的逻辑,将之前存入的注解信息 取出来

获取注解

// 1. 获取当前class
Class<?> clazz = context.getClass();
// 2. 根据class获取class上面的InjectLayout注解
InjectLayout annotation = clazz.getAnnotation(InjectLayout.class);


步骤:

1、首先可以通过 Class 对象的 isAnnotationPresent() 方法判断它是否应用了某个注解

    public boolean isAnnotationPresent(Class<? extends Annotation> annotationClass) {}

2、然后通过 getAnnotation() 或者是 getAnnotations() 方法来获取 Annotation 对象

    public <A extends Annotation> A getAnnotation(Class<A> annotationClass) {} 返回指定类型的注解
    public Annotation[] getAnnotations() {} 返回注解到这个元素上的所有注解
    
3、 如果获取到的 Annotation 如果不为 null,则就可以调用它们的属性方法了    

注解是将参数信息存储到了class文件的常量池里面,在创建实例的时候,会通过getConstantPool()获取出来,是一个byte[]流,需要进行转换。

2.2 常见场景

如组件化框架、view注解框架、面向编译器/apt使用、自定义注解+拦截器或者AOP,使用自定义注解设计框架等。

2.3 注解的作用:

1、生成文档,通过代码里标识的元数据生成javadoc文档。

2、编译检查,通过代码里标识的元数据让编译器在编译期间进行检查验证。(源码时注解)

3、编译时动态处理,编译时通过代码里标识的元数据动态处理,例如动态生成代码。 (编译时注解)

4、运行时动态处理,运行时通过代码里标识的元数据动态处理,例如使用反射注入实例。(运行时注解)

三、注解原理及分类

3.1 原理

java注解的原理是利用反射机制来实现的。当运行java程序时,java虚拟机会加载java类,并通过反射机制来获取类中的注解信息。通过反射机制可以获取某个类上、属性上或者方法上的注解信息。从而通过注解的信息来完成相应的操作。

如下图:反射相关的类Class, Method, Field都实现了AnnotationElement接口,因此,只要我们通过反射拿到Class, Method, Field类,就能够通过getAnnotation(Class)拿到我们想要的注解并取值

下面是两个相关的概念:

1. 注解处理器(Annotation Processor)是javac的一个工具,它用来在编译时扫描和处理注解(Annotation),注解处理器是运行它自己的虚拟机JVM中。

2. AnnotatedElement 接口是所有程序元素(Class、Method和Constructor)的父接口,所以程序通过反射获取了某个类的AnnotatedElement对象之后,程序就可以调用该对象的方法来访问Annotation信息。

3.2 元注解:

元注解就是解释注解的注解。(负责对其它注解进行说明的注解)
在JDK 1.5中提供了4个标准的元注解:
@Target,@Retention,@Documented,@Inherited, 
在JDK 1.8中提供了两个元注解  @Repeatable 和 @Native 。

  •   @Target

指定注解运用到的地方。

Target注解用来说明那些被它所注解的注解类可修饰的对象范围。

类比作标签,原本标签是想贴到什么地方就贴到什么地方,但是有了@Target的存在,它张贴的地方就非常具体了,比如只能张贴到方法上、类上、方法参数上等。

TYPE, // 类、接口、枚举类

FIELD, // 成员变量(包括:枚举常量)

METHOD, // 成员方法

PARAMETER, // 方法参数

CONSTRUCTOR, // 构造方法

LOCAL_VARIABLE, // 局部变量

ANNOTATION_TYPE, // 注解类

PACKAGE, // 可用于修饰:包

TYPE_PARAMETER, // 类型参数,JDK 1.8 新增

TYPE_USE // 使用类型的任何地方,JDK 1.8 新增

  • @Retention

相当于一个时间戳。

描述注解保留的时间范围(即:被描述的注解在它所修饰的类中可以被保留到何时) 。

类比作标签,原本标签是想贴到什么地方就贴到什么地方,想贴到什么时候就贴到什么时候,但是有了@Retention的存在,就相当于加了一个时间戳,时间戳指明了标签张贴的时间周期。

取值如下:

RetentionPolicy.SOURCE:注解只在源码阶段保留,在编译器进行编译时它将被丢弃忽视,这类注解不会被编译进入.class文件

RetentionPolicy.CLASS:注解只被保留到编译进行的时候,它并不会被加载到JVM中,这类注解会被编译进入.class文件,但加载结束后并不会存在于内存中

RetentionPolicy.RUNTIME:注解可以保留到程序运行的时候,会被加载进入到JVM中,所以程序运行时可以获取到。

只有注解被定义为RUNTIME后,该注解才能是运行时可见,当class文件被装载时被保存在class文件中的Annotation才会被虚拟机读取。

  • @Documented

在使用 javadoc 工具为类生成帮助文档时是否要保留其注解信息

  • @Inherited

被它修饰的Annotation将具有继承性。

如果某个类使用了被@Inherited修饰的Annotation,则其子类将自动具有该注解。

Inherited是继承的意思,但是它并不是说注解本身可以继承,而是说如果一个超类被@Inherited注解过的注解进行注解的话(注解了B注解,B在注解其他),那么它的子类没有被任何注解应用的话,那么这个子类就继承了超类的注解

  • @Repeatable (Java8)

重复注解

四、自定义注解例子

4.1 手写ButterKnife框架(使用运行时注解的方式)

Demo 下载 :

其核心思想是java的ioc(inversion of control),也叫di(dependency injection,依赖注入),是一种面向对象编程中的设计模式。

来写一下布局文件的注入,比如我们不想写烦人的setContentView方法,直接用个注解来搞定,

首先,开始定义一个布局注解类:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface InjectLayout {
    int value();
}

创建一个类来获取布局文件并设置contentview:
获取到注解类中的值,然后通过反射执行activity中的setContentView方法
class InjectUtils {
    static void injectLayout(Context context) {
        // 1. 获取当前class
        Class<?> clazz = context.getClass();
        // 2. 根据class获取class上面的InjectLayout注解
        InjectLayout annotation = clazz.getAnnotation(InjectLayout.class);
        // 判空
        if (annotation == null) return;
        
        // 3. 获取注解中的值,这里就是布局文件的id
        int layoutId = annotation.value();
        try {
            // 4. 获取activity中的setContentView方法
            Method method = clazz.getMethod("setContentView", int.class);
            // 5. 执行setContentView方法,传入layoutId参数
            method.invoke(context, layoutId);
        } catch (Exception e) {
        }
    }
}

view的注入逻辑

static void injectView(Context context) {
        // 1. 获取当前class
        Class<?> clazz = context.getClass();

        // 2. 获取activity中所有的成员变量
        Field[] declaredFields = clazz.getDeclaredFields();
        // 3. 开始遍历
        for (Field field : declaredFields) {
            field.setAccessible(true);
            // 4. 获取字段上面的InjectView注解
            InjectView annotation = field.getAnnotation(InjectView.class);
            // 5. 如果字段上面没有注解,就不用处理了
            if (annotation == null) {
                return;
            }
            int viewId = annotation.value();
            try {
                // 6. 获取 findViewById 方法
                Method findViewMethod = clazz.getMethod("findViewById", int.class);
                // 7. 执行方法,获取View
                View view = (View) findViewMethod.invoke(context, viewId);
                // 8. 把view赋值给该字段
                field.set(context, view);
            } catch (Exception e) {
            }
        }
    }


事件的注入思路就是通过事件类型获取事件的类型和方法名,然后通过代理取到事件的方法,当执行事件的时候自动执行我们在activity中定义的事件方法。

 使用:

@InjectLayout(R.layout.activity_java)
public class JavaActivity extends AppCompatActivity {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }
}

有个布局文件
xxxxx

4.2 再来一个编译时注解的例子,以组件化为例:

1、引入AutoService,用于自动生成SPI清单文件

2、自定义注解处理器,自动将activity注入到Map集合中的,以供后续使用

3、生成path文件

由于篇幅所限,请移步apt文章查看

每个模块都相当于一个组(group),每个组里面由于有多个Activity, 所以每个Activity又维护了一个路径(path),当我们要跳转的时候,通过group找到对应的模块,再通过path找到具体的class。

1、定义两个注解

@Target(ElementType.TYPE) // 该注解作用在类之上
@Retention(RetentionPolicy.CLASS) // 要在编译时进行一些预处理操作,注解会在class文件中存在
public @interface IRouter {
    // 详细路由路径(必填),如:"/app/MainActivity"
    String path();
    // 路由组名(选填,如果开发者不填写,可以从path中截取出来)
    String group() default "";
}

@Target(ElementType.FIELD) // 该注解作用在属性之上
@Retention(RetentionPolicy.CLASS) // 要在编译时进行一些预处理操作,注解会在class文件中存在
public @interface Parameter {
    // 不填写name的注解值表示该属性名就是key,填写了就用注解值作为key
    // 从getIntent()方法中获取传递参数值
    String name() default "";
}

2、创建注解编译处理器模块 compiler

在注解处理器模块的gradle中引入AutoService,用于帮我们生成MATE-INF.services下的文件,需要这个文件系统才能帮我们识别是一个注解处理器.

3、编译模块自定义编译时注解 AbstractProcessor

这个类主要用于解析注解并生成文件

class IRouterProcessor : AbstractProcessor() {
    override fun process(annotations: MutableSet<out TypeElement>?, roundEnv: RoundEnvironment?): Boolean {
        
        // 获取所有的被注解的节点
        Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(IRouter.class);
        
        // 获取注解的path变量
        IRouter iRouter = element.getAnnotation(IRouter.class);
        val path = iRouter.path
        
        生成path
        用javapoet生成对应的文件

        return true
    }
}
public abstract boolean process(Set<? extends TypeElement> var1, RoundEnvironment var2);

这个就是所有注解的元素的集合,它的泛型是TypeElement的下限类型

我们注解的每一个元素,其实就是被包装成了一个个的Element放进了Set集合中

Element有以下几个实现类,代表了不同的元素

PackageElement             表示一个包程序元素。提供对有关包及其成员的信息的访问 
ExecutableElement         表示某个类或接口的方法、构造方法或初始化程序(静态或实例) 
TypeElement             表示一个类或接口程序元素。提供对有关类型及其成员的信息的访问 
VariableElement         表示一个字段、enum 常量、方法或构造方法参数、局部变量或异常参数

Element节点中的API

getEnclosedElements()     返回该元素直接包含的子元素 
getEnclosingElement()     返回包含该element的父element,与上一个方法相反 
getKind()                 返回element的类型,判断是哪种element 
getModifiers()             获取修饰关键字,入public static final等关键字 
getSimpleName()            获取名字,不带包名 
getQualifiedName()         获取全名,如果是类的话,包含完整的包名路径 
getParameters()         获取方法的参数元素,每个元素是一个VariableElement 
getReturnType()         获取方法元素的返回值 
getConstantValue()         如果属性变量被final修饰,则可以使用该方法获取它的值

 javapoet是square推出的开源java代码生成框架,提供Java Api生成.java源文件 这个框架功能非常实用.

4、定义路由管理类,路由管理器,辅助完成交互通信

分别用于存储每个module及module下的路径

使用:

@IRouter(path = "/order/ooo”)
public class DerActivity extends AppCompatActivity {

@IRouter(path = "/user/test”)
public class UserActivity extends AppCompatActivity {
   
   

猜你喜欢

转载自blog.csdn.net/fumeidonga/article/details/130513712