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

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


  • 首页

  • 归档

  • 搜索

手把手教你汇编 Debug

发表于 2021-11-22

关于汇编的第一篇文章: 爱了爱了,这篇寄存器讲的有点意思

Hello大家好,我是程序员cxuan!我们上篇文章了解了一下基本的寄存器,这篇文章我们来进行实际操作一下。

原文链接:手把手教你汇编 Debug

我们以后将会用到很多 Debug 命令,这里我们先来熟悉一下它们。

Debug 是什么

Debug 是 Windows / Dos 操作系统提供的一种功能。使用 Debug 能让我们方便查看 CPU 各种寄存器的值、内存情况,方便我们调试指令、跟踪程序的运行过程。

接下来我们会用到很多 debug 命令,但是使用这些命令的前提是,你需要在电脑上安装一下 debug,Windows/Mac 都可以安装,获取链接我已经给你找出来了。阿,忘记说了,我们这里使用的是 Dos box来模拟汇编的操作环境。

传送门(Mac 和 Windows 都是):www.dosbox.com/download.ph…

image-20211017223618159

下载完成后打开 DosBox ,打开之后是这样的。

image-20211017223818599

此时我们输入 debug 命令应该提示的是

image-20211017223853097

因为我们还没有进行连接和挂载,此时我们执行

1
lua复制代码mount c D:\debug

执行这条命令时,你需要现在 D 盘下创建一个 debug 文件夹,然后我们挂载到 debug 下面。

并且执行 C: 切换到 C 盘路径下。

image-20211017224242053

此时我们就可以执行 debug 命令了。

image-20211017224458748

这里需要注意一点,我在 Windows 10 系统下搭建 Debug 环境时,在挂载完成后输入 debug ,还是提示 Illegal command:debug ,此时你需要再下载一个 debug.exe ,贴心的我也把下载地址给你了。

下载地址:pan.baidu.com/s/177arSA34… 密码:3akd

需要下载里面的 debug.exe,然后把它放在你挂载的路径下,这里我挂载的路径时 D 盘下的 debug 文件夹。

放置完成之后,再输入 debug 就可以了。


因为每次打开 Dosbox 都会执行上面这些命令,真的好烦,那怎么办呢?一个简单的办法是在 Dosbox 安装路径下找到

image-20211017225250971

打开之后,在末尾键入

image-20211017225326436

就 OK 了,下次直接打开 Dosbox ,会默认执行这三条命令,至此,就是我搭建 Dosbox 遇到的所有问题了。

Debug 实战

玩儿汇编得学会用 Debug ,Debug 是一种调试程序,通过 Debug 能让我们能够看到内存值,跟踪堆栈情况,看到寄存器所暂存的内容等,同时也能够更好地帮助我们理解汇编代码,所以学会 Debug ,非常重要,这是一种不可或缺的动手能力。

下面我们会用到几种 Debug 命令,这里先简单介绍下。

image-20211117222634775

Debug 命令有很多,不过常用的一般就上面这几个。

好了,现在我们直接进入正题,开始在 Dosbox 上正式进行 Debug 操作,首先打开 Dosbox。

嗯。。。。。。这个界面我们打开很多次了。

那我写个命令呢? 好吧,没演示过,下面就来了!

Debug -r

亲,用 Debug -r 就可以查看和修改 CPU 寄存器内容了呢。

image-20211117224711832

查看寄存器内容。

image-20211117225112134

这里需要注意一下 -r 大小写的问题,Debug -r 是查看寄存器内容。而 -R 则是无效指令。

上图列出来了很多寄存器,你可能觉得无从下手,不要乱,我们先从最基本的开始入手,也就是 CS 和 IP,CS(Code Segment)是代码段寄存器,一般也被称为段基址,可以认为是程序访问的入口,CPU 需要从 CS 中找到从哪个位置开始取指执行,但是我们还不知道要取哪一段,这时候 IP 的作用就体现出来了,IP(Instruction Pointer)就是指令指针寄存器,也叫做偏移地址,它会告诉我们从段基址开始,取哪一段的地址。

可以使用段基址:偏移地址来确定内存中的指定地址。

这里我们只是简单聊一下这两个寄存器的概念,要了解这两个寄存器的具体作用,可以看笔者的上一篇文章

使用 -r 也能够修改寄存器的内容,如下所示

image-20211119182455417

-r 一般的格式是 -r 寄存器,然后系统会进行冒号提示,后面就是你要修改的内容。

Debug -d

使用 -d 指令可以查看内存中的内容。

image-20211119183448427

输出的内存值默认是按照 CS:IP 的地址开始的,由于 CS 的值默认是 073F,而 IP 默认是 0100,所以 -d 的内存值是 073F:0100 。

-d 的格式很多,下面只介绍一下常用的几种格式。

形似 -d 1000:0 这种 -d 段基址 偏移地址的格式可以产生如下输出。

image-20211119184935344

如上图所示,Debug 会列出指定内存单元中的的内容。上图中的每一个 00 都表示 8 位,如果是 4A,那么这八位展开来说就是 0010 1011 。每一行有 16 个 8 位,所以构成了 128 位内存地址。

为什么都是 00 呢,因为内存单元的值没有被改写,说白了就是这块内存区域没有存值,如何改写我们后面回收。

每一行的中间都有一个 -,这个是为了便于我们阅读来设置的,- 号前后都有 8 个内存单元,这样便于查看。

右侧几个 …… 表示每个内存单元可显示的 ASCII 码字符,因为内存没有值,所以也没有对应的 ASCII 码。我们可以数一下,每行有 16 个 . ,这表示每一个 00 都对应了一个 ASCII 码。

我们可以使用 -d 1000:9 这种 -d 段基址:起始偏移地址 格式来显示从 1000 的第几位开始。

image-20211119194232683

Debug 从 1000:9 开始,一直到 1000:88,一共是 128 个字节,第一行中的 1000:0 ~ 1000:8 中的内容没有显示。

还可以使用 -d 1000:0 9 这种 -d 段基址:起始偏移地址 结尾偏移地址的格式来输出。

image-20211119194856418

还可以是使用 -d 偏移地址来在不指定段基址的情况下,查看内存值。

image-20211119201832383

Debug -e

上面说的都是查看内存中指定位置或者区域的值,下面我们要来改写一下内存值。

使用 -e 可以改写内存值,比如我们想要改写 1000:0 ~ 1000:f 中的内容,可以使用 -e 1000:0 0 1 2 3 4 5 6 7 8 9 0 a b c d e f 这种方式,如下图所示。

image-20211119201236510

这里需要注意下,在进行 -e 改写的时候,每个值中间都有一个空格,如果没有空格的话,会当做一个内存值来看待。

然后用 -d 1000:0 看到我们刚改写的内存值。

还可以使用提问的方式来逐个修改从某一地址开始的内存单元的内容。

还是用 1000:100 来举例子,输出 -e 1000:100 后按下回车键。

image-20211119205201568

如上图所示,可以看到我们先输入了一次 -e 1000:100 这个指令,然后按下了回车键。

注意,如果这里你按下了回车键,就相当于整个 -e 改写的过程已经完成。

如果你想要继续改写后面内存中的值,你需要按下空格键。

我们改写了 1000:100 之后的内存值,然后使用 -d 1000:100 查看我们改写的内容是否生效。

-e 命令还可以支持写入字符,比如我们可以向 1000:0 这个位置开始写入数值和字符,-e 1000:0 1 ‘a’ 2 ‘b’ e ‘c’ 。

image-20211119210708341

如上图所示,当我们向内存写入字符 ‘a’ ‘b’ ‘c’ 的时候,会自动转换为 ASCII 码进行存储,在最右侧可以找到刚刚写入的字符。

Debug -u

如何向内存中写入一段机器码呢?比如我们想要在内存中写入一段机器码。

image-20211119220152007

我们可以使用 -e 来进行写入,向内存中写入 b8 01 00 b9 02 00 01 c8 这个机器码,如下所示

image-20211119224014093

我们使用 -e 写入之后,使用 -d 查看内存值,可以发现我们刚刚写入的值,但是却看不到机器码,所以机器码该如何看呢?

别急,还有个 -u 命令,这个就是看机器码的,如下图所示,我们使用 -u 命令显示我们写入的机器码。

image-20211119224851565

可以看到 1000:0000 ~ 1000:0006 这个内存地址使我们写入的机器码,-u 这个命令就是将内存单元的内容翻译为汇编指令并显示。

-u 输出的结果分为三部分显示:

  • 最左侧是每一条机器指令的地址;
  • 中间是机器指令;
  • 最右侧是机器指令执行的汇编指令。

1000:0 处存放的是写入的机器码 B8 01 00 组成的机器指令,对应的汇编指令是 MOV AX,0001。

1000:0003 处存放的是写入的机器码 B9 02 00 组成的机器指令,对应的汇编指令是 MOV CX,0002。

1000:0006 处存放的是写入的机器码 C1 C8 所组成的机器指令,对应的汇编指令是 add ax,cx。

Debug -t

上面介绍的一系列指令包括我们上面提到的 Debug -e 机器码都是向内存中进行写入,那么如何执行这些指令呢?

我们可以使用 Debug -t 来执行写入的指令。使用 Debug -t 可以执行由 CS:IP 指向的指令。

既然是 -t 能够执行从 CS:IP 指向的命令,所以我们有必要将 CS:IP 指向 1000:0(因为我们前面将指令写在了 1000:0 处)。

首先我们需要执行 -r cs 1000 ,-r ip 0 把 CS:IP 赋值为 1000:0。

然后执行 -t 指令,下图是已经执行过的指令截图。

image-20211120060826534

可以看到,执行完 -t 指令之后,MOV AX,0001 这条指令被执行,当前 AX 寄存器的内容变为了 0001,这条汇编指令的意思就是把 0001 移动到 AX 寄存器中。

继续执行 -t 之后,我们可以看到寄存器的变化。

image-20211120061105506

Debug -a

毕竟机器指令不是那么好懂,写入很不方便,所以有没有办法能够支持我们直接写入汇编指令呢?还真有,Debug 提供了 -a 这种方式来实现汇编指令的写入。如下图所示

image-20211120230320647

可以看到,我们使用了 -a 命令来对 1000:0 进行写入,分别输入 mov ax,1 mov bx,2 mov cx,3 add ax,bx add ax,cx add ax,ax 指令,然后按回车进行确定执行。

我们使用 -d 1000:0 f 可以看到从偏移地址 0 处开始的第 f 个内存指令(因为最大写入的地址只是 f)。

image-20211120231016386

上图中的 1000:000F 为什么有值呢,因为我们上面已经执行过这个写入了。

另外,使用 -a 可以从一个预设的地址处开始输入指令。

总结

今天和大家聊了一下 Debug 的基本用法,主要包括

  • -r 查看、修改寄存器中的内容
  • -d 查看内存中的指令
  • -e 修改内存中的内容
  • -u 可以将内存中的内容解释为机器指令和对应的汇编指令
  • -t 执行 CS:IP 处的指令
  • -a 以汇编得形式向内存写入内容

汇编指令的选项有很多,上面介绍的这些属于经常用到的指令,这些指令要能够熟练使用。

最后给大家推荐一下我自己的Github ,里面有非常多的硬核文章,绝对会对你有帮助。

本文转载自: 掘金

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

Go(二)结构体

发表于 2021-11-22

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

作者:lomtom

个人网站:lomtom.top,

个人公众号:博思奥园

你的支持就是我最大的动力。

Go系列:

  1. Go(一)基础入门
  2. Go(二)结构体
  3. Go(三)Go配置文件

在Go中没有类的概念,取而代之,我觉得Go中的结构体却在充当着类的角色。

但是在Go中通过结构体的内嵌再配合接口比面向对象具有更高的扩展性和灵活性。

比面向对象具有更高的扩展性和灵活性。

1 类型别名和自定义类型

1.1 自定义类型

和java一样,在Go语言中有一些基本的数据类型,如string、整型、浮点型、布尔等数据类型

在Go语言中可以使用type关键字来定义自定义类型。

自定义类型是定义了一个全新的类型。我们可以基于内置的基本类型定义,也可以通过struct定义。例如:

1
2
go复制代码//将MyInt定义为int类型
type MyInt int

通过Type关键字的定义,MyInt就是一种新的类型,它具有int的特性。

1.2 类型别名

类型别名是Go1.9版本添加的新功能。

类型别名规定:TypeAlias只是Type的别名,本质上TypeAlias与Type是同一个类型。就像一个孩子小时候有小名、乳名,上学后用学名,英语老师又会给他起英文名,但这些名字都指的是他本人。

1
go复制代码type TypeAlias = Type

我们之前见过的rune和byte就是类型别名,他们的定义如下:

1
2
go复制代码type byte = uint8
type rune = int32

1.3类型定义和类型别名的区别

类型别名与类型定义表面上看只有一个等号的差异,我们通过下面的这段代码来理解它们之间的区别。

1
2
3
4
5
6
7
8
9
10
11
12
13
go复制代码//类型定义
type NewInt int

//类型别名
type MyInt = int

func main() {
var a NewInt
var b MyInt

fmt.Printf("type of a:%T\n", a) //type of a:main.NewInt
fmt.Printf("type of b:%T\n", b) //type of b:int
}

结果显示a的类型是main.NewInt,表示main包下定义的NewInt类型。b的类型是int。MyInt类型只会在代码中存在,编译完成时并不会有MyInt类型。

2 结构体

Go语言中的基础数据类型可以表示一些事物的基本属性,但是当我们想表达一个事物的全部或部分属性时,这时候再用单一的基本数据类型明显就无法满足需求了,

Go语言提供了一种自定义数据类型,可以封装多个基本数据类型,这种数据类型叫结构体,英文名称struct。 也就是我们可以通过struct来定义自己的类型了。

Go语言中通过struct来实现面向对象。

2.1 结构体的定义

同样,使用type和struct关键字也可以用来定义结构体,具体代码格式如下:

1
2
3
4
5
go复制代码type 类型名 struct {
字段名 字段类型
字段名 字段类型
…
}

其中:

  1. 类型名:标识自定义结构体的名称,在同一个包内不能重复。
  2. 字段名:表示结构体字段名。结构体中的字段名必须唯一。
  3. 字段类型:表示结构体字段的具体类型。

举个例子,我们定义一个Article (文章)结构体,代码如下:

1
2
3
4
5
6
go复制代码type Article struct {
author string
title string
content string
articleId int
}

同样类型的字段也可以写在一行,

1
2
3
4
go复制代码type Article struct {
author,title,content string
articleId int
}

这样我们就拥有了一个Article 的自定义类型,它有author,title,content,articleId四个字段,分别表示作者、标题、内容和文章编号。

这样我们使用这个Article 结构体就能够很方便的在程序中表示和存储文章信息了。

语言内置的基础数据类型是用来描述一个值的,而结构体是用来描述一组值的。
比如一篇文章有作者、标题、内容和文章编号等,本质上是一种聚合型的数据类型

2.2 结构体实例化

只有当结构体实例化时,才会真正地分配内存。也就是必须实例化后才能使用结构体的字段。

结构体本身也是一种类型,我们可以像声明内置类型一样使用var关键字声明结构体类型。

1
csharp复制代码var 结构体实例 结构体类型

如果需要访问结构体的字段就可以使用结构体名.属性名来访问该结构体的属性

2.3 基本实例化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
go复制代码type Article struct {
author, title, content string
articleId int
}

func main() {
var article Article
article.author = "lomtom"
article.title = "Go(二)结构体"
article.content = "这是内容"
article.articleId = 119995581
fmt.Printf("%v", article) // {lomtom Go(二)结构体 这是内容 119995581}
fmt.Printf("%#v", article) // {author:"lomtom", title:"Go(二)结构体", content:"这是内容", articleId:119995581}
}

2.4 创建指针类型结构体

我们还可以通过使用new关键字对结构体进行实例化,得到的是结构体的地址。

实例化格式

1
2
3
go复制代码var article = new(Article)
或者
var article *Article= new(Article)

获取属性值\设置属性值

1
2
3
go复制代码article.author = "lomtom"
或者
(*article).author = "lomtom"

在这里就可以看得出go设计的巧妙,也同样是为了程序员使用方便,底层会对article.author = "lomtom"进行处理,会给 article加上 取值运算 (*article).author = "lomtom"

具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
go复制代码type Article struct {
author, title, content string
articleId int
}

func main() {
var article = new(Article)
article.author = "lomtom"
article.title = "Go(二)结构体"
article.content = "这是内容"
article.articleId = 119995581
fmt.Printf("%v", article) // &{lomtom Go(二)结构体 这是内容 119995581}
fmt.Printf("%#v", article) // &{author:"lomtom", title:"Go(二)结构体", content:"这是内容", articleId:119995581}
}

从打印的结果中我们可以看出p2是一个结构体指针。

实际上,他等同于

1
2
3
4
5
6
7
8
9
10
11
12
13
14
go复制代码type Article struct {
author, title, content string
articleId int
}

func main() {
var article *Article= new(Article)
(*article).author = "lomtom"
(*article).title = "Go(二)结构体"
(*article).content = "这是内容"
(*article).articleId = 119995581
fmt.Printf("%v", article) // &{lomtom Go(二)结构体 这是内容 119995581}
fmt.Printf("%#v", article) // &{author:"lomtom", title:"Go(二)结构体", content:"这是内容", articleId:119995581}
}

2.5 取结构体的地址实例化

使用&对结构体进行取地址操作相当于对该结构体类型进行了一次new实例化操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
go复制代码type Article struct {
author, title, content string
articleId int
}

func main() {
var article *Article= &Article{}
(*article).author = "lomtom"
(*article).title = "Go(二)结构体"
(*article).content = "这是内容"
(*article).articleId = 119995581
fmt.Printf("%v", article) // &{lomtom Go(二)结构体 这是内容 119995581}
fmt.Printf("%#v", article) // &{author:"lomtom", title:"Go(二)结构体", content:"这是内容", articleId:119995581}
}

对于结构体的赋值,可以在初始化时进行赋值:

1
2
3
4
5
6
7
8
9
10
11
12
13
go复制代码var article =  &Article{
"lomtom",
"Go(二)结构体",
"这是内容",
119995581,
}

var article = &Article{
author: "lomtom",
title: "Go(二)结构体",
content: "这是内容",
articleId: 119995581,
}

值得注意的是:

1.必须初始化结构体的所有字段。
2.初始值的填充顺序必须与字段在结构体中的声明顺序一致。
3.该方式不能和键值初始化方式混用。
4.属性名要么全部省略,要么全部保留

在使用上来说,使用new创建指针类型结构体和使用&是一样的

3 结构体特殊用法

3.1 构造函数

Go语言的结构体没有构造函数,我们可以自己实现。

例如,下方的代码就实现了一个Article的构造函数。 因为struct是值类型,如果结构体比较复杂的话,值拷贝性能开销会比较大,所以该构造函数返回的是结构体指针类型。

1
2
3
4
5
go复制代码func newArticle(author, title, content string, articleId int) *Article {
return &Article{
author,title,content,articleId,
}
}

调用构造函数

1
2
3
go复制代码	article := newArticle("lomtom","Go(二)结构体","这是内容",119995581)
fmt.Printf("%v", article) // &{lomtom Go(二)结构体 这是内容 119995581}
fmt.Printf("%#v", article) // &{author:"lomtom", title:"Go(二)结构体", content:"这是内容", articleId:119995581}

3.2 set和get方法

同样的,我们也可以实现结构体自己的set、get方法

1
2
3
4
5
6
7
8
9
10
11
12
go复制代码type article struct {
author, title, content string
articleId int
}

func (article *article) GetAuthor() string {
return article.author
}

func (article *article) SetAuthor(author string) {
article.author = author
}

使用

1
2
3
4
5
6
go复制代码func main() {
article := entity.NewArticle("lomtom","Go(二)结构体","这是内容",119995581)
fmt.Printf("%v", article.GetAuthor()) // lomtom
article.SetAuthor("小布丁")
fmt.Printf("%v", article.GetAuthor()) // 小布丁
}

3.3 结构体嵌套使用

一个结构体中可以嵌套包含另一个结构体或结构体指针。

定义两个结构体,在Article 结构体中加入Type 属性

1
2
3
4
5
6
7
8
9
10
go复制代码type Type struct {
name string
icon string
}

type Article struct {
author, title, content string
articleId int
myType Type
}

当然,在使用中我们可以省略属性名,而只需指定类型即可,例如

1
2
3
4
5
6
7
8
9
10
go复制代码type Type struct {
name string
string
}

type Article struct {
author, title, content string
articleId int
Type
}

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
go复制代码func main() {
article := &Article{
"lomtom",
"Go(二)结构体",
"这是内容",
119995581,
Type{
"Go",
"icon-go",
},
}
fmt.Printf("%v", article) // &{lomtom Go(二)结构体 这是内容 119995581 {Go icon-go}}
}

3.4 实现“继承”

Go语言中使用结构体也可以实现其他编程语言中面向对象的继承。

首先,定义一个结构体动物,有一个属性(feet),所有动物都由腿,并且定义一个动物的方法(getFeet)

1
2
3
4
5
6
7
8
go复制代码type Animal  struct {
feet int
}


func (a *Animal) getFeet() {
fmt.Printf("有%v条腿" , a.feet)
}

定义一个结构体:狗,拥有一个名字,并且属性加上动物,通过嵌套匿名结构体实现继承,那么狗也可以使用动物的方法

1
2
3
4
go复制代码type Dog struct {
name string
*Animal
}

使用

1
2
3
4
5
6
7
8
9
10
go复制代码func main() {
dog := &Dog{
"旺财",
&Animal{
4,
},
}
fmt.Println(dog.name) // 旺财
dog.getFeet() // 有4条腿
}

另外定义另一个结构体,人类(高级动物),同样拥有一个名字,并且会说话

1
2
3
4
5
6
7
8
go复制代码type Person struct {
name string
*Animal
}

func (a *Person) say() {
fmt.Printf("我叫%v,我厉害吧,我会说话",a.name )
}

使用,那么人类也可以使用getFeet方法,除此之外,还能使用自己的方法say

1
2
3
4
5
6
go复制代码func main() {
person := &Person{"道科",&Animal{3}}
fmt.Println(person.name) // 道科
person.say() // 我叫道科,我厉害吧,我会说话
person.getFeet() // 有3条腿
}

3.5 值传递/地址传递

在之前的实例化中,有通过一般方法进行实例化的。也有通过指针\地址进行实例化的,那么在Go中值传递和地址传递有什么区别呢?

同样的Article结构体

1
2
3
4
go复制代码type Article struct {
author, title, content string
articleId int
}

拥有两个方法,一个通过值进行参数传递,一个通过地址进行参数传递,起作用都是修改标题,那么结果会是怎么样呢?

1
2
3
4
5
6
7
go复制代码func (a Article) setTitle1() {
a.title = "1"
}

func (a *Article) setTitle2() {
a.title = "2"
}

执行方法,并且进行打印

1
2
3
4
5
6
7
8
9
10
11
12
13
14
go复制代码func main() {

article := &Article{
"lomtom",
"Go(二)结构体",
"这是内容",
119995581,
}
fmt.Printf("%v", article) // &{lomtom Go(二)结构体 这是内容 119995581}
article.setTitle1()
fmt.Printf("%v", article) // &{lomtom Go(二)结构体 这是内容 119995581}
article.setTitle2()
fmt.Printf("%v", article) // &{lomtom 2 这是内容 119995581}
}

可以看到通过值传递的,并且实现真正的修改,这是为什么呢?

因为在Go中通过值传递,实质上是拷贝了一份新的内容,在setTitle1中的a实际上与article指向的并不是指的同一个地址了。

本文转载自: 掘金

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

Spring Cloud Gateway一次请求调用源码解析

发表于 2021-11-22

简介: 最近通过深入学习Spring Cloud Gateway发现这个框架的架构设计非常简单、有效,很多组件的设计都非常值得学习,本文就Spring Cloud Gateway做一个简单的介绍,以及针对一次请求Spring Cloud Gateway的处理流程做一个较为详细的分析。

作者 | 寻筝

来源 | 阿里技术公众号

一 前言

最近通过深入学习Spring Cloud Gateway发现这个框架的架构设计非常简单、有效,很多组件的设计都非常值得学习,本文就Spring Cloud Gateway做一个简单的介绍,以及针对一次请求Spring Cloud Gateway的处理流程做一个较为详细的分析。

二 简介

Spring Cloud Gateway 即Spring官方推出的一款API网关,该框架包含了Spring5、SpringBoot2、Project Reactor,其中底层通信框架用的netty。Spring Cloud Gateway在推出之初的时候,Netflix公司已经推出了类似功能的API网关框架ZUUL,但ZUUL有一个缺点是通信方式是阻塞的,虽然后来升级到了非阻塞式的ZUUL2,但是由于Spring Cloud Gateway已经推出一段时间,同时自身也面临资料少、维护性较差的因素没有被广泛应用。

1 关键术语

在使用Spring Cloud Gateway的时候需要理解三个模块,即

Route:

即一套路由规则,是集URI、predicate、filter等属性的一个元数据类。

Predicate:

这是Java8函数式编程的一个方法,这里可以看做是满足什么条件的时候,route规则进行生效。

Filter:

filter可以认为是Spring Cloud Gateway最核心的模块,熔断、安全、逻辑执行、网络调用都是filter来完成的,其中又细分为gateway filter和global filter,区别在于是具体一个route规则生效还是所有route规则都生效。

可以先上一段代码来看看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
less复制代码 @RequestMapping("/paramTest")
public Object paramTest(@RequestParam Map<String,Object> param) {
return param.get("name");
}

@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("path_route", r ->
r.path("/get")
.filters(f -> f.addRequestParameter("name", "value"))
.uri("forward:///paramTest"))
.build();
}
  • route方法代表的就是一个路由规则;
  • path方法代表的就是一个predicate,背后的现实是PathRoutePredicateFactory,在这段代码的含义即当路径包含/get的时候,当前规则生效。
  • filters方法的意思即给当前路由规则添加一个增加请求参数的filter,每次请求都对参数里添加 name:value 的键值对;
  • uri 方法的含义即最终路由到哪里去,这里的forward前缀会将请求交给spring mvc的DispatcherHandler进行路由,进行本机的逻辑调用,除了forward以外还可以使用http、https前缀进行http调用,lb前缀可以在配置注册中心后进行rpc调用。

上图是Spring Cloud Gateway官方文档给出的一个工作原理图,Spring Cloud Gateway 接收到请求后进行路由规则的匹配,然后交给web handler 进行处理,web handler 会执行一系列的filter逻辑。

三 流程分析

1 接受请求

Spring Cloud Gateway的底层框架是netty,接受请求的关键类是ReactorHttpHandlerAdapter,做的事情很简单,就是将netty的请求、响应转为http的请求、响应并交给一个http handler执行后面的逻辑,下图为该类的源码仅保留核心逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
less复制代码    @Override
public Mono< Void> apply(HttpServerRequest request, HttpServerResponse response) {
NettyDataBufferFactory bufferFactory = new NettyDataBufferFactory(response.alloc());
ServerHttpRequest adaptedRequest;
ServerHttpResponse adaptedResponse;
//转换请求
try {
adaptedRequest = new ReactorServerHttpRequest(request, bufferFactory);
adaptedResponse = new ReactorServerHttpResponse(response, bufferFactory);
}
catch (URISyntaxException ex) {
if (logger.isWarnEnabled()) {
...
}
...
return this.httpHandler.handle(adaptedRequest, adaptedResponse)
.doOnError(ex -> logger.warn("Handling completed with error: " + ex.getMessage()))
.doOnSuccess(aVoid -> logger.debug("Handling completed with success"));
}

2 WEB过滤器链

http handler做的事情第一是将request 和 response转为一个exchange,这个exchange非常核心,是各个filter之间参数流转的载体,该类包含request、response、attributes(扩展字段),接着做的事情就是web filter链的执行,其中的逻辑主要是监控。

其中WebfilterChainParoxy 又会引出新的一条filter链,主要是安全、日志、认证相关的逻辑,由此可见Spring Cloud Gateway的过滤器设计是层层嵌套,扩展性很强。

3 寻找路由规则

核心类是RoutePredicateHandlerMapping,逻辑也非常简单,就是把所有的route规则的predicate遍历一遍看哪个predicate能够命中,核心代码是:

1
2
3
4
5
kotlin复制代码return this.routeLocator.getRoutes()
.filter(route -> {
...
return route.getPredicate().test(exchange);
})

因为我这里用的是path进行过滤,所以背后的逻辑是PathRoutePredicateFactory来完成的,除了PathRoutePredicateFactory还有很多predicate规则。

这些路由规则都能从官方文档上找到影子。

4 核心过滤器链执行

找到路由规则后下一步就是执行了,这里的核心类是FilteringWebHandler,其中的源码为:

做的事情很简单:

  1. 获取route级别的过滤器
  2. 获取全局过滤器
  3. 两种过滤器放在一起并根据order进行排序
  4. 执行过滤器链

因为我的配置里包含了一个添加请求参数的逻辑,所以红线箭头处就是我配置的gateway filter名为 AddRequestParameterGatewayFilterFactory,其余全是Gloabl Filter,这些过滤器的功能主要是url解析,请求转发,响应回写等逻辑,因为我们这里用的是forward schema,所以请求转发会由ForwardRoutingFilter进行执行。

5 请求转发

ForwardRoutingFilter做的事情也很简单,直接复用了spring mvc的能力,将请求提交给dispatcherHandler进行处理,dispatcherHandler会根据path前缀找到需要目标处理器执行逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
scss复制代码@Override
public Mono< Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
URI requestUrl = exchange.getRequiredAttribute(GATEWAY_REQUEST_URL_ATTR);

String scheme = requestUrl.getScheme();
if (isAlreadyRouted(exchange) || !"forward".equals(scheme)) {
return chain.filter(exchange);
}
setAlreadyRouted(exchange);

//TODO: translate url?

if (log.isTraceEnabled()) {
log.trace("Forwarding to URI: "+requestUrl);
}

return this.dispatcherHandler.handle(exchange);
}

6 响应回写

响应回写的核心类是NettyWriteResponseFilter,但是大家可以注意到执行器链中NettyWriteResponseFilter的排序是在最前面的,按道理这种响应处理的类应该是在靠后才对,这里的设计比较巧妙。大家可以看到chain.filter(exchange).then(),意思就是执行到我的时候直接跳过下一个,等后面的过滤器都执行完后才执行这段逻辑,这种行为控制的方法值得学习。

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
less复制代码@Override
public Mono< Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// NOTICE: nothing in "pre" filter stage as CLIENT_RESPONSE_ATTR is not added
// until the WebHandler is run
return chain.filter(exchange).then(Mono.defer(() -> {
HttpClientResponse clientResponse = exchange.getAttribute(CLIENT_RESPONSE_ATTR);

if (clientResponse == null) {
return Mono.empty();
}
log.trace("NettyWriteResponseFilter start");
ServerHttpResponse response = exchange.getResponse();

NettyDataBufferFactory factory = (NettyDataBufferFactory) response.bufferFactory();
//TODO: what if it's not netty

final Flux< NettyDataBuffer> body = clientResponse.receive()
.retain() //TODO: needed?
.map(factory::wrap);

MediaType contentType = response.getHeaders().getContentType();
return (isStreamingMediaType(contentType) ?
response.writeAndFlushWith(body.map(Flux::just)) : response.writeWith(body));
}));
}

四 总结

整体读完Spring Cloud Gateway请求流程代码后,有几点感受:

  1. 过滤器是Spring Cloud Gateway最核心的设计,甚至于可以夸张说Spring Cloud Gateway是一个过滤器链执行框架而不是一个API网关,因为API网关实际的请求转发、请求响应回写都是在过滤器中做的,这些是Spring Cloud Gateway感知不到的逻辑。
  2. Spring Cloud Gateway路由规则获取的模块具备优化的空间,因为是循环遍历进行获取的,如果每个route规则较多,predicate规则较复杂,就可以考虑用map进行优化了,当日route规则,predicate规则也不会很复杂,兼顾到代码的可读性,当前方式也没有什么问题。
  3. 作为API网关框架,内置了非常多的过滤器,如果有过滤器的卸载功能可能会更好,用户可用根据实际情况卸载不必要的功能,背后减少的逻辑开销,在调用量极大的API网关场景,收益也会很可观。

原文链接

本文为阿里云原创内容,未经允许不得转载。

本文转载自: 掘金

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

整个大活,采集8个代理IP站点,为Python代理池铺路,爬

发表于 2021-11-22

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

很多爬虫大佬都会建立自己的,IP 代理池,你想知道 IP 代理池是如何创建的吗?
如果你恰巧有此需求,欢迎阅读本文。

本案例为爬虫 120 例专栏中的一例,顾使用 requests + lxml 进行实现。

从 89IP 网开始

代理 IP 目标网站之一为:www.89ip.cn/index_1.htm…,首先编写随机返回 User-Agent 的函数,也可以将该函数的返回值设置为请求头,即 headers 参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
python复制代码def get_headers():
uas = [
"Mozilla/5.0 (compatible; Baiduspider/2.0; +http://www.baidu.com/search/spider.html)",
"Mozilla/5.0 (compatible; Baiduspider-render/2.0; +http://www.baidu.com/search/spider.html)",
"Baiduspider-image+(+http://www.baidu.com/search/spider.htm)",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.81 YisouSpider/5.0 Safari/537.36",
"Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)",
"Mozilla/5.0 (compatible; Googlebot-Image/1.0; +http://www.google.com/bot.html)",
"Sogou web spider/4.0(+http://www.sogou.com/docs/help/webmasters.htm#07)",
"Sogou News Spider/4.0(+http://www.sogou.com/docs/help/webmasters.htm#07)",
"Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0);",
"Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)",
"Sosospider+(+http://help.soso.com/webspider.htm)",
"Mozilla/5.0 (compatible; Yahoo! Slurp China; http://misc.yahoo.com.cn/help.html)"
]
ua = random.choice(uas)
headers = {
"user-agent": ua,
"referer": "https://www.baidu.com"
}
return headers

上述代码中的 uas 变量,使用的是各大搜索引擎的 UA,后续案例将会继续扩展该列表字段,争取成为单独的模块。

列表随机选择一个值,使用 random.choice ,请提前导入 random 模块。

编写 requests 请求函数

提取公用的请求函数,便于后续扩展为多个代理站点采集数据。

1
2
3
4
5
6
7
8
python复制代码def get_html(url):
headers = get_headers()
try:
res = requests.get(url, headers=headers, timeout=5)
return res.text
except Exception as e:
print("请求网址异常", e)
return None

上述代码首先调用 get_headers 函数,获取请求头,之后通过 requests 发起基本请求。

编写 89IP 网解析代码

下面的步骤分为两步,首先编写针对 89IP 网的提取代码,然后再对其进行公共函数提取。

提取部分代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
python复制代码def ip89():
url = "https://www.89ip.cn/index_1.html"
text = get_html(url)
ip_xpath = '//tbody/tr/td[1]/text()'
port_xpath = '//tbody/tr/td[2]/text()'
# 待返回的IP与端口列表
ret = []
html = etree.HTML(text)
ips = html.xpath(ip_xpath)
ports = html.xpath(port_xpath)
# 测试,正式运行删除本部分代码
print(ips,ports)
ip_port = zip(ips, ports)
for ip, port in ip_port:

item_dict = {
"ip": ip.strip(),
"port": port.strip()
}
ret.append(item_dict)

return ret

上述代码首先获取网页响应,之后通过 lxml 进行序列化操作,即 etree.HTML(text),然后通过 xpath 语法进行数据提取,最后拼接成一个包含字典项的列表,进行返回。

其中解析部分可以进行提取,所以上述代码可以分割为两个部分。

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
python复制代码# 代理IP网站源码获取部分
def ip89():
url = "https://www.89ip.cn/index_1.html"
text = get_html(url)
ip_xpath = '//tbody/tr/td[1]/text()'
port_xpath = '//tbody/tr/td[2]/text()'
ret = format_html(text, ip_xpath, port_xpath)
print(ret)

# HTML解析部分
def format_html(text, ip_xpath, port_xpath):
# 待返回的IP与端口列表
ret = []
html = etree.HTML(text)
ips = html.xpath(ip_xpath)
ports = html.xpath(port_xpath)
# 测试,正式运行删除本部分代码
print(ips,ports)
ip_port = zip(ips, ports)
for ip, port in ip_port:

item_dict = {
"ip": ip.strip(), # 防止出现 \n \t 等空格类字符
"port": port.strip()
}
ret.append(item_dict)

return ret

测试代码,得到如下结果。
在这里插入图片描述

扩展其它代理 IP 地址

在 89IP 代理网代码编写完毕之后,就可以进行其它站点的扩展实现了,各站点扩展如下:

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
python复制代码def ip66():
url = "http://www.66ip.cn/1.html"
text = get_html(url)
ip_xpath = '//table/tr[position()>1]/td[1]/text()'
port_xpath = '//table/tr[position()>1]/td[2]/text()'
ret = format_html(text, ip_xpath, port_xpath)
print(ret)

def ip3366():
url = "https://proxy.ip3366.net/free/?action=china&page=1"
text = get_html(url)
ip_xpath = '//td[@data-title="IP"]/text()'
port_xpath = '//td[@data-title="PORT"]/text()'
ret = format_html(text, ip_xpath, port_xpath)
print(ret)

def ip_huan():
url = "https://ip.ihuan.me/?page=b97827cc"
text = get_html(url)
ip_xpath = '//tbody/tr/td[1]/a/text()'
port_xpath = '//tbody/tr/td[2]/text()'
ret = format_html(text, ip_xpath, port_xpath)
print(ret)

def ip_kuai():
url = "https://www.kuaidaili.com/free/inha/2/"
text = get_html(url)
ip_xpath = '//td[@data-title="IP"]/text()'
port_xpath = '//td[@data-title="PORT"]/text()'
ret = format_html(text, ip_xpath, port_xpath)
print(ret)

def ip_jiangxi():
url = "https://ip.jiangxianli.com/?page=1"
text = get_html(url)
ip_xpath = '//tbody/tr[position()!=7]/td[1]/text()'
port_xpath = '//tbody/tr[position()!=7]/td[2]/text()'
ret = format_html(text, ip_xpath, port_xpath)
print(ret)

def ip_kaixin():
url = "http://www.kxdaili.com/dailiip/1/1.html"
text = get_html(url)
ip_xpath = '//tbody/tr/td[1]/text()'
port_xpath = '//tbody/tr/td[2]/text()'
ret = format_html(text, ip_xpath, port_xpath)
print(ret)

可以看到,进行公共方法提取之后,各个站点之间的代码都十分相似,上述内容都是只提取了一页数据,扩展到其它页面,在后文实现,在这之前,需要先处理一个特殊的站点:www.nimadaili.com/putong/1/。

该代理站点与上述站点存在差异,即 IP 与端口在一个 td 单元格中,如下图所示:在这里插入图片描述
需要针对该网站提供一个特殊的解析函数,如下所示,在代码中通过字符串分割进行 IP 与端口号的提取。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
python复制代码def ip_nima():
url = "http://www.nimadaili.com/putong/1/"
text = get_html(url)
ip_xpath = '//tbody/tr/td[1]/text()'
ret = format_html_ext(text, ip_xpath)
print(ret)

# 扩展HTML解析函数
def format_html_ext(text, ip_xpath):
# 待返回的IP与端口列表
ret = []
html = etree.HTML(text)
ips = html.xpath(ip_xpath)
print(ips)
for ip in ips:

item_dict = {
"ip": ip.split(":")[0],
"port": ip.split(":")[1]
}
ret.append(item_dict)

return ret

获取到的 IP 进行验证

获取到的 IP 进行可用性验证,并将可用 IP 存储到文件中。

检测方式有两种,代码分别如下:

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
python复制代码import telnetlib

# 代理检测函数
def check_ip_port(ip_port):
for item in ip_port:
ip = item["ip"]
port = item["port"]

try:
tn = telnetlib.Telnet(ip, port=port,timeout=2)
except:
print('[-] ip:{}:{}'.format(ip,port))
else:
print('[+] ip:{}:{}'.format(ip,port))
with open('ipporxy.txt','a') as f:
f.write(ip+':'+port+'\n')
print("阶段性检测完毕")


def check_proxy(ip_port):
for item in ip_port:
ip = item["ip"]
port = item["port"]
url = 'https://api.ipify.org/?format=json'
proxies= {
"http":"http://{}:{}".format(ip,port),
"https":"https://{}:{}".format(ip,port),
}
try:
res = requests.get(url, proxies=proxies, timeout=3).json()
if 'ip' in res:
print(res['ip'])

except Exception as e:
print(e)

第一种是通过 telnetlib 模块的 Telnet 方法实现,第二种通过请求固定地址实现。

扩大 IP 检索量

上述所有的 IP 检测都是针对一页数据实现,接下来修改为多页数据。依旧拿 89IP 举例。

在该函数参数中新增加一个 pagesize 变量,然后使用循环实现即可。

1
2
3
4
5
6
7
8
9
10
11
12
python复制代码def ip89(pagesize):

url_format = "https://www.89ip.cn/index_{}.html"
for page in range(1,pagesize+1):
url = url_format.format(page)
text = get_html(url)
ip_xpath = '//tbody/tr/td[1]/text()'
port_xpath = '//tbody/tr/td[2]/text()'
ret = format_html(text, ip_xpath, port_xpath)
# 检测代理是否可用
check_ip_port(ret)
# check_proxy(ret)

此时代码运行得到如下结果:

在这里插入图片描述
上述代码,当 IP 可用时,已经对 IP 进行了存储。

1
2
python复制代码with open('ipporxy.txt','a') as f:
f.write(ip+':'+port+'\n')

评论时间

代码下载地址:codechina.csdn.net/hihell/pyth…,可否给个 Star。

==来都来了,不发个评论,点个赞吗?==

本文转载自: 掘金

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

IDEA注释模板,惊艳了!动作要快,姿势要帅!

发表于 2021-11-22

一、类注释

打开 IDEA 的 Settings,点击 Editor-->File and Code Templates,点击右边 File 选项卡下面的Class,在其中添加图中红框内的内容:

1
perl复制代码/** * @author jitwxs * @date ${YEAR}年${MONTH}月${DAY}日 ${TIME} */

注 意

图片

在我提供的示例模板中,说明了作者和时间,IDEA 支持的所有的模板参数在下方的 Description 中被列出来。

保存后,当你创建一个新的类的时候就会自动添加类注释。如果你想对接口也生效,同时配置上图中的Interface 项即可。

二、方法注释

不同于目前网络上互相复制粘贴的方法注释教程,本文将实现以下功能:

  • 根据形参数目自动生成 @param 注解
  • 根据方法是否有返回值智能生成 @Return 注解

相较于类模板,为方法添加注释模板就较为复杂,首先在 Settings 中点击 Editor-->Live Templates。

点击最右边的 +,首先选择 2. Template Group... 来创建一个模板分组:

图片

在弹出的对话框中填写分组名,我这里叫做 userDefine:

图片

然后选中刚刚创建的模板分组 userDefine,然后点击 +,选择 1. Live Template:

图片

此时就会创建了一个空的模板,我们修改该模板的 Abbreviation、Description 和 Template text。需要注意的是,Abbreviation 必须为 *,最后检查下 Expand with 的值是否为 Enter 键。

图片

上图中· Template text 内容如下,请直接复制进去,需要注意首行没有 /,且 \* 是顶格的。

1
less复制代码* *  * @author jitwxs * @date $date$ $time$$param$ $return$ */

注意到右下角的 No applicable contexts yet 了吗,这说明此时这个模板还没有指定应用的语言:

图片

点击 Define,在弹框中勾选Java,表示将该模板应用于所有的 Java 类型文件。

图片

设置 applicable contexts

还记得我们配置 Template text 时里面包含了类似于 $date$ 这样的参数,此时 IDEA 还不认识这些参数是啥玩意,下面我们对这些参数进行方法映射,让 IDEA 能够明白这些参数的含义。点击 Edit variables 按钮:

图片

为每一个参数设置相对应的 Expression:

图片

设置 Expression

需要注意的是,date 和 time 的 Expression 使用的是 IDEA 内置的函数,直接使用下拉框选择就可以了,而 param 这个参数 IDEA 默认的实现很差,因此我们需要手动实现,代码如下:

1
scss复制代码groovyScript("def result = '';def params = \"${_1}\".replaceAll('[\\\\[|\\\\]|\\\\s]', '').split(',').toList(); for(i = 0; i < params.size(); i++) {if(params[i] != '')result+='* @param ' + params[i] + ((i < params.size() - 1) ? '\\r\\n ' : '')}; return result == '' ? null : '\\r\\n ' + result", methodParameters())

另外 return 这个参数我也自己实现了下,代码如下:

1
swift复制代码groovyScript("return \"${_1}\" == 'void' ? null : '\\r\\n * @return ' + \"${_1}\"", methodReturnType())

注:你还注意到我并没有勾选了 Skip if defined 属性,它的意思是如果在生成注释时候如果这一项被定义了,那么鼠标光标就会直接跳过它。我并不需要这个功能,因此有被勾选该属性。

点击 OK 保存设置,大功告成!

三、检验成果

3.1 类注释

类注释只有在新建类时才会自动生成,效果如下:

图片

类注释

3.2 方法注释

将演示以下几种情况:

  1. 无形参
  2. 单个形参
  3. 多个形参
  4. 无返回值
  5. 有返回值

图片

方法注释

四、Q & A

(1)为什么模板的 Abbreviation 一定要叫 \* ?Expand with 要保证是 Enter 键?

答:因为 IDEA 模板的生成逻辑是 模板名 + 生成键,当生成键是 Enter 时,我们输入 * + Enter 就能够触发模板。

这也同时说明了为什么注释模板首行是一个 * 了,因为当我们先输入 /*,然后输入 * + Enter,触发模板,首行正好拼成了 /**,符合 Javadoc 的规范。

(2)注释模板中为什么有一行空的 \*?

答:因为我习惯在这一行写方法说明,所以就预留了一行空的写,你也可以把它删掉。

(3)注释模板中 $time$$param$ 这两个明明不相干的东西为什么紧贴在一起?

答:首先网上提供的大部分 param 生成函数在无参情况下仍然会生成一行空的 @param,因此我对param 函数的代码进行修改,使得在无参情况下不生成 @param,但是这就要求 $param$ 要和别人处在同一行中,不然没法处理退格。

(4)为什么 return 参数不使用 methodReturnType(), 而要自己实现?

答:methodReturnType() 在无返回值的情况下会返回 void,这并没有什么意义,因此我对 methodReturnType() 返回值进行了处理,仅在有返回值时才生成。

(5)为什么 $return$ 不是单独一行?

答:因为当 methodReturnType() 返回 null 时,无法处理退格问题,原因同第三点。

本文转载自: 掘金

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

SpringBoot加email服务,你说有没有搞头? Sp

发表于 2021-11-22

SpringBoot加email服务,你说有没有搞头?

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

  1. 开启邮箱POP3/SMTP服务

登录qq邮箱后,点击左上方的设置,选择账户,如下图。

1637150849(1).png

然后一直往下滑,看到如下图的POP3/SMTP服务,点击开启,会让帮定的密保手机号发个[短信]到指定号码,然后会收到一个授权码,这个授权码在appliction.properties配置中会用到,一定要好好保存,开启后如下图

image.png

  1. springboot项目添加依赖

创建springboot项目,添加email依赖

1
2
3
4
xml复制代码<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>

我的依赖如下:

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
xml复制代码<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.5.3</version>
</dependency>

<!-- https://mvnrepository.com/artifact/com.google.code.gson/gson -->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.2</version>
</dependency>

<!-- 模板引擎,发送模板邮件用到 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

<!-- email-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>

</dependencies>
  1. 配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
properties复制代码######qq邮箱########
#协议
spring.mail.protocol=smtp
#邮箱服务器地址
spring.mail.host=smtp.qq.com
#邮箱服务器地址
spring.mail.username=xxx@qq.com
#这里的password不是登录密码,是开启POP3之后设置的客户端授权码
#邮箱密码,开启POP3/SMTP服务时会有给你
spring.mail.password=自己POP3/SMTP服务密码
#编码格式
spring.mail.default-encoding=UTF-8
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true
spring.mail.properties.mail.smtp.starttls.required=true
##默认端口25,使用465端口时,需要添加配置:
spring.mail.port=465
spring.mail.properties.mail.smtp.ssl.enable=true
  1. 邮件服务操作类

springboot引用模块都通常都提供一个xxxTmplate,方便我们开发,我们可以封装一个EmailTmplate

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
157
java复制代码package com.ljw.task.config;

import lombok.Getter;
import org.springframework.boot.autoconfigure.mail.MailProperties;
import org.springframework.core.io.FileSystemResource;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Service;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.IContext;

import javax.annotation.Resource;
import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;

/**
* @Description: 邮件操作类
* @Author: jianweil
* @date: 2021/11/19 15:07
*/
@Service
@Getter
public class EmailTemplate {

@Resource
private JavaMailSender javaMailSender;

@Resource
private TemplateEngine templateEngine;

//@Resource
//private MailProperties mailProperties;

/**
* 发送简单文本邮件
*
* @param from 发送者邮箱
* @param to 接受者邮箱
* @param subject 邮件主题
* @param text 邮件内容
*/
public void sendTextMail(String from, String to, String subject, String text) {
SimpleMailMessage message = new SimpleMailMessage();
// 发送者
message.setFrom(from);
// 接收者
message.setTo(to);
//邮件主题
message.setSubject(subject);
// 邮件内容
message.setText(text);
javaMailSender.send(message);
}


/**
* 发送Html邮件
*
* @param from 发送者邮箱
* @param to 接受者邮箱
* @param subject 邮件主题
* @param html 邮件内容 带html标签
*/
public void sendHtmlMail(String from, String to, String subject, String html) {
try {
MimeMessage message = javaMailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, true);
helper.setFrom(from);
helper.setTo(to);
helper.setSubject(subject);
helper.setText(html, true);
javaMailSender.send(message);
} catch (MessagingException e) {
throw new RuntimeException("Messaging Exception !", e);
}
}


/**
* 发送附件邮件
*
* @param from 发送者邮箱
* @param to 接受者邮箱
* @param subject 邮件主题
* @param text 邮件内容
* @param attachmentFilename 附件名称
* @param file 附件
*/
public void sendAttachmentMail(String from, String to, String subject, String text, String attachmentFilename, FileSystemResource file) {
MimeMessage message = javaMailSender.createMimeMessage();
try {
MimeMessageHelper helper = new MimeMessageHelper(message, true);
helper.setFrom(from);
helper.setTo(to);
helper.setSubject(subject);
helper.setText(text);
//加入邮件
helper.addAttachment(attachmentFilename, file);
javaMailSender.send(message);
} catch (MessagingException e) {
throw new RuntimeException("Messaging Exception !", e);
}
}

/**
* 发送内联资源邮件
*
* @param from 发送者邮箱
* @param to 接受者邮箱
* @param subject 邮件主题
* @param text 邮件内容,包含内联文件id 如: String text = "<html><body>宫崎骏电影图片:<img src='cid:" + contentId + "' ></body></html>";
* @param contentId 内联文件id
* @param file 文件
*/
public void sendInlineResourceMail(String from, String to, String subject, String text, String contentId, FileSystemResource file) {
MimeMessage message = javaMailSender.createMimeMessage();
try {
MimeMessageHelper helper = new MimeMessageHelper(message, true);
helper.setFrom(from);
helper.setTo(to);
helper.setSubject(subject);
helper.setText(text, true);
helper.addInline(contentId, file);
javaMailSender.send(message);
} catch (MessagingException e) {
throw new RuntimeException("Messaging Exception !", e);
}
}

/**
* 发送模板邮件
*
* @param from 发送者邮箱
* @param to 接受者邮箱
* @param subject 邮件主题
* @param context 内容类,和模板匹配参数,如下配置id参数的值为myvaluesitest:
* Context context = new Context();
* context.setVariable("id","myvaluesitest");
* @param template templates目录下的模板文件名,如templates/emailTemplate.html则传:emailTemplate
*/
public void sendTemplateMail(String from, String to, String subject, IContext context, String template) {
MimeMessage message = javaMailSender.createMimeMessage();
try {
MimeMessageHelper helper = new MimeMessageHelper(message, true);
helper.setFrom(from);
helper.setTo(to);
helper.setSubject(subject);
String text = templateEngine.process(template, context);
helper.setText(text, true);
javaMailSender.send(message);
} catch (MessagingException e) {
throw new RuntimeException("Messaging Exception !", e);
}
}

}
  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
java复制代码package com.ljw.task.config;

import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.mail.MailProperties;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.core.io.FileSystemResource;
import org.thymeleaf.context.Context;

import javax.annotation.Resource;
import java.io.File;

/**
* @Description: todo
* @Author: jianweil
* @date: 2021/11/21 18:45
*/
@SpringBootTest
class EmailTemplateTest {

@Resource
private EmailTemplate emailTemplate;
@Resource
private MailProperties mailProperties;

public String getSender() {
return mailProperties.getUsername();
}

public String getReceiver() {
return mailProperties.getUsername();
}

/**
* 文本
*/
@Test
void sendTextMail() {
emailTemplate.sendTextMail(getSender(), getReceiver(), "标题:sendTextMail", "文本内容");
}

/**
* 带html标签
*/
@Test
void sendHtmlMail() {
StringBuffer sb = new StringBuffer();
sb.append("<h1>大标题-h1</h1>")
.append("<p style='color:#F00'>红色字</p>")
.append("<p style='text-align:right'>右对齐</p>");
emailTemplate.sendHtmlMail(getSender(), getReceiver(), "标题:sendHtmlMail", sb.toString());
}

/**
* 带附件
*/
@Test
void sendAttachmentMail() {
FileSystemResource file = new FileSystemResource(new File("src/main/resources/static/images/avatar.jpg"));
emailTemplate.sendAttachmentMail(getSender(), getReceiver(), "标题:sendAttachmentMail", "我发了一个附件给你注意查收", "附件名.jpg", file);
}

/**
* 发送内联资源邮件
*/
@Test
void sendInlineResourceMail() {
String imgId = "avatar";
String content = "<html><body>宫崎骏电影图片:<img src='cid:" + imgId + "' ></body></html>";
FileSystemResource res = new FileSystemResource(new File("src/main/resources/static/images/avatar.jpg"));
emailTemplate.sendInlineResourceMail(getSender(), getReceiver(), "标题:sendInlineResourceMail", content, imgId, res);
}

/***
* 发送模板邮件
*/
@Test
void sendTemplateMail() {
Context context = new Context();
context.setVariable("id", "hello");
emailTemplate.sendTemplateMail(getSender(), getReceiver(), "标题:sendTemplateMail", context, "helloTemplate");
}
}

发送模板邮件用到的模板类:helloTemplate.html

image.png
helloTemplate.html:

1
2
3
4
5
6
7
8
9
10
11
java复制代码<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8"/>
<title>模板邮件</title>
</head>
<body>
您好,这是模板邮件,请点击下面的链接完成跳转,
<a href="#" th:href="@{'http://www.baidu.com/s?wd='+${id}}">跳转百度</a> <a th:text=" ${id}"></a>。
</body>
</html>

发送附件用到的图片:avatar.jpg

image.png
avatar.jpg:
avatar.jpg

这里就不贴测试发送邮件的截图,发送人和接收人换成自己邮箱,自己测试下就行。

  1. 常用业务分析

  1. 用户注册后需要激活账号才能使用的场景:

用户注册成功,保存数据到redis,设置过期时间,并发送邮件信息到指定邮箱,该邮件含有用户唯一标识的key的超链接,用户点击该超链接则回访到我们的激活接口,验证信息正确则激活。如在设定时间内没有点击激活则激活失败。

  1. 其他需要通知的场景

本文转载自: 掘金

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

终于签完了,1000 本

发表于 2021-11-22

是的,今天我终于完成了我的新书《Python3网络爬虫开发实战(第二版)》的签名,一共 1000 本,整个过程感觉着实不容易啊。

签完我就来写这篇文章了,感觉左右胳膊完全不是一个感觉。

1

先说下事情的来龙去脉吧,因为很多读者想要一本签名版的书籍,毕竟有需求就有市场。于是编辑和我商讨之后,让我对其中的一部分进行签名。

其实当时第一版出来的时候也签了一些,那会签的少,我记得只有三百多本。

遥想那会,还是 2018 年,那会第一版的书刚刚印刷出来,那会我是去河北的一个印厂签名的。印厂把新鲜出炉的扉页的那一沓拿给我,我一个个进行签名,我还有当年签名的照片:

我去,我当年这么瘦的吗?

这里简单说下扉页这一沓是怎么回事,有朋友就会好奇问为啥不直接拿给你一张纸,而是一沓纸呢?这是因为印刷的时候是用的一张大纸,比如正反面一张大纸就能覆盖书的二三十页内容,然后印刷完毕之后,这张大纸就会被折叠,最后就得到了这小小的一沓。

所以,每一本书,我只需要在这一小沓纸上签名,最后印厂再拿回去进行锁线、装订那就得到完整的书了。

2

好了,再说回签名的事。

今年和往年的签名流程不一样了。

印厂还是在河北,然而由于疫情原因,北京疫情管控非常严格,进京必须要 2 日之内的核算证明。另外去的话还不能光我一人吧,加上编辑还有同行同事肯定至少也得三个人,就感觉比较麻烦。

于是我们就决定这次不去印厂签名了(所以就没法拍照片了)。

那怎么办呢?

我们就决定让印厂把书的扉页都拿给我,然后我签完了再运回印厂,印厂再进行组装。

于是乎,在周二的时候,我收到了整整两大箱子书扉页。

好家伙,这数量着实不少啊,而且特别重。

另外我住的地方其实是没有电梯的 6 楼,光搬上去就废了好大的劲。

后来,从周二开始,周二、三、四、五,我每天下班之后都签一点,每天两百多本。

差不多下班后要花将近两小时签名吧,我也没仔细计算,可能中间有少许休息时间,所以这几天光签名得花了得有七八小时?

3

然后一直到今晚上,哦不,应该是凌晨,终于是签完了。

感觉手都签麻了。

这是一开始的时候签的:

这是最后的时候签的:

哪个好看?一定是第二张好看对不对?!更加飘逸对不对?(逃

最后晒一下完整成果吧,整整两箱子,都签好啦!

我联系了印厂,印厂周六会有师傅过来我家取。

等师傅取回印厂之后,印厂就会安排锁线、装订、裁切等各个环节,最后完整的书就有啦。

4

然后到这里,就会有朋友问了,现在书到底啥时候能买到啊?

其实非签名版的书已经印刷好啦,前天的时候编辑已经拿到了样书,今天还有给书的专门拍摄,这里给大家看看样子哈。

这是编辑发我的图,我其实还没拿到实体书。

然后我就问编辑,这书厚不厚啊?

编辑说,厚!

然后给我发了一张测量图:

好家伙,我直接好家伙,4.3 cm 厚,大家感受一下吧。

加上用的纸还算比较好的,好像说有两三斤重?

emm,具体到手之后再体验下吧。

印刷完了之后,接下来就是入库销售了。

然后具体的售卖时间,编辑说周末或下周一会知道入库时间是什么时候,但我预估下周应该大家就可以开始购买啦。

签名版的话可能会先预售,就是发货较晚的意思,因为我刚刚签完,组装还需要几天时间,到时候应该会在京东上直接销售签名版。

售价的话,应该会打 7 折,原价 139.8,所以打完折是 98 元。

嗯嗯!总之,大家马上就可以买到啦,实在让大家久等了,到时候我会在公号第一时间发布最新消息,非常感谢大家的支持!

撒花!

更多精彩内容,请关注我的公众号「进击的 Coder」和「崔庆才丨静觅」。

本文转载自: 掘金

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

基于SSM实现记账系统demo 1 前言 2 环境搭建 3

发表于 2021-11-22

1 前言

  本demo是基于SSM框架完成的一个小项目,用于初学者进行基本的框架熟练度练习。源码获取方式在文末,以下是程序运行的几个页面:
在这里插入图片描述
在这里插入图片描述

2 环境搭建

2.1 基本架构

在这里插入图片描述
1. 添加依赖包,可以通过properties统一框架版本

1
2
3
xml复制代码<properties>
<springversion>5.0.8.RELEASE</springversion>
</properties>
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
xml复制代码<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>

<!-- 加入ServletAPI -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
<!-- MySQL依赖 start -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.38</version>
</dependency>
<!-- 加入MyBatis 依赖 start -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.2.8</version>
</dependency>
<!-- 引入Spring(包含SpringMVC) 依赖 start -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${springversion}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>${springversion}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-oxm</artifactId>
<version>${springversion}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>${springversion}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>${springversion}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${springversion}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>${springversion}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>${springversion}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>${springversion}</version>
</dependency>
<!-- 引用插件依赖:MyBatis整合Spring,如果mybatis版本在3.4及以上版本
mybatis-spring的版本要在1.3以上 -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>1.2.2</version>
</dependency>
<!-- JSTL -->
<dependency>
<groupId>jstl</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
<!-- 德鲁伊数据连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.9</version>
</dependency>
<!-- pagehelper -->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>4.1.6</version>
</dependency>
<!--处理json-->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.5</version>
</dependency>
<!--javaee-->
<dependency>
<groupId>javax</groupId>
<artifactId>javaee-api</artifactId>
<version>7.0</version>
<scope>provided</scope>
</dependency>
<!--文件上传下载-->
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.3.1</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.4</version>
</dependency>
</dependencies>

如果遇到声明式事务报错,需要添加下面的依赖包

1
2
3
4
5
xml复制代码<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.1</version>
</dependency>

2. 添加spring配置文件
实现8个关键步骤

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
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd">

<!--1 配置数据源-->
<bean id="db" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/kaikeba0917"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</bean>

<!--2 创建sqlSessionFactory-->
<bean id="fac" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="db"/>
<!--指定mybatis的配置文件路径-->
<property name="configLocation" value="classpath:mybatis.xml"/>
<!--指定mybatis的mapper文件路径-->
<property name="mapperLocations" value="classpath:mapper/*.xml"/>
</bean>

<!--3 创建sqlSessionTemplate(这个类没有无参构造)-->
<bean class="org.mybatis.spring.SqlSessionTemplate">
<constructor-arg name="sqlSessionFactory" ref="fac"/>
</bean>

<!--4 配置事务-->
<bean id="mytx" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="db"/>
</bean>
<tx:annotation-driven transaction-manager="mytx"/>

<!--5 启用springMVC注解-->
<mvc:annotation-driven/>

<!--6 配置视图解析器-->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/"/>
<property name="suffix" value=".jsp"/>
</bean>

<!--7 扫描注解包-->
<context:component-scan base-package="com.wangjiawei"/>

<!--8 配置静态资源访问-->
<mvc:default-servlet-handler/>

</beans>

3. 配置web.xml文件,同时加载spring配置文件

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
xml复制代码<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" version="2.5">


<display-name>Archetype Created Web Application</display-name>

<!--1 配置前端控制器-->
<servlet>
<servlet-name>aa</servlet-name>
<servlet-class>
org.springframework.web.servlet.DispatcherServlet
</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring.xml</param-value>
</init-param>
</servlet>

<servlet-mapping>
<servlet-name>aa</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>

<!--2 处理post乱码-->
<filter>
<filter-name>bb</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>utf-8</param-value>
</init-param>
</filter>

<filter-mapping>
<filter-name>bb</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

</web-app>

4. 创建数据表,添加项目包结构,包括实体类等
  给dao,service,web(controller)包下使用注解创建对象,给service,web中的属性注入对象
dao:

1
2
3
java复制代码public interface BooksDao {
public List<Books> getAll();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码@Repository
public class BookDaoImpl implements BooksDao {

/** 该对象来源于配置文件
*
*/
@Resource
private SqlSessionTemplate sqlSessionTemplate;

@Override
public List<Books> getAll() {
return sqlSessionTemplate.selectList("com.wangjiawei.dao.BooksDao.getAll");
}
}

service:

1
2
3
java复制代码public interface BooksService {
public List<Books> getAll();
}
1
2
3
4
5
6
7
8
9
10
java复制代码@Service
public class BooksServiceImpl implements BooksService {
@Resource
private BooksDao dao;

@Override
public List<Books> getAll() {
return dao.getAll();
}
}

web(controller):

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码@Controller
public class BooksController {
@Resource
private BooksService booksService;

@RequestMapping("/getallbooks")
public String getAll(ModelMap map){
List<Books> books = booksService.getAll();
map.addAttribute("booklist", books);
return "show";
}
}

5. 配置文件代码mybatis.xml 及相应的 BooksMapper.xml
mybatis.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>

<!--给类起别名-->
<typeAliases>
<!-- <typeAlias type="com.wangjiawei.bean.Student" alias="stu"></typeAlias>-->
<package name="com.wangjiawei.bean"/>
</typeAliases>

<!--分页插件-->
<plugins>
<!-- PageHelper4.1.6 -->
<plugin interceptor="com.github.pagehelper.PageHelper">
<property name="dialect" value="mysql"/>
</plugin>
</plugins>

</configuration>

BooksMapper.xml:

1
2
3
4
5
6
7
8
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.wangjiawei.dao.BooksDao">
<select id="getAll" resultType="books">
select * from books
</select>
</mapper>

6. 配置controller文件
使用的注解:
@Controller
@RequestMapping
@AutoWired
@Qualifier(“empBiz”)
(同上面的步骤4)

7. 添加service,dao层
使用的注解:
@Service
@AutoWired
(1) dao层省略了实现类
(2) dao 层只定义接口,由小树叶创建dao层对象以及扫描mapper文件
注:当添加了spring-jdbc的jar包后,会自动提交事务

8. 前端测试网页
index.jsp:

1
2
3
4
5
6
html复制代码<html>
<body>
<h2>Hello World!</h2>
<a href="/getallbooks">getallbooks</a>
</body>
</html>

show.jsp:

1
2
3
4
5
6
7
8
9
10
11
12
13
html复制代码<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<h1>show.jsp</h1>
<c:forEach items="${booklist}" var="book">
${book.bookId}——${book.bookName}
</c:forEach>
</body>
</html>

关于其中的配置文件:
  当项目启动时加载web.xml文件,web.xml文件加载spring.xml文件,spring的配置文件又去加载了mybatis的配置文件与mapper文件。

2.2 省略dao实现类

先将上面的dao实现类删掉

在这里插入图片描述
使用MapperScannerConfigurer替代SqlSessionTemplate

1
2
3
4
5
xml复制代码<!--省略实现类-->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.dao"></property>
<property name="sqlSessionFactoryBeanName" value="fac"></property>
</bean>

注意:此时也可以测试使用属性文件的方式来加载数据源(支持属性文件链接数据源)
在这里插入图片描述

2.3 ssm整合mybatis反向生成工具

依赖包:

1
2
3
4
5
xml复制代码<dependency>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-core</artifactId>
<version>1.3.5</version>
</dependency>

加载插件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
xml复制代码<build>
<plugins>
<plugin>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-maven-plugin</artifactId>
<version>1.3.5</version>
<configuration>
<!--配置文件的路径-->
<configurationFile>src/main/resources/generatorConfig.xml</configurationFile>
<overwrite>true</overwrite>
</configuration>
<dependencies>
<dependency>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-core</artifactId>
<version>1.3.5</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>

根据插件中的内容,在resource文件夹下创建generatorConfig.xml文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
"http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<!-- 配置生成器 -->
<generatorConfiguration>
<!--数据库驱动jar -->
<classPathEntry
location="F:\develop\maven_repository\mysql\mysql-connector-java\5.1.6\mysql-connector-java-5.1.6.jar" />
<context id="MyBatis" targetRuntime="MyBatis3">
<!--去除注释 -->
<commentGenerator>
<property name="suppressAllComments" value="true" />
</commentGenerator>
<!--数据库连接 -->
<jdbcConnection driverClass="com.mysql.jdbc.Driver"
connectionURL="jdbc:mysql://localhost:3306/kaikeba0917"
userId="root"
password="root">
</jdbcConnection>
<!--生成实体类 指定包名 以及生成的地址 (可以自定义地址,但是路径不存在不会自动创建
使用Maven生成在target目录下,会自动创建) -->
<javaModelGenerator targetPackage="com.wangjiawei.bean"
targetProject="D:\程序员\开课吧新职课\11SSM\5SSM整合\ssm\src\main\java">
<property name="trimStrings" value="true" />
</javaModelGenerator>
<!--生成SQLmapper文件 -->
<sqlMapGenerator targetPackage="mapper"
targetProject="D:\程序员\开课吧新职课\11SSM\5SSM整合\ssm\src\main\resources">
</sqlMapGenerator>
<!--生成Dao文件,生成接口 -->
<javaClientGenerator type="XMLMAPPER"
targetPackage="com.wangjiawei.dao"
targetProject="D:\程序员\开课吧新职课\11SSM\5SSM整合\ssm\src\main\java">
</javaClientGenerator>
<table tableName="student" enableCountByExample="false"
enableUpdateByExample="false" enableDeleteByExample="false"
enableSelectByExample="false" selectByExampleQueryId="false">
</table>
</context>
</generatorConfiguration>

maven类型的web项目加载mybatis-generator插件:

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
注意:反向生成后,最好把该指令删除,否则不小心点了之后,又会生成一遍代码,尤其是mapper.xml文件中sql语句又会生成一遍,运行时会报错。

3 记账项目

3.1 需求分析

3.1.1 记账管理

在这里插入图片描述
其中,查询条件中类型为下拉列表,可选值有“不限,支出,收入,转账,借出,借入,还入,还出”。日期输入框需要输入“yyyy-MM-dd”格式的日期字符串,点击“搜索”按钮提交表单,提交时如果输入项不符合输入要求,则显示相应提示信息。列表中根据记账类别在金额前添加相应的“+,-”符号,如果类别为“支出,借出,还出”时在金额前添加“-”。
如果类别为“收入,借入,还入”时在金额前添加“+”。如果根据输入项进行查询后没有找到账单数据,则给出提示信息,如图所示:

在这里插入图片描述
点击“记账”按钮后,进入记账页面。

3.1.2 记账

在这里插入图片描述

  • 类型属性是单选框, 标题输入框最长输入不能超过25 字符, 日期输入框需要输入“yyyy-MM-dd”格式的日期字符串,金额输入框必须为大于0 的数,说明输入框中最大输入长度为250 字符。
  • 点击“重置”按钮则恢复初始值
  • 点击“保存”按钮执行保存功能。提交数据至Controller 前必须使用JS 验证。如果各属性输入值不符合要求则需提示用户。
  • 点击“返回”按钮放弃当前记账操作。并返回首页。

功能补充:在“说明”列后加一列,叫“操作”。用户可以删除或修改数据。
分页显示:上一页 下一页 首页 尾页 当前页码 总页数 总条数

3.1.3 参考数据库

在这里插入图片描述
在这里插入图片描述

1
2
3
4
5
6
7
8
9
10
11
12
13
sql复制代码create table bills(
id int primary key auto_increment,
title varchar(50),
billtime date,
typeid int,
price double,
explains varchar(50)
);

create table billtype(
id int primary key auto_increment,
bname varchar(5)
);

3.2 环境搭建

先创建基本的目录

在这里插入图片描述
然后按照上面的步骤配置pom.xml、spring.xml、mybatis.xml、generatorConfig.xml,接着使用mybatis的自动生成功能自动生成

3.3 查询数据

配置web.xml中的请求分发器,以及处理post乱码

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
xml复制代码<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" version="2.5">
<display-name>Archetype Created Web Application</display-name>

<!--1 配置前端控制器-->
<servlet>
<servlet-name>aa</servlet-name>
<servlet-class>
org.springframework.web.servlet.DispatcherServlet
</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring.xml</param-value>
</init-param>
</servlet>

<servlet-mapping>
<servlet-name>aa</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>

<!--2 处理post乱码-->
<filter>
<filter-name>bb</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>utf-8</param-value>
</init-param>
</filter>

<filter-mapping>
<filter-name>bb</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>


</web-app>

3.3.1 展示下拉列表和基本信息

在这里插入图片描述
dao:
BillsMapper

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
java复制代码package com.wangjiawei.dao;

import com.wangjiawei.bean.Bills;

import java.util.List;

public interface BillsMapper {

/**
* 查询所有账单
* @return
*/
public List<Bills> getBills();

int deleteByPrimaryKey(Integer id);

int insert(Bills record);

int insertSelective(Bills record);

Bills selectByPrimaryKey(Integer id);

int updateByPrimaryKeySelective(Bills record);

int updateByPrimaryKey(Bills record);
}

在BillsMapper.xml中添加对应实现:

1
2
3
4
5
6
7
8
9
10
11
12
xml复制代码<resultMap id="BaseResultMap" type="com.wangjiawei.bean.Bills">
<id column="id" jdbcType="INTEGER" property="id" />
<result column="title" jdbcType="VARCHAR" property="title" />
<result column="billtime" jdbcType="DATE" property="billtime" />
<result column="typeid" jdbcType="INTEGER" property="typeid" />
<result column="price" jdbcType="DOUBLE" property="price" />
<result column="explains" jdbcType="VARCHAR" property="explains" />
</resultMap>

<select id="getBills" resultMap="BaseResultMap">
select * from bills
</select>

BillTypeMapper

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
java复制代码package com.wangjiawei.dao;

import com.wangjiawei.bean.Billtype;

import java.util.List;

public interface BilltypeMapper {

/**
* 查询所有账单类型
* @return
*/
public List<Billtype> getTypes();

int deleteByPrimaryKey(Integer id);

int insert(Billtype record);

int insertSelective(Billtype record);

Billtype selectByPrimaryKey(Integer id);

int updateByPrimaryKeySelective(Billtype record);

int updateByPrimaryKey(Billtype record);
}

在BillTypeMapper.xml中添加对应实现:

1
2
3
4
5
6
7
8
xml复制代码<resultMap id="BaseResultMap" type="com.wangjiawei.bean.Billtype">
<id column="id" jdbcType="INTEGER" property="id" />
<result column="bname" jdbcType="VARCHAR" property="bname" />
</resultMap>

<select id="getTypes" resultMap="BaseResultMap">
select * from billtype
</select>

service层:
BillsService接口:

1
2
3
4
5
6
7
8
java复制代码public interface BillsService {

/**
* 查询所有账单
* @return
*/
public List<Bills> getBills();
}

BillsServiceImpl实现类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码@Service
public class BillsServiceImpl implements BillsService {

@Resource
private BillsMapper billsMapper;

/**
* 查询所有账单
*
* @return
*/
@Override
public List<Bills> getBills() {
return billsMapper.getBills();
}
}

BillTypesService接口:

1
2
3
4
5
6
7
8
java复制代码public interface BillTypesService {

/**
* 查询所有账单类型
* @return
*/
public List<Billtype> getTypes();
}

BillTypesService实现类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码@Service
public class BillTypesServiceImpl implements BillTypesService {
@Resource
private BilltypeMapper billtypeMapper;

/**
* 查询所有账单类型
*
* @return
*/
@Override
public List<Billtype> getTypes() {
return billtypeMapper.getTypes();
}
}

controller层:
BillsController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码@Controller
public class BillsController {
@Resource
private BillTypesService typesService;
@Resource
private BillsService billsService;

@RequestMapping("/gettypes")
public String gettypes(ModelMap map){
// 1 查询所有账单类型
List<Billtype> types = typesService.getTypes();
// 2 查询所有的账单
List<Bills> bills = billsService.getBills();
// 保存数据给前台
map.addAttribute("types", types);
map.addAttribute("bills", bills);
return "show";
}
}

相应的前端界面:
index.jsp

1
2
3
4
5
6
7
html复制代码<html>
<body>
<script type="application/javascript">
location.href = "/gettypes";
</script>
</body>
</html>

show.jsp

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
html复制代码<%--
Created by IntelliJ IDEA.
User: 12291
Date: 2020/10/30
Time: 10:26
To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<h1>记账管理</h1>

<p>
<form>
类型:
<select>
<option value="-1">不限</option>
<c:forEach items="${types}" var="tp">
<option value="${tp.id}">${tp.bname}</option>
</c:forEach>
</select>

时间:
从<input type="text" name="begin">到<input type="text" name="end">
<input type="submit" value="搜索">
</form>
<input type="button" value="记账">
</p>
<table border="1" width="500">
<tr>
<td>标题</td>
<td>记账时间</td>
<td>类别</td>
<td>金额</td>
<td>说明</td>
<td>操作</td>
</tr>

<c:forEach items="${bills}" var="bill">
<tr>
<td>${bill.title}</td>
<td><fmt:formatDate value="${bill.billtime}" pattern="yyyy-MM-dd"/></td>
<td>类别</td>
<td>${bill.price}</td>
<td>${bill.explains}</td>
<td>删除 修改</td>
</tr>
</c:forEach>
</table>
</body>
</html>

3.3.2 前端页面【类别】部分的显示

这里需要两表联查:

1
sql复制代码select * from bills b, billtype t where b.typeid = t.id;

在这里插入图片描述
先添加实体类的一对多关系:
Bills类中添加属性

1
java复制代码private Billtype billtype;

BillType中添加属性

1
java复制代码private List<Bills> billsList;

相应的要修改mapper.xml:
BillsMapper.xml(要展示类别只用修改这一个就行了)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
xml复制代码<resultMap id="rs1" type="bills">
<id column="id" jdbcType="INTEGER" property="id" />
<result column="title" jdbcType="VARCHAR" property="title" />
<result column="billtime" jdbcType="DATE" property="billtime" />
<result column="typeid" jdbcType="INTEGER" property="typeid" />
<result column="price" jdbcType="DOUBLE" property="price" />
<result column="explains" jdbcType="VARCHAR" property="explains" />

<association property="billtype" javaType="com.wangjiawei.bean.Billtype">
<id column="id" jdbcType="INTEGER" property="id" />
<result column="bname" jdbcType="VARCHAR" property="bname" />
</association>
</resultMap>

<select id="getBills" resultMap="rs1">
select * from bills b, billtype t where b.typeid = t.id;
</select>

修改前端show.jsp:
在这里插入图片描述
在这里插入图片描述

3.3.3 前端页面【金额】部分的显示

支出,借出时金额应该有一个负号
只用在前端界面中使用choose when即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
html复制代码<c:forEach items="${bills}" var="bill">
<tr>
<td>${bill.title}</td>
<td><fmt:formatDate value="${bill.billtime}" pattern="yyyy-MM-dd"/></td>
<td>${bill.billtype.bname}</td>
<td>
<c:choose>
<c:when test="${bill.billtype.bname =='支出' || bill.billtype.bname =='借出' || bill.billtype.bname =='还出'}">
-${bill.price}
</c:when>
<c:when test="${bill.billtype.bname =='收入' || bill.billtype.bname =='借入' || bill.billtype.bname =='还入'}">
+${bill.price}
</c:when>
<c:otherwise>
${bill.price}
</c:otherwise>
</c:choose>
</td>
<td>${bill.explains}</td>
<td>删除 修改</td>
</tr>
</c:forEach>

在这里插入图片描述

3.3.4 通过时间和类型模糊查询

实际上,查询所有的语句和模糊查询的语句是同一种语句,只不过模糊查询的语句是在查询所有语句的基础上进行一些拼接,这两种查询可以调用同一个接口。

前端页面:
为搜索这一部分添加一个form

1
2
3
4
5
6
7
8
9
10
11
12
13
14
html复制代码<form action="/getAllBills" method="post">
类型:
<select name="typeid">
<option value="-1">不限</option>
<c:forEach items="${types}" var="tp">
<option value="${tp.id}" ${tid==tp.id?'selected':''}>${tp.bname}</option>
</c:forEach>
</select>

时间:
从<input type="text" name="begin" value="${begintime}">到<input type="text" name="end" value="${endtime}">
<input type="submit" value="搜索">
</form>
<input type="button" value="记账">

controller层:
添加查询所有的方法 getAllBills。
其中添加了回显操作,由于回显操作也要根据type的id进行匹配展示(见前端界面),所以这里也要查询一下所有的type。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码/**
* 查询所有账单
* @return
*/
@RequestMapping("/getAllBills")
public String getBills(Integer typeid, String begin, String end, ModelMap map){

List<Bills> bills = billsService.getBills(typeid, begin, end);
map.addAttribute("bills", bills);
// 数据回显
// 将模糊查询的条件值再返回给前台
map.addAttribute("tid", typeid);
map.addAttribute("begintime", begin);
map.addAttribute("endtime", end);

List<Billtype> types = typesService.getTypes();
map.addAttribute("types", types);

return "show";
}

service层:
dao接口:

1
java复制代码public List<Bills> getBills(int typeid, String begin, String end);

dao实现类:

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码/**
* 查询所有账单
*
* @return
*/
@Override
public List<Bills> getBills(int typeid, String begin, String end) {
Map params = new HashMap();
params.put("tid", typeid);
params.put("begin", begin);
params.put("end", end);
return billsMapper.getBills(params);
}

dao:
修改BillsMapper的getBills方法

1
java复制代码public List<Bills> getBills(Map map);

相应的配置文件BillsMapper.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
xml复制代码<resultMap id="rs1" type="bills">
<id column="id" jdbcType="INTEGER" property="id" />
<result column="title" jdbcType="VARCHAR" property="title" />
<result column="billtime" jdbcType="DATE" property="billtime" />
<result column="typeid" jdbcType="INTEGER" property="typeid" />
<result column="price" jdbcType="DOUBLE" property="price" />
<result column="explains" jdbcType="VARCHAR" property="explains" />

<association property="billtype" javaType="com.wangjiawei.bean.Billtype">
<id column="id" jdbcType="INTEGER" property="id" />
<result column="bname" jdbcType="VARCHAR" property="bname" />
</association>
</resultMap>

<select id="getBills" resultMap="rs1">
select * from bills b, billtype t where b.typeid = t.id
<if test="tid != -1">
and t.id=#{tid}
</if>
<if test="begin!=null and begin!=''">
and b.billtime>=#{begin}
</if>
<if test="end!=null and end!=''">
and b.billtime <![CDATA[ <= ]]> #{end}
</if>
</select>

3.3.5 分页展示

使用工具类PageHelper进行分页操作,在service层进行这个操作。

service:
分页查询需要接收新的参数,当前页数:index和每页显示条数size
接口

1
java复制代码public PageInfo<Bills> getBills(int typeid, String begin, String end, int index, int size);

实现类:

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
java复制代码@Service
public class BillsServiceImpl implements BillsService {

@Resource
private BillsMapper billsMapper;

/**
* 查询所有账单
*
* @return
*/
@Override
public PageInfo<Bills> getBills(int typeid, String begin, String end, int index, int size) {
Map params = new HashMap();
params.put("tid", typeid);
params.put("begin", begin);
params.put("end", end);

// 1 指定分页数据
PageHelper.startPage(index, size);
// 2 查询数据
List<Bills> bills = billsMapper.getBills(params);
// 3 创建分页工具类
PageInfo<Bills> info = new PageInfo<>(bills);

return info;
}
}

controller层:
controller中修改查询操作的调用,传入一个size和index,并且返回给前端的数据变成PageInfo对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码@RequestMapping("/gettypes")
public String gettypes(ModelMap map){
// 1 查询所有账单类型
List<Billtype> types = typesService.getTypes();

// 2 查询所有的账单
PageInfo<Bills> info = billsService.getBills(-1, null, null, 1, PageUtil.PAGESIZE);

// 保存数据给前台
map.addAttribute("types", types);
map.addAttribute("info", info);

return "show";
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码@RequestMapping("/getAllBills")
public String getBills(@RequestParam(defaultValue = "1") int index, @RequestParam(defaultValue = "-1") Integer typeid, String begin, String end, ModelMap map){

PageInfo<Bills> info = billsService.getBills(typeid, begin, end, index, PageUtil.PAGESIZE);
map.addAttribute("info", info);
// 数据回显
// 将模糊查询的条件值再返回给前台
map.addAttribute("tid", typeid);
map.addAttribute("begintime", begin);
map.addAttribute("endtime", end);

List<Billtype> types = typesService.getTypes();
map.addAttribute("types", types);

return "show";
}

其中index被定义为一个常量3

1
2
3
java复制代码public interface PageUtil {
public int PAGESIZE = 3;
}

前端页面:
后台返回给前端的数据变成PageInfo对象;
要控制点击上一页,下一页时显示的范围;
分页查询的时候记得要带上模糊查询的参数;

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
html复制代码<%--
Created by IntelliJ IDEA.
User: 12291
Date: 2020/10/30
Time: 10:26
To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<h1>记账管理</h1>

<p>
<form action="/getAllBills" method="post">
类型:
<select name="typeid">
<option value="-1">不限</option>
<c:forEach items="${types}" var="tp">
<option value="${tp.id}" ${tid==tp.id?'selected':''}>${tp.bname}</option>
</c:forEach>
</select>

时间:
从<input type="text" name="begin" value="${begintime}">到<input type="text" name="end" value="${endtime}">
<input type="submit" value="搜索">
</form>
<input type="button" value="记账">
</p>

<table border="1" width="500">
<tr>
<td>标题</td>
<td>记账时间</td>
<td>类别</td>
<td>金额</td>
<td>说明</td>
<td>操作</td>
</tr>

<c:if test="${info.list.size() > 0}">
<c:forEach items="${info.list}" var="bill">
<tr>
<td>${bill.title}</td>
<td><fmt:formatDate value="${bill.billtime}" pattern="yyyy-MM-dd"/></td>
<td>${bill.billtype.bname}</td>
<td>
<c:choose>
<c:when test="${bill.billtype.bname =='支出' || bill.billtype.bname =='借出' || bill.billtype.bname =='还出'}">
-${bill.price}
</c:when>
<c:when test="${bill.billtype.bname =='收入' || bill.billtype.bname =='借入' || bill.billtype.bname =='还入'}">
+${bill.price}
</c:when>
<c:otherwise>
${bill.price}
</c:otherwise>
</c:choose>
</td>
<td>${bill.explains}</td>
<td>删除 修改</td>
</tr>
</c:forEach>
</c:if>

<c:if test="${info.list.size() == 0}">
<tr>
<td colspan="6"> <h3>没有找到任何数据</h3> </td>
</tr>
</c:if>

<tr>
<td colspan="6">
<a href="/getAllBills?typeid=${tid}&begin=${begintime}&end=${endtime}">首页</a>

<a href="/getAllBills?index=${info.prePage==0?1:info.prePage}&typeid=${tid}&begin=${begintime}&end=${endtime}">上一页</a>

<a href="/getAllBills?index=${info.nextPage==0?info.pages:info.nextPage}&typeid=${tid}&begin=${begintime}&end=${endtime}">下一页</a>

<a href="/getAllBills?index=${info.pages}&typeid=${tid}&begin=${begintime}&end=${endtime}">尾页</a>

总页数:${info.pages}

总条数:${info.total}

</td>
</tr>

</table>
</body>
</html>

3.4 记账功能(新增数据)

前端页面:
注意,页面中的name要和后台Bill类的属性名一致

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
html复制代码<%--
Created by IntelliJ IDEA.
User: 12291
Date: 2020/10/30
Time: 16:52
To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<h1>记账</h1>
<form action="/insertBill" method="post">
<p>类型:
<c:forEach items="${types}" var="ty">
<input type="radio" value="${ty.id}" name="typeid">${ty.bname}
</c:forEach>
</p>
<p>标题:<input type="text" style="width: 500px" name="title"></p>
<p>日期:<input type="text" name="billtime">金额:<input type="text" name="price"></p>
<p>说明:<textarea cols="50" rows="4" name="explains"></textarea></p>
<input type="reset" value="重置">
<input type="submit" value="保存">
</form>
</body>
</html>

dao层的代码是自动生成的,所以已经有了insert方法,直接从service层开始写:
BillsService中添加:

1
java复制代码public int insert(Bills record);

实现类,注意要开启事务:

1
2
3
4
5
java复制代码@Override
@Transactional
public int insert(Bills record) {
return billsMapper.insert(record);
}

controller:

1
2
3
4
5
6
7
8
9
10
java复制代码@RequestMapping("/insertBill")
public String add(Bills bills){
int insert = billsService.insert(bills);
if (insert > 0){
// 回到主页面
return "redirect:/gettypes";
}
// 回到新增页面
return "redirect:/getBillType";
}

测试的时候需要注意,由于采用的是默认springMVC日期输入格式,所以需要的是2020/2/2这种以斜杠分割的方式。

3.5 更新账单

更新账单其实分了两个操作,一个是查询账单,一个是修改账单

3.5.1 查询账单

show.jsp中为修改操作添加a标签

1
html复制代码<a href="/findById?bid=${bill.id}">修改</a>

controller定义对应方法:

1
2
3
4
5
6
7
8
java复制代码@RequestMapping("/findById")
public String findById(int bid, ModelMap map){
Bills bills = billsService.selectByPrimaryKey(bid);
List<Billtype> types = typesService.getTypes();
map.addAttribute("bills", bills);
map.addAttribute("types", types);
return "update";
}

service层:
接口:

1
java复制代码Bills selectByPrimaryKey(Integer id);

实现类:

1
2
3
4
java复制代码@Override
public Bills selectByPrimaryKey(Integer id) {
return billsMapper.selectByPrimaryKey(id);
}

dao是自动生成的:

1
2
3
4
5
6
7
8
9
sql复制代码<sql id="Base_Column_List">
id, title, billtime, typeid, price, explains
</sql>
<select id="selectByPrimaryKey" parameterType="java.lang.Integer" resultMap="BaseResultMap">
select
<include refid="Base_Column_List" />
from bills
where id = #{id,jdbcType=INTEGER}
</select>

前端界面:
controller会返回一个update.jsp页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
html复制代码<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<h1>更新</h1>
<form action="/updateBill" method="post">
<input type="hidden" name="id" value="${bills.id}">
<p>类型:
<c:forEach items="${types}" var="ty">
<input type="radio" value="${ty.id}" ${ty.id==bills.typeid?"checked":""} name="typeid">${ty.bname}
</c:forEach>
</p>
<p>标题:<input type="text" style="width: 500px" name="title" value="${bills.title}"></p>
<p>日期:<input type="text" name="billtime" value="<fmt:formatDate value="${bills.billtime}" pattern="yyyy/MM/dd"/>">金额:<input type="text" name="price" value="${bills.price}"></p>
<p>说明:<textarea cols="50" rows="4" name="explains">${bills.explains}</textarea></p>
<input type="reset" value="重置">
<input type="submit" value="保存">
</form>
</body>
</html>

3.5.2 修改操作

controller:

1
2
3
4
5
6
7
8
java复制代码@RequestMapping("/updateBill")
public String updateBill(Bills bills){
int i = billsService.updateByPrimaryKey(bills);
if (i > 0){
return "redirect:/gettypes";
}
return "redirect:/findById?bid=" + bills.getId();
}

修改成功返回show页面,修改失败返回update页面。

service层:

1
2
3
4
java复制代码@Override
public int updateByPrimaryKey(Bills record) {
return billsMapper.updateByPrimaryKey(record);
}

dao也是自动生成的:

1
2
3
4
5
6
7
8
9
sql复制代码<update id="updateByPrimaryKey" parameterType="com.wangjiawei.bean.Bills">
update bills
set title = #{title,jdbcType=VARCHAR},
billtime = #{billtime,jdbcType=DATE},
typeid = #{typeid,jdbcType=INTEGER},
price = #{price,jdbcType=DOUBLE},
explains = #{explains,jdbcType=VARCHAR}
where id = #{id,jdbcType=INTEGER}
</update>

3.5.3 删除操作

show.jsp中为删除添加一个按钮

1
html复制代码<a href="/deleteById?bid=${bill.id}">删除</a>

controller层:
添加删除方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码@RequestMapping("/deleteById")
public void delete(int bid, HttpServletResponse response){
int i = billsService.deleteByPrimaryKey(bid);
response.setContentType("text/html;charset=utf-8");
try {
PrintWriter writer = response.getWriter();
if (i > 0){
writer.print("<script>alert('删除成功');location.href='/gettypes'</script>");
return;
}
writer.print("<script>alert('删除失败');location.href='/gettypes'</script>");
} catch (IOException e) {
e.printStackTrace();
}
}

service层:
接口

1
java复制代码public int deleteByPrimaryKey(Integer id);

实现类

1
2
3
4
5
java复制代码@Override
@Transactional
public int deleteByPrimaryKey(Integer id) {
return billsMapper.deleteByPrimaryKey(id);
}

关于源码:
  大家可以通过我的下载主业进行下载,或者公众号里由有我个人的联系方式
在这里插入图片描述

本文转载自: 掘金

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

解密电商系统-读写分离各种实现优缺点与解决方案

发表于 2021-11-22

数据库读写分离,之前也说过这次在回顾下吧:主从复制,proxy读写分离,jar包进行读写分离。三种方式体量不同,根据自己的需求找到最合适的,一起说下。

什么是读写分离(一)

我们一般应用访问数据库无非是读取数据、修改数据、插入数据、删除数据。
而我们对数据库一般分为: master(主库也是写库) slave(从库也为读库)
而读写分离的意思就是:所有的写(insert update delete)操作走主库、其他走从库。

  • ① 目的是什么?

我们一般应用对数据库而言都是“读多写少”,也就是说对数据库读取数据的压力比较大。读写分离的主要目的是降低主库的压力。 降低主库的读的压力。只是降低主库读的压力,并不是说不能用主库来查询(下单立刻查询订单状态)。

  • ② 前提条件:
  1. 读库 slave 需要跟写库 master 的数据一致。
  2. 写数据必须写到证据库。
  3. 读取数据必须到读库,这不一定,特殊的业务需求可能需要走主库,主从同步需要时间,可以强制路由走主库。
  • ③ 行业用的多的

主多从 mysql 集群方案,至少两个库。一主一从。

  • ④ 主从复制

Master my.cnf 配置:

1
2
3
4
5
bash复制代码binlog-do-db=tlshop
binlog-ignore-db=mysql
binlog_format=mixed
log-bin=mysql-bin
server-id=1

Slave my.cnf 配置

1
2
3
4
5
6
7
8
bash复制代码replicate-do-db=tlshop
replicate-ignore-db=mysql
server-id=2
#以下的配置是5.6 之前版本
#master-host=192.168.0.15
#master-port=3306
#master-user=root
#master-password=123456

slave 动态配置节点信息

1.进入master数据库,输入

1
bash复制代码 show master status\G

2.进入slave数据库,输入

1
2
3
4
bash复制代码change master to master_host='192.168.0.15', master_user='root', master_password='123456',
master_log_file='(输入1中show master status里面的 File)', master_log_pos=‘(输入1中show master status里面的 Position)’;

slave start;//启动

读写分离(二)

  • ① 业界方案

代理层 proxy: Atlas 开源软件
应用层: Sharding-jdbc

  • ② 代理层 Atlas

官网github.com/Qihoo360/At…

Atlas 是由 Qihoo 360 公司 Web 平台部基础架构团队开发维护的一个基于 MySQL 协议的数据中间层项目。它在 MySQL 官方推出的 MySQL-Proxy 0.8.2 版本的基础上,修改了大量bug,添加了很多功能特性。目前该项目在 360 公司内部得到了广泛应用,很多 MySQL 很多 MySQL 业务已经接入了 Atlas 平台,每天承载的读写请求数达几十亿条。同时,有超过 50 家公司在生产环境中部署了 Atlas,超过 800 人已加入了我们的开发者交流群,并且些数字还在不断增加。

主要功能

  1. 读写分离。
  2. 从库负载均衡。
  3. IP 过滤。
  4. 自动分表。
  5. DBA 可平滑上下线 DB。
  6. 自动摘除宕机的 DB。

Atlas 配置

强制路由:
注释的方式强制走主库。
Alatas:

1
bash复制代码/*master*/ select 字段 from 表名

业务需要 下单即查

Sharding-jdbc
Sharding-JDBC 是一个开源的分布式数据库中间件,它无需额外部署和依赖,完全兼容 JDBC和各种 ORM 框架。 Sharding-JDBC 作为面向开发的微服务云原生基础类库,完整的实现了分库分表、读写分离和分布式主键功能,并初步实现了柔性事务。之前有专门的文章『互联网架构』(65)
shardingsphere.apache.org/index_zh.ht…

1
2
3
4
5
6
7
8
9
10
11
12
13
bash复制代码<dependency>
<groupId>io.shardingjdbc</groupId>
<artifactId>sharding-jdbc-core-spring-namespace</artifactId>
<version>${sharding-jdbc.version}</version>
</dependency>
<dependency>
<groupId>io.shardingjdbc</groupId>
<artifactId>sharding-jdbc-core</artifactId>
<version>${sharding-jdbc.version}</version>
</dependency>

<master-slave:data-source id="dataSource" master-data-source-name="dataSourceMaster"
slave-data-source-names="dataSourceSlave,dataSourceSlave,dataSourceSlave" strategytype="ROUND_ROBIN" />

PS:Alatas:

  1. 程序不需要管主从配置的具体细节
  2. 实现原理是 proxy,所以性能上会下降
  3. 而且需要维护其高可用
  4. 减少了程序员技能要求
  5. 只支持 mysql

Sharding-jdbc:

  1. 主从配置在程序中,所以增加了程序员的技术要求
  2. 实现原理是 jdbc 增强,所以支持任何数据库类型 性能比上面那个强
  3. 而且不需要维护。
  4. Mysql、 Oracle、 sql server

本文转载自: 掘金

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

SpringloC容器的依赖注入源码解析(6)—— doCr

发表于 2021-11-22

进入到上面的applyMergedBeanDefinitionPostProcessors方法里:

1
2
3
4
5
6
7
8
9
10
java复制代码protected void applyMergedBeanDefinitionPostProcessors(RootBeanDefinition mbd, Class<?> beanType, String beanName) {
for (BeanPostProcessor bp : getBeanPostProcessors()) {
if (bp instanceof MergedBeanDefinitionPostProcessor) {
MergedBeanDefinitionPostProcessor bdp = (MergedBeanDefinitionPostProcessor) bp;
// 重点关注AutowiredAnnotationBeanPostProcessor,该类会把@Autowired等标记的
// 需要依赖注入的成员变量或者方法实例给记录下来,方便后续populateBean使用
bdp.postProcessMergedBeanDefinition(mbd, beanType, beanName);
}
}
}

其中实现了postProcessMergedBeanDefinition方法的一个实现类MergedBeanDefinitionPostProcessor 是要分析的重点
请添加图片描述
请添加图片描述
构造方法:

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码public AutowiredAnnotationBeanPostProcessor() {
this.autowiredAnnotationTypes.add(Autowired.class);
this.autowiredAnnotationTypes.add(Value.class);
try {
this.autowiredAnnotationTypes.add((Class<? extends Annotation>)
ClassUtils.forName("javax.inject.Inject", AutowiredAnnotationBeanPostProcessor.class.getClassLoader()));
logger.trace("JSR-330 'javax.inject.Inject' annotation found and supported for autowiring");
}
catch (ClassNotFoundException ex) {
// JSR-330 API not available - simply skip.
}
}

把Autowired和Value标签加入到类型为Set的autowiredAnnotationTypes里。

当BeanName是自定义的WelcomeController时,从applyMergedBeanDefinitionPostProcessors方法的postProcessMergedBeanDefinition处进入断点:

1
2
3
4
5
java复制代码@Override
public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class<?> beanType, String beanName) {
InjectionMetadata metadata = findAutowiringMetadata(beanName, beanType, null);
metadata.checkConfigMembers(beanDefinition);
}

findAutowiringMetadata是寻找被注解的元数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码private InjectionMetadata findAutowiringMetadata(String beanName, Class<?> clazz, @Nullable PropertyValues pvs) {
// Fall back to class name as cache key, for backwards compatibility with custom callers.
String cacheKey = (StringUtils.hasLength(beanName) ? beanName : clazz.getName());
// Quick check on the concurrent map first, with minimal locking.
// 从容器中查找是否有给定类的autowire相关注解元信息
InjectionMetadata metadata = this.injectionMetadataCache.get(cacheKey);
if (InjectionMetadata.needsRefresh(metadata, clazz)) {
synchronized (this.injectionMetadataCache) {
metadata = this.injectionMetadataCache.get(cacheKey);
if (InjectionMetadata.needsRefresh(metadata, clazz)) {
if (metadata != null) {
metadata.clear(pvs);
}
metadata = buildAutowiringMetadata(clazz);
// 将得到的给定类autowire相关注解源信息存储在容器缓存中
this.injectionMetadataCache.put(cacheKey, metadata);
}
}
}
return metadata;
}

该方法扫描到类里的属性或者方法有对应的注解,就会将其封装起来,最终封装成InjectionMetadata实例返回,进入到InjectionMetadata里:
请添加图片描述
其中包含了哪些需要注入的元素,以及元素要注入到哪个目标类中,targetClass代表目标bean的class对象,injectedElements保存的是需要被注入的元素,即被Autowired或Value注解的属性或方法,checkedElements里面只保存了由Spring默认处理的属性、方法。

  injectedElements集合的泛型是InjectedElement,保存单个InjectedElement的值
请添加图片描述
member用于保存被Autowired、Value注解标记的属性,isField用来决定member是Field还是方法,PropertyDescriptor可以对属性反射读写操作


回到findAutowiringMetadata方法,cacheKey是metadata在容器缓存中的名字,获取到了之后会先在容器缓存里查一下先前有没有获取到InjectionMetadata对象,之后会执行needsRefresh方法

1
2
3
java复制代码public static boolean needsRefresh(@Nullable InjectionMetadata metadata, Class<?> clazz) {
return (metadata == null || metadata.targetClass != clazz);
}

该方法判断是否需要重新去扫描获取bean,如果不需要就会直接从缓存里返回先前注册的metadata实例。

在 if 里用到了双重锁检查机制,最终会调用

1
java复制代码metadata = buildAutowiringMetadata(clazz);

创建出metadata来,进入到buildResourceMetadata:

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
java复制代码private InjectionMetadata buildAutowiringMetadata(final Class<?> clazz) {
if (!AnnotationUtils.isCandidateClass(clazz, this.autowiredAnnotationTypes)) {
return InjectionMetadata.EMPTY;
}

List<InjectionMetadata.InjectedElement> elements = new ArrayList<>();
Class<?> targetClass = clazz;

do {
final List<InjectionMetadata.InjectedElement> currElements = new ArrayList<>();
// 收集被@Autowired或者@Value标记的Field
// 利用反射机制获取给定类中所有的声明字段,获取字段上的注解信息
ReflectionUtils.doWithLocalFields(targetClass, field -> {
MergedAnnotation<?> ann = findAutowiredAnnotation(field);
if (ann != null) {
if (Modifier.isStatic(field.getModifiers())) {
if (logger.isInfoEnabled()) {
logger.info("Autowired annotation is not supported on static fields: " + field);
}
return;
}
boolean required = determineRequiredStatus(ann);
// 将当前字段元信息封装,添加在返回的集合中
currElements.add(new AutowiredFieldElement(field, required));
}
});

ReflectionUtils.doWithLocalMethods(targetClass, method -> {
Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(method);
if (!BridgeMethodResolver.isVisibilityBridgeMethodPair(method, bridgedMethod)) {
return;
}
MergedAnnotation<?> ann = findAutowiredAnnotation(bridgedMethod);
if (ann != null && method.equals(ClassUtils.getMostSpecificMethod(method, clazz))) {
if (Modifier.isStatic(method.getModifiers())) {
if (logger.isInfoEnabled()) {
logger.info("Autowired annotation is not supported on static methods: " + method);
}
return;
}
if (method.getParameterCount() == 0) {
if (logger.isInfoEnabled()) {
logger.info("Autowired annotation should only be used on methods with parameters: " +
method);
}
}
boolean required = determineRequiredStatus(ann);
PropertyDescriptor pd = BeanUtils.findPropertyForMethod(bridgedMethod, clazz);
currElements.add(new AutowiredMethodElement(method, required, pd));
}
});

elements.addAll(0, currElements);
targetClass = targetClass.getSuperclass();
}
while (targetClass != null && targetClass != Object.class);

return InjectionMetadata.forElements(elements, clazz);
}

首先看一下该类是否有资格成为被Autowired或Value标记的候选类,主要是看一下class对象是不是普通的class,即这两个注解在这个class里有没有表示别的意思,判断逻辑如下:

1
2
3
4
5
6
7
8
9
java复制代码public static boolean isCandidateClass(Class<?> clazz, String annotationName) {
if (annotationName.startsWith("java.")) {
return true;
}
if (AnnotationsScanner.hasPlainJavaAnnotationsOnly(clazz)) {
return false;
}
return true;
}

注解有java开头的注解就算是候选者;如果clazz是以java.打头或是Order类型的,则标签不会起作用,不能算做候选者


回到AutowiredAnnotationBeanPostProcessor.java的buildAutowiringMetadata方法
请添加图片描述
之后会通过循环来遍历class里面的属性元素以及方法元素,不管是何种元素都会执行findAutowiredAnnotation方法收集被Aotowired或Value标记的Field

进入到findAutowiredAnnotation方法里:

1
2
3
4
5
6
7
8
9
10
11
java复制代码@Nullable
private MergedAnnotation<?> findAutowiredAnnotation(AccessibleObject ao) {
MergedAnnotations annotations = MergedAnnotations.from(ao);
for (Class<? extends Annotation> type : this.autowiredAnnotationTypes) {
MergedAnnotation<?> annotation = annotations.get(type);
if (annotation.isPresent()) {
return annotation;
}
}
return null;
}

isPresent方法判断是否被两种标签修饰,是的话将标记信息返回


回到buildAutowiringMetadata,
请添加图片描述
之后判断Field是否是静态的,是的话会抛异常,因为静态Field不支持注入

之后再会获得Autowired里面的require属性

1
java复制代码boolean required = determineRequiredStatus(ann);

获取到相关信息之后就会将field和required属性添加到list里面

1
java复制代码currElements.add(new AutowiredFieldElement(field, required));

之后就会把收集到的被注解标记的属性以及方法实例,存储到集合中
请添加图片描述

最后返回:

1
java复制代码return InjectionMetadata.forElements(elements, clazz);

进入到forElements方法里:

1
2
3
java复制代码public static InjectionMetadata forElements(Collection<InjectedElement> elements, Class<?> clazz) {
return (elements.isEmpty() ? InjectionMetadata.EMPTY : new InjectionMetadata(clazz, elements));
}

该方法会将收集到的被标记的元素放入到新创建的InjectionMetadata实例里

请添加图片描述


回到findAutowiringMetadata,
请添加图片描述
最后将metadata存储到缓存里


回到postProcessMergedBeanDefinition方法

1
2
3
4
5
java复制代码@Override
public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class<?> beanType, String beanName) {
InjectionMetadata metadata = findAutowiringMetadata(beanName, beanType, null);
metadata.checkConfigMembers(beanDefinition);
}

在获取到了metadata之后,会调用checkConfigMembers方法,进入到方法里:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码public void checkConfigMembers(RootBeanDefinition beanDefinition) {
Set<InjectedElement> checkedElements = new LinkedHashSet<>(this.injectedElements.size());
for (InjectedElement element : this.injectedElements) {
Member member = element.getMember();
if (!beanDefinition.isExternallyManagedConfigMember(member)) {
beanDefinition.registerExternallyManagedConfigMember(member);
checkedElements.add(element);
if (logger.isTraceEnabled()) {
logger.trace("Registered injected element on class [" + this.targetClass.getName() + "]: " + element);
}
}
}
this.checkedElements = checkedElements;
}

会把需要spring容器用默认策略注入的injectedelements集合给保存到前面说的checkedElements里,这样就完成了收集被Autowired和Value标记的属性和方法,有了实例之后就能通过调用其inject方法在合适的时候进行属性值的注入


回到doCreateBean
请添加图片描述

本文转载自: 掘金

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

1…241242243…956

开发者博客

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