Git多用户环境隔离提交

多用户仓库提交

背景

想要将开源项目和日常使用的两个账号区分开来,各自推送代码。

具体原理就是通过token结合远程仓库地址和本地用户信息配置完成git隔离。

步骤

  1. github个人页面选择setting->developer settings->personal access tokens创建两个账号各自的token,token记得赋予读写权限

  2. 修改工程的远程地址

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    # 查看一下当前仓库配置信息,主要是看下源仓库远程链接
    git config --local -l

    # 删除当前关联关系
    git remote rm origin

    # 关联新格式的远程仓库
    # 举个例子,假如我的仓库为https://github.com/luyublog/myrepo.git
    # 那么替换后的格式即为https://luyublog:token@github.com/luyublog/myrepo.git
    git remote add origin https://your_user_name:your_token@github.com/your_user_name/your_repo_name.git

    # 重设一下跟踪分支,这里我直接关联的main分支
    git branch --set-upstream-to=origin/main

    # 拉取一下查看是否成功
    git pull

遇到的问题

pull时报错:当前分支没有跟踪信息

这个错误信息表示当前分支没有和任何远程分支建立跟踪关系,所以Git不知道你想要从哪个远程分支pull更新,关联一个远程分支即可,这里我直接关联main分支

1
git branch --set-upstream-to=origin/main

一些常用命令

1
2
3
4
5
6
7
8
9
# 查看仓库的本地配置
git config --local -l

# 全局配置
git config --global -l

# 系统级配置
git config --system -l

linux上启用comfast无线网卡

背景

用u盘给电脑装上debian12后发现之前的comfast无线网卡无法正常工作,之后查看商品页面发现确实没写支持linux设备,所以只能试试自己收集驱动启动

环境

  • 系统: debian12
  • 无线网卡型号:comfast 926AC

步骤

1. 收集固件

这里建议先跟随这篇文章查询一下自己的网卡信息,如果有具体信息则可以不用往下看,直接按文章操作即可。

但我的ID是0e8d:2870,在网站上只有一行描述:”This is the emulated CD-ROM driver storage device that comes with some WiFi adapters.”

CD-ROM一般自动挂载在/media下,我检查后发现里面是.exe后缀的驱动安装程序,那就只能自行寻找驱动了。

通过谷歌获取到0e8d:2870对应的芯片是Mediatek MT7612U,通常应该加载的是mt76系列模块

随后通过命令安装对应模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 安装通用模块,这里如果找不到包需要额外添加non-free软件源
sudo apt install firmware-misc-nonfree

# 加载我需要的mt76模块 MT7612U 属于 MT76x2 系列设备。
sudo modprobe mt76x2u

# 查看模块是否成功加载
lsmod | grep mt76

# 查看设备识别信息
sudo dmesg | grep mt76

# 最关键的查看是否出现无线网卡
ip link show

我运行ip link show后并没有看到对应网卡出现,通过lsusb发现网卡还是被识别为”Bus 001 Device 011: ID 0e8d:2870 MediaTek Inc. Љ” 和之前看到的一样,还是只是一个CD-ROM设备,也就是说可能光有驱动还不行,还要从软件层面开启

2. 转换网卡形态

目前虽然有了对应驱动,但是网卡还是被识别为CD-ROM,通过CHATGPT了解到应该是因为作为免驱无线网卡,默认以CD-ROM模式启动,我们需要将其转换为网卡形态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 安装对应工具
sudo apt install usb-modeswitch

# 切换形态,以我为例,我的设备ID为0e8d:2870,那么我的切换命令为
sudo usb_modeswitch -v 0e8d -p 2870 -R

# 查看网卡信息,这时应该有一个新网卡出现
ip link show

# 再运行lsusb应该就会看到设备名称变更了 我的从"MediaTek Inc. Љ"变为"MediaTek Inc. MT7612U 802.11a/b/g/n/ac Wireless Adapter"
lsusb

# 也可以查看下设备识别日志里的相关信息
sudo dmesg | tail -n 50

3. 激活并连接wifi

网卡转换后可以插拔后运行”ip link show”来检查识别是否正常,正常的话就可以激活并连接wifi了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 激活对应网卡,这里的wln0是你运行ip link show对应的无线网卡名称
sudo ip link set wln0 up

# 安装网络管理工具,方便连接wifi
sudo apt install network-manager

# 查看可连接wifi列表
sudo nmcli dev wifi list

# 连接
sudo nmcli dev wifi connect "wifi名称" password "密码"

# 查看连接状态和网络接口信息:
nmcli connection show
nmcli dev status

win11安装双系统

背景

记录一下win11装双系统流程

参考

根据Windows10+Ubuntu18.04双系统简单安装指北跟随操作即可

步骤

  1. windows系统下找到目标盘压缩卷
  2. 使用rufus制作启动盘
  3. 进入bois选择u盘启动
  4. 安装系统,选择graphic install,分盘方式选新手推荐
  5. bois按需调整启动顺序

问题

无线网卡没网

一般的windows平台免驱无线网卡都不支持linux,改为网线连接后执行”sudo dhclient enp4s0”通过dhcp自动获取网络

尝试激活无线网卡

参考:linux启用comfast无线网卡

apt更新失败

默认源来自于iso镜像,换为网络源后再update

创建news2tg的docker镜像

创建镜像

1.创建dockerfile文件

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
# 使用多阶段构建
FROM rust:latest AS rust-builder
WORKDIR /usr/src/news2tg

# 安装git,protobuf-compiler等工具
RUN apt-get update && apt-get install -y git && apt-get install -y protobuf-compiler
RUN protoc --version

# 从GitHub克隆Rust仓库
RUN git clone https://github.com/cheedonghu/news2tg.git .
RUN cargo build --release

FROM python:3.11
WORKDIR /app

RUN apt-get update && apt-get install -y git

# 从GitHub克隆Python仓库
RUN git clone https://github.com/cheedonghu/hacker-news-digest.git .

# 安装Python依赖
RUN pip install --no-cache-dir -r ./page_content_extractor/requirements.txt

# 从Rust构建阶段复制编译好的二进制文件
COPY --from=rust-builder /usr/src/news2tg/target/release/news2tg /app/news2tg

# 创建配置文件目录
RUN mkdir /config

# 创建日志目录
RUN mkdir /logs

# 设置环境变量指向配置文件位置
ENV RUST_CONFIG_PATH=/config/config.toml

# 暴露端口(其实没必要暴露)
EXPOSE 50051

# 下载 wait-for-it.sh 脚本
RUN curl -o /usr/local/bin/wait-for-it.sh https://raw.githubusercontent.com/vishnubob/wait-for-it/master/wait-for-it.sh
RUN chmod +x /usr/local/bin/wait-for-it.sh

# 启动命令
CMD ["sh", "-c", "python -m page_content_extractor.main & wait-for-it.sh localhost:50051 -- ./news2tg -c $RUST_CONFIG_PATH >> /logs/news2tg.log 2>&1"]

2.构建镜像

1
2
# 最好在网络通畅环境下运行,避免网络问题
docker build -t news2tg .

3.(可选)推送到dockerhub

这里我build机器和运行的机器不一致所以需要推送后在另一台机器拉取镜像运行。

1
2
3
4
5
6
7
8
# 先登录
docker login

# 再给镜像加上标签
docker tag news2tg cheedonghu/news2tg:latest

# 推送镜像
docker push cheedonghu/news2tg:latest

使用镜像

步骤

1
2
3
4
5
6
7
8
9
10
11
# 1. 创建docker-compose.yml 按需复制下面的yml
nano docker-compose.yml

# 2. 创建挂载的文件夹
mkdir ./logs && mkdir ./config

# 3. 把配置文件放在config文件夹下
nano ./config/config.toml

# 4. 运行
docker compose up -d

使用本地构建的镜像

1
2
3
4
5
6
7
8
9
10
services:
news_app:
image: news2tg
volumes:
- ./config:/config
- ./logs:/config/logs
restart: unless-stopped
volumes:
config:
logs:

使用dockerhub上的镜像

1
2
3
4
5
6
7
8
9
10
services:
news_app:
image: cheedonghu/news2tg:latest
volumes:
- ./config:/config
- ./logs:/config/logs
restart: unless-stopped
volumes:
config:
logs:

其他方式

如果是本机直接跑则构建运行即可。

1
2
3
4
5
6
7
8
9
10
11
12
services:
news2tg:
build:
context: .
dockerfile: path/to/your/Dockerfile
volumes:
- ./config:/config
- ./logs:/config/logs
restart: unless-stopped
volumes:
config:
logs:

SpringAI简单使用

SpringAI

介绍

SpringAI

了解原因

  1. AI趋势
  2. Spring生态

特性

  1. 支持大量模型OpenAI, Microsoft, Amazon, Google, and Huggingface.
  2. 支持大量模型输出:问答,做图,语音处理等
  3. 方便使用的已封装API接口:用户交互api,模型交互api
  4. 支持大量的向量数据库及相关交互
  5. 支持模型函数调用
  6. SpringBoot自动配置支持
  7. ETL(Extract, Transform,and Load) framework for Data Engineering

使用案例

  1. 基于大模型的问答
  2. 基于大模型的文生图

基于大模型的文档问答

实现方式

  1. Fine Tuning

  2. Prompt Stuffing

  3. Function Calling

Fine Tuning

将目标数据与模型一起训练

优点:

  • 与模型一体,不用额外处理

缺点:

  • 重新训练非常耗费资源

Prompt Stuffing

Prompt填充,代表技术:Retrieval Augmented Generation (RAG),先从向量数据库中获取相关信息然后填充到prompt中一起发送给大模型,从而达到大模型引用外部知识目的

将文档存入向量数据库这一步骤很重要,大致分为两步

  1. 将文档按照语义边界进行分割。避免将同一语义信息从中间进行分割(例如不要把一个function从中间分割了)
  2. 将文档进一步分割使其大小符合模型token限制

优点:

  • 方便灵活

缺点:

  • 受prompt限制,受相似度查询准确率限制,受大模型稳定性限制

Function Calling

大模型在训练完成后知识库便固定了正常无法获取外部数据,函数调用技术允许提供外部数据给大模型使用。

优点:

  • 灵活

缺点:

  • 不是所有大模型都有函数接口

SpringAI实现基于大模型的文档问答示例

环境信息

  • 框架:SpringAI 0.8.1
  • 大模型:LLAMA3-7B(ollama本地部署)
  • 检索方式:RAG - Prompt Stuffing
  • 向量数据库:Neo4j

核心代码

没啥特别的基本都有starter,按文档往pom和配置文件加下参数就能跑起来

这里更多的难点是在如何更好与大模型交互,在于prompt的编写

出现的问题记录

由于是SpringAI早期开发版本所以会有各种各样问题

1.Neo4j的向量维度限制

当前Neo4j的配置类Neo4jVectorStoreConfig源码将向量维度限制在了2048,而llama3模型为4096

1
2
3
4
5
public Builder withEmbeddingDimension(int newEmbeddingDimension) {
Assert.isTrue(newEmbeddingDimension >= 1 && newEmbeddingDimension <= 2048, "Dimension has to be withing the boundaries 1 and 2048 (inclusively)");
this.embeddingDimension = newEmbeddingDimension;
return this;
}

临时性的解决措施:通过反射修改bean的属性

2.Pinecone数据库相似度查询

当使用Pinecone作为向量数据库时,进行相似度查询时,相似度度量方式有特定限制,采用cosine度量可能会导致查询不到目标信息。这是PineconeVectorStorec.class源码,注意其中score部分限制

1
2
3
4
5
6
7
8
9
10
11
12
return queryResponse.getMatchesList()
.stream()
.filter(scoredVector -> scoredVector.getScore() >= request.getSimilarityThreshold()) // here my socres are negative numbers
.map(scoredVector -> {
var id = scoredVector.getId();
Struct metadataStruct = scoredVector.getMetadata();
var content = metadataStruct.getFieldsOrThrow(CONTENT_FIELD_NAME).getStringValue();
Map<String, Object> metadata = extractMetadata(metadataStruct);
metadata.put(DISTANCE_METADATA_FIELD_NAME, 1 - scoredVector.getScore());
return new Document(id, content, metadata);
})
.toList();

解决办法: 社区反馈会在1.0.0-M1版本支持更多度量方式,或者自己重写(然后去PR)

完整Demo

demo

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

/**
* 下单报文接口
* @author: east
* @date: 2023/11/23
*/
public interface OrderRequest<T extends OrderInfo> {
/**
* 将不同渠道报文转为T的统一格式数据(比如转为落表所需数据)
* 通过T extends OrderInfo限制模板类型为OrderInfo及其子类
*
* @return T 统一数据
*/
public T generateOrderInfo();
}

/**
* 账单通用数据
*/
public class OrderInfo {
protected BigDecimal amt;
protected String serial;
protected String rcvNo;

public OrderInfo(BigDecimal amt, String serial, String rcvNo) {
this.amt = amt;
this.serial = serial;
this.rcvNo = rcvNo;
}

public OrderInfo() {
}

@Override
public String toString() {
return "OrderInfo{" +
"amt=" + amt +
", serial='" + serial + '\'' +
", rcvNo='" + rcvNo + '\'' +
'}';
}
}

下单接口

这里简化一下,只定义落表接口。落表接口接收不同类型的要落表信息,但要落表信息都是基础落表信息(BaseSavedInfo.java)的子类,所以使用”extends SavedInfo”关键字去限制一下,具体的实现类则必须传入SavedInfo或者其子类。因此这里可以使用模板接口或者模板方法,定义方法,这两种方法各有其特点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 下单要落表的最基本信息,如果除了账单信息,其他渠道需要落特别的数据表,则基础本类并重写Save接口即可,满足开闭原则
* 账单信息
* 其他信息自行继承本类实现
*
* @author: east
* @date: 2023/11/25
*/
public class BaseSavedInfo {
protected OrderInfo orderInfo;

public BaseSavedInfo(OrderInfo orderInfo) {
this.orderInfo = orderInfo;
}

@Override
public String toString() {
return "SavedInfo{" +
"orderInfo=" + orderInfo +
'}';
}
}
模板方法接口Save

这里Save接口有一个模板方法save,他的方法模板类型限制在BaseSavedInfo及其子类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 保存下单相关信息, 使用模板方法
*
* @author: east
* @date: 2023/11/23
*/
public interface Save {
/**
* 保存下单数据
*
* @param savedInfo 相关信息:账单表,统计表
*/
public <T extends BaseSavedInfo> void save(T savedInfo);
}
模板接口SaveT

这里定义的为一个模板接口SaveT,模板范围限制在BaseSavedInfo及其子类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 保存下单相关信息, 模板类
*
* @author: east
* @date: 2023/11/23
*/
public interface SaveT<T extends BaseSavedInfo> {
/**
* 保存下单数据
*
* @param savedInfo 相关信息:账单表,统计表
*/
public void save(T savedInfo);
}

下单行为抽象类

这里设置一个下单行为的抽象类目的是为了将下单流程组装起来,因为涉及到Save接口作为依赖,所以接口不适用需要换为抽象类。这里抽象类的定义可以分为三种

使用Save接口的抽象下单类:方式一
1
2
3
4
5
6
7
8
9
10
11
12
13
public abstract class AbstractOrder<T extends OrderRequest<?>> {
// 使用Save接口作为其依赖
protected Save save;

public AbstractOrder(Save save) {
this.save = save;
}

/**
* 下单接口;T为实现了OrderRequest接口的请求报文
*/
public abstract void order(T orderRequest);
}
  • 这里的”<T extends OrderRequest<?>” 表示AbstractOrder是一个模板类,且其实现类的模板类型限制在OrderRequest接口,这样做的好处是所有下单虚函数的实现类都实现了OrderRequest接口,从而统一下单行为,满足同一个标准(当然也可以不指定),后面的’?’表示任意通配符,也就是OrderRequest接口的泛型这里不关心

  • AbstractOrder抽象下单类使用Save接口作为其依赖因此直接定义Save类型的成员变量即可。如果是SaveT类型则需要特别处理

  • “public abstract void order(T orderRequest);” 表示这是一个抽象下单接口,接收一个模板类型参数,也就是实现了OrderRequest接口的请求报文体。该报文体根据下单渠道自行定义

使用SaveT接口的抽象下单类: 方式二
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public abstract class AbstractOrderTR<T extends OrderRequest<?>, R extends BaseSavedInfo> {
// 使用SaveT接口作为其依赖
// 用模板指定了参数的类型为R
protected SaveT<R> save;

public AbstractOrderTR(SaveT<R> save) {
this.save = save;
}

/**
* 下单
*/
public abstract void order(T orderRequest);
}
  • 这里AbstractOrderTR抽象下单类使用SaveT接口作为其依赖,而且在类上通过模板”R extends BaseSavedInfo”指定SaveT模板类型为BaseSavedInfo及其子类
使用SaveT接口的抽象下单类: 方式三
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public abstract class AbstractOrderT<T extends OrderRequest<?>> {
// 使用SaveT接口作为其依赖
// 不通过了类模板指定,仅用通配符参数限定,
protected SaveT<? extends BaseSavedInfo> save; // note 如果实现类中不指定SaveT的类型,则save接口无法使用
// 原因: 编译器没那么聪明根据子类传入的saveT子对象推断出SaveT模板具体对应哪一个类,只能在这里明确指定或者子类重写

// 也可以为SaveT<?> 仅通配符不做限制
// protected SaveT<?> save;

// 也可以不限制,仅使用SaveT
// protected SaveT save;

public AbstractOrderT(SaveT<? extends BaseSavedInfo> save) {
this.save = save;
}

/**
* 下单
*/
public abstract void order(T orderRequest);
}
  • 这里AbstractOrderTR抽象下单类使用SaveT接口作为其依赖,但不像方式二通过模板来限制SaveT模板类型,而是使用SaveT<? extends BaseSavedInfo>通配符,来限制该模板类型仅为BaseSavedInfo及其子类,当然也可以不限制仅用SaveT<?>和SaveT。但是SaveT和(SaveT<?>,SaveT<? extends BaseSavedInfo>)不一样,SaveT并没有使用通配符,它更类似于方式一的用法,只是一个基类。而SaveT<?>和SaveT<? extends BaseSavedInfo>因为都作为通配符使用,所以在AbstractOrderTR的实现类AbstractOrderTRImp中不能直接使用,因为编译器并不能确定此处SaveT的具体类型,详见

定义实现

这里用外部渠道下单和内部渠道下单来模拟实现。一些实体类定义不具体展示,详细可在这里查看完整包

内部渠道下单实现类

InnerOrder类继承了抽象下单类AbstractOrder(方式一),并指定了模板类型为InnerOrderRequest(内部请求报文)

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
/**
* 内部下单实现类
*
* @author: east
* @date: 2023/11/24
*/
public class InnerOrder extends AbstractOrder<InnerOrderRequest> {

// InnerSaveOrder为Save接口实现类
public InnerOrder(InnerSaveOrder save) {
super(save);
}

//InnerOrderRequest为OrderRequest实现类
@Override
public void order(InnerOrderRequest innerOrderRequest) {
try {

// 校验等接口调用....

// InnerBaseSavedInfo为内部渠道需要落表的数据,是BaseSavedInfo的子类
// 落表 这里save为模板方法
save.save(new InnerBaseSavedInfo(innerOrderRequest.generateOrderInfo(), "this is inner special message"));

} catch (Exception e) {
throw new RuntimeException(e);
}
}

// public static void main(String[] args) {
// InnerSaveOrder innerSaveOrder = new InnerSaveOrder();
// InnerOrder innerOrder = new InnerOrder(innerSaveOrder);
// innerOrder.order(new InnerOrderRequest(BigDecimal.ONE, "innerSerial", "rcvNo"));
// }

}

/**
* 内部渠道保存账单
* Save接口的实现类
*
* @author: east
* @date: 2023/11/24
*/
public class InnerSaveOrder implements Save {

/**
* 保存下单数据
*
* @param savedInfo 相关信息:账单表,统计表
*/
@Override
public <T extends BaseSavedInfo> void save(T savedInfo) {

// 落表
System.out.println("内部渠道下单信息开始落表");

// todo 这里有个很大的问题就是如果不通过强制类型转换无法拿到特定种类的数据
// InnerSavedInfo innerSavedInfo=(InnerSavedInfo) savedInfo;
// System.out.println("内部渠道特有数据获取: "+innerSavedInfo.getSpecialMsg());

System.out.println(savedInfo.toString());
}
}
  • 从代码里面可以看到Save接口采用模板方法这种方式有个麻烦的地方在于实现类中因为是重写save方法所以也必须是模板方法,虽然这时已经明确知道参数类型为BaseSavedInfo的实现类InnerSavedInfo,但是在方法中无法直接获取其特定属性(比如获取InnerSavedInfo子类独有的字段),暂时没明白除了强制类型转换外该怎么解决

外部渠道下单实现类(AbstractOrderTR)

OuterOrderTR继承了下单虚函数AbstractOrderTR(方式二),并指定了AbstractOrderTR的两个模板为OuterOrderRequest和OuterBaseSavedInfo,从而明确指定了抽象类的SaveT这个成员函数的模板类型为OuterBaseSavedInfo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class OuterOrderTR extends AbstractOrderTR<OuterOrderRequest, OuterBaseSavedInfo> {

public OuterOrderTR(OuterSaveOrderImp save) {
super(save);
}

@Override
public void order(OuterOrderRequest outerOrderRequest) {
try {

// 校验等逻辑....

// 落表
// note 这里通过模板类全量指定
save.save(new OuterBaseSavedInfo(outerOrderRequest.generateOrderInfo()));

} catch (Exception e) {
throw new RuntimeException(e);
}
}

// public static void main(String[] args) {
// OuterSaveOrderImp saveOrderAction = new OuterSaveOrderImp();
// OuterOrderTR orderAction = new OuterOrderTR(saveOrderAction);
// orderAction.order(new OuterOrderRequest(BigDecimal.ONE, "outSerialTR", "rcvNo"));
// }

}

外部渠道下单实现类(AbstractOrderT)

这里OuterOrderT继承了AbstractOrderT虚函数方式三,成员函数SaveT<? extends BaseSavedInfo> save只通过上界通配符限定,我一开始觉得编译器能通过传入的OuterBaseSavedInfo类对象来初始化SaveT对象时推断出SaveT的模板类型为OuterBaseSavedInfo而不是InnerBaseSavedInfo(BaseSavedInfo的另一个实现类),但实际上编译器好像无法推测出save的类型导致SaveT对象的public void save(T savedInfo)方法无法接收任何参数(除了null),这里和List<? extends Number> list无法list.add(1)问题很像,调用save方法时会导致类型安全问题,无法编译通过。

相关解释

解决办法为明确指定SaveT模板类型

1
protected SaveT<OuterBaseSavedInfo> save;

类定义如下

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
public class OuterOrderT extends AbstractOrderT<OuterOrderRequest> {

protected SaveT<OuterBaseSavedInfo> save; // note 通配符如果不在模板指定,则只有在这里明确指定,save的方法才能正常调用, 可以将这行去掉试试

public OuterOrderT(OuterSaveOrderImp save) {
super(save);
this.save = save;
}

@Override
public void order(OuterOrderRequest outerOrderRequest) {
try {

// 校验等逻辑....

// 落表
// 通过父类调用 note: 这里没有通过模板指定(如果也不在成员函数指定的话则会报错,? extends通配只是占位,不指定类型则无法操作
save.save(new OuterBaseSavedInfo(outerOrderRequest.generateOrderInfo()));

} catch (Exception e) {
throw new RuntimeException(e);
}
}

// public static void main(String[] args) {
// OuterSaveOrderImp saveOrderAction = new OuterSaveOrderImp();
// OuterOrderT orderAction = new OuterOrderT(saveOrderAction);
// orderAction.order(new OuterOrderRequest(BigDecimal.ONE, "outSerialT", "rcvNo"));
// }

}

总结

  • 上面的三个实现类大概展示了通配符与模板使用的示例,以及一些设计原则(依赖倒置,接口隔离,单一,开闭)的使用,以及其中的一些坑,建议大家结合具体的示例或实际的业务来理解。

  • 对于上界和下界通配符,个人感觉<? extends ClassA>这种用得多一点,这种相当于指定了一个规范ClassA,后续传入的参数都要实现这个规范ClassA或者继承ClassA。至于上下界的定义为什么那么叫,个人感觉没那么重要

完整代码见:

简略版

详细版

docker安装kafka

背景

最近学习到Kafka相关知识,所以用docker在vps上安装单节点版和集群版方便后续学习

环境

  • Debian10
  • Kafka:

单节点版

使用docker compose安装。注意在yml目录下创建一个data目录放上两个文件message.json和update_run.sh,做好权限修改后up一下就行

docker-compose.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
85
86
87
version: '2'
services:

kafka1:
image: confluentinc/cp-kafka:7.2.1
hostname: kafka1
container_name: kafka1
ports:
- "9092:9092"
- "9997:9997"
environment:
KAFKA_BROKER_ID: 1
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: 'CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT'
KAFKA_ADVERTISED_LISTENERS: 'PLAINTEXT://kafka1:29092,PLAINTEXT_HOST://154.40.46.38:9092'
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0
KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1
KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1
KAFKA_JMX_PORT: 9997
KAFKA_JMX_HOSTNAME: localhost
KAFKA_PROCESS_ROLES: 'broker,controller'
KAFKA_NODE_ID: 1
KAFKA_CONTROLLER_QUORUM_VOTERS: '1@kafka1:29093'
KAFKA_LISTENERS: 'PLAINTEXT://kafka1:29092,CONTROLLER://kafka1:29093,PLAINTEXT_HOST://0.0.0.0:9092'
KAFKA_INTER_BROKER_LISTENER_NAME: 'PLAINTEXT'
KAFKA_CONTROLLER_LISTENER_NAMES: 'CONTROLLER'
KAFKA_LOG_DIRS: '/tmp/kraft-combined-logs'
volumes:
- ./data/update_run.sh:/tmp/update_run.sh
command: "bash -c 'if [ ! -f /tmp/update_run.sh ]; then echo \"ERROR: Did you forget the update_run.sh file that came with this docker-compose.yml file?\" && exit 1 ; else /tmp/update_run.sh && /etc/confluent/docker/run ; fi'"

schemaregistry1:
image: confluentinc/cp-schema-registry:7.2.1
ports:
- 18085:8085
depends_on:
- kafka1
volumes:
- ./data/jaas:/conf
environment:
SCHEMA_REGISTRY_KAFKASTORE_BOOTSTRAP_SERVERS: PLAINTEXT://kafka1:29092
SCHEMA_REGISTRY_KAFKASTORE_SECURITY_PROTOCOL: PLAINTEXT
SCHEMA_REGISTRY_HOST_NAME: schemaregistry1
SCHEMA_REGISTRY_LISTENERS: http://schemaregistry1:8085

# ui那边SCHEMA模块默认账户密码: admin/letmein
SCHEMA_REGISTRY_AUTHENTICATION_METHOD: BASIC
SCHEMA_REGISTRY_AUTHENTICATION_REALM: SchemaRegistryProps
SCHEMA_REGISTRY_AUTHENTICATION_ROLES: admin
SCHEMA_REGISTRY_OPTS: -Djava.security.auth.login.config=/conf/schema_registry.jaas

SCHEMA_REGISTRY_SCHEMA_REGISTRY_INTER_INSTANCE_PROTOCOL: "http"
SCHEMA_REGISTRY_LOG4J_ROOT_LOGLEVEL: INFO
SCHEMA_REGISTRY_KAFKASTORE_TOPIC: _schemas

kafka-init-topics:
image: confluentinc/cp-kafka:7.2.1
volumes:
- ./data/message.json:/data/message.json
depends_on:
- kafka1
command: "bash -c 'echo Waiting for Kafka to be ready... && \
cub kafka-ready -b kafka1:29092 1 30 && \
kafka-topics --create --topic users --partitions 3 --replication-factor 1 --if-not-exists --bootstrap-server kafka1:29092 && \
kafka-topics --create --topic messages --partitions 2 --replication-factor 1 --if-not-exists --bootstrap-server kafka1:29092 && \
kafka-console-producer --bootstrap-server kafka1:29092 --topic users < /data/message.json'"

# ui控制
kafka-ui:
container_name: kafka-ui
image: provectuslabs/kafka-ui:latest
ports:
- 18080:8080
depends_on:
- kafka1
- schemaregistry1
environment:
KAFKA_CLUSTERS_0_NAME: local
KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka1:29092
KAFKA_CLUSTERS_0_METRICS_PORT: 9997
KAFKA_CLUSTERS_0_SCHEMAREGISTRY: http://schemaregistry1:8085
KAFKA_CLUSTERS_0_SCHEMAREGISTRYAUTH_USERNAME: admin
KAFKA_CLUSTERS_0_SCHEMAREGISTRYAUTH_PASSWORD: letmein
AUTH_TYPE: "LOGIN_FORM"
SPRING_SECURITY_USER_NAME: username
SPRING_SECURITY_USER_PASSWORD: passwd

ref: https://blog.csdn.net/dghkgjlh/article/details/133418837

https://flxdu.cn/2023/01/23/Kafka-in-Docker-%E5%8D%95%E8%8A%82%E7%82%B9-%E5%A4%9A%E8%8A%82%E7%82%B9/

update_run.sh

1
2
3
4
5
6
7
8
9
10
11
# This script is required to run kafka cluster (without zookeeper)
#!/bin/sh

# Docker workaround: Remove check for KAFKA_ZOOKEEPER_CONNECT parameter
sed -i '/KAFKA_ZOOKEEPER_CONNECT/d' /etc/confluent/docker/configure

# Docker workaround: Ignore cub zk-ready
sed -i 's/cub zk-ready/echo ignore zk-ready/' /etc/confluent/docker/ensure

# KRaft required step: Format the storage directory with a new cluster ID
echo "kafka-storage format --ignore-formatted -t $(kafka-storage random-uuid) -c /etc/kafka/kafka.properties" >> /etc/confluent/docker/ensure

message.json

放个空的就行

1
{}

Kafka集群版

这里用多个docker模拟集群的多节点, 直接复制yml然后up就行

参考

docker-compose.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
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
# Copyright VMware, Inc.
# SPDX-License-Identifier: APACHE-2.0

version: "2"

services:
kafka-0:
image: docker.io/bitnami/kafka:3.6
ports:
- "9092:9092"
restart: unless-stopped
environment:
# KRaft settings
# 结点序号1
- KAFKA_CFG_NODE_ID=0
# 结点角色:controller,broker(同一时间只会有一个controller)
- KAFKA_CFG_PROCESS_ROLES=controller,broker
# controller选举投票权结点
- KAFKA_CFG_CONTROLLER_QUORUM_VOTERS=0@kafka-0:9093,1@kafka-1:9093,2@kafka-2:9093
# 禁用自动创建topic
- KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE=false
# 日志保留时间
- KAFKA_CFG_LOG_RETENTION_HOURS=168
# 单个分区大小
- KAFKA_CFG_LOG_RETENTION_BYTES=1073741824
# 启用日志压缩
- KAFKA_CFG_LOG_CLEANER_ENABLE=true
# 消息体最大为1M
- KAFKA_CFG_MESSAGE_MAX_BYTES=1000012
# ISR中最小个数,小于这个生产者可能发送失败,数据丢失风险增加,可靠性降低
- KAFKA_CFG_MIN_IN_SYNC_REPLICAS=2
# 集群ID
- KAFKA_KRAFT_CLUSTER_ID=abcdefghijklmnopqrstuv
# Listeners
# 服务监听部分,监听器PLAINTEXT(没错这只是个监听器名称)监听9092,监听器CONTROLLER监听9093端口
- KAFKA_CFG_LISTENERS=PLAINTEXT://:9092,CONTROLLER://:9093
# 连接建议监听器为PLAINTEXT,端口为9092,这里为了外部访问需要指定外部ip(如果外部有代理则这里就用集群内部格式就行)
- KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://103.171.26.248:9092
# 映射监听器名称和使用的协议,PLAINTEXT使用的协议是PLAINTEXT,CONTROLLER也用的PLAINTEXT(比较方便)
- KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=PLAINTEXT:PLAINTEXT,CONTROLLER:PLAINTEXT
# 指定用于控制器通信的监听器
- KAFKA_CFG_CONTROLLER_LISTENER_NAMES=CONTROLLER
# broker间通信使用的监听器
- KAFKA_CFG_INTER_BROKER_LISTENER_NAME=PLAINTEXT
# Clustering
# 主题消费记录偏移量的副本个数(记录消费组消费主题的偏移量的副本)
- KAFKA_CFG_OFFSETS_TOPIC_REPLICATION_FACTOR=3
# Kafka 会在集群中的三个不同节点上保持事务状态日志的副本
- KAFKA_CFG_TRANSACTION_STATE_LOG_REPLICATION_FACTOR=3
# 至少有两个副本(包括领导副本)需要是同步的,才能继续事务操作
- KAFKA_CFG_TRANSACTION_STATE_LOG_MIN_ISR=2
volumes:
- kafka_0_data:/bitnami/kafka
kafka-1:
image: docker.io/bitnami/kafka:3.6
ports:
- "9093:9092"
restart: unless-stopped
environment:
# KRaft settings
- KAFKA_CFG_NODE_ID=1
- KAFKA_CFG_PROCESS_ROLES=controller,broker
- KAFKA_CFG_CONTROLLER_QUORUM_VOTERS=0@kafka-0:9093,1@kafka-1:9093,2@kafka-2:9093
- KAFKA_KRAFT_CLUSTER_ID=abcdefghijklmnopqrstuv
- KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE=false
- KAFKA_CFG_LOG_RETENTION_HOURS=168
- KAFKA_CFG_LOG_RETENTION_BYTES=1073741824
- KAFKA_CFG_LOG_CLEANER_ENABLE=true
- KAFKA_CFG_MESSAGE_MAX_BYTES=1000012
- KAFKA_CFG_MIN_IN_SYNC_REPLICAS=2
# Listeners
- KAFKA_CFG_LISTENERS=PLAINTEXT://:9092,CONTROLLER://:9093
- KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://103.171.26.248:9093
- KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=PLAINTEXT:PLAINTEXT,CONTROLLER:PLAINTEXT
- KAFKA_CFG_CONTROLLER_LISTENER_NAMES=CONTROLLER
- KAFKA_CFG_INTER_BROKER_LISTENER_NAME=PLAINTEXT
# Clustering
- KAFKA_CFG_OFFSETS_TOPIC_REPLICATION_FACTOR=3
- KAFKA_CFG_TRANSACTION_STATE_LOG_REPLICATION_FACTOR=3
- KAFKA_CFG_TRANSACTION_STATE_LOG_MIN_ISR=2
volumes:
- kafka_1_data:/bitnami/kafka
kafka-2:
image: docker.io/bitnami/kafka:3.6
ports:
- "9094:9092"
restart: unless-stopped
environment:
# KRaft settings
- KAFKA_CFG_NODE_ID=2
- KAFKA_CFG_PROCESS_ROLES=controller,broker
- KAFKA_CFG_CONTROLLER_QUORUM_VOTERS=0@kafka-0:9093,1@kafka-1:9093,2@kafka-2:9093
- KAFKA_KRAFT_CLUSTER_ID=abcdefghijklmnopqrstuv
- KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE=false
- KAFKA_CFG_LOG_RETENTION_HOURS=168
- KAFKA_CFG_LOG_RETENTION_BYTES=1073741824
- KAFKA_CFG_LOG_CLEANER_ENABLE=true
- KAFKA_CFG_MESSAGE_MAX_BYTES=1000012
- KAFKA_CFG_MIN_IN_SYNC_REPLICAS=2
# Listeners
- KAFKA_CFG_LISTENERS=PLAINTEXT://:9092,CONTROLLER://:9093
- KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://103.171.26.248:9094
- KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=PLAINTEXT:PLAINTEXT,CONTROLLER:PLAINTEXT
- KAFKA_CFG_CONTROLLER_LISTENER_NAMES=CONTROLLER
- KAFKA_CFG_INTER_BROKER_LISTENER_NAME=PLAINTEXT
# Clustering
- KAFKA_CFG_OFFSETS_TOPIC_REPLICATION_FACTOR=3
- KAFKA_CFG_TRANSACTION_STATE_LOG_REPLICATION_FACTOR=3
- KAFKA_CFG_TRANSACTION_STATE_LOG_MIN_ISR=2
volumes:
- kafka_2_data:/bitnami/kafka

volumes:
kafka_0_data:
driver: local
kafka_1_data:
driver: local
kafka_2_data:
driver: local

查看集群内的topic

1
2
3
4
5
# 进入容器
docker exec -it [容器名称或ID] /bin/bash

# 调用kafka脚本
kafka-topics.sh --bootstrap-server localhost:9092 --list

集群内新建一个topic

1
2
# 用脚本创建
kafka-topics.sh --create --bootstrap-server localhost:9092 --replication-factor 3 --partitions 3 --topic TOPIC_LY_TEST

查看某个topic信息

1
kafka-topics.sh --describe --topic TOPIC_LY_TEST --bootstrap-server localhost:9092

Win11配置两个git用户

背景

该方法太麻烦,不建议使用,建议参考这篇Git多用户环境隔离提交

有两个github账号,一个主要负责公开的内容,一个私人的,需要在同一台电脑上满足代码提交且互不干扰。核心操作分为三步:

  1. 配置ssh的config文件
  2. 切换用户
  3. 关闭全局用户名称(可选)

测试环境

  • Win: 11
  • OpenSSH: 8.6
  • Git: 2.39.1.windows.1

1. 配置.ssh文件夹下config文件

生成key

自己有秘钥对就不用生成了

1
2
3
ssh-keygen -t rsa -b 4096 -C "luyublog@gmail.com"

输入保存位置,密码(最好需要,否则我的环境下后续会报错)等信息,这里有几个账号就生成几次

绑定key

将public key与github关联

更改ssh配置文件

目录:~/.ssh/ 下的 config,没有则新建一个,IdentityFile后面接私钥的路径,内容如下:

1
2
3
4
5
6
7
8
9
Host east
HostName github.com
User git
IdentityFile the/path/to/your/privake_key

Host luyublog
HostName github.com
User git
IdentityFile ~/.ssh/id_rsa_github_luyublog

检查本地的OpenSSH服务安装状态

设置->可选功能->OpenSSH 保证已经安装好了服务。

增加key

这里我是切换到了密钥对所在路径执行的命令所以不用带路径

1
2
3
4
5
6
7
8
// 增加第一个秘钥
ssh-add the_path_to_your_prikey_key

// 增加第二个秘钥
ssh-add id_rsa_github_luyublog

# 验证识别是否正确,如果ok的话会有成功提示
ssh -T luyublog

增加key时报错:Error connecting to agent: No such file or directory

原因:OpenSSH服务没有开启
参考

解决办法:

1
2
3
4
5
6
7
8
9
10
11
12
# 查看服务状态,显示Stopped说明就是有安装且关闭状态,开启一下就行
get-service ssh*

结果:
Status Name DisplayName
------ ---- -----------
Stopped ssh-agent OpenSSH Authentication Agent

# 手动开启服务
Set-Service -Name ssh-agent -StartupType Manual

Start-Service ssh-agent

增加key时报错:Bad permissions. Try removing permissions for user balabala

原因:私钥权限太低,建议重新生成带密码的的密钥对

2. 切换用户

其实没有切换用户的这个功能,git是根据上面的config和你的远程仓库@后面的host来判断使用哪个用户提交。所以使用不同的用户核心在于远程仓库的配置

配置示例

最好找一个现有测试工程验证,在测试仓库的路径下执行以下命令

1
2
3
4
5
6
7
8
# 清除仓库之前的关联
git remote rm origin

# 添加新的ssh格式仓库地址,注意这里格式:git@luyublog:luyublog:第一个luyublog是config里面host,第二个是github的用户名,不能直接使用github提供的ssh链接

git remote add origin git@luyublog:luyublog/SpringBootDemo.git

# 随便修改点东西然后提交,如果是当前用户的仓库且远程地址配置正确应该就会弹出密码验证,不是的话应该就报没权限了,建议检查仓库地址是否配置正确

提交时报错: Permission denied

git@github.com: Permission denied (publickey). Could not read from remote repository. Please make sure you have the correct access rights and the repository exists

可能的原因:

  1. 用户不对,用了a用户去提交b用户的仓库
  2. 检查远程仓库格式不对,直接使用了github提供的链接

解决:

  1. 检查上文 配置示例 切换正确远程仓库
  2. 还是检查上文的仓库链接格式 配置示例

3. 关闭全局name和email(可选)

如果你的两个账户需要区分名称那么关闭name和email的全局配置很重要,不然两个用户提交的名称都是一样就尴尬了。但是这里有个奇怪的地方就是信息改成局部后在github上看到的名称和你config设置的并不一样貌似用的github用户名,这里我没有深究原因。注意,关闭全局配置后每个工程都要重新设置一遍局部信息。

1
2
3
4
5
6
7
8
9
10
# 看下当前全局配置
git config --global -l

# 取消掉全局配置
git config --global --unset user.name
git config --global --unset user.email

# 设置局部配置,不设置无法提交
git config user.name luyublog
git config user.email luyublog@gmail.com

参考

  1. https://juejin.cn/post/6844903831000596488

  2. 如何在一台电脑上管理/切换多个github账户

修改经过Spring Gateway的Json数据

背景

使用Spring Cloud Gateway作为网关时经常会需要对报文内的json数据进行修改,但是目前看到的实现方法看起来都很复杂,这里提供一种使用Spring官方提供的ModifyRequestBodyGatewayFilterFactory类来修改json报文的方法

依赖

  • Spring Boot版本:2.7.15
  • Hutool: 5.8.21
  • Java: 11

实现逻辑

实现分为两个部分

  • filter:在自定义的filter内注入ModifyRequestBodyGatewayFilterFactory类,然后调用,该类会自动完成对修改结果的重新包装。
  • service: 自定义的service实现ModifyRequestBodyGatewayFilterFactory类的RewriteFunction<T, R>接口完成对json数据的处理

自定义filter

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
@Component
@Slf4j
public class RequestModifyFilter implements GlobalFilter, Ordered {
@Autowired
private ModifyRequestBodyGatewayFilterFactory modifyRequestBodyFilter;
@Autowired
private JsonRequestBodyRewriteService jsonRequestBodyRewriteService;

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
MediaType mediaType = exchange.getRequest().getHeaders().getContentType();
if (MediaType.APPLICATION_json.isCompatibleWith(mediaType)) {
// 纯json报文处理逻辑
return modifyRequestBodyFilter
.apply(
new ModifyRequestBodyGatewayFilterFactory.Config()
.setRewriteFunction(byte[].class, byte[].class, jsonRequestBodyRewriteService))
.filter(exchange, chain);
} else {
return filter(exchange, chain);
}

}

@Override
public int getOrder() {
return OrderConstant.REQUEST_MODIFY_FILTER.getOrder();
}
}

自定义service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Service
@Slf4j
public class JsonRequestBodyRewriteService implements RewriteFunction<byte[], byte[]> {
@Override
public Publisher<byte[]> apply(ServerWebExchange exchange, byte[] body) {
jsonObject request = jsonUtil.parseObj(body);
log.info("原始报文:{}", request.toString());
try {
request.set("empId", "2345");
request.set("department", "Engineering");

log.info("修改后报文:{}", request);
return Mono.just(request.toString().getBytes());
} catch (Exception e) {
log.error("修改报文时出错",e);
throw e;
}
}
}

相关代码

https://github.com/eastcukt/demo-gatway

参考

https://amitkhurana92.medium.com/request-body-transformation-in-spring-cloud-gateway-bb9c234d5f3d

修改经过Spring Gateway的表单中的Json数据

背景

使用Spring Cloud Gateway作为网关时有时候一个请求是既包含excel又包含json的表单数据,出于各种层面考虑网关需要获取并更新其中的json数据

依赖

  • Spring Boot版本:2.7.15
  • Hutool: 5.8.21
  • Java: 11

实现逻辑

实现分为2个部分

  1. 使用上文提到的ModifyRequestBodyGatewayFilterFactory类来修改请求体,这样最后就不用我们手动包装
  2. 核心service通过将表单转为String,然后根据其中的boundary进行分割,提取修改json报文部分后再进行组装

注意:示例代码的核心service处理的表单内容只是2个,Json数据的key指定为json,另一个excel文件流

自定义filter

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
@Component
@Slf4j
public class RequestModifyFilter implements GlobalFilter, Ordered {
@Autowired
private ModifyRequestBodyGatewayFilterFactory modifyRequestBodyFilter;
@Autowired
private JsonRequestBodyRewriteService jsonRequestBodyRewriteService;
@Autowired
private FormDataRequestBodyRewriteService formDataRequestBodyRewriteService;

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
MediaType mediaType = exchange.getRequest().getHeaders().getContentType();
if (MediaType.APPLICATION_JSON.isCompatibleWith(mediaType)) {
// 纯json报文处理逻辑
return modifyRequestBodyFilter
.apply(
new ModifyRequestBodyGatewayFilterFactory.Config()
.setRewriteFunction(byte[].class, byte[].class, jsonRequestBodyRewriteService))
.filter(exchange, chain);
} else if (MediaType.MULTIPART_FORM_DATA.isCompatibleWith(mediaType)) {
// form表单数据处理
return modifyRequestBodyFilter
.apply(
new ModifyRequestBodyGatewayFilterFactory.Config()
.setRewriteFunction(byte[].class, byte[].class, formDataRequestBodyRewriteService))
.filter(exchange, chain);
} else {
return filter(exchange, chain);
}

}

@Override
public int getOrder() {
return OrderConstant.REQUEST_MODIFY_FILTER.getOrder();
}
}

核心service

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
@Service
@Slf4j
public class FormDataRequestBodyRewriteService implements RewriteFunction<byte[], byte[]> {
private final String BOUNDARY_PREFIX_IN_CONTENT_TYPE = "----WebKitFormBoundary";
private final String BOUNDARY_PREFIX_IN_FORM_DATA = "------WebKitFormBoundary";
private final String BOUNDARY_SUFFIX = "--\r\n";

@Override
public Publisher<byte[]> apply(ServerWebExchange exchange, byte[] body) {
String finalResultString = "";

// 将表单转为字符串格式从而根据boundary分割表单数据。注意这里不能用默认编码
String request = StrUtil.str(body, StandardCharsets.ISO_8859_1);

// 获取boundary的随机字符信息
String contentType = exchange.getRequest().getHeaders().getContentType().toString();
String randomStr = contentType.substring(contentType.indexOf(BOUNDARY_PREFIX_IN_CONTENT_TYPE) + BOUNDARY_PREFIX_IN_CONTENT_TYPE.length());

// 这里和前端约定json数据的表单key为json
String keyPart = "^\r\nContent-Disposition: form-data; name=\"json\"";
Pattern r = Pattern.compile(keyPart);

// 根据表单内分割线进行分割。并通过关键段落keyPart来找到目标json数据
String[] split = request.split(BOUNDARY_PREFIX_IN_FORM_DATA + randomStr);
for (int x = 0; x < split.length - 1; x++) {
Matcher m = r.matcher(split[x]);
if (m.find()) {
// 找到了json报文部分数据
String originalJsonString = split[x];

// 找到 JSON 数据的起始和结束位置
int startIndex = originalJsonString.indexOf("{\"");
int endIndex = originalJsonString.indexOf("\"}") + 2;
// 提取 JSON 数据
String jsonData = originalJsonString.substring(startIndex, endIndex);
log.info("原始报文为:{}", jsonData);

JSONObject jsonObject = JSONUtil.parseObj(jsonData);
jsonObject.set("empId", "2345");
jsonObject.set("department", "Engineering");
String modifiedString = originalJsonString.substring(0, startIndex) + jsonObject + originalJsonString.substring(endIndex);
log.info("修改后报文为:{}", modifiedString);

// 重新组装split数组
finalResultString = finalResultString + modifiedString + BOUNDARY_PREFIX_IN_FORM_DATA + randomStr;
} else {
// 重组表单数据
finalResultString = finalResultString + split[x] + BOUNDARY_PREFIX_IN_FORM_DATA + randomStr;
}
}

// 补上最后一截数据
finalResultString = finalResultString + BOUNDARY_SUFFIX;

return Mono.just(finalResultString.getBytes(StandardCharsets.ISO_8859_1));
}
}

相关代码

https://github.com/eastcukt/demo-gatway

其他

核心service获取表单中的json数据逻辑挺复杂,根本原因是没有合适的方法进行对象转换,如果有像使用@RequestPart(value = “json”)注解一样方便的方法将会非常方便也不用自己截取,各位大佬有更方便的方法感谢分享一下

参考

https://blog.csdn.net/qq_36966137/article/details/128536391