Skip to content


问题引入

在三层架构代码中,如果说我们需要更换实现类,比如由于业务的变更,UserServiceImpl 不能满足现有的业务需求,我们需要切换为 UserServiceImpl2 这套实现,就需要修改 Contorller 的代码,需要创建 UserServiceImpl2 的实现 new UserServiceImpl2()

Service 中调用 Dao,也是类似的问题。这种呢,我们就称之为层与层之间耦合

基本介绍

内聚与耦合

(1)内聚:软件中各个功能模块内部的功能联系

(2)耦合:衡量软件中各个层/模块之间的依赖、关联的程度

软件设计原则

原则:高内聚低耦合

(1)高内聚:指的是一个模块中各个元素之间的联系的紧密程度,如果各个元素(语句、程序段)之间的联系程度越高,则内聚性越高,即 "高内聚"

(2)低耦合:指的是软件中各个层、模块之间的依赖关联程序越低越好

解耦

问题分析

目前层与层之间是存在耦合的,Controller 耦合了 Service、Service 耦合了 Dao。而 高内聚、低耦合的目的是使程序模块的可重用性、移植性大大增强

那最终我们的目标呢,就是做到层与层之间,尽可能的降低耦合,甚至解除耦合

解耦思路

(1) 首先不能在 EmpController 中使用 new 对象

此时,就存在另一个问题了,不能 new,就意味着没有业务层对象(程序运行就报错),怎么办呢?

(1)提供一个容器,容器中存储一些对象(例:UserService 对象)

(2)Controller 程序从容器中获取 UserService 类型的对象

(2)将要用到的对象交给一个容器管理

(3)应用程序中用到这个对象,就直接从容器中获取

解耦实现


IOC 控制反转

Bean 的声明

前面我们提到 IOC 控制反转,就是将对象的控制权交给 Spring 的 IOC 容器,由 IOC 容器创建及管理对象。IOC 容器创建的对象称为 bean 对象

在之前的入门案例中,要把某个对象交给 IOC 容器管理,需要在类上添加一个注解:@Component

而 Spring 框架为了更好的标识 web 应用程序开发当中,bean 对象到底归属于哪一层,又提供了 @Component 的衍生注解

注意点

(1)声明 bean 的时候,可以通过注解的 value 属性指定 bean 的名字,如果没有指定,默认为类名首字母小写

(2)使用以上四个注解都可以声明 bean,但是在 springboot 集成 web 开发中,声明控制器 bean 只能用 @Controller

组件扫描

以上声明 bean 的四大注解不一定会生效,需要被组件扫描注解 @ComponentScan 扫描才可以生效

该注解虽然没有显式配置,但是实际上已经包含在了启动类声明注解 @SpringBootApplication 中,默认扫描的范围是启动类所在包及其子包(注意扫描包的范围)

DI 依赖注入

@Autowired 的三种用法

(1)属性注入

java
@RestController
public class UserController {

    //方式一: 属性注入
    @Autowired
    private UserService userService;

}

优点:代码简洁、方便快速开发

缺点:隐藏了类之间的依赖关系、可能会破坏类的封装性,没有 settergetter 方法,底层是通过反射实现赋值

(2)构造器注入

java
@RestController
public class UserController {

    //方式二: 构造器注入
    private final UserService userService;

    @Autowired //如果当前类中只存在一个构造函数, @Autowired可以省略
    public UserController(UserService userService) {
        this.userService = userService;
    }

}

优点:能清晰地看到类的依赖关系、提高了代码的安全性

缺点:代码繁琐、如果构造参数过多,可能会导致构造函数臃肿

注意:如果只有一个构造函数,@Autowired 注解可以省略。(通常来说,也只有一个构造函数)

(3)setter 注入

java
/**
 * 用户信息Controller
 */
@RestController
public class UserController {

    //方式三: setter注入
    private UserService userService;

    @Autowired
    public void setUserService(UserService userService) {
        this.userService = userService;
    }

}

优点:保持了类的封装性,依赖关系更清晰

缺点:需要额外编写 setter 方法,增加了代码量

如何选择?

在项目开发中,基于@Autowired 进行依赖注入时,基本都是第一种和第二种方式。(官方推荐第二种方式,因为会更加规范)但是在企业项目开发中,很多的项目中,也会选择第一种方式因为更加简洁、高效(在规范性方面进行了妥协)

多个相同 bean 报错

如果在 IOC 容器中,存在多个相同类型的 bean 对象。此时,我们启动项目会发现,控制台报错

出现错误的原因呢,是因为在 Spring 的容器中,UserService 这个类型的 bean 存在两个,框架不知道具体要注入哪个 bean 使用,所以就报错了

解决方案

方案一:使用 @Primary 注解

当存在多个相同类型的 Bean 注入时,加上 @Primary 注解,来确定默认的实现

java
@Primary
@Service
public class UserServiceImpl implements UserService {
}

方案二:使用 @Qualifier 注解

指定当前要注入的 bean 对象,在 @Qualifier 的 value 属性中,指定注入的 bean 的名称,@Qualifier 注解不能单独使用,必须配合 @Autowired 使用

java
@RestController
public class UserController {
    @Qualifier("userServiceImpl")
    @Autowired
    private UserService userService;
}

方案三:使用 @Resource 注解

按照 bean 的名称进行注入。通过 name 属性指定要注入的 bean 的名称

java
@RestController
public class UserController {
    @Resource(name = "userServiceImpl")
    private UserService userService;
}

⭐ 区别两个依赖注入注解

@Autowiredspring 框架提供的注解,而 @ResourceJDK 提供的注解

@Autowired 默认是按照类型注入,而 @Resource 是按照名称注入

同名 Controller 注入冲突

在 Spring 中默认会以类名(首字母小写)作为 Bean 名称,对于类名相同的 Controller,在 Bean 注入时会导致冲突

通过 @RestController 的 value 属性自定义 bean 的名称,可以避免冲突

案例实现

思路分析

(1)Controller 依赖 Servie,Service 依赖 Dao

(2)控制反转(IOC):把 Service 和 Dao 交给容器管理,使用 @Component 注解

(3)依赖注入(DI):为 Conntroller 及 Service 主语运行时所依赖的对象,使用 @Autowired 注解

实现方法

(1)将 Service 及 Dao 层的实现类,交给 IOC 容器管理

在实现类加上 @Component 注解,就代表把当前类产生的对象交给 IOC 容器管理

UserDaoImpl

java
@Component
public class UserDaoImpl implements UserDao {
    @Override
    public List<String> findAll() {
        InputStream in = this.getClass().getClassLoader().getResourceAsStream("user.txt");
        ArrayList<String> lines = IoUtil.readLines(in, StandardCharsets.UTF_8, new ArrayList<>());
        return lines;
    }
}

UserServiceImpl

java
@Component
public class UserServiceImpl implements UserService {

    private UserDao userDao;

    @Override
    public List<User> findAll() {
        List<String> lines = userDao.findAll();
        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());
        return userList;
    }
}

(2)为 Controller 及 Service 注入运行时所依赖的对象

UserServiceImpl

java
@Component
public class UserServiceImpl implements UserService {

    @Autowired
    private UserDao userDao;

    @Override
    public List<User> findAll() {
        List<String> lines = userDao.findAll();
        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());
        return userList;
    }
}

UserController

java
@RestController
public class UserController {

    @Autowired
    private UserService userService;

    @RequestMapping("/list")
    public List<User> list(){
        //1.调用Service
        List<User> userList = userService.findAll();
        //2.响应数据
        return userList;
    }

}