网站公告

本次更新:没啥更新的

“钱塘江上潮信起,今日方知我是我”

Skip to content

注解 ​

字数: 0 字 时长: 0 分钟

Java 注解 ​

字数: 0 字 时长: 0 分钟

注解(Annotation)是 Java 5 引入的一种元数据机制。

它可以附加在:

text
类

方法

字段

参数

构造方法

包

模块

类型使用位置

等程序元素上,用来描述这些代码的额外信息。

注解本身通常不会主动执行任何业务逻辑,真正产生作用的是:

text
编译器

注解处理器

反射代码

框架

AOP

等读取注解之后,根据其中的信息执行相应的处理。

可以简单理解为:

text
Java 代码
    +
Annotation 元数据
    ↓
编译器 / JVM / 框架读取
    ↓
执行对应逻辑

例如 Spring 中非常常见:

java
@Autowired
private UserService userService;

@Autowired 本身不会主动创建或者注入对象。

真正完成依赖注入的是:

text
Spring 容器
    ↓
发现 @Autowired
    ↓
解析注解
    ↓
寻找对应 Bean
    ↓
完成依赖注入

假设项目中只是引入了 @Autowired 所在的依赖,但根本没有启动 Spring 容器:

java
public class AutowiredTest {

    @Autowired
    private String name;

    public static void main(String[] args) {

        System.out.println(
                "kerwim,一名 Java 牛马"
        );
    }
}

那么:

java
@Autowired

并不会凭空完成注入。

所以注解最核心的理解就是:

注解负责提供元数据,真正的行为由读取和处理注解的程序决定。

1. 注解的基本结构 ​

Java 使用:

java
@interface

定义注解。

例如:

java
public @interface JsonField {
}

虽然写法是:

java
@interface

但它并不是普通接口。

这是 Java 专门用于:

text
声明注解类型

的语法。

使用注解时则通过:

java
@

开头:

java
@JsonField
private String name;

可以理解为:

text
@interface
    ↓
定义注解


@
    ↓
使用注解

2. 注解的生命周期 ​

注解能够保留到程序的哪个阶段,由:

java
@Retention

决定。

它的值来自:

java
RetentionPolicy

主要有三种。

2.1 SOURCE ​

java
RetentionPolicy.SOURCE

表示注解只存在于源代码阶段。

例如:

java
@Retention(RetentionPolicy.SOURCE)
public @interface Demo {
}

生命周期:

text
.java
 ↓
注解存在
 ↓
javac 编译
 ↓
.class
 ↓
注解不再保留

这种注解通常用于:

text
编译期检查

源码分析

代码生成辅助

2.2 CLASS ​

java
RetentionPolicy.CLASS

表示注解会被记录到:

text
.class 字节码文件

中。

但是默认情况下不会保留到运行时供普通反射读取。

生命周期:

text
.java
 ↓
.class
 ↓
注解仍然存在
 ↓
运行时普通反射不可获取

CLASS 也是:

java
@Retention

没有显式指定时的默认策略。

2.3 RUNTIME ​

java
RetentionPolicy.RUNTIME

表示注解不仅会写入字节码,而且在程序运行时仍然保留。

因此可以通过:

text
反射

读取。

例如:

java
@Retention(
        RetentionPolicy.RUNTIME
)
public @interface JsonField {
}

这种策略在各种框架中非常常见,例如:

text
Spring

MyBatis

Jackson

JUnit

Hibernate

因为框架通常需要在运行时检查:

text
这个类有没有某个注解?

这个方法有没有某个注解?

这个字段上的注解参数是什么?

三种策略可以直接记成:

RetentionPolicy源码字节码运行时反射
SOURCE✓✗✗
CLASS✓✓✗
RUNTIME✓✓✓

3. 注解可以使用在哪里 ​

注解能够修饰什么位置,通过:

java
@Target

指定。

@Target 使用的是:

java
ElementType

枚举。

常见类型有:

ElementType作用位置
TYPE类、接口、枚举、注解等类型声明
FIELD字段、枚举常量
METHOD方法
PARAMETER方法或构造方法参数
CONSTRUCTOR构造方法
LOCAL_VARIABLE局部变量
ANNOTATION_TYPE其他注解
PACKAGE包
TYPE_PARAMETER泛型类型参数
TYPE_USE类型使用位置
MODULE模块
RECORD_COMPONENTRecord 组件

例如:

java
@Target(ElementType.FIELD)
public @interface JsonField {
}

表示这个注解只能放在:

text
字段

上。

正确:

java
@JsonField
private String name;

如果放到方法上:

java
// 编译错误
// @JsonField
// public void test() {
// }

就不允许。

如果一个注解可以使用在多个位置,可以写:

java
@Target({
        ElementType.TYPE,
        ElementType.METHOD
})
public @interface Log {
}

这样:

java
@Log
public class UserService {

    @Log
    public void save() {
    }
}

都合法。

4. 元注解 ​

用来修饰其他注解的注解,通常称为:

text
元注解
Meta Annotation

最常见的几个包括:

text
@Retention

@Target

@Documented

@Inherited

@Repeatable

其中最常用的两个就是:

java
@Retention

和:

java
@Target

例如:

java
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface JsonField {
}

含义就是:

text
@Retention(RUNTIME)
    ↓
运行时仍然保留


@Target(FIELD)
    ↓
只能标记字段

5. 自定义注解 ​

下面自己定义一个:

java
@JsonField

用于标记:

哪些字段需要参与简单的 JSON 序列化。

定义:

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

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

    String value() default "";
}

这里有几个重点。

5.1 @interface ​

java
public @interface JsonField {
}

表示定义一个注解类型。

5.2 Retention ​

java
@Retention(
        RetentionPolicy.RUNTIME
)

表示:

text
JsonField

运行时仍然存在。

这样后面才能通过反射读取。

5.3 Target ​

java
@Target(ElementType.FIELD)

表示:

text
@JsonField

只能用于字段。

5.4 注解属性 ​

注解中的:

java
String value() default "";

看起来像一个方法,但它实际上声明的是:

text
注解元素 / 注解属性

这里定义了:

text
名称:value

类型:String

默认值:""

所以可以写:

java
@JsonField(
        value = "writerName"
)
private String name;

6. 为什么经常叫 value ​

注解中的属性不一定必须叫:

java
value

例如完全可以定义:

java
public @interface JsonField {

    String name() default "";
}

然后使用:

java
@JsonField(
        name = "writerName"
)
private String name;

但是:

text
value

有一个特殊的语法糖。

如果注解只有一个需要设置的属性,并且名字叫:

java
value

那么使用时可以省略:

text
value =

原本:

java
@JsonField(
        value = "writerName"
)

可以简化为:

java
@JsonField("writerName")

所以:

java
@JsonField("writerName")

实际上等价于:

java
@JsonField(
        value = "writerName"
)

这也是很多注解喜欢使用:

java
value

作为主要属性名称的原因。

7. default 默认值 ​

定义:

java
String value() default "";

表示:

text
value 默认值为空字符串

因此使用时可以完全不传参数:

java
@JsonField
private String bookName;

这时候:

java
value()

返回:

text
""

如果没有:

java
default

例如:

java
public @interface JsonField {

    String value();
}

那么使用时就必须提供:

java
@JsonField("writerName")
private String name;

不能直接写:

java
@JsonField

8. 注解属性支持哪些类型 ​

注解属性并不是任意 Java 类型都可以使用。

常见合法类型包括:

text
基本数据类型

String

Class

枚举

注解

以上类型的数组

例如:

java
public @interface Demo {

    String name();

    int age() default 18;

    Class<?> type();

    String[] tags() default {};
}

不能随便写:

java
// 不允许
// List<String> value();

所以注解更适合保存:

text
简单、静态的元数据

而不是复杂业务对象。

9. 使用自定义注解 ​

假设有一个:

java
Writer

类。

包含:

text
age

name

bookName

三个字段。

其中:

text
name

bookName

需要参与序列化。

可以这样写:

java
public class Writer {

    private int age;

    @JsonField("writerName")
    private String name;

    @JsonField
    private String bookName;

    public Writer(
            int age,
            String name,
            String bookName) {

        this.age = age;
        this.name = name;
        this.bookName = bookName;
    }

    public int getAge() {
        return age;
    }

    public String getName() {
        return name;
    }

    public String getBookName() {
        return bookName;
    }

    @Override
    public String toString() {

        return "Writer{" +
                "age=" + age +
                ", name='" + name + '\'' +
                ", bookName='" +
                bookName + '\'' +
                '}';
    }
}

这里:

java
@JsonField("writerName")
private String name;

表示序列化的时候使用:

text
writerName

作为字段名。

而:

java
@JsonField
private String bookName;

没有指定:

text
value

因此使用字段本身的:

text
bookName

作为名称。

age 没有:

java
@JsonField

因此我们后面的序列化程序会忽略它。

10. 通过反射读取注解 ​

注解真正有意义的地方在于:

其他代码可以读取它,然后根据注解完成不同的逻辑。

下面写一个简单的:

java
JsonSerializer

通过反射读取:

java
@JsonField

注解。

java
import java.lang.reflect.Field;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.stream.Collectors;

public class JsonSerializer {

    public static String serialize(
            Object object)
            throws IllegalAccessException {

        Class<?> objectClass =
                object.getClass();

        Map<String, Object> jsonElements =
                new LinkedHashMap<>();

        Field[] fields =
                objectClass
                        .getDeclaredFields();

        for (Field field : fields) {

            // 只处理带有 @JsonField 的字段
            if (!field.isAnnotationPresent(
                    JsonField.class)) {

                continue;
            }

            // 教学示例:
            // 允许反射读取 private 字段
            field.setAccessible(true);

            String key =
                    getSerializedKey(
                            field
                    );

            Object value =
                    field.get(object);

            jsonElements.put(
                    key,
                    value
            );
        }

        return toJsonString(
                jsonElements
        );
    }

    private static String getSerializedKey(
            Field field) {

        JsonField jsonField =
                field.getAnnotation(
                        JsonField.class
                );

        String annotationValue =
                jsonField.value();

        if (annotationValue.isEmpty()) {

            return field.getName();
        }

        return annotationValue;
    }

    private static String toJsonString(
            Map<String, Object> jsonMap) {

        String elements =
                jsonMap.entrySet()
                        .stream()
                        .map(entry ->
                                "\""
                                        + entry.getKey()
                                        + "\":\""
                                        + String.valueOf(
                                                entry.getValue()
                                        )
                                        + "\""
                        )
                        .collect(
                                Collectors.joining(",")
                        );

        return "{" + elements + "}";
    }
}

整个执行过程可以拆成几步。

10.1 获取 Class ​

java
Class<?> objectClass =
        object.getClass();

获得对象对应的:

text
Class 对象

后面通过它进行反射操作。

10.2 获取字段 ​

java
Field[] fields =
        objectClass.getDeclaredFields();

获得当前类声明的字段。

例如:

text
age

name

bookName

10.3 判断有没有注解 ​

java
field.isAnnotationPresent(
        JsonField.class
)

判断当前字段是否存在:

java
@JsonField

如果没有,就跳过。

10.4 获取注解对象 ​

java
JsonField jsonField =
        field.getAnnotation(
                JsonField.class
        );

可以获得当前字段上的:

java
JsonField

注解对象。

接下来就可以:

java
jsonField.value();

读取注解属性。

10.5 获取 private 字段 ​

示例中:

java
field.setAccessible(true);

允许后续反射读取当前字段。

然后:

java
field.get(object);

就能获取该对象对应字段的值。

需要注意:

setAccessible(true) 属于突破普通访问检查的反射能力,现代 Java 的模块系统和强封装环境下还可能受到额外限制。

这里作为理解注解与反射配合使用的教学案例即可。

11. 测试自定义注解 ​

测试:

java
public class JsonFieldTest {

    public static void main(
            String[] args)
            throws IllegalAccessException {

        Writer kerwim =
                new Writer(
                        18,
                        "kerwim",
                        "牛马程序员"
                );

        String json =
                JsonSerializer.serialize(
                        kerwim
                );

        System.out.println(json);
    }
}

输出类似:

json
{
  "writerName": "kerwim",
  "bookName": "牛马程序员"
}

为了方便展示,上面进行了格式化。

程序真正拼接出来可能类似:

text
{"writerName":"kerwim","bookName":"牛马程序员"}

这里发生了三件事。

age:

java
private int age;

没有:

java
@JsonField

所以不会序列化。

name:

java
@JsonField("writerName")
private String name;

指定了:

text
writerName

所以最终:

text
name
    ↓
writerName

bookName:

java
@JsonField
private String bookName;

没有指定注解值,因此:

text
bookName

继续使用字段自己的名字。

整体流程就是:

text
Writer
  ↓
反射扫描字段
  ↓
有没有 @JsonField?
  ↓
有
  ↓
读取 @JsonField.value()
  ↓
读取字段值
  ↓
拼接 JSON

需要说明的是:

这个 JsonSerializer 只是为了理解“注解 + 反射”的工作原理,并不是一个真正完整的 JSON 序列化器。

真正的 JSON 序列化还需要处理:

text
数字

boolean

null

数组

集合

嵌套对象

特殊字符转义

循环引用

实际开发一般直接使用:

text
Jackson

Gson

Fastjson2

等成熟工具。

12. 常见的 Java 内置注解 ​

Java 自己也提供了很多常见注解。

12.1 @Override ​

java
@Override
public String toString() {
    return "hello";
}

表示:

当前方法应该重写父类或者接口中的方法。

最大的作用是:

text
让编译器帮我们检查重写是否正确

12.2 @Deprecated ​

java
@Deprecated
public void oldMethod() {
}

表示:

text
这个 API 已经过时

调用时 IDE 或编译器通常会给出警告。

12.3 @SuppressWarnings ​

用于抑制指定的编译器警告。

例如:

java
@SuppressWarnings("unchecked")
public void test() {
}

12.4 @FunctionalInterface ​

用于表示:

text
函数式接口

例如:

java
@FunctionalInterface
public interface Callback {

    void call();
}

编译器会检查这个接口是否满足函数式接口要求。

13. 注解与 AOP ​

自定义注解在实际开发中一个非常经典的用途就是:

text
AOP

尤其是在 Spring 项目中,经常会看到:

text
自定义注解
    +
AOP 切面

这种组合。

例如希望自动记录方法执行日志,可以先定义:

java
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface LogOperation {

    String value() default "";
}

业务方法:

java
public class OrderService {

    @LogOperation("创建订单")
    public void createOrder() {

        System.out.println(
                "创建订单"
        );
    }
}

这时候:

java
@LogOperation

本身并不会输出任何日志。

真正的流程通常是:

text
调用 createOrder()
    ↓
Spring AOP 拦截方法
    ↓
检查有没有 @LogOperation
    ↓
读取注解信息
    ↓
执行日志逻辑
    ↓
调用真正的业务方法

所以项目中经常可以看到类似的自定义注解:

text
@Log

@OperationLog

@RateLimit

@Lock

@Idempotent

@DataPermission

@Permission

@RepeatSubmit

例如:

java
@RateLimit
public void query() {
}

背后的逻辑可能就是:

text
AOP 拦截
    ↓
发现 @RateLimit
    ↓
读取注解参数
    ↓
执行 Redis / Lua 限流逻辑
    ↓
通过才执行业务方法

所以:

注解负责“声明我要什么”,AOP 或框架负责“真正去做”。

这也是实际 Java 项目中自定义注解最常见、最重要的应用方式之一。

14. 小结 ​

Java 注解可以简单理解为:

text
给代码贴标签
    ↓
标签中携带元数据
    ↓
编译器 / 反射 / 框架读取
    ↓
完成对应处理

几个核心知识点:

text
@interface
    ↓
定义注解


@Retention
    ↓
注解保留多久


@Target
    ↓
注解能放在哪里


RUNTIME
    ↓
运行时可以通过反射读取


value
    ↓
唯一主要属性时可以省略属性名


default
    ↓
提供默认值

而自定义注解最常见的工作方式就是:

text
自定义注解
    +
反射

或者在 Spring 中:

text
自定义注解
    +
AOP

最终形成:

text
业务代码
    ↓
只负责声明意图


框架代码
    ↓
统一处理公共逻辑

一句话概括:

注解本身只是元数据,真正赋予注解能力的是读取并处理它的编译器、反射代码或框架。

Noctiflorous Garden · MIT License