文章目录
-
- [一、前后端分离下的 Java 后端三层架构](#一、前后端分离下的 Java 后端三层架构)
-
- [1.1 三层定义](#1.1 三层定义)
- [1.2 代码分层对应示例](#1.2 代码分层对应示例)
- [1.3 分层规范命名](#1.3 分层规范命名)
- [二、Spring 两大核心:IoC 与 AOP(重点:IoC)](#二、Spring 两大核心:IoC 与 AOP(重点:IoC))
-
- [2.1 IoC 概念](#2.1 IoC 概念)
- [2.2 IoC 容器的两大操作](#2.2 IoC 容器的两大操作)
- [2.3 IoC 容器的优势](#2.3 IoC 容器的优势)
- [三、将 Bean 交给 Spring 管理的两类注解](#三、将 Bean 交给 Spring 管理的两类注解)
-
- [3.1 五大类注解(写在类上)](#3.1 五大类注解(写在类上))
- [3.2 @Bean 方法注解(写在配置类的方法上)](#3.2 @Bean 方法注解(写在配置类的方法上))
- [3.3 容器获取 Bean](#3.3 容器获取 Bean)
- [四、DI 依赖注入的三种方式](#四、DI 依赖注入的三种方式)
-
- [4.1 三种注入方式代码示例](#4.1 三种注入方式代码示例)
- [4.2 三种注入方式优缺点对比](#4.2 三种注入方式优缺点对比)
- [五、@Autowired 多 Bean 冲突问题与解决方案](#五、@Autowired 多 Bean 冲突问题与解决方案)
-
- [5.1 问题根源](#5.1 问题根源)
- [5.2 三种解决方案](#5.2 三种解决方案)
- [5.3 @Autowired vs @Resource 核心区别](#5.3 @Autowired vs @Resource 核心区别)
- 全文总结
- 核心知识点复盘
- [常见问题 / 避坑指南](#常见问题 / 避坑指南)
本文以 Spring Boot 为基石,系统梳理后端三层架构设计思想、IoC 控制反转的底层逻辑、Bean 管理注解的分类与用法、DI 依赖注入的三种方式及其选型依据,最后深入剖析多 Bean 冲突的解决方案。全文以"概念 → 原理 → 实战 → 避坑"为主线,适合学习复盘与技术分享。
一、前后端分离下的 Java 后端三层架构
1.1 三层定义
在现代前后端分离架构中,后端 Java 项目的代码组织通常遵循三层架构 模式。每一层各司其职,边界清晰:

表现层(Controller)
距离用户最近的层级。负责接收前端发送的 HTTP 请求,调用业务层处理逻辑,最终将处理结果封装为响应数据返回前端。Controller 本身不应该包含复杂的业务判断------它只做"接待和转交"。
场景代入 :电商商品列表接口。Controller 接收
GET /product/list?page=1&size=20请求,调用 Service 获取数据后,将包含商品图片、价格、店铺信息的列表以 JSON 格式返回给前端。
业务逻辑层(Service)
项目的核心大脑。所有业务规则、数据加工、复杂计算都在这一层完成。Service 接收 Controller 的调用,协调多个 Dao 完成数据读写,并在中间插入业务逻辑。
场景代入:商品原价叠加国家补贴计算、汇总用户评价数量和星级、将库存数字转为"可购/缺货"的文字标签。这些逻辑与数据存储无关,与页面展示也无关------它们属于"业务规则"。
数据层(Dao / Repository)
与数据库直接交互的唯一入口。执行 SQL 或 ORM 操作,完成数据的增删改查,将数据库记录映射为 Java 对象,供 Service 层使用。
场景代入:从数据库中查询商品原价、库存数量、店铺信息等原始数据,返回给 Service 做进一步加工。
三层之间的调用关系可以用下图直观理解:
[前端请求] → Controller → Service → Dao → [数据库]
↑ ↑ ↑
接收请求 业务处理 数据访问
返回响应 数据加工 CRUD操作

核心原则:上层依赖下层,下层不感知上层。Controller 知道 Service 的存在,Service 知道 Dao 的存在,但反过来 Dao 完全不需要知道是谁在调用它。
1.2 代码分层对应示例
以下是一个简化但完整的图书管理场景,展示三层如何协作:
java
// ======================== Dao 层:模拟数据库查询 ========================
// 实际项目中这里会使用 MyBatis / JPA 与真实数据库交互
public List<Book> mockData() {
List<Book> books = new ArrayList<>();
books.add(new Book("Spring实战", "张三", 1)); // status=1 表示可借阅
books.add(new Book("Java编程思想", "李四", 0)); // status=0 表示已借出
return books;
}
// ======================== Controller 层:接收请求、返回响应 ========================
@RequestMapping("/getList")
public List<Book> getList() {
// 1. 调用 Dao 层获取原始数据
List<Book> books = mockData();
// 2. 调用 Service 层做业务处理(状态码 → 中文描述)
for (Book book : books) {
if (book.getStatus() == 1) {
book.setStatusCN("可借阅");
} else {
book.setStatusCN("不可借阅");
}
}
// 3. 封装结果返回前端
return books;
}
代码思路解析:
- Dao 层只做一件事:从"数据源"获取原始记录,不掺杂任何业务判断。
- Service 层的逻辑是"状态码转中文描述"------这是典型的业务加工,与展示方式无关。
- Controller 层编排流程:先取数据、再加工、最后返回。它不关心数据从哪来,也不关心加工细节。
关键点 :三层分离的本质是关注点分离。当需求变化时,只需要修改对应的层------比如数据库从 MySQL 换成 PostgreSQL,只改 Dao 层;业务规则调整,只改 Service 层;接口返回格式变化,只改 Controller 层。
1.3 分层规范命名
在 Spring 项目中,有一套约定俗成的命名规范,遵循它能让团队协作更高效:
| 规范项 | 说明 | 示例 |
|---|---|---|
| 类名 | 大驼峰(PascalCase) | UserController、BookService、BookDao |
| Bean 默认名称 | 类名首字母小写(小驼峰) | UserController → userController |
| 特殊命名规则 | 类名前两位均大写时,Bean 名与类名一致 | UController → UController |
第二条规则背后的原因:Java Bean 命名规范要求将类名首字母转为小写来生成默认 Bean 名称。但如果前两个字母都是大写(如 UController),则保持类名原样,因为 Spring 认为这可能是一个缩写词(如 URLParser),不应该破坏其大写结构。
java
// 类名:UserController → Bean 默认名称:userController
@Controller
public class UserController { }
// 类名:UController → Bean 默认名称:UController(前两位大写,保持原样)
@Controller
public class UController { }
这个细节在实际开发中容易踩坑------当你用 getBean("uController") 获取 UController 时会报错,正确的名称是 "UController"。
二、Spring 两大核心:IoC 与 AOP(重点:IoC)
Spring 框架有两大核心思想:IoC(控制反转) 和 AOP(面向切面编程)。本文聚焦 IoC,它是 Spring 的基石,也是理解后续 DI(依赖注入)的前提。
2.1 IoC 概念
IoC(Inversion of Control,控制反转) :将对象(Bean)的创建、销毁、生命周期管理的控制权 ,从业务代码手中反转给 Spring 第三方容器。
这个概念比较抽象,我们用一个汽车制造的场景来对比说明。
传统方式(v1:上层控制下层)
java
// Tire.java ------ 轮胎,最底层组件
public class Tire {
private int size;
public Tire(int size) {
this.size = size;
System.out.println("tire size: " + size);
}
}
// Bottom.java ------ 底盘,依赖轮胎
public class Bottom {
private Tire tire;
public Bottom(int size) {
this.tire = new Tire(size); // ⚠️ 自己 new 轮胎
System.out.println("bottom init...");
}
}
// Framework.java ------ 车架,依赖底盘
public class Framework {
private Bottom bottom;
public Framework(int size) {
this.bottom = new Bottom(size); // ⚠️ 自己 new 底盘
System.out.println("framework init...");
}
}
// Car.java ------ 整车,依赖车架
public class Car {
private Framework framework;
public Car(int size) {
this.framework = new Framework(size); // ⚠️ 自己 new 车架
System.out.println("car init...");
}
public void run() {
System.out.println("car run...");
}
}
// Main.java ------ 启动
public class Main {
public static void main(String[] args) {
Car car = new Car(18);
car.run();
}
}
问题分析 :Car 控制 Framework 的创建,Framework 控制 Bottom 的创建,Bottom 控制 Tire 的创建。每一层都主动 new 自己的依赖。当底层 Tire 的需求变化时------比如新增一个 color 参数------你会发现从 Bottom → Framework → Car 全部需要修改,牵一发而动全身。这就是高耦合的代价。
IoC 解耦后(v2:容器控制依赖,下层注入上层)
java
// Tire.java ------ 新增 color 属性,但这就是唯一的改动点
public class Tire {
private int size;
private String color;
public Tire(int size, String color) {
this.size = size;
this.color = color;
System.out.println("tire size: " + size + ", color: " + color);
}
}
// Bottom.java ------ 不再自己 new,改为接收注入
public class Bottom {
private Tire tire;
public Bottom(Tire tire) { // 通过构造方法接收
this.tire = tire;
System.out.println("bottom init...");
}
}
// Framework.java ------ 同理
public class Framework {
private Bottom bottom;
public Framework(Bottom bottom) {
this.bottom = bottom;
System.out.println("framework init...");
}
}
// Car.java ------ 同理
public class Car {
private Framework framework;
public Car(Framework framework) {
this.framework = framework;
System.out.println("car init...");
}
public void run() {
System.out.println("car run...");
}
}
// Main.java ------ 由外部统一组装,控制权反转
public class Main {
public static void main(String[] args) {
// 先创建最底层,再逐层注入上层------这就是 IoC 的思想
Tire tire = new Tire(18, "red");
Bottom bottom = new Bottom(tire);
Framework framework = new Framework(bottom);
Car car = new Car(framework);
car.run();
}
}
对比总结:
| 维度 | 传统方式(v1) | IoC 方式(v2) |
|---|---|---|
| 对象创建者 | 每个类自己 new 依赖 |
外部组装,通过构造方法传入 |
| 修改成本 | Tire 加一个参数,所有上层全改 | Tire 加 color,只改 Main 中的装配代码 |
| 耦合度 | 高(上层依赖下层具体实现) | 低(上层只依赖下层抽象) |
| 控制权 | 在业务代码手中 | 反转给了外部(容器/Main) |
一句话总结 IoC :原来是我们自己 new 对象控制一切,现在是 Spring 容器帮我们创建和管理对象,我们需要时容器直接"送过来"------控制权反转了。
2.2 IoC 容器的两大操作
Spring IoC 容器本质上是一个"对象管家",它提供两个核心操作:
-
存 Bean(IoC) :把对象交给 Spring 容器管理。通过注解(
@Component、@Service等)标记类,Spring 启动时会扫描并实例化它们。 -
取 Bean(DI,依赖注入) :容器自动将依赖对象赋值给需要它的类。通过
@Autowired等注解,Spring 在运行时把对应的 Bean 注入到目标位置。存:@Component / @Service / @Bean ... → 告诉 Spring "这个类归你管"
取:@Autowired / @Resource ... → 告诉 Spring "我需要那个 Bean"
2.3 IoC 容器的优势
- 资源集中管理:所有对象由容器统一创建、配置、销毁,无需在代码中分散管理生命周期。
- 降低耦合度 :依赖关系由容器维护,底层修改不影响上层代码------如 v2 示例中 Tire 新增
color参数,Car、Framework、Bottom 的代码完全不变。 - 简化开发 :开发者不需要手动
new对象和管理依赖链,专注业务逻辑即可。
三、将 Bean 交给 Spring 管理的两类注解
Spring 提供两种方式将对象注册为容器管理的 Bean:五大类注解 和 @Bean 方法注解。
3.1 五大类注解(写在类上)
五大注解中,@Controller、@Service、@Repository、@Configuration 本质上都是 @Component 的衍生注解,功能完全一致,只是用于标记类所处的层级,增强代码可读性。
| 注解 | 分层归属 | 作用 |
|---|---|---|
@Controller |
表现层 | 标记控制器,接收前端请求 |
@Service |
业务层 | 标记业务逻辑类 |
@Repository |
数据层 | 标记数据库操作类 |
@Configuration |
配置层 | 标记配置类,存放 @Bean 定义 |
@Component |
通用组件 | 不属于以上分层的工具类 |

实际项目中的使用示例:
java
// 表现层 ------ 接收请求
@Controller
public class UserController {
// 处理用户相关 HTTP 请求
}
// 业务层 ------ 处理业务逻辑
@Service
public class UserService {
public void hello() {
System.out.println("hello, UserService...");
}
}
// 配置层 ------ 存放项目配置和 @Bean 定义
@Configuration
public class UserConfiguration {
public void hello() {
System.out.println("hello, UserConfiguration...");
}
}
// 通用组件 ------ 不属于上面任何层的工具类
@Component("userComponent2") // 可自定义 Bean 名称
public class UserComponent {
public void hello() {
System.out.println("hello, UserComponent...");
}
}
关键理解 :这五个注解在 Spring 容器眼中没有本质区别,换成
@Component项目一样能跑。使用@Service而非@Component纯粹是为了让读代码的人一眼看出这个类的职责。
3.2 @Bean 方法注解(写在配置类的方法上)
@Bean 注解用于方法级别 ,必须放在带有五大注解(通常是 @Configuration 或 @Component)的类中,将方法的返回值注册为 Spring 容器中的一个 Bean。
适用场景:
- 管理第三方库的类 :第三方 jar 包中的类,你无法修改源码添加
@Component,只能用@Bean方法返回它的实例。 - 同一类型需要多个不同实例:比如配置多个数据源、多个 Redis 连接,每个实例用不同的方法名区分。
java
@Component
public class UserComponent {
// 方法名 "userInfo" 即 Bean 的默认名称
@Bean
public UserInfo userInfo() {
return new UserInfo("zmt", "123456"); // 用户名 zmt,密码 123456
}
// @Primary 标记此 Bean 为同类型中的首选
@Primary
@Bean
public UserInfo userInfo2() {
return new UserInfo("zm", "12345"); // 另一个配置的用户
}
}
代码思路解析 :上面的代码在 Spring 容器中创建了两个 UserInfo 类型的 Bean------一个叫 userInfo,一个叫 userInfo2。当通过类型获取 UserInfo 时,Spring 默认会选 @Primary 标记的 userInfo2,避免因多个同类型 Bean 而报错。
Bean 命名规则补充:
| 注解方式 | 默认 Bean 名称 | 示例 |
|---|---|---|
| 五大类注解 | 类名首字母小写 | UserService → userService |
| 五大类注解(前两位大写) | 保持类名不变 | UController → UController |
@Bean 方法注解 |
方法名 | userInfo() → userInfo |
| 自定义名称 | 通过 value 属性 |
@Component("myName") 或 @Bean("myName") |
补充:value 属性是字符串数组,单个值时可以省略大括号 {}。一个 Bean 可以设置多个别名(如 @Bean({"userInfo1", "u1"})),同一个类型可以在容器中存在多个不同名称的 Bean。
3.3 容器获取 Bean
Spring 通过 ApplicationContext(应用上下文)提供 getBean() 方法来获取容器中的 Bean。有三种重载方式:
java
@SpringBootApplication
public class SpringIoCApplication {
public static void main(String[] args) {
// 启动 Spring 容器,返回上下文对象
ConfigurableApplicationContext context =
SpringApplication.run(SpringIoCApplication.class, args);
// 方式1:按类型获取(局限性:同类型多个 Bean 时会报错)
UserController bean = context.getBean(UserController.class);
// 方式2:按名称获取(需要强制类型转换)
UserController bean1 = (UserController) context.getBean("userController");
// 方式3:按名称 + 类型获取(推荐,类型安全且精准)
UserController bean2 = context.getBean("userController", UserController.class);
// 验证:三种方式获取的是同一个对象(Spring Bean 默认单例)
System.out.println(bean == bean1); // true
System.out.println(bean == bean2); // true
}
}
注意 :
@SpringBootApplication默认只扫描启动类所在包及其子包。如果 Bean 定义在外层包中,需要额外使用@ComponentScan("com.zmt.springioc")指定扫描路径,否则getBean()会抛出NoSuchBeanDefinitionException。
四、DI 依赖注入的三种方式
DI(Dependency Injection,依赖注入)是 IoC 的具体实现------容器在创建 Bean 时,自动将它所依赖的其他 Bean 注入进来。Spring 中主要通过 @Autowired 实现自动装配。
4.1 三种注入方式代码示例
方式一:属性注入
java
@Controller
public class UserController {
@Autowired
private UserService userService; // 直接在字段上标注
public void hello() {
userService.hello();
}
}
最简洁的写法,Spring 通过反射直接把 Bean 赋值给字段。
方式二:构造方法注入(推荐)
java
@Controller
public class UController {
private final UserService userService; // ✅ 支持 final 修饰
// 当类只有一个构造方法时,@Autowired 可以省略
public UController(UserService userService) {
this.userService = userService;
}
public void hello() {
userService.hello();
}
}
Spring 在实例化 Bean 时调用构造方法,将依赖作为参数传入。这是目前最推荐的方式。
方式三:Setter 方法注入
java
@Controller
public class UController {
private UserService userService;
@Autowired
public void setUserService(UserService userService) {
this.userService = userService;
}
public void hello() {
userService.hello();
}
}
Spring 先通过无参构造创建对象,再调用 Setter 方法注入依赖。
4.2 三种注入方式优缺点对比
| 维度 | 属性注入 | 构造方法注入 | Setter 注入 |
|---|---|---|---|
| 代码简洁度 | ✅ 最简洁 | ❌ 多依赖时代码较长 | 中等 |
支持 final |
❌ 不支持 | ✅ 支持 | ❌ 不支持 |
| 不可变性 | ❌ 注入后可被修改 | ✅ 构造后不可变 | ❌ setter 可多次调用 |
| 空指针风险 | ❌ 使用时才暴露 | ✅ 初始化时即完成,无空指针 | 中等 |
| 脱离框架可用 | ❌ 仅 IoC 容器可用 | ✅ 可手动 new 传参 | 中等 |
| 运行时修改 | ❌ 不支持 | ❌ 不支持 | ✅ 可重新配置 |
推荐结论 :日常开发优先使用构造方法注入。理由有三:
- 依赖明确:构造方法的参数列表清楚告诉你这个类需要哪些依赖,不会出现"依赖越加越多而无人察觉"的情况。
- 不可变性 :配合
final关键字,依赖一旦注入就不会被篡改,安全性更高。 - 测试友好 :单元测试时可以直接
new UserController(mockUserService)传入 mock 对象,无需启动 Spring 容器。
补充说明:历史版本中 Spring 3.x 推荐 Setter 注入,Spring 4.x 起官方推荐构造方法注入。如果你看到旧项目大量使用 Setter 注入,那是历史原因,新代码建议统一使用构造方法注入。
五、@Autowired 多 Bean 冲突问题与解决方案
5.1 问题根源
@Autowired 的默认匹配规则是按类型(byType )查找。当容器中同一个接口(或父类)存在多个实现类的 Bean 时,Spring 无法判断该选哪一个,抛出 NoUniqueBeanDefinitionException 异常。
java
// 容器中存在两个 UserInfo 类型的 Bean:userInfo 和 userInfo2
// 以下代码会直接报错 ------ Spring 不知道你要哪个
@Autowired
private UserInfo userInfo; // ❌ NoUniqueBeanDefinitionException
5.2 三种解决方案
方案一:@Primary ------ 设置默认首选
在其中一个 Bean 上标注 @Primary,当按类型匹配到多个 Bean 时,优先选择它。
java
@Component
public class UserComponent {
@Bean
public UserInfo userInfo() {
return new UserInfo("zmt", "123456");
}
@Primary // 标记为同类型的默认选择
@Bean
public UserInfo userInfo2() {
return new UserInfo("zm", "12345");
}
}
适用场景 :大部分情况下都用这个 Bean,少数特殊情况单独指定。比如项目中 90% 的地方用主数据源,只有报表模块用只读数据源------主数据源加 @Primary,报表模块用 @Qualifier 单独指定。
方案二:@Qualifier ------ 精确指定 Bean 名称
搭配 @Autowired,通过 Bean 名称精确指定要注入哪一个。
java
@Controller
public class UserController {
@Autowired
@Qualifier("userInfo2") // 指定注入名为 "userInfo2" 的 Bean
private UserInfo userInfo;
public void hello() {
System.out.println(userInfo); // 输出 userInfo2 的实例
}
}
适用场景 :需要精确控制注入哪个 Bean,不受 @Primary 影响。
方案三:@Resource ------ JDK 原生注解,默认按名称匹配
java
@Controller
public class UserController {
@Resource(name = "userInfo2") // JDK 原生,直接指定 Bean 名称
private UserInfo userInfo;
public void hello() {
System.out.println(userInfo);
}
}
@Resource 是 Java 标准注解(javax.annotation.Resource),不依赖 Spring,默认按名称 匹配。如果 name 匹配失败,会退化为按类型匹配。
5.3 @Autowired vs @Resource 核心区别
| 维度 | @Autowired | @Resource |
|---|---|---|
| 来源 | Spring 框架提供 | JDK 原生自带(jakarta.annotation.Resource) |
| 默认匹配规则 | 优先按类型匹配 | 优先按 Bean 名称匹配 |
| 名称指定方式 | 无 name 属性,需配合 @Qualifier("xxx") |
直接通过 name 属性指定 @Resource(name="xxx") |
| 找不到时行为 | 抛异常(required=false 除外) | 名称找不到时退化为按类型查找 |
| 框架耦合 | 与 Spring 强耦合 | 与 Spring 解耦,可迁移到其他 IoC 框架 |
选型建议:
- 项目确定长期使用 Spring →
@Autowired+ 构造方法注入是标准组合。 - 需要框架无关性或代码可移植性 → 使用
@Resource。 - 团队规范优先,两个都能用,保持一致性最重要------不要在一个项目中混用两种风格。
全文总结
本文以 Spring Boot 为实践环境,从三层架构设计 出发,梳理了 Controller → Service → Dao 的职责边界与命名规范;进而深入 Spring 两大核心之一 IoC 控制反转 ,通过汽车装配的 v1→v2 演进直观展示了"控制反转"如何降低代码耦合;接着系统讲解了 Bean 管理的两类注解 (五大类注解 + @Bean 方法注解)及 getBean() 的三种获取方式;然后对比了 DI 依赖注入的三种写法 ,给出"优先使用构造方法注入"的推荐结论;最后分析了多 Bean 冲突的三种解决方案 及 @Autowired 与 @Resource 的差异选型。
核心知识点复盘
| 知识点 | 一句话总结 |
|---|---|
| 三层架构 | Controller 接待,Service 加工,Dao 存取------各司其职,单向依赖 |
| IoC 控制反转 | 对象创建权从业务代码反转到 Spring 容器,底层修改不影响上层 |
| 五大类注解 | @Component 及其衍生注解,功能相同,仅用于分层语义 |
| @Bean 方法注解 | 管理第三方类或同一类型多个实例,Bean 名默认等于方法名 |
| Bean 命名规则 | 类注解默认小驼峰;前两位大写保持原样;@Bean 默认方法名 |
| getBean() | 支持按类型、按名称、按名称+类型三种方式获取 |
| 属性注入 | 简洁但不支持 final,脱离容器无法使用 |
| 构造方法注入 | 支持 final、不可变、无空指针------官方推荐方式 |
| Setter 注入 | 支持运行时修改,但有被篡改的风险 |
| @Primary | 同类型多 Bean 时设置默认首选 |
| @Qualifier | 搭配 @Autowired 精确指定 Bean 名称 |
| @Resource | JDK 原生,默认按名称匹配,框架无关 |
常见问题 / 避坑指南
Q1:三层架构中,Service 层的逻辑能不能直接写在 Controller 里?
技术上可以,项目也能跑。但这样做会导致 Controller 臃肿、代码无法复用、单元测试困难。正确的做法是 Controller 只做参数校验和结果封装,业务逻辑必须下沉到 Service。
Q3:@Bean 方法注解能脱离五大注解单独使用吗?
不能。@Bean 必须放在被 Spring 管理的类中(即该类本身需要标注 @Component 或其衍生注解),否则 Spring 根本不会扫描到这个类,方法上的 @Bean 也不会生效。
Q4:构造方法注入时,@Autowired 什么时候可以省略?
当类只有一个构造方法 时,Spring 会自动将它识别为注入点,@Autowired 可以省略。但如果有多个构造方法,就必须在你想使用的那个上面标注 @Autowired。
Q5:@Resource 和 @Autowired 应该选哪个?
如果项目确定长期使用 Spring 生态,用 @Autowired + 构造方法注入是最佳实践。如果代码可能需要移植到其他 IoC 框架(如 Quarkus),或者团队要求减少 Spring 耦合,用 @Resource。两者选其一,统一风格,不要混用。
Q6:@Primary 和 @Qualifier 同时存在时,哪个优先级更高?
@Qualifier 的优先级高于 @Primary。@Qualifier 是精确指定,"我说要哪个就哪个";@Primary 是默认值,"没特别说明时用这个"。当两者同时出现,以 @Qualifier 为准。
