后端Web实战(IOC+DI)
后端Web实战(IOC+DI)
今日目标
目标
Web开发的基础知识 ,包括 Tomcat、Servlet、HTTP协议、SpringBoot入门案例等,我们都已经学习完毕了,那接下来,我们就要进入Web开发的实战篇。在实战篇中,我们将通过一个案例,来讲解Web开发的核心技术。
我们先来看一下,在这个实战篇中,我们都要完成哪些功能。
上述需求,都是在这个案例中,我们需要完成的功能 。
今天主要完成如下功能:
- 开发规范
- 环境准备
- 查询部门
- 分层解耦(IOC+DI)
1. 开发规范
1.1 前后端分离开发
前言
现在的企业项目开发有2种开发模式:前后台混合开发和前后台分离开发。
1. 前后台混合开发,顾名思义就是前台后台代码混在一起开发,如下图所示:

这种开发模式有如下缺点:
- 沟通成本高:后台人员发现前端有问题,需要找前端人员修改,前端修改成功,再交给后台人员使用
- 分工不明确:后台开发人员需要开发后台代码,也需要开发部分前端代码。很难培养专业人才
- 不便管理:所有的代码都在一个工程中
- 难以维护:前端代码更新,和后台无关,但是需要整个工程包括后台一起重新打包部署。
2. 我们目前基本都是采用的前后台分离开发方式,如下图所示:

我们将原先的工程分为前端工程和后端工程2个工程,然后前端工程交给专业的前端人员开发,后端工程交给专业的后端人员开发。
前端页面需要数据,可以通过发送异步请求,从后台工程获取。但是,我们前后台是分开来开发的,那么前端人员怎么知道后台返回数据的格式呢?后端人员开发,怎么知道前端人员需要的数据格式呢?
针对这个问题,我们前后台统一制定一套规范!我们前后台开发人员都需要遵循这套规范开发,这就是我们的接口文档。接口文档有离线版和在线版本,接口文档示可以查询今天提供资料/接口文档里面的资料。
那么接口文档的内容怎么来的呢?
是我们后台开发者根据产品经理提供的产品原型和需求文档所撰写出来的,产品原型示例可以参考今天提供资料/页面原型里面的资料。
那么基于前后台分离开发的模式下,我们后台开发者开发一个功能的具体流程如何呢? 如下图所示:

- 需求分析:首先我们需要阅读需求文档,分析需求,理解需求。
- 接口定义:查询接口文档中关于需求的接口的定义,包括地址,参数,响应数据类型等等
- 前后台并行开发:各自按照接口文档进行开发,实现需求
- 测试:前后台开发完了,各自按照接口文档进行测试
- 前后段联调测试:前段工程请求后端工程,测试功能
总结
课堂作业
- 为什么项目开发主流是前后端分离?这样做有什么好处?
- 前后台分离开发的模式,后台开发者开发一个功能的具体流程分为哪些步骤?🎤
1.2 Restful
Restful
我们的案例是基于当前最为主流的前后端分离模式 进行开发。

在前后端分离的开发模式中,前后端开发人员都需要根据提前定义好的接口文档,来进行前后端功能的开发。
后端开发人员:必须严格遵守提供的接口文档进行后端功能开发(保障开发的功能可以和前端对接)
而在前后端进行交互的时候,我们需要基于当前主流的REST风格的API接口进行交互。
什么是REST风格呢?
- REST(Representational State Transfer),表述性状态转换,它是一种软件架构风格。
传统URL风格如下:
http:/user/getById?id=1 GET:查询id为1的用户
http:/user/saveUser POST:新增用户
http:/user/updateUser POST:修改用户
http:/user/deleteUser?id=1 GET:删除id为1的用户
我们看到,原始的传统URL呢,定义比较复杂,而且将资源的访问行为对外暴露出来了。
基于REST风格URL如下:
http://localhost:8080/users/1 GET:查询id为1的用户
http://localhost:8080/users POST:新增用户
http://localhost:8080/users PUT:修改用户
http://localhost:8080/users/1 DELETE:删除id为1的用户
其中总结起来,就一句话:通过URL定位要操作的资源,通过HTTP动词(请求方式)来描述具体的操作。
在REST风格的URL中,通过四种请求方式,来操作数据的增删改查。
- GET : 查询
- POST :新增
- PUT :修改
- DELETE :删除
我们看到如果是基于REST风格,定义URL,URL将会更加简洁、更加规范、更加优雅。
注意事项:
- REST是风格,是约定方式,约定不是规定,可以打破
- 描述模块的功能通常使用复数 ,也就是加s的格式来描述,表示此类资源,而非单个资源。如:users、emps、books…
代码操作
apifox发送4种类型的请求方式
准备代码
/**
* 控制层:接收前端请求的
*/
@RestController
public class FormController {
//前端访问什么地址,才能调用这个方法尼?
@RequestMapping("/login")
public String login(String name, String password) {
System.out.println("用户名:" + name + " 密码:" + password);
if ("yange".equals(name) && "123".equals(password)) {
return "login ok!";
}
return "login error!";
}
}
发起GET请求截图:
发起POst请求截图:
发起Put请求截图:

发起Delete请求截图:

总结
课堂作业
- 使用apifox发送4种类型的请求,如通上述案例🎤
2.2 工程搭建
工程搭建
今天一上来,就要搭建工作,这样流畅度会高一点。
- 工程名字:edu_management
- jdk:8或者11
- springboot版本:2.7.4
- 编码:utf-8
- maven坐标:
- 组织名字:cn.yangeit
- 项目名字:edu_management
- 版本:1.0
- 依赖坐标:
<!-- 如果安装的是1.8 就改成1.8 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.4</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- web的开发的起步依赖,包含tomcat -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 单元测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--可以免除getset方法的书写-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
</dependency>
<!--工具类-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.27</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<!--作用: 项目打包时,把需要的各种依赖包都打到jar包中,jar包可以独立运行,使用“java -jar”可以直接运行-->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.7.4</version>
</plugin>
</plugins>
</build>
代码操作
1). 使用maven方式,创建SpringBoot工程,并且在resources下创建application.yml配置文件,并粘贴上述提供的依赖,

2). 准备基础的包结构

书写测试HelloController,测试工程是否ok!
@RestController
public class HelloController {
//前端访问什么地址,才能调用这个方法尼?
@RequestMapping("/hello")
public String hello(String username){
System.out.println(username);
return "hello +"+username;
}
}
访问截图:
总结
课堂作业
- 根据上述的提示信息,完成工程项目的创建,和测试Controller🎤
3. 查询部门
3.1 查询部门基本实现
查询部门基本实现
需求
查询所有的部门数据:将 dept.txt
文件中存储的部门数据,查询出来展示在部门管理的页面中。

代码操作
实现思路
- 加载并读取dept.txt文本中的数据
- 解析文本中的数据,并将其封装为集合
- 响应数据(json格式)

1. 将dept.txt文件放到resources文件夹中
dept.txt文件的内容
1,教研部,2023-01-01 12:00:00
2,学工部,2023-02-01 12:00:00
3,研发部,2023-03-01 12:00:00
4,人事部,2023-04-01 12:00:00
5,行政部,2023-05-01 12:00:00
根据dept.txt,创建Dept实体类,并且提供id name updateTime等属性,并且提供get和set方法
2. DeptController控制类代码如下:
@RestController
public class DeptController {
@RequestMapping("/depts")
public List<Dept> list2(){
//1. 加载文件 , 获取原始数据
InputStream in = this.getClass().getClassLoader().getResourceAsStream("dept.txt");
List<String> lines = IoUtil.readLines(in, "UTF-8",new ArrayList<>());
//2. 对原始数据进行处理 , 组装部门数据
List<Dept> deptList = lines.stream().map(line -> {
String[] parts = line.split(",");
Integer id = Integer.parseInt(parts[0]);
String name = parts[1];
LocalDateTime updateTime = LocalDateTime.parse(parts[2],
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
return new Dept(id, name, updateTime);
}).collect(Collectors.toList());
//2. 响应数据
return deptList;
}
}
打开Apifox,来测试当前接口:

新建请求,请求 http://localhost:8080/depts

@ResponseBody
前面我们学习过HTTL协议的交互方式:请求响应模式(有请求就有响应)。那么Controller程序呢,除了接收请求外,还可以进行响应。
在我们前面所编写的controller方法中,都已经设置了响应数据。
1. controller方法中的return的结果,怎么就可以响应给浏览器呢?
答案:使用@ResponseBody注解
@ResponseBody注解:
- 类型:方法注解、类注解
- 位置:书写在Controller方法上或类上
- 作用:将方法返回值直接响应给浏览器,如果返回值类型是实体对象/集合,将会转换为JSON格式后在响应给浏览器
但是在我们所书写的Controller中,只在类上添加了@RestController注解、方法添加了@RequestMapping注解,并没有使用@ResponseBody注解,怎么给浏览器响应呢?
这是因为,我们在类上加了@RestController注解,而这个注解是由两个注解组合起来的,分别是:@Controller 、@ResponseBody。 那也就意味着,我们在类上已经添加了@ResponseBody注解了,而一旦在类上加了@ResponseBody注解,就相当于该类所有的方法中都已经添加了@ResponseBody注解。
提示: 前后端分离的项目中,一般直接在请求处理类上加@RestController注解,就无需在方法上加@ResponseBody注解了。
总结
课堂作业
- lombok依赖有何作用!!
- 为什么Controller中的方法,返回值是json格式的?🎤
- 参考上述代码,将controller代码进行书写,并使用apifox进行测试,观察结果是否为json格式。
- 上述案例是返回所有部门,如果要你写一个getDeptByID 通过Id获得某个部门,应该怎么构建代码!! 练习一下把!!!
3.2 统一响应结果
统一响应结果
1). 刚才我们执行查询部门操作 ,查询返回的结果是一个List<Dept>
,原始代码及响应给前端的结果如下:

2). 如果我们还要实现一个需求,根据ID查询部门名称 ,原始代码及响应给前端的结果如下:

3). 如果我们还要实现一个需求,根据ID查询部门数据 ,原始代码及响应给前端的结果如下:

大家会发现:
- 上述的每一个需求,我们都实现了,但是呢,所有的Controller的方法的返回值是各式各样的,什么样的都有,响应的结果,也是各式各样。
- 如果做一个大型项目,要实现的需求、功能非常多,如果按照这种方案来,最终就会造成项目不便管理、难以维护。
为了解决这个问题,我们就需要统一响应结果。 也就是说,无论什么实现什么功能,最终响应给前端的格式应该是统一的 。
统一响应结果代码操作
统一响应结果说明

前端:只需要按照统一格式的返回结果进行解析(仅一种解析方案),就可以拿到数据。
统一的返回结果使用类来描述,在这个结果中包含:
- 响应状态码 :当前请求是成功,还是失败
- 状态码信息 :给页面的提示信息
- 返回的数据 :给前端响应的数据(字符串、对象、集合)
1. 定义在一个实体类Result来包含以上信息。代码如下:
import lombok.Data;
import java.io.Serializable;
/**
* 后端统一返回结果
*/
@Data//自动生成get set 方法
public class Result {
private Integer code; //编码:1成功,0和其它数字为失败
private String msg; //错误信息
private Object data; //数据
public static Result success() {
Result result = new Result();
result.code = 1;
return result;
}
public static Result success(Object object) {
Result result = new Result();
result.data = object;
result.code = 1;
return result;
}
public static Result error(String msg) {
Result result = new Result();
result.msg = msg;
result.code = 0;
return result;
}
}
明确了为什么要统一响应结果,以及如何封装统一响应结果之后,接下来,我们就来将刚才完成的查询部门的功能完善一下。 分为如下两步操作:
- 引入统一响应结果 Result (资料中提供)
- 改造DeptController中的方法返回值
@RestController
public class DeptController {
@RequestMapping("/depts")
public Result list2(){
//1. 加载文件 , 获取原始数据
InputStream in = this.getClass().getClassLoader().getResourceAsStream("dept.txt");
List<String> lines = IoUtil.readLines(in, "UTF-8",new ArrayList<>());
//2. 对原始数据进行处理 , 组装部门数据
List<Dept> deptList = lines.stream().map(line -> {
String[] parts = line.split(",");
Integer id = Integer.parseInt(parts[0]);
String name = parts[1];
LocalDateTime updateTime = LocalDateTime.parse(parts[2],
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
return new Dept(id, name, updateTime);
}).collect(Collectors.toList());
//2. 响应数据
return Result.success(deptList);
}
}
打开Apifox进行测试:

总结
在测试时候发现,即使我们将请求方式设置为 POST
PUT
DELETE
,也都是可以请求成功的。 如下所示:

这是因为,我们服务器端,也就是Controller程序中并没有限制 该接口的请求方式,那么此时任何请求方式都是可以的。 如果要设置请求方式,可以通过如下两种方式来设置:
- 在controller的方法上,声明
@RequestMapping
注解的method属性,通过method属性指定请求方式。 如下:
@RequestMapping(value = "/depts", method = RequestMethod.GET)
- 直接使用
@GetMapping
来替换 @RequestMapping 注解,@GetMapping其实就是对@RequestMapping的封装,并限定了请求方式为GET。
@GetMapping("/depts")
课堂作业
- 为什么要统一结果响应!🎤
- 结合主流的网站,一般统一结果响应的类主要有哪些变量?
- 根据上述提示,完成同一结果响应类的封装,以及代码的改装!
3.3 前后端联调测试
前后端联调测试
联调测试
完成了查询部门的功能,我们也通过 Apifox 工具测试通过了,下面我们再基于前后端分离的方式进行接口联调 。具体操作如下:
- 将资料中提供的 "前端环境" 文件夹中的压缩包,拷贝到一个没有中文不带空格 的目录下。
2、拷贝到一个没有中文不带空格 的目录后,进行解压 (解压到当前目录)
3、双击 nginx.exe
启动Nginx,一闪而过,就说明nginx以启动完成。
如果在任务管理器中,能看到上述两个进程,就说明nginx已经启动成功。
4、打开浏览器,访问:http://localhost:90
5、测试:系统信息管理 -> 查询部门列表
请求访问流程
前端工程请求服务器的地址为 http://localhost:90/api/depts
,是如何访问到后端的tomcat服务器的?

其实这里,是通过前端服务Nginx中提供的反向代理 功能实现的。

浏览器发起请求,请求的是localhost:90 ,那其实请求的是nginx服务器。
在nginx服务器中呢,并没有对请求直接进行处理,而是将请求转发给了后端的tomcat服务器,最终由tomcat服务器来处理该请求。
这个过程就是通过nginx的反向代理实现的。 那为什么浏览器不直接请求后端的tomcat服务器,而是直接请求nginx服务器呢, 主要有以下几点原因:
- 安全:由于后端的tomcat服务器一般都会搭建集群,会有很多的服务器,把所有的tomcat暴露给前端,让前端直接请求tomcat,对于后端服务器是比较危险的。
- 灵活:基于nginx的反向代理实现,更加灵活,后端想增加、减少服务器,对于前端来说是无感知的,只需要在nginx中配置即可。
- 负载均衡:基于nginx的反向代理,可以很方便的实现后端tomcat的负载均衡操作。
具体的请求访问流程如下:

nginx服务器配置文件:
location:用于定义匹配特定uri请求的规则。
^~ /api/:表示精确匹配,即只匹配以/api/开头的路径。
rewrite:该指令用于重写匹配到的uri路径。
proxy_pass:该指令用于代理转发,它将匹配到的请求转发给位于后端的指令服务器。
总结
课堂作业
- 在非中文目录中运行nginx服务器,并访问http://localhost:90/api/depts 返回前端网页,观察是否正常显示🎤
- 为什么要使用Nginx服务器部署前端代码,而java代码是在tomcat服务器中,这样前后端分离部署有什么好处?
- 使用hutool工具包改造程序,depts.json新格式如下拓展题
[{
"id": 1,
"name": "教研部",
"updateTime": 1672545600000
}, {
"id": 2,
"name": "学工部",
"updateTime": 1675224000000
}, {
"id": 3,
"name": "研发部",
"updateTime": 1677643200000
}, {
"id": 4,
"name": "人事部",
"updateTime": 1680321600000
}, {
"id": 5,
"name": "行政部",
"updateTime": 1682913600000
}]
提示:
//使用hutool工具包中的FileUtil工具类,将json文件读成json字符串
String strings = FileUtil.readUtf8String("D://depts.json");
//将json字符串 使用工具类变成集合
List<Dept> list = JSONUtil.toList(strings, Dept.class);
//使用随机工具类,随机生成整数
int id = RandomUtil.randomInt();
//将对象转化成json字符串
String jsonStr = JSONUtil.toJsonStr(list);
//将字符串以utf-8的格式写入到文件
FileUtil.writeUtf8String(jsonStr,"D://depts.json");
如果你能运用上述的提示,完成部门的增加删除修改和查询改造,你的能力将在完成的瞬间,得到巨大提升!!
4. 分层解耦
4.1 三层架构
三层架构
上述案例的功能,我们虽然已经实现,但是呢,我们会发现案例中:解析文本文件中的数据,处理数据的逻辑代码,给页面响应的代码全部都堆积在一起了,全部都写在controller方法中了。

当前程序的这个业务逻辑还是比较简单的,如果业务逻辑再稍微复杂一点,我们会看到Controller方法的代码量就很大了。
- 当我们要修改操作数据部分的代码,需要改动Controller
- 当我们要完善逻辑处理部分的代码,需要改动Controller
- 当我们需要修改数据响应的代码,还是需要改动Controller
这样呢,就会造成我们整个工程代码的复用性比较差,而且代码难以维护 。 那如何解决这个问题呢?其实在现在的开发中,有非常成熟的解决思路,那就是分层开发。
介绍
在我们进行程序设计以及程序开发时,尽可能让每一个接口、类、方法的职责更单一些(单一职责原则)。
单一职责原则:一个类或一个方法,就只做一件事情,只管一块功能。
这样就可以让类、接口、方法的复杂度更低,可读性更强,扩展性更好,也更利用后期的维护。
我们之前开发的程序呢,并不满足单一职责原则。下面我们来分析下之前的程序:

那其实我们上述案例的处理逻辑呢,从组成上看可以分为三个部分:
- 数据访问:负责业务数据的维护操作,包括增、删、改、查等操作。
- 逻辑处理:负责业务逻辑处理的代码。
- 请求处理、响应数据:负责,接收页面的请求,给页面响应数据。
按照上述的三个组成部分,在我们项目开发中呢,可以将代码分为三层,如图所示:

- Controller:控制层。接收前端发送的请求,对请求进行处理,并响应数据。
- Service:业务逻辑层。处理具体的业务逻辑。
- Dao:数据访问层(Data Access Object),也称为持久层。负责数据访问操作,包括数据的增、删、改、查。
基于三层架构的程序执行流程,如图所示:

- 前端发起的请求,由Controller层接收(Controller响应数据给前端)
- Controller层调用Service层来进行逻辑处理(Service层处理完后,把处理结果返回给Controller层)
- Serivce层调用Dao层(逻辑处理过程中需要用到的一些数据要从Dao层获取)
- Dao层操作文件中的数据(Dao拿到的数据会返回给Service层)
思考:按照三层架构的思想,如何要对业务逻辑(Service层)进行变更,会影响到Controller层和Dao层吗?
答案:不会影响。 (程序的扩展性、维护性变得更好了)
代码操作
分层的具体操作
我们使用三层架构思想,来改造下之前的程序:
- 控制层包名:com.itheima.controller
- 业务逻辑层包名:com.itheima.service.impl
- 数据访问层包名:com.itheima.dao.impl

1). 控制层:接收前端发送的请求,对请求进行处理,并响应数据
@RestController
public class DeptController {
private DeptServiceImpl deptService = new DeptServiceImpl();
@GetMapping("/depts")
public Result list(){
//1. 调用deptService
List<Dept> deptList = deptService.queryDeptList();
//2. 响应数据
return Result.success(deptList);
}
}
2). 业务逻辑层:处理具体的业务逻辑
public class DeptServiceImpl {
private DeptDaoImpl deptDao= new DeptDaoImpl();
@Override
public List<Dept> queryDeptList() {
//1. 调用deptDao
List<String> lines = deptDao.queryDeptList();
//2. 对原始数据进行处理 , 组装部门数据
List<Dept> deptList = lines.stream().map(line -> {
String[] parts = line.split(",");
Integer id = Integer.parseInt(parts[0]);
String name = parts[1];
LocalDateTime updateTime = LocalDateTime.parse(parts[2],
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
return new Dept(id, name, updateTime);
}).collect(Collectors.toList());
//......
return deptList;
}
}
3). 数据访问层:负责数据的访问操作,包含数据的增、删、改、查
public class DeptDaoImpl {
@Override
public List<String> queryDeptList(){
//1. 加载文件 , 获取原始数据
InputStream in = this.getClass().getClassLoader().getResourceAsStream("dept.txt");
List<String> lines = IoUtil.readLines(in, "UTF-8",new ArrayList<>());
return lines;
}
}
具体的请求调用流程:

问题分析
Dao层在进行获取数据时,可能是从文件中获取 ,也可能有数据库中获取 ,那也就意味着Dao层的实现方式有多种 。

如果Dao层的实现方式有多种,如何增强程序的扩展性呢 ?
答案就是:接口、面相接口编程。
那么接下来,我们就需要为Dao层、Service层来设计对应的接口,并让实现类继承对应的接口 。
代码操作:
1). Dao层
接口:
public interface DeptDao {
//查询全部部门数据
public List<String> queryDeptList();
}
实现:
public class DeptDaoImpl implements DeptDao {
@Override
public List<String> queryDeptList(){
InputStream in = this.getClass().getClassLoader().getResourceAsStream("dept.txt");
List<String> lines = IoUtil.readLines(in, "UTF-8",new ArrayList<>());
return lines;
}
}
2). Service层
接口:
public interface DeptService {
//查询所有的部门数据
public List<Dept> queryDeptList();
}
实现:
public class DeptServiceImpl implements DeptService {
private DeptDao deptDao= new DeptDaoImpl();
@Override
public List<Dept> queryDeptList() {
//1. 调用deptDao
List<String> lines = deptDao.queryDeptList();
//2. 对原始数据进行处理 , 组装部门数据
List<Dept> deptList = lines.stream().map(line -> {
String[] parts = line.split(",");
Integer id = Integer.parseInt(parts[0]);
String name = parts[1];
LocalDateTime updateTime = LocalDateTime.parse(parts[2],
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
return new Dept(id, name, updateTime);
}).collect(Collectors.toList());
//......
return deptList;
}
}
3). Controller层
由于Controller层,就是接收请求,响应数据,不涉及到数据的访问、也不涉及逻辑处理,所以一般可以不要接口。
@RestController
public class DeptController {
private DeptService deptService = new DeptServiceImpl();
@GetMapping("/depts")
public Result list(){
//1. 调用deptService
List<Dept> deptList = deptService.queryDeptList();
//2. 响应数据
return Result.success(deptList);
}
}
总结
三层架构的好处
- 复用性强
- 便于维护
- 利用扩展
课堂作业
- 三层架构的好处有哪些?🎤
- 参考三层设计,将案例代码进行拆分,并进行测试!
4.2 分层解耦
分层解耦
问题分析
由于我们现在在程序中,需要什么对象,直接new一个对象 new DeptServiceImpl()

如果说我们需要更换实现类,比如由于业务的变更,DeptServiceImpl 不能满足现有的业务需求,我们需要切换为 DeptServiceImpl2 这套实现,就需要修改Contorller的代码,需要创建 DeptServiceImpl2 的实现new DeptServiceImpl2()
。

Service中调用Dao,也是类似的问题。这种呢,我们就称之为层与层之间 耦合 了。 那什么是耦合呢 ?
首先需要了解软件开发涉及到的两个概念:内聚和耦合。
- 内聚:软件中各个功能模块内部的功能联系。
- 耦合:衡量软件中各个层/模块之间的依赖、关联的程度。
软件设计原则:高内聚低耦合。
高内聚:指的是一个模块中各个元素之间的联系的紧密程度,如果各个元素(语句、程序段)之间的联系程度越高,则内聚性越高,即 "高内聚"。
低耦合:指的是软件中各个层、模块之间的依赖关联程序越低越好。
目前层与层之间是存在耦合的,Controller耦合了Service、Service耦合了Dao。而 高内聚、低耦合的目的是使程序模块的可重用性、移植性大大增强。

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

代码操作
解耦思路
之前我们在编写代码时,需要什么对象,就直接new一个就可以了。 这种做法呢,层与层之间代码就耦合了,当service层的实现变了之后, 我们还需要修改controller层的代码。
那应该怎么解耦呢?
1). 首先不能在EmpController中使用new对象。代码如下:

此时,就存在另一个问题了,不能new,就意味着没有业务层对象(程序运行就报错),怎么办呢?
我们的解决思路是:
- 提供一个容器,容器中存储一些对象(例:DeptService对象)
- Controller程序从容器中获取DeptService类型的对象
2). 将要用到的对象交给一个容器管理。

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

那问题来了,我们如何将对象交给容器管理呢?程序运行时,容器如何为程序提供依赖的对象呢?
我们想要实现上述解耦操作,就涉及到Spring中的两个核心概念:
- 控制反转: Inversion Of Control,简称IOC。
对象的创建控制权由程序自身转移到外部(容器),这种思想称为控制反转。 对象的创建权由程序员主动创建转移到容器(由容器创建、管理对象)。这个容器称为:IOC容器或Spring容器
- 依赖注入: Dependency Injection,简称DI。
容器为应用程序提供运行时,所依赖的资源,称之为依赖注入。
程序运行时需要某个资源,此时容器就为其提供这个资源。
例:EmpController程序运行时需要EmpService对象,Spring容器就为其提供并注入EmpService对象
IOC容器中创建、管理的对象,称之为:bean对象
总结
课堂作业
- 高内聚、低耦合的目的是什么?🎤
- Spring中的两个核心概念分别是什么?
- Spring中的对象,我们一般叫什么对象?
4.3 IOC&DI入门
IOC&DI入门
控制反转: Inversion Of Control,简称IOC。对象的创建控制权由程序自身转移到外部(容器),这种思想称为控制反转。
对象的创建权由程序员主动创建转移到容器(由容器创建、管理对象)。这个容器称为:IOC容器或Spring容器
依赖注入: Dependency Injection,简称DI。容器为应用程序提供运行时,所依赖的资源,称之为依赖注入。
代码操作
1). 将Service及Dao层的实现类,交给IOC容器管理
在实现类加上 @Component
注解,就代表把当前类产生的对象交给IOC容器管理。
A. DeptDaoImpl
@Component
public class DeptDaoImpl implements DeptDao {
@Override
public List<String> queryDeptList(){
//1. 加载文件 , 获取原始数据
InputStream in = this.getClass().getClassLoader().getResourceAsStream("dept.txt");
List<String> lines = IoUtil.readLines(in, "UTF-8",new ArrayList<>());
return lines;
}
}
B. DeptServiceImpl
@Component
public class DeptServiceImpl implements DeptService {
private DeptDao deptDao;
@Override
public List<Dept> queryDeptList() {
//1. 调用deptDao
List<String> lines = deptDao.queryDeptList();
//2. 对原始数据进行处理 , 组装部门数据
List<Dept> deptList = lines.stream().map(line -> {
String[] parts = line.split(",");
Integer id = Integer.parseInt(parts[0]);
String name = parts[1];
LocalDateTime updateTime = LocalDateTime.parse(parts[2],
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
return new Dept(id, name, updateTime);
}).collect(Collectors.toList());
//......
return deptList;
}
}
2). 为Controller 及 Service注入运行时所依赖的对象
通过 @Autowired
注解为应用程序提供运行时依赖的对象。
A. DeptServiceImpl
@Component
public class DeptServiceImpl implements DeptService {
@Autowired
private DeptDao deptDao;
@Override
public List<Dept> queryDeptList() {
//1. 调用deptDao
List<String> lines = deptDao.queryDeptList();
//2. 对原始数据进行处理 , 组装部门数据
List<Dept> deptList = lines.stream().map(line -> {
String[] parts = line.split(",");
Integer id = Integer.parseInt(parts[0]);
String name = parts[1];
LocalDateTime updateTime = LocalDateTime.parse(parts[2],
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
return new Dept(id, name, updateTime);
}).collect(Collectors.toList());
//......
return deptList;
}
}
B. DeptController
@RestController
public class DeptController {
@Autowired
private DeptService deptService ;
@GetMapping("/depts")
public Result list(){
//1. 调用deptService
List<Dept> deptList = deptService.queryDeptList();
//2. 响应数据
return Result.success(deptList);
}
}
启动服务,运行测试。 打开浏览器,地址栏直接访问:http://localhost:90

4.4 IOC详解
通过IOC和DI的入门程序呢,我们已经基本了解了IOC和DI的基础操作。接下来呢,我们学习下IOC控制反转和DI依赖注入的细节。
IOC详解
Bean的声明
前面我们提到IOC控制反转 ,就是将对象的控制权交给Spring的IOC容器,由IOC容器创建及管理对象。IOC容器创建的对象称为bean对象 。
在之前的入门案例中,要把某个对象交给IOC容器管理,需要在类上添加一个注解:@Component
而Spring框架为了更好的标识web应用程序开发当中,bean对象到底归属于哪一层,又提供了@Component
的衍生注解:
注解 | 说明 | 位置 |
---|---|---|
@Component | 声明bean的基础注解 | 不属于以下三类时,用此注解 |
@Controller | @Component 的衍生注解 | 标注在控制层类上 |
@Service | @Component 的衍生注解 | 标注在业务层类上 |
@Repository | @Component 的衍生注解 | 标注在数据访问层类上(由于与mybatis整合,用的少) |
可以使用
@Service
注解声明Service层的bean。 使用@Repository
注解声明Dao层的bean。 代码实现如下:
代码操作
Service层
@Service
public class DeptServiceImpl implements DeptService {
@Autowired
private DeptDao deptDao= new DeptDaoImpl();
@Override
public List<Dept> queryDeptList() {
//1. 调用deptDao
List<String> lines = deptDao.queryDeptList();
//2. 对原始数据进行处理 , 组装部门数据
List<Dept> deptList = lines.stream().map(line -> {
String[] parts = line.split(",");
Integer id = Integer.parseInt(parts[0]);
String name = parts[1];
LocalDateTime updateTime = LocalDateTime.parse(parts[2],
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
return new Dept(id, name, updateTime);
}).collect(Collectors.toList());
//......
return deptList;
}
}
Dao层
@Repository
public class DeptDaoImpl implements DeptDao {
@Override
public List<String> queryDeptList(){
//1. 加载文件 , 获取原始数据
InputStream in = this.getClass().getClassLoader().getResourceAsStream("dept.txt");
List<String> lines = IoUtil.readLines(in, "UTF-8",new ArrayList<>());
return lines;
}
}
注意1: 声明bean的时候,可以通过注解的value属性指定bean的名字,如果没有指定,默认为类名首字母小写
注意2:使用以上四个注解都可以声明bean,但是在springboot集成web开发中,声明控制器bean只能用@Controller。
组件扫描
问题:使用前面学习的四个注解声明的bean,一定会生效吗?
答案:不一定。(原因:bean想要生效,还需要被组件扫描)
前面声明bean的四大注解,要想生效,还需要被组件扫描注解
@ComponentScan
扫描。该注解虽然没有显式配置,但是实际上已经包含在了启动类声明注解
@SpringBootApplication
中,默认扫描的范围是启动类所在包及其子包。

总结
课堂作业
- 谈谈你对IOC的理解!!🎤
@Controller
和@RestController
有什么区别?
4.5 DI详解
DI详解
上一小节我们讲解了控制反转IOC的细节,接下来呢,我们学习依赖注解DI的细节。
依赖注入,是指IOC容器要为应用程序去提供运行时所依赖的资源,而资源指的就是对象。
在入门程序案例中,我们使用了@Autowired这个注解,完成了依赖注入的操作,而这个Autowired翻译过来叫:自动装配 。
@Autowired注解,默认是按照类型进行自动装配的(去IOC容器中找某个类型的对象,然后完成注入操作)
入门程序举例:在EmpController运行的时候,就要到IOC容器当中去查找EmpService这个类型的对象,而我们的IOC容器中刚好有一个EmpService这个类型的对象,所以就找到了这个类型的对象完成注入操作。
那如果在IOC容器中,存在多个相同类型的bean对象,会出现什么情况呢?

此时,我们运行程序,看到控制台已经报错了。

如何解决上述问题呢? 👇
代码操作
Spring提供了以下几种解决方案:
- @Primary
- @Qualifier
- @Resource
方案一:使用@Primary注解
当存在多个相同类型的Bean注入时,加上@Primary注解,来确定默认的实现。
@Primary
@Service
public class DeptServiceImpl implements DeptService {
}
方案二:使用@Qualifier注解
指定当前要注入的bean对象。 在@Qualifier的value属性中,指定注入的bean的名称。 @Qualifier注解不能单独使用,必须配合@Autowired使用
@RestController
public class DeptController {
@Qualifier("deptServiceImpl")
@Autowired
private DeptService deptService;
方案三:使用@Resource注解
是按照bean的名称进行注入。通过name属性指定要注入的bean的名称。
@RestController
public class DeptController {
@Resource(name = "deptServiceImpl")
private DeptService deptService;
总结
课堂作业
面试题 : @Autowird 与 @Resource的区别?
- @Autowired 是spring框架提供的注解,而@Resource是JDK提供的注解
- @Autowired 默认是按照类型注入,而@Resource是按照名称注入