Java基石--注解让你也能写框架

前言

理解一个概念,需要从:为什么是它?它怎么用?它用在哪些场景?从这三个方面去阐述一个概念、术语从而更好的理解它。

网上不少关于注解的文章写的挺好的,不过大多集中在注解的使用上,本次我们围绕以上三个角度去阐述注解的概念。

通过本篇文章,你将了解到:

  1. 为什么需要注解?
  2. 注解的原理
  3. 注解的常规用法
  4. 注解用在哪些场景?

1. 为什么需要注解?

注释

平时我们写代码,比如新建一个类的时候会自动/手动在类前面添加注释如:

java 复制代码
package com.fish.annotation;

/**
 * Dog类用于展示作者信息
 * 该类包含一个display方法,用于输出作者信息
 *
 * @author 小鱼人
 */
public class Dog {
    public void display(String author) {
        System.out.println("Dog author:" + author);
    }
}

注释的作用是:

  1. 让自己温故知新,让别人了解前因后果
  2. 是给人看的

查看Dog.class:

可以看出,注释只在.java里显示,编译为.class后就没了,因此注释是给人看的。

同样的,不仅是类上有注释,我们可以在源码的任何地方添加注释,前提是我们得让编译器知道我们这是注释。

大部分的语言注释标准是:"//" 或者 "/** */",包含在里面的内容都被编译器当成注释,编译时会丢弃这部分的信息。

注释只能给人看,现在想要在代码运行的时候能够知道这个类的编写者是谁?

你可能会说:这简单啊,在类里定义一个属性(author),默认设置为编写者,这不就妥了吗?

java 复制代码
/**
 * Dog类用于展示作者信息
 * 该类包含一个display方法,用于输出作者信息
 *
 * @author 小鱼人
 */
public class Dog {
    String author = "小鱼人";
    public void display(String author) {
        System.out.println("Dog author:" + author);
    }
}

增加属性当然是可以的,但考虑以下两点:

  1. 它是否是该类的非必须的属性(没有它编译是否正常、运行是否正常)
  2. 它是否仅用于标记代码的额外功能(给代码打上标签)?

因此,我们需要一种额外的方式来给代码添加标签,在Java里,这种标签叫注解(注释也是一种标签)。

注解

注释、注解仅一字之差,它们都是用来给代码打标签的,只是用在不同的场景。

那编译器怎么区分两者呢?

  1. "//" 之后,包裹在"/** */" 里面的表示注释
  2. "@"之后的代表注解

定义简单的注解如下:

java 复制代码
package com.fish.annotation;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)
public @interface AuthorAnnotation {
    String author();
}

该注解名为AuthorAnnotation,有一个author()方法,用来记录作者名字。

然后再用它去修饰Dog类:

java 复制代码
/**
 * Dog类用于展示作者信息
 * 该类包含一个display方法,用于输出作者信息
 *
 * @author 小鱼人
 */
@AuthorAnnotation(author = "小鱼人")
public class Dog {
    public void display(String author) {
        System.out.println("Dog author:" + author);
    }
}

当我们运行程序的时候,就可以从Dog取出AuthorAnnotation注解,从而取到它的author值。

即使我们不使用注解,也不会影响Dog的编译、运行。

2. 注解的原理

注解的原理

定义注解:

java 复制代码
public @interface AuthorAnnotation {
    String author();
}

查看AuthorAnnotation.class文件:

发现它是抽象的注解类型,并实现了Annotation接口。

@interface表示注解类型。

注解里的每个方法代表一个属性。

元注解

java 复制代码
@Retention(RetentionPolicy.RUNTIME)
public @interface AuthorAnnotation {
    String author();
}

用于注解其他注解的注解,@AuthorAnnotation是我们自定义的注解,@Retention是系统提供的,用来注解AuthorAnnotation,因此@Retention称为元注解,除了它还有:@Target、@Documented等。

3. 注解的常规用法

注解的生命周期(保留策略)

注解有三种生命周期:

  1. 注解只在源码级别(.java)生效
  2. 注解在源码级别(.java)、编译级别(.class)生效
  3. 注解在源码级别(.java)、编译级别(.class)生效、运行级别(运行时)生效

可以看出,注解的三种生命周期范围是逐级递增,注解的生命周期可以使用元注解:@Retention指定。

1. 定义第1种生命周期注解:

java 复制代码
@Retention(RetentionPolicy.SOURCE)
public @interface AuthorAnnotation {
    String author();
}

在另一个类引用它:

java 复制代码
@AuthorAnnotation(author = "小鱼人")
public class Dog {
}

查看Dog.class:

发现它里面并没有AuthorAnnotation注解的任何信息。是因为我们设置了: @Retention(RetentionPolicy.SOURCE),意思是注解只在源码级别保留,因此.class里并没有它。

那么此种注解有什么作用呢?通常提供给编译器使用,比如我们熟知的:@Override 注解

java 复制代码
    @Override
    public String getName() {
        return name;
    }

查看 @Override定义:

java 复制代码
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}

也是源码级别的,编译器随时监测.java文件里的编译规范,一旦发现某个方法需要重写父类方法就会提示使用@Override修饰,因此它的生命周期停留在源码阶段足矣。

除了@Override,@Native、@SuppressWarnings的生命周期也是源码级别的。

此种级别的生命周期主要用在:编译器的代码检测和提示等。

2. 定义第2种生命周期注解:

java 复制代码
@Retention(RetentionPolicy.CLASS)
public @interface AuthorAnnotation {
    String author();
}

在另一个类引用它:

java 复制代码
@AuthorAnnotation(author = "小鱼人")
public class Dog {
}

并查看Dog.class:

可以看出Dog.class里存有AuthorAnnotation,并且后面有个invisible注释其为运行期不可见,也就是不能通过反射读取Dog类上的注解。

Android里比较流行的路由库ARouter使用注解配置路由信息,其注解如下:

此种级别的生命周期主要用在:编译期代码自动生成,常用在一些生成中间代码文件的框架。

大致流程是:扫描生成的.class文件,从中提取出注解信息(借助:AbstractProcessor),从而决定是否需要生成.java文件。

3. 定义第3种生命周期注解:

java 复制代码
@Retention(RetentionPolicy.RUNTIME)
public @interface AuthorAnnotation {
    String author();
}

查看Dog.class:

改class文件里不仅保留了AuthorAnnotation,并且运行时也可以通过反射读取Dog类上的注解。

此种级别的生命周期主要用在:运行期间通过反射读取注解信息。

广泛用于各种Java框架里,包括著名的SpringBoot,如下为它处理网络请求的类:

这些注解的生命周期都是运行时。

我们主要关注第3种生命周期的注解,如若注解不显式指明@Retention,那么默认是RetentionPolicy.CLASS。

注解的作用域

一个类里的关键元素有:类本身、构造函数、方法、方法参数、属性。

若要限定注解只能在某些元素上使用,可使用元注解:@Target

它可取的值以及对应能修饰的不同元素如下:

ElementType.TYPE:类、接口、枚举

ElementType.FIELD:字段

ElementType.METHOD:方法

ElementType.PARAMETER:参数

ElementType.CONSTRUCTOR:构造器

ElementType.LOCAL_VARIABLE:局部变量

ElementType.ANNOTATION_TYPE:注解类型

ElementType.PACKAGE:包

ElementType.TYPE_PARAMETER:类型参数

ElementType.TYPE_USE:类型使用

如若不显示指明@Target,那么默认的作用域是所有类型。

我们通常使用到的是前五种类型,接下来看看如何在运行期解析注解。

1. 注解修饰类

定义注解:

java 复制代码
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface ClassAnnotation {
    String author();
}

使用注解:

java 复制代码
@ClassAnnotation(author = "小鱼人")
public class Dog {
}

解析注解:

java 复制代码
public class Main {
    public static void main(String[] args) throws Exception {
        Dog dog = new Dog();
        if (dog.getClass().isAnnotationPresent(ClassAnnotation.class)) {
            ClassAnnotation annotation = dog.getClass().getAnnotation(ClassAnnotation.class);
            String author = annotation.author();
            System.out.print("author:"+author);
        }
    }
}

通过反射获得Dog类上的注解,拿到注解对象后即可调用注解里的方法(实际获取的是属性值)。

需要注意的是获取注解前最好先判断注解是否存在(isAnnotationPresent)。

当我们需要修改Dog作者时,只需要修改注解的值即可。

2. 注解修饰方法和属性

定义注解:

java 复制代码
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MethodAnnotation {
    String author() default "小鱼人马甲";
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface FieldAnnotation {
    String author() default "小鱼人马甲";
}

使用注解:

java 复制代码
@ClassAnnotation(author = "小鱼人")
public class Dog {

    @FieldAnnotation(author = "小鱼人-属性")
    private String name;

    @MethodAnnotation(author = "小鱼人-方法")
    private void displayAuthor(String author) {
        System.out.println("author:" + author);
    }
}

解析注解:

java 复制代码
public class Main {
    public static void main(String[] args) throws Exception {
        Dog dog = new Dog();
        Class<?> dogClass = dog.getClass();
        Method method = dogClass.getDeclaredMethod("displayAuthor", String.class);
        if (method.isAnnotationPresent(MethodAnnotation.class)) {
            //获取方法上的注解
            MethodAnnotation methodAnnotation = method.getAnnotation(MethodAnnotation.class);
            String author = methodAnnotation.author();
            //调用方法
            method.setAccessible(true);
            method.invoke(dog, author);
        }

        Field field = dogClass.getDeclaredField("name");
        if (field.isAnnotationPresent(FieldAnnotation.class)) {
            //获取字段上的注解
            FieldAnnotation fieldAnnotation = field.getAnnotation(FieldAnnotation.class);
            String author = fieldAnnotation.author();
            //调用方法
            field.setAccessible(true);
            field.set(dog, author);
            System.out.println("name:" + field.get(dog));
        }
    }
}

通过以上对类、方法、属性上的注解的动态解析,我们明白了注解作用域的使用方式,其它未列出的方式与此类似。

如果注解里只有一个属性名为:value,那么在引用该注解赋值时,可以省略名称,如下:

java 复制代码
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface DefaultValueAnnotation {
    String value() default "fish";
}

@DefaultValueAnnotation("I'm default")//无需明确写 @DefaultValueAnnotation(value = "I'm default")
public class Cat {
}

4. 注解用在哪些场景?

其中运行期解析很常见,SpringBoot里大量的注解在运行期解析,它的流程大致如下:

  1. 启动时扫描组件,解析标记了 @Controller、@RestController 的类,并生成Bean对象
  2. 将 @RequestMapping注解的方法和注解的路径绑定
  3. 当有接口请求到来后,根据接口的路径匹配第2步的缓存,找到对应的方法,反射调用

使用注解即可实现了解耦的目的,也让你写的代码高大上起来,当熟悉了注解的使用后,我们就可以写写框架练手了。

本篇到此就结束了,若是有帮助,请一键三连~

下篇将重点分析代理,尤其是动态代理,敬请期待。

相关推荐
Star在努力6 分钟前
20-C语言:第21~22天笔记
java·c语言·笔记
汪子熙23 分钟前
如何使用 Node.js 代码下载 Github issue 到本地
javascript·后端
冒泡的肥皂24 分钟前
2PL-事务并发(二
数据库·后端·mysql
xiaok31 分钟前
nginx反向代理中server块中的配置信息详解
后端
LH_R37 分钟前
OneTerm 开源堡垒机 | 历时三个月重构大更新
运维·后端·安全
久下不停雨37 分钟前
单例模式代码实现
后端
用户849137175471638 分钟前
JDK 17 实战系列(第2期):核心语言特性深度解析
java·后端
自由的疯40 分钟前
Java 8 新特性之 Lambda 表达式
java·后端·架构
meiguiyulu41 分钟前
深入理解线程生命周期:从创建到终止的全状态解析
后端
自由的疯42 分钟前
Java 17 新特性之 Text Blocks(文本块)
java·后端·架构