前言:本文基于若依前后端分离版本(Spring Boot 3.3.0 + Vue 3 + Activiti 8.1.0)进行改造,相关教程可以在网上找到。在撰写此博客期间,笔者刚刚开始接触 Java Web,本系列下的文章内容包含大量“个人初期”视角,注意鉴别。

前言

在初次接触Java web开发时看着项目中一大堆文件夹,还是有些无从下手的,所以在写每个功能前还是需要了解一下后端整体的项目结构的。

业务A == iams-业务A

后端项目打开会看到好多模块,每个模块在业务逻辑上是独立的,相互之间是可以被调用的(但是不能A调用B,B同时再调用A,会导致循环依赖)。

比如若依的项目,在这个视频中介绍了课程类的添加方式,实现了对课程数据的增删查改,只不过它把后端的代码都放到了admin模块下面,其实我们也可以新增一个模块,将这个代码放进去,就像下图一样,每个模块负责各自的业务。

如何形象化一个后端模块?——以用户管理为例

其实不是很想用添加用户这个作为演示,他的服务、实体、数据库映射放在了system模块里面,但是控制层却放到了admin的web文件夹下,总体上不影响的

前端的任务——(带着参数)提需求

具体来说我们期望实现四个功能:增删改查,那么就需要向后端发送请求,以增加为例。前端向后端发送的应该是json数据,形式上看着像键值对,包含新增用户的名称、昵称等信息,后端接收到的就是这样的一个数据结构。

1
2
3
4
5
6
7
8
9
10
{
"deptId": 103,
"userName": "liuliuliu",
"nickName": "Altria",
"password": "123456",
"sex": "0",
"status": "0",
"postIds": [],
"roleIds": [2]
}

根据前端向后端访问时的方法、参数、路径不同,后端将会依次进行调用不同的服务去处理,添加用户的方法不仅会携带上面的参数,他的请求类型可能时:post,访问地址可能:xxxx/adduser。

后端的任务

从前端的视角来看,后端的任务应该总结为两个方面:

  • 1、辨别前端的具体要求(增删查改具体哪一个)
  • 2、根据要求实现增删查改的具体操作。

如何形象化一个后端模块?——controller层:门童?信使?管家?还是医院分诊台?

Controller层就是用来接收前端请求,并且辨别前端的具体要求,再去调用具体的实现方法,(分诊台护士将患者分配到不同的诊室就诊一样。)而分诊或者鉴别的依据,来源于前端具体的请求链接(还有请求的方法),用户管理的请求都以“/system/user“开头(看上一页的左上图)都会被这个类进行处理。

以新增用户这里,暂且略过@开头的注解,直接看函数的形参,SysUser似乎并不是前端发来的请求体,它实际上是一个domain层的类(下一节介绍,现阶段依旧可以朴素理解为前端发来的json数据)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@RestController
@RequestMapping("/system/user")
public class SysUserController extends BaseController
{
@Autowired
private ISysUserService userService;

@Autowired
private ISysRoleService roleService;

@Autowired
private ISysDeptService deptService;

@Autowired
private ISysPostService postService;
@PreAuthorize("@ss.hasPermi('system:user:add')")
@Log(title = "用户管理", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@Validated @RequestBody SysUser user)
{
deptService.checkDeptDataScope(user.getDeptId());
roleService.checkRoleDataScope(user.getRoleIds());
if (!userService.checkUserNameUnique(user))
{
return error("新增用户'" + user.getUserName() + "'失败,登录账号已存在");
}
else if (StringUtils.isNotEmpty(user.getPhonenumber()) && !userService.checkPhoneUnique(user))
{
return error("新增用户'" + user.getUserName() + "'失败,手机号码已存在");
}
else if (StringUtils.isNotEmpty(user.getEmail()) && !userService.checkEmailUnique(user))
{
return error("新增用户'" + user.getUserName() + "'失败,邮箱账号已存在");
}
user.setCreateBy(getUsername());
user.setPassword(SecurityUtils.encryptPassword(user.getPassword()));
return toAjax(userService.insertUser(user));
}
}

略过前面的检查与创建用户信息之类的步骤,直接看return语句中的userService.insertUser(user),这就是controller的作用,检查需求,调用服务,返回结果

如何形象化一个后端模块?——domain层:

现在我们知道了控制层如何鉴别请求类型的(其实就是判断请求的路径),那么该去调用具体的方法写入数据了吧!但是这里有一个问题,前端送来的一大坨数据不够美观、不够易用,调用服务总不能靠json传递吧,这可是面向对象的语言,我们需要对它进行简单的包装,从一个键值对转换为具体的类(domain层中的类)而且这个过程,是由上页中的函数类型:AjaxResult自己转换的!

我们要添加用户,需要处理用户业务相关的控制层,前端发来的用户的数据,也应该用一个用户类去包装一下。同理,如果是其他业务,比如请假,我们就需要请假的控制层、请假类。

如何形象化一个后端模块?——service层:无情的增删查改机器人

(个人视角!)
Service层常见接口与实现分离,其实不是什么高大上的东西,C++的类文件和头文件也是这种,一个声明一个实现。这里我们直接从实现部分,也就是service/impl文件夹下的内容开始。

牢记我们的目的:新增一个用户信息。所以我们要找插入数据的函数insertUser(user),详见controller调用的函数。

按照最初的目标,我们在这里应该要书写sql语句直接操作数据库,实现增删查改,但实际上它却又调用了一个新的服务,叫userMapper。也就是这个新服务实现了数据库的操作,还是以调用函数的方式实现的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Service
public class SysUserServiceImpl implements ISysUserService
{
@Autowired
private SysUserMapper userMapper;

@Override
@Transactional
public int insertUser(SysUser user)
{
// 新增用户信息
int rows = userMapper.insertUser(user);
// 新增用户岗位关联
insertUserPost(user);
// 新增用户与角色管理
insertUserRole(user);
return rows;
}
}

(增删改查的业务逻辑比较简单,所以很容易觉得:后端没必要这么麻烦呀,还分这么多层。实际在其他业务环节下,这个地方才是重点写代码的地方。)

如何形象化一个后端模块?——mapper层:将sql的查询结果绑定到具体的函数上

核心作用:将sql语句与Java的方法关联起来。

如果我们要添加信息,在服务层可能写sql语句:inser *****,之类的。一个自然的想法:我们可以将这部分的sql封装成函数,调用函数,等同于执行sql。而这个封装的结果,就是mapper层服务。正如服务层我们将接口与实现分开,mapper层也是如此,在mapper层中定义接口,

1
2
3
4
5
6
7
8
9
10
public interface SysUserMapper
{
/**
* 新增用户信息
*
* @param user 用户信息
* @return 结果
*/
public int insertUser(SysUser user);
}

src/main/resources/mapper/system/SysUserMapper.xml实现上面的接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<insert id="insertUser" parameterType="SysUser" useGeneratedKeys="true" keyProperty="userId">
insert into sys_user(
<if test="userId != null and userId != 0">user_id,</if>
<if test="deptId != null and deptId != 0">dept_id,</if>
<if test="userName != null and userName != ''">user_name,</if>
<if test="nickName != null and nickName != ''">nick_name,</if>
<if test="email != null and email != ''">email,</if>
<if test="avatar != null and avatar != ''">avatar,</if>
<if test="phonenumber != null and phonenumber != ''">phonenumber,</if>
<if test="sex != null and sex != ''">sex,</if>
<if test="password != null and password != ''">password,</if>
<if test="status != null and status != ''">status,</if>
<if test="createBy != null and createBy != ''">create_by,</if>
<if test="remark != null and remark != ''">remark,</if>
create_time
)values(
<if test="userId != null and userId != ''">#{userId},</if>
<if test="deptId != null and deptId != ''">#{deptId},</if>
<if test="userName != null and userName != ''">#{userName},</if>
<if test="nickName != null and nickName != ''">#{nickName},</if>
<if test="email != null and email != ''">#{email},</if>
<if test="avatar != null and avatar != ''">#{avatar},</if>
<if test="phonenumber != null and phonenumber != ''">#{phonenumber},</if>
<if test="sex != null and sex != ''">#{sex},</if>
<if test="password != null and password != ''">#{password},</if>
<if test="status != null and status != ''">#{status},</if>
<if test="createBy != null and createBy != ''">#{createBy},</if>
<if test="remark != null and remark != ''">#{remark},</if>
sysdate()
)
</insert>

整体结构:

另一个角度?

前文的叙述中我们以前端的请求展开流程的,如果我们换一个角度以实体业务优先,比如我们要处理请假业务:

  • 1、首先定义的应该是请假类,请假类包含什么信息?(姓名、年龄、理由、工号),
  • 2、如何对请假表进行增删改查?创建mapper映射,这就是为什么上一页中实体类有一个箭头到mapper
  • 3、针对请假业务书写service层。
  • 4、定义前端可以访问的接口,也就是控制层。

从以上这个角度似乎更好理解一点?