拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

Spring核心原理与实战:IOC/DI、Bean生命周期、三级缓存与AOP

Spring核心原理与实战:IOC/DI、Bean生命周期、三级缓存与AOP 说到Spring几乎每个做Java开发的人都能聊上几句有人说它是“一堆注解一个容器”有人说它太复杂了启动起来一堆黑魔法也有人用Spring Boot写了几年业务代码遇到事务失效、循环依赖这种问题时依然一脸懵。今天这篇不打算把Spring知识点像清单一样罗列出来让你背而是从一个实践者的视角把Spring最核心的运行原理、注解体系、常见坑位和面试高频考点串成一条完整的线。文中会涉及IOC/DI、Bean生命周期、三级缓存、手写mini Spring、常用注解拆解、事务与AOP的实现机制、Spring Boot中的注解实战、常见报错排查等内容。无论你是刚接触Spring的新人还是准备面试的进阶选手或是已经在生产环境“踩坑无数”的老兵这篇都能给你一些不一样的启发。1. Spring运行原理与Bean生命周期拆解1.1 IOC/DI把“找对象”这件事交出去Spring的根基不是那些花里胡哨的注解而是IOC控制反转和DI依赖注入。什么叫控制反转你可以把它想象成开餐厅传统方式是你自己当天去菜市场买菜、自己洗菜切菜从锅碗瓢盆到菜单全包在自己手里——这是“正转”所有依赖都由你自己创建和管理。而Spring的模式是你只需要在菜单上写“我要做一道回锅肉”供应链容器会按约定把肉、蒜苗、豆瓣酱批量送到厨房你拿到手直接用就行。放到代码里就是以前你写UserService userService new UserService()现在你只声明Autowired private UserService userService;容器负责找到UserService对应的Bean创建好再注入进来。Spring的依赖注入支持三种方式构造器注入public UserController(UserService userService)Setter注入通过set方法字段注入Autowired直接打在字段上工作里最常见的是字段注入写起来确实爽但拆解下来问题也不小类与类之间的依赖关系藏在字段里IDE和代码审查者一眼看不出来字段注入的对象在单元测试里很难直接构造更重要的是它无法把依赖声明为final无法保证不可变性。所以很多严格的项目会建议尽量使用构造器注入字段注入能不用就不用。不过在实际开发里字段注入的便利性太突出了规范是规范落地时大家还是普遍在用。如果你的项目对工程质量要求高可以约定控制器层用构造器注入Service内部字段用Resource或Autowired都行核心是团队认同并保持一致。1.2 Bean的生命周期从定义到销毁的完整路线之前有个朋友面试回来跟我说面试官问“Bean的生命周期”他把网上背的十几步一口气说完了面试官反问“那Autowired在哪一步生效”他一下子卡住了。Bean生命周期不能只背序号要理解每个阶段Spring到底做了什么。一条比较完整的链路是这样的解析配置类和注解生成BeanDefinition包括类名、Scope、懒加载标记、初始化方法名等元数据实例化Bean。默认调用无参构造器或你指定的构造器此时对象还是一张“白纸”所有属性都是默认值属性填充populateBean这一步会处理字段上的Autowired、Resource、Value把依赖对象注入进来处理Aware回调BeanNameAware、BeanFactoryAware、ApplicationContextAware等把Bean的“身份信息”告诉你BeanPostProcessor.postProcessBeforeInitialization初始化前的钩子执行初始化逻辑PostConstruct注解方法、InitializingBean接口、自定义init-method三者的执行顺序就是写的这个顺序BeanPostProcessor.postProcessAfterInitialization初始化后的钩子AOP代理通常在这里生成Bean就绪进入单例池一级缓存容器关闭时执行销毁逻辑PreDestroy、DisposableBean、custom destroy method这里有个一直容易混淆的点Autowired是属性填充阶段干的活PostConstruct才是初始化阶段干的活。如果你在Autowired字段还没填充完就想去用别的Bean那肯定报空指针。另外InstantiationAwareBeanPostProcessor可以在构造器执行之前拦截Bean的创建如果它返回了一个非null对象后面的正常创建流程就直接短路了。给你一个理解线索整个生命周期就像一条流水线BeanPostProcessor相当于流水线上的质检点前前后后各安插一个让你能自定义干预Spring的创建流程。AOP代理就是利用“初始化后”这个质检点塞进去的。1.3 Spring三级缓存与循环依赖面试必考的原理拆解循环依赖说白了就是A依赖BB依赖A创建A的时候发现要B创建B的时候又发现要A两个对象谁都无法先完成属性填充死锁了。Spring解决setter循环依赖靠的是三个Map也就是常说的三级缓存缓存名称存储内容特点singletonObjects成品Bean完全初始化完毕的对象earlySingletonObjects早期Bean已实例化但未完成属性填充/初始化的对象singletonFactoriesObjectFactory工厂延迟生成早期引用的工厂每个Bean一个流程长这样创建A执行构造器实例化出对象A后把A的ObjectFactory放入三级缓存singletonFactories填充A的属性发现引用了B于是先尝试获取B容器里没有B就创建B同样把B的ObjectFactory放入三级缓存填充B的属性发现引用了A于是去获取A。一级缓存里没有成品A二级缓存里也没有早期A但三级缓存里有A的ObjectFactory于是调用它内部会执行getEarlyBeanReference得到一个早期引用这个早期引用放入二级缓存三级缓存的ObjectFactory移除B拿这个早期A引用完成属性填充随后完成初始化B进入一级缓存回到AA拿到B完成属性填充继续走初始化流程最终A也进入一级缓存看到没三级缓存的精髓在于“延迟”。如果A没有循环依赖它就不会去提前生成代理对象一旦检测到循环依赖才通过第三级的工厂提前暴露引用为AOP代理留出操作空间。有人会问用二级缓存不也能提前暴露吗其实如果没有AOP二级缓存确实够用。但Spring要考虑一个场景Bean在最终初始化后会被AOP代理而循环依赖发生时B需要提前拿到A的引用这个引用很可能已经是代理对象。如果只用二级缓存Spring就无法判断到底该存普通对象还是代理对象也没法保证最终返回给调用者的A是不是同一个代理。第三级缓存存放ObjectFactory恰恰是为了把“生成代理的时机”往后拖延到不得不做的那一步避免每个Bean都提前走一遍代理创建流程。还有三个限制必须记住构造器注入解决不了循环依赖因为构造器执行之前没法提前暴露引用prototype作用域不缓存也无法解决想打破循环依赖可以加Lazy让其中一个依赖延迟加载。2. 手写一个mini Spring用10个类理解容器本质热搜词里有“手写Spring”我觉得这是理解Spring最好的方式。不用照着源码抄把核心骨架撸一遍比读十篇源码分析都有用。2.1 组件扫描与BeanDefinition的收集Spring容器的起点是扫描。我们要先定义几个注解Target(ElementType.TYPE) Retention(RetentionPolicy.RUNTIME) public interface Component { String value() default ; }注意Retention必须设置成RUNTIME否则JVM运行期间通过反射读不到这个注解。这是自己写自定义注解时最容易忽略的一点后面3.4节我还会展开讲。扫描的逻辑很简单通过ClassLoader拿到basePackage对应的资源路径遍历路径下所有.class文件用反射读取类上的Component注解更严谨的会直接用ASM读取避免加载部分无关类触发静态代码块为带注解的类生成BeanDefinition存类名、Scope等元数据我当初手写的时候用的是Class.forName()直接加载类结果扫描了一堆不相关的类把静态初始化代码也执行了还踩了一堆坑。后来才明白为什么Spring要搞一个ClassPathBeanDefinitionScanner扫描阶段只收集“定义”不实例化等真正创建Bean时再加载。2.2 依赖注入与单例池的雏形然后是容器的核心一个简化版ApplicationContextpublic class MiniApplicationContext { private MapString, BeanDefinition beanDefinitionMap new HashMap(); private MapString, Object singletonObjects new ConcurrentHashMap(); public void scan(String basePackage) { // 收集BeanDefinition } public void refresh() { // 先创建所有非懒加载的单例Bean for (String beanName : beanDefinitionMap.keySet()) { getBean(beanName); } } public Object getBean(String beanName) { if (singletonObjects.containsKey(beanName)) { return singletonObjects.get(beanName); } // 实例化 - 属性填充 - 初始化 - 放入单例池 return createBean(beanName); } }属性填充这一步是注解注入的关键private void populateBean(Object instance, Class? clazz) { for (Field field : clazz.getDeclaredFields()) { if (field.isAnnotationPresent(Autowired.class)) { field.setAccessible(true); Object dependency getBean(field.getName()); // 按名称找 field.set(instance, dependency); } } }如果只有这样一个singletonObjects遇到A依赖B、B依赖A就彻底死了。所以哪怕在mini版容器里你也需要提前把完成构造但未填充属性的“早期Bean”放到一个Map里让循环依赖的注入能先拿到引用。我实际操作下来觉得自己写一遍“早期Bean暴露”的逻辑三级缓存为什么要存在理解立刻就不一样了。2.3 手写过程的实操体会与收获说几个我手写时踩过的坑field.setAccessible(true)必须在Java模块化环境下对导出包可见否则反射会抛IllegalAccessException真实项目里如果遇到模块化约束也要注意。注入依赖时不能拿着一个key查到底得先处理字段类型按类型找候选Bean多个候选再按名称过滤。这就是Autowired和Resource运行时行为差异的来源。代理处理不能偷懒。如果A引用了有接口的实现类JDK动态代理能搞定但类没实现接口想要代理就得靠CGLIB生成子类而CGLIB的子类方案对final类无效。Spring Boot 2.x之后默认AOP代理方式是CGLIB底层已有自己的解决思路但原理始终是这两板斧。手写一遍之后你会明白Spring远没有想象中神秘它的核心就是BeanDefinition描述Bean的一切元数据singletonObjects单例池createBean实例化、填充、初始化三步曲BeanPostProcessor扩展点各种注解告诉容器“你该在哪个时机做什么”3. Spring常用注解体系逐个击破3.1 组件注册注解从Component到Bean先说组件注解。Component是Spring最通用的组件标记标注一个类由容器托管。在此基础上派生出了三个带语义的注解Controller控制器层、MVCService服务层、业务逻辑Repository数据访问层、DAO它们本质都是Component只是语义不同。Spring扫描时会一视同仁地注册成Bean但AOP切面和事务注解在扫描时可能会根据注解类型做额外处理——比如Repository会被翻译为数据访问异常转换器识别的标记。Bean注解则是方法级别的注册方式通常写在Configuration配置类里Configuration public class AppConfig { Bean public RestTemplate restTemplate() { return new RestTemplate(); } }Configuration和Component有一个容易被忽略的区别被Configuration标记的类生成的Bean方法会被CGLIB增强调用另一个Bean方法时返回的是容器里的单例而把相同代码放在Component里每次调用Bean方法都会执行方法体可能产生新实例。这个坑在配置类里互相调用Bean时最容易暴露。如果你的Bean有多个实例需要区分用Primary标注主候选者用Qualifier指定具体名字这两个注解通常配合Autowired一起用。3.2 依赖注入注解Autowired、Resource与Value的差异Autowired是Spring原生注解默认按类型注入。如果同一类型有多个Bean没有加Primary会报错可以配合Qualifier(userDao2)指定名称。required false时找不到依赖不会报错而是注入null但这种写法往往是隐患我建议线上代码尽量别这么干。Resource是JSR-250规范里的注解在Spring里同样可用默认按名称注入名称找不到会退化为按类型找。区别从代码可见对比项AutowiredResource来源SpringJSR-250标准默认方式按类型按名称多候选时需要Qualifier配合可通过name属性指定字段名匹配不强依赖默认用字段名匹配Value则是注入配置值的利器。它可以注入字面量、占位符、SpEL表达式Value(${app.name}) private String appName; Value(#{systemProperties[user.home]}) private String userHome; Value(#{userService.getDefaultUserName()}) private String defaultUserName;注意区分${...}和#{...}两套语法前者是配置占位符从Environment和PropertySource里取值后者是SpEL表达式可以调用Bean方法、访问对象属性。很多新人会把这两者混在一起用导致Value(${#xxx})这种错误写法。说到SpEL它其实不只是给Value用的。Spring缓存注解Cacheable(key #id)、异步线程池选择Async(taskExecutor)、事务管理器名称等场景都会用到表达式。理解SpEL的语法等于多了一把动态配置的钥匙。3.3 配置与条件注解Configuration、ConditionalOnXxxConfiguration标记的类本身也是Bean它承载了三类字段Bean方法、组件扫描、属性绑定。前面说过Configuration类会经过CGLIB增强保证Bean单例。ConfigurationProperties则负责把配置文件里的前缀绑定到Java对象上app: name: demo version: 1.0.0Component ConfigurationProperties(prefix app) public class AppProperties { private String name; private String version; }Spring Boot的自动装配能力来自EnableAutoConfiguration。它会读取META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件把一堆配置类加载进来再通过ConditionalOnClass、ConditionalOnProperty、ConditionalOnMissingBean等条件注解决定到底哪些自动配置生效。这就是为什么你引入一个spring-boot-starter-data-redis之后什么都不用配置就能用上RedisTemplate——自动配置类上写着ConditionalOnClass(RedisOperations.class)类一存在条件就为真配置就生效。以后看到“为什么我加了依赖就多了一堆Bean”的时候脑子里要浮现出Conditional这几个字。3.4 Java原生注解与Lombok注解容易忽略但很重要的那些Java自带注解是注解体系的元老很多人反而没啥感觉Override让编译器检查方法签名是否正确Deprecated标记过时SuppressWarnings(unchecked)压制编译警告FunctionalInterface声明函数式接口仅允许一个抽象方法自定义注解的基础是元注解Target指定注解能标注的位置TYPE、FIELD、METHOD、PARAMETER、ANNOTATION_TYPE等RetentionCLASS还是RUNTIME。需要反射读取请务必用RUNTIMEDocumented是否生成到JavaDocInherited是否被子类继承在实际业务里自定义注解多半是配合AOP用的。比如做一个操作日志注解Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface OperationLog { String module() default ; String action() default ; }Aspect Component public class OperationLogAspect { Around(annotation(operationLog)) public Object around(ProceedingJoinPoint pjp, OperationLog operationLog) { MethodSignature signature (MethodSignature) pjp.getSignature(); Method method signature.getMethod(); OperationLog log method.getAnnotation(OperationLog.class); // 记录日志 return pjp.proceed(); } }Lombok注解在生产环境极其常见其中两个高频但理解容易出岔子EqualsAndHashCode的作用是自动生成equals和hashCode方法。它默认只基于当前类所有非static、非transient字段。一旦类有父类且父类也有业务字段必须设置callSuper true否则子类的equals会忽略父类字段。举个真实踩坑例子两个子类A和B继承同一个父类PP里有id字段子类里各有一个name字段。如果callSuper false那么两个子类只要name相同equals就可能返回true即使它们的父类id完全不一样。这个Bug极其隐蔽又不报错最容易在Set集合去重时暴露。SneakyThrows的作用就更有意思了用它标注的方法可以在不声明throws的情况下抛出受检异常Lombok通过字节码处理把编译器检查“蒙混过关”了。典型场景是Lambda表达式里处理IOExceptionFiles.list(Paths.get(/tmp)).forEach(path - { // 这里想抛出IOException throw new IOException(mock); });在Java原生写法里这个Lambda需要显式捕获或异常包装代码会变得很难看。加上SneakyThrows后SneakyThrows public void listFiles() { Files.list(Paths.get(/tmp)).forEach(path - { throw new IOException(mock); }); }但我要提醒一句SneakyThrows等于把受检异常变成了非受检异常调用方意识不到这里有异常需要处理生产代码里尽量保留原异常的可控性只在特别需要简化样板代码的地方使用。自定义注解的一个高频报错“attribute value must be constant”。这其实是Java编译器的约束注解属性只允许编译期常量不能是运行时变量。比如你不能写Component(config.getBeanName())编译器直接报错。解决办法是把这个值定义为static final常量或者放弃用注解动态传值的想法改用BeanDefinitionRegistry编程式注册Bean。理解了这一点以后就不会在注解配置文件参数时白费力气。4. AOP与事务注解的代理真相4.1 AOP是如何通过代理生效的Spring AOP的底层是动态代理只有两条路线目标类实现了接口默认走JDK动态代理基于接口生成代理对象目标类没有接口走CGLIB通过生成子类来代理Spring Boot 2.x之后默认强制CGLIB即使有接口也优先用CGLIB。原因是JDK代理的对象只能强转成接口类型你要是把它注入到一个具体实现类里就会报ClassCastExceptionCGLIB则没有这个问题。AOP的注解家族包括Aspect标记切面类Pointcut定义切入点Before/After/AfterReturning/AfterThrowing/Around五类通知Around最灵活可以做前置、后置、异常处理还能控制目标方法是否执行。切入点表达式最常见的是execution(public * com.example.service.*.*(..)) annotation(com.example.annotation.OperationLog)注解式切入annotation在业务系统里用得多它的原理是Spring在运行时通过动态代理检查目标方法上是否存在指定注解。这也意味着注解必须用RUNTIME保留策略否则反射根本读不到。4.2 Transactional深度解析七种失效场景Transactional是Spring声明式事务的重头戏可以写在类上或方法上支持配置传播行为、隔离级别、超时时间、只读、回滚规则等属性Transactional( propagation Propagation.REQUIRED, isolation Isolation.REPEATABLE_READ, timeout 5, rollbackFor Exception.class ) public void doBiz() {}它之所以能工作是因为Spring为标注了事务注解的Bean生成代理进入方法前开启事务方法正常结束则提交抛出异常则回滚。理解了代理机制下面七个失效场景就顺理成章了方法不是public。Spring AOP默认只拦截public方法private、protected方法上的事务注解不生效甚至Spring根本不会为private方法创建事务代理。同类方法自调用。this.doBiz()调用的是原始对象的方法不经过代理对象。事务切面逻辑根本没进自然没有事务。异常被捕获吞掉。方法内部catch了异常没继续抛出事务管理器感知不到异常也就不会回滚。抛出受检异常。Spring默认只对RuntimeException和Error回滚。如果你抛了一个IOException但没有显式设置rollbackFor Exception.class事务不会回滚数据照常提交。Bean没有被Spring管理。类上没有Component/ServiceSpring根本不会帮你生成代理事务注解形同虚设。数据库引擎不支持事务。比如MySQL的MyISAM引擎就完全不支持事务Transactional写得再标准也白搭。多线程中调用事务方法。事务是绑定到当前线程的子线程里执行的事务操作不会纳入父线程的事务范围两个线程拿到的连接也不一样。4.3 事务代理的坑同类调用与自注入自调用是事务失效里最隐蔽也最高频的一种。怎么解决把需要事务的方法放到另一个Service类里让跨类调用走代理在当前类里注入自己的代理Service public class UserService { Autowired private UserService self; public void outer() { self.inner(); // 走代理对象 } Transactional public void inner() { // 事务逻辑 } }或者用AopContext.currentProxy()但需要在启动类开启EnableAspectJAutoProxy(exposeProxy true)另外要说明的是事务注解的传播行为REQUIRED是默认值外层有事务就加入外层REQUIRES_NEW会挂起当前事务新开一个物理事务NESTED则是在外层事务里嵌套一个带保存点savepoint的逻辑子事务它和REQUIRES_NEW最大的区别在于回滚范围——NESTED回滚后外层还能继续提交。这个差别面试经常问到实际使用时可以按需选择。还有一点容易被忽略事务注解可以写接口上Spring在解析事务属性时如果目标类方法上没有注解会去接口上找所以通常也能生效。但官方和大多数实践都建议把注解写在实现类方法上尽量减少依赖接口注解的隐式行为。5. Spring Boot与MVC注解实战5.1 请求映射注解RequestMapping家族与RequestBody能否加两个参数MVC层的注解是Web开发每天都要碰的RequestMapping是最基础的映射注解它又派生出了GetMapping处理GET请求PostMapping处理POST请求PutMapping处理PUT请求DeleteMapping处理DELETE请求PatchMapping处理PATCH请求参数绑定注解包括PathVariableURL路径参数、RequestParam查询参数、RequestHeader请求头、CookieValueCookie等。这里有一个经常被问到的考题RequestBody能不能加两个参数答案是不能。一个HTTP请求体只能被解析一次RequestBody绑定的目标是整个请求体反序列化出来的对象两个RequestBody参数会让Spring无法确定该把body内容给谁。实际运行起来会直接报错要求一个方法只能有一个RequestBody参数。正确做法是封装成一个DTOPostMapping(/order) public Result createOrder(RequestBody OrderRequest request) { // OrderRequest 内部包含 userInfo 和 orderInfo }如果你有两个来源不同格式的数据比如JSON body query参数那不是两个RequestBody而是RequestBody RequestParam的组合这样完全没问题。做参数校验时记得加Validated或Valid配合NotNull、NotBlank、Size等校验注解参数不对会直接抛出MethodArgumentNotValidException配合全局异常处理器统一返回错误。5.2 Spring Boot自动装配与常用注解SpringBootApplication本质上是一个组合注解SpringBootConfiguration EnableAutoConfiguration ComponentScanSpringBootConfiguration就是一个配置类标记EnableAutoConfiguration开启自动装配ComponentScan扫描当前包及其子包下的组件日常开发常用的还有EnableAsyncAsync异步执行。注意异步方法同样有自调用失效问题而且异步异常在主线程捕获不到EnableSchedulingScheduled定时任务。要注意定时任务默认是单线程的多个任务会排队阻塞需要配置线程池Transactional数据库事务EnableCachingCacheable/CacheEvict/CachePut缓存逻辑日志这块虽然不是注解但和Spring Boot配置耦合很深。推荐的做法是配置文件里用logging.level.com.exampleDEBUG控制级别复杂场景用logback-spring.xml定义链路日志结构。千万别把日志全打成System.out尤其在生产环境和性能排查地形同虚设。5.3 Spring Security与Spring Cloud Alibaba场景中的注解在Spring Security中方法级权限控制常用EnableMethodSecurity // Spring Security 6.x新注解替代旧的EnableGlobalMethodSecurity public class SecurityConfig {}PreAuthorize(hasRole(ADMIN)) public void adminOnly() {}这意味着进入该方法前Spring Security会检查当前登录用户是否有对应权限。注意Spring Security框架本身没有和Spring Boot强绑定但你一旦引入starter自动配置就会把所有请求纳入安全过滤链所以很多新手“加了Security之后接口全401了”的困惑本质上也是自动装配在起作用。Spring Cloud Alibaba生态里EnableDiscoveryClient开启服务发现LoadBalanced让RestTemplate具备负载均衡能力SentinelResource为方法添加流控降级逻辑。这些注解背后依然是AOP或自动配置只是切面做的事从“事务”变成了“注册发现、负载均衡、熔断”。顺带提一个热搜词Spring Boot 3中spring-cloud-starter-oauth2已经被废弃。Boot 3整体迁移到了jakarta.*命名空间OAuth2相关能力拆分成了spring-boot-starter-oauth2-resource-server资源服务器和spring-boot-starter-oauth2-client客户端。老项目如果还在用oauth2 starter升级时要注意这个变化否则一堆import全部报红。如果你想在业务系统里快速集成一个自己的权限注解思路和之前的OperationLog一样定义注解 - 在拦截器或AOP中读取 - 配合SecurityContextHolder获取当前用户 - 做权限判断。框架提供的注解只是现成方案理解背后的机制才能自己扩展。5.4 Spring AI中的注解扩展热搜词里出现了Spring AI这里简单说几句。Spring AI是最近很热的方向它把AI模型的调用封装成了Spring风格的组件。值得注意的是Tool注解作用是把一个Java方法暴露给AI模型作为“工具调用”Function Calling。比如Component public class WeatherTool { Tool(name getWeather, description 查询指定城市的天气) public String getWeather(String city) { // 调用天气服务 return 晴天25度; } }AI在对话过程中如果判断需要查询天气就会触发这个Java方法拿到结果再组织回复。整体思路还是注解代理只不过代理的对象从普通Service换成了与大模型交互的通道。6. 高频报错与避坑指南6.1 Component报错“attribute value must be constant”该怎么办前面在自定义注解时提过这个报错这里专门展开一下。它不是因为Spring本身的问题而是Java编译器的规定注解元素必须是编译期常量只能使用基本类型、字符串、枚举、Class或这些类型的数组且每个值都必须是常量表达式。最常见的错误写法Component(beanName) // 这里的beanName是一个变量 public class UserService {}正确写法是Component(userService) public class UserService {}或者定义一个static final String BEAN_NAME userService;然后Component(BEAN_NAME)。如果你真的想在运行时动态控制Bean名、属性等就别用注解硬杠了改为编程式注册GenericBeanDefinition definition new GenericBeanDefinition(); definition.setBeanClass(UserService.class); registry.registerBeanDefinition(dynamicBeanName, definition);6.2 IDEA写注解小写不联想是怎么回事热搜里有“IDEA 2025.3.6在写注解时输入小写字母不会联想注解”。先说结论这不是Spring的bug而是IDEA代码补全的“大小写敏感”设置问题。IDEA默认的Code Completion里Case sensitive completion选项通常为All letters意味着输入autowired不会匹配到Autowired。解决办法File - Settings - Editor - General - Code Completion把Case sensitive completion改成None或First letter也可以直接取消勾选Match case选项这样输小写也能联想起注解。还有一种可能是Spring插件没安装或版本不匹配IDEA识别不到Spring上下文中的注解。检查Settings - Plugins里Spring和Spring Boot插件是否启用即可。6.3 Spring Boot 3的依赖迁移注意事项除开OAuth2 starter的废弃Spring Boot 3还有几个对老项目影响很大的变化命名空间从javax.*迁到jakarta.*一些自动配置类路径调整例如spring.factories改为AutoConfiguration.importsServlet API、Validation API也同步迁移到jakarta遇到依赖报红时优先检查是不是javax相关的旧依赖没换。我自己的经验是升级项目前先在IDE里全局搜javax.把它们全部替换成jakarta.再逐个处理starter版本会比运行时一个个报错再修要高效得多。6.4 高频问题速查表把新手最容易踩的坑汇总成一个表现象可能原因排查方向Autowired报找不到BeanBean没有Component注解 / 包没有被扫描到检查启动类的位置和ComponentScan范围Autowired报多个候选Bean同一接口有多个实现类加Primary或QualifierTransactional不生效方法非public / 同类自调用 / 异常被catch / 没rollbackFor逐条排查代理链路Value取不到值配置文件位置不对 / 占位符拼写错误确认application.yml加载路径和key循环依赖启动报错构造器循环依赖加Lazy或重构依赖Spring Boot接口全401Security自动配置生效配置PermitAll或加SecurityFilterChain定时任务不执行没加EnableScheduling / 任务被阻塞检查线程池配置Redis缓存不生效没加EnableCaching / key生成策略不对检查缓存注解和序列化配置最后再说点实际的我在实际使用Spring这么多年最大的体会是框架的知识点永远不是靠“背”能解决的。三级缓存为什么要设计三层事务为什么会失效AOP为什么走代理这些问题背后的答案其实紧紧咬着一个词——“时机”。Spring框架本质上是在一套固定的创建流程上安排了大量的扩展点注解只是你告诉容器“在某个时机帮我做点什么”的入口。如果你正在准备面试我建议你优先把Bean生命周期、循环依赖、事务失效、AOP原理这几个主题按“为什么”的方式讲给自己听讲到能脱离笔记说清楚为止。如果你刚入门Spring不要一上来啃源码先把常用注解用熟再手写一个mini容器很多概念会自动打通。最后分享一个小技巧遇到比较难啃的Spring源码类可以先用IDE的Debugger在关键方法上跑一遍Demo看看堆栈里的调用链比单纯读源码快得多。Spring的源码本身也是“优雅设计”的活教材你读得越多越能体会到那些注解背后隐藏的工程智慧。
返回列表