
1. 先搞清楚你到底在“交”什么做Spring开发这么多年我带过不少新人发现很多人写着Service、Component其实根本没想明白一个事——把一个bean对象交给Spring容器管理这句话到底是什么意思说白了Spring容器就是个大管家。你把自己的对象交给他他就帮你管着什么时候创建、什么时候初始化、什么时候销毁、谁依赖谁、谁先谁后。你只管在要用的时候伸手要不用自己new了。这背后就是Spring最核心的IoC控制反转思想对象不是你自己new出来的而是容器给你的。那问题来了你手上的对象怎么才能让这个大管家接管这也是Spring面试里几乎必问的基础题——“有哪几种方式可以把bean交给Spring容器”我统计算下来绝大部分人只能答出来一个Component注解稍微好点的能说出XML配置至于第三种、第四种基本就卡壳了。这篇文章我就按自己实际开发中的经验把“把bean交给Spring容器”这件事掰开揉碎了讲清楚。我整理了三种最主流的注册方式XML配置、注解扫描、Java配置类。这三种不是相互替代的关系而是对应着不同的发展阶段和不同的使用场景。你把这三种吃透了Spring的IoC容器对你来说就不再是黑盒后面再看什么BeanFactoryPostProcessor、BeanPostProcessor、循环依赖三级缓存都会顺很多。这篇文章适合谁刚学Spring、整天被applicationContext.xml还是Configuration搞晕的人以及想系统梳理Spring IoC基础、准备面试的人。我保证用大白话讲清楚同时把容易踩的坑也给你标出来。2. 三种方式的完整对比与选型思路2.1 三种方式各自是什么先说结论把bean交给Spring容器核心就三种路子第一种XML配置。这是Spring 1.0时代就有的老办法。你在applicationContext.xml里写一行bean iduserService classcom.example.UserService/容器启动的时候就会读取这个XML文件把你指定的类实例化然后放进容器里。第二种注解扫描。Spring 2.5开始引入Component这类注解到了Spring 3.0已经比较成熟了。你在类上标一个Component、Service、Repository、Controller然后在配置里告诉Spring去哪儿扫描容器就会自动把带有这些注解的类注册成bean。第三种Java配置类。Spring 3.0推荐的方式也是现在Spring Boot的主流做法。你写一个带Configuration的类里面用Bean标注方法方法的返回值就是你要交给容器的对象。这三种方式本质都是在做一件事告诉容器“有这个类你来管它”。它们的不同之处在于信息放在哪里——XML文件里、类注解里、还是配置类的方法里。2.2 为什么需要三种方式可能有人会问一个Component不就能搞定所有场景吗为什么还要留着XML和Bean这个问题问到点子上了。我自己做项目多年实际感受是每种方式都有它不可替代的场景。XML配置现在看起来老但它有个优势——不改代码就能改配置。在一些老旧的银行、政府项目里XML配置依然是主流因为运维改个bean的配置不用碰Java代码重新编译。而且有些框架早期版本只支持XML扩展你去看那些老项目的源码还会发现classpath:spring-*.xml这种写法。注解扫描的好处是零配置、开发效率高团队协作的时候每个人只关心自己写的那个类标个注解就完事。但它的问题也很明显你没法控制bean的创建过程比如某个bean的构造函数需要传参数、需要做一些初始化逻辑纯注解就不太好搞。Java配置类是结合了两者优点既能做到类型安全编译器帮你检查又能精细控制bean的创建过程Bean方法里想怎么写就怎么写。Spring Boot默认的全注解配置就是建立在Java配置类的基础上的。2.3 三种方式的选型建议我个人的经验是新项目、Spring Boot项目无脑选注解扫描Java配置类组合。实体类、Service、Controller用Component系列注解需要复杂初始化的、需要接入第三方库的、需要根据条件决定是否创建的用Bean放在Configuration类里。老项目维护优先沿用现有的XML风格不要为了“新”而混用容易造成维护混乱。如果实在要迁移推荐渐进式引入Java配置类新建的类用新方式老的不动。框架/中间件封装如果你们团队做了个自己的starter或者中间件对外提供自动配置那Bean配合条件注解ConditionalOnXxx是唯一正解因为使用方不需要关心你的内部构造逻辑。3. 实操三种方式手把手复现我下面用一个很常见的用户服务UserService作为例子分别用三种方式把它交给Spring容器。为了让内容更贴近实际项目我把示例设计得稍微有点复杂度UserService依赖一个UserDao构造函数需要传一个数据库连接字符串这样能顺便展示每种方式是如何处理依赖的。先定义两个类后面三种方式都会用到它们// UserDao.java public class UserDao { private String dbUrl; public UserDao(String dbUrl) { this.dbUrl dbUrl; } public void queryUser(String userId) { System.out.println([ dbUrl ] 查询用户 userId); } } // UserService.java public class UserService { private UserDao userDao; public UserService(UserDao userDao) { this.userDao userDao; } public void getUser(String userId) { userDao.queryUser(userId); } }请注意这两个类目前都只是普通类没加任何Spring注解也没有实现任何Spring接口。下面我分别用三种方式让Spring来接管它们。3.1 方式一XML配置注册新建一个applicationContext.xml内容如下?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 iduserDao classcom.example.demo.xml.UserDao constructor-arg namedbUrl valuejdbc:mysql://localhost:3306/test/ /bean bean iduserService classcom.example.demo.xml.UserService constructor-arg nameuserDao refuserDao/ /bean /beans然后在Java代码里加载这个配置文件public class XmlDemo { public static void main(String[] args) { // 加载classpath下的XML配置文件 ClassPathXmlApplicationContext context new ClassPathXmlApplicationContext(applicationContext.xml); // 从容器中获取userService对象 UserService userService context.getBean(userService, UserService.class); userService.getUser(1001); context.close(); } }运行一下控制台会打印[jdbc:mysql://localhost:3306/test] 查询用户1001看到没有userService和userDao都不是我自己new的而是从context这个容器里拿出来的。而且userService的构造函数需要的userDao参数容器会预先创建好userDao然后自动注入进去。这就是IoC最直观的体现。XML方式有几个关键点你需要知道上面这种用constructor-arg传参的方式适合构造函数注入。如果你用的是setter方法注入就要换成property name... value.../或property name... ref.../。constructor-arg的顺序和名字要准确name属性指向构造函数的参数名如果你用了编译时没有保留参数名那就要用index属性0、1来指定位置。默认情况下容器里的这个bean是单例的context.getBean(userService)拿多少次都是同一个对象。3.2 方式二注解扫描注册用注解方式我们不需要手写XML里的bean标签而是直接在类上加注解然后扫描。修改UserDao和UserService// UserDao.java —— 给类加 Repository 注解 Repository public class UserDao { private String dbUrl; // 注意如果要用注解方式dbUrl怎么给 // 最简单的方式是直接赋值或者使用 Value 注解 Value(${user.db.url:jdbc:mysql://localhost:3306/test}) private String dbUrl; public void queryUser(String userId) { System.out.println([ dbUrl ] 查询用户 userId); } } // UserService.java —— 给类加 Service 注解 Service public class UserService { // 注入UserDao推荐构造器注入 private final UserDao userDao; public UserService(UserDao userDao) { this.userDao userDao; } public void getUser(String userId) { userDao.queryUser(userId); } }然后在配置类或XML中开启扫描。如果用纯注解方式可以这样写Configuration ComponentScan(basePackages com.example.demo.annotation) public class AnnotationConfig { public static void main(String[] args) { AnnotationConfigApplicationContext context new AnnotationConfigApplicationContext(AnnotationConfig.class); UserService userService context.getBean(UserService.class); userService.getUser(1002); context.close(); } }这里ComponentScan告诉Spring“去com.example.demo.annotation这个包下找那些标了Component系列注解的类。”找到UserDao、UserService之后容器会自己实例化它们并处理依赖关系。需要注意注解扫描的依赖注入有几种写法构造器注入上面的例子Spring会检查UserService构造器识别出它需要UserDao类型的参数然后从容器中找一个UserDao的bean传进去。字段注入Autowired private UserDao userDao;这种写法代码简洁但我不推荐在生产环境大量使用因为不方便测试、也不容易发现循环依赖。setter注入Autowired放在setter方法上主要用于一些必须创建完bean之后再设置值的场景。还有一个细节很多人第一次容易踩坑Component、Service、Repository、Controller这四个注解在Spring容器眼里其实是同一个东西。Service、Repository、Controller内部都标了Component之所以分出来是为了让开发者明确类的分层职责。你拿Service标注一个Dao实现类Spring照样能扫到只是语义上不规范而已。3.3 方式三Java配置类注册这种方式在Spring Boot里最常用。我们不用在UserDao、UserService上标任何注解当然标了也能混用而是在一个Configuration类里通过Bean方法显式声明beanConfiguration public class AppConfig { Bean public UserDao userDao() { return new UserDao(jdbc:mysql://localhost:3306/test); } Bean public UserService userService() { return new UserService(userDao()); } }然后在启动的时候public class JavaConfigDemo { public static void main(String[] args) { AnnotationConfigApplicationContext context new AnnotationConfigApplicationContext(AppConfig.class); UserService userService context.getBean(UserService.class); userService.getUser(1003); context.close(); } }这个写法和直接new有个决定性区别userService()方法里虽然调用了userDao()方法但Spring在默认情况下会拦截这个调用返回的是容器里的单例bean而不是重新创建一个新的UserDao。这就是Configuration号称“全注解配置”的核心魔法——你的配置类本身也会被CGLIB代理掉所有Bean方法的调用都会被容器接管。所以你在userService()方法里调用userDao()是安全的不会出现两个不同的UserDao实例。但如果你把这个类标成Component而不是Configuration那Spring就不会代理它这时userDao()就真的会执行两次产生两个实例。这个细节有点反直觉我见过不少人在网上提问“Configuration和Component有什么区别”问到深处这正是关键区别之一。Java配置类的优势在于你可以在方法里做任何事Bean public UserService userService() { UserDao dao userDao(); // 可以在这里加日志、加缓存、做条件判断 if (dao null) { throw new IllegalStateException(userDao不能为null); } return new UserService(dao); }这种灵活度纯注解扫描是做不到的。4. 进阶注册bean时的生命周期与高级用法4.1 不只是注册还要关心怎么“出生”和“销毁”把bean交给容器之后很多人觉得就完事了。但实际开发中我们经常要介入bean的创建和销毁过程比如初始化连接池、加载配置文件、释放资源。三种方式都支持配置初始化和销毁方法XML方式bean iddataSource classcom.example.MyDataSource init-methodinit destroy-methodclose/注解方式可以实现InitializingBean、DisposableBean接口也可以用PostConstruct、PreDestroy注解。我个人更推荐注解因为不用引入Spring的接口和Spring解耦Component public class MyDataSource { PostConstruct public void init() { System.out.println(连接池初始化完成); } PreDestroy public void close() { System.out.println(连接池关闭); } }Java配置类方式Bean(initMethod init, destroyMethod close) public MyDataSource myDataSource() { return new MyDataSource(); }还有一个容易忽略的点Spring在创建bean的过程中会经过一系列BeanPostProcessor的加工例如Autowired依赖注入、PostConstruct回调其实都是通过BeanPostProcessor实现的。你可以自己写一个BeanPostProcessor在bean初始化前后做手脚但那是比较进阶的操作了暂时不展开。4.2 有选择地注册条件注解与Lazy实际项目中经常会遇到“这个bean要不要创建得看环境配置”的需求。比如你配了Redis就初始化Redis连接没配就不用创建。在Java配置类里配合Spring Boot的ConditionalOnProperty可以做到这种动态注册Configuration public class CacheConfig { Bean ConditionalOnProperty(name app.cache.enabled, havingValue true) public CacheService cacheService() { return new RedisCacheService(); } }XML时代要实现这种逻辑相当费劲得写PropertyPlaceholderConfigurer配合Spring Expression Language相比之下Java配置类干净太多了。这也是为什么我在新项目里极度推荐方式三的原因——它让“有条件地注册bean”变成了一件自然的事。另外Lazy注解也值得了解一下。默认情况下Spring容器启动时会立即创建所有单例bean这被称为饿汉式加载。如果你有些bean创建很耗时、而且不是每次启动都必须用可以加Lazy让它在第一次被获取时才创建懒加载Bean Lazy public HeavyService heavyService() { return new HeavyService(); }注意一个坑懒加载在大多数情况下不是银弹反而会让你“启动时不报错运行时才报错”。所以不要看到bean就加Lazy只有在明确有性能瓶颈时再用。5. 常见问题与排查技巧实录5.1 三种方式混用时的优先级问题很多团队在迁移期会同时存在XML和注解配置这时候如果你用同一个bean名在两种方式里都定义了会发生什么Spring容器默认情况下是后注册的覆盖先注册的除非你设置了allowBeanDefinitionOverridingfalse那就会直接启动失败。我建议的办法是在迁移期间新bean统一用Java配置类注册老的XML先不动并且给bean命名加上前缀区分比如XML里叫userServiceJava配置里叫userServiceV2。这样既能平滑过渡又不会把问题藏起来。5.2 注解扫描不到bean这个问题99%是包路径写错了。ComponentScan默认扫描的是你标注配置类所在包及子包如果你把配置类放在com.example.config而你的UserService在com.example.service那就扫不到。排查方法很简单干脆把ComponentScan的basePackages明确写全别省。Spring Boot就更方便了程序入口类上加SpringBootApplication它会扫描主类所在包及子包所以请把你的主类放在包的根目录上。5.3 循环依赖当两个bean互相依赖时比如A需要BB又需要A容器创建谁都会失败。Spring通过“三级缓存”机制解决了单例模式下、基于属性或setter注入的循环依赖。但如果你用的是构造器注入Spring直接无法创建启动就报错。我之前有段时间总想“构造器注入最规范”然后就把所有bean都改成了构造器注入结果项目里原本能跑的循环依赖直接爆了。后来我调试源码才明白Spring的三级缓存主要是为属性注入设计的构造器循环依赖没有“先给个临时对象”这一步所以解决不了。如果项目里存在循环依赖先别忙着重构把循环依赖本身拆掉才是正道。比如把B依赖A的逻辑改成事件发布、或者抽出一个公共的C让A和B同时依赖C。5.4 用类名getBean时类型重复有时候你会遇到NoUniqueBeanDefinitionException意思是容器里有不止一个UserService类型的bean。这种情况常见于多实现场景Service public class UserServiceImplA implements UserService {} Service public class UserServiceImplB implements UserService {}用context.getBean(UserService.class)就会报错。解决方式加上Primary指定默认使用哪一个。用Qualifier(userServiceImplA)明确指定bean名。构造器注入时配合Qualifier。5.5 实战速查表问题现象根本原因推荐解法NoSuchBeanDefinitionException容器中没有注册这个bean检查包扫描路径、Bean方法是否执行、类名是否写错NoUniqueBeanDefinitionException同类型bean有多个加Primary或Qualifier指定BeanDefinitionOverrideExceptionbean名重复调整命名或允许覆盖BeanCreationException: 循环依赖构造器循环依赖重构代码拆解依赖环路Value取不到值属性名写错或者配置来源没加载检查properties文件位置、检查占位符拼写手动new出来的对象里的Autowired为null对象没经过容器管理不要自己new让容器创建我还想告诉你一个排查技巧如果你不确定某个类到底有没有被容器管理可以在启动时的日志里看“Pre-instantiating singletons”这段输出或者写一个ApplicationRunner用context.getBeanDefinitionNames()把所有bean名打出来。这一招在新人期帮过我太多次了。6. 写在最后的一些大实话说了这么多最后分享一点我的个人体会。学Spring最忌讳的就是“背面试题”比如谁问“三种方式”你就答三句话那没用。这三种方式的本质是你对“对象管理权”的移交方式的理解——你是在用配置文件声明、用注解标记、还是用Java代码构造背后是不同时期对“配置复杂度”和“类型安全”的取舍。我自己当年从XML时代走过来的后来切到注解时代再后来用Spring Boot全注解感受最深的不是代码变少了而是出错的时机变了。XML时代一个bean配置写错了启动时给你一大段异常注解时代包路径搞错了也是启动时报错到了Java配置类时代很多错误在编译期就能拦住。这就是为什么Spring Boot要在Spring Framework之上再封装一层——它把“约定大于配置”推到了极致让开发者把精力放在业务上。如果你现在还在用ClassPathXmlApplicationContext不要觉得它过时了去看看老项目的源码你会学到很多Spring容器最本质的东西。如果你已经在用Spring Boot我强烈建议你找个周末把一个简单的Service改成纯XML方式手动注册一遍。这个过程会让你对“自动配置”产生敬畏——原来背后干了这么多事。最后再说一个小技巧如果你以后看Spring源码打开AnnotationConfigApplicationContext的refresh()方法你会看到invokeBeanFactoryPostProcessors和finishBeanFactoryInitialization这些方法名它们分别对应着“解析配置类、注册bean定义”和“实例化所有单例bean”两个阶段。理解了这两个阶段你就理解了bean的一生。这三种注册方式说白了都是在第一阶段往BeanDefinitionRegistry里塞BeanDefinition只是塞的方式不一样罢了。