SpringBoot和Axios数据的传递和接收-Restful完全版

SpringBoot和Axios数据的传递和接收-Restful完全版

一、基础知识铺垫

Axios使用

使用axios发送请求,一般有三个最常用的属性。

属性 含义
url 请求的端点 URL
method HTTP 请求方法(如 get, post, put, delete, patch 等)。
params / data 如果 methodgetdelete,使用 params 来传递 URL 查询参数。如果是 post, put, patch,则使用 data 传递请求体数据。通常是一个对象 {}。

HTTP请求方式

Restful风格定义了多种请求方式。

方式 简介 常用场景
GET 请求指定的资源。通常用来获取或查询资源。 读取或查询资源,如获取用户列表或特定用户的详细信息。
POST 向指定资源提交数据,请求服务器进行处理(如创建或修改)。数据包含在请求体中。 创建新资源(如新用户、新帖子),或提交用户数据表单。
PUT 用请求体中的数据替换目标资源的所有当前表示。 更新现有资源的全部内容,如编辑用户的完整个人信息。
PATCH 对资源应用部分修改。 更新资源的一部分,如修改用户的邮箱地址或密码。
DELETE 删除指定的资源。 删除资源,如删除用户账户或帖子。

数据传输方式

方式 介绍
URL路径参数(Path Variables 通过 URL 的路径部分传递数据。在 Spring Boot 中使用 @PathVariable 注解获取。适用于 RESTful 风格的 API,例如获取特定资源的详情。
查询参数(Query Parameters 通过 URL 的查询字符串(?key=value 形式)传递数据。在 Spring Boot 中使用 @RequestParam 注解获取。适用于 GETDELETE 请求。
请求体(Request Body 通过 HTTP 请求的 body 部分传递数据。在 Spring Boot 中使用 @RequestBody 注解获取。适用于 POST , PUTPATCH请求,发送复杂的数据结构。

SpringBoot获取数据的方式

需要提及的是,单单从”获取数据”的角度,我们可以把DeleteGet归为一类,把PutPatchPost归为一类。

  • 前者在axios中使用params传递参数,属于Query Parameters
  • 后者在axios中使用data传递参数,属于Request Body
  • 无论是哪一种,都可以有Path Variables

在 Spring Boot(及一般的 HTTP 服务开发)中,将请求分为”GET 体系”和”POST 体系”可能会导致一些混淆,因为每种 HTTP 方法(GET、POST、PUT、PATCH、DELETE 等)都设计有其独特的用途和语义。不过,如果我们从”如何获取请求中的数据”这个角度来看,可以有一种比较宽泛的分类方式,尤其是关注于数据是通过 URL 还是请求体传递。

体系 获取数据的常用注解
Path Variables @PathVariable
Get、Delete类 @RequestParam@ModelAttribute
Post、Put、Patch类 @RequestBody

二、基础传递代码示例

除了特殊的数据类型,普通的数据传递,默认以axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8';为策略。

(一)Path Variables

Path Variables数据在url上,无关乎get还是post

language-javascript
1
2
3
4
5
6
7
8
9
10
return request({

url: '/test/users/123',
method: 'get'
});
return request({

url: '/test/users/345/info',
method: 'post'
});
language-java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@RestController
@RequestMapping("/test")
public class CourseTestController {


@GetMapping("/users/{userId}")
public String getUser(@PathVariable String userId) {

return "Received GET request for User ID: " + userId;
}

@PostMapping("/users/{userId}/info")
public String updateUser(@PathVariable String userId) {

return "Received POST request for User ID: " + userId;
}

}

(二)Get、Delete

@RequestParam

@RequestParam 主要用于将单个请求参数绑定到方法的参数上,通过指定 valuename 属性,你可以明确告诉 Spring Boot 请求参数的实际名称。

language-javascript
1
2
3
4
5
6
7
8
9
10
return request({

url: '/users',
method: 'get',
params:{

type:"1",
status:"2",
}
});
language-java
1
2
3
4
5
@GetMapping("/users")
public String getUser(@RequestParam String type, @RequestParam(name = "status") String userStatus) {

return "Received GET request for" + type + " " + userStatus;
}

@ModelAttribute

利用 @ModelAttribute 注解。这个注解会告诉 Spring Boot,应该将请求中的查询参数自动绑定到方法参数对象的属性上。

language-javascript
1
2
3
4
5
6
7
8
9
10
return request({

url: '/users',
method: 'get',
params:{

type:"1",
status:"2",
}
});
language-java
1
2
3
4
5
6
7
8
9
10
11
@GetMapping("/users")
public String getUser(@ModelAttribute Query query) {

return "Received GET request for" + query.toString();
}
@Data
class Query{

String type;
String status;
}

通常情况下,我们会将所有的查询参数封装到一个对象中,而不是分开为两个对象,除非这两个对象在逻辑上代表着完全不同的东西,且您希望显式地区分它们。如果您确实有特定的理由需要这样做,也是可行的。比如第二个Query2表示分页查询时的分页参数

language-javascript
1
2
3
4
5
6
7
8
9
10
return request({

url: '/users',
method: 'delete',
params:{

type:"1",
status:"2",
}
});
language-java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
	@DeleteMapping("/users")
public String deleteUser(@ModelAttribute Query1 query1, @ModelAttribute Query2 query2) {

return "Received GET request for" + query1.toString() + query2.toString();
}
@Data
class Query1{

String type;
}
@Data
class Query2{

String userStatus;
// 如果您希望整个对象通过 @ModelAttribute 来绑定,同时又有个别属性名不匹配
// 您可以在后端对象中添加 setter 方法,并在其中处理名称不匹配的问题
// 注意:Lombok @Data 注解会生成默认的 setter 方法,
// 所以如果使用 Lombok,您需要手动添加一个额外的 setter 方法来处理不匹配的情况
public void setStatus(String status) {

this.userStatus = status;
}
}

(三)Post、Put、Patch

@RequestBody

language-javascript
1
2
3
4
5
6
7
8
9
10
11
12
return request({

url: '/users',
method: 'post',
data:{

userId: 123,
userName: "John Doe",
userAge: 30,
userSex: "Male"
}
});
language-java
1
2
3
4
5
6
7
8
9
10
11
12
13
   @PostMapping("/users")
public String getUser(@RequestBody UserVo userVo) {

return userVo.toString();
}
@Data
class UserVo{

Long userId;
String userName;
Long userAge;
String userSex;
}

Spring Boot 后端的 UserVo 类中的属性名和前端传递的 JSON 对象的键名不一致时,可以使用@JsonProperty

language-javascript
1
2
3
4
5
6
7
8
9
10
11
12
return request({

url: '/users',
method: 'put',
data:{

userId: 123,
userName: "John Doe",
userAge: 32,
userSex: "Male"
}
});
language-java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
   @PutMapping("/users")
public String getUser(@RequestBody UserVo userVo) {

return userVo.toString();
}
@Data
class UserVo{

Long userId;
@JsonProperty("userName")
String name;
Long userAge;
String userSex;
}
language-javascript
1
2
3
4
5
6
7
8
9
10
return request({

url: '/users',
method: 'patch',
data:{

userId: 123,
userAge: 34,
}
});
language-java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
   @PatchMapping("/users")
public String getUser(@RequestBody UserVo userVo) {

return userVo.toString();
}
@Data
class UserVo{

Long userId;
@JsonProperty("userName")
String name;
Long userAge;
String userSex;
}

如果你不想额外写一个类作为@RequestBody的参数,你可以选择使用Map或者JsonNode

language-java
1
2
3
4
5
6
7
@PutMapping("/users")
public R<Boolean> getUser(@RequestBody Map<String, Object> body) {

Long id = Long.valueOf(body.get("userId").toString());
String mind = (String) body.get("name");
// 业务逻辑
}
language-java
1
2
3
4
5
6
7
@PutMapping("/users")
public R<Boolean> getUser(@RequestBody JsonNode body) {

Long id = body.get("userId").asLong();
String mind = body.get("name").asText();
// 业务逻辑
}

三、稍微复杂一点的传递

(一)数组

如果用的是Post,那么一般一切安好。

language-javascript
1
2
3
4
5
6
7
8
9
10
return request({

url: '/users',
method: 'post',
data:{

userId: 123,
userOrder: ['1223', '3445', '556'],
}
});
language-java
1
2
3
4
5
6
7
8
9
10
11
   @PostMapping("/users")
public String getUser(@RequestBody UserVo userVo) {

return userVo.toString();
}
@Data
class UserVo{

Long userId;
List<String> userOrder;
}

如果用的是Get,就需要注意默认情况下,Spring Boot 期望列表或数组类型的查询参数以特定的格式传递,例如:userOrder=1223&userOrder=3445&userOrder=556。但在你的例子中,由于是通过 axios 发送请求,并且当你在请求的 params 中包含一个数组时,axios 会将数组转换为 userOrder[0]=1223&userOrder[1]=3445&userOrder[2]=556 的格式,这与 Spring Boot 的默认期望不匹配

language-javascript
1
2
3
4
5
6
7
8
9
10
return request({

url: '/users',
method: 'get',
params:{

userId: 123,
userOrder: ['1223', '3445', '556'].join(','),
}
});
language-java
1
2
3
4
5
@GetMapping("/users")
public String getUser(@RequestParam String userId, @RequestParam List<String> userOrder) {

return userId + "\n" + userOrder.toString();
}

对于数组元素是简单类型,直接用字符’,’拼接即可,变成userOrder=1223,3445,556,Springboot也能匹配。或者可以去参考qs.stringify也就是qs库的用法。

如果数组元素比较复杂呢?如果你仍然坚持使用get,建议去阅读qs使用。一般的做法是用post,可以省去很多麻烦。

language-javascript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
return request({

url: '/users',
method: 'post',
data:{

userId: 123,
userOrder: [
{
id: '1223', name: 'Order1' },
{
id: '3445', name: 'Order2' },
{
id: '556', name: 'Order3' }
]
}
});
language-java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
   @PostMapping("/users")
public String getUser(@RequestBody UserVo userVo) {

return userVo.toString();
}

@Data
class UserVo{

Long userId;
List<Item> userOrder;
}
@Data
class Item{

String id;
String name;
}

(二)GET/POST复合型

对于复合型请求,即在URL中通过查询参数(例如分页信息)传递部分数据,同时在请求体中通过JSON传递更复杂的数据(例如筛选条件),Spring Boot可以通过同时使用@RequestParam@RequestBody注解来接收这两种类型的数据。

language-javascript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const pageParams = {

page: 1,
size: 10
};
return request({

url: `/users?page=${
pageParams.page}&size=${
pageParams.size}`,
method: 'post',
data:{

userId: 123,
userName: "Jack"
}
});
language-java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
   @PostMapping("/users")
public String getUser(
@RequestBody UserVo userVo,
@RequestParam("page") int page,
@RequestParam("size") int size
) {

return userVo.toString();
}
@Data
class UserVo{

Long userId;
String userName;
}

四、特殊数据

(一)文件

上传文件时,使用的 Content-Type 通常是 multipart/form-data。这种类型允许将请求体中的数据作为一系列部分(parts)发送,每个部分可以包含文件内容或其他数据。这种方式非常适合文件上传,因为它支持在单个请求中发送文件数据及其他表单字段

language-javascript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const formData = new FormData();
formData.append('file', file);
formData.append('filename', file.name);
formData.append('chunkIndex', i.toString());
formData.append('totalChunks', totalChunks.toString());
formData.append('md5', md5);
const rsp = await addChunk(formData);

export const addChunk = (data: any) => {

return request({

url: '/video/chunk',
method: 'post',
data: data
});
};
language-java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@PostMapping(value = "/video/chunk")
public R<String> handleChunkUpload(
@RequestParam("file") MultipartFile file,
@RequestParam("md5") String md5,
@RequestParam("filename") String filename,
@RequestParam("chunkIndex") int chunkIndex,
@RequestParam("totalChunks") int totalChunks) {

if (ObjectUtil.isNull(file)) {

return R.fail("上传文件不能为空");
}
Boolean b = mediaFilesService.handleChunkUpload(file, md5);
if (b){

return R.ok();
}else {

return R.fail();
}
}

Vue 3 的框架 Element Plus中,el-upload 组件用于文件上传,它底层使用的也是 multipart/form-data 这种 Content-Type 来上传文件。这是因为 multipart/form-data 允许在一个请求中发送多部分数据,包括文本字段和文件,非常适合文件上传的场景

language-html
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
<template>
<el-upload
action="http://example.com/upload"
:data="extraData"
:on-success="handleSuccess"
:on-error="handleError"
>
<el-button size="small" type="primary">点击上传</el-button>
</el-upload>
</template>

<script setup>
import {
ElMessage } from 'element-plus';
import {
ref } from 'vue';

// 额外数据
const extraData = ref({

userId: "123",
description: "这是一个文件描述"
});

const handleSuccess = (response, file, fileList) => {

// 文件上传成功的回调
ElMessage.success('文件上传成功');
};

const handleError = (err, file, fileList) => {

// 文件上传失败的回调
ElMessage.error('文件上传失败');
};
</script>

language-java
1
2
3
4
5
6
7
8
9
10
11
12
@RestController
public class FileUploadController {

@PostMapping("/upload")
public String handleFileUpload(
@RequestParam("file") MultipartFile file,
@RequestParam("userId") String userId,
@RequestParam("description") String description) {

return "文件上传成功,用户ID: " + userId + ",描述: " + description;
}
}

(二)Cookies

s
aultValue`指定的默认值 “”会被使用。

SpringBoot后端Long数据传到前端js精度损失问题

SpringBoot后端Long数据传到前端js精度损失问题

方案一、修改后端

在对应的字段上添加注解,将Long转为String后传输。

language-java
1
2
@JsonFormat(shape = JsonFormat.Shape.STRING)
private Long payNo;

方案二、修改前端

js对应的结果接收上使用BigInt

language-js
1
2
3
4
5
6
xxx().then((res) => {

if(res){

this.payNo = String(BigInt(res.payNo))
}
Docker部署xxl-job调度器并结合SpringBoot测试

Docker部署xxl-job调度器并结合SpringBoot测试

一、Docker部署

1. 创建数据库

去Github下载最新发布的源码,https://github.com/xuxueli/xxl-job/releases,找到/xxl-job/doc/db/tables_xxl_job.sql文件,对数据库进行执行即可,脚本里面包含数据库的创建。

2. 启动容器

参考官方中文文档,写出如下docker-compose示例。使用-e PARAMS: ""来指定一些变量,包括数据库信息,一般需要根据自身情况修改。

language-yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
version: "3.8"
networks:
docker_xuecheng:
ipam:
config:
- subnet: 172.20.0.0/16

services:
xxl-job:
container_name: xxl-job
image: xuxueli/xxl-job-admin:2.4.0
volumes:
- ./xxl_job/logs:/data/applogs
ports:
- "8088:8080"
environment:
PARAMS: '
--spring.datasource.url=jdbc:mysql://172.20.0.2:3306/xxl_job?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai
--spring.datasource.username=root
--spring.datasource.password=1009'
networks:
docker_xuecheng:
ipv4_address: 172.20.3.1

3. 访问

访问http://192.168.101.65:8088/xxl-job-admin/即可。

4. 新建执行器

新增一个简单的testHandler执行器。

二、SpringBoot整合

1. 模块注册到执行器

在对应模块引入依赖

language-xml
1
2
3
4
<dependency>
<groupId>com.xuxueli</groupId>
<artifactId>xxl-job-core</artifactId>
</dependency>

并指定执行器的appname

language-yml
1
2
3
4
5
6
7
8
9
10
11
12
xxl:
job:
admin:
addresses: http://192.168.101.65:8088/xxl-job-admin
executor:
appname: testHandler
address:
ip:
port: 9999
logpath: /data/applogs/xxl-job/jobhandler
logretentiondays: 30
accessToken: default_token

2. 创建配置类

在源码中找到src/main/java/com/xxl/job/executor/core/config/XxlJobConfig.java,复制到模块代码中。如下

language-java
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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
package com.xuecheng.media.config;

import com.xxl.job.core.executor.impl.XxlJobSpringExecutor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
* xxl-job config
*
* @author xuxueli 2017-04-28
*/
@Configuration
public class XxlJobConfig {

private Logger logger = LoggerFactory.getLogger(XxlJobConfig.class);

@Value("${xxl.job.admin.addresses}")
private String adminAddresses;

@Value("${xxl.job.accessToken}")
private String accessToken;

@Value("${xxl.job.executor.appname}")
private String appname;

@Value("${xxl.job.executor.address}")
private String address;

@Value("${xxl.job.executor.ip}")
private String ip;

@Value("${xxl.job.executor.port}")
private int port;

@Value("${xxl.job.executor.logpath}")
private String logPath;

@Value("${xxl.job.executor.logretentiondays}")
private int logRetentionDays;


@Bean
public XxlJobSpringExecutor xxlJobExecutor() {

logger.info(">>>>>>>>>>> xxl-job config init.");
XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
xxlJobSpringExecutor.setAdminAddresses(adminAddresses);
xxlJobSpringExecutor.setAppname(appname);
xxlJobSpringExecutor.setAddress(address);
xxlJobSpringExecutor.setIp(ip);
xxlJobSpringExecutor.setPort(port);
xxlJobSpringExecutor.setAccessToken(accessToken);
xxlJobSpringExecutor.setLogPath(logPath);
xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays);

return xxlJobSpringExecutor;
}

/**
* 针对多网卡、容器内部署等情况,可借助 "spring-cloud-commons" 提供的 "InetUtils" 组件灵活定制注册IP;
*
* 1、引入依赖:
* <dependency>
* <groupId>org.springframework.cloud</groupId>
* <artifactId>spring-cloud-commons</artifactId>
* <version>${version}</version>
* </dependency>
*
* 2、配置文件,或者容器启动变量
* spring.cloud.inetutils.preferred-networks: 'xxx.xxx.xxx.'
*
* 3、获取IP
* String ip_ = inetUtils.findFirstNonLoopbackHostInfo().getIpAddress();
*/


}

3. 启动测试

重启模块,访问XXL-JOB网页端,查看情况。如果执行器的OnLine 机器地址有一个信息,表示模块绑定成功。

三、任务发布-普通任务

1. 编写任务代码

源代码中有任务代码示例,路径为src/main/java/com/xxl/job/executor/service/jobhandler/SampleXxlJob.java,仿照写一个简单的任务,如下。

language-java
1
2
3
4
5
6
7
8
9
10
11
12
@Component
public class SampleXxlJob {

/**
* 1、简单任务示例(Bean模式)
*/
@XxlJob("demoJobHandler")
public void demoJobHandler() throws Exception {

System.out.println("处理视频");
}
}

2. 创建任务

选择执行器,并指定JobHandler

3. 启动任务

启动刚才创建的任务

对应模块的日志可以看到每10秒打印一次输出。

XXL-JOB网页管理也可以看到相关任务执行记录。

四、任务发布-分片任务

1. 编写任务代码

language-java
1
2
3
4
5
6
7
8
9
10
@XxlJob("shardingJobHandler")
public void shardingJobHandler() throws Exception {

// 分片参数
int shardIndex = XxlJobHelper.getShardIndex();
int shardTotal = XxlJobHelper.getShardTotal();

System.out.println("分片参数:当前分片序号 = " + shardIndex + ", 总分片数 = " + shardTotal);

}

2. 启动多个实例

添加虚拟机参数-Dserver.port=63051 -Dxxl.job.executor.port=9998,前者区分程序端口,后者区分执行器端口。

3. 创建任务

创建任务之前,检查一下两个模块是否注册到指定执行器。

随后创建任务,指定执行器JobHandler,同时路由策略选择分片广播

4. 启动任务

启动任务后,观察两个模块的日志。

同时任务记录也在XXL-JOB管理网页中可以查询到。

五、动态扩容

当运行分片任务时,又添加一个新的模块示例,此时分片任务会自动扩容再分配。如图,我们再复制一个运行配置。

然后将其运行,等待一会,执行器可以看到有3个绑定的机器。

新增的运行实例日志如下,

同时,先前两个运行实例的日志发送了变化,如下

参考资料

SpringBoot使用当前类代理类(内部事务)解决方案

SpringBoot使用当前类代理类(内部事务)解决方案

Spring Boot 开发中,我们时常遇到需要在一个类的内部调用自己的其他方法,并且这些方法可能需要事务支持。这种场景通常发生在业务逻辑较为复杂的服务类中,其中一些操作需要确保数据的一致性和完整性。本文将以 MediaFileServiceImpl 类为例,探讨如何在 Spring Boot 中有效地使用当前类的代理类来处理内部事务。

一、场景描述

考虑一个典型的例子:在 MediaFileServiceImpl 服务类中,upload 方法需要调用 upload2Mysql 方法。这里,upload2Mysql 方法是事务性的,意味着它涉及到数据库操作,这些操作需要在一个事务中被处理。如果直接在 upload 方法中调用 upload2Mysql,由于 Spring 的代理方式,事务管理可能不会被正确应用,因为实际上是在同一个实例内部进行方法调用,绕过了 Spring 的代理。

language-java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Service
@Slf4j
public class MediaFileServiceImpl implements MediaFileService {


@Lazy
@Autowired
private MediaFileService mediaFileService;

@Override
public UploadFileResultDto upload() {

// ... 一些业务逻辑 ...
mediaFileService.upload2Mysql();
// ... 其他业务逻辑 ...
}

@Transactional
@Override
public MediaFiles upload2Mysql() {

// ... 事务性操作 ...
}
}

二、解决方案

1. 使用 @Lazy(推荐)

MediaFileServiceImpl 类中使用 @Lazy 注解解决循环依赖问题。

language-java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Service
@Slf4j
public class MediaFileServiceImpl implements MediaFileService {


@Lazy
@Autowired
private MediaFileService mediaFileService;

@Override
public UploadFileResultDto upload() {

// ... 一些业务逻辑 ...
mediaFileService.upload2Mysql();
// ... 其他业务逻辑 ...
}

@Transactional
@Override
public MediaFiles upload2Mysql() {

// ... 事务性操作 ...
}
}

2. 使用方法注入

方法注入是一种允许在运行时动态注入方法实现的技术。这里,我们通过一个简单的例子来说明如何应用方法注入。

language-java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Service
@Slf4j
public abstract class MediaFileServiceImpl implements MediaFileService {


@Override
public UploadFileResultDto upload() {

// ... 一些业务逻辑 ...
getMediaFileService().upload2Mysql();
// ... 其他业务逻辑 ...
}

@Transactional
@Override
public MediaFiles upload2Mysql() {

// ... 事务性操作 ...
}

@Lookup
protected abstract MediaFileService getMediaFileService();
}

3. 使用 ApplicationContext

这种方法通过 ApplicationContext 获取当前类的代理。

language-java
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
@Service
@Slf4j
public class MediaFileServiceImpl implements MediaFileService {


@Autowired
private ApplicationContext context;

private MediaFileService getMediaFileService() {

return context.getBean(MediaFileService.class);
}

@Override
public UploadFileResultDto upload() {

// ... 一些业务逻辑 ...
getMediaFileService().upload2Mysql();
// ... 其他业务逻辑 ...
}

@Transactional
@Override
public MediaFiles upload2Mysql() {

// ... 事务性操作 ...
}
}

4. 分离服务层

将事务性方法移至另一个服务类中。

language-java
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
@Service
@Slf4j
public class MediaFileServiceImpl implements MediaFileService {


@Autowired
private MediaFileTransactionalService transactionalService;

@Override
public UploadFileResultDto upload() {

// ... 一些业务逻辑 ...
transactionalService.upload2Mysql();
// ... 其他业务逻辑 ...
}
}

@Service
@Transactional
class MediaFileTransactionalService {


public MediaFiles upload2Mysql() {

// ... 事务性操作 ...
}
}

5. AspectJ 代理模式

使用 AspectJ 代理模式而不是默认的 JDK 动态代理。

language-java
1
2
3
4
5
6
@EnableAspectJAutoProxy(proxyTargetClass = true)
@Configuration
public class AppConfig {

// ... 其他配置 ...
}

然后,您的 MediaFileServiceImpl 类保持不变。

SpringCloud + Nacos配置文件加载顺序和优先级详解

SpringCloud + Nacos配置文件加载顺序和优先级详解

在微服务架构中,合理地管理和理解配置文件的加载顺序与优先级对于确保应用的稳定性和灵活性至关重要。特别是在使用 Spring Cloud Alibaba Nacos 作为配置中心的场景下,这一点显得尤为重要。本文将基于一个具体的 bootstrap.yml 配置示例,深入探讨这些概念,并介绍如何通过 Nacos 配置实现本地配置的优先级设置。

一、加载顺序与优先级

1. 示例配置

首先,我们看一下示例的 bootstrap.yml 配置:

language-yaml
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
spring:
application:
name: content-api
cloud:
nacos:
server-addr: 192.168.101.65:8848
discovery:
namespace: dev
group: xuecheng-plus-project
config:
namespace: dev
group: xuecheng-plus-project
file-extension: yaml
refresh-enabled: true
extension-configs:
- data-id: content-service-${
spring.profiles.active}.yaml
group: xuecheng-plus-project
refresh: true
shared-configs:
- data-id: swagger-${
spring.profiles.active}.yaml
group: xuecheng-plus-common
refresh: true
- data-id: logging-${
spring.profiles.active}.yaml
group: xuecheng-plus-common
refresh: true
profiles:
active: dev

2. 配置文件分类

Spring Cloud Alibaba Nacos 环境中,我们主要遇到以下类型的配置文件:

  1. 本地配置文件

    • bootstrap.yml / bootstrap.yaml
    • application.yml / application.yaml
  2. Nacos 配置中心的配置文件

    • 共享配置文件 (shared-configs)
    • 扩展配置文件 (extension-configs)
    • 项目应用名配置文件 (${spring.application.name}.yaml / .properties)

3. 加载顺序

  1. **bootstrap.yml / bootstrap.yaml**:首先加载,用于配置应用的启动环境。
  2. Nacos 配置中心的配置文件
    • 先加载 共享配置文件 (shared-configs)
    • 然后是 扩展配置文件 (extension-configs)
    • 最后是 项目应用名配置文件 (${spring.application.name}.yaml / .properties)
  3. **application.yml / application.yaml**:在 Nacos 配置加载之后。

4. 优先级

  1. 项目应用名配置文件:具有最高优先级。
  2. 扩展配置文件:次之,覆盖共享配置。
  3. 共享配置文件:优先级低于扩展配置。
  4. **本地 application.yml / application.yaml**:优先级低于所有从 Nacos 加载的配置。
  5. **本地 bootstrap.yml / bootstrap.yaml**:优先级最低。

二、本地配置优先的设置

Nacos 中,可以通过特定的配置来设置本地配置优先。这可以在 bootstrap.ymlapplication.yml 文件中设置:

language-yaml
1
2
3
4
spring:
cloud:
config:
override-none: true

override-none 设置为 true 时,本地配置文件 (application.yml / application.yaml) 将具有最高的优先级,即使这些配置在 Nacos 中也有定义。这种设置适用于需要在不同环境中覆盖远程配置中心配置的场景。

结论

了解和正确应用 Spring Cloud Alibaba Nacos 中配置文件的加载顺序和优先级,对于确保微服务的正确运行至关重要。此外,通过配置 override-nonetrue,可以灵活地实现本地配置优先的需求,进一步增强了配置管理的灵活性。这些特性使得 Spring Cloud Alibaba Nacos 成为管理微服务配置的强大工具。

MybatisPlus二级映射和关联对象ResultMap

MybatisPlus二级映射和关联对象ResultMap

在我们的教程中,我们设计了一个课程内容的数据结构,包含章节和相关资源。这种结构非常适合在线教育平台或电子学习系统,其中课程内容需要被组织成不同的章节和子章节,每个子章节可能关联特定的学习资源。

这将是一个很好的示例来展示 MyBatis 中如何使用一对多(<collection>)和一对一(<association>)映射。

一、业务背景

1. 数据库表结构

  1. 章节表 (chapter)
    这张表包含所有章节的信息,其中包括大章节和小章节。大章节作为容器,可以包含多个小章节。
    • id (章节ID)
    • parent_id (父章节ID,用于区分大章节和小章节)
    • name (章节名称)
    • courseId (课程ID)
language-java
1
2
3
4
5
6
7
public class Chapter {

private Long id;
private Long parentId;
private String name;
private Long courseId;
}
  1. 资源表 (resource)
    这张表包含与小章节相关联的资源信息。
    • id (资源ID)
    • section_id (章节ID,关联到章节表)
    • name (资源名称)
language-java
1
2
3
4
5
6
public class Resource {

private Long id;
private Long sectionId;
private String name;
}

2. 需求

要求根据courseId查询出指定课程的信息,包括大章节、小章节、资源,并以一定结构返回,比如

language-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
[
{

"id": 1,
"parentId": null,
"name": "Chapter 1",
"courseId": 100,
"subChapters": [
{

"id": 11,
"parentId": 1,
"name": "Section 1.1",
"courseId": 100,
"resource": {

"id": 101,
"sectionId": 11,
"name": "Introduction Video"
}
},
{

"id": 12,
"parentId": 1,
"name": "Section 1.2",
"courseId": 100,
"resource": null
}
],
"resource": null
}
// other...
]

所以我们定义一个Dto如下

language-java
1
2
3
4
5
6
7
public class ChapterDto extends Chapter {

private List<ChapterDto> subChapters;
private Resource resource;

// 构造器、getter和setter
}

二、使用映射直接得到指定结构

ChapterMapper.xml 文件中,我们定义 SQL 查询以及结果映射。

language-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
32
33
34
35
36
<mapper namespace="com.example.mapper.ChapterMapper">

<resultMap id="ChapterDtoMap" type="com.example.dto.ChapterDto">
<id column="chapter_id" property="id" />
<result column="parent_id" property="parentId" />
<result column="name" property="name" />
<result column="courseId" property="courseId" />
<collection property="subChapters" ofType="com.example.dto.ChapterDto">
<id column="sub_chapter_id" property="id" />
<result column="sub_parent_id" property="parentId" />
<result column="sub_name" property="name" />
<result column="sub_courseId" property="courseId" />
<association property="resource" javaType="com.example.model.Resource">
<id column="resource_id" property="id" />
<result column="section_id" property="sectionId" />
<result column="resource_name" property="name" />
</association>
</collection>
</resultMap>

<select id="selectChaptersWithResources" resultMap="ChapterDtoMap">
SELECT
c.id AS chapter_id, c.parent_id, c.name, c.courseId,
sc.id AS sub_chapter_id, sc.parent_id AS sub_parent_id, sc.name AS sub_name, sc.courseId AS sub_courseId,
r.id AS resource_id, r.section_id, r.name AS resource_name
FROM
chapter c
LEFT JOIN
chapter sc ON c.id = sc.parent_id
LEFT JOIN
resource r ON sc.id = r.section_id
WHERE
c.courseId = #{courseId} AND c.parent_id IS NULL
</select>

</mapper>

三、其他文件

1. Mapper

language-java
1
2
3
4
public interface ChapterMapper {

List<ChapterDto> selectChaptersWithResources(Long courseId);
}

2. Service

language-java
1
2
3
4
5
6
7
8
9
10
11
@Service
public class ChapterService {

@Autowired
private ChapterMapper chapterMapper;

public List<ChapterDto> getChaptersWithResources(Long courseId) {

return chapterMapper.selectChaptersWithResources(courseId);
}
}

3. Controller

language-java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RestController
@RequestMapping("/chapters")
public class ChapterController {

@Autowired
private ChapterService chapterService;

@GetMapping("/{courseId}")
public ResponseEntity<List<ChapterDto>> getChapters(@PathVariable Long courseId) {

List<ChapterDto> chapters = chapterService.getChaptersWithResources(courseId);
return ResponseEntity.ok(chapters);
}
}

四、概念理解

一级映射

在提供的 resultMap 中,一级映射是针对 ChapterDto类的直接属性的映射。这意味着数据库中的列(如 chapter_id, parent_id等)直接映射到 ChapterDto类的相应属性(如 id, parent_id等),这部分映射是非常直接的。

二级映射

二级映射用于处理复杂的对象关系,比如当一个对象包含其他对象或对象的集合时。这通常在处理一对多关系时出现,例如,一个章节结构(ChapterDto)可能包含多个子章节。

聚合

这种聚合是根据您在 <collection> 标签中定义的规则进行的。MyBatis 会识别哪些行应该被映射为独立的实例,哪些行应该作为子元素聚合到其他实例中。

五、标签使用

1. 标签

用途:用于映射一对多关系。在这个例子中,ChapterDto类包含一个 Chapter 类型的列表,这代表了大章节和小章节之间的一对多关系。

常用属性

  • property:指定要映射到的目标属性名称。
  • ofType:指定集合中元素的类型。

2. 标签

用途:用于映射一对一关系。在您的例子中,ChapterDto包含一个 Resource 类型的属性,这代表了小章节和资源之间的一对一关系。
常用属性

  • property:指定要映射到的目标属性名称。
  • javaType:指定关联对象的类型。
SpringBoot不同的@Mapping使用

SpringBoot不同的@Mapping使用

一、介绍

一般@Mapping类注解在Spring框架中用于将HTTP请求映射到对应的处理器方法。它们各自对应于不同类型的HTTP方法,主要用于RESTful Web服务中。以下是每个注解的作用:

  1. @GetMapping: 用于映射HTTP GET请求到处理器方法。通常用于读取操作。

  2. @PostMapping: 用于映射HTTP POST请求到处理器方法。通常用于创建操作。

  3. @PutMapping: 用于映射HTTP PUT请求到处理器方法。通常用于更新操作,其中更新操作是幂等的,意味着多次执行同一个请求会得到相同的结果。

  4. @DeleteMapping: 用于映射HTTP DELETE请求到处理器方法。通常用于删除操作。

  5. @PatchMapping: 用于映射HTTP PATCH请求到处理器方法。PATCH与PUT类似,但通常用于部分更新资源。

  6. @RequestMapping: 是一个通用的注解,可以用于映射任何HTTP方法,通过指定method属性来明确映射类型。如果不指定method,它将映射到所有HTTP方法。这是最早的注解,后来为了方便使用,根据HTTP方法细分出了上述的专用注解。

二、使用

在Spring框架中,不同的@Mapping注解通常与不同的数据获取方式结合使用,以适应各种HTTP请求的特点:

  1. @GetMapping:

    • 用于获取数据,通常与@RequestParam一起使用来接收查询参数。

    • 示例: 获取URL中查询参数的值。

      language-java
      1
      2
      3
      4
      5
      @GetMapping("/users")
      public String getUsers(@RequestParam String name) {

      // ...
      }
    • 也可以用@PathVariable来获取URL中的路径变量。

      language-java
      1
      2
      3
      4
      5
      @GetMapping("/users/{id}")
      public String getUserById(@PathVariable Long id) {

      // ...
      }
  2. @PostMapping:

    • 用于创建数据,通常与@RequestBody结合使用来接收请求体中的内容,如JSON或XML。

    • 示例: 从请求体中获取JSON对象。

      language-java
      1
      2
      3
      4
      5
      @PostMapping("/users")
      public String addUser(@RequestBody User user) {

      // ...
      }
    • 有时也与@RequestParam结合使用来处理表单数据。

  3. @PutMapping:

    • 用于更新数据,通常与@RequestBody结合使用来接收请求体中的内容。

    • 示例: 从请求体中获取JSON对象来更新数据。

      language-java
      1
      2
      3
      4
      5
      @PutMapping("/users/{id}")
      public String updateUser(@PathVariable Long id, @RequestBody User user) {

      // ...
      }
  4. @DeleteMapping:

    • 用于删除数据,通常与@PathVariable结合使用来指定要删除的资源的ID。

    • 示例: 删除指定ID的用户。

      language-java
      1
      2
      3
      4
      5
      @DeleteMapping("/users/{id}")
      public String deleteUser(@PathVariable Long id) {

      // ...
      }
  5. @PatchMapping:

    • 用于部分更新资源,与@RequestBody结合使用来接收部分数据。

    • 示例: 部分更新用户信息。

      language-java
      1
      2
      3
      4
      5
      @PatchMapping("/users/{id}")
      public String patchUser(@PathVariable Long id, @RequestBody Map<String, Object> updates) {

      // ...
      }
  6. @RequestMapping:

    • 这是一个通用注解,可以用来处理任何HTTP方法,其具体方法由method属性指定。
    • 数据获取方式取决于具体的HTTP方法。

每个注解的使用依赖于HTTP请求的语义,以及你如何打算处理请求数据。总体而言,@RequestParam@PathVariable用于从URL获取数据,而@RequestBody用于处理复杂的请求体内容。

SpringBoot注解@GetMapping处理Get请求

SpringBoot注解@GetMapping处理Get请求

一、如何从URL中获取参数

当你想从URL中获取名为courseId的参数时,可以使用@GetMapping("/course/{courseId}")@GetMapping("/course")两种办法,主要体现在URL模式和如何获取参数上。

  1. 使用@GetMapping("/course/{courseId}"):

    • 这种方式表示你正在定义一个REST风格的API,其中{courseId}是URL的一部分,通常被称为路径变量(path variable)。

    • 你可以通过在方法的参数中加上@PathVariable注解来获取这个路径变量。例如:

      language-java
      1
      2
      3
      4
      5
      @GetMapping("/course/{courseId}")
      public String getCourse(@PathVariable String courseId) {

      // 使用courseId
      }
    • 这种方式适合于当courseId是必需的,并且每个课程的URL都是唯一的情况。

  2. 使用@GetMapping("/course"):

    • 这种方式下,URL不直接包含courseId。相反,courseId可以作为请求参数(query parameter)来传递。

    • 你可以通过在方法的参数中加上@RequestParam注解来获取这个请求参数。例如:

      language-java
      1
      2
      3
      4
      5
      @GetMapping("/course")
      public String getCourse(@RequestParam String courseId) {

      // 使用courseId
      }
    • 这种方式适合于当你想要让courseId作为一个可选参数或者你希望从一组标准的URL中筛选特定课程的情况。

二、获取多个参数

当URL中有多个参数时,@GetMapping("/course/{courseId}")@GetMapping("/course")的使用方式和它们之间的区别仍然基于路径变量(Path Variable)和请求参数(Request Parameter)的概念。这两种方法可以根据参数的性质和用途灵活组合使用。

  1. 使用路径变量(Path Variables):

    • 当你使用@GetMapping("/course/{courseId}")并且URL中有多个参数时,这些参数通常是URL路径的一部分,并且每个参数都是资源定位的关键部分。

    • 例如,如果你有一个URL像这样:/course/{courseId}/module/{moduleId},你可以这样使用:

      language-java
      1
      2
      3
      4
      5
      @GetMapping("/course/{courseId}/module/{moduleId}")
      public String getModule(@PathVariable String courseId, @PathVariable String moduleId) {

      // 使用courseId和moduleId
      }
    • 在这个例子中,courseIdmoduleId都是路径的一部分,用来定位特定的资源。

  2. 使用请求参数(Request Parameters):

    • 当你使用@GetMapping("/course")并且URL中有多个参数时,这些参数通常作为URL的查询字符串(query string)。

    • 例如,URL可能是这样的:/course?courseId=123&moduleId=456,你可以这样使用:

      language-java
      1
      2
      3
      4
      5
      @GetMapping("/course")
      public String getCourse(@RequestParam String courseId, @RequestParam String moduleId) {

      // 使用courseId和moduleId
      }
    • 在这个例子中,courseIdmoduleId是作为查询字符串的一部分传递的,通常用于过滤、排序或其他非资源定位的操作。

三、用一个类接收多个参数

对于URL中包含多个请求参数(如 /course?courseId=123&moduleId=456),你可以使用一个Java类来接收所有这些参数。这种方法可以使代码更加整洁,尤其是当处理具有多个参数的复杂请求时。

这里是如何使用一个类来接收所有请求参数的示例:

  1. 定义一个类来表示请求参数 :
    首先,你需要定义一个类,其属性对应于URL中的请求参数。例如,对于参数courseIdmoduleId,你可以定义如下类:

    language-java
    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
    public class CourseQuery {

    private String courseId;
    private String moduleId;

    // 标准的getter和setter方法
    public String getCourseId() {

    return courseId;
    }

    public void setCourseId(String courseId) {

    this.courseId = courseId;
    }

    public String getModuleId() {

    return moduleId;
    }

    public void setModuleId(String moduleId) {

    this.moduleId = moduleId;
    }

    // 可以添加构造函数、toString()等
    }
  2. 在控制器方法中使用这个类 :
    接下来,在你的控制器方法中,你可以将这个类作为参数,并使用@ModelAttribute注解(或者不使用任何注解,因为Spring MVC会自动将请求参数绑定到类的属性上):

    language-java
    1
    2
    3
    4
    5
    @GetMapping("/course")
    public String getCourse(@ModelAttribute CourseQuery query) {

    // 使用query.getCourseId()和query.getModuleId()
    }

    或者,如果你不使用@ModelAttribute注解,Spring MVC默认也会按照相同的方式处理这个参数:

    language-java
    1
    2
    3
    4
    5
    @GetMapping("/course")
    public String getCourse(CourseQuery query) {

    // 使用query.getCourseId()和query.getModuleId()
    }

使用这种方式,你可以轻松地处理包含许多参数的复杂请求,同时保持代码的清晰和可维护性。这也让你能够在需要时对参数进行验证,比如使用Spring的验证框架来确保传入的参数满足特定条件。

四、疑问

Q:定义一个类来表示请求参数时,不需要使用@RequestParam吗?还是说@RequestParam用于简单的类型获取单个参数,比如String?

在定义一个类来表示请求参数时,通常不需要在类的属性上使用@RequestParam注解。当你使用一个类来接收请求参数,Spring MVC的数据绑定机制会自动将请求中的参数名称映射到类的属性上。这是基于类的属性名和请求参数名的匹配来完成的。

使用类接收参数:

  • 当你有多个请求参数,并且想要将它们组织在一个类中时,你可以定义一个类,其属性与请求参数的名称相对应。

  • 在控制器方法中,你可以使用这个类的实例作为参数,而无需为每个属性单独使用@RequestParam。Spring会自动处理参数到类属性的映射。

  • 例如:

    language-java
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public class CourseQuery {

    private String courseId;
    private String moduleId;

    // getter和setter省略
    }

    @GetMapping("/course")
    public String getCourse(CourseQuery query) {

    // 使用query.getCourseId()和query.getModuleId()
    }

使用@RequestParam:

  • @RequestParam通常用于获取单个请求参数,特别是当这个参数是一个简单类型(如Stringint等)时。

  • 当你只需要从请求中获取一个或少数几个参数,而不是一个复杂对象时,@RequestParam是一个很好的选择。

  • 例如:

    language-java
    1
    2
    3
    4
    5
    @GetMapping("/course")
    public String getCourse(@RequestParam String courseId, @RequestParam String moduleId) {

    // 直接使用courseId和moduleId
    }

总结来说,当处理包含多个参数的复杂请求时,使用一个类来封装这些参数可以使代码更加清晰和易于维护。而@RequestParam适用于处理更简单或直接的参数获取需求。在实际的应用中,你可以根据具体情况选择最适合的方法。

Q:假如我已经有一个在/course上的controller了,如下
@PostMapping("/course") public CourseBaseInfoDto createCourseBase(@RequestBody AddCourseDto dto)
我还能继续写
@GetMapping("/course") public String getCourse(@RequestParam String courseId, @RequestParam String moduleId)
吗?

是的,你完全可以在同一个控制器(Controller)中同时定义一个@PostMapping和一个@GetMapping方法,即使它们都映射到了相同的路径(如/course)。这是因为HTTP请求的方法类型(GET、POST等)作为映射的一部分,允许区分不同的操作。在你的例子中,一个方法用于处理POST请求,另一个用于处理GET请求。

  1. @PostMapping("/course") 用于创建课程(通常是添加新资源),接收一个请求体(RequestBody)。

  2. @GetMapping("/course") 用于获取课程信息(通常是读取资源),接收URL中的请求参数(RequestParam)。

你的控制器可能看起来像这样:

language-java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@RestController
@RequestMapping("/path-to-controller")
public class CourseController {


@PostMapping("/course")
public CourseBaseInfoDto createCourseBase(@RequestBody AddCourseDto dto) {

// 创建课程的逻辑
}

@GetMapping("/course")
public String getCourse(@RequestParam String courseId, @RequestParam String moduleId) {

// 获取课程信息的逻辑
}
}

在这种配置中,当一个POST请求发送到/course时,createCourseBase方法将被调用;而当一个GET请求发送到/course时,带有请求参数的getCourse方法将被调用。

这种方法是RESTful API设计中的常见实践,它允许你在同一路径上对资源进行不同类型的操作,同时保持了代码的清晰和逻辑的分离。

SpringBoot自定义全局异常处理器

SpringBoot自定义全局异常处理器

一、介绍

Springboot框架提供两个注解帮助我们十分方便实现全局异常处理器以及自定义异常

  • @ControllerAdvice@RestControllerAdvice(推荐)
  • @ExceptionHandler

二、实现

1. 定义全局异常处理器

定义GlobalExceptionHandler类,拦截所有异常。
@RestControllerAdvice注解使得你可以在GlobalExceptionHandler 中处理异常,@ExceptionHandle注解用于将指定异常绑定到处理的函数上。如下使用@ExceptionHandler(Exception.class)即对所有异常进行捕获处理。

language-java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {


@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public RestErrorResponse exception(Exception e){

//record log
log.error("系统异常{}", e.getMessage(),e);
//decode errorException
String errMessage = "系统异常";
return new RestErrorResponse(errMessage);
}
}
language-java
1
2
3
4
5
6
@Data
@AllArgsConstructor
public class RestErrorResponse implements Serializable {

private String errMessage;
}

事实上,写到这里已经可以用了,RestErrorResponse 用来承载错误信息到前端,因为@RestControllerAdvice已经包含了@ResponseBody

2. 自定义异常类

继承RuntimeException 异常类写一个自定义的异常类。这么做主要是能够使用自定义的枚举类来更优雅的抛出错误。

language-java
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
@Data
public class XueChengPlusException extends RuntimeException {


private String errMessage;

public XueChengPlusException() {

super();
}

public XueChengPlusException(String errMessage) {

super(errMessage);
this.errMessage = errMessage;
}

public static void cast(CommonError commonError){

throw new XueChengPlusException(commonError.getErrMessage());
}
public static void cast(String errMessage){

throw new XueChengPlusException(errMessage);
}

}
language-java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Getter
public enum CommonError {

UNKOWN_ERROR("执行过程异常,请重试。"),
PARAMS_ERROR("非法参数"),
OBJECT_NULL("对象为空"),
QUERY_NULL("查询结果为空"),
REQUEST_NULL("请求参数为空");

private String errMessage;

private CommonError( String errMessage) {

this.errMessage = errMessage;
}
}

同时,对于GlobalExceptionHandler 也要做一些修改,一方面处理自定义异常,另一方处理其余异常。

language-java
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
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {


@ExceptionHandler(XueChengPlusException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public RestErrorResponse customException(XueChengPlusException e){

//record log
log.error("系统异常{}", e.getErrMessage(),e);
//decode errorException
String errMessage = e.getErrMessage();
return new RestErrorResponse(errMessage);
}

@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public RestErrorResponse exception(Exception e){

//record log
log.error("系统异常{}", e.getMessage(),e);
//decode errorException
String errMessage = CommonError.UNKOWN_ERROR.getErrMessage();
return new RestErrorResponse(errMessage);
}
}

三、使用

在程序中任意地方抛出异常,controllerservicedao层都可以,比如

language-java
1
throw new RuntimeException("价格不能为空且必须大于0");

这时走的就是

language-java
1
2
@ExceptionHandler(Exception.class)
public RestErrorResponse exception(Exception e)

除此之外,可以这样抛出自定义异常,比如

language-java
1
XueChengPlusException.cast(CommonError.PARAMS_ERROR);
language-java
1
XueChengPlusException.cast("其他的消息");
language-java
1
throw new XueChengPlusException(CommonError.OBJECT_NULL.getErrMessage());
language-java
1
throw new XueChengPlusException("其他的消息");

这时走的就是

language-java
1
2
@ExceptionHandler(XueChengPlusException.class)
public RestErrorResponse customException(XueChengPlusException e)

四、疑问

Q:疑问,XueChengPlusException异常类继承自RuntimeException ,而RuntimeException 继承自Exception,为什么触发customException而不是exception?

在这个全局异常处理器中,当抛出一个XueChengPlusException异常时,它会被customException(XueChengPlusException e)方法处理,而不是exception(Exception e)方法。

这是因为Spring框架的异常处理机制会优先匹配最具体的异常类型。在您的代码中,XueChengPlusExceptionRuntimeException(以及Exception)的子类,因此它更具体。所以,当抛出一个XueChengPlusException异常时,Spring会优先调用处理XueChengPlusException的方法,而不是处理Exception的方法。

这种行为确实表明全局异常处理器有一定的优先级和覆盖逻辑。具体来说,处理器会优先处理更具体的异常类型,如果没有找到匹配的处理器,那么它会寻找处理更一般异常类型的处理器。

SpringBoot基于Redis(7.2)分片集群实现读写分离

SpringBoot基于Redis(7.2)分片集群实现读写分离

一、前置提要

SpringBoot访问Redis分片集群和Redis哨兵模式,使用上没有什么区别。唯一的区别在于application.yml配置上不一样。

二、集群搭建

首先,无论如何,得先有一个Redis分片集群,具体可以参考下面这篇文章

搭建完成后大致得到如下图描述的一个集群。

三、SpringBoot访问分片集群

其次,具体如何结合IdeaDocker让本地开发的SpringBoot项目访问Redis分片集群,可以参考下面这篇文章

要注意的是,yaml文件要从

language-yaml
1
2
3
4
5
6
7
8
9
10
spring:
redis:
sentinel:
master: mymaster
nodes:
- 172.30.1.11:26379
- 172.30.1.12:26379
- 172.30.1.13:26379
password: 1009
password: 1009

变成

language-yaml
1
2
3
4
5
6
7
8
9
10
spring:
redis:
cluster:
nodes:
- 172.30.2.11:6379
- 172.30.2.12:6379
- 172.30.2.13:6379
- 172.30.2.21:6379
- 172.30.2.22:6379
- 172.30.2.23:6379

其余基本一致。