说明:三层架构开发时目前开发的主流,我这里通过一个案例,来分析非三层架构开发的不利之处,以及三层架构开发的好处。
案例说明:打开员工信息页,页面要显示所有员工的信息;前端页面已提供,后端通过读取本地emp.xml文件,显示员工信息或者提供本地资源(图片资源存放在项目中的resources文件夹中)的路径返回给前端,以此来模拟实际的开发流程(实际开发信息是放在数据库中的)。
分析:后端需要做的,就是读取员工信息,封装成一个对象,返回给前端。
前端页面如下,需要注意axios.get()中的地址,表示后台查询员工所有信息的方法所需要的映射地址要与这个一致。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>员工信息</title>
</head>
<link rel="stylesheet" href="element-ui/index.css">
<script src="./js/vue.js"></script>
<script src="./element-ui/index.js"></script>
<script src="./js/axios-0.18.0.js"></script>
<body>
<h1 align="center">员工信息列表展示</h1>
<div id="app">
<el-table :data="tableData" style="width: 100%" stripe border >
<el-table-column prop="name" label="姓名" align="center" min-width="20%"></el-table-column>
<el-table-column prop="age" label="年龄" align="center" min-width="20%"></el-table-column>
<el-table-column label="图像" align="center" min-width="20%">
<template slot-scope="scope">
<el-image :src="scope.row.image" style="width: 80px; height: 50px;"></el-image>
</template>
</el-table-column>
<el-table-column prop="gender" label="性别" align="center" min-width="20%"></el-table-column>
<el-table-column prop="job" label="职位" align="center" min-width="20%"></el-table-column>
</el-table>
</div>
</body>
<style>
.el-table .warning-row {
background: oldlace;
}
.el-table .success-row {
background: #f0f9eb;
}
</style>
<script>
new Vue({
el: "#app",
data() {
return {
tableData: []
}
},
mounted(){
axios.get('/listEmp2').then(res=>{
if(res.data.code){
this.tableData = res.data.data;
}
});
},
methods: {
}
});
</script>
</html>
资源放在resources文件夹下
存放员工信息的emp.xml文件放在桌面文件夹里
统一响应结果
开始之前,先介绍一下统一响应结果。因为后端返回给前端的数据,数据类型是不可确定的,不可能每次都前后端开发人员沟通协商,这样就增加了沟通的成本。所以,可以做一个约定,后端把反馈的结果封装成一个对象,对象的属性里有:请求处理状态、请求的信息、数据三个属性。以后所有的请求,反馈结果的都是这样一个对象。前端开发人员就可以通过对象的值判断请求是否处理成功、提示什么信息、以及附带的数据,以供使用。
package com.essay.utils;
/**
* 统一响应结果
*/
public class Result {
/**
* 请求状态:1表示成功、0表示失败
*/
private Integer code;
/**
* 请求信息
*/
private String msg;
/**
* 数据
*/
private Object data;
/* set()、get()、toString()方法 */
/**
* 请求失败的处理方案
* @param msg 失败信息
* @return
*/
public static Result error(String msg) {
Result result = new Result();
result.setCode(0);
result.setMsg(msg);
return result;
}
/**
* 请求成功的处理方案
* @param msg 成功信息
* @param obj 数据
* @return
*/
public static Result success(String msg, Object obj) {
Result result = new Result();
result.setCode(1);
result.setMsg(msg);
result.setData(obj);
return result;
}
}
将这个类,放在项目的utils包中,同时还需要一个解析xml文件的工具类(这里面的代码不是本文的重点,只需知道是用来解析xml文件的就行)
pom.xml文件中需要添加依赖(SpringBoot依赖、解析xml文件工具类所需的依赖)
<!--springboot项目-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.12</version>
<relativePath/>
</parent>
<dependencies>
<!--页面项目所需依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--解析xml文件所需依赖-->
<dependency>
<groupId>org.dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>2.1.3</version>
</dependency>
</dependencies>
非三层架构开发
接下来,用非三层架构的方式,来实现这个查询所有员工信息的功能;
第一步:创建Emp类
package com.essay.domain;
public class Emp {
private String name;
private Integer age;
private String image;
private String gender;
private String job;
public Emp() {
}
public Emp(String name, Integer age, String image, String gender, String job) {
this.name = name;
this.age = age;
this.image = image;
this.gender = gender;
this.job = job;
}
/* set()、get()、toString()方法 */
}
第二步:创建EmpComtroller类和启动类
EmpComtroller类,即实现查询员工所有信息功能的类
package com.essay.web;
import com.essay.domain.Emp;
import com.essay.utils.Result;
import com.essay.utils.XmlParserUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
public class EmpContorller {
@RequestMapping("/listEmp")
public Result listEmp() {
// 读取emp.xml文件中的信息
String path = "C:\\Users\\1\\Desktop\\Emp\\emp.xml";
List<Emp> list = XmlParserUtils.parse(path, Emp.class);
// 将返回的员工信息集合封装成Result对象,返回给前端
return Result.success("成功", list);
}
}
启动类
package com.essay;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Start {
public static void main(String[] args) {
SpringApplication.run(Start.class, args);
}
}
第三步:启动
功能做完,启动项目
可以看到,功能实现了,只是员工的信息中,性别、职务分别是用数字表示的(以后数据库中也是类似这样表示的),因此还需对返回的数据进一步处理。比如性别中1表示男性,2表示女性;职务中,1表示总经理、2表示部门经理之类的;
@RequestMapping("/listEmp")
public Result listEmp() {
// 读取emp.xml文件中的信息
String path = "C:\\Users\\1\\Desktop\\Emp\\emp.xml";
List<Emp> list = XmlParserUtils.parse(path, Emp.class);
// 对集合中的数据进行处理
list.stream().forEach(emp -> {
if ("1".equals(emp.getGender())){
emp.setGender("男性");
}
if ("2".equals(emp.getGender())){
emp.setGender("女性");
}
if ("1".equals(emp.getJob())){
emp.setJob("总经理");
}
if ("2".equals(emp.getJob())){
emp.setJob("部门经理");
}
if ("3".equals(emp.getJob())){
emp.setJob("组员");
}
});
// 将返回的员工信息集合封装成Result对象,返回给前端
return Result.success("成功", list);
}
处理后重新启动,可以看到,达到预想的结果了。
小结
目前的开发结构,可以看出功能内的代码逻辑性差,结构松散,违背单一职责原则,业务稍有改动,类中的代码都需要同步修改(比如xml文件路径更改了,职务中的数字表示的含义发生变化了,类中的代码都有同步修改),这是非三层架构开发的缺点。
分析
通过分析以上代码,一个查询功能的操作分为三步:
第一步,读取xml文件(访问数据库),获取员工信息;【数据访问层】
第二步,对数据进行处理,将员工信息中的性别、职务转换为文字表述;【业务逻辑层】
第三步,将查询结果封装成一个Result对象,返回给前端;【控制层/页面层】
如果以后所有的请求都按照这三步来划分,将请求一层一层去处理。就便于管理代码,提高代码的维护性,这就是三层架构开发。
三层架构开发
在前面的基础上,把查询员工信息功能的代码分成三部分:
控制层/页面层(Controller):接收请求,调用业务逻辑对象处理,返回处理结果;
业务逻辑层(Service):调用数据访问对象查询数据,处理数据;
数据访问层(Dao):解析emp.xml文件,读取员工信息;
修改代码如下:
控制层/页面层(Controller):接收请求,响应数据
import com.essay.domain.Emp;
import com.essay.service.EmpService;
import com.essay.service.impl.EmpServiceImpl;
import com.essay.utils.Result;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* 控制层/页面层(Controller)
*/
@RestController
public class EmpContorller {
/**
* 创建业务逻辑对象
*/
private EmpService empService = new EmpServiceImpl();
@RequestMapping("/listEmp")
public Result listEmp() {
// 调用service去处理逻辑
List<Emp> list = empService.listEmp();
// 返回响应结果
return Result.success("成功", list);
}
}
业务逻辑层(Service):逻辑处理
import com.essay.dao.EmpDao;
import com.essay.dao.impl.EmplDaoImpl;
import com.essay.domain.Emp;
import com.essay.service.EmpService;
import java.util.List;
/**
* 业务逻辑层(Service)
*/
public class EmpServiceImpl implements EmpService {
/**
* 创建数据访问对象
*/
private EmpDao empDao = new EmplDaoImpl();
/**
* 处理查询所有员工的业务
*/
@Override
public List<Emp> listEmp() {
// 数据来源于Dao
List<Emp> list = empDao.listEmp();
// 对集合中的数据进行处理
list.stream().forEach(emp -> {
if ("1".equals(emp.getGender())) {
emp.setGender("男性");
}
if ("2".equals(emp.getGender())) {
emp.setGender("女性");
}
if ("1".equals(emp.getJob())) {
emp.setJob("总经理");
}
if ("2".equals(emp.getJob())) {
emp.setJob("财务");
}
if ("3".equals(emp.getJob())) {
emp.setJob("组员");
}
});
return list;
}
}
数据访问层(Dao):数据访问
import com.essay.dao.EmpDao;
import com.essay.domain.Emp;
import com.essay.utils.XmlParserUtils;
import java.util.List;
/**
* 数据访问层(Dao)
*/
public class EmplDaoImpl implements EmpDao {
@Override
public List<Emp> listEmp() {
// 读取emp.xml文件中的信息
String path = "C:\\Users\\1\\Desktop\\Emp\\emp.xml";
List<Emp> list = XmlParserUtils.parse(path, Emp.class);
return list;
}
}
实际上,会给业务逻辑层(Service)、数据访问层(Dao)各自创建一个接口,让其实现类去执行操作;
业务逻辑对象(Service)接口
import com.essay.domain.Emp;
import java.util.List;
/**
* 业务逻辑对象(Service)接口
*/
public interface EmpService {
/**
* 查询所有员工信息
* @return
*/
List<Emp> listEmp();
}
数据访问层(Dao)接口
import com.essay.domain.Emp;
import java.util.List;
/**
* 数据访问对象接口
*/
public interface EmpDao {
/**
* 查询所有员工信息
* @return
*/
List<Emp> listEmp();
}
增加注解
虽然使用了三层架构,但层与层之间的耦合性还是很高的,如果项目中开发了多套ServiceImpl(业务处理方案),想要在多套处理方案中切换的话,就只能修改代码,手动选择其中的一套。
这时就可以使用Spring提供的注解,来帮我们自动创建、使用对象,
@Repository注解:表示当前类为数据访问层对象
@Service注解:表示当前类为业务逻辑层对象
@Controller注解:表示当前类为控制/页面层对象(@RestController包括了@Controller,所以使用@RestController也可以)
然后需要创建对象时,使用@AutoWired注解,程序会自动根据类型寻找对应的类对象
启动
大功告成,启动
到此,基于SpringBoot的三层架构开发的查询所有员工信息的功能就完成了。
控制反转&依赖注入
上面所用到的注解,是Spring的一个特性,即控制反转&依赖注入。
在程序启动时,启动类会扫描本包和子包,识别到对应注解(@Service、@Repository等)的类,将类创建到IOC容器中,这个过程称为控制反转;之后在需要使用到对象时(@Autowired),会从IOC容器中取出来,这个过程称为依赖注入。
因为启动类启动时只会扫描子包,所以启动类不能放在某个包里面,或者说启动类要在所有子包之上,否则本包之外和启动类之上的包中标记的注解不会生效。
启动程序后,可以在Actuator的Beans中找到我们三层架构的类对象;
@Autowired注解,默认是按照类型进行,如果存在多个相同类型的bean,程序会报错
可以通过以下方式解决:
(1)使用@Primary注解,设置“主要的bean”,发生重名时,优先使用该bean;
(2)使用@Qualifier注解,指名取指定的bean;如果类没有设置名称的话,@Qualifier里面填需要使用的bean类名(bean名为类名的首字母小写)
(3)使用JDK的@Resource注解,指名取指定的类对象。不属于Spring的注解,且使用了此注解,上面就不需要加@Autowired注解了。这种方式与前两种方式格格不入,故不推荐使用;
总结
三层架构开发,把一个前端请求分为三个部分,层层递进,增强了代码的逻辑,提高了代码的复用性,便于后续的维护。
另外,使用统一响应结果,统一了返回给前端页面的结果,减少了前后端开发人员的交流成本,也更规范了程序开发。
最后,在三层架构的基础上,使用Spring注解,将管理对象的工作交给了IOC容器,降低了层与层之间依赖、关联程度,实现了系统的高内聚低耦合。
参考:【黑马程序员2023新版JavaWeb开发教程,实现javaweb企业开发全流程(涵盖Spring+MyBatis+SpringMVC+SpringBoot等)】 https://www.bilibili.com/video/BV1m84y1w7Tb/?p=79&share_source=copy_web&vd_source=8d1a3172aa5ba3ea7bdfa82e535732a8 (P73-79)