SpringCloud + Nacos环境下抽取Feign独立模块并支持MultipartFile

SpringCloud + Nacos环境下抽取Feign独立模块并支持MultipartFile

一、前提条件和背景

1. 前提

已经部署好Nacos,本文以192.168.101.65:8848为例。

2. 背景

有两个微服务mediacontent,都已经注册到Nacos
后者通过引用Feign实现远程调用前者。
两个微服务都被分为3个子模块:api、service、model,对应三层架构。

请根据自身情况出发阅读本文。

二、Feign模块

1. 依赖引入

首先需要Feign依赖和扩展。

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
<!--   openfeign     -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
<version>4.1.0</version>
</dependency>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>
<!--feign支持Multipart格式传参-->
<dependency>
<groupId>io.github.openfeign.form</groupId>
<artifactId>feign-form</artifactId>
<version>3.8.0</version>
</dependency>
<dependency>
<groupId>io.github.openfeign.form</groupId>
<artifactId>feign-form-spring</artifactId>
<version>3.8.0</version>
</dependency>

需要测试依赖(可选),为了MockMultipartFile类才引入的,非必需功能。

language-xml
1
2
3
4
5
6
7
<!--    测试    -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>6.1.2</version>
<scope>compile</scope>
</dependency>

其次需要涉及到的微服务的数据模型,根据个人情况而定。

如果只想要它们的数据模型,而不引入不必要的依赖,可以使用通配符*全部过滤掉。

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
<!--   数据模型pojo     -->
<dependency>
<groupId>com.xuecheng</groupId>
<artifactId>xuecheng-plus-media-model</artifactId>
<version>0.0.1-SNAPSHOT</version>
<exclusions>
<exclusion>
<groupId>*</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.xuecheng</groupId>
<artifactId>xuecheng-plus-content-model</artifactId>
<version>0.0.1-SNAPSHOT</version>
<exclusions>
<exclusion>
<groupId>*</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>

2. application.yaml配置

填入以下内容,大抵为超时熔断处理。(可选),甚至可以留空。

language-yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
feign:
hystrix:
enabled: true
circuitbreaker:
enabled: true
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 30000
ribbon:
ConnectTimeout: 60000
ReadTimeout: 60000
MaxAutoRetries: 0
MaxAutoRetriesNextServer: 1

3. 扩展支持MultipartFile

新建一个配置类,如下,
主要是Encoder feignEncoder()使得Feign支持MultipartFile类型传输。
MultipartFile getMultipartFile(File file)是一个工具方法,和配置无关。

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
@Configuration
public class MultipartSupportConfig {


@Autowired
private ObjectFactory<HttpMessageConverters> messageConverters;

@Bean
@Primary//注入相同类型的bean时优先使用
@Scope("prototype")
public Encoder feignEncoder() {

return new SpringFormEncoder(new SpringEncoder(messageConverters));
}

//将file转为Multipart
public static MultipartFile getMultipartFile(File file) {

try {

byte[] content = Files.readAllBytes(file.toPath());
MultipartFile multipartFile = new MockMultipartFile(file.getName(),
file.getName(), Files.probeContentType(file.toPath()), content);
return multipartFile;
} catch (IOException e) {

e.printStackTrace();
XueChengPlusException.cast("File->MultipartFile转化失败");
return null;
}
}
}

4. 将media-api注册到feign

新建一个类,如下。
@FeignClient(value)要和服务名称对上,即media模块spring.application.name=media-api
@FeignClient(path)要和服务前缀路径对上,即media模块server.servlet.context-path=/media
然后MediaClient中的方法定义尽量和media模块对应的controller函数保持一致。

language-java
1
2
3
4
5
6
7
8
9
10
11
@FeignClient(value = "media-api", path = "/media")
public interface MediaClient {


@RequestMapping(value = "/upload/coursefile", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public UploadFileResultDto upload(
@RequestPart("filedata") MultipartFile file,
@RequestParam(value = "objectName", required = false) String objectName
);
}

三、Media模块

被调用方media模块无需做什么修改。

四、Content模块

测试在content-api上操作。

1. 引入依赖

content模块需要引入刚才feign模块的依赖。

language-xml
1
2
3
<dependency>
<!-- 根据自身情况引入 -->
</dependency>

2. 启用FeignClient

在启动类上加上@EnableFeignClients注解。

3. 测试

新建测试类,如下

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
package com.xuecheng.content.service.jobhandler;

import com.xuecheng.feign.client.MediaClient;
import com.xuecheng.media.model.dto.UploadFileResultDto;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.web.multipart.MultipartFile;

@SpringBootTest
class CoursePublishTaskTest {

@Autowired
MediaClient mediaClient;

@Test
void generateCourseHtml() {

MultipartFile file = new MockMultipartFile(
"filedata",
"filename.txt",
"text/plain",
"Some dataset...".getBytes()
);
UploadFileResultDto upload = mediaClient.upload(file, "/static-test/t-1");
System.out.println(upload);
}
}

启动Media模块,启动测试方法,
具体的Debug和检验,可以通过Media模块对应的controller函数打印日志,检查是否通过MediaClient 被触发。

五、需要澄清的几点

  1. Feign模块不需要注册到Nacos且不需要服务发现
    正确。feign-client模块只是一个包含Feign客户端接口的库,它自身并不是一个独立的微服务。因此,它不需要注册到Nacos,也不需要服务发现功能。这个模块只是被其他微服务模块(如content模块)作为依赖引入。这样做的主要目的是为了代码的重用和解耦,允许任何微服务通过引入这个依赖来调用其他服务。

  2. 只有调用者(如content模块)需要使用@EnableFeignClients注解,被调用者(如media模块)不需要
    正确。@EnableFeignClients注解是用来启用Feign客户端的,它告诉Spring Cloud这个服务将会使用Feign来进行远程服务调用。因此,只有需要使用Feign客户端的服务(在这个例子中是content模块)需要添加这个注解。而被调用的服务(如media模块),只需作为普通的Spring Boot应用运行,提供REST API即可,无需使用@EnableFeignClients

  3. 如何在服务间共享数据模型(如DTOs)而不引入不必要的依赖。
    解决这个问题的一种方法是创建一个共享的库或模块,这个库包含所有服务共享的数据模型。另一种使用依赖剥离,使用通配符(*)可以排除pom.xml中特定依赖的所有传递性依赖。

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 类保持不变。

AutoHotKey(V2)控制Windows扩展屏视频并开机自启

AutoHotKey(V2)控制Windows扩展屏视频并开机自启

起因是因为买了一块扩展屏幕,经常用来播放教学视频,而主屏幕用于实战操作,但是每次对视频进行控制时都要把鼠标移动过去,点击,再回来找到原来的代码位置,很难受。所以用AutoHotKey写了一个快捷键脚本。

一、参考资料

官方AutoHotKey V2 Docs

Windows设置开机自启

二、脚本功能介绍

language-python
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
CoordMode "Mouse", "Screen"  ; 将鼠标坐标模式设置为整个屏幕
SendMode "Event" ; 设置发送模式为事件模式

ExtendBilibili(key, count)
{

xpos := 0
ypos := 0
originalWinHwnd := WinExist("A") ; 获取当前活动窗口的HWND
MouseGetPos &xpos, &ypos
; MsgBox Format("1- {1} {2}", xpos, ypos)
MouseMove 2300, 1050, 0 ; 移动到视频范围
MouseClick "left", , , , 5 ; 点击一处无关紧要的地方,使视频应用聚焦
Loop count ; 根据提供的次数重复发送键
{

Send key
}
; MsgBox Format("2- {1} {2}", xpos, ypos)
Sleep 50
MouseMove xpos, ypos, 0 ; 鼠标移回原处
WinActivate("ahk_id " . originalWinHwnd) ; 激活原始窗口
}

#!Space:: ; Win + Alt + Space
{

ExtendBilibili("{Space}", 1)
}

#!Left:: ; Win + Alt + Left
{

ExtendBilibili("{Left}", 3)
}

#!Right:: ; Win + Alt + Right
{

ExtendBilibili("{Right}", 2)
}

#!Up:: ; Win + Alt + Up
{

xpos := 0
ypos := 0
originalWinHwnd := WinExist("A") ; 获取当前活动窗口的HWND
MouseGetPos &xpos, &ypos
MouseMove 2300, 1050, 1
MouseClick "left", , , , 0
Sleep 2000
MouseMove xpos, ypos, 0
WinActivate("ahk_id " . originalWinHwnd) ; 激活原始窗口
}

Docker多节点部署Minio分布式文件系统并测试

Docker多节点部署Minio分布式文件系统并测试

一、前提准备

准备如下文件夹和文件

language-txt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
./
├── docker-compose-minio.yml
├── .env
├── env
│ ├── minio.env
├── minio
│ ├── minio1
│ │ ├── data1
│ │ └── data2
│ ├── minio2
│ │ ├── data1
│ │ └── data2
│ ├── minio3
│ │ ├── data1
│ │ └── data2
│ └── minio4
│ ├── data1
│ └── data2

二、文件配置

1. .env

language-env
1
MINIO_VERSION=RELEASE.2024-01-29T03-56-32Z

2. env/minio.env

language-env
1
2
MINIO_ROOT_USER=minio
MINIO_ROOT_PASSWORD=minio123

3. docker-compose-minio.yml

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
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
81
82
83
84
version: "3.8"
networks:
docker_xuecheng:
ipam:
config:
- subnet: 172.20.0.0/16

services:
minio1:
container_name: minio1
image: minio/minio:${
MINIO_VERSION}
volumes:
- ./minio/minio1/data1:/data1
- ./minio/minio1/data2:/data2
ports:
- "9001:9000"
- "9011:9001"
env_file:
- ./env/minio.env
command: server --address ":9000" --console-address ":9001" http://172.20.2.{
1...4}/data{
1...2}
networks:
docker_xuecheng:
ipv4_address: 172.20.2.1

minio2:
container_name: minio2
image: minio/minio:${
MINIO_VERSION}
volumes:
- ./minio/minio2/data1:/data1
- ./minio/minio2/data2:/data2
ports:
- "9002:9000"
- "9012:9001"
env_file:
- ./env/minio.env
command: server --address ":9000" --console-address ":9001" http://172.20.2.{
1...4}/data{
1...2}
networks:
docker_xuecheng:
ipv4_address: 172.20.2.2

minio3:
container_name: minio3
image: minio/minio:${
MINIO_VERSION}
volumes:
- ./minio/minio3/data1:/data1
- ./minio/minio3/data2:/data2
ports:
- "9003:9000"
- "9013:9001"
env_file:
- ./env/minio.env
command: server --address ":9000" --console-address ":9001" http://172.20.2.{
1...4}/data{
1...2}
networks:
docker_xuecheng:
ipv4_address: 172.20.2.3

minio4:
container_name: minio4
image: minio/minio:${
MINIO_VERSION}
volumes:
- ./minio/minio4/data1:/data1
- ./minio/minio4/data2:/data2
ports:
- "9004:9000"
- "9014:9001"
env_file:
- ./env/minio.env
command: server --address ":9000" --console-address ":9001" http://172.20.2.{
1...4}/data{
1...2}
networks:
docker_xuecheng:
ipv4_address: 172.20.2.4

三、测试

访问宿主机ip:9011,输入账号密码。

language-txt
1
2
MINIO_ROOT_USER=minio
MINIO_ROOT_PASSWORD=minio123

点到Monitoring -> Metrics

四、Java测试

1. 引入依赖

language-xml
1
2
3
4
5
6
7
8
9
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

2. 增删改

在这之前先去网页端,创建一个Bucket

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
package com.xuecheng.media;

import io.minio.*;
import io.minio.errors.*;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.compress.utils.IOUtils;
import org.junit.jupiter.api.Test;

import java.io.*;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;

public class MinioTest {

private MinioClient minioClient = MinioClient.builder()
.endpoint("http://192.168.101.65:9001") //改成你的宿主机ip
.credentials("minio", "minio123")
.build();

@Test
public void testCreate() throws IOException, ServerException, InsufficientDataException, ErrorResponseException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException {

ObjectWriteResponse file = minioClient.uploadObject(
UploadObjectArgs.builder()
.bucket("test")
.filename("C:\\Users\\mumu\\Desktop\\1C6091EF9671978A9F1B6C6F8A3666FD.png")
.object("1.png")
.build()
);
}

@Test
public void testDelete() throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException {

minioClient.removeObject(
RemoveObjectArgs.builder()
.bucket("test")
.object("12.msi")
.build()
);
}

@Test
public void testGet() throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException {

InputStream inputStream = minioClient.getObject(
GetObjectArgs.builder()
.bucket("test")
.object("1.png")
.build()
);
FileOutputStream outputStream = new FileOutputStream(new File("C:\\Users\\mumu\\Desktop\\2.png"));
IOUtils.copy(inputStream, outputStream);
}

}

SpringCloud Gateway(4.1.0) 返回503:原因分析与解决方案

SpringCloud Gateway(4.1.0) 返回503:原因分析与解决方案

一、环境版本

Version
spring-cloud-dependencies 2023.0.0
spring-cloud-starter-gateway 4.1.0
Nacos v2.3.0

二、原因分析

Spring Cloud Gateway 的早期版本中,Ribbon 被用作默认的负载均衡器。随着Spring Cloud的发展,RibbonSpring Cloud LoadBalancer 替代。在过渡期间,为了兼容,Spring Cloud 同时支持了 RibbonSpring Cloud LoadBalancer。然而,从 Spring Cloud 2020.0.0 版本开始,Ribbon 被废弃,Spring Cloud LoadBalancer 成为了推荐的负载均衡方案。

在这个版本变动中,为了提供更大的灵活性,spring-cloud-starter-loadbalancer 被标记为了可选依赖,不再默认包含在 Spring Cloud Gateway 中。因此,在使用 4.1.0 版本的 Spring Cloud Gateway 并需要服务发现和负载均衡功能时,如果没有显式包含这个依赖,就会导致无法处理 lb://URI,从而返回503错误。

三、解决方案

要解决这个问题,您需要在您的项目的 POM 文件中显式添加 spring-cloud-starter-loadbalancer 依赖:

language-xml
1
2
3
4
5
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
<version>4.1.0</version>
</dependency>

添加后,确保重启应用程序以使配置生效。这样,Spring Cloud Gateway 就能够正确处理基于服务发现的负载均衡,从而避免503错误。

通过理解 Spring Cloud 的历史演变和适应其依赖管理的变化,我们可以更好地维护和优化我们的微服务架构。

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:指定关联对象的类型。
spring-boot-starter-validation常用注解

spring-boot-starter-validation常用注解

一、使用

要使用这些注解,首先确保在你的 Spring Boot 应用的 pom.xml 文件中添加了 spring-boot-starter-validation 依赖。然后,你可以将这些注解应用于你的模型类字段上。在你的控制器方法中,你可以使用 @Valid@Validated 注解来触发验证,例如:

language-java
1
2
3
4
5
6
@PostMapping("/users")
public ResponseEntity<?> createUser(@Valid @RequestBody User user) {

// 如果存在验证错误,会抛出异常
// 正常业务逻辑
}

在这个例子中,如果 User 对象的字段不满足注解定义的验证规则,Spring 将抛出一个异常,你可以通过全局异常处理或控制器层的异常处理来处理这些异常,并向用户返回适当的响应。

二、常用注解

spring-boot-starter-validation 依赖包引入了 Java Bean Validation API(通常基于 Hibernate Validator 实现),提供了一系列注解来帮助你对 Java 对象进行验证。以下是一些常用的验证注解及其含义和使用方式:

  1. @NotNull : 确保字段不是 null

    language-java
    1
    2
    3
    4
    5
    6
    public class User {

    @NotNull(message = "用户名不能为空")
    private String username;
    // 其他字段和方法
    }
  2. @NotEmpty : 确保字段既不是 null 也不是空(对于字符串意味着长度大于0,对于集合意味着至少包含一个元素)。

    language-java
    1
    2
    3
    4
    5
    6
    public class User {

    @NotEmpty(message = "密码不能为空")
    private String password;
    // 其他字段和方法
    }
  3. @NotBlank : 确保字符串字段不是 null 且至少包含一个非空白字符。

    language-java
    1
    2
    3
    4
    5
    6
    public class User {

    @NotBlank(message = "邮箱不能为空且不能只包含空格")
    private String email;
    // 其他字段和方法
    }
  4. @Size: 确保字段(字符串、集合、数组)符合指定的大小范围。

    language-java
    1
    2
    3
    4
    5
    6
    public class User {

    @Size(min = 2, max = 30, message = "用户名长度必须在2到30之间")
    private String username;
    // 其他字段和方法
    }
  5. @Min@Max: 对数值类型字段设置最小值和最大值。

    language-java
    1
    2
    3
    4
    5
    6
    7
    public class User {

    @Min(value = 18, message = "年龄必须大于等于18")
    @Max(value = 100, message = "年龄必须小于等于100")
    private int age;
    // 其他字段和方法
    }
  6. @Email: 确保字段是有效的电子邮件地址。

    language-java
    1
    2
    3
    4
    5
    6
    public class User {

    @Email(message = "无效的邮箱格式")
    private String email;
    // 其他字段和方法
    }
  7. @Pattern: 确保字符串字段匹配正则表达式。

    language-java
    1
    2
    3
    4
    5
    6
    public class User {

    @Pattern(regexp = "^[a-zA-Z0-9]+$", message = "用户名只能包含字母和数字")
    private String username;
    // 其他字段和方法
    }
  8. @Positive@PositiveOrZero: 确保数值字段是正数或者正数和零。

    language-java
    1
    2
    3
    4
    5
    6
    public class Product {

    @Positive(message = "价格必须是正数")
    private BigDecimal price;
    // 其他字段和方法
    }

三、@Valid or @Validated ?

@Valid@Validated 注解都用于数据验证,但它们在使用和功能上有一些差异:

  1. @Valid:

    • 来源于 JSR 303/JSR 380 Bean Validation API。
    • 可以用在方法参数上,以触发对传递给该方法的对象的验证。这通常在 Spring MVC 中用于验证带有 @RequestBody@ModelAttribute 注解的参数。
    • 不支持验证组的概念,这意味着不能控制验证的顺序或验证特定的子集。

    示例:

    language-java
    1
    2
    3
    4
    5
    @PostMapping("/users")
    public ResponseEntity<?> createUser(@Valid @RequestBody User user) {

    // 业务逻辑
    }
  2. @Validated(推荐):

    • 是 Spring 的特有注解,不是 JSR 303/JSR 380 的一部分。
    • 支持验证组,允许您更灵活地指定在特定情况下应用哪些验证约束。例如,可以根据不同的操作(如创建、更新)定义不同的验证规则。
    • 可以用在类型级别(在类上)和方法参数上。在类型级别使用时,它会触发该类中所有带有验证注解的方法的验证。

    示例:

    language-java
    1
    2
    3
    4
    5
    @PostMapping("/users")
    public ResponseEntity<?> createUser(@Validated @RequestBody User user) {

    // 业务逻辑
    }

在实际使用中,如果你需要简单的验证功能,@Valid 是一个很好的选择。如果你需要更复杂的验证逻辑,比如验证组,那么 @Validated 更适合。此外,@Validated 可以应用在类级别,从而对一个类的多个方法进行验证,这在使用 Spring 服务层时非常有用。

四、分组校验

分组校验(Group Validation)是一种在 Java Bean Validation 中用于在不同上下文中应用不同验证规则的方法。这对于那些在不同情况下(例如,创建 vs 更新)需要不同验证规则的对象特别有用。

1. 分组校验的基本概念

在分组校验中,你可以定义多个接口(通常为空)来表示不同的验证组。然后,你可以在验证注解中指定这些接口,以表明该注解仅在验证特定组时应用。

例如,你可能有一个User类,其中某些字段在创建用户时是必需的,但在更新用户时可能是可选的。

2. 定义验证组

首先,定义两个空接口作为验证组:

language-java
1
2
3
4
public interface OnCreate {
}
public interface OnUpdate {
}

3. 应用分组到模型

然后,在你的模型类中使用这些接口作为验证注解的参数:

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


@NotNull(groups = OnCreate.class)
private Long id;

@NotBlank(groups = {
OnCreate.class, OnUpdate.class})
private String username;

@Email(groups = {
OnCreate.class, OnUpdate.class})
private String email;

// 其他字段和方法
}

在这个例子中,id 字段仅在创建用户时需要验证(OnCreate组),而 usernameemail 字段在创建和更新用户时都需要验证。

4. 在控制器中使用分组

最后,在你的控制器方法中,使用 @Validated 注解指定要应用的验证组:

language-java
1
2
3
4
5
6
7
8
9
10
11
@PostMapping("/users")
public ResponseEntity<?> createUser(@Validated(OnCreate.class) @RequestBody User user) {

// 创建用户的业务逻辑
}

@PutMapping("/users")
public ResponseEntity<?> updateUser(@Validated(OnUpdate.class) @RequestBody User user) {

// 更新用户的业务逻辑
}

在这个例子中,createUser 方法只会验证属于 OnCreate 组的字段,而 updateUser 方法则只会验证属于 OnUpdate 组的字段。这样,你就可以根据不同的操作自定义验证逻辑了。

5. 默认组如何总是被校验

当字段没有指定groups时,属于默认组,当@Validated(OnUpdate.class)指定了特定的组后,属于默认组的字段将不再被校验,一个个都加上groups太麻烦,如何让默认组字段总是被校验呢?
如下

language-java
1
2
3
4
5
import jakarta.validation.groups.Default;
public interface OnCreate extends Default{
}
public interface OnUpdate extends Default{
}

6. 总结

通过使用分组校验,你可以为同一个对象的不同操作设置不同的验证规则,这在复杂应用中非常有用。这种方法提高了代码的灵活性和可维护性。

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用于处理复杂的请求体内容。