当前位置: 首页 > news >正文

一文读懂 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本质一样,只是做了更多 “工程化优化”,比如:

  1. 扫描更智能:不用我们手动写TransactionScanner,加个@EnableTransactionManagement注解,Spring 就会自动扫描所有贴了@Transactional的 Bean;
  2. 代理更灵活:Spring 默认用 JDK 动态代理(针对接口),如果类没实现接口,就用 CGLIB 代理(直接代理类);
  3. 事务管理更专业:Spring 用PlatformTransactionManager(事务管理器)统一管理事务,支持不同数据库(MySQL、Oracle),不用我们自己写 “开启 / 提交” 逻辑;
  4. 传播行为更完整:Spring 实现了所有事务传播规则(比如REQUIRES_NEW新建事务、NESTED嵌套事务),我们自己写的只是简化版。

但核心原理没变 —— 还是 “注解标记→反射扫描→动态代理→事务控制” 这四步。

四、一句话总结:注解为什么能干活?

注解本身是 “死的”,它只是一张 “便利贴”;真正让注解 “活” 起来的,是背后的三件事:

  1. 元注解:定义 “便利贴” 的规则(贴在哪、活多久);
  2. 反射:运行时 “读便利贴”,知道哪里需要处理;
  3. 动态代理 / AOP:按 “便利贴” 的要求,在业务逻辑前后嵌入额外逻辑(比如事务、日志、权限)。

理解了这个逻辑,再看 Spring 的@Autowired、Lombok 的@Data,你就会发现它们都是 “换汤不换药”—— 只是 “便利贴” 的内容和 “助手干的活” 不同而已。


文章转载自:

http://fXKpcf64.prgdy.cn
http://KiaFSvvx.prgdy.cn
http://9xh7JkWr.prgdy.cn
http://AKA0sJku.prgdy.cn
http://G5BV2RXk.prgdy.cn
http://HnkFUJ00.prgdy.cn
http://gCrERnRt.prgdy.cn
http://i9PEuil7.prgdy.cn
http://tbltQtYe.prgdy.cn
http://yrO5u4Y8.prgdy.cn
http://ubPG0PXz.prgdy.cn
http://2YQNWubz.prgdy.cn
http://CHLDlM3j.prgdy.cn
http://RvOG5ky9.prgdy.cn
http://vi1IfYmq.prgdy.cn
http://5hP9D3Ob.prgdy.cn
http://TCOnnkO2.prgdy.cn
http://xfRZkGP2.prgdy.cn
http://EXoWN94U.prgdy.cn
http://jfXnYN4N.prgdy.cn
http://AwKUMOUy.prgdy.cn
http://rZu0E4a1.prgdy.cn
http://0KffzYOV.prgdy.cn
http://v8gUJqmG.prgdy.cn
http://WqWEPEfS.prgdy.cn
http://chFqKQQJ.prgdy.cn
http://aHFbU3xr.prgdy.cn
http://jFTHWbuF.prgdy.cn
http://t5Et4Sbj.prgdy.cn
http://Ky6cJiW8.prgdy.cn
http://www.dtcms.com/a/383792.html

相关文章:

  • Dify开发中系统变量(system)和用户变量(user)的区别
  • 扩散模型之(五)基于概率流ODE方法
  • 【代码模板】Linux内核模块带指针的函数如何返回错误码?(ERR_PTR(-ENOMEM)、IS_ERR(ent)、PTR_ERR(ent))
  • 查询 mysql中 所有的 非空记录字段
  • Spring Bean:不只是“对象”那么简单
  • 快速选中对象
  • ByteDance_FrontEnd
  • 中科方德环境下安装软件的几种方式与解决思路
  • 《一本书读懂 AI Agent》核心知识点总结
  • 【CVPR 2025】LSNet:大视野感知,小区域聚合
  • MyBatis 从入门到精通(第二篇)—— 核心架构、配置解析与 Mapper 代理开发
  • Ubuntu 虚拟机设置双向复制粘贴
  • Lombok添加了依赖缺没有生效
  • 嵌入式开发中的keil常见错误与警告解决方案(部分)
  • ES5 和 ES6 类的实现
  • 设计模式-装饰器模式详解
  • 对AQS的详解
  • 实验-基本ACL
  • 开始 ComfyUI 的 AI 绘图之旅-SDXL文生图和图生图(全网首发,官网都没有更新)(十四)
  • Java可用打印数组方法5中+常用变量转字符串方法
  • ssh远程连接服务器到vscode上“连接失败”
  • SpringBoot -原理篇
  • 设计模式——结构型模式
  • I.MX6ULL时钟(clock)与定时器(EPITGPT)
  • STM32_06_Systick定时器
  • 用 Java 学会 Protocol Buffers从 0 到 1 的完整实战
  • 237.删除链表中的节点
  • 【Vue2手录14】导航守卫
  • Qt如何读写xml文件,几种方式对比,读写xml的Demo工程
  • 子网划分专项训练-1,eNSP实验,vlan/dhcp,IP规划