[TOC]
用户中心项目-readme
这个文档用于梳理该项目的开发历程。
前端初始化
使用Ant Design Pro初始化前端项目
Ant Design Pro = React + Ant Design + Umi,具有强大的构建CRUB页面的能力,这正是我们项目所需要的。
安装Ant Design Pro,初始化前端项目。具体操作参考Ant Design Pro官方文档 => 开始使用
这项目使用的版本是V5
Ant Design Pro的页面
“页面”指的是:配置了路由,可以直接通过url链接访问到的模块。在项目中,页面文件后缀为.tsx
,全部存放在src/pages
目录下
后端初始化
初始化Springboot项目的三种方式:
- github上寻找相应现成的模板项目
- 在网站spring.io上根据需求勾选,会自动生成一套模版项目的压缩包
- 借助IDEA生成(推荐)
使用IDEA初始化项目结构
一般的Springboot项目所需要勾选的基本依赖:
工具类:Lombok、Spring Boot DevTools、Spring Configuration Processor
Web开发:Spring Web
数据库:(常用)MySQL Driver、Mybatis Framework
IDEA没有提供Mybatis-Plus的选项,而当前的项目基本流行使用Mybatis-plus,所以我们自己在pom文件中添加依赖,也很简单,直接搜索Mybatis-Plus的官方文档即可。
实用工具类库:(每个项目都可以引这些,方便写代码)
- apache commons lang
- com.google.code.gson
初始化数据库
IDEA同样集成了连接数据库的功能,自行探索即可~
项目demo试运行
在初始化项目后,我们应该写一个简单的项目demo来测试我们目前工作的正确性。
配置application.yml文件:
1
2
3
4
5
6
7
8
9
10spring:
application:
name: user-center
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/user-center
username: root
password: zmx004200412
server:
port: 8080编写一个用户实体类User
1
2
3
4
5
6
7
public class User {
private Long id;
private String name;
private Integer age;
private String email;
}编写一个User的Mapper类
1
2public interface UserMapper extends BaseMapper<User> {
}创建数据库,在Mybatis-Plus的官方文档中提供了一段创建User表并插入一些数据的SQL语句,直接CV即可。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18DROP TABLE IF EXISTS `user`;
CREATE TABLE `user`
(
id BIGINT NOT NULL COMMENT '主键ID',
name VARCHAR(30) NULL DEFAULT NULL COMMENT '姓名',
age INT NULL DEFAULT NULL COMMENT '年龄',
email VARCHAR(50) NULL DEFAULT NULL COMMENT '邮箱',
PRIMARY KEY (id)
);
DELETE FROM `user`;
INSERT INTO `user` (id, name, age, email) VALUES
(1, 'Jone', 18, 'test1@baomidou.com'),
(2, 'Jack', 20, 'test2@baomidou.com'),
(3, 'Tom', 28, 'test3@baomidou.com'),
(4, 'Sandy', 21, 'test4@baomidou.com'),
(5, 'Billie', 24, 'test5@baomidou.com');在Test类来编写测试方法查询User数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class SampleTest {
//@Resource与@Autowired都是实现自动注入的注解,在这里使用@Autowired也可以。
//二者的区别是:前者按照名称匹配,后者按照类型匹配。因此,使用@Autowired时,要在mapper类中增加@Mapper注解。以让spring知道这是一个可以使用的bean
private UserMapper userMapper;
public void testSelect() {
System.out.println(("----- selectAll method test ------"));
List<User> userList = userMapper.selectList(null);
Assert.isTrue(5 == userList.size(), "");
userList.forEach(System.out::println);
}
}测试通过
1
2
3
4
5
6//输出
User(id=1, name=Jone, age=18, email=test1@baomidou.com)
User(id=2, name=Jack, age=20, email=test2@baomidou.com)
User(id=3, name=Tom, age=28, email=test3@baomidou.com)
User(id=4, name=Sandy, age=21, email=test4@baomidou.com)
User(id=5, name=Billie, age=24, email=test5@baomidou.com)
数据库设计
User表
字段名 | 类型 | 解释 |
---|---|---|
id | bigint | 用户ID |
userAccount | varchar | 用户账号 |
nickname | varchar | 昵称 |
avatar | varchar | 头像url |
userPassword | varchar | 登录密码 |
varchar | 邮箱 | |
phone | varchar | 电话 |
gender | tinyint | 性别(男0) |
status | tinyint | 状态(默认正常0) |
userRole | int | 用户状态(0-普通用户,1-管理员) |
createTime | datetime | 创建时间,默认值设为current_timestamp() |
updateTime | datetime | 更新时间,默认值设为current_timestamp() |
isDelete | tinyint | 是否逻辑删除(默认否0) |
createTime、updateTime、isDelete是每个表都要有的字段,与业务无关。
为updateTime字段添加触发器,修改数据时自动更新:
1 | ALTER TABLE user |
Mybatis-plus
驼峰规则
Mybatis-plus 默认开启自动驼峰命名规则(camel case)映射,即从经典数据库列名 A_COLUMN(下划线命名) 到经典 Java 属性名 aColumn(驼峰命名) 的类似映射。
但我们数据库字段的设计就是选用了驼峰式,所以可以把机制关闭。
如果有需要则可以不设置。
1 | mybatis-plus: |
Mybatis-plus提供的逻辑删除功能:
什么是逻辑删除?对数据进行删除操作时,将删除操作转为更新操作,更新操作为:将数据的逻辑删除字段置为有效,表示该条数据已被删除。
使用方法:
为每个表增加一个字段
isDelete:{0,1}
在
application.yml
配置:1
2
3
4
5
6mybatis-plus:
global-config:
db-config:
logic-delete-field: isDelete # 全局逻辑删除字段名
logic-delete-value: 1 # 逻辑已删除值
logic-not-delete-value: 0 # 逻辑未删除值在表对应的实体类中声明成员变量
isDelete
,加上@TableLogic
注解,表示这是逻辑删除字段
后端开发
MybatisX代码生成器
在编写业务逻辑前,肯定要先写好domain(实体类)、对应的Mapper类(操作数据库的对象)、对应的Mapper.xml(定义了Mapper类与数据库的关联)、对应service类(包括常用的增删改查)以及serviceImpl类。
MybatisX插件可以根据数据库中表的结构,一键生成上述代码,提高工作效率。
使用方法:安装后,在IDEA数据库工具中,找到表并右键即可找到使用
使用MybatisX生成代码后,也应该写一个单元测试来保证工作的正确性。
单测代码:
1 |
|
generatorAllSetter
该插件可以一键生成对一个实体类所有字段的setter或getter的调用的代码,提高工作效率。
功能开发 | 注册逻辑
- 接收参数:账号、密码、校验码
- 校验用户的账号、密码、校验码是否符合格式,账号是否重复
- 对密码加密
- 向数据库中插入数据
MybatisX生成的Impl类是继承了框架自带的ServiceImpl类,所以有很多自带的实用方法:
save():插入数据,参数是表对应的实体类,例如向user表中插入数据就传入一个User对象
count():根据条件查询并返回数据库中符合条件的数据个数,参数是一个QueryWrapper<>对象,用来封装查询条件。
list():根据条件查询数据库中符合条件的数据,返回的是一个List集合
QueryWrapper具体是一个条件构造器。
1. QueryWrapper.eq():按条件精准查询
2. QueryWrapper.like():按条件模糊查询
3. 还有很多功能强大的方法,支持动态构建
使用时的关键代码:以User对象的userAccount为例:
1 | QueryWrapper<User> queryWrapper=new QueryWrapper<>(); |
上述代码的意思是,按照变量userAccount的值,查询数据库User表中字段“userAccount”,并返回与值相同的数据个数。
单元测试 | 注册逻辑
编写注册逻辑的单元测试
功能开发 | 登录功能
- 接收参数:用户账号、用户密码
- 校验用户的账号、密码、校验码是否符合格式,账号是否重复
- 对用户账号与密码在数据库中进行匹配:
- 若成功,则先将用户信息脱敏,然后从request取得用户的登录态(session),然后将用户信息保存在session中,并将session保存在服务器上(Springboot封装的tomcat,这一步应该由Spring自动完成),设置session失效时间。最后返回用户信息(脱敏后的)。
- 若失败,则返回null,表示登录失败。
session与cookie——服务器如何知道是哪个用户登录了?
- 连接服务器时(未进行登录操作),服务器得到一个session(匿名会话),并返回给前端。
- 登录成功后,服务器重新取得session并保存,并在session中添加用户信息(K-V格式),返回给前端一个cookie,并要求前端将cookie保存在浏览器。cookie中含有
JSESSIONID
,对应着唯一的session。 - 前端再次发送请求时(相同的域名),在请求头中带上cookie去请求。
- 服务器通过cookie与session的对应关系,找到session,从而辨认用户。
session与cookie的生效范围
- 后端服务器有各自的域名,例如
baidu.com
,本机则是localhost
。 - cookie的生效范围是与域名绑定的,浏览器不区分端口。
- session不仅区分域名,还区分端口,同一域名下不同端口的session也是独立的,因此返回的cookie值也是不同的。
- 于是可以得出:
- 向指定的域名发送请求(无论端口),浏览器就会带上对应域名的cookie
- 接收到同一域名的不同端口返回的cookie,后接收到的cookie会覆盖掉前接收的。
- 综上所述均是默认情况,实际情况可以根据需求,灵活调整 cookie|session 与 域名|端口 的绑定关系。
功能开发 | 登录注册接口
application.yml 指定接口全局api(相当于在所有接口的@RequestMapping()
前加上了”/api“)
1 | server: |
登录接口逻辑简单。这里只写一些编写规范。
接口编写规范:
@RestController:Spring 框架识别到这是一个接口,那么接口返回的所有数据都将自动转为 JSON 格式。
GET请求的参数放在请求头中,接受参数使用@RequestParam注解;POST请求的参数放在请求体中,接收参数使用@RequestBody注解
@RequestBody:常用于处理POST请求。使用后,Spring 会自动把请求体的 JSON 化数据转换为 Java 对象(请求体的JSON化由前端完成)。我们需要自己封装RequestBody请求体类用来接收数据,且要实现Serialization接口。Spingboot会自动实现JSON数据的key与RequestBody类的成员变量的映射(需要同名)
以用户注册接口为例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21//用于封装用户注册的参数的RequestBody
public class UserLoginRequest implements Serializable {
private static final long serialVersionUID = 5743377063917044862L;
private String userAccount;
private String userPassword;
}
//前端请求体
{
"userAccount": "Michael",
"userPassword": "0123456789"
}
//后端接口
public User doLogin({ UserLoginRequest userLoginRequest, HttpServletRequest httpServletRequest)
String userAccount=userLoginRequest.getUserAccount();
String userPassword=userLoginRequest.getUserPassword();
}@RequestParam:常用于处理GET请求。用来从 URL 或请求体中获取特定的参数并将其绑定到方法参数中,两个参数之间要同名,且参数的类型有限制,一般不能是自定义的类,只能是:基本数据类型、List、Map、Date等。
还提供了两个十分常用的属性:
- dafaultValue:默认值
- required:默认情况下,
@RequestParam
会将请求中缺失的参数视为错误,抛出 400 错误(Bad Request
)。如果某个参数是可选的,可以使用required
属性来控制。
接口尽可能少的涉及业务逻辑,除非业务逻辑真的很简单,否则还是将逻辑写在service中。
Controller会使用到的注解:
- @RestController
- @RequestMapping
- @Resource
- @PostMapping、@GetMapping等
- @RequestBody、@RequestParam
单元测试 | 登录注册接口
IDEA自带HTTP测试工具(无敌了666):工具 => HTTP客户端 => 在HTTP客户端中创建请求 => 添加请求
使用示例:
1 | //post请求 |
功能开发 | 用户管理接口
用户管理逻辑中一定要有鉴权!!,保证只有管理员可以调用这些接口
- 查询用户
- 按昵称查询(模糊查询)
- 删除用户
单元测试 | 用户管理接口
同样使用IDEA的HTTP测试功能。
优化代码
前端开发
Ant Design Pro初始化的项目已经是一套现成的前端系统,有登录页面以及主页,在此基础上进行开发将很方便
前端开发 | 登录页面
修改页面的展示内容
修改用到的参数名、参数类型,与后端接口保持一致
修改登录API的请求路径,调用自己的后端接口:
src/services/ant-design-pro/api.ts
开启代理解决跨域问题:
config/proxy.ts
配置代理:
1
2
3
4
5
6
7
8
9
10dev: {
// localhost:8000/api/** -> https://preview.pro.ant.design/api/**
'/api/': {
// 要代理的地址
target: 'http://localhost:8080',
// 配置了这个可以从 http 代理到 https
// 依赖 origin 的功能可能需要这个,比如 cookie
changeOrigin: true,
},
},这样的配置可以将以“/api”开头的请求(根路径为http://localhost:8000,即前端项目根路径)代理到http://localhost:8080(后端项目根路径)
Umi默认开启token,即Umi会自动在请求头上加入token,这会导致请求路径与后端接口不匹配(后端暂时还未考虑token功能),因此需要关闭:
在
src/requestErrorConfig.ts
将下面这段配置注释掉:1
2
3
4
5
6
7
8// 请求拦截器
requestInterceptors: [
(config: RequestOptions) => {
// 拦截请求配置,进行个性化处理。
const url = config?.url?.concat('?token = 123');
return { ...config, url };
},
],
单元测试 | 登录功能
测试登录功能是否能在前后端之间联通
前端开发 | 注册页面
注册页面与登录页面样式类似,直接先复制
src/pages/User/Login
再做修改:修改页面的展示内容
需要将
button
上的“登录”改成“注册”/Login/index.tsx
页面中找不到设置button
的内容,因为button
是被封装在LoginForm
中的。追溯
LoginForm
的源码:ctrl+B
追溯到LoginForm
源码的ts文件,alt+f1
选择项目视图 ,可以快速定位的到文件的路径:node_modules/@ant-design/pro-form/es/layouts/LoginForm/index.d.ts
在同目录下,找到
LoginForm
源码的js文件js源码中有这样一段代码定义了
button
的内容:1
2
3
4var submitter = proFormProps.submitter === false ? false : _objectSpread(_objectSpread({
searchConfig: {
submitText: intl.getMessage('loginForm.submitText', '登录')
}说明
LoginForm
有一个submitter
属性,submitter
有一个searchConfig
属性,searchConfig
有一个submitText
属性,规定了button
的内容回到
/Login/index.tsx
,设置LoginForm
的属性,成功修改button
的内容1
2
3
4
5
6
7
8
9<LoginForm
//修改按钮的内容“登录”为“注册”
submitter={{
searchConfig: {
submitText: "注册"
}
}}
...
>...</LoginForm>
修改用到的参数名、参数类型,与后端接口保持一致 => (其实这一步挺难的,因为Ant Design Pro的封装程度太高了,需要很仔细的检查)
修改页面逻辑
- 修改注册API的请求路径
- 注册成功后重定向到登录页面
- Ant Design Pro默认:若当前为非登录页面且用户信息为空时,会自动重定向到登录页面。我们需要修改这段逻辑,使得当前页面为注册页面时,前端不会自动重定向。这段逻辑在
src/app.tsx
的getInitialState()
函数中。
配置路由
config/routes.ts
,使得浏览器可以访问到新建的注册页面
app.tsx
是前端项目的全局入口文件,里面定义了许多的全局公共信息,十分重要
getInitialState()
:页面加载时调用,作用:查询当前用户登录态,若无(即session为空),则重定向到登录页面;若有(即session不为空),则查询当前用户信息并返回给前端,赋值给前端的一个全局变量:currentUser
layout()
:控制页面的全局布局
单元测试 | 注册功能
测试注册功能是否能在前后端之间联通
功能开发 | 获取用户登录态
实现获取用户登录态,若用户再次打开网页时(session有效期间),不用重复登录,后端就知道是哪个用户在访问。
编写后端接口
getCurrrentUser():User
:在用户第一次登录时,我们已经将登录成功的用户信息存放在了session中。因此,只需要再次从session中取出用户信息,并根据id从数据库中重新获取(session中存放的是第一次登录时的用户信息,但是在两次登录之间,用户的某些数据可能已经改变)并返回。若session为空,说明没有用户登录,接口返回null,前端逻辑会重定向到登录页面。修改代码,使前后端接口的返回数据类型保持一致
src/services/ant-design-pro/api.ts
这个文件中定义了所有前端对后端接口的调用,查询当前用户的调用为currentUser()
,其返回的是一个{data: API.CurrentUser}
的对象,其有一个成员data
,类型是API.CurrentUser
。而后端返回的是一个User
类型的数据,{data: API.CurrentUser}
与User
显然不匹配,因此我们要进行修改:{data: API.CurrentUser}
修改为API.CurrentUser
,即直接将User
赋给API.CurrentUser
,确认前端API.CurrentUser
与后端User
的对应关系修改
API.CurrentUser
的结构,与User
保持一致,Ant Design Pro会自动建立同名字段的联系修改用到
{data: API.CurrentUser}
和API.CurrentUser
的其他处代码
开发进行到这里时,我们注册并登录账号后,就可以进入到系统主页。
完整登录注册逻辑:
- 注册
- 第一次登录,后端校验账号密码正确,将User存放在session中,返回校验成功的信息给前端,以及cookie。
- 前端接收返回后重新加载页面,调用
getInitialState()
,getInitialState()
调用queryUserInfo()
向后端请求获取当前用户信息User并附带cookie - 后端由cookie找到对应的session,并取出用户信息。
- 后端根据用户身份信息,重新从数据库中获取一次用户最新信息User,返回User给前端
getInitialState()
根据User信息,重定向到用户主页,第一次登录成功。- session有效期间,不用重复登录即可访问主页
功能开发 | 用户管理
Ant Design Pro也为前端页面访问提供了简单的权限管理手段,通过路由routes.ts
为页面配置access
属性即可。
校验权限的逻辑在src/access.ts
。
用户管理页面开发
**一个 CRUD 页面 = ProLayout+ ProTable + Form **
ProLayout - 高级布局
ProLayout 可以提供一个标准又不失灵活的中后台标准布局,同时提供一键切换布局形态、自动生成菜单等功能。与 PageContainer 配合使用可以自动生成面包屑、页面标题。
在Ant Design Pro中,ProLayout配置在config.ts
中,默认全局开启,即每个页面都在应用。
页面可以在routes.ts
中通过layout
属性来单独配置是否开启ProLayout,例如:登录注册页面都不需要,就可以设置layout: false
一键切换布局形态:通过页面右侧抽屉UI界面一键切换。
自动生成菜单:所有页面,每个页面都作为一个菜单项,根据在路由文件
routes.ts
中路由的层级关系来生成菜单并显示在页面侧边。但是也可以通过在
routes.ts
中配置属性来个性化菜单显示,具体可参考官方文档。面包屑:显示当前页面在系统层级结构中的位置,并能向上返回。需要用
<PageContainer></PageContainer>
包裹住页面的HTML部分,面包屑才会生效。
ProTable - 高级表单
ProComponents提供了一系列封装好的前端组件,只需要在官网搜索样式复制代码再修改即可。这里用到的是ProTable组件
复制ProTable的代码作为UserManage的页面:
src/pages/Admin/UserManage/index.tsx
。配合ProLayout,就能得到一个CRUB页面为页面配置路由
编写后端接口
修改页面代码,设计符合需求的表单
需求:表格显示所有用户的信息,且能根据条件搜索特定用户
通过columns定义表格有哪些列,列的属性:
dataIndex:列的类型,与User的成员字段对应
title:显示的名称
render:如果是图片,例如头像,则需要渲染
1
2
3
4
5render: (_, record) => (
<div>
<Image src={record.avatar} width={50}/>
</div>
),valueType:用来指定列的数据类型。
ProTable
支持多种valueType
,如text
、date
、money
等,可以通过此属性来启用格式化和特定行为。valueEnum:为数据的值与含义建立对应关系,例如数据中userRole的枚举值为{0,1},分别代表普通用户与管理员。
框架会根据stauts的值为数据添加颜色,default为灰色,success为绿色,error为红色
1
2
3
4
5
6
7
8
9
10valueEnum: {
0: {
text: '普通用户',
status: 'default',
},
1: {
text: '管理员',
status: 'success',
},
},。。。(其他)
定义查询用户信息的接口API。
1
2
3
4
5
6
7
8/** 查询用户 GET /api/user/selectUser */
export async function selectUserByNickname(options?: { [key: string]: any }) {
return request<API.CurrentUser[]>('/api/user/selectUser', {
method: 'GET',
//...(options || {}),
params: options,
});
}options?: { [key: string]: any }
:这是函数的一个参数,叫做options
。options?
的意思是 该参数是可选的,即调用该函数时可以选择传入该参数,也可以不传入。{ [key: string]: any }
是一个 索引签名,意味着options
可以是一个 对象,对象的键(key
)是字符串类型,值(value
)可以是任意类型。这使得options
可以接收任何属性和对应的值。例如,options
可以是{ nickname: 'JohnDoe', age: 30 }
。params: options
表示将options
作为params
显式传递给request
函数,request
会自动将它转化为 URL 查询字符串,添加到请求 URL 后面...(options || {})
这是原本框架的代码,使用了展开运算符,将options
对象的属性直接添加到请求配置对象中,但options
只是直接展开到配置对象,而没有明确指定为params
,当请求是 GET 时,后端可能会接受不到参数,所以被注释掉,并改为使用显式传递。
调用定义好的接口
下面这段代码在
<ProTable>...</ProTable>
源码中,其功能就是发送查询用户信息的请求。1
2
3
4
5
6
7
8
9request={async (params, sort, filter) => {
console.log(sort, filter);
await waitTime(2000);
return request<{
data: GithubIssueItem[];
}>('https://proapi.azurewebsites.net/github/issues', {
params,
});
}}现在结合上面定义好的接口,对其修改
1
2
3
4
5
6
7
8request={async (params, sort, filter) => {
console.log(sort, filter);
// await waitTime(2000);
const list = await selectUserByNickname(params);
return {
data: list
}
}}params
是从搜索表单收集到的查询条件,它是一个键值对对象,可以直接传入接口中
最终效果
在调出管理页时,Ant Design Pro会自动发送一次查询请求,表格中就能显示所有用户信息。
也可以根据上方的搜索栏,按条件查询用户
功能开发 | 用户注销
EZ
- 前端逻辑在
src/components/RightContent/AvatarDropdown.tsx
中 - 编写后端接口,只要将session的用户信息清空即可。
后端优化
所有的优化都要明确目的。
数据返回、异常处理
定义通用返回类
BaseResponse<T>
目的:给返回对象添加信息,告诉前端这个请求在业务层面上是成功还是失败
1
2
3
4
5
6
7
8
9
10
11
public class BaseResponse<T> implements Serializable {
private int code;
private T data;
private String message;
private String description;
//构造函数
...
}code
:错误码data
:数据,是泛型,增强通用性message
:解释description
:更细节的解释
定义异常错误码
ErrorCode
1
2
3
4
5
6
7
8
9
10
11
12
13public enum ErrorCode {
PARAM_ERROR(4000,"请求参数不合法",""),
PARAM_NULL(4001,"请求参数为空",""),
ACCESS_ERROR(3000,"权限不足",""),
NOT_LOGIN(3001,"未登录",""),
UNKNOWN_ERROR(6000,"未知异常",""),
SYSTEM_ERROR(5000,"系统内部错误",""),
DATABASE_ERROR(3002,"数据库操作失败","");
private final int code;
private final String message;
private final String description;
}BaseResponse<T>
与Errorcode
配合使用为
BaseResponse
添加构造函数constructor
,传入ErrorCode
作为param
1
2
3
4
5public BaseResponse(ErrorCode errorCode){
this.code=errorCode.getCode();
this.message=errorCode.getMessage();
this.description=errorCode.getDescription();
}定义一个工具类
ResultUtil
用来创建BaseResponse
- 目的:代码更简洁、容易阅读
1
2
3
4
5
6
7
8
9
10
11
12public class ResultUtil {
public static <T> BaseResponse<T> success(T data ){
return new BaseResponse<>(2000,data,"ok");
}
public static <T> BaseResponse<T> error(ErrorCode errorCode){
return new BaseResponse<>(errorCode);
}
...
}封装全局异常处理:
定义业务异常类
BusinessException
- 目的:相比与Java自带的异常类,
description
能够记录更多的信息。 - 在抛出异常时,用
description
记录详细的报错信息
1
2
3
4
5
6public class BusinessException extends RuntimeException{
private final int code;
private final String description;
}- 父类
RuntimeException
中有成员变量message
,所以不用再定义
- 目的:相比与Java自带的异常类,
编写全局异常处理器
GlobalExceptionHandler
- 捕获所有的异常,无论是业务相关的异常还是系统内部的异常,无论是在
controller
还是在service
中, - 目的是:
- 集中处理业务相关的异常,让前端知道更详细的报错信息
- 对前端屏蔽系统内部的异常,不暴露服务器内部信息
- 集中记录日志
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class GlobalExceptionHandler {
public BaseResponse businessExceptionHandler(BusinessException e){
log.error("BusinessException: "+e.getMessage()+":"+e.getDescription());
return ResultUtil.error(e.getCode(),e.getMessage(),e.getDescription());
}
public BaseResponse runtimeExceptionHandler(RuntimeException e){
log.error("RuntimeException: "+e.getMessage());
return ResultUtil.error(ErrorCode.SYSTEM_ERROR);
}
}- 捕获所有的异常,无论是业务相关的异常还是系统内部的异常,无论是在
@ExceptionHandler(BusinessException.class)
表示只捕获BusinessException
类型的异常
完成后,例如:当出现有封禁用户试图登录时,后端控制台就会打印异常日志:
2025-01-27 22:22:34.967 ERROR 22600 --- [nio-8080-exec-7] c.m.u.exception.GlobalExceptionHandler : BusinessException: 请求参数不合法:账号已被封禁
修改前端相应代码
完整逻辑:
- 出现错误,有不同的错误类型
ErrorCode
,再根据具体情况记录详细信息description
,构建并抛出BusinessException
Handler
捕获异常,集中处理,记录日志- 在
Handler
中使用ResultUtil
创建BaseResponse
,返回给前端
功能完善
- 用户信息删除
- 用户信息修改
- 支持更多查询条件
项目部署
多环境理论
什么是多环境?
- 指同一套代码在不同的阶段根据实际需要调整配置并部署到不同的机器上
为什么需要多环境?
- 每个环境互不影响
- 区分不同的阶段:开发、测试、生产
- 对项目进行优化
- 本地日志级别
- 精简依赖,减小项目体积
- 项目的环境/参数可以调整,比如JVM参数
- 针对不同的环境做不同的事情
多环境分类:
- 本地环境
- 开发环境(远程开发),多人同时连一台机器,同时开发
- 测试环境,独立的数据库、独立的服务器
- 预发布环境:(体验服)与正式服一致,数据库一致
- 正式环境(公开对外的环境)
前端多环境实战
Ant Design Pro官方文档中,详细了介绍多环境方法
在开发中经常会有一些需求,根据应用运行的不同环境进行不同的逻辑处理。
比如,dev
环境使用 dev
的对应的 Url,而线上则使用 prod
对应的 Url。 或者,在某些特定的环境需要打开只有在该环境下才会生效的功能。
获取当前运行环境名称
在 Pro 的脚手架中有这样的一个环境变量 REACT_APP_ENV
,该变量代表当前应用所处环境的具体名称。如 dev、test、pre、prod 等。
如若需要在 config
外的非 node 环境文件中使用该环境变量,则需要在 config
导出默认 defineConfig()
时配置 define{}
。
示例代码如下:
1 | // config/config.ts |
使用该变量示例代码如下:
1 | // src/components/RightContent/index.tsx |
多环境多份配置文件
Pro 脚手架默认使用 Umi 作为底层框架,在 Umi 内可通过指定 UMI_ENV
环境变量来区分不同环境的配置文件,UMI_ENV
需要在 package.json
内配置。
示例配置如下:
1 | { |
当 UMI_ENV
为 test
时,则必须在 config 目录下配置 config.test.ts
文件来管理 test
环境下的不同变量,Umi 框架会在 deep merge 后形成最终配置。
示例代码如下:
1 | // config/config.test.ts test环境对应的配置文件 |
变量使用示例:
1 | // src/services/user.ts |
配置文件夹 config 下的结构:
1 | ant-design-pro |
Ant Design Pro使用 REACT_APP_ENV
变量来代表当前所处的环境:
npm run start
时,REACT_APP_ENV == dev
- 本地启动、监听端口、自动更新
npm run build
时,REACT_APP_ENV == prod
- 项目打包构建
- Terminal使用
serve
工具启动(npm i -g serve) - 不会自动更新,因为代码不会更改了
后端多环境
服务器选择:CentOS 7.6 以上
不同的环境加载不同的配置文件:
application.yml
:通用配置,所有环境启动都会加载application.yml
:只有开发环境启动才会加载application-prod.yml
:只有生产环境启动才会加载
项目打包成 jar 包 :
- IDEA => maven => Lifecycle => package
Terminal运行jar包:
java -jar {JAR_PATH}
可以后接不同的参数,例如:
--spring.profiles.active=prod
--server.port=8081
部署方式 | Nginx + Springboot
总结
体会:新学一个框架,多看官方文档
知识点:
学习了Ant Design Pro前端项目的代码结构,理解了一些重要文件在整个项目的作用
config.ts
:全局配置文件routes.ts
:路由配置proxy.ts
:代理配置package.json
:项目启动命令app.ts
:项目入口文件api.ts
:请求方法APIaccess.ts
:前端权限校验typings.d.ts
:自定义数据类型index.tsx
:页面文件
MyBatis X:代码生成
数据库表必备三字段:createTime、updateTime、isDelete
Controller层的几个注解:
- @RestController:Spring 框架识别到这是一个接口,那么接口返回的所有数据都将自动转为 JSON 格式。
- @RequestBody:常用于处理POST请求。使用后,Spring 会自动把请求体的 JSON 化数据转换为 Java 对象(请求体的JSON化由前端完成)。我们需要自己封装RequestBody请求体类用来接收数据,且要实现Serialization接口。Spingboot会自动实现JSON数据的key与RequestBody类的成员变量的映射(需要同名)
- @RequestParam:常用于处理GET请求。用来从 URL 或请求体中获取特定的参数并将其绑定到方法参数中,两个参数之间要同名,且参数的类型有限制,一般不能是自定义的类,只能是:基本数据类型、List、Map、Date等。
使用自定义请求实体类用来接收请求参数
如何维护用户登录态——session与cookie
session与cookie的生效范围
熟悉了ProTable组件
前端虽然不懂具体的语法,但是明白 ”可以在哪些地方完成哪些功能“ ,具体的代码可以百度或GPT
熟悉了后端 通用返回 以及 异常处理 的一整套模式
待拓展点:
- 单机登录改为分布式登录(已在伙伴匹配项目实现)
- 后端添加全局请求拦截器(统一判断用户权限,统一记录请求日志)
- 用户管理的查询用户业务,改为分页查询实现(已在伙伴匹配项目实现),避免数据量过多。