SpringBoot基于哨兵模式的Redis(7.2)集群实现读写分离

SpringBoot基于哨兵模式的Redis(7.2)集群实现读写分离

环境

  • docker desktop for windows 4.23.0
  • redis 7.2
  • Idea

一、前提条件

先根据以下文章搭建一个Redis集群

部署完后,redis集群看起来大致如下图

二、SpringBoot访问Redis集群

1. 引入依赖

需要注意的是lettuce-core版本问题,不能太旧,否则不兼容新版的Redis

language-xml
1
2
3
4
5
6
7
8
9
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
<version>6.1.4.RELEASE</version> <!-- 或更高版本 -->
</dependency>

2. yaml配置

application.yml加入以下配置。第一个password是用于sentinel节点验证,第二个password用于数据节点验证。

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

这里关于sentinelip问题后面会讲解。

3. 设置读写分离

在任意配置类中写一个Bean,本文简单起见,直接写在SpringBoot启动类了。

language-java
1
2
3
4
5
@Bean
public LettuceClientConfigurationBuilderCustomizer clientConfigurationBuilderCustomizer(){

return clientConfigurationBuilder -> clientConfigurationBuilder.readFrom(ReadFrom.REPLICA_PREFERRED);
}

这里的ReadFrom是配置Redis的读取策略,是一个枚举,包括下面选择:

  • MASTER:从主节点读取
  • MASTER_PREFERRED:优先从master节点读取,master不可用才读取replica
  • REPLICA:从slave (replica)节点读取
  • REPLICA_PREFERRED:优先从slave (replica)节点读取,所有的slave都不可用才读取master

至于哪些节点支持读,哪些支持写,因为redis 7 默认给从节点设置为只读,所以可以认为只有主节点有读写权限,其余只有读权限。如果情况不一致,就手动给每一个redis-server的配置文件都加上这一行。

language-txt
1
replica-read-only yes

4. 简单的controller

写一个简单的controller,等会用于测试。

language-java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@RestController
public class HelloController {


@Autowired
private StringRedisTemplate redisTemplate;

@GetMapping("/get/{key}")
public String hi(@PathVariable String key) {

return redisTemplate.opsForValue().get(key);
}

@GetMapping("/set/{key}/{value}")
public String hi(@PathVariable String key, @PathVariable String value) {

redisTemplate.opsForValue().set(key, value);
return "success";
}
}

三、运行

首先,因为所有redis节点都在一个docker bridge网络中,所以基于Idea编写的项目在宿主机(Windows)中运行spirngboot程序,不好去和redis集群做完整的交互。

虽然说无论是sentinel还是redis-server都暴露了端口到宿主机,我们可以通过映射的端口分别访问它们,但是我们的程序只访问sentinelsentinel管理redis-serversentinel会返回redis-serverip来让我们的程序来访问redis-server,这里的ipdocker bridge网络里的ip,所以即使我们的程序拿到ip也访问不了redis-server

这个时候就需要将我们的项目放到一个docker容器中运行,然后把这个容器放到和redis同一网络下,就像下图。

具体如何快捷让Idea结合Docker去运行SpringBoot程序,可以参考下面这篇文章。

记得要暴露你的程序端口到宿主机,这样才方便测试。

四、测试

1. 写

浏览器访问localhost:8080/set/num/7799

查看SpringBoot容器日志,可以看到向主节点172.30.1.2:6379发送写请求。

language-txt
1
2
3
4
5
6
7
8
9
10
11
01-06 07:23:59:848 DEBUG 1 --- [nio-8080-exec-6] io.lettuce.core.RedisChannelHandler      : dispatching command AsyncCommand [type=SET, output=StatusOutput [output=null, error='null'], commandType=io.lettuce.core.protocol.Command]
01-06 07:23:59:848 DEBUG 1 --- [nio-8080-exec-6] i.l.c.m.MasterReplicaConnectionProvider : getConnectionAsync(WRITE)
01-06 07:23:59:848 DEBUG 1 --- [nio-8080-exec-6] io.lettuce.core.RedisChannelHandler : dispatching command AsyncCommand [type=SET, output=StatusOutput [output=null, error='null'], commandType=io.lettuce.core.protocol.Command]
01-06 07:23:59:848 DEBUG 1 --- [nio-8080-exec-6] i.lettuce.core.protocol.DefaultEndpoint : [channel=0x9b4ebc85, /172.30.1.5:46700 -> /172.30.1.2:6379, epid=0xf] write() writeAndFlush command AsyncCommand [type=SET, output=StatusOutput [output=null, error='null'], commandType=io.lettuce.core.protocol.Command]
01-06 07:23:59:848 DEBUG 1 --- [nio-8080-exec-6] i.lettuce.core.protocol.DefaultEndpoint : [channel=0x9b4ebc85, /172.30.1.5:46700 -> /172.30.1.2:6379, epid=0xf] write() done
01-06 07:23:59:848 DEBUG 1 --- [oEventLoop-4-10] io.lettuce.core.protocol.CommandHandler : [channel=0x9b4ebc85, /172.30.1.5:46700 -> /172.30.1.2:6379, epid=0xf, chid=0x16] write(ctx, AsyncCommand [type=SET, output=StatusOutput [output=null, error='null'], commandType=io.lettuce.core.protocol.Command], promise)
01-06 07:23:59:849 DEBUG 1 --- [oEventLoop-4-10] io.lettuce.core.protocol.CommandEncoder : [channel=0x9b4ebc85, /172.30.1.5:46700 -> /172.30.1.2:6379] writing command AsyncCommand [type=SET, output=StatusOutput [output=null, error='null'], commandType=io.lettuce.core.protocol.Command]
01-06 07:23:59:851 DEBUG 1 --- [oEventLoop-4-10] io.lettuce.core.protocol.CommandHandler : [channel=0x9b4ebc85, /172.30.1.5:46700 -> /172.30.1.2:6379, epid=0xf, chid=0x16] Received: 5 bytes, 1 commands in the stack
01-06 07:23:59:851 DEBUG 1 --- [oEventLoop-4-10] io.lettuce.core.protocol.CommandHandler : [channel=0x9b4ebc85, /172.30.1.5:46700 -> /172.30.1.2:6379, epid=0xf, chid=0x16] Stack contains: 1 commands
01-06 07:23:59:851 DEBUG 1 --- [oEventLoop-4-10] i.l.core.protocol.RedisStateMachine : Decode done, empty stack: true
01-06 07:23:59:852 DEBUG 1 --- [oEventLoop-4-10] io.lettuce.core.protocol.CommandHandler : [channel=0x9b4ebc85, /172.30.1.5:46700 -> /172.30.1.2:6379, epid=0xf, chid=0x16] Completing command AsyncCommand [type=SET, output=StatusOutput [output=OK, error='null'], commandType=io.lettuce.core.protocol.Command]

2. 读

浏览器访问localhost:8080/get/num

查看SpringBoot容器日志,会向两个从节点之一发送读请求。

language-txt
1
2
3
4
5
6
7
8
9
10
11
01-06 07:25:45:342 DEBUG 1 --- [io-8080-exec-10] io.lettuce.core.RedisChannelHandler      : dispatching command AsyncCommand [type=GET, output=ValueOutput [output=null, error='null'], commandType=io.lettuce.core.protocol.Command]
01-06 07:25:45:342 DEBUG 1 --- [io-8080-exec-10] i.l.c.m.MasterReplicaConnectionProvider : getConnectionAsync(READ)
01-06 07:25:45:342 DEBUG 1 --- [io-8080-exec-10] io.lettuce.core.RedisChannelHandler : dispatching command AsyncCommand [type=GET, output=ValueOutput [output=null, error='null'], commandType=io.lettuce.core.protocol.Command]
01-06 07:25:45:342 DEBUG 1 --- [io-8080-exec-10] i.lettuce.core.protocol.DefaultEndpoint : [channel=0x96ae68cf, /172.30.1.5:38102 -> /172.30.1.4:6379, epid=0x1c] write() writeAndFlush command AsyncCommand [type=GET, output=ValueOutput [output=null, error='null'], commandType=io.lettuce.core.protocol.Command]
01-06 07:25:45:342 DEBUG 1 --- [io-8080-exec-10] i.lettuce.core.protocol.DefaultEndpoint : [channel=0x96ae68cf, /172.30.1.5:38102 -> /172.30.1.4:6379, epid=0x1c] write() done
01-06 07:25:45:342 DEBUG 1 --- [oEventLoop-4-11] io.lettuce.core.protocol.CommandHandler : [channel=0x96ae68cf, /172.30.1.5:38102 -> /172.30.1.4:6379, epid=0x1c, chid=0x23] write(ctx, AsyncCommand [type=GET, output=ValueOutput [output=null, error='null'], commandType=io.lettuce.core.protocol.Command], promise)
01-06 07:25:45:343 DEBUG 1 --- [oEventLoop-4-11] io.lettuce.core.protocol.CommandEncoder : [channel=0x96ae68cf, /172.30.1.5:38102 -> /172.30.1.4:6379] writing command AsyncCommand [type=GET, output=ValueOutput [output=null, error='null'], commandType=io.lettuce.core.protocol.Command]
01-06 07:25:45:346 DEBUG 1 --- [oEventLoop-4-11] io.lettuce.core.protocol.CommandHandler : [channel=0x96ae68cf, /172.30.1.5:38102 -> /172.30.1.4:6379, epid=0x1c, chid=0x23] Received: 10 bytes, 1 commands in the stack
01-06 07:25:45:346 DEBUG 1 --- [oEventLoop-4-11] io.lettuce.core.protocol.CommandHandler : [channel=0x96ae68cf, /172.30.1.5:38102 -> /172.30.1.4:6379, epid=0x1c, chid=0x23] Stack contains: 1 commands
01-06 07:25:45:346 DEBUG 1 --- [oEventLoop-4-11] i.l.core.protocol.RedisStateMachine : Decode done, empty stack: true
01-06 07:25:45:346 DEBUG 1 --- [oEventLoop-4-11] io.lettuce.core.protocol.CommandHandler : [channel=0x96ae68cf, /172.30.1.5:38102 -> /172.30.1.4:6379, epid=0x1c, chid=0x23] Completing command AsyncCommand [type=GET, output=ValueOutput [output=[B@7427ef47, error='null'], commandType=io.lettuce.core.protocol.Command]

3. 额外测试

以及还有一些额外的测试,可以自行去尝试,检验,这里列举一些,但具体不再赘述。

  1. 关闭两个从节点容器,等待sentinel完成维护和通知后,测试读数据和写数据会请求谁?
  2. 再次开启两个从节点,等待sentinel完成操作后,再关闭主节点,等待sentinel完成操作后,测试读数据和写数据会请求谁?
  3. 再次开启主节点,等待sentinel完成操作后,测试读数据和写数据会请求谁?
Idea连接Docker在本地(Windows)开发SpringBoot

Idea连接Docker在本地(Windows)开发SpringBoot

当一些需要的服务在docker容器中运行时,因为docker网络等种种原因,不得不把在idea开发的springboot项目放到docker容器中才能做测试或者运行。

1. 新建运行配置

2. 修改运行目标

3. 设置新目标Docker

推荐使用openjdk镜像即可,运行选项就是平时运行Docker的形参,--rm是指当容器停止时自动删除,-p暴露端口,一般都需要。包括--network指定网络有需要也可以加上。

等待idea自动执行完成,下一步

保持默认即可,创建。

4. 选择运行主类

根据自己的情况选择一个。

5. 运行

成功。

JavaWeb若依分页插件使用

JavaWeb若依分页插件使用

后端只需要在你本身的contoller调用service取得数据那条命令前后加上2行代码,就可以使用mybatis的分页插件。

language-java
1
2
3
4
5
6
7
8
@GetMapping("move/search")
public AjaxResult moveSearch(StockMoveSearchDTO dto){

startPage();//第一句
List<BztStockMoveHeadExp> list = stockMgntService.moveSearch(dto);//这是自己本身的逻辑代码
TableDataInfo dataTable = getDataTable(list);//第二句
return success(dataTable);
}

整体看起来,startPage像是加载了某些配置,然后基于这些配置getDataTable对你本来的逻辑进行了修改,实现了分页排序效果。

1.原理解析

ctrl左键点击startPage(),两次追溯源码实现,可以看到以下代码

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

PageDomain pageDomain = TableSupport.buildPageRequest();
Integer pageNum = pageDomain.getPageNum();
Integer pageSize = pageDomain.getPageSize();
String orderBy = SqlUtil.escapeOrderBySql(pageDomain.getOrderBy());
Boolean reasonable = pageDomain.getReasonable();
PageHelper.startPage(pageNum, pageSize, orderBy).setReasonable(reasonable);
}

PageHelper.startPage(pageNum, pageSize, orderBy)myabtis的分页插件,

  • 第一个参数是当前页码
  • 第二个参数是每一页数量
  • 第三个参数是排序信息,包括排序列和排序顺序

PageHelper会拦截你的sql代码然后加上类似limit ,order by等语句实现所谓的分页排序查询,数据截取是在sql中完成的,很快,所以你不必担心这个插件是不是在数据选完之后再分割导致性能问题。

那么问题来了,pageNum, pageSize, orderBy这三个参数从哪里来?毋庸置疑,肯定是从前端来,但是却不需要你写在dto中,因为若依会从你传到后端的数据中截取这三个参数。ctrl左键点击buildPageRequest(),追溯源码实现,你会看到以下代码。

language-java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static PageDomain getPageDomain()
{

PageDomain pageDomain = new PageDomain();
pageDomain.setPageNum(Convert.toInt(ServletUtils.getParameter(PAGE_NUM), 1));
pageDomain.setPageSize(Convert.toInt(ServletUtils.getParameter(PAGE_SIZE), 10));
pageDomain.setOrderByColumn(ServletUtils.getParameter(ORDER_BY_COLUMN));
pageDomain.setIsAsc(ServletUtils.getParameter(IS_ASC));
pageDomain.setReasonable(ServletUtils.getParameterToBool(REASONABLE));
return pageDomain;
}

public static PageDomain buildPageRequest()
{

return getPageDomain();
}

大概意思就是会从你传到后端的数据从截取以下字段

  • pageNum
  • pageSize
  • orderByColumn
  • isAsc
    orderby由后两个字段组成。所以在你前端传到后端的数据中,添加这4个字段和值即可实现分页效果和排序效果。

2. 前端示例

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
38
39
40
41
42
<template>
<el-table :data="tableList" @sort-change="handleSortChange">
<el-table-column prop="formCreateTime"
label="录入日期"
sortable="custom"
:sort-orders="['ascending', 'descending']"
>
<template #default="scope">
{
{ new Date(scope.row.formCreateTime).toLocaleDateString() }}
</template>
</el-table-column>
</template>

<script setup>
const queryParams = reactive({

pageNum: undefined,
pageSize: undefined,
orderByColumn: 'formCreateTime',
isAsc: 'descending',
})
const tableList = ref([])

const handleSortChange=(column)=>{

queryParams.orderByColumn = column.prop;
queryParams.isAsc = column.order;
onSubmitQuiet();
}
const onSubmitQuiet = () => {

queryParams.pageNum = 1;
queryParams.pageSize = 10;
moveSearch(queryParams).then(rp => {

// console.log(rp)
tableList.value = rp.data.rows;
pageTotal.value = rp.data.total;
})
}
</script>

这是一个简单的表格,只包含一列,当点击这一列表头时,触发handleSortChange,更新orderByColumnisAsc 然后重新从后端查询一遍,注意用Get方式。pageNumpageSize 可以通过el-pagination组件更新,这里就不写了。后端返回值包含rows表示具体数据是一个list,total表示数据量。需要注意的时传给后端的orderByColumn所代表的字符串(这里是formCreateTime)应该和后端对应domain对应的变量名一致。

3. 后端示例

language-java
1
2
3
4
5
6
7
8
@GetMapping("move/search")
public AjaxResult moveSearch(StockMoveSearchDTO dto){

startPage();
List<BztStockMoveHeadExp> list = stockMgntService.moveSearch(dto);
TableDataInfo dataTable = getDataTable(list);
return success(dataTable);
}

其中List<BztStockMoveHeadExp> list = stockMgntService.moveSearch(dto);只需按照原来的逻辑写就行,不需要考虑分页或者排序。

JavaWeb 自动编号器Utils,用于各种自增长的编号,单号等

JavaWeb 自动编号器Utils,用于各种自增长的编号,单号等

使用以下技术栈

  • springboot
  • mybatisplus
  • mysql

一、首先设计一张表

表名假设为auto_numbering

名称 数据类型 备注 描述
id bigint 主键
name varchar(64) 编号名称
prefix varchar(8) 前缀
inti_value bigint 初始值
current_value bigint 当前值
length int 编号长度(不包含前缀)

假设有两条数据

id name prefix inti_value current_value length
1 stock_input SI 0 2 8
2 product_code PC 0 5 8

第一条表示入库单编号,编号的规则是,前缀为SI,当前编号数为2,长度为8,那么下一次使用入库单号时,对应的current_value加1变为3,得到的单号应该是字符串"SI00000003" 。第二条表示产品编号,同理,对应的current_value加1变为6,应该得到字符串"PC00000006"

那么如何设计这样一个自动编号工具类呢?这个工具类的方法最好是static,这样可以直接得到自动编号器的返回值,同时又要去让数据表对应的current_value自增。

二、创建对应的Domain类、Mapper接口、编号名称枚举类

domain

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
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;

@Data
@EqualsAndHashCode(callSuper = true)
@Accessors(chain = true)
@TableName("auto_numbering")
public class AutoNumbering{


private static final long serialVersionUID = 1L;

/**
* 主键
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;

/**
* 编号名称
*/
private String name;

/**
* 前缀
*/
private String prefix;

/**
* 初始值
*/
private Long intiValue;

/**
* 当前值
*/
private Long currentValue;

/**
* 编号长度(不包含前缀)
*/
private Integer length;

}

mapper

language-java
1
2
3
4
5
6
7
8
9
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface AutoNumberingMapper extends BaseMapper<AutoNumbering> {


}

enum

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
public enum AutoNumberingEnum {


STOCK_INPUT("stock_input", "入库编号"),
PRODUCT_CODE("product_code", "产品编号");

private final String name;
private final String remark;

AutoNumberingEnum(String name, String remark) {

this.name = name;
this.remark = remark;
}

/**
* 获取枚举名称
*
* @return name
*/
public String getName() {

return this.name;
}

/**
* 获取枚举描述
*
* @return remark
*/
public String getRemark() {

return this.remark;
}
}

三、写一个Service接口和实现类

service接口类

language-java
1
2
3
4
5
6
7
8
9
10
public interface AutoNumberingService {


/**
* 获取下一个编号
* @param param 自动编号枚举值
* @return 自动编号字符串
*/
String getNextNo(AutoNumberingEnum param);
}

serviceimpl

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
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.springframework.beans.factory.annotation.Autowired;
import java.io.*;
import java.util.ArrayList;
import java.util.List;

@Slf4j
@Service
public class AutoNumberingServiceImpl implements AutoNumberingService {


@Autowired
private AutoNumberingMapper autoNumberingMapper ;

/**
* 自动编号方法
*
* @param param 自动编号枚举值
* @return 编号
*/
@Transactional
public String getNextNo(AutoNumberingEnum param) {


// 查找当前的采番配置信息
LambdaQueryWrapper<AutoNumbering> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(AutoNumbering::getName, param.getName());
List<AutoNumbering> AutoNumberings = AutoNumberingMapper.selectList(wrapper);
// 未获取到配置信息
if (AutoNumberings.isEmpty()) {

return null;
} else {

// 规则获取
AutoNumbering autoNumbering = AutoNumberings.get(0);
// 前缀
String prefix = autoNumbering.getPrefix();
// 长度
Integer length = autoNumbering.getLength();
// 顺番
Long currentNo = autoNumbering.getCurrentValue();
// 更新原数据
currentNo++;
}
autoNumbering.setCurrentValue(currentNo);
AutoNumberingMapper.updateById(autoNumbering);

// 生成编号
String formatNo = StringUtils.leftPad(String.valueOf(currentNo), length, "0");
return String.format("%s%s", prefix, formatNo);
}
}

四、写一个Utils类静态获取编号

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
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class NoGeneratorUtil {


private static AutoNumberingService autoNumberingService;

@Autowired
public void setAutoNumberingService(AutoNumberingService autoNumberingService) {

NoGeneratorUtil.autoNumberingService = autoNumberingService;
}

/**
* 获取最新的入库单编号
* @return 最新的入库单编号
*/
public static String getNextStockInputNo() {

return autoNumberingService.getNextNo(AutoNumberingEnum.STOCK_INPUT);
}

/**
* 获取最新的产品编号
* @return 最新的c'p 编号
*/
public static String getNextProductNo() {

return autoNumberingService.getNextNo(AutoNumberingEnum.PRODUCT_CODE);
}

这段代码是SpringBoot框架中的一种常见用法,用于将SpringBoot管理的bean注入到静态变量中。那么,autoNumberingService是如何被注入的呢?实际上,当SpringBoot启动时,它会扫描所有的类,并为带有@Component(或者@Service等)注解的类创建实例。在创建NoGeneratorUtil实例的过程中,SpringBoot会调用所有带有@Autowired注解的setter方法,包括setAutoNumberingService方法,从而将AutoNumberingService的实现类注入到autoNumberingService静态变量中。

五、使用

之后如下使用即可一行代码获取最新编号

language-java
1
2
String string1 = NoGeneratorUtil.getNextStockInputNo();
String string2 = NoGeneratorUtil.getNextProductNo();
JavaWeb 若依RuoYi-Vue3框架将Mybatis切换成MybatisPlus

JavaWeb 若依RuoYi-Vue3框架将Mybatis切换成MybatisPlus

这里是官方的做法

mybatisSqlSessionFactoryBean,而mybatis-plusMybatisSqlSessionFactoryBean,所以一般最好是项目中使用一个最好,当然想要共存也可以,mybatis-plus的版本最好要高。这里只讲如何切换成MyBatisPlus。

一、修改yml

application.yml中将mybatis配置注释并写上新的mybatisplus,如下所示

language-yaml
1
2
3
4
5
6
7
8
9
10
11
12
13
## MyBatis配置
#mybatis:
# # 搜索指定包别名
# typeAliasesPackage: com.ruoyi.**.domain
# # 配置mapper的扫描,找到所有的mapper.xml映射文件
# mapperLocations: classpath*:mapper/**/*Mapper.xml
# # 加载全局的配置文件
# configLocation: classpath:mybatis/mybatis-config.xml
mybatis-plus:
# 配置要扫描的xml文件目录,classpath* 代表所有模块的resources目录 classpath 不加星号代表当前模块下的resources目录
mapper-locations: classpath*:mapper/**/*Mapper.xml
# 实体扫描,*通配符
typeAliasesPackage: com.ruoyi.**.domain

二、maven导入mybatisplus包,如下

language-xml
1
2
3
4
5
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>最新版本</version>
</dependency>

三、注释SqlSessionFactoryBean

找到com.ruoyi.framework.config.MyBatisConfig,注释public SqlSessionFactory sqlSessionFactory(DataSource dataSource)如下

language-java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//    @Bean
// public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception
// {

// String typeAliasesPackage = env.getProperty("mybatis.typeAliasesPackage");
// String mapperLocations = env.getProperty("mybatis.mapperLocations");
// String configLocation = env.getProperty("mybatis.configLocation");
// typeAliasesPackage = setTypeAliasesPackage(typeAliasesPackage);
// VFS.addImplClass(SpringBootVFS.class);
//
// final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
// sessionFactory.setDataSource(dataSource);
// sessionFactory.setTypeAliasesPackage(typeAliasesPackage);
// sessionFactory.setMapperLocations(resolveMapperLocations(StringUtils.split(mapperLocations, ",")));
// sessionFactory.setConfigLocation(new DefaultResourceLoader().getResource(configLocation));
// return sessionFactory.getObject();
// }

到这里就完成了切换,快去试试 吧。

资料参考
若依框架集成mybatis换成mybatis-plus记录

JavaWeb el-upload图片上传SpringBoot保存并且前端使用img src直接访问(基于RuoYi-Vue3)

JavaWeb el-upload图片上传SpringBoot保存并且前端使用img src直接访问(基于RuoYi-Vue3)

一、Vue前端

language-html
1
2
3
4
5
6
7
8
9
10
11
12
<el-upload
v-model:file-list="createParams.fileList"
action="http://localhost:8080/DataMgt/uploadPic"
:headers="{ Authorization: 'Bearer ' + getToken() }"
list-type="picture-card"
:on-success="handleSuccess"
:before-upload="handlePicBeforeUpload"
:on-preview="handlePictureCardPreview"
:on-remove="handleRemove"
>
<el-icon><Plus /></el-icon>
</el-upload>

需要注意的是action填写后端api路径,同时header需要填写token不然会被拦截,其余的没有影响可以翻阅文档理解。

二、SpringBoot后端

language-java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@RestController
@RequestMapping("/DataMgt")
public class Home extends BaseController {

private static String picDir = "C:\\Users\\mumu\\Desktop\\images\\";

@RequestMapping("/uploadPic")
public AjaxResult uploadPic(MultipartFile file) throws IOException {

String filePath = picDir+ UUID.randomUUID().toString() + file.getOriginalFilename().substring(file.getOriginalFilename().lastIndexOf('.'));
File saveF = new File(filePath);
logger.info("save pic:"+filePath);
file.transferTo(saveF);
return success(filePath);
}
}

picDir填写的是图片保存到哪个文件夹路径,无论你是windows开发环境还是linux生产环境,这个路径都应该是绝对路径。
现在图片保存到了这个路径,那么我们前端imgsrc又如何才能经由后端直接访问图片呢?

第一,需要通过SpringBoot的@Configuration配置一个映射,如下

language-java
1
2
3
4
5
6
7
8
9
10
@Configuration
public class StaticConfig implements WebMvcConfigurer {

@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {

registry.addResourceHandler("/images/**")
.addResourceLocations("file:C:\\Users\\mumu\\Desktop\\images\\");
}
}

意思是将C:\\Users\\mumu\\Desktop\\images\\路径映射到/images/,那么当你访问http://localhost:8080/images/1.png时其实就是在访问C:\\Users\\mumu\\Desktop\\images\\1.png

第二,需要在若依的com.ruoyi.framework.config.SecurityConfigconfigure函数中中配置访问权限,将/images/添加其中,对所有人开放,如下

language-java
1
.antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**", "/images/**").permitAll()

最后,在前端,像这样就可以经由后端访问图片啦。

language-html
1
<el-image src="http://localhost:8080/images/1.png" style="width: 200px;height: 200px"></el-image>

三、延伸

这是多图片上传组件,但其实每张都会和后端交互一次。

如果这个图片墙和文字搭配在一起成为一个图文动态发布页面,那么创建主副数据表,主表保存idtext表示id和文字,副表保存master_idname,表示主表id,图片名。
当新建动态时,在前端即刻生成当前时间作为图文动态的唯一标识。
当上传图片时,以唯一标识作为master_id并以保存的名称作为name插入副表。
当动态提交时,以唯一标识作为id并以文本内容作为text插入主表。
当取消时,以唯一标识为查询条件删除主副表相关数据和图片资源。

JavaWeb Springboot后端接收前端Date变量

JavaWeb Springboot后端接收前端Date变量

如果前端传输的日期符合ISO 8601格式,那么后端用@PostMapping + @RequestBody那么springboot就会自动解析。

如果传输的日期格式不符合ISO 8601格式或者传输用的是Get方法,那么在接收传输的实体类中加上@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss"),这里面的pattern对应传来的日期格式。

language-html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
ISO 8601是一种国际标准的日期和时间表示方式。这种格式的主要特点包括:

- 时间日期按照年月日时分秒的顺序排列,大时间单位在小时间单位之前。
- 每个时间单位的位数固定,不足时于左补0。
- 提供两种方法来表示时间:其一为只有数字的基础格式;其二为添加分隔符的扩展格式,让人能更容易阅读。

具体来说,ISO 8601的日期和时间表示方式的格式为`YYYY-MM-DDTHH:mm:ss.sssZ`,其中:

- `YYYY`代表四位数年份。
- `MM`代表月份。
- `DD`代表天数。
- `T`作为日期和时间的分隔符。
- `HH`代表小时。
- `mm`代表分钟。
- `ss.sss`代表秒和毫秒。
- `Z`代表的是时区。

例如,

"2023年11月15日06时16分50秒"可以表示为"2023-11-15T06:16:50"。
"2023-11-15"是ISO 8601日期格式的一个子集。在ISO 8601中,日期可以表示为"YYYY-MM-DD"的格式。因此,"2023-11-15"是符合ISO 8601日期格式的。

JavaWeb后端解析前端传输的excel文件(SpringBoot+Vue3)

JavaWeb后端解析前端传输的excel文件(SpringBoot+Vue3)

一、前端

前端拿Vue3+ElementPlus做上传示例

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
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
<template>
<el-button type="primary" @click="updateData" size="small" plain>数据上传</el-button>
<el-dialog :title="upload.title" v-model="upload.open" width="400px" align-center append-to-body>
<el-upload
ref="uploadRef"
:limit="1"
accept=".xlsx, .xls"
:headers="upload.headers"
:action="upload.url"
:disabled="upload.isUploading"
:on-progress="handleFileUploadProgress"
:on-success="handleFileSuccess"
:auto-upload="false"
drag
>
<el-icon class="el-icon--upload">
<upload-filled/>
</el-icon>
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
<template #tip>
<div class="el-upload__tip text-center">
<span>仅允许导入xls、xlsx格式文件。</span>
</div>
</template>
</el-upload>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="submitFileForm">确 定</el-button>
<el-button @click="upload.open = false">取 消</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup>

const upload = reactive({

// 是否显示弹出层(用户导入)
open: false,
// 弹出层标题(用户导入)
title: "标题",
// 是否禁用上传
isUploading: false,
// 是否更新已经存在的用户数据
updateSupport: 0,
// 设置上传的请求头部
headers: {
Authorization: "Bearer " + getToken()},
// 上传的地址
url: import.meta.env.VITE_APP_BASE_API + "/yourControllerApi"
});

//数据上传
const updateData = () => {

upload.title = "数据上传";
upload.open = true;
}

//文件上传中处理
const handleFileUploadProgress = (event, file, fileList) => {

upload.isUploading = true;
console.log(upload.url)
};

//文件上传成功处理
const handleFileSuccess = (rp, file, fileList) => {

//这里的rp就是后端controller的响应数据
console.log(rp)
upload.open = false;
upload.isUploading = false;
proxy.$refs["uploadRef"].handleRemove(file);
};
</script>

二、后端

  1. 导入依赖
language-xml
1
2
3
4
5
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>4.1.2</version>
</dependency>
  1. controller接收file
language-java
1
2
3
4
5
@PostMapping("/yourControllerApi")
public AjaxResult importData(@RequestBody MultipartFile file){

return stockMgntService.importData(file);
}
  1. service具体实现
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
@Service
public class StockMgntServiceImpl implements StockMgntService {

@Override
public AjaxResult importData(MultipartFile file) {

try(InputStream is = file.getInputStream();) {

Workbook workbook = WorkbookFactory.create(is);
int numberOfSheets = workbook.getNumberOfSheets();
if (numberOfSheets != 1){

//要处理多个sheet,循环就可以
System.out.println("只允许有1个Sheet");
return null;
}
//取得第一个sheet
Sheet sheet = workbook.getSheetAt(0);
//获取最后一行的索引,但通常会比预想的要多出几行。
int lastRowNum = sheet.getLastRowNum();
//循环读取每一行
for (int rowNum = 0; rowNum < lastRowNum; rowNum++) {

Row rowData = sheet.getRow(rowNum);
if (rowData != null){

//可以使用rowData.getLastCellNum()获取最后一列的索引,这里只有6行,是假设现在的excel模板是固定的6行
for(int cellNum = 0; cellNum < 6; ++cellNum){

Cell cell = rowData.getCell(cellNum);
if (cell == null){
continue;}
cell.setCellType(CellType.STRING);
if (!cell.getStringCellValue().isEmpty()){

System.out.println(cell.getStringCellValue());
}
/*
利用这个方法可以更好的将所有数据以String获取到
Cell cell = productCodeRow.getCell(cellNum , Row.MissingCellPolicy.RETURN_BLANK_AS_NULL);
String cellValue = dataFormatter.formatCellValue(cell );
*/
}
}
}
}catch (Exception e) {

System.out.println(e.getMessage());
throw new RuntimeException();
}
}
}

三、结语

这样的方式只能处理简单的excel,对于结构更复杂的excel,需要其他工具utils结合。
以及可以结合SpringBoot有更好的poi使用方法。

JavaWeb后端将excel文件传输到前端浏览器下载(SpringBoot+Vue3)

JavaWeb后端将excel文件传输到前端浏览器下载(SpringBoot+Vue3)

一、后端

  1. 导入依赖
language-xml
1
2
3
4
5
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>4.1.2</version>
</dependency>
  1. controller层
    其中@RequestParam String moveNo是前端传入的参数,而HttpServletResponse response不是,前者可以没有,后者一定要有。
language-java
1
2
3
4
5
6
7
@GetMapping("/yourControllerApi")
public AjaxResult exportData(@RequestParam String moveNo, HttpServletResponse response){

System.out.println(moveNo);
System.out.println(response);
return stockMgntService.exportData(moveNo, response);
}
  1. service层
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
@Service
public class StockMgntServiceImpl implements StockMgntService {

@Override
public AjaxResult exportData(String moveNo, HttpServletResponse response) {

//这里是从后台resources文件夹下读取一个excel模板
ClassPathResource resource = new ClassPathResource("template/模板.xlsx");
try(InputStream fis = resource.getInputStream();
Workbook workbook = WorkbookFactory.create(fis);
ServletOutputStream os = response.getOutputStream()
){

//这块写入你的数据
//往第一个sheet的第一行的第2列和第5列写入数据
Sheet sheet = workbook.getSheetAt(0);
sheet.getRow(0).getCell(1, Row.MissingCellPolicy.CREATE_NULL_AS_BLANK).setCellValue("test");
sheet.getRow(0).getCell(5, Row.MissingCellPolicy.CREATE_NULL_AS_BLANK).setCellValue("test");
/*......你的操作......*/
//这块开始配置传输
response.setCharacterEncoding("UTF-8");
response.setContentType("application/vnd.ms-excel");
response.setHeader("Content-disposition", "attachment;filename=template.xlsx");
response.flushBuffer();
workbook.write(os);
os.flush();
os.close();
}catch (IOException e) {

throw new RuntimeException(e);
}
return null;
}
}

二、前端

axios向对应api发送请求并携带参数,然后使用Blob来接收后端的OutputStream输出流并保存下载保存到本地浏览器。

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
<template>
<el-button type="primary" @click="downloadData" size="small" plain>模板下载</el-button>
</template>

<script setup>
const downloadData = () => {

console.log(queryParams.moveNo)
axios({

url: '/yourControllerApi',
method: 'get',
params: {
moveNo:'0000212132142'},
responseType: "blob"
}).then(rp=>{

console.log(rp)
const blob = new Blob([rp], {
type: 'application/vnd.ms-excel'});
let link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.setAttribute('download', '模板下载.xlsx');
link.click();
link = null;
});
}
</script>
云服务器Docker部署SpringBoot+Redis(Ubuntu)

云服务器Docker部署SpringBoot+Redis(Ubuntu)

参考文件夹结构

language-bash
1
2
3
4
5
6
7
MyTest01
├── javaDocker.sh
├── redisDocker.sh
├── Redis
│ └── data
├── SpringBoot
└── MyWeb01-SpringBoot-0.0.1-SNAPSHOT.jar

一、起手式:配置环境

1.镜像

拉取以下两个镜像

language-bash
1
2
docker pull openjdk:17
docker pull redis

二、启动Redis容器

redisDocker.sh脚本写入以下内容,然后bash运行
脚本意思是将redis数据映射到主机

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

containerName="RedisTest01"
RedisData="/root/MyTest01/Redis/data"

docker run -d --name "$containerName" \
-v "$RedisData":/data \
-p 6379:6379 \
redis

三、配置SpringBoot容器并简单测试

添加如下配置到application.yml

language-yaml
1
2
3
4
5
6
spring:
data:
redis:
host: redisdb
port: 6379
password:

写一个简单的测试如下

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
package com.example.myweb01springboot.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
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;

import javax.sql.DataSource;
import java.sql.SQLException;

@RestController
@RequestMapping("/Home")
@CrossOrigin(origins = "*", allowedHeaders = "*")
public class TestController {

@Autowired
private RedisTemplate<String, String> redisTemplate;

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

// 设置一个键值对
redisTemplate.opsForValue().set("name", "张三");

// 获取一个键值对
String name = redisTemplate.opsForValue().get("name");

System.out.println(name);

return "Success!"+name;
}
}

然后maven打包成jar,参考文件夹结构,将jar放入指定位置。
将以下内容写入javaDocker.sh脚本并bash运行
脚本意思是,向名为MySQLTest01的容器建立网络链接(单向的),它的名字(IP,主机名)为db,于是此容器可以通过db:3306访问MySQLTest01容器的mysql服务。

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

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

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

开放云服务器安全组入站规则8081端口,浏览器访问云服务器IP:8081/Home/Kmo验证。

(完)