第一个 Spring 程序:XML 与注解两种装配方式
这一篇不用 Spring Boot,只用 Maven + spring-context,把 Spring 容器手工立起来一次:写一个 XML 文件说清「有哪些对象、谁需要谁」,然后用 getBean 向容器要对象。整个过程只有两种写法要对比——XML 与注解;只有两条注入路线要分清——构造器注入(<constructor-arg>)与属性注入(<property>)。跑通它,后面所有「Boot 帮你自动做好了」的话你都能翻译成具体步骤。
Spring 容器是一台自动售货机。 你自己 new 对象,等于钻进机器里自己造一瓶饮料;用容器,是货在机器里早已摆好,你只按编号按一下(getBean("helloService")),它就掉出来给你。货道编号就是 Bean 的 id——编号贴错、按错、货道空了,就是本篇第九节报错速查表里的三种典型故障。而且注意:单例商品不是随取随新造,整台机器只放一瓶,你和下一个人按同一个编号,拿到的是同一瓶。
BeanDefinition 是配料单,Bean 是端上桌的菜。 容器启动时先把 XML 里每个 <bean> 抄成一张配料单(类名、构造参数、作用域、是否延迟上菜),全部夹进登记本(BeanDefinitionRegistry);到 refresh() 那一步才照单炒菜,炒好的 singleton 放进保温柜(singletonObjects)。所以你 getBean 拿到的是成品菜,从来不会现场为你炒一份——这也解释了为什么写错 class 会连累整桌菜都上不来。

学完这一篇,你要能回答三个问题:
- 我从
new ClassPathXmlApplicationContext("applicationContext.xml")那一刻起,容器已经替我做了哪几件事? <constructor-arg>和<property>都能「把依赖塞进去」,它们创建对象的时机、失败的表现有什么不同?- XML 装配和注解装配各适合什么场景?同一个容器能不能两者混着用?
面试里问「Spring 怎么装配一个 Bean」,很多人第一反应是 @SpringBootApplication。可那个注解替你藏了太多东西——包扫描、自动配置、内嵌容器全被打包进去,你根本看不到容器是怎么被一层层拼装出来的。这一篇我们故意不用 Spring Boot,只用 Maven + spring-context,从零把 ApplicationContext 立起来。看懂它,后面 Boot 的每一处"魔法"都会变成可解释的流程。
先看依赖。多数人第一个疑问是:为什么不是 spring-core?
<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.example</groupId> <artifactId>spring-xml-demo</artifactId> <version>1.0.0</version> <packaging>jar</packaging> <properties> <maven.compiler.release>17</maven.compiler.release> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <spring.version>6.1.6</spring.version> </properties> <dependencies> <!-- 只声明这一个:容器本体 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>${spring.version}</version> </dependency> </dependencies></project>spring-context会传递依赖spring-core、spring-beans、spring-aop、spring-expression,无需逐个手写spring-core里只有工具类(ClassUtils、Resource、OrderComparator),它没有容器——ApplicationContext住在spring-context- 选
spring-context等于「容器 + Bean 工厂 + AOP + SpEL」一次到齐,这正是 Boot 那套 starter 最终帮你拼出来的东西
提示:想确认依赖树,在项目根目录执行 mvn dependency:tree,你会看到 spring-context 下面挂着 4 个兄弟模块——这就是"引一个、来一串"的传递依赖。
先约定目录结构,后面每个文件都有明确归属:
spring-xml-demo/├── pom.xml└── src/main/ ├── java/com/example/ │ ├── HelloService.java │ ├── HelloServiceImpl.java │ ├── GreetingDao.java │ ├── SimpleDataSource.java │ └── Main.java └── resources/ └── applicationContext.xml
先是接口,再是实现:
package com.example;public interface HelloService { String sayHello(String name);}实现类故意留一个构造器参数,它是我们验证"注入是否生效"的探针:
package com.example;public class HelloServiceImpl implements HelloService { private final String prefix; // 没有无参构造器:意味着必须由容器(或调用方)把 prefix 传进来 public HelloServiceImpl(String prefix) { this.prefix = prefix; } @Override public String sayHello(String name) { return prefix + ", " + name + "!"; }}启动类只需两步:启动容器、从容器取 Bean:
package com.example;import org.springframework.context.ApplicationContext;import org.springframework.context.support.ClassPathXmlApplicationContext;public class Main { public static void main(String[] args) { // 1. 启动容器:它会读取 classpath 根目录下的 applicationContext.xml ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml"); // 2. 按名字取 Bean —— 注意:不是 new HelloServiceImpl("Hello") HelloService service = ctx.getBean("helloService", HelloService.class); System.out.println(service.sayHello("Spring")); // 3. 关闭容器,触发所有单例的销毁回调 ((ClassPathXmlApplicationContext) ctx).close(); }}此时运行 Main,控制台打印:
Hello, Spring!- 输出里的
Hello不是硬编码,而是从applicationContext.xml注入进来的prefix getBean("helloService", HelloService.class)第二个参数做类型校验,取错类型立即报错,比强转安全close()不是装饰:没有它,@PreDestroy、DisposableBean这类销毁回调永远不会执行
getBean 这一句到底发生了什么,值得单独看一遍——同一个调用,三种下场:名字没登记直接抛异常、单例从池子里现取、原型每次照图纸重造。动画走完五步,最后一步的 == 判断就是分辨前两种的最快办法:


把下面这段贴到 src/main/resources/applicationContext.xml:
<?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 是容器里的名字,class 是要实例化的全限定类名 --> <bean id="helloService" class="com.example.HelloServiceImpl"> <!-- ② 构造器注入:index 从 0 开始,按位置对应构造器参数 --> <constructor-arg index="0" value="Hello"/> </bean> <!-- ③ 属性注入:先无参构造,再调用 setter 赋值 --> <bean id="greetingDao" class="com.example.GreetingDao"> <property name="dataSource" ref="dataSource"/> <property name="timeout" value="3000"/> </bean> <!-- ④ 被引用的 Bean(ref 指向的就是它的 id) --> <bean id="dataSource" class="com.example.SimpleDataSource" scope="singleton"/> <!-- ⑤ 原型作用域:每次 getBean 都返回一个全新的对象 --> <bean id="requestLogger" class="com.example.RequestLogger" scope="prototype"/></beans>id="helloService":Bean 在容器中的唯一名字,getBean("helloService")靠它定位<constructor-arg index="0" value="Hello"/>:构造器注入,index从 0 开始;同名参数有多个时用它最稳<property name="dataSource" ref="dataSource"/>:setter 注入,ref表示"引用另一个 Bean",被引用者必须存在<property name="timeout" value="3000"/>:value注入字面量,Spring 会把字符串"3000"自动转成intscope="singleton":默认作用域,容器内只有一个实例;prototype则每次请求都新建
坑:<property name="xxx"> 是靠 setter 的方法名反射匹配的——name="dataSource" 要求类里有 setDataSource(...)。少写、拼错、或只有字段没有 setter,启动时都会抛 NotWritablePropertyException。
上面那五行说的都是「静态的样子」。把一个 <bean> 标签从磁盘读进容器、再登记好,中间其实有四步——这张图点着看,尤其是第 ③ 步:图纸登记完之前,一个对象都不存在:
XML 之外,Spring 也支持用 Java 类描述装配规则。一个 @Configuration 类本身就是 Bean,@Bean 方法的方法名就是 Bean 名:
package com.example;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.ComponentScan;import org.springframework.context.annotation.Configuration;@Configuration // 声明配置类@ComponentScan("com.example") // 扫描该包及其子包下的 @Component 系列注解public class AppConfig { @Bean // 方法返回值注册为 Bean,Bean 名 = 方法名 helloService public HelloService helloService() { return new HelloServiceImpl("Hello"); }}业务类用 @Service 标记,靠 @Autowired 声明它需要什么——注意这里用的是构造器注入:
package com.example;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;@Service // @Component 的语义化变体:表示业务层组件public class OrderService { private final HelloService helloService; @Autowired // Spring 4.3+ 单构造器可省略,但显式写更清楚 public OrderService(HelloService helloService) { this.helloService = helloService; } public String greet(String user) { return helloService.sayHello(user); }}启动方式换成读取配置类:
ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);OrderService orderService = ctx.getBean(OrderService.class);System.out.println(orderService.greet("Annotation"));@ComponentScan默认只扫当前配置类所在包及其子包;包名写错是最常见的"Bean 找不到"来源@Service/@Repository/@Controller都是@Component的语义化别名,扫描时完全等价- 构造器注入让
helloService可以是final——对象一旦建好依赖就不可变,天然线程安全
| 维度 | XML 装配 | 注解装配 |
|---|---|---|
| 可读性 | 配置集中,一眼看全 | 配置贴着代码,就近阅读 |
| 重构友好 | 类名 / 包名改了就断,需全局搜 | IDE 可重命名、可跳转 |
| 编译期检查 | 无,错误留到启动才暴露 | 类不存在编译即报错 |
| 修改成本 | 改配置不用重新编译 | 改代码需重新构建 |
| 适合场景 | 基础设施、第三方类、需运维改配置 | 自有业务代码、快速迭代 |
两者不是二选一的对立——同一个容器里可以同时存在 XML 与注解定义的 Bean,它们共享同一份 BeanDefinitionRegistry。

上面那张表比的是「气质」。真正让人卡住的是一件更具体的事:同一条配置,两种写法各是哪个词。来玩一局配对——左边是你在老项目 XML 里看到的标签,右边是它在新项目里的等价写法:
ApplicationContext 是面向使用者的高层接口,常见实现按"定义来源"划分:
| 实现类 | 定义来源 | 典型场景 |
|---|---|---|
ClassPathXmlApplicationContext | classpath 下的 XML | 传统 XML 项目 |
FileSystemXmlApplicationContext | 文件系统路径的 XML | 外置可编辑配置 |
AnnotationConfigApplicationContext | Java 配置类 | 注解 / 纯 Java 项目 |
AnnotationConfigWebApplicationContext | Java 配置类 | Web 环境 |
XmlWebApplicationContext | web.xml 指定 | 老式 Servlet 应用 |
而 BeanFactory 是最底层的容器接口,默认实现是 DefaultListableBeanFactory。二者最大的差异是实例化时机:
// BeanFactory:用到谁才创建谁(懒加载)DefaultListableBeanFactory factory = new DefaultListableBeanFactory();// ↑ 此刻只是"登记了定义",一个 Bean 都还没实例化// ApplicationContext:启动时就把所有单例预实例化ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");// ↑ 构造完成时,所有非 lazy 的单例都已建好要点:一句话记住——BeanFactory 懒加载,ApplicationContext 启动时预实例化单例。后者在 BeanFactory 之上还叠加了事件发布、国际化、资源加载、AOP 自动代理等企业级能力,实际项目几乎只用 ApplicationContext。
给日志开启 debug 级别后,一次典型启动输出如下:
10:21:33.114 [main] INFO o.s.c.s.ClassPathXmlApplicationContext - Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@1b6d358610:21:33.182 [main] INFO o.s.b.f.xml.XmlBeanDefinitionReader - Loading XML bean definitions from class path resource [applicationContext.xml]10:21:33.251 [main] INFO o.s.b.f.s.DefaultListableBeanFactory - Pre-instantiating singletons in ...: defining beans [helloService,greetingDao,dataSource,requestLogger]; root of factory hierarchyHello, Spring!Refreshing ...:refresh()被调用,容器启动的总入口,后续所有阶段都由它编排Loading XML bean definitions:XmlBeanDefinitionReader把每个<bean>读成一份BeanDefinitiondefining beans [helloService, greetingDao, ...]:注册完成,方括号里的顺序即"先注册、后实例化"的顺序Pre-instantiating singletons:开始预实例化单例,构造函数里的日志会出现于这一行之后Hello, Spring!:main 从容器取出 Bean 并调用,说明装配链路全部打通
包扫描路径没覆盖到。@ComponentScan("com.example.service") 只扫一个子包,写在 com.example.web 下的 @Service 就成了孤魂野鬼,getBean 时报 NoSuchBeanDefinitionException: No qualifying bean of type ...。定位方法:先确认类在哪个包,再把扫描路径放宽到共同父包。
自己 new 出来的对象不受容器管理。new OrderService(new HelloServiceImpl("hi")) 能跑,但它不在容器里——@Autowired 不会被触发、AOP 代理不会生效、@Transactional 形同虚设。凡是需要容器能力(注入、事务、切面)的类,都必须从容器取而不是自己造。
循环依赖报错初体验。A 的构造器需要 B,B 的构造器又需要 A,启动时直接抛 BeanCurrentlyInCreationException。XML 时代 setter 注入能借助提前暴露的半成品破环,所以循环常常"看起来能启动";一旦改成构造器注入,循环依赖会立刻暴露。看到这个异常,先别急着加 @Lazy,先问自己:这两个类是不是把职责缠在了一起?
下面这个演示把"容器如何创建 Bean、何时跳过 Bean"做成了可切换的开关。改条件,看装配结果连锁变化:
按钮按完一轮,就该自己动手问容器了。这台控制台连着浏览器里的真容器,回显全部由内核当场算——它装的这条链(dataSource → jdbcTemplate → userRepository → userService → dispatcherServlet)正是第三节 <constructor-arg> 与 <property> 两种注入的活样本。下面这一串的顺序,就是排查「我的 Bean 到底进没进容器」的正确顺序:
cond 改的是装配条件,内核会顺手换一个新容器重走一遍创建流程,所以不用你再补一次 boot。beans 里那些状态从「已创建」回到「未创建」正是同一件事的另一面——容器换了,池子自然清空。这也解释了为什么 logs 每次都要重新打一遍。
再往前一步,当你把这套装配搬进 Boot 工程,pom 就从「手挑三行 spring-*」变成「勾 starter」。别抄——勾一遍,看生成器给每一项配的理由:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.4</version> <!-- 版本由 BOM 统管,子依赖不写 version -->
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>demo-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
<properties>
<java.version>17</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>前面第七节读的是日志,这一节把镜头拉到容器内部。下面这个实验有四档,请按顺序各点一次:先 「读取并解析」,看磁盘上的 XML 怎么变成内存里的东西;再 「注册 BeanDefinition」,看「配料单」被夹进登记本的过程;然后 「getBean 现场」,看你按编号取货的那一下到底发生了什么;最后 「写错 class 会怎样」——这档最值得看,因为它会告诉你为什么一个字母打错会让整台机器停摆。看完这四档,本篇的报错速查表基本就不用背了。
BeanDefinition 这个词在本篇出现了三次,值得单独拆开看。它是容器内部的 Bean 说明书——你在 XML 或注解里写的每一句话,都会先被翻译成它,然后才轮到场实例化。四档分别看:扫描与解析、scope/lazy 属性落在哪、注册表本身长什么样、以及最实用的最后一档「同类型多个 Bean」(这也是 getBean(XxxService.class) 报 NoUniqueBeanDefinitionException 的根源)。

new ClassPathXmlApplicationContext(...) 这一步里藏着 refresh()——它是容器启动的总编排方法,十二个动作按固定顺序执行。小白不需要背下十二步,只需要知道两件事:定义先全部登记完,对象才开始批量创建;任何一步抛异常,整个容器就不会启动。建议先看 「只盯关键四步」,再看 「某一步失败会怎样」,最后有兴趣才展开完整十二步。
XML 里 <constructor-arg> 与 <property> 的区别不是语法口味,而是两条不同的创建路线:前者要求类有匹配的构造器、参数不齐就直接建不出来;后者先调无参构造拿到半成品,再逐个找 setter 赋值,所以它一定能让对象先存在。第四档 「遇到循环依赖」 正好解释第八节那条警告——为什么 setter 时代的循环能启动、构造器时代立刻炸。
别急着改代码,先在这里改配置。两个旋钮:scope(singleton 还是 prototype)和有没有配 <constructor-arg>。输出会同时告诉你两次 getBean 拿到的是不是同一个对象、hashCode 的形状对不对得上——这是判断「作用域是否生效」最快的办法。
getBean 两次 == truecom.example.HelloServiceImpl@1b6d3586com.example.HelloServiceImpl@1b6d3586# 第二次没有重新调构造器,构造器日志只打印 1 次
| 报错原文(片段) | 真实原因 | 30 秒自救 | 深挖看第几篇 |
|---|---|---|---|
org.springframework.beans.factory.BeanDefinitionStoreException: IOException parsing XML document from class path resource [beans.xml]; nested exception is java.io.FileNotFoundException: class path resource [beans.xml] cannot be opened because it does not exist | 文件名/路径写错,或 XML 放进了 src/main/java 没被打包进 classpath | 看 src/main/resources 下是否真有该文件;构造参数要和 resources 根目录下的相对路径一字不差 | 第 2 篇 · Maven 与目录结构 |
java.lang.ClassNotFoundException: com.exmple.HelloServiceImpl(常包在 BeanCreationException 里) | <bean class="..."> 的全限定类名拼错,或该类所在依赖没进 classpath | 在 IDEA 里 Ctrl+N 搜这个类名,能搜到就把包名复制回去;package 语句才是真包名 | 第 1 篇 · 包与 classpath |
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'userService' defined in class path resource [beans.xml]: Instantiation of bean failed; nested exception is ... No default constructor found | 容器要用无参构造建对象,但类里只有带参构造器(写了带参构造后编译器不再送你无参的那个) | 要么补一个 public UserService() {},要么在 XML 里老老实实写 <constructor-arg> | 本节第十二、十三节实验 |
org.springframework.beans.NotWritablePropertyException: Invalid property 'timeout' of bean class [com.example.GreetingDao]: Bean property 'timeout' is not writable or has an invalid setter method | <property name="timeout"> 找不到对应的 setTimeout(int):拼错、大小写不符、只有字段没有 setter、或 setter 参数类型对不上 | 只看 setter 方法名:name="x" 必须有 setX。补 setter 或改名;private 的 setter 不算 | 第 6 篇 · 属性与类型转换 |
NoSuchBeanDefinitionException: No qualifying bean of type 'com.example.HelloService' available | 类压根没被登记:XML 忘了写 <bean>、<import> 没引那个文件、或 @ComponentScan 的包路径没覆盖到 | 先打印 ctx.getBeanDefinitionNames() 看名单里有没有它,再决定是补配置还是放宽扫描 | 第 8 篇 · 包扫描与自动配置 |
NoSuchBeanDefinitionException: No bean named 'helloServcie' available(按 id 取时) | getBean("...") 里的 id 拼错,或大小写不一致——id 是区分大小写的字符串 | 把 id 交给常量而不是手打:String ID = "helloService";需要别名用 <alias name="helloService" alias="hello"/> | 本节第十一节 getbean 档 |
以上六条几乎都发生在容器启动那一刻,所以排查顺序永远是「先看文件名对不对 → 再看 class 能不能编译跳转 → 再看 id/name 是否一字不差 → 最后才怀疑 Spring」。
表里第一条是新手最先撞上的:容器还没起来,就先告诉你文件找不到。别急着背结论——在那一长串里点出你认为的凶手帧,点错了也会给你解释:
照教程写完 XML、把文件放进项目,然后敲下 new ClassPathXmlApplicationContext("beans.xml") —— 程序连你自己那一行日志都没来得及打印就退出了,退出码 1。
把本篇第二、三节的四个文件(HelloService、HelloServiceImpl、SimpleDataSource、GreetingDao)与 applicationContext.xml 补齐,跑通 Main:
package com.example;public class GreetingDao { private SimpleDataSource dataSource; private int timeout; public void setDataSource(SimpleDataSource dataSource) { this.dataSource = dataSource; } public void setTimeout(int timeout) { this.timeout = timeout; } public String query() { return "dao@" + System.identityHashCode(dataSource) + " timeout=" + timeout; }}SimpleDataSource ds = ctx.getBean("dataSource", SimpleDataSource.class);System.out.println(ctx.getBean("greetingDao", GreetingDao.class).query());预期输出(@ 后的数字随机器而变,但两次运行同一进程时应相同):
dao@1234567 timeout=3000只做三处改动,各自记下你观察到的现象:
- 给
greetingDao加上scope="prototype",连续getBean两次比较==—— 你会观察到:从true变成false,且defining beans [...]里它虽然还在名单上,却不会被预实例化。 - 把
<property name="timeout" value="3000"/>改成<property name="timeOut" value="3000"/>—— 你会观察到:启动直接失败,报NotWritablePropertyException,而且报错信息里明确写着Bean property 'timeOut' is not writable。 - 删掉
HelloServiceImpl的带参构造器以外的所有改动,改为把<constructor-arg index="0" value="Hello"/>整行注释掉 —— 你会观察到:BeanCreationException ... No default constructor found,故障点在Pre-instantiating singletons那一行日志之后。
做一个「图书角借阅登记」小程序:BookDao(模拟存储)、BookService(业务)、AuditLogger(记录一行日志),三者全部用 XML 装配,main 里不许出现一个 new。
验收清单:
- [ ]
bookService通过<constructor-arg ref="bookDao"/>拿到 DAO,字段是final - [ ]
auditLogger用<property name="enabled" value="true"/>注入开关,且有对应的setEnabled(boolean) - [ ] 有一个 Bean 是
prototype,两次getBean的hashCode不同 - [ ] 用
<alias>给bookService起个别名service,两种名字都能取到同一个对象 - [ ] 故意把某个
class写错,把你看到的完整异常栈第一行抄进笔记
不看笔记,说出容器启动后 singletonObjects 里有什么、BeanDefinitionRegistry 里有什么,两者差别在哪一步产生?答不出就回到第十五节沙盘重跑一次。
<property name="dataSource" ref="dataSource"/> 这一行,name 匹配的是什么、ref 匹配的是什么?如果我说「name 对应字段名」,你能立刻纠正我吗?
getBean("helloService", HelloService.class) 为什么不写成 getBean("helloService") 再强转?第二个参数帮你在哪一步就发现问题?
同一个容器里既有 XML 定义的 Bean 又有 @ComponentScan 扫出来的 Bean,可能吗?它们共用哪一个数据结构?
ApplicationContext 和 BeanFactory 的时机差异,能让你解释「为什么配置写错了程序连 main 的第二行都没跑到就崩了」吗?
投币才出货,编号就是 id;配料单先夹好,菜在后面炒;不写 scope 就是单例,改 prototype 才每次新。
这一篇真正要带走三件事——spring-context 是容器的最小依赖;XML 靠 constructor-arg / property 声明注入,注解靠 @Configuration / @ComponentScan / @Autowired;BeanFactory 懒加载、ApplicationContext 启动即预实例化单例。把容器"看得见",后面的自动配置就不再神秘。