最近群里有人发了个 DeepSeek 的回答截图问的正是这句话Component 和 Configuration 都是 Spring 中重要的注解但它们的用途和特性有所不同。这个问题几乎每轮 Java 面试都会被翻出来但很多人给出的答案就一句“Configuration 是 Component 的衍生注解”完全没说到点子上。我翻了源码跑了几个验证用例把这两个注解从组件扫描到代理生成的过程完整走了一遍今天整理一份可以直接拿来当面试底稿、也能指导实际项目避坑的拆解笔记。先放结论Configuration 确实“是一种”Component但 Spring 对它的处理要比普通组件重得多。默认情况下Configuration 会被 CGLIB 代理增强从而保证类内 Bean 方法之间的调用返回的是容器中的同一实例而 Component 里的 Bean 方法不会做这种拦截。这背后涉及 Full 模式、Lite 模式、ConfigurationClassPostProcessor 等一系列机制。看完这篇文章你不仅能答清楚面试题还能解释你项目里那个 Bean 为什么被创建了好几次。1. 先分清职责这两个注解分别是干什么的1.1 ComponentSpring 容器里的“基础居民”Component 说白了就是“我是一个可以被 Spring 容器管理的类”的标记。Spring 启动时组件扫描器会扫描指定包下带有 Component 的类把它们实例化、建立依赖关系、放进容器。它的衍生注解 Service、Repository、Controller 只是在不同分层语义上换了个名字底层处理方式完全一样所以你可以把这三个注解都理解为 Component 的“马甲”。平时写业务代码Controller、Service、Mapper 基本都是这么被注册进去的。Component 本身没有任何“工厂”含义它就是你业务代码里的一个 Bean。即便在里面写了 Bean 方法Spring 也只会把它当 Lite 模式处理不会像真正的配置类那样做特殊增强。这一点很多人一开始没概念后面我会重点讲。1.2 Configuration不只是普通 Bean更是 Bean 的“工厂”Configuration 同样带有 Component 元注解所以它也能被扫描到容器里也有一条对应的 BeanDefinition。但 Spring 给它的定位不是“业务对象”而是“Bean 定义工厂”。在这个类里写的 Bean 方法会被 Spring 解析成 BeanDefinition用于向容器注册其他 Bean。关键点来了如果这个类被声明为 Full 模式也就是默认的 ConfigurationSpring 会在运行时用 CGLIB 生成一个代理类来替代原始对象。代理会拦截所有 Bean 方法调用从而保证你在这个类里通过方法调用拿到的对象和容器里那个 Bean 是同一个。这种设计对“Bean 依赖另一个 Bean”的配置场景非常重要。1.3 第一层关系Configuration 本身就是 Component这点很多人没意识到。打开 Configuration 的源码注解定义上清清楚楚写着 Component。也就是说Spring 的自动扫描逻辑在没有排除规则的情况下能把 Configuration 当成普通组件扫进去。区别在于扫描之后会有额外的处理流程ConfigurationClassPostProcessor 会识别出这是一个配置类并对它做进一步解析和增强。所以你可以这样理解Component 是父集Configuration 是子集但子集额外获得了“工厂方法增强”的能力。面试时如果只说“Configuration 是 Component 的衍生注解”不算错但只答了对一半。真正值钱的另一半是 Full 模式和 Lite 模式的区别。2. 关键差异同一个 Bean 方法为什么一个单例一个走方法体2.1 先写两段代码复现差异空说无用直接上测试。先建两个类一个用 Configuration一个用 Component内部结构完全一样。public class User { private Long id; private String name; // 省略 getter/setter/toString }Service public class UserService { private final User user; public UserService(User user) { this.user user; } public User getUser() { return user; } }配置类版本Configuration public class ConfigDemo { Bean public User user() { return new User(); } Bean public UserService userService() { User u1 user(); User u2 user(); System.out.println(userService内部两次调用user(): (u1 u2)); return new UserService(u1); } }普通组件版本Component public class ComponentDemo { Bean public User user() { return new User(); } Bean public UserService userService() { User u1 user(); User u2 user(); System.out.println(userService内部两次调用user(): (u1 u2)); return new UserService(u1); } }再写一个应用启动类分别把其中一个配置类启用另一个注释掉观察输出。SpringBootApplication public class DemoApplication implements ApplicationRunner { Autowired private ApplicationContext context; Override public void run(ApplicationArguments args) { UserService userService context.getBean(UserService.class); User user context.getBean(User.class); System.out.println(容器中的user与userService持有的user是否相同: (userService.getUser() user)); } }2.2 输出结果Configuration 下实例相同Component 下实例不同跑完两轮结果非常直观用 Configuration 时控制台输出true和true意思是 userService 内部两次调用 user() 拿到的对象一样容器中的 user 也和 userService 持有的 user 是同一个。换成 Component 后控制台输出false和false意思是两次调用 user() 各 new 了一个对象容器中的 user 和 userService 内部持有的 user 也不是同一个。这就是面试里最常考的点。很多人以为只要方法上标了 BeanSpring 就会保证单例。实际上单例能不能保证取决于宿主类是不是 Configuration以及 proxyBeanMethods 开没开。2.3 为什么会有这种差异Full 模式与 Lite 模式Spring 源码把配置类分成两类Full 和 Lite。Full 模式指类上标注了 Configuration且 proxyBeanMethods 属性不显式为 false。这种配置类会被 CGLIB 增强Bean 方法之间互相调用时代理会先去容器里找有没有同名 Bean有就返回已有的没有才真正执行方法创建新对象并注册。Lite 模式指 Configuration(proxyBeanMethodsfalse)、普通 Component/Service 类里的 Bean 方法甚至一些只标了 ComponentScan、Import 的类。这些情况不会被 CGLIB 增强Bean 方法就是普通 Java 方法每次调用都重新执行方法体所以会出现多个实例。这个术语在 Spring 源码里出现得很频繁理解后你会发现网上那些“为什么我的 Bean 方法执行了多次”的问题基本一眼就能定位。3. 源码级拆解ConfigurationClassPostProcessor 和代理增强3.1 配置类是如何被识别和注册的Spring 容器 refresh 时会走到 invokeBeanFactoryPostProcessors 这步而 ConfigurationClassPostProcessor 是内置的 BeanDefinitionRegistryPostProcessor专门负责处理配置类。它的工作分为几步。第一步遍历容器里当前所有候选的 BeanDefinition调用 ConfigurationClassUtils.checkConfigurationClassCandidate 判断哪些类算配置类。判断逻辑大致是这样如果类上有 Configuration 且 proxyBeanMethods 不为 false标记为 FULL如果类上有 Component、ComponentScan、Import、ImportResource 或者存在 Bean 方法标记为 LITE。第二步把识别出来的配置类交给 ConfigurationClassParser 解析搜集所有 Bean 方法、Import 的类、ComponentScan 的包信息。第三步使用 ConfigurationClassBeanDefinitionReader 把 Bean 方法转换成 BeanDefinition 注册进容器。第四步如果是 Full 模式通过 ConfigurationClassEnhancer 生成 CGLIB 代理类并把配置类的 BeanDefinition 替换成代理类的定义。如果你以后想手写一个精简版 Spring这个流程几乎是必抄作业。核心思路就是“先收集定义再增强实例”。3.2 CGLIB 代理BeanMethodInterceptor 的关键逻辑ConfigurationClassEnhancer 生成的代理类里有一个 CallbackFilter只对标了 Bean 的方法做拦截其他普通方法直接透传避免不必要的开销。拦截器内部逻辑可以简化成下面几步根据当前方法上的 Bean 注解解析出 beanName默认是方法名。判断 BeanFactory 里是否已经存在这个 Bean。如果已经存在直接返回 getBean(beanName) 的结果。如果还没创建调用 cglibMethodProxy.invokeSuper 去执行配置类原始方法把返回值注册进容器再返回。给个伪代码感受一下Object intercept(Object configInstance, Method beanMethod, MethodProxy methodProxy) { String beanName resolveBeanName(beanMethod); if (beanFactory.containsBean(beanName)) { return beanFactory.getBean(beanName); } Object bean methodProxy.invokeSuper(configInstance, args); beanFactory.registerSingleton(beanName, bean); return bean; }这个设计非常巧妙。它让配置类从“一份 Bean 定义说明书”变成了“一个有状态的工厂代理”。方法调用变成了容器的查询动作天然保证同一个 Bean 在容器内只创建一次。而 Lite 模式没有这一步所以才会出现每次调用都 new 的问题。3.3 proxyBeanMethodsfalse 时发生了什么如果给 Configuration 加上proxyBeanMethods falseSpring 就不会再生成 CGLIB 代理类。此时配置类退化为 Lite 模式Bean 方法之间的直接调用不再有“单例保证”。代价也很清楚如果方法里写了userService() { return new UserService(user()); }每次调用 userService() 都会触发一次 user() 的普通方法执行user 对象可能被多次创建。如果这些对象没有被容器注册就会出现“容器里有 User但 UserService 里注入的是另一个 User”这种诡异情况。为什么 Spring Boot 里很多自动配置类都用了proxyBeanMethods false因为自动配置类的 Bean 一般是通过方法参数从容器里拿依赖而不是通过直接调用另一个 Bean 方法完成串联。既然没有内部方法调用需求何必为了一个用不到的代理能力付出启动时的 CGLIB 生成和拦截开销。这个开关本质上是一种性能换语义的选择。4. 实际选型什么场景用 Configuration什么场景用 Component4.1 最常见的配置类用法Configuration 适合定义跨模块的 Bean比如数据库连接池、RedisTemplate、消息队列客户端、第三方 SDK 等。尤其是 Bean 之间存在依赖关系且依赖的一方需要容器中的单例时用默认的 Configuration 最安全。举个真实例子创建 JdbcTemplate 时依赖 DataSourceConfiguration public class DataSourceConfig { Bean public DataSource dataSource() { HikariDataSource ds new HikariDataSource(); ds.setJdbcUrl(jdbc:mysql://localhost:3306/demo); ds.setUsername(root); ds.setPassword(123456); return ds; } Bean public JdbcTemplate jdbcTemplate() { return new JdbcTemplate(dataSource()); } }在 Full 模式下dataSource()这个方法调用会被代理拦截返回的是容器里那一个连接池。如果你把这个类改成 ComponentJdbcTemplate 里拿到的就是另一个新创建的 DataSource连接池白配了性能还可能出现连接泄漏。这种问题很难肉眼发现只有压力测试时才会暴露。4.2 普通组件里放 Bean 要格外小心有人图省事直接把 Bean 方法写在 Service 里。Spring 确实允许容器也能识别并注册这个 Bean但后果就是前面说的 Lite 模式陷阱。之前排查过一个案例某个模块的 Service 里用 Bean 生成了一个 RestTemplate又在另一个 Bean 方法里手动调用了这个 RestTemplate 方法结果每个请求都 new 一个 RestTemplate底层连接池完全失效。表面看 runner 里日志正常实际内存里对象地址全不一样。如果你的代码里确实有“普通组件 Bean”的情况最好把 Bean 方法挪到专门的 Configuration 类中。容器能够正常注册业务类也能通过 Autowired 拿到同一个实例不会绕弯。这条规矩看似简单但能省掉大量隐性问题。4.3 Configuration 与 Autowired、Import 的配合Configuration 类里也可以写 Autowired 字段注入其他 Bean。虽然现在推荐构造器注入但在配置类中通过方法参数注入会更常见Configuration public class AppConfig { Bean public JdbcTemplate jdbcTemplate(DataSource dataSource) { return new JdbcTemplate(dataSource); } }注意这里 JdbcTemplate 的 DataSource 是作为方法参数传入的不是方法内部调用 dataSource()。只要参数名或者类型能在容器中找到对应 BeanSpring 就会在创建 JdbcTemplate 前先把 DataSource 解析好。这种方法天然规避了 Lite 模式下 Bean 方法间调用失效的问题。Import 则可以把多个配置类组合起来很适合模块化拆分。比如一个项目里有多个独立模块每个模块有自己的 Configuration主配置类用 Import 引入它们启动时就能一次性加载。这种组装方式经常出现在中间件封装、通用基础组件设计里。5. 常见问题与排查实录5.1 为什么 Bean 方法在普通组件里被调用了两次出现“Bean 方法执行了多次”的问题时第一反应不是怀疑 JVM而是看宿主类是不是 Configuration以及有没有把 proxyBeanMethods 设为 false。如果类是 Component/Service那必然每次方法调用都会重新执行。排查方式很简单在 Bean 方法第一行打日志数一下调用次数然后对比容器里 Bean 的对象地址。还有一类情况是配置类被手动new出来了。比如有人走了个后门在业务代码里写new DataSourceConfig().dataSource()那代理完全不参与结果可想而知。记住Spring 里的配置类实例应该由容器创建不要自己 new。5.2 代理失效final、私有方法、静态方法Spring 用 CGLIB 生成配置类的子类所以 final 类没办法被代理final 方法也不能被重写拦截。私有方法天然不行Bean 方法也不能声明成 private否则启动时会直接报错。另一个容易踩的点是 static Bean 方法。static 方法的调用不依赖对象实例Spring 处理路径也不太一样默认 Full 模式下的代理拦截对 static 方法并不生效。如果配置类里混用了 static Bean 方法和实例 Bean 方法要特别注意内部互相调用时的语义差异。如果你发现自己在配置类里通过this调用 Bean 方法却拿不到容器中的同一个 Bean先检查是不是处于 Lite 模式或者配置类被什么方式破坏了继承链。多数情况不是你写错了而是模式不对。5.3 高频面试题速答模板面试官如果问“Configuration 和 Component 有什么区别”你可以按这个模板来回答既有层次又不啰嗦Configuration 是 Spring 的配置类注解内部有 Component 元注解所以天然也是容器 Bean。默认 Full 模式下Configuration 会被 CGLIB 增强Bean 方法之间互相调用时会查询容器并返回已存在的单例对象。Component 是普通组件注解如果它里面写了 Bean 方法属于 Lite 模式没有代理方法调用会直接执行方法体多次调用可能产生多个实例。如果配置类不需要内部方法间调用可以把 proxyBeanMethods 设为 false减少 CGLIB 代理的启动开销。讨论“静态 Bean 方法”和“final 类”的时候可以适当延伸表示你对底层代理机制有理解。这套回答能覆盖原理、表现、应用场景和性能优化四个维度基本够用了。6. 从一次线上事故看这两个注解的实际分量去年排查过一个老项目OOM 前的报警日志里发现某个队列监听器持有的对象一直在增长。翻代码时发现那个监听器在一个 Component 里定义了一个 Bean 方法然后另一个普通方法频繁调用它。因为 Lite 模式不拦截每次调用都会 new 一个对象监听器持有的引用也一直在变最终导致 GC 无法回收。后来把 Bean 方法挪到了 Configuration 类中让调用方通过 Autowired 注入问题立刻消失。整个过程算不上高深但如果一开始就理解 Component 和 Configuration 的这个差异完全不用等到线上崩溃才查出来。我个人在实际项目中一直保留一条规矩凡是 Bean 方法宿主类必须是 Configuration且除非确认配置类内部不存在 Bean 方法互相调用否则不要轻易动 proxyBeanMethods。这条规矩看起来保守但真的能帮你过滤掉一大批莫名其妙的“重复创建”和“单例失效”问题。面试复盘时也不要只背结论自己动手写两个类跑一下比看十篇博客都管用。