前言:本文基于若依前后端分离版本(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 {
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、定义前端可以访问的接口,也就是控制层。
从以上这个角度似乎更好理解一点?