ARTICLE DETAIL

资讯详情

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

Spring Bean 管理三种方式:XML、注解与 JavaConfig 详解

Spring Bean 管理三种方式:XML、注解与 JavaConfig 详解 1. 一上来先搞清楚把 bean 交给容器到底是在干什么很多刚接触 Spring 的同学看了无数教程背了一堆注解最后还是一脸懵到底什么叫“把一个对象交给 Spring 容器管理”我换个直白的说法——你不再自己 new 对象了你只负责告诉 Spring“这个类归你管以后谁要用它你给它发一个就行。”至于这个对象什么时候创建、创建几个、怎么装配依赖、什么时候销毁全部由 Spring 容器统一调度。这听起来很简单但真正写代码的时候很多人会在这件事上栽跟头。最常见的报错就是热搜词里那两条Unsatisfied dependency expressed through field和Consider defining a bean of type java.lang.Long in your configuration.。这些报错本质上就一句话你想要的 beanSpring 容器里根本没有或者容器不知道这个类归它管。所以这篇文章我想彻底把“交给容器管理”这件事拆开聊清楚。Spring 里把一个类交给容器管理标准做法其实就三个方向XML 配置、注解扫描、JavaConfig 配置类。这三个方向不是谁取代谁的关系而是适用场景完全不同。我见过不少团队明明用 Spring Boot 还在硬写 XML也见过老项目里为了“统一风格”把所有类都塞进配置类结果配置类膨胀到上千行。这些都属于没想明白“三种方式各自解决什么问题”。下面我按理解难度从低到高把每条路的原理和实操都过一遍。在开始之前先说一个贯穿全文的基础认知Spring 容器管理 bean 的底层核心机制是BeanFactory和它的高级实现ApplicationContext。不管是 XML、注解还是配置类最终做的事情都是一样的——把类的定义信息BeanDefinition注册进容器然后容器根据这份“生产图纸”在合适的时机创建实例、完成依赖注入。所以你看那三种方式可以理解成三种写“生产图纸”的语法图纸本身最终都会被解析成BeanDefinition。这个底层机制搞懂了后面所有细节就都串起来了。2. 三种方式全景对比各自适合什么场景先给一张总表把三种方式放在一起看后面再逐个深入。方式代表写法核心优势主要局限常见使用场景XML 配置bean iduserService classcom.example.UserService/不侵入代码改配置无需重新编译写起来冗长类型安全差工程化弱老系统维护、部分中间件集成、框架底层注解扫描Component/Service/Repository/Controller开发效率高类即声明直观对已有类侵入性强必须改源码Spring Boot 项目、新业务模块开发JavaConfigConfigurationBean类型安全可编程适合装配第三方类配置类容易膨胀需要维护额外类第三方库集成、条件装配、复杂依赖配置我的建议很简单新项目无脑用注解 JavaConfig 组合XML 能不用就不用但必须看得懂。为什么因为 Spring Boot 的自动配置机制本身就是建立在 JavaConfig 之上的你天天用的SpringBootApplication背后就是一坨复杂的Configuration。你要是看不懂配置类遇到自动配置失效的问题根本无从下手。但这里要特别提醒一句“注解优于 XML”不等于“注解万能”。有些场景下 XML 依然有不可替代的价值。比如你要把一个类同时注册成多个不同名字的 bean或者需要在运行时动态调整 bean 的属性XML 的bean标签反而更灵活。再比如很多老项目里别人写好的 XML 里配了无数property注入你如果不懂 XML连排查问题都做不到。所以三种方式不是三选一而是“会用注解干活能看懂 XML会写配置类”三件事都要掌握。这也是面试里 Spring 部分必考的内容热搜词里“spring面试题”经常挂着原因就在这里。3. XML 配置方式最古老但最直白的一条路3.1 从最简单的实例化开始XML 方式的核心就是一个beans根标签包着一堆bean子标签。每个bean标签对应一个类的“生产图纸”。我拿一个最基础的用户服务举例?xml version1.0 encodingUTF-8? beans xmlnshttp://www.springframework.org/schema/beans xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd bean iduserService classcom.example.service.UserService/ /beans然后启动容器拿到这个 beanClassPathXmlApplicationContext context new ClassPathXmlApplicationContext(beans.xml); UserService userService context.getBean(userService, UserService.class); userService.doSomething();这段代码背后发生了什么ClassPathXmlApplicationContext启动时会读取beans.xml解析出BeanDefinition注册到容器然后容器在默认情况下非懒加载时单例池里直接创建好这个实例等着你用。我之前遇到过一个很搞笑的 bug项目启动后一切正常但一调用userService.doSomething()就报错查了半天发现是类名写错了Spring 启动时居然没报出来。原因是我把bean配置成了懒加载Spring 启动时根本不创建实例等到getBean才去创建才发现类不存在。这个坑后面细说。3.2 构造器注入与属性注入的 XML 写法XML 里给 bean 配置依赖有两种常用姿势构造器注入和 setter 属性注入。构造器注入用constructor-argsetter 注入用property。bean iduserService classcom.example.service.UserService !-- 构造器注入按索引或按名称匹配参数 -- constructor-arg nameuserDao refuserDao/ constructor-arg namemaxRetry value3/ !-- setter 注入要求 UserService 必须有 setUserDao 方法 -- property nameuserDao refuserDao/ /bean bean iduserDao classcom.example.dao.UserDao/这里的refuserDao表示引用容器里另一个 bean 的 idvalue3表示注入一个基本类型的值。Spring 会做类型转换字符串3自动转成int。这背后其实是 Spring 的TypeConverter体系在起作用支持 String 到任意基本类型的转换。我个人强烈建议优先使用构造器注入而不是 setter 注入。原因有两点第一构造器注入能保证依赖不可变且 bean 在被创建的那一刻就是完整可用的状态不会出现“new 出来但属性还是 null”的半成品第二构造器注入天然支持final字段这在写不可变对象时非常有用。但 XML 的构造器注入有个隐患如果构造器有多个同类型参数必须用index或name显式指定否则 Spring 是按参数顺序匹配的很容易配错。这在实际项目中真的会遇到——两个 String 类型的参数顺序一换线上数据就乱了。3.3 静态工厂与实例工厂被忽略的老方法除了直接new一个类XML 还支持通过工厂方法创建 bean。这在老项目集成第三方框架时很常见。静态工厂的写法bean idconnection classcom.example.factory.ConnectionFactory factory-methodcreateConnection/对应的工厂类是一个静态方法public class ConnectionFactory { public static Connection createConnection() { return new Connection(); } }实例工厂的写法稍微绕一点需要先定义一个工厂 bean再通过factory-bean引用它bean idfactory classcom.example.factory.ConnectionFactory/ bean idconnection factory-beanfactory factory-methodcreateConnection/说实话这种写法在现在的业务代码里已经很少见了但在 Spring 的底层整合代码里还经常出现。比如整合 MyBatis 时SqlSessionFactoryBean就是个典型的工厂类。你如果去看它源码里面核心逻辑就是getObject()方法本质就是一种工厂模式。所以这个知识点不是没用而是藏在了框架内部。这里有个细节值得注意工厂方法创建的对象Spring 容器无法干预它的构造过程只能干预它的生命周期管理。也就是说init-method和destroy-method依然有效但依赖注入就没法自动完成了。所以如果工厂方法返回的对象需要依赖 Spring 容器里的其他 bean你需要在工厂类内部自己搞定或者用后面说的 JavaConfig 方式包装一层更优雅。3.4 XML 里那些容易踩的坑XML 方式用起来虽然直白但坑也不少我把自己踩过的整理一下。第一个坑是bean 的 id 重复。Spring 在启动的时候如果发现同一份 XML 里有两个相同的 id会直接报错。但如果是一份 XML 里配了另外一份 import 进来的 XML 里也配了同名的行为会有点微妙——默认情况下后加载的会覆盖先加载的allowBeanDefinitionOverriding默认行为不同版本不一致。这种隐蔽的覆盖问题排查起来特别折磨人。我的建议是id 命名务必全项目唯一最好带模块前缀比如orderService、userService。第二个坑是lazy-init掩盖启动错误。前面说的那个类名写错、启动时不报错的问题就是lazy-inittrue导致的。我在实际项目里排查过这个问题最后发现是一个同事为了让“启动变快”给大量 bean 加上了懒加载。结果启动是快了但很多配置错误全部延迟到运行时才暴露线上环境一调用就报错比启动时直接报错难排查十倍。所以我的建议是默认不要开启懒加载只有对启动耗时确实有明显影响、且可能根本不会被用到的 bean 才考虑。第三个坑是autowire属性的误用。XML 里可以配autowirebyType或byName但这种方式非常隐蔽你看一个 bean 的配置时根本看不出来它注入了哪些依赖debug 的时候满世界找。这个特性在 Spring 3.0 之后基本就被废弃推荐了取而代之的是注解驱动的自动注入。如果你在维护老项目时看到这种配置建议逐步迁移掉不然团队协作会很难受。4. 注解方式Spring Boot 时代的绝对主流4.1 四个注解的本质区别其实只有一个注解方式的入门门槛低到几乎不需要解释在类上加一个Component它就归容器管了。但很多老手也不一定能说清楚Component、Service、Repository、Controller这四个注解到底有什么区别。答案是它们四个在 Spring 容器眼里没有任何区别本质都是Component的派生注解。唯一的差别是语义不同Service表示业务层Repository表示数据访问层Controller表示 Web 层。这些语义在某些场景下会被框架感知——比如Repository上的持久化异常转换Controller上的请求映射处理。但如果你在一个工具类上用了ServiceSpring 照常创建不会报错只是语义上不严谨。Service public class UserService { private final UserDao userDao; public UserService(UserDao userDao) { this.userDao userDao; } } Repository public class UserDao { public User findById(Long id) { // 模拟数据库查询 return new User(id, 张三); } }在使用注解方式时有一个前置条件必须满足Spring 必须扫描到这个类所在的包。Spring Boot 的启动类SpringBootApplication里默认定义了ComponentScan扫描范围是启动类所在包及其子包。如果你把一个Service类放到启动类所在包的子包之外Spring 就不会扫描到然后就会得到那个经典报错Consider defining a bean of type com.example.dao.UserDao in your configuration.这个报错我见过太多人卡住。排查思路很简单先看这个类有没有加注解再看它的包路径是否在启动类的扫描范围之内。绝大多数情况是包路径放错了位置。4.2 扫描配置ComponentScan 的细节如果你的项目里确实有某些类不在启动类的子包下就需要手动指定扫描范围。ComponentScan支持basePackages和basePackageClasses两种指定方式推荐用后者——basePackageClasses可以避免字符串拼写错误导致扫描失效SpringBootApplication ComponentScan(basePackages {com.example.core, com.example.module}) public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }这里有一个很大的坑如果同时配置了多个ComponentScan或者SpringBootApplication自带的扫描逻辑和自定义的扫描范围冲突可能导致某些 bean 被重复扫描或者完全漏扫。举个例子启动类在com.example.application包下默认扫描整个com.example及其子包如果你又显式加了ComponentScan(basePackages com.example.application)相当于把扫描范围缩回去了那些原本能被扫描到的com.example.module下的类就全部失效。这类问题不是启动时报错而是运行时注入失败特别难排查。所以我个人的习惯是能用默认扫描就不自定义。确实有需要自定义时宁可把扫描范围写得大一些也不要写得刚刚好给自己留点容错空间。4.3 Scope、Lazy、Primary、DependsOn注解方式的生命周期控制把类交给容器之后你还需要控制它的“存在形式”。最常用的是Scope默认值是singleton单例意味着整个容器里只有这一个实例。另一个极端的值是prototype意思是每次getBean都新建一个实例。Service Scope(prototype) public class PrototypeService { }这里有个非常容易踩的认知误区很多人以为Scope(prototype)会让依赖注入的地方每次拿到新实例。实际上如果在单例 bean 里注入 prototype beanSpring 默认只会注入一次之后永远用同一个实例。因为单例 bean 在创建时Spring 就把依赖注进去了不会感知后续的 prototype 重新创建。要解决这个问题需要用到ObjectProvider或者Lookup注解。这个坑在企业应用里特别典型比如一个单例的任务调度器里需要每次执行都用新的状态机实例。Lazy注解对应 XML 里的lazy-init作用是延迟 bean 的创建。但和 XML 时代不同注解方式下Lazy还有一个特殊用法可以标注在注入点上表示“注入一个代理对象真正调用时才去容器里拿真实 bean”。这在解决构造器循环依赖时很有用后面我细说。Primary解决的是“多个同类型 bean 注入哪个”的问题。比如有两个UserService类型的 bean你注入UserService时 Spring 会报NoUniqueBeanDefinitionException。解决办法要么用Qualifier指定名称要么在其中一个上标记Primary。这个注解是重构的大杀器——当你想给某个接口换默认实现时只需要在旧实现上不加Primary新实现上加所有没有显式指定Qualifier的注入点就自动切换到新实现不用改一堆调用代码。DependsOn控制 bean 的创建顺序。Spring 的默认创建顺序主要依赖依赖关系图A 依赖 BA 创建之前 B 一定创建好了。但如果你有两个 bean 之间没有依赖关系却又希望先创建某一个比如一个负责初始化缓存的 bean 必须在另一个 bean 启动前准备好数据就需要DependsOn了。4.4 注解方式最容易被忽略的问题扫描的代价说了这么多注解的好处我也得说一个它不那么美好的面扫描是有性能代价的。每次启动时Spring 会把扫描路径下所有类都读一遍判断有没有目标注解。项目小的时候无所谓项目大了几千个类启动时间就会明显变慢。Spring Boot 支持了Indexed注解通过spring-context-indexer编译期生成候选类索引来优化这一点但实际项目里用的人不多毕竟现代机器上这点启动时间对大部分应用可以接受。不过如果你在写一个对启动时间极其敏感的 CLI 工具或者 Serverless 函数倒是可以考虑用 JavaConfig 显式指定 bean减少扫描成本。但话说回来这个场景比较小众大多数时候你不会为启动省这几百毫秒去牺牲开发效率。5. JavaConfig 配置类最灵活、最类型安全的方式5.1 从 Configuration 到 BeanJavaConfig 里核心就是Configuration标注的类和Bean标注的方法。每有一个Bean方法容器里就多一个 bean。Configuration public class AppConfig { Bean public UserDao userDao() { return new UserDao(); } Bean public UserService userService() { return new UserService(userDao()); } }拿到容器的方式也变了不再读 XML 文件而是使用AnnotationConfigApplicationContextAnnotationConfigApplicationContext context new AnnotationConfigApplicationContext(AppConfig.class); UserService userService context.getBean(UserService.class);这个方法执行完之后容器里就有了userDao和userService两个 bean。看起来就是一个普通方法调用的过程但里面有很重要的一条知识带Configuration的类默认是被 CGLIB 增强过的userDao()这个方法的调用不会真的每次都 new 一个新的UserDao而是先从容器里拿。所以你即使反复调用userDao()方法拿到的还是同一个单例实例。这是 JavaConfig 和普通 Java 方法最大的区别也是它设计得最巧妙的地方。但如果Configuration的类被标记为proxyBeanMethods falseSpring Boot 里经常能看到这么配CGLIB 增强就会失效userDao()方法的每次调用都会返回一个全新实例。这个差异在单体应用里可能感觉不到但在某些依赖注入场景下会导致意想不到的 bug。所以我的理解是proxyBeanMethods true默认值适合有内部方法调用的配置类false适合那些没有内部调用的简单配置类来提升启动性能。除非你确定配置类内部不会互相调用否则别改成 false。5.2 各种场景下如何优雅地写 Bean如果说注解方式是“把类自己交出去”那Bean方式更像是“你手动把对象递给容器”。最常见的场景是你要把一个无法加注解的类放进容器典型分两类第三方库里的类比如 Redis 连接工厂、HTTP 客户端以及当前你没法改源码的类。这时候Bean几乎是唯一选择。Configuration public class HttpClientConfig { Bean public RestTemplate restTemplate() { return new RestTemplateBuilder() .setConnectTimeout(Duration.ofSeconds(3)) .setReadTimeout(Duration.ofSeconds(5)) .build(); } }另一个高频场景是条件装配。Spring Boot 的自动配置大量依赖这个能力。ConditionalOnProperty、ConditionalOnClass、ConditionalOnMissingBean这些条件注解可以用在Bean方法上意思是“满足条件才创建这个 bean不满足就跳过”。这个机制的威力在于你的配置类可以写死但实际容器里有没有某个 bean取决于运行时环境。这在写通用组件、框架插件时是核心利器。Bean ConditionalOnProperty(name app.cache.enabled, havingValue true) public CacheManager cacheManager() { return new ConcurrentMapCacheManager(userCache); }还有个容易被忽略的标准是Bean方法的名字。方法的名称默认就是 bean 的 id。如果你定义了getUserService()方法bean 名就是getUserService不是userService。如果后续用Qualifier(userService)注入会失败。所以建议写Bean方法时方法名就叫userService()、redisTemplate()和生成的 bean 名保持一致避免不必要的认知负担。5.3 Bean 方法间调用的真相CGLIB 代理前面说了Configuration类默认被 CGLIB 增强这里展开讲一行代码背后的原理。你写Configuration public class AppConfig { Bean public UserDao userDao() { return new UserDao(); } Bean public UserService userService(UserDao userDao) { // 注意我并没有在这里调用 userDao() 方法 // 而是通过参数接收 return new UserService(userDao); } }注意一点在userService()方法里我使用了参数UserDao userDao来接收依赖而不是直接调用userDao()方法。这其实利用了 Spring 容器对Bean方法参数的隐式解析——Spring 会自动从容器里找到UserDao类型或名字匹配的 bean 传进来。这种写法比直接方法调用在变量传递上更清晰也避免了 CGLIB 代理相关的困惑。如果你选择直接调用userDao()方法那就要理解代理的作用。CGLIB 会生成一个AppConfig的子类重写所有Bean方法方法内部先去容器里检查有没有对应的 beansingleton 作用域下命中 DefaultSingletonBeanRegistry 里的单例池有就直接返回没有才执行真正的new UserDao()。所以userDao()方法实际上只会在第一次被真正执行一次。如果Configuration变成了Component或者proxyBeanMethods falseCGLIB 增强就会丢失。userService()里调用userDao()时就会每次都执行一遍new UserDao()创建出一个新的实例。如果UserDao有状态比如内部有个计数器那这个计数器在每个UserService实例里都会被重置。我当时排查过一个类似的 bug一个全局状态机在注入后每次访问状态都不对查了半天发现是配置类上误写了Component而不是Configuration。5.4 FactoryBean另一种把对象交给容器的姿势除了BeanJavaConfig 时代还有一个容易混淆的概念FactoryBeanT。它的玩法是——你定义了一个类来实现FactoryBeanT接口然后把这个类本身注册成 beanSpring 在拿到这个 bean 时会调用getObject()方法把返回的T类型对象当真正的 bean 用而FactoryBean本身反而成了“工厂”。比如 MyBatis 的SqlSessionFactoryBean就是一个经典实现Configuration public class MyBatisConfig { Bean public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource) { SqlSessionFactoryBean factoryBean new SqlSessionFactoryBean(); factoryBean.setDataSource(dataSource); return factoryBean; } }容器里被注入的其实是SqlSessionFactory类型getObject()返回的类型不是SqlSessionFactoryBean。如果你需要拿到FactoryBean本身要用getBean(sqlSessionFactory)加个前缀。这是一个很冷门的语法但了解它之后再看很多框架源码就不会懵了。那FactoryBean和Bean有什么区别简单说Bean是“方法返回对象交给容器”FactoryBean是“把这个类交出去容器自动调用它的getObject()”。前者是配置层面的手段后者是创建对象的工厂抽象。实际工作中你写业务代码用Bean就够了但读框架源码时一定会碰到FactoryBean所以这个点值得了解不用深究。6. 三种依赖注入手段对比字段、Setter 与构造器把 bean 交给容器之后紧接着的问题就是依赖注入。Spring 支持三种注入方式虽然这不是“把对象交给容器”的直接动作但它是容器管理 bean 的核心部分而且很多报错都出在这里。注入方式写法优点缺点适用场景字段注入Autowired private UserDao userDao;最简洁难测试、隐藏依赖、无法 final不推荐Setter 注入Autowired public void setUserDao(UserDao dao) {...}可选依赖友好依赖可变、可能遗漏可选依赖构造器注入private final UserDao userDao; public UserService(UserDao userDao){...}不可变、必填依赖清晰构造器参数多时稍显冗长推荐字段注入大概是 Spring 初学时代用得最多的写法因为它最省事。但它在工程上的问题不小第一private字段的注入靠的是反射IDE 和编译期根本不知道这个依赖存在第二单元测试时无法直接 new 一个对象把依赖塞进去必须借助 Spring 或者反射工具第三被注入的字段不能是final破坏了对象不可变性。所以我一直建议团队里新代码不要用字段注入。构造器注入还有一个隐藏的优势在测试的时候你可以很自然地new UserService(mockUserDao)完全不需要 Spring 参与。这在写单元测试时爽到飞起。如果类的构造器参数太多往往说明这个类的职责过重这时应该考虑拆分而不是换一种注入方式来逃避问题。7. 生命周期和三级缓存容器接管后到底做了什么7.1 bean 的一生把 bean 交给容器之后容器并不是简单地new一下就完事。一个普通 bean 在容器里的完整生命周期大致是扫描/解析配置生成BeanDefinition实例化通过构造器创建对象此时对象还不是完整 bean属性填充完成Autowired、Value等依赖注入初始化前置执行BeanPostProcessor.postProcessBeforeInitialization初始化执行PostConstruct、InitializingBean.afterPropertiesSet()、init-method初始化后置执行BeanPostProcessor.postProcessAfterInitialization这里会生成 AOP 代理对象使用业务调用销毁执行PreDestroy、DisposableBean.destroy()、destroy-method。这里面最容易被忽略的是BeanPostProcessor。它是 Spring 容器“开挂”的核心机制AOP、Autowired本身、事务注解等等底层全是靠它实现。你可以把BeanPostProcessor理解成“每个 bean 出生后都要过一遍的流水线工位”每个工位可以对 bean 做加工。所以当你看到一个 bean 莫名其妙多了某些代理行为时大概率就是某个BeanPostProcessor干的。7.2 三级缓存为什么是三级而不是一级三级缓存是和循环依赖强相关的机制。当你有两个 bean 互相引用时比如 A 依赖 B、B 依赖 A如果按常规流程创建 A → 发现需要 B → 创建 B → 发现需要 A → 又回到创建 A这就死循环了。Spring 的三级缓存解决这个问题一级缓存singletonObjects存放完整的单例 bean二级缓存earlySingletonObjects存放提前暴露的“早期引用”对象已 new 出但属性还没填充完成三级缓存singletonFactories存放创建对象的工厂用来生成早期引用。流程简化说就是创建 A → A 的实例化完成、属性未填充时先把 A 的工厂放入三级缓存 → 填充属性时发现需要 B → 去创建 B → B 的属性填充时发现需要 A → 从三级缓存拿到 A 的工厂 → 生成 A 的早期引用 → 把这个早期引用注入给 B → B 创建完成 → 继续完成 A 的属性填充最后把完整 A 放入一级缓存。这里有个常见的误区三级缓存不是用来解决性能问题的而是为了解决“AOP 代理对象与普通对象不一致”的问题。如果只有一级缓存早期 A 是普通对象但最终 A 可能被BeanPostProcessor包装成代理对象那 B 里持有的早期 A 引用和最终的单例 A 就不是同一个对象了。三级缓存加ObjectFactory的设计确保早期暴露时就能通过getEarlyBeanReference拿到最终一致的代理对象。需要明确的是这只是 Spring 解决自身创建流程中循环依赖的一种机制并且只对单例、非构造器注入的 bean 有效。构造器注入的循环依赖无法通过三级缓存解决因为构造器在实例化阶段就必须拿到依赖对象此时第三级缓存还没用上。Scope(prototype)的 bean 循环依赖也无法解决因为原型 bean 不存在“提前暴露”的意义。我见到的团队规范里很多直接禁止循环依赖这确实是最干净的方案——用Lazy注解在构造器参数上打断循环也行但治标不治本。8. 高频报错排查实录从报错入手反推原理8.1UnsatisfiedDependencyException缺依赖的典型场景这个报错提示非常长但核心关键词就是Unsatisfied dependency expressed through field xxx直译就是“字段 xxx 的依赖无法满足”。比如Unsatisfied dependency expressed through field userDao; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type com.example.dao.UserDao available排查这个报错按这个顺序走基本不会漏看提示里是哪个字段找到具体类是哪个检查这个类是否加了Component/Service等注解——没加就是没交出去检查包路径是否在扫描范围内——不在就是漏扫描检查注入的类型是否有多个实现——有多个就看看有没有Primary或Qualifier。我之前排查过一个贼隐蔽的案例一个接口有两个实现类都加了Service但其中一个实现类里的Value(${app.timeout})配置的值在配置文件中不存在。结果 Spring 启动时直接报UnsatisfiedDependencyException而不是报Value解析失败因为Value解析失败发生在属性填充阶段先于Autowired的类型匹配判断。很多人一看这个报错就去查依赖注入查了半天没结果其实是配置值的问题。所以我建议这类报错先看完整堆栈不要只看第一行。完整堆栈里那个Caused by往往才是根本原因。8.2Consider defining a bean of type java.lang.Long报错的信号远比字面意思多热搜词里有一条很经典的报错Consider defining a bean of type java.lang.Long in your configuration.。这个报错看着荒诞因为Long是 JDK 类型你不可能去容器里注册一个Longbean。它实际的含义是某个地方试图注入一个Long类型但容器里没有这个类型的 bean。什么场景会出现这种报错最常见的是自动配置类里的条件判断失败。我举个例子一个类构造器是public CacheManager(Long timeout)你想通过Bean构造它但容器里没有Long类型的 bean。这时候 Spring 就报这个。它其实是在提醒你要么注册一个Long类型的 bean要么把Long改成从配置读取的Value(${cache.timeout})要么修改构造器参数。这种报错最麻烦的地方在于报错信息里提到的 bean 类型比如Long、String、Integer往往是基本类型你第一反应是“这怎么可能有 bean”。但这恰恰说明你的某个Bean方法或者组件的构造器参数设计得不合理把基本类型当注入依赖了。Spring 的设计哲学是需要配置值优先用Value读取配置文件需要复杂对象比如DataSource、RestTemplate才用依赖注入。把Long丢给容器管理本质上是“想用容器管理一切”的思路走偏了。8.3 循环依赖与三级缓存之外的问题循环依赖相关报错的标志性文案是The dependencies of some of the beans in the application context form a cycle。看到这个报错先确认循环链路到底是怎么形成的。最常见的两种构造器循环依赖new A需要 Bnew B需要 A。此时三级缓存不生效必须手动拆环。代理类循环依赖A 被切面代理B 依赖 AC 依赖 B 的深度链路。我处理循环依赖的经验是尽量从设计上消灭循环依赖而不是用Lazy或setter注入去“绕过”。循环依赖往往意味着职责划分不清晰。比如 A 依赖 BB 又依赖 A很可能 A 和 B 有一部分逻辑应该抽到 C 里。但如果确实有暂时绕不开的情况Lazy是开销最小的方案——它在构造器参数上标注后Spring 会注入一个代理对象真正调用时才去容器里找目标 bean。注意Lazy不解决循环依赖的“循环”本身它只是打断了“此时必须拿到完整对象”的约束。8.4 常见问题速查表报错关键信息可能原因推荐排查方向No qualifying bean of type ... available依赖类没加注解、包没扫描到、类型有多个实现检查注解、扫描路径、Primary/QualifierUnsatisfied dependency expressed through field ...字段类型容器里没有看完整堆栈的Caused by找原始原因The dependencies of some of the beans form a cycle循环依赖画依赖图拆环或使用LazyBeanDefinitionStoreExceptionXML 解析失败或配置类有问题看具体配置行号和标签BeanCreationExceptionbean 创建过程异常初始化方法抛错等看堆栈Caused by重点检查PostConstruct和BeanPostProcessorNoUniqueBeanDefinitionException同一类型有多个 bean加Primary或指定QualifierBeanNameNotUniqueExceptionbean 名称冲突统一 bean 命名规范检查配置9. 三种方式如何选型与混用很多人问一个问题三种方式能不能混用能而且 Spring Boot 项目里每天都在混用。关键在于理解各自的边界。我的推荐分层是这样的业务层类用Service、Repository、Controller这类派生注解让 Spring 自动发现第三方库实例用ConfigurationBean集中装配便于统一管理超时、连接池等参数老项目或特殊中间件保留 XML 配置但要通过 import 方式整合进来不要和注解混在同一个类里处理。还有一点Configuration类里可以配合ImportResource把 XML 引入进来实现“新代码用配置类老配置不破坏”的平滑过渡Configuration ImportResource(classpath:legacy-beans.xml) public class AppConfig { }这个注解在系统迁移时特别有用。我处理过一个老项目从 XML 全部迁移到注解的痛苦过程当时就是先用ImportResource把旧的 XML 留着保证系统能继续运行然后一个个把 XML 里的 bean 改成Bean或注解方式每改一个就删一段 XML。整个过程持续了好几周但系统一直稳定运行没有任何一个晚上因为迁移加班。这种渐进式改造的思路比“一个大版本全部改完”要稳妥得多。10. 最后分享几个小经验写到这里核心内容已经讲得差不多了。最后我再分享几个在实际项目中验证过的小经验希望对你有帮助。第一遇到 bean 相关的报错先看启动日志最前面的异常堆栈而不是中间的业务报错。Spring 在启动失败时通常会把所有上下文加载失败的异常都列一遍但根因往往只有一个。翻到Caused by那一行看到的信息远比报错标题有价值。第二不要在 bean 的构造器或PostConstruct里做耗时太长的操作。尤其是单例 bean这些操作会在启动阶段阻塞容器初始化。我有一个项目之前把数据预热逻辑放在PostConstruct里导致启动要两分钟。后来改成用ApplicationReadyEvent监听器来触发预热启动时间瞬间降到十秒。容器管理 bean 的“初始化”阶段不是什么事情都适合做的。第三尽量让 bean 的创建和使用保持简单。如果一个Configuration类里的逻辑越来越复杂各种条件判断、属性注入、循环调用你就要考虑拆分了。配置类和普通代码一样需要维护过度设计的配置类比业务代码更难改。最后如果你看完了这篇文章我建议你亲手做一个小实验用三种方式分别把同一个类交给容器然后调试看看ApplicationContext.getBeanDefinitionNames()里打印出来的 bean 信息。你会发现三种方式最终都殊途同归——容器里注册的都是BeanDefinition只是“注册的入口”不同。这个实验做完你对 Spring 容器管理 bean 的理解就真的打通了。
返回列表