国庆节10月1日
--


把 IOC 与 DI 的细节一次讲完——声明 bean 的四大注解(@Component 与它的三个衍生注解各用在哪层、bean 名字默认类名首字母小写)、组件扫描的范围(@ComponentScan 藏在 @SpringBootApplication 里,只扫启动类所在包及其子包)、依赖注入的三种方式与各自优缺点,以及同类型多个 bean 冲突时的三种解决方案(@Primary / @Qualifier / @Resource),全部附本机实测
.webp)
上一篇(分层解耦与IOC-DI入门)用 @Component + @Autowired 把三处 new 换掉了,页面照旧能跑。但还有三个问题没回答:
@Component 一个吗?——业务层、数据访问层有没有更贴切的写法?@Autowired 这种注入方式有几种写法?如果同一个类型有两个 bean,容器该给我哪一个?这一篇就是第 49/55 页那张目录的最后两节(PPT 第 64 页与第 68 页又各翻了一次同一张导航页,分别停在”IOC 详解”和”DI 详解”上):IOC 详解(PPT 第 65-68 页)与 DI 详解(PPT 第 69-71 页)。
PPT 第 65 页给了完整的表格——要把某个对象交给 IOC 容器管理,就在对应的类上加上下面这些注解之一:
| 注解 | 说明 | 位置 |
|---|---|---|
@Component | 声明 bean 的基础注解 | 不属于以下三类时,用此注解 |
@Controller | @Component 的衍生注解 | 标注在控制层类上 |
@Service | @Component 的衍生注解 | 标注在业务层类上 |
@Repository | @Component 的衍生注解 | 标注在数据访问层类上(由于与 mybatis 整合,用的少) |
先要理解”衍生注解”这四个字:@Controller、@Service、@Repository 的源码上都标着 @Component——功能完全一样(都是把类交给容器),区别只是语义:让人(和后来的读代码的人)一眼看出这个类属于哪一层。所以:
混用不会报错(比如业务层写 @Component 照样能注入),但规范写法是按层选注解——三个衍生注解是”带标签的 @Component”。
注意一个例外(PPT 第 67 页专门强调):在 SpringBoot 集成 web 开发中,声明控制器 bean 只能用 @Controller(或用它的组合注解 @RestController)——因为 SpringMVC 需要认出哪些类是请求处理类。
对着这个案例把注解归位:
1// 控制层:UserController2@RestController // = @Controller + @ResponseBody,控制器就用它3public class UserController { /* …… */ }1// 业务层实现类:UserServiceImpl2@Service // ← 原先是 @Component,现在换成更贴切的 @Service3public class UserServiceImpl implements UserService { /* …… */ }1// 数据访问层实现类:UserDaoImpl2@Repository // ← 数据访问层用 @Repository(课程代码里这个注解后面还留了一条注释写法,可选地指定名字)3public class UserDaoImpl implements UserDao { /* …… */ }课程最终代码的 UserDaoImpl 写的是 @Repository//("userDao")——注释掉的 ("userDao") 就是下面”自己指定 bean 名字”的写法示例;UserServiceImpl2 上也留着一行被注释的 //@Primary,那是 PPT 第 70 页的方案一(本篇后面会用到)。
PPT 第 65 页最后还有一条”注意”:
声明 bean 的时候,可以通过注解的 value 属性指定 bean 的名字,如果没有指定,默认为类名首字母小写。
两种写法对比:
1@Repository // 名字默认 = 类名首字母小写:userDaoImpl2public class UserDaoImpl implements UserDao { /* …… */ }1@Repository("userDao") // 手动指定名字:userDao(value 属性,括号里直接写字符串)2@Component("userServiceA")3public class UserServiceImpl implements UserService { /* …… */ }名字有什么用?——同类型有多个 bean 时,靠名字区分(本篇”DI 详解(二)“的 @Qualifier / @Resource 里写的那个字符串,就是 bean 的名字)。所以这条默认规则必须记牢:
| 类名 | 默认 bean 名字 |
|---|---|
UserServiceImpl | userServiceImpl |
UserServiceImpl2 | userServiceImpl2 |
UserDaoImpl | userDaoImpl |
(本机实测里 @Qualifier("userServiceImpl")、@Resource(name = "userServiceImpl2") 能生效,靠的就是它——名字拼错的话,容器会告诉你”没有叫这个名字的 bean”。)
PPT 第 66 页:
前面声明 bean 的四大注解,要想生效,还需要被组件扫描注解
@ComponentScan扫描。该注解虽然没有显式配置,但是实际上已经包含在了启动类声明注解
@SpringBootApplication中,默认扫描的范围是启动类所在包及其子包。
这段话有三层意思:
@ComponentScan);@SpringBootApplication 是个”组合注解”,它的内部已经包含了 @ComponentScan,所以我们从来没写过它、却发现 @Component 好使;com.itheima.controller、com.itheima.service、com.itheima.dao 必须待在启动类包(com.itheima)里面。
TliasManagementApplication 就在 src/main/java/com/itheima/ 下,controller、dao、pojo、service 四个包都是它的子包;“启动类所在包及其子包”里所有的 @Component/@Controller/@Service/@Repository 类才会被扫到、创建成 bean启动类本身(课程代码,注释直接写在注解后面):
1package com.itheima;2
3import org.springframework.boot.SpringApplication;4import org.springframework.boot.autoconfigure.SpringBootApplication;5
6@SpringBootApplication // 默认扫描当前包及其子包7public class SpringbootWebDemoApplication {8
9 public static void main(String[] args) {10 SpringApplication.run(SpringbootWebDemoApplication.class, args);11 }12
13}| PPT 的问题 | 答案 |
|---|---|
| 声明 bean 的注解有哪几个? | @Controller、@Service、@Repository、@Component(前三个是 @Component 的衍生注解) |
| 注意事项?(PPT 同页给的补充) | ① 在 SpringBoot 集成 web 开发中,声明控制器 bean 只能用 @Controller(或含它的组合注解);② 声明 bean 的注解要想生效,需要被扫描到——启动类默认扫描当前包及其子包 |
“默认只扫启动类所在包及其子包”这句结论,本机做了个实验来验:
本机实测(Spring Boot 3.2.8 / 内嵌 Tomcat / JDK 17)
把 @RestController 的控制器挪到启动类所在包之外:启动类是 com.itheima.SpringbootWebDemoApplication,控制器改成 com.outside.OutsideController(加了 @RestController、@RequestMapping("/outside"))。
1$ curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/outside2404现象很”安静”:应用照样启动成功、日志里没有报错,只是这个接口根本不存在(404)——因为 com.outside 不是 com.itheima 的子包,控制器类压根没被扫描到,@RequestMapping 映射自然也没注册。
所以遇到”类上注解都写对了,接口却 404 / 注入却说找不到 bean”,第一条就要检查包的位置:控制器、业务层、数据访问层的类,必须在启动类所在包及其子包里。(真要让包外的类生效,就得显式配置 @ComponentScan 把范围扩大——但课程规范做法是把代码放在启动类包下面。)
基于
@Autowired进行依赖注入的常见方式有如下三种。
三种写法的完整代码(以 UserController 注入 UserService 为例):
1// ① 属性注入:直接标在成员变量上2@RestController3public class UserController {4 @Autowired5 private UserService userService;6 // ……7}1// ② 构造器注入:写成构造方法的参数,标在构造方法上2@RestController3public class UserController {4 private final UserService userService;5
6 @Autowired7 public UserController(UserService userService) {8 this.userService = userService;9 }10 // ……11}1// ③ setter 注入:写成 setter 方法的参数,标在 setter 方法上2@RestController3public class UserController {4 private UserService userService;5
6 @Autowired7 public void setUserService(UserService userService) {8 this.userService = userService;9 }10 // ……11}PPT 第 69 页给每种方式都配了优缺点:
| 方式 | 优点(PPT 原文) | 缺点(PPT 原文) |
|---|---|---|
| ① 属性注入 | 代码简洁、方便快速开发 | 隐藏了类之间的依赖关系、可能会破坏类的封装性 |
| ② 构造器注入 | 能清晰地看到类的依赖关系、提高了代码的安全性 | 代码繁琐、如果构造参数过多,可能会导致构造函数臃肿 |
| ③ setter 注入 | 保持了类的封装性,依赖关系更清晰 | 需要额外编写 setter 方法,增加了代码量 |
还有一条注意(PPT 第 69 页最后一行,本机实测也印证了):
注意:如果只有一个构造函数,
@Autowired注解可以省略。
1@RestController2public class UserController {3
4 private final UserService userService;5
6 // 类里只有这一个构造方法 → @Autowired 可以不写,Spring 也会用它来完成注入7 public UserController(UserService userService) {8 this.userService = userService;9 }10}课程最终代码的 UserController 把三种写法都留着(注释形式),只把当前要用的那一种打开——这种”一个文件里放三种对比写法”的做法很值得学,改回来时不用翻讲义:
1//方式一: 属性注入2//@Autowired3//private UserService userService;4
5//方式二: 构造器注入6//private final UserService userService;7////@Autowired ---> 如果当前类中只存在一个构造函数, @Autowired可以省略8//public UserController(UserService userService) {9// this.userService = userService;10//}11
12//方式三: setter注入13//private UserService userService;14//@Autowired15//public void setUserService(UserService userService) {16// this.userService = userService;17//}PPT 第 70 页先给结论,再摆现场:
@Autowired注解,默认是按照类型进行注入的。如果存在多个相同类型的 bean,将会报出如下错误:

Field userService in com.itheima.controller.UserController required a single bean, but 2 were found,下面列出两个候选 bean(userServiceImpl、userServiceImpl2)以及它们各自的 class 文件路径,最后给出 Action 建议:把其中一个标成 @Primary、改成接受多个 bean、或用 @Qualifier 指明要哪一个本机实测(Spring Boot 3.2.8 / JDK 17)——写两个实现类 UserServiceImpl、UserServiceImpl2 都 implements UserService、都交给容器,而 UserController 里只写 @Autowired private UserService userService;:
1***************************2APPLICATION FAILED TO START3***************************4
5Description:6
7Field userService in com.itheima.controller.UserController required a single bean, but 2 were found:8 - userServiceImpl: defined in URL [...]9 - userServiceImpl2: defined in URL [...]10
11Action:12
13Consider marking one of the beans as @Primary, updating the consumer to accept multiple beans, or using @Qualifier to identify the bean that should be consumed异常链里的关键句(说明了根因):
1UnsatisfiedDependencyException: Error creating bean with name 'userController':2Unsatisfied dependency expressed through field 'userService':3No qualifying bean of type 'com.itheima.service.UserService' available:4expected single matching bean but found 2: userServiceImpl,userServiceImpl2关键句就一句话:“期望一个匹配的 bean,却找到了两个”——@Autowired 按类型找,UserService 类型下有两个候选,它不知道挑谁,于是在启动阶段就”罢工”了。这也和 PPT 第 70 页贴的报错一字不差。
PPT 给了三个方案,本机把三个都跑了一遍:
| 方案 | 写法 | 本机实测结果 |
|---|---|---|
方案一:@Primary | 在 UserServiceImpl 类上加 @Primary(意思是”同类型多个时,优先用它”) | 启动成功,注入的是 userServiceImpl(/list 返回的 id 是 1、2、3…) |
方案二:@Qualifier | @Qualifier("userServiceImpl") 和 @Autowired 一起标在成员变量上(按名字指定) | 启动成功,注入的是 userServiceImpl |
方案三:@Resource | @Resource(name = "userServiceImpl2") 标在成员变量上(JavaEE 规范的注解,也按名字) | 启动成功,注入的是 userServiceImpl2(/list 返回的 id 是 201、202…,因为 2 号实现类把 id 都加了 200) |
三种写法的代码对照(都改在 UserController / UserServiceImpl 上):
1// 方案一:@Primary —— 给"首选的那个"实现类贴个标签2@Service3@Primary // UserServiceImpl2 也存在,但优先注入我4public class UserServiceImpl implements UserService { /* …… */ }1// 方案二:@Qualifier —— 在注入处按名字点名2@RestController3public class UserController {4
5 @Autowired6 @Qualifier("userServiceImpl") // 名字 = 类名首字母小写7 private UserService userService;8}1// 方案三:@Resource —— 换一个注解,直接按名字注入2@RestController3public class UserController {4
5 @Resource(name = "userServiceImpl2") // 注意包是 jakarta.annotation.Resource6 private UserService userService;7}**怎么知道到底注入了哪一个?**本机用的办法很朴素:让 2 号实现类把 id 都加 200(id + 200)。
1注入 userServiceImpl → curl /list → [{"id":1,...},{"id":2,...}, …] (原始数据)2注入 userServiceImpl2 → curl /list → [{"id":201,...},{"id":202,...}, …] (加了 200 的那份)返回的 id 会”说话”,一眼就能看出容器挑了谁——这比只看日志靠谱。本机实测三种方案的行为都符合预期:方案一、二都进了 1 号实现,方案三进了 2 号实现。
另外,构造器注入与 setter 注入也一样能解决冲突(本机的对照实验):
| 对照实验 | 写法 | 实测结果 |
|---|---|---|
构造器注入 + @Primary | private final UserService userService; + 构造方法,不写 @Autowired | 启动成功(印证”只有一个构造函数时 @Autowired 可以省略”) |
setter 注入 + @Resource | @Resource(name = "userServiceImpl2") 标在 setter 方法上 | 启动成功,注入 userServiceImpl2 |
@Resource 与 @Autowired 的区别(PPT 第 71 页)#PPT 第 71 页先把依赖注入的注解总结成一张小表,再对比两个注解:
依赖注入的注解
@Autowired:默认按照类型自动装配- 如果同类型的 bean 存在多个:
@Primary/@Autowired+@Qualifier/@Resource
@Resource与@Autowired区别?
@Autowired是 Spring 框架提供的注解,而@Resource是 JavaEE 规范提供的@Autowired默认是按照类型注入,而@Resource默认是按照名称注入
整理成对照表:
@Autowired | @Resource | |
|---|---|---|
| 谁提供的 | Spring 框架 | JavaEE 规范(Spring Boot 3 里包名是 jakarta.annotation.Resource) |
| 默认注入方式 | 按类型(byType) | 按名称(byName) |
| 同类型多个 bean 时 | 需要配合 @Primary 或 @Qualifier 指定 | 直接写 @Resource(name = "bean名字") 指定即可 |
| 能不能用在构造器上 | 能(构造器注入的标配) | 不能(它标在字段/setter 上) |
| 本案例怎么选 | 只有一个实现时最省事 | 想明确”就要名字叫这个的 bean”时更直接 |
| 问题 | 答案 |
|---|---|
| 声明 bean 的四个注解? | @Component(基础注解)、@Controller(控制层)、@Service(业务层)、@Repository(数据访问层,与 mybatis 整合后用的少);后三个都是 @Component 的衍生注解 |
| bean 名字怎么定? | 注解的 value 属性可以指定名字(如 @Repository("userDao"));不指定时默认为类名首字母小写(UserServiceImpl → userServiceImpl) |
| 注解为什么能生效? | 四大注解要被组件扫描 @ComponentScan 扫到才行;@ComponentScan 虽然没显式配置,但已包含在启动类注解 @SpringBootApplication 里 |
| 默认扫描范围? | 启动类所在包及其子包;实测把控制器放到包外(com.outside)→ 启动正常但接口 404(映射根本没注册) |
| 三种注入方式? | 属性注入(简洁、快,但隐藏依赖关系、可能破坏封装)、构造器注入(依赖关系清晰、安全,但代码繁琐、参数多会臃肿)、setter 注入(保持封装、依赖清晰,但要多写 setter 方法) |
| 构造器注入的小福利? | 如果只有一个构造函数,@Autowired 可以省略 |
| 同类型多个 bean 会怎样? | @Autowired 默认按类型注入,遇到两个候选就启动失败:required a single bean, but 2 were found(本机实测报错原文) |
| 怎么解决? | ① 类上加 @Primary(首选);② 注入处用 @Autowired + @Qualifier("bean名字");③ 注入处用 @Resource(name = "bean名字")(三者实测都能启动成功) |
@Resource 与 @Autowired 的区别? | 来源不同(Spring 框架 vs JavaEE 规范);默认注入方式不同(按类型 vs 按名称) |
@Component(声明 bean 的基础注解,不属于下面三类时用)、@Controller(控制层)、@Service(业务层)、@Repository(数据访问层,由于与 mybatis 整合,用的少);后三个都是 @Component 的衍生注解@Controller(或 @RestController);声明 bean 的注解要被扫描到才生效@Repository("userDao"));没有指定时默认为类名首字母小写(UserServiceImpl → userServiceImpl)@ComponentScan 扫到才生效;它虽然没有显式配置,但实际上已经包含在启动类注解 @SpringBootApplication 中,默认扫描的范围是启动类所在包及其子包——实测把控制器放到包外(com.outside.OutsideController,启动类在 com.itheima):应用启动正常、日志无报错,但访问那个接口是 404(映射根本没注册)@Autowired 标在成员变量上)、构造器注入(标在构造方法上,字段常用 private final)、setter 注入(标在 setter 方法上)@Autowired 注解可以省略(本机实测:构造器不写 @Autowired 配 @Primary → 启动成功)@Autowired 默认按类型注入;两个实现类都交给容器时报 Field userService ... required a single bean, but 2 were found(异常链:No qualifying bean of type ... available: expected single matching bean but found 2: userServiceImpl,userServiceImpl2)@Primary(实测注入 userServiceImpl);② @Autowired + @Qualifier("userServiceImpl")(实测注入 userServiceImpl);③ @Resource(name = "userServiceImpl2")(实测注入 userServiceImpl2)@Resource 与 @Autowired 的区别:@Autowired 是 Spring 框架提供的注解、默认按照类型注入;@Resource 是 JavaEE 规范提供的、默认按照名称注入2-1 给三层的类挑”最贴切”的 bean 注解 一个三层架构的工程里,有三个类要交给 IOC 容器管理:
com.itheima.controller.EmpController(控制层,处理请求、响应数据);com.itheima.service.impl.EmpServiceImpl(业务层);com.itheima.dao.impl.EmpDaoImpl(数据访问层)。要求:
empDao 的写法。
(练习文件 test_39_声明Bean的注解.java 里给了三个类的骨架和写作区。)一级 · 思路:四个注解里,一个”通用”,另外三个分别对应三层——按类所在的层选;默认名字记”类名首字母变小写”
二级 · 方法:控制层 @Controller(本案例其实用 @RestController)、业务层 @Service、数据访问层 @Repository;指定名字写在注解括号里(value 属性)
三级 · 骨架:@____ / @____ / @____("____");默认名字 = empController? 注意——bean 名字由类名决定
1// ① 控制层:SpringBoot 集成 web 开发中,声明控制器 bean 只能用 @Controller2// (更常用的是它的组合注解 @RestController = @Controller + @ResponseBody)3@RestController4public class EmpController { /* …… */ }1// ② 业务层2@Service3public class EmpServiceImpl implements EmpService { /* …… */ }1// ③ 数据访问层(顺手把 bean 名字显式指定为 empDao)2@Repository("empDao")3public class EmpDaoImpl implements EmpDao { /* …… */ }@Controller(或 @RestController):在 SpringBoot 集成 web 开发中,声明控制器的 bean 只能用 @Controller——SpringMVC 靠它(以及 @RequestMapping 等)识别请求处理类;换成 @Component / @Service 之类的名字虽然也能被扫描成 bean,但不是”控制器”的规范写法。| 类名 | 默认 bean 名字 |
|---|---|
EmpController | empController |
EmpServiceImpl | empServiceImpl |
EmpDaoImpl | empDaoImpl(上面显式指定后是 empDao) |
2-2 三种依赖注入方式各写一遍
一个控制器 DeptController 需要使用业务对象 DeptService(接口)。请用三种不同的写法完成注入:
test_39_三种依赖注入方式.java 里按三种写法给了写作区。)一级 · 思路:三种写法的区别只在”依赖从哪个入口进来”——成员变量、构造方法、setter 方法
二级 · 方法:注解都是 @Autowired,只是标的位置不同;构造器写法里字段用 private final;“可以不写注解”的那一种,前提和”类里有几个构造方法”有关
三级 · 骨架:@____ private DeptService deptService; / private final DeptService deptService; + public DeptController(DeptService deptService) { this.deptService = ____; } / public void setDeptService(DeptService deptService) { this.deptService = ____; }
1// ① 属性注入2@RestController3public class DeptController {4 @Autowired5 private DeptService deptService;6 // 优点:代码简洁、方便快速开发7 // 缺点:隐藏了类之间的依赖关系、可能会破坏类的封装性8}1// ② 构造器注入2@RestController3public class DeptController {4
5 private final DeptService deptService;6
7 @Autowired8 public DeptController(DeptService deptService) {9 this.deptService = deptService;10 }11 // 优点:能清晰地看到类的依赖关系、提高了代码的安全性12 // 缺点:代码繁琐、如果构造参数过多,可能会导致构造函数臃肿13}1// ③ setter 注入2@RestController3public class DeptController {4
5 private DeptService deptService;6
7 @Autowired8 public void setDeptService(DeptService deptService) {9 this.deptService = deptService;10 }11 // 优点:保持了类的封装性,依赖关系更清晰12 // 缺点:需要额外编写 setter 方法,增加了代码量13}@Autowired 注解可以省略;本机实测构造器不写 @Autowired 也能启动成功)。 2-3 两个同类型的 bean 打起来了,给三个解决方案
工程里有一个业务接口 UserService,现在有两个实现类都被交给了容器:
1@Service2public class UserServiceImpl implements UserService { /* …… */ }3
4@Service5public class UserServiceImpl2 implements UserService { /* …… */ }控制层里是这么写的:
1@RestController2public class UserController {3 @Autowired4 private UserService userService;5 // ……6}启动工程时直接失败。要求:
test_39_同类型多个Bean的冲突.java 里给了报错原文和三种方案的写作区。)一级 · 思路:报错的实质是”要一个、给了俩”;三种改法分别是”指定首选""按名字点名""换一个按名字注入的注解”
二级 · 方法:① 实现类上加 @Primary;② 注入处 @Qualifier("userServiceImpl") 配 @Autowired;③ 注入处 @Resource(name = "userServiceImpl2")
三级 · 骨架:@____ public class UserServiceImpl implements UserService {...} / @____ @____("userServiceImpl") private UserService userService; / @____(name = "____") private UserService userService;
1Field userService in com.itheima.controller.UserController required a single bean, but 2 were found:2 - userServiceImpl: defined in URL [...]3 - userServiceImpl2: defined in URL [...]@Autowired 默认按照类型注入——它拿着 UserService 这个类型去容器里找,找到了两个候选 bean(userServiceImpl 和 userServiceImpl2),规则是”一个萝卜一个坑”、又不能替开发者做选择,于是启动阶段直接失败。| 改法 | 改哪里 | 改什么 | 最终注入 |
|---|---|---|---|
① @Primary | 实现类上 | 给 UserServiceImpl 加 @Primary(首选) | userServiceImpl(/list 返回 id 1、2、3…) |
② @Autowired + @Qualifier | 注入处(成员变量上) | @Qualifier("userServiceImpl") 指明 bean 名字 | userServiceImpl |
③ @Resource | 注入处 | @Resource(name = "userServiceImpl2")(按名称注入) | userServiceImpl2(/list 返回 id 201、202…,因为 2 号实现类把 id 加了 200) |
List<UserService>(或在构造器里接收 List<UserService> / Map<String, UserService>),容器会把所有同类型的 bean 都给你,自己按需挑选。另外也可以用 ApplicationContext 按名字主动 getBean("userServiceImpl") 取(本机报错的 Action 里就提到了 “updating the consumer to accept multiple beans” 这种思路)。
1@Autowired2private List<UserService> userServiceList; // 拿到两个实现3// 或者4@Autowired5private Map<String, UserService> userServiceMap; // key 就是 bean 名字 2-4 接口为什么又 404 了(扫描范围排查)
小张把一个正常运行的工程改了目录结构:启动类还在 com.itheima(SpringbootWebDemoApplication),但他把控制器挪到了 com.itheima.other 之外的新包 com.outside 里(类名 OutsideController,注解 @RestController + @RequestMapping("/outside") 都写了)。重启后:
http://localhost:8080/outside 是 404。请回答:
test_39_组件扫描范围排查.java 里给了写作区。)一级 · 思路:注解只负责”打标记”,谁来读这些标记才是关键——找到”负责扫描的那个注解”和它的默认范围
二级 · 方法:@SpringBootApplication 里已包含 @ComponentScan,默认只扫启动类所在包及其子包;要么把类挪回 com.itheima 下面,要么显式加 @ComponentScan({"com.itheima", "com.outside"})
三级 · 骨架:@SpringBootApplication + @____({"com.itheima","com.outside"})(加在启动类上)
@SpringBootApplication 内部已包含 @ComponentScan,默认扫描的范围是启动类所在包及其子包。com.outside 不是 com.itheima 的子包 → 这个 @RestController 类没有被创建成 bean、@RequestMapping 也没有被注册成映射 → 应用当然能启动(没有 bean 也不算错),访问这个地址就是 404。本机实测(Spring Boot 3.2.8)就是这个结果:
1$ curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/outside24041// 办法一:把类挪回启动类所在包及其子包(课程规范做法)2// com.outside.OutsideController → com.itheima.controller.OutsideController1// 办法二:显式配置扫描范围(把包外那个包也加进来)2@SpringBootApplication3@ComponentScan({"com.itheima", "com.outside"}) // 覆盖默认范围4public class SpringbootWebDemoApplication {5 public static void main(String[] args) {6 SpringApplication.run(SpringbootWebDemoApplication.class, args);7 }8}UserDaoImpl 放到包外时,UserServiceImpl 里 @Autowired private UserDao userDao; 会在启动阶段报”找不到类型为 UserDao 的 bean”(No qualifying bean of type 'com.itheima.dao.UserDao' available 这一类),而不是等到运行时报错——注入发生在启动阶段,所以扫不到 bean 时启动就失败;控制器是”注册映射”,扫不到只是 404,不影响启动。两者的检查顺序都是:先看包的位置,再看注解有没有写、有没有写错位置。 3-1 把”注解 + 注入 + 冲突”三件事在一个工程里全过一遍
在上一篇改造好的”三层 + IOC/DI”工程上继续做,每一步都验证一次 /list:
@RestController、业务层用 @Service、数据访问层用 @Repository,并在注释里写出它们的默认 bean 名字;顺手把数据访问层的 bean 名字显式改成 userDaoImpl(value 属性);@Service("userServiceA")(显式命名),观察工程能否正常启动、/list 是否照旧;@Autowired 去掉,确认仍能启动;@Primary / @Qualifier / @Resource),每次都用 curl -s http://localhost:8080/list 的 id 是 1 开头还是 201 开头判断究竟注入了哪一个,把三次结果记到练习文件里;@ComponentScan 显式指定范围),确认恢复 200;test_39_综合_注解注入与冲突处理.java 里按这 7 步给了写作区。)涉及知识点
| 知识点 | 在这里的应用 |
|---|---|
| 四大注解 | @RestController(控制层)、@Service(业务层)、@Repository(数据访问层)、@Component(通用) |
| bean 名字 | 默认类名首字母小写;用注解的 value 属性显式指定(@Service("userServiceA")) |
| 组件扫描 | @SpringBootApplication 内含 @ComponentScan,默认只扫启动类包及其子包 |
| 三种注入方式 | 属性注入 / 构造器注入(单构造方法可省注解)/ setter 注入 |
| 按类型 vs 按名称 | @Autowired 按类型;@Resource 按名称;@Qualifier 给按类型加一个”按名字”的限定 |
| 冲突排查 | required a single bean, but 2 were found → 用数据(id 是否 +200)判断实际注入了谁 |
一级 · 思路:注解决定”哪些对象进容器、叫什么名字”,注入注解决定”从容器里拿哪一个”;冲突的本质是”类型相同、名字不同,容器不敢替你选”
二级 · 方法:按层换注解(@RestController / @Service / @Repository);注入方式就是 @Autowired 换三个位置(成员变量 / 构造方法 / setter);冲突三招 @Primary / @Qualifier("名字") / @Resource(name = "名字");扫描范围检查”包在不在启动类下面”
三级 · 骨架:@____("userServiceA") / @____(name = "____") / 报错关键词 = required a single bean, but 2 were found
3-1 七步结果:
1@RestController public class UserController { } // 默认 bean 名 userController2@Service public class UserServiceImpl implements UserService { } // userServiceImpl3@Repository("userDaoImpl") public class UserDaoImpl implements UserDao { } // 显式写成了 userDaoImpl(默认本来是 userDaoImpl,这里等于写全)@Service("userServiceA") 后:启动正常、/list 照旧——因为注入处还是按类型找(@Autowired private UserService userService;),容器里只有一个 UserService 类型的 bean,名字叫什么不影响。/list 都是 200 + application/json):
1// 属性注入2@Autowired private UserService userService;3// 构造器注入(只有一个构造方法 → @Autowired 可以省略)4private final UserService userService;5public UserController(UserService userService) { this.userService = userService; }6// setter 注入7@Autowired public void setUserService(UserService userService) { this.userService = userService; }1Field userService in com.itheima.controller.UserController required a single bean, but 2 were found:2 - userServiceImpl: defined in URL [...]3 - userServiceImpl2: defined in URL [...]No qualifying bean of type 'com.itheima.service.UserService' available: expected single matching bean but found 2: userServiceImpl,userServiceImpl2| 方案 | 写法 | /list 返回的 id | 说明 |
|---|---|---|---|
@Primary | UserServiceImpl 类上加 @Primary | 1、2、3… | 注入了 1 号实现 |
@Qualifier | @Qualifier("userServiceImpl") + @Autowired | 1、2、3… | 按名字点名 1 号实现 |
@Resource | @Resource(name = "userServiceImpl2") | 201、202… | 按名字点名 2 号实现(它把 id 加了 200) |
curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/outside 得到 404(应用能启动);挪回来或给启动类加 @ComponentScan({"com.itheima","com.outside"}) 后恢复 200。@Component/@Service/@Repository(别加在接口上),注入处有没有 @Autowired(别写在方法体里的局部变量上);
③ 最后看有没有冲突——同类型是不是有多个 bean(required a single bean, but 2 were found),是的话用 @Primary / @Qualifier / @Resource 指定,并用”数据特征”(比如 id 有没有 +200)确认实际注入了哪一个。如果你喜欢,那么欢迎来到我的世界!
了解更多暂未播放



"所有的相遇都是久别重逢。"—— 未知


