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

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


  • 首页

  • 归档

  • 搜索

Redis系列-我用1W字总结了所有的点,确定不了解一下吗?

发表于 2021-07-31

一、Redis简介

说到Redis, 我们的第一反应就是“快”。

下面来看几个问题:

  1. Redis是单线程的吗?

其实这么说不完全正确,我们知道Redis是一个Key-Value的非关系型数据库,我们所理解的Redis单线程主要是指网络IO和K-V的读写是由一个主线程来完成的。但Redis的其他功能,比如说持久化、异步删除、集群数据同步,其实是开启了额外的线程来完成的。
2. Redis单线程为什么还能这么快?

因为Redis是基于内存的,所有的运算都是内存级别的,而且单线程避免了多线程的切换性能耗损问题。
3. Redis单线程如何处理那么多并发客户端连接?

这里就要扯到NIO多路复用模型了,由于本篇主要是Redis的学习记录,这里等Netty的时候再详细学习。

二、Redis的基本数据结构在大厂是怎么用的?

Redis支持5种数据结构:String、Hash、List、Set、ZSet

PS: String类型可能是大部分人经常用做缓存,但是Redis不仅仅是只能做缓存而已,它的其他数据结构也很强大,甚至在早些年,Redis就支撑起了新浪微博的大部分核心功能,权重不亚于我们的开发语言。

下面的例子建立在各位对Redis的API熟悉的情况下

  1. 1
    arduino复制代码String
    • 单值缓存(可以实现常规的缓存)
set key value


get key
* **对象缓存** (可以实现分布式Session, Key为sessionId, Value为用户对象)


mset user\_name boom user\_age 25


mget user\_name user\_age


![image.png](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/2d0df040487416b7efb966801d97d7097102ce78a2f79f69e4f4dfe897fdf9f0)
* **计数器**(可以做限流, 阅读数,点赞数,分布式唯一ID等等)


自增:incr num


自减:decr num


加N: incrby num N


减N: decrby num N


![image.png](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/55e0899a0fa0e1935086b62e78e8bbd3965f36ad87cbb59737345d081923f64c)


![image.png](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/bb34feb74db69aee5bdaa82a609e89aa11bc981cd624cea39deaa1a507b98bbc)
* **分布式锁**


setnx key value (返回1获取锁成功,0失败)


![image.png](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/615cb3758fde672a03170efc4535ce61454ae6760082e974fff4768288bd0077)
  1. 1
    复制代码Hash
    • 对象缓存 (它相比于String, 更适合存放对象)
hmset user name boom age 25


hmget user name age


![image.png](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/a1323eb094df2e4ad9f3321514f6dbe9adacdb51ed2e649f9b0b165ffb66ea38)


**很经典的一个例子: 购物车**


添加商品到购物车: hset cart\_用户id 商品id 购买数量


增加购物车商品数量: hincrby cart\_用户id 商品id 要增加的数量


获取商品总数: hlen cart\_用户id


删除商品 hdel cart\_用户id


获取购物车所有的商品: hgetall cart\_用户id


![image.png](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/1abfb4a48e2072d8c1af28687cd309eedcd4b02fd6f5851ae0c38c775e521eeb)
  1. 1
    复制代码List
    • 实现队列(FIFO)
Lpush(左边进) + Rpop(右边出)
* **实现栈(FILO)**


Lpush(左边进) + Lpop(左边出)
* **实现阻塞队列**


Lpush(左边进) + BRpop(相比于Rpop会阻塞)
* **很经典的一个例子:公众号、微博消息推送**


我关注了公众号A


    1. 公众号A发了篇文章: Lpush msg\_公众号A的id 文章id
    2. 我要查看公众号A最新的消息(一页四个消息): Lrange msg\_公众号A的id 0 4![image.png](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/35efb7fe4e11fca0213c55280d9f0375a7afbfb9a559156d81a6819da9758efd)
  1. 1
    javascript复制代码Set
    • 集合操作
![image.png](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/4218231649726f76f75dc7358a6627e58d8a22d4fdb61361c5b0d60c52399721)求交集: sinter set1 set2 set3 结果为 {c}

求并集: sunion set1 set2 set3 结果为 {a,b,c,d,e}

求差集: sdiff set1 set2 set3 结果为 {a}

* **很经典的一个例子:微博的关注模型**


![image.png](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/a50920fa96a89a82d3aef9c7280799c604bd39d6416f7fd79f106aa427c6dd3f)


    1. boom关注了a,b,c: sadd boom a b c
    2. Tom关注了b,c,d: sadd tom b c d
    3. b关注了tom: sadd b tom
    4. boom和tom的共同关注的人: sinter boom tom 得到c
    5. boom关注的人也关注了tom: sismember tom b
    6. boom可能认识的人: sdiff tom b
  1. 1
    复制代码ZSet
    • ZSet常用操作
    1. 往有序集合key中加入带分值元素: ZADD key score member [[score member]…]
    2. 从有序集合key中删除元素: ZREM key member [member…]
    3. 返回有序集合key中元素member的分值: ZSCORE key member
    4. 为有序集合key中元素member的分值加上increment: ZINCRBY key increment member
    5. 返回有序集合key中元素个数: ZCARD key
    6. 正序获取有序集合key从start下标到stop下标的元素: ZRANGE key start stop [WITHSCORES]
    7. 倒序获取有序集合key从start下标到stop下标的元素: ZREVRANGE key start stop [WITHSCORES]
    + **ZSet集合操作**并集计算: ZUNIONSTORE destkey numkeys key [key ...]


交集计算: ZINTERSTORE destkey numkeys key [key…]


    + **很经典的一个例子:微博热搜排行榜**


    ![image.png](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/23f0a387dd0e850544bf1a13c49bc8ef7c3db8fc2c59f04182d65588f38facc2)


        1. 点击新闻: ZINCRBY hotNews\_20210728 基金大跌
        2. 展示当日排行前十: ZREVRANGE hotNews\_20210728 0 9 WITHSCORES
        3. 七日搜索榜单计算: ZUNIONSTORE hotNews\_20210722\_20210728 7 hotNews\_20210722 hotNews\_20210723... hotNews\_20210728
        4. 展示七日排行前十: ZREVRANGE hotNews\_20210722\_20210728 0 9 WITHSCORES

三、Redis持久化

Redis最大的特点就是基于内存的,既然是基于内存,那么当Redis服务挂掉或者服务器宕机,数据则会丢失,所以Redis不可避免的得对数据持久化做一些处理,像MySQL,MQ数据就保留在磁盘上,那Redis同理。

Redis有3种持久化方式: RDB、AOF、混合持久化

RDB

1
2
3
4
5
6
7
8
9
10
11
perl复制代码在默认情况下, Redis 将内存数据库快照保存在名字为 dump.rdb 的二进制文件中。
你可以对 Redis 进行设置, 让它在“ N 秒内数据集至少有 M 个改动”这一条件被满足时,自动保存一次数据集。
比如说, 设置`save 60 1000`会让 Redis 在满足“ 60 秒内有至少有 1000 个键被改动”这一条件时, 自动保存一次数据集。
关闭RDB只需要将所有的save保存策略注释掉即可

还可以手动执行命令生成RDB快照,客户端执行命令save或bgsave可以生成dump.rdb文件,
每次命令执行都会将所有redis内存快照到一个新的rdb文件里,并覆盖原有rdb快照文件。

save是同步命令,bgsave是异步命令,bgsave会从Redis主进程fork(fork()是linux函数)出一个子进程专门用来生成rdb快照文件

Redis默认是使用的bgsave

save与bgsave对比:

image.png

AOF

1
2
3
4
5
6
7
8
9
10
11
12
13
14
markdown复制代码AOF 持久化: 将修改的每一条指令记录进文件appendonly.aof中

你可以通过修改配置文件来打开 AOF 功能:`appendonly yes`

每当 Redis 执行一个改变数据集的命令时(比如 SET), 这个命令就会被追加到 AOF 文件的末尾。
这样的话, 当 Redis 重新启动时, 程序就可以通过重新执行 AOF 文件中的命令来达到重建数据集的目的。

你可以配置 Redis 多久才将数据 fsync 到磁盘一次。
有三个选项:
1. appendfsync always:每次有新命令追加到 AOF 文件时就执行一次 fsync ,非常慢,也非常安全。
2. appendfsync everysec:每秒 fsync 一次,足够快(和使用 RDB 持久化差不多),并且在故障时只会丢失 1 秒钟的数据。
3. appendfsync no:从不 fsync ,将数据交给操作系统来处理。更快,也更不安全的选择。

推荐(并且也是默认)的措施为每秒 fsync 一次, 这种 fsync 策略可以兼顾速度和安全性。

AOF和RDB对比:

image.png

混合持久化(加强版的AOF)

1
2
3
4
5
6
7
8
9
10
11
12
bash复制代码重启 Redis 时,我们很少使用 RDB来恢复数据,因为会丢失大量数据。
我们通常使用 AOF 日志重放,但是重放 AOF 日志性能相对 RDB来说要慢很多,这样在 Redis 实例很大的情况下,启动需要花费很长的时间。
Redis 为了解决这个问题,带来了一个新的持久化方式——混合持久化。

通过如下配置可以开启混合持久化:`aof-use-rdb-preamble yes`

如果开启了混合持久化,AOF在重写时,不再是单纯将内存数据转换为RESP命令写入AOF文件,
而是将重写这一刻之前的内存做RDB快照处理,并且将RDB快照内容和增量的AOF修改内存数据的命令存在一起,都写入新的AOF文件,
新的文件一开始不叫appendonly.aof,等到重写完新的AOF文件才会进行改名,原子的覆盖原有的AOF文件,完成新旧两个AOF文件的替换。

于是在 Redis 重启的时候,可以先加载 RDB 的内容,然后再重放增量 AOF 日志就可以完全替代之前的 AOF 全量文件重放,
因此重启效率大幅得到提升。

混合持久化AOF文件结构:

image.png

四、Redis主从、哨兵、集群分析

主从架构

image.png

Redis主从工作原理

  1. 如果你为master配置了一个slave,不管这个slave是否是第一次连接上Master,它都会发送一个SYNC命令(redis2.8版本之前的命令) master请求复制数据。(从2.8版本开始,redis改用可以支持部分数据复制的命令PSYNC去master同步数据)
  2. master收到SYNC命令后,会在后台进行数据持久化通过bgsave生成最新的rdb快照文件,持久化期间,master会继续接收客户端的请求,它会把这些可能修改数据集的请求缓存在内存中。当持久化进行完毕以后,master会把这份rdb文件数据集发送给slave,slave会把接收到的数据进行持久化生成rdb,然后再加载到内存中。然后,master再将之前缓存在内存中的命令发送给slave。
  3. 当master与slave之间的连接由于某些原因而断开时,slave能够自动重连Master,如果master收到了多个slave并发连接请求,它只会进行一次持久化,而不是一个连接一次,然后再把这一份持久化的数据发送给多个并发连接的slave。
  4. 当master和slave断开重连后,一般都会对整份数据进行复制。但从redis2.8版本开始,master和slave断开重连后支持部分复制。

数据部分复制

master会在其内存中创建一个复制数据用的缓存队列,缓存最近一段时间的数据,master和它所有的slave都维护了复制的数据下标offset和master的进程id,因此,当网络连接断开后,slave会请求master继续进行未完成的复制,从所记录的数据下标开始。如果master进程id变化了,或者从节点数据下标offset太旧,已经不在master的缓存队列里了,那么将会进行一次全量数据的复制。

  • Redis主从全量复制:

image.png

  • Redis主从部分复制:

image.png

哨兵架构

image.png

sentinel哨兵是特殊的Redis服务,不提供读写服务,主要用来监控Redis实例节点。

哨兵架构下client端第一次从哨兵找出redis的主节点,后续就直接访问redis的主节点,不会每次都通过sentinel代理访问redis的主节点,当redis的主节点发生变化,哨兵会第一时间感知到,并且将新的redis主节点通知给client端(这里面redis的client端一般都实现了订阅功能,订阅sentinel发布的节点变动消息)

在redis3.0以前的版本要实现集群一般是借助哨兵sentinel工具来监控master节点的状态,如果master节点异常,则会做主从切换,将某一台slave作为master,哨兵的配置略微复杂,并且性能和高可用性等各方面表现一般,特别是在主从切换的瞬间存在访问瞬断的情况,而且哨兵模式只有一个主节点对外提供服务,没法支持很高的并发,且单个主节点内存也不宜设置得过大,否则会导致持久化文件过大,影响数据恢复或主从同步的效率

哨兵Leader选举流程

当一个master服务器被某sentinel视为客观下线状态后,该sentinel会与其他sentinel协商选出sentinel的leader进行故障转移工作。每个发现master服务器进入客观下线的sentinel都可以要求其他sentinel选自己为sentinel的leader,选举是先到先得。同时每个sentinel每次选举都会自增配置纪元(选举周期),每个纪元中只会选择一个sentinel的leader。如果所有超过一半的sentinel选举某sentinel作为leader。之后该sentinel进行故障转移操作,从存活的slave中选举出新的master,这个选举过程跟集群的master选举很类似。

哨兵集群只有一个哨兵节点,redis的主从也能正常运行以及选举master,如果master挂了,那唯一的那个哨兵节点就是哨兵leader了,可以正常选举新master。

不过为了高可用一般都推荐至少部署三个哨兵节点。为什么推荐奇数个哨兵节点原理跟集群奇数个master节点类似。

集群架构

image.png

redis集群是一个由多个主从节点群组成的分布式服务器群,它具有复制、高可用和分片特性。Redis集群不需要sentinel哨兵也能完成节点移除和故障转移的功能。需要将每个节点设置成集群模式,这种集群模式没有中心节点,可水平扩展,据官方文档称可以线性扩展到上万个节点(官方推荐不超过1000个节点)。redis集群的性能和高可用性均优于之前版本的哨兵模式,且集群配置非常简单

Redis集群原理分析

Redis Cluster 将所有数据划分为 16384 个 slots(槽位),每个节点负责其中一部分槽位。槽位的信息存储于每个节点中。

当 Redis Cluster 的客户端来连接集群时,它也会得到一份集群的槽位配置信息并将其缓存在客户端本地。这样当客户端要查找某个 key 时,可以直接定位到目标节点。同时因为槽位的信息可能会存在客户端与服务器不一致的情况,还需要纠正机制来实现槽位信息的校验调整。

槽位定位算法

Cluster默认会对 key 值使用 CRC16 算法进行 hash 得到一个整数值,然后用这个整数值对 16384 进行取模来得到具体槽位: HASH_SLOT = CRC16(key) mod 16384

跳转重定位

当客户端向一个错误的节点发出了指令,该节点会发现指令的 key 所在的槽位并不归自己管理,这时它会向客户端发送一个特殊的跳转指令携带目标操作的节点地址,告诉客户端去连这个节点去获取数据。客户端收到指令后除了跳转到正确的节点上去操作,还会同步更新纠正本地的槽位映射表缓存,后续所有 key 将使用新的槽位映射表。

image.png

Redis集群节点间的通信机制

redis cluster节点间采取gossip协议进行通信

  • 维护集群的元数据有两种方式:集中式和gossip
  • 集中式: 优点在于元数据的更新和读取,时效性非常好,一旦元数据出现变更立即就会更新到集中式的存储中,其他节点读取的时候立即就可以立即感知到;不足在于所有的元数据的更新压力全部集中在一个地方,可能导致元数据的存储压力。
  • gossip:

116144643385.gif
gossip协议包含多种消息,包括ping,pong,meet,fail等等。

ping:每个节点都会频繁给其他节点发送ping,其中包含自己的状态还有自己维护的集群元数据,互相通过ping交换元数据;

pong: 返回ping和meet,包含自己的状态和其他信息,也可以用于信息广播和更新;

fail: 某个节点判断另一个节点fail之后,就发送fail给其他节点,通知其他节点,指定的节点宕机了。

meet:某个节点发送meet给新加入的节点,让新节点加入集群中,然后新节点就会开始与其他节点进行通信,不需要发送形成网络的所需的所有CLUSTER MEET命令。发送CLUSTER MEET消息以便每个节点能够达到其他每个节点只需通过一条已知的节点链就够了。由于在心跳包中会交换gossip信息,将会创建节点间缺失的链接。

gossip协议的优点在于元数据的更新比较分散,不是集中在一个地方,更新请求会陆陆续续,打到所有节点上去更新,有一定的延时,降低了压力;缺点在于元数据更新有延时可能导致集群的一些操作会有一些滞后。

ps: 10000端口 , 每个节点都有一个专门用于节点间通信的端口,就是自己提供服务的端口号+10000,比如7001,那么用于节点间通信的就是17001端口。 每个节点每隔一段时间都会往另外几个节点发送ping消息,同时其他几点接收到ping消息之后返回pong消息。

网络抖动: 真实世界的机房网络往往并不是风平浪静的,它们经常会发生各种各样的小问题。比如网络抖动就是非常常见的一种现象,突然之间部分连接变得不可访问,然后很快又恢复正常。为解决这种问题,Redis Cluster 提供了一种选项cluster-node-timeout,表示当某个节点持续 timeout 的时间失联时,才可以认定该节点出现故障,需要进行主从切换。如果没有这个选项,网络抖动会导致主从频繁切换 (数据的重新复制)。

Redis集群选举原理分析

当slave发现自己的master变为FAIL状态时,便尝试进行Failover,以期成为新的master。由于挂掉的master可能会有多个slave,从而存在多个slave竞争成为master节点的过程, 其过程如下:

  1. slave发现自己的master变为FAIL
  2. 将自己记录的集群currentEpoch加1,并广播FAILOVER_AUTH_REQUEST 信息
  3. 其他节点收到该信息,只有master响应,判断请求者的合法性,并发送FAILOVER_AUTH_ACK,对每一个epoch只发送一次ack
  4. 尝试failover的slave收集master返回的FAILOVER_AUTH_ACK
  5. slave收到超过半数master的ack后变成新Master(这里解释了集群为什么至少需要三个主节点,如果只有两个,当其中一个挂了,只剩一个主节点是不能选举成功的)
  6. slave广播Pong消息通知其他集群节点。

ps: 从节点并不是在主节点一进入 FAIL 状态就马上尝试发起选举,而是有一定延迟,一定的延迟确保我们等待FAIL状态在集群中传播,slave如果立即尝试选举,其它masters或许尚未意识到FAIL状态,可能会拒绝投票。

集群是否完整才能对外提供服务?

1
2
perl复制代码当redis.conf的配置cluster-require-full-coverage为no时,表示当负责一个插槽的主库下线且没有相应的从库进行故障恢复时,
集群仍然可用,如果为yes则集群不可用。

Redis集群为什么至少需要三个master节点,并且推荐节点数为奇数?

1
2
3
4
5
6
复制代码因为新master的选举需要大于半数的集群master节点同意才能选举成功,如果只有两个master节点,当其中一个挂了,
是达不到选举新master的条件的。

奇数个master节点可以在满足选举该条件的基础上节省一个节点,比如三个master节点和四个master节点的集群相比,
大家如果都挂了一个master节点都能选举新master节点,如果都挂了两个master节点都没法选举新master节点了,
所以奇数的master节点更多的是从节省机器资源角度出发说的。

五、Redis缓存淘汰算法

缓存淘汰策略

因为Redis是基于内存的,内存的空间是非常宝贵的,所以数据不可能无上限的存储,必定会存在一个淘汰策略定期删除一些key。

那Redis的缓存淘汰策略有两种:定时删除和惰性删除

PS: 如果Redis采用的是主从架构,那么以上两种淘汰策略是基于Redis主库的,每当主库触发了淘汰策略,即会在AOF文件写入一个del命令,而从库的淘汰策略是基于主从同步来完成的。

  • 定时删除:

Redis将每个设置了过期时间的key放到一个独立的Hash表中,默认每秒定时遍历这个hash而不是整个Redis内存空间,并且Redis不会遍历所有的key,而是采用一种贪心策略。步骤如下:

1、从过期key字典中,随机找20个key。

2、删除20个key中过期的key。

3、如果2中过期的key超过1/4,则重复第一步。

如果有大量的key在同一时间段内过期,就会造成数据库的集中访问,就是缓存雪崩!

  • 惰性删除:

因为定时删除会漏掉一部分已过期的key而没有被删除,所以Redis引入一个惰性删除来删除那些漏掉了的key。

客户端访问的时候,会对这个key的过期时间进行检查,如果过期了就立即删除。

内存淘汰机制

思考一下,如果定期删除漏掉了大量的key, 且我们后面也没有访问这些key, 没有触发惰性删除,那么内存中会残留大量垃圾key, 直到某一个时刻,Redis内存总会被填满,此时Redis会触发他的内存淘汰机制。

Redis配置文件中可以设置maxmemory,内存的最大使用量,到达限度时会执行内存淘汰机制。没有配置时,默认为no-eviction

Redis中的内存淘汰机制:

image.png

六、从Redis底层搞懂它的渐进式Rehash

在Redis中,键值对(Key-Value)存储方式是由字典(Dict)保存的,而字典底层是通过哈希表来实现的。通过哈希表中的节点保存字典中的键值对。我们知道当HashMap中由于Hash冲突(负载因子)超过某个阈值时,出于链表性能的考虑,会进行Resize的操作。Redis也一样

在Redis的具体实现中,使用了一种叫做 渐进式哈希(Rehash) 的机制来提高字典的缩放效率,避免 rehash 对服务器性能造成影响,假如Redis中有大量的key, 如果一次性对全部的数据进行Rehash, 可能会导致Redis在一段时间内停止服务。

Redis中hash表的结点如下:

image.png

在Redis中,哈希表扩容需要将 哈希表0 里面的所有键值对 rehash 到 哈希表1 里面, 但是, 这个 rehash 动作并不是一次性完成的, 而是分多次、渐进式地完成的。

渐进式rehash的详细步骤

  1. 为 哈希表1 分配空间,且空间大小为哈希表0的两倍, 让字典同时持有 哈希表0 和 哈希表1 两个哈希表。
  2. 在字典中维持一个索引计数器变量 rehashidx(即哈希表的下标) , 并将它的值设置为 0 , 表示 rehash 工作正式开始。
  3. 在 rehash 进行期间, 每次对字典执行CRUD操作时, 程序除了执行指定的操作以外, 还会顺带将 哈希表0 在 rehashidx 索引上的所有键值对 rehash 到 哈希表1 , 当 rehash 工作完成之后, 程序将 rehashidx 属性的值+1。
  4. 随着字典操作的不断执行, 最终在某个时间点上, 哈希表0 的所有键值对都会被 rehash 至 哈希表1 , 这时程序将 rehashidx 属性的值设为 -1 , 表示 rehash 操作已完成。

渐进式rehash期间的CRUD操作

因为在进行渐进式 rehash 的过程中, 字典会同时使用 哈希表0 和 哈希表1 两个哈希表, 所以在渐进式 rehash 进行期间, 字典的CRUD操作会在两个哈希表上进行, 比如要在字典里面查找一个键的话, 程序会先在 哈希表0 里面进行查找, 如果没找到的话, 就会继续到 哈希表1 里面进行查找, 诸如此类。

另外, 在渐进式 rehash 执行期间, 新添加到字典的键值对一律会被保存到 哈希表1 里面, 而 哈希表0则不再进行任何添加操作: 这一措施保证了 哈希表0 包含的键值对数量会只减不增, 并随着 rehash 操作的执行而最终变成空表。

渐进式rehash带来的问题

渐进式rehash避免了redis阻塞,可以说非常完美,但是由于在rehash时,需要分配一个新的hash表,在rehash期间,同时有两个hash表在使用,会使得redis内存使用量瞬间突增,如果当前Redis结点的内存占用量达到maxmemory, 会触发内存淘汰机制,导致大量的Key被驱逐。

七、BitMap如何解决上亿日活的统计问题?

什么是 BitMap

BitMap,即位图,其实也就是 byte 数组,用二进制表示,只有 0 和 1 两个数字。我们知道8bit=1Byte,所以bitmap本身会极大的节省储存空间。

如图所示:

image.png

BitMap 有啥用?

假设某平台的用户数上亿,现在需要要统计日活、周活、月活

  1. 用MySQL实现,虽然用MySQL是能实现的,但是为了实现一个统计功能,对MySQL来说将会是一次灾难性的打击。
  2. 用Redis的自增,用户登录,我就+1,但是这样统计日活、周活的时候,会出现重复的情况。
  3. 在涉及到大数据统计的时候,不妨想想BitMap, 它就是为大数据统计而生。

BitMap统计日活

为了统计今日登录的用户数,我们建立了一个bitmap,每一位标识一个用户ID(假设是1-1亿,空间使用量1亿/8/1024/1024=11MB, 仅仅只要11MB就能统计1亿用户的日活,且速度相当之快)。每次用户登录时会执行一次setbit key user_id 1。将bitmap中对应位置的值置为1,时间复杂度是O(1)。执行bitcount key 统计bitmap结果有多少个1(即活跃用户数)。

image.png


假设7月25日–7月31日用户登录情况如下: 左边表示日期,右边为登录的用户id

20210731:{100,101,102,105}

20210730:{101,102,103}

20210729:{101,102}

20210728:{100,101,102}

20210727:{100,101,102,103}

20210726:{101,102,103}

20210725:{100,101,102,105}

统计一周内连续登录用户:

bitop语法:bitop operation destkey key [key …], operation 可以是 AND 、 OR 、 NOT 、 XOR 这四种操作中的任意一种:

  • BITOP AND destkey key [key ...] ,对一个或多个 key 求逻辑并,并将结果保存到 destkey 。
  • BITOP OR destkey key [key ...] ,对一个或多个 key 求逻辑或,并将结果保存到 destkey 。
  • BITOP XOR destkey key [key ...] ,对一个或多个 key 求逻辑异或,并将结果保存到 destkey 。
  • BITOP NOT destkey key ,对给定 key 求逻辑非,并将结果保存到 destkey 。

除了 NOT 操作之外,其他操作都可以接受一个或多个 key 作为输入。    

返回值:保存到 destkey 的字符串的长度,和输入 key 中最长的字符串长度相等

描述:对一个或多个保存二进制位的字符串 key 进行位元操作,并将结果保存到 destkey 上。

即我们通过bitop对2021-07-25到2021-07-31进行逻辑并运算,结果放入result中,如下:最后得到七天内连续登录的用户数为2。

判断某个用户是否在七天内连续登录,只需要getbit result 用户id, 返回1,即七天内连续登录

image.png

统计周活:

通过bitop对2021-07-25到2021-07-31进行逻辑或运算,结果放入result中

bitop or result activity_20210725 activity_20210726 activity_20210727 activity_20210728 activity_20210729 activity_20210730 activity_20210731

然后bitcount result 即得到周活。

image.png

八、大名鼎鼎的Redis跳跃表

Redis 的 zset 是个复合结构, 是由一个 哈希表 和 skiplist 组成的, 其中 hash 用来保存 value 和 score 对应关系,skiplist 用来给 score 排序,在这里我们着重介绍 skiplist 的实现。

SkipList 跳跃表

因为zset需要高效的插入和删除,所以底层不适合使用数组实现, 数组插入删除的时间复杂度为O(n)。 需要使用链表, 链表的插入删除的时间复杂度为O(1), 当插入新元素时需要根据 score插入到链表合适的位置,保证链表的有序性, 高效的办法是通过二分查找去找到插入点。

那么问题就来了, 二分查找的对象必须是有序数组, 只有数组支持快速定位, 链表做不到该怎么办呢? 这时,跳跃表出场了。

image.png

如图所示, 采用了空间换时间的思想。跳跃表在链表的基础上加入了层级L0~L3的概念, Redis 的跳跃表共有 64 层, 可容纳 2642^{64}264 个元素.每个元素的层级是随机分配的,分配 L0 的概率是 100%,就是说每个元素至少会有一层.分配到 L1 的概率是 50%, 分配到 L2 的概率是 25%, 往上以此类推。

每个 kv 对应的结构为zslnode.kv 之间使用指针形成有序的双向链表.同一层的 kv 会使用指针串起来.每层元素的遍历都是从跳跃表的头指针 kv header 出发。

header 的结构也是 zslnode,当中 value 为 null, score 为 -1排在最前面, 为1排在最后面。

1
2
3
4
5
6
7
8
9
10
11
12
js复制代码struct zslnode{
string value;
double score;
zslnode*[] forwards; //多层连接的指针
zslnode* backward; //回溯指针
}

struct zsl{
zslnode* header; //跳跃表头指针
int maxLevel; //当前节点的最高层
map<String,zslnode*> ht; //hash 中的键值对
}

查找

介绍完 skiplist的数据结构后,我们来具体看下skiplist 是怎样快速定位元素的.

image.png

在上图中,查找元素 117,skiplist 会从 header 的顶层出发遍历搜索找到第一个比目标元素小的开始降一层,直到降到最底层找到 117这个节点, 搜索路径为:

  1. 比较 21, 比 21 大,往后面找
  2. 比较 37, 比 37大,比链表最大值小,从 37 的下面一层开始找
  3. 比较 71, 比 71 大,比链表最大值小,从 71 的下面一层开始找
  4. 比较 85, 比 85 大,从后面找
  5. 比较 117, 等于 117, 找到了节点。

整个查找过程算法的时间复杂度为O(log(n))O(log(n))O(log(n)).

插入

先确定该元素要占据的层数 K(采用丢硬币的方式,这完全是随机的)
然后在 Level 1 … Level K 各个层的链表都插入元素。

例子:插入 119, K = 2
image.png

如果 K 大于链表的层数,则要添加新的层。

例子:插入 119, K = 4
image.png

删除

在各个层中找到包含 x 的节点,使用标准的 删除链表中结点 的方法删除该节点。

例子:删除 71
image.png

九、MySQL和Redis双写、读写不一致问题

在大并发下,同时操作数据库与缓存会存在数据不一致性问题

双写不一致

image.png

读写并不一致

image.png

解决方案

  1. 对于并发几率很小的数据(如个人维度的订单数据、用户数据等),这种几乎不用考虑这个问题,很少会发生缓存不一致,可以给缓存数据加上过期时间,每隔一段时间触发读的主动更新即可。
  2. 就算并发很高,如果业务上能容忍短时间的缓存数据不一致(如商品名称,商品分类菜单等),缓存加上过期时间依然可以解决大部分业务对于缓存的要求。
  3. 如果不能容忍缓存数据不一致,可以通过加读写锁保证并发读写或写写的时候按顺序排好队,读读的时候相当于无锁
  4. 也可以用阿里开源的canal通过监听数据库的binlog日志及时的去修改缓存,但是引入了新的中间件,增加了系统的复杂度。

PS: canal的工作原理就是把自己伪装成MySQL slave,模拟MySQL slave的交互协议向MySQL Mater发送 dump协议,MySQL mater收到canal发送过来的dump请求,开始推送binary log给canal,然后canal解析binary log,再发送到存储目的地。

image.png

总结:以上我们针对的都是读多写少的情况加入缓存提高性能,如果写多读多的情况又不能容忍缓存数据不一致,那就没必要加缓存了,可以直接操作数据库。放入缓存的数据应该是对实时性、一致性要求不是很高的数据。

本文转载自: 掘金

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

开源低代码平台开发实践二:从 0 构建一个基于 ER 图的低

发表于 2021-07-31

前后端分离了!

第一次知道这个事情的时候,内心是困惑的。

前端都出去搞 SPA,SEO 们同意吗?

后来,SSR 来了。

他说:“SEO 们同意了!”

任何人的反对,都没用了,时代变了。

各种各样的 SPA 们都来了,还有穿着跟 SPA 们一样衣服的各种小程序们。

为他们做点什么吧?于是 rxModels 诞生了,作为一个不希望被抛弃的后端,它希望能以更便捷的方式服务前端。

顺便把如何设计制作也分享出来吧,说不定会有一些借鉴意义。即便有不合理的地方,也会有人友善的指出来。

保持开放,付出与接受会同时发生,是双向受益的一个过程。

rxModels 是什么?

一个款开源、通用、低代码后端。

使用 rxModels,只需要绘制 ER 图就可以定制一个开箱即用的后端。提供粒度精确到字段的权限管理功能,并对实例级别的权限管理提供表达式支持。

主要模块有:图形化的实体、关系管理界面( rx-models Client),通用JSON格式的数据操作接口服务( rx-models ),前端调用辅助 Hooks 库( rxmodels-swr )等。

rxModels 基于 TypeScript,NestJS,TypeORM 和 Antv x6 实现。

TypeScript 的强类型支持,可以把一些错误在编译时就解决掉了,IDE有了强类型的支持,可以自动引入依赖,提高了开发效率,节省了时间。

TypeScript 编译以后的目标执行码时JS,一种运行时解释语言,这个特性赋予了 rxModels 动态发布实体和热加载 指令 的能力。用户可以使用 指令 实现业务逻辑,扩展通用 JSON 数据接口。给 rxModels 增加了更多使用场景。

NestJS 有助于代码的组织,使其拥有一个良好的架构。

TypeORM 是一款轻量级 ORM 库,可以把对象模型映射到关系数据库。它能够 “分离实体定义”,传入 JSON 描述就可以构建数据库,并对数据库提供面向对象的查询支持。得益于这个特性,图形化的业务模型转换成数据库数据库模型,rxModels 仅需要少量代码就可以完成。

AntV X6 功能相对已经比较全面了,它支持在节点(node)里面嵌入 React组件,利用这个个性,使用它来绘制 ER 图,效果非常不错。如果后面有时间,可以再写一篇文章,介绍如何使用 AntV x6绘制 ER 图。

要想跟着本文,把这个项目一步步做出来,最好能够提前学习一下本节提到的技术栈。

rxModels 目标定位

主要为中小项目服务。

为什么不敢服务大项目?

真不敢,作者是业余程序员,没有大项目相关的任何经验。

梳理数据及数据映射

先看一下演示,从直观上知道项目的样子:rxModels演示 。

界面

元数据定义

元数据(Meta),用于描述业务实体模型的数据。一部分元数据转化成 TypeORM 实体定义,随之生成数据库;另一部分元数据业务模型是图形信息,比如实体的大小跟位置,关系的位置跟形状等。

需要转化成 TypeORM 实体定义的元数据有:

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
typescript复制代码import { ColumnMeta } from "./column-meta";

/**
* 实体类型枚举,目前仅支持普通实体跟枚举实体,
* 枚举实体类似语法糖,不映射到数据库,
* 枚举类型的字段映射到数据库是string类型
*/
export enum EntityType{
NORMAL = "Normal",
ENUM = "Enum",
}

/**
* 实体元数据
*/
export interface EntityMeta{
/** 唯一标识 */
uuid: string;

/** 实体名称 */
name: string;

/** 表名,如果tableName没有被设置,会把实体名转化成蛇形命名法,并以此当作表名 */
tableName?: string;

/** 实体类型 */
entityType?: EntityType|"";

/** 字段元数据列表 */
columns: ColumnMeta[];

/** 枚举值JSON,枚举类型实体使用,不参与数据库映射 */
enumValues?: any;
}
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
typescript复制代码
/**
* 字段类型,枚举,目前版本仅支持这些类型,后续可以扩展
*/
export enum ColumnType{

/** 数字类型 */
Number = 'Number',

/** 布尔类型 */
Boolean = 'Boolean',

/** 字符串类型 */
String = 'String',

/** 日期类型 */
Date = 'Date',

/** JSON类型 */
SimpleJson = 'simple-json',

/** 数组类型 */
SimpleArray = 'simple-array',

/** 枚举类型 */
Enum = 'Enum'
}

/**
* 字段元数据,基本跟 TypeORM Column 对应
*/
export interface ColumnMeta{

/** 唯一标识 */
uuid: string;

/** 字段名 */
name: string;

/** 字段类型 */
type: ColumnType;

/** 是否主键 */
primary?: boolean;

/** 是否自动生成 */
generated?: boolean;

/** 是否可空 */
nullable?: boolean;

/** 字段默认值 */
default?: any;

/** 是否唯一 */
unique?: boolean;

/** 是否是创建日期 */
createDate?: boolean;

/** 是否是更新日期 */
updateDate?: boolean;

/** 是否是删除日期,软删除功能使用 */
deleteDate?: boolean;

/**
* 是否可以在查询时被选择,如果这是为false,则查询时隐藏。
* 密码字段会使用它
*/
select?: boolean;

/** 长度 */
length?: string | number;

/** 当实体是枚举类型时使用 */
enumEnityUuid?:string;

/**
* ============以下属性跟TypeORM对应,但是尚未启用
*/
width?: number;
version?: boolean;
readonly?: boolean;
comment?: string;
precision?: number;
scale?: number;
}
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
typescript复制代码/**
* 关系类型
*/
export enum RelationType {
ONE_TO_ONE = 'one-to-one',
ONE_TO_MANY = 'one-to-many',
MANY_TO_ONE = 'many-to-one',
MANY_TO_MANY = 'many-to-many',
}

/**
* 关系元数据
*/
export interface RelationMeta {
/** 唯一标识 */
uuid: string;

/** 关系类型 */
relationType: RelationType;

/** 关系的源实体标识 */
sourceId: string;

/** 关系目标实体标识 */
targetId: string;

/** 源实体上的关系属性 */
roleOnSource: string;

/** 目标实体上的关系属性 */
roleOnTarget: string;

/** 拥有关系的实体ID,对应 TypeORM 的 JoinTable 或 JoinColumn */
ownerId?: string;
}

不需要转化成 TypeORM 实体定义的元数据有:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
typescript复制代码/**
* 包的元数据
*/
export interface PackageMeta{
/** ID,主键 */
id?: number;

/** 唯一标识 */
uuid: string;

/** 包名 */
name: string;

/**实体列表 */
entities?: EntityMeta[];

/**ER图列表 */
diagrams?: DiagramMeta[];

/**关系列表 */
relations?: RelationMeta[];
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
typescript复制代码import { X6EdgeMeta } from "./x6-edge-meta";
import { X6NodeMeta } from "./x6-node-meta";

/**
* ER图元数据
*/
export interface DiagramMeta {
/** 唯一标识 */
uuid: string;

/** ER图名称 */
name: string;

/** 节点 */
nodes: X6NodeMeta[];

/** 关系的连线 */
edges: X6EdgeMeta[];
}
1
2
3
4
5
6
7
8
9
10
11
12
typescript复制代码export interface X6NodeMeta{
/** 对应实体标识uuid */
id: string;
/** 节点x坐标 */
x?: number;
/** 节点y坐标 */
y?: number;
/** 节点宽度 */
width?: number;
/** 节点高度 */
height?: number;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
typescript复制代码import { Point } from "@antv/x6";

export type RolePosition = {
distance: number,
offset: number,
angle: number,
}
export interface X6EdgeMeta{
/** 对应关系 uuid */
id: string;

/** 折点数据 */
vertices?: Point.PointLike[];

/** 源关系属性位置标签位置 */
roleOnSourcePosition?: RolePosition;

/** 目标关系属性位置标签位置 */
roleOnTargetPosition?: RolePosition;
}

rxModels有一个后端服务,基于这些数据构建数据库。

rxModels有一个前端管理界面,管理并生产这些数据。

服务端 rx-models

整个项目的核心,基于NestJS构建。需要安装TypeORM,只安装普通 TypeORM 核心项目,不需要安装 NestJS 封装版。

1
2
3
4
5
console复制代码nest new rx-models

cd rx-models

npm install npm install typeorm

这只是关键安装,其他的库,不一一列举了。

具体项目已经完成,代码地址:github.com/rxdrag/rx-m…。

第一个版本承担技术探索的任务,仅支持 MySQL 足够了。

通用JSON接口

设计一套接口,规定好接口语义,就像 GraphQL 那样。这样做的是优势,就是不需要接口文档,也不需要定义接口版本了。

接口以 JSON 为参数,返回也是 JSON 数据,可以叫 JSON 接口。

查询接口

接口描述:

1
2
3
4
5
6
7
8
9
10
typescript复制代码url: /get/jsonstring...
method: get
返回值:{
data:any,
pagination?:{
pageSize: number,
pageIndex: number,
totalCount: number
}
}

URL 长度是 2048 个字节,这个长度传递一个查询字符串足够用了,在查询接口中,可以把 JSON 查询参数放在 URL 里,使用 get 方法查数据。

把 JSON 查询参数放在 URL 里,有一个明显的优势,就是客户端可以基于 URL 缓存查询结果,比如使用 SWR 库。

有个特别需要注意的点就是URL转码,要不然查询时,like 使用 % 会导致后端出错。所以,给客户端写一套查询 SDK,封装这些转码类操作是有必要的。

查询接口示例

传入实体名字,就可以查询实体的实例,比如要查询所有的文章(Post),可以这么写:

1
2
3
json复制代码{
"entity": "Post"
}

要查询 id = 1 的文章,则这样写:

1
2
3
4
json复制代码{
"entity": "Post",
"id": 1
}

把文章按照标题和日期排序,这么写:

1
2
3
4
5
6
7
json复制代码{
"entity": "Post",
"@orderBy": {
"title": "ASC",
"updatedAt": "DESC"
}
}

只需要查询文章的 title 字段,这么写:

1
2
3
4
json复制代码{
"entity": "Post",
"@select": ["title"]
}

这么写也可以:

1
2
3
json复制代码{
"entity @select(title)": "Post"
}

只取一条记录:

1
2
3
4
json复制代码{
"entity": "Post",
"@getOne": true
}

或者:

1
2
3
json复制代码{
"entity @getOne": "Post"
}

只查标题中有“水”字的文章:

1
2
3
4
json复制代码{
"entity": "Post",
"title @like": "%水%"
}

还需要更复杂的查询,内嵌类似 SQL 的表达式吧:

1
2
3
4
json复制代码{
"entity": "Post",
"@where": "name %like '%风%' and ..."
}

数据太多了,分页,每页25条记录取第一页:

1
2
3
4
json复制代码{
"entity": "Post",
"@paginate": [25, 0]
}

或者:

1
2
3
json复制代码{
"entity @paginate(25, 0)": "Post"
}

关系查询,附带文章的图片关系 medias :

1
2
3
4
json复制代码{
"entity": "Post",
"medias": {}
}

关系嵌套:

1
2
3
4
5
6
json复制代码{
"entity": "Post",
"medias": {
"owner":{}
}
}

给关系加个条件:

1
2
3
4
5
6
json复制代码{
"entity": "Post",
"medias": {
"name @like": "%风景%"
}
}

只取关系的前5个

1
2
3
4
json复制代码{
"entity": "Post",
"medias @count(5)": {}
}

聪明的您,可以按照这个方向,对接口做进一步的设计更改。

@ 符号后面的,称之为 指令。

把业务逻辑放在指令里,可以对接口进行非常灵活的扩展。比如在文章内容(content)底部附加加一个版权声明,可以定义一个 @addCopyRight 指令:

1
2
3
4
json复制代码{
"entity": "Post",
"@addCopyRight": "content"
}

或者:

1
2
3
json复制代码{
"entity @addCopyRight(content)": "Post"
}

指令看起来是不是像一个插件?

既然是个插件,那就赋予它热加载的能力!

通过管理界面,上传第三方指令代码,就可以把指令插入系统。

第一版不支持指令上传功能,但是架构设计已经预留了这个能力,只是配套的界面没做。

post 接口

接口描述:

1
2
3
4
makefile复制代码url: /post
method: post
参数: JSON
返回值: 操作成功的对象

通过post方法,传入JSON数据。

预期post接口具备这样的能力,传入一组对象组合(或者说附带关系约束的对象树),直接把这组对象同步到数据库。

如果给对象提供了id字段,则更新已有对象,没有提供id字段,则创建新对象。

post接口示例

上传一篇文章,带图片关联,可以这么写:

1
2
3
4
5
6
7
8
9
10
json复制代码{
"Post": {
"title": "轻轻的,我走了",
"content": "...",
// 作者关联 id
"author": 1,
// 图片关联 id
"medias":[3, 5, 6 ...]
}
}

也可以一次传入多篇文章

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
json复制代码{
"Post": [
{
"id": 1,
"title": "轻轻的,我走了",
"content": "内容有所改变...",
"author": 1,
"medias":[3, 5, 6 ...]
},
{
"title": "正如,我轻轻的来",
"content": "...",
"author": 1,
"medias": [6, 7, 8 ...]
}
]
}

第一篇文章有id字段,是更新数据库的操作,第二篇文章没有id字段,是创建新的。

也可以传入多个实体的实例,类似这样,同时传入文章(Post)跟媒体(Media)的实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
json复制代码{
"Post": [
{
...
},
{
...
}
],
"Media": [
{
...
}
]
}

可以把关联一并传入,如果一篇文章关联一个 SeoMeta 对象,创建文章时,一并创建 SeoMeta:

1
2
3
4
5
6
7
8
9
10
11
12
13
json复制代码{
"Post": {
"title": "轻轻的,我走了",
"content": "...",
"author": 1,
"medias":[3, 5, 6 ...],
"seoMeta":{
"title": "诗篇解读:轻轻的,我走了|诗篇解读网",
"descript": "...",
"keywords": "诗篇,解读,诗篇解读"
}
}
}

传入这个参数,会同时创建两个对象,并在它们之间建立关联。

正常情况下删除这个关联,可以这样写:

1
2
3
4
5
6
7
8
9
json复制代码{
"Post": {
"title": "轻轻的,我走了",
"content": "...",
"author": 1,
"medias":[3, 5, 6 ...],
"seoMeta":null
}
}

这样的方式保存文章,会删除跟 SeoMeta 的关联,但是 SeoMeta 的对象并没有被删除。别的文章也不需要这个 SeoMeta,不主动删除它,数据库里就会生成一条垃圾数据。

保存文章的时候,添加一个 @cascade 指令,能解决这个问题:

1
2
3
4
5
6
7
8
9
json复制代码{
"Post @cascade(medias)": {
"title": "轻轻的,我走了",
"content": "...",
"author": 1,
"medias":[3, 5, 6 ...],
"seoMeta":null
}
}

@cascade 指令会级联删除与之关联的 SeoMeta 对象。

这个指令能放在关联属性上,写成这样吗?

1
2
3
4
5
6
7
8
9
json复制代码{
"Post": {
"title": "轻轻的,我走了",
"content": "...",
"author": 1,
"medias @cascade":[3, 5, 6 ...],
"seoMeta":null
}
}

最好不要这样写,客户端用起来不会很方便。

自定义指令可以扩展post接口,比如,要加一个发送邮件的业务,可以开发一个 @sendEmail 指令:

1
2
3
4
5
6
7
8
json复制代码{
"Post @sendEmail(title, content, water@rxdrag.com)": {
"title": "轻轻的,我走了",
"content": "...",
"author": 1,
"medias @cascade":[3, 5, 6 ...],
}
}

假设每次保存文章成功后,sendEmail 指令都会把标题跟内容,发送到指定邮箱。

update 接口

接口描述:

1
2
3
4
makefile复制代码url: /update
method: post
参数: JSON
返回值: 操作成功的对象

post 接口已经具备了 update 功能了,为什么还要再做一个 update 接口?

有时候,需要一个批量修改一个或者几个字段的能力,比如把指定的消息标记为已读。

为了应对这样的场景,设计了 update 接口。假如,要所有文章的状态更新为“已发布”:

1
2
3
4
5
6
json复制代码{
"Post": {
"status": "published",
"@ids":[3, 5, 6 ...],
}
}

基于安全方面的考虑,接口不提供条件指令,只提供 @ids 指令(遗留原因,演示版不需要@符号,直接写 ids 就行,后面会修改)。

delete 接口

接口描述:

1
2
3
4
makefile复制代码url: /delete
method: post
参数: JSON
返回值: 被删除的对象

delete 接口跟 update 接口一样,不提供条件指令,只接受 id 或者 id 数组。

要删除文章,只需要这么写:

1
2
3
json复制代码{
"Post": [3, 5, ...]
}

这样的删除,跟 update 一样,也不会删除跟文章相关的对象,级联删除的话需要指令 @cascade。

级联删除 SeoMeta,这么写:

1
2
3
json复制代码{
"Post @cascade(seoMeta)": [3, 5, ...]
}

upload 接口

1
2
3
4
5
makefile复制代码url: /upload
method: post
参数: FormData
headers: {"Content-Type": "multipart/form-data;boundary=..."}
返回值: 上传成功后生成RxMedia对象

rxModels 最好提供在线文件管理服务功能,跟第三方的对象管理服务,比如腾讯云、阿里云、七牛什么的,结合起来。

第一版先不实现跟第三方对象管理的整合,文件存在本地,文件类型仅支持图片。

用实体 RxMedia 管理这些上传的文件,客户端创建FormData,设置如下参数:

1
2
3
4
5
json复制代码{
"entity": "RxMedia",
"file": ...,
"name": "文件名"
}

全部JSON接口介绍完了,接下就是如何实现并使用这些接口。

继续之前,说一下为什么选用JSON,而不用其他方式。

为什么不用 oData

开始这个项目的时候,对 oData 并不了解。

简单查了点资料,说是,只有在需要Open Data(开放数据给其他组织)时候,才有必要按照OData协议设计RESTful API。

如果不是把数据开放给其他组织,引入 oData 增加了发杂度。需要开发解析oData参数解析引擎。

oData 出了很长时间,并没有多么流行,还不如后来的 GraphQL 知名度高。

为什么不用 GraphQL?

尝试过,没用起来。

一个人,做开源项目,只能接入现有的开源生态。一个人什么都做,是不可能完成的任务。

要用GraphQL,只能用现有的开源库。现有的主流 GraphQL 开源库,大部分都是基于代码生成的。前一篇文章说过,不想做一个基于代码生成的低代码项目。

还有一个原因,目标定位是中小项目。GraphQL对这些中小项目来说,有两个问题:1、有些笨重;2、用户的学习成本高。

有的小项目就三五个页面,拉一个轻便的小后端,很短时间就搭起来了,没有必要用 GraphQL。

GraphQL的学习成本并不低,有些中小项目的用户是不愿意付出这些学习成本的。

综合这些因素,第一个版本的接口,没有使用 GraphQL。

使用 GraphQL 的话,需要怎么做?

跟一些朋友交流的时候,有些朋友对 GraphQL 还是情有独钟的。并且经过几年的发展,GraphQL 的热度慢慢开始上来了。

假如使用 GraphQL 做一个类似项目,需要怎么做呢?

需要自己开发一套 GraphQL 服务端,这个服务端类似 Hasura,不能用代码生成机制,使用动态运行机制。Hasura 把 GQL 编译成 SQL,你可以选择这样做,也可以不选择这样做,只要能不经过编译过程,就把对象按照 GQL 查询要求,拉出来就行。

需要在 GraphQL 的框架下,充分考虑权限管理,业务逻辑扩展和热加载等方面。这就需要对 GraphQL 有比较深入的理解。

如果要做低代码前端,那么还需要做一个特殊的前端框架,像 apollo 这样的 GraphQL 前端库库,并不适合做低代码前端。因为低代码前端需要动态类型绑定,这个需求跟这些前端库的契合,并不是特别理想。

每一项,都需要大量时间跟精力,不是一个人能完成的工作,需要一个团队。

或有一天,有机会,作者也想进行这样方面的尝试。

但也未必会成功,GraphQL 本身并不代表什么,假如它能够使用者带来实实在在的好处,才是被选择的理由。

登录验证接口

使用 jwt 验证机制,实现两个登录相关的接口。

1
2
3
4
5
6
7
typescript复制代码url: /auth/login
method: post
参数: {
username: string,
password: string
}
返回值:jwt token
1
2
3
vbnet复制代码url: /auth/me
method: get
返回值: 当前登录用户,RxUser类型

这两个接口实现起来,没有什么难的,跟着NestJs文档做一下就行了。

元数据存储

客户端通过 ER 图的形式生产的元数据,存储在数据库,一个实体 RxPackage就够了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
typescript复制代码export interface RxPackage {
/* id 数据库主键 */
id: number;

/** 唯一标识uuid,当不同的项目之间共享元数据时,这个字段很有用 */
uuid: string;

/** 包名 */
name: string;

/** 包的所有实体元数据,以JSON形式存于数据库 */
entities: any;

/** 包的所有 ER 图,以JSON形式存于数据库 */
diagrams?: any;

/** 包的所有关系,以JSON形式存于数据库 */
relations?: any;
}

数据映射完成后,在界面中看到的一个包的所有内容,就对应 rx_package 表的一条数据记录。

这些数据怎么被使用呢?

我们给包增加一个发布功能,如果包被发布,就根据这条数据库记录,做一个JSON文件,放在 schemas 目录下,文件名就是 ${uuid}.json。

服务端创建 TypeORM 连接时,热加载这些JSON文件,并把它们解析成 TypeORM 实体定义数据。

应用安装接口

rxModels 的最终目标是,发布一个代码包,使用者通过图形化界面安装即可,不要接触代码。

安装

两页向导,即可完成安装,需要接口:

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
typescript复制代码url: install
method: post
参数: {
/** 数据库类型 */
type: string;

/** 数据库所在主机 */
host: string;

/** 数据库端口 */
port: string;

/** 数据库schema名 */
database: string;

/** 数据登录用户 */
username: string;

/** 数据库登录密码 */
password: string;

/** 超级管理员登录名 */
admin: string;

/** 超级管理员密码 */
adminPassword: string;

/** 是否创建演示账号 */
withDemo: boolean;
}

还需要一个查询是否已经安装的接口:

1
2
3
4
5
vbnet复制代码url: /is-installed
method: get
返回值: {
installed: boolean
}

只要完成这些接口,后端的功能就实现了,加油!

架构设计

得益于 NestJs 优雅的框架,可以把整个后端服务分为以下几个模块:

  • auth, 普通 NestJS module,实现登录验证接口。本模块很简单,后面不会单独介绍了。
  • package-manage, 元数据的管理发布模块。
  • install, 普通 NestJS module,实现安装功能。
  • schema, 普通 NestJS module,管理系统元数据,并把前面定义的格式的元数据,转化成 TypeORM 能接受的实体定义,核心代码是 SchemaService。
  • typeorm, 对 TypeORM 的封装,提供带有元数据定义的 Connection,核心代码是TypeOrmService,该模块没有 Controller。
  • magic, 项目最核心模块,通用JSON接口实现模块。
  • directive, 指令定义模块,定义指令功能用到的基础类,热加载指令,并提供指令检索服务。
  • directives, 所有指令实现类,系统从这个目录热加载所有指令。
  • magic-meta, 解析JSON参数用到的数据格式,主要使用模块是 magic,由于 directive 模块也会用到这些数据,为了避免模块之间的循环依赖,把这部分数据抽出来,单独作为一个模块,那两个模块同时依赖这个模块。
  • entity-interface, 系统种子数据类型接口,主要用于 TypeScript 编译器的类型识别。客户端的代码导出功能导出的文件,直接复制过来的。客户端也会复制一份同样的代码来用。

包管理 package-manage

提供一个接口 publishPackages。把参数传入的元数据,发布到系统里,同步到数据库模式:

  • 就是一个包一个文件,放在根目录的 schemas 目录下,文件名就是包的 uuid + .json 后缀。
  • 通知 TypeORM 模块重新创建数据库连接,同时同步数据库。

安装模块 install

模块内有一个种子文件 install.seed.json,里面是系统预置的一些实体,格式就是上文定义的元数据格式,这些数据统一组织在 System 包里。

客户端没有完成的时候,手写了一个 ts 文件用于调试,客户端完成以后,直接利用包的导出功能,导出了一个 JSON 文件,替换了手写的 ts 文件。相当于基础数据部分,可以自举了。

这个模块的核心代码在 InstallService 里,它分步完成:

  • 把客户端传来的数据库配置信息,写入根目录的dbconfig.json 文件。
  • 把install.seed.json文件里面的预定义包发布。直接调用上文说的 publishPackages 实现发布功能。

元数据管理模块 schema

该模块提供一个 Controller,名叫 SchemaController。提供一个 get 接口 /published-schema,用于获取已经发布的元数据信息。

这些已经发布的元数据信息可以被客户端的权限设置模块使用,因为只有已经发布的模块,对它设置权限才有意义。低代码可视化编辑前端,也可以利用这些信息,进行下拉选择式的数据绑定。

核心类 SchemaService,还提供了更多的功能:

  • 从 /schemas 目录下,加载已经发布的元数据。
  • 把这些元数据组织成列表+树的结构,提供按名字、按UUID等方式的查询服务。
  • 把元数据解析成 TypeORM 能接受的实体定义 JSON。

封装 TypeORM

自己写一个 ORM 库工作量是很大的,不得不使用现成的,TypeORM 是个不错的选择,一来,她像个年轻的姑娘,漂亮又活力四射。二来,她不像 Prisma 那么臃肿。

为了迎合现有的 TyeORM,有些地方不得不做妥协。这种低代码项目后端,比较理想的实现方式自己做一个 ORM 库,完全根据自己的需求实现功能,那样或许就有青梅竹马的感觉了,但是需要团队,不是一个人能完成。

既然是一个人,那么就安心做一个人能做的事情好了。

TypeORM 只有一个入口能够传入实体定义,就是 createConnection。需要在这个函数调用前,解析完元数据,分离出实体定义。这个模块的 TypeOrmService 完成这些 connection 的管理工作,依赖的 schema 模块的 SchemaService。

通过 TypeOrmService 可以重启当前连接(关闭并重新创建),以更新数据库定义。创建连接的时候,使用 install 模块创建的 dbconfig.json 文件获取数据库配置。注意,TypeORM 的 ormconfig.json 文件是没有被使用的。

magic 模块

在 magic 模块,不管查询还是更新,每一个接口实现的操作,都在一个完整的事务里。

难道查询接口也要包含在一个事务里?

是的,因为有的时候查询可能会包含一些简单操作数据库的指令,比如查询一篇文章的时候,顺便把它的阅读次数 +1。

magic 模块的增删查改等操作,都受到权限的约束,把它的核心模块 MagicInstanceService 传递给指令,指令代码里可以放心使用它的接口操作数据库,不需要关心权限问题。

MagicInstanceService

MagicInstanceService 是接口 MagicService 的实现。接口定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
typescript复制代码import { QueryResult } from 'src/magic-meta/query/query-result';
import { RxUser } from 'src/entity-interface/RxUser';

export interface MagicService {
me: RxUser;

query(json: any): Promise<QueryResult>;

post(json: any): Promise<any>;

delete(json: any): Promise<any>;

update(json: any): Promise<any>;
}

magic 模块的 Controller 直接调用这个类,实现上文定义的接口。

AbilityService

权限管理类,查询当前登录用户的实体跟字段的权限配置。

query

/magic/query 目录,实现 /get/json... 接口的代码。

MagicQuery 是核心代码,实现查询业务逻辑。它使用 MagicQueryParser 把传入的 JSON 参数,解析成一棵数据树,并分离相关指令。数据结构定义在 /magic-meta/query 目录。代码量太大,没有精力一一解析。自己翻阅一下,有问题可以跟作者联系。

需要特别注意的是 parseWhereSql 函数。这个函数负责解析类似 SQL Where 格式的语句,使用了开源库 sql-where-parser。

把它放在这个目录,是因为 magic 模块需要用到它,同时 directive 模块也需要用到它,为了避免模块的循环依赖,把它独立抽到这个目录。

/magic/query/traverser 目录存放一些遍历器,用于处理解析后的树形数据。

MagicQuery 使用 TypeORM 的 QueryBuilder 构建查询。关键点:

  • 使用 directive 模块的 QueryDirectiveService 获取指令处理类。指令处理类可以:1、构建 QueryBuilder 用到的条件语句,2、过滤查询结果。
  • 从 AbilityService 拿到权限配置,根据权限配置修改 QueryBuilder, 根据权限配置过滤查询结果中的字段。
  • QueryBuilder 用到的查询语句分两部分:1、影响查询结果数量的语句,比如 take 指令、paginate指令。这些指令只是要截取指令数量的结果;2、其他没有这种影响的查询语句。因为分页时,需要返回一个总的记录条数,用第二类查询语句先查一次数据库,获得总条数,然后加入第一类查询语句获得查询结果。

post

/magic/post 目录,实现 /post 接口的代码。

MagicPost 类是核心代码,实现业务逻辑。它使用 MagicPostParser 把传入的JSON参数,解析成一棵数据树,并分离相关指令。数据结构定义在 /magic-meta/post 目录。它可以:

  • 递归保存关联对象,理论上可以无限嵌套。
  • 根据 AbilityService 做权限检查。
  • 使用 directive 模块的 PostDirectiveService 获取指令处理类, 在实例保存前跟保存后会调用指令处理程序,详情请翻阅代码。

update

/magic/update 目录,实现 /update 接口的代码。

功能简单,代码也简单。

delete

/magic/delete 目录,实现 /delete 接口的代码。

功能简单,代码也简单。

upload

/magic/upload 目录,实现 /upload 接口的代码。

upload 目前功能比较简单,后面可以考添加一些裁剪指令等功能。

directive 模块

指令服务模块。热加载指令,并对这些指令提供查询服务。

这个模块也比较简单,热加载使用的是 require 语句。

关于后端,其它模块就没什么好说的,都很简单,直接看一下代码就好。

客户端 rx-models-client

需要一个客户端,管理生产并管理元数据,测试通用数据查询接口,设置实体权限,安装等。创建一个普通的 React 项目, 支持 TypeScript。

1
console复制代码npx create-react-app rx-models-client--template typescript

这个项目已经完成了,在GitHub上,代码地址:github.com/rxdrag/rx-m…。

代码量有点多,全部在这里展开解释,有点放不下。只能挑关键点说一下,有问题需要交流的话,请跟作者联系。

ER图 - 图形化的业务模型

这个模块是客户端的核心,看起来比较唬人,其实一点都不难。目录 src/components/entity-board下,是该模块全部代码。

得益于 Antv X6,使得这个模块的制作比预想简单了许多。

X6 充当的角色,只是一个视图层。它只负责渲染实体图形跟关系连线,并传回一些用户交互事件。它用于撤销、重做的操作历史功能,在这个项目里用不上,只能全部自己写。

Mobx 在这个模块也占非常重要的地位,它管理了所有的状态并承担了部分业务逻辑。低代码跟拖拽类项目,Mobx 确实非常好用,值得推荐。

定义 Mobx Observable 数据

上文定义的元数据,每一个对应一个 Mobx Observable 类,再加一个根索引类,这数据相互包含,构成一个树形结构,在 src/components/entity-board/store 目录下。

  • EntityBoardStore, 处于树形结构的根节点,也是该模块的整体状态数据,它记录下面这些信息:
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
typescript复制代码export class EntityBoardStore{
/**
* 是否有修改,用于未保存提示
*/
changed = false;

/**
* 所有的包
*/
packages: PackageStore[];

/**
* 当前正在打开的 ER 图
*/
openedDiagram?: DiagramStore;

/**
* 当前使用的 X6 Graph对象
*/
graph?: Graph;

/**
* 工具条上的关系被按下,记录具体类型
*/
pressedLineType?: RelationType;

/**
* 处在鼠标拖动划线的状态
*/
drawingLine: LineAction | undefined;

/**
* 被选中的节点
*/
selectedElement: SelectedNode;

/**
* Command 模式,撤销列表
*/
undoList: Array<Command> = [];

/**
* Command 模式,重做列表
*/
redoList: Array<Command> = [];

/**
* 构造函数传入包元数据,会自动解析成一棵 Mobx Observable 树
*/
constructor(packageMetas:PackageMeta[]) {
this.packages = packageMetas.map(
packageMeta=> new PackageStore(packageMeta,this)
);
makeAutoObservable(this);
}

/**
* 后面大量的set方法,就不需要了展开了
*/
...

}
  • PackageStore, 树形完全跟上文定义的 PackageMeta 一致,区别就是 meta 相关的全都换成了 store 相关的:
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
typescript复制代码export class PackageStore{
id?: number;
uuid: string;
name: string;
entities: EntityStore[] = [];
diagrams: DiagramStore[] = [];
relations: RelationStore[] = [];
status: PackageStatus;

constructor(meta:PackageMeta, public rootStore: EntityBoardStore){
this.id = meta.id;
this.uuid = meta?.uuid;
this.name = meta?.name;
this.entities = meta?.entities?.map(
meta=>new EntityStore(meta, this.rootStore, this)
)||[];
this.diagrams = meta?.diagrams?.map(
meta=>new DiagramStore(meta, this.rootStore, this)
)||[];
this.relations = meta?.relations?.map(
meta=>new RelationStore(meta, this)
)||[];
this.status = meta.status;
makeAutoObservable(this)
}

/**
* 省略set方法
*/
...


/**
* 最后提供一个把 Store 逆向转成元数据的方法,用于往后端发送数据
*/
toMeta(): PackageMeta {
return {
id: this.id,
uuid: this.uuid,
name: this.name,
entities: this.entities.map(entity=>entity.toMeta()),
diagrams: this.diagrams.map(diagram=>diagram.toMeta()),
relations: this.relations.map(relation=>relation.toMeta()),
status: this.status,
}
}
}

依此类推,可以做出 EntityStore、ColumnStore、RelationStore 和 DiagramStore。

前面定义的 X6NodeMeta 和 X6EdgeMeta 不需要制作相应的 store 类,因为没法通过 Mobx 的机制更新 X6 的视图,要用其它方式完成这个工作。

DiagramStore 主要为展示 ER 图提供数据。给它添加两个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
typescript复制代码export type NodeConfig = X6NodeMeta & {data: EntityNodeData};
export type EdgeConfig = X6EdgeMeta & RelationMeta;

export class DiagramStore {
...

/**
* 获取当前 ER 图所有的节点,利用 mobx 更新机制,
* 只要数据有更改,调用该方法的视图会自动被更新,
* 参数只是用了指示当前选中的节点,或者是否需要连线,
* 这些状态会影响视图,可以在这里直接传递给每个节点
*/
getNodes(
selectedId:string|undefined,
isPressedRelation:boolean|undefined
): NodeConfig[]

/**
* 获取当前 ER 图所有的连线,利用 mobx 更新机制,
* 只要数据有更改,调用该方法的视图会自动被更新
*/
getAndMakeEdges(): EdgeConfig[]

}

如何使用 Mobx Observable 数据

使用 React 的 Context,把上面定义的 store 数据传递给子组件。

定义 Context:

1
2
3
typescript复制代码export const EnityContext = createContext<EntityBoardStore>({} as EntityBoardStore);
export const EntityStoreProvider = EnityContext.Provider;
export const useEntityBoardStore = (): EntityBoardStore => useContext(EnityContext);

创建 Context:

1
2
3
4
5
6
7
8
9
typescript复制代码...
const [modelStore, setModelStore] = useState(new EntityBoardStore([]));

...
return (
<EntityStoreProvider value = {modelStore}>
...
</EntityStoreProvider>
)

使用的时候,直接在子组件里调用 const rootStore = useEntityBoardStore() 就可以拿到数据了。

树形编辑器

利用 Mui的树形控件 + Mobx 对象,代码并不复杂,感兴趣的话,翻翻看看,有疑问留言或者联系作者。

如何使用 AntV X6

X6 支持在节点里嵌入 React 组件,定义一个组件 EntityView 嵌入进去就好。X6 相关代码都在这个目录下:

1
css复制代码src/componets/entity-board/grahp-canvas

业务逻辑被拆分成很多 React Hooks:

  • useEdgeChange, 处理关系线被拖动
  • useEdgeLineDraw, 处理画线动过
  • useEdgeSelect, 处理关系线被选中
  • useEdgesShow, 渲染关系线,包括更新
  • useGraphCreate, 创建 X6 的 Grpah对象
  • useNodeAdd, 处理拖入一个节点的动作
  • useNodeChange, 处理实体节点被拖动或者改变大小
  • useNodeSelect, 处理节点被选中
  • useNodesShow, 渲染实体节点,包括更新

撤销、重做

撤销、重做不仅跟 ER 图相关,还跟整个 store 树相关。这就是说,X6 的撤销、重做机制用不了,只能自己重新做。

好在设计模式中的 Command 模式还算简单,定义一些 Command,并定义好正负操作,可以很容易完成。实现代码在:

1
bash复制代码src/componets/entity-board/command

全局状态 AppStore

按照上问的方法,利用 Mobx 做一个全局的状态管理类 AppStore,用于管理整个应用的状态,比如弹出操作成功提示,弹出错误信息等。

代码在 src/store 目录下。

接口测试

代码在 src/components/api-board 目录下。

很简单一个模块,代码应该很容易懂。使用了 rxmodels-swr 库,直接参考它的文档就好。

JSON 输入控件,使用了 monaco 的 react 封装:react-monaco-editor,使用起来很简单,安装稍微麻烦一点,需要安装 react-app-rewired。

monaco 用的还不熟练,后面熟练了可以加入如下功能输入提示和代码校验等功能。

权限管理

代码在 src/components/auth-board 目录下。

这个模块之主要是后端数据的组织跟接口定义,前端代码很少,基于rxmodels-swr 库完成。

权限定义支持表达式,表达式类似 SQL 语句,并内置了变量 $me 指代当前登录用户。

前端输入时,需要对 SQL 表达式进行校验,所以也引入了开源库 sql-where-parser。

安装、登录

安装代码在 src/components/install 目录下。

登录页面是 src/components/login.tsx。

代码一眼就能瞅明白。

后记

这篇文章挺长的,但是还不确定有没有把需要说的说清楚,有问题的话留言或者联系作者吧。

演示能跑起来以后,就已经冒着被踢的危险,在几个 QQ 群发了一下。收到了很多反馈,非常感谢热心的朋友们。

rxModels,终于走出去了第一步…

与前端的第一次接触

rxModels来了,热情的走向前端们。

前端们皱起了眉头,说:“离远点儿,你不是我们理想中的样子。”

rxModels 说:“我还会改变,还会成长,未来的某一天,我们一定是最好的搭档。”

下一篇文章

《从 0 构建一个可视化低代码前端》,估计要等一段时间了,要先把前端重构完。

本文转载自: 掘金

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

Python 中 raise 和 raise/from 的使

发表于 2021-07-31

Python 中 raise 和 raise/from 的使用方法


  1. 参考资料

  • Python “raise from” usage
  • The raise statement
  • Built-in Exceptions

  1. 代码比较

今天在看《Effective Python》的时候第一次见到 raise A from B 的用法,所以在网上查了一下。
下面用代码比较一下 raise 和 raise/from 的区别。

  • raise.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
python复制代码# raise
try:
raise ValueError
except Exception as e:
raise IndexError
"""
Traceback (most recent call last):
File "raise.py", line 3, in <module>
raise ValueError
ValueError

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
File "raise.py", line 5, in <module>
raise IndexError
IndexError
"""
  • raisefrom.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
python复制代码# raise/from
try:
raise ValueError
except Exception as e:
raise IndexError from e
"""
Traceback (most recent call last):
File "raisefrom.py", line 3, in <module>
raise ValueError
ValueError

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
File "raisefrom.py", line 5, in <module>
raise IndexError from e
IndexError
"""

上面的 raise 和 raise/from 都是放在 except 异常处理块中的。
可以观察到 raise 和 raise/from 最大的区别是异常提示信息不一样:

  • raise 是:During handling of the above exception, another exception occurred:。
    即“在处理上面的异常时,发生了另外一个异常:”。
  • 而 raise/from 是:The above exception was the direct cause of the following exception:。
    即“上面的异常是接下来的异常的直接原因:”。

  1. 用法解释

2.1 raise

当在 except 块或者 finally 块中出现异常时(包括使用单独的 raise 重新抛出异常的情况),之前的异常会被附加到新异常的 __context__ 属性上。

except 块中的语句叫做异常处理器 exception handler,它们是处理异常的语句。

而在其他任何地方抛出异常,都不会设置 __context__ 属性。
这样打印出来的异常信息就会包含这么一句话:During handling of the above exception, another exception occurred:。


2.2 raise A from B

raise A from B 语句用于连锁 chain 异常。
from 后面的 B 可以是:

  • 异常类
  • 异常实例
  • None(Python 3.3 的新特性)

如果 B 是异常类或者异常实例,那么 B 会被设置为 A 的 __cause__ 属性,表明 A异常 是由 B异常 导致的。
这样打印出来的异常信息就会包含这样一句话:The above exception was the direct cause of the following exception:。
与此同时,在 Python 3.3 中 A异常 的 __suppress_context__ 属性会被设置为 True,这样就抑制了 A异常 的 __context__ 属性,即忽略 __context__ 属性。
于是 Python 就不会自动打印异常上下文 exception context,而是使用 __cause__ 属性来打印异常的引发者。

在 Python 3.3 中,B 还可以是 None:raise A异常 from None。
这样相当于把 __suppress_context__ 属性设置为 True,从而禁用了 __context__ 属性,Python 不会自动展示异常上下文。
比如下面这段代码,注意到只显示了 IndexError 一个异常信息:
raisefromnone.py

1
2
3
4
5
6
7
8
9
10
11
12
python复制代码# raise ... from None
# 禁用异常上下文属性
try:
raise ValueError
except Exception as e:
raise IndexError from None
"""
Traceback (most recent call last):
File "raisefromnone.py", line 6, in <module>
raise IndexError from None
IndexError
"""

  1. 总结

在 except 或者 finally 块中使用 raise 和 raise/from 语句需要注意:

  • raise 会设置后面异常的 __context__ 属性为前面的异常。
    异常信息中会有 During handling of the above exception, another exception occurred:。
  • raise A异常 from B异常 会把 A异常 的 __cause__ 属性设置为 B异常。
    同时设置 __suppress_context__ 属性为 True,从而忽略 __context__ 属性,不打印异常上下文信息。
    异常信息中会有 The above exception was the direct cause of the following exception:。
  • raise A异常 from None 会设置 A异常 的 __suppress_context__ 属性为 True,这样会忽略它的 __context__ 属性,不会自动显示异常上下文信息。

完成于 2018.11.21

本文转载自: 掘金

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

老鱼进阶篇-Spring IOC容器初始化全流程源码解读分析

发表于 2021-07-31

[TOC]

1、XML方式BeanDefination注册流程

1.1、测试demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码// 方法一,已过期
String path = "spring/beans.xml";
Resource resource = new ClassPathResource(path);
// BeanDefination注册流程
XmlBeanFactory beanFactory = new XmlBeanFactory(resource );

// 方法二
String path = "spring/beans.xml";
// 创建DefaultListableBeanFactory工厂,这也就是Spring的基本容器
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
// 创建BeanDefinition阅读器
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(beanFactory);
// 进行BeanDefinition注册流程
reader.loadBeanDefinitions(path);

// 总结:两种方法最终都是通过XmlBeanDefinitionReader开启解析注册流程

// 方法三:通过高级容器进行创建
// 创建IoC容器,并进行初始化
ApplicationContext context = new ClassPathXmlApplicationContext("spring/spring-ioc.xml");
// 获取Bean的实例
Student bean = (Teacher) context.getBean(Teacher.class);

1.2、源码分析

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
java复制代码【XmlBeanFactory】#构造方法
@@XmlBeanDefinitionReader
# loadBeanDefinitions(EncodedResource resource)
# doLoadBeanDefinitions(InputSource inputSource, Resource resource)
// 通过DOM4J加载解析XML文件,最终形成Document对象
#Document doc = doLoadDocument(inputSource, resource)
// 通过对Document对象的操作,完成BeanDefinition的加载和注册工作
# registerBeanDefinitions(doc, resource)
// 创建BeanDefinitionDocumentReader来解析Document对象,完成BeanDefinition解析
@@BeanDefinitionDocumentReader
//解析过程入口,BeanDefinitionDocumentReader只是个接口
# registerBeanDefinitions(doc, ..)
// 具体的实现过程在DefaultBeanDefinitionDocumentReader完成
@@DefaultBeanDefinitionDocumentReader
// 真正实现BeanDefinition解析和注册工作
# registerBeanDefinitions(doc, ..)
// 委托给BeanDefinitionParserDelegate
// 从Document的根元素开始进行BeanDefinition的解析
# doRegisterBeanDefinitions(doc.getDocumentElement())
# parseBeanDefinitions(Eleme(ele)nt root)
// bean标签、import标签、alias标签,则使用默认解析规则
# parseDefaultElement(Element ele)
# processBeanDefinition(ele)
@@BeanDefinitionParserDelegate
# parseBeanDefinitionElement(ele)
// 最终注册方法
#BeanDefinitionReaderUtils
#registerBeanDefinition
// 解析自定义标签,如:<aop:config>
#parseCustomElement()
// 委托给BeanDefinitionParserDelegate
@@BeanDefinitionParserDelegate
# parseCustomElement(..)
# BeanDefinitionParser(..)
#parse(Element element, ..)
...省略
// 最终注册方法
#BeanDefinitionReaderUtils
#registerBeanDefinition

【ClassPathXmlApplicationContext】#构造方法
# setConfigLocations(configLocations)
# refresh()
@@AbstractApplicationContext
# refresh()
public void refresh() {
synchronized (this.startupShutdownMonitor) {
// Prepare this context for refreshing.
// STEP 1: 刷新预处理
prepareRefresh();

// Tell the subclass to refresh the internal bean factory.
// STEP 2:
// a) 创建IoC容器(DefaultListableBeanFactory)
// b) 加载解析XML文件(最终存储到Document对象中)
// c) 读取Document对象,并完成BeanDefinition的加载和注册工作
ConfigurableListableBeanFactory beanFactory =
obtainFreshBeanFactory();

// Prepare the bean factory for use in this context.
// STEP 3: 对IoC容器进行一些预处理(设置一些公共属性)
prepareBeanFactory(beanFactory);

try {
// Allows post-processing of the bean factory
// in context subclasses.
// STEP 4:
postProcessBeanFactory(beanFactory);

// Invoke factory processors registered as beans
// in the context.
// STEP 5: 调用BeanFactoryPostProcessor后置处理器
// 对BeanDefinition处理
invokeBeanFactoryPostProcessors(beanFactory);

// Register bean processors that intercept bean creation.
// STEP 6: 注册BeanPostProcessor后置处理器
registerBeanPostProcessors(beanFactory);

// Initialize message source for this context.
// STEP 7: 初始化一些消息源(比如处理国际化的i18n等消息源)
initMessageSource();

// Initialize event multicaster for this context.
// STEP 8: 初始化应用事件广播器
initApplicationEventMulticaster();

// Initialize other special beans
// in specific context subclasses.
// STEP 9: 初始化一些特殊的bean
onRefresh();

// Check for listener beans and register them.
// STEP 10: 注册一些监听器
registerListeners();

// Instantiate all remaining (non-lazy-init) singletons.
// STEP 11: 实例化剩余的单例bean(非懒加载方式)
// 注意事项:Bean的IoC、DI和AOP都是发生在此步骤
finishBeanFactoryInitialization(beanFactory);

// Last step: publish corresponding event.
// STEP 12: 完成刷新时,需要发布对应的事件
finishRefresh();
} catch (BeansException ex) {
// Destroy already created singletons
// to avoid dangling resources.
destroyBeans();

// Reset 'active' flag.
cancelRefresh(ex);
...
} finally {
// Reset common introspection caches in Spring's core, since
// might not ever need metadata for singleton beans anymore
resetCommonCaches();
}
}
}
// 通过此方法来解析相关BeanDefinition, 还会完成其余附属功能
# obtainFreshBeanFactory()
@@AbstractRefreshableApplicationContext
// 如果之前有IoC容器,则销毁
# refreshBeanFactory()
// 创建IoC容器,也就是DefaultListableBeanFactory
# createBeanFactory()
// 设置工厂的属性:是否允许BeanDefinition覆盖和是否允许循环依赖
# customizeBeanFactory(beanFactory);
// 调用载入BeanDefinition的方法,在当前类中只定义了抽象的
// loadBeanDefinitions方法,具体的实现调用子类容器
# loadBeanDefinitions(beanFactory); //钩子方法
@@ AbstractXmlApplicationContext
# loadBeanDefinitions(DefaultListableBeanFactory beanFactory)
@@XmlBeanDefinitionReader
// XML Bean读取器调用其父类
// AbstractBeanDefinitionReader读取定位的资源
@@ AbstractBeanDefinitionReader
# loadBeanDefinitions(configLocations)
...
// 委派调用其子类
// XmlBeanDefinitionReader的方法,实现加载功能
# loadBeanDefinitions(Resource resource)
// 至此,回到上面1或者2的流程处理

2、注解方式BeanDefination注册流程

2.1 测试代码

1
2
java复制代码AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext("com.spring.test.ioc.annotation.po");
Teacher student = context.getBean(Teacher.class);

2.2 源码分析

1
2
3
4
5
6
java复制代码【AnnotationConfigApplicationContext】#构造方法
# scan(basePackages);
# refresh();
@@AbstractApplicationContext
# refresh()
// 解析源码见1.2说明

3、IOC容器启动核心流程(12步)

3.1 prepareRefresh刷新预处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码protected void prepareRefresh() {
// 1、这个方法设置context的启动日期。
this.startupDate = System.currentTimeMillis();

// 2、设置context当前的状态,是活动状态还是关闭状态。
this.closed.set(false);
this.active.set(true);

// 3、初始化context environment(上下文环境)中的占位符属性来源。
// 扩展覆盖protected void initPropertySources()方法,加载自定义环境属性值
// 如:getEnvironment().getSystemProperties().put("name","bobo");
initPropertySources();

// 4、验证所有必需的属性。
// 如:上一步getEnvironment().setRequiredProperties("username");
// 则在当前方法会进行校验,不存在则抛出异常
getEnvironment().validateRequiredProperties();

// 5、创建一些监听器事件的集合
this.earlyApplicationEvents = new LinkedHashSet<>();
}

3.2 obtainFreshBeanFactory创建默认工厂

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
java复制代码protected ConfigurableListableBeanFactory obtainFreshBeanFactory() {
// 主要是通过该方法完成IoC容器的刷新
refreshBeanFactory();
ConfigurableListableBeanFactory beanFactory = getBeanFactory();
return beanFactory;
}

protected final void refreshBeanFactory() throws BeansException {
// 1、如果之前有IoC容器,则销毁
if (hasBeanFactory()) {
// 销毁bean
destroyBeans();
// 关闭bean工厂
closeBeanFactory();
}
// 2、创建IoC容器,也就是DefaultListableBeanFactory
DefaultListableBeanFactory beanFactory = createBeanFactory();
beanFactory.setSerializationId(getId());
// 3、设置工厂的属性:是否允许BeanDefinition覆盖和是否允许循环依赖
customizeBeanFactory(beanFactory);
// 4、调用载入BeanDefinition的方法,在当前类中只定义了抽象的loadBeanDefinitions方法,
// 具体的实现调用子类容器如:AnnotationConfigWebApplicationContext、
// AbstractXmlApplicationContext
loadBeanDefinitions(beanFactory);
synchronized (this.beanFactoryMonitor) {
// 5、将创建好的bean工厂的引用交给的context来管理。
this.beanFactory = beanFactory;
}
}

3.3 prepareBeanFactory 配置预处理容器

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
java复制代码// 配置这个工厂的标准环境,比如context的类加载器和后处理器
protected void prepareBeanFactory(ConfigurableListableBeanFactory beanFactory) {
// 1、设置beanFactory的类加载器
beanFactory.setBeanClassLoader(getClassLoader());
// 2、设置属性解析器
beanFactory.setBeanExpressionResolver(new StandardBeanExpressionResolver(beanFactory.getBeanClassLoader()));
beanFactory.addPropertyEditorRegistrar(new
ResourceEditorRegistrar(this,getEnvironment()));

// 3、添加到后置处理器列表, 新创建的 ApplicationContextAwareProcessor
// 入参为当前 ApplicationContext, 为实现Aware接口的bean设置对应的对象
beanFactory.addBeanPostProcessor(new ApplicationContextAwareProcessor(this));
// 4、忽略自动属性装配/赋值,默认只有BeanFactoryAware被忽略,要忽略其他类型,需要单独设置
// 此处目的是为了交给用户自定义实现Aware功能,基于自定义后置处理器处理Bean依赖
// 忽略该接口的实现类中和接口setter方法入参类型相同的依赖。
// 这样的做法使得ApplicationContextAware和BeanFactoryAware中的ApplicationContext或 // BeanFactory依赖在自动装配时被忽略,而统一由框架设置依赖,如ApplicationContextAware接口 // 的设置会在ApplicationContextAwareProcessor类中完成.
beanFactory.ignoreDependencyInterface(EnvironmentAware.class);
beanFactory.ignoreDependencyInterface(EmbeddedValueResolverAware.class);
beanFactory.ignoreDependencyInterface(ResourceLoaderAware.class);
beanFactory.ignoreDependencyInterface(ApplicationEventPublisherAware.class);
beanFactory.ignoreDependencyInterface(MessageSourceAware.class);
beanFactory.ignoreDependencyInterface(ApplicationContextAware.class);

// 5、该方法的主要作用就是指定该类型接口,如果外部要注入该类型接口的对象,则会注入我们
// 指定的对象,而不会去管其他接口实现类(因为多个实现类不知道注入哪个,在这里明确指定)
beanFactory.registerResolvableDependency(BeanFactory.class, beanFactory);
beanFactory.registerResolvableDependency(ResourceLoader.class, this);
beanFactory.registerResolvableDependency(ApplicationEventPublisher.class, this);
beanFactory.registerResolvableDependency(ApplicationContext.class, this);

// 6、用于处理实现ApplicationListener接口的bean,bean初始化后添加监听
beanFactory.addBeanPostProcessor(new ApplicationListenerDetector(this));

// 7、增加对AspectJ的支持
if (beanFactory.containsBean(LOAD_TIME_WEAVER_BEAN_NAME)) {
beanFactory.addBeanPostProcessor(new
LoadTimeWeaverAwareProcessor(beanFactory));
beanFactory.setTempClassLoader(new
ContextTypeMatchClassLoader(beanFactory.getBeanClassLoader()));
}
// 8、给容器中注册Singleton组件,ConfigurableEnviroment,systemProperties,
// systemEnvironment,添加到singletonObjects(ConcurrentHashMap类型)中
if (!beanFactory.containsLocalBean(ENVIRONMENT_BEAN_NAME)) {
beanFactory.registerSingleton(ENVIRONMENT_BEAN_NAME, getEnvironment());
}
if (!beanFactory.containsLocalBean(SYSTEM_PROPERTIES_BEAN_NAME)) {
beanFactory.registerSingleton(SYSTEM_PROPERTIES_BEAN_NAME,
getEnvironment().getSystemProperties());
}
if (!beanFactory.containsLocalBean(SYSTEM_ENVIRONMENT_BEAN_NAME)) {
beanFactory.registerSingleton(SYSTEM_ENVIRONMENT_BEAN_NAME,
getEnvironment().getSystemEnvironment());
}
}

3.4 postProcessBeanFactory 重写自定义修改bean工厂方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码// 因为此方法的参数是BeanFactory,所以我们可以重写此方法,然后针对beanFactory进行一些修改。
@Override
protected void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
// ServletContextAwareProcessor中拿到应用上下文持有的servletContext引用和servletConfig引用
// 1.添加ServletContextAwareProcessor处理器
beanFactory.addBeanPostProcessor(new
ServletContextAwareProcessor(this.servletContext, this.servletConfig));
// 在自动注入时忽略指定的依赖接口,通常被应用上下文用来注册以其他方式解析的依赖项
beanFactory.ignoreDependencyInterface(ServletContextAware.class);
beanFactory.ignoreDependencyInterface(ServletConfigAware.class);

// 2.注册web应用的scopes
WebApplicationContextUtils.registerWebApplicationScopes(beanFactory,
this.servletContext);

// 3.注册和环境有关的beans
WebApplicationContextUtils.registerEnvironmentBeans(beanFactory, this.servletContext, this.servletConfig);
}

3.5 invokeBeanFactoryPostProcessors BeanFactory处理器

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
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
java复制代码protected void invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory 	
beanFactory) {
// 1.getBeanFactoryPostProcessors(): 拿到当前应用上下文beanFactoryPostProcessors
// 变量中的值,默认为空的,除非自己手动调用 context.addBeanFactoryPostProcessor
// (beanFactoryPostProcessor)完成自定义添加
// 2.invokeBeanFactoryPostProcessors: 实例化并调用所有已注册的BeanFactoryPostProcessor
PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(beanFactory,
getBeanFactoryPostProcessors());

if (beanFactory.getTempClassLoader() == null &&
beanFactory.containsBean(LOAD_TIME_WEAVER_BEAN_NAME)) {
beanFactory.addBeanPostProcessor(new LoadTimeWeaverAwareProcessor(beanFactory));
beanFactory.setTempClassLoader(new
ContextTypeMatchClassLoader(beanFactory.getBeanClassLoader()));
}
}

public static void invokeBeanFactoryPostProcessors(
ConfigurableListableBeanFactory beanFactory, List<BeanFactoryPostProcessor>
beanFactoryPostProcessors) {
Set<String> processedBeans = new HashSet<String>();
// 1. beanFactory为DefaultListableBeanFactory, DefaultListableBeanFactory实现了
// BeanDefinitionRegistry接口,判定为true
if (beanFactory instanceof BeanDefinitionRegistry) {
BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory;
// 1.1 保存普通的BeanFactoryPostProcessor
List<BeanFactoryPostProcessor> regularPostProcessors = new
LinkedList<BeanFactoryPostProcessor>();
// 1.2 保存BeanDefinitionRegistryPostProcessor
List<BeanDefinitionRegistryPostProcessor> registryProcessors = new
LinkedList<BeanDefinitionRegistryPostProcessor>();
// 2. 遍历入参中beanFactoryPostProcessors, 将BeanDefinitionRegistryPostProcessor
// 和普通BeanFactoryPostProcessor区分开
for (BeanFactoryPostProcessor postProcessor : beanFactoryPostProcessors) {
if (postProcessor instanceof BeanDefinitionRegistryPostProcessor) {
// 2.1. 如果是BeanDefinitionRegistryPostProcessor
BeanDefinitionRegistryPostProcessor registryProcessor =
(BeanDefinitionRegistryPostProcessor) postProcessor;
// 2.2. 执行BeanDefinitionRegistryPostProcessor接口的
// postProcessBeanDefinitionRegistry方法
registryProcessor.postProcessBeanDefinitionRegistry(registry);
// 2.3. 添加到registryProcessors(用于最后执行postProcessBeanFactory方法)
registryProcessors.add(registryProcessor);
} else {
// 3.1. 普通的BeanFactoryPostProcessor添加到regularPostProcessors
// (用于最后执行postProcessBeanFactory方法)
regularPostProcessors.add(postProcessor);
}
}

// 保存本次要执行的所有BeanDefinitionRegistryPostProcessor
List<BeanDefinitionRegistryPostProcessor> currentRegistryProcessors = new
ArrayList<BeanDefinitionRegistryPostProcessor>();

// 4. 找出所有实现PriorityOrdered接口的BeanDefinitionRegistryPostProcessor实现类
// 找出所有实现BeanDefinitionRegistryPostProcessor接口的Bean的beanName
String[] postProcessorNames = beanFactory.getBeanNamesForType
(BeanDefinitionRegistryPostProcessor.class, true, false);

// 5. 遍历postProcessorNames
for (String ppName : postProcessorNames) {
// 5.1. 判定是否实现了PriorityOrdered接口
if (beanFactory.isTypeMatch(ppName, PriorityOrdered.class)) {
// 5.2. 获取ppName对应的bean实例, 添加到currentRegistryProcessors中,
// beanFactory.getBean方法会触发创建ppName对应的bean实例
currentRegistryProcessors.add(beanFactory.getBean(ppName,
BeanDefinitionRegistryPostProcessor.class));
// 5.3. 将实现类名添加到processedBeans,防止重复
processedBeans.add(ppName);
}
}
// 6. 进行排序(根据是否实现PriorityOrdered、Ordered接口和order值来排序)
sortPostProcessors(currentRegistryProcessors, beanFactory);
// 7. 添加到registryProcessors(用于最后执行postProcessBeanFactory方法)
registryProcessors.addAll(currentRegistryProcessors);
// 8. 遍历currentRegistryProcessors, 执行postProcessBeanDefinitionRegistry方法
invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry);
// 9. 清空currentRegistryProcessors
currentRegistryProcessors.clear();

// 10. 找出所有实现BeanDefinitionRegistryPostProcessor接口的类,
// 重复查找是因为执行完上面的BeanDefinitionRegistryPostProcessor,
// 可能会新增了其他的BeanDefinitionRegistryPostProcessor
postProcessorNames = beanFactory.getBeanNamesForType
(BeanDefinitionRegistryPostProcessor.class, true, false);
for (String ppName : postProcessorNames) {
// 校验是否实现了Ordered接口,并且还未执行过
if (!processedBeans.contains(ppName) && beanFactory.isTypeMatch(ppName,
Ordered.class)) {
currentRegistryProcessors.add(beanFactory.getBean(ppName,
BeanDefinitionRegistryPostProcessor.class));
processedBeans.add(ppName);
}
}
sortPostProcessors(currentRegistryProcessors, beanFactory);
registryProcessors.addAll(currentRegistryProcessors);
// 11. 遍历currentRegistryProcessors, 执行postProcessBeanDefinitionRegistry方法
invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry);
currentRegistryProcessors.clear();

// 12. 最后查找所有剩下的BeanDefinitionRegistryPostProcessors
boolean reiterate = true;
while (reiterate) {
reiterate = false;
// 12.1 找出所有实现BeanDefinitionRegistryPostProcessor接口的类
postProcessorNames = beanFactory.getBeanNamesForType
(BeanDefinitionRegistryPostProcessor.class, true, false);
for (String ppName : postProcessorNames) {
// 12.2 跳过已经执行过的
if (!processedBeans.contains(ppName)) {
currentRegistryProcessors.add(beanFactory.getBean(ppName,
BeanDefinitionRegistryPostProcessor.class));
processedBeans.add(ppName);
// 12.3 如果有BeanDefinitionRegistryPostProcessor被执行,
// 则有可能会产生新的BeanDefinitionRegistryPostProcessor,
// 因此这边将reiterate赋值为true, 代表需要再循环查找一次
reiterate = true;
}
}
sortPostProcessors(currentRegistryProcessors, beanFactory);
registryProcessors.addAll(currentRegistryProcessors);
// 13. 遍历currentRegistryProcessors, 执行postProcessBeanDefinitionRegistry方法
invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors,
registry);
currentRegistryProcessors.clear();
}

// 14. 调用所有BeanDefinitionRegistryPostProcessor的postProcessBeanFactory方法
// (BeanDefinitionRegistryPostProcessor继承自BeanFactoryPostProcessor)
invokeBeanFactoryPostProcessors(registryProcessors, beanFactory);
// 15. 调用入参beanFactoryPostProcessors中的普通BeanFactoryPostProcessor的
// postProcessBeanFactory方法
invokeBeanFactoryPostProcessors(regularPostProcessors, beanFactory);
} else {
// 直接调用传过来的BeanDefinitionRegistryPostProcessor的postProcessBeanFactory方法
invokeBeanFactoryPostProcessors(beanFactoryPostProcessors, beanFactory);
}

// 到这里入参beanFactoryPostProcessors和容器中的所BeanDefinitionRegistryPostProcessor
// 已经全部处理完毕,下面开始处理容器中的所有BeanFactoryPostProcessor

// 16.找出所有实现BeanFactoryPostProcessor接口的类
String[] postProcessorNames =
beanFactory.getBeanNamesForType(BeanFactoryPostProcessor.class, true, false);
// 17. 保存实现了PriorityOrdered接口的BeanFactoryPostProcessor
List<BeanFactoryPostProcessor> priorityOrderedPostProcessors = new
ArrayList<BeanFactoryPostProcessor>();
// 保存实现了Ordered接口的BeanFactoryPostProcessor的beanName
List<String> orderedPostProcessorNames = new ArrayList<String>();
// 保存普通BeanFactoryPostProcessor的beanName
List<String> nonOrderedPostProcessorNames = new ArrayList<String>();
// 18. 遍历postProcessorNames, 将BeanFactoryPostProcessor
// 按实现PriorityOrdered、实现Ordered接口、普通三种区分开
for (String ppName : postProcessorNames) {
if (processedBeans.contains(ppName)) {
// 18.1 跳过已经执行过的
} else if (beanFactory.isTypeMatch(ppName, PriorityOrdered.class)) {
// 18.2 添加实现了PriorityOrdered接口的BeanFactoryPostProcessor
priorityOrderedPostProcessors.add(beanFactory.getBean(ppName,
BeanFactoryPostProcessor.class));
} else if (beanFactory.isTypeMatch(ppName, Ordered.class)) {
// 18.3 添加实现了Ordered接口的BeanFactoryPostProcessor的beanName
orderedPostProcessorNames.add(ppName);
} else {
// 18.4 添加剩下的普通BeanFactoryPostProcessor的beanName
nonOrderedPostProcessorNames.add(ppName);
}
}

// 19. 对priorityOrderedPostProcessors排序
sortPostProcessors(priorityOrderedPostProcessors, beanFactory);
// 20. 调用所有实现PriorityOrdered接口的BeanFactoryPostProcessor
// 遍历priorityOrderedPostProcessors, 执行postProcessBeanFactory方法
invokeBeanFactoryPostProcessors(priorityOrderedPostProcessors, beanFactory);

// 21. 调用所有实现Ordered接口的BeanFactoryPostProcessor
List<BeanFactoryPostProcessor> orderedPostProcessors = new
ArrayList<BeanFactoryPostProcessor>();
for (String postProcessorName : orderedPostProcessorNames) {
// 21.1 获取postProcessorName对应的bean实例, 添加到orderedPostProcessors, 准备执行
orderedPostProcessors.add(beanFactory.getBean(postProcessorName,
BeanFactoryPostProcessor.class));
}
// 21.2 对orderedPostProcessors排序
sortPostProcessors(orderedPostProcessors, beanFactory);
// 21.3 遍历orderedPostProcessors, 执行postProcessBeanFactory方法
invokeBeanFactoryPostProcessors(orderedPostProcessors, beanFactory);

// 22.调用所有剩下的BeanFactoryPostProcessor
List<BeanFactoryPostProcessor> nonOrderedPostProcessors = new
ArrayList<BeanFactoryPostProcessor>();
for (String postProcessorName : nonOrderedPostProcessorNames) {
// 22.1 获取postProcessorName对应的bean实例, 添加到nonOrderedPostProcessors
nonOrderedPostProcessors.add(beanFactory.getBean(postProcessorName,
BeanFactoryPostProcessor.class));
}
// 22.2 遍历nonOrderedPostProcessors, 执行postProcessBeanFactory方法
invokeBeanFactoryPostProcessors(nonOrderedPostProcessors, beanFactory);

// 23. 清除元数据缓存(mergedBeanDefinitions、allBeanNamesByType、
// singletonBeanNamesByType),
// 因为后处理器可能已经修改了原始元数据,例如: 替换值中的占位符
beanFactory.clearMetadataCache();
}

3.6 registerBeanPostProcessors 注册后置处理器

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
java复制代码// 注册用来拦截bean创建的BeanPostProcessor bean.这个方法需要在所有的application bean初始化之前调、
// 用。把这个注册的任务委托给了PostProcessorRegistrationDelegate来完成。
protected void registerBeanPostProcessors(ConfigurableListableBeanFactory beanFactory) {
PostProcessorRegistrationDelegate.registerBeanPostProcessors(beanFactory, this);
}

public static void registerBeanPostProcessors(ConfigurableListableBeanFactory
beanFactory,AbstractApplicationContext applicationContext) {
// 1. 查找所有实现BeanPostProcessor的后置处理器名称
String[] postProcessorNames = beanFactory.getBeanNamesForType
(BeanPostProcessor.class, true, false);
// 2. 计算处理器总数
int beanProcessorTargetCount = beanFactory.getBeanPostProcessorCount() + 1 +
postProcessorNames.length;
beanFactory.addBeanPostProcessor(new BeanPostProcessorChecker(beanFactory,
beanProcessorTargetCount));

// 3. 对所有的BeanPostProcessor按照PriorityOrdered、Ordered和普通的进行分组存储
List<BeanPostProcessor> priorityOrderedPostProcessors = new ArrayList<>();
List<BeanPostProcessor> internalPostProcessors = new ArrayList<>();
List<String> orderedPostProcessorNames = new ArrayList<>();
List<String> nonOrderedPostProcessorNames = new ArrayList<>();
for (String ppName : postProcessorNames) {
if (beanFactory.isTypeMatch(ppName, PriorityOrdered.class)) {
BeanPostProcessor pp = beanFactory.getBean(ppName,
BeanPostProcessor.class);
priorityOrderedPostProcessors.add(pp);
if (pp instanceof MergedBeanDefinitionPostProcessor) {
internalPostProcessors.add(pp);
}
}
else if (beanFactory.isTypeMatch(ppName, Ordered.class)) {
orderedPostProcessorNames.add(ppName);
}
else {
nonOrderedPostProcessorNames.add(ppName);
}
}

// 4. 排序并注册所有实现了PriorityOrdered的后置处理器
sortPostProcessors(priorityOrderedPostProcessors, beanFactory);
registerBeanPostProcessors(beanFactory, priorityOrderedPostProcessors);

// 5. 排序并注册所有实现了Ordered的后置处理器
List<BeanPostProcessor> orderedPostProcessors = new ArrayList<>();
for (String ppName : orderedPostProcessorNames) {
BeanPostProcessor pp = beanFactory.getBean(ppName, BeanPostProcessor.class);
orderedPostProcessors.add(pp);
if (pp instanceof MergedBeanDefinitionPostProcessor) {
internalPostProcessors.add(pp);
}
}
sortPostProcessors(orderedPostProcessors, beanFactory);
registerBeanPostProcessors(beanFactory, orderedPostProcessors);

// 6. 注册所有普通的后置处理器
List<BeanPostProcessor> nonOrderedPostProcessors = new ArrayList<>();
for (String ppName : nonOrderedPostProcessorNames) {
BeanPostProcessor pp = beanFactory.getBean(ppName, BeanPostProcessor.class);
nonOrderedPostProcessors.add(pp);
if (pp instanceof MergedBeanDefinitionPostProcessor) {
internalPostProcessors.add(pp);
}
}
registerBeanPostProcessors(beanFactory, nonOrderedPostProcessors);

// 排序并注册所有实现了MergedBeanDefinitionPostProcessor的应用处理器
sortPostProcessors(internalPostProcessors, beanFactory);
registerBeanPostProcessors(beanFactory, internalPostProcessors);

// 重新注册用来自动探测内部ApplicationListener的post-processor,
// 这样可以将他们移到处理器链条的末尾
beanFactory.addBeanPostProcessor(new
ApplicationListenerDetector(applicationContext));
}

3.7 initMessageSource 初始化消息源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码// 初始化MessageSource接口的一个实现类。这个接口提供了消息处理功能。主要用于国际化/i18n。
// 此部分代码不做详细阐述
protected void initMessageSource() {
ConfigurableListableBeanFactory beanFactory = getBeanFactory();
if (beanFactory.containsLocalBean(MESSAGE_SOURCE_BEAN_NAME)) {
this.messageSource = beanFactory.getBean(MESSAGE_SOURCE_BEAN_NAME,
MessageSource.class);
if (this.parent != null && this.messageSource
instanceof HierarchicalMessageSource) {
HierarchicalMessageSource hms = (HierarchicalMessageSource)
this.messageSource;
if (hms.getParentMessageSource() == null) {
hms.setParentMessageSource(getInternalParentMessageSource());
}
}
}
else {
DelegatingMessageSource dms = new DelegatingMessageSource();
dms.setParentMessageSource(getInternalParentMessageSource());
this.messageSource = dms;
beanFactory.registerSingleton(MESSAGE_SOURCE_BEAN_NAME, this.messageSource);
}
}

3.8 initApplicationEventMulticaster 初始化应用事件广播器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码// 为context初始化一个事件监听多路广播器(ApplicationEventMulticaster)
protected void initApplicationEventMulticaster() {
ConfigurableListableBeanFactory beanFactory = getBeanFactory();
// 检查是否给context配了一个ApplicationEventMulticaster实现类
if (beanFactory.containsLocalBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME)) {
this.applicationEventMulticaster =
beanFactory.getBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME,
ApplicationEventMulticaster.class);
}
else {
// 如果没有,就使用默认的实现类 SimpleApplicationEventMulticaster
this.applicationEventMulticaster = new
SimpleApplicationEventMulticaster(beanFactory);
beanFactory.registerSingleton(APPLICATION_EVENT_MULTICASTER_BEAN_NAME,
this.applicationEventMulticaster);
}
}

3.9 onRefresh初始化特殊的Bean

1
2
3
4
5
6
7
8
9
10
java复制代码// 在AbstractApplicationContext的子类中初始化其他特殊的bean。其实就是初始化ThemeSource接口的实例。
// 这个方法需要在所有单例bean初始化之前调用。
protected void onRefresh() throws BeansException {
// 是个空壳方法,在AnnotationApplicationContex上下文中没有实现,
// 可能在spring后面的版本会去扩展。
}
@Override
protected void onRefresh() {
this.themeSource = UiApplicationContextUtils.initThemeSource(this);
}

3.10 registerListeners 注册应用监听器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
java复制代码// 注册应用的监听器。就是注册实现了ApplicationListener接口的监听器bean,这些监听器是注册到
// ApplicationEventMulticaster中的。这不会影响到其它监听器bean。在注册完以后,还会将其前期的事件发布
// 给相匹配的监听器。
protected void registerListeners() {
// 获取实现了ApplicationListener的监听器
for (ApplicationListener<?> listener : getApplicationListeners()) {
// 手动注册的监听器绑定到广播器
getApplicationEventMulticaster().addApplicationListener(listener);
}

// 取到监听器的名称
String[] listenerBeanNames = getBeanNamesForType(ApplicationListener.class, true,
// 设置到广播器 false);
for (String listenerBeanName : listenerBeanNames) {
getApplicationEventMulticaster().addApplicationListenerBean(listenerBeanName);
}

// 如果存在早期应用事件,发布事件消息
Set<ApplicationEvent> earlyEventsToProcess = this.earlyApplicationEvents;
this.earlyApplicationEvents = null;
if (earlyEventsToProcess != null) {
for (ApplicationEvent earlyEvent : earlyEventsToProcess) {
getApplicationEventMulticaster().multicastEvent(earlyEvent);
}
}
}

3.11 finishBeanFactoryInitialization 实例化非懒加载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
java复制代码// 完成bean工厂的初始化工作。这一步非常复杂,也非常重要,涉及到了bean的创建。第二步中只是完成了
// BeanDefinition的定义、解析、处理、注册。但是还没有初始化bean实例。这一步 实例化剩余的单例bean(非懒
// 加载方式). 注意事项:Bean的IoC、DI和AOP都是发生在此步骤
protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory
beanFactory) {
// 1. 初始化此上下文的转换服务
...
// 2. 如果beanFactory之前没有注册嵌入值解析器,则注册默认的嵌入值解析器:
// 主要用于注解属性值的解析。
...
// 3. 初始化LoadTimeWeaverAware Bean实例对象
String[] weaverAwareNames = beanFactory.getBeanNamesForType
(LoadTimeWeaverAware.class, false, false);
for (String weaverAwareName : weaverAwareNames) {
getBean(weaverAwareName);
}

// Stop using the temporary ClassLoader for type matching.
beanFactory.setTempClassLoader(null);

// 4. 冻结所有bean定义,注册的bean定义不会被修改
// 或进一步后处理,因为马上要创建 Bean 实例对象了
beanFactory.freezeConfiguration();

// 实例化单例Bean, 将在第四节【Bean初始化流程分析】进行讲解
beanFactory.preInstantiateSingletons();
}

3.12 finishRefresh 完成刷新发布应用事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
java复制代码// 完成context的刷新。主要是调用LifecycleProcessor的onRefresh()方法,并且发布事件
//(ContextRefreshedEvent)。
protected void finishRefresh() {
// Clear context-level resource caches (such as ASM metadata from scanning).
clearResourceCaches();

// 为此上下文初始化生命周期处理器
// 初始化所有的 LifecycleProcessor
// 在 Spring 中还提供了 Lifecycle 接口, Lifecycle 中包含 start/stop 方法,
// 实现此接口后 Spring 会保证在启动的时候调用其 start 方法开始生命周期,
// 并在 Spring 关闭的时候调用 stop 方法来结束生命周期,通常用来配置后台程序,在启动后一直运行
// (如对 MQ 进行轮询等)。ApplicationContext 的初始化最后正是保证了这一功能的实现。
initLifecycleProcessor();

// 将刷新完成事件广播到生命周期处理器
getLifecycleProcessor().onRefresh();

// 广播上下文刷新完成事件
publishEvent(new ContextRefreshedEvent(this));

// 将spring容器注册到LiveBeansView
// 和MBeanServer和MBean有关的。相当于把当前容器上下文,注册到MBeanServer里面去。
// MBeanServer持有了容器的引用,就可以拿到容器的所有内容,也就让Spring支持到了MBean的相关功能
LiveBeansView.registerApplicationContext(this);
}

​

4、预告: 下一篇幅将对Spring Bean的实例化过程源码部分以及AOP容器的创建及调用流程做详细分析。

本文转载自: 掘金

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

最清晰的SSM整合 最清晰的SSM整合

发表于 2021-07-31

最清晰的SSM整合

Spring 、SpringMVC、Mybatis 三大框架整合相关配置

三层分开配置

ssm整合资源关系
maven 的 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
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
xml复制代码<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>

<!-- https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
</dependency>

<!-- https://mvnrepository.com/artifact/javax.servlet.jsp/javax.servlet.jsp-api -->
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>javax.servlet.jsp-api</artifactId>
<version>2.2.1</version>
<scope>provided</scope>
</dependency>
<!--数据库驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<!-- 数据库连接池 -->
<dependency>
<groupId>com.mchange</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.5.2</version>
</dependency>

<!--Servlet - JSP -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
</dependency>
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>jsp-api</artifactId>
<version>2.2</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>

<!--Mybatis-->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.2</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>2.0.2</version>
</dependency>

<!--Spring-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.1.9.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.1.9.RELEASE</version>
</dependency>

<!--Lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.10</version>
</dependency>
</dependencies>
<build>
<!-- build配置resources,防止我们资源导出失败-->
<resources>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/*.properties</include>
<include>**/*.xml</include>
</includes>
<filtering>false</filtering>
</resource>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.properties</include>
<include>**/*.xml</include>
</includes>
<filtering>false</filtering>
</resource>
</resources>
</build>
  1. 首先是总的配置文件 applicationContext.xml
1
2
3
4
5
6
7
8
9
10
11
java复制代码<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">

<import resource="spring-dao.xml"/>
<import resource="spring-service.xml"/>
<import resource="spring-mvc.xml"/>

</beans>
  1. MVC 层配置 spring-mvc.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/mvc
https://www.springframework.org/schema/mvc/spring-mvc.xsd">

<!-- 配置SpringMVC -->
<!-- 1.开启SpringMVC注解驱动 -->
<mvc:annotation-driven />
<!-- 2.静态资源默认servlet配置-->
<mvc:default-servlet-handler/>

<!-- 3.配置jsp 显示ViewResolver视图解析器 -->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="viewClass" value="org.springframework.web.servlet.view.JstlView" />
<property name="prefix" value="/WEB-INF/jsp/" />
<property name="suffix" value=".jsp" />
</bean>

<!-- 4.扫描web相关的bean -->
<context:component-scan base-package="tech.zhjweb.controller" />

</beans>
  1. web.xml配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
xml复制代码<?xml version="1.0" encoding="utf-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1"
metadata-complete="true">

<!--DispatcherServlet-->
<servlet>
<servlet-name>DispatcherServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<!--加载总配置文件-->
<param-value>classpath:applicationContext.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>DispatcherServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>

<!--encodingFilter-->
<filter>
<filter-name>encodingFilter</filter-name>
<filter-class>
org.springframework.web.filter.CharacterEncodingFilter
</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>utf-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>encodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

<!--Session过期时间-->
<session-config>
<session-timeout>15</session-timeout>
</session-config>

</web-app>
  1. Servics 层配置 spring-service.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">

<!-- 扫描service相关的bean -->
<context:component-scan base-package="tech.zhjweb.service" />

<!--BookServiceImpl注入到IOC容器中-->
<bean id="BookServiceImpl" class="tech.zhjweb.service.BookServiceImpl">
<property name="bookMapper" ref="bookMapper"/>
</bean>

<!-- 配置事务管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!-- 注入数据库连接池 -->
<property name="dataSource" ref="dataSource" />
</bean>

<!-- AOP事物支持-->

</beans>
  1. Dao 层配置 spring-dao.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd">

<!-- 配置整合mybatis -->
<!-- 1.关联数据库文件 -->
<context:property-placeholder location="classpath:database.properties"/>

<!-- 2.数据库连接池 -->
<!--数据库连接池
dbcp 半自动化操作 不能自动连接
c3p0 自动化操作(自动的加载配置文件 并且设置到对象里面)
druid: (企业用得很多)
hikari:
-->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<!-- 配置连接池属性 -->
<property name="driverClass" value="${jdbc.driver}"/>
<property name="jdbcUrl" value="${jdbc.url}"/>
<property name="user" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>

<!-- c3p0连接池的私有属性 -->
<property name="maxPoolSize" value="30"/>
<property name="minPoolSize" value="10"/>
<!-- 关闭连接后不自动commit -->
<property name="autoCommitOnClose" value="false"/>
<!-- 获取连接超时时间 -->
<property name="checkoutTimeout" value="10000"/>
<!-- 当获取连接失败重试次数 -->
<property name="acquireRetryAttempts" value="2"/>
</bean>

<!-- 3.配置SqlSessionFactory对象 -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<!-- 注入数据库连接池 -->
<property name="dataSource" ref="dataSource"/>
<!-- 配置MyBaties全局配置文件:mybatis-config.xml -->
<property name="configLocation" value="classpath:mybatis-config.xml"/>
</bean>

<!-- 4.配置扫描Dao接口包,动态实现Dao接口注入到spring容器中 -->
<!--解释 :https://www.cnblogs.com/jpfss/p/7799806.html-->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<!-- 注入sqlSessionFactory -->
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
<!-- 给出需要扫描Dao接口包 -->
<property name="basePackage" value="tech.zhjweb.dao"/>
</bean>

</beans>
  1. Mybatis 层配置 mybatis-config.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
xml复制代码<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- 配置数据源 spring做-->
<typeAliases>
<package name="tech.zhjweb.pojo"/>
</typeAliases>

<mappers>
<mapper class="tech.zhjweb.dao.BookMapper"/>
</mappers>
</configuration>
  1. 数据库配置 database.properties
1
2
3
4
5
xml复制代码jdbc.driver=com.mysql.jdbc.Driver
# url 后参数 安全连接、字符集 注:MySQL8.0要加时区设置
jdbc.url=jdbc:mysql://localhost:3306/ssmbuild?useSSL=true&useUnicode=true&characterEncoding=utf8
jdbc.username=root
jdbc.password=root
  1. 日志配置 log4j.properties
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
xml复制代码log4j.rootLogger=INFO,Console,File  
#定义日志输出目的地为控制台
log4j.appender.Console=org.apache.log4j.ConsoleAppender
log4j.appender.Console.Target=System.out
#可以灵活地指定日志输出格式,下面一行是指定具体的格式
log4j.appender.Console.layout = org.apache.log4j.PatternLayout
log4j.appender.Console.layout.ConversionPattern=[%c] - %m%n

#文件大小到达指定尺寸的时候产生一个新的文件
log4j.appender.File = org.apache.log4j.RollingFileAppender
#指定输出目录
log4j.appender.File.File = ./logs/ssm.log
#定义文件最大大小
log4j.appender.File.MaxFileSize = 10MB
# 输出所以日志,如果换成DEBUG表示输出DEBUG以上级别日志
log4j.appender.File.Threshold = ALL
log4j.appender.File.layout = org.apache.log4j.PatternLayout
log4j.appender.File.layout.ConversionPattern =[%p] [%d{yyyy-MM-dd HH\:mm\:ss}][%c]%m%n

本文转载自: 掘金

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

springmvc 防止表单重复提交

发表于 2021-07-31

最近在本地开发测试的时候,遇到一个表单重复提交的现象。
因为网络延迟的问题,我点击了两次提交按钮,数据库里生成了两条记录。其实这种现象以前也有遇到过,一般都是提交后把按钮置灰,无法再次提交,这是很常见的客户端处理的方式。
但是这不是从根本上解决问题,虽然客户端解决了多次提交的问题,但是接口中依旧存在着问题。假设我们不是从客户端提交,而是被其他的系统调用,当遇到网络延迟,系统补偿的时候,还会遇到这种问题

1、通过session中的token验证

  1. 初始化页面时生成一个唯一token,将其放在页面隐藏域和session中
  2. 拦截器拦截请求,校验来自页面请求中的token与session中的token是否一致
  3. 判断,如果一致则提交成功并移除session中的token,不一致则说明重复提交并记录日志

步骤1:创建自定义注解

1
2
3
4
5
6
java复制代码@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Token {
boolean save() default false;
boolean remove() default false;
}

步骤2:创建自定义拦截器(@slf4j是lombok的注解)

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
java复制代码@Slf4j
public class RepeatSubmitInterceptor extends HandlerInterceptorAdapter {

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
HandlerMethod handlerMethod = null;
try {
handlerMethod = (HandlerMethod)handler;
} catch (Exception e) {
return true;
}
Method method = handlerMethod.getMethod();

Token token = method.getAnnotation(Token.class);
if(token != null ){
boolean saveSession = token.save();
if(saveSession){
request.getSession(true).setAttribute("token", UUID.randomUUID());
}

boolean removeSession = token.remove();
if(removeSession){
if(isRepeatSubmitSession(request)){
log.info("repeat submit session :" + request.getServletPath());
response.sendRedirect("/error/409");
return false;
}
request.getSession(true).removeAttribute("token");
}
}
return true;
}

private boolean isRepeatSubmitSession(HttpServletRequest request){
String sessionToken = String.valueOf(request.getSession(true).getAttribute("token") == null ? "" : request.getSession(true).getAttribute("token"));
String clientToken = String.valueOf(request.getParameter("token") == null ? "" : request.getParameter("token"));
if(sessionToken == null || sessionToken.equals("")){
return true;
}
if(clientToken == null || clientToken.equals("")){
return true;
}
if(!sessionToken.equals(clientToken)){
return true;
}
return false;
}

}

步骤3:将自定义拦截器添加到配置文件

1
2
3
4
xml复制代码<mvc:interceptor>
<mvc:mapping path="/**"/>
<bean class="com.chinagdn.base.common.interceptor.RepeatSubmitInterceptor"/>
</mvc:interceptor>

使用案例

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码	//save = true 用于生成token
@Token(save = true)
@RequestMapping(value = "save", method = RequestMethod.GET)
public String save(LoginUser loginUser, Model model) throws Exception {
return "sys/user/edit";
}
//remove = true 用于验证token
@Token(remove = true)
@RequestMapping(value = "save", method = RequestMethod.POST)
public String save(@Valid LoginUser loginUser, Errors errors, RedirectAttributes redirectAttributes, Model model) throws Exception {
//.....
}

jsp页面隐藏域添加token

1
html复制代码	<input type="hidden" name="token" value="${sessionScope.token}">

2、通过当前用户上一次请求的url和参数验证重复提交

  1. 拦截器拦截请求,将上一次请求的url和参数和这次的对比
  2. 判断,是否一致说明重复提交并记录日志
    步骤1:创建自定义注解
1
2
3
4
5
java复制代码@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SameUrlData {

}

步骤2:创建自定义拦截器

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

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
//是否有 SameUrlData 注解
SameUrlData annotation = method.getAnnotation(SameUrlData.class);
if (annotation != null) {
if (repeatDataValidator(request)) {//如果重复相同数据
response.sendRedirect("/error/409");
return false;
} else {
return true;
}
}
return true;
} else {
return super.preHandle(request, response, handler);
}
}

/**
* 验证同一个url数据是否相同提交 ,相同返回true
* @param httpServletRequest
* @return
*/
private boolean repeatDataValidator(HttpServletRequest httpServletRequest) {
String params = JsonMapper.toJsonString(httpServletRequest.getParameterMap());
String url = httpServletRequest.getRequestURI();
Map<String, String> map = new HashMap<>();
map.put(url, params);
String nowUrlParams = map.toString();//

Object preUrlParams = httpServletRequest.getSession().getAttribute("repeatData");
if (preUrlParams == null) { //如果上一个数据为null,表示还没有访问页面
httpServletRequest.getSession().setAttribute("repeatData", nowUrlParams);
return false;
} else { //否则,已经访问过页面
if (preUrlParams.toString().equals(nowUrlParams)) { //如果上次url+数据和本次url+数据相同,则表示城府添加数据
return true;
} else { //如果上次 url+数据 和本次url加数据不同,则不是重复提交
httpServletRequest.getSession().setAttribute("repeatData", nowUrlParams);
return false;
}
}
}

}

步骤3:将自定义拦截器添加到配置文件

1
2
3
4
xml复制代码<mvc:interceptor>
<mvc:mapping path="/**"/>
<bean class="com.chinagdn.base.common.interceptor.SameUrlDataInterceptor"/>
</mvc:interceptor>

使用案例

1
2
3
4
5
6
java复制代码	//在controller层使用  @SameUrlData 注解即可
@SameUrlData
@RequestMapping(value = "save", method = RequestMethod.POST)
public String save(@Valid LoginUser loginUser, Errors errors, RedirectAttributes redirectAttributes, Model model) throws Exception {
//.....
}

本文转载自: 掘金

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

spring cloud使用zookeeper作为服务注册中

发表于 2021-07-31

现在用spring cloud框架搭建微服务已经是比较流行了,毕竟这个是spring提供给我们的一个框架,可以很好配合spring boot使用,很快就可以搭建出一个微服务来,大大的减少了工作量。

这里就谈一下,各个微服务之间要怎么的互相调用。这个就需要一个有服务注册和发现功能的服务,所有的微服务注册到这个服务上面,这样的话,所有的微服务就客户互相的调用了。

服务的注册和发现功能,目前有两种形式:一种是客户端发现(eureka),比较流行。另一种是服务端发现(zookeeper或consul)。

这里主要谈一下使用zookeeper作为服务注册中心与配置中心。

一、服务注册中心

1.安装zookeeper

解压zookeeper:
1
复制代码tar -xvf zookeeper-3.4.10.tar.gz

)

启动zookeeper:
1
2
3
4
5
bash复制代码cd zookeeper-3.4.10
cd conf
cp zoo_sample.cfg zoo.cfg
cd ../bin
sh zkServer.sh start

)

2.引入依赖

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

)

3.创建微服务,并以zookeeper作为服务注册中心。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typescript复制代码package com.garlic.springcloudzookeeperclientapp;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

/**
* zookeeper作为服务注册中心,应用启动类
* @author llsydn
*/
@SpringBootApplication
@EnableDiscoveryClient
public class SpringCloudZookeeperClientAppApplication {
public static void main(String[] args) {
SpringApplication.run(SpringCloudZookeeperClientAppApplication.class, args);
}
}

)

对应的application.properties

1
2
3
4
5
6
7
8
ini复制代码## 配置应用名称
spring.application.name=spring-cloud-zookeeper-client-app
## 配置服务端口
server.port=8080
## 关闭安全控制
management.security.enabled=false
## 配置zookeeper地址
spring.cloud.zookeeper.connect-string=localhost:2181

)

使用DiscoveryClient获取注册服务列表

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
kotlin复制代码package com.garlic.springcloudzookeeperclientapp.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

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

/**
* 提供Rest Api,根据实例名称获取注册服务列表
*
* @author llsydn
* @create 2018-5-11 20:47
*/
@RestController
@RequestMapping("/zookeeper")
public class ZookeeperController {
@Value("${spring.application.name}")
private String instanceName;
private final DiscoveryClient discoveryClient;
@Autowired
public ZookeeperController(DiscoveryClient discoveryClient) {
this.discoveryClient = discoveryClient;
}
@GetMapping
public String hello() {
return "Hello,Zookeeper.";
}
@GetMapping("/services")
public List<String> serviceUrl() {
List<ServiceInstance> list = discoveryClient.getInstances(instanceName);
List<String> services = new ArrayList<>();
if (list != null && list.size() > 0 ) {
list.forEach(serviceInstance -> {
services.add(serviceInstance.getUri().toString());
});
}
return services;
}
}

)

注:可以启动不同的实例,此处我启动了端口8080与8081两个实例,然后使用端点可以查询到所注册的服务列表。同样可以通过zookeeper相关命令查询到说注册的服务列表

1
2
3
4
5
6
7
8
9
csharp复制代码sh zkCli.sh
[services, zookeeper]
[zk: localhost:2181(CONNECTED) 1] ls /
[services, zookeeper]
[zk: localhost:2181(CONNECTED) 2] ls /services
[spring-cloud-zookeeper-client-app]
[zk: localhost:2181(CONNECTED) 3] ls /services/spring-cloud-zookeeper-client-app
[be61af3d-ffc2-4ffc-932c-26bc0f94971c, bcf21ece-e9e1-4a91-b985-8828688370b8]
[zk: localhost:2181(CONNECTED) 4]

)

二、配置中心

1.使用zkCli创建配置信息

1
2
3
4
5
6
7
8
9
10
11
12
13
bash复制代码[zk: localhost:2181(CONNECTED) 27] create /config ""
Created /config
[zk: localhost:2181(CONNECTED) 28] create /config ""
Created /config/garlic
[zk: localhost:2181(CONNECTED) 29] create /config/garlic/name "default"
Created /config/garlic/name
[zk: localhost:2181(CONNECTED) 30] set /config/garlic-dev/name "dev"
Node does not exist: /config/garlic-dev/name
[zk: localhost:2181(CONNECTED) 31] create /config/garlic-dev/name "dev"
Created /config/garlic-dev/name
[zk: localhost:2181(CONNECTED) 32] create /config/garlic-test/name "test"
Created /config/garlic-test/name
[zk: localhost:2181(CONNECTED) 33] create /config/garlic-prod/name "prod"

)

2.使用controller来动态获取zookeeper配置中心的数据

2.1maven依赖

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

)

2.2bootstrap.properties

1
2
3
4
5
6
7
8
ini复制代码## 启用zookeeper作为配置中心
spring.cloud.zookeeper.config.enabled = true
## 配置根路径
spring.cloud.zookeeper.config.root = config
## 配置默认上下文
spring.cloud.zookeeper.config.defaultContext = garlic
## 配置profile分隔符
spring.cloud.zookeeper.config.profileSeparator = -

)

spring.cloud.zookeeper.config.root对应zkCli创建的config目录,defaultContext对应创建的garlic或garlic-* 目录,根据profile来确定获取dev还是test或者prod配置

2.3application.properties

1
2
3
4
5
6
7
ini复制代码## 配置应用名称
spring.application.name=spring-cloud-zookeeper-config-app
## 配置服务端口
server.port=10000
## 关闭安全控制
management.security.enabled=false
spring.profiles.active=dev

)

2.4使用controller来动态获取zookeeper配置中心的数据

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
kotlin复制代码package com.garlic.springcloudzookeeperconfigapp.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.core.env.Environment;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
* 提供Rest Api,获取配置在zookeeper中的配置信息
*
* @author llsydn
* @create 2018-5-11 20:50
*/
@RestController
@RequestMapping("/zookeeper")
@RefreshScope // 必须添加,否则不会自动刷新name的值
public class ZookeeperController {
@Autowired
private Environment environment;
@Value("${name}")
private String name;
@GetMapping
public String hello() {
return "Hello, " + name;
}
@GetMapping("/env")
public String test() {
String name = environment.getProperty("name");
System.out.println(name);
return "Hello," + name;
}
}

)

启动配置实例之后,可以通过zkCli修改garlic下name的值,然后通过访问端点来查看值是否变化。至此,使用zookeeper作为服务注册中心与配置中心就完成了,我们可以通过使用zookeeper作为配置中心,然后使用zuul作为API网关,配置动态路由,为服务提供者,配置数据库连接相关信息。

本文转载自: 掘金

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

java实现利用阿里巴巴开源的easyexcel进行对exc

发表于 2021-07-30

​

目录

前言

一、引入easyexcel的maven

二、读取excel代码示例

1、bean需要和excel的列对应

demo

2、Controller层

demo

3、service层

demo

4、listener层

demo

5、dao层

demo

二、导出excel代码示例


前言

平常的功能大家应该都会用到导入导出excel的功能,比如通过读excel的方式将excel的数据导入到数据库中。当然实现的方式有很多,今天我介绍的是利用阿里开源的easyexcel项目来完成功能。大家也可以自己看easyexcel的文档进行开发。当然这里因为刚好工作的时候用到了,所以将自己写的demo分享出来,大家就可以更快节省时间完成功能,大家可以参考,也可以直接拿来用,实际在你们自己的开发过程当中适当修改。

EasyExcel是一个基于Java的简单、省内存的读写Excel的开源项目。在尽可能节约内存的情况下支持读写百M的Excel。

github地址:github.com/alibaba/eas…

文档地址:alibaba-easyexcel.github.io/

一、引入easyexcel的maven

1
2
3
4
5
6
7
8
9
10
xml复制代码        <dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>2.1.1</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.8</version>
</dependency>

首先springBoot项目结构大家看一下,除了最简单的三层架构,dao、service和controller,这里还需要用到listener

)​

二、读取excel代码示例

1、bean需要和excel的列对应

可以通过index和name将bean和excel的列对应起来,但是官方不建议index和name同时夹杂在一起用,顾名思义,在一个bean中要么就是用index,要么就是用name对应

eg:@ExcelProperty(index = 2)

eg:@ExcelProperty(“日期标题”)

demo

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
typescript复制代码package com.xxx.xxxx.xxxx;


import com.alibaba.excel.annotation.ExcelProperty;
import lombok.Data;
/**
* Created by yjl on 2019/12/25.
*/
@Data
public class RoomMonitorCoverDataBean {


private String TIME_ID; //时间

@ExcelProperty(index = 0)
private String PROV_NAME; //省

@ExcelProperty(index = 1)
private String AREA_NAME; //地市

@DateTimeFormat("yyyy年MM月")
@ExcelProperty(index = 2)
private String MONTH_DESC; //月份,DateTimeFormat表示对日期进行格式化,不要的可以去掉




public String getTIME_ID() {
return TIME_ID;
}

public void setTIME_ID(String TIME_ID) {
this.TIME_ID = TIME_ID;
}

public String getPROV_NAME() {
return PROV_NAME;
}

public void setPROV_NAME(String PROV_NAME) {
this.PROV_NAME = PROV_NAME;
}

public String getAREA_NAME() {
return AREA_NAME;
}

public void setAREA_NAME(String AREA_NAME) {
this.AREA_NAME = AREA_NAME;
}

public String getMONTH_DESC() {
return MONTH_DESC;
}

public void setMONTH_DESC(String MONTH_DESC) {
this.MONTH_DESC = MONTH_DESC;
}


}

2、Controller层

很简单,就注入service,引用service的方法,我们在service里进行入参的判断或者其他业务处理等,这里我就直接放一个impl的了,interface是一样的名字,放一个没有实现的方法就可以了,当然你也可以不要那个interface

demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
typescript复制代码@RequestMapping("/mrePortController")
public class MrePortControllerCSVImpl implements IMrePortControllerCSV {


@Autowired
private MrePortServiceCSV mrePortServiceCSV;


@RequestMapping(value = "/saveRoomMonitorCoverData", method = RequestMethod.POST)
@Override
public JSONObject saveRoomMonitorCoverData(MultipartFile file, String timeType, String userArea) {

return mrePortServiceCSV.saveRoomMonitorCoverData(file, timeType,userArea);

}




}

3、service层

这里我们进行逻辑判断,可以对入参或者其他一些东西进行判断,

主要功能是

1
2
3
4
scss复制代码// 将excel表中的数据入库
// 有个很重要的点 xxxxxListener 不能被spring管理,要每次读取excel都要new,然后里面用到spring可以构造方法传进去
// 这里 需要指定读用哪个class去读,默认读取第一个sheet 文件流会自动关闭
EasyExcel.read(file.getInputStream(), RoomMonitorCoverDataBean.class, new RoomMonitorCoverDataListener(mrePortDao,tableName,timeType)).sheet().doRead();

demo

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
typescript复制代码@Component
public class MrePortServiceCSVImpl implements MrePortServiceCSV {

@Autowired
private MrePortDao mrePortDao;


private Logger logger = LoggerFactory.getLogger(this.getClass());

@Override
public JSONObject saveRoomMonitorCoverData(MultipartFile file, String timeType,String userArea) {
JSONObject jo = new JSONObject();

try {


//可以进行必传参数校验等
//这里省略不写了

//导入示例:excel名称:小小鱼儿小小林_浙江201912.xlsx
String fileName = file.getOriginalFilename().replaceAll("\.xlsx|\.xls","");
System.out.println("file.getName():"+file.getName()+",file.getOriginalFilename():"+file.getOriginalFilename());
String[] split = fileName.split("_");
String file_tableName = split[0]; //文件名称表名称
String file_areaNameAndDate = split[1]; //文件名称地市和月份 eg:浙江201912
//String file_version = split[2]; //文件名称版本号
if ("小小鱼儿小小林".equals(file_tableName)){ //只能上传该名称的文件
String file_areaName = file_areaNameAndDate.substring(0, 2); //文件名称地市
String file_date = file_areaNameAndDate.substring(2); //文件名称月份

if (userArea.equals(file_areaName)){ //只能上传自己权限范围内的地市数据
//Long currentDate = Long.parseLong(getCurrentDate("yyyyMM"));
String currentDate = getCurrentDate("yyyyMM");

if (currentDate.equals(file_date)){ //只能上传当月的数据
String tableName = "xxxxxxxxx";

// 将excel表中的数据入库
// 有个很重要的点 xxxxxListener 不能被spring管理,要每次读取excel都要new,然后里面用到spring可以构造方法传进去
// 这里 需要指定读用哪个class去读,默认读取第一个sheet 文件流会自动关闭
EasyExcel.read(file.getInputStream(), RoomMonitorCoverDataBean.class, new RoomMonitorCoverDataListener(mrePortDao,tableName,timeType)).sheet().doRead();

jo.put("msg","导入成功");
jo.put("resultCode",0);
jo.put("response","success");

}else {
jo.put("msg","您只能上传当月"+currentDate+"的数据");
jo.put("resultCode",-1);
jo.put("response","");
return jo;
}


}else {
jo.put("msg","您暂时没有权限上传"+file_areaName+"的数据");
jo.put("resultCode",-1);
jo.put("response","");
return jo;
}

}else {
jo.put("msg","请选择《小小鱼儿小小林》文件再上传");
jo.put("resultCode",-1);
jo.put("response","");
return jo;
}


}catch (IOException i){
i.printStackTrace();
jo.put("msg","IOException,请重试");
jo.put("resultCode",-1);
jo.put("response","");
} catch (Exception e){
e.printStackTrace();
jo.put("msg","导入异常,请重试");
jo.put("resultCode",-1);
jo.put("response","");
}

return jo;

}




}

4、listener层

注意这个不是mvc的三层架构,这里加listener是因为easyexcel需要用到,实际上解析excel的过程就是在这里实行的,如果要对解析出来的每一行数据或者其中的一列进行另外的判断或者做其他处理,是要到这个类里面写逻辑

RoomMonitorCoverDataListener.java

demo

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
arduino复制代码/**
* Created by yjl on 2019/12/27.公众号:zygxsq
*/
public class RoomMonitorCoverDataListener extends AnalysisEventListener<RoomMonitorCoverDataBean> {

private Logger LOGGER = LoggerFactory.getLogger(this.getClass());
private static final int BATCH_COUNT = 1000; //每隔1000条存数据库,然后清理list ,方便内存回收
List<RoomMonitorCoverDataBean> list = new ArrayList<RoomMonitorCoverDataBean>();


MrePortDao mrePortDao;
String timeType;
String tableName;
/**
* 如果使用了spring,请使用有参构造方法。每次创建Listener的时候需要把spring管理的类传进来
*/
public RoomMonitorCoverDataListener(){}
public RoomMonitorCoverDataListener(MrePortDao mrePortDao) {
this.mrePortDao = mrePortDao;
}
public RoomMonitorCoverDataListener(MrePortDao mrePortDao,String timeType) {
this.mrePortDao = mrePortDao;
this.timeType = timeType;
}
public RoomMonitorCoverDataListener(MrePortDao mrePortDao,String tableName,String timeType) {
this.mrePortDao = mrePortDao;
this.tableName = tableName;
this.timeType = timeType;
}

/**
* 这个每一条数据解析都会来调用
* @param data
* @param analysisContext
*/
@Override
public void invoke(RoomMonitorCoverDataBean data, AnalysisContext analysisContext) {
LOGGER.info("解析到一条数据:{}", JSON.toJSONString(data));
//------------------begin------------------------
// 这里对excel表里的数据进行逻辑处理、筛选等,如果不需要对表里的数据进行处理,这里可以删除
String area_name = data.getAREA_NAME();
String month_desc = data.getMONTH_DESC();
//先删除表里已经存在的,再保存新的
mrePortDao.deleteTableData(tableName,timeType," AREA_NAME='"+area_name+"' AND MONTH_DESC='"+month_desc+"' ");
//-------------------end-----------------------
list.add(data);
// 达到BATCH_COUNT了,需要去存储一次数据库,防止数据几万条数据在内存,容易OOM
if (list.size() >= BATCH_COUNT) {
saveData();
// 存储完成清理 list
list.clear();
}
}

/**
* 所有数据解析完成了 都会来调用
* @param analysisContext
*/
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
// 这里也要保存数据,确保最后遗留的数据也存储到数据库
saveData();
LOGGER.info("所有数据解析完成!");
}


/**
* 加上存储数据库
*/
private void saveData() {
LOGGER.info("{}条数据,开始存储数据库!", list.size());
mrePortDao.saveRoomMonitorCoverData(list,timeType);
LOGGER.info("存储数据库成功!");
}
}

5、dao层

因为我们这里还是用的springJPA,所以大家可以看到上面我处理excel中的数据,要对原来数据库中的数据进行删除的时候,还是用拼接的那种格式,这里大家要结合自己的框架进行修改,相信作为技术人,你们改改还是可以的,如果有遇到不会改的,可以关注我的公众号:zygxsq,公众号里有我的微信方式,可以联系我

demo

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
java复制代码/**
* Created by yjl on 2019/12/20.
*/
@Component
public class MrePortDaoImpl implements MrePortDao {
@PersistenceContext
private EntityManager em;

@Autowired
protected JdbcTemplate jdbcTemplate;

private Logger logger = LoggerFactory.getLogger(this.getClass());


@Transactional
@Override
public Integer saveRoomMonitorCoverData(List<RoomMonitorCoverDataBean> params, String timeType) {
String tableName = "xxxxxxx";
StringBuilder sql = new StringBuilder();
sql.append("INSERT INTO ")
.append(tableName)
.append("(")

.append("TIME_ID,") //时间
.append("PROV_NAME,") //省
.append("AREA_NAME,") //地市
.append("MONTH_DESC,") //月份

.append(")VALUES(")
.append("?,?,?,?")

.append(")")
;


int[] len = jdbcTemplate.batchUpdate(sql.toString(), new BatchPreparedStatementSetter() {

@Override
public void setValues(PreparedStatement ps, int i) throws SQLException {
RoomMonitorCoverDataBean pojo = params.get(i);
String month_desc = pojo.getMONTH_DESC();
String time_id = month_desc.replaceAll("\D","")+"000000";
ps.setString(1,time_id); //时间
ps.setString(2,pojo.getPROV_NAME()); //省
ps.setString(3,pojo.getAREA_NAME()); //地市
ps.setString(4,month_desc); //月份

}

@Override
public int getBatchSize() {
return params.size();
}
});
return len.length;
}






@Transactional
@Modifying
@Query
@Override
public Integer deleteTableData(String tableName, String timeType, String whereInfo) {

String sql="DELETE FROM "+tableName+" WHERE 1=1 AND "+whereInfo;
logger.info("删除表"+tableName+"里"+whereInfo+"的数据sql:"+sql);
int deleteCnt = em.createNativeQuery(sql).executeUpdate();
logger.info("成功删除"+tableName+"表中"+whereInfo+"的"+deleteCnt+"条数据");
return deleteCnt;
}
}

二、导出excel代码示例

待更新

​

本文转载自: 掘金

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

Dubbo30 Filter 过滤链

发表于 2021-07-30

首先分享之前的所有文章 , 欢迎点赞收藏转发三连下次一定 >>>> 😜😜😜

文章合集 : 🎁 juejin.cn/post/694164…

Github : 👉 github.com/black-ant

CASE 备份 : 👉 gitee.com/antblack/ca…

一 . 前言

上一篇看完了 Dubbo 3.0 的 Server 端接收 , 这一篇来看一下 Dubbo 的过滤链 . 过滤链也是整个流程中非常重要的一环 .

二 . 处理的起点

1
2
3
4
java复制代码// 调用逻辑
C- ChannelEventRunnable # run : 调用的起点 , 此处接受到消息
C- ExchangeHandler # reply : 发起 Invoke 处理流程
C- FilterChainBuilder # invoke : 开启 Invoke Filter 链构建流程

2.1 Invoke 发起流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码// 上一篇文章已经看过了 , 通过构建一个 ExchangeHandlerAdapter 发起 reply 调用
private ExchangeHandler requestHandler = new ExchangeHandlerAdapter() {

@Override
public CompletableFuture<Object> reply(ExchangeChannel channel, Object message) throws RemotingException {
// 只保留核心逻辑
Invocation inv = (Invocation) message;
// 此处实际调用 FilterChainBuilder
Invoker<?> invoker = getInvoker(channel, inv);
//...........
Result result = invoker.invoke(inv);

}
}

2.2 Filter 链构建流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
java复制代码C- FilterChainBuilder
// 这里构建 Filter 链 , 发起调用操作
public Result invoke(Invocation invocation) throws RpcException {
Result asyncResult;
try {
// filter 链的处理
asyncResult = filter.invoke(nextNode, invocation);
} catch (Exception e) {
// 省略 ,主要是进行 Listerner 的监听 , 又在玩异步
throw e;
} finally {

}
return asyncResult.whenCompleteWithContext((r, t) -> {
// 处理完成后 ,还是要通知监听器
if (filter instanceof ListenableFilter) {
ListenableFilter listenableFilter = ((ListenableFilter) filter);
Filter.Listener listener = listenableFilter.listener(invocation);
try {

if (listener != null) {
if (t == null) {
listener.onResponse(r, originalInvoker, invocation);
} else {
listener.onError(t, originalInvoker, invocation);
}
}
} finally {
listenableFilter.removeListener(invocation);
}

} else if (filter instanceof FILTER.Listener) {
// 对 BaseFilter.Listener 接口进行处理
FILTER.Listener listener = (FILTER.Listener) filter;
if (t == null) {
listener.onResponse(r, originalInvoker, invocation);
} else {
listener.onError(t, originalInvoker, invocation);
}
}
});
}

2.3 Filter 链的调用结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
java复制代码// 来看一下 FilterChainBuilder , 他的核心对象是其中的一个内部类
class FilterChainNode<T, TYPE extends Invoker<T>, FILTER extends BaseFilter> implements Invoker<T>{
// 源 Invoke 类型
TYPE originalInvoker;
// 下一个 invoke 节点
Invoker<T> nextNode;
// 当前的 filter 对象
FILTER filter;
}

// 下面看一下Filter 的构建的地方 -> DefaultFilterChainBuilder
@Override
public <T> Invoker<T> buildInvokerChain(final Invoker<T> originalInvoker, String key, String group) {
Invoker<T> last = originalInvoker;
// 加载外部 Extension
List<Filter> filters = ExtensionLoader.getExtensionLoader(Filter.class).getActivateExtension(originalInvoker.getUrl(), key, group);

if (!filters.isEmpty()) {
for (int i = filters.size() - 1; i >= 0; i--) {
final Filter filter = filters.get(i);
final Invoker<T> next = last;
// 可以看到 , 这里循环创建了 FilterChainNode , 这里是责任链模式
last = new FilterChainNode<>(originalInvoker, next, filter);
}
}

return last;
}


// 这里是SPI 的加载方式 , 对应的 SPI 类为
dubbo-rpc-api/META-INF.dubbo.internal -> org.apache.dubbo.rpc.Filter

// 其中包含如下内容
echo=org.apache.dubbo.rpc.filter.EchoFilter
generic=org.apache.dubbo.rpc.filter.GenericFilter
genericimpl=org.apache.dubbo.rpc.filter.GenericImplFilter
token=org.apache.dubbo.rpc.filter.TokenFilter
accesslog=org.apache.dubbo.rpc.filter.AccessLogFilter
classloader=org.apache.dubbo.rpc.filter.ClassLoaderFilter
context=org.apache.dubbo.rpc.filter.ContextFilter
exception=org.apache.dubbo.rpc.filter.ExceptionFilter
executelimit=org.apache.dubbo.rpc.filter.ExecuteLimitFilter
deprecated=org.apache.dubbo.rpc.filter.DeprecatedFilter
compatible=org.apache.dubbo.rpc.filter.CompatibleFilter
timeout=org.apache.dubbo.rpc.filter.TimeoutFilter
tps=org.apache.dubbo.rpc.filter.TpsLimitFilter


// PS : Dubbo SPI 的加载方式类似 , 以后有时间再仔细看

SPI 可以看一下 @ juejin.cn/post/698945…

2.4 Dubbo 3.0 对比 Dubbo 2.0 的区别

2 者最大的区别在于 Filter 的机构变动 ,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码// Step 1 : Filter 结构
@SPI
public interface Filter extends BaseFilter {
}


// Step 2 : BaseFilter 结构
public interface BaseFilter {

// 始终调用实现中的invoke.invoke(),将请求移交给下一个筛选器节点
Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException;

interface Listener {
// 当调用结束后 ,根据情况选择是否调用
void onResponse(Result appResponse, Invoker<?> invoker, Invocation invocation);
void onError(Throwable t, Invoker<?> invoker, Invocation invocation);
}
}

// 这里接口里面套了一个接口 , 虽然知道可以这么用 ,但是从来没做过 , 原来还能这么用
至于实现的时候 , 和内部类使用方式一样 :
public class ExceptionFilter implements Filter, Filter.Listener

三 . Filter 链

Filter 体系结构

Filter-ListenableFilter.png

主要的 Filter 的列表如下 :

  • EchoFilter
  • ClassLoaderFilter
  • GenericFilter
  • ContextFilter
  • ExceptionFilter
  • MonitorFilter
  • TimeoutFilter
  • TraceFilter

3.1 EchoFilter

ECHO 表示回声 , 该类的作用主要是为了检测服务是否可用

1
2
3
4
5
6
7
8
java复制代码public Result invoke(Invoker<?> invoker, Invocation inv) throws RpcException {
// String $ECHO = "$echo";
if (inv.getMethodName().equals($ECHO) && inv.getArguments() != null && inv.getArguments().length == 1) {
// 这里直接构建了一个异步 Result 返回 , 回声服务只为了检测服务是否可用
return AsyncRpcResult.newDefaultAsyncResult(inv.getArguments()[0], inv);
}
return invoker.invoke(inv);
}

3.2 ClassLoaderFilter

该 Filter 主要为了对 ClassLoader 进行二次处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码// 将当前执行线程类 ClassLoader 设置为服务接口的类 ClassLoader
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
ClassLoader ocl = Thread.currentThread().getContextClassLoader();
// 对 ClassLoader 进行切换
Thread.currentThread().setContextClassLoader(invoker.getInterface().getClassLoader());
try {
return invoker.invoke(invocation);
} finally {
// 这里重新放入最初的 ClassLoad
Thread.currentThread().setContextClassLoader(ocl);
}
}

// PS : 这里进行切换的原因猜测应该和双亲委派有关 , 整个体系的 classLoader 不能处理外部 jar 加载的类
// 当出现这种多 ClassLoader 时, 又因为双亲委派的机制 , 父加载类没有加载成功 , 又最终的 classLoader 加载了 , 这里就需要进行相关的切换

3.3 GenericFilter

GenericFilter 同样实现了2个接口 : Filter, Filter.Listener , 这就意味着会有 Response 处理 ,

TODO : GenericFilter 这个类主要用途还没搞清楚 ,而且这个类太长了, 后面搞清楚了再开一个单章

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
java复制代码GenericFilter implements Filter, Filter.Listener

@Override
public Result invoke(Invoker<?> invoker, Invocation inv) throws RpcException {
// String $INVOKE = "$invoke";
// String $INVOKE_ASYNC = "$invokeAsync";
if ((inv.getMethodName().equals($INVOKE) || inv.getMethodName().equals($INVOKE_ASYNC))
&& inv.getArguments() != null
&& inv.getArguments().length == 3
&& !GenericService.class.isAssignableFrom(invoker.getInterface())) {
//................
}
return invoker.invoke(inv);
}


@Override
public void onResponse(Result appResponse, Invoker<?> invoker, Invocation inv) {
if ((inv.getMethodName().equals($INVOKE) || inv.getMethodName().equals($INVOKE_ASYNC))
&& inv.getArguments() != null
&& inv.getArguments().length == 3
&& !GenericService.class.isAssignableFrom(invoker.getInterface())) {

//................
}
}

@Override
public void onError(Throwable t, Invoker<?> invoker, Invocation invocation) {

}

3.4 ContextFilter

ContextFilter 主要是对上下文进行处理 , 上下文中存放的是当前调用过程中所需的环境信息

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
java复制代码static {
UNLOADING_KEYS = new HashSet<>(128);
UNLOADING_KEYS.add(PATH_KEY);
UNLOADING_KEYS.add(INTERFACE_KEY);
UNLOADING_KEYS.add(GROUP_KEY);
UNLOADING_KEYS.add(VERSION_KEY);
UNLOADING_KEYS.add(DUBBO_VERSION_KEY);
UNLOADING_KEYS.add(TOKEN_KEY);
UNLOADING_KEYS.add(TIMEOUT_KEY);
UNLOADING_KEYS.add(TIMEOUT_ATTACHMENT_KEY);

// 删除async属性以避免传递给以下调用链
UNLOADING_KEYS.add(ASYNC_KEY);
UNLOADING_KEYS.add(TAG_KEY);
UNLOADING_KEYS.add(FORCE_USE_TAG);
}

@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
Map<String, Object> attachments = invocation.getObjectAttachments();
if (attachments != null) {
Map<String, Object> newAttach = new HashMap<>(attachments.size());
// attachments 中主要为请求的元数据信息
// {"input":"265","dubbo":"2.0.2","path":"org.apache.dubbo.metadata.MetadataService","version":"1.0.0","group":"dubbo-demo-annotation-provider"}
for (Map.Entry<String, Object> entry : attachments.entrySet()) {
String key = entry.getKey();
// ["path","_TO","group","dubbo.tag","version","dubbo.force.tag","dubbo","interface","timeout","token","async"]
if (!UNLOADING_KEYS.contains(key)) {
// 收集在排除之外的属性
newAttach.put(key, entry.getValue());
}
}
attachments = newAttach;
}

// 设置 invoke 到 ServiceContext5 中
RpcContext.getServiceContext().setInvoker(invoker)
.setInvocation(invocation);

RpcContext context = RpcContext.getServerAttachment();

context.setLocalAddress(invoker.getUrl().getHost(), invoker.getUrl().getPort());

String remoteApplication = (String) invocation.getAttachment(REMOTE_APPLICATION_KEY);
if (StringUtils.isNotEmpty(remoteApplication)) {
RpcContext.getServiceContext().setRemoteApplicationName(remoteApplication);
} else {
RpcContext.getServiceContext().setRemoteApplicationName((String) context.getAttachment(REMOTE_APPLICATION_KEY));
}

// 调用超时时间 , 未配置为 -1
long timeout = RpcUtils.getTimeout(invocation, -1);
if (timeout != -1) {
// pass to next hop
RpcContext.getClientAttachment().setObjectAttachment(TIME_COUNTDOWN_KEY, TimeoutCountDown.newCountDown(timeout, TimeUnit.MILLISECONDS));
}

// 可能已经在这个过滤器之前添加了一些附件到RpcContext
if (attachments != null) {
if (context.getObjectAttachments() != null) {
// 此处会报前文的额外属性进行设置
context.getObjectAttachments().putAll(attachments);
} else {
context.setObjectAttachments(attachments);
}
}

if (invocation instanceof RpcInvocation) {
((RpcInvocation) invocation).setInvoker(invoker);
}

try {
context.clearAfterEachInvoke(false);
return invoker.invoke(invocation);
} finally {
context.clearAfterEachInvoke(true);
RpcContext.removeServerAttachment();
RpcContext.removeServiceContext();
// 对于异步场景,我们必须从当前线程中删除上下文,因此我们总是为同一线程的下一次调用创建一个新的RpcContext
RpcContext.getClientAttachment().removeAttachment(TIME_COUNTDOWN_KEY);
RpcContext.removeServerContext();
}
}

@Override
public void onResponse(Result appResponse, Invoker<?> invoker, Invocation invocation) {
// 将附件传递到结果
appResponse.addObjectAttachments(RpcContext.getServerContext().getObjectAttachments());
}

@Override
public void onError(Throwable t, Invoker<?> invoker, Invocation invocation) {

}

3.5 ExceptionFilter

ExceptionFilter 主要是针对 response 返回中的异常进行处理

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
java复制代码@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
return invoker.invoke(invocation);
}


@Override
public void onResponse(Result appResponse, Invoker<?> invoker, Invocation invocation) {
if (appResponse.hasException() && GenericService.class != invoker.getInterface()) {
try {
Throwable exception = appResponse.getException();

// 如果它被检查异常,直接抛出
if (!(exception instanceof RuntimeException) && (exception instanceof Exception)) {
return;
}
// 如果异常出现在签名中,直接抛出
try {
Method method = invoker.getInterface().getMethod(invocation.getMethodName(), invocation.getParameterTypes());
Class<?>[] exceptionClasses = method.getExceptionTypes();
for (Class<?> exceptionClass : exceptionClasses) {
if (exception.getClass().equals(exceptionClass)) {
return;
}
}
} catch (NoSuchMethodException e) {
return;
}

// .. 省略 : 对于在方法签名中未找到的异常,在服务器日志中打印ERROR消息


// 如果异常类和接口类在同一个jar文件中,则直接抛出
String serviceFile = ReflectUtils.getCodeBase(invoker.getInterface());
String exceptionFile = ReflectUtils.getCodeBase(exception.getClass());
if (serviceFile == null || exceptionFile == null || serviceFile.equals(exceptionFile)) {
return;
}
// 如果是JDK异常,直接抛出
String className = exception.getClass().getName();
if (className.startsWith("java.") || className.startsWith("javax.")) {
return;
}
// 如果是 Dubbo 异常,直接抛出
if (exception instanceof RpcException) {
return;
}

// 否则,用RuntimeException包装并返回给客户机
appResponse.setException(new RuntimeException(StringUtils.toString(exception)));
} catch (Throwable e) {

}
}
}

3.6 MonitorFilter

MonitorFilter 用于监控操作 ,他会 调用截取器,并将收集关于此调用的调用数据并将其发送到监视中心

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
java复制代码// Invoke 流程
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
if (invoker.getUrl().hasAttribute(MONITOR_KEY)) {
// private static final String MONITOR_FILTER_START_TIME = "monitor_filter_start_time";
// private static final String MONITOR_REMOTE_HOST_STORE = "monitor_remote_host_store";
invocation.put(MONITOR_FILTER_START_TIME, System.currentTimeMillis());
invocation.put(MONITOR_REMOTE_HOST_STORE, RpcContext.getServiceContext().getRemoteHost());
getConcurrent(invoker, invocation).incrementAndGet(); // count up
}
return invoker.invoke(invocation); // proceed invocation chain
}

// 主要的 Monitor 流程是以下流程
@Override
public void onResponse(Result result, Invoker<?> invoker, Invocation invocation) {
if (invoker.getUrl().hasAttribute(MONITOR_KEY)) {
collect(invoker, invocation, result,
(String) invocation.get(MONITOR_REMOTE_HOST_STORE),
(long) invocation.get(MONITOR_FILTER_START_TIME), false);

getConcurrent(invoker, invocation).decrementAndGet(); // count down
}
}

@Override
public void onError(Throwable t, Invoker<?> invoker, Invocation invocation) {
if (invoker.getUrl().hasAttribute(MONITOR_KEY)) {
collect(invoker, invocation, null, (String) invocation.get(MONITOR_REMOTE_HOST_STORE), (long) invocation.get(MONITOR_FILTER_START_TIME), true);
getConcurrent(invoker, invocation).decrementAndGet(); // count down
}
}
private void collect(Invoker<?> invoker, Invocation invocation, Result result, String remoteHost, long start, boolean error) {
try {
Object monitorUrl;
// 获取上文构建的 MONITOR_KEY
monitorUrl = invoker.getUrl().getAttribute(MONITOR_KEY);
if(monitorUrl instanceof URL) {
//
Monitor monitor = monitorFactory.getMonitor((URL) monitorUrl);
if (monitor == null) {
return;
}
// 创建数据的url
URL statisticsURL = createStatisticsUrl(invoker, invocation, result, remoteHost, start, error);
// 收集监控数据
monitor.collect(statisticsURL.toSerializableURL());
}
} catch (Throwable t) {
logger.warn("Failed to monitor count service " + invoker.getUrl() + ", cause: " + t.getMessage(), t);
}
}

// PS : 这里涉及到 MonitorService 对象 , 这里先不深入 ,只是过一下流程
C- MonitorService
- void collect(URL statistics) : 收集监测数据
- List<URL> lookup(URL query)

3.7 TimeoutFilter

对超时情况进行处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
return invoker.invoke(invocation);
}

@Override
public void onResponse(Result appResponse, Invoker<?> invoker, Invocation invocation) {
// String TIME_COUNTDOWN_KEY = "timeout-countdown";
Object obj = RpcContext.getClientAttachment().getObjectAttachment(TIME_COUNTDOWN_KEY);
if (obj != null) {
TimeoutCountDown countDown = (TimeoutCountDown) obj;
//
if (countDown.isExpired()) {
// // 在超时的情况下清除响应
((AppResponse) appResponse).clear();
// 这里仅仅是打印一个 log
if (logger.isWarnEnabled()) {
logger.warn("invoke timed out. method: " + invocation.getMethodName() + " arguments: " +
Arrays.toString(invocation.getArguments()) + " , url is " + invoker.getUrl() +
", invoke elapsed " + countDown.elapsedMillis() + " ms.");
}
}
}
}

3.8 TraceFilter

这个 Filter 主要对请求进行跟踪 , 该对象中存在一个集合用于维护 TRACES Key 和 Channel 的列表 , 在 invoke 调用完成后 , 会将日志发送到相关的 Channel 中

主要是监听 某个接口的任意方法或者某个方法 n次

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
java复制代码// 这里涉及到一个主要的集合
private static final ConcurrentMap<String, Set<Channel>> TRACERS = new ConcurrentHashMap<>();


@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {

// 常见的获取前后调用时间的方式
long start = System.currentTimeMillis();
Result result = invoker.invoke(invocation);
long end = System.currentTimeMillis();

// 如果 trace 存在
if (TRACERS.size() > 0) {

// 构建 trace key , 为代理接口.代理方法
String key = invoker.getInterface().getName() + "." + invocation.getMethodName();
Set<Channel> channels = TRACERS.get(key);
if (channels == null || channels.isEmpty()) {
key = invoker.getInterface().getName();
channels = TRACERS.get(key);
}

// 从集合中获取对应的 channel 集合
if (CollectionUtils.isNotEmpty(channels)) {
for (Channel channel : new ArrayList<>(channels)) {
// 如果 channel 为连接或者出现异常 , 都会从集合中移除该 channel
if (channel.isConnected()) {
try {
int max = 1;
Integer m = (Integer) channel.getAttribute(TRACE_MAX);
if (m != null) {
max = m;
}
int count = 0;
AtomicInteger c = (AtomicInteger) channel.getAttribute(TRACE_COUNT);
if (c == null) {
c = new AtomicInteger();
channel.setAttribute(TRACE_COUNT, c);
}
count = c.getAndIncrement();
// 此处涉及到2个重要的参数 , 他们分别表示监听的最大次数和当前监听的次数
// private static final String TRACE_MAX = "trace.max";
// private static final String TRACE_COUNT = "trace.count";
if (count < max) {
String prompt = channel.getUrl().getParameter(Constants.PROMPT_KEY, Constants.DEFAULT_PROMPT);
channel.send("\r\n" + RpcContext.getServiceContext().getRemoteAddress() + " -> "
+ invoker.getInterface().getName()
+ "." + invocation.getMethodName()
+ "(" + JSON.toJSONString(invocation.getArguments()) + ")" + " -> " + JSON.toJSONString(result.getValue())
+ "\r\nelapsed: " + (end - start) + " ms."
+ "\r\n\r\n" + prompt);
}
if (count >= max - 1) {
channels.remove(channel);
}
} catch (Throwable e) {
channels.remove(channel);
}
} else {
channels.remove(channel);
}
}
}
}
return result;
}

// PS : 此处补充一下 Trace 的添加流程 , TraceFilter 中提供了2个方法对 Trace Channel 进行管理
public static void addTracer(Class<?> type, String method, Channel channel, int max)
- String key = method != null && method.length() > 0 ? type.getName() + "." + method : type.getName();
?- 可用注意到 , tracer key j

public static void removeTracer(Class<?> type, String method, Channel channel)

// 而 Trace 主要的管理和处理类为 TraceTelnetHandler
public class TraceTelnetHandler implements TelnetHandler {

}

总结

  • Filter 链通过 SPI 加载
  • Filter 链通过 FilterChainBuilder 构建
  • Filter 中通过 invocation.getObjectAttachments() 获取属性

这篇文章深度不大 , 主要是由于笔者还在表层学习 , 第二由于这篇文章是一个概括性的文章 , 后续要慢慢的深入 Dubbo 3.0 相关

本文转载自: 掘金

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

Mac系统下mongodb的安装、启动 Mac系统下mong

发表于 2021-07-30

Mac系统下mongodb的安装配置

mongodb的安装

1.官网下载地址:www.mongodb.com/try/downloa…
进去之后选择社区版
截屏2021-07-30 下午3.17.07.png

2.将下载好的文件解压缩,重命名为mongodb,

3.打开访达按 ⬆️+Command+G ,在前往中输入/usr/local, 将重命名的文件放在/usr/local 目录下

4.配置环境变量,打开终端,输入“open -e .bash_profile”:
再打开的文件中输入“export PATH=${PATH}:/usr/local/mongodb/bin”

截屏2021-07-30 下午3.28.40.png

截屏2021-07-30 下午3.29.58.png
然后输入“source .bash_profile”,使配置立即生效。

截屏2021-07-30 下午3.33.04.png
5.输入mongod –version ,看到版本号则说明安装成功。

截屏2021-07-30 下午3.34.36.png

6.在/usr/local/mongodb 目录下创建两个文件夹: data 和 log。

7.在终端中,先进入data和log所在的目录,也就是/usr/local/mongodb ,然后输入”mongod –dbpath data –logpath log/mongod.log –logappend”,启动mongodb服务(当前终端不要关闭)

截屏2021-12-29 上午11.30.21.png
8.在新的终端中输入”mongo” 连接数据库

截屏2021-12-29 上午11.31.39.png

截屏2021-12-29 上午11.31.58.png

本文转载自: 掘金

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

1…586587588…956

开发者博客

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