ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Spring注解实战:从Bean注入、三级缓存到事务失效与自定义注解

Spring注解实战:从Bean注入、三级缓存到事务失效与自定义注解 1. Spring注解体系的正确打开方式先搞清楚它解决什么问题入行这些年我面试过不少人也帮团队带过很多新人。大家提到Spring注解第一反应往往是背了一堆注解名比如Component、Autowired、Bean、Transactional问起来都能说出个大概但真到排查问题的时候却常常抓瞎。说白了他们只记住了注解的样子没理解注解在Spring里到底扮演什么角色。1.1 从XML到注解Spring为什么把控制权交给了注解早期Spring用XML配置所有Bean的定义都写在applicationContext.xml里。写过那种配置的人都有体会一个稍大点的项目配置文件动辄几百行Bean之间的依赖关系要靠人肉维护改个Bean名字要在XML和代码里同时搜。那时候的Spring被称为配置地狱也不冤枉。后来Spring引入了注解很多人以为这只是写法变简洁了其实不完全是。注解的本质是一种元数据——它附加在类、方法、字段上告诉Spring容器这个类需要被管理这个方法需要被增强这个字段需要注入什么。XML是外部元数据注解是内嵌元数据两者都是元数据只是存放位置和表达方式不同。理解了这一点你再看注解就豁然开朗了Spring容器的核心工作只有两件事一是扫描这些元数据二是根据元数据创建和管理Bean。注解不是魔法它只是把本来写在XML里的信息搬到了代码里让Spring通过反射去读取。1.2 注解的生效逻辑并不是写上就生效新手最容易踩的坑是以为加个注解Spring就自动处理了。真实情况是注解本身只是一堆接口和属性定义真正干活的是一大堆扫描器、解析器和BeanPostProcessorBean后置处理器。比如Autowired可以自动注入依赖的是AutowiredAnnotationBeanPostProcessorAsync能让方法异步执行依赖的是AsyncAnnotationBeanPostProcessor。所以你在项目里加了一个自定义注解却发现完全没反应第一个要检查的就是有没有什么组件去解析这个注解如果没有任何解析器那这个注解就是个装饰品什么都不干。这个思路贯穿Spring注解学习的始终。1.3 一套好用的分类方法把注解按职责切分Spring生态的注解非常多但别慌我建议你按职责分成这么几类类别代表注解解决什么问题Bean定义类Component、Service、Repository、Controller、Bean告诉容器有哪些Bean需要管理依赖注入类Autowired、Resource、Qualifier、Value告诉容器Bean之间怎么装配配置类Configuration、PropertySource、Import、Scope告诉容器配置信息从哪来、Bean的作用域生命周期类PostConstruct、PreDestroy、Lazy告诉容器Bean创建前后做什么AOP增强类Aspect、Before、After、Around、Pointcut给方法织入横切逻辑事务类Transactional、EnableTransactionManagement管理数据库事务边界条件装配类Conditional、ConditionalOnProperty、Profile按条件决定是否创建Bean我每次带新人都让ta先建一张这样的表把注解归类后面学起来就不会乱。因为你会发现注解之间是有层级的先有Bean定义才有依赖注入再有生命周期管理之后才能谈AOP和事务。2. Bean的注解注入与Spring三级缓存机制容器到底干了什么活热搜词里bean 的注解注入spring三级缓存原理频繁出现这其实是一件事的两面。注解注入是怎么做三级缓存是遇到问题时容器怎么兜底。2.1 Component族注解与Bean的注册流程先说注入。Component、Service、Repository、Controller本质上是同一族注解Service这些只是Component的语义化别名目的都是为了让Spring扫描到并注册为Bean。很多人问能不能只用一个Component覆盖所有情况技术上可以但工程上不建议。原因很简单语义化命名方便团队协作事务、AOP等模块可以通过注解类型快速做差异化处理而且代码可读性差很多。Bean注册的完整链路是这样的Spring启动时ConfigurationClassPostProcessor处理配置类根据ComponentScan指定的包路径扫描class文件。扫描到带有Component族注解的类后Spring判断是否有候选构造器生成BeanDefinition放进容器。实例化阶段通过反射调用构造器创建原始对象。紧接着是依赖注入阶段处理Autowired、Resource等注解。如果Bean实现了BeanNameAware、ApplicationContextAware等接口则回调设置相应信息。执行BeanPostProcessor的前置处理比如PostConstruct。初始化阶段执行InitializingBean接口的afterPropertiesSet方法或自定义的init-method。BeanPostProcessor后置处理比如AOP代理就在这里生成。最终把成品Bean放入单例池供业务使用。这九步不是玄学是面试高频题Bean生命周期的标准答案。你实际排查问题的时候也会用到这套流程来定位容器卡在哪一步了代理对象什么时候生成的为什么Autowired注入的对象和getBean拿到的可能不是同一个2.2 三级缓存到底解决了什么问题循环依赖是老话题但真要解释透的人不多。A依赖BB依赖A两者都是单例如果没有缓存策略容器会陷入先创建AA要B创建BB又要AA还没建完的死循环。Spring的解法很巧妙用三级缓存一级缓存singletonObjects存放完整的成品Bean状态是初始化完毕、可对外使用。二级缓存earlySingletonObjects存放半成品Bean状态是实例化完成、但还没初始化完成。三级缓存singletonFactories存放ObjectFactory工厂对象作用是支持提前暴露Bean的引用。执行流程大致是创建A时实例化完成后把A的工厂放进三级缓存填充属性时发现需要B于是创建BB填充属性时发现需要A这时候从三级缓存拿到A的工厂通过getEarlyBeanReference拿到A的提前引用放入二级缓存返回给B完成注入B初始化完成后A继续初始化最终都进入一级缓存。经常有人问为什么不能只用二级缓存这个问题特别好。答案是三级缓存里存的是ObjectFactory目的不是简单暴露引用而是允许在提前暴露阶段做AOP代理。正常情况下AOP代理是在Bean初始化完成后生成的但循环依赖场景下B在A初始化完成前就拿到了A的引用如果这里不做代理B持有了原始对象的引用而其他依赖A的Bean拿到的是代理对象就会产生同一个Bean有两种形态的诡异问题。Spring通过ObjectFactory把这个决定权后置到A被真正需要的那一刻保证提前暴露的引用也能被代理。2.3 循环依赖的边界条件并非所有循环都能解不过得说清楚三级缓存的循环依赖解决方案只适用于单例Bean 非构造器注入的场景。以下情况都会导致循环依赖直接报错构造器注入的循环依赖此时对象还没实例化完成根本进不了三级缓存。Async注解的Bean因为异步代理在早期阶段就生成了另一个代理对象容易打破缓存逻辑。非单例作用域Bean比如Scope(prototype)容器根本不做缓存。我在项目里见过最典型的问题是明明代码看起来没问题一启动就循环依赖报错。结果是用了构造器注入两个类互相通过构造器依赖。解决方案也不是去绕开报错而是重构依赖关系——把循环依赖拆出来比如抽一个第三方的Service或者用Lazy延迟其中一个的初始化。循环依赖本身是一种设计坏味道能不依赖容器的兜底能力就别依赖。2.4 手写一个简化版容器的价值如果你真想弄懂注解注入和三级缓存我强烈推荐一个笨办法自己写一个极简IoC容器。不需要太多代码核心就三步扫描指定包下的类识别自定义的MyComponent注解。用反射实例化对象。扫描字段上的MyAutowired注解从容器Map里取出对应Bean完成注入。写完之后你会发现自己对扫描-注册-注入这套逻辑的理解比背十遍面试题都牢。而且有了这个基础再去看Spring源码就不晕了。网上有人开源了简化版Spring框架比如手写Spring相关的项目思路跟我说的差不多如果实在不想从零写也可以找这类项目配合源码读。3. 事务注解剖析Transactional为什么有时候会失效Spring事务是面试重灾区也是线上问题高发区。项目里最常见的写法就是给Service方法加一个Transactional然后以为万事大吉。但根据我的经验事务注解失效的情况远比想象中多而且踩坑的人往往连为什么失效都没头绪。3.1 Transactional的底层逻辑基于AOP的动态代理先明确一件事Transactional不是靠方法本身实现的它依赖Spring的AOP机制。Spring容器中实际存在两个对象——一个是目标对象你写的Service实现类实例一个是代理对象通过动态代理生成的增强版。当外部调用某个方法时你拿到的其实是代理对象代理对象会在方法执行前开启事务执行后提交或回滚。理解了代理就理解了大部分失效场景。核心原则是Spring事务的拦截器只能拦截通过代理对象发起的调用。如果方法调用绕过了代理事务逻辑根本不会触发。3.2 六大经典失效场景与排查思路我把这几年碰到的失效场景整理成一张表格每个都对应具体的代码特征失效场景原因典型代码示例同类内部调用this.method()走的是目标对象而非代理对象ServiceA内a()方法b()方法b()上虽有Transactional但由a()的this调用触发private方法标注事务动态代理不会拦截private方法因为子类无法重写父类private方法某个私有方法上加了Transactional异常被catch吞掉事务拦截器看到的是正常返回不会触发回滚try-catch里catch了RuntimeException且不重新抛出抛出检查型异常Spring默认只回滚RuntimeException和ErrorIOException等不回滚方法抛出Exception事务不标记回滚自调用但在别的类里拆开自己调的Service没有走Spring容器的代理Beannew出另一个Service实例来调用传播行为配置不当REQUIRED之外的传播行为如REQUIRES_NEW、NOT_SUPPORTED改变了事务边界外层事务方法内调用了REQUIRES_NEW方法内层回滚不影响外层整体排查事务问题时我一般按这个顺序走先确认调用方是否通过代理进入再检查异常有没有被吞然后看异常类型最后看事务传播配置。九成问题出在这四个环节。关于同类内部调用失效很多人会觉得束手无策——总不能不让类内部互相调用吧。有几个可行的解决办法一是把内部调用拆到另一个Service类中让被调用的方法走代理链路二是用AopContext.currentProxy()拿到当前代理对象再调用三是把Transactional移到外部调用入口的方法上。我个人最推荐第一种因为它让代码的模块边界更清晰可读性也更好。3.3 传播机制的具体选择事务传播不是什么高深概念本质是多个事务方法互相调用时如何处理事务边界。用的最多的就是REQUIRED和REQUIRES_NEW。REQUIRED的意思是如果当前已有事务就直接加入如果没有就新建一个。这是默认值适合绝大多数业务场景——一个请求链路里的数据库操作应该同生共死。REQUIRES_NEW的意思是无论如何都开启新事务当前事务会挂起。它适合这种场景主流程中记录一条日志即使主流程最终失败回滚日志记录也要成功保存。如果你把日志插入也放在同一个事务里那主流程一失败日志也跟着没了运营和排障的人什么都看不到。我在日志场景的实践是Transactional(propagation Propagation.REQUIRES_NEW) public void recordOperationLog(String userId, String action) { // 独立事务不受外层回滚影响 }但这里也有个容易忽略的坑REQUIRES_NEW会短暂挂起当前事务如果外层事务持有数据库连接而你开了新的独立事务意味着需要额外一个连接。在高并发场景下这可能导致连接池被快速打满。所以日志场景我建议评估下能否用异步或者消息队列而不是每次都在事务里硬开新连接。3.4 顺便说说SneakyThrows这类注解战士热搜词里出现了sneakythrows注解的作用说明不少人在实际代码里见过它。SneakyThrows是Lombok提供的注解作用是偷偷抛出检查型异常让方法签名上不用写throws也能编译通过。我见过不少人用它来规避异常处理比如在Java代码里把IOException直接抛出去。这种做法在工具类里偶尔用可以但我不建议在业务代码里大规模使用。原因很简单异常也是一种信息检查型异常强迫你思考这个操作失败了应该怎么办你用一个注解把它干掉错误处理的责任就被转移到了调用方身上而调用方也许根本不知道有异常需要处理。Spring事务里有个更实际的坑如果方法里用SneakyThrows把一个检查型异常扔出去了事务默认不会回滚因为默认只回滚RuntimeException和Error而你因为用了SneakyThrows看起来方法签名又很干净阀门被悄悄打开了。真要在这类代码里保证回滚需要明确配置rollbackFor Exception.class。这些注解组合在一起一个比一个能藏问题。4. 手写自定义注解从定义到AOP落地全流程热搜里自定义注解和注解和spel表达式频繁出现说明大家不只是想了解Spring内置注解更想知道怎么造自己的轮子。自定义注解本身不难难的是准入设计——你到底想用注解表达什么信息、在什么时候生效、谁来消费它。4.1 注解的基本语法与元注解自定义注解在Java中是通过interface声明的。常见的定义方式Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface RateLimit { int limit() default 100; long timeout() default 60; }有三个关键的注解的注解需要理解Target限定注解能贴在什么位置METHOD方法、TYPE类、FIELD字段、PARAMETER参数等。Retention注解保留策略RUNTIME表示运行期还能通过反射读出来。绝大部分自定义业务注解必须是RUNTIME否则Spring根本读不到。Documented把这个注解写进Javadoc文档。如果你想这个注解被Spring自动扫描到还需要加上CommonsLog之类的辅助注解吗不需要。自定义注解本身不需要任何Spring框架注解来修饰需要的是有东西去处理它。这个东西通常就是AOP切面或BeanPostProcessor。4.2 配合Aspect实现权限校验或操作日志最常见的自定义注解落地方式是AOP。举个例子项目里需要记录每个操作人做了什么我通常这样实现定义一个OperationLog注解包含action描述、模块名称等属性。写一个Aspect切面Aspect Component public class OperationLogAspect { Around(annotation(operationLog)) public Object recordLog(ProceedingJoinPoint joinPoint, OperationLog operationLog) throws Throwable { long start System.currentTimeMillis(); try { Object result joinPoint.proceed(); // 记录成功日志包含operationLog.action() return result; } catch (Throwable throwable) { // 记录异常日志 throw throwable; } finally { long cost System.currentTimeMillis() - start; // 记录耗时等其他信息 } } }Around(annotation(operationLog)) 这段切点表达式意思是只拦截那些标注了OperationLog的方法并自动把注解实例绑定到方法参数operationLog上。这样你在切面里就能直接读到注解属性非常方便。在设计日志类注解的时候我建议把操作类型和业务内容分开。操作类型放大类比如ADD、UPDATE、DELETE业务内容放具体描述或者参数标记。后续做数据报表时你会发现这种分层让你能快速筛选而不需要把整段日志文字拉出来解析。4.3 注解与SpEL表达式的结合玩法热搜里有个词条叫注解和spel表达式这个组合在实际项目中很实用。比如很多注解的属性不只是静态值需要根据方法参数动态计算。典型的例子是缓存注解。Spring Cache的Cacheable支持SpEL表达式Cacheable(value userInfo, key #userId) public UserInfo getUser(Long userId) { ... }这里的#userId引用的是方法参数这个功能实际上是由Spring表达式解析器SpelExpressionParser在运行期完成的。自定义注解也可以借鉴这个思路在注解属性里写模板表达式切面执行时解析它。比如自定义一个DataScope注解限制用户只查自己权限范围内的数据Aspect Component public class DataScopeAspect { Before(annotation(dataScope)) public void applyDataScope(JoinPoint joinPoint, DataScope dataScope) { // dataScope.expression() 可能是 #deptId // 从方法参数中解析出deptId拼接到SQL条件里 } }这样注解就从一个静态标签变成了可动态计算的指令弹性大很多。我自己的体会是注解加SpEL是成本很低但回报很高的组合它能让业务代码保持整洁又能应对复杂的动态逻辑。4.4 关于IDE联想注解的小问题热搜里有个很具体的词条idea2025.3.6在写注解时输入小写字母不会联想注解。这个其实是个IDE配置问题。IntelliJ IDEA里代码自动补全对大小写的匹配策略是可以调的如果你输入小写字母不联想看两处设置里搜索Match case把代码补全选项里的Match case取消勾选。检查是否安装的插件或者代码模板干扰了注解联想。这类问题通常跟Spring本身无关但会严重影响手写注解和Bean时的开发体验。如果遇到别急着改代码先看看IDE设置和项目的JDK级别。5. Spring Boot生态下的配置注解、日志体系与WebSocket集成如果说Spring解决了Bean管理问题那Spring Boot解决的就是怎么少写配置且快速启动的问题。但现在很多人的Spring Boot学习路径是有问题的一上来就抄别人的pom依赖和application.yml遇到问题一头雾水。这里我想重点讲两块一是配置文件加载和注解绑定二是日志体系顺便提一下WebSocket集成中典型的yaml配置和日志坑。5.1 第一个Spring Boot程序从启动类到配置加载任何一个Spring Boot项目入口都是SpringBootApplication标注的类。拆开看它其实是三个注解的组合SpringBootConfiguration继承自Configuration、EnableAutoConfiguration开启自动装配、ComponentScan扫描当前包及其子包。自动装配是这个时代最核心的能力之一。Spring Boot的spring.factories文件把各种自动配置类的路径列出来启动时逐一尝试加载再通过ConditionalOnClass、ConditionalOnMissingBean这类条件注解决定什么条件下才创建哪个Bean。如果你想知道某个配置为什么生效了去查自动配置类的Conditional注解比瞎猜高效得多。配置文件加载这一环我强烈建议用ConfigurationProperties而不是Value来绑定一组配置。原因有几点Value模式面向单个属性零散且不好维护。ConfigurationProperties可以批量绑定前缀一致的配置项自动做类型转换和校验。用IDE写配置时还能获得字段提示。实战示例app: name: demo max-file-size: 10MBComponent ConfigurationProperties(prefix app) public class AppProperties { private String name; private DataSize maxFileSize; // 标准的getter/setter }注意DataSize类型Spring Boot会帮你把10MB这种人类可读的格式转成字节单位。这种细节在日常开发中能省很多转换逻辑。5.2 Spring Boot日志别把System.out.println当日志用经常有同学的项目里满屏System.out.println一上生产就抓瞎。Spring Boot的默认日志体系是Logback SLF4J使用姿势很固定private static final Logger log LoggerFactory.getLogger(UserServiceImpl.class);更优雅的可以用Slf4j注解Lombok声明log对象然后直接写log.info()。但要注意Slf4j依赖Lombok如果你写了一个自定义注解项目又不想引入Lombok直接用上面的声明式写也不麻烦。日志级别上我建议遵循这个原则DEBUG给开发和排查问题INFO记录关键业务入口与结果WARN记录可恢复的异常或非预期状态ERROR记录必须人工介入的问题。生产环境通常开INFO级别但你要清楚改级别不是改配置文件重启——可以用日志的在线调整机制很多团队用Spring Boot Admin配合实现。这里有个容易踩坑的地方日志框架的jar包冲突。如果你用Spring Boot又另外引入了一些底层依赖log4j或commons-logging的第三方库启动时可能报SLF4J的Multiple bindings警告甚至日志直接不输出。排查方法就是看依赖树用mvn dependency:tree找到多余的日志绑定jar并exclude掉。5.3 Spring Boot集成WebSocket时的yaml配置与踩坑WebSocket是热搜词里一个很实的场景。Spring Boot集成WebSocket不算复杂但yaml配置有几个细节值得注意server: servlet: context-path: /apiWebSocket路径一般和context-path是独立开的很多人在这里栽跟头。如果项目设置了context-path为/api那WebSocket的握手路径默认是独立于这个前缀的你注册endpoint为/ws客户端连的可能是ws://域名:端口/ws而不是ws://域名:端口/api/ws。具体行为取决于你用的版本和注册方式但一定要先确认这个否则前端老连不上你会以为是配置问题。另外WebSocket连接数是有限的生产环境要评估并发连接量多实例部署时要考虑是否引入消息广播。很多人把WebSocket当数据库连接来用一条连接频繁收发大量消息不规划心跳检测和超时断连几天后服务端连接池就爆了。WebSocket的心跳检测一定要做我通常在Handler的afterConnectionEstablished里启动定时任务发送ping超过N次无响应就关闭连接。6. 新生态里的注解变局Spring AI、云原生与虚拟线程Spring生态不会停在一个地方。这两天看热搜词Spring AI、Spring AI Alibaba、Java 21 Spring Boot 3.5虚拟线程、Spring Cloud Alibaba都出现在其中。这些新东西看起来都跟注解相关但玩法有了一些变化。6.1 Spring AI中的Tool注解把方法暴露给大模型Spring AI是Spring官方推出的AI应用开发框架。在当前阶段最实用的一个注解是Tool。它的作用是把Java方法暴露给大模型作为可调用工具。也就是说你在方法上标注Tool并写好描述和参数规格模型就能在对话中自动决定要不要调用这个方法来获取实时信息。Service public class WeatherService { Tool(name getCurrentWeather, description 根据城市名获取当前温度) public String getCurrentWeather(String city) { // 调用天气接口 return 20度; } }实测下来有个关键点Tool的描述质量直接决定模型是否调用它。模型是完全靠方法名和描述来猜这个工具是干什么的。描述写得太模糊模型会在该调用的时候不调用参数名和描述不清晰模型又会传入莫名其妙的参数。我自己之前写过一个工具方法description里没写参数格式必须是城市中文全名结果模型传了拼音进来一度以为框架解析有问题。6.2 Spring AI Alibaba的NL2SQL思路Spring AI Alibaba是阿里体系对Spring AI的扩展其中比较受关注的是NL2SQL——把自然语言转成SQL查询。这种场景特别适合企业内部的数据查询辅助工具。但要落地注解依然有用你可以把数据表的字段信息通过注解绑定让AI在生成SQL时参考业务元数据而不是真的让它凭空理解数据库结构。我自己试过的链路是用户提问 - Spring AI把问题结构化 - 通过Tool调用元数据查询方法 - 拼接提示词让模型生成SQL - 执行并校验SQL - 返回结构化结果。整个过程中安全性是最需要注意的环节。生成SQL后必须做权限校验和行级过滤不能直接拿用户原始问题去拼SQL更不能让模型生成的SQL有注入风险。NL2SQL的难点不在AI模型多聪明而在于边界控制做得多严密。6.3 Java 21虚拟线程与Spring Boot 3.5的配置要点Java 21正式发布了虚拟线程Virtual ThreadsSpring Boot 3.2开始支持Spring Boot 3.5版本已经把这套用法做得相当成熟。虚拟线程的意义在于线程不再是宝贵资源你可以为每个请求开一个轻量级虚拟线程阻塞操作不再浪费昂贵的平台线程。对Web应用的直观感受是高并发下的吞吐量大幅提升尤其是那些IO密集型的场景比如大量访问数据库、调用外部API。启用方式简单但有一点要特别小心虚拟线程下不能使用ThreadLocal做一个长生命周期缓存的方式非常容易出问题。虚拟线程的数量远多于物理线程ThreadLocal如果被用在请求级数据传递场景其清理逻辑一旦没做好内存占用会成倍增长。Spring的TransactionSynchronizationManager和SecurityContextHolder底层都涉及ThreadLocal在高并发虚拟线程模式下需要你格外注意上下文传递的准确性和销毁时机。Spring Boot 3.5的配置示例spring: threads: virtual: enabled: true就这一行应用的调度器就会切到虚拟线程。如果你是做微服务的还要评估现有线程池、连接池是否兼容虚拟线程尤其是那些自己new固定大小线程池的代码。虚拟线程适合阻塞型任务但不适合CPU密集型计算任务。别什么都往里扔否则核心计算照样会抢占资源。6.4 Spring Cloud Alibaba与版本矩阵的自我救赎Spring Cloud Alibaba这几年成了国内微服务的主流选择但是版本对应关系是个永远绕不开的坑。每次一升级就是全家桶的dependencies互相对版本。我在这个坑里的体会是不要随便从网上复制别人的版本号。直接看官方版本说明按照Release Notes确认Spring Boot版本、Spring Cloud版本、Spring Cloud Alibaba版本三者的对应关系并且在pom里显式声明这些版本不能依赖传递解析去猜。io.github.openfeign.querydsl这类偏门的依赖如果和Spring Boot版本对不上很容易在运行时出现NoSuchMethodError。遇到这种问题最快的方法是用mvn dependency:tree看实际生效版本然后去对应仓库看它依赖了哪个OpenFeign版本。最怕的是问哪个版本好用版本问题没有好用只有匹配。你的依赖树里每一层都去对齐看起来麻烦但这是微服务工程长期稳定运行的基础。写在最后注解学习是一条由表及里的路做了这么多年Spring相关项目我的体会是注解学到最后拼的不是记住多少注解而是理解框架的运行机制。今天看到的Transactional可能明天换成声明式事务今天的三级缓存可能明天被GraalVM和AOT模式重写。但Spring内核处理问题的思路比如基于元数据驱动、通过后置处理器扩展、用代理织入横切逻辑这些底层的设计模式是稳定的。给还在学习阶段的朋友一个建议每学一个注解都去想一想三个问题——这个注解被谁解析它在什么阶段生效如果它没生效可能的原因是什么把这三个问题想透你就不再是背注解的人而是用注解的人。实践出真知多写几遍手撕源码和调试一定比焦虑地刷面试题更有用。
返回列表