简

人生短暂,学海无边,而大道至简。


  • 首页

  • 归档

  • 分类

  • 标签

Elasticsearch必知(转)

发表于 2019-03-03 | 分类于 Elasticsearch

1、集群规模
Elasticsearch的优点在于它是非常容易扩展。但,索引和查询时间可能因许多因素而异。在集群规模层面一方面要考虑数据量,另一方面比较重要的衡量因素是项目/产品的指标要求。

要想达到吞吐量和CPU利用率的指标要求,建议进行一定量的测试,以确认集群承担的负载和性能瓶颈问题。

测试工具推荐:Apache Jmeter。

网上会有很多的一线互联网公司等的“他山之石”,但,方案仅供参考,需要自己结合业务场景、硬件资源进行反复测试验证。

2、节点职责
Elasticsearch节点可以是主节点(Master),数据节点(Data),客户端/路由节点(Client)或某种组合。 大多数人大规模集群选择专用主节点(至少3个),然后选择一些数据和客户端节点。

建议:职责分离,并您针对特定工作负载优化每种类型的节点的分配。

例如,通过分离客户端和数据节点提升性能。 客户端节点处理传入的HTTP请求,这使得数据节点为查询提供服务。

这并不是绝对的,有大量网友在社区反馈,分离客户端节点并没有提升性能,因实际场景而异,大规模数据增量的业务场景,职责分离必然是大势所趋。

3、安全
近期,未加任何安全防护措施的Elastic安全事件频发。建议在应用程序API和Elasticsearch层之间以及Elasticsearch层和内部网络之间保护您的Elasticsearch集群。

6.3+版本之后,xpack插件已经集成到Elastic产品线。(收费)

加一层Nginx代理,能防止未经授权的访问。

其他选型推荐:search-guard,readonlyRest等。

“裸奔的风险非常大”,进阶阅读:你的Elasticsearch在裸奔吗?

4、数据建模
4.1 使用别名
业务层面使用别名进行检索、聚合操作。

别名的好处:
1)将应用和索引名称隔离;
2)可以方便的实现跨索引检索。

4.2 数据类型选型
若不指定数据类型的动态映射机制,比如:字符串类型会默认存储为text和keyword两种类型,势必会增加存储成本。
建议:针对业务场景需求,静态的手动指定好每个字段的数据类型。

考虑因素包含但不限于:
1)是否需要索引;
2)是否需要存储;
3)是否需要分词;
4)是否需要聚合;
5)是否需要多表关联(nested类型、join或者是宽表存储);
6)是否需要快速响应(keyword和long类型选型)
……
此处的设计时间不能省。

进阶阅读:干货 | 论Elasticsearch数据建模的重要性

5、检索选型
Elasticsearch查询DSL非常庞大。如果业务场景不需要计算评分,推荐使用过滤器filter。因为基于缓存,更高效。
查询相关的API包含但不限于:

match/multi_match

match_phrase/match_phrase_prefix

term/terms

wildcard/regexp

query_string

选型前,建议通过Demo验证一下是否符合预期。

了解如何编写高效查询是一回事,但让它们返回最终用户期望的结果是另一回事。

业务实战中,建议花一些时间调整分析器、分词和评分,以便ES返回期望的正确的命中。

6、监控和警报
请务必考虑一个完全独立的“监视”集群机制,该机制仅用于捕获有关群集运行状况的统计信息,并在出现问题时提醒您。

监控作用:能通过可视化的方式,直观的看到内存、JVM、CPU、负载、磁盘等的使用情况,以对可能的突发情况及早做出应对方案。

警报作用:异常实时预警。

ES6.X xpack已经集成watcher工具。它会监视某些条件,并在满足这些条件时提醒您。

举例:当某些状态(例如JVM堆)达到阈值时,您可以采取一些操作(发送电子邮件,调用Web钩子等)。

如果你的业务场景是:几乎实时地将数据写入Elasticsearch并希望在数据与某些模式匹配时收到警报,则推荐使用ElastAlert。

https://github.com/Yelp/elastalert

7、节点配置和配置管理
一旦拥有多个节点,就每个节点在软件版本、配置等方面保持同步变得具有挑战性。

有许多开源工具可以帮助解决这个问题。推荐:Chef和Ansible帮助管理Elasticsearch集群。

Ansible可以自动执行升级和配置传播,而无需在任何Elasticsearch节点上安装任何其他软件。

当前可能看不到对自动化的巨大需求,如果要从小规模开始发展,并且希望能够快速发展的话,一个使用Ansible编写的常见任务库可以使你在几分钟内从裸服务器转到完全配置的Elasticsearch节点,无需人工干预。

增量索引的管理推荐:rollover + curator + crontab,6.6版本的新特性:Index Lifecycle Management(索引生命周期管理),推荐尝鲜使用。

8、备份和恢复
经常被问到的问题1“ES中误删除的数据(delete或者delete_by_query)能恢复吗?”
——答案:如果做了备份,是可以的。如果没有,不可以。

问题2:“迁移节点,直接data路径原封不动拷贝可以吗?”
——答案:不可以,不推荐。推荐使用reindex或其他工具实现。

对于高可用性的业务系统,数据的备份功能非常重要。 由于数据的存储可能会涉及多个节点,依赖OS级文件系统备份可能会很冒险。

推荐使用Elasticsearch内置的“快照”功能,可以备份您的索引。

9、API选型
Elastic官方支持API,包含:JAVA、Java Script、.net、PHP、python、Ruby。
Elastic民间API(社区贡献)非常庞大:C++、Go等20多种。

API选型推荐使用:官方API。

原因:
1)版本更新及时、
2)新特性支持适配更新及时。

http://t.cn/EMUzubT

http://t.cn/EMUzubH

DSL开发推荐使用的Kibana的Dev-tool,非常高效、方便。

10、数据接入
将数据索引到Elasticsearch很容易。 根据数据源和其他因素,您可以自己编写,也可以使用Elastic中的Logstash工具。

Logstash可以查看日志文件或其他输入,然后有效地将数据索引到集群中。

其他大数据组件或开源项目也有类似的功能,举例:

kafka-connector,flume,canal等。

选型中,不一棵树上吊死,综合对比性能和稳定性,找适合自己业务场景的最为重要。

小结
安装和运行开箱即用的Elasticsearch集群非常简单。 使其适用于你的实际业务场景并满足你的性能指标非常不容易。

希望这个列表能助力你的Elastic方案选型,为选型扫清障碍。

期待反馈交流心得!

参考:http://t.cn/EMUZw6N

Elasticsearch 7介绍(转)

发表于 2019-03-02 | 分类于 Elasticsearch
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
Elasticsearch 7.0 默认自带 JDK
默认节点名称为主机名。
默认分片数改为1,不再是5。
Elasticsearch 7.0 没有 Type 了,包括 API 层面的。type会在8.X版本彻底移除。
hits.total返回对象,而非仅结果值
Kibana 支持全局开启“黑暗”模式

查询相关性速度优化
Weak-AND算法在Term Query查询场景有3700%的性能提升。
间隔查询(Intervals queries)
某些搜索用例(例如,法律和专利搜索)引入了查找单词或短语彼此相距一定距离的记录的需要。

Elasticsearch 7.0中的间隔查询引入了一种构建此类查询的全新方式,与之前的方法(跨度查询span queries)相比,使用和定义更加简单。

与跨度查询相比,间隔查询对边缘情况的适应性更强。

引入新的集群协调子系统
移除 minimum_master_nodes 参数,让 Elasticsearch 自己选择可以形成仲裁的节点。

典型的主节点选举现在只需要很短的时间就可以完成。
集群的伸缩变得更安全、更容易,并且可能造成丢失数据的系统配置选项更少了。

节点更清楚地记录它们的状态,有助于诊断为什么它们不能加入集群或为什么无法选举出主节点。

2.4 升级 Elasticsearch 7,0 ,不再内存溢出
新的 Circuit Breaker 在JVM 堆栈层面监测内存使用,Elasticsearch 比之前更加健壮。

设置indices.breaker.fielddata.limit的默认值已从JVM堆大小的60%降低到40%。

2.5 时间戳纳秒级支持,提升数据精度
利用纳秒精度支持加强时间序列用例

到目前为止,Elasticsearch仅以毫秒精度存储时间戳。 7.0增加了几个零并带来了纳秒精度,这提高了高频数据采集用户存储和排序所需数据的精度。

3、Elasticsearch 7升级注意事项
3.0 升级前必知必会
查看新版本的重大更改特性,并对7.0.0的代码和配置进行必要的更改。

如果您使用自定义插件,请确保兼容版本可用。

在升级生产集群之前,在开发环境中测试升级。

备份您的数据! 您必须拥有数据快照才能回滚到早期版本。

3.1 升级API
Rolling upgrade ——滚动升级允许Elasticsearch集群一次升级一个节点,升级不会中断服务。

不支持在升级期间在同一群集中运行多个版本的Elasticsearch,因为无法将已升级的节点复制到运行旧版本的节点。

3.2 版本升级路线
小版本之间升级:举例:5.4.1升级到5.6

平滑升级——从5.6版本到6.7版本

平滑升级——从6.7版本到7.0.0版本

3.3 借助Reindex升级索引数据
Elasticsearch可以读取在先前主要版本中创建的索引。如果您在5.x或之前创建了索引,则必须在升级到7.0.0之前重新索引或删除它们。

如果存在不兼容的索引,Elasticsearch节点将无法启动。

3.4 ELK Stack要一起升级
升级到新版本的Elasticsearch时,需要升级Elastic Stack中的每个产品。

3.5 6.6或更早版本集群,需要先关闭
要从6.6或更早版本直接升级到7.0.0,必须关闭群集,安装7.0.0并重新启动。

3.6 切记,7.0+版本`无type`的索引结构。
这点,如果考虑未来更新版本,在6.X或者更早版本的项目中,就严格按照7.x规范走,这样升级会相对比较省事。

4、Elasticsearch 版本更新太快了,学不动了,肿么办?
在这里插入图片描述
一方面,我们感叹ES的更新速度,的确从2016年的2.X到2019年的7.0,版本更新速度超乎想象。

另一方面,实际业务开发中,还在使用1.X,2.X,5.X,甚至还没有用过6.X的朋友非常多,小伙伴不禁有了“学不动了”的感慨。

4.1 新版本的变
变是永恒的,尤其是基于开源软件加上上市公司的推动。

实际上,高版本较低版本,主要在性能上的提升和部分新功能点的实现。

新版本更高效。
比如:6.6+提出的ilm索引生命周期管理,你如果关注Elastic Meetup的话,印象ebay和阿里还有其他公司自己就实现过类似功能。

原有版本有类似的功能,只不过是非常、非常麻烦、繁琐,所以,才有了ilm的诞生。

新版本迎合了市场的需求。
比如:7.0的黑暗模式,实际在grafana或类似竞品BI中都有类似的功能,猜测Kibana升级一方面是用户需求,另一方面也是竞品分析的结果。

新版本性能极大提升。
比如:7.0的terms融合新算法,有37倍的提升。

4.2 新版本的不变
《暗时间》作者刘未鹏说过“底层的技术永远不过时”。

不必说倒排索引机制不会变,也不必说Lucene的改动也相对较小。单是:ES的基础功能全文检索、多种聚合等几乎不会有太大的变动。

4.3 还存在学不动吗?
夯实打牢基础基本功,理解ELK更新的变与不变。80-90%+的时间关注基础,10%左右的时间关注增量的变化即可。

ES介绍和安装配置

发表于 2019-03-01 | 分类于 Elasticsearch
介绍

Elasticsearch 是一个分布式可扩展的实时搜索和分析引擎,一个建立在全文搜索引擎 Apache Lucene,底层封装了Lucene,但是直接用 Lucene,必须自己写代码去调用它的接口。Elastic提供了 REST API 的操作接口,开箱即用,它不仅包括了全文搜索功能,还可以进行以下工作:

  • 分布式实时文件存储,并将每一个字段都编入索引,使其可以被搜索。
  • 实时分析的分布式搜索引擎。
  • 可以扩展到上百台服务器,处理PB级别的结构化或非结构化数据。
基本概念

Elasticsearch是面向文档型数据库,一条数据在这里就是一个文档,用JSON作为文档序列化的格式。

1
2
3
4
5
6
{
"id": 790490,
"userId": "924a42993d6f4eaf8f1e3485f7a69e28",
"accid": "9470b64def9749ea878d43855e0cbee6",
"mark": null
}

和关系型数据库类型对比:
关系数据库——>数据库——> 表——> 行——>列(Columns)
Elasticsearch——>索引(Index)——>类型(type)——>文档(Docments) ——>字段(Fields)

一个 Elasticsearch 集群可以包含多个索引(数据库),也就是说其中包含了很多类型(表)。这些类型中包含了很多的文档(行),然后每个文档中又包含了很多的字段(列)。Elasticsearch的交互,可以使用Java API,也可以直接使用HTTP的Restful API方式。

Node 与 Cluster

Elastic 本质上是一个分布式数据库,允许多台服务器协同工作,每台服务器可以运行多个 Elastic 实例。

单个 Elastic 实例称为一个节点(node)。一组节点构成一个集群(cluster)。

Index

Elastic 会索引所有字段,经过处理后写入一个反向索引(Inverted Index)。查找数据的时候,直接查找该索引。

所以,Elastic 数据管理的顶层单位就叫做 Index(索引)。它是单个数据库的同义词。每个 Index (即数据库)的名字必须是小写。

下面的命令可以查看当前节点的所有 Index。

1
$ curl -X GET 'http://localhost:9200/_cat/indices?v'
Document

Index 里面单条的记录称为 Document(文档)。许多条 Document 构成了一个 Index。

Document 使用 JSON 格式表示,下面是一个例子。

1
2
3
4
5
{
"user": "张三",
"title": "工程师",
"desc": "数据库管理"
}

同一个 Index 里面的 Document,不要求有相同的结构(scheme),但是最好保持相同,这样有利于提高搜索效率。

Type

Document 可以分组,比如weather这个 Index 里面,可以按城市分组(北京和上海),这种分组就叫做 Type,它是虚拟的逻辑分组,用来过滤 Document。

不同的 Type 应该有相似的结构(schema),举例来说,id字段不能在这个组是字符串,在另一个组是数值。这是与关系型数据库的表的一个区别。性质完全不同的数据(比如products和logs)应该存成两个 Index,而不是一个 Index 里面的两个 Type。

下面的命令可以列出每个 Index 所包含的 Type。

1
$ curl 'localhost:9200/_mapping?pretty=true'

根据规划,Elastic 6.x 版只允许每个 Index 包含一个 Type,7.x 版将会彻底移除 Type。

Elasticsearch安装配置

下载安装包,解压,直接启动,默认端口9200。
如果启动报错max virtual memory areas vm.maxmapcount [65530] is too low,配置一下linux环境:sysctl -w vm.max_map_count=262144

成功启动后,请求9200端口,Elastic 返回一个 JSON 对象,包含当前节点、集群、版本等信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ curl localhost:9200

{
"name" : "atntrTf",
"cluster_name" : "elasticsearch",
"cluster_uuid" : "tf9250XhQ6ee4h7YI11anA",
"version" : {
"number" : "5.5.1",
"build_hash" : "19c13d0",
"build_date" : "2017-07-18T20:44:24.823Z",
"build_snapshot" : false,
"lucene_version" : "6.6.0"
},
"tagline" : "You Know, for Search"
}

默认情况下,只允许本机访问,可以修改安装目录的config/elasticsearch.yml文件,去掉network.host的注释,将它的值改成0.0.0.0开启远程访问,然后重新启动 Elastic。

Windows下安装:
下载包:
https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-7.2.0-windows-x86_64.zip
解压后运行:elasticsearch.bat
浏览器查看:

完成安装!

安装Elasticsearch集群
解压安装包scp分发到不同的主机,对应修改每一个配置config/elasticsearch.yml:

1
2
3
4
5
6
7
8
Cluster.name: chenkl  #(同一集群要一样)
Node.name: node-1 #(同一集群要不一样)
Network.Host: 192.168.57.4 #这里不能写127.0.0.1要写真实IP
#防止脑裂的配置
discovery.zen.ping.multicast.enabled: false
discovery.zen.ping_timeout: 120s
client.transport.ping_timeout: 60s
discovery.zen.ping.unicast.hosts:["192.168.57.4","192.168.57.5", "192.168.57.6"] #这里是ES的节点IP

新建一个ES用户(所有的ES节点都要新建用户),并改密码  

1
2
3
4
5
#由于安全问题,ES是不能使用Root用户运行的
$ useradd esuser
$ passwd esuser
#将ES授权给esuser
$ chown -R esuser:esuser elasticsearch

分别启动每一台

1
2
3
4
#启动
$ cd /usr/local/elasticsearch-2.2.0
$ ./bin/elasticsearch
$ bin/elasticsearch -d #(后台运行)

安装插件

索引

Elasticsearch最关键的就是提供强大的索引能力了,Elasticsearch索引的精髓:一切设计都是为了提高搜索的性能 。
为了提高搜索的性能,难免会牺牲某些其他方面,比如插入更新。比如插入一个json对象字符串,这个对象有多个fields,在插入这些数据到Elasticsearch的同时,Elasticsearch还默认的为这些字段建立索引–倒排索引,因为Elasticsearch最核心功能是搜索。

Elasticsearch是如何做到快速索引的?
Elasticsearch使用的倒排索引比关系型数据库的B-Tree索引快,为什么呢?

什么是B-Tree索引?
二叉树查找效率是logN,同时插入新的节点不必移动全部节点,所以用树型结构存储索引,能同时兼顾插入和查询的性能。因此在这个基础上,再结合磁盘的读取特性(顺序读/随机读),传统关系型数据库采用了B-Tree/B+Tree这样的数据结构:

为了提高查询的效率,减少磁盘寻道次数,将多个值作为一个数组通过连续区间存放,一次寻道读取多个数据,同时也降低树的高度。

什么是倒排索引?

ID Name Age Sex
1 Kate 24 Female
2 John 24 Male
3 Bill 29 Male
如图所示的一个表数据,ID是Elasticsearch自建的文档id,那么Elasticsearch建立的索引如下:
Name:
Term Posting List
Kate 1
John 2
Bill 3
Age:
Term Posting List
24 [1,2]
29 3
Sex:
Term Posting List
Female 1
Male [2,3]

Posting List
Elasticsearch分别为每个field都建立了一个倒排索引,Kate, John, 24, Female这些叫term,而[1,2]就是Posting List。Posting list就是一个int的数组,存储了所有符合某个term的文档id。

通过posting list这种索引方式似乎可以很快进行查找,比如要找age=24的同学,爱回答问题的小明马上就举手回答:我知道,id是1,2的同学。但是,如果这里有上千万的记录呢?如果是想通过name来查找呢?

Term Dictionary
Elasticsearch为了能快速找到某个term,将所有的term排个序,二分法查找term,logN的查找效率,就像通过字典查找一样,这就是Term Dictionary。现在再看起来,似乎和传统数据库通过B-Tree的方式类似啊,为什么说比B-Tree的查询快呢?

Term Index
B-Tree通过减少磁盘寻道次数来提高查询性能,Elasticsearch也是采用同样的思路,直接通过内存查找term,不读磁盘,但是如果term太多,term dictionary也会很大,放内存不现实,于是有了Term Index,就像字典里的索引页一样,A开头的有哪些term,分别在哪页,可以理解term index是一颗树:

这棵树不会包含所有的term,它包含的是term的一些前缀。通过term index可以快速地定位到term dictionary的某个offset,然后从这个位置再往后顺序查找。

所以term index不需要存下所有的term,而仅仅是他们的一些前缀与Term Dictionary的block之间的映射关系,再结合FST(Finite State Transducers)的压缩技术,可以使term index缓存到内存中。从term index查到对应的term dictionary的block位置之后,再去磁盘上找term,大大减少了磁盘随机读的次数。

假设我们现在要将mop, moth, pop, star, stop and top(term index里的term前缀)映射到序号:0,1,2,3,4,5(term dictionary的block位置)。最简单的做法就是定义个Map<string, integer=””>,大家找到自己的位置对应入座就好了,但从内存占用少的角度想想,有没有更优的办法呢?答案就是:FST
FST以字节的方式存储所有的term,这种压缩方式可以有效的缩减存储空间,使得term index足以放进内存,但这种方式也会导致查找时需要更多的CPU资源。


压缩技巧
Elasticsearch里除了上面说到用FST压缩term index外,对posting list也有压缩技巧。

嗯,我们再看回最开始的例子,如果Elasticsearch需要对同学的性别进行索引(这时传统关系型数据库已经哭晕在厕所……),会怎样?如果有上千万个同学,而世界上只有男/女这样两个性别,每个posting list都会有至少百万个文档id。 Elasticsearch是如何有效的对这些文档id压缩的呢?

Frame Of Reference
增量编码压缩,将大数变小数,按字节存储
首先,Elasticsearch要求posting list是有序的(为了提高搜索的性能,再任性的要求也得满足),这样做的一个好处是方便压缩,看下面这个图例:

如果数学不是体育老师教的话,还是比较容易看出来这种压缩技巧的。

原理就是通过增量,将原来的大数变成小数仅存储增量值,再精打细算按bit排好队,最后通过字节存储,而不是大大咧咧的尽管是2也是用int(4个字节)来存储。

Roaring bitmaps
说到Roaring bitmaps,就必须先从bitmap说起。Bitmap是一种数据结构,假设有某个posting list:

[1,3,4,7,10]

对应的bitmap就是:

[1,0,1,1,0,0,1,0,0,1]

非常直观,用0/1表示某个值是否存在,比如10这个值就对应第10位,对应的bit值是1,这样用一个字节就可以代表8个文档id,旧版本(5.0之前)的Lucene就是用这样的方式来压缩的,但这样的压缩方式仍然不够高效,如果有1亿个文档,那么需要12.5MB的存储空间,这仅仅是对应一个索引字段(我们往往会有很多个索引字段)。于是有人想出了Roaring bitmaps这样更高效的数据结构。

Bitmap的缺点是存储空间随着文档个数线性增长,Roaring bitmaps需要打破这个魔咒就一定要用到某些指数特性:

将posting list按照65535为界限分块,比如第一块所包含的文档id范围在065535之间,第二块的id范围是65536131071,以此类推。再用<商,余数>的组合表示每一组id,这样每组里的id范围都在0~65535内了,剩下的就好办了,既然每组id不会变得无限大,那么我们就可以通过最有效的方式对这里的id存储。

“为什么是以65535为界限?”

程序员的世界里除了1024外,65535也是一个经典值,因为它=2^16-1,正好是用2个字节能表示的最大数,一个short的存储单位,注意到上图里的最后一行“If a block has more than 4096 values, encode as a bit set, and otherwise as a simple array using 2 bytes per value”,如果是大块,用节省点用bitset存,小块就豪爽点,2个字节我也不计较了,用一个short[]存着方便。

那为什么用4096来区分大块还是小块呢?

个人理解:都说程序员的世界是二进制的,4096*2bytes = 8192bytes < 1KB, 磁盘一次寻道可以顺序把一个小块的内容都读出来,再大一位就超过1KB了,需要两次读。


联合索引
如果多个field索引的联合查询,倒排索引如何满足快速查询的要求呢?
利用跳表(Skip list)的数据结构快速做“与”运算,或者利用上面提到的bitset按位“与” 先看看跳表的数据结构:

将一个有序链表level0,挑出其中几个元素到level1及level2,每个level越往上,选出来的指针元素越少,查找时依次从高level往低查找,比如55,先找到level2的31,再找到level1的47,最后找到55,一共3次查找,查找效率和2叉树的效率相当,但也是用了一定的空间冗余来换取的。

假设有下面三个posting list需要联合索引:

如果使用跳表,对最短的posting list中的每个id,逐个在另外两个posting list中查找看是否存在,最后得到交集的结果。

如果使用bitset,就很直观了,直接按位与,得到的结果就是最后的交集。

Elasticsearch的索引思路

将磁盘里的东西尽量搬进内存,减少磁盘随机读取次数(同时也利用磁盘顺序读特性),结合各种奇技淫巧的压缩算法,用及其苛刻的态度使用内存。


使用Elasticsearch进行索引时需要注意:

不需要索引的字段,一定要明确定义出来,因为默认是自动建索引的
同样的道理,对于String类型的字段,不需要analysis的也需要明确定义出来,因为默认也是会analysis的
选择有规律的ID很重要,随机性太大的ID(比如java的UUID)不利于查询

关于最后一点,个人认为有多个因素:

其中一个(也许不是最重要的)因素: 上面看到的压缩算法,都是对Posting list里的大量ID进行压缩的,那如果ID是顺序的,或者是有公共前缀等具有一定规律性的ID,压缩比会比较高;

另外一个因素: 可能是最影响查询性能的,应该是最后通过Posting list里的ID到磁盘中查找Document信息的那步,因为Elasticsearch是分Segment存储的,根据ID这个大范围的Term定位到Segment的效率直接影响了最后查询的性能,如果ID是有规律的,可以快速跳过不包含该ID的Segment,从而减少不必要的磁盘读次数,具体可以参考这篇如何选择一个高效的全局ID方案(评论也很精彩)

新建和删除 Index

新建 Index:$ curl -X PUT ‘localhost:9200/weather’

服务器返回一个 JSON 对象,里面的acknowledged字段表示操作成功。

1
2
3
4
{
"acknowledged":true,
"shards_acknowledged":true
}

删除Index:$ curl -X DELETE ‘localhost:9200/weather’

中文分词设置

首先,安装中文分词插件,如ik或smartcn。

$ ./bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v5.5.1/elasticsearch-analysis-ik-5.5.1.zip

安装以后,新建一个 Index,指定需要分词的字段。这一步根据数据结构而异,下面的命令只针对本文。基本上,凡是需要搜索的中文字段,都要单独设置一下。

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
$ curl -X PUT 'localhost:9200/accounts' -d '
{
"mappings": {
"person": {
"properties": {
"user": {
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "ik_max_word"
},
"title": {
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "ik_max_word"
},
"desc": {
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "ik_max_word"
}
}
}
}
}'
上面代码中,首先新建一个名称为accounts的 Index,里面有一个名称为person的 Type。person有三个字段。

user、title、desc这三个字段都是中文,而且类型都是文本(text),所以需要指定中文分词器,不能使用默认的英文分词器。

Elastic 的分词器称为 analyzer。我们对每个字段指定分词器。

1
2
3
4
5
"user": {
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "ik_max_word"
}

上面代码中,analyzer是字段文本的分词器,search_analyzer是搜索词的分词器。ik_max_word分词器是插件ik提供的,可以对文本进行最大数量的分词。

数据操作

新增记录
向指定的 /Index/Type 发送 PUT 请求,就可以在 Index 里面新增一条记录。比如,向/accounts/person发送请求,就可以新增一条人员记录。

$ curl -X PUT ‘localhost:9200/accounts/person/1’ -d ‘
{
“user”: “张三”,
“title”: “工程师”,
“desc”: “数据库管理”
}’

服务器返回的 JSON 对象,会给出 Index、Type、Id、Version 等信息。

{
“_index”:”accounts”,
“_type”:”person”,
“_id”:”1”,
“_version”:1,
“result”:”created”,
“_shards”:{“total”:2,”successful”:1,”failed”:0},
“created”:true
}

新增记录的时候,也可以不指定 Id,这时要改成 POST 请求。

$ curl -X POST ‘localhost:9200/accounts/person’ -d ‘
{
“user”: “李四”,
“title”: “工程师”,
“desc”: “系统管理”
}’

上面代码中,向/accounts/person发出一个 POST 请求,添加一个记录。这时,服务器返回的 JSON 对象里面,_id字段就是一个随机字符串。

{
“_index”:”accounts”,
“_type”:”person”,
“_id”:”AV3qGfrC6jMbsbXb6k1p”,
“_version”:1,
“result”:”created”,
“_shards”:{“total”:2,”successful”:1,”failed”:0},
“created”:true
}

注意,如果没有先创建 Index(这个例子是accounts),直接执行上面的命令,Elastic 也不会报错,而是直接生成指定的 Index。所以,打字的时候要小心,不要写错 Index 的名称。

** 查看记录**
向/Index/Type/Id发出 GET 请求,就可以查看这条记录。

$ curl ‘localhost:9200/accounts/person/1?pretty=true’

上面代码请求查看/accounts/person/1这条记录,URL 的参数pretty=true表示以易读的格式返回。

返回的数据中,found字段表示查询成功,_source字段返回原始记录。

{
“_index” : “accounts”,
“_type” : “person”,
“_id” : “1”,
“_version” : 1,
“found” : true,
“_source” : {
“user” : “张三”,
“title” : “工程师”,
“desc” : “数据库管理”
}
}

如果 Id 不正确,就查不到数据,found字段就是false。

$ curl ‘localhost:9200/weather/beijing/abc?pretty=true’

{
“_index” : “accounts”,
“_type” : “person”,
“_id” : “abc”,
“found” : false
}

删除记录
删除记录就是发出 DELETE 请求。

$ curl -X DELETE ‘localhost:9200/accounts/person/1’

更新记录
更新记录就是使用 PUT 请求,重新发送一次数据。

$ curl -X PUT ‘localhost:9200/accounts/person/1’ -d ‘
{
“user” : “张三”,
“title” : “工程师”,
“desc” : “数据库管理,软件开发”
}’

{
“_index”:”accounts”,
“_type”:”person”,
“_id”:”1”,
“_version”:2,
“result”:”updated”,
“_shards”:{“total”:2,”successful”:1,”failed”:0},
“created”:false
}

上面代码中,我们将原始数据从”数据库管理”改成”数据库管理,软件开发”。 返回结果里面,有几个字段发生了变化。

“_version” : 2,
“result” : “updated”,
“created” : false
可以看到,记录的 Id 没变,但是版本(version)从1变成2,操作类型(result)从created变成updated,created字段变成false,因为这次不是新建记录。

数据查询
返回所有记录

使用 GET 方法,直接请求/Index/Type/_search,就会返回所有记录。

$ curl ‘localhost:9200/accounts/person/_search’

{
“took”:2,
“timed_out”:false,
“_shards”:{“total”:5,”successful”:5,”failed”:0},
“hits”:{
“total”:2,
“max_score”:1.0,
“hits”:[
{
“_index”:”accounts”,
“_type”:”person”,
“_id”:”AV3qGfrC6jMbsbXb6k1p”,
“_score”:1.0,
“_source”: {
“user”: “李四”,
“title”: “工程师”,
“desc”: “系统管理”
}
},
{
“_index”:”accounts”,
“_type”:”person”,
“_id”:”1”,
“_score”:1.0,
“_source”: {
“user” : “张三”,
“title” : “工程师”,
“desc” : “数据库管理,软件开发”
}
}
]
}
}

上面代码中,返回结果的 took字段表示该操作的耗时(单位为毫秒),timed_out字段表示是否超时,hits字段表示命中的记录,里面子字段的含义如下。

total:返回记录数,本例是2条。
max_score:最高的匹配程度,本例是1.0。
hits:返回的记录组成的数组。
返回的记录中,每条记录都有一个_score字段,表示匹配的程序,默认是按照这个字段降序排列。

全文搜索

Elastic 的查询非常特别,使用自己的查询语法,要求 GET 请求带有数据体。

$ curl ‘localhost:9200/accounts/person/_search’ -d ‘
{
“query” : { “match” : { “desc” : “软件” }}
}’
上面代码使用 Match 查询,指定的匹配条件是desc字段里面包含”软件”这个词。返回结果如下。

{
“took”:3,
“timed_out”:false,
“_shards”:{“total”:5,”successful”:5,”failed”:0},
“hits”:{
“total”:1,
“max_score”:0.28582606,
“hits”:[
{
“_index”:”accounts”,
“_type”:”person”,
“_id”:”1”,
“_score”:0.28582606,
“_source”: {
“user” : “张三”,
“title” : “工程师”,
“desc” : “数据库管理,软件开发”
}
}
]
}
}

Elastic 默认一次返回10条结果,可以通过size字段改变这个设置。

$ curl ‘localhost:9200/accounts/person/_search’ -d ‘
{
“query” : { “match” : { “desc” : “管理” }},
“size”: 1
}’
上面代码指定,每次只返回一条结果。

还可以通过from字段,指定位移。

$ curl ‘localhost:9200/accounts/person/_search’ -d ‘
{
“query” : { “match” : { “desc” : “管理” }},
“from”: 1,
“size”: 1
}’

上面代码指定,从位置1开始(默认是从位置0开始),只返回一条结果。

逻辑运算

如果有多个搜索关键字, Elastic 认为它们是or关系。

$ curl ‘localhost:9200/accounts/person/_search’ -d ‘
{
“query” : { “match” : { “desc” : “软件 系统” }}
}’

上面代码搜索的是软件 or 系统。

如果要执行多个关键词的and搜索,必须使用布尔查询。

$ curl ‘localhost:9200/accounts/person/_search’ -d ‘
{
“query”: {
“bool”: {
“must”: [
{ “match”: { “desc”: “软件” } },
{ “match”: { “desc”: “系统” } }
]
}
}
}’

shiro与springboot整合

发表于 2019-02-25 | 分类于 shiro

shiro主要有三大功能模块

1. Subject:主体,一般指用户。
2. SecurityManager:安全管理器,管理所有Subject,可以配合内部安全组件。(类似于SpringMVC中的DispatcherServlet)
3. Realms:用于进行权限信息的验证,一般需要自己实现。

具体功能

1. Authentication:身份认证/登录(账号密码验证)。
2. Authorization:授权,即角色或者权限验证。
3. Session Manager:会话管理,用户登录后的session相关管理。
4. Cryptography:加密,密码加密等。
5. Web Support:Web支持,集成Web环境。
6. Caching:缓存,用户信息、角色、权限等缓存到如redis等缓存中。
7. Concurrency:多线程并发验证,在一个线程中开启另一个线程,可以把权限自动传播过去。
8. Testing:测试支持;
9. Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问。
10. Remember Me:记住我,登录后,下次再来的话不用登录了。

整合shiro

创建数据库表如下

20200513145018

20200513145212

pom

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
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.hu</groupId>
<artifactId>spring-boot-shiro</artifactId>
<version>1.0-SNAPSHOT</version>

<packaging>jar</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.3.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.1.3</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.10</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.13</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.48</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

控制器

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
@Controller
@RequestMapping("/user")
public class UsersController {

@Autowired
private UserService userService;

@RequestMapping("login")
public ModelAndView login(User user, ServletRequest request) {
ModelAndView view = new ModelAndView();
//得到 Subject,并且通过用户名/密码创建 身份验证 Token(即用户身份/凭证)
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(user.getUsername(), user.getPassword());
if (!subject.isAuthenticated()) {
subject.login(token);
}
//获取上一次请求路径
SavedRequest savedRequest = WebUtils.getSavedRequest(request);
String url = "";
if (savedRequest != null) {
url = savedRequest.getRequestUrl();
} else {
url = "list";
}
view.setViewName("redirect:" + url);
return view;
}

@RequestMapping("list")
public ModelAndView list() {
ModelAndView view = new ModelAndView();
view.addObject("users", this.userService.getUsers());
view.setViewName("/page/list.html");
return view;
}

@RequestMapping("register")
public ModelAndView register(User user) throws Exception{
ModelAndView view = new ModelAndView();
if (!StringUtils.isEmpty(user.getUsername()) && !StringUtils.isEmpty(user.getPassword())) {
userService.createUser(user);
}
view.setViewName("redirect:/login.html");
return view;
}

@RequestMapping("logout")
@ResponseBody
public String logout(User user) {
Subject subject = SecurityUtils.getSubject();

/*Session session = subject.getSession(false);
if(session!=null){
session.removeAttribute(WebUtils.SAVED_REQUEST_KEY);
}*/
subject.logout();

return "已注销";
}

// 权限控制
@RequiresPermissions("sys:user:add")
@RequestMapping("add")
public ModelAndView add(User user) throws Exception{
ModelAndView view = new ModelAndView();
if (!StringUtils.isEmpty(user.getUsername()) && !StringUtils.isEmpty(user.getPassword())) {
userService.createUser(user);
view.setViewName("redirect:list");
} else {
view.addObject("action","add");
view.setViewName("/page/add.html");
}
return view;
}

//代表用户角色为admin才可以访问 相当于 subject.checkRole("admin")
@RequiresRoles("admin")
@RequestMapping("/update")
public ModelAndView update(User user) {
ModelAndView view = new ModelAndView();
if (!StringUtils.isEmpty(user.getUsername()) && !StringUtils.isEmpty(user.getPassword())) {
userService.changePassword(user.getUsername(), user.getPassword());
view.setViewName("redirect:list");
} else {
view.addObject("action","update");
view.addObject("user", this.userService.getUser(user.getId()));
view.setViewName("/page/add.html");
}
return view;
}

//代表用户有新增的权限才可以访问
// 相当于 subject.checkPermitted("system:user:delete")
@RequiresPermissions("sys:user:delete")
@RequestMapping("delete")
public ModelAndView delete(User user) throws Exception {
if (user.getId() != null) {
this.userService.deleteUser(user.getId());
}
ModelAndView view = new ModelAndView();
view.setViewName("redirect:list");
return view;
}
}

业务类

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
package com.hu.service.impl;

import com.hu.dao.PermissionDao;
import com.hu.dao.UserDao;
import com.hu.dao.UserRoleDao;
import com.hu.entity.User;
import com.hu.entity.UserRole;
import com.hu.service.UserService;
import com.hu.util.PasswordHelper;
import com.hu.util.Util;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;

@Service
public class UserServiceImpl implements UserService {

@Autowired
private UserDao userDao;
@Autowired
private UserRoleDao userRoleDao;
@Autowired
private PermissionDao permissionDao;

@Override
@Transactional
public User createUser(User user) throws Exception {
if (this.userDao.findUserByUsername(user.getUsername()) != null) {
throw new Exception("用户名已存在");
}
user.setUserId(Util.getUUID());
PasswordHelper.encryptPassword(user);
user.setStatus(0);
return userDao.save(user);
}

@Override
public int deleteUser(Integer id) throws Exception {
return this.userDao.deleteUser(id);
}

@Override
public User getUser(Integer id) {
return this.userDao.getOne(id);
}

@Override
public int changePassword(String userId, String newPassword) {
User user = this.userDao.findUserByUsername(userId);
user.setPassword(newPassword);
PasswordHelper.encryptPassword(user);
return userDao.save(user) == null ? -1 : 1;
}

@Override
public void addUserRoles(String userId, String... roleIds) {
List<UserRole> roles = new ArrayList<>();
for (String roleId : roleIds) {
roles.add(new UserRole(userId, roleId));
}
this.userRoleDao.saveAll(roles);
}

@Override
public void delUserRoles(String userId, String... roleIds) {
for (String roleId : roleIds) {
this.userRoleDao.delRole(userId, roleId);
}
}

@Override
public List<User> getUsers() {
return this.userDao.findAll();
}

@Override
public User findByUsername(String username) {
return this.userDao.findUserByUsername(username);
}

@Override
public Set<String> findRoles(String username) {
return this.userDao.findRoles(username);
}

@Override
public Set<String> findPermissions(String username) {
return this.userDao.findPermissions(username);
}
}

主html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN" >
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>用户信息</title>
<link href="../css/bootstrap.min.css" rel="stylesheet">
<script src="../js/jquery-2.1.0.min.js"></script>
<script src="../js/bootstrap.min.js"></script>
<style type="text/css">
td, th {
text-align: center;
}
</style>
</head>
<body>
<div class="container">
<h3 style="text-align: center">用户列表</h3>
<table border="1" class="table table-bordered table-hover">
<tr>
<td colspan="2" align="center"><a class="btn btn-primary" href="add.html" th:href="@{/user/add}">添加用户</a></td>
<td colspan="2" align="center"><a class="btn btn-primary" href="logout.html" th:href="@{/user/logout}">退出</a></td>
</tr>
<tr class="success">
<th>ID</th>
<th>用户名</th>
<th>操作</th>
</tr>
<tr th:each="user:${users}">
<td th:text="${user.userId}"></td>
<td th:text="${user.username}"></td>
<td><a class="btn btn-default btn-sm" href="update.html" th:href="@{/user/update(id=${user.id})}">修改</a>&nbsp;
<a class="btn btn-default btn-sm" href="delete.html" th:href="@{/user/delete(id=${user.id})}">删除</a></td>
</tr>
</table>
</div>
</body>
</html>

密码规则

1
2
3
4
5
6
7
8
9
10
11
12
13
public class PasswordHelper {
private static RandomNumberGenerator randomNumberGenerator = new SecureRandomNumberGenerator();
//这些要与Realm中一致
private static String algorithmName = "md5";
private final static int hashIterations = 1;

static public void encryptPassword(User user) {
//加盐
user.setSalt(randomNumberGenerator.nextBytes().toHex());
String newPassword = new SimpleHash(algorithmName, user.getPassword(), ByteSource.Util.bytes(user.getSalt()),hashIterations).toHex();
user.setPassword(newPassword);
}
}

Realm

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
@Component
public class UserRealm extends AuthorizingRealm implements Serializable {

@Autowired
private UserService userService;

/**
* 授权
* @param principals
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
String username = (String) principals.getPrimaryPrincipal();
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
return authorizationInfo;
}

/**
* 认证
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(
AuthenticationToken token) throws AuthenticationException {
String username = (String) token.getPrincipal();
User user = userService.findByUsername(username);
if(user == null){
throw new UnknownAccountException(); //没找到账号
}

if(user.getStatus()==1){
throw new LockedAccountException(); //账号被锁定
}

SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
user.getUsername(),
user.getPassword(),
ByteSource.Util.bytes(user.getSalt()),
getName());
return authenticationInfo;
}


}

ShiroConfig

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

@Bean
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
//如果没有登录,返回的json
/*shiroFilterFactoryBean.getFilters().put("authc", new UserFilter() {
@Override
protected void redirectToLogin(ServletRequest request, ServletResponse response) throws IOException {
HttpServletResponse resp = (HttpServletResponse) response;
HttpResponseUtils.renderJson(resp, com.hu.util.R.UNLOGIN());
}
});*/

//拦截器.
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
filterChainDefinitionMap.put("/500*", "anon");
filterChainDefinitionMap.put("/reg.html", "anon");
filterChainDefinitionMap.put("/user/login*", "anon");
filterChainDefinitionMap.put("/user/register*", "anon");
filterChainDefinitionMap.put("/user/**", "authc");
filterChainDefinitionMap.put("/page/**", "authc");
filterChainDefinitionMap.put("/**", "authc");
//不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
shiroFilterFactoryBean.setLoginUrl("/login.html");
//未授权跳转
shiroFilterFactoryBean.setUnauthorizedUrl("/page/fail.html");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}

//凭证匹配器 密码校验交给Shiro的SimpleAuthenticationInfo进行处理
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName("md5");//使用MD5算法;
//hashedCredentialsMatcher.setHashIterations(2);//散列的次数
return hashedCredentialsMatcher;
}


//自定义Realm
@Bean
public UserRealm myShiroRealm() {
UserRealm myShiroRealm = new UserRealm();
myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
return myShiroRealm;
}

//获得SecurityManager 实例
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
//指定 securityManager 的 realms 实现,可以设置多个
//securityManager 会按照 realms 指定的顺序进行身份认证。此处我们使用显示指定顺序的方式指定了 Realm 的顺序,
// 如果不设置,那么 securityManager 会按照 realm 声明的顺序进行使用(即无需设置 realms 属性,其会自动发现 )。
securityManager.setRealm(myShiroRealm());
return securityManager;
}

@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor(){
return new LifecycleBeanPostProcessor();
}
}

测试,必须要登录才可以使用功能。

权限控制

权限表增加权限,在角色表添加角色,并且选了用户和角色的对应关系。

20200513145910

20200513145925

20200513145957

授权

1
2
3
4
5
6
7
8
9
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
String username = (String) principals.getPrimaryPrincipal();
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
//在数据库中查询用户拥有的角色/权限
authorizationInfo.setRoles(userService.findRoles(username));
authorizationInfo.setStringPermissions(userService.findPermissions(username));
return authorizationInfo;
}

在配置类中加入AOP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
 // 授权使用注解  需要开启Spring AOP
// 自动代理所有的advisor
// DefaultAdvisorAutoProxyCreator创建代理更加通用强大
// a.指定一个DefaultAdvisorAutoProxyCreator Bean的定义.
// b.会自动代理Advisor的类
@Bean
@DependsOn({"lifecycleBeanPostProcessor"})
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator proxyCreator = new DefaultAdvisorAutoProxyCreator();
proxyCreator.setProxyTargetClass(true);
return proxyCreator;
}

// 属于StaticMethodMatcherPointcutAdvisor
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(){
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager());
return advisor;
}

增加处理类:

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
@Configuration
public class MyException implements HandlerExceptionResolver {

@Override
public ModelAndView resolveException(HttpServletRequest request,
HttpServletResponse response, Object handler, Exception ex) {

ModelAndView modelAndView = new ModelAndView();
Map<String, Object> attributes = new HashMap<>();
if (ex instanceof UnauthorizedException) {
attributes.put("msg", "用户无权限");
} else if (ex instanceof UnknownAccountException) {
attributes.put("msg", "用户名密码有误");
} else if (ex instanceof IncorrectCredentialsException) {
attributes.put("msg", "用户名密码有误");
} else if (ex instanceof LockedAccountException) {
attributes.put("msg", "账号已被锁定");
} else {
attributes.put("msg", ex.getMessage());
}
modelAndView.addAllObjects(attributes);
modelAndView.setViewName("/500");
return modelAndView;
}
}

在控制器中加入相应的注解,测试、没有权限的不能访问相关功能。

会话管理

会话管理器管理着应用中所有 Subject 的会话的创建、维护、删除、失效、验证等工作。是 Shiro 的核心组件,顶层组件 SecurityManager 直接继承了 SessionManager,且提供了SessionsSecurityManager 实现直接把会话管理委托给相应的 SessionManager,DefaultSecurityManager 及 DefaultWebSecurityManager 默认 SecurityManager 都继承了 SessionsSecurityManager。

Shiro 提供了三个默认实现:

DefaultSessionManager:DefaultSecurityManager 使用的默认实现,用于 JavaSE 环境;
ServletContainerSessionManager:DefaultWebSecurityManager 使用的默认实现,用于 Web 环境,其直接使用 Servlet 容器的会话;
DefaultWebSessionManager:用于 Web 环境的实现,可以替代 ServletContainerSessionManager,自己维护着会话,直接废弃了 Servlet 容器的会话管理。

可以设置会话的全局过期时间(毫秒为单位),默认30分钟:sessionManager. globalSessionTimeout=1800000

会话监听器

会话监听器用于监听会话创建、过期及停止事件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class MySessionListener1 implements SessionListener {  
@Override
public void onStart(Session session) {//会话创建时触发
System.out.println("会话创建:" + session.getId());
}
@Override
public void onExpiration(Session session) {//会话过期时触发
System.out.println("会话过期:" + session.getId());
}
@Override
public void onStop(Session session) {//退出/会话过期时触发
System.out.println("会话停止:" + session.getId());
}
}

如果只想监听某一个事件,可以继承SessionListenerAdapter实现:

1
2
3
4
5
6
public class MySessionListener2 extends SessionListenerAdapter {  
@Override
public void onStart(Session session) {
System.out.println("会话创建:" + session.getId());
}
}

会话存储 / 持久化

Shiro 提供 SessionDAO 用于会话的 CRUD,即 DAO(Data Access Object)模式实现,如 DefaultSessionManager 在创建完 session 后会调用该方法;如保存到关系数据库/文件系统/NoSQL 数据库;即可以实现会话的持久化;返回会话 ID;主要此处返回的ID.equals(session.getId());

1
2
3
4
5
6
7
8
9
10
//如DefaultSessionManager在创建完session后会调用该方法;如保存到关系数据库/文件系统/NoSQL数据库;即可以实现会话的持久化;返回会话ID;主要此处返回的ID.equals(session.getId());  
Serializable create(Session session);
//根据会话ID获取会话
Session readSession(Serializable sessionId) throws UnknownSessionException;
//更新会话;如更新会话最后访问时间/停止会话/设置超时时间/设置移除属性等会调用
void update(Session session) throws UnknownSessionException;
//删除会话;当会话过期/会话停止(如用户退出时)会调用
void delete(Session session);
//获取当前所有活跃用户,如果用户量多此方法影响性能
Collection<Session> getActiveSessions();

20200513172101

AbstractSessionDAO提供了SessionDAO的基础实现,如生成会话ID等;CachingSessionDAO提供了对开发者透明的会话缓存的功能,只需要设置相应的CacheManager即可;MemorySessionDAO直接在内存中进行会话维护;而EnterpriseCacheSessionDAO提供了缓存功能的会话维护,默认情况下使用MapCache实现,内部使用ConcurrentHashMap保存缓存的会话。

Shiro 提供了使用 Ehcache 进行会话存储,Ehcache 可以配合 TerraCotta 实现容器无关的分布式集群。

首先增加依赖:

1
2
3
4
5
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.2.2</version>
</dependency>

shiro-web.ini 文件:

1
2
3
4
5
6
sessionDAO=org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO
sessionDAO. activeSessionsCacheName=shiro-activeSessionCache
sessionManager.sessionDAO=$sessionDAO
cacheManager = org.apache.shiro.cache.ehcache.EhCacheManager
cacheManager.cacheManagerConfigFile=classpath:ehcache.xml
securityManager.cacheManager = $cacheManager
sessionDAO. activeSessionsCacheName:设置 Session 缓存名字,默认就是 shiro-activeSessionCache;
cacheManager:缓存管理器,用于管理缓存的,此处使用 Ehcache 实现;
cacheManager.cacheManagerConfigFile:设置 ehcache 缓存的配置文件;
securityManager.cacheManager:设置 SecurityManager 的 cacheManager,会自动设置实现了 CacheManagerAware 接口的相应对象,如 SessionDAO 的 cacheManager;

配置 ehcache.xml:

1
2
3
4
5
6
7
8
<cache name="shiro-activeSessionCache"
maxEntriesLocalHeap="10000"
overflowToDisk="false"
eternal="false"
diskPersistent="false"
timeToLiveSeconds="0"
timeToIdleSeconds="0"
statistics="true"/>

Cache 的名字为 shiro-activeSessionCache,即设置的 sessionDAO 的 activeSessionsCacheName 属性值。

另外可以通过如下 ini 配置设置会话 ID 生成器:

1
2
sessionIdGenerator=org.apache.shiro.session.mgt.eis.JavaUuidSessionIdGenerator
sessionDAO.sessionIdGenerator=$sessionIdGenerator

用于生成会话 ID,默认就是 JavaUuidSessionIdGenerator,使用 java.util.UUID 生成。

如果需要存储到数据库 自定义实现 SessionDAO,继承 CachingSessionDAO 即可:

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
public class MySessionDAO extends CachingSessionDAO {
private JdbcTemplate jdbcTemplate = JdbcTemplateUtils.jdbcTemplate();
protected Serializable doCreate(Session session) {
Serializable sessionId = generateSessionId(session);
assignSessionId(session, sessionId);
String sql = "insert into sessions(id, session) values(?,?)";
jdbcTemplate.update(sql, sessionId, SerializableUtils.serialize(session));
return session.getId();
}
protected void doUpdate(Session session) {
if(session instanceof ValidatingSession && !((ValidatingSession)session).isValid()) {
return; //如果会话过期/停止 没必要再更新了
}
String sql = "update sessions set session=? where id=?";
jdbcTemplate.update(sql, SerializableUtils.serialize(session), session.getId());
}
protected void doDelete(Session session) {
String sql = "delete from sessions where id=?";
jdbcTemplate.update(sql, session.getId());
}
protected Session doReadSession(Serializable sessionId) {
String sql = "select session from sessions where id=?";
List<String> sessionStrList = jdbcTemplate.queryForList(sql, String.class, sessionId);
if(sessionStrList.size() == 0) return null;
return SerializableUtils.deserialize(sessionStrList.get(0));
}
}

doCreate/doUpdate/doDelete/doReadSession 分别代表创建 / 修改 / 删除 / 读取会话;此处通过把会话序列化后存储到数据库实现;接着在 shiro-web.ini 中配置:

sessionDAO=com.github.zhangkaitao.shiro.chapter10.session.dao.MySessionDAO

其他设置和之前一样,因为继承了 CachingSessionDAO;所有在读取时会先查缓存中是否存在,如果找不到才到数据库中查找。

使用Redis作为Session缓存

需要shiro重写SessionDAO,继承AbstractSessionDAO,实现Redis Session的增刪改查操作

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
@Data
public class ShiroRedisSessionDao extends AbstractSessionDAO {

private RedisTemplate<Serializable, Object> redisTemplate;

@Override
public Serializable create(final Session session) {
Serializable id = super.create(session);
redisTemplate.opsForValue().set(Contans.getKeys(id), session, 3600L, TimeUnit.MINUTES);
return id;
}

@Override
protected Serializable doCreate(Session session) {
Serializable sessionId = super.generateSessionId(session);
super.assignSessionId(session, sessionId);
return sessionId;
}

//根据会话ID获取会话
@Override
public Session readSession(Serializable sessionId) throws UnknownSessionException {
Session session = doReadSession(sessionId);
if (session == null) {
throw new UnknownSessionException("There is no session with id [" + sessionId + "]");
}
return session;
}

@Override
protected Session doReadSession(final Serializable sessionId) {
Session session = (SimpleSession) this.redisTemplate.opsForValue().get(Contans.getKeys(sessionId));
if (session == null) {
throw new UnknownSessionException("There is no session with id [" + sessionId + "]");
} else {
return session;
}
}

@Override
public void update(final Session session) throws UnknownSessionException {
if (session instanceof SimpleSession) {
final Serializable id = session.getId();
redisTemplate.opsForValue().set(Contans.getKeys(id), session, 3600L, TimeUnit.MINUTES);
}
}

@Override
public void delete(Session session) {
redisTemplate.delete(CollectionUtils.arrayToList(Contans.getKeys(session.getId())));
}

// 获取当前所有活跃用户,如果用户量多此方法影响性能
@Override
public Collection<Session> getActiveSessions() {
return null;
}
}

配置Redis不做介绍

配置ShiroConfig

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
    /* @Bean
public SimpleCookie rememberMeCookie(){
//这个参数是cookie的名称,对应前端的checkbox的name = rememberMe
SimpleCookie simpleCookie = new SimpleCookie("rememberMe");
//setcookie的httponly属性如果设为true的话,会增加对xss防护的安全系数。它有以下特点:
//setcookie()的第七个参数
//设为true后,只能通过http访问,javascript无法访问
//防止xss读取cookie
simpleCookie.setHttpOnly(true);
simpleCookie.setPath("/");
//记住我cookie生效时间30天 ,单位秒;
simpleCookie.setMaxAge(2592000);
return simpleCookie;
}
//cookie管理对象;记住我功能,rememberMe管理器
@Bean
public CookieRememberMeManager rememberMeManager(){
CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
cookieRememberMeManager.setCookie(rememberMeCookie());
//rememberMe cookie加密的密钥 建议每个项目都不一样 默认AES算法 密钥长度(128 256 512 位)
cookieRememberMeManager.setCipherKey(Base64.decode("4AvVheFLUs0KTA3K23frgdg=="));
return cookieRememberMeManager;
}*/
//自定义Realm
@Bean
public UserRealm myShiroRealm() {
UserRealm myShiroRealm = new UserRealm();
return myShiroRealm;
}

//配置核心安全事务管理器
@Bean
public SecurityManager securityManager(SessionDAO sessionDAO) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
//指定 securityManager 的 realms 实现,可以设置多个
//securityManager 会按照 realms 指定的顺序进行身份认证。此处我们使用显示指定顺序的方式指定了 Realm 的顺序,
// 如果不设置,那么 securityManager 会按照 realm 声明的顺序进行使用(即无需设置 realms 属性,其会自动发现 )。
securityManager.setRealm(myShiroRealm());
//配置自定义session管理,使用redis
securityManager.setSessionManager(sessionManager(sessionDAO));
//配置记住我
// securityManager.setRememberMeManager(rememberMeManager());
//配置redis缓存
// securityManager.setCacheManager(cacheManager());
return securityManager;
}

/**
* 解决无权限页面不跳转 shiroFilterFactoryBean.setUnauthorizedUrl("/page/fail.html") 无效
*
* shiro的源代码ShiroFilterFactoryBean.Java定义的filter必须满足filter instanceof AuthorizationFilter,
* 只有perms,roles,ssl,rest,port才是属于AuthorizationFilter,而anon,authcBasic,auchc,user是AuthenticationFilter,
* 所以unauthorizedUrl设置后页面不跳转 Shiro注解模式下,登录失败与没有权限都是通过抛出异常。
* 并且默认并没有去处理或者捕获这些异常。在SpringMVC下需要配置捕获相应异常来通知用户信息
* @return
*/
@Bean
public SimpleMappingExceptionResolver simpleMappingExceptionResolver() {
SimpleMappingExceptionResolver simpleMappingExceptionResolver=new SimpleMappingExceptionResolver();
Properties properties=new Properties();
//这里的 /unauthorized 是页面,不是访问的路径
properties.setProperty("org.apache.shiro.authz.UnauthorizedException","/page/fail.html");
properties.setProperty("org.apache.shiro.authz.UnauthenticatedException","/page/fail.html");
simpleMappingExceptionResolver.setExceptionMappings(properties);
return simpleMappingExceptionResolver;
}

@Bean
public SessionDAO sessionDAO(RedisTemplate redisTemplate) {
ShiroRedisSessionDao sessionDao = new ShiroRedisSessionDao();
sessionDao.setRedisTemplate(redisTemplate);
return sessionDao;
}
//配置会话管理器
//SessionManager是session的真正管理者,负责shiro的session管理;
@Bean("sessionManager")
public SessionManager sessionManager(SessionDAO sessionDAO) {
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
sessionManager.setSessionDAO(sessionDAO);
sessionManager.setSessionIdCookie(simpleCookie());
List<SessionListener> arrayList = new ArrayList<>();
arrayList.add(sessionListener());
sessionManager.setSessionListeners(arrayList);
return sessionManager;
}

@Bean("sessionListener")
public SessionListener sessionListener() {
return new ShiroRedisSessionListener();
}

//配置保存sessionId的cookie
//这里的cookie 不是上面的记住我 cookie
@Bean
public SimpleCookie simpleCookie() {
//这个参数是cookie的名称
SimpleCookie simpleCookie = new SimpleCookie("sid");
//setcookie的httponly属性如果设为true的话,会增加对xss防护的安全系数。它有以下特点:
//setcookie()的第七个参数
//设为true后,只能通过http访问,javascript无法访问
//防止xss读取cookie
simpleCookie.setHttpOnly(true);
simpleCookie.setPath("/");
//maxAge=-1表示浏览器关闭时失效此Cookie
simpleCookie.setMaxAge(-1);
return simpleCookie;
}

会话验证

定期的检测会话是否过期,Shiro 提供了会话验证调度器 SessionValidationScheduler实现。

可以通过如下 ini 配置开启会话验证:

1
2
3
4
5
6
sessionValidationScheduler=org.apache.shiro.session.mgt.ExecutorServiceSessionValidationScheduler
sessionValidationScheduler.interval = 3600000
sessionValidationScheduler.sessionManager=$sessionManager
sessionManager.globalSessionTimeout=1800000
sessionManager.sessionValidationSchedulerEnabled=true
sessionManager.sessionValidationScheduler=$sessionValidationScheduler

Shiro 也提供了使用 Quartz 会话验证调度器:

1
2
3
sessionValidationScheduler=org.apache.shiro.session.mgt.quartz.QuartzSessionValidationScheduler
sessionValidationScheduler.sessionValidationInterval = 3600000
sessionValidationScheduler.sessionManager=$sessionManager

使用时需要导入 shiro-quartz 依赖:

1
2
3
4
5
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-quartz</artifactId>
<version>1.2.2</version>
</dependency>

会话验证调度器实现都是直接调用 AbstractValidatingSessionManager 的 validateSessions 方法进行验证,其直接调用 SessionDAO 的 getActiveSessions 方法获取所有会话进行验证,如果会话比较多,会影响性能;可以考虑如分页获取会话并进行验证,如:

1
2
3
4
@Override
public Collection<Session> getActiveSessions() {
return null;
}

在此方法中分页获取,如果在会话过期时不想删除过期的会话,可以通过如下 ini 配置进行设置:

sessionManager.deleteInvalidSessions=false

默认是开启的,在会话过期后会调用 SessionDAO 的 delete 方法删除会话:如会话时持久化存储的,可以调用此方法进行删除。

如果是在获取会话时验证了会话已过期,将抛出 InvalidSessionException。

sessionFactory

sessionFactory 是创建会话的工厂,根据相应的 Subject 上下文信息来创建会话;默认提供了 SimpleSessionFactory 用来创建 SimpleSession 会话。

首先自定义一个 Session:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class OnlineSession extends SimpleSession {
public static enum OnlineStatus {
on_line("在线"), hidden("隐身"), force_logout("强制退出");
private final String info;
private OnlineStatus(String info) {
this.info = info;
}
public String getInfo() {
return info;
}
}
private String userAgent; //用户浏览器类型
private OnlineStatus status = OnlineStatus.on_line; //在线状态
private String systemHost; //用户登录时系统IP
//省略其他
}

OnlineSession 用于保存当前登录用户的在线状态,支持如离线等状态的控制。

接着自定义 SessionFactory:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class OnlineSessionFactory implements SessionFactory {
@Override
public Session createSession(SessionContext initData) {
OnlineSession session = new OnlineSession();
if (initData != null && initData instanceof WebSessionContext) {
WebSessionContext sessionContext = (WebSessionContext) initData;
HttpServletRequest request = (HttpServletRequest) sessionContext.getServletRequest();
if (request != null) {
session.setHost(IpUtils.getIpAddr(request));
session.setUserAgent(request.getHeader("User-Agent"));
session.setSystemHost(request.getLocalAddr() + ":" + request.getLocalPort());
}
}
return session;
}
}

根据会话上下文创建相应的 OnlineSession。

最后在 shiro-web.ini 配置文件中配置:

1
2
sessionFactory=org.apache.shiro.session.mgt.OnlineSessionFactory
sessionManager.sessionFactory=$sessionFactory

缓存

Shiro 提供了类似于 Spring 的 Cache 抽象,即 Shiro 本身不实现 Cache,但是对 Cache 进行了又抽象,方便更换不同的底层 Cache 实现。Shiro提供的缓存抽象API接口正是:org.apache.shiro.cache.CacheManager。

Shiro支持在2个地方定义缓存管理器,既可以在SecurityManager中定义,也可以在Realm中定义,任选其一即可。

通常我们都会自定义Realm实现,例如将权限数据存放在数据库中,那么在Realm实现中定义缓存管理器再合适不过了。

Shiro设计成既可以在Realm,也可以在SecurityManager中设置缓存管理器,分别在Realm和SecurityManager定义的缓存管理器,他们的区别和联系,请看源码:
下面,我们追踪一下org.apache.shiro.mgt.RealmSecurityManage的源码实现:

1
2
3
4
5
6
7
8
9
10
11
protected void applyCacheManagerToRealms() {
CacheManager cacheManager = getCacheManager();
Collection<Realm> realms = getRealms();
if (cacheManager != null && realms != null && !realms.isEmpty()) {
for (Realm realm : realms) {
if (realm instanceof CacheManagerAware) {
((CacheManagerAware) realm).setCacheManager(cacheManager);
}
}
}
}

真正使用CacheManager的组件是Realm。

缓存方案

基于Redis的集中式缓存方案

本地缓存的实现有几种方式:(1)直接存放到JVM堆内存(2)使用NIO存放在堆外内存,自定义实现或者借助于第三方缓存组件。
不论是采用集中式缓存还是使用本地缓存,shiro的权限数据本身都是直接存放在本地的,不同的是缓存标志的存放位置。采用本地缓存方案是,我们将缓存标志也存放在本地,这样就避免了查询缓存标志的网络请求,能更进一步提升缓存效率

缓存更新

shiro框架的服务端进行了多实例部署,首先需要对session进行同步,因为shiro的认证信息是存放在session中的;其次,当前端操作在某个实例上修改了权限时,需要通知后端服务的多个实例重新获取最新的权限数据。

zookeeper最核心的功能就是统一配置,同时还可以用来实现服务注册与发现,在这里使用的zookeeper特性是:watcher机制。当某个节点的状态发生改变时,监控该节点状态的组件将会收到通知。

缓存实现

把权限信息存到redis

Cache

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
@Data
public class ShiroRedisCache<K, V> implements Cache<K, V> {
private RedisTemplate<K, V> redisTemplate;
private K keyPrefix;
private RedisSerializer<Object> keySerializer=new JdkSerializationRedisSerializer();
private RedisSerializer<Object> valueSerializer=new JdkSerializationRedisSerializer();

public ShiroRedisCache(K keyPrefix, RedisTemplate<K, V> redisTemplate) {
this.keyPrefix = keyPrefix;
this.redisTemplate = redisTemplate;
}

@Override
public void clear() throws CacheException {

}

@SuppressWarnings("unchecked")
@Override
public V get(K k) throws CacheException {
final byte[] keyByte = getKey(k);
byte[] result = this.redisTemplate.execute(new RedisCallback<byte[]>() {
@Override
public byte[] doInRedis(RedisConnection connection) throws DataAccessException {
return connection.get(keyByte);
}
});
return (V) this.valueSerializer.deserialize(result);
}

public byte[] getKey(K k) {
if (k instanceof String) {
return (this.keyPrefix+(String) k).getBytes();
}
return this.keySerializer.serialize(k);
}

@Override
public Set<K> keys() {
final byte[] pattern=(this.keyPrefix+"*").getBytes();
return this.redisTemplate.execute(new RedisCallback<Set<K>>(){
@SuppressWarnings("unchecked")
@Override
public Set<K> doInRedis(RedisConnection connection) throws DataAccessException {
Set<byte[]> keys=connection.keys(pattern);
if(CollectionUtils.isEmpty(keys)){
return Collections.emptySet();
}
Set<K> newKeys = new HashSet<K>();
for(byte[] key:keys){
newKeys.add((K)key);
}
return newKeys;
}

});
}

@Override
public V put(K k, final V v) throws CacheException {
final byte[] key = getKey(k);
final V perV=get(k);
final byte[] values = this.valueSerializer.serialize(v);
return this.redisTemplate.execute(new RedisCallback<V>() {
@Override
public V doInRedis(RedisConnection connection) throws DataAccessException {
connection.set(key, values);
return perV;
}
});
}

@Override
public V remove(K k) throws CacheException {
final byte[] key = getKey(k);
final V perV=get(k);
return this.redisTemplate.execute(new RedisCallback<V>() {
@Override
public V doInRedis(RedisConnection connection) throws DataAccessException {
connection.del(key);
return perV;
}
});
}

@Override
public int size() {
return this.redisTemplate.execute(new RedisCallback<Long>() {
@Override
public Long doInRedis(RedisConnection connection) throws DataAccessException {
return connection.dbSize();
}
}).intValue();
}

@Override
public Collection<V> values() {
Set<K> keys = keys();
if (!CollectionUtils.isEmpty(keys)) {
List<V> values = new ArrayList<V>(keys.size());
for (K key : keys) {
V value = get(key);
if (value != null) {
values.add(value);
}
}
return Collections.unmodifiableList(values);
} else {
return Collections.emptyList();
}
}
public static void main(String[] args){
System.out.println("Cd".matches("^[A-Z]+$"));
}
}

CacheManager

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Data
public class ShiroRedisCacheManager implements CacheManager {

private final ConcurrentHashMap<String, ShiroRedisCache> caches = new ConcurrentHashMap<String, ShiroRedisCache>();

private RedisTemplate redisTemplate;

private String keyPrefix;

@SuppressWarnings("unchecked")
@Override
public <K, V> Cache<K, V> getCache(String name) throws CacheException {
ShiroRedisCache<K, V> cache = (ShiroRedisCache<K, V>) caches.get(name);
if (cache == null) {
cache = new ShiroRedisCache<>(keyPrefix, this.redisTemplate);
caches.put(name, cache);
}
return cache;
}
}
1
2
3
4
5
6
7
public class Contans {
public static String keyPrefix = "shiro-session:";
public static String keyPrefixCache = "shiro-cache:";
public static String getKeys(Serializable id) {
return Contans.keyPrefix + id.toString();
}
}

修改配置信息,把缓存管理器配置到Shiro中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
    //配置核心安全事务管理器
@Bean
public SecurityManager securityManager(SessionDAO sessionDAO,CacheManager cacheManager) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
//指定 securityManager 的 realms 实现,可以设置多个
//securityManager 会按照 realms 指定的顺序进行身份认证。此处我们使用显示指定顺序的方式指定了 Realm 的顺序,
// 如果不设置,那么 securityManager 会按照 realm 声明的顺序进行使用(即无需设置 realms 属性,其会自动发现 )。
securityManager.setRealm(myShiroRealm());
//配置自定义session管理,使用redis
securityManager.setSessionManager(sessionManager(sessionDAO));
//配置记住我
// securityManager.setRememberMeManager(rememberMeManager());
//配置redis缓存
securityManager.setCacheManager(cacheManager);
return securityManager;
}

@Bean
public CacheManager cacheManager(RedisTemplate redisTemplate){
ShiroRedisCacheManager shiroRedisCacheManager = new ShiroRedisCacheManager();
shiroRedisCacheManager.setKeyPrefix(Contans.keyPrefixCache);
shiroRedisCacheManager.setRedisTemplate(redisTemplate);
return shiroRedisCacheManager;
}
之后,访问用到权限的页面,就会在redis中缓存授权信息。

详细,见 https://github.com/huingsn/my-project

单点登录

https://www.w3cschool.cn/shiro/48371if3.html

BeanFactory和FactoryBean

发表于 2019-02-22 | 分类于 spring

1、 BeanFactory

BeanFactory定义了 IOC 容器的最基本形式,并提供了 IOC 容器应遵守的的最基本的接口,也就是 Spring IOC 所遵守的最底层和最基本的编程规范。

在 Spring 代码中, BeanFactory 只是个接口,并不是 IOC 容器的具体实现,但是 Spring 容器给出了很多种实现,如 DefaultListableBeanFactory 、 XmlBeanFactory 、 ApplicationContext 等,都是附加了某种功能的实现。

2、 FactoryBean

一般情况下,Spring 通过反射机制的方式实现类实例化 Bean ,如:bean标签、@Component、@Bean。

但是在某些情况下,实例化 Bean 过程比较复杂,如果按照传统的方式,则需要在 中提供大量的配置信息。配置方式的灵活性是受限的,这时采用编码的方式可能会得到一个简单的方案。 Spring 为此提供了一个FactoryBean的工厂类接口,用户可以通过实现该接口定制实例化 Bean 的逻辑。

FactoryBean就是一个Bean,FactoryBean接口的地位很重要, Spring 自身就提供了 70 多个 FactoryBean 的实现。它们隐藏了实例化一些复杂 Bean 的细节,给上层应用带来了便利。能生产或者修饰对象生成的工厂 Bean.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public interface FactoryBean<T> {
//返回由 FactoryBean 创建的 Bean 实例,如果 isSingleton() 返回 true ,则该实例会放到 Spring 容器中单实例缓存池中
@Nullable
T getObject() throws Exception;

//返回 FactoryBean 创建的 Bean 类型。
@Nullable
Class<?> getObjectType();

//返回由 FactoryBean 创建的 Bean 实例的作用域是 singleton 还是 prototype
default boolean isSingleton() {
return true;
}

}

如果一个类实现了,那么在spring中就会有两个对象,一个是getObject()返回的对象,一个是当前实现FactoryBean的对象。getBean() 方法返回的不是 FactoryBean 本身,而是 FactoryBean#getObject() 方法所返回的对象,相当于 FactoryBean#getObject() 代理了 getBean() 方法。

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
public class User {
public void print(){
System.out.println("User");
}
}

@Component("mybean")
public class MyBean implements FactoryBean<User> {
@Override
public User getObject() throws Exception {
return new User();
}

@Override
public Class<?> getObjectType() {
return User.class;
}

@Override
public boolean isSingleton() {
return true;
}

public void print(){
System.out.println("MyBean");
}
}

@Configuration
@ComponentScan("org.hu.factorybean")
public class AppConfig {

}

public class Test {
public static void main(String[] args) {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
//就是getObject()返回的对象
User user = (User)applicationContext.getBean("mybean");
user.print();//输出User
//当前实现FactoryBean的对象要加个符号才能获取到
MyBean myBean = (MyBean) applicationContext.getBean("&mybean");
myBean.print();//输出MyBean
}
}

使用场景

FactoryBean在Spring中最为典型的一个应用就是用来创建AOP的代理对象。

如Mybatis-spring 中的 sqlSessionFactroyBean.

我们知道AOP实际上是Spring在运行时创建了一个代理对象,也就是说这个对象,是我们在运行时创建的,而不是一开始就定义好的,这很符合工厂方法模式。更形象地说,AOP代理对象通过Java的反射机制,在运行时创建了一个代理对象,在代理对象的目标方法中根据业务要求织入了相应的方法。这个对象在Spring中就是——ProxyFactoryBean。

spring源码@Import注解

发表于 2019-02-20 | 分类于 spring

Spring 3.0之前,创建Bean可以通过xml配置文件与扫描特定包下面的类来将类注入到Spring IOC容器内。而在Spring 3.0之后提供了JavaConfig的方式,也就是将IOC容器里Bean的元信息以java代码的方式进行描述。

1
2
3
4
5
6
7
8
9
10
11
12
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Import {

/**
* {@link Configuration}, {@link ImportSelector}, {@link ImportBeanDefinitionRegistrar}
* or regular component classes to imports.
*/
Class<?>[] value();

}

@Import可以配合 Configuration ,ImportSelector, ImportBeanDefinitionRegistrar 来使用, 表示也可以把Import当成普通的Bean使用,@Import只允许放到类上面,不能放到方法上。

普通使用
1
2
3
4
5
@Configuration
@Import(value={BookServiceImpl.class})
public class Config {

}

这种方式有一些问题,那就是只能使用类的无参构造方法来创建bean,对于有参数的构造方法就不行了。

结合ImportBeanDefinitionRegistrar接口
1
2
3
4
5
6
7
public interface ImportBeanDefinitionRegistrar {
/**
* @param importingClassMetadata annotation metadata of the importing class 通过这个参数可以拿到类的元数据信息
* @param registry current bean definition registry 通过这个参数可以操作IOC容器
*/
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry);
}

使用一个类来实现这个接口

1
2
3
4
5
6
7
8
public class BookServiceBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {

public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata,BeanDefinitionRegistry registry) {
BeanDefinitionBuilder bookService = BeanDefinitionBuilder.rootBeanDefinition(BookServiceImpl.class);
registry.registerBeanDefinition("bookService", bookService.getBeanDefinition());
}

}

接着我们在@Import注解引入的地方只需要修改为引入UserServiceBeanDefinitionRegistrar

1
2
3
4
5
@Configuration
@Import(value={BookServiceBeanDefinitionRegistrar.class})
public class Config {

}

这样就把bookService注入到IOC。

结合ImportSelector接口

ImportSelector接口是至spring中导入外部配置的核心接口,在SpringBoot的自动化配置和@EnableXXX(功能性注解)都有它的存在

ImportSelector接口的源码如下:

1
2
3
public interface ImportSelector {
String[] selectImports(AnnotationMetadata importingClassMetadata);
}

@Import注解是将指定的Bean加入到IOC容器之中进行管理,ImportSelector接口只有一个selectImports方法,该方法将返回一个数组,也就是类实例名称,@Import注解将会把返回的Bean加入到IOC容器中进行管理。

示例:

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
public class MySelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
//importingClassMetadata.getMetaAnnotationTypes()
return new String[]{MyBeanPostProcessor.class.getName()};
}
}

@Retention(RetentionPolicy.RUNTIME)
@Import(MySelector.class)
public @interface SelectorEnable {
}

public class MyBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
System.out.println(beanName);
if ("userService".equals(beanName)) {
System.out.println(11111);
bean = Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class<?>[]{UserService.class}, new MyInvocationHandler(bean));
}
return bean;
}

@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
return null;
}
}

@Configuration
@ComponentScan("com.hu")
@SelectorEnable //或者不用注解,直接@Import(MySelector.class)
public class AppConfig {

}

这样就可以动态注入一些信息。

Spring源码分析—MVC源码

发表于 2019-02-02 | 分类于 spring

Spring MVC运行过程

1、用户发送请求至前端控制器DispatcherServlet。

2、DispatcherServlet收到请求调用HandlerMapping处理器映射器。

3、处理器映射器找到具体的处理器(可以根据xml配置、注解进行查找),生成处理器对象及处理器拦截器(如果有则生成)一并返回给DispatcherServlet。

4、 DispatcherServlet调用HandlerAdapter处理器适配器。

5、HandlerAdapter经过适配调用具体的处理器(Controller控制器)。

6、Controller执行完成返回ModelAndView。

7、HandlerAdapter将controller执行结果ModelAndView返回给DispatcherServlet。

8、DispatcherServlet将ModelAndView传给ViewReslover视图解析器。

9、ViewReslover解析后返回具体View.

10、DispatcherServlet根据View进行渲染视图

11、DispatcherServlet响应用户。

几大组件:
组件 | 说明
—|—
DispatcherServlet | Spring MVC 的核心组件,是请求的入口,负责协调各个组件工作
HandlerMapping | 内部维护了一些 <访问路径, 处理器> 映射,负责为请求找到合适的处理器
HandlerAdapter | 处理器的适配器。Spring 中的处理器的实现多变,比如用户处理器可以实现 Controller 接口,也可以用 @RequestMapping 注解将方法作为一个处理器等,这就导致 Spring 不知道怎么调用用户的处理器逻辑。所以这里需要一个处理器适配器,由处理器适配器去调用处理器的逻辑。
ViewResolver | 视图解析器的用途不难理解,用于将视图名称解析为视图对象 View。
View | 视图对象用于将模板渲染成 html 或其他类型的文件。比如 InternalResourceView 可将 jsp 渲染成 html。

DispatcherServlet 源码

20200517174956

Aware:在 Spring 中,Aware 类型的接口可以用于向 Spring获取框架中的信息。当实现了 ApplicationContextAware 接口时,实例通过接口方法 setApplicationContext 传给该 bean。
EnvironmentCapable: 仅包含一个方法定义 getEnvironment,通过该方法可以获取到环境变量对象。
HttpServletBean:是 HttpServlet 抽象类的简单拓展。HttpServletBean 覆写了父类中的无参 init 方法,并在该方法中将 ServletConfig 里的配置信息设置到子类对象中,比如 DispatcherServlet。
FrameworkServlet:Spring Web 框架中的一个基础类,该类会在初始化时创建一个容器。同时该类覆写了 doGet、doPost 等方法,并将所有类型的请求委托给 doService 方法去处理。doService 是一个抽象方法,需要子类实现。
DispatcherServlet:即协调各个组件工作和初始化各种组件,比如 HandlerMapping、HandlerAdapter 等。

入口方法:doDispatch

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
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;

WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

try {
ModelAndView mv = null;
Exception dispatchException = null;

try {
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);

// 获取可处理当前请求的处理器 Handler
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}

// 获取确定当前请求的处理程序适配器
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

// 处理 last-modified 消息头
String method = request.getMethod();
boolean isGet = "GET".equals(method);
if (isGet || "HEAD".equals(method)) {
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if (logger.isDebugEnabled()) {
logger.debug("Last-Modified value for [" + getRequestUri(request) + "] is: " + lastModified);
}
if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
return;
}
}
// 执行拦截器 preHandle 方法
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}

// 调用处理器逻辑
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
// 如果 controller 未返回 view 名称,这里生成默认的 view 名称
applyDefaultViewName(processedRequest, mv);
// 执行拦截器 preHandle 方法
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
dispatchException = ex;
}
catch (Throwable err) {
// As of 4.3, we're processing Errors thrown from handler methods as well, making them available for @ExceptionHandler methods and other scenarios.
dispatchException = new NestedServletException("Handler dispatch failed", err);
}
// 解析并渲染视图
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception ex) {
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch (Throwable err) {
triggerAfterCompletion(processedRequest, response, mappedHandler,
new NestedServletException("Handler processing failed", err));
}
finally {
if (asyncManager.isConcurrentHandlingStarted()) {
// Instead of postHandle and afterCompletion
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
}
else {
// Clean up any resources used by a multipart request.
if (multipartRequestParsed) {
cleanupMultipart(processedRequest);
}
}
}
}

private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
@Nullable Exception exception) throws Exception {

boolean errorView = false;

if (exception != null) {
if (exception instanceof ModelAndViewDefiningException) {
logger.debug("ModelAndViewDefiningException encountered", exception);
mv = ((ModelAndViewDefiningException) exception).getModelAndView();
}
else {
Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
mv = processHandlerException(request, response, handler, exception);
errorView = (mv != null);
}
}

// 处理程序是否返回要呈现的视图?
if (mv != null && !mv.wasCleared()) {
render(mv, request, response);
if (errorView) {
WebUtils.clearErrorRequestAttributes(request);
}
}
else {
if (logger.isDebugEnabled()) {
logger.debug("Null ModelAndView returned to DispatcherServlet with name '" + getServletName() +
"': assuming HandlerAdapter completed request handling");
}
}

if (WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
// Concurrent handling started during a forward
return;
}

if (mappedHandler != null) {
mappedHandler.triggerAfterCompletion(request, response, null);
}
}

protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
// 确定请求的区域设置并将其应用于响应。
Locale locale =
(this.localeResolver != null ? this.localeResolver.resolveLocale(request) : request.getLocale());
response.setLocale(locale);

View view;
String viewName = mv.getViewName();
if (viewName != null) {
// 需要解析视图名称。
view = resolveViewName(viewName, mv.getModelInternal(), locale, request);
if (view == null) {
throw new ServletException("Could not resolve view with name '" + mv.getViewName() +
"' in servlet with name '" + getServletName() + "'");
}
}
else {
// No need to lookup: the ModelAndView object contains the actual View object.
view = mv.getView();
if (view == null) {
throw new ServletException("ModelAndView [" + mv + "] neither contains a view name nor a " +
"View object in servlet with name '" + getServletName() + "'");
}
}

// 委托视图对象进行渲染
if (logger.isDebugEnabled()) {
logger.debug("Rendering view [" + view + "] in DispatcherServlet with name '" + getServletName() + "'");
}
try {
if (mv.getStatus() != null) {
response.setStatus(mv.getStatus().value());
}
// 渲染视图,并将结果返回给用户。
view.render(mv.getModelInternal(), request, response);
}
catch (Exception ex) {
if (logger.isDebugEnabled()) {
logger.debug("Error rendering view [" + view + "] in DispatcherServlet with name '" +
getServletName() + "'", ex);
}
throw ex;
}
}

Web容器

一个 Web 应用通常有两个容器。一个容器用于加载 Web 层的类,比如接口 Controller、HandlerMapping、ViewResolver 等,叫做 web 容器;另一个容器用于加载业务逻辑相关的类,比如 service、dao 层的一些类,业务容器;在容器初始化的过程中,业务容器会先于 web 容器进行初始化;web 容器初始化时,会将业务容器作为父容器,这是因为web 容器中的一些 bean 会依赖于业务容器中的 bean。

父容器配置分析

配置

1
2
3
4
5
6
7
8
9
10
<web-app>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:application.xml</param-value>
</context-param>
</web-app>

ContextLoaderListener分析

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
158
159
160
161
162
public class ContextLoaderListener extends ContextLoader implements ServletContextListener {
public ContextLoaderListener() {
}
public ContextLoaderListener(WebApplicationContext context) {
super(context);
}
/**
* 初始化根web应用程序上下文
*/
@Override
public void contextInitialized(ServletContextEvent event) {
initWebApplicationContext(event.getServletContext());
}
@Override
public void contextDestroyed(ServletContextEvent event) {
closeWebApplicationContext(event.getServletContext());
ContextCleanupListener.cleanupAttributes(event.getServletContext());
}

}

public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
/*
* 如果 ServletContext 中 ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE 属性值 不为空时,说明有其他监听器设置了这个属性。
* Spring 不能替换掉别的监听器设置 的属性值,所以这里抛出异常。
*/
if (servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE) != null) {
throw new IllegalStateException(
"Cannot initialize context because there is already a root application context present - " +
"check whether you have multiple ContextLoader* definitions in your web.xml!");
}

Log logger = LogFactory.getLog(ContextLoader.class);
servletContext.log("Initializing Spring root WebApplicationContext");
if (logger.isInfoEnabled()) {
logger.info("Root WebApplicationContext: initialization started");
}
long startTime = System.currentTimeMillis();

try {
// 将上下文存储在本地实例变量中,以确保它在ServletContext关闭时可用。
if (this.context == null) {
// 创建 WebApplicationContext
this.context = createWebApplicationContext(servletContext);
}
if (this.context instanceof ConfigurableWebApplicationContext) {
ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context;
if (!cwac.isActive()) {
// 加载父 ApplicationContext,一般情况下,业务容器不会有父容器, 除非进行配置
if (cwac.getParent() == null) {
ApplicationContext parent = loadParentContext(servletContext);
cwac.setParent(parent);
}
// 配置并刷新 WebApplicationContext
configureAndRefreshWebApplicationContext(cwac, servletContext);
}
}
// 设置 ApplicationContext 到 servletContext 中
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);

ClassLoader ccl = Thread.currentThread().getContextClassLoader();
if (ccl == ContextLoader.class.getClassLoader()) {
currentContext = this.context;
}
else if (ccl != null) {
currentContextPerThread.put(ccl, this.context);
}

if (logger.isDebugEnabled()) {
logger.debug("Published root WebApplicationContext as ServletContext attribute with name [" +
WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE + "]");
}
if (logger.isInfoEnabled()) {
long elapsedTime = System.currentTimeMillis() - startTime;
logger.info("Root WebApplicationContext: initialization completed in " + elapsedTime + " ms");
}

return this.context;
}
catch (RuntimeException ex) {
logger.error("Context initialization failed", ex);
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, ex);
throw ex;
}
catch (Error err) {
logger.error("Context initialization failed", err);
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, err);
throw err;
}
}
//首先会调用 determineContextClass 判断创建什么类型的容器,默认为 XmlWebApplicationContext。
protected WebApplicationContext createWebApplicationContext(ServletContext sc) {
// 判断创建什么类型的容器,默认类型为 XmlWebApplicationContext
Class<?> contextClass = determineContextClass(sc);
if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
throw new ApplicationContextException("Custom context class [" + contextClass.getName() +
"] is not of type [" + ConfigurableWebApplicationContext.class.getName() + "]");
}
// 通过反射创建容器
return (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);
}

//然后调用 instantiateClass 方法通过反射的方式创建容器实例。
protected Class<?> determineContextClass(ServletContext servletContext) {
//读取用户自定义配置
String contextClassName = servletContext.getInitParameter(CONTEXT_CLASS_PARAM);
if (contextClassName != null) {
try {
return ClassUtils.forName(contextClassName, ClassUtils.getDefaultClassLoader());
}
catch (ClassNotFoundException ex) {
throw new ApplicationContextException(
"Failed to load custom context class [" + contextClassName + "]", ex);
}
}
else {
//若无自定义配置,则获取默认的容器类型,默认类型为 XmlWebApplicationContext。
contextClassName = defaultStrategies.getProperty(WebApplicationContext.class.getName());
try {
return ClassUtils.forName(contextClassName, ContextLoader.class.getClassLoader());
}
catch (ClassNotFoundException ex) {
throw new ApplicationContextException(
"Failed to load default context class [" + contextClassName + "]", ex);
}
}
}

//刷新
protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc) {
if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
// 从 ServletContext 中获取用户配置的 contextId 属性
String idParam = sc.getInitParameter(CONTEXT_ID_PARAM);
if (idParam != null) {
wac.setId(idParam);
}
else {
// 设置一个默认的容器 id
wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +
ObjectUtils.getDisplayString(sc.getContextPath()));
}
}

wac.setServletContext(sc);
// 获取 contextConfigLocation 配置
String configLocationParam = sc.getInitParameter(CONFIG_LOCATION_PARAM);
if (configLocationParam != null) {
wac.setConfigLocation(configLocationParam);
}

// The wac environment's #initPropertySources will be called in any case when the context
// is refreshed; do it eagerly here to ensure servlet property sources are in place for
// use in any post-processing or initialization that occurs below prior to #refresh
ConfigurableEnvironment env = wac.getEnvironment();
if (env instanceof ConfigurableWebEnvironment) {
((ConfigurableWebEnvironment) env).initPropertySources(sc, null);
}

customizeContext(sc, wac);
// 刷新容器
wac.refresh();
}

Web 容器创建

web容器的创建是在HttpServletBean抽象时,覆写了父类 HttpServlet 中的 init 方法。在此方法中来创建。

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
//HttpServletBean覆写了init方法,对初始化过程做了一些处理。
@Override
public final void init() throws ServletException {
if (logger.isDebugEnabled()) {
logger.debug("Initializing servlet '" + getServletName() + "'");
}

// 获取 ServletConfig 中的配置信息并设置到目标对象中
PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
if (!pvs.isEmpty()) {
try {
//为当前对象 创建一个 BeanWrapper, 方便读/写对象属性。
BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext());
bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, getEnvironment()));
initBeanWrapper(bw);
// 设置配置信息到目标对象中
bw.setPropertyValues(pvs, true);
}
catch (BeansException ex) {
if (logger.isErrorEnabled()) {
logger.error("Failed to set bean properties on servlet '" + getServletName() + "'", ex);
}
throw ex;
}
}

// 后续初始化
initServletBean();

if (logger.isDebugEnabled()) {
logger.debug("Servlet '" + getServletName() + "' configured successfully");
}
}

//
protected WebApplicationContext initWebApplicationContext() {
// 获取父容器,从 ServletContext 中获取,也就是 ContextLoaderListener 创建的容器
WebApplicationContext rootContext =
WebApplicationContextUtils.getWebApplicationContext(getServletContext());
WebApplicationContext wac = null;

if (this.webApplicationContext != null) {
// A context instance was injected at construction time -> use it
wac = this.webApplicationContext;
if (wac instanceof ConfigurableWebApplicationContext) {
ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;
if (!cwac.isActive()) {
// The context has not yet been refreshed -> provide services such as setting the parent context, setting the application context id, etc
if (cwac.getParent() == null) {
// 设置 rootContext 为父容器
cwac.setParent(rootContext);
}
// 配置并刷新容器
configureAndRefreshWebApplicationContext(cwac);
}
}
}
if (wac == null) {
// 尝试从 ServletContext 中获取容器
wac = findWebApplicationContext();
}
if (wac == null) {
// 创建容器,并将 rootContext 作为父容器
wac = createWebApplicationContext(rootContext);
}

if (!this.refreshEventReceived) {
// Either the context is not a ConfigurableApplicationContext with refresh
// support or the context injected at construction time had already been
// refreshed -> trigger initial onRefresh manually here.
onRefresh(wac);
}

if (this.publishContext) {
// 将上下文发布为servlet上下文属性。
String attrName = getServletContextAttributeName();
// 将创建好的容器设置到 ServletContext 中
getServletContext().setAttribute(attrName, wac);
if (this.logger.isDebugEnabled()) {
this.logger.debug("Published WebApplicationContext of servlet '" + getServletName() +
"' as ServletContext attribute with name [" + attrName + "]");
}
}

return wac;
}

protected WebApplicationContext createWebApplicationContext(@Nullable ApplicationContext parent) {
// 获取容器类型,默认为 XmlWebApplicationContext.class
Class<?> contextClass = getContextClass();
if (this.logger.isDebugEnabled()) {
this.logger.debug("Servlet with name '" + getServletName() +
"' will try to create custom WebApplicationContext context of class '" +
contextClass.getName() + "'" + ", using parent context [" + parent + "]");
}
if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
throw new ApplicationContextException(
"Fatal initialization error in servlet with name '" + getServletName() +
"': custom WebApplicationContext class [" + contextClass.getName() +
"] is not of type ConfigurableWebApplicationContext");
}
// 通过反射实例化容器
ConfigurableWebApplicationContext wac =
(ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);

wac.setEnvironment(getEnvironment());
wac.setParent(parent);
String configLocation = getContextConfigLocation();
if (configLocation != null) {
wac.setConfigLocation(configLocation);
}
// 配置并刷新容器
configureAndRefreshWebApplicationContext(wac);

return wac;
}

protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac) {
if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
// 设置容器 id
if (this.contextId != null) {
wac.setId(this.contextId);
}
else {
// 生成默认 id
wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +
ObjectUtils.getDisplayString(getServletContext().getContextPath()) + '/' + getServletName());
}
}

wac.setServletContext(getServletContext());
wac.setServletConfig(getServletConfig());
wac.setNamespace(getNamespace());
wac.addApplicationListener(new SourceFilteringListener(wac, new ContextRefreshListener()));

// The wac environment's #initPropertySources will be called in any case when the context
// is refreshed; do it eagerly here to ensure servlet property sources are in place for
// use in any post-processing or initialization that occurs below prior to #refresh
ConfigurableEnvironment env = wac.getEnvironment();
if (env instanceof ConfigurableWebEnvironment) {
((ConfigurableWebEnvironment) env).initPropertySources(getServletContext(), getServletConfig());
}
// 后置处理,子类可以覆盖进行一些自定义操作。
postProcessWebApplicationContext(wac);
applyInitializers(wac);
// 刷新容器
wac.refresh();
}

web容器创建过程:

  • 1、从 ServletContext 中获取 ContextLoaderListener 创建的容器
  • 2、若 this.webApplicationContext != null 条件成立,仅设置父容器和刷新容器即可
  • 3、尝试从 ServletContext 中获取容器,若容器不为空,则无需执行步骤4
  • 4、创建容器,并将 rootContext 作为父容器
  • 5、设置容器到 ServletContext 中

Spring源码分析—AOP源码

发表于 2019-01-28 | 分类于 spring

AOP实现

连接点 Joinpoint

在 Spring AOP 中,仅支持方法级别的连接点。

1
2
3
4
5
6
public interface Joinpoint {
/** 用于执行拦截器链中的下一个拦截器逻辑 */
Object proceed() throws Throwable;
Object getThis();
AccessibleObject getStaticPart();
}

proceed 方法是核心,该方法用于执行拦截器逻辑。

一个方法调用就是一个连接点,一下方法调用这个接口的定义:

1
2
3
4
5
6
7
8

public interface Invocation extends Joinpoint {
Object[] getArguments();
}

public interface MethodInvocation extends Invocation {
Method getMethod();
}

切点Pointcut

切点是用于选择连接点的

1
2
3
4
5
6
7
8
9
10
public interface Pointcut {

/** 返回一个类型过滤器 */
ClassFilter getClassFilter();

/** 返回一个方法匹配器 */
MethodMatcher getMethodMatcher();

Pointcut TRUE = TruePointcut.INSTANCE;
}

Pointcut 接口中定义了两个接口,分别用于返回类型过滤器和方法匹配器。

1
2
3
4
5
6
7
8
9
10
11
12
public interface ClassFilter {
boolean matches(Class<?> clazz);
ClassFilter TRUE = TrueClassFilter.INSTANCE;

}

public interface MethodMatcher {
boolean matches(Method method, Class<?> targetClass);
boolean matches(Method method, Class<?> targetClass, Object... args);
boolean isRuntime();
MethodMatcher TRUE = TrueMethodMatcher.INSTANCE;
}

上面的两个接口均定义了 matches 方法,用户只要实现了 matches 方法,即可对连接点进行选择。

通常是用 AspectJ 表达式对连接点进行选择。Spring 中提供了一个 AspectJ 表达式切点类 - AspectJExpressionPointcut。

20200516182057

这个类最终实现了 Pointcut、ClassFilter 和 MethodMatcher 接口,因此该类具备了通过 AspectJ 表达式对连接点进行选择的能力。

通知 Advice

通知 Advice 即我们定义的横切逻辑,Spring 中定义了以下几种通知类型:

前置通知(Before advice)- 在目标方便调用前执行通知
后置通知(After advice)- 在目标方法完成后执行通知
返回通知(After returning advice)- 在目标方法执行成功后,调用通知
异常通知(After throwing advice)- 在目标方法抛出异常后,执行通知
环绕通知(Around advice)- 在目标方法调用前后均可执行自定义逻辑
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public interface Advice {

}
/** BeforeAdvice */
public interface BeforeAdvice extends Advice {

}

public interface MethodBeforeAdvice extends BeforeAdvice {

void before(Method method, Object[] args, Object target) throws Throwable;
}

/** AfterAdvice */
public interface AfterAdvice extends Advice {

}

public interface AfterReturningAdvice extends AfterAdvice {

void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable;
}

通知接口的具体实现类:

20200516182539

切面Aspect

有了切点 Pointcut 和通知 Advice,由于这两个模块目前还是分离的,我们需要把它们整合在一起,就有了切面。

在 AOP 中,切面只是一个概念,并没有一个具体的接口或类与此对应。Spring 有一个接口的用途和切面很像,切点通知器 PointcutAdvisor。

1
2
3
4
5
6
7
8
9
10
public interface Advisor {

Advice getAdvice();
boolean isPerInstance();
}

public interface PointcutAdvisor extends Advisor {

Pointcut getPointcut();
}

Advisor 中有一个 getAdvice 方法,用于返回通知。PointcutAdvisor 在 Advisor 基础上,新增了 getPointcut 方法,用于返回切点对象。因此 PointcutAdvisor 的实现类即可以返回切点,也可以返回通知。

织入Weaving

有了连接点、切点、通知,以及切面等,Spring 是通过何种方式将通知织入到目标方法上的。

通过实现后置处理器 BeanPostProcessor 接口。该接口是 Spring 提供的一个拓展接口,通过实现该接口,可在 bean 初始化前后做一些自定义操作。在 bean 初始化完成后,即 bean 执行完初始化方法(init-method)。Spring通过切点对 bean 类中的方法进行匹配。若匹配成功,则会为该 bean 生成代理对象,并将代理对象返回给容器。容器向后置处理器输入 bean 对象,得到 bean 对象的代理,这样就完成了织入过程。

AOP 入口

Spring AOP 抽象代理创建器实现了 BeanPostProcessor 接口,并在 bean 初始化后置处理过程中向 bean 中织入通知。创建代理对象的入口方法是在类AbstractAutoProxyCreator。

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
public abstract class AbstractAutoProxyCreator extends ProxyProcessorSupport
implements SmartInstantiationAwareBeanPostProcessor, BeanFactoryAware {
//其他代码略过...
/**
* 它是在bean实例化之前调用的,主要是针对切面类。
*/
@Override
public Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) throws BeansException {
Object cacheKey = getCacheKey(beanClass, beanName);

if (!StringUtils.hasLength(beanName) || !this.targetSourcedBeans.contains(beanName)) {
if (this.advisedBeans.containsKey(cacheKey)) {
return null;
}
//加载所有增强
if (isInfrastructureClass(beanClass) || shouldSkip(beanClass, beanName)) {
this.advisedBeans.put(cacheKey, Boolean.FALSE);
return null;
}
}

// Create proxy here if we have a custom TargetSource.
// Suppresses unnecessary default instantiation of the target bean:
// The TargetSource will handle target instances in a custom fashion.
TargetSource targetSource = getCustomTargetSource(beanClass, beanName);
if (targetSource != null) {
if (StringUtils.hasLength(beanName)) {
this.targetSourcedBeans.add(beanName);
}
Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(beanClass, beanName, targetSource);
Object proxy = createProxy(beanClass, beanName, specificInterceptors, targetSource);
this.proxyTypes.put(cacheKey, proxy.getClass());
return proxy;
}

return null;
}

@Override
public boolean postProcessAfterInstantiation(Object bean, String beanName) {
return true;
}

@Override
public PropertyValues postProcessPropertyValues(
PropertyValues pvs, PropertyDescriptor[] pds, Object bean, String beanName) {

return pvs;
}

@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) {
return bean;
}

// 这个方法是在bean实例化之后调用的,它是适用于所有需要被代理的类的
@Override
public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) throws BeansException {
if (bean != null) {
Object cacheKey = getCacheKey(bean.getClass(), beanName);
if (!this.earlyProxyReferences.contains(cacheKey)) {
return wrapIfNecessary(bean, beanName, cacheKey);
}
}
return bean;
}

/**
* 如果bean可以被代理,就把将它代理。
*/
protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
//如果已经处理过 直接返回
if (StringUtils.hasLength(beanName) && this.targetSourcedBeans.contains(beanName)) {
return bean;
}
//如果当前类是增强类
if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) {
return bean;
}
/*
* 如果是基础设施类(Pointcut、Advice、Advisor 等接口的实现类),或是应该跳过的类,
* 则不应该生成代理,此时直接返回 bean
*/
if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) {
this.advisedBeans.put(cacheKey, Boolean.FALSE);
return bean;
}

// 为目标 bean 查找合适的通知器
// 校验此类是否应该被代理,获取这个类的增强
Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
/*
* 若 specificInterceptors != null,即 specificInterceptors != DO_NOT_PROXY,
* 则为 bean 生成代理对象,否则直接返回 bean
*/
if (specificInterceptors != DO_NOT_PROXY) {
this.advisedBeans.put(cacheKey, Boolean.TRUE);
//创建代理
Object proxy = createProxy(
bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
this.proxyTypes.put(cacheKey, proxy.getClass());
//返回代理对象,此时 IOC 容器输入 bean,得到 proxy。这时beanName 对应的 bean 是代理对象,而非原始的 bean
return proxy;
}

this.advisedBeans.put(cacheKey, Boolean.FALSE);
return bean;
}

protected Object createProxy(Class<?> beanClass, @Nullable String beanName,
@Nullable Object[] specificInterceptors, TargetSource targetSource) {

if (this.beanFactory instanceof ConfigurableListableBeanFactory) {
AutoProxyUtils.exposeTargetClass((ConfigurableListableBeanFactory) this.beanFactory, beanName, beanClass);
}

ProxyFactory proxyFactory = new ProxyFactory();
//使用proxyFactory对象copy当前类中的相关属性
proxyFactory.copyFrom(this);
//判断是否使用Cglib动态代理
if (!proxyFactory.isProxyTargetClass()) {
//如果配置开启使用则直接设置开启
if (shouldProxyTargetClass(beanClass, beanName)) {
proxyFactory.setProxyTargetClass(true);
}
else {
//如果没有配置开启则判断bean是否有合适的接口使用JDK的动态代理(JDK动态代理必须是带有接口的类,如果类没有实现任何接口则只能使用Cglib动态代理)
evaluateProxyInterfaces(beanClass, proxyFactory);
}
}
//添加所有增强
Advisor[] advisors = buildAdvisors(beanName, specificInterceptors);
proxyFactory.addAdvisors(advisors);
//设置要代理的类
proxyFactory.setTargetSource(targetSource);
//TODO:Spring的一个扩展点,默认实现为空。留给我们在需要对代理进行特殊操作的时候实现
customizeProxyFactory(proxyFactory);

proxyFactory.setFrozen(this.freezeProxy);
if (advisorsPreFiltered()) {
proxyFactory.setPreFiltered(true);
}
//使用代理工厂获取代理对象
return proxyFactory.getProxy(getProxyClassLoader());
}
}

创建代理对象主要是wrapIfNecessary方法,首先校验的是是否需要生成代理对象,不需要就直接返回。

查找适合的通知器

在织入之前需要为目标 bean 查找合适的通知器

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
//获取增强类
@Override
@Nullable
protected Object[] getAdvicesAndAdvisorsForBean(
Class<?> beanClass, String beanName, @Nullable TargetSource targetSource) {
// 查找合适的通知器
List<Advisor> advisors = findEligibleAdvisors(beanClass, beanName);
if (advisors.isEmpty()) {
return DO_NOT_PROXY;
}
return advisors.toArray();
}

protected List<Advisor> findEligibleAdvisors(Class<?> beanClass, String beanName) {
//获取容器中的所有增强
List<Advisor> candidateAdvisors = findCandidateAdvisors();
//筛选可应用在 beanClass 上的 Advisor,通过 ClassFilter 和 MethodMatcher
//验证beanClass是否该被代理,如果是则返回适用于这个bean的增强(为当前的Bean匹配自己的增强)
List<Advisor> eligibleAdvisors = findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName);
extendAdvisors(eligibleAdvisors);
if (!eligibleAdvisors.isEmpty()) {
eligibleAdvisors = sortAdvisors(eligibleAdvisors);
}
return eligibleAdvisors;
}

Spring 先查询出所有的通知器,然后再调用 findAdvisorsThatCanApply 对通知器进行筛选。

查找通知器

AbstractAdvisorAutoProxyCreator 中的 findCandidateAdvisors 是个空壳方法,所有逻辑封装在了一个 BeanFactoryAdvisorRetrievalHelper 的 findAdvisorBeans 方法中。

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
public List<Advisor> findAdvisorBeans() {
// Determine list of advisor bean names, if not cached already.
String[] advisorNames = this.cachedAdvisorBeanNames;
if (advisorNames == null) {
// 从容器中查找 Advisor 类型 bean 的名称
advisorNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(
this.beanFactory, Advisor.class, true, false);
// 设置缓存
this.cachedAdvisorBeanNames = advisorNames;
}
if (advisorNames.length == 0) {
return new ArrayList<>();
}

List<Advisor> advisors = new ArrayList<>();
// 遍历 advisorNames
for (String name : advisorNames) {
if (isEligibleBean(name)) {
// 忽略正在创建中的 advisor bean
if (this.beanFactory.isCurrentlyInCreation(name)) {
if (logger.isDebugEnabled()) {
logger.debug("Skipping currently created advisor '" + name + "'");
}
}
else {
try {
//调用 getBean 方法从容器中获取名称为 name 的 bean,并将 bean 添加到 advisors 中
advisors.add(this.beanFactory.getBean(name, Advisor.class));
}
catch (BeanCreationException ex) {
Throwable rootCause = ex.getMostSpecificCause();
if (rootCause instanceof BeanCurrentlyInCreationException) {
BeanCreationException bce = (BeanCreationException) rootCause;
String bceBeanName = bce.getBeanName();
if (bceBeanName != null && this.beanFactory.isCurrentlyInCreation(bceBeanName)) {
if (logger.isDebugEnabled()) {
logger.debug("Skipping advisor '" + name +
"' with dependency on currently created bean: " + ex.getMessage());
}
// Ignore: indicates a reference back to the bean we're trying to advise.
// We want to find advisors other than the currently created bean itself.
continue;
}
}
throw ex;
}
}
}
}
return advisors;
}

从容器中查找所有类型为 Advisor 的 bean 对应的名称,,遍历 advisorNames,并从容器中获取对应的 bean。

findCandidateAdvisors

20200516223128

1
2
3
4
5
6
7
8
9
10
11
protected List<Advisor> findCandidateAdvisors() {
// Add all the Spring advisors found according to superclass rules.
// 调用父类的方法加载配置文件中的AOP声明(注解与XML都存在的时候)
List<Advisor> advisors = super.findCandidateAdvisors();
// Build Advisors for all AspectJ aspects in the bean factory.
if (this.aspectJAdvisorsBuilder != null) {
// 获取所有的增强的代码实现
advisors.addAll(this.aspectJAdvisorsBuilder.buildAspectJAdvisors());
}
return advisors;
}
buildAspectJAdvisors

@Aspect 注解的解析过程。

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
/**
* 1.获取所有beanName
* 2.找出所有标记Aspect注解的类
* 3.对标记Aspect的类提取增强器
*/
public List<Advisor> buildAspectJAdvisors() {
//所有Aspect类的名称集合
List<String> aspectNames = this.aspectBeanNames;

if (aspectNames == null) {
synchronized (this) {
aspectNames = this.aspectBeanNames;
//单例模式:双重检查
if (aspectNames == null) {
List<Advisor> advisors = new ArrayList<>();
aspectNames = new ArrayList<>();
//获取所有Bean名称
String[] beanNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(
this.beanFactory, Object.class, true, false);
for (String beanName : beanNames) {
//判断是否符合条件,比如说有时会排除一些类,不让这些类注入进Spring
if (!isEligibleBean(beanName)) {
continue;
}
// We must be careful not to instantiate beans eagerly as in this case they
// would be cached by the Spring container but would not have been weaved.
Class<?> beanType = this.beanFactory.getType(beanName);
if (beanType == null) {
continue;
}
//判断Bean的Class上是否标识@Aspect注解
if (this.advisorFactory.isAspect(beanType)) {
aspectNames.add(beanName);
AspectMetadata amd = new AspectMetadata(beanType, beanName);
if (amd.getAjType().getPerClause().getKind() == PerClauseKind.SINGLETON) {
MetadataAwareAspectInstanceFactory factory =
new BeanFactoryAspectInstanceFactory(this.beanFactory, beanName);
List<Advisor> classAdvisors = this.advisorFactory.getAdvisors(factory);
if (this.beanFactory.isSingleton(beanName)) {
//将解析的Bean名称及类上的增强缓存起来,每个Bean只解析一次
this.advisorsCache.put(beanName, classAdvisors);
}
else {
this.aspectFactoryCache.put(beanName, factory);
}
advisors.addAll(classAdvisors);
}
else {
// Per target or per this.
if (this.beanFactory.isSingleton(beanName)) {
throw new IllegalArgumentException("Bean with name '" + beanName +
"' is a singleton, but aspect instantiation model is not singleton");
}
MetadataAwareAspectInstanceFactory factory =
new PrototypeAspectInstanceFactory(this.beanFactory, beanName);
this.aspectFactoryCache.put(beanName, factory);
//advisorFactory.getAdvisors方法会从@Aspect标识的类上获取@Before,@Pointcut等注解的信息及其标识的方法的信息,生成增强
advisors.addAll(this.advisorFactory.getAdvisors(factory));
}
}
}
this.aspectBeanNames = aspectNames;
return advisors;
}
}
}

if (aspectNames.isEmpty()) {
return Collections.emptyList();
}
List<Advisor> advisors = new ArrayList<>();
//从缓存中获取当前Bean的切面实例,如果不为空,则指明当前Bean的Class标识了@Aspect,且有切面方法
for (String aspectName : aspectNames) {
List<Advisor> cachedAdvisors = this.advisorsCache.get(aspectName);
if (cachedAdvisors != null) {
advisors.addAll(cachedAdvisors);
}
else {
MetadataAwareAspectInstanceFactory factory = this.aspectFactoryCache.get(aspectName);
advisors.addAll(this.advisorFactory.getAdvisors(factory));
}
}
return advisors;
}

过程是:

获取容器中所有 bean 的名称(beanName)
遍历上一步获取到的 bean 名称数组,并获取当前 beanName 对应的 bean 类型(beanType)
根据 beanType 判断当前 bean 是否是一个的 Aspect 注解类,若不是则不做任何处理
调用 advisorFactory.getAdvisors 获取通知器

其中的this.advisorFactory.getAdvisors(factory)方法解释:

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
// 各个增强器的获取方法的实现
@Override
public List<Advisor> getAdvisors(MetadataAwareAspectInstanceFactory aspectInstanceFactory) {
//获取所有Aspect类、类名称、并校验
Class<?> aspectClass = aspectInstanceFactory.getAspectMetadata().getAspectClass();
String aspectName = aspectInstanceFactory.getAspectMetadata().getAspectName();
validate(aspectClass);

// We need to wrap the MetadataAwareAspectInstanceFactory with a decorator
// so that it will only instantiate once.
MetadataAwareAspectInstanceFactory lazySingletonAspectInstanceFactory =
new LazySingletonAspectInstanceFactoryDecorator(aspectInstanceFactory);

List<Advisor> advisors = new ArrayList<>();
//获取这个类所有的增强方法
for (Method method : getAdvisorMethods(aspectClass)) {
//生成增强实例 调用生成增强实例的方法
Advisor advisor = getAdvisor(method, lazySingletonAspectInstanceFactory, advisors.size(), aspectName);
if (advisor != null) {
advisors.add(advisor);
}
}

// 如果需要增强且配置了延迟增强则在第一个位置添加同步实例化增强方法
if (!advisors.isEmpty() && lazySingletonAspectInstanceFactory.getAspectMetadata().isLazilyInstantiated()) {
Advisor instantiationAdvisor = new SyntheticInstantiationAdvisor(lazySingletonAspectInstanceFactory);
advisors.add(0, instantiationAdvisor);
}
// 获取属性中配置DeclareParents注解的增强v
for (Field field : aspectClass.getDeclaredFields()) {
Advisor advisor = getDeclareParentsAdvisor(field);
if (advisor != null) {
advisors.add(advisor);
}
}

return advisors;
}

//生成增强实例的方法
@Override
@Nullable
public Advisor getAdvisor(Method candidateAdviceMethod, MetadataAwareAspectInstanceFactory aspectInstanceFactory,
int declarationOrderInAspect, String aspectName) {
//再次校验类的合法性
validate(aspectInstanceFactory.getAspectMetadata().getAspectClass());
//获取切点,切点表达式的包装类里面包含这些东西:execution(public * com.hu.service....(..))
AspectJExpressionPointcut expressionPointcut = getPointcut(
candidateAdviceMethod, aspectInstanceFactory.getAspectMetadata().getAspectClass());
if (expressionPointcut == null) {
return null;
}
//根据方法、切点、AOP实例工厂、类名、序号生成切面实例
return new InstantiationModelAwarePointcutAdvisorImpl(expressionPointcut, candidateAdviceMethod,
this, aspectInstanceFactory, declarationOrderInAspect, aspectName);
}

getAdvisor 方法包含两个主要步骤,一个是获取 AspectJ 表达式切点,另一个是创建 Advisor 实现类。

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
//获取 AspectJ 表达式切点
@Nullable
private AspectJExpressionPointcut getPointcut(Method candidateAdviceMethod, Class<?> candidateAspectClass) {
//查询方法上的切面注解,根据注解生成相应类型的AspectJAnnotation,在调用AspectJAnnotation的构造函数的同时 根据注解value或pointcut属性得到切点表达式,有argNames则设置参数名称
AspectJAnnotation<?> aspectJAnnotation =
AbstractAspectJAdvisorFactory.findAspectJAnnotationOnMethod(candidateAdviceMethod);
//过滤那些不含@Before, @Around, @After, @AfterReturning, @AfterThrowing注解的方法
if (aspectJAnnotation == null) {
return null;
}
//生成带表达式的切面切入点,设置其切入点表达式
AspectJExpressionPointcut ajexp =
new AspectJExpressionPointcut(candidateAspectClass, new String[0], new Class<?>[0]);
ajexp.setExpression(aspectJAnnotation.getPointcutExpression());
if (this.beanFactory != null) {
ajexp.setBeanFactory(this.beanFactory);
}
return ajexp;
}
@Nullable
protected static AspectJAnnotation<?> findAspectJAnnotationOnMethod(Method method) {
for (Class<?> clazz : ASPECTJ_ANNOTATION_CLASSES) {
AspectJAnnotation<?> foundAnnotation = findAnnotation(method, (Class<Annotation>) clazz);
if (foundAnnotation != null) {
return foundAnnotation;
}
}
return null;
}

//创建 Advisor 实现类
public InstantiationModelAwarePointcutAdvisorImpl(AspectJExpressionPointcut declaredPointcut,
Method aspectJAdviceMethod, AspectJAdvisorFactory aspectJAdvisorFactory,
MetadataAwareAspectInstanceFactory aspectInstanceFactory, int declarationOrder, String aspectName) {

this.declaredPointcut = declaredPointcut;
this.declaringClass = aspectJAdviceMethod.getDeclaringClass();
this.methodName = aspectJAdviceMethod.getName();
this.parameterTypes = aspectJAdviceMethod.getParameterTypes();
this.aspectJAdviceMethod = aspectJAdviceMethod;
this.aspectJAdvisorFactory = aspectJAdvisorFactory;
this.aspectInstanceFactory = aspectInstanceFactory;
this.declarationOrder = declarationOrder;
this.aspectName = aspectName;

if (aspectInstanceFactory.getAspectMetadata().isLazilyInstantiated()) {
// Static part of the pointcut is a lazy type.
Pointcut preInstantiationPointcut = Pointcuts.union(
aspectInstanceFactory.getAspectMetadata().getPerClausePointcut(), this.declaredPointcut);

// Make it dynamic: must mutate from pre-instantiation to post-instantiation state.
// If it's not a dynamic pointcut, it may be optimized out
// by the Spring AOP infrastructure after the first evaluation.
this.pointcut = new PerTargetInstantiationModelPointcut(
this.declaredPointcut, preInstantiationPointcut, aspectInstanceFactory);
this.lazy = true;
}
else {
// A singleton aspect.
this.pointcut = this.declaredPointcut;
this.lazy = false;
//初始化对应的增强器*
this.instantiatedAdvice = instantiateAdvice(this.declaredPointcut);
}
}

private Advice instantiateAdvice(AspectJExpressionPointcut pointcut) {
Advice advice = this.aspectJAdvisorFactory.getAdvice(this.aspectJAdviceMethod, pointcut,
this.aspectInstanceFactory, this.declarationOrder, this.aspectName);
return (advice != null ? advice : EMPTY_ADVICE);
}
@Override
@Nullable
public Advice getAdvice(Method candidateAdviceMethod, AspectJExpressionPointcut expressionPointcut,
MetadataAwareAspectInstanceFactory aspectInstanceFactory, int declarationOrder, String aspectName) {

Class<?> candidateAspectClass = aspectInstanceFactory.getAspectMetadata().getAspectClass();
validate(candidateAspectClass);
// 获取 Advice 注解
AspectJAnnotation<?> aspectJAnnotation =
AbstractAspectJAdvisorFactory.findAspectJAnnotationOnMethod(candidateAdviceMethod);
if (aspectJAnnotation == null) {
return null;
}

if (!isAspect(candidateAspectClass)) {
throw new AopConfigException("Advice must be declared inside an aspect type: " +
"Offending method '" + candidateAdviceMethod + "' in class [" +
candidateAspectClass.getName() + "]");
}

if (logger.isDebugEnabled()) {
logger.debug("Found AspectJ method: " + candidateAdviceMethod);
}

AbstractAspectJAdvice springAdvice;
// 按照注解类型生成相应的 Advice 实现类
switch (aspectJAnnotation.getAnnotationType()) {
case AtPointcut:
if (logger.isDebugEnabled()) {
logger.debug("Processing pointcut '" + candidateAdviceMethod.getName() + "'");
}
return null;
case AtAround:
springAdvice = new AspectJAroundAdvice(
candidateAdviceMethod, expressionPointcut, aspectInstanceFactory);
break;
case AtBefore:
springAdvice = new AspectJMethodBeforeAdvice(
candidateAdviceMethod, expressionPointcut, aspectInstanceFactory);
break;
case AtAfter:
springAdvice = new AspectJAfterAdvice(
candidateAdviceMethod, expressionPointcut, aspectInstanceFactory);
break;
case AtAfterReturning:
springAdvice = new AspectJAfterReturningAdvice(
candidateAdviceMethod, expressionPointcut, aspectInstanceFactory);
AfterReturning afterReturningAnnotation = (AfterReturning) aspectJAnnotation.getAnnotation();
if (StringUtils.hasText(afterReturningAnnotation.returning())) {
springAdvice.setReturningName(afterReturningAnnotation.returning());
}
break;
case AtAfterThrowing:
springAdvice = new AspectJAfterThrowingAdvice(
candidateAdviceMethod, expressionPointcut, aspectInstanceFactory);
AfterThrowing afterThrowingAnnotation = (AfterThrowing) aspectJAnnotation.getAnnotation();
if (StringUtils.hasText(afterThrowingAnnotation.throwing())) {
springAdvice.setThrowingName(afterThrowingAnnotation.throwing());
}
break;
default:
throw new UnsupportedOperationException(
"Unsupported advice type on method: " + candidateAdviceMethod);
}
//设置通知方法所属的类
springAdvice.setAspectName(aspectName);
//设置通知的序号,同一个类中有多个切面注解标识的方法时,按上方说的排序规则来排序,其序号就是此方法在列表中的序号,第一个就是0
springAdvice.setDeclarationOrder(declarationOrder);
//获取通知方法的所有参数
String[] argNames = this.parameterNameDiscoverer.getParameterNames(candidateAdviceMethod);
//将通知方法上的参数设置到通知中
if (argNames != null) {
springAdvice.setArgumentNamesFromStringArray(argNames);
}
//校验方法参数并绑定
springAdvice.calculateArgumentBindings();

return springAdvice;
}

获取通知器(getAdvisors)整个过程的逻辑,如下:

从目标 bean 中获取不包含 Pointcut 注解的方法列表
遍历上一步获取的方法列表,并调用 getAdvisor 获取当前方法对应的 Advisor
创建 AspectJExpressionPointcut 对象,并从方法中的注解中获取表达式,最后设置到切点对象中
创建 Advisor 实现类对象 InstantiationModelAwarePointcutAdvisorImpl
调用 instantiateAdvice 方法构建通知
调用 getAdvice 方法,并根据注解类型创建相应的通知

例如其中一个的实现:

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
public class AspectJAfterAdvice extends AbstractAspectJAdvice
implements MethodInterceptor, AfterAdvice, Serializable {

public AspectJAfterAdvice(
Method aspectJBeforeAdviceMethod, AspectJExpressionPointcut pointcut, AspectInstanceFactory aif) {

super(aspectJBeforeAdviceMethod, pointcut, aif);
}


@Override
public Object invoke(MethodInvocation mi) throws Throwable {
try {
return mi.proceed();
}
finally {
invokeAdviceMethod(getJoinPointMatch(), null, null);
}
}

@Override
public boolean isBeforeAdvice() {
return false;
}

@Override
public boolean isAfterAdvice() {
return true;
}

}

protected Object invokeAdviceMethod(
@Nullable JoinPointMatch jpMatch, @Nullable Object returnValue, @Nullable Throwable ex)
throws Throwable {
// 调用通知方法,并向其传递参数
return invokeAdviceMethodWithGivenArgs(argBinding(getJoinPoint(), jpMatch, returnValue, ex));
}

protected Object invokeAdviceMethodWithGivenArgs(Object[] args) throws Throwable {
Object[] actualArgs = args;
if (this.aspectJAdviceMethod.getParameterCount() == 0) {
actualArgs = null;
}
try {
ReflectionUtils.makeAccessible(this.aspectJAdviceMethod);
// TODO AopUtils.invokeJoinpointUsingReflection
// 反射调用通知方法
return this.aspectJAdviceMethod.invoke(this.aspectInstanceFactory.getAspectInstance(), actualArgs);
}
catch (IllegalArgumentException ex) {
throw new AopInvocationException("Mismatch on arguments to advice method [" +
this.aspectJAdviceMethod + "]; pointcut expression [" +
this.pointcut.getPointcutExpression() + "]", ex);
}
catch (InvocationTargetException ex) {
throw ex.getTargetException();
}
}

查找合适的通知器

findAdvisorsThatCanApply

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
protected List<Advisor> findAdvisorsThatCanApply(
List<Advisor> candidateAdvisors, Class<?> beanClass, String beanName) {

ProxyCreationContext.setCurrentProxiedBeanName(beanName);
try {
return AopUtils.findAdvisorsThatCanApply(candidateAdvisors, beanClass);
}
finally {
ProxyCreationContext.setCurrentProxiedBeanName(null);
}
}

public static List<Advisor> findAdvisorsThatCanApply(List<Advisor> candidateAdvisors, Class<?> clazz) {
if (candidateAdvisors.isEmpty()) {
return candidateAdvisors;
}
List<Advisor> eligibleAdvisors = new ArrayList<>();
// 引介增强
for (Advisor candidate : candidateAdvisors) {
// 筛选 IntroductionAdvisor 类型的通知器
if (candidate instanceof IntroductionAdvisor && canApply(candidate, clazz)) {
eligibleAdvisors.add(candidate);
}
}
boolean hasIntroductions = !eligibleAdvisors.isEmpty();
for (Advisor candidate : candidateAdvisors) {
if (candidate instanceof IntroductionAdvisor) {
// already processed
continue;
}
//对普通bean的处理
if (canApply(candidate, clazz, hasIntroductions)) {
eligibleAdvisors.add(candidate);
}
}
return eligibleAdvisors;
}

//引介增强与普通bean的处理最后都是进的同一个方法,只不过是引介增强的第三个参数默认使用的false
public static boolean canApply(Advisor advisor, Class<?> targetClass, boolean hasIntroductions) {
//如果存在排除的配置
if (advisor instanceof IntroductionAdvisor) {
/*
* 从通知器中获取类型过滤器 ClassFilter,并调用 matchers 方法进行匹配。
* ClassFilter 接口的实现类 AspectJExpressionPointcut 为例,该类的
* 匹配工作由 AspectJ 表达式解析器负责,具体匹配细节这个就没法分析了,我
* AspectJ 表达式的工作流程不是很熟
*/
return ((IntroductionAdvisor) advisor).getClassFilter().matches(targetClass);
}
else if (advisor instanceof PointcutAdvisor) {
PointcutAdvisor pca = (PointcutAdvisor) advisor;
// 对于普通类型的通知器,这里继续调用重载方法进行筛选
return canApply(pca.getPointcut(), targetClass, hasIntroductions);
}
else {
// It doesn't have a pointcut so we assume it applies.
return true;
}
}

public static boolean canApply(Pointcut pc, Class<?> targetClass, boolean hasIntroductions) {
Assert.notNull(pc, "Pointcut must not be null");
//切点上是否存在排除类的配置
if (!pc.getClassFilter().matches(targetClass)) {
return false;
}
//验证注解的作用域是否可以作用于方法上
MethodMatcher methodMatcher = pc.getMethodMatcher();
if (methodMatcher == MethodMatcher.TRUE) {
// No need to iterate the methods if we're matching any method anyway...
return true;
}

IntroductionAwareMethodMatcher introductionAwareMethodMatcher = null;
if (methodMatcher instanceof IntroductionAwareMethodMatcher) {
introductionAwareMethodMatcher = (IntroductionAwareMethodMatcher) methodMatcher;
}
/*
* 查找当前类及其父类(以及父类的父类等等)所实现的接口,由于接口中的方法是 public,
* 所以当前类可以继承其父类,和父类的父类中所有的接口方法
*/
Set<Class<?>> classes = new LinkedHashSet<>();
if (!Proxy.isProxyClass(targetClass)) {
classes.add(ClassUtils.getUserClass(targetClass));
}
classes.addAll(ClassUtils.getAllInterfacesForClassAsSet(targetClass));

for (Class<?> clazz : classes) {
Method[] methods = ReflectionUtils.getAllDeclaredMethods(clazz);
//获取类所实现的所有接口和所有类层级的方法,循环验证
for (Method method : methods) {
if (introductionAwareMethodMatcher != null ?
introductionAwareMethodMatcher.matches(method, targetClass, hasIntroductions) :
methodMatcher.matches(method, targetClass)) {
return true;
}
}
}

return false;
}

创建代理对象

通知器选好了,接下来就要通过代理的方式将通知器(Advisor)所持有的通知(Advice)织入到 bean 的某些方法前后。

先创建 AopProxy 对象,然后再调用该对象的 getProxy 方法创建实际的代理类。

1
2
3
4
5
6
7
public interface AopProxy {

/** 创建代理对象 */
Object getProxy();

Object getProxy(ClassLoader classLoader);
}

Spring 在为目标 bean 创建代理的过程中根据 bean 是否实现接口,以及一些其他配置来决定使用 AopProxy 何种实现类为目标 bean 创建代理对象。

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
protected Object createProxy(Class<?> beanClass, @Nullable String beanName,
@Nullable Object[] specificInterceptors, TargetSource targetSource) {

if (this.beanFactory instanceof ConfigurableListableBeanFactory) {
AutoProxyUtils.exposeTargetClass((ConfigurableListableBeanFactory) this.beanFactory, beanName, beanClass);
}

ProxyFactory proxyFactory = new ProxyFactory();
//使用proxyFactory对象copy当前类中的相关属性
proxyFactory.copyFrom(this);
//判断是否使用Cglib动态代理
if (!proxyFactory.isProxyTargetClass()) {
//如果配置开启使用则直接设置开启
if (shouldProxyTargetClass(beanClass, beanName)) {
proxyFactory.setProxyTargetClass(true);
}
else {
//如果没有配置开启则判断bean是否有合适的接口使用JDK的动态代理(JDK动态代理必须是带有接口的类,如果类没有实现任何接口则只能使用Cglib动态代理)
evaluateProxyInterfaces(beanClass, proxyFactory);
}
}
//添加所有增强
Advisor[] advisors = buildAdvisors(beanName, specificInterceptors);
proxyFactory.addAdvisors(advisors);
//设置要代理的类
proxyFactory.setTargetSource(targetSource);
//TODO:Spring的一个扩展点,默认实现为空。留给我们在需要对代理进行特殊操作的时候实现
customizeProxyFactory(proxyFactory);

proxyFactory.setFrozen(this.freezeProxy);
if (advisorsPreFiltered()) {
proxyFactory.setPreFiltered(true);
}
//使用代理工厂获取代理对象
return proxyFactory.getProxy(getProxyClassLoader());
}

getProxy 这里有两个方法调用,一个是调用 createAopProxy 创建 AopProxy 实现类对象,然后再调用 AopProxy 实现类对象中的 getProxy 创建代理对象。

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
//创建代理对象。
protected final synchronized AopProxy createAopProxy() {
if (!this.active) {
activate();
}
return getAopProxyFactory().createAopProxy(this);
}

@Override
public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
/*
* config.isOptimize() - 是否需要优化
* config.isProxyTargetClass() - 检测 proxyTargetClass 的值,前面的代码会设置这个值
* hasNoUserSuppliedProxyInterfaces(config) 目标 bean 是否实现了接口
*/
if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) {
Class<?> targetClass = config.getTargetClass();
if (targetClass == null) {
throw new AopConfigException("TargetSource cannot determine target class: " +
"Either an interface or a target is required for proxy creation.");
}
if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
// 创建 JDK 动态代理
return new JdkDynamicAopProxy(config);
}
// 创建 CGLIB 代理,ObjenesisCglibAopProxy 继承自 CglibAopProxy
return new ObjenesisCglibAopProxy(config);
}
else {
// 创建 JDK 动态代理
return new JdkDynamicAopProxy(config);
}
}

JdkDynamicAopProxy 最终调用 Proxy.newProxyInstance 方法创建代理对象。

执行过程

得到了 bean 的代理对象,通知也以合适的方式插在了目标方法的前后。当目标方法被多个通知匹配到时,Spring 通过引入拦截器链来保证每个通知的正常执行。

对于 JDK 动态代理,代理逻辑封装在 InvocationHandler 接口实现类的 invoke 方法中。JdkDynamicAopProxy 实现了 InvocationHandler 接口。

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
@Override
@Nullable
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
MethodInvocation invocation;
Object oldProxy = null;
boolean setProxyContext = false;

TargetSource targetSource = this.advised.targetSource;
Object target = null;

try {
//equals方法处理
if (!this.equalsDefined && AopUtils.isEqualsMethod(method)) {
// The target does not implement the equals(Object) method itself.
return equals(args[0]);
}
//hash处理
else if (!this.hashCodeDefined && AopUtils.isHashCodeMethod(method)) {
// The target does not implement the hashCode() method itself.
return hashCode();
}
else if (method.getDeclaringClass() == DecoratingProxy.class) {
// There is only getDecoratedClass() declared -> dispatch to proxy config.
return AopProxyUtils.ultimateTargetClass(this.advised);
}
else if (!this.advised.opaque && method.getDeclaringClass().isInterface() &&
method.getDeclaringClass().isAssignableFrom(Advised.class)) {
// Service invocations on ProxyConfig with the proxy config...
return AopUtils.invokeJoinpointUsingReflection(this.advised, method, args);
}

Object retVal;
//如果配置内部方法调用的增强 // 如果 expose-proxy 属性为 true,则暴露代理对象
if (this.advised.exposeProxy) {
// 向 AopContext 中设置代理对象
oldProxy = AopContext.setCurrentProxy(proxy);
setProxyContext = true;
}

// Get as late as possible to minimize the time we "own" the target,
// in case it comes from a pool.
target = targetSource.getTarget();
Class<?> targetClass = (target != null ? target.getClass() : null);

// 获取当前方法的拦截器链
List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);

// 如果拦截器链为空,则直接执行目标方法
if (chain.isEmpty()) {
//如果没有拦截器直接调用切点方法
Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);
retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse);
}
else {
// 创建一个方法调用器,并将拦截器链传入其中
invocation = new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain);
//执行拦截器链
retVal = invocation.proceed();
}

// 获取方法返回值类型
Class<?> returnType = method.getReturnType();
//返回结果
if (retVal != null && retVal == target &&
returnType != Object.class && returnType.isInstance(proxy) &&
!RawTargetAccess.class.isAssignableFrom(method.getDeclaringClass())) {
// 如果方法返回值为 this,即 return this; 则将代理对象 proxy 赋值给 retVal
retVal = proxy;
}
// 如果返回值类型为基础类型,比如 int,long 等,当返回值为 null,抛出异常
else if (retVal == null && returnType != Void.TYPE && returnType.isPrimitive()) {
throw new AopInvocationException(
"Null return value from advice does not match primitive return type for: " + method);
}
return retVal;
}
finally {
if (target != null && !targetSource.isStatic()) {
// Must have come from TargetSource.
targetSource.releaseTarget(target);
}
if (setProxyContext) {
// Restore old proxy.
AopContext.setCurrentProxy(oldProxy);
}
}
}

流程如下:

检测 expose-proxy 是否为 true,若为 true,则暴露代理对象
获取适合当前方法的拦截器
如果拦截器链为空,则直接通过反射执行目标方法
若拦截器链不为空,则创建方法调用 ReflectiveMethodInvocation 对象
调用 ReflectiveMethodInvocation 对象的 proceed() 方法启动拦截器链
处理返回值,并返回该值

获取所有的拦截器

拦截器是指用于对目标方法的调用进行拦截的一种工具,如通知前置通知。这里说明其中一种通知连接器类型。

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
public class MethodBeforeAdviceInterceptor implements MethodInterceptor, BeforeAdvice, Serializable {

//前置通知
private final MethodBeforeAdvice advice;


/**
* Create a new MethodBeforeAdviceInterceptor for the given advice.
* @param advice the MethodBeforeAdvice to wrap
*/
public MethodBeforeAdviceInterceptor(MethodBeforeAdvice advice) {
Assert.notNull(advice, "Advice must not be null");
this.advice = advice;
}


@Override
public Object invoke(MethodInvocation mi) throws Throwable {
// 执行前置通知逻辑
this.advice.before(mi.getMethod(), mi.getArguments(), mi.getThis());
// 通过 MethodInvocation 调用下一个拦截器,若所有拦截器均执行完,则调用目标方法
return mi.proceed();
}

}

如何获取拦截器

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
//获取拦截器
public List<Object> getInterceptorsAndDynamicInterceptionAdvice(Method method, @Nullable Class<?> targetClass) {
MethodCacheKey cacheKey = new MethodCacheKey(method);
// 从缓存中获取
List<Object> cached = this.methodCache.get(cacheKey);
// 缓存未命中,则进行下一步处理
if (cached == null) {
// 获取所有的拦截器
cached = this.advisorChainFactory.getInterceptorsAndDynamicInterceptionAdvice(
this, method, targetClass);
// 存入缓存
this.methodCache.put(cacheKey, cached);
}
return cached;
}

@Override
public List<Object> getInterceptorsAndDynamicInterceptionAdvice(
Advised config, Method method, @Nullable Class<?> targetClass) {

// This is somewhat tricky... We have to process introductions first,
// but we need to preserve order in the ultimate list.
List<Object> interceptorList = new ArrayList<Object>(config.getAdvisors().length);
Class<?> actualClass = (targetClass != null ? targetClass : method.getDeclaringClass());
boolean hasIntroductions = hasMatchingIntroductions(config, actualClass);
// registry 为 DefaultAdvisorAdapterRegistry 类型
AdvisorAdapterRegistry registry = GlobalAdvisorAdapterRegistry.getInstance();
// 遍历通知器列表
for (Advisor advisor : config.getAdvisors()) {
if (advisor instanceof PointcutAdvisor) {
// Add it conditionally.
PointcutAdvisor pointcutAdvisor = (PointcutAdvisor) advisor;
//调用 ClassFilter 对 bean 类型进行匹配,无法匹配则说明当前通知器不适合应用在当前 bean 上
if (config.isPreFiltered() || pointcutAdvisor.getPointcut().getClassFilter().matches(actualClass)) {
// 将 advisor 中的 advice 转成相应的拦截器
MethodMatcher mm = pointcutAdvisor.getPointcut().getMethodMatcher();
// 通过方法匹配器对目标方法进行匹配
if (MethodMatchers.matches(mm, method, actualClass, hasIntroductions)) {
MethodInterceptor[] interceptors = registry.getInterceptors(advisor);
// 若 isRuntime 返回 true,则表明 MethodMatcher 要在运行时做一些检测
if (mm.isRuntime()) {
// Creating a new object instance in the getInterceptors() method
// isn't a problem as we normally cache created chains.
for (MethodInterceptor interceptor : interceptors) {
interceptorList.add(new InterceptorAndDynamicMethodMatcher(interceptor, mm));
}
}
else {
interceptorList.addAll(Arrays.asList(interceptors));
}
}
}
}
else if (advisor instanceof IntroductionAdvisor) {
IntroductionAdvisor ia = (IntroductionAdvisor) advisor;
// IntroductionAdvisor 类型的通知器,仅需进行类级别的匹配即可
if (config.isPreFiltered() || ia.getClassFilter().matches(actualClass)) {
Interceptor[] interceptors = registry.getInterceptors(advisor);
interceptorList.addAll(Arrays.asList(interceptors));
}
}
else {
Interceptor[] interceptors = registry.getInterceptors(advisor);
interceptorList.addAll(Arrays.asList(interceptors));
}
}

return interceptorList;
}

@Override
public MethodInterceptor[] getInterceptors(Advisor advisor) throws UnknownAdviceTypeException {
List<MethodInterceptor> interceptors = new ArrayList<>(3);
Advice advice = advisor.getAdvice();
// 若 advice 是 MethodInterceptor 类型的,直接添加到 interceptors 中即可。
if (advice instanceof MethodInterceptor) {
interceptors.add((MethodInterceptor) advice);
}
//对于 AspectJMethodBeforeAdvice 等类型的通知,由于没有实现 MethodInterceptor 接口,所以这里需要通过适配器进行转换
for (AdvisorAdapter adapter : this.adapters) {
if (adapter.supportsAdvice(advice)) {
interceptors.add(adapter.getInterceptor(advisor));
}
}
if (interceptors.isEmpty()) {
throw new UnknownAdviceTypeException(advisor.getAdvice());
}
return interceptors.toArray(new MethodInterceptor[0]);
}

启动拦截器链

ReflectiveMethodInvocation 贯穿于拦截器链执行的始终,是核心。 proceed 方法用于启动启动拦截器链。

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
//所有的增强都在这个拦截器
@Override
@Nullable
public Object proceed() throws Throwable {
// 执行完所有的增强后执行切点方法后
if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) {
// 执行目标方法
return invokeJoinpoint();
}
//获取下一个要执行的拦截器
Object interceptorOrInterceptionAdvice =
this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);
if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher) {
// Evaluate dynamic method matcher here: static part will already have
// been evaluated and found to match.
InterceptorAndDynamicMethodMatcher dm =
(InterceptorAndDynamicMethodMatcher) interceptorOrInterceptionAdvice;
if (dm.methodMatcher.matches(this.method, this.targetClass, this.arguments)) {
// 调用拦截器逻辑
return dm.interceptor.invoke(this);
}
else {
// 如果匹配失败,则忽略当前的拦截器
return proceed();
}
}
else {
// 调用拦截器逻辑,并传递 ReflectiveMethodInvocation 对象
return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this);
}
}

proceed 根据 currentInterceptorIndex 来确定当前应执行哪个拦截器,并在调用拦截器的 invoke 方法时,将自己作为参数传给该方法。

执行目标方法

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
@Nullable
protected Object invokeJoinpoint() throws Throwable {
return AopUtils.invokeJoinpointUsingReflection(this.target, this.method, this.arguments);
}

public static Object invokeJoinpointUsingReflection(@Nullable Object target, Method method, Object[] args)
throws Throwable {

// 使用反射来调用方法。
try {
ReflectionUtils.makeAccessible(method);
return method.invoke(target, args);
}
catch (InvocationTargetException ex) {
// Invoked method threw a checked exception.
// We must rethrow it. The client won't see the interceptor.
throw ex.getTargetException();
}
catch (IllegalArgumentException ex) {
throw new AopInvocationException("AOP configuration seems to be invalid: tried calling method [" +
method + "] on target [" + target + "]", ex);
}
catch (IllegalAccessException ex) {
throw new AopInvocationException("Could not access method [" + method + "]", ex);
}
}

Spring源码分析—延迟加载

发表于 2019-01-27 | 分类于 spring

lazy-init 延迟加载机制分析

对于被修饰为lazy-init的bean Spring 容器初始化阶段不会进⾏ init 并且依赖注⼊,当第⼀次进⾏getBean时候才进⾏初始化并依赖注⼊对于⾮懒加载的bean,getBean的时候会从缓存⾥头获取,因为容器初始化阶段 Bean 已经初始化完成并缓存了起来。

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
public void preInstantiateSingletons() throws BeansException {
if (logger.isDebugEnabled()) {
logger.debug("Pre-instantiating singletons in " + this);
}
//所有bean的名字
List<String> beanNames = new ArrayList<>(this.beanDefinitionNames);

// 触发所有非延迟加载单例beans的初始化,主要步骤为调用getBean
for (String beanName : beanNames) {
//合并父BeanDefinition
RootBeanDefinition bd = getMergedLocalBeanDefinition(beanName);
// 不是抽象类、是单例的且不是懒加载的
if (!bd.isAbstract() && bd.isSingleton() && !bd.isLazyInit()) {
// 处理 FactoryBean
if (isFactoryBean(beanName)) {
//如果是FactoryBean则加上&
Object bean = getBean(FACTORY_BEAN_PREFIX + beanName);
if (bean instanceof FactoryBean) {
final FactoryBean<?> factory = (FactoryBean<?>) bean;
// 判断当前 FactoryBean 是否是 SmartFactoryBean 的实现
boolean isEagerInit;
if (System.getSecurityManager() != null && factory instanceof SmartFactoryBean) {
isEagerInit = AccessController.doPrivileged((PrivilegedAction<Boolean>)
((SmartFactoryBean<?>) factory)::isEagerInit,
getAccessControlContext());
}
else {
isEagerInit = (factory instanceof SmartFactoryBean &&
((SmartFactoryBean<?>) factory).isEagerInit());
}
if (isEagerInit) {
getBean(beanName);
}
}
}
else {
// 不是FactoryBean的直接使用此方法进行初始化
getBean(beanName);
}
}
}

// 如果bean实现了 SmartInitializingSingleton 接口的,那么在这里得到回调
for (String beanName : beanNames) {
Object singletonInstance = getSingleton(beanName);
if (singletonInstance instanceof SmartInitializingSingleton) {
final SmartInitializingSingleton smartSingleton = (SmartInitializingSingleton) singletonInstance;
if (System.getSecurityManager() != null) {
AccessController.doPrivileged((PrivilegedAction<Object>) () -> {
smartSingleton.afterSingletonsInstantiated();
return null;
}, getAccessControlContext());
}
else {
smartSingleton.afterSingletonsInstantiated();
}
}
}
}

Spring源码分析——资源加载分析

发表于 2019-01-25 | 分类于 spring

统一资源Resource

org.springframework.core.io.Resource 为 Spring 框架所有资源的抽象和访问接口,它继承 org.springframework.core.io.InputStreamSource接口。
20200510155119

FileSystemResource :对 java.io.File 类型资源的封装,只要是跟 File 打交道的,基本上与 FileSystemResource 也可以打交道。支持文件和 URL 的形式,实现 WritableResource 接口,且从 Spring Framework 5.0 开始,FileSystemResource 使用 NIO2 API进行读/写交互。
ByteArrayResource :对字节数组提供的数据的封装。如果通过 InputStream 形式访问该类型的资源,该实现会根据字节数组的数据构造一个相应的 ByteArrayInputStream。
UrlResource :对 java.net.URL类型资源的封装。内部委派 URL 进行具体的资源操作。
ClassPathResource :class path 类型资源的实现。使用给定的 ClassLoader 或者给定的 Class 来加载资源。
InputStreamResource :将给定的 InputStream 作为一种资源的 Resource 的实现类。

org.springframework.core.io.AbstractResource ,为 Resource 接口的默认抽象实现。它实现了 Resource 接口的大部分的公共实现.

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
158
159
160
161
162
163
public abstract class AbstractResource implements Resource {

/**
* 判断文件是否存在,若判断过程产生异常(因为会调用SecurityManager来判断),就关闭对应的流
*/
@Override
public boolean exists() {
try {
// 基于 File 进行判断
return getFile().exists();
} catch (IOException ex) {
// Fall back to stream existence: can we open the stream?
// 基于 InputStream 进行判断
try {
InputStream is = getInputStream();
is.close();
return true;
} catch (Throwable isEx) {
return false;
}
}
}

/**
* 直接返回true,表示可读
*/
@Override
public boolean isReadable() {
return true;
}

/**
* 直接返回 false,表示未被打开
*/
@Override
public boolean isOpen() {
return false;
}

/**
* 直接返回false,表示不为 File
*/
@Override
public boolean isFile() {
return false;
}

/**
* 抛出 FileNotFoundException 异常,交给子类实现
*/
@Override
public URL getURL() throws IOException {
throw new FileNotFoundException(getDescription() + " cannot be resolved to URL");

}

/**
* 基于 getURL() 返回的 URL 构建 URI
*/
@Override
public URI getURI() throws IOException {
URL url = getURL();
try {
return ResourceUtils.toURI(url);
} catch (URISyntaxException ex) {
throw new NestedIOException("Invalid URI [" + url + "]", ex);
}
}

/**
* 抛出 FileNotFoundException 异常,交给子类实现
*/
@Override
public File getFile() throws IOException {
throw new FileNotFoundException(getDescription() + " cannot be resolved to absolute file path");
}

/**
* 根据 getInputStream() 的返回结果构建 ReadableByteChannel
*/
@Override
public ReadableByteChannel readableChannel() throws IOException {
return Channels.newChannel(getInputStream());
}

/**
* 获取资源的长度
* <p>
* 这个资源内容长度实际就是资源的字节长度,通过全部读取一遍来判断
*/
@Override
public long contentLength() throws IOException {
InputStream is = getInputStream();
try {
long size = 0;
byte[] buf = new byte[255]; // 每次最多读取 255 字节
int read;
while ((read = is.read(buf)) != -1) {
size += read;
}
return size;
} finally {
try {
is.close();
} catch (IOException ex) {
}
}
}

/**
* 返回资源最后的修改时间
*/
@Override
public long lastModified() throws IOException {
long lastModified = getFileForLastModifiedCheck().lastModified();
if (lastModified == 0L) {
throw new FileNotFoundException(getDescription() +
" cannot be resolved in the file system for resolving its last-modified timestamp");
}
return lastModified;
}

protected File getFileForLastModifiedCheck() throws IOException {
return getFile();
}

/**
* 抛出 FileNotFoundException 异常,交给子类实现
*/
@Override
public Resource createRelative(String relativePath) throws IOException {
throw new FileNotFoundException("Cannot create a relative resource for " + getDescription());
}

/**
* 获取资源名称,默认返回 null ,交给子类实现
*/
@Override
@Nullable
public String getFilename() {
return null;
}

/**
* 返回资源的描述
*/
@Override
public String toString() {
return getDescription();
}

@Override
public boolean equals(Object obj) {
return (obj == this ||
(obj instanceof Resource && ((Resource) obj).getDescription().equals(getDescription())));
}

@Override
public int hashCode() {
return getDescription().hashCode();
}

}
上一页1…91011…25下一页
初晨

初晨

永远不要说你知道本质,更别说真相了。

249 日志
46 分类
109 标签
近期文章
  • WebSocket、Socket、TCP、HTTP区别
  • Springboot项目的接口防刷
  • 深入理解Volatile关键字及其实现原理
  • 使用vscode搭建个人笔记环境
  • HBase介绍安装与操作
© 2018 — 2020 Copyright
由 Hexo 强力驱动
|
主题 — NexT.Gemini v5.1.4