注解
字数: 0 字 时长: 0 分钟
Java 注解
字数: 0 字 时长: 0 分钟
注解(Annotation)是 Java 5 引入的一种元数据机制。
它可以附加在:
类
方法
字段
参数
构造方法
包
模块
类型使用位置等程序元素上,用来描述这些代码的额外信息。
注解本身通常不会主动执行任何业务逻辑,真正产生作用的是:
编译器
注解处理器
反射代码
框架
AOP等读取注解之后,根据其中的信息执行相应的处理。
可以简单理解为:
Java 代码
+
Annotation 元数据
↓
编译器 / JVM / 框架读取
↓
执行对应逻辑例如 Spring 中非常常见:
@Autowired
private UserService userService;@Autowired 本身不会主动创建或者注入对象。
真正完成依赖注入的是:
Spring 容器
↓
发现 @Autowired
↓
解析注解
↓
寻找对应 Bean
↓
完成依赖注入假设项目中只是引入了 @Autowired 所在的依赖,但根本没有启动 Spring 容器:
public class AutowiredTest {
@Autowired
private String name;
public static void main(String[] args) {
System.out.println(
"kerwim,一名 Java 牛马"
);
}
}那么:
@Autowired并不会凭空完成注入。
所以注解最核心的理解就是:
注解负责提供元数据,真正的行为由读取和处理注解的程序决定。
1. 注解的基本结构
Java 使用:
@interface定义注解。
例如:
public @interface JsonField {
}虽然写法是:
@interface但它并不是普通接口。
这是 Java 专门用于:
声明注解类型的语法。
使用注解时则通过:
@开头:
@JsonField
private String name;可以理解为:
@interface
↓
定义注解
@
↓
使用注解2. 注解的生命周期
注解能够保留到程序的哪个阶段,由:
@Retention决定。
它的值来自:
RetentionPolicy主要有三种。
2.1 SOURCE
RetentionPolicy.SOURCE表示注解只存在于源代码阶段。
例如:
@Retention(RetentionPolicy.SOURCE)
public @interface Demo {
}生命周期:
.java
↓
注解存在
↓
javac 编译
↓
.class
↓
注解不再保留这种注解通常用于:
编译期检查
源码分析
代码生成辅助2.2 CLASS
RetentionPolicy.CLASS表示注解会被记录到:
.class 字节码文件中。
但是默认情况下不会保留到运行时供普通反射读取。
生命周期:
.java
↓
.class
↓
注解仍然存在
↓
运行时普通反射不可获取CLASS 也是:
@Retention没有显式指定时的默认策略。
2.3 RUNTIME
RetentionPolicy.RUNTIME表示注解不仅会写入字节码,而且在程序运行时仍然保留。
因此可以通过:
反射读取。
例如:
@Retention(
RetentionPolicy.RUNTIME
)
public @interface JsonField {
}这种策略在各种框架中非常常见,例如:
Spring
MyBatis
Jackson
JUnit
Hibernate因为框架通常需要在运行时检查:
这个类有没有某个注解?
这个方法有没有某个注解?
这个字段上的注解参数是什么?三种策略可以直接记成:
| RetentionPolicy | 源码 | 字节码 | 运行时反射 |
|---|---|---|---|
SOURCE | ✓ | ✗ | ✗ |
CLASS | ✓ | ✓ | ✗ |
RUNTIME | ✓ | ✓ | ✓ |
3. 注解可以使用在哪里
注解能够修饰什么位置,通过:
@Target指定。
@Target 使用的是:
ElementType枚举。
常见类型有:
| ElementType | 作用位置 |
|---|---|
TYPE | 类、接口、枚举、注解等类型声明 |
FIELD | 字段、枚举常量 |
METHOD | 方法 |
PARAMETER | 方法或构造方法参数 |
CONSTRUCTOR | 构造方法 |
LOCAL_VARIABLE | 局部变量 |
ANNOTATION_TYPE | 其他注解 |
PACKAGE | 包 |
TYPE_PARAMETER | 泛型类型参数 |
TYPE_USE | 类型使用位置 |
MODULE | 模块 |
RECORD_COMPONENT | Record 组件 |
例如:
@Target(ElementType.FIELD)
public @interface JsonField {
}表示这个注解只能放在:
字段上。
正确:
@JsonField
private String name;如果放到方法上:
// 编译错误
// @JsonField
// public void test() {
// }就不允许。
如果一个注解可以使用在多个位置,可以写:
@Target({
ElementType.TYPE,
ElementType.METHOD
})
public @interface Log {
}这样:
@Log
public class UserService {
@Log
public void save() {
}
}都合法。
4. 元注解
用来修饰其他注解的注解,通常称为:
元注解
Meta Annotation最常见的几个包括:
@Retention
@Target
@Documented
@Inherited
@Repeatable其中最常用的两个就是:
@Retention和:
@Target例如:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface JsonField {
}含义就是:
@Retention(RUNTIME)
↓
运行时仍然保留
@Target(FIELD)
↓
只能标记字段5. 自定义注解
下面自己定义一个:
@JsonField用于标记:
哪些字段需要参与简单的 JSON 序列化。
定义:
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
public @interface JsonField {
}表示定义一个注解类型。
5.2 Retention
@Retention(
RetentionPolicy.RUNTIME
)表示:
JsonField运行时仍然存在。
这样后面才能通过反射读取。
5.3 Target
@Target(ElementType.FIELD)表示:
@JsonField只能用于字段。
5.4 注解属性
注解中的:
String value() default "";看起来像一个方法,但它实际上声明的是:
注解元素 / 注解属性这里定义了:
名称:value
类型:String
默认值:""所以可以写:
@JsonField(
value = "writerName"
)
private String name;6. 为什么经常叫 value
注解中的属性不一定必须叫:
value例如完全可以定义:
public @interface JsonField {
String name() default "";
}然后使用:
@JsonField(
name = "writerName"
)
private String name;但是:
value有一个特殊的语法糖。
如果注解只有一个需要设置的属性,并且名字叫:
value那么使用时可以省略:
value =原本:
@JsonField(
value = "writerName"
)可以简化为:
@JsonField("writerName")所以:
@JsonField("writerName")实际上等价于:
@JsonField(
value = "writerName"
)这也是很多注解喜欢使用:
value作为主要属性名称的原因。
7. default 默认值
定义:
String value() default "";表示:
value 默认值为空字符串因此使用时可以完全不传参数:
@JsonField
private String bookName;这时候:
value()返回:
""如果没有:
default例如:
public @interface JsonField {
String value();
}那么使用时就必须提供:
@JsonField("writerName")
private String name;不能直接写:
@JsonField8. 注解属性支持哪些类型
注解属性并不是任意 Java 类型都可以使用。
常见合法类型包括:
基本数据类型
String
Class
枚举
注解
以上类型的数组例如:
public @interface Demo {
String name();
int age() default 18;
Class<?> type();
String[] tags() default {};
}不能随便写:
// 不允许
// List<String> value();所以注解更适合保存:
简单、静态的元数据而不是复杂业务对象。
9. 使用自定义注解
假设有一个:
Writer类。
包含:
age
name
bookName三个字段。
其中:
name
bookName需要参与序列化。
可以这样写:
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 + '\'' +
'}';
}
}这里:
@JsonField("writerName")
private String name;表示序列化的时候使用:
writerName作为字段名。
而:
@JsonField
private String bookName;没有指定:
value因此使用字段本身的:
bookName作为名称。
age 没有:
@JsonField因此我们后面的序列化程序会忽略它。
10. 通过反射读取注解
注解真正有意义的地方在于:
其他代码可以读取它,然后根据注解完成不同的逻辑。
下面写一个简单的:
JsonSerializer通过反射读取:
@JsonField注解。
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
Class<?> objectClass =
object.getClass();获得对象对应的:
Class 对象后面通过它进行反射操作。
10.2 获取字段
Field[] fields =
objectClass.getDeclaredFields();获得当前类声明的字段。
例如:
age
name
bookName10.3 判断有没有注解
field.isAnnotationPresent(
JsonField.class
)判断当前字段是否存在:
@JsonField如果没有,就跳过。
10.4 获取注解对象
JsonField jsonField =
field.getAnnotation(
JsonField.class
);可以获得当前字段上的:
JsonField注解对象。
接下来就可以:
jsonField.value();读取注解属性。
10.5 获取 private 字段
示例中:
field.setAccessible(true);允许后续反射读取当前字段。
然后:
field.get(object);就能获取该对象对应字段的值。
需要注意:
setAccessible(true)属于突破普通访问检查的反射能力,现代 Java 的模块系统和强封装环境下还可能受到额外限制。
这里作为理解注解与反射配合使用的教学案例即可。
11. 测试自定义注解
测试:
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);
}
}输出类似:
{
"writerName": "kerwim",
"bookName": "牛马程序员"
}为了方便展示,上面进行了格式化。
程序真正拼接出来可能类似:
{"writerName":"kerwim","bookName":"牛马程序员"}这里发生了三件事。
age:
private int age;没有:
@JsonField所以不会序列化。
name:
@JsonField("writerName")
private String name;指定了:
writerName所以最终:
name
↓
writerNamebookName:
@JsonField
private String bookName;没有指定注解值,因此:
bookName继续使用字段自己的名字。
整体流程就是:
Writer
↓
反射扫描字段
↓
有没有 @JsonField?
↓
有
↓
读取 @JsonField.value()
↓
读取字段值
↓
拼接 JSON需要说明的是:
这个
JsonSerializer只是为了理解“注解 + 反射”的工作原理,并不是一个真正完整的 JSON 序列化器。
真正的 JSON 序列化还需要处理:
数字
boolean
null
数组
集合
嵌套对象
特殊字符转义
循环引用实际开发一般直接使用:
Jackson
Gson
Fastjson2等成熟工具。
12. 常见的 Java 内置注解
Java 自己也提供了很多常见注解。
12.1 @Override
@Override
public String toString() {
return "hello";
}表示:
当前方法应该重写父类或者接口中的方法。
最大的作用是:
让编译器帮我们检查重写是否正确12.2 @Deprecated
@Deprecated
public void oldMethod() {
}表示:
这个 API 已经过时调用时 IDE 或编译器通常会给出警告。
12.3 @SuppressWarnings
用于抑制指定的编译器警告。
例如:
@SuppressWarnings("unchecked")
public void test() {
}12.4 @FunctionalInterface
用于表示:
函数式接口例如:
@FunctionalInterface
public interface Callback {
void call();
}编译器会检查这个接口是否满足函数式接口要求。
13. 注解与 AOP
自定义注解在实际开发中一个非常经典的用途就是:
AOP尤其是在 Spring 项目中,经常会看到:
自定义注解
+
AOP 切面这种组合。
例如希望自动记录方法执行日志,可以先定义:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface LogOperation {
String value() default "";
}业务方法:
public class OrderService {
@LogOperation("创建订单")
public void createOrder() {
System.out.println(
"创建订单"
);
}
}这时候:
@LogOperation本身并不会输出任何日志。
真正的流程通常是:
调用 createOrder()
↓
Spring AOP 拦截方法
↓
检查有没有 @LogOperation
↓
读取注解信息
↓
执行日志逻辑
↓
调用真正的业务方法所以项目中经常可以看到类似的自定义注解:
@Log
@OperationLog
@RateLimit
@Lock
@Idempotent
@DataPermission
@Permission
@RepeatSubmit例如:
@RateLimit
public void query() {
}背后的逻辑可能就是:
AOP 拦截
↓
发现 @RateLimit
↓
读取注解参数
↓
执行 Redis / Lua 限流逻辑
↓
通过才执行业务方法所以:
注解负责“声明我要什么”,AOP 或框架负责“真正去做”。
这也是实际 Java 项目中自定义注解最常见、最重要的应用方式之一。
14. 小结
Java 注解可以简单理解为:
给代码贴标签
↓
标签中携带元数据
↓
编译器 / 反射 / 框架读取
↓
完成对应处理几个核心知识点:
@interface
↓
定义注解
@Retention
↓
注解保留多久
@Target
↓
注解能放在哪里
RUNTIME
↓
运行时可以通过反射读取
value
↓
唯一主要属性时可以省略属性名
default
↓
提供默认值而自定义注解最常见的工作方式就是:
自定义注解
+
反射或者在 Spring 中:
自定义注解
+
AOP最终形成:
业务代码
↓
只负责声明意图
框架代码
↓
统一处理公共逻辑一句话概括:
注解本身只是元数据,真正赋予注解能力的是读取并处理它的编译器、反射代码或框架。