第一个 Spring 程序:XML 与注解两种装配方式

bee2026-10-0853 分钟0 次阅读
不依赖 Spring Boot,用 Maven + spring-context 从零装配容器:XML 声明 Bean、注解扫描、构造器与 setter 注入、作用域,两种方式一次对比清楚。
1 / 112
小节
〇、30 秒看懂
2 / 112

这一篇不用 Spring Boot,只用 Maven + spring-context,把 Spring 容器手工立起来一次:写一个 XML 文件说清「有哪些对象、谁需要谁」,然后用 getBean 向容器要对象。整个过程只有两种写法要对比——XML 与注解;只有两条注入路线要分清——构造器注入(<constructor-arg>)与属性注入(<property>)。跑通它,后面所有「Boot 帮你自动做好了」的话你都能翻译成具体步骤。

3 / 112
类比

Spring 容器是一台自动售货机。 你自己 new 对象,等于钻进机器里自己造一瓶饮料;用容器,是货在机器里早已摆好,你只按编号按一下(getBean("helloService")),它就掉出来给你。货道编号就是 Bean 的 id——编号贴错、按错、货道空了,就是本篇第九节报错速查表里的三种典型故障。而且注意:单例商品不是随取随新造,整台机器只放一瓶,你和下一个人按同一个编号,拿到的是同一瓶。

4 / 112
类比

BeanDefinition 是配料单,Bean 是端上桌的菜。 容器启动时先把 XML 里每个 <bean> 抄成一张配料单(类名、构造参数、作用域、是否延迟上菜),全部夹进登记本(BeanDefinitionRegistry);到 refresh() 那一步才照单炒菜,炒好的 singleton 放进保温柜(singletonObjects)。所以你 getBean 拿到的是成品菜,从来不会现场为你炒一份——这也解释了为什么写错 class 会连累整桌菜都上不来。

5 / 112
架构图
图 · beans.xml 里都有什么
图 · beans.xml 里都有什么
6 / 112

学完这一篇,你要能回答三个问题:

7 / 112
  1. 我从 new ClassPathXmlApplicationContext("applicationContext.xml") 那一刻起,容器已经替我做了哪几件事?
  2. <constructor-arg> 和 <property> 都能「把依赖塞进去」,它们创建对象的时机、失败的表现有什么不同?
  3. XML 装配和注解装配各适合什么场景?同一个容器能不能两者混着用?
8 / 112
小节
一、为什么先学"裸"Spring:从 spring-context 说起
9 / 112

面试里问「Spring 怎么装配一个 Bean」,很多人第一反应是 @SpringBootApplication。可那个注解替你藏了太多东西——包扫描、自动配置、内嵌容器全被打包进去,你根本看不到容器是怎么被一层层拼装出来的。这一篇我们故意不用 Spring Boot,只用 Maven + spring-context,从零把 ApplicationContext 立起来。看懂它,后面 Boot 的每一处"魔法"都会变成可解释的流程。

10 / 112

先看依赖。多数人第一个疑问是:为什么不是 spring-core?

11 / 112
代码对照
代码xml
<?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 个兄弟模块——这就是"引一个、来一串"的传递依赖。

12 / 112
小节
二、第一个 Bean 与 main 方法
13 / 112

先约定目录结构,后面每个文件都有明确归属:

14 / 112
text
spring-xml-demo/├── pom.xml└── src/main/    ├── java/com/example/    │   ├── HelloService.java    │   ├── HelloServiceImpl.java    │   ├── GreetingDao.java    │   ├── SimpleDataSource.java    │   └── Main.java    └── resources/        └── applicationContext.xml
15 / 112
原理动画
动图 · 一个 Bean 的诞生之路
动图 · 一个 Bean 的诞生之路
16 / 112

先是接口,再是实现:

17 / 112
java
package com.example;public interface HelloService {    String sayHello(String name);}
18 / 112

实现类故意留一个构造器参数,它是我们验证"注入是否生效"的探针:

19 / 112
java
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 + "!";    }}
20 / 112

启动类只需两步:启动容器、从容器取 Bean:

21 / 112
java
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();    }}
22 / 112

此时运行 Main,控制台打印:

23 / 112
代码对照
代码text
Hello, Spring!
解读
  • 输出里的 Hello 不是硬编码,而是从 applicationContext.xml 注入进来的 prefix
  • getBean("helloService", HelloService.class) 第二个参数做类型校验,取错类型立即报错,比强转安全
  • close() 不是装饰:没有它,@PreDestroy、DisposableBean 这类销毁回调永远不会执行
24 / 112

getBean 这一句到底发生了什么,值得单独看一遍——同一个调用,三种下场:名字没登记直接抛异常、单例从池子里现取、原型每次照图纸重造。动画走完五步,最后一步的 == 判断就是分辨前两种的最快办法:

25 / 112
原理动画
动图 · 一次 getBean 的三种下场
动图 · 一次 getBean 的三种下场
26 / 112
小节
三、XML 装配:applicationContext.xml 全解
27 / 112
架构图
图 1 · XML 装配时容器做了什么
图 1 · XML 装配时容器做了什么
28 / 112

把下面这段贴到 src/main/resources/applicationContext.xml:

29 / 112
代码对照
代码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" 自动转成 int
  • scope="singleton":默认作用域,容器内只有一个实例;prototype 则每次请求都新建

坑:<property name="xxx"> 是靠 setter 的方法名反射匹配的——name="dataSource" 要求类里有 setDataSource(...)。少写、拼错、或只有字段没有 setter,启动时都会抛 NotWritablePropertyException。

30 / 112

上面那五行说的都是「静态的样子」。把一个 <bean> 标签从磁盘读进容器、再登记好,中间其实有四步——这张图点着看,尤其是第 ③ 步:图纸登记完之前,一个对象都不存在:

31 / 112
交互图解
流程一个 <bean> 标签的落地路线(点着看)1 / 4
从 ① 点到 ④,第 ③ 步是新手最容易跳过去的一格
→
→
→
① 定位资源
ClassPathXmlApplicationContext("applicationContext.xml") 先去 classpath 上找这个文件。找不到就是 FileNotFoundException——注意这发生在容器构造的那一刻,还没轮到任何 Bean。文件名、大小写、以及它到底有没有被打包进 resources,都在这一步算账。
全部看懂了记法:读文件 → 抄图纸 → 登记 → 生产。前三步一个对象都没有,你的代码在第 ④ 步才第一次跑起来。
32 / 112
小节
四、注解装配:@Configuration 与组件扫描
33 / 112

XML 之外,Spring 也支持用 Java 类描述装配规则。一个 @Configuration 类本身就是 Bean,@Bean 方法的方法名就是 Bean 名:

34 / 112
java
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");    }}
35 / 112

业务类用 @Service 标记,靠 @Autowired 声明它需要什么——注意这里用的是构造器注入:

36 / 112
java
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);    }}
37 / 112

启动方式换成读取配置类:

38 / 112
代码对照
代码java
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——对象一旦建好依赖就不可变,天然线程安全
39 / 112
小节
五、两种方式对比
40 / 112
对照表
维度XML 装配注解装配
可读性配置集中,一眼看全配置贴着代码,就近阅读
重构友好类名 / 包名改了就断,需全局搜IDE 可重命名、可跳转
编译期检查无,错误留到启动才暴露类不存在编译即报错
修改成本改配置不用重新编译改代码需重新构建
适合场景基础设施、第三方类、需运维改配置自有业务代码、快速迭代
41 / 112
说明

两者不是二选一的对立——同一个容器里可以同时存在 XML 与注解定义的 Bean,它们共享同一份 BeanDefinitionRegistry。

42 / 112
架构图
图 · 两种写法,同一份图纸
图 · 两种写法,同一份图纸
43 / 112

上面那张表比的是「气质」。真正让人卡住的是一件更具体的事:同一条配置,两种写法各是哪个词。来玩一局配对——左边是你在老项目 XML 里看到的标签,右边是它在新项目里的等价写法:

44 / 112
配对闯关
闯关XML 标签 ↔ 注解等价写法已配对 0/8 · 配错 0
八组都是同一件事的两种说法。左列来自你接手的老项目,右列来自第 6 篇之后你会写的
先点左边一个
45 / 112
小节
六、ApplicationContext 家族与 BeanFactory 的区别
46 / 112

ApplicationContext 是面向使用者的高层接口,常见实现按"定义来源"划分:

47 / 112
对照表
实现类定义来源典型场景
ClassPathXmlApplicationContextclasspath 下的 XML传统 XML 项目
FileSystemXmlApplicationContext文件系统路径的 XML外置可编辑配置
AnnotationConfigApplicationContextJava 配置类注解 / 纯 Java 项目
AnnotationConfigWebApplicationContextJava 配置类Web 环境
XmlWebApplicationContextweb.xml 指定老式 Servlet 应用
48 / 112

而 BeanFactory 是最底层的容器接口,默认实现是 DefaultListableBeanFactory。二者最大的差异是实例化时机:

49 / 112
代码对照
代码java
// BeanFactory:用到谁才创建谁(懒加载)DefaultListableBeanFactory factory = new DefaultListableBeanFactory();// ↑ 此刻只是"登记了定义",一个 Bean 都还没实例化// ApplicationContext:启动时就把所有单例预实例化ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");// ↑ 构造完成时,所有非 lazy 的单例都已建好
解读

要点:一句话记住——BeanFactory 懒加载,ApplicationContext 启动时预实例化单例。后者在 BeanFactory 之上还叠加了事件发布、国际化、资源加载、AOP 自动代理等企业级能力,实际项目几乎只用 ApplicationContext。

50 / 112
小节
七、启动日志逐行解读
51 / 112

给日志开启 debug 级别后,一次典型启动输出如下:

52 / 112
代码对照
代码text
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> 读成一份 BeanDefinition
  • defining beans [helloService, greetingDao, ...]:注册完成,方括号里的顺序即"先注册、后实例化"的顺序
  • Pre-instantiating singletons:开始预实例化单例,构造函数里的日志会出现于这一行之后
  • Hello, Spring!:main 从容器取出 Bean 并调用,说明装配链路全部打通
53 / 112
小节
八、三个高频坑
54 / 112
坑

包扫描路径没覆盖到。@ComponentScan("com.example.service") 只扫一个子包,写在 com.example.web 下的 @Service 就成了孤魂野鬼,getBean 时报 NoSuchBeanDefinitionException: No qualifying bean of type ...。定位方法:先确认类在哪个包,再把扫描路径放宽到共同父包。

55 / 112
坑

自己 new 出来的对象不受容器管理。new OrderService(new HelloServiceImpl("hi")) 能跑,但它不在容器里——@Autowired 不会被触发、AOP 代理不会生效、@Transactional 形同虚设。凡是需要容器能力(注入、事务、切面)的类,都必须从容器取而不是自己造。

56 / 112
警告

循环依赖报错初体验。A 的构造器需要 B,B 的构造器又需要 A,启动时直接抛 BeanCurrentlyInCreationException。XML 时代 setter 注入能借助提前暴露的半成品破环,所以循环常常"看起来能启动";一旦改成构造器注入,循环依赖会立刻暴露。看到这个异常,先别急着加 @Lazy,先问自己:这两个类是不是把职责缠在了一起?

57 / 112
小节
九、上手体验:真实容器装配现场
58 / 112

下面这个演示把"容器如何创建 Bean、何时跳过 Bean"做成了可切换的开关。改条件,看装配结果连锁变化:

59 / 112
内核实验
60 / 112

按钮按完一轮,就该自己动手问容器了。这台控制台连着浏览器里的真容器,回显全部由内核当场算——它装的这条链(dataSource → jdbcTemplate → userRepository → userService → dispatcherServlet)正是第三节 <constructor-arg> 与 <property> 两种注入的活样本。下面这一串的顺序,就是排查「我的 Bean 到底进没进容器」的正确顺序:

61 / 112
内核控制台
62 / 112
提示

cond 改的是装配条件,内核会顺手换一个新容器重走一遍创建流程,所以不用你再补一次 boot。beans 里那些状态从「已创建」回到「未创建」正是同一件事的另一面——容器换了,池子自然清空。这也解释了为什么 logs 每次都要重新打一遍。

63 / 112
小节
十、新项目该用 XML 还是注解?
64 / 112
决策
决策你接手一个 2013 年的 SSM 老项目,团队要继续往上加新功能,但明确不重写。新模块的 Bean 用哪种方式装配?
65 / 112

再往前一步,当你把这套装配搬进 Boot 工程,pom 就从「手挑三行 spring-*」变成「勾 starter」。别抄——勾一遍,看生成器给每一项配的理由:

66 / 112
生成器
生成器从裸 spring-context 搬到 Boot 时,pom 会变成这样pom.xml2 / 6
本篇第一节手写的 spring-context / spring-beans / spring-core 三行,在这里由 starter 的传递依赖带进来——所以勾选项里根本看不到它们。先勾 Web 与 Test 建立基准,再看加 JPA / MySQL 时多出来的那行 scope
产物
<?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>
勾了这些,代价与理由在这里
parent继承 3.3.4 的 starter-parent 之后,所有 spring-boot-starter-* 都不用写版本号;一旦有人手写给某个 starter 加 version,就以那条为准——这是依赖版本漂移最常见的原因。
Web做接口就绕不开它: DispatcherServlet、内嵌 Tomcat、JSON 序列化全在这个 starter 里。
Testscope=test;@SpringBootTest、MockMvc、AssertJ 都在里面,漏了就找不到 @Test。
67 / 112
小节
十一、内核实验一:XML 容器启动全过程
68 / 112

前面第七节读的是日志,这一节把镜头拉到容器内部。下面这个实验有四档,请按顺序各点一次:先 「读取并解析」,看磁盘上的 XML 怎么变成内存里的东西;再 「注册 BeanDefinition」,看「配料单」被夹进登记本的过程;然后 「getBean 现场」,看你按编号取货的那一下到底发生了什么;最后 「写错 class 会怎样」——这档最值得看,因为它会告诉你为什么一个字母打错会让整台机器停摆。看完这四档,本篇的报错速查表基本就不用背了。

69 / 112
内核实验
TeaVMXML 容器启动全过程未启动
依次切 parse → register → getbean → typo,重点看每个阶段结束时内存里多了什么
场景参数
点「运行演示」,在浏览器内真实执行 Java 编译出的内核算法,逐步看它怎么跑。
70 / 112
小节
十二、内核实验二:BeanDefinition 是怎么诞生的
71 / 112

BeanDefinition 这个词在本篇出现了三次,值得单独拆开看。它是容器内部的 Bean 说明书——你在 XML 或注解里写的每一句话,都会先被翻译成它,然后才轮到场实例化。四档分别看:扫描与解析、scope/lazy 属性落在哪、注册表本身长什么样、以及最实用的最后一档「同类型多个 Bean」(这也是 getBean(XxxService.class) 报 NoUniqueBeanDefinitionException 的根源)。

72 / 112
内核实验
TeaVMBeanDefinition 的诞生未启动
切到 registry 看 beanName → BeanDefinition 这张表,再看 multi 为什么按类型取会歧义
场景参数
点「运行演示」,在浏览器内真实执行 Java 编译出的内核算法,逐步看它怎么跑。
73 / 112
小节
十三、内核实验三:refresh() 十二步与某一步失败
74 / 112
原理动画
动图 · refresh() 期间谁先出生
动图 · refresh() 期间谁先出生
75 / 112

new ClassPathXmlApplicationContext(...) 这一步里藏着 refresh()——它是容器启动的总编排方法,十二个动作按固定顺序执行。小白不需要背下十二步,只需要知道两件事:定义先全部登记完,对象才开始批量创建;任何一步抛异常,整个容器就不会启动。建议先看 「只盯关键四步」,再看 「某一步失败会怎样」,最后有兴趣才展开完整十二步。

76 / 112
内核实验
TeaVMrefresh() 十二步未启动
先 key 后 fail,看清失败那档里异常是在哪一步被抛出、容器如何回滚已建好的对象
场景参数
点「运行演示」,在浏览器内真实执行 Java 编译出的内核算法,逐步看它怎么跑。
77 / 112
小节
十四、内核实验四:三种注入的装配时序
78 / 112

XML 里 <constructor-arg> 与 <property> 的区别不是语法口味,而是两条不同的创建路线:前者要求类有匹配的构造器、参数不齐就直接建不出来;后者先调无参构造拿到半成品,再逐个找 setter 赋值,所以它一定能让对象先存在。第四档 「遇到循环依赖」 正好解释第八节那条警告——为什么 setter 时代的循环能启动、构造器时代立刻炸。

79 / 112
内核实验
TeaVM三种注入的装配时序未启动
对比 constructor / setter / field 三条时间线,最后切 cycle 看断点落在哪一步
场景参数
点「运行演示」,在浏览器内真实执行 Java 编译出的内核算法,逐步看它怎么跑。
80 / 112
小节
十五、沙盘:XML 里改哪一行会怎样
81 / 112

别急着改代码,先在这里改配置。两个旋钮:scope(singleton 还是 prototype)和有没有配 <constructor-arg>。输出会同时告诉你两次 getBean 拿到的是不是同一个对象、hashCode 的形状对不对得上——这是判断「作用域是否生效」最快的办法。

82 / 112
沙盘
沙盘XML 里改哪一行会怎样
运行结果
getBean 两次 == true
com.example.HelloServiceImpl@1b6d3586
com.example.HelloServiceImpl@1b6d3586
# 第二次没有重新调构造器,构造器日志只打印 1 次
这是默认形态:整个容器共用一瓶饮料,货道里永远只有这一瓶。
83 / 112
小节
十六、随堂自测
84 / 112
随堂自测
随堂自测`ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");` 这一行执行完(还没调用任何 getBean),容器里的非懒加载单例 Bean 已经被创建出来了。这句话对吗?
先自己选一个,选中立刻告诉你对不对
85 / 112
随堂自测
随堂自测XML 里 `<constructor-arg>` 和 `<property>` 的根本区别是什么?
先自己选一个,选中立刻告诉你对不对
86 / 112
小节
十七、常见报错速查
87 / 112
对照表
报错原文(片段)真实原因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 档
88 / 112
提示

以上六条几乎都发生在容器启动那一刻,所以排查顺序永远是「先看文件名对不对 → 再看 class 能不能编译跳转 → 再看 id/name 是否一字不差 → 最后才怀疑 Spring」。

89 / 112

表里第一条是新手最先撞上的:容器还没起来,就先告诉你文件找不到。别急着背结论——在那一长串里点出你认为的凶手帧,点错了也会给你解释:

90 / 112
报错急救
报错急救BeanDefinitionStoreException

照教程写完 XML、把文件放进项目,然后敲下 new ClassPathXmlApplicationContext("beans.xml") —— 程序连你自己那一行日志都没来得及打印就退出了,退出码 1。

Exception in thread "main" 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
at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.loadBeanDefinitions(XmlBeanDefinitionReader.java:342)
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:570)
at org.springframework.context.support.ClassPathXmlApplicationContext.<init>(ClassPathXmlApplicationContext.java:144)
Caused by: java.io.FileNotFoundException: class path resource [beans.xml] cannot be opened because it does not exist
at org.springframework.core.io.ClassPathResource.getInputStream(ClassPathResource.java:208)
at com.example.Main.main(Main.java:11)
点你认为的「凶手行」(可反复试)
不会也没关系:先猜异常名,再猜哪一行在做决定。
91 / 112
小节
十八、动手练习
92 / 112
小节
第一档 · 照做
93 / 112

把本篇第二、三节的四个文件(HelloService、HelloServiceImpl、SimpleDataSource、GreetingDao)与 applicationContext.xml 补齐,跑通 Main:

94 / 112
java
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;    }}
95 / 112
java
SimpleDataSource ds = ctx.getBean("dataSource", SimpleDataSource.class);System.out.println(ctx.getBean("greetingDao", GreetingDao.class).query());
96 / 112

预期输出(@ 后的数字随机器而变,但两次运行同一进程时应相同):

97 / 112
text
dao@1234567 timeout=3000
98 / 112
小节
第二档 · 变体
99 / 112

只做三处改动,各自记下你观察到的现象:

100 / 112
  1. 给 greetingDao 加上 scope="prototype",连续 getBean 两次比较 == —— 你会观察到:从 true 变成 false,且 defining beans [...] 里它虽然还在名单上,却不会被预实例化。
  2. 把 <property name="timeout" value="3000"/> 改成 <property name="timeOut" value="3000"/> —— 你会观察到:启动直接失败,报 NotWritablePropertyException,而且报错信息里明确写着 Bean property 'timeOut' is not writable。
  3. 删掉 HelloServiceImpl 的带参构造器以外的所有改动,改为把 <constructor-arg index="0" value="Hello"/> 整行注释掉 —— 你会观察到:BeanCreationException ... No default constructor found,故障点在 Pre-instantiating singletons 那一行日志之后。
101 / 112
小节
第三档 · 造一个
102 / 112

做一个「图书角借阅登记」小程序:BookDao(模拟存储)、BookService(业务)、AuditLogger(记录一行日志),三者全部用 XML 装配,main 里不许出现一个 new。

103 / 112

验收清单:

104 / 112
  • [ ] bookService 通过 <constructor-arg ref="bookDao"/> 拿到 DAO,字段是 final
  • [ ] auditLogger 用 <property name="enabled" value="true"/> 注入开关,且有对应的 setEnabled(boolean)
  • [ ] 有一个 Bean 是 prototype,两次 getBean 的 hashCode 不同
  • [ ] 用 <alias> 给 bookService 起个别名 service,两种名字都能取到同一个对象
  • [ ] 故意把某个 class 写错,把你看到的完整异常栈第一行抄进笔记
105 / 112
小节
十九、要点自查
106 / 112
自检

不看笔记,说出容器启动后 singletonObjects 里有什么、BeanDefinitionRegistry 里有什么,两者差别在哪一步产生?答不出就回到第十五节沙盘重跑一次。

107 / 112
自检

<property name="dataSource" ref="dataSource"/> 这一行,name 匹配的是什么、ref 匹配的是什么?如果我说「name 对应字段名」,你能立刻纠正我吗?

108 / 112
自检

getBean("helloService", HelloService.class) 为什么不写成 getBean("helloService") 再强转?第二个参数帮你在哪一步就发现问题?

109 / 112
自检

同一个容器里既有 XML 定义的 Bean 又有 @ComponentScan 扫出来的 Bean,可能吗?它们共用哪一个数据结构?

110 / 112
自检

ApplicationContext 和 BeanFactory 的时机差异,能让你解释「为什么配置写错了程序连 main 的第二行都没跑到就崩了」吗?

111 / 112
口诀

投币才出货,编号就是 id;配料单先夹好,菜在后面炒;不写 scope 就是单例,改 prototype 才每次新。

112 / 112
总结

这一篇真正要带走三件事——spring-context 是容器的最小依赖;XML 靠 constructor-arg / property 声明注入,注解靠 @Configuration / @ComponentScan / @Autowired;BeanFactory 懒加载、ApplicationContext 启动即预实例化单例。把容器"看得见",后面的自动配置就不再神秘。