@Configuration的CGLIB代理:Full模式vs Lite模式的本质区别
@Configuration的CGLIB代理:Full模式vs Lite模式的本质区别
适读人群:Spring Boot中级开发者,希望彻底搞清楚@Configuration原理的读者 | 阅读时长:约16分钟
开篇故事
有一次代码审查,我看到同事写了这样的代码:
@Configuration
public class AppConfig {
@Bean
public ServiceA serviceA() {
return new ServiceA(dataSource()); // 直接调用dataSource()方法
}
@Bean
public ServiceB serviceB() {
return new ServiceB(dataSource()); // 又调用一次dataSource()
}
@Bean
public DataSource dataSource() {
HikariDataSource ds = new HikariDataSource();
ds.setJdbcUrl("jdbc:mysql://...");
return ds;
}
}我问他:这里dataSource()被调用了两次,会创建两个连接池吗?
他想了想,不确定地说:应该不会吧?Spring会缓存?
他说对了结果,但原因说不清楚。而且更危险的是,如果某天有人把@Configuration改成了@Component,结果就会变——真的会创建两个连接池。
这就是Full模式和Lite模式的区别,也是@ConfigurationCGLIB代理的核心价值所在。今天我们把这个机制从源码层面讲清楚。
一、问题背景
1.1 Full模式 vs Lite模式
Spring中有两种方式定义@Bean方法所在的类:
Full模式(@Configuration(proxyBeanMethods = true),默认):
- 类会被CGLIB代理
@Bean方法被拦截,每次调用都会先去容器查,有则返回缓存的Bean- 保证同一个
@Bean方法被直接调用多次,返回的是同一个Bean实例
Lite模式(@Configuration(proxyBeanMethods = false)或用@Component/@ComponentScan等):
- 类不被CGLIB代理
@Bean方法就是普通方法,每次调用都会执行方法体,创建新对象- 启动更快(没有CGLIB代理创建开销),适合不需要方法间相互引用的场景
1.2 什么时候选哪种模式
需要在@Bean方法内直接调用另一个@Bean方法 → Full模式(proxyBeanMethods=true,默认)
@Bean方法之间没有直接调用关系,全部通过参数注入 → Lite模式(proxyBeanMethods=false)Spring Boot 2.x之后大量内置@Configuration都改用了Lite模式,因为启动性能更好。
二、源码核心路径解析
2.1 CGLIB代理的创建时机
关键类:org.springframework.context.annotation.ConfigurationClassEnhancer
// ConfigurationClassPostProcessor.java 第478行(简化)
public void enhanceConfigurationClasses(ConfigurableListableBeanFactory beanFactory) {
ConfigurationClassEnhancer enhancer = new ConfigurationClassEnhancer();
// ...
for (String beanName : beanFactory.getBeanDefinitionNames()) {
BeanDefinition beanDef = beanFactory.getBeanDefinition(beanName);
// 判断是否是Full模式的@Configuration
if (ConfigurationClassUtils.isFullConfigurationClass(beanDef)) {
// 用CGLIB增强,返回代理子类的Class对象
Class<?> enhancedClass = enhancer.enhance(configClass, this.beanClassLoader);
if (configClass != enhancedClass) {
// 把BeanDefinition的beanClass替换为代理类
beanDef.setBeanClass(enhancedClass);
}
}
}
}2.2 isFullConfigurationClass判断逻辑
// ConfigurationClassUtils.java 第246行
public static boolean isFullConfigurationClass(BeanDefinition beanDef) {
return FULL_CONFIGURATION_CLASS_ATTRIBUTE.equals(
beanDef.getAttribute(CONFIGURATION_CLASS_ATTRIBUTE));
}这个属性是在ConfigurationClassParser解析时设置的:
// ConfigurationClassUtils.java(简化)
static boolean isConfigurationCandidate(AnnotationMetadata metadata) {
// Full模式:有@Configuration且proxyBeanMethods=true
Map<String, Object> config = metadata.getAnnotationAttributes(Configuration.class.getName());
if (config != null && !Boolean.FALSE.equals(config.get("proxyBeanMethods"))) {
// 标记为FULL
beanDef.setAttribute(CONFIGURATION_CLASS_ATTRIBUTE, FULL);
return true;
}
// Lite模式:有@Bean、@Component、@ComponentScan、@Import、@ImportResource
if (metadata.isAnnotated(Component.class.getName()) ||
metadata.hasAnnotatedMethods(Bean.class.getName()) || ...) {
beanDef.setAttribute(CONFIGURATION_CLASS_ATTRIBUTE, LITE);
return true;
}
return false;
}2.3 CGLIB拦截器:BeanMethodInterceptor
这是Full模式的核心:每次调用@Bean方法时,CGLIB拦截器会介入:
// ConfigurationClassEnhancer.java 内部类 BeanMethodInterceptor
private static class BeanMethodInterceptor implements MethodInterceptor, ConditionalCallback {
@Override
public Object intercept(Object enhancedConfigInstance, Method beanMethod,
Object[] beanMethodArgs, MethodProxy cglibMethodProxy) throws Throwable {
// 获取BeanFactory(从代理实例中取,通过$$beanFactory字段)
ConfigurableBeanFactory beanFactory = getBeanFactory(enhancedConfigInstance);
// 获取Bean名称
String beanName = BeanAnnotationHelper.determineBeanNameFor(beanMethod);
// 处理@Scope(scopedProxy=INTERFACES/TARGET_CLASS)的特殊情况
if (BeanAnnotationHelper.isScopedProxy(beanMethod)) {
// ...
}
// 核心判断:当前是否已经在创建这个Bean的过程中?
// 如果不是(说明是被外部直接调用),就从容器获取
if (isCurrentlyInvokedFactoryMethod(beanMethod)) {
// 当前就是在创建这个Bean,正常执行原方法
return cglibMethodProxy.invokeSuper(enhancedConfigInstance, beanMethodArgs);
}
// 否则,从BeanFactory获取(保证单例)
return resolveBeanReference(beanMethod, beanMethodArgs, beanFactory, beanName);
}
private Object resolveBeanReference(Method beanMethod, Object[] beanMethodArgs,
ConfigurableBeanFactory beanFactory, String beanName) {
// ... 处理当前是否在创建中(循环依赖)
boolean alreadyInCreation = beanFactory.isCurrentlyInCreation(beanName);
try {
if (alreadyInCreation) {
beanFactory.setCurrentlyInCreation(beanName, false);
}
// 关键:从容器getBean,而不是直接执行方法
Object beanInstance = (beanMethodArgs.length == 0 ?
beanFactory.getBean(beanName) :
beanFactory.getBean(beanName, beanMethodArgs));
// ...
return beanInstance;
}
finally {
if (alreadyInCreation) {
beanFactory.setCurrentlyInCreation(beanName, true);
}
}
}
}这就是魔法所在:Full模式下,dataSource()被调用时,CGLIB拦截器会先调beanFactory.getBean("dataSource"),如果已经创建好了就直接返回,保证单例。
2.4 完整调用链对比
三、完整代码示例
3.1 验证Full模式和Lite模式的行为差异
// Full模式配置
@Configuration // proxyBeanMethods=true(默认)
public class FullModeConfig {
@Bean
public DataSource dataSource() {
HikariDataSource ds = new HikariDataSource();
ds.setJdbcUrl("jdbc:h2:mem:test");
ds.setMaximumPoolSize(10);
System.out.println("Creating DataSource: " + System.identityHashCode(ds));
return ds;
}
@Bean
public JdbcTemplate jdbcTemplate() {
// Full模式:dataSource()会被拦截,返回容器中的单例
return new JdbcTemplate(dataSource());
}
@Bean
public NamedParameterJdbcTemplate namedJdbcTemplate() {
// 同一个DataSource实例!
return new NamedParameterJdbcTemplate(dataSource());
}
}
// Lite模式配置
@Configuration(proxyBeanMethods = false) // Lite模式
public class LiteModeConfig {
@Bean
public DataSource dataSource() {
HikariDataSource ds = new HikariDataSource();
ds.setJdbcUrl("jdbc:h2:mem:test");
System.out.println("Creating DataSource: " + System.identityHashCode(ds));
return ds;
}
// Lite模式正确写法:通过参数注入,而不是直接调用方法
@Bean
public JdbcTemplate jdbcTemplate(DataSource dataSource) { // 通过参数注入!
return new JdbcTemplate(dataSource);
}
@Bean
public NamedParameterJdbcTemplate namedJdbcTemplate(DataSource dataSource) {
return new NamedParameterJdbcTemplate(dataSource);
}
}3.2 验证CGLIB代理是否生效
@SpringBootApplication
public class Demo {
public static void main(String[] args) {
ApplicationContext ctx = SpringApplication.run(Demo.class, args);
// Full模式下,@Configuration类本身是CGLIB代理
FullModeConfig fullConfig = ctx.getBean(FullModeConfig.class);
System.out.println("Full模式类名: " + fullConfig.getClass().getName());
// 输出类似: com.example.FullModeConfig$$SpringCGLIB$$0
System.out.println("是否CGLIB代理: " + fullConfig.getClass().getName().contains("CGLIB"));
// Lite模式下,是原始类
LiteModeConfig liteConfig = ctx.getBean(LiteModeConfig.class);
System.out.println("Lite模式类名: " + liteConfig.getClass().getName());
// 输出: com.example.LiteModeConfig(原始类)
// 验证DataSource是否是同一个对象
DataSource ds1 = ctx.getBean("dataSource", DataSource.class);
DataSource ds2 = ctx.getBean("dataSource", DataSource.class);
System.out.println("同一个DataSource: " + (ds1 == ds2)); // true(单例)
}
}3.3 生产级别的Lite模式最佳实践
// 推荐:大型项目中使用Lite模式 + 参数注入,更快更清晰
@Configuration(proxyBeanMethods = false)
public class DatabaseConfig {
// 通过@Value注入,不依赖方法调用
@Value("${spring.datasource.url}")
private String url;
@Value("${spring.datasource.username}")
private String username;
@Value("${spring.datasource.password}")
private String password;
@Bean
@Primary
public DataSource primaryDataSource() {
HikariConfig config = new HikariConfig();
config.setJdbcUrl(url);
config.setUsername(username);
config.setPassword(password);
config.setMaximumPoolSize(20);
config.setMinimumIdle(5);
return new HikariDataSource(config);
}
// 参数注入,明确依赖关系,Lite模式下安全
@Bean
public JdbcTemplate jdbcTemplate(DataSource primaryDataSource) {
return new JdbcTemplate(primaryDataSource);
}
@Bean
public PlatformTransactionManager transactionManager(DataSource primaryDataSource) {
return new DataSourceTransactionManager(primaryDataSource);
}
}四、踩坑实录
坑1:把@Configuration改成@Component后连接池翻倍
这就是开头的场景。@Component注解的类是Lite模式,dataSource()每次调用都是new,两个Bean依赖同一个dataSource()方法,就会创建两个HikariDataSource实例,占用两倍连接数。在生产环境数据库连接池耗尽的问题可能就是这么来的。
排查方法:在DataSource的构造函数打断点,看被调用了几次。或者打印System.identityHashCode(dataSource()),Full模式下调两次hashCode相同,Lite模式下不同。
坑2:Lite模式下@Bean方法private导致报错
@Configuration(proxyBeanMethods = false)
public class Config {
@Bean
private DataSource dataSource() { // 编译不报错,运行报错!
return new HikariDataSource();
}
}Spring 6.x会在启动时检测并报IllegalStateException: @Bean method must not be private。Full模式下也一样。@Bean方法必须是public或protected(允许被CGLIB子类重写)。
坑3:proxyBeanMethods=false时@Bean方法是final导致代理失败
Full模式(默认)下,如果@Bean方法是final的,CGLIB无法重写,会报错:
@Configuration // Full模式
public class Config {
@Bean
public final DataSource dataSource() { // 错误!CGLIB无法代理final方法
return new HikariDataSource();
}
}报错:Unable to proxy interface-implementing method ... as it is marked as final。
解决:要么去掉final,要么改用proxyBeanMethods = false(Lite模式不需要代理方法)。
坑4:CGLIB代理导致类不能是final
Full模式下,@Configuration类本身不能是final的,因为CGLIB需要创建子类。
@Configuration
public final class Config { // 错误!CGLIB需要继承这个类
// ...
}Kotlin类默认是final的,所以Kotlin写Spring配置类时要加open关键字,或者使用kotlin-spring插件(它会自动把需要代理的类变为open):
// Kotlin需要显式open(或用kotlin-spring插件)
@Configuration
open class KotlinConfig {
@Bean
open fun dataSource(): DataSource = HikariDataSource()
}五、总结与延伸
Full模式和Lite模式的核心区别:
| 特性 | Full模式 | Lite模式 |
|---|---|---|
| proxyBeanMethods | true(默认) | false |
| CGLIB代理 | 有 | 无 |
| @Bean方法间调用 | 保证单例 | 每次新建 |
| 启动性能 | 稍慢 | 更快 |
| 适用场景 | @Bean方法间有直接调用 | @Bean间无直接调用,用参数注入 |
Spring Boot的@SpringBootApplication继承了@SpringBootConfiguration,它是Full模式的。但Spring Boot的大量内置@AutoConfiguration都用了proxyBeanMethods = false来提升启动性能。
如果你的项目有大量@Configuration类,统一改为Lite模式(前提是@Bean方法间无直接调用),可以明显改善启动时间。
下一篇聊@Conditional条件装配,Spring Boot自动配置的核心机制。
