- 发布日期
分层解耦-IOC与DI
- 作者
- 姓名
- 缘
- 社交账号
文章目录
为什么需要分层
一个简单的 Web 接口,可以把所有代码都写在 Controller 中。
例如,查询用户列表时,Controller 同时完成:
- 接收浏览器请求。
- 读取用户数据文件。
- 解析数据。
- 封装用户对象。
- 将结果响应给浏览器。
@RequestMapping("/list")
public List<User> list() {
// 1. 加载并读取文件
InputStream in = this.getClass()
.getClassLoader()
.getResourceAsStream("user.txt");
ArrayList<String> lines = IoUtil.readLines(
in,
StandardCharsets.UTF_8,
new ArrayList<>()
);
// 2. 解析数据并封装成对象
List<User> userList = lines.stream().map(line -> {
String[] parts = line.split(",");
Integer id = Integer.parseInt(parts[0]);
String username = parts[1];
String password = parts[2];
String name = parts[3];
Integer age = Integer.parseInt(parts[4]);
LocalDateTime updateTime = LocalDateTime.parse(
parts[5],
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
);
return new User(
id,
username,
password,
name,
age,
updateTime
);
}).collect(Collectors.toList());
// 3. 响应数据
return userList;
}
这段代码能够运行,但存在两个明显问题:
- 复用性差。
- 难以维护。
Controller 既负责接收请求,又负责业务处理和数据访问,承担了过多职责。以后数据来源、解析规则或者业务需求发生变化,都需要修改同一个类。
解决思路是按照不同职责拆分代码。
三层架构
Web 项目中常见的三层架构包括:
- Controller:控制层。
- Service:业务逻辑层。
- Dao:数据访问层。
浏览器
↓
Controller
↓
Service
↓
Dao
↓
数据源
每一层只处理自己负责的事情。
Controller控制层
Controller 负责:
- 接收前端发送的请求。
- 获取请求参数。
- 调用 Service 处理业务。
- 将处理结果响应给前端。
@RestController
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@RequestMapping("/list")
public List<User> list() {
return userService.list();
}
}
Controller 不负责读取文件,也不负责解析用户数据,只负责控制一次请求的处理流程。
Service业务逻辑层
Service 负责处理具体的业务逻辑。
public interface UserService {
List<User> list();
}
实现类:
@Service
public class UserServiceImpl implements UserService {
private final UserDao userDao;
public UserServiceImpl(UserDao userDao) {
this.userDao = userDao;
}
@Override
public List<User> list() {
List<String> lines = userDao.list();
return lines.stream().map(line -> {
String[] parts = line.split(",");
Integer id = Integer.parseInt(parts[0]);
String username = parts[1];
String password = parts[2];
String name = parts[3];
Integer age = Integer.parseInt(parts[4]);
LocalDateTime updateTime = LocalDateTime.parse(
parts[5],
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
);
return new User(
id,
username,
password,
name,
age,
updateTime
);
}).collect(Collectors.toList());
}
}
这层负责将 Dao 返回的原始数据解析、转换并封装成用户对象。
Dao数据访问层
Dao 全称为 Data Access Object,即数据访问对象。
Dao 负责所有数据访问操作,例如:
- 从文件读取数据。
- 从数据库查询数据。
- 新增、修改和删除数据。
public interface UserDao {
List<String> list();
}
实现类:
@Repository
public class UserDaoImpl implements UserDao {
@Override
public List<String> list() {
InputStream in = this.getClass()
.getClassLoader()
.getResourceAsStream("user.txt");
return IoUtil.readLines(
in,
StandardCharsets.UTF_8,
new ArrayList<>()
);
}
}
当前实现从 user.txt 读取数据。以后改为数据库时,主要修改 Dao 层,不需要让 Controller 直接处理数据库代码。
三层之间的调用关系
Controller:接收请求、响应数据
↓
Service:处理业务逻辑
↓
Dao:访问数据
每一层都向下一层发起调用:
UserController
↓
UserService
↓
UserDao
这种拆分遵循单一职责原则:
一个类或者一个模块只负责一类职责。
分层之后:
- Controller 可以专注请求和响应。
- Service 可以专注业务规则。
- Dao 可以专注数据访问。
- 同一段业务或数据访问代码可以被多个接口复用。
- 修改某一层时,对其他层的影响更小。
耦合与内聚
耦合
耦合用于衡量不同层、不同模块或者不同类之间的依赖关联程度。
例如:
public class UserController {
private final UserService userService =
new UserServiceImpl();
}
UserController 在内部直接创建 UserServiceImpl,因此它明确依赖这个实现类。
如果要将业务实现替换成:
UserServiceOtherImpl
就必须修改 Controller:
private final UserService userService =
new UserServiceOtherImpl();
Controller 与具体实现类绑定得很紧,这就是耦合程度较高。
内聚
内聚表示一个模块内部各个功能之间的联系程度。
例如,Dao 层内部的方法都围绕数据访问展开:
查询数据
新增数据
修改数据
删除数据
这些功能属于同一类职责,因此 Dao 层内部的内聚程度较高。
高内聚低耦合
软件设计通常追求:
高内聚、低耦合。
高内聚表示:
- 同一个模块内部的功能联系紧密。
- 每一个模块都有明确、集中的职责。
低耦合表示:
- 模块之间尽量减少对具体实现的依赖。
- 替换某个实现时,尽量不修改其他模块。
三层架构解决了职责混乱的问题,但仅仅把代码拆成 Controller、Service 和 Dao,并不代表已经完成解耦。
如果 Controller 仍然自己 new UserServiceImpl(),Service 仍然自己 new UserDaoImpl(),各层之间依然直接依赖具体实现类。
public class UserController {
private final UserService userService =
new UserServiceImpl();
}
public class UserServiceImpl implements UserService {
private final UserDao userDao =
new UserDaoImpl();
}
要进一步降低耦合,需要使用 IOC 和 DI。
IOC与DI
IOC控制反转
IOC 全称为 Inversion Of Control,即控制反转。
原本,对象由程序自己创建:
UserService userService = new UserServiceImpl();
这里的对象创建控制权掌握在 Controller 自己手中。
使用 IOC 后,程序不再自己创建需要的对象,而是将对象的创建和管理交给外部容器。
原来:
Controller ──new──> UserServiceImpl
使用IOC后:
Spring容器 ──创建并管理──> UserServiceImpl
Controller ──使用────────> UserService
所谓“控制反转”,反转的就是对象创建和管理的控制权:
- 原来由应用程序自己创建对象。
- 现在由 Spring IOC 容器创建并管理对象。
Bean对象
由 IOC 容器创建和管理的对象,称为 Bean。
例如:
@Service
public class UserServiceImpl implements UserService {
}
Spring 扫描到 @Service 后,会创建 UserServiceImpl 对象,并将该对象放入 IOC 容器中管理。
这个由 Spring 管理的 UserServiceImpl 对象就是 Bean。
IOC容器
├── userController
├── userServiceImpl
└── userDaoImpl
Bean 本质上仍然是普通 Java 对象,只是它不再由程序手动 new,而是由 Spring 创建和管理。
DI依赖注入
DI 全称为 Dependency Injection,即依赖注入。
一个对象运行时需要使用另一个对象,这个被使用的对象就是它的依赖。
例如:
public class UserController {
private UserService userService;
}
UserController 需要调用 UserService,因此 UserService 是 UserController 的依赖。
依赖注入表示:
IOC 容器在程序运行时,将对象所依赖的资源提供给它。
IOC容器中已经存在 UserService Bean
↓
Spring将它注入 UserController
↓
UserController可以直接调用 UserService
IOC 和 DI 的关系:
- IOC:对象交给容器创建和管理。
- DI:容器将对象需要的依赖提供给它。
IOC解决“对象由谁创建”
DI解决“对象创建后如何提供给使用者”
IOC与DI入门
完成分层解耦主要分为两步:
- 将 Dao 和 Service 的实现类交给 IOC 容器管理。
- 为 Controller 和 Service 注入运行时所依赖的对象。
将实现类交给IOC容器
Dao 实现类:
@Component
public class UserDaoImpl implements UserDao {
@Override
public List<String> list() {
InputStream in = this.getClass()
.getClassLoader()
.getResourceAsStream("user.txt");
return IoUtil.readLines(
in,
StandardCharsets.UTF_8,
new ArrayList<>()
);
}
}
Service 实现类:
@Component
public class UserServiceImpl implements UserService {
private final UserDao userDao;
public UserServiceImpl(UserDao userDao) {
this.userDao = userDao;
}
@Override
public List<User> list() {
List<String> lines = userDao.list();
// 解析数据并封装为 User 对象
return new ArrayList<>();
}
}
@Component 表示将当前类声明为 Bean,由 IOC 容器创建和管理。
注解应该添加在实现类上,而不是接口上:
// 正确:实现类可以被创建为对象
@Component
public class UserServiceImpl implements UserService {
}
// 错误:接口本身不能直接创建对象
@Component
public interface UserService {
}
注入所依赖的对象
Controller 依赖 UserService:
@RestController
public class UserController {
@Autowired
private UserService userService;
@RequestMapping("/list")
public List<User> list() {
return userService.list();
}
}
Service 依赖 UserDao:
@Component
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao;
@Override
public List<User> list() {
List<String> lines = userDao.list();
// 解析数据并封装为 User 对象
return new ArrayList<>();
}
}
@Autowired 会让 Spring 从 IOC 容器中寻找匹配的 Bean,并完成依赖注入。
程序不再出现:
new UserServiceImpl();
new UserDaoImpl();
对象的创建和组装都交给 Spring 容器处理。
声明Bean的注解
要将一个对象交给 IOC 容器管理,可以在类上使用以下注解:
| 注解 | 说明 | 常用位置 |
|---|---|---|
@Component | 声明 Bean 的基础注解 | 不属于下面三层时使用 |
@Controller | @Component 的衍生注解 | 控制层 |
@Service | @Component 的衍生注解 | 业务逻辑层 |
@Repository | @Component 的衍生注解 | 数据访问层 |
按照三层架构编写时,通常使用更能体现职责的注解:
@RestController
public class UserController {
}
@Service
public class UserServiceImpl implements UserService {
}
@Repository
public class UserDaoImpl implements UserDao {
}
@RestController 可以理解为:
@Controller + @ResponseBody
它既将类声明为 Controller Bean,又会将方法返回值直接写入 HTTP 响应体。
Bean名称
声明 Bean 时可以通过注解的 value 属性指定名称:
@Service("userService")
public class UserServiceImpl implements UserService {
}
如果没有手动指定,默认 Bean 名称通常是类名首字母小写:
UserServiceImpl → userServiceImpl
UserDaoImpl → userDaoImpl
这个名称会在 @Qualifier 或 @Resource 指定 Bean 时使用。
组件扫描
类上添加 Bean 注解后,还需要被 Spring 的组件扫描发现,注解才会生效。
Spring Boot 启动类通常使用:
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
@SpringBootApplication 中已经包含组件扫描功能,默认扫描:
启动类所在包以及它的所有子包。
例如:
com.yuan
├── Application.java
├── controller
│ └── UserController.java
├── service
│ └── UserServiceImpl.java
└── dao
└── UserDaoImpl.java
启动类位于 com.yuan 包下时,三个子包中的 Bean 都能够被扫描到。
如果某个实现类放在启动类所在包之外,Spring 默认可能扫描不到它。
依赖注入的三种方式
基于 @Autowired 的依赖注入常见方式有三种:
- 属性注入。
- 构造函数注入。
- Setter 注入。
属性注入
@RestController
public class UserController {
@Autowired
private UserService userService;
}
优点:
- 代码简洁。
- 适合快速开发。
缺点:
- 类之间的依赖关系被隐藏在字段中。
- 可能破坏类的封装性。
- 不创建 Spring 容器时,不方便手动构造和测试对象。
构造函数注入
@RestController
public class UserController {
private final UserService userService;
@Autowired
public UserController(UserService userService) {
this.userService = userService;
}
}
如果类中只有一个构造函数,@Autowired 可以省略:
@RestController
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
}
优点:
- 能够清楚看到当前类依赖哪些对象。
- 依赖可以使用
final修饰。 - 对象创建完成后,依赖就已经准备好。
- 更方便进行单元测试。
缺点:
- 依赖过多时,构造函数参数会变多。
Setter注入
@RestController
public class UserController {
private UserService userService;
@Autowired
public void setUserService(UserService userService) {
this.userService = userService;
}
}
优点:
- 保持字段私有,依赖关系比较清晰。
- 可以在对象创建后再设置依赖。
缺点:
- 需要额外编写 Setter 方法。
- 依赖可能在对象创建完成后仍未设置。
多个同类型Bean的处理
@Autowired 默认按照类型寻找 Bean。
例如:
@Autowired
private UserService userService;
Spring 会在 IOC 容器中寻找所有实现了 UserService 的 Bean。
只有一个实现类时:
UserService
↑
UserServiceImpl
Spring 可以直接完成注入。
如果存在两个实现类:
@Service
public class UserServiceImpl implements UserService {
}
@Service
public class UserServiceOtherImpl implements UserService {
}
IOC 容器中就存在两个 UserService 类型的 Bean:
userServiceImpl
userServiceOtherImpl
此时仅按照类型无法确定应该注入哪一个,Spring 会报错。
可以使用三种方式解决:
@Primary@Qualifier@Resource
使用@Primary
在某个实现类上添加 @Primary:
@Primary
@Service
public class UserServiceImpl implements UserService {
}
当 Spring 按照 UserService 类型注入时,会优先选择这个 Bean。
@Autowired
private UserService userService;
此时注入的是 UserServiceImpl。
使用@Qualifier
@Qualifier 与 @Autowired 配合,根据 Bean 名称指定实现:
@RestController
public class UserController {
@Autowired
@Qualifier("userServiceOtherImpl")
private UserService userService;
}
userServiceOtherImpl 来自默认 Bean 名称:
UserServiceOtherImpl
↓
userServiceOtherImpl
也可以在声明 Bean 时手动指定名称:
@Service("otherUserService")
public class UserServiceOtherImpl implements UserService {
}
注入时使用:
@Autowired
@Qualifier("otherUserService")
private UserService userService;
使用@Resource
@Resource 可以直接根据名称注入:
@RestController
public class UserController {
@Resource(name = "userServiceImpl")
private UserService userService;
}
@Autowired 与 @Resource 的区别:
| 注解 | 来源 | 默认匹配方式 |
|---|---|---|
@Autowired | Spring 框架 | 按照类型注入 |
@Resource | Java EE 规范 | 按照名称注入 |
分层解耦后的完整关系
浏览器
↓
UserController Bean
↓ DI
UserService Bean
↓ DI
UserDao Bean
↓
数据源
对象创建过程:
@Component / @Service / @Repository
↓
Spring扫描到实现类
↓
IOC容器创建并管理Bean
对象依赖过程:
Controller需要Service
Service需要Dao
↓
Spring从IOC容器中寻找Bean
↓
通过DI完成依赖注入
最终,各层只依赖接口:
private final UserService userService;
private final UserDao userDao;
具体实现对象由 Spring 容器决定并提供。
以后替换实现类时,Controller 和 Service 不需要再手动修改 new 对象的代码,各层之间的耦合程度因此降低。
核心概念总结
| 概念 | 含义 |
|---|---|
| 三层架构 | 将项目拆分为 Controller、Service 和 Dao |
| Controller | 接收请求、调用业务层、响应数据 |
| Service | 处理具体业务逻辑 |
| Dao | 负责数据访问 |
| 高内聚 | 一个模块内部的功能围绕同一职责 |
| 低耦合 | 模块之间尽量不直接依赖具体实现 |
| IOC | 将对象创建和管理的控制权交给容器 |
| DI | 容器为对象提供运行时所依赖的资源 |
| Bean | IOC 容器创建和管理的对象 |
@Component | 声明一个基础 Bean |
@Service | 声明业务层 Bean |
@Repository | 声明数据访问层 Bean |
@Autowired | 默认按照类型进行依赖注入 |
@Primary | 指定同类型 Bean 中的优先实现 |
@Qualifier | 配合 @Autowired 按名称选择 Bean |
@Resource | 默认按照名称进行依赖注入 |
分层解决的是职责划分问题,IOC 和 DI 解决的是对象创建与依赖关系问题。三者结合,才能让代码更加容易复用和维护。