注解是 Java 里最容易被误解的特性之一。很多人天天用
@Override、@Autowired,但说不清它到底做了什么。本文用最直觉的方式讲清楚注解的本质,然后手把手写一个自定义注解。
目录
- 注解的本质:便利贴,不是魔法
- 注解的三部曲:定义、贴、处理
- 内置注解:你每天都在用的那几个
- 元注解:给注解加注解
- 实战:手写一个路由注解
- 进阶:编译时处理(APT)
- 注解 vs 装饰器:跨语言对比
- 真实案例拆解
- 总结
注解的本质:便利贴,不是魔法
先看一段代码:
@Override
public String toString() {
return "hello";
}
@Override 做了什么?答案是:什么都没做。
它不会修改 toString() 的行为,不会注入任何逻辑,不会在运行时产生任何副作用。它只是告诉编译器:"我声明这个方法是重写父类的,如果我拼错了方法名,请报错。"
这就是注解的本质——元数据标记。它是贴在代码上的便利贴,本身没有任何行为。真正干活的是读这个便利贴的人(编译器、框架、注解处理器)。
注解 = 便利贴(只是标记)
处理器 = 读便利贴的人(真正干活)
没有处理器的注解,就是一张没人看的便利贴——贴了也白贴。
注解的三部曲:定义、贴、处理
任何注解的使用都分三步:
① 定义注解 → 设计便利贴的格式(有哪些字段要填)
② 贴注解 → 把便利贴贴到代码上(填好字段值)
③ 处理注解 → 有人来读便利贴,根据内容做事
用一个生活类比:
① 设计一种便利贴:「紧急任务」,字段有 [负责人] [截止日期]
② 在某个文件夹上贴一张:「紧急任务:负责人=张三,截止日期=周五」
③ 项目经理每天扫一遍所有便利贴,把紧急任务排到日程表里
如果没有第③步的项目经理,便利贴就只是一张纸。
内置注解:你每天都在用的那几个
Java 自带了几个注解,它们的处理器是编译器本身:
// 1. @Override — 编译器检查:这个方法是否真的重写了父类方法
@Override
public String toString() { return "hello"; }
// 如果写成 tostring()(小写 s),编译器直接报错
// 2. @Deprecated — 编译器警告:这个方法已过时,别用了
@Deprecated
public void oldMethod() { }
// 调用 oldMethod() 时,IDE 会画删除线,编译器会 warning
// 3. @SuppressWarnings — 告诉编译器:别给我报这个警告
@SuppressWarnings("unchecked")
List<String> list = (List<String>) rawList;
// 没有这个注解,编译器会警告 unchecked cast
// 4. @FunctionalInterface — 编译器检查:这个接口是否只有一个抽象方法
@FunctionalInterface
public interface Runnable {
void run();
}
这几个注解的处理器都是 javac 编译器,不需要你写任何处理逻辑。
元注解:给注解加注解
定义自己的注解时,需要用"元注解"来描述这个注解的行为。元注解就是"注解的注解"。
两个最重要的元注解:
@Target — 这个注解能贴在哪
@Target(ElementType.TYPE) // 只能贴在类/接口上
@Target(ElementType.METHOD) // 只能贴在方法上
@Target(ElementType.FIELD) // 只能贴在字段上
@Target({ElementType.TYPE, ElementType.METHOD}) // 类和方法都行
@Retention — 这个注解活多久
这是最关键的,决定了注解在什么阶段可以被读取:
@Retention(RetentionPolicy.SOURCE) // 只在源码中存在,编译后就没了
@Retention(RetentionPolicy.CLASS) // 保留到 .class 文件,但运行时读不到
@Retention(RetentionPolicy.RUNTIME) // 运行时也能读到(通过反射)
| RetentionPolicy | 存活阶段 | 谁来处理 | 典型例子 |
|---|---|---|---|
| SOURCE | 源码 → ❌编译后消失 | 编译器 | @Override、@SuppressWarnings |
| CLASS | 源码 → .class文件 → ❌运行时消失 | APT 注解处理器 | @WidgetAnnotation(编译时生成代码) |
| RUNTIME | 源码 → .class文件 → 运行时 | 反射 | @Autowired、@JavascriptInterface |
选哪个取决于你的处理器在什么时候工作:
- 编译器检查 → SOURCE
- 编译时生成代码 → CLASS
- 运行时动态处理 → RUNTIME
实战:手写一个路由注解
假设我们在做一个 Web 框架,想实现类似 Spring 的路由映射:
@Route("/api/users")
public class UserController {
@Route("/list")
public String listUsers() {
return "user list";
}
@Route("/detail")
public String getUser() {
return "user detail";
}
}
框架自动把 URL 路径映射到对应的方法,不需要手动注册。
第一步:定义注解
import java.lang.annotation.*;
@Target({ElementType.TYPE, ElementType.METHOD}) // 可以贴在类和方法上
@Retention(RetentionPolicy.RUNTIME) // 运行时可读(因为要用反射)
public @interface Route {
String value(); // 路径,如 "/api/users"
}
就这么几行。@interface 是定义注解的关键字,value() 是注解的属性。当注解只有一个属性且叫 value 时,使用时可以省略属性名:@Route("/api/users") 等价于 @Route(value = "/api/users")。
第二步:贴注解
@Route("/api/users")
public class UserController {
@Route("/list")
public String listUsers() {
return "[{\"name\": \"Alice\"}, {\"name\": \"Bob\"}]";
}
@Route("/detail")
public String getUser() {
return "{\"name\": \"Alice\", \"age\": 30}";
}
}
@Route("/api/orders")
public class OrderController {
@Route("/list")
public String listOrders() {
return "[{\"id\": 1001}]";
}
}
到这一步,代码的行为没有任何变化。@Route 只是贴了个标签。
第三步:运行时处理
这是关键——写一个路由注册器,通过反射读取注解,构建 URL → 方法的映射表:
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
public class Router {
// URL → 处理方法的映射表
private Map<String, RouteHandler> routeMap = new HashMap<>();
/**
* 扫描一个 Controller 类,读取 @Route 注解,注册路由
*/
public void register(Class<?> controllerClass) throws Exception {
// ① 读取类上的 @Route 注解
Route classRoute = controllerClass.getAnnotation(Route.class);
String basePath = (classRoute != null) ? classRoute.value() : "";
// ② 创建 Controller 实例
Object instance = controllerClass.getDeclaredConstructor().newInstance();
// ③ 遍历所有方法,找带 @Route 的
for (Method method : controllerClass.getDeclaredMethods()) {
Route methodRoute = method.getAnnotation(Route.class);
if (methodRoute != null) {
String fullPath = basePath + methodRoute.value();
routeMap.put(fullPath, new RouteHandler(instance, method));
System.out.println("注册路由: " + fullPath
+ " → " + controllerClass.getSimpleName()
+ "." + method.getName() + "()");
}
}
}
/**
* 处理请求:根据 URL 找到对应方法并执行
*/
public String handleRequest(String url) throws Exception {
RouteHandler handler = routeMap.get(url);
if (handler == null) {
return "404 Not Found: " + url;
}
return (String) handler.method.invoke(handler.instance);
}
private static class RouteHandler {
Object instance;
Method method;
RouteHandler(Object instance, Method method) {
this.instance = instance;
this.method = method;
}
}
}
使用:
public class App {
public static void main(String[] args) throws Exception {
Router router = new Router();
// 注册 Controller
router.register(UserController.class);
router.register(OrderController.class);
// 模拟请求
System.out.println(router.handleRequest("/api/users/list"));
System.out.println(router.handleRequest("/api/users/detail"));
System.out.println(router.handleRequest("/api/orders/list"));
System.out.println(router.handleRequest("/api/unknown"));
}
}
输出:
注册路由: /api/users/list → UserController.listUsers()
注册路由: /api/users/detail → UserController.getUser()
注册路由: /api/orders/list → OrderController.listOrders()
[{"name": "Alice"}, {"name": "Bob"}]
{"name": "Alice", "age": 30}
[{"id": 1001}]
404 Not Found: /api/unknown
整个过程:
@Route注解本身什么都没做,只是在类和方法上贴了路径信息Router.register()通过反射读取这些注解,构建了 URL → 方法的映射表- 请求来了,查表找到方法,反射调用
注解只是数据,反射才是引擎。
进阶:编译时处理(APT)
上面的例子用的是运行时反射,还有一种更高效的方式:编译时处理(APT,Annotation Processing Tool)。
区别:
| 维度 | 运行时反射 | 编译时 APT |
|---|---|---|
| 处理时机 | 程序运行时 | javac 编译时 |
| 性能开销 | 每次启动都要扫描 | 编译时一次性生成代码,运行时零开销 |
| Retention | RUNTIME | CLASS 或 SOURCE |
| 产物 | 无(直接执行逻辑) | 生成新的 .java 文件 |
| 典型框架 | Spring(@Autowired) | Dagger(@Inject)、ButterKnife(@BindView) |
APT 的思路是:编译时扫描注解,自动生成 Java 源码文件,这些文件和你手写的代码一起编译。运行时直接用生成的代码,不需要反射。
编译时:
你的代码(带注解)
│
▼
APT 注解处理器扫描注解
│
▼
自动生成 Java 文件(如 Router_Generated.java)
│
▼
一起编译成 .class
运行时:
直接调用 Router_Generated 的方法,零反射
比如把上面的路由例子改成 APT 方式,处理器会在编译时生成:
// 编译时自动生成的文件,不需要手写
public class Router_Generated {
public static void registerAll(Router router) {
router.addRoute("/api/users/list",
new UserController()::listUsers);
router.addRoute("/api/users/detail",
new UserController()::getUser);
router.addRoute("/api/orders/list",
new OrderController()::listOrders);
}
}
运行时直接调用 Router_Generated.registerAll(router),不需要反射扫描,启动更快。
Android 开发中大量使用 APT,因为移动端对启动速度敏感,反射太慢。
注解 vs 装饰器:跨语言对比
如果你熟悉 Python/TypeScript,可能会把注解和装饰器搞混。它们语法相似,但本质不同:
# Python 装饰器:运行时包装函数,改变了行为
@login_required
def dashboard(request):
return render("dashboard.html")
# 等价于:
dashboard = login_required(dashboard)
# dashboard 已经不是原来的函数了,被包了一层
// Java 注解:只是贴标签,不改变任何行为
@Route("/dashboard")
public String dashboard() {
return render("dashboard.html");
}
// dashboard() 还是原来的 dashboard(),没有任何变化
| 维度 | Java 注解 | Python/TS 装饰器 |
|---|---|---|
| 本质 | 元数据标记(便利贴) | 高阶函数(包装器) |
| 是否改变行为 | ❌ 不改变 | ✅ 直接改变 |
| 谁来处理 | 需要额外的处理器(反射/APT) | 装饰器函数本身就是处理器 |
| 处理时机 | 编译时或运行时 | 运行时(类/函数定义时) |
| 能否访问函数体 | ❌ 不能 | ✅ 可以包装、替换、增强 |
TypeScript 的装饰器 + reflect-metadata 是最接近 Java 注解的:
// TypeScript:装饰器贴标签 + reflect-metadata 存储 + 框架读取
// 这个组合和 Java 注解的工作方式几乎一样
@Controller('/api/users') // 贴标签
class UserController {
@Get('/list') // 贴标签
listUsers() { return [] }
}
// NestJS 框架在启动时通过 Reflect.getMetadata() 读取标签
// 构建路由表——和我们上面写的 Router 一模一样
真实案例拆解
Spring 的 @Autowired 怎么实现的
@Service
public class UserService {
@Autowired
private UserRepository userRepository; // 自动注入,不需要 new
}
背后的三部曲:
① 定义:@Autowired 是 Spring 定义的注解,Retention = RUNTIME
② 贴:开发者在字段上贴 @Autowired
③ 处理:Spring 容器启动时——
- 扫描所有 Bean 类
- 对每个 Bean,遍历所有字段
- 发现带 @Autowired 的字段
- 通过反射 field.set(bean, 依赖对象) 注入
核心处理逻辑(简化版):
// Spring 内部的处理器(简化)
for (Field field : bean.getClass().getDeclaredFields()) {
if (field.isAnnotationPresent(Autowired.class)) {
Object dependency = container.getBean(field.getType());
field.setAccessible(true);
field.set(bean, dependency); // 反射注入
}
}
@Autowired 本身没有注入能力,是 Spring 的 AutowiredAnnotationBeanPostProcessor 在运行时通过反射完成的注入。
Android 的 @WidgetAnnotation 怎么实现的
快应用框架中,每个原生组件(text、image、div)都用注解标记:
// ① 定义注解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS) // 注意:CLASS,不是 RUNTIME
public @interface WidgetAnnotation {
String name(); // 组件名,如 "text"
String[] methods() default {}; // 暴露给 JS 的方法
TypeAnnotation[] types() default {};
}
// ② 贴注解
@WidgetAnnotation(
name = "text",
methods = { "animate", "getBoundingClientRect", "focus" }
)
public class Text extends AbstractText<TextLayoutView> { ... }
// ③ 编译时处理(APT)
// AnnotationProcessor 在编译时扫描所有 @WidgetAnnotation
// 自动生成组件注册表:
// "text" → Text.class
// "image" → Image.class
// "div" → Div.class
// 运行时 ComponentFactory 直接查表,不需要反射
这里用 CLASS 而不是 RUNTIME,因为 Android 对启动速度要求高,编译时生成代码比运行时反射快得多。
总结
注解 = 元数据标记
= 贴在代码上的便利贴
= 本身不执行任何逻辑
注解的价值 = 处理器的价值
编译器处理 → @Override(检查方法签名)
APT 处理 → @WidgetAnnotation(编译时生成注册表)
反射处理 → @Autowired(运行时依赖注入)
没有处理器的注解 = 没人看的便利贴 = 废纸
记住三部曲:定义 → 贴 → 处理。前两步是声明式的(说"是什么"),第三步是命令式的(做"怎么办")。注解的所有"魔法"都藏在第三步里。
如果这篇文章对你有帮助,欢迎点赞收藏。有问题欢迎评论区讨论。