前言

通过简单的示例介绍SpringBoot的三层架构。

版本:

  • Maven:3.6.1
  • JDK:17
  • SpringBoot:3.3.2
  • API测试工具:Postman

概述

三层架构

后端的处理逻辑大体可以分为三个部分:

  • 数据访问
  • 逻辑处理
  • 接收请求,响应数据

而在开发过程中,需要尽量让每一个部分的职责更加单一(单一职责原则),便于后期的拓展维护等操作。

由此出现了开发的三层架构:

  • Controller:控制层。接收前端发送的请求,对请求进行处理,并响应数据。
  • Service:业务逻辑层。处理具体的业务逻辑。
  • DAO(Data Access Object):数据访问层(持久层)。负责数据访问操作,包括数据的增、删、改、查。

设计原则

有了三层架构的设计思想,我们便可以遵循这一思想进行业务开发。

但是这里会遇到一个问题:尽管分模块进行代码编写,但免不了要进行模块与模块之间(层与层)的数据交互。

举个最简单的例子,Controller层要想拿到Service层处理过后的数据并返回给前端,最起码也要创建一个Service层的对象。假设最初的业务逻辑用的是A方案,那么在Controller层就需要创建一个ServiceA对象,但是后面需求有改变,要求使用业务逻辑的B方案,那么Controller层就需要改变创建ServiceB对象。

private Service service = new ServiceA() —> private Service service = new ServiceB()

很明显,目前的方式在改动Service层代码的同时需要额外修改Controller层的代码(Service层和DAO层亦是如此),称层与层之间存在耦合。自然我们是希望尽量降低这种耦合,以避免改动一个模块的同时影响另一个模块,这就引出了软件设计原则:高内聚低耦合

高内聚低耦合

  • 内聚:软件中各个功能模块内部的功能联系。

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

  • 软件设计原则:高内聚低耦合

IOC(控制反转)&DI(依赖注入)

SpringBoot为层与层提供了一个解耦方案(以Controller层和Service层交互为例):

首先构造一个容器,用于存储Bean对象,Service层可以把目前业务逻辑所需要创建的对象放在容器中,而在程序运行时,Controller层需要一个Service层的对象,它就会从容器中寻找看是否有一个Service类型的对象,如果有则直接取出调用。如此一来,如果Service层的业务逻辑发生变化,也不影响Controller层。

那么Bean对象如何交给容器管理?容器又如何为模块提供对应的Bean对象?这就引出了SpringBoot中重要的两个概念:IOC(控制反转)DI(依赖注入)

  • 控制反转(Inversion Of Control,IOC):对象的创建控制权由程序自身转移到外部(容器),这种思想称为控制反转。
  • 依赖注入(Dependency Injection,DI):容器为应用程序提供运行时所需要的依赖资源,称为依赖注入。

注解实现

  • @Component:用于实现IOC操作。表示将当前类交给IOC容器管理,成为IOC容器中的Bean。

    Spring框架为了更好地标识Web应用开发中Bean对象到底归属于哪一层,又提供了@Component的三个衍生注解:

    注解 说明 位置
    @Component 声明Bean的基础注解 不属于以下三类时,用此注解
    @Controller @Component的衍生注解 标注在控制器类上(Controller)(一般用@RestController替代)
    @Service @Component的衍生注解 标注在业务类上(Service)
    @Repository @Component的衍生注解 标注在数据访问类上(DAO)(由于与MyBatis整合,很少使用)
  • @Autowired:用于实现DI操作。运行时,IOC容器会提供该类型的Bean对象,并赋值给该变量。

项目实例构建

通过一个简单的实例说明SpringBoot的三层架构,具体项目目录如下:

这里先说明一下:POJO层(Plain Ordinary Java Object):存放JavaBeans,本示例直接通过DAO层创建POJO层的JavaBeans进行数据访问,忽略数据库操作,因此也没设计Mapper层。

Lombok

在编写类时,使用lombok插件(新版IDEA已集成)简化书写。

Lombok是一个实用的Java类库,能通过注解的形式自动生成构造器、getter/setter、equals、hashcode、toString等方法,并可以自动化生成日志变量,简化java开发、提高效率。

lombok的坐标:

1
2
3
4
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>

tips:可以在类中直接引用lombok的注解,再通过IDEA自动引入。

lombok注解说明:

注解 作用
@Getter/@Setter 为所有的属性提供getter()setter()方法
@ToString 会给类自动生成易阅读的toString()方法
@EqualsAndHashCode 根据类所拥有的非静态字段自动重写equals()方法和hashCode()方法
@Data 相当于@Getter+@Setter+@ToString+@EqualsAndHashCode
@NoArgsConstructor 为实体类生成无参的构造器方法
@AllArgsConstructor 为实体类生成除了static修饰的字段之外带有各参数的构造器方法

User类:

1
2
3
4
5
6
7
8
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
private String name;
private Integer age;
private Address address;
}

Address类:

1
2
3
4
5
6
7
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Address {
private String province;
private String city;
}

Result类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result {
private Integer code;
private String msg;
private Object data;

// 用于快速返回Result对象
public static Result success(Object data) {
return new Result(1, "success", data);
}

public static Result success() {
return new Result(1, "success", null);
}

public static Result error(String msg) {
return new Result(0, msg, null);
}
}

额外说明:以DAO层的实现为例,本层的实例是要被Service层所调用,而DAO的实现方式可能有很多(可能访问本地文件的数据,可能访问数据库的数据等),而想要灵活地切换各种实现,可以采用面向接口的方式编程,因此在实现之前就需要创建一个接口,方便Service层调用。(Service层亦是如此)。

三层架构实现

DAO层

首先在dao目录下创建UserDao接口:

1
2
3
public interface UserDao {
public ArrayList<User> listUser();
}

然后在dao目录下创建impl.UserDaoImpl实现类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Repository  //将当前类交给IOC容器管理,成为IOC容器中的bean
public class UserDaoImpl implements UserDao {
@Override
public ArrayList<User> listUser() {
ArrayList<User> list = new ArrayList<>();
User user1 = new User("张三", 18, new Address("四川", "成都"));
User user2 = new User("李四", 20, new Address("浙江", "杭州"));
User user3 = new User("王五", 22, new Address("湖北", "武汉"));
list.add(user1);
list.add(user2);
list.add(user3);
return list;
}
}

这里创建三个User对象,并添加至list集合中返回。

Service层

首先在service目录中创建UserService接口:

1
2
3
public interface UserService {
public ArrayList<User> listUser();
}

然后在service目录下创建impl.UserServiceImpl实现类:

1
2
3
4
5
6
7
8
9
10
11
@Service  //将当前类交给IOC容器管理,成为IOC容器中的bean
public class UserServiceImpl implements UserService {
@Autowired //运行时,IOC容器会提供该类型的bean对象,并赋值给该变量 - 依赖注入
private UserDao userDao;
@Override
public ArrayList<User> listUser() {
ArrayList<User> list = userDao.listUser();
list.remove(list.size() - 1); //删除最后一条数据
return list;
}
}

这里删除list集合中的最后一个User对象,并返回list集合。

Controller层

controller目录中创建UserController类:

1
2
3
4
5
6
7
8
9
10
11
//@Controller
@RestController
public class UserController {
@Autowired //运行时,IOC容器会提供该类型的bean对象,并赋值给该变量 - 依赖注入
private UserService userService;
@RequestMapping("/getUserList")
public Result listUser() {
ArrayList<User> list = userService.listUser();
return Result.success(list);
}
}

接收前端发来的请求并返回数据。

说明:

@RestController = @Controller + @ResponseBody(因此这里直接使用@RestController

访问http://localhost:8080/getUserList进行测试:

一个包含两个User对象的list集合以JSON格式成功返回。


后记

大一暑期实训的时候就写过基于Servlet的三层架构的Web应用,那时还一知半解,只是照着老师的代码敲。通过几年的开发经历,对于三层架构诞生构想、实现过程有了更深的理解。