Spring AMQP(3.1.1)设置ConfirmCallback和ReturnsCallback

Spring AMQP(3.1.1)设置ConfirmCallback和ReturnsCallback

环境如下

Version
SpringBoot 3.2.1
spring-amqp 3.1.1
RabbitMq 3-management

一、起因

老版本的spring-amqpCorrelationData上设置ConfirmCallback。但是今天却突然发现correlationData.getFuture()没有addCallback函数了。

查询文档和帖子后,发现ConfirmCallbackReturnsCallback都需要在RabbitTemplate中设置,同时ConfirmCallback中默认无法得到消息内容,如果想在ConfirmCallback中把消息内容存到数据库等地方进行记录,怎么办呢?

参考手册

二、代码

1. 定义exchange和queue

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


//交换机
public static final String PAYNOTIFY_EXCHANGE_FANOUT = "paynotify_exchange_fanout";
//支付通知队列
public static final String PAYNOTIFY_QUEUE = "paynotify_queue";
//支付结果通知消息类型
public static final String MESSAGE_TYPE = "payresult_notify";


//声明交换机,且持久化
@Bean(PAYNOTIFY_EXCHANGE_FANOUT)
public FanoutExchange paynotify_exchange_fanout() {

// 三个参数:交换机名称、是否持久化、当没有queue与其绑定时是否自动删除
return new FanoutExchange(PAYNOTIFY_EXCHANGE_FANOUT, true, false);
}
//支付通知队列,且持久化
@Bean(PAYNOTIFY_QUEUE)
public Queue paynotify_queue() {

return QueueBuilder.durable(PAYNOTIFY_QUEUE).build();
}

//交换机和支付通知队列绑定
@Bean
public Binding binding_paynotify_queue(@Qualifier(PAYNOTIFY_QUEUE) Queue queue, @Qualifier(PAYNOTIFY_EXCHANGE_FANOUT) FanoutExchange exchange) {

return BindingBuilder.bind(queue).to(exchange);
}
}

2. RabbitTemplate

在上面的类中继续添加RabbitTemplate ,并设置ConfirmCallbackReturnsCallback

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
@Bean
public RabbitTemplate rabbitTemplate(final ConnectionFactory connectionFactory) {

final RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
//设置confirm callback
rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {

String body = "1";
if (correlationData instanceof EnhancedCorrelationData) {

body = ((EnhancedCorrelationData) correlationData).getBody();
}
if (ack) {

//消息投递到exchange
log.debug("消息发送到exchange成功:correlationData={},message_id={} ", correlationData, body);
System.out.println("消息发送到exchange成功:correlationData={},message_id={}"+correlationData+body);
} else {

log.debug("消息发送到exchange失败:cause={},message_id={}",cause, body);
System.out.println("消息发送到exchange失败:cause={},message_id={}"+cause+body);
}
});

//设置return callback
rabbitTemplate.setReturnsCallback(returned -> {

Message message = returned.getMessage();
int replyCode = returned.getReplyCode();
String replyText = returned.getReplyText();
String exchange = returned.getExchange();
String routingKey = returned.getRoutingKey();
// 投递失败,记录日志
log.error("消息发送失败,应答码{},原因{},交换机{},路由键{},消息{}",
replyCode, replyText, exchange, routingKey, message.toString());
});
return rabbitTemplate;
}

3. EnhancedCorrelationData

原始的CorrelationData,目前已经无法从中获取消息内容,也就是说现在的ConfirmCallback无法获取到消息的内容,因为设计上只关注是否投递到exchange成功。如果需要在ConfirmCallback中获取消息的内容,需要扩展这个类,并在发消息的时候,放入自定义数据。

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

private final String body;

public EnhancedCorrelationData(String id, String body) {

super(id);
this.body = body;
}

public String getBody() {

return body;
}
}

4. 发送消息

EnhancedCorrelationData把消息本身放进去,或者如果你有表记录消息,你可以只放入其id。这样触发ConfirmCallback的时候,就可以获取消息内容。

language-java
1
2
3
4
5
6
7
8
9
public void notifyPayResult() {

String message = "TEST Message";
Message message1 = MessageBuilder.withBody(message.getBytes(StandardCharsets.UTF_8))
.setDeliveryMode(MessageDeliveryMode.PERSISTENT)
.build();
CorrelationData correlationData = new EnhancedCorrelationData(UUID.randomUUID().toString(), message.toString());
rabbitTemplate.convertAndSend(PayNotifyConfig.PAYNOTIFY_EXCHANGE_FANOUT,"", message1, correlationData);
}
从RSA角度出发解析JWT原理

从RSA角度出发解析JWT原理

在今天的数字化世界中,安全地传递信息变得越来越重要。JSON Web TokenJWT)作为一种流行的开放标准,为简化服务器与客户端之间的安全交流提供了一种高效的方式。本文旨在深入解析JWT的工作原理,并通过示例演示如何使用JWT进行安全通信。我们将从JWT的基本组成部分讲起,探讨公密钥加密的原理,解释为什么JWT是安全的,以及如何验证JWT的有效性。

一、JWT介绍

1. JWT组成部分

JWT由三个部分组成,它们分别是头部(Header)、载荷(Payload)、和签名(Signature)。通过将这三部分用点(.)连接起来,形成了一个完整的JWT字符串。例如:xxxxx.yyyyy.zzzzz

2. 头部(Header)

头部通常由两部分组成:令牌类型(typ)和签名算法(alg)。例如:

language-json
1
2
3
4
5
{

"kid": "33bd1cad-62a6-4415-89a6-c2c816f3d3b1",
"alg": "RS256"
}
  • kid (Key ID):密钥标识符,用于指明验证JWT签名时使用的密钥。在含有多个密钥的系统中,这可以帮助接收者选择正确的密钥进行验证。这里的值33bd1cad-62a6-4415-89a6-c2c816f3d3b1是一个UUID,唯一标识了用于签名的密钥。
  • alg (Algorithm):指明用于签名的算法,这里是RS256,表示使用RSA签名算法和SHA-256散列算法。这种算法属于公钥/私钥算法,意味着使用私钥进行签名,而用公钥进行验证。

3. 载荷(Payload)

载荷包含了所要传递的信息,这些信息以声明(claims)的形式存在。声明可以是用户的身份标识,也可以是其他任何必要的信息。载荷示例:

language-json
1
2
3
4
5
6
7
8
9
10
{

"sub": "XcWebApp",
"aud": "XcWebApp",
"nbf": 1707373072,
"iss": "http://localhost:63070/auth",
"exp": 1707380272,
"iat": 1707373072,
"jti": "62e885c5-6b3f-49a2-aa10-b2e872a52b33"
}
  • sub (Subject):主题,标识了这个JWT的主体,通常是指用户的唯一标识。这里XcWebApp可能是一个应用或用户标识。
  • aud (Audience):受众,标识了这个JWT的预期接收者。这里同样是XcWebApp,意味着这个JWT是为XcWebApp这个应用或服务生成的。
  • nbf (Not Before):生效时间,这个时间之前,JWT不应被接受处理。这里的时间是Unix时间戳格式,表示JWT生效的具体时间。
  • iss (Issuer):发行者,标识了这个JWT的发行方。这里是http://localhost:63070/auth,表明JWT由本地的某个认证服务器发行。
  • exp (Expiration Time):过期时间,这个时间之后,JWT不再有效。同样是Unix时间戳格式,表示JWT过期的具体时间。
  • iat (Issued At):发行时间,JWT创建的时间。这提供了JWT的时间信息,也是Unix时间戳格式。
  • jti (JWT ID):JWT的唯一标识符,用于防止JWT被重放(即两次使用同一个JWT)。这里的值是一个UUID,确保了JWT的唯一性。

4. 签名(Signature)

签名是对头部和载荷的加密保护,确保它们在传输过程中未被篡改。根据头部中指定的算法(例如HS256),使用私钥对头部和载荷进行签名。

二、深入理解JWT签名验证

1. 签名生成

  1. 哈希处理 :首先对JWT的头部和载荷部分(经过Base64编码并用.连接的字符串)进行哈希处理,生成一个哈希值。这个步骤是为了确保数据的完整性,即使是微小的改动也会导致哈希值有很大的不同。
  2. 私钥加密哈希值 :然后使用发行者的私钥对这个哈希值进行加密,生成的结果就是JWT的签名部分。这个加密的哈希值(签名)附加在JWT的后面。

2. 签名验证

  1. 哈希处理 :接收方收到JWT后,会独立地对其头部和载荷部分进行同样的哈希处理,生成一个哈希值。
  2. 公钥解密签名 :接收方使用发行者的公钥对签名(加密的哈希值)进行解密,得到另一个哈希值。
  3. 哈希值比较比较这两个哈希值。如果它们相同,就证明了JWT在传输过程中未被篡改,因为只有相应的私钥能够生成一个对应的、能够通过公钥解密并得到原始哈希值的签名。

3. 为什么JWT是安全的

由于破解RSA算法的难度极高,没有私钥就无法生成有效的签名,因此无法伪造签名。这就是为什么使用公钥进行签名验证是安全的原因。

三、如何验证JWT是否有效

对于一个JWT,我们可以使用Base64解码,获取前两部分信息,可以进行token是否过期等验证,具体取决于前两部分具体内容,这是可以人为设置的。
对于签名部分,可以用公钥去验证其有效性(即是否被纂改)。

四、 Why JWT?

为什么是JWT?
首先是不可伪造的安全性,其次,因为你会发现,只要有公钥就可以验证JWT这种token,这也就意味着对于微服务来说,任意微服务都有能力自行验证JWT,而不需要额外的验证模块。这种自校验没有用到网络通信,性能十分好。同时,JWT有时间限制,一定程度上也提高了最坏情况的安全性。

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. 总结

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

SpringBoot3整合MyBatisPlus

SpringBoot3整合MyBatisPlus

一、起因

随着SpringBoot3的发布,mybatisplus也在不断更新以适配spirngboot3 。目前仍然处于维护升级阶段,最初2023.08时,官方宣布对SpringBoot3的原生支持,详情看这里

但是对于较新版本的SpringBoot3,仍然有很多bug,甚至无法启动,摸爬滚打又游历社区后,实践后得到一套成功的版本搭配,具体如下

Version
Java 17
Spring Boot 3.2.1
Spring Cloud 2023.0.0
Mybatis Plus 3.5.5

二、引入依赖

language-xml
1
2
3
4
5
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>3.5.5</version>
</dependency>

同时官方也提供了Demo项目,详情看这里,里面使用的版本搭配如下

Version
Java 17
Spring Boot 3.2.0
Mybatis Plus 3.5.5-SNAPSHOT
SpringBoot3整合OpenAPI3(Swagger3)

SpringBoot3整合OpenAPI3(Swagger3)

swagger2更新到3后,再使用方法上发生了很大的变化,名称也变为OpenAPI3

官方文档

一、引入依赖

language-xml
1
2
3
4
5
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>${springdoc-openapi.version}</version>
</dependency>
language-yml
1
2
3
4
5
6
7
8
9
10
server:
servlet:
context-path: /content
springdoc:
api-docs:
enabled: true
path: /v3/api-docs
swagger-ui:
enabled: true
path: /swagger-ui.html

openapi3使用十分方便,做到这里后,你可以直接通过以下网址访问swagger页面。

language-html
1
http://<ip>:<port>/content/swagger-ui/index.html

二、使用

1. @OpenAPIDefinition + @Info

用于定义整个 API 的信息,通常放在主应用类上。可以包括 API 的标题、描述、版本等信息。

language-java
1
2
3
4
5
6
7
8
9
10
@SpringBootApplication
@Slf4j
@OpenAPIDefinition(info = @Info(title = "内容管理系统", description = "对课程相关信息进行管理", version = "1.0.0"))
public class ContentApplication {

public static void main(String[] args) {

SpringApplication.run(ContentApplication.class, args);
}
}

2. @Tag

用于对 API 进行分组。可以在控制器类或方法级别上使用。

language-java
1
2
3
4
5
6
@Tag(name = "课程信息编辑接口")
@RestController("content")
public class CourseBaseInfoController {

}

3. @Operation

描述单个 API 操作(即一个请求映射方法)。可以提供操作的摘要、描述、标签等。

language-java
1
2
3
4
5
6
7
8
9
10
11
12
@Operation(summary = "课程查询接口")
@PostMapping("/course/list")
public PageResult<CourseBase> list(
PageParams params,
@RequestBody(required = false) QueryCourseParamsDto dto){


CourseBase courseBase = new CourseBase();
courseBase.setCreateDate(LocalDateTime.now());

return new PageResult<CourseBase>(new ArrayList<CourseBase>(List.of(courseBase)),20, 2, 10);
}

4. @Parameter

用于描述方法参数的额外信息,例如参数的描述、是否必需等。

language-java
1
2
3
4
5
6
7
8
9
10
11
12
@Operation(summary = "课程查询接口")
@PostMapping("/course/list")
public PageResult<CourseBase> list(
@Parameter(description = "分页参数") PageParams params,
@Parameter(description = "请求具体内容") @RequestBody(required = false) QueryCourseParamsDto dto){


CourseBase courseBase = new CourseBase();
courseBase.setCreateDate(LocalDateTime.now());

return new PageResult<CourseBase>(new ArrayList<CourseBase>(List.of(courseBase)),20, 2, 10);
}

5. @Schema

描述模型的结构。可以用于类级别(标注在模型类上)或字段级别。

language-java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PageParams {

//当前页码
@Schema(description = "页码")
private Long pageNo = 1L;

//每页记录数默认值
@Schema(description = "每页条目数量")
private Long pageSize =10L;
}

6. @ApiResponse

描述 API 响应的预期结果。可以指定状态码、描述以及返回类型。

language-java
1
2
3
4
@ApiResponse(responseCode = "200", description = "Successfully retrieved user")
public User getUserById(@PathVariable Long id) {

}
云服务器Docker部署SpringBoot+Vue前后端(Ubuntu)

云服务器Docker部署SpringBoot+Vue前后端(Ubuntu)

本文创作环境
华为云Ubuntu22.04
需要对以下知识具备一定了解和经验

  • Linux和Docker使用基础
  • Vue基本使用
  • SpringBoot基本使用

一、起手式-环境配置

1.远程服务器免密

在远程服务器执行ssh-keygen -t rsa,得到如下三个文件

language-bash
1
2
3
4
5
root@hecs-295176:~# ls -lh .ssh/
total 12K
-rw------- 1 root root 570 Oct 21 15:14 authorized_keys
-rw------- 1 root root 2.6K Oct 21 15:09 id_rsa
-rw-r--r-- 1 root root 570 Oct 21 15:09 id_rsa.pub

id_rsa.pub内容复制到authorized_keys,然后将id_rsa下载到本地。
在VsCode使用Remote-SSH配置远程免密登录,例如

language-bash
1
2
3
4
5
Host huaweiYun
HostName xxx.xxx.xxx.xxx
User root
Port 22
IdentityFile "C:\Users\mumu\.ssh\id_rsa"

2.安装Docker

执行以下命令安装Docker并检查docker命令是否可以使用

language-bash
1
2
3
4
5
apt update
apt upgrade
apt install docker.io
docker ps -a
docker images -a

二、Vue前端部署

1.参考文件夹结构

文件先不用创建,按照下面结构先把文件夹创建出来
然后将你的Vue打包后的dist文件夹替换下面的dist文件夹(如果没有Vue的打包文件夹本人建议先在index.html随便写点东西等会看能不能访问)

language-html
1
2
3
4
5
6
7
8
9
10
11
12
13
/root
├── conf
│ └── nginx
│ ├── default.conf
│ └── nginx.conf
└── Vue
├── MyTest01
│ ├── dist
│ │ └── index.html
│ └── logs
│ ├── access.log
│ └── error.log
└── nginxDocker.sh

2.nginx

拉取nginx镜像并创建nginx容器

language-bash
1
2
docker pull nginx
docker run -itd nginx

把里面的两个配置文件复制到主机

language-bash
1
2
docker cp containerName:/etc/nginx/nginx.conf ~/conf/nginx/nginx.conf
docker cp containerName:/etc/nginx/conf.d/default.conf ~/conf/nginx/default.conf

编辑default.conf 文件,修改以下内容:

  • listen 80; 改为 listen 8080;,表示 nginx 容器监听 8080 端口。
  • root /usr/share/nginx/html; 改为 root /usr/share/nginx/dist;,表示 nginx容器的根目录为 /usr/share/nginx/dist
  • index index.html index.htm; 改为 index index.html;,表示 nginx 容器的默认首页为 index.html。

参考第一小步文件夹结构,把以下内容写入nginxDocker.sh

language-bash
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/bin/bash

containerName="Test01"
nginxConf="/root/conf/nginx/nginx.conf"
defaultConf="/root/conf/nginx/default.conf"
logsPath="/root/Vue/MyTest01/logs"
vuePath="/root/Vue/MyTest01/dist"

docker run -d --name "$containerName" \
-v "$nginxConf":/etc/nginx/nginx.conf \
-v "$defaultConf":/etc/nginx/conf.d/default.conf \
-v "$logsPath":/var/log/nginx \
-v "$vuePath":/usr/share/nginx/dist \
-p 8080:8080 \
nginx

命令行运行这个sh脚本并查看当前容器列表确认容器已经在运行中

language-bash
1
2
bash Vue/nginxDocker.sh
docker ps -a

3.开放8080端口

如果你使用的VsCode远程连接的服务器,那么可以先通过端口转发,在本地访问前端服务。
如果想要别人通过公网访问,需要去购买云服务器的平台,修改服务器的安全组配置,添加入站规则,开放8080端口。
之后使用IP+8080即可访问这个docker容器里的前端服务。

4.复盘

首先是更新便捷性,使用-v挂载文件到容器,我们可以直接修改主机的dist文件夹内容而不必对容器做任何操作,前端服务就可以自动update,其它-v挂载的文件都可以在主机直接修改而不必连入容器中修改,同时重启容器即可一定保证所有服务重启。
其次是多开便捷性,以上流程就是一个包裹了前端服务的docker占一个端口,如果有多个Vue前端,使用不同端口即可。
总而言之,都是选择Docker容器化的优势所在。

三、SpringBoot后端部署

1.参考文件夹结构

将maven打包好的jar包如下图放入对应位置

language-bash
1
2
3
4
SpringBoot
├── javaDocker.sh
└── MyTest01
└── demo-0.0.1-SNAPSHOT.jar

2.openjdk17

以java17举例,拉取对应docker镜像(java8对应的镜像是java:8 )

language-bash
1
docker pull openjdk:17

如下编写javaDocker.sh脚本

language-bash
1
2
3
4
5
6
7
8
9
#!/bin/bash

containerName="JavaTest01"
SpringBootPath="/root/SpringBoot/MyTest01/demo-0.0.1-SNAPSHOT.jar"

docker run -d --name "$containerName" \
-p 8081:8081 \
-v "$SpringBootPath":/app/your-app.jar \
openjdk:17 java -jar /app/your-app.jar

命令行运行这个sh脚本并查看当前容器列表确认容器已经在运行中

language-bash
1
2
bash SpringBoot/javaDocker.sh
docker ps -a

3.开放8081端口

需要去购买云服务器的平台,修改服务器的安全组配置,添加入站规则,开放8081端口。
打开浏览器,输入IP:8081然后跟上一些你在程序中写的api路径,验证是否有返回。

四、Vue->Axios->SpringBoot前后端通信简单实现

vue这里使用ts + setup +组合式 语法举例,前端代码如下
意思是向IP为xxx.xxx.xxx.xxx的云服务器的8081端口服务发送路径为/Home/Kmo的请求

language-html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<template>
<div>
Your Remote JavaDocker State : {
{ line }}
</div>
</template>

<script setup lang="ts">
import {
ref } from "vue";
import axios from "axios";
const line = ref("fail");
axios.get("http://xxx.xxx.xxx.xxx:8081/Home/Kmo").then(rp=>{

line.value = rp.data
})
</script>


<style scoped>
</style>

SpringBoot后端写一个简单Controller类,代码如下

language-java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.kmo.demo.controller;

import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@CrossOrigin(originPatterns = "*", allowCredentials = "true")
@RestController
@RequestMapping("Home")
public class TestController {


@GetMapping("/Kmo")
public String test(){

return "Success!";
}

}

分别打包放到云服务指定文件夹,然后restart重启两个docker容器即可,在本地浏览器访问IP:8080看看效果吧。

(完)

JavaWeb MyBatisPlus添加一个能够批量插入的Mapper通用方法

JavaWeb MyBatisPlus添加一个能够批量插入的Mapper通用方法

众所周知,mybatisplus提供的BaseMapper里只有单条插入的方法,没有批量插入的方法,
而在Service层的批量插入并不是真的批量插入,实际上是遍历insert,但也不是一次insert就一次IO,而是到一定数量才会去IO一次,性能不是很差,但也不够好。

怎么才能实现真正的批量插入呢?

这里是mybatisplus官方的演示仓库,可以先去了解一下。

一、注册自定义通用方法流程

  1. 把自定义方法写到BaseMapper,因为没法改BaseMapper,所以继承一下它
language-java
1
2
3
4
5
public interface MyBaseMapper<T> extends BaseMapper<T> {


int batchInsert(@Param("list") List<T> entityList);
}

MyBaseMapper扩展了原有的BaseMapper,所以你之后的Mapper层都继承自MyBaseMapper而不是BaseMapper即可。

  1. 把通用方法注册到mybatisplus
language-java
1
2
3
4
5
6
7
8
9
10
11
@Component
public class MySqlInjector extends DefaultSqlInjector {

@Override
public List<AbstractMethod> getMethodList(Class<?> mapperClass, TableInfo tableInfo) {

List<AbstractMethod> defaultMethodList = super.getMethodList(mapperClass, tableInfo);
defaultMethodList.add(new BatchInsert("batchInsert"));
return defaultMethodList;
}
}

关键的一句在于defaultMethodList.add(new BatchInsert("batchInsert"));,意为注册一个新的方法叫batchInsert,具体实现在BatchInsert类。

  1. 实现BatchInsert类
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
public class BatchInsert extends AbstractMethod {

public BatchInsert(String name) {

super(name);
}

@Override
public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {

KeyGenerator keyGenerator = NoKeyGenerator.INSTANCE;
SqlMethod sqlMethod = SqlMethod.INSERT_ONE;
String columnScript = getAllInsertSqlColumn(tableInfo.getFieldList());
String valuesScript = SqlScriptUtils.convertForeach(LEFT_BRACKET + getAllInsertSqlProperty("item.", tableInfo.getFieldList()) + RIGHT_BRACKET,
LIST, null, "item", COMMA);
String keyProperty = null;
String keyColumn = null;
// 表包含主键处理逻辑,如果不包含主键当普通字段处理
if (StringUtils.isNotBlank(tableInfo.getKeyProperty())) {

if (tableInfo.getIdType() == IdType.AUTO) {

/* 自增主键 */
keyGenerator = Jdbc3KeyGenerator.INSTANCE;
keyProperty = tableInfo.getKeyProperty();
// 去除转义符
keyColumn = SqlInjectionUtils.removeEscapeCharacter(tableInfo.getKeyColumn());
} else if (null != tableInfo.getKeySequence()) {

keyGenerator = TableInfoHelper.genKeyGenerator(methodName, tableInfo, builderAssistant);
keyProperty = tableInfo.getKeyProperty();
keyColumn = tableInfo.getKeyColumn();
}
}
String sql = String.format(sqlMethod.getSql(), tableInfo.getTableName(), columnScript, valuesScript);
SqlSource sqlSource = super.createSqlSource(configuration, sql, modelClass);
return this.addInsertMappedStatement(mapperClass, modelClass, methodName, sqlSource, keyGenerator, keyProperty, keyColumn);
}

/**
* 获取 insert 时所有列名组成的sql片段
* @param fieldList 表字段信息列表
* @return sql 脚本片段
*/
private String getAllInsertSqlColumn(List<TableFieldInfo> fieldList) {

return LEFT_BRACKET + fieldList.stream()
.map(TableFieldInfo::getColumn).filter(Objects::nonNull).collect(joining(COMMA + NEWLINE)) + RIGHT_BRACKET;
}

/**
* 获取 insert 时所有属性值组成的sql片段
* @param prefix 前缀
* @param fieldList 表字段信息列表
* @return sql 脚本片段
*/
private String getAllInsertSqlProperty(final String prefix, List<TableFieldInfo> fieldList) {

final String newPrefix = prefix == null ? EMPTY : prefix;
return fieldList.stream()
.map(i -> i.getInsertSqlProperty(newPrefix).replace(",", ""))
.filter(Objects::nonNull)
.collect(joining(COMMA + NEWLINE));
}
}

二、BatchInsert具体实现逻辑解析

以如下简单的表user举例,

列名 描述 类型
id 主键,自增 bigint
user_name 用户名 varchar(64)
user_age 用户年龄 int

对于的entity大抵如下

language-java
1
2
3
4
5
6
7
8
9
10
11
@Data
@EqualsAndHashCode(callSuper = true)
@Accessors(chain = true)
@TableName("user")
public class User{

@TableId(value = "id", type = IdType.AUTO)
private Long id;
private String userName;
private int userAge;
}

那么对于batchInsert,我们希望传入List<User>并希望得到类似如下的mybtaisplus xml sql语句

language-xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<insert id="batchInsert" parameterType="java.util.List">
insert into user(
id,
user_name,
user_age
)values
<foreach collection="list" item="item" separator=",">
(
#{item.id},
#{item.userName},
#{item.userAge}
)
</foreach>
</insert>

但是我们并不自己写这个xml,不然这需要对每一个数据表都要写一个,就像不那么硬的硬代码一样,我们希望有段逻辑,只需要传入entity,就能自己解析其中列名和对应的属性名,生成这段xml实现批量插入的功能。

假设你的UserMapper已经继承自MyBaseMapper,如果调用UserMapper.bacthInsert(List<User> entityList),那么会进入这个函数

language-java
1
2
@Override
public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo)

其中mapperClass是映射器类,modelClass是模型类,我们并不需要了解,最主要的是tableInfo,这是表信息,它包含了关于数据库表的各种信息,如表名、列名、主键等。这个参数提供了详细的表信息,这对于生成针对特定表的SQL语句是必要的。

然后执行如下

language-java
1
2
3
4
5
6
7
//如果你的表名没有主键,那么你需要指定keyGenerator 为NoKeyGenerator,
//因为重写injectMappedStatement最后需要返回return this.addInsertMappedStatement
//其中就需要KeyGenerator
KeyGenerator keyGenerator = NoKeyGenerator.INSTANCE;
//SqlMethod.INSERT_ONE就是"INSERT INTO %s %s VALUES %s"
//我们依据表的信息生成列名sql片段和属性名sql片段后填入%s就可以得到近似最后的xml sql
SqlMethod sqlMethod = SqlMethod.INSERT_ONE;

然后执行如下

language-java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//tableInfo.getFieldList()会得到一个包含数据表列信息(不包含主键)的TableFieldInfo类组成的List
String columnScript = getAllInsertSqlColumn(tableInfo.getFieldList());
//这行代码就是在调用这个函数
private String getAllInsertSqlColumn(List<TableFieldInfo> fieldList) {

return LEFT_BRACKET + fieldList.stream()
//从TableFieldInfo中只拿取列名
.map(TableFieldInfo::getColumn)
//过滤null
.filter(Objects::nonNull)
//在元素间以逗号和分行分割
.collect(joining(COMMA + NEWLINE)) + RIGHT_BRACKET;
}
//对于User表,这个函数返回以下String
/*
(user_name,
user_age)
*/

然后执行如下

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
//首先调用了getAllInsertSqlProperty
String valuesScript = SqlScriptUtils.convertForeach(
// 这也是个内置函数,可以直接去看看
LEFT_BRACKET + getAllInsertSqlProperty("item.", tableInfo.getFieldList()) + RIGHT_BRACKET,
LIST,
null,
"item",
COMMA
);
//LEFT_BRACKET + getAllInsertSqlProperty("item.", tableInfo.getFieldList()) + RIGHT_BRACKET
//得到
/*
(#{userName},
#{userAge})
*/
//经过convertForeach函数后,得到如下字符串
/*
<foreach collection="list" item="item" separator=",">
(#{userName},
#{userAge})
</foreach>
*/

//getAllInsertSqlProperty函数如下
private String getAllInsertSqlProperty(final String prefix, List<TableFieldInfo> fieldList) {

//这里newPrefix 就是"item."
final String newPrefix = prefix == null ? EMPTY : prefix;
return fieldList.stream()
//i.getInsertSqlProperty("item.")是内置函数,假设i现在遍历到了user_name列
//那么得到的就是"#{userName},"
//然后,被删了
//所以本来每个元素从TableFieldInfo变成了形如"#{userName}"的字符串
.map(i -> i.getInsertSqlProperty(newPrefix).replace(",", ""))
.filter(Objects::nonNull)
//在元素间插入逗号和分行
.collect(joining(COMMA + NEWLINE));
}
//对于User表,这个函数返回以下String
/*
#{userName},
#{userAge}
*/

然后执行如下

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
		//定义主键属性名
String keyProperty = null;
//定义主键列名
String keyColumn = null;
// 表包含主键处理逻辑,如果不包含主键当普通字段处理
if (StringUtils.isNotBlank(tableInfo.getKeyProperty())) {

if (tableInfo.getIdType() == IdType.AUTO) {

/* 自增主键 */
keyGenerator = Jdbc3KeyGenerator.INSTANCE;
keyProperty = tableInfo.getKeyProperty();
// 去除转义符
keyColumn = SqlInjectionUtils.removeEscapeCharacter(tableInfo.getKeyColumn());
} else if (null != tableInfo.getKeySequence()) {

keyGenerator = TableInfoHelper.genKeyGenerator(methodName, tableInfo, builderAssistant);
keyProperty = tableInfo.getKeyProperty();
keyColumn = tableInfo.getKeyColumn();
}
}
//这段代码没什么好说的,就是根据不同情况,得到三个变量
//keyGenerator keyProperty keyColumn

然后执行如下

language-java
1
2
3
4
5
6
7
8
9
String sql = String.format(sqlMethod.getSql(), tableInfo.getTableName(), columnScript, valuesScript);
//就是把表名user,列名片段,属性名片段,填入%s中,得到如下
/*
INSERT INTO user (user_name,
user_age) VALUES <foreach collection="list" item="item" separator=",">
(#{userName},
#{userAge})
</foreach>
*/

然后执行如下

language-java
1
2
3
4
//这两句没有什么可说的,是重写injectMappedStatement函数的必要的操作
//自定义的内容就在于sql和主键
SqlSource sqlSource = super.createSqlSource(configuration, sql, modelClass);
return this.addInsertMappedStatement(mapperClass, modelClass, methodName, sqlSource, keyGenerator, keyProperty, keyColumn);