开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

Maven 简介及安装 Maven

发表于 2021-11-27

这是我参与11月更文挑战的第13天,活动详情查看:2021最后一次更文挑战

Maven

简介

Maven是Apaache组织中的开源项目,主要服务于基于Java平台的项目管理,依赖管理和信息管理。

为什么需要学习Maven

  • 在Javaweb开发中,需要使用大量的jar包,我们需要手动导入
  • 需要一个自动帮我们导入和配置这个jar包

Maven因此诞生了!

Maven特性

  • 依赖管理系统
1
2
3
4
5
xml复制代码        <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>2.5.6</version>
</dependency>
+ 包类型:`jar`包或`war`包
+ `groupId`:用于表示实际项目-公司名称
+ `artifactId`:用于表示实际项目中的一个`Maven`模块-项目名
+ `version`:用于表示该项目的版本
  • 多模块构建

我们可以将一个项目分解为多个模块。

  • 一致的目录结构

maven的核心思想就是约定大于配置,并且制定了一套项目目录结构作为标准的Java项目结构。

  • 一致的构建模型和插件机制

Maven项目架构管理工具

依赖JDK

建议JDK版本在1.8以上

Maven安装

下载地址:maven.apache.org/download.cg…

image-20211127114132801.png

下载完成后解压即可,解压目录不要有空格和中文。

目录结构说明

  • bin:该目录包含了mvn运行的脚本,这些脚本用来配置java命令,准备好classpath和相关的Java系统属性,然后执行Java命令。
  • boot:该目录下文件是一个类加载器,相对于默认的java类加载器,它提供了更丰富的语法以方便配置,maven用该框架,加载自己的类库
  • conf:该目录下包含了一个非常重要的文件settings.xml,Maven核心配置文件
  • lib:该目录包含了所有Maven运行时需要的Java类库,``Maven本身是分模块开发的,此外这里还包含一些Maven`用到的第三方依赖

配置环境变量

  • M2_HOME: maven目录下的bin目录
  • MAVEN_HOME:maven目录
  • Path:在系统的path中配置 %MAVEN_HOME%\bin

验证

image-20211127120747439.png

验证发现有问题:

  • 重启电脑
  • 假如安装过多版本,查看是否有设置过的环境变量

阿里云镜像

一般访问外网获取依赖速度较慢,所以我们使用国内的镜像仓库。

  • 镜像:mirrors
  • 国内建议使用阿里云的镜像

打开 maven 的配置文件( windows 机器一般在 maven 安装目录的 conf/settings.xml ),在<mirrors></mirrors>标签中添加 mirror 子节点:

1
2
3
4
5
6
xml复制代码<mirror>
<id>aliyunmaven</id>
<mirrorOf>*</mirrorOf>
<name>阿里云公共仓库</name>
<url>https://maven.aliyun.com/repository/public</url>
</mirror>

配置本地仓库

仓库一般分为两种:本地和远程仓库。

建立本地仓库:

打开 maven 的配置文件( windows 机器一般在 maven 安装目录的 conf/settings.xml ),在<localRepository></localRepository>修改,默认配置地址是${user.home}/.m2/repository,我们将默认地址更换为${MAVEN_HOME}/repo:

1
xml复制代码  <localRepository>${MAVEN_HOME}/repo</localRepository>

在Maven安装目录内创建repo文件夹

在IDEA中使用Maven

IDEA中有默认的Maven配置,我们需要将Maven替换成我们自己的

image-20211127124127866.png

更换仓库后,IDEA会帮我们自动下载依赖。

Maven项目目录结构

目录 说明
${basedir} 存放pom.xml和所有子目录
${basedir}/src/main/java 项目的java源代码
${basedir}/src/main/resources 项目的资源,比如说配置文件
${basedir}/src/test/java 项目的测试类
${basedir}/src/test/resources 测试使用的资源

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Java8 如何使用Group By 聚合操作集合数据?

发表于 2021-11-27

这是我参与11月更文挑战的第19天,活动详情查看:2021最后一次更文挑战

🤞 个人主页:@青Cheng序员石头

🤞 粉丝福利:加粉丝群 一对一问题解答,获取免费的丰富简历模板、提高学习资料等,做好新时代的卷卷王!

上文 《Java8 | 如何优雅地初始化Map & List2Map?》有讲到如何初始化Map以及List2Map的方式,有很多小伙伴提了不少优秀的建议,一鼓作气,继续写,坐等小伙伴围观。这篇文章就实际开发中很有可能用到的集合数据Group By处理,实践多种方法解决问题。

一、开发问题

集合数据分组很多在实际开发过程中是相当常见,比如传给前端的产品数据按照类型进行分组。最常见的方式是遍历整个集合,然后通过判断类型构造存储不同类型的集合。那么有没有更好的办法,Java8 groupingBy能帮到我们。

同样地,
假如我们有一个类Product,如下面的代码所示。

1
2
3
4
5
6
7
8
9
10
less复制代码@Getter
@Setter
@ToString
@Builder
class Product{
private Long id;
private String category;
private String name;
private int count;
}

我们现在获取到了List<Product>格式的数据。

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码static List<Product> getList(){
final List<Product> productList = new ArrayList<>(100);
for(int i =1;i<=100;i++){
productList.add(Product.builder()
.id((long) i)
.name("name"+i)
.category("category"+i%9)
.count(i)
.build());
}
return productList;
}

接下来我们对这个数据进行一些复杂的处理。

二、最简单的单列处理

按照category类型进行分组。

1
2
java复制代码final Map<String, List<Product>> maps = productList.stream()
.collect(Collectors.groupingBy(Product::getCategory));

这里对于 Collectors.groupingBy的参数
Function<? super T, ? extends K> classifier的返回值作为Key
。

三、分组后统计指定列的总数

按照category类型进行分组,并且统计每个类型的count总数。

1
2
less复制代码Map<String, Integer> maps = productList.stream()
.collect(Collectors.groupingBy(Product::getCategory, summingInt(Product::getCount)));

按照上面的代码就能得到从分组结果中得到总和。这上面是聚合操作,如果要做筛选操作,比如查看类型分组下数量最多的产品怎么做?

用maxBy(comparingInt(*))即可。

1
2
less复制代码Map<String, Optional<Product>> maps = productList.stream()
.collect(Collectors.groupingBy(Product::getCategory, maxBy(comparingInt(Product::getCount))));

用这些JDK现有的方法,在内存中转化数据的格式,能极大的提高开发效率。


少年,没看够?点击石头的详情介绍,随便点点看看,说不定有惊喜呢?欢迎支持点赞/关注/评论,有你们的支持是我更文最大的动力,多谢啦!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

FastAPI 入门系列 之 请求!

发表于 2021-11-27

这是我参与11月更文挑战的第26天,活动详情查看:2021最后一次更文挑战。

上篇文章我们了解了 FastAPI 以及它的简单使用,那么这篇文章就来一起学习一下 FastAPI 的请求,了解一下 FastAPI 是如何设置各种形式的请求参数的。

Path 参数

Path(路径)参数在路由里用大括号{}括起来指定,然后在定义方法时接收该参数,另外,还可以指定参数的类型,例如:

1
2
3
python复制代码@app.get("/user/{user_id}")
async def hello_user(user_id: int):
return {"Hello user": user_id}

上述代码中,使用user_id: int指定参数的类型为 int,FastAPI 会通过 Pydantic 模块进行自动校验,如果传入的类型不正确,会得到类似于如下的错误信息:

1
2
3
4
5
6
7
8
9
10
11
12
json复制代码{
"detail": [
{
"loc": [
"path",
"user_id"
],
"msg": "value is not a valid integer",
"type": "type_error.integer"
}
]
}

我们不需要再去手动校验参数,可以更专注于编写业务代码,提高开发效率。

Query 参数

函数声明的参数不属于路径参数时,它们就被自动解释为Query(查询)参数,就是 url? 之后用&分割的 key-value 键值对。

1
2
3
python复制代码@app.get("/users")
async def users(page: int, page_size: int = 10, name: str = None):
return {"page": page, "page_size": page_size, "data": []}

同样,我们可以指定参数类型,并且可以设置默认值,如上 page_size 参数的默认值为10,如果不上传该参数,那就取默认值10。当然也可以把默认值设置None,表示可选的查询参数,比如上面的 name 参数,该参数可传可不传。

另外,Pydantic 模型支持很丰富的数据类型,除了常用的 str、int、float、List、Dict外,还有时间、日期、uuid等类型。

请求体参数

设置请求体参数的话,我们需要定义一个数据模型类,且继承 Pydantic 模块的BaseModel类,如下:

1
2
3
4
python复制代码class User(BaseModel):
name: str
password: str = '123456'
description: str = None

同样,如果指定数据类型和默认值,如果设置了默认值,那么该参数不是必传的。

然后在定义函数时,将参数指定为数据模型类 User 类型即可:

1
2
3
python复制代码@app.post("/add")
async def add(user: User):
return {"name": user.name, "password": user.password}

FastAPI 会自动将定义的模型类转化为JSON Schema,Schema 成为 OpenAPI 生成模式的一部分,并显示在 API 交互文档中,查看 API 交互文档如下,该接口将接收application/json类型的参数。

image.png

我们还可以定义更加复杂的请求模型类,例如属性为 List 列表、Dict 字典类型,或者是一个嵌套的模型类。

FastAPI 支持同时定义 Path 参数、Query 参数和请求体参数,FastAPI 将会正确识别并获取数据。

  • 如果参数在 url 中也声明了,它将被解释为 path 参数
  • 如果参数是单一类型(例如int、float、str、bool等),它将被解释为 query 参数
  • 如果参数类型为继承 Pydantic 模块的BaseModel类的数据模型类,则它将被解释为请求体参数

Form Data

FastAPI 可以使用Form组件来接收表单数据,需要先使用pip install python-multipart命令进行安装。

声明 Form Data 表单参数的方式与 Path 参数或 Query 参数相同,如下:

1
2
3
python复制代码@app.post("/login")
async def login(*, name: str = Form(...), password: str = Form(...)):
return {"Hello user": name}

参数接收必须使用Form,否则参数将被解释为Query参数或请求体参数。

Request 请求对象

想要直接访问 Request 对象时,需要在函数中声明Request类型的参数,FastAPI 就会自动传递 Request 对象给这个参数,我们就可以获取到 Request 对象及其属性信息,例如 header、url、cookie、session 等。

1
2
3
python复制代码@app.post("/login")
async def login(request: Request):
return {"Hello": request.url}

另外,如果想要获取请求的 cookie、header 信息,还可以通过在函数内声明 Cookie 类型、Header 类型的参数进行接收,FastAPI 也会帮我们自动处理并接收这些信息,非常方便。

Path、Query、Body、Form、Cookie、Header等类都继承自 Param 类,使用他们我们可以方便的声明获取不同类型的请求参数。

原创不易,如果小伙伴们觉得有帮助,麻烦点个赞再走呗~

最后,感谢女朋友在工作和生活中的包容、理解与支持 !

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

不丢失像素的切图

发表于 2021-11-27

图片切割

将一个原有图片根据传进来的横切数切成小图。

思路:

获取图片长宽算出切成多少块,然后判断是否丢失像素。

版本一:

Dingtalk_20211124164821.jpg

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
java复制代码File file = new File("btg.jpg"); // 项目目录下有名为btg.jpg的图片

FileInputStream fis = new FileInputStream(file);

BufferedImage image = ImageIO.read(fis); //把文件读到图片缓冲流中

int rows = 4; //定义图片要切分成多少块

int cols = 4;

int chunks = rows * cols;

int chunkWidth = image.getWidth() / cols; // 计算每一块小图片的高度和宽度

int chunkHeight = image.getHeight() / rows;

int count = 0;

BufferedImage imgs[] = new BufferedImage[chunks];

for (int x = 0; x < rows; x++) {
for (int y = 0; y < cols; y++) {
//初始化BufferedImage

imgs[count] = new BufferedImage(chunkWidth, chunkHeight, image.getType());

//画出每一小块图片

Graphics2D gr = imgs[count++].createGraphics();

gr.drawImage(image, 0, 0, chunkWidth, chunkHeight, chunkWidth * y, chunkHeight * x, chunkWidth * y + chunkWidth, chunkHeight * x + chunkHeight, null);

gr.dispose();

}

}

System.out.println("切分完成");

//保存小图片到文件中

for (int i = 0; i < imgs.length; i++) {
ImageIO.write(imgs[i], "jpg", new File("img" + i + ".jpg"));

}

System.out.println("小图片创建完成");

此时发现一个问题,每个图片都是固定大小。丢掉了一些像素,违背了我们的初衷

思路:

此时我们只能将最右边和最下边的宽高进行计算,右下角的宽高比较特殊,右下角呢个图片宽高=原图宽高-(算出的每格大小 * (列/行 - 1))

Dingtalk_20211124165912.jpg

具体实现:

方便前段使用将行列都从1开始计数

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
java复制代码public static void main(String[] args) throws Exception {
File file = new File("xxx"); // 项目目录下有名为btg.jpg的图片

FileInputStream fis = new FileInputStream(file);

BufferedImage image = ImageIO.read(fis); //把文件读到图片缓冲流中

//定义图片要切分成多少块
int rows = 7;

// 算出每块高度是多少
int chunkHeight = image.getHeight()/rows;

// 算出多少列
int cols = image.getWidth()/chunkHeight;

// 算出每列多少宽度
int chunkWidth = image.getWidth()/cols;

int chunks = rows * cols;

int count = 0;

BufferedImage imgs[] = new BufferedImage[chunks];

for (int i = 1; i <= rows; i++) {
for (int j = 1; j <= cols; j++) {

// 判断是不是最后一块
if (j == cols) {
// 算出最后一块宽度是多少
int wight = image.getWidth() - (chunkWidth * (j - 1));
if (i == rows) {
// 算出最后一块宽高
int hight = image.getHeight() - (chunkHeight * (i - 1));
imgs[count] = new BufferedImage(wight, hight, image.getType());
Graphics2D gr = imgs[count++].createGraphics();
// image原图 sx2 - sx1 = 图片宽度 sy2 - sy1 = 图片高度
gr.drawImage(image, 0, 0, wight, hight,
image.getWidth(), chunkHeight * (i - 1),
chunkWidth * (j - 1) , image.getHeight(), null);
gr.dispose();
System.out.println("sx1==="+wight+"sy1===="+(chunkHeight * (i - 1))+
"sx2===="+image.getWidth()+ "sy2===="+image.getHeight());
}else {
// 最后一列
imgs[count] = new BufferedImage(wight, chunkHeight, image.getType());
Graphics2D gr = imgs[count++].createGraphics();
// image原图 sx2 - sx1 = 图片宽度 sy2 - sy1 = 图片高度
gr.drawImage(image, 0, 0, wight, chunkHeight,
chunkWidth * (j - 1), chunkHeight * (i - 1),
image.getWidth(), (chunkHeight * (i - 1)) + chunkHeight, null);
gr.dispose();
System.out.println("sx1==="+(chunkWidth * (j - 1))+"sy1===="+(chunkHeight * (i - 1)) +
"sx2===="+ image.getWidth()+"sy2===="+((chunkHeight * (i - 1)) + chunkHeight));
}
}else {
if (i == rows) {
int hight = image.getHeight() - (chunkHeight * (i - 1));
imgs[count] = new BufferedImage(chunkWidth, hight, image.getType());
Graphics2D gr = imgs[count++].createGraphics();
gr.drawImage(image, 0, 0, chunkWidth, hight,
chunkWidth * (j-1), chunkHeight * (i-1),
chunkWidth * (j-1)+chunkWidth, image.getHeight(), null);
gr.dispose();
System.out.println("sx1==="+(chunkWidth * (j-1)+chunkWidth)+"sy1===="+(chunkHeight * (i-1))+"sx2===="+chunkWidth * (j-1)+chunkWidth
+"sy2===="+image.getHeight());
}else {
imgs[count] = new BufferedImage(chunkWidth, chunkHeight, image.getType());
Graphics2D gr = imgs[count++].createGraphics();
gr.drawImage(image, 0, 0, chunkWidth, chunkHeight,
chunkWidth * (j-1), chunkHeight * (i-1),
(chunkWidth * (j-1)) + chunkWidth, (chunkHeight * (i-1)) + chunkHeight, null);
gr.dispose();
System.out.println("sx1==="+(chunkWidth * (j-1))+
"sy1===="+(chunkHeight * (i-1))+
"sx2===="+(chunkWidth * (j-1) + chunkWidth)
+"sy2===="+(chunkHeight * (i-1) + chunkHeight));

}
}
ImageIO.write(imgs[count-1], "jpg", new File("D:\\bbb\\"+"img" + (count-1) + ".jpg"));
}
}
System.out.println("切分完成");

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

☕【并发技术系列】「Web请求读取系列」如何构建一个可重复读

发表于 2021-11-27

这是我参与11月更文挑战的第27天,活动详情查看:2021最后一次更文挑战

前提背景

项目中需要记录用户的请求参数便于后面查找问题,对于这种需求一般可以通过Spring中的拦截器或者是使Servlet中的过滤器来实现。这里我选择使用过滤器来实现,就是添加一个过滤器,然后在过滤器中获取到Request对象,将Reques中的信息记录到日志中。

问题介绍

在调用request.getReader之后重置HttpRequest:

有时候我们的请求是post,但我们又要对参数签名,这个时候我们需要获取到body的信息,但是当我们使用HttpServletRequest的getReader()和getInputStream()获取参数后,后面不管是框架还是自己想再次获取body已经没办法获取。当然也有一些其他的场景,可能需要多次获取的情况。

可能抛出类似以下的异常

1
kotlin复制代码java.lang.IllegalStateException: getReader() has already been called for this request

因此,针对这问题,给出一下解决方案:

定义过滤器解决

使用过滤器很快我实现了统一记录请求参数的的功能,整个代码实现如下:

1
2
3
4
5
6
7
8
9
10
java复制代码@Slf4j
@Component
public class CheckDataFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
Map<String, String[]> parameterMap = request.getParameterMap();
log.info("请求参数:{}", JSON.toJSONString(parameterMap));
filterChain.doFilter(request,response);
}
}

上面的实现方式对于GET请求没有问题,可以很好的记录前端提交过来的参数。对于POST请求就没那么简单了。根据POST请求中Content-Type类型我们常用的有下面几种:

  • application/x-www-form-urlencoded:这种方式是最常见的方式,浏览器原生的form表单就是这种方式提交。
  • application/json:这种方式也算是一种常见的方式,当我们在提交一个复杂的对象时往往采用这种方式。
  • multipart/form-data:这种方式通常在使用表单上传文件时会用。

注意:上面三种常见的POST方式我实现的过滤器有一种是无法记录到的,当Content-Type为application/json时,通过调用Request对象中getParameter相关方法是无法获取到请求参数的。

application/json解决方案及问题

想要该形式的请求参数能被打印,我们可以通过读取Request中流的方式来获取请求JSON请求参数,现在修改代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码@Slf4j
@Component
public class CheckDataFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
Map<String, String[]> parameterMap = request.getParameterMap();
log.info("请求参数:{}",JSON.toJSONString(parameterMap));
ByteArrayOutputStream out = new ByteArrayOutputStream();
IOUtils.copy(request.getInputStream(),out);
log.info("请求体:{}", out.toString(request.getCharacterEncoding()));
filterChain.doFilter(request,response);
}
}

上面的代码中我通过获取Request中的流来获取到请求提交到服务器中的JSON数据,最后在日志中能打印出客户端提交过来的JSON数据。但是最后接口的返回并没有成功,而且在Controller中也无法获取到请求参数,最后程序给出的错误提示关键信息为:Required request body is missing。

之所以会出现异常是因为Request中的流只能读取一次,我们在过滤器中读取之后如果后面有再次读取流的操作就会导致服务异常,简单的说就是Request中获取的流不支持重复读取。

所以这种方案Pass

扩展HttpServletRequest

HttpServletRequestWrapper

通过上面的分析我们知道了问题所在,对于Request中流无法重复读取的问题,我们要想办法让其支持重复读取。

难道我们要自己去实现一个Request,且我们的Request中的流还支持重复读取,想想就知道这样做很麻烦了。

幸运的是Servlet中提供了一个HttpServletRequestWrapper类,这个类从名字就能看出它是一个Wrapper类,就是我们可以通过它将原先获取流的方法包装一下,让它支持重复读取即可。

创建一个自定义类

继承HttpServletRequestWrapper实现一个CustomHttpServletRequest并且写一个构造函数来缓存body数据,先将RequestBody保存为一个byte数组,然后通过Servlet自带的HttpServletRequestWrapper类覆盖getReader()和getInputStream()方法,使流从保存的byte数组读取。

1
2
3
4
5
6
7
8
java复制代码public class CustomHttpServletRequest extends HttpServletRequestWrapper {
private byte[] cachedBody;
public CustomHttpServletRequest(HttpServletRequest request) throws IOException {
super(request);
InputStream is = request.getInputStream();
this.cachedBody = StreamUtils.copyToByteArray(is);
}
}
重写getReader()
1
2
3
4
5
java复制代码@Override
public BufferedReader getReader() throws IOException {
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(this.cachedBody);
return new BufferedReader(new InputStreamReader(byteArrayInputStream));
}
重写getInputStream()
1
2
3
4
java复制代码@Override
public ServletInputStream getInputStream() throws IOException {
return new CachedBodyServletInputStream(this.cachedBody);
}

然后再Filter中将ServletRequest替换为ServletRequestWrapper。代码如下:

实现ServletInputStream

创建一个继承了ServletInputStream的类

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
java复制代码public class CachedBodyServletInputStream extends ServletInputStream {
private InputStream cachedBodyInputStream;
public CachedBodyServletInputStream(byte[] cachedBody) {
this.cachedBodyInputStream = new ByteArrayInputStream(cachedBody);
}

@Override
public boolean isFinished() {
try {
return cachedBodyInputStream.available() == 0;
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return false;
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setReadListener(ReadListener readListener) {
throw new UnsupportedOperationException();
}
@Override
public int read() throws IOException {
return cachedBodyInputStream.read();
}
}

创建一个Filter加入到容器中

既然要加入到容器中,可以创建一个Filter,然后加入配置
我们可以简单的继承OncePerRequestFilter然后实现下面方法即可。

1
2
3
4
5
6
java复制代码@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
CustomHttpServletRequest customHttpServletRequest =
new CustomHttpServletRequest(httpServletRequest);
filterChain.doFilter(customHttpServletRequest, httpServletResponse);
}

然后,添加该Filter加入即可,在上面的过滤器中先调用了getParameterMap方法获取参数,然后再获取流,如果我先getInputStream然后再调用getParameterMap会导致参数解析失败。

例如,将过滤器中代码调整顺序为如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码@Slf4j
@Component
public class CheckDataFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//使用包装Request替换原始的Request
request = new CustomHttpServletRequest(request);
//读取流中的内容
ByteArrayOutputStream out = new ByteArrayOutputStream();
IOUtils.copy(request.getInputStream(),out);
log.info("请求体:{}", out.toString(request.getCharacterEncoding()));
Map<String, String[]> parameterMap = request.getParameterMap();
log.info("请求参数:{}",JSON.toJSONString(parameterMap));
filterChain.doFilter(request,response);
}
}

调整了getInputStream和getParameterMap这两个方法的调用时机,最后却会产生两种结果,这让我一度以为这个是个BUG。最后我从源码中知道了为啥会有这种结果,如果我们先调用getInputStream,这将会getParameterMap时不会去解析参数,以下代码是SpringBoot中嵌入的tomcat实现。

org.apache.catalina.connector.Request:

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
java复制代码protected void parseParameters() {
parametersParsed = true;
Parameters parameters = coyoteRequest.getParameters();
boolean success = false;
try {
// Set this every time in case limit has been changed via JMX
parameters.setLimit(getConnector().getMaxParameterCount());
// getCharacterEncoding() may have been overridden to search for
// hidden form field containing request encoding
Charset charset = getCharset();
boolean useBodyEncodingForURI = connector.getUseBodyEncodingForURI();
parameters.setCharset(charset);
if (useBodyEncodingForURI) {
parameters.setQueryStringCharset(charset);
}
// Note: If !useBodyEncodingForURI, the query string encoding is
// that set towards the start of CoyoyeAdapter.service()
parameters.handleQueryParameters();
if (usingInputStream || usingReader) {
success = true;
return;
}
String contentType = getContentType();
if (contentType == null) {
contentType = "";
}
int semicolon = contentType.indexOf(';');
if (semicolon >= 0) {
contentType = contentType.substring(0, semicolon).trim();
} else {
contentType = contentType.trim();
}
if ("multipart/form-data".equals(contentType)) {
parseParts(false);
success = true;
return;
}
if( !getConnector().isParseBodyMethod(getMethod()) ) {
success = true;
return;
}
if (!("application/x-www-form-urlencoded".equals(contentType))) {
success = true;
return;
}
int len = getContentLength();
if (len > 0) {
int maxPostSize = connector.getMaxPostSize();
if ((maxPostSize >= 0) && (len > maxPostSize)) {
Context context = getContext();
if (context != null && context.getLogger().isDebugEnabled()) {
context.getLogger().debug(
sm.getString("coyoteRequest.postTooLarge"));
}
checkSwallowInput();
parameters.setParseFailedReason(FailReason.POST_TOO_LARGE);
return;
}
byte[] formData = null;
if (len < CACHED_POST_LEN) {
if (postData == null) {
postData = new byte[CACHED_POST_LEN];
}
formData = postData;
} else {
formData = new byte[len];
}
try {
if (readPostBody(formData, len) != len) {
parameters.setParseFailedReason(FailReason.REQUEST_BODY_INCOMPLETE);
return;
}
} catch (IOException e) {
// Client disconnect
Context context = getContext();
if (context != null && context.getLogger().isDebugEnabled()) {
context.getLogger().debug(
sm.getString("coyoteRequest.parseParameters"), e);
}
parameters.setParseFailedReason(FailReason.CLIENT_DISCONNECT);
return;
}
parameters.processParameters(formData, 0, len);
} else if ("chunked".equalsIgnoreCase(
coyoteRequest.getHeader("transfer-encoding"))) {
byte[] formData = null;
try {
formData = readChunkedPostBody();
} catch (IllegalStateException ise) {
// chunkedPostTooLarge error
parameters.setParseFailedReason(FailReason.POST_TOO_LARGE);
Context context = getContext();
if (context != null && context.getLogger().isDebugEnabled()) {
context.getLogger().debug(
sm.getString("coyoteRequest.parseParameters"),
ise);
}
return;
} catch (IOException e) {
// Client disconnect
parameters.setParseFailedReason(FailReason.CLIENT_DISCONNECT);
Context context = getContext();
if (context != null && context.getLogger().isDebugEnabled()) {
context.getLogger().debug(
sm.getString("coyoteRequest.parseParameters"), e);
}
return;
}
if (formData != null) {
parameters.processParameters(formData, 0, formData.length);
}
}
success = true;
} finally {
if (!success) {
parameters.setParseFailedReason(FailReason.UNKNOWN);
}
}
}

上面代码从方法名字可以看出就是用来解析参数的,其中有一处关键的信息如下:

1
2
3
4
java复制代码        if (usingInputStream || usingReader) {
success = true;
return;
}

这个判断的意思是如果usingInputStream或者usingReader为true,将导致解析中断直接认为已经解析成功了。这个是两个属性默认都为false,而将它们设置为true的地方只有两处,分别为getInputStream和getReader,源码如下:

getInputStream()
1
2
3
4
5
6
7
8
9
10
11
java复制代码public ServletInputStream getInputStream() throws IOException {
if (usingReader) {
throw new IllegalStateException(sm.getString("coyoteRequest.getInputStream.ise"));
}
//设置usingInputStream 为true
usingInputStream = true;
if (inputStream == null) {
inputStream = new CoyoteInputStream(inputBuffer);
}
return inputStream;
}
getReader()
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
java复制代码public BufferedReader getReader() throws IOException {
if (usingInputStream) {
throw new IllegalStateException(sm.getString("coyoteRequest.getReader.ise"));
}
if (coyoteRequest.getCharacterEncoding() == null) {
// Nothing currently set explicitly.
// Check the content
Context context = getContext();
if (context != null) {
String enc = context.getRequestCharacterEncoding();
if (enc != null) {
// Explicitly set the context default so it is visible to
// InputBuffer when creating the Reader.
setCharacterEncoding(enc);
}
}
}
//设置usingReader为true
usingReader = true;
inputBuffer.checkConverter();
if (reader == null) {
reader = new CoyoteReader(inputBuffer);
}
return reader;
}

为何在tomcat要如此实现呢?tomcat如此实现可能是有它的道理,作为Servlet容器那必须按照Servlet规范来实现,通过查询相关文档还真就找到了Servlet规范中的内容,下面是Servlet3.1规范中关于参数解析的部分内容:

总结

为了获取请求中的参数我们要解决的核心问题就是让流可以重复读取即可,同时注意先读取流会导致getParameterMap时参数无法解析这两点关键点即可。

参考资料

  • www.cnblogs.com/alter888/p/…
  • www.iteye.com/blog/zhangb…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

JDK中Lambda表达式的序列化与SerializedLa

发表于 2021-11-27

前提

笔者在下班空余时间想以Javassist为核心基于JDBC写一套摒弃反射调用的轻量级的ORM框架,过程中有研读mybatis、tk-mapper、mybatis-plus和spring-boot-starter-jdbc的源代码,其中发现了mybatis-plus中的LambdaQueryWrapper可以获取当前调用的Lambda表达式中的方法信息(实际上是CallSite的信息),这里做一个完整的记录。本文基于JDK11编写,其他版本的JDK不一定合适。

神奇的Lambda表达式序列化

之前在看Lambda表达式源码实现的时候没有细看LambdaMetafactory的注释,这个类顶部大量注释中其中有一段如下:

简单翻译一下就是:可序列化特性。一般情况下,生成的函数对象(这里应该是特指基于Lambda表达式实现的特殊函数对象)不需要支持序列化特性。如果需要支持该特性,FLAG_SERIALIZABLE(LambdaMetafactory的一个静态整型属性,值为1 << 0)可以用来表示函数对象是序列化的。一旦使用了支持序列化特性的函数对象,那么它们以SerializedLambda类的形式序列化,这些SerializedLambda实例需要额外的”捕获类”的协助(捕获类,如MethodHandles.Lookup的caller参数所描述),详细信息参阅SerializedLambda。

在LambdaMetafactory的注释中再搜索一下FLAG_SERIALIZABLE,可以看到这段注释:

大意为:设置了FLAG_SERIALIZABLE标记后生成的函数对象实例会实现Serializable接口,并且会存在一个名字为writeReplace的方法,该方法的返回值类型为SerializedLambda。调用这些函数对象的方法(前面提到的”捕获类”)的调用者必须存在一个名字为$deserializeLambda$的方法,如SerializedLambda类所描述。

最后看SerializedLambda的描述,注释有四大段,这里贴出并且每小段提取核心信息:

各个段落大意如下:

  • 段落一:SerializedLambda是Lambda表达式的序列化形式,这类存储了Lambda表达式的运行时信息
  • 段落二:为了确保Lambda表达式的序列化实现正确性,编译器或者语言类库可以选用的一种方式是确保writeReplace方法返回一个SerializedLambda实例
  • 段落三:SerializedLambda提供一个readResolve方法,其职能类似于调用”捕获类”中静态方法$deserializeLambda$(SerializedLambda)并且把自身实例作为入参,该过程理解为反序列化过程
  • 段落四: 序列化和反序列化产生的函数对象的身份敏感操作的标识形式(如System.identityHashCode()、对象锁定等等)是不可预测的

最终的结论就是:如果一个函数式接口实现了Serializable接口,那么它的实例就会自动生成了一个返回SerializedLambda实例的writeReplace方法,可以从SerializedLambda实例中获取到这个函数式接口的运行时信息。这些运行时信息就是SerializedLambda的属性:

属性 含义
capturingClass “捕获类”,当前的Lambda表达式出现的所在类
functionalInterfaceClass 名称,并且以”/“分隔,返回的Lambda对象的静态类型
functionalInterfaceMethodName 函数式接口方法名称
functionalInterfaceMethodSignature 函数式接口方法签名(其实是参数类型和返回值类型,如果使用了泛型则是擦除后的类型)
implClass 名称,并且以”/“分隔,持有该函数式接口方法的实现方法的类型(实现了函数式接口方法的实现类)
implMethodName 函数式接口方法的实现方法名称
implMethodSignature 函数式接口方法的实现方法的方法签名(实是参数类型和返回值类型)
instantiatedMethodType 用实例类型变量替换后的函数式接口类型
capturedArgs Lambda捕获的动态参数
implMethodKind 实现方法的MethodHandle类型

举个实际的例子,定义一个实现了Serializable的函数式接口并且调用它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码public class App {

@FunctionalInterface
public interface CustomerFunction<S, T> extends Serializable {

T convert(S source);
}

public static void main(String[] args) throws Exception {
CustomerFunction<String, Long> function = Long::parseLong;
Long result = function.convert("123");
System.out.println(result);
Method method = function.getClass().getDeclaredMethod("writeReplace");
method.setAccessible(true);
SerializedLambda serializedLambda = (SerializedLambda)method.invoke(function);
System.out.println(serializedLambda.getCapturingClass());
}
}

执行的DEBUG信息如下:

这样就能获取到函数式接口实例在调用方法时候的调用点运行时信息,甚至连泛型参数擦除前的类型都能拿到,那么就可以衍生出很多技巧。例如:

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
java复制代码public class ConditionApp {

@FunctionalInterface
public interface CustomerFunction<S, T> extends Serializable {

T convert(S source);
}

@Data
public static class User {

private String name;
private String site;
}

public static void main(String[] args) throws Exception {
Condition c1 = addCondition(User::getName, "=", "throwable");
System.out.println("c1 = " + c1);
Condition c2 = addCondition(User::getSite, "IN", "('throwx.cn','vlts.cn')");
System.out.println("c1 = " + c2);
}

private static <S> Condition addCondition(CustomerFunction<S, String> function,
String operation,
Object value) throws Exception {
Condition condition = new Condition();
Method method = function.getClass().getDeclaredMethod("writeReplace");
method.setAccessible(true);
SerializedLambda serializedLambda = (SerializedLambda) method.invoke(function);
String implMethodName = serializedLambda.getImplMethodName();
int idx;
if ((idx = implMethodName.lastIndexOf("get")) >= 0) {
condition.setField(Character.toLowerCase(implMethodName.charAt(idx + 3)) + implMethodName.substring(idx + 4));
}
condition.setEntityKlass(Class.forName(serializedLambda.getImplClass().replace("/", ".")));
condition.setOperation(operation);
condition.setValue(value);
return condition;
}

@Data
private static class Condition {

private Class<?> entityKlass;
private String field;
private String operation;
private Object value;
}
}

// 执行结果
c1 = ConditionApp.Condition(entityKlass=class club.throwable.lambda.ConditionApp$User, field=name, operation==, value=throwable)
c1 = ConditionApp.Condition(entityKlass=class club.throwable.lambda.ConditionApp$User, field=site, operation=IN, value=('throwx.cn','vlts.cn'))

很多人会担心反射调用的性能,其实在高版本的JDK,反射性能已经大幅度优化,十分逼近直接调用的性能,更何况有些场景是少量反射调用场景,可以放心使用。

前面花大量篇幅展示了SerializedLambda的功能和使用,接着看Lambda表达式的序列化与反序列化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码public class SerializedLambdaApp {

@FunctionalInterface
public interface CustomRunnable extends Serializable {

void run();
}

public static void main(String[] args) throws Exception {
invoke(() -> {
});
}

private static void invoke(CustomRunnable customRunnable) throws Exception {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(customRunnable);
oos.close();
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()));
Object target = ois.readObject();
System.out.println(target);
}
}

结果如下图:

Lambda表达式序列化原理

关于Lambda表达式序列化的原理,可以直接参考ObjectStreamClass、ObjectOutputStream和ObjectInputStream的源码,这里直接说结论:

  • 前提条件:待序列化对象需要实现Serializable接口
  • 待序列化对象中如果存在writeReplace方法,则直接基于传入的实例反射调用此方法得到的返回值类型作为序列化的目标类型,对于Lambda表达式就是SerializedLambda类型
  • 反序列化的过程刚好是逆转的过程,调用的方法为readResolve,刚好前面提到SerializedLambda也存在同名的私有方法
  • Lambda表达式的实现类型是VM生成的模板类,从结果上观察,序列化前的实例和反序列化后得到的实例属于不同的模板类,对于前一小节的例子某次运行的结果中序列化前的模板类为club.throwable.lambda.SerializedLambdaApp$$Lambda$14/0x0000000800065840,反序列化后的模板类为club.throwable.lambda.SerializedLambdaApp$$Lambda$26/0x00000008000a4040

ObjectStreamClass是序列化和反序列化实现的类描述符,关于对象序列化和反序列化的类描述信息可以从这个类里面的成员属性找到,例如这里提到的writeReplace和readResolve方法

图形化的过程如下:

获取SerializedLambda的方式

通过前面的分析,得知有两种方式可以获取Lambda表达式的SerializedLambda实例:

  • 方式一:基于Lambda表达式实例和Lambda表达式的模板类反射调用writeReplace方法,得到的返回值就是SerializedLambda实例
  • 方式二:基于序列化和反序列化的方式获取SerializedLambda实例

基于这两种方式可以分别编写例子,例如反射方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码// 反射方式
public class ReflectionSolution {

@FunctionalInterface
public interface CustomerFunction<S, T> extends Serializable {

T convert(S source);
}

public static void main(String[] args) throws Exception {
CustomerFunction<String, Long> function = Long::parseLong;
SerializedLambda serializedLambda = getSerializedLambda(function);
System.out.println(serializedLambda.getCapturingClass());
}

public static SerializedLambda getSerializedLambda(Serializable serializable) throws Exception {
Method writeReplaceMethod = serializable.getClass().getDeclaredMethod("writeReplace");
writeReplaceMethod.setAccessible(true);
return (SerializedLambda) writeReplaceMethod.invoke(serializable);
}
}

序列化和反序列方式会稍微复杂,因为ObjectInputStream.readObject()方法会最终回调SerializedLambda.readResolve()方法,导致返回的结果是一个新模板类承载的Lambda表达式实例,所以这里需要想办法中断这个调用提前返回结果,方案是构造一个和SerializedLambda相似但是不存在readResolve()方法的影子类型:

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
java复制代码package cn.vlts;
import java.io.Serializable;

/**
* 这里注意一定要和java.lang.invoke.SerializedLambda同名,可以不同包名,这是为了"欺骗"ObjectStreamClass中有个神奇的类名称判断classNamesEqual()方法
*/
@SuppressWarnings("ALL")
public class SerializedLambda implements Serializable {
private static final long serialVersionUID = 8025925345765570181L;
private Class<?> capturingClass;
private String functionalInterfaceClass;
private String functionalInterfaceMethodName;
private String functionalInterfaceMethodSignature;
private String implClass;
private String implMethodName;
private String implMethodSignature;
private int implMethodKind;
private String instantiatedMethodType;
private Object[] capturedArgs;

public String getCapturingClass() {
return capturingClass.getName().replace('.', '/');
}
public String getFunctionalInterfaceClass() {
return functionalInterfaceClass;
}
public String getFunctionalInterfaceMethodName() {
return functionalInterfaceMethodName;
}
public String getFunctionalInterfaceMethodSignature() {
return functionalInterfaceMethodSignature;
}
public String getImplClass() {
return implClass;
}
public String getImplMethodName() {
return implMethodName;
}
public String getImplMethodSignature() {
return implMethodSignature;
}
public int getImplMethodKind() {
return implMethodKind;
}
public final String getInstantiatedMethodType() {
return instantiatedMethodType;
}
public int getCapturedArgCount() {
return capturedArgs.length;
}
public Object getCapturedArg(int i) {
return capturedArgs[i];
}
}


public class SerializationSolution {

@FunctionalInterface
public interface CustomerFunction<S, T> extends Serializable {

T convert(S source);
}

public static void main(String[] args) throws Exception {
CustomerFunction<String, Long> function = Long::parseLong;
cn.vlts.SerializedLambda serializedLambda = getSerializedLambda(function);
System.out.println(serializedLambda.getCapturingClass());
}

private static cn.vlts.SerializedLambda getSerializedLambda(Serializable serializable) throws Exception {
try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos)) {
oos.writeObject(serializable);
oos.flush();
try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray())) {
@Override
protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
Class<?> klass = super.resolveClass(desc);
return klass == java.lang.invoke.SerializedLambda.class ? cn.vlts.SerializedLambda.class : klass;
}
}) {
return (cn.vlts.SerializedLambda) ois.readObject();
}
}
}
}

被遗忘的$deserializeLambda$方法

前文提到,Lambda表达式实例反序列化的时候会调用java.lang.invoke.SerializedLambda.readResolve()方法,神奇的是,此方法源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码private Object readResolve() throws ReflectiveOperationException {
try {
Method deserialize = AccessController.doPrivileged(new PrivilegedExceptionAction<>() {
@Override
public Method run() throws Exception {
Method m = capturingClass.getDeclaredMethod("$deserializeLambda$", SerializedLambda.class);
m.setAccessible(true);
return m;
}
});

return deserialize.invoke(null, this);
}
catch (PrivilegedActionException e) {
Exception cause = e.getException();
if (cause instanceof ReflectiveOperationException)
throw (ReflectiveOperationException) cause;
else if (cause instanceof RuntimeException)
throw (RuntimeException) cause;
else
throw new RuntimeException("Exception in SerializedLambda.readResolve", e);
}
}

看起来就是”捕获类”中存在一个这样的静态方法:

1
2
3
4
5
6
java复制代码class CapturingClass {

private static Object $deserializeLambda$(SerializedLambda serializedLambda){
return [serializedLambda] => Lambda表达式实例;
}
}

可以尝试检索”捕获类”中的方法列表:

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
java复制代码public class CapturingClassApp {

@FunctionalInterface
public interface CustomRunnable extends Serializable {

void run();
}

public static void main(String[] args) throws Exception {
invoke(() -> {
});
}

private static void invoke(CustomRunnable customRunnable) throws Exception {
Method writeReplaceMethod = customRunnable.getClass().getDeclaredMethod("writeReplace");
writeReplaceMethod.setAccessible(true);
java.lang.invoke.SerializedLambda serializedLambda = (java.lang.invoke.SerializedLambda)
writeReplaceMethod.invoke(customRunnable);
Class<?> capturingClass = Class.forName(serializedLambda.getCapturingClass().replace("/", "."));
ReflectionUtils.doWithMethods(capturingClass, method -> {
System.out.printf("方法名:%s,修饰符:%s,方法参数列表:%s,方法返回值类型:%s\n", method.getName(),
Modifier.toString(method.getModifiers()),
Arrays.toString(method.getParameterTypes()),
method.getReturnType().getName());
},
method -> Objects.equals(method.getName(), "$deserializeLambda$"));
}
}

// 执行结果
方法名:$deserializeLambda$,修饰符:private static,方法参数列表:[class java.lang.invoke.SerializedLambda],方法返回值类型:java.lang.Object

果真是存在一个和之前提到的java.lang.invoke.SerializedLambda注释描述一致的”捕获类”的SerializedLambda实例转化为Lambda表达式实例的方法,因为搜索多处地方都没发现此方法的踪迹,猜测$deserializeLambda$是方法由VM生成,并且只能通过反射的方法调用,算是一个隐藏得比较深的技巧。

小结

JDK中的Lambda表达式功能已经发布很多年了,想不到这么多年后的今天才弄清楚其序列化和反序列化方式,虽然这不是一个复杂的问题,但算是最近一段时间看到的比较有意思的一个知识点。

参考资料:

  • JDK11源码
  • Mybatis-Plus相关源码

(本文完 e-a-20211127 c-2-d)

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

你有没有觉得邮件发送人固定配置在yml文件中是不妥当的呢?S

发表于 2021-11-27

这是我参与11月更文挑战的第27天,活动详情查看:2021最后一次更文挑战

明月当天,不知道你有没有思念的人

前言

之前其实已经写过SpringBoot异步发送邮件,但是今天在一个小项目中要用到发送邮件时,我突然觉得邮件发送人只有一个,并且固定写在yml文件中,就是非常的不妥当,就想着怎么整成一个动态的。

在写之前已经翻过很多博客了,该踩的坑都踩的差不多了,我是实现之后写的文章,有问题大家可以一起交流。

小声bb(对于CSDN我已经逐渐变得麻木了,真的简称CV大法现场)。

于是就有了下面这篇文章啦….

一、需求分析

默认大家都已经会 SpringBoot 集成 邮件发送啦哈,不行的,点一下上文的链接啦。

我先说说我想要达到什么样的效果:

  1. 邮件发送人可以是多个,yml文件中是兜底配置(即数据库中没有一个可用时,使用yml文件中配置的邮件发送人)
  2. 项目启动后,我也可以临时增加邮件发送人,或者禁用掉某个邮件发送人(操作完也无需重启项目即可生效)
  3. 发送邮件内容为html;另外异步发送邮件(可有可无,大家都会)

思路其实蛮简单的,就只要做到每次我们新添加或者修改邮件发送人配置的时候,对JavaSendMailImpl这个类重新初始化即可。这个地方没啥可讲的,就是不让框架给我们自动配置,我们手动来即可。

二、详细步骤

2.1、编码

1)yml配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
yml复制代码spring:  
mail:
host: smtp.163.com
username: nxxxxxx@163.com
password: IXXXXXXXXXN(开启允许第三方登录后的授权码)
default-encoding: utf-8
protocol: smtps
properties:
mail:
smtp:
port: 465
auth: true
starttls:
enable: true
required: true

注意:关于邮件的协议protocol:smtps的配置,我最开始也是配置的smtp,我当时报的错误是一个no provider for smtp错误,我之前也写过一直用的是这个smtp协议,但是报了这个错误,我就去搜索,然后找到有篇博客说,

SMTPS协议

SMTPS (SMTP-over-SSL)是SMTP协议基于SSL安全协议之上的一种变种协议,它继承了SSL安全协议的非对称加密的高度安全可靠性,可防止邮件泄露。SMTPS和SMTP协议一样,也是用来发送邮件的,只是更安全些,防止邮件被黑客截取泄密,还可实现邮件发送者抗抵赖功能。防止发送者发送之后删除已发邮件,拒不承认发送过这样一份邮件。端口465和587便是基于SMTPS协议开放的。

465端口(SMTPS)︰它是SMTPS协议服务所使用的其中一个端口,它在邮件的传输过程中是加密传输(SSL/TLS)的,相比于SMTP协议攻击者无法获得邮件内容,邮件在一开始就被保护了起来。

所以实际上我们使用的配置应该是stmps。


另外建个properties资源类 与 配置文件一一对应

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码/**
* @author crush
*/
@Data
@Component
@ConfigurationProperties(prefix = "spring.mail")
public class MailProperties {
/** * 用户名 */
private String username;
/** * 授权码 */
private String password;
/** * host */
private String host;
/** * 端口 */
private Integer port;
/*** 协议 */
private String protocol;
/** * 默认编码*/
private String defaultEncoding;
}

2.2、建表

根据yml文件,我们大致知道了要建立张什么样的数据表了哈。

image-20211127003820993

这些大家都可以自定义哈,根据自己需求来建哈。

根据数据表建一个pojo类。

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
java复制代码/**
* @Author: crush
* @Date: 2021-11-26 18:28
* version 1.0
*/
@Data
@Accessors(chain = true)
@TableName("tb_email")
public class MailPO {

private String emailHost;
private String emailUsername;
private String emailPassword;
private Integer emailPort=465;
/** * 协议 */
private String protocol="smtps";

/** * 默认编码 */
private String defaultEncoding="utf-8";
/**
* 使用状态,1:正在使用,2:禁用,3:停用
* TODO 后期应该更改为 枚举类来进行实现
*/
private Integer state=1;
/** * 创建时间 */
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
/*** 修改时间 */
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
}

如果不是用mybatis-plus 可以把创建时间和修改时间去掉@TableField(fill = FieldFill.INSERT)是Mybatis-plus中的注解。另外我主键是设置了自增,所以就空了。至于返回的类我用的vo包下的。

2.3、mapper、service层

1
2
3
java复制代码@Repository
public interface MailMapper extends BaseMapper<MailPO> {
}

service

1
2
3
4
5
6
7
8
9
10
11
java复制代码/**
* @Author: crush
* @Date: 2021-11-26 15:55
* version 1.0
*/
public interface MailService {

void send(MailDTO mailDTO);

boolean addMailPerson(MailPO mailPO);
}

impl

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
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
java复制代码import cn.hutool.core.util.IdUtil;
/**
* @author crush
* 邮箱发送实现类
*/
@Service
public class MailServiceImpl implements MailService {

@Autowired
MailSenderConfig senderConfig;

@Autowired
MailProperties mailProperties;

@Autowired
MailMapper mailMapper;

// 这里之前配置了一个线程池,上文的链接中有,就不说了哈
// @Async("taskExecutor")
@Override
public void send(MailDTO mailDTO) {
String context = "<!DOCTYPE html>\n" +
"<html lang=\"en\">\n" +
"\n" +
"<head>\n" +
" <meta charset=\"UTF-8\" />\n" +
" <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n" +
" <title>xxxx邮件</title>\n" +
" <style>\n" +
" body {\n" +
" margin: 0;\n" +
" padding: 0;\n" +
" }\n" +
" \n" +
" .email {\n" +
" position: relative;\n" +
" width: 100%;\n" +
" /* background-color: rgba(0, 0, 0, 1); */\n" +
" }\n" +
" \n" +
" .main {\n" +
" left: 0;\n" +
" right: 0;\n" +
" margin: auto;\n" +
" width: 80%;\n" +
" max-width: 800px;\n" +
" box-sizing: content-box;\n" +
" }\n" +
" \n" +
" .main .title {\n" +
" /* color: white; */\n" +
" display: inline-flex;\n" +
" align-items: center;\n" +
" }\n" +
" \n" +
" .main .title span {\n" +
" margin: 0 10px;\n" +
" }\n" +
" \n" +
" .main table {\n" +
" width: 100%;\n" +
" }\n" +
" \n" +
" .main table tbody td {\n" +
" /* background-color: white; */\n" +
" padding: 20px;\n" +
" text-align: left;\n" +
" border-bottom: 1px solid rgb(161, 161, 161);\n" +
" }\n" +
" \n" +
" tfoot td p {\n" +
" color: rgb(161, 161, 161);\n" +
" font-size: 13px;\n" +
" }\n" +
" \n" +
" a {\n" +
" color: rgb(161, 161, 161);\n" +
" text-decoration: none;\n" +
" }\n" +
" \n" +
" a:hover {\n" +
" border-bottom: 1px solid rgb(161, 161, 161);\n" +
" }\n" +
" </style>\n" +
"</head>\n" +
"\n" +
"<body>\n" +
" <div class=\"email\">\n" +
" <div class=\"main\">\n" +
" <table>\n" +
" <thead>\n" +
" <tr>\n" +
" <td>\n" +
" <h1 class=\"title\">\n" +
" <img width=\"60\" src=\"xxxxx\" alt=\"\" />\n" +
" <span>" + mailDTO.getTitle() + "</span>\n" +
" </h1>\n" +
" </td>\n" +
" </tr>\n" +
" </thead>\n" +
" <tbody>\n" +
" <tr>\n" +
" <td>\n" +
" " + mailDTO.getContent() + "\n" +
" </td>\n" +
" </tr>\n" +
" </tbody>\n" +
" <tfoot>\n" +
" <tr>\n" +
" <td>\n" +
" <p>邮件由系统自动发送,请勿直接回复。</p>\n" +
" <p>官方网站:\n" +
" <a href=\"https://blog.csdn.net/weixin_45821811?spm=1000.2115.3001.5343\">宁在春博客</a>\n" +
" </p>\n" +
" </td>\n" +
" </tr>\n" +
" </tfoot>\n" +
" </table>\n" +
" </div>\n" +
" </div>\n" +
"</body>\n" +
"\n" +
"</html>";

JavaMailSenderImpl mailSender = senderConfig.getSender();
//创建一个SimpleMailMessage对象
MimeMessage mimeMessage = mailSender.createMimeMessage();
//需要创建一个MimeMessageHelper对象,相关参数和简单邮件类似
try {
MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true);
//发件人
helper.setFrom(mailSender.getUsername());
//收件人 这个收件人可以是数组的,只是我这只需要单个 就没多做了。
helper.setTo(mailDTO.getMail());
helper.setSubject("验证码");
//将邮件内容设置为html格式
// 发送
helper.setText( context, true);
mailSender.send(mimeMessage);
} catch (MessagingException e) {
e.printStackTrace();
}

}

// 添加就清空初始化的信息,重新初始化一遍即可。
@Override
public boolean addMailPerson(MailPO mailPO) {
if(mailMapper.insert(mailPO)>0){
senderConfig.clear();
senderConfig.buildMailSender();
return true;
}
return false;
}
}

用到的MailDto

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码/**
* @author crush
* 邮箱发送-前端传输参数
*/
@Data
public class MailDTO implements Serializable {

/*** 接受邮箱账户*/
private String mail;
/*** 邮箱标题*/
private String title;
/** * 要发送的内容*/
private String content;
}

2.4、MailSenderConfig 配置类

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
java复制代码/**
* @author crush
*/
@Slf4j
@Component
@AllArgsConstructor
public class MailSenderConfig {

private final List<JavaMailSenderImpl> senderList;

private final MailProperties mailProperties;

private final MailMapper mailMapper;

/**
* 初始化 sender
* PostConstruct注解用于需要在依赖注入完成后执行任何初始化的方法。 必须在类投入使用之前调用此方法
* 因为刚开始我觉得这种方式(@PostConstruct) 不合适,就是没能做到修改了马上就能用的那种感觉。
* 但是后来写完才发现,其实只要每次添加新的邮件发送人时,都重新初始化一次就可以了。
* 后来我又用启动事件监听器。@PostConstruct 后来就没去测试了。
* 理论添加、修改完 调用这个初始化方法就可以了。
*/
// @PostConstruct
public void buildMailSender() {
log.info("初始化mailSender");
List<MailPO> mails = mailMapper.selectList(new QueryWrapper<MailPO>().eq("state", 1));
/**
* 需求:原本就是打算做成一个动态的邮件发送人,因为如果总是用一个邮件发送验证码或者是那种打扰短信,速度一旦太过于频繁,就会造成邮件发送错误。
* 思路:从数据库中拿到所有可用的邮件发送人,然后封装起来,之后发送邮件时,再进行随机的选择即可。
* 另外一种方式就是这是动态的。
* 最后就是加个兜底的,如果数据库中查询不到邮件发送人,我们使用配置文件中的发送邮件的配置。
*/
if(mails!=null&&!mails.isEmpty()){
mails.forEach(mail -> {
JavaMailSenderImpl javaMailSender = new JavaMailSenderImpl();
javaMailSender.setDefaultEncoding(mail.getDefaultEncoding());
javaMailSender.setHost(mail.getEmailHost());
javaMailSender.setPort(mail.getEmailPort());
javaMailSender.setProtocol(mail.getProtocol());
javaMailSender.setUsername(mail.getEmailUsername());
javaMailSender.setPassword(mail.getEmailPassword());
// 添加数据
senderList.add(javaMailSender);
});
}
else{
JavaMailSenderImpl javaMailSender = new JavaMailSenderImpl();
javaMailSender.setDefaultEncoding(mailProperties.getDefaultEncoding());
javaMailSender.setHost(mailProperties.getHost());
javaMailSender.setPort(mailProperties.getPort());
javaMailSender.setProtocol(mailProperties.getProtocol());
javaMailSender.setUsername(mailProperties.getUsername());
javaMailSender.setPassword(mailProperties.getPassword());
// 添加数据
senderList.add(javaMailSender);
}

}

/**
* 获取MailSender
*
* @return CustomMailSender
*/
public JavaMailSenderImpl getSender() {
if (senderList.isEmpty()) {
buildMailSender();
}
// 随机返回一个JavaMailSender
return senderList.get(new Random().nextInt(senderList.size()));
}

/**
* 清理 sender
*/
public void clear() {
senderList.clear();
}
}

2.5、监听器

一两句没啥说的,可以直接通过idea进去看源码上的doc注解。下次再一起研究。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码/**
* 初始化操作
* 目前只定义了动态设置邮件发送人的操作
* @Author: crush
* @Date: 2021-11-26 19:51
* version 1.0
*/
@Slf4j
@Configuration
@Order(Ordered.HIGHEST_PRECEDENCE)
public class StartListener implements ApplicationListener<ApplicationStartedEvent> {

MailSenderConfig mailSenderConfig;

public StartListener(MailSenderConfig mailSenderConfig) {
this.mailSenderConfig = mailSenderConfig;
}

@SneakyThrows
@Override
public void onApplicationEvent(@NotNull ApplicationStartedEvent event) {
this.mailSenderConfig.buildMailSender();
}
}

2.6、controller

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
java复制代码/**
* @Author: crush
* @Date: 2021-11-26 16:10
* version 1.0
*/
@RestController
@RequestMapping("/email")
public class MailController {

@Autowired
private MailService mailService;

@PostMapping("/send")
public String send(@RequestBody MailDTO mailDTO){
mailService.send(mailDTO);
return "发送成功!!!可能会稍有延迟,请查看邮箱信息!!";
}

@PostMapping("/addConfig")
public String addMailPerson(@RequestBody MailPO mailPO){
String message=mailService.addMailPerson(mailPO)?"添加成功!!!不过,请注意:可能会有延迟":"添加失败,请稍后重试!!";
return message;
}

}

三、测试

image-20211127011047427

模板大致就是如下状态吧。 具体样式大家再自己调吧 😁

image-20211127011111724

​

是添加进去的

image-20211127011530374

多点了一次哈。

image-20211127011522418

我再点击发送邮件,因为是随机数的方式,我们多测试几次,总会用到这个错误的邮件发送人的,用到了就表示我们已经成功啦哈。

因为添加的随便输入的,肯定是失败的哈。但是可以确定我们用到了我们项目启动后加入的邮件发送人啦。 你们可以填入争取的试一试。

image-20211127011707568

结束了结束啦。

没写小demo,没啥源码。

后语

大家一起加油!!!如若文章中有不足之处,请大家及时指出,在此郑重感谢。

纸上得来终觉浅,绝知此事要躬行。

大家好,我是博主宁在春:主页

一名喜欢文艺却踏上编程这条道路的小青年。

希望:我们,待别日相见时,都已有所成。


难得回到后端肝篇文,又拾起后端了,之后还会接着写Vue的,肯定会把专栏写完的。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Javaer 都需要知道的 G1 垃圾收集器

发表于 2021-11-27

这是我参与11月更文挑战的第27天,活动详情查看:2021最后一次更文挑战

G1 收集器

G1 收集器是面向服务端的一款垃圾收集器。其设计目标是为了取代 CMS 收集器的。G1 收集器有内存整理的过程,所以不会产生内存碎片,而且 STW 的停顿时间更加可控,G1 收集器添加了预测机制,用户可以指定停顿时间。

1 Region

在传统的垃圾收集器中,新生代和老年代的内存是连续的。而在 G1 收集器虽然保留了新生代和老年代,但是他们的内存可以是不连续的。G1 收集器把内存划分为一个个的 Region,新生代和老年代由许多不连续的 Region 组成。

G1内存划分

图片来源:https://tech.meituan.com/2016/09/23/g1.html
可以看到,虽然保留了新生代和老年代的概念,但是二者之间不再是物理隔离了。而是由不同的 Region 构成。在 G1 收集器之前的其他收集器都是在整个新生代,或者老年代范围内进行垃圾收集,G1 收集器则是在 Region 内进行。G1 收集器会维护一个优先列表,根据回收时间,优先回收价值最大的 Region。G1 收集器的内存碎片整理也是在两个不同的 Region 之间通过”复制“算法进行的。

可以通过 -XX:G1HeapRegionSize设定一个 Region 的大小,取值范围从1M到32M,且是2的指数。

2 Remembered Set

我们之前说过 GC 是发生在新生代或者是老年代的。但是在进行可达性分析时,我们似乎有意避开了一个特殊的情况。如果老年代的对象引用了新生代的对象,那么在进行 新生代的 GC 时,我们需要进行整个老年代的扫描,获取引用关系,停顿时间将大大提高。

其实 JVM 并没有在新生代 GC 时扫描整个老年代。而是利用了空间换时间的办法。JVM 使用了一种叫卡表(Card Table)的数据结构来记录老年代对象到新生代的引用,当一个对象的引用关系发生改变时,首先去更新这张表(这个动作称为 Write Barrier 或者叫写屏障)。这样,在新生代进行 GC 时可以扫描这张表获取引用关系,而不必扫描整个老年代。

同样的问题,在 G1 收集器里也会出现。如果出现跨 Region 之间的引用关系,就需要扫描所有的 Region 了。与卡表类似,在 G1 收集器里出现了 Remembered Set,它的主要功能就是解决跨 Region 的引用问题。在 G1 中每一个 Region 都有一个与之对应的 Remembered Set,在进行引用类型的写操作时,同样会产生写屏障,然后会检查是否属于两个不同的 Region,如果是的话,会将卡表里的信息同步到对应的 Remembered Set 中,同样在进行 GC 时,只要扫描 Remembered Set 就可以了。

3 GC 过程

G1 的收集过程与 CMS 很相似。主要分为 4 个步骤:

  • 初始标记(Initial Marking)

初始阶段标记 GC Roots 能直接关联到的对象。这一阶段需要停顿线程,但是耗时较短。

  • 并发标记(Concurrent Marking)

这一阶段是进行对象的可达性分析。耗时较长,但是可以与用户线程并行。

  • 最终标记(Final Marking)

同样,这一阶段是为了修正在并发标记时用户线程变更的内容。其实是 JVM 把并发标记阶段的变更都记录在了 Remembered Set Logs 里,在这一阶段只要把 Remembered Set Logs 的数据合并到 Remembered Set 里就可以了。这一阶段需要停顿线程,但是可以并行标记。

  • 筛选回收(Live Data Counting and Evacuation)

这一阶段首先对各个 Region 进行排序,然后根据设定的停顿时间来定制回收计划。最后进行垃圾回收。

G1收集器

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

落地网关soul(shenyu)过程中的一些实践 前言 网关

发表于 2021-11-27

这是我参与11月更文挑战的第23天,活动详情查看:2021最后一次更文挑战

前言

这款网关属于业务网关类型,目前已经提交apache孵化,并更名为shenyu,原名soul。

本文依然会使用soul这个名字,是因为我在落地这款网关的时候,当时尚未更名,版本是org.dromara:soul:2.2.1,并且我们使用到了现在,因为没有时间升级到新版本,与最新的主版本已经脱离太久,所以本文暂时会使用soul这个老名字。

以下的说明或者其中的一些问题也只是基于这个版本说明,在最新的版本中可能都已经解决,也请勿直接对齐理解。

其中有一部分比较通用的整理下代码提交到社区了,还有不少因为代码写的随意或者是针对我们自己的场景所以没有提交社区。另外一些是一直想着整理下提交到社区,但是感觉实现可能太low,也没时间整理,所以未提交社区。

网关选型

为什么选择soul

我们在对一个开源组件选型的时候,可能会基于很多方面进行考量,比如业务场景、技术储备、组件的功能特性、组件的性能、社区活跃度等等。

我们选择这款网关却是以我们组的现状来决定的。

之前有遇到有同学问我们为什么选择soul,我当时的解释是人力成本,事实上这个确实是我们选择它的一个原因。对于一款业务网关来说,业内的几款开源网关(除了zuul),性能差异性在一些性能对比的文档中,其实可以看出来,应该还没到几个量级的差距吧。毕竟不是流量网关,性能也确实不算我们唯一考量的因素。

我们组比较看重的是它的一个扩展性、比较完备的控制台、灵活的路由规则,因为这些可以减少我们更多的开发时间。

顺便解释下,我们为什么没有选择spring cloud gateway,如果是为了稳定性,它可以算是首选,毕竟依托于spring社区,作为spring cloud的组件,知名度很大的,遗憾的是它并没有一个比较让我们中意的控制台。

当时我们组三个人,除了TL,只有我和另一个研发,网关落地由我负责执行,在极短时间内我是没法快速开发一个控制台出来。

网关选型

网关选型主要由TL负责了,在老大分析了十几款网关后,最终决定了2款,fizz和soul,然后由我来做最后调研,分析选择哪一款。

我当时简单了解了下两款网关,下面是一个对比(在现在看来可能下面的有些说明已经不适用,并且鉴于当时也没花太多时间了解,下面的有些描述不够准确,所以这个对比仅供参考)

特性支持 Soul Fizz
可视化管理平台 有 有
路由规则 调用方根据请求url(原始url)路由,支持模糊匹配、正则等。自身提供了相关插件,可以对url的映射作简单调整 支持网关节点进行分组,不同的网关节点支持不同的路由规则。根据url路由,支持url别名映射路由。路由类型支持:服务编排、服务发现、反向代理
插件作用域 应用级别、接口级别 服务(接口)级别
服务信息暴露 自动注册(代码浸入)、手工相对烦琐 手工维护,服务提供方代码层面无浸入
多语言支持 java 支持自动注册,其它语言手工配置 无代码浸入,手工注册http接口,语言无关性
支持的协议 http\spring cloud\dubbo\tars\soaf http \spring cloud
自定义插件配置 支持,并且相当灵活,自定义插件采用maven依赖方式,代码无耦合 支持,但是需要在fizz上开发,代码耦合
服务编排 不支持 支持的比较完善,但复杂业务存在使用成本
默认插件 支持多种:鉴权、熔断、限流、流量白名单 仅支持鉴权、流控
接口鉴权 签名认证方式,waf插件支持白名单,默认不启用 默认MD5签名并支持自定义插件认证方式,且支持应用白名单
用户权限配置 不支持,不够完善 自带一套相对完备的权限配置模块,支持角色配置、菜单权限、数据权限、人员组织信息等。
是否开源 开源 控制台是商业授权使用
监控 支持prometheus 自带监控面板

Fizz相对Soul最引人的地方应该是服务编排部分了,只是它的控制台不够开源,既然作为商用,我是完全不能考虑的,最终选择了Soul。

其它一些因素

我们在选型的时候可能技术储备方面也会作为很重要的一个参考,很多人会更倾向选择自己熟悉的。我这边以前在公司做的微服务组件大多都是自研的,开源组件都是因为要作对标而去了解,在实际项目中不会用的特别多,因此不会因为“感情”作为评判的标准。

另外说下社区活跃度,目前还是很赞的。当时选型落地的时候提交个别bug或pr也都是很快被处理了。相对之前在rocketmq提的一些pr,最慢的跨度达到了快半年才处理,有些特殊场景的bug我都只能先在本地处理,等不到下个版本发布。或许rocketmq相对已经很稳定了,也可能是处理慢的一个原因。

落地实践

在有了基本了解、确认之后就是下面的一些常规操作了:

springCloud插件菜单配置增强

springCloud插件默认是java自动注册创建相关路由规则,在web端手工配置(仅在springCloud插件上配置)类似于divide插件的作用不支持注册中心,想支持注册中心还需要在其它菜单(元数据管理)进行配置,配置略烦琐(当时刚了解这个,也不够熟悉)。

我们公司是java和go语言为主,少量php和python,当时只有java支持自动注册,所以使用上不够友好。

另外,考虑到java项目自动注册需要依赖相关jar包及代码上需要配置注解,存在一定的代码侵入,去给业务同学推广,他们也有些不太好接受自动注册的使用方式。

我们使用的注册中心是nacos,所以在spring cloud插件的注册上进行了适当改造,可以手工快速配置。

我当时为了减少对soul原有代码的侵入,特地使用切面的方式拦截请求,想着后续升级soul到新版本的时候,这部分代码可以直接copy过去,类似于下面这样:

只是没想到后面改的代码越来越多也不好升级了。

nacos版本对齐

刚开始用的时候,nacos版本与我们用nacos版本不一致,然后发现这里nacos版本也比较乱,有多个版本,所以硬生生改成统一版本了。

但是这部分没有提交到社区,因为这部分没验证不敢提,有些nacos版本是被其中一些插件依赖的,我也不太清楚哪里有用到。

只是对我们用到的地方进行测试,保证没有问题即可。对于其它插件依赖的nacos版本,修改之后因为也没时间测,担心有潜在风险就没提交社区,不过后来社区应该都把nacos客户端版本升到2.x了吧。

梳理文档:规则配置、插件定制规范

因为是给业务同学用的,所以需要一个统一的规范,包括创建的选择器、规则格式。但是因为这一版菜单权限及数据权限还不够完善,最终还是没有完全对外开放由他们自己配置,接入项目的时候还是由我来配置使用。

集成业务侧提供的插件

梳理插件使用规范,业务同学根据他们自己的业务定制插件集成进来。

集成插件这部分还是不太方便的,他们开发完,需要我这边重新打包部署,有问题他们修改后,还需要我重新发布,这个流程太繁琐也耽误双方时间。我本来是考虑支持osgi方式动态加载依赖解决的,只是一直没时间搞,搁置到现在,后续很长一段时期估计也没空了。

迁移老网关过滤器作为soul插件

老网关有个认证授权的过滤器,我改造成soul的插件集成进来,老网关依赖的是qconf,硬编码配置,我顺便直接改造支持nacos做负载均衡器动态配置(业务上的一些改造),基于公司的单点登录方式,目前基本是公司大部分项目接入网关采用的统一认证授权的方式了。

集群化部署、提供域名

联系运维的同学基于nginx集群化部署,对外支持公网域名、内网域名。

跨域问题

soul自身支持跨域的,不过当时有个小bug,配置项不生效,这个问题当时提交社区修复了。

日志平台

采集日志到ELK,有针对地日志处理。

开发logging插件

可以打印请求头、请求体、响应头、响应体等信息,主要是有些情况线上排查问题比较方便(尤其是自定义插件对请求头添加相关字段,查看请求响应信息),该插件将代码规范后,把通用逻辑整理出来已提交社区。

后来一直想再打印请求耗时,到现在过了几个月也没花点时间在这上面增加这个功能。


其实到这一步,已经基本可以对外使用了, 也开始陆续接入一些项目了。

下面一些后续的开发及配置是我后来偶尔抽一点时间赶紧搞下,陆陆续续加的。


老网关平滑迁移方案

老网关基于spring cloud gateway做的,对于其中的很多项目不同的路由规则配置方式需要平滑迁移到新网关,所以对soul的上context_path和rewrite插件做了针对我们使用场景的部分微调整,可以平滑将spring cloud gateway的部分(只有我们用到的那几种)配置规则修改为soul的配置方式。

注册中心排除部分节点

这是某次业务侧提的一个需求:有A、B、C3个服务,A调C(A->C),B调C(B->C),假定C在注册中心上注册了5个实例,A调C的某些接口时候,只能打到前3个实例上,B调C的时候,只能打到后3个实例上。

我也想不明白为啥有这种需求:只调用其中部分实例。

所以加的这个功能,注册中心做负载均衡的时候可以排队某些节点,如下:

开发完成之后,我更多是用到这个场景下了:有些nacos节点临时没法下线,又不能把流量打过去,就现在这上面排除掉。

这部分代码之前还想着完善后提交社区,后来到现在过了几个月也没继续完善,我在自己项目实现有点粗糙,功能凑合能用,也不知道社区是否觉得需要这个功能(不一定通用),所以暂时没抽时间把这部分完善一下提交社区。

后续有时间还是要试着提交一下。

监控、告警

没有直接使用社区提供的监控面板,主要是有些指标我不太关心,然后又加了几个指标做了相关监控、告警:

我平常重点关注的指标大概这几个(后来我加的,也没来得及提交社区,等后续工作不忙了,斟酌一下代码完善后再提交社区):

单个网关实例的tps和每个服务的tps:

最近一分钟网关转发异常的服务及异常次数:

最近1小时YGC的频次:

最近1小时FGC的频次:

在性能上我没想好用哪个指标作告警合适,因此目前只用YGC或FGC频繁告警作为性能预警,下面是一个以前告警的示例:

如果网关转发异常也作了个告警(主要是超时 或者我们业务鉴权失败异常,转发失败一直没空作监控和告警)

结语

还有很多涉及我们业务上的地方需要优化和完善,不过因为时间不充裕就还没顾得上,后续有更多的优化再行补充,有好的实现也会尽量抽时间提交社区。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

如何用python群发工资条

发表于 2021-11-27

这是我参与11月更文挑战的第5天,活动详情查看:2021最后一次更文挑战

前言

1
2
3
复制代码每逢工资发放日,便是财务痛苦时~~
没啥文采,不过上面这句话却道出了人在不停重复劳作时,一种痛苦的心态。
身为程序猿的我们,这个时候当然要挺身而出啦,话不多说,撸起袖子写吧。

代码实现

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
python复制代码import openpyxl
from openpyxl import load_workbook
import yagmail
import keyring
from datetime import *

wb = load_workbook("test.xlsx", data_only = True)
sheet = wb.active

yagmail.register("997131679@qq.com", "AAA")
pwd = keyring.get_password("yagmail", "997131679@qq.com")
yag = yagmail.SMTP(user="997131679@qq.com", host="smtp.qq.com", password=pwd)

count=0
table_header="<thead>"
for row in sheet:
count += 1
if count == 1:
for cell in row:
if cell.column != "B":
table_header += f"<th>{cell.value}</th>"
table_header += "</thead>"
continue
else:
row_text = ""
for cell in row:
if cell.column == "B":
continue
row_text += f"<td>{cell.value}</td>"
row_text += "</tr>"
name = row[0].value
email = row[1].value

contents = f"""
<h3>{name}, hello: </h3>
<p>Please check your {date.today().year}-{date.today().month} salary.</p>
<table border="1px solid black">{table_header}{row_text}</table>
"""
yag.send(f"{email}", f"Wenfang limited company{date.today().year}-{date.today().month}-salary status", contents)
print(f"{name}发送完毕")
print("所有发送完毕")

注意点:yagmail.register(“997131679@qq.com“, “AAA”),第二个参数”AAA”是需要去QQ邮箱设置里获取的,步骤见下图所示:

  1. 点击设置在这里插入图片描述
  2. 切换到【账户】一栏,点击开启SMTP
    在这里插入图片描述
    会出现弹层,提示发送信息给对应的账户,短信发送成功后,即可获取到参数”AAA”的值

运行python文件,出现如下结果
在这里插入图片描述

查看邮箱,已发现新邮件
在这里插入图片描述

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…155156157…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%