跳到主内容
创建时间: 2026-09-16最后更新: 2026-09-16

心跳项目从第一篇开始就在使用 @SpringBootApplication、@RestController 和 @GetMapping。这些注解不会像普通方法一样自己执行代码,它们只是把额外信息附加到类或方法上。Spring 在启动和请求处理阶段读取这些信息,再决定注册组件、映射路径或创建代理。

要理解这个过程,需要先掌握 Java 注解与反射的边界。本章只观察 JDK 提供的机制;下一篇再看 Spring 容器怎样利用这些机制管理对象。

1. 注解

注解是一种结构化元数据,可以标记类、方法、字段、参数等程序元素:

src/main/java/cn/usehook/spring/controller/HealthController.java
1
@RestController
2
public class HealthController {
3
4
@GetMapping("/api/v1/health")
5
public HealthResponse health() {
6
return HealthResponse.success("UP", "spring");
7
}
8
}

这里的两个注解表达不同信息:

注解标记位置表达的信息
@RestController类这个类型承担 REST 控制器职责
@GetMapping方法这个方法处理指定路径的 GET 请求

注解本身不会创建 HealthController,也不会监听端口。真正执行这些工作的仍然是普通 Java 对象和框架代码。注解只提供框架可以读取的声明。

注解可以包含属性。@GetMapping("/api/v1/health") 是 @GetMapping(path = "/api/v1/health") 的简写,因为该注解的 value 与 path 互为别名。不同注解的属性由各自类型定义,不能假设所有注解都支持相同名称。

2. 自定义注解

使用 @interface 可以定义注解类型。下面创建一个只负责标记心跳操作的注解:

src/main/java/cn/usehook/spring/basics/HealthOperation.java
01
package cn.usehook.spring.basics;
02
03
import java.lang.annotation.Documented;
04
import java.lang.annotation.ElementType;
05
import java.lang.annotation.Retention;
06
import java.lang.annotation.RetentionPolicy;
07
import java.lang.annotation.Target;
08
09
@Documented
10
@Target(ElementType.METHOD)
11
@Retention(RetentionPolicy.RUNTIME)
12
public @interface HealthOperation {
13
14
String value() default "health-check";
15
}

标记注解定义的注解叫元注解:

元注解当前作用
@Target(METHOD)只允许标记方法
@Retention(RUNTIME)注解保留到运行期,可以被反射读取
@Documented生成 API 文档时包含这个注解

保留策略还包括 SOURCE 和 CLASS。SOURCE 只存在于源码,编译后丢弃;CLASS 会写入 class 文件,但默认不能通过运行时反射读取;Spring 需要在应用运行时识别的注解通常使用 RUNTIME。

注解属性只能使用 Java 允许的有限类型,例如基本类型、String、Class、枚举、注解以及这些类型的数组。属性不能保存任意运行时对象,默认值也必须是编译期可以写进注解的值。

现在创建一个独立练习类并标记它的方法:

src/main/java/cn/usehook/spring/basics/HealthProbe.java
1
package cn.usehook.spring.basics;
2
3
public class HealthProbe {
4
5
@HealthOperation("default-health-check")
6
public String check() {
7
return "UP";
8
}
9
}

给方法添加这个注解不会直接改变方法行为。只有其他代码主动查找并解释 @HealthOperation,它才会参与程序逻辑。

3. Class 对象

JVM 为加载的每种类型维护一个 Class 对象,它描述类型名称、修饰符、构造方法、字段、方法和注解等信息:

class-object.java
1
Class<HealthService> type = HealthService.class;
2
3
System.out.println(type.getName());
4
System.out.println(type.getSimpleName());

HealthService.class 不会创建 HealthService 实例,它只取得描述这个类型的对象。已有对象也可以通过 getClass() 取得实际运行类型:

runtime-type.java
1
Object service = "spring";
2
Class<?> runtimeType = service.getClass();

变量的声明类型是 Object,运行对象的类型是 String。反射观察的是实际传入的 Class 对象,因此框架可以在不知道具体实现源码的情况下检查运行类型。

Class.forName("cn.usehook.spring.health.HealthService") 也能按完整类名加载类型,但字符串拼写错误只能在运行时发现。源码中已经知道类型时,优先使用 HealthService.class,可以获得编译器检查和重构支持。

4. 反射读取

反射 API 允许程序在运行时检查类型。下面读取所有带 @HealthOperation 的公开方法:

src/main/java/cn/usehook/spring/basics/AnnotationInspector.java
01
package cn.usehook.spring.basics;
02
03
import java.lang.reflect.Method;
04
05
public class AnnotationInspector {
06
07
public static void main(String[] args) {
08
for (Method method : HealthProbe.class.getDeclaredMethods()) {
09
HealthOperation operation = method.getAnnotation(HealthOperation.class);
10
11
if (operation != null) {
12
System.out.println(method.getName() + ": " + operation.value());
13
}
14
}
15
}
16
}

getDeclaredMethods() 返回当前类声明的方法,getAnnotation() 在方法上查找指定运行时注解。找不到时返回 null,所以读取属性前要先检查结果。

反射还可以取得构造方法并创建对象,也可以读取或调用方法。不过,这不会消除正常的 Java 规则:构造参数仍要满足类型要求,调用仍可能抛出异常,访问非公开成员还涉及更严格的封装和模块边界。

框架使用反射是因为它需要处理应用提供的许多未知类型。普通业务代码如果在编译时已经知道要调用哪个方法,直接调用通常更清楚,也能获得完整类型检查,不应该为了显得通用而改成反射。

5. Spring 注解

Spring 会在不同阶段读取不同注解:

spring-annotation-flow.txt
01
应用启动
02
-> 扫描 @Component、@Service、@RestController
03
-> 读取 @Configuration 与 @Bean
04
-> 创建 Bean 并解析构造参数
05
06
请求到达
07
-> 查找 @GetMapping、@PostMapping 等映射
08
-> 根据参数注解解析请求数据
09
10
方法调用
11
-> 代理读取事务、缓存或自定义切点元数据

同一个注解只在理解它的处理器存在时有效。例如,单独创建一个带 @RestController 的对象不会让它成为 HTTP 控制器;它必须进入 Spring 应用上下文,MVC 基础设施也必须存在。

Spring 还支持组合注解。@RestController 自身组合了 @Controller 与 @ResponseBody,@SpringBootApplication 则组合了配置、自动配置和组件扫描相关能力。组合注解让一组经常一起使用的元数据拥有更明确的业务名称。

注解、反射和代理不是同一件事。注解保存元数据,反射负责检查类型结构,代理则可以包围对象的方法调用。Spring 会按具体功能组合这些机制,但学习时要先分清各自职责。

6. 使用边界

注解适合表达横跨许多类型且结构稳定的声明,例如组件角色、请求映射、事务边界和校验约束。普通业务分支仍应写成可以直接阅读和测试的 Java 方法,不能把所有规则隐藏在自定义注解中。

自定义注解至少需要回答三个问题:

  1. 允许标记哪些程序元素;
  2. 保留到哪个阶段;
  3. 由哪段代码在什么时候读取并执行相应行为。

缺少第三项的运行时注解只是没有消费者的元数据。反过来,框架能够通过类型、接口或配置文件明确完成的功能,也不一定必须再发明一个注解。

basics 包中的自定义注解、练习类型和检查器只用于观察 Java 机制,不是心跳接口运行所需的生产组件。完成练习后可以保留它们供复习,也可以删除,不会影响 Controller 和 Service。

到这里,已经可以准确理解「Spring 扫描注解」这句话:Spring 读取类上的运行时元数据,但真正需要管理的仍然是对象以及对象之间的依赖。下一篇从手动 new 出发,建立对象依赖图,再让 IoC 容器接管创建与组装。