Spring从3.0开始引入基于Java的配置方式,到了Spring Boot时代,Java配置已经成为绝对主流。相比传统的XML配置,Java配置类型安全、支持编译期检查、重构友好,还能直接在配置代码里写业务逻辑。不过不少人在使用时会把@Configuration和@Component混为一谈,或者遇到@Bean方法调用不生效、配置类被多次实例化等问题。这篇文章把Java配置的核心用法和底层机制讲清楚,并整理一份实用的避坑清单。

一、@Configuration的核心机制:CGLIB代理
要理解Java配置,必须先理解@Configuration注解的本质。Spring在处理被@Configuration标注的类时,会通过CGLIB生成该类的一个代理子类,这个代理会拦截所有被@Bean标注的方法。拦截的目的只有一个:保证同一个Bean在容器中只被创建一次,也就是实现单例语义。
举个例子,假设配置类中有两个@Bean方法,第二个方法内部直接调用了第一个方法。如果没有代理机制,第二次调用会真的执行方法体,创建出一个新的对象,破坏单例。有了代理之后,容器会先检查该方法对应的Bean是否已存在于容器中,存在就直接返回已有实例,而不是重复执行方法体。这就是@Bean方法之间可以直接互相调用的原因。
@Configuration
public class AppConfig {
@Bean
public DataSource dataSource() {
// 直接 new 一个数据源
HikariDataSource ds = new HikariDataSource();
ds.setJdbcUrl("jdbc:mysql://127.0.0.1:3306/demo");
ds.setUsername("root");
return ds;
}
@Bean
public JdbcTemplate jdbcTemplate() {
// 这里直接调用 dataSource() 方法
// 由于 CGLIB 代理的存在,拿到的不是新对象,而是容器中已存在的那个 Bean
return new JdbcTemplate(dataSource());
}
}
需要特别注意的是,@Configuration默认的代理模式是proxyBeanMethods = true。如果把它显式设为false,Spring就不会生成代理,此时@Bean方法之间的调用会变成普通Java方法调用,每次调用都产生新对象。Spring Boot 2.2之后很多自动配置类都加上了这个属性来提升启动速度,因为跳过代理可以减少启动时的字节码生成开销,代价是不能再依赖方法间的内部调用。
二、@Bean与@Component的区别及组件扫描配置
注册Bean主要有两条路:一是给类加上@Component、@Service等构造型注解,再配合组件扫描自动发现;二是针对第三方库的类,因为无法修改源码加注解,只能通过@Bean方法手工声明。两者最终都会注册到容器中,但适用场景完全不同。
组件扫描通过@ComponentScan配置,默认扫描配置类所在包及其子包。如果项目分层清晰,把配置类放在根包下,通常不需要额外指定。但如果第三方类分散在多个包里,就需要显式指定扫描路径,例如:
@Configuration
@ComponentScan(
basePackages = {"com.example.service", "com.example.dao"},
excludeFilters = @ComponentScan.Filter(
type = FilterType.REGEX,
pattern = "com\\.example\\.dao\\.legacy\\..*"
)
)
public class RootConfig {
// 排除了 legacy 包下所有遗留 DAO,避免被误注册
}
这里有个容易忽略的细节:扫描范围设得过大(比如直接扫到第三方框架的包)会把不相关的类注册进容器,轻则启动变慢,重则出现意料之外的Bean。建议扫描范围尽量精确到业务包,而不是图省事用通配符覆盖大范围包路径。
另外,@Bean方法上还可以配合@Profile、@Conditional、@Lazy等注解实现条件化装配。比如开发环境用内存数据库,生产环境用MySQL,就可以用两个带不同Profile的@Bean方法来切换,代码集中一处,比XML中维护多份配置文件清晰得多。
三、多配置文件的组合与依赖注入
实际项目的配置往往不止一个类,Spring提供了几种组合方式。@Import用于引入其他配置类,@ImportResource用于兼容旧的XML配置,@PropertySource用于加载属性文件。配置类之间还可以通过构造器注入互相引用,形成清晰的依赖关系。
@Configuration
@PropertySource("classpath:app.properties")
@Import({DataSourceConfig.class, WebConfig.class})
public class RootConfig2 {
@Bean
public OrderService orderService(DataSource dataSource) {
// 不在方法里调用其他 @Bean 方法,而是声明参数让容器注入
// 这种写法在 proxyBeanMethods = false 时也能正常工作
return new OrderService(dataSource);
}
}
这里展示的写法值得推荐:不在方法体内调用其他Bean方法,而是把依赖声明为方法参数,由容器负责注入。这种写法不依赖CGLIB代理,即使关闭了代理也能正确工作,而且让依赖关系显式化,便于阅读和测试。
在迁移存量项目时,Java配置和XML配置可以共存一段时间,通过@ImportResource把旧的XML引入进来,逐步把XML中的Bean定义翻译成Java代码,最终完全移除XML文件。这种渐进式迁移的方式风险最低,不必一次性重写全部配置。
四、常见坑点与注意事项
坑一:漏写@Configuration导致行为异常。如果只写了@Bean方法但类上没有@Configuration注解,Spring会以lite模式处理它,此时@Bean方法之间的调用不会被代理,每次调用都会新建对象。排查这类问题的关键现象是:单例Bean在某些地方变成了多例。
坑二:代理类限制。CGLIB通过继承生成代理子类,因此@Configuration类不能是final的,@Bean方法也不能是final或private的,否则启动时会直接报错或代理失效。这也是为什么配置类的@Bean方法永远应该是public的。
坑三:Bean名称冲突。@Bean默认以方法名作为Bean名称,两个配置类中如果存在同名方法,启动时会抛出BeanDefinitionOverrideException(在默认禁止覆盖的版本中)。解决方法是通过@Bean("自定义名称")显式指定不同的名称。
坑四:不要在配置类里写复杂逻辑。配置类只负责装配,不要塞业务代码进去。配置类在启动早期就会被容器解析处理,逻辑复杂会拖慢启动速度,也让问题排查变得困难。
五、Java配置与XML配置怎么选
新项目毫无疑问应该首选Java配置:类型安全、IDE支持好、重构时可以全局追踪引用,配合Spring Boot的自动配置体系几乎是标准做法。XML配置的优势在于配置与代码分离,某些运维场景下改配置不用重新编译,但在持续交付流程中这个优势已经不明显。
简单总结选型原则:新代码一律用Java配置;维护老项目时允许XML与Java共存,通过@ImportResource桥接,但不建议再在XML里新增Bean定义。无论选择哪种方式,保持团队内部风格统一比选哪种更重要,混用两种风格又没有清晰的边界划分,才是配置混乱的根源。
Spring Java配置@Configuration@Bean修改时间:2026-09-05 22:50:55