news 2026/10/9 6:40:53

Spring Bean注册三种方式:XML配置、注解扫描与Java配置类

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring Bean注册三种方式:XML配置、注解扫描与Java配置类

做 Java 开发的人,每天都要跟 Spring 打交道。但有一个问题,我这两年问过不少候选人,也问过团队里的新人:你业务代码里随手写的一个类,到底通过什么方式变成 Spring 容器里的 bean?多数人能答出“@Component 扫描”,可再追问一句“如果不让扫描,你怎么把它交给容器?”,就开始支支吾吾了。

这是今天想认真聊聊的话题:把一个 bean 对象交给 Spring 容器管理,一共有三种常见方式——XML 配置、注解扫描、Java 配置类。三种方式都能让容器认识你的类、替你实例化、注入依赖、管理生命周期,但适用场景、写法习惯、排查思路完全不一样。这篇文章既适合刚学 Spring 的同学,也适合用了很久 Spring Boot 却没怎么碰过 XML 的老开发。我会把每种方式的写法、背后的原理、实际使用中的坑一起讲清楚,争取你看完就能在项目里对号入座。

1. 三种方式全景:为什么会有三种写法

1.1 先搞清楚“交给容器管理”到底意味着什么

很多新手写 Spring 代码,写了一个月也没想明白,容器到底帮我们做了什么。其实核心就一句话:你不再自己new对象,而是告诉容器“这个类归你管”,然后由容器负责创建实例、完成依赖注入、执行初始化回调,并在容器关闭时执行销毁逻辑。

这里说的容器,通常指ApplicationContext。它就像一个工厂加仓库的合体,BeanFactory负责生产,ApplicationContext在它之上加了事件、国际化、资源加载等能力。所谓“bean 交给容器管理”,本质上是你把一个类的“生产说明书”注册进去,容器按照BeanDefinition里的描述去实例化、装配、缓存。

为什么要费这个劲?因为你的业务系统里有太多对象互相依赖,如果每个地方都自己new,代码就会耦合得没法看。容器把对象之间的依赖关系统一收口,谁依赖谁、谁来初始化、什么时候销毁,全部由容器按配置决定。这也是为什么面试总爱问控制反转(IoC)和依赖注入(DI),这两个概念落到代码上,就是“怎么把 bean 交出去”和“怎么把依赖拿进来”两件事。

1.2 三种方式的演进脉络与选择逻辑

Spring 从 1.x 走到现在,注册 bean 的方式其实是一条“不断减少样板代码、不断靠近代码本身”的路线。

早期只有 XML 方式。所有 bean 都要在applicationContext.xml里声明,依赖关系靠<property>、<constructor-arg>描述。好处是配置完全外部化,运维和架构师可以只改 XML 不动代码;坏处也很明显——文件很长,写起来啰嗦,改一处依赖要翻半天,而且字符串类型的 class 名没有任何编译期检查,写错了只有运行时报错。

Spring 2.5 开始引入注解方式,@Component、@Repository、@Service、@Controller直接标在类上,配合<context:component-scan>或@ComponentScan,容器自动扫描包路径下的类并注册成 bean。这种方式让“类即配置”,写业务代码时顺手就把 bean 注册了,开发效率大幅提升。代价是类与容器产生了隐式约定——你不看注解,根本不知道这个类会被容器管,排查问题时需要额外留个心眼。

Spring 3.0 又推出了 Java 配置方式,@Configuration配合@Bean,用普通 Java 方法代替 XML 标签。这是目前 Spring Boot 项目里的绝对主流,因为它既有 XML 的“集中管理”优点,又有注解的“类型安全”优点,方法返回值类型、方法名都受编译器保护,重构时 IDE 也能帮你同步改。

三种方式不是互相取代,而是并存互补。实际一个大型项目里经常是 JavaConfig 为主、注解标业务类、个别历史模块还留着 XML。你要能同时看懂这三种写法,才算真正摸清了 Spring 的注册机制。

对比维度XML 配置注解扫描Java 配置类
出现时机Spring 1.xSpring 2.5+Spring 3.0+
配置位置独立 XML 文件类上注解配置类方法
类型安全弱,字符串引用中,类名由编译器检查强,返回值与方法签名可重构
维护成本高,文件易膨胀低,就近声明中,集中在配置类
典型场景老项目、第三方集成自有业务类第三方类、混合装配、Boot 项目

2. 方式一:XML 配置方式——老项目的基石

2.1 最基本的 bean 声明与依赖注入写法

虽然现在新项目很少从零写 XML 了,但老项目里仍到处是这种配置。你要是接手过一个 SSM 时代的系统,连<bean>都看不懂就会很被动。先看最基础的一段:

<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <bean id="userDao" class="com.example.repository.UserDao"/> <bean id="userService" class="com.example.service.UserService"> <property name="userDao" ref="userDao"/> </bean> </beans>

对应的 Java 类不需要加任何 Spring 注解:

public class UserService { private UserDao userDao; public void setUserDao(UserDao userDao) { this.userDao = userDao; } }

启动时这样加载:

ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml"); UserService userService = context.getBean("userService", UserService.class);

这里有几个关键点。id是 bean 在容器里的唯一名字,class必须写全限定名;<property>对应类的 setter 方法,name="userDao"会去找setUserDao方法,ref="userDao"表示引用容器里另一个 bean,而不是字面量字符串。如果你写的是<property name="userDao" value="userDao"/>,容器就会尝试把字符串"userDao"转成 UserDao 类型,然后直接报类型转换错误,这是新手最容易踩的坑。

如果依赖是通过构造器传入的,用<constructor-arg>:

<bean id="userService" class="com.example.service.UserService"> <constructor-arg index="0" ref="userDao"/> <constructor-arg index="1" value="testUser"/> </bean>
public class UserService { private final UserDao userDao; private String defaultName; public UserService(UserDao userDao, String defaultName) { this.userDao = userDao; this.defaultName = defaultName; } }

index从 0 开始,对应构造器参数顺序;ref依然表示“引用某个 bean”,value表示“直接给一个字面量值”。用index虽然啰嗦,但最稳定,不会因为你调整构造器参数名而挂掉。

2.2 XML 里的自动装配、生命周期回调与占位符

手动把每个<property>写全,时间长了会非常烦躁。XML 提供了简化方案,autowire属性可以放在<bean>上,让容器按类型或名字自动寻找依赖:

<bean id="userService" class="com.example.service.UserService" autowire="byType"/>

byType表示按类型匹配,容器看到UserService需要UserDao,就去容器里找一个 UserDao 类型的实例塞进去;byName会按属性名找 bean 的 id,比如属性叫userDao,就找 id 为userDao的 bean;constructor表示按构造器参数类型自动匹配。这种方式省事,但可读性差,一旦容器里同类型出现两个 bean,byType就会因为分不清该用哪个而抛异常。我的建议是:XML 里宁可写全依赖关系,也别依赖自动装配偷懒,排查问题的时候,写明白比省事儿重要得多。

XML 还能直接配置初始化和销毁方法:

<bean id="userService" class="com.example.service.UserService" init-method="init" destroy-method="destroy"/>

init-method在容器完成属性注入后调用,适合做资源打开、参数校验;destroy-method在容器关闭时调用,适合释放连接、清理线程池。对应类里写两个无参方法就行,不需要继承什么接口。

老项目里还常见属性占位符,把数据库连接这类配置从 XML 中抽到 properties 文件:

<context:property-placeholder location="classpath:jdbc.properties"/> <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> </bean>

需要额外引入context命名空间。使用${key}语法读取 properties 里的值,这才是 XML 方式真正不可替代的优势之一:通过外部配置文件就能调整环境差异,不用动 Java 代码。

2.3 XML 方式实操中的三个经典问题

第一,bean id 冲突不会在启动时立即报错的情况很少,但同 id 重复定义时会抛BeanDefinitionStoreException,遇到这种提示先全局搜一下 XML 里的重复 id。

第二,大量使用<constructor-arg index="0" ref="..."/>的代码,在 Java 8 之后其实可以不用 index,直接用参数名匹配,但前提是编译时开启-parameters参数。老项目没开的话,用名字匹配会直接报“Ambiguous constructor argument types”,排查起来很费劲。

第三,XML 配置的类如果加了@Component之类的注解,两套机制同时生效,容器里容易出现“两个同类实例”,一个 id 是 XML 里写的,一个是注解扫描自动生成的。实际项目里我见过因为这种“重复注册”导致的诡异线上问题——某处注入的是 XML 那个,另一处注入的是注解那个,两边状态不一致。所以同一个 bean 的注册方式要统一,别混着来。

3. 方式二:注解扫描方式——日常开发的主力

3.1 让容器“扫描”到你的 Bean

注解方式的核心是两个动作:在类上打上注解,再告诉容器去哪里扫描。Spring 提供了四个层级的注册注解,功能是一样的,区别只是语义不同,方便你区分分层职责:

  • @Component:通用的 Spring 组件,哪个层都能用
  • @Repository:数据访问层,Spring 会自动把一些持久化异常翻译成统一的数据访问异常
  • @Service:业务层,标注业务逻辑
  • @Controller:Web 控制层,配合 Spring MVC 处理请求

扫描配置可以写在 XML 里,也可以写在配置类上。老项目多见于 XML:

<context:component-scan base-package="com.example"/>

新项目多见于配置类:

@Configuration @ComponentScan("com.example") public class AppConfig { }

只要base-package指定的包路径覆盖了你的类,容器启动时就会扫描该包及其子包下所有带有注册注解的类,自动创建BeanDefinition。这里有个很容易忽视的细节:@Repository、@Service、@Controller元注解上都带着@Component,所以扫描器看到它们会一视同仁地处理,注册出来的 bean 默认名字是“类名首字母小写”,比如UserService对应的 bean 名是userService。

3.2 三种注入姿势:字段注入、Setter 注入和构造器注入

类被扫描进容器后,接下来就是怎么把依赖拿进来。注解方式下最常见的是@Autowired,用法有三种:

// 字段注入:最简洁,但破坏封装 @Service public class UserService { @Autowired private UserDao userDao; } // Setter 注入:可后期替换实现 @Service public class UserService { private UserDao userDao; @Autowired public void setUserDao(UserDao userDao) { this.userDao = userDao; } } // 构造器注入:Spring 官方推荐,字段可设为 final @Service public class UserService { private final UserDao userDao; @Autowired public UserService(UserDao userDao) { this.userDao = userDao; } }

Spring 4.3 以后有个简化规则:如果类只有一个构造器,那么@Autowired可以省略不写。上面这个例子如果只有一个有参构造器,去掉@Autowired也能正常工作。我实际项目里会优先用构造器注入,因为能顺手把依赖设为final,对象的依赖关系在创建那一刻就固定下来,不会出现一个 bean 在运行中途被换掉依赖的意外情况。字段注入写起来最快,但单元测试时想替换 mock 对象就得靠反射,而且容易让类被容器“绑定死”。你可以根据团队风格选,但构造器注入的“不可变性”带来的安全感,是另两种给不了的。

@Autowired的匹配规则值得背下来:先按类型找,如果找到多个,再用字段名或参数名去匹配;还是不行,就用@Qualifier("userDao")指定 bean 名。比如容器里有两个 UserDao 实现:

@Repository public class MysqlUserDao implements UserDao { } @Repository public class RedisUserDao implements UserDao { }

此时直接@Autowired private UserDao userDao;会因为没有唯一候选而启动失败,可以写成:

@Autowired @Qualifier("mysqlUserDao") private UserDao userDao;

注意@Qualifier和@Autowired配合使用,值对应 bean 名,默认就是类名首字母小写。还有一个 JSR-250 规范里的@Resource,按名字注入,@Resource(name = "redisUserDao"),老的 SSH 项目里很常见,看老代码时建议认识一下。

3.3 注解扫描最容易翻车的几个细节

细节一,扫描路径漏了。base-package范围太窄,类扫不进来,启动时直接NoSuchBeanDefinitionException。这个错误我排过很多次,往往不是代码写错,而是包结构不规整,比如把 service 放在 module 外面。排查时先看一眼组件扫描的包范围,再确认类的位置,比盯代码快多了。

细节二,@Autowired在静态字段上无效。Spring 的依赖注入发生在实例化之后,静态字段不属于任何实例,容器没法管。解决办法是给静态字段提供一个实例 setter,或者从容器里手动拿:

@Component public class SpringContextHolder implements ApplicationContextAware { private static ApplicationContext context; @Override public void setApplicationContext(ApplicationContext applicationContext) { context = applicationContext; } public static <T> T getBean(Class<T> clazz) { return context.getBean(clazz); } }

这个工具类在工具类想要 bean 的场景里非常实用,注意ApplicationContextAware是 Spring 容器回调方法,容器启动后会自动把当前上下文塞进来,既然要用容器,就得承受这个“侵入”。

细节三,构造器循环依赖。A 的构造器需要 B,B 的构造器需要 A,这种循环依赖在注解方式下会直接启动失败。为什么?因为 Spring 的“三级缓存”机制只用来解决属性注入和 Setter 注入的循环依赖,构造器阶段对象还没造出来,根本没法提前曝光一个“早期引用”,所以构造器循环依赖是死局。设计时留意一下,Service 层之间的相互引用尽量通过方法拆分避免,别相信循环依赖是能靠框架兜底的好事。

4. 方式三:Java 配置类方式——Spring Boot 的主流玩法

4.1 @Configuration + @Bean 的基本用法

Spring Boot 项目里,你打开任意一个启动类,都能看到@SpringBootApplication,这个组合注解里就包含了@Configuration和@ComponentScan。所以 Java 配置类并不是什么“进阶技巧”,而是 Spring Boot 世界里的日常基础。它解决的问题很明确:有些类不是你写的,没法在上面加@Component;有些 bean 需要复杂的初始化逻辑;有些依赖关系需要根据环境切换。这些场景用@Bean方法最合适。

@Configuration public class DataSourceConfig { @Bean public DataSource dataSource() { HikariDataSource dataSource = new HikariDataSource(); dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/demo"); dataSource.setUsername("root"); dataSource.setPassword("123456"); dataSource.setMaximumPoolSize(20); return dataSource; } }

方法名就是 bean 名,这里注册的 bean 叫dataSource,返回类型DataSource是 bean 的类型。调用方只需要按类型注入即可:

@Service public class UserService { private final DataSource dataSource; public UserService(DataSource dataSource) { this.dataSource = dataSource; } }

如果@Bean方法需要依赖其他 bean,直接把依赖写成方法参数,Spring 会自动从容器里取,这个过程叫“方法参数注入”:

@Bean public JdbcTemplate jdbcTemplate(DataSource dataSource) { return new JdbcTemplate(dataSource); }

这里完全不用再写@Autowired,Spring 看到DataSource参数,就从容器里找对应的 bean 传进来。这个方法比在类里到处写注入点更清爽,因为配置类的职责就是把“怎么生成对象”这一件事讲清楚。

4.2 一个高频面试点:@Bean 方法间调用为什么拿到的是同一个对象

新手最容易困惑的写法是这样的:

@Configuration public class AppConfig { @Bean public UserDao userDao() { return new UserDao(); } @Bean public UserService userService() { UserService service = new UserService(); service.setUserDao(userDao()); // 这里调用的是谁? return service; } }

直觉上你可能会说:这不就是调了new UserDao()吗,userService 里拿到的是新对象,userDao() 只是方法名而已。但事实上,在默认配置下,AppConfig这个配置类会被 Spring 用 CGLIB 动态代理增强,userDao()方法被重写,每次调用都会先去容器里查一下“有没有这个 bean,有就直接返回,没有才 new”。所以上面的userService()里调用userDao(),拿到的是容器中的那个单例 UserDao,而不是新对象。

这个机制靠的是@Configuration里的proxyBeanMethods属性,默认是true,也就是“代理方法调用”。如果你把配置类改成:

@Configuration(proxyBeanMethods = false) public class AppConfig { // ... }

那 Spring 会走 lite 模式,不再代理配置类,此时userService()里调用userDao()就是普通 Java 方法调用,每次 new 一个新的 UserDao。这个参数是 Spring 5.2 引入的,Spring Boot 内部大量用false来优化启动速度,因为代理配置类本身有成本。

说到这,我得提醒一句:你在排查问题的时候,如果发现“我调用了两次 @Bean 方法,怎么返回同一个对象”,别惊讶,这就是默认代理行为。想确认容器里到底是不是同一个对象,可以用==比较一下,或者看启动日志里有没有 “Spring CGLIB proxy for AppConfig” 的字样。

4.3 Java 配置方式的高级扩展:条件装配、Import 和 FactoryBean

JavaConfig 可玩的东西比前面两种多得多。比如条件装配,Spring 4 提供了@Conditional,Spring Boot 进一步封装成@ConditionalOnProperty、@ConditionalOnClass、@ConditionalOnMissingBean等,让某些 bean 按环境变量或类路径条件决定是否注册:

@Bean @ConditionalOnProperty(name = "cache.type", havingValue = "redis") public Cache redisCache() { return new RedisCache(); } @Bean @ConditionalOnProperty(name = "cache.type", havingValue = "local") public Cache localCache() { return new LocalCache(); }

这个写法在配置多环境、多实现时相当好用,切换缓存方案只需要改一行配置,不用删改代码。

@Import用来合并多个配置类,让其他配置类里的 bean 一并生效:

@Configuration @Import({DataSourceConfig.class, CacheConfig.class}) public class AppConfig { }

它还能直接导入普通类,被导入的类不需要任何注解就会注册成一个 bean,这在引第三方库、或者想绕过扫描路径限制时很管用。

再聊聊FactoryBean。它是 Spring 里一个很重要的扩展点,不是普通 bean,而是“专门生产 bean 的 bean”。实现FactoryBean<T>接口后,你通过getBean("xxx")拿到的是getObject()返回的对象,而getObjectType()声明这个对象的类型。FactoryBean适合那些构造过程特别复杂的对象,比如 Kafka 消费者工厂、MyBatis 的 SqlSessionFactory,在实际框架代码里出场率极高。面试时如果被问到 “BeanFactory 和 FactoryBean 的区别”,其实问的就是这个扩展接口,记住一个它是容器接口,一个它是 bean 工厂即可。

5. 三种方式怎么选——实战选型建议

5.1 不同场景下的注册策略

聊完三种方式本身,我把实际项目里的选择策略整理一下,方便你对号入座。

如果是在维护老项目,第一原则是“跟着现有项目走”。存量代码全是 XML,你非要在新模块里写 JavaConfig,两套体系并存会让新人崩溃。老项目里新增一个 bean,先看同类 bean 是怎么注册的,模仿着来比引入新机制更重要。这一点吃过亏的人会很有同感——架构统一性有时比“新潮写法”值钱。

如果是新项目,尤其是 Spring Boot 项目,默认就是 JavaConfig。启动类本身是配置类,业务类用@Service、@Repository注册,第三方组件、数据库连接池、缓存客户端这类无法加注解的类,用@Bean方法集中配置。注解负责“这个类是我业务的一部分,自动注册”,@Bean负责“这个类对象怎么创建,我手动控制”,两者配合起来信息量刚刚好。

如果你的团队里有那种“配置洁癖”倾向的人,JavaConfig 也很友好,可以把所有@Bean集中到一个XxxAutoConfiguration类里,模块边界一目了然。我见过不少团队把配置类按功能拆分,比如DataSourceConfig、RedisConfig、MqConfig,再通过@Import统一收口,清晰度是 XML 时代很难达到的。

5.2 面试和排查时经常牵出的衍生问题

“把一个 bean 交给容器”看着简单,背后能牵出一串问题。最常见的是问@Component和@Bean的区别,我建议你从两个角度答:作用目标不同,@Component标在类上,@Bean标在方法上;使用场景不同,@Component适合自己写的类,@Bean适合第三方类、需要定制初始化逻辑的对象。再进一步,@Component注册的 bean 名默认是类名首字母小写,@Bean注册的默认是方法名,你想自定义就用@Bean("customName")。

另一个经常被问的是“容器里同一个类型有多个 bean 怎么办”,这就是@Qualifier和@Resource的用武之地。你甚至可以注入List<UserDao>,Spring 会把容器中所有 UserDao 实例按顺序收集成一个列表,这在策略模式里非常好用:

@Service public class PriceCalculator { private final List<DiscountStrategy> strategies; public PriceCalculator(List<DiscountStrategy> strategies) { this.strategies = strategies; } }

还有一个隐藏考点是@Lazy。如果你在@Bean上加了@Lazy,bean 不会在容器启动时立刻实例化,而是第一次被使用才创建。启动慢、依赖关系重、不需要启动即用的组件,可以靠它优化启动时间。但是要注意,延迟加载会把“启动时报错”推迟到“第一次用到时报错”,排查时定位问题的时间会变长,能用尽量不用。

真正的面试重头戏还是三级缓存。Spring 解决单例 bean 循环依赖靠的是三级缓存:一级缓存存成品 bean,二级缓存存早期暴露的半成品,三级缓存存 ObjectFactory,用来生成早期引用。这套机制能解决 setter/property 注入的循环依赖,但解决不了构造器循环依赖。回答时如果能主动补一句“所以设计 Service 时要避免构造器相互引用”,基本就能把面试官的追问拦截住了。

6. 常见问题排查实录

6.1 高频异常速查表

实际项目里的报错千奇百怪,但围绕“bean 注册与注入”的异常,翻来覆去就那几种。我整理成了速查表,遇到对应异常可以照着思路查:

异常关键字典型原因排查思路
NoSuchBeanDefinitionException容器里根本没有这个类型或名字的 bean确认类有没有注册注解;确认扫描路径;确认@Bean方法所在配置类有没有被加载
NoUniqueBeanDefinitionException找到了多个同类型 bean,不知道该用哪个加@Qualifier指定 bean 名;或用@Primary标记首选;或重新梳理同类型 bean 是否重复注册
UnsatisfiedDependencyException某个依赖没能注入成功,通常是类型或构造器参数不匹配看异常堆栈里最下面那段,定位是哪个 bean 的哪个属性;检查依赖对象的注册情况
BeanCurrentlyInCreationException循环依赖,而且是构造器循环,Spring 解决不了打断循环,比如把其中一个依赖改成 setter 注入,或引入中间层解耦
BeanDefinitionStoreException配置有问题,比如 XML 里重复 id、class 找不到检查 XML 里 class 全限定名、jar 包依赖是否齐全
IllegalStateException: Failed to load ApplicationContext启动阶段直接失败,具体原因在上面异常里看 Caused by 那一串,逐层剥到根因

6.2 排查思路和一份避坑清单

排查 bean 相关的问题,我个人的固定流程是这样:先看启动日志里报错堆栈的 Caused by,定位到具体 bean;再确认这个 bean 是用哪种方式注册的——XML 就去查 bean 定义,注解就确认扫描路径覆盖没有,JavaConfig 就确认配置类有没有被@Import或@ComponentScan引入。确认注册“有没有”之后,再确认注入“对不对”,比如同名 bean 冲突、@Qualifier写错、外部属性没生效。

如果启动没报错,但运行到某个地方发现注入的 bean 是空指针,那基本是下面几种情况:类确实没被容器管理(比如你用new创建了对象,Spring 不会处理它);静态字段上写了@Autowired;注入点写在了父类、抽象类里但扫描路径没覆盖到;或者@Bean方法里自己用new返回了对象,却忘了给属性赋值。碰到这种“看不见的 null”,先确认你是不是绕过了容器自己 new 了对象,这一步能解决一半问题。

再说几个我实际踩过的坑。第一个,component-scan的 base-package 写得太宽,把org之类的大包也扫进去,启动时类路径扫描耗时暴增,甚至出现奇怪的注解生效。扫描路径应该精确到你的业务根包,别图方便写个顶级包。第二个,@Bean方法和类上的@Component同时存在,同类型注册了两个,又没加@Primary,依赖注入时就会NoUniqueBeanDefinitionException。第三个,@Autowired默认是必须要有对象,如果这个依赖是可选的,记得加required = false,否则一个缺失的依赖会直接拖垮整个启动过程。

还有个隐藏极深的坑:@Configuration类里如果方法上加了static,那是配合@Bean的“静态工厂方法”用法,它不会走 CGLIB 代理,也不会被增强,方法间调用每次都是 new。这不算 bug,但如果你期待它和普通@Bean方法行为一致,就会莫名其妙地发现“怎么拿到的是两个对象”。知道有这回事,排查时就不慌。

最后说一个我常用的排查工具:在启动类里临时写一个ApplicationRunner,把容器里所有 bean 名字打出来看一眼,比反复猜快得多。

@Component public class BeanPrinter implements ApplicationRunner { private final ApplicationContext context; public BeanPrinter(ApplicationContext context) { this.context = context; } @Override public void run(ApplicationArguments args) { String[] beanNames = context.getBeanDefinitionNames(); System.out.println("容器中 bean 总数: " + beanNames.length); for (String name : beanNames) { System.out.println(name); } } }

看到输出里有没有你期待的 bean 名,注册问题基本就能定位出七八成。这个方法是我排查了几十次 bean 异常之后总结出来的“笨办法”,但它一直最管用。

我个人的体会是,这三种方式从来不是替代关系,而是同一个问题的三个侧面。XML 让你看见最原始的装配逻辑,注解让你在日常开发里少写样板代码,JavaConfig 则在类型安全和灵活组装之间找到了平衡。真要成为能独当一面的开发者,这三种写法都得认得出、读得懂、排查得了。最后分享一个小技巧:当你怀疑某个 bean 是不是真的被容器管理时,别光盯着注解看,先把容器里所有 bean 定义打出来看一眼,往往一眼就真相大白。Spring 的思路并不复杂,复杂的是细节;把这些细节吃透,你面对的不再是一个“魔法容器”,而是一个完全可控的组件体系。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/9 6:40:38

GitHub日榜刷榜指南:从热榜雷达到开源项目选型实战

其实不必等到某个特殊节点才去刷榜单&#xff0c;我每天早上的固定动作&#xff0c;就是打开 GitHub 的热榜页面&#xff0c;把当天的日榜从头到尾翻一遍。2026-10-04 这一天也不例外。很多人把热榜日榜当作“新闻联播”看待——扫一眼标题&#xff0c;看到感兴趣的库就点个 St…

作者头像 李华
网站建设 2026/10/9 6:40:20

Java JSP洛阳旅游管理系统开发实战:数据库、Servlet与权限控制全解析

简介&#xff1a;基于 JavaJSP 的洛阳旅游管理系统是一套面向毕业设计场景的完整源码与数据库资源&#xff0c;适合计算机相关专业学生用于课程设计、毕设参考或项目二次开发。系统采用 JSPjQueryServletJDBC 的经典技术组合&#xff0c;涵盖前台门户展示与后台管理两端&#x…

作者头像 李华
网站建设 2026/10/9 6:40:18

agency-agents:多智能体协作架构的工程实践与落地指南

1. “agency-agents”不是新框架&#xff0c;而是开发者对智能体协作范式的集体命名共识最近在多个技术社区、GitHub Issues 和内部工程文档里频繁看到agency-agents这个词——它既不指向某个开源仓库的官方名称&#xff0c;也不属于任何一家大厂发布的 SDK。我翻过 Anthropic …

作者头像 李华
网站建设 2026/10/9 6:40:02

C++高性能服务器框架Address模块设计:统一IPv4/IPv6与Unix地址抽象

1. 从“连IP都写不好”到统一地址抽象先聊个真实场景。你写一个网络服务&#xff0c;监听端口用0.0.0.0:8080&#xff0c;客户端连的时候要用127.0.0.1:8080&#xff0c;到了线上又变成192.168.1.100:8080。短连接还好&#xff0c;一旦涉及IPv4、IPv6、域名解析、Unix套接字&am…

作者头像 李华
网站建设 2026/10/9 6:39:55

非下采样小波包精细滤波与包络谱分析:轴承故障诊断实战指南

做轴承故障诊断的人&#xff0c;十有八九都被“提特征”这件事折磨过。设备一旦出现早期点蚀、剥落或者轻微磨损&#xff0c;振动信号里其实不是没有故障信息&#xff0c;而是故障产生的瞬态冲击被强背景噪声盖得严严实实。常规频谱分析很难直接看出问题&#xff0c;这时候“非…

作者头像 李华
网站建设 2026/10/9 6:38:58

claude-mem:给Claude API加跨会话长期记忆的开源工具

写个给Claude加记忆的开源小工具&#xff1a;claude-mem。最近在折腾AI Agent工作流时&#xff0c;我发现一个很绕不过去的痛点&#xff1a;Claude每次对话都是“无状态”的&#xff0c;它不记得你上次说过什么。你告诉过它的偏好、项目背景、代码规范&#xff0c;换个会话就全…

作者头像 李华