概述
本篇文章主要记录如何开发一个通用的 SpringBoot 工程开发框架的项目模板,这样后续需要开发项目时就可以直接开箱直用了,省区了很多重复步骤。
项目初始化
创建项目:
按照我的选项来选,然后点击 create,等待文件索引完成。
然后我们就开始引入 pom 依赖,这里依然还是选用 spring boot 2.x 中比较新的版本,因为现在国内使用 spring boot 3.x 的公司还是比较少的,因为项目稳定才是最重要的嘛,而且虽然 spring boot 3.x 已经出来一段时间了,但是配套的生态还是没那么健全的,因此现在用风险太高,容易出很多的兼容性问题,因此还是选用 spring boot 2.x 的较新版本。
pom 依赖文件如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>org.thesea</groupId><artifactId>why-backend</artifactId><version>1.0-SNAPSHOT</version><properties><maven.compiler.source>8</maven.compiler.source><maven.compiler.target>8</maven.compiler.target><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><java.version>1.8</java.version><project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding><spring-boot.version>2.7.6</spring-boot.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- 切面编程 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency><!-- 数据库操作:https://mp.baomidou.com/ --><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.5.9</version></dependency><!-- 工具库:https://doc.hutool.cn/pages/index/ --><dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.8.26</version></dependency><!-- 接口文档:https://doc.xiaominfo.com/docs/quick-start#spring-boot-2--><dependency><groupId>com.github.xiaoymin</groupId><artifactId>knife4j-openapi2-spring-boot-starter</artifactId><version>4.4.0</version></dependency><dependency><groupId>com.mysql</groupId><artifactId>mysql-connector-j</artifactId><scope>runtime</scope></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency></dependencies><dependencyManagement><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-dependencies</artifactId><version>${spring-boot.version}</version><type>pom</type><scope>import</scope></dependency></dependencies></dependencyManagement></project>
注意上面 MySQL 的 Maven 坐标看着有些奇怪:
但实际上它和我们之前看到的更为普遍的坐标是同一个意思:
虽然有些版本或镜像库中可能提供 com.mysql:mysql-connector-j,但标准的、经过广泛验证的依赖配置是使用 mysql:mysql-connector-java
。如果依赖引入不正确,Spring Boot 可能找不到对应的驱动类,从而抛出 “Failed to determine a suitable driver class” 的错误。但这里我就懒得改了,如果产生问题就换上面这个更标准的写法就行,反正我这里是没有问题的。
然后创建如下工作目录:
启动主程序类,发现报错:
这个错误提示是因为 SpringBoot 应用会在启动的时候进行数据库连接信息的检查,而应用在启动时无法获取到足够的数据库连接信息。通过正确配置数据源的 URL、用户名、密码及驱动类,并确保引入相应的 JDBC 驱动依赖,可以解决这个问题。
因此还需要创建资源目录,在资源目录下面创建配置文件,其中进行数据库源的配置:
对于上面的 yml 文件中的内容做如下解释:
现在项目创建完之后先启动确定没有报错:
访问:
访问成功,因此项目就初始化好啦!
整合常见依赖
在上面的过程中,我们引入了 mybatis-plus 框架,其需要进行如下配置。
Mybatis-plus 常见配置信息
1、在 SpringBoot 的主启动类上添加包扫描注解,让Spring知道去哪里扫描 mapper 数据处理层,也就是和数据库访问 SQL 处理相关的内容。
首先在主程序类同级目录中创建一个 Mapper 层,这个包下存放的就是所有和数据库处理相关的接口逻辑:
然后在 SpringBoot 的主启动类上添加包扫描注解,让Spring知道去哪里扫描 mapper 数据处理层:
最后还可以在配置文件中进行一些常见的配置:
mybatis-plus:configuration:map-underscore-to-camel-case: false# only print sql log in developmentlog-impl: org.apache.ibatis.logging.stdout.StdOutImplglobal-config:db-config:logic-delete-field: isDeleted # 全局逻辑删除的实体字段名logic-delete-value: 1 # 逻辑已删除(默认为1)logic-not-delete-value: 0 # 逻辑未删除(默认为0)
解释如下:
另外可以看到之前引入 pom 依赖的时候我们没有引入 mybatis,这是因为 mybatis 已经集成在了 mybatis-plus 当中了,如果再引入 mybatis 可能会出现一些冲突问题,因此这里就不再引入 mybatis 了。
可以通过 maven 依赖树进行查看:
效果如下图所示,记得把右上角的树形结构给勾上,这样会更方便浏览一些:
Hutool 工具大全库
这个没什么内容需要说的,就是一个大而全的工具库,引入对应 maven 依赖即可,里面提供了非常多的小工具,可以阅读官方文档进行搜索。
Knife4j API 接口文档自动生成工具
前端开发怎么知道我们写了哪些接口呢?
难道非要我们后端开发自己写一个文档来给前端看吗?费时费力,肯定不会这样的。
因此我们还需要整合一个工具,就是当我们写完代码之后接口文档就自动生成了岂不是非常完美。
非常主流的就是Swagger 接口文档生成工具,然后国人在 Swagger 这个工具的上面进行了进一步的封装让其更加的方便使用,也就是 Knife4J 接口工具。
其功能非常简单:就是写完代码,就自动生成接口文档。
其依赖我们也已经引入了,然后 Knife4J是怎么知道我们的接口在哪儿长什么样的呢?
肯定就是要在配置文件中进行配置啦:
knife4j:enable: trueopenapi:title: 接口文档version: 1.0group:default:group-name: 默认分组api-rule: packageapi-rule-resources:- com.why.backend.controller
解析如下:
访问下面这个 url 地址我们就能找到接口文档了,比 Swagger 更加的清晰好用:
AOP 切面编程
这个也已经在 pom 中引入了捏,引入之后,最好还需要做一个配置。
就是在主启动类上添加一个注解 @EnableAspectJAutoProxy:
因为其中的 proxyTargetClass 默认是为 false 的,我们需要手动开启一下。
这个涉及到的是一个比较高级的功能,实际上不开启也没啥事,不过还是最好开一下比较专业嘛。
通用代码封装
自定义异常
新建一个 exception 包,其下面放的所有跟自定义异常处理相关的类。
首先是自定义的错误码:
package com.why.backend.exception;import lombok.Getter;@Getter
public enum ErrorCode {SUCCESS(0, "ok"),PARAMS_ERROR(40000, "请求参数错误"),NOT_LOGIN_ERROR(40100, "未登录"),NO_AUTH_ERROR(40101, "无权限"),NOT_FOUND_ERROR(40400, "请求数据不存在"),FORBIDDEN_ERROR(40300, "禁止访问"),SYSTEM_ERROR(50000, "系统内部异常"),OPERATION_ERROR(50001, "操作失败");/*** 状态码*/private final int code;/*** 信息*/private final String message;ErrorCode(int code, String message) {this.code = code;this.message = message;}}
自定义异常类:
package com.why.backend.exception;import lombok.Getter;/*** 自定义业务异常*/
@Getter
public class BusinessException extends RuntimeException{/*** 错误码*/private final int code;public BusinessException(int code, String message){super(message);this.code = code;}public BusinessException(ErrorCode errorCode){super(errorCode.getMessage());this.code = errorCode.getCode();}public BusinessException(ErrorCode errorCode,String message){super(message);this.code = errorCode.getCode();}
}
接下来是自定义异常断言类,为什么要有这个类?因为传统方式抛异常不优雅,传统方式抛异常如下:
if(a > b){throw new BusinessException(SystemError);
}
这种抛异常的方式就得写三行代码,不优雅,而断言异常就非常简单一行即可,示例如下:
assert a > b;
这样一行代码即可完成,如果 a <= b 的话就会直接抛异常了。
因此我们封装一个异常断言类:
package com.why.backend.exception;/*** 异常处理工具类*/
public class ThrowUtils {/*** 条件成立则抛异常** @param condition 条件* @param runtimeException 异常*/public static void throwIf(boolean condition, RuntimeException runtimeException) {if (condition) {throw runtimeException;}}/*** 条件成立则抛异常** @param condition 条件* @param errorCode 错误码*/public static void throwIf(boolean condition, ErrorCode errorCode) {throwIf(condition, new BusinessException(errorCode));}/*** 条件成立则抛异常** @param condition 条件* @param errorCode 错误码* @param message 错误信息*/public static void throwIf(boolean condition, ErrorCode errorCode, String message) {throwIf(condition, new BusinessException(errorCode, message));}
}
现在我们抛异常就只需要一行代码就可以了:
ThrowUtils.throwIf((a>b),ErrorCode.Error);
非常方便。
出现异常以后,我们如何返回呢?因此肯定需要一个全局的统一返回结果的类。
统一返回结果类:请求响应体封装
我们新建一个 common 包,在其下封装请求响应体封装类:
package com.why.backend.common;import com.why.backend.exception.ErrorCode;
import lombok.Data;import java.io.Serializable;/*** 全局响应封装类** @param <T>*/@Data
public class BaseResponse<T> implements Serializable {private int code;private T data;private String message;public BaseResponse(int code, T data, String message) {this.code = code;this.data = data;this.message = message;}public BaseResponse(int code, T data) {this(code, data, "");}public BaseResponse(ErrorCode errorCode) {this(errorCode.getCode(), null, errorCode.getMessage());}
}
但是这样在使用的时候还是不够优雅,比如我们使用时如下:
既然是以后再也不会重复写的通用代码,那肯定还是尽量更加简洁一点,因此我们还是可以再封装一个 ResultUtil 类来更加简化这个过程:
package com.why.backend.common;import com.why.backend.exception.ErrorCode;/*** 响应结果工具类*/
public class ResultUtils {/*** 成功** @param data 数据* @param <T> 数据类型* @return 响应*/public static <T> BaseResponse<T> success(T data) {return new BaseResponse<>(0, data, "ok");}/*** 失败** @param errorCode 错误码* @return 响应*/public static BaseResponse<?> error(ErrorCode errorCode) {return new BaseResponse<>(errorCode);}/*** 失败** @param code 错误码* @param message 错误信息* @return 响应*/public static BaseResponse<?> error(int code, String message) {return new BaseResponse<>(code, null, message);}/*** 失败** @param errorCode 错误码* @return 响应*/public static BaseResponse<?> error(ErrorCode errorCode, String message) {return new BaseResponse<>(errorCode.getCode(), null, message);}
}
现在返回结果就如下:
就更加简洁了。
全局异常处理封装
还有一个问题,如果我们的代码出现了意料之外的异常,那我们肯定也得考虑到怎么处理掉意外之外的异常,否则前端就会展示非常乱七八糟的异常信息:
比如这种。
我们必须捕获意外的异常然后返回一个对用户更加友好的结果。
全局异常处理器:
package com.why.backend.exception;import com.why.backend.common.BaseResponse;
import com.why.backend.common.ResultUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {@ExceptionHandler(BusinessException.class)public BaseResponse<?> businessExceptionHandler(BusinessException e) {log.error("BusinessException", e);return ResultUtils.error(e.getCode(), e.getMessage());}@ExceptionHandler(RuntimeException.class)public BaseResponse<?> runtimeExceptionHandler(RuntimeException e) {log.error("RuntimeException", e);return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "系统错误");}
}
分页请求类封装
因为分页这类请求参数都是相同的,因此也可以单独抽离出来成为一个工具类,之后相同的参数请求就可以不用再专门新建一个类了。
package com.why.backend.common;import lombok.Data;/*** 通用的分页请求类*/
@Data
public class PageRequest {/*** 当前页号*/private int current = 1;/*** 页面大小*/private int pageSize = 10;/*** 排序字段*/private String sortField;/*** 排序顺序(默认降序)*/private String sortOrder = "descend";
}
删除请求包装类封装
因为删除一般都是根据主键 id 来进行删除嘛,也是非常通用的,因此封装起来,接收要请求删除数据的 id 作为参数:
package com.why.backend.common;import lombok.Data;import java.io.Serializable;/*** 通用的删除请求类*/
@Data
public class DeleteRequest implements Serializable {/*** id*/private Long id;private static final long serialVersionUID = 1L;
}
全局跨域工具类封装
跨域是指浏览器访问的 URL(前端地址)和后端接口地址的域名(或端口号)不一致导致的,浏览器为了安全,默认禁止跨域请求访问。
为了开发调试方便,我们可以通过全局跨域配置,让整个项目所有的接口支持跨域,解决跨域报错,新建配置包,用于存放所有的配置相关代码。
新建 config 包用于存放所有配置相关的代码,全局跨域配置代码如下:
package com.why.backend.config;import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;/*** 全局跨域配置*/@Configuration
public class CorsConfig implements WebMvcConfigurer {@Overridepublic void addCorsMappings(CorsRegistry registry) {// 覆盖所有请求registry.addMapping("/**")// 允许发送 Cookie.allowCredentials(true)// 放行哪些域名(必须用 patterns,否则 * 会和 allowCredentials 冲突).allowedOriginPatterns("*").allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS").allowedHeaders("*").exposedHeaders("*");}
}
健康检查接口封装
编写示例接口,移除 controller 包下的其他代码,让项目干净一些,然后编写一个纯净的 /health 接口用于健康检査:
package com.why.backend.controller;import com.why.backend.common.BaseResponse;
import com.why.backend.common.ResultUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;/*** 健康检查接口*/@RestController
@RequestMapping("/")
public class HealthController {/*** 应用健康检查** @return 应用运行正常*/@GetMapping("/health")public BaseResponse<String> health() {return ResultUtils.success("OK");}
}
初始化完成、测试与最终项目结构与代码地址
测试:
最终项目结构:
这样一个通用的 SpringBoot 项目开发框架就整完啦。
代码仓库地址:后端 SpringBoot 工程脚手架项目Gitee地址;
整合通用CRUD业务、用户权限登录注册
需求分析
对于用户模块,通常要具有下列功能:
- 用户注册
- 用户登录
- 获取当前登录用户
- 用户注销
- 用户权限控制
- 【管理员】管理用户
具体分析每个需求:
1、用户注册:用户可以通过输入账号、密码、确认密码进行注册
2、用户登录:用户可以通过输入账号和密码登录
3、获取当前登录用户:得到当前已经登录的用户信息(不用重复登录)
4、用户注销:用户可以退出登录
5、用户权限控制:用户又分为普通用户和管理员,管理员拥有整个系统的最高权限,比如可以管理其他用户
6、用户管理:仅管理员可用,可以对整个系统中的用户进行管理,比如搜索用户、删除用户
方案设计
实现用户模块的难度不大,方案设计阶段我们需要确认:
- 库表设计
- 用户登录流程
- 如何对用户权限进行控制?
库表设计
库名:backend
表名:user(用户表)
核心设计
用户表的核心是用户登录凭证(账号密码)和个人信息,SQL 如下:
-- 创建数据库create database if not exists backend;-- 切换库use backend;-- 用户表
create table if not exists user
(id bigint auto_increment comment 'id' primary key,userAccount varchar(256) not null comment '账号',userPassword varchar(512) not null comment '密码',userName varchar(256) null comment '用户昵称',userAvatar varchar(1024) null comment '用户头像',userProfile varchar(512) null comment '用户简介',userRole varchar(256) default 'user' not null comment '用户角色:user/admin',editTime datetime default CURRENT_TIMESTAMP not null comment '编辑时间',createTime datetime default CURRENT_TIMESTAMP not null comment '创建时间',updateTime datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间',isDelete tinyint default 0 not null comment '是否删除',UNIQUE KEY uk_userAccount (userAccount),INDEX idx_userName (userName)) comment '用户' collate = utf8mb4_unicode_ci;
几个注意事项:
1、editTime 和 updateTime 的区别:editTime 表示用户编辑个人信息的时间(需要业务代码来更新),而 updateTime 表示这条用户记录任何字段发生修改的时间(由数据库自动更新)。
2、给唯一值添加唯一键(唯一索引),比如账号userAccount,利用数据库天然防重复,同时可以增加查询效率。
3、给经常用于查询的字段添加索引,比如用户昵称userName,可以增加查询效率。
建议养成好习惯,将库表设计 SQL 保存到项目的目录中,比如新建 sql/create_table.sql 文件,这样其他开发者就能更快地了解项目。
然后全选右键执行:
执行完之后就创建完成了:
现在我们就可以用 IDEA 自带的数据库管理工具来进行数据的操作了。
拓展设计
1、如果要实现会员功能,可以对表进行如下扩展:
1)给 userRole 字段新增枚举值 vip ,表示会员用户,可根据该值判断用户权限;
2)新增会员过期时间字段,可用于记录会员有效期;
3)新增会员兑换码字段,可用于记录会员的开通方式
4)新增会员编号字段,可便于定位用户并提供额外服务,并增加会员归属感
对应的 SQL 如下:
vipExpireTime datetime null comment '会员过期时间',
vipCode varchar(128) null comment '会员兑换码',
vipNumber bigint null comment '会员编号'
2、如果要实现用户邀请功能,可以对表进行如下扩展:
1)新增 shareCode 分享码字段,用于记录每个用户的唯一邀请标识,可拼接到邀请网址后面,比如
https://mianshiya.com/?shareCode=xxx
2)新增 inviteUser 字段,用于记录该用户被哪个用户邀请了,可通过这个字段查询某用户邀请的用户列表。
对应的 SQL 如下:
shareCode varchar(20) DEFAULT NULL COMMENT '分享码',
inviteUser bigint DEFAULT NULL COMMENT '邀请用户 id'
用户登录流程
这个部分我们使用 Spring 的分布式 Session 结合 Redis 的登录方案,Spring Session 的介绍如下:
具体的使用细节会在登录接口开发的过程中说清楚的,总之有了 SpringSession,以后 Session 会话就是存储在 Redis 中的了,我们操作 HttpSession 都是通过Redis来完成,但我们不需要亲自操作Redis,因为相关操作都由Spring在底层为我们做完了,我们就像以前一样操作 HttpSession 即可。
关于 Cookie 和 Session 可以看我的这篇文章:Cookie和Session技术;
登录业务流程如下:
1、建立初始会话:前端与服务器建立连接后,服务器会为该客户端创建一个初始的匿名 Session,并将其状态保存下来。这个 Session 的 ID 会作为唯一标识,返回给前端。
2、登录成功,更新会话信息:当用户在前端输入正确的账号密码并提交到后端验证成功后,后端会更新该用户的 Session,将用户的登录信息(如用户 ID、用户名等)保存到与该 Session 关联的存储中。同时,服务器会生成一个 Set-Cookie 的响应头,指示前端保存该用户的 Session ID。
3、前端保存 Cookie:前端接收到后端的响应后,浏览器会自动根据 Set-Cookie 指令,将 Session ID 存储到浏览器的 Cookie 中,与该域名绑定。
4、带 Cookie 的后续请求:当前端再次向相同域名的服务器发送请求时,浏览器会自动在请求头中附带之前保存的 Cookie,其中包含 Session ID。
5、后端验证会话:服务器接收到请求后,从请求头中提取 Session ID,找到对应的 Session 数据。
6、获取会话中存储的信息:后端通过该 Session 获取之前存储的用户信息(如登录名、权限等),从而识别用户身份并执行相应的业务逻辑。
用户权限控制
可以将接口分为 4 种权限:
- 未登录也可以使用
- 登录用户才能使用
- 未登录也可以使用,但是登录用户能进行更多操作(比如
登录后查看全文) - 仅管理员才能使用
传统的权限控制方法是,在每个接口内单独编写逻辑:先获取到当前登录用户信息,然后判断用户的权限是否符合要求。
这种方法最灵活,但是会写很多重复的代码,而且其他开发者无法一眼得知接口所需要的权限。
权限校验其实是一个比较通用的业务需求,一般会通过Spring AOP 切面 + 自定义权限校验注解 实现统一的接口拦截和权限校验;如果有特殊的权限校验逻辑,再单独在接口中编码。
如果需要更复杂更灵活的权限控制,可以引入 Shiro / Spring Security / Sa-Token 等专门的权限管理框架。
后端接口开发
以后每次开发新接口时,都可以遵循以下流程进行开发。
数据访问层代码生成
CRUD这些代码都是通用的,因此没必要手写,我们直接使用代码生成器自动生成即可,推荐安装一个插件:mybatisx:
安装这个插件以后,我们就可以使用它对数据库表进行一键生成表所对应的 controller、mapper、service、serviceimpl 层的通用代码了。
选中要生成代码的表:
点击 Generator:
这里要将 module path 中的内容填好,然后其它的都不用改,next:
对于这一页我们要改一些东西,因为我们的使用的 ORM 类型是 Mybatis-Plus ,因此我们要改用 mp3 而不是 None,然后需要生成注释 Comment,需要生成toString/hashCode/equals但是是改用lombok的形式,然后数据库字段名称需要和实体类名称一一对应,勾上 Actual Column,template 我们也选择 mp3 的格式,勾选完成应该如下:
然后点击 Finish 即可生成代码:
可以看到这些文件是红色的,这是因为我们没有将它们add到我们的 Git 当中,我们可以直接右键项目的根目录,点击 Git :
然后就绿了:
这样我们就可以用 Git 来管理这些代码了,如果这些代码不想要的话,我们还可以回滚随时还原回去:
这里就不再演示。
然后我们新建一个 model 包用来存放我们所有的实体类,在该包下创建一个 entity 包,将我们的 user 实体类从 generate 包中拖进去:
然后另外几个包中的内容也以此类推,然后直接删除掉 generate 包即可:
对生成的数据模型修正
实体类修正
生成的代码也许不能完全满足我们的要求,比如数据库实体类,我们可以手动更改其字段配置,指定主键策略和逻辑删除。
- id 默认是连续生成的,容易被爬虫抓取,所以更换策略为ASSIGN_ID 雪花算法生成。
- 数据删除时默认为彻底删除记录,如果出现误删,将难以恢复,所以采用逻辑删除 —— 通过修改 isDelete 字段为 1 表示已失效的数据。
修改的代码如下:
package com.why.backend.model.entity;import com.baomidou.mybatisplus.annotation.*;import java.io.Serializable;
import java.util.Date;
import lombok.Data;/*** 用户* @TableName user*/
@TableName(value ="user")
@Data
public class User implements Serializable {private static final long serialVersionUID = 8027039040596212323L;/*** id*/@TableId(type = IdType.ASSIGN_ID)private Long id;/*** 账号*/private String userAccount;/*** 密码*/private String userPassword;/*** 用户昵称*/private String userName;/*** 用户头像*/private String userAvatar;/*** 用户简介*/private String userProfile;/*** 用户角色:user/admin*/private String userRole;/*** 编辑时间*/private Date editTime;/*** 创建时间*/private Date createTime;/*** 更新时间*/private Date updateTime;/*** 是否删除*/@TableLogicprivate Integer isDelete;
}
这里必须要让实体实现序列化接口,因为后面我们要做基于分布式Session的登录操作,没有这个序列化号的话会出问题,当然如果不使用分布式Session技术的话其实加不加无所谓的,都不会有什么问题。
什么问题呢?问题如下:
实现 Serializable 接口的作用:
serialVersionUID 是用来标识类的版本的一种机制。这个值理论上可以任意指定,只要是一个 long 类型的常量即可。
添加用户角色枚举类
对于用户角色这样值的数量有限的、可枚举的字段,最好定义一个枚举类,便于在项目中获取值、减少枚举值输入错误的情况。
在 model.enums 包下新建 UserRoleEnum:
package com.why.backend.model.enums;import cn.hutool.core.util.ObjUtil;
import lombok.Getter;/*** 用户角色枚举*/
@Getter
public enum UserRoleEnum {USER("用户", "user"),ADMIN("管理员", "admin");private final String text;private final String value;UserRoleEnum(String text, String value) {this.text = text;this.value = value;}/*** 根据 value 获取枚举** @param value 枚举值的value* @return 枚举值*/public static UserRoleEnum getEnumByValue(String value) {if (ObjUtil.isEmpty(value)) {return null;}for (UserRoleEnum anEnum : UserRoleEnum.values()) {if (anEnum.value.equals(value)) {return anEnum;}}return null;}
}
其中,getEnumByValue 是通过 value 找到具体的枚举对象。
另外如果枚举值特别多,可以 Map 缓存所有枚举值来加速查找,而不是遍历列表。
下面依次进行各功能接口的开发。
用户注册
创建DTO/VO数据模型
在 model.dto.user 下新建用于接受请求参数的类:
package com.why.backend.model.dto;import lombok.Data;import java.io.Serializable;/*** 用户注册请求*/
@Data
public class UserRegisterRequest implements Serializable {private static final long serialVersionUID = 3191241716373120793L;/*** 账号*/private String userAccount;/*** 密码*/private String userPassword;/*** 确认密码*/private String checkPassword;
}
在 Java 接口开发中,为每个接口定义一个专门的类来接收请求参数,可以提高代码的可读性和维护性,便于对参数进行统一验证和扩展,同时减少接口方法参数过多导致的复杂性,有助于在复杂场景下更清晰地管理和传递数据。
这里有个快速生成序列化ID的插件:GenerateSerialVersionUID:
然后就可以像用 IDEA 自动生成构造器方法那样来快速生成序列化ID啦,右键然后选择 Generate 就会出现下面的弹窗:
但实际上这个 Serializable 并不是很重要,主要是用来防止序列化对象恢复后发现和原对象不一致(也就是在序列化到反序列化过程中对象可能被修改过)用的,基本上是遇不到这个问题的,不写也没有什么关系,但我们这里使用到了分布式Session登录的逻辑,因此需要进行序列化操作,所以必须要写上。
Service服务层开发
在 service 包的 UserService 中增加方法声明:
package com.why.backend.service;import com.why.backend.model.dto.UserRegisterRequest;
import com.why.backend.model.entity.User;
import com.baomidou.mybatisplus.extension.service.IService;/**
* @author TheSea
* @description 针对表【user(用户)】的数据库操作Service
* @createDate 2025-03-31 17:20:24
*/
public interface UserService extends IService<User> {/*** 用户注册** @param userRegisterRequest 请求对象* @return 新用户 id*/long userRegister(UserRegisterRequest userRegisterRequest);
}
在 UserServiceImpl 中增加实现代码,注意多补充一些校验条件:
/*** 用户注册** @param userRegisterRequest 请求对象* @return 新用户 id*/@Overridepublic long userRegister(UserRegisterRequest userRegisterRequest) {//1、校验参数if(StrUtil.hasBlank(userRegisterRequest.getUserAccount(),userRegisterRequest.getUserPassword(),userRegisterRequest.getCheckPassword())){throw new BusinessException(ErrorCode.PARAMS_ERROR,"参数为空");}if(userRegisterRequest.getUserAccount().length() < 4){throw new BusinessException(ErrorCode.PARAMS_ERROR,"用户账号过短");}if(userRegisterRequest.getUserPassword().length() < 8 || userRegisterRequest.getCheckPassword().length() < 8){throw new BusinessException(ErrorCode.PARAMS_ERROR,"用户密码过短");}if(!userRegisterRequest.getUserPassword().equals(userRegisterRequest.getCheckPassword())){throw new BusinessException(ErrorCode.PARAMS_ERROR,"两次输入的密码不一致");}//2、检查用户账号是否和数据库中已有的重复QueryWrapper<User> queryWrapper = new QueryWrapper<>();queryWrapper.eq("userAccount",userRegisterRequest.getUserAccount());long count = this.baseMapper.selectCount(queryWrapper);if(count > 0){throw new BusinessException(ErrorCode.PARAMS_ERROR,"账号重复");}//3、密码一定要加密String encryptPassword = getEncryptPassword(userRegisterRequest.getUserPassword());//4、插入数据到数据库中User user = new User();user.setUserAccount(userRegisterRequest.getUserAccount());user.setUserPassword(encryptPassword);user.setUserName("无名");user.setUserRole(UserRoleEnum.USER.getValue());boolean saveResult = this.save(user);if(!saveResult){throw new BusinessException(ErrorCode.SYSTEM_ERROR,"注册失败,数据库错误");}//这一步是 MybatisPlus 框架自动做的,叫主键回填,因此不需要从数据库中查也能知道主键id是什么return user.getId();}
注意,上述代码中,我们需要将用户密码加密后进行存储。可以封装一个方法,便于后续复用:
/*** 密码加密实现* @param userPassword 用户密码* @return 加密后密码*/@Overridepublic String getEncryptPassword(String userPassword){// 加盐,混淆密码final String SALT = "why";return DigestUtils.md5DigestAsHex((SALT + userPassword).getBytes());}
Controller层接口开发
在 controller 包中新建 UserController,新增用户注册接口:
@RestController
@RequestMapping("/user")
public class UserController {@Resourceprivate UserService userService;/*** 用户注册*/@PostMapping("/register")public BaseResponse<Long> userRegister(@RequestBody UserRegisterRequest userRegisterRequest) {ThrowUtils.throwIf(userRegisterRequest == null, ErrorCode.PARAMS_ERROR);long result = userService.userRegister(userRegisterRequest);return ResultUtils.success(result);}
}
测试接口
每开发完一个接口,都可以使用 Swagger 接口文档来测试:
校验失败的情况:
校验成功插入数据库:
数据库中出现数据:
测试成功。
用户登录
分布式Session登录的解决方案
采用分布式Session 的登录方式,步骤如下:
引入Spring Session 和 Redis 客户端依赖:
然后配置 Redis 相关信息,主要配置一个redis的地址,然后再配置一下 Tomcat 服务器的 session 的过期时间,因为使用了SpringSession,因此 Session 的控制就交给了 Spring,我们直接在 Sping 的配置项中进行配置即可:
启用 Spring Session:通过配置类启用 Spring Session,将 Redis 作为 Session 存储介质。
在 config 包下写入 Spring Session 的配置类:
package com.why.backend.config;import org.springframework.context.annotation.Configuration;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;/*** SpringSession会话配置*/
@Configuration
@EnableRedisHttpSession
public class SessionConfig {}
现在我们就可以像以前那样直接使用 HttpSession 了。启用 Spring Session 后,它会自动拦截对 Session 的操作,并将数据存储到 Redis 中,实现与 Redis 的交互,我们无需手动编写相关与 Redis 交互的代码了。
一句话就是之前怎么使用 Session 技术现在就怎么使用即可,底层与 Redis 的通信细节不需要我们关心。
创建DTO/VO数据模型
在 model.dto.user 下新建用于接受请求参数的类:
/*** 用户登录请求*/
@Data
public class UserLoginRequest implements Serializable {private static final long serialVersionUID = 3191241716373120793L;/*** 账号*/private String userAccount;/*** 密码*/private String userPassword;
}
返回给前端时的一些字段是不需要提供的,这个过程称为 数据脱敏
,因此我们要返回一个封装的 VO 视图对象给前端。
在 model.vo 包下新建 LoginUserVO 类,表示脱敏后的登录用户信息:
/*** 脱敏后的用户登录信息*/
@Data
public class LoginUserVO implements Serializable {/*** 用户 id*/private Long id;/*** 账号*/private String userAccount;/*** 用户昵称*/private String userName;/*** 用户头像*/private String userAvatar;/*** 用户简介*/private String userProfile;/*** 用户角色:user/admin*/private String userRole;/*** 创建时间*/private Date createTime;/*** 更新时间*/private Date updateTime;private static final long serialVersionUID = 1L;
}
在 constant 包下新建 UserConstant 类,统一声明用户相关的常量:
package com.why.backend.constant;/*** 用户相关常量*/
public interface UserConstant {/*** 用户登录态键*/String USER_LOGIN_STATE = "user_login";// region 权限/*** 默认角色*/String DEFAULT_ROLE = "user";/*** 管理员角色*/String ADMIN_ROLE = "admin";// endregion
}
Service服务层开发
在 service 包的 UserService 中增加方法声明:
在 UserServiceImpl 中增加实现代码,注意多补充一些校验条件,在用户登录成功后,将用户信息存储在当前的 Session 中。代码如下:
注意,由于注册用户时存入数据库的密码是加密后的,查询用户信息时,也要对用户输入的密码进行同样算法的加密,才能跟数据库的信息对应上。
可以把上述的 Session 理解为一个 Map,可以给 Map 设置 key 和 value,每个不同的 SessionID 对应的 Session 存储都是不同的,不用担心会污染。所以上述代码中,给 Session 设置了固定的 key(USER_LOGIN_STATE),可以将这个 key 值提取为常量,便于后续获取(constant 包下的 UserConstant 类,统一声明了用户相关的常量)。
Controller层接口开发
在 UserController 中新增用户登录接口:
/*** 用户登录*/@PostMapping("/login")public BaseResponse<LoginUserVO> userLogin(@RequestBody UserLoginRequest userLoginRequest, HttpServletRequest request) {ThrowUtils.throwIf(userLoginRequest == null, ErrorCode.PARAMS_ERROR);LoginUserVO loginUserVO = userService.userLogin(userLoginRequest,request);return ResultUtils.success(loginUserVO);}
测试接口
在 Redis 中:
因此登录功能完成了就。
获取当前登录用户接口
可以从 request 请求对象对应的 Session 中直接获取到之前保存的登录用户信息,无需其他请求参数。
先写一个用于 service 层之间内部使用的获取当前登录用户信息的方法,此处为了保证获取到的数据始终是最新的,先从 Session 中获取登录用户的 id,然后从数据库中查询最新的结果。代码如下:
/*** Service 层内部服务之间用于获取当前登录用户信息* @param request 请求域* @return 完整的用户信息*/public User getLoginUser(HttpServletRequest request){//判断是否登录Object userObj = request.getSession().getAttribute(UserConstant.USER_LOGIN_STATE);User currentUser = (User) userObj;if(currentUser == null || currentUser.getId() == null){throw new BusinessException(ErrorCode.NOT_LOGIN_ERROR);}//从数据库中查询做一个数据一致性更新(如果是追求性能的话,这一步可以忽略,做数据弱一致性即可)//用户有可能登录之后修改了自己的一些信息,因此我们还需要从数据库中再查一遍Long userId = currentUser.getId();currentUser = this.getById(userId);if(currentUser == null){throw new BusinessException(ErrorCode.NOT_LOGIN_ERROR);}return currentUser;}
然后再去写一个接口方法用于返回给前端用的获取当前登录用户信息:
/*** 获得脱敏后的登录用户信息* @param user 完整的用户信息* @return 脱敏后的登录用户信息*/public LoginUserVO getLoginUserVO(User user) {if(user == null) return null;LoginUserVO loginUserVO = new LoginUserVO();BeanUtil.copyProperties(user,loginUserVO);return loginUserVO;}
在 UserController 中新增获取当前登录用户接口:
用户注销
可以从 request 请求对象对应的 Session 中直接获取到之前保存的登录用户信息,来完成注销,无需其他请求参数。
在 service 包的 UserService 中增加方法声明:
在 UserServiceImpl 中增加实现代码,从 Session 中移除掉当前用户的登录态即可:
在 UserController 中新增用户注销接口:
用户权限控制
这里使用AOP来实现这个过程。
权限校验其实是一个比较通用的业务需求,一般会通过 Spring AOP 切面 + 自定义权限校验
注解实现统一的接口拦截和权限校验;如果有特殊的权限校验逻辑,再单独在接口中编码。
权限校验注解
首先编写权限校验注解,放到 annotation 包下:
package com.why.backend.annotation;import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AuthCheck {/*** 必须具有某个角色*/String mustRole() default "";}
权限校验切面
编写权限校验 AOP,采用环绕通知,在 打上该注解的方法 执行前后进行一些额外的操作,比如校验权限。
代码如下,放到 aop 包下:
package com.why.backend.aop;import com.why.backend.annotation.AuthCheck;
import com.why.backend.exception.BusinessException;
import com.why.backend.exception.ErrorCode;
import com.why.backend.model.entity.User;
import com.why.backend.model.enums.UserRoleEnum;
import com.why.backend.service.UserService;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;@Aspect
@Component
public class AuthInterceptor {@Resourceprivate UserService userService;/*** 执行拦截* @param joinPoint 切入点* @param authCheck 权限校验注解* @return*/@Around("@annotation(authCheck)")public Object doInterceptor(ProceedingJoinPoint joinPoint, AuthCheck authCheck) throws Throwable {String mustRole = authCheck.mustRole();RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();//获取当前登录用户User loginUser = userService.getLoginUser(request);UserRoleEnum mustRoleEnum = UserRoleEnum.getEnumByValue(mustRole);//如果不需要权限,那就放行继续执行原有方法逻辑if(mustRoleEnum == null){return joinPoint.proceed();}//以下的代码:必须有权限,才会通过UserRoleEnum userRoleEnum = UserRoleEnum.getEnumByValue(loginUser.getUserRole());if(userRoleEnum == null){throw new BusinessException(ErrorCode.NO_AUTH_ERROR);}//要求必须有管理员权限,但用户没有管理员权限,拒绝if(UserRoleEnum.ADMIN.equals(mustRoleEnum) && !UserRoleEnum.ADMIN.equals(userRoleEnum)){throw new BusinessException(ErrorCode.NO_AUTH_ERROR);}//通过权限校验,放行return joinPoint.proceed();}}
解释如下:
“表示该类是一个切面(Aspect),AOP 框架会扫描并识别该类中定义的通知(Advice)。”这一句是什么意思?
使用注解
只要给方法添加了 @AuthCheck 注解,就必须要登录,否则会抛出异常。
可以设置 mustRole 为管理员,这样仅管理员才能使用该接口:
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
对于不需要登录就能使用的接口,不需要使用该注解。
AOP切面编程一般流程总结
下面是一个例子:
用户信息CRUD管理
用户管理功能具体可以拆分为:
- 【管理员】创建用户
- 【管理员】根据 id 删除用户
- 【管理员】更新用户
- 【管理员】分页获取用户列表(需要脱敏)
- 【管理员】根据 id 获取用户(未脱敏)
- 根据 id 获取用户(脱敏)
创建DTO/VO数据模型
每个操作都需要提供一个请求类,都放在 dto.user 包下:
用户创建请求:
@Data
public class UserAddRequest implements Serializable {/*** 用户昵称*/private String userName;/*** 账号*/private String userAccount;/*** 用户头像*/private String userAvatar;/*** 用户简介*/private String userProfile;/*** 用户角色: user, admin*/private String userRole;private static final long serialVersionUID = 1L;
}
用户更新请求:
@Data
public class UserUpdateRequest implements Serializable {/*** id*/private Long id;/*** 用户昵称*/private String userName;/*** 用户头像*/private String userAvatar;/*** 简介*/private String userProfile;/*** 用户角色:user/admin*/private String userRole;private static final long serialVersionUID = 1L;
}
用户查询请求,需要继承公共包中的 PageRequest 来支持分页查询:
@EqualsAndHashCode(callSuper = true)
@Data
public class UserQueryRequest extends PageRequest implements Serializable {/*** id*/private Long id;/*** 用户昵称*/private String userName;/*** 账号*/private String userAccount;/*** 简介*/private String userProfile;/*** 用户角色:user/admin/ban*/private String userRole;private static final long serialVersionUID = 1L;
}
@EqualsAndHashCode(callSuper = true) 是 Lombok 提供的注解,用于自动生成 equals() 和 hashCode() 方法。
设置 callSuper = true 表示在生成这两个方法时,会调用父类的 equals() 和 hashCode() 方法,也就是将父类中的属性也纳入到比较和哈希值的计算中。
这对于继承了非空父类(例如这里的 PageRequest)且父类中包含需要参与对象相等性比较的字段非常有用。
由于要提供获取用户信息的接口,需要和获取当前登录用户接口一样对用户信息进行脱敏。
在 model.vo 包下新建 UserVO,表示脱敏后的用户:
@Data
public class UserVO implements Serializable {/*** id*/private Long id;/*** 账号*/private String userAccount;/*** 用户昵称*/private String userName;/*** 用户头像*/private String userAvatar;/*** 用户简介*/private String userProfile;/*** 用户角色:user/admin*/private String userRole;/*** 创建时间*/private Date createTime;private static final long serialVersionUID = 1L;
}
这里没有删除操作,因为删除比较模板化,就是根据ID来删除某个记录,因此我们之前就将它抽象成了一个通用类了,在 common 包下:
Service服务层开发
在 UserService 中编写获取脱敏后的单个用户信息、获取脱敏后的用户列表方法:
@Override
public UserVO getUserVO(User user) {if (user == null) {return null;}UserVO userVO = new UserVO();BeanUtils.copyProperties(user, userVO);return userVO;
}@Override
public List<UserVO> getUserVOList(List<User> userList) {if (CollUtil.isEmpty(userList)) {return new ArrayList<>();}return userList.stream().map(this::getUserVO).collect(Collectors.toList());
}
除了上述方法外,对于分页查询接口,需要根据用户传入的参数来构造 SQL 查询。由于使用 MyBatis Plus 框架,不用自己拼接 SQL 了,而是通过构造 QueryWrapper 对象来生成 SQL 查询。
可以在 UserService 中编写一个方法,专门用于将查询请求转为QueryWrapper 对象:
@Override
public QueryWrapper<User> getQueryWrapper(UserQueryRequest userQueryRequest) {if (userQueryRequest == null) {throw new BusinessException(ErrorCode.PARAMS_ERROR, "请求参数为空");}Long id = userQueryRequest.getId();String userAccount = userQueryRequest.getUserAccount();String userName = userQueryRequest.getUserName();String userProfile = userQueryRequest.getUserProfile();String userRole = userQueryRequest.getUserRole();String sortField = userQueryRequest.getSortField();String sortOrder = userQueryRequest.getSortOrder();QueryWrapper<User> queryWrapper = new QueryWrapper<>();queryWrapper.eq(ObjUtil.isNotNull(id), "id", id);queryWrapper.eq(StrUtil.isNotBlank(userRole), "userRole", userRole);queryWrapper.like(StrUtil.isNotBlank(userAccount), "userAccount", userAccount);queryWrapper.like(StrUtil.isNotBlank(userName), "userName", userName);queryWrapper.like(StrUtil.isNotBlank(userProfile), "userProfile", userProfile);queryWrapper.orderBy(StrUtil.isNotEmpty(sortField), sortOrder.equals("ascend"), sortField);return queryWrapper;
}
Controller层接口开发
上述功能其实都是样板代码,俗称 “增删改查”。
代码实现比较简单,注意添加对应的权限注解、做好参数校验即可:
/*** 创建用户*/
@PostMapping("/add")
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
public BaseResponse<Long> addUser(@RequestBody UserAddRequest userAddRequest) {ThrowUtils.throwIf(userAddRequest == null, ErrorCode.PARAMS_ERROR);User user = new User();BeanUtils.copyProperties(userAddRequest, user);// 默认密码 12345678final String DEFAULT_PASSWORD = "12345678";String encryptPassword = userService.getEncryptPassword(DEFAULT_PASSWORD);user.setUserPassword(encryptPassword);boolean result = userService.save(user);ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);return ResultUtils.success(user.getId());
}/*** 根据 id 获取用户(仅管理员)*/
@GetMapping("/get")
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
public BaseResponse<User> getUserById(long id) {ThrowUtils.throwIf(id <= 0, ErrorCode.PARAMS_ERROR);User user = userService.getById(id);ThrowUtils.throwIf(user == null, ErrorCode.NOT_FOUND_ERROR);return ResultUtils.success(user);
}/*** 根据 id 获取包装类*/
@GetMapping("/get/vo")
public BaseResponse<UserVO> getUserVOById(long id) {BaseResponse<User> response = getUserById(id);User user = response.getData();return ResultUtils.success(userService.getUserVO(user));
}/*** 删除用户*/
@PostMapping("/delete")
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
public BaseResponse<Boolean> deleteUser(@RequestBody DeleteRequest deleteRequest) {if (deleteRequest == null || deleteRequest.getId() <= 0) {throw new BusinessException(ErrorCode.PARAMS_ERROR);}boolean b = userService.removeById(deleteRequest.getId());return ResultUtils.success(b);
}/*** 更新用户*/
@PostMapping("/update")
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
public BaseResponse<Boolean> updateUser(@RequestBody UserUpdateRequest userUpdateRequest) {if (userUpdateRequest == null || userUpdateRequest.getId() == null) {throw new BusinessException(ErrorCode.PARAMS_ERROR);}User user = new User();BeanUtils.copyProperties(userUpdateRequest, user);boolean result = userService.updateById(user);ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);return ResultUtils.success(true);
}/*** 分页获取用户封装列表(仅管理员)** @param userQueryRequest 查询请求参数*/
@PostMapping("/list/page/vo")
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
public BaseResponse<Page<UserVO>> listUserVOByPage(@RequestBody UserQueryRequest userQueryRequest) {ThrowUtils.throwIf(userQueryRequest == null, ErrorCode.PARAMS_ERROR);long current = userQueryRequest.getCurrent();long pageSize = userQueryRequest.getPageSize();Page<User> userPage = userService.page(new Page<>(current, pageSize),userService.getQueryWrapper(userQueryRequest));Page<UserVO> userVOPage = new Page<>(current, pageSize, userPage.getTotal());List<UserVO> userVOList = userService.getUserVOList(userPage.getRecords());userVOPage.setRecords(userVOList);return ResultUtils.success(userVOPage);
}
通用问题解决
分页功能修复
必须要注意,本项目使用的 MybatisPlus 框架 v3.5.9 版本引入分页插件的方式和之前不同!
v3.5.9 版本后需要独立安装分页插件依赖!!!
在 pom.xml 中引入分页插件依赖:
<!-- MyBatis Plus 分页插件 -->
<dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-jsqlparser-4.9</artifactId>
</dependency>
光引入这一条,大概率是无法成功下载依赖的,还要在 pom.xml 的依赖管理配置中补充 mybatis-plus-bom :
<dependencyManagement><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-dependencies</artifactId><version>${spring-boot.version}</version><type>pom</type><scope>import</scope></dependency><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-bom</artifactId><version>3.5.9</version><type>pom</type><scope>import</scope></dependency></dependencies>
</dependencyManagement>
依赖下载成功后,在 config 包下新建 MyBatis Plus 拦截器配置,添加分页插件:
@Configuration
@MapperScan("com.why.backend.mapper")
public class MyBatisPlusConfig {/*** 拦截器配置** @return {@link MybatisPlusInterceptor}*/@Beanpublic MybatisPlusInterceptor mybatisPlusInterceptor() {MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();// 分页插件interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));return interceptor;}
}
然后重启项目即可。
数据精度恢复
由于前端 JS 的精度范围有限,我们后端返回的 id 范围过大(Long类型),导致前端精度丢失,会影响前端页面获取到的数据结果。
为了解决这个问题,可以在后端 config 包下新建一个全局 JSON 配置,将整个后端 Spring MVC 接口返回值的长整型数字转换为字符串进行返回,从而集中解决问题。
/*** Spring MVC Json 配置*/
@JsonComponent
public class JsonConfig {/*** 添加 Long 转 json 精度丢失的配置*/@Beanpublic ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) {ObjectMapper objectMapper = builder.createXmlMapper(false).build();SimpleModule module = new SimpleModule();module.addSerializer(Long.class, ToStringSerializer.instance);module.addSerializer(Long.TYPE, ToStringSerializer.instance);objectMapper.registerModule(module);return objectMapper;}
}
然后重启项目即可,后续可以按需完善上述代码。
然后在开发别的CRUD模块时可以直接复制这个用户相关的然后进行更改即可,后面开发项目将非常快。
完整代码的地址:SpringBoot 快速开发框架项目模板;