一文读懂 Java 注解运行原理
你有没有想过,为什么在 Spring 中给方法加个@Transactional
注解,就能自动实现事务的开启、提交和回滚?明明没写一行事务控制代码,程序却像 “懂事” 一样处理异常 —— 这背后,就是 Java 注解的 “魔法”。
其实注解本质很简单:它就像一张 “便利贴”,贴在类、方法或字段上,只负责 “标记信息”,本身不做任何逻辑。真正起作用的,是背后 “读便利贴的工具”(反射)和 “按便利贴做事的助手”(动态代理)。今天我们就以事务注解为例,一步步拆透这个过程,让你再也不困惑 “注解为啥能干活”。
一、先搞懂:注解到底是什么?
要理解事务注解,得先明白注解的基础 —— 它不是 “功能代码”,而是 “元数据”(描述数据的数据)。
举个生活例子:你在书本某页贴了张便利贴,写着 “这里要考”。便利贴本身不会让你记住知识点,它只是 “标记” 了重点;真正让你复习的,是 “看到便利贴后去看书” 的你。注解也是如此:
- 注解 = 便利贴(只标记,不做事);
- 反射 =“看便利贴的眼睛”(读取注解信息);
- 动态代理 =“按便利贴做事的助手”(根据注解信息执行逻辑)。
注解的 3 种 “生命周期”(关键分类)
不是所有注解都能 “活” 到程序运行时 —— 根据保留时间,注解分 3 类,而事务注解正属于最特殊的 “运行时注解”:
类型 | 保留阶段 | 作用场景 | 例子 |
---|---|---|---|
源码注解 | 只在.java 文件中存在 | 给编译器看,编译后消失 | @Override (检查重写) |
编译时注解 | 保留到.class 文件,JVM 不加载 | 编译时生成代码(如 Lombok) | @Data (自动生成 get/set) |
运行时注解 | 保留到 JVM 运行时 | 程序运行中动态处理 | @Transactional (事务) |
事务需要在程序运行时根据 “是否有异常” 决定提交或回滚,所以必须用 “运行时注解”—— 只有它能被反射读取到。
二、手把手拆透:事务注解的运行全流程
我们不直接讲 Spring 的@Transactional
(太复杂),先自己写一个简化版事务注解@MyTransactional
,从 “定义注解→使用注解→实现逻辑” 走一遍,你会发现核心原理其实很简单。
第一步:写一张 “事务便利贴”(定义注解)
首先要定义注解本身 —— 就像设计便利贴的格式:要写哪些信息(比如 “事务出问题要回滚哪些异常”“事务传播规则”)。
Java 中定义注解需要用@interface
关键字,还要加 “元注解”(描述注解的注解),告诉 JVM 这张 “便利贴” 的规则(比如贴在方法上、活到老程序运行时):
java
import java.lang.annotation.*;// 元注解1:这张便利贴只能贴在“方法”上
@Target(ElementType.METHOD)
// 元注解2:这张便利贴要活到老程序运行时(关键!)
@Retention(RetentionPolicy.RUNTIME)
// 元注解3:子类能继承父类的这张便利贴
@Inherited
public @interface MyTransactional {// 便利贴内容1:事务传播行为(默认“有事务就加入,没就新建”)Propagation propagation() default Propagation.REQUIRED;// 便利贴内容2:事务隔离级别(默认用数据库的)Isolation isolation() default Isolation.DEFAULT;// 便利贴内容3:哪些异常要回滚(默认空,后面我们补逻辑)Class<? extends Throwable>[] rollbackFor() default {};
}// 给“传播行为”定义选项(简单版,懂意思就行)
enum Propagation {REQUIRED, // 有事务就加入,没就新建REQUIRES_NEW // 不管有没有,都新建事务
}// 给“隔离级别”定义选项(默认用数据库的)
enum Isolation {DEFAULT
}
到这里,我们的 “事务便利贴” 就设计好了 —— 它只能贴在方法上,能记录 “传播规则”“回滚异常” 等信息,并且能活到程序运行时。
第二步:贴便利贴(在业务方法上用注解)
接下来,我们写一个转账业务,把 “事务便利贴” 贴在需要事务的方法上 —— 告诉程序:“这个转账方法要管事务!”
java
public class UserService {// 贴便利贴:标记这个转账方法需要事务@MyTransactional(propagation = Propagation.REQUIRED, // 传播规则:有事务就加入rollbackFor = Exception.class // 所有Exception都回滚)public void transferMoney(String from, String to, int amount) throws Exception {// 步骤1:扣减转账方余额deduct(from, amount);// 模拟异常:转账金额超过1000就报错(测试回滚)if (amount > 1000) {throw new Exception("转账金额不能超过1000!");}// 步骤2:增加收款方余额add(to, amount);}// 扣钱方法(简化,实际是数据库操作)private void deduct(String account, int amount) {System.out.println(account + " 扣减 " + amount + " 元");}// 加钱方法(简化,实际是数据库操作)private void add(String account, int amount) {System.out.println(account + " 增加 " + amount + " 元");}
}
现在,transferMoney
方法上贴了@MyTransactional
—— 但此时它还只是个 “标记”,程序不知道该怎么处理,需要下一步的 “读便利贴” 和 “做事”。
第三步:读便利贴(用反射识别注解)
程序运行时,怎么知道哪个方法贴了 “事务便利贴”?答案是反射——Java 提供的反射机制,能在运行时 “偷看” 类和方法的信息,包括是否有注解。
我们写一个 “便利贴读取工具”,专门检查方法上有没有@MyTransactional
,并获取注解里的信息:
java
import java.lang.reflect.Method;public class TransactionScanner {// 检查方法是否贴了“事务便利贴”public static boolean hasTransactionNote(Method method) {// isAnnotationPresent:反射的核心方法,判断是否有某个注解return method.isAnnotationPresent(MyTransactional.class);}// 读取“便利贴”上的信息(比如回滚异常、传播规则)public static MyTransactional getTransactionInfo(Method method) {// getAnnotation:获取注解对象,进而拿到注解里的属性return method.getAnnotation(MyTransactional.class);}
}
有了这个工具,程序就能在运行时 “扫描” 所有方法 —— 发现贴了@MyTransactional
的,就知道 “这个方法需要事务处理”。
第四步:雇个 “事务助手”(动态代理)
现在知道了哪个方法要事务,但怎么在方法执行前后加 “开启事务”“提交 / 回滚” 的逻辑?总不能改业务代码吧(比如在transferMoney
里加事务代码)—— 这时候需要动态代理。
动态代理就像一个 “助手”:你调用业务方法时,实际是先调用助手,助手先做 “开启事务”,再帮你调用业务方法,最后根据结果做 “提交” 或 “回滚”。而且这个助手是程序运行时动态创建的,不用你手动写。
我们写一个 “事务助手”(代理类),核心逻辑是 “拦截方法调用,嵌入事务控制”:
java
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;// 事务助手:实现InvocationHandler,负责拦截方法调用
public class TransactionProxy implements InvocationHandler {// 目标对象:就是我们要代理的业务类(比如UserService)private final Object target;// 把业务类传进来public TransactionProxy(Object target) {this.target = target;}// 生成代理对象(就是“助手”本身)public Object getProxy() {return Proxy.newProxyInstance(target.getClass().getClassLoader(), // 用和目标对象一样的类加载器target.getClass().getInterfaces(), // 实现目标对象的所有接口this // 拦截逻辑由自己处理);}// 核心:拦截方法调用,这里写事务逻辑@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {// 1. 先检查:这个方法有没有贴“事务便利贴”if (!TransactionScanner.hasTransactionNote(method)) {// 没贴:直接执行业务方法,不处理事务return method.invoke(target, args);}// 2. 贴了:读取便利贴信息(比如回滚哪些异常)MyTransactional transactionInfo = TransactionScanner.getTransactionInfo(method);Object result = null;try {// 3. 事务第一步:开启事务(实际项目中由数据库连接控制,这里简化打印)System.out.println("=== 开启事务 ===");// 4. 执行真正的业务方法(比如转账)result = method.invoke(target, args);// 5. 没报错:提交事务System.out.println("=== 提交事务 ===");} catch (Exception e) {// 6. 报错了:判断要不要回滚(根据便利贴的rollbackFor)if (needRollback(e, transactionInfo.rollbackFor())) {System.out.println("=== 回滚事务 ===");} else {System.out.println("=== 不回滚事务 ===");}throw e; // 把异常抛出去,让上层知道}return result;}// 辅助方法:判断异常是否需要回滚(根据注解里的rollbackFor)private boolean needRollback(Exception e, Class<? extends Throwable>[] rollbackFor) {if (rollbackFor.length == 0) {// 没指定回滚异常:默认只回滚RuntimeException(比如空指针)return e instanceof RuntimeException;}// 指定了:只要异常是rollbackFor里的类型,就回滚for (Class<? extends Throwable> exType : rollbackFor) {if (exType.isInstance(e)) {return true;}}return false;}
}
第五步:测试!看 “助手” 怎么干活
现在所有零件都齐了,我们写个测试类,看看 “事务助手” 能不能正常工作 —— 分别测试 “正常转账” 和 “异常转账”,观察事务是否提交或回滚:
java
public class TransactionTest {public static void main(String[] args) {// 1. 创建业务对象(真正的转账逻辑)UserService userService = new UserService();// 2. 给业务对象雇个“事务助手”(创建代理对象)UserService transactionHelper = (UserService) new TransactionProxy(userService).getProxy();try {// 测试1:正常转账(500元,没超过1000)System.out.println("【测试正常转账】");transactionHelper.transferMoney("张三", "李四", 500);System.out.println("\n-------------------------\n");// 测试2:异常转账(2000元,超过1000,会抛异常)System.out.println("【测试异常转账】");transactionHelper.transferMoney("张三", "李四", 2000);} catch (Exception e) {System.out.println("异常信息:" + e.getMessage());}}
}
运行结果分析
先看输出:
【测试正常转账】
=== 开启事务 ===
张三 扣减 500 元
李四 增加 500 元
=== 提交事务 ===-------------------------【测试异常转账】
=== 开启事务 ===
张三 扣减 2000 元
=== 回滚事务 ===
异常信息:转账金额不能超过1000!
这说明 “事务助手” 完全生效了:
- 正常转账:先开启事务,执行扣钱 + 加钱,最后提交事务;
- 异常转账:开启事务后,扣钱后抛异常,助手检测到异常,执行回滚(相当于扣钱操作被 “撤销” 了)。
三、回到 Spring:@Transactional 其实是一个道理
我们自己写的@MyTransactional
是简化版,而 Spring 的@Transactional
本质一样,只是做了更多 “工程化优化”,比如:
- 扫描更智能:不用我们手动写
TransactionScanner
,加个@EnableTransactionManagement
注解,Spring 就会自动扫描所有贴了@Transactional
的 Bean; - 代理更灵活:Spring 默认用 JDK 动态代理(针对接口),如果类没实现接口,就用 CGLIB 代理(直接代理类);
- 事务管理更专业:Spring 用
PlatformTransactionManager
(事务管理器)统一管理事务,支持不同数据库(MySQL、Oracle),不用我们自己写 “开启 / 提交” 逻辑; - 传播行为更完整:Spring 实现了所有事务传播规则(比如
REQUIRES_NEW
新建事务、NESTED
嵌套事务),我们自己写的只是简化版。
但核心原理没变 —— 还是 “注解标记→反射扫描→动态代理→事务控制” 这四步。
四、一句话总结:注解为什么能干活?
注解本身是 “死的”,它只是一张 “便利贴”;真正让注解 “活” 起来的,是背后的三件事:
- 元注解:定义 “便利贴” 的规则(贴在哪、活多久);
- 反射:运行时 “读便利贴”,知道哪里需要处理;
- 动态代理 / AOP:按 “便利贴” 的要求,在业务逻辑前后嵌入额外逻辑(比如事务、日志、权限)。
理解了这个逻辑,再看 Spring 的@Autowired
、Lombok 的@Data
,你就会发现它们都是 “换汤不换药”—— 只是 “便利贴” 的内容和 “助手干的活” 不同而已。