缘博客
发布日期

分层解耦-IOC与DI

作者
  • 姓名
    社交账号

为什么需要分层

一个简单的 Web 接口,可以把所有代码都写在 Controller 中。

例如,查询用户列表时,Controller 同时完成:

  1. 接收浏览器请求。
  2. 读取用户数据文件。
  3. 解析数据。
  4. 封装用户对象。
  5. 将结果响应给浏览器。
@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 项目中常见的三层架构包括:

  1. Controller:控制层。
  2. Service:业务逻辑层。
  3. 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,因此 UserServiceUserController 的依赖。

依赖注入表示:

IOC 容器在程序运行时,将对象所依赖的资源提供给它。

IOC容器中已经存在 UserService Bean
Spring将它注入 UserController
UserController可以直接调用 UserService

IOC 和 DI 的关系:

  • IOC:对象交给容器创建和管理。
  • DI:容器将对象需要的依赖提供给它。
IOC解决“对象由谁创建”
DI解决“对象创建后如何提供给使用者”

IOC与DI入门

完成分层解耦主要分为两步:

  1. 将 Dao 和 Service 的实现类交给 IOC 容器管理。
  2. 为 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 的依赖注入常见方式有三种:

  1. 属性注入。
  2. 构造函数注入。
  3. 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 会报错。

可以使用三种方式解决:

  1. @Primary
  2. @Qualifier
  3. @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 的区别:

注解来源默认匹配方式
@AutowiredSpring 框架按照类型注入
@ResourceJava 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容器为对象提供运行时所依赖的资源
BeanIOC 容器创建和管理的对象
@Component声明一个基础 Bean
@Service声明业务层 Bean
@Repository声明数据访问层 Bean
@Autowired默认按照类型进行依赖注入
@Primary指定同类型 Bean 中的优先实现
@Qualifier配合 @Autowired 按名称选择 Bean
@Resource默认按照名称进行依赖注入

分层解决的是职责划分问题,IOC 和 DI 解决的是对象创建与依赖关系问题。三者结合,才能让代码更加容易复用和维护。