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

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


  • 首页

  • 归档

  • 搜索

面试必备!TCP协议经典十五连问!

发表于 2021-07-11

前言

TCP协议是大厂面试必问的知识点。整理了15道非常经典的TCP面试题,希望大家都找到理想的offer呀

image.png

  • 公众号:捡田螺的小男孩
  • github地址,感谢每一颗star

1. 讲下TCP三次握手流程

开始客户端和服务器都处于CLOSED状态,然后服务端开始监听某个端口,进入LISTEN状态

  • 第一次握手(SYN=1, seq=x),发送完毕后,客户端进入 SYN_SEND 状态
  • 第二次握手(SYN=1, ACK=1, seq=y, ACKnum=x+1), 发送完毕后,服务器端进入 SYN_RCVD 状态。
  • 第三次握手(ACK=1,ACKnum=y+1),发送完毕后,客户端进入 ESTABLISHED 状态,当服务器端接收到这个包时,也进入 ESTABLISHED 状态,TCP 握手,即可以开始数据传输。

2.TCP握手为什么是三次,不能是两次?不能是四次?

TCP握手为什么是三次呢?为了方便理解,我们以谈恋爱为例子:两个人能走到一起,最重要的事情就是相爱,就是我爱你,并且我知道,你也爱我,接下来我们以此来模拟三次握手的过程:

为什么握手不能是两次呢?

如果只有两次握手,女孩子可能就不知道,她的那句我也爱你,男孩子是否收到,恋爱关系就不能愉快展开。

为什么握手不能是四次呢?

因为握手不能是四次呢?因为三次已经够了,三次已经能让双方都知道:你爱我,我也爱你。而四次就多余了。

3. 讲下TCP四次挥手过程

  1. 第一次挥手(FIN=1,seq=u),发送完毕后,客户端进入FIN_WAIT_1 状态
  2. 第二次挥手(ACK=1,ack=u+1,seq =v),发送完毕后,服务器端进入CLOSE_WAIT 状态,客户端接收到这个确认包之后,进入 FIN_WAIT_2 状态
  3. 第三次挥手(FIN=1,ACK1,seq=w,ack=u+1),发送完毕后,服务器端进入LAST_ACK 状态,等待来自客户端的最后一个ACK。
  4. 第四次挥手(ACK=1,seq=u+1,ack=w+1),客户端接收到来自服务器端的关闭请求,发送一个确认包,并进入 TIME_WAIT状态,等待了某个固定时间(两个最大段生命周期,2MSL,2 Maximum Segment Lifetime)之后,没有收到服务器端的 ACK ,认为服务器端已经正常关闭连接,于是自己也关闭连接,进入 CLOSED 状态。服务器端接收到这个确认包之后,关闭连接,进入 CLOSED 状态。

4. TCP挥手为什么需要四次呢?

举个例子吧!

小明和小红打电话聊天,通话差不多要结束时,小红说“我没啥要说的了”,小明回答“我知道了”。但是小明可能还会有要说的话,小红不能要求小明跟着自己的节奏结束通话,于是小明可能又叽叽歪歪说了一通,最后小明说“我说完了”,小红回答“知道了”,这样通话才算结束。

5. TIME-WAIT 状态为什么需要等待 2MSL

2MSL,2 Maximum Segment Lifetime,即两个最大段生命周期

  • 1个 MSL 保证四次挥手中主动关闭方最后的 ACK 报文能最终到达对端
  • 1个 MSL 保证对端没有收到 ACK 那么进行重传的 FIN 报文能够到达

6.TCP 和 UDP 的区别

  1. TCP面向连接((如打电话要先拨号建立连接);UDP是无连接的,即发送数据之前不需要建立连接。
  2. TCP要求安全性,提供可靠的服务,通过TCP连接传送的数据,不丢失、不重复、安全可靠。而UDP尽最大努力交付,即不保证可靠交付。
  3. TCP是点对点连接的,UDP一对一,一对多,多对多都可以
  4. TCP传输效率相对较低,而UDP传输效率高,它适用于对高速传输和实时性有较高的通信或广播通信。
  5. TCP适合用于网页,邮件等;UDP适合用于视频,语音广播等
  6. TCP面向字节流,UDP面向报文

7. TCP报文首部有哪些字段,说说其作用

  • 16位端口号:源端口号,主机该报文段是来自哪里;目标端口号,要传给哪个上层协议或应用程序
  • 32位序号:一次TCP通信(从TCP连接建立到断开)过程中某一个传输方向上的字节流的每个字节的编号。
  • 32位确认号:用作对另一方发送的tcp报文段的响应。其值是收到的TCP报文段的序号值加1。
  • 4位头部长度:表示tcp头部有多少个32bit字(4字节)。因为4位最大能标识15,所以TCP头部最长是60字节。
  • 6位标志位:URG(紧急指针是否有效),ACk(表示确认号是否有效),PSH(缓冲区尚未填满),RST(表示要求对方重新建立连接),SYN(建立连接消息标志接),FIN(表示告知对方本端要关闭连接了)
  • 16位窗口大小:是TCP流量控制的一个手段。这里说的窗口,指的是接收通告窗口。它告诉对方本端的TCP接收缓冲区还能容纳多少字节的数据,这样对方就可以控制发送数据的速度。
  • 16位校验和:由发送端填充,接收端对TCP报文段执行CRC算法以检验TCP报文段在传输过程中是否损坏。注意,这个校验不仅包括TCP头部,也包括数据部分。这也是TCP可靠传输的一个重要保障。
  • 16位紧急指针:一个正的偏移量。它和序号字段的值相加表示最后一个紧急数据的下一字节的序号。因此,确切地说,这个字段是紧急指针相对当前序号的偏移,不妨称之为紧急偏移。TCP的紧急指针是发送端向接收端发送紧急数据的方法。

8. TCP 是如何保证可靠性的

  • 首先,TCP的连接是基于三次握手,而断开则是四次挥手。确保连接和断开的可靠性。
  • 其次,TCP的可靠性,还体现在有状态;TCP会记录哪些数据发送了,哪些数据被接受了,哪些没有被接受,并且保证数据包按序到达,保证数据传输不出差错。
  • 再次,TCP的可靠性,还体现在可控制。它有报文校验、ACK应答、超时重传(发送方)、失序数据重传(接收方)、丢弃重复数据、流量控制(滑动窗口)和拥塞控制等机制。

9. TCP 重传机制

超时重传

TCP 为了实现可靠传输,实现了重传机制。最基本的重传机制,就是超时重传,即在发送数据报文时,设定一个定时器,每间隔一段时间,没有收到对方的ACK确认应答报文,就会重发该报文。

这个间隔时间,一般设置为多少呢?我们先来看下什么叫RTT(Round-Trip Time,往返时间)。

RTT就是,一个数据包从发出去到回来的时间,即数据包的一次往返时间。超时重传时间,就是Retransmission Timeout ,简称RTO。

RTO设置多久呢?

  • 如果RTO比较小,那很可能数据都没有丢失,就重发了,这会导致网络阻塞,会导致更多的超时出现。
  • 如果RTO比较大,等到花儿都谢了还是没有重发,那效果就不好了。

一般情况下,RTO略大于RTT,效果是最好的。一些小伙伴会问,超时时间有没有计算公式呢?有的!有个标准方法算RTO的公式,也叫Jacobson / Karels 算法。我们一起来看下计算RTO的公式

1. 先计算SRTT(计算平滑的RTT)

1
ini复制代码SRTT = (1 - α) * SRTT + α * RTT  //求 SRTT 的加权平均

2. 再计算RTTVAR (round-trip time variation)

1
scss复制代码RTTVAR = (1 - β) * RTTVAR + β * (|RTT - SRTT|) //计算 SRTT 与真实值的差距

3. 最终的RTO

1
ini复制代码RTO = µ * SRTT + ∂ * RTTVAR  =  SRTT + 4·RTTVAR

其中,α = 0.125,β = 0.25, μ = 1,∂ = 4,这些参数都是大量结果得出的最优参数。

但是,超时重传会有这些缺点:

  • 当一个报文段丢失时,会等待一定的超时周期然后才重传分组,增加了端到端的时延。
  • 当一个报文段丢失时,在其等待超时的过程中,可能会出现这种情况:其后的报文段已经被接收端接收但却迟迟得不到确认,发送端会认为也丢失了,从而引起不必要的重传,既浪费资源也浪费时间。

并且,TCP有个策略,就是超时时间间隔会加倍。超时重传需要等待很长时间。因此,还可以使用快速重传机制。

快速重传

快速重传机制,它不以时间驱动,而是以数据驱动。它基于接收端的反馈信息来引发重传。

一起来看下快速重传流程:

快速重传流程

发送端发送了 1,2,3,4,5,6 份数据:

  • 第一份 Seq=1 先送到了,于是就 Ack 回 2;
  • 第二份 Seq=2 也送到了,假设也正常,于是ACK 回 3;
  • 第三份 Seq=3 由于网络等其他原因,没送到;
  • 第四份 Seq=4 也送到了,但是因为Seq3没收到。所以ACK回3;
  • 后面的 Seq=4,5的也送到了,但是ACK还是回复3,因为Seq=3没收到。
  • 发送端连着收到三个重复冗余ACK=3的确认(实际上是4个,但是前面一个是正常的ACK,后面三个才是重复冗余的),便知道哪个报文段在传输过程中丢失了,于是在定时器过期之前,重传该报文段。
  • 最后,接收到收到了 Seq3,此时因为 Seq=4,5,6都收到了,于是ACK回7.

但快速重传还可能会有个问题:ACK只向发送端告知最大的有序报文段,到底是哪个报文丢失了呢?并不确定!那到底该重传多少个包呢?

是重传 Seq3 呢?还是重传 Seq3、Seq4、Seq5、Seq6 呢?因为发送端并不清楚这三个连续的 ACK3 是谁传回来的。

带选择确认的重传(SACK)

为了解决快速重传的问题:应该重传多少个包? TCP提供了SACK方法(带选择确认的重传,Selective Acknowledgment)。

SACK机制就是,在快速重传的基础上,接收端返回最近收到的报文段的序列号范围,这样发送端就知道接收端哪些数据包没收到,酱紫就很清楚该重传哪些数据包啦。SACK标记是加在TCP头部选项字段里面的。

SACK机制

如上图中,发送端收到了三次同样的ACK=30的确认报文,于是就会触发快速重发机制,通过SACK信息发现只有30~39这段数据丢失,于是重发时就只选择了这个30~39的TCP报文段进行重发。

D-SACK

D-SACK,即Duplicate SACK(重复SACK),在SACK的基础上做了一些扩展,,主要用来告诉发送方,有哪些数据包自己重复接受了。DSACK的目的是帮助发送方判断,是否发生了包失序、ACK丢失、包重复或伪重传。让TCP可以更好的做网络流控。来看个图吧:

D-SACK简要流程

10. 聊聊TCP的滑动窗口

TCP 发送一个数据,需要收到确认应答,才会发送下一个数据。这样有个缺点,就是效率会比较低。

这就好像我们面对面聊天,你说完一句,我应答后,你才会说下一句。那么,如果我在忙其他事情,没有能够及时回复你。你说完一句后,要等到我忙完回复你,你才说下句,这显然很不现实。

为了解决这个问题,TCP引入了窗口,它是操作系统开辟的一个缓存空间。窗口大小值表示无需等待确认应答,而可以继续发送数据的最大值。

TCP头部有个字段叫win,也即那个16位的窗口大小,它告诉对方本端的TCP接收缓冲区还能容纳多少字节的数据,这样对方就可以控制发送数据的速度,从而达到流量控制的目的。

通俗点讲,就是接受方每次收到数据包,在发送确认报文的时候,同时告诉发送方,自己的缓存区还有多少空余空间,缓冲区的空余空间,我们就称之为接受窗口大小。这就是win。

TCP 滑动窗口分为两种: 发送窗口和接收窗口。发送端的滑动窗口包含四大部分,如下:

  • 已发送且已收到ACK确认
  • 已发送但未收到ACK确认
  • 未发送但可以发送
  • 未发送也不可以发送

  • 虚线矩形框,就是发送窗口。
  • SND.WND: 表示发送窗口的大小,上图虚线框的格子数就是14个。
  • SND.UNA: 一个绝对指针,它指向的是已发送但未确认的第一个字节的序列号。
  • SND.NXT:下一个发送的位置,它指向未发送但可以发送的第一个字节的序列号。

接收方的滑动窗口包含三大部分,如下:

  • 已成功接收并确认
  • 未收到数据但可以接收
  • 未收到数据并不可以接收的数据

  • 虚线矩形框,就是接收窗口。
  • REV.WND: 表示接收窗口的大小,上图虚线框的格子就是9个。
  • REV.NXT:下一个接收的位置,它指向未收到但可以接收的第一个字节的序列号。

11. 聊聊TCP的流量控制

TCP三次握手,发送端和接收端进入到ESTABLISHED状态,它们即可以愉快地传输数据啦。

但是发送端不能疯狂地向接收端发送数据,因为接收端接收不过来的话,接收方只能把处理不过来的数据存在缓存区里。如果缓存区都满了,发送方还在疯狂发送数据的话,接收方只能把收到的数据包丢掉,这就浪费了网络资源啦。

TCP 提供一种机制可以让发送端根据接收端的实际接收能力控制发送的数据量,这就是流量控制。

TCP通过滑动窗口来控制流量,我们看下流量控制的简要流程吧:

首先双方三次握手,初始化各自的窗口大小,均为 400 个字节。

TCP的流量控制

  1. 假如当前发送方给接收方发送了200个字节,那么,发送方的SND.NXT会右移200个字节,也就是说当前的可用窗口减少了200 个字节。
  2. 接受方收到后,放到缓冲队列里面,REV.WND =400-200=200字节,所以win=200字节返回给发送方。接收方会在 ACK 的报文首部带上缩小后的滑动窗口200字节
  3. 发送方又发送200字节过来,200字节到达,继续放到缓冲队列。不过这时候,由于大量负载的原因,接受方处理不了这么多字节,只能处理100字节,剩余的100字节继续放到缓冲队列。这时候,REV.WND = 400-200-100=100字节,即win=100返回发送方。
  4. 发送方继续干活,发送100字节过来,这时候,接受窗口win变为0。
  5. 发送方停止发送,开启一个定时任务,每隔一段时间,就去询问接受方,直到win大于0,才继续开始发送。

12. TCP的拥塞控制

拥塞控制是作用于网络的,防止过多的数据包注入到网络中,避免出现网络负载过大的情况。它的目标主要是最大化利用网络上瓶颈链路的带宽。它跟流量控制又有什么区别呢?流量控制是作用于接收者的,根据接收端的实际接收能力控制发送速度,防止分组丢失的。

我们可以把网络链路比喻成一根水管,如果我们想最大化利用网络来传输数据,那就是尽快让水管达到最佳充满状态。

发送方维护一个拥塞窗口cwnd(congestion window) 的变量,用来估算在一段时间内这条链路(水管)可以承载和运输的数据(水)的数量。它大小代表着网络的拥塞程度,并且是动态变化的,但是为了达到最大的传输效率,我们该如何知道这条水管的运送效率是多少呢?

一个比较简单的方法就是不断增加传输的水量,直到水管快要爆裂为止(对应到网络上就是发生丢包),用 TCP 的描述就是:

只要网络中没有出现拥塞,拥塞窗口的值就可以再增大一些,以便把更多的数据包发送出去,但只要网络出现拥塞,拥塞窗口的值就应该减小一些,以减少注入到网络中的数据包数。

实际上,拥塞控制主要有这几种常用算法

  • 慢启动
  • 拥塞避免
  • 拥塞发生
  • 快速恢复

慢启动算法

慢启动算法,表面意思就是,别急慢慢来。它表示TCP建立连接完成后,一开始不要发送大量的数据,而是先探测一下网络的拥塞程度。由小到大逐渐增加拥塞窗口的大小,如果没有出现丢包,每收到一个ACK,就将拥塞窗口cwnd大小就加1(单位是MSS)。每轮次发送窗口增加一倍,呈指数增长,如果出现丢包,拥塞窗口就减半,进入拥塞避免阶段。

  • TCP连接完成,初始化cwnd = 1,表明可以传一个MSS单位大小的数据。
  • 每当收到一个ACK,cwnd就加一;
  • 每当过了一个RTT,cwnd就增加一倍; 呈指数让升

为了防止cwnd增长过大引起网络拥塞,还需设置一个慢启动阀值ssthresh(slow start threshold)状态变量。当cwnd到达该阀值后,就好像水管被关小了水龙头一样,减少拥塞状态。即当cwnd >ssthresh时,进入了拥塞避免算法。

拥塞避免算法

一般来说,慢启动阀值ssthresh是65535字节,cwnd到达慢启动阀值后

  • 每收到一个ACK时,cwnd = cwnd + 1/cwnd
  • 当每过一个RTT时,cwnd = cwnd + 1

显然这是一个线性上升的算法,避免过快导致网络拥塞问题。

拥塞发生

当网络拥塞发生丢包时,会有两种情况:

  • RTO超时重传
  • 快速重传

如果是发生了RTO超时重传,就会使用拥塞发生算法

  • 慢启动阀值sshthresh = cwnd /2
  • cwnd 重置为 1
  • 进入新的慢启动过程

这真的是辛辛苦苦几十年,一朝回到解放前。其实还有更好的处理方式,就是快速重传。发送方收到3个连续重复的ACK时,就会快速地重传,不必等待RTO超时再重传。

image.png

慢启动阀值ssthresh 和 cwnd 变化如下:

  • 拥塞窗口大小 cwnd = cwnd/2
  • 慢启动阀值 ssthresh = cwnd
  • 进入快速恢复算法

快速恢复

快速重传和快速恢复算法一般同时使用。快速恢复算法认为,还有3个重复ACK收到,说明网络也没那么糟糕,所以没有必要像RTO超时那么强烈。

正如前面所说,进入快速恢复之前,cwnd 和 sshthresh已被更新:

1
2
ini复制代码- cwnd = cwnd /2
- sshthresh = cwnd

然后,真正的快速算法如下:

  • cwnd = sshthresh + 3
  • 重传重复的那几个ACK(即丢失的那几个数据包)
  • 如果再收到重复的 ACK,那么 cwnd = cwnd +1
  • 如果收到新数据的 ACK 后, cwnd = sshthresh。因为收到新数据的 ACK,表明恢复过程已经结束,可以再次进入了拥塞避免的算法了。

13. 半连接队列和 SYN Flood 攻击的关系

TCP进入三次握手前,服务端会从CLOSED状态变为LISTEN状态,同时在内部创建了两个队列:半连接队列(SYN队列)和全连接队列(ACCEPT队列)。

什么是半连接队列(SYN队列) 呢? 什么是全连接队列(ACCEPT队列) 呢?回忆下TCP三次握手的图:

三次握手

  • TCP三次握手时,客户端发送SYN到服务端,服务端收到之后,便回复ACK和SYN,状态由LISTEN变为SYN_RCVD,此时这个连接就被推入了SYN队列,即半连接队列。
  • 当客户端回复ACK, 服务端接收后,三次握手就完成了。这时连接会等待被具体的应用取走,在被取走之前,它被推入ACCEPT队列,即全连接队列。

SYN Flood是一种典型的DoS (Denial of Service,拒绝服务) 攻击,它在短时间内,伪造不存在的IP地址,向服务器大量发起SYN报文。当服务器回复SYN+ACK报文后,不会收到ACK回应报文,导致服务器上建立大量的半连接半连接队列满了,这就无法处理正常的TCP请求啦。

主要有 syn cookie和SYN Proxy防火墙等方案应对。

  • syn cookie:在收到SYN包后,服务器根据一定的方法,以数据包的源地址、端口等信息为参数计算出一个cookie值作为自己的SYNACK包的序列号,回复SYN+ACK后,服务器并不立即分配资源进行处理,等收到发送方的ACK包后,重新根据数据包的源地址、端口计算该包中的确认序列号是否正确,如果正确则建立连接,否则丢弃该包。
  • SYN Proxy防火墙:服务器防火墙会对收到的每一个SYN报文进行代理和回应,并保持半连接。等发送方将ACK包返回后,再重新构造SYN包发到服务器,建立真正的TCP连接。

14. Nagle 算法与延迟确认

Nagle算法

如果发送端疯狂地向接收端发送很小的包,比如就1个字节,那么亲爱的小伙伴,你们觉得会有什么问题呢?

TCP/IP协议中,无论发送多少数据,总是要在数据前面加上协议头,同时,对方接收到数据,也需要发送ACK表示确认。为了尽可能的利用网络带宽,TCP总是希望尽可能的发送足够大的数据。Nagle算法就是为了尽可能发送大块数据,避免网络中充斥着许多小数据块。

Nagle算法的基本定义是:任意时刻,最多只能有一个未被确认的小段。 所谓“小段”,指的是小于MSS尺寸的数据块,所谓“未被确认”,是指一个数据块发送出去后,没有收到对方发送的ACK确认该数据已收到。

Nagle算法的实现规则:

  • 如果包长度达到MSS,则允许发送;
  • 如果该包含有FIN,则允许发送;
  • 设置了TCP_NODELAY选项,则允许发送;
  • 未设置TCP_CORK选项时,若所有发出去的小数据包(包长度小于MSS)均被确认,则允许发送;
  • 上述条件都未满足,但发生了超时(一般为200ms),则立即发送。

延迟确认

如果接受方刚接收到发送方的数据包,在很短很短的时间内,又接收到第二个包。那么请问接收方是一个一个地回复好点,还是合并一起回复好呢?

接收方收到数据包后,如果暂时没有数据要发给对端,它可以等一段时再确认(Linux上默认是40ms)。如果这段时间刚好有数据要传给对端,ACK就随着数据传输,而不需要单独发送一次ACK。如果超过时间还没有数据要发送,也发送ACK,避免对端以为丢包。

但是有些场景不能延迟确认,比如发现了乱序包、接收到了大于一个 frame 的报文,且需要调整窗口大小等。

一般情况下,Nagle算法和延迟确认不能一起使用,Nagle算法意味着延迟发,延迟确认意味着延迟接收,酱紫就会造成更大的延迟,会产生性能问题。

15. TCP的粘包和拆包

TCP是面向流,没有界限的一串数据。TCP底层并不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行包的划分,所以在业务上认为,一个完整的包可能会被TCP拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这就是所谓的TCP粘包和拆包问题。

TCP的粘包和拆包

为什么会产生粘包和拆包呢?

  • 要发送的数据小于TCP发送缓冲区的大小,TCP将多次写入缓冲区的数据一次发送出去,将会发生粘包;
  • 接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包;
  • 要发送的数据大于TCP发送缓冲区剩余空间大小,将会发生拆包;
  • 待发送数据大于MSS(最大报文长度),TCP在传输前将进行拆包。即TCP报文长度-TCP头部长度>MSS。

解决方案:

  • 发送端将每个数据包封装为固定长度
  • 在数据尾部增加特殊字符进行分割
  • 将数据分为两部分,一部分是头部,一部分是内容体;其中头部结构大小固定,且有一个字段声明内容体的大小。

参考与感谢

  • TCP 的那些事儿(下)
  • 面试头条你需要懂的 TCP 拥塞控制原理
  • 30张图解: TCP 重传、滑动窗口、流量控制、拥塞控制发愁
  • TCP协议灵魂之问,巩固你的网路底层基础
  • TCP粘包和拆包
  • 百度百科

本文转载自: 掘金

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

王者并发课-铂金10:能工巧匠-ThreadLocal如何为

发表于 2021-07-11

欢迎来到《王者并发课》,本文是该系列文章中的第23篇,铂金中的第10篇。

说起ThreadLocal,相信你对它的名字一定不陌生。在并发编程中,它有着较高的出场率,并且也是面试中的高频面试题之一,所以其重要性不言而喻。当然,它也可能曾经让你在夜里辗转反侧,或让你在面试时闪烁其词。因为,ThreadLocal虽然使用简单,但要理解它的原理又似乎并不容易。

然而,正所谓明知山有虎,偏向虎山行。在本文中,我将和你一起学习ThreadLocal的用法及其原理,啃下这块硬骨头。

关于ThreadLocal的用法和原理,网上也有着非常多的资料可以查阅。遗憾的是,这其中的大部分资料在讲解时都不够透彻。有的是蜻蜓点水,没有把必要的细节讲清楚,有的则比较片面,只讲了其中的某个点。

所以,当并发王者课系列写到这篇文章的时候,如何才能简明扼要地把ThreadLocal介绍清楚,让读者能在一篇文章中透彻地理解它,但同时又要避免万字长文读不下去,是我最近一直在思考的问题。为此,在综合现有资料的基础上,我精心设计了一些配图,尽可能地让文章图文并茂,以帮助你相对轻松地理解ThreadLocal中的精要。然而,每个读者的背景不同,理解也就不同。所以,对于你认为的并没有讲清楚的地方,希望你在评论区留言反馈,我会尽量调整完善,争取让你“一文读懂”。

一、ThreadLocal使用场景初体验

夫子的疑惑:在什么场景下需要使用ThreadLocal?

在王者峡谷中,每个英雄都有着自己的领地和庄园。在庄园里,按照功能职责的不同又划分为不同的区域,比如有圈养野怪的区域,还有存放金币以及武器等不同区域。当然,这些区域都是英雄私有的,不能混淆错乱。

所以,铠在打野和获得金币时,可以把他打的野怪放进自己庄园里,那是他的私有空间。同样,兰陵王和其他英雄也是如此。这个设计如下图所示:

现在,我们就来编写一段代码模拟上述的场景:

  • 铠在打野和获得金币时,放进他的私有空间里;
  • 兰陵王在打野和获得金币时,放进他的私有空间里;
  • 他们的空间都位于王者峡谷中。

以下是我们编写的一段代码。在代码中,我们定义了一个wildMonsterLocal变量,用于存放英雄们打野时获得的野怪;而coinLocal则用于存放英雄们所获得的金币。于是,铠将他所打的棕熊放进了圈养区,并将获得的500金币放进了金币存放区;而兰陵王则将他所打的野狼放进了圈养区,并将获得的100金币放进了金币存放区。

过了一阵子之后,他们分别取走他们存放的野怪和金币。

主要示例如下所示。在阅读下面示例代码时,要着重注意对ThreadLocal的get和set方法的调用。

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
java复制代码//私人野怪圈养区
private static final ThreadLocal < WildMonster > wildMonsterLocal = new ThreadLocal < > ();
//私人金币存放区
private static final ThreadLocal < Coin > coinLocal = new ThreadLocal < > ();

public static void main(String[] args) {
Thread 铠 = newThread("铠", () -> {
try {
say("今天打到了一只棕熊,先把它放进圈养区,改天再享用!");
wildMonsterLocal.set(new Bear("棕熊"));
say("路上杀了一些兵线,获得了一些金币,也先存起来!");
coinLocal.set(new Coin(500));

Thread.sleep(2000);
note("\n过了一阵子...\n");
say("从圈养区拿到了一只:", wildMonsterLocal.get().getName());
say("金币存放区现有金额:", coinLocal.get().getAmount());
} catch (InterruptedException e) {}
});

Thread 兰陵王 = newThread("兰陵王", () -> {
try {
Thread.sleep(1000);
say("今天打到了一只野狼,先把它放进圈养区,改天再享用!");
wildMonsterLocal.set(new Wolf("野狼"));
say("路上杀了一些兵线,获得了一些金币,也先存起来!");
coinLocal.set(new Coin(100));

Thread.sleep(2000);
say("从圈养区拿到了一只:", wildMonsterLocal.get().getName());
say("金币存放区现有金额:", coinLocal.get().getAmount());
} catch (InterruptedException e) {}
});
铠.start();
兰陵王.start();
}

示例代码中用到的类如下所示:

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复制代码@Data
private static class WildMonster {
protected String name;
}

private static class Wolf extends WildMonster {
public Wolf(String name) {
this.name = name;
}
}

private static class Bear extends WildMonster {
public Bear(String name) {
this.name = name;
}
}

@Data
private static class Coin {
private int amount;

public Coin(int amount) {
this.amount = amount;
}
}

示例代码运行结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
shell复制代码铠:今天打到了一只棕熊,先把它放进圈养区,改天再享用!
铠:路上杀了一些兵线,获得了一些金币,也先存起来!
兰陵王:今天打到了一只野狼,先把它放进圈养区,改天再享用!
兰陵王:路上杀了一些兵线,获得了一些金币,也先存起来!

过了一阵子...

铠:从圈养区拿到了一只:棕熊
铠:金币存放区现有金额:500
兰陵王:从圈养区拿到了一只:野狼
兰陵王:金币存放区现有金额:100

Process finished with exit code 0

从运行的结果中,可以清楚地看到,在过了一阵子之后,铠和兰陵王分别取到了他们之前存放的野怪和金币,并且丝毫不差。

以上,就是ThreadLocal应用的典型。在多线程并发场景中,如果你需要为每个线程设置可以跨越类和方法层面的私有变量,那么你就需要考虑使用ThreadLocal了。注意,这里有两个要点,一是变量为某个线程独享,二是变量可以在不同方法甚至不同的类中共享。

ThreadLocal在软件设计中的应用场景非常多。举个简单的例子,在一次请求中,如果你需要设置一个traceId来跟踪请求的完整调用链路,那么你就需要一个能跨越类和方法的变量,这个变量可以让线程在不同的类中自由获取,且不会出错,其过程如下图所示:

二、ThreadLocal原理解析

对于ThreadLocal,一般来说被提及最多的可能就是那个经典的面试问题:谈谈你对ThreadLocal内存泄露的理解。这个问题看起来很简单,但要回答到点子上的话,就必须对其源码有足够理解。当然,背诵面试题的答案扯一通“软引用”、“内存回收”巴拉巴拉也是可以的,毕竟大部分的面试官也是半吊子。

接下来,我们会结合上文的场景,以及它的示例代码来讲解ThreadLocal的原理,让你找到关于这个问题的真正答案。

1. 源码分析

如果对ThreadLocal理解有困难的话,很大的可能是:你没有理清不同概念之间的关系。所以,理解ThreadLocal源码的第一步是找出它的相关概念,并理清它们之间的关系,即Thread、ThreadLocalMap和ThreadLocal。正是这三个关键概念,唱出了一台好戏。当然,如果细分的话,你也可以把Entry单独拎出来。

关键概念1:Thread类

为什么Thread在关键概念中排名第一,因为ThreadLocal就是为它而生的。那Thread和ThreadLocal是什么关系呢?我们这就来看看Thread的源码:

1
2
3
4
5
6
7
java复制代码class Thread implements Runnable {
...
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
...
}

没有什么比源码展现更清晰的了。你可以非常直观地看到,Thread中有一个变量:threadLocals. 通俗地说,这个变量就是用来存放当前线程的一些私有数据的,并且可以存放多个私有数据,毕竟线程是可以携带多个私有数据的,比如它可以携带traceId,也自然可以携带userId等数据。理解了这个变量的用途之后,再看看它的类型,也就是ThreadLocal.ThreadLocalMap.你看,Thread就这样和ThreadLocal扯上了关系,所以接下来我们来看另外一个关键概念。

关键概念2:ThreadLocalMap类

从Thread的源码中你已经看到,Thread是用ThreadLocalMap来存放线程私有数据的。这里,我们先暂且撇开ThreadLocal,来直接看ThreadLocalMap的源码:

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复制代码static class ThreadLocalMap {

...
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;

Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}

/**
* The table, resized as necessary.
* table.length MUST always be a power of two.
*/
private Entry[] table;
...

}

ThreadLocalMap中最关键的属性就是Entry[] table,正是它实现了线程私有多数据的存储。而Entry则是继承了WeakReference,并且Entry的Key类型是ThreadLocal. 看到这里,先不要想着ThreadLocalMap的其他源码,你现在应当理解的是,table是线程私有数据存储的地方,而ThreadLocalMap的其他源码不过都是为了table数据的存与取而存在的。这是你对ThreadLocalMap理解的关键,不要把自己迷失在错综复杂的其他源码中。

关键概念3:ThreadLocal类

现在,目光终于到了ThreadLocal这个类上。Thread中使用到了ThreadLocalMap,而接下来你会发现ThreadLocal不过是封装了一些对ThreadLocalMap的操作。你看,ThreadLocal中的get()、set()、remove()等方法都是在操作ThreadLocalMap. 在各种操作之前,都会通过getMap()方法拿到当前线程的ThreadLocalMap.

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

...

// 获取当前线程的数据
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
// 初始化数据
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}

// 设置当前线程的数据
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}

public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}

// 获取线程私有数据存储的关键,虽然操作在ThreadLocal中,但是实际操作的是Thread中的threadLocals变量
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}

// 初始化线程的t.threadLocals变量,设置为空值
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
...
}

如果,此时你对相关概念及其源码的理解仍然感到困惑,那就对了。下面这幅图,将结合相关概念和示例代码,来还原这其中的相关概念和它们之间的关系,这幅图值得你反复细品。

在上面这幅图中,你需要如下这些细节:

  • 有两个线程:铠和兰陵王;
  • 有两个ThreadLocal对象,它们分别用于存放线程的私有数据,即英雄们的野怪和金币;
  • 线程铠和线程兰陵王都有一个ThreadLocal.ThreadLocalMap的变量,用来存放不同的ThreadLocal,即wildMonsterLocal和coinLocal这两个变量都会放进ThreadLocalMap的table里,也就是entry数组中;
  • 当线程向ThreadLocalMap中放入数据时,它的key会指向ThreadLocal对象,而value则是ThreadLocal中的值。比如,当铠将棕熊放入wildMonsterLocal中时,对应Entry的key是wildMonsterLocal,而value则是new Bear(),即棕熊。当兰陵王放入野怪时,同理;当铠放入金币时,也是同理;
  • 当Entry的key指向ThreadLocal对象时,比如指向wildMonsterLocal或coinLocal时,注意,是弱引用,是弱引用,是弱引用,是弱引用!重要的事情,说四遍。看图中的红线虚线,或ThreadLocalMap源码中的WeakReference.

如果你已经看明白上面这幅图,那么下面这幅图中的关系也就应该一目了既然。否则,如果你似乎看不明白它,请回到上面继续品上面那幅图,直到你对下图一目了然。

2. 使用指南

接下来,将为你简单介绍ThreadLocal的一些常见高频用法。

(1)创建ThreadLocal

像创建其他对象一样创建即可,没有什么特别之处。

1
java复制代码ThreadLocal < WildMonster > wildMonsterLocal = new ThreadLocal < > ();

在对象创建完成之后,每个线程便可以向其中读写数据。当然,每个线程都只能看到它们自己的数据。

(2)设置ThreadLocal的值

1
java复制代码wildMonsterLocal.set(new Bear("棕熊"));

(3)取出ThreadLocal的值

1
java复制代码wildMonsterLocal.get();

在读取数据时需要注意的是,如果此时还没有数据设置进来,那么将会调用setInitialValue方法来设置初始值并返回给调用方。

(4)初始化ThreadLocal的值

1
2
3
4
5
6
java复制代码private ThreadLocal wildMonsterLocal = new ThreadLocal<WildMonster>() {
@Override
protected WildMonster initialValue() {
return new WildMonster();
}
};

在对ThreadLocal进行get操作时,如果当前尚未进行过数据设置,那么会执行初始化动作,如果你此时希望设置初始值,可以重写它的initialValue方法。

3. 如何理解ThreadLocal的内存泄露问题

首先,你要理解弱引用这个概念。在Java中,引用分为强引用、弱引用、软引用、虚幻引用等不同的引用类型,而不同的引用类型对应的则是不同的垃圾回收策略。如果你对此不熟的话,建议可以去检索相关资料,也可以看这篇。

对于弱引用,在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。但是,即便是偶尔发生,也足够造成问题。

当你理解了弱引用和对应的垃圾回收策略之后,此刻,请回到上面的那幅图:

在这幅图里,Entry的key指向ThreadLocal对象时,用的正是弱引用,图中已经红色箭头标注。这里的红色虚线会造成上面问题呢?你想想看,如果此时ThreadLocal对象被回收时,那么Entry中的key就编程了null. 可是,虽然key(wildMonsterLocal)变成了null,value的值(new Bear(“棕熊”))还是强引用,它还会继续存在,但实际已经没有用了,所以会造成这个Entry就废了,但是因为value的存在却不能被回收。于是,内存泄露就这样产生了。

那既然如此,为什么要使用弱引用?

相信你一定有这个疑问,如果没有,这篇文章你可能需要再读一遍。明知这里会产生内存泄露的风险,却仍然使用弱引用的原因在于:当ThreadLocal对象没有强引用时,它们需要被清理,否则它们长期存在于ThreadLocalMap中,也是一种内存泄露。你看,问题就是这样的一环扣着一环。

最佳实践:如何避免内存泄露

那么,既然事已如此,如何避免内存泄露呢?这里给出一个可行的最佳实践:在调用完成后,手动执行remove()方法。

1
2
3
4
5
6
7
8
java复制代码private static final ThreadLocal<WildMonster> wildMonsterLocal = new ThreadLocal<>();

try{
wildMonsterLocal.get();
...
}finally{
wildMonsterLocal.remove();
}

除此之外,ThreadLocal也给出一个方案:在调用set方法设置时,会调用replaceStaleEntry方法来检查key为null的Entry。如果发现有key为null的Entry,那么会将它的value也设置为null,这样Entry便可以被回收。当然,如果你没有再调用set方法,那么这个方案就是无效的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码private void set(ThreadLocal < ? > key, Object value) {
...
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
ThreadLocal < ? > k = e.get();

if (k == key) {
e.value = value;
return;
}

if (k == null) {
replaceStaleEntry(key, value, i); //看这里
return;
}
}

...
}

小结

以上就是关于ThreadLocal的全部内容。在学习ThreadLocal时,首先要理解的是它的应用场景,即它所要解决的问题。其次,对它的源码要有一定的了解。在了解源码时,要注意从Thread、ThreadLocal和ThreadLocalMap三个概念出发,理解他们之间的关系。如此,你才能完全理解常见的内存泄露问题是怎么一回事。

正文到此结束,恭喜你又上了一颗星✨

夫子的试炼

  • 尝试向你的朋友解释ThreadLocal内存泄露是如何发生的。

延伸阅读与参考资料

  • 《王者并发课》大纲与更新进度总览

关于作者

从业近十年,先后从事敏捷与DevOps咨询、Tech Leader和管理等工作,对分布式高并发架构有丰富的实战经验。热衷于技术分享和特定领域书籍翻译,掘金小册《高并发秒杀的设计精要与实现》作者。


关注公众号【MetaThoughts】,及时获取文章更新和文稿。

如果本文对你有帮助,欢迎点赞、关注、监督,我们一起从青铜到王者。

本文转载自: 掘金

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

Mybatis-plus最简实现分页查询

发表于 2021-07-11

前言

四步实现myBatis-plus的分页查询:
添加依赖包->添加Interceptor->定义三件套->使用page函数;

一、添加依赖的Jar包

1
2
3
4
5
6
7
8
9
10
11
xml复制代码<!--MP插件-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.3.1</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.3.1</version>
</dependency>

二、添加Interceptor

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码@Configuration
public class MybatisPlusPageInterceptor {
/**
* mybatis-plus分页插件
*/
@Bean
public PaginationInterceptor paginationInterceptor() {
PaginationInterceptor page = new PaginationInterceptor();
return page;
}

}

三、添加DO,Mapper,Service三件套

我们以TStation,TStationMapper,TStationServiceImpl为例

image.png

四、在service中添加分页查询函数

1
2
3
4
5
6
7
8
9
10
java复制代码@Service
public class TStationServiceImpl extends ServiceImpl<TStationMapper, TStation> implements ITStationService {
@Override
public IPage<TStation> listPage(Map queryParam,Integer page,Integer size) {
LambdaQueryWrapper<TStation> queryWrapper = Wrappers.lambdaQuery();
//根据queryParam设置Wrapper的查询条件
//比如:queryWrapper.eq(TStation::getFrameNo,queryParam.get("frameName"));
return page(new Page<>(page,size), queryWrapper);
}
}

总结:

1.分页查询时,需要通过Page对象设置查询条件“current”和“size”,Page构造函数如下:

1
2
3
java复制代码public Page(long current, long size) {
this(current, size, 0);
}

2.分页查的结果为IPage,用来存储查询结果,可以通过getRecord()来获取具体结果集;

1
2
3
4
java复制代码IPage<TStation> containerPage = itStationService.listPage(null,1,20);//取第1页,每页20条
containerPage.getRecords().forEach(record -> {
//可以对结果集中的数据干点啥
});

3.IPage的查询条件,不仅限于current,size;还有其他查询条件可以设置,如:total,isSearchCount;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
arduino复制代码public Page(long current, long size) {
this(current, size, 0);
}

public Page(long current, long size, long total) {
this(current, size, total, true);
}

public Page(long current, long size, boolean isSearchCount) {
this(current, size, 0, isSearchCount);
}

public Page(long current, long size, long total, boolean isSearchCount) {
if (current > 1) {
this.current = current;
}
this.size = size;
this.total = total;
this.isSearchCount = isSearchCount;
}

本文转载自: 掘金

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

真的!Autowired和Resource注解使用的正确

发表于 2021-07-11

介绍

今天使用Idea写代码的时候,看到之前的项目中显示有warning的提示,去看了下,是如下代码?

1
2
java复制代码@Autowired
private JdbcTemplate jdbcTemplate;

提示的警告信息

1
2
3
4
5
bash复制代码Field injection is not recommended Inspection info: Spring Team recommends: "Always use constructor based dependency injection in your beans. Always use assertions for mandatory dependencies".

这段是Spring工作组的建议,大致翻译一下:
属性字段注入的方式不推荐,检查到的问题是:Spring团队建议:"始终在bean中使用基于构造函数的依赖项注入,
始终对强制性依赖项使用断言"

如图

Field注入警告

注入方式

虽然当前有关Spring Framework(5.0.3)的文档仅定义了两种主要的注入类型,但实际上有三种:

  • 基于构造函数的依赖注入
1
2
3
4
5
6
7
8
java复制代码public class UserServiceImpl implents UserService{
private UserDao userDao;

@Autowired
public UserServiceImpl(UserDao userDao){
this.userDao = userDao;
}
}
  • 基于Setter的依赖注入
1
2
3
4
5
6
7
8
java复制代码public class UserServiceImpl implents UserService{
private UserDao userDao;

@Autowired
public serUserDao(UserDao userDao){
this.userDao = userDao;
}
}
  • 基于字段的依赖注入
1
2
3
4
java复制代码public class UserServiceImpl implents UserService{
@Autowired
private UserDao userDao;
}

基于字段的依赖注入方式会在Idea当中吃到黄牌警告,但是这种使用方式使用的也最广泛,因为简洁方便.您甚至可以在一些Spring指南中看到这种注入方法,尽管在文档中不建议这样做.(有点执法犯法的感觉)

微信搜:Javapub

如图

Spring自己的文档

基于字段的依赖注入缺点

  • 对于有final修饰的变量不好使  

Spring的IOC对待属性的注入使用的是set形式,但是final类型的变量在调用class的构造函数的这个过程当中就得初始化完成,这个是基于字段的依赖注入做不到的地方.只能使用基于构造函数的依赖注入的方式

  • 掩盖单一职责的设计思想

我们都知道在OOP的设计当中有一个单一职责思想,如果你采用的是基于构造函数的依赖注入的方式来使用Spring的IOC的时候,当你注入的太多的时候,这个构造方法的参数就会很庞大,类似于下面.当你看到这个类的构造方法那么多参数的时候,你自然而然的会想一下:这个类是不是违反了单一职责思想?.但是使用基于字段的依赖注入不会让你察觉,你会很沉浸在@Autowired当中

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

private AccountService accountService;
private UserService userService;
private IDService idService;
private RoleService roleService;
private PermissionService permissionService;
private EnterpriseService enterpriseService;
private EmployeeService employService;
private TaskService taskService;
private RedisService redisService;
private MQService mqService;

public SystemLogDto(AccountService accountService,
UserService userService,
IDService idService,
RoleService roleService,
PermissionService permissionService,
EnterpriseService enterpriseService,
EmployeeService employService,
TaskService taskService,
RedisService redisService,
MQService mqService) {
this.accountService = accountService;
this.userService = userService;
this.idService = idService;
this.roleService = roleService;
this.permissionService = permissionService;
this.enterpriseService = enterpriseService;
this.employService = employService;
this.taskService = taskService;
this.redisService = redisService;
this.mqService = mqService;
}
}
  • 与Spring的IOC机制紧密耦合

当你使用基于字段的依赖注入方式的时候,确实可以省略构造方法和setter这些个模板类型的方法,但是,你把控制权全给Spring的IOC了,别的类想重新设置下你的某个注入属性,没法处理(当然反射可以做到).本身Spring的目的就是解藕和依赖反转,结果通过再次与类注入器(在本例中为Spring)耦合,失去了通过自动装配类字段而实现的对类的解耦,从而使类在Spring容器之外无效.

  • 隐藏依赖性

当你使用Spring的IOC的时候,被注入的类应当使用一些public类型(构造方法,和setter类型方法)的方法来向外界表达:我需要什么依赖.但是基于字段的依赖注入的方式,基本都是private形式的,private把属性都给封印到class当中了.

  • 无法对注入的属性进行安检

基于字段的依赖注入方式,你在程序启动的时候无法拿到这个类,只有在真正的业务使用的时候才会拿到,一般情况下,这个注入的都是非null的,万一要是null怎么办,在业务处理的时候错误才爆出来,时间有点晚了,如果在启动的时候就暴露出来,那么bug就可以很快得到修复(当然你可以加注解校验).如果你想在属性注入的时候,想根据这个注入的对象操作点东西,你无法办到.我碰到过的例子:一些配置信息啊,有些人总是会配错误,等到了自己测试业务阶段才知道配错了,例如线程初始个数不小心配置成了3000,机器真的是狂叫啊!这个时候就需要再某些Value注入的时候做一个检测机制.

结论

通过上面,我们可以看到,基于字段的依赖注入方式有很多缺点,我们应当避免使用基于字段的依赖注入.推荐的方法是使用基于构造函数和基于setter的依赖注入.对于必需的依赖项,建议使用基于构造函数的注入,以使它们成为不可变的,并防止它们为null。对于可选的依赖项,建议使用基于Setter的注入

后记

翻译自field-injection-is-not-recommended,加入了自己的白话理解!

本文转载自: 掘金

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

花钱都买不到的绝版Oracle数据库最全版本安装包(精心整理

发表于 2021-07-11

「本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!」

作者简介

  • 作者:LuciferLiu,中国DBA联盟(ACDU)成员。
  • 目前主要从事Oracle DBA工作,曾从事 Oracle 数据库开发工作,主要服务于生产制造,汽车金融等行业。
  • 现拥有Oracle OCP,OceanBase OBCA认证,擅长Oracle数据库运维开发,备份恢复,安装迁移,Linux自动化运维脚本编写等。

前言

  • 很多朋友苦于下载不到Oracle的软件安装包,于是我整理了一下上传到网盘以供下载。

在这里插入图片描述
从上图可以看出,随着Oracle版本升级,对以前的版本不再支持,也就意味着,官网不再提供下载方式,也就导致很多朋友无法下载到对应版本的安装包和补丁包,于是我就精心整理了一版,分享给大家。

  • 已收集的Oracle版本,包含最新PSU&&RU补丁:
  • Oracle 9i
  • Oracle 10g
  • Oracle 11G
  • Oracle 12C
  • Oracle 18C
  • Oracle 19C

注意:操作系统主要包括 Linux_64,Windows_64,IBM_AIX_64。

下载方式:

  • 微信公众号(Lucifer三思而后行)私信回复:Oracle安装包。

索引目录

  • 以下为各版本安装包的索引目录:

1 Oracle 9i版本

1
2
3
4
5
6
7
8
9
10
11
12
bash复制代码9i
├── 9204
│ ├── p2617419_10102_GENERIC.zip
│ ├── p3006854_9204_LINUX.zip
│ ├── p3119415_9204_LINUX.zip
│ ├── p3238244_9204_LINUX.zip
│ ├── p4198954_21_LINUX.zip
│ └── p4547809_92080_Linux-x86-64.zip
└── setup
├── ship_9204_linux_disk1.cpio.gz
├── ship_9204_linux_disk2.cpio.gz
└── ship_9204_linux_disk3.cpio.gz

2 Oracle 10g版本

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
bash复制代码├── 10g
│ ├── 10205_AIX
│ │ ├── 10gr2_aix5l64_client.cpio.gz
│ │ ├── 10gr2_aix5l64_cluster.cpio.gz
│ │ ├── 10gr2_aix5l64_companion.cpio.gz
│ │ ├── 10gr2_aix5l64_database.cpio.gz
│ │ ├── 10gr2_aix5l64_gateways.cpio.gz
│ │ ├── p12419392_10205_AIX64-5L.zip
│ │ ├── p12879933_10204_AIX5L.zip
│ │ ├── p13343471_10205_AIX64-5L.zip
│ │ ├── p16619894_10205_AIX64-5L.zip
│ │ ├── p20299014_10205_AIX64-5L.zip
│ │ ├── p6810189_10204_AIX5L.zip
│ │ ├── p6880880_102000_AIX64-5L.zip
│ │ ├── p8202632_10205_AIX64-5L_1of2.zip
│ │ ├── p8202632_10205_AIX64-5L_2of2.zip
│ │ └── p9952245_10205_AIX64-5L.zip
│ ├── 10205_HP-UX
│ │ ├── 10204patch
│ │ │ ├── p6367097_102044_HPUX-IA64.zip
│ │ │ ├── p6367097_10204_HPUX-IA64.zip
│ │ │ └── p8705958_10204_HPUX-IA64.zip
│ │ ├── 10gr2_client_hpi.zip
│ │ ├── 10gr2_clusterware_hpi.zip
│ │ ├── 10gr2_database_hpi.zip
│ │ ├── p16619894_10205_HPUX-IA64.zip
│ │ ├── p5337014_10203_HPUX-IA64.zip
│ │ ├── p6810189_10204_HPUX-IA64.zip
│ │ ├── p8202632_10205_HPUX-IA64.zip
│ │ └── p9952245_102050_HP64.zip
│ ├── 10205_HP32
│ ├── 10205_Linux_x86-64
│ │ ├── 10201_client_linux_x86_64.cpio.gz
│ │ ├── 10201_clusterware_linux_x86_64.cpio.gz
│ │ ├── 10201_companion_linux_x86_64.cpio.gz
│ │ ├── 10201_database_linux_x86_64.cpio.gz
│ │ ├── 10201_gateways_linux_x86_64.cpio.gz
│ │ ├── p12879933_10204_Linux-x86-64.zip
│ │ ├── p13343471_10205_Linux-x86-64.zip
│ │ ├── p14275629_10205_Linux-x86-64.zip
│ │ ├── p16619894_10205_Linux-x86-64.zip
│ │ ├── p20299014_10205_Linux-x86-64.zip
│ │ ├── p6810189_10204_Linux-x86-64.zip
│ │ ├── p6880880_102000_Linux-x86-64.zip
│ │ ├── p8202632_10205_Linux-x86-64.zip
│ │ ├── p8705958_10204_Linux-x86-64.zip
│ │ └── p9952245_10205_Linux-x86-64.zip
│ ├── 10205_Solaris_x86
│ │ ├── 10202_client_solx86.zip
│ │ ├── 10202_clusterware_solx86.zip
│ │ ├── 10202_database_solx86.zip
│ │ └── p6810189_10204_Solarisx86.zip
│ ├── 10205_Solaris_x86-64
│ │ ├── 10201_client_solx86_64.zip
│ │ ├── 10201_clusterware_solx86_64.zip
│ │ ├── 10201_database_solx86_64.zip
│ │ ├── p6810189_10204_Solaris86-64.zip
│ │ └── p8202632_10205_Solaris86-64.zip
│ ├── 10205_Windows
│ │ ├── 10201_client_win32.zip
│ │ ├── 10201_clusterware_win32.zip
│ │ ├── 10201_companion_win32.zip
│ │ ├── 10201_database_win32.zip
│ │ ├── 10201_gateways_win32.zip
│ │ ├── 10203_vista_w2k8_x86_production_client.zip
│ │ ├── 10203_vista_w2k8_x86_production_db.zip
│ │ ├── p13928775_10204_Win32.zip
│ │ ├── p6810189_10204_Win32.zip
│ │ └── p8202632_10205_WINNT.zip
│ └── 10205_Windows_x86-64
│ ├── 102010_win64_x64_client.zip
│ ├── 102010_win64_x64_clusterware.zip
│ ├── 102010_win64_x64_companion.zip
│ ├── 102010_win64_x64_database.zip
│ ├── 10204_vista_w2k8_x64_production_client.zip
│ ├── 10204_vista_w2k8_x64_production_companion.zip
│ ├── 10204_vista_w2k8_x64_production_crs.zip
│ ├── 10204_vista_w2k8_x64_production_db(1).zip
│ ├── 10204_vista_w2k8_x64_production_db.zip
│ ├── p6810189_10204_WINNT64.zip
│ └── p8202632_10205_WINNT64.zip

3 Oracle 11G版本

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
bash复制代码├── 11g
│ ├── 11201
│ │ ├── Client
│ │ │ ├── Linux
│ │ │ │ ├── 32
│ │ │ │ │ └── V17488-01.zip
│ │ │ │ └── 64
│ │ │ │ └── V17532-01.zip
│ │ │ └── Windows
│ │ │ ├── 32
│ │ │ │ └── V20606-01.zip
│ │ │ └── 64
│ │ │ └── V20609-01.zip
│ │ ├── IBM_AIX_64
│ │ │ ├── DB
│ │ │ │ ├── PSU
│ │ │ │ │ └── p12419378_112010_AIX64-5L_2.zip
│ │ │ │ ├── V19105-01_1of2.zip
│ │ │ │ └── V19105-01_2of2.zip
│ │ │ └── Grid
│ │ │ └── V19102-01.zip
│ │ ├── Linux_64
│ │ │ ├── DB
│ │ │ │ ├── PSU
│ │ │ │ │ └── p12419378_112010_Linux-x86-64.zip
│ │ │ │ ├── V17530-01_1of2.zip
│ │ │ │ └── V17530-01_2of2.zip
│ │ │ └── Grid
│ │ │ └── V17531-01.zip
│ │ └── Windows_64
│ │ ├── DB
│ │ │ ├── Bundle\ Patch
│ │ │ │ └── p12429529_112010_MSWIN-x86-64.zip
│ │ │ ├── V20610-01_1of2.zip
│ │ │ └── V20610-01_2of2.zip
│ │ └── Grid
│ │ └── V20612-01.zip
│ ├── 11202
│ │ ├── IBM_AIX_64
│ │ │ ├── PSU
│ │ │ │ ├── DB
│ │ │ │ │ └── p17082367_112020_AIX64-5L.zip
│ │ │ │ └── GI
│ │ │ │ └── p17272753_112020_AIX64-5L.zip
│ │ │ ├── p10098816_112020_AIX64-5L_1of7.zip
│ │ │ ├── p10098816_112020_AIX64-5L_2of7.zip
│ │ │ ├── p10098816_112020_AIX64-5L_3of7.zip
│ │ │ └── p10098816_112020_AIX64-5L_4of7.zip
│ │ ├── Linux_64
│ │ │ ├── PSU
│ │ │ │ ├── DB
│ │ │ │ │ └── p17082367_112020_Linux-x86-64.zip
│ │ │ │ └── GI
│ │ │ │ └── p17272753_112020_AIX64-5L.zip
│ │ │ ├── p10098816_112020_Linux-x86-64_1of7.zip
│ │ │ ├── p10098816_112020_Linux-x86-64_2of7.zip
│ │ │ ├── p10098816_112020_Linux-x86-64_3of7.zip
│ │ │ └── p10098816_112020_Linux-x86-64_4of7.zip
│ │ └── Windows_64
│ │ ├── Bundle\ Patch
│ │ │ └── p17363838_112020_MSWIN-x86-64.zip
│ │ ├── p10098816_112020_MSWIN-x86-64_1of7.zip
│ │ ├── p10098816_112020_MSWIN-x86-64_2of7.zip
│ │ ├── p10098816_112020_MSWIN-x86-64_3of7.zip
│ │ └── p10098816_112020_MSWIN-x86-64_4of7.zip
│ ├── 11203
│ │ ├── IBM_AIX_64
│ │ │ ├── PSU
│ │ │ │ ├── DB
│ │ │ │ │ └── p20760997_112030_AIX64-5L.zip
│ │ │ │ └── Grid
│ │ │ │ └── p20996944_112030_AIX64-5L.zip
│ │ │ ├── p10404530_112030_AIX64-5L_1of7.zip
│ │ │ ├── p10404530_112030_AIX64-5L_2of7.zip
│ │ │ ├── p10404530_112030_AIX64-5L_3of7.zip
│ │ │ └── p10404530_112030_AIX64-5L_4of7.zip
│ │ ├── Linux_64
│ │ │ ├── PSU
│ │ │ │ ├── DB
│ │ │ │ │ └── p20760997_112030_Linux-x86-64.zip
│ │ │ │ └── Grid
│ │ │ │ └── p20996944_112030_Linux-x86-64.zip
│ │ │ ├── p10404530_112030_Linux-x86-64_1of7.zip
│ │ │ ├── p10404530_112030_Linux-x86-64_2of7.zip
│ │ │ ├── p10404530_112030_Linux-x86-64_3of7.zip
│ │ │ └── p10404530_112030_Linux-x86-64_4of7.zip
│ │ └── Windows_64
│ │ ├── PSU
│ │ │ └── DB
│ │ │ └── p21104036_112030_MSWIN-x86-64.zip
│ │ ├── p10404530_112030_MSWIN-x86-64_1of7_2.zip
│ │ ├── p10404530_112030_MSWIN-x86-64_2of7_2.zip
│ │ ├── p10404530_112030_MSWIN-x86-64_3of7_2.zip
│ │ └── p10404530_112030_MSWIN-x86-64_4of7_2.zip
│ └── 11204
│ ├── IBM_AIX_64
│ │ ├── PSU
│ │ │ ├── DB
│ │ │ │ └── p31537677_112040_AIX64-5L.zip
│ │ │ └── Grid
│ │ │ └── p31718723_112040_AIX64-5L.zip
│ │ ├── p13390677_112040_AIX64-5L_1of7.zip
│ │ ├── p13390677_112040_AIX64-5L_2of7.zip
│ │ ├── p13390677_112040_AIX64-5L_3of7.zip
│ │ └── p13390677_112040_AIX64-5L_4of7.zip
│ ├── Linux_64
│ │ ├── PSU
│ │ │ ├── DB
│ │ │ │ └── p31537677_112040_Linux-x86-64.zip
│ │ │ ├── Grid
│ │ │ │ └── p31718723_112040_Linux-x86-64.zip
│ │ │ ├── p18370031_112040_Linux-x86-64.zip
│ │ │ └── p6880880_112000_Linux-x86-64.zip
│ │ ├── p13390677_112040_Linux-x86-64_1of7.zip
│ │ ├── p13390677_112040_Linux-x86-64_2of7.zip
│ │ ├── p13390677_112040_Linux-x86-64_3of7.zip
│ │ └── p13390677_112040_Linux-x86-64_4of7.zip
│ └── Windows_64
│ ├── PSU
│ │ └── DB
│ │ └── p31659823_112040_MSWIN-x86-64.zip
│ ├── p13390677_112040_MSWIN-x86-64_1of7.zip
│ ├── p13390677_112040_MSWIN-x86-64_2of7.zip
│ ├── p13390677_112040_MSWIN-x86-64_3of7.zip
│ └── p13390677_112040_MSWIN-x86-64_4of7.zip

4 Oracle 12C版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
bash复制代码├── 12c
│ ├── Client
│ │ ├── Linux
│ │ │ └── V839967-01.zip
│ │ └── Windows
│ │ └── V980724-01.zip
│ ├── IBM_AIX_64
│ │ ├── DB
│ │ │ └── V839965-01.zip
│ │ └── Grid
│ │ └── V840017-01.zip
│ ├── Linux_64
│ │ ├── DB
│ │ │ └── LINUX.X64_122010_db_home.zip
│ │ ├── Grid
│ │ │ ├── LINUX.X64_122010_grid_home.zip
│ │ │ └── cvuqdisk-1.0.10-1.rpm
│ │ └── p6880880_122010_Linux-x86-64.zip
│ └── Windows_64
│ ├── DB
│ │ └── V839963-01.zip
│ ├── Grid
│ │ └── V840015-01.zip
│ └── WINDOWS.X64_122010_db_home.zip

5 Oracle 18C版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
bash复制代码├── 18c
│ ├── Client
│ │ ├── Linux
│ │ │ ├── V978968-01.zip
│ │ │ └── V978978-01.zip
│ │ └── Windows
│ │ ├── V979520-01.zip
│ │ └── V979757-01.zip
│ ├── IBM_AIX_64
│ │ ├── DB
│ │ │ └── V980552-01.zip
│ │ └── Grid
│ │ └── V980556-01.zip
│ ├── Linux_64
│ │ ├── DB
│ │ │ └── LINUX.X64_180000_db_home.zip
│ │ └── Grid
│ │ └── LINUX.X64_180000_grid_home.zip
│ └── Windows_64
│ ├── DB
│ │ └── V979518-01.zip
│ └── Grid
│ └── V979522-01.zip

6 Oracle 19C版本

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
bash复制代码├── 19c
│ ├── Client
│ │ ├── Linux
│ │ │ ├── 32
│ │ │ │ └── V982070-01.zip
│ │ │ └── 64
│ │ │ └── V982064-01.zip
│ │ └── Windows
│ │ ├── 32
│ │ │ └── V982695-01.zip
│ │ └── 64
│ │ └── V982658-01.zip
│ ├── IBM_AIX_64
│ │ ├── DB
│ │ │ └── V982583-01.zip
│ │ └── Grid
│ │ └── V982588-01.zip
│ ├── Linux_64
│ │ ├── DB
│ │ │ └── LINUX.X64_193000_db_home.zip
│ │ ├── Grid
│ │ │ └── LINUX.X64_193000_grid_home.zip
│ │ └── RU
│ │ ├── p32545008_190000_Linux-x86-64.zip
│ │ ├── p32545013_190000_Linux-x86-64.zip
│ │ └── p6880880_190000_Linux-x86-64.zip
│ └── Windows_64
│ ├── DB
│ │ └── V982656-01.zip
│ └── Grid
│ └── V982662-01.zip

如需要其他版本Oracle安装包或者补丁包,可以关注我。


本次分享到此结束啦~

如果觉得文章对你有帮助,点赞、收藏、关注、评论,一键四连支持,你的支持就是我创作最大的动力。

本文转载自: 掘金

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

ElementUI 树型组件 el-tree 后台数据结构构

发表于 2021-07-11

前言

Vue+ElementUI 是目前项目开发中普遍使用的前端技术,我们在开发中肯定会遇到用树形展示数据的需求,比如公司和部门,公司下面有多个部门,部门下面又有多个班组,每个部门或者班组下有相关人员,此时我们就可以使用ElementUI 的 tree 组件来实现,前端根据官网上的例子,需要的数据是这样的:

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
javascript复制代码<el-tree :data="data" :props="defaultProps" @node-click="handleNodeClick"></el-tree>
<script>
export default {
data() {
return {
data: [{
label: '一级 1',
children: [{
label: '二级 1-1',
children: [{
label: '三级 1-1-1'
}]
}]
}, {
label: '一级 2',
children: [{
label: '二级 2-1',
children: [{
label: '三级 2-1-1'
}]
}, {
label: '二级 2-2',
children: [{
label: '三级 2-2-1'
}]
}]
}],
defaultProps: {
children: 'children',
label: 'label'
}
};
},
methods: {
handleNodeClick(data) {
console.log(data);
}
}
};
</script>

效果是这样的:
在这里插入图片描述
接下来就是后台去构建这样结构的数据,然后提供接口给前端了。

本篇文章的例子最终实现的效果是这样的:

在这里插入图片描述

后台返回的数据是这样的:

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
json复制代码{
"msg":"操作成功",
"code":200,
"data":[
{
"id":1,
"label":"停电性质",
"children":[
{
"id":2,
"label":"故障停电",
"children":[
{
"id":6,
"label":"短路"
}
]
},
{
"id":3,
"label":"预安排停电"
},
{
"id":4,
"label":"临时停电"
},
{
"id":5,
"label":"限电",
"children":[
{
"id":24,
"label":"测试"
}
]
}
]
}
]
}

一、数据库表结构设计

1
2
3
4
5
6
7
8
9
sql复制代码create table SYS_NATURE_TREE
(
nature_tree_id NUMBER not null,
nature_tree_name VARCHAR2(50),
parent_id NUMBER,
parent_ids VARCHAR2(50),
show_order NUMBER,
enable_flag NUMBER
)

其中 nature_tree_id 是主键、nature_tree_name 就是我们要展示的树节点的名称了,parent_id 是当前节点的父级,parent_ids 是当前节点的所有父级,用‘,’隔开。

二、具体实现

  1. entity

SYS_NATURE_TREE 对应的实体类:

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

private Integer natureTreeId;

private String natureTreeCode;

private String natureTreeName;

private Integer parentId;

private String parentIds;

private Integer showOrder;

private Integer enableFlag;

/** 子节点 List 集合 */
private List<NatureTree> children;

/** 构造器、get set 方法等省略了 **/
...

实体类里加了子节点的 List 集合字段

  1. controller

代码如下:

1
2
3
4
5
6
7
8
java复制代码	@GetMapping("/blackoutNatureTreeSelect")
public BaseApiService blackoutNatureTreeSelect(NatureTree natureTree)
{
// 先查出来 SYS_NATURE_TREE 表中的所有数据
List<NatureTree> natureTrees = blackoutNatureService.selectNatureTreeList(natureTree);
// 返回构建好的数据
return BaseApiService .success(blackoutNatureService.buildNatureTreeSelect(natureTrees));
}

思路是先将 sys_nature_tree 表中的数据全部查出来,然后作为参数传入构建数据结构的方法 buildNatureTreeSelect。重点是这个 service 方法 buildNatureTreeSelect

  1. service

代码如下:

1
2
3
4
5
6
java复制代码	@Override
public List<TreeSelect> buildNatureTreeSelect(List<NatureTree> natureTrees)
{
List<NatureTree> listNatureTrees = buildNatureTree(natureTrees);
return listNatureTrees.stream().map(TreeSelect::new).collect(Collectors.toList());
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
java复制代码	@Override
public List<NatureTree> buildNatureTree(List<NatureTree> natureTrees) {
// 初步处理好的数据
List<NatureTree> returnList = new ArrayList<NatureTree>();
// 所有树 treeId 的 List 集合
List<Integer> tempList = new ArrayList<Integer>();
for (NatureTree natureTree : natureTrees)
{
tempList.add(natureTree.getNatureTreeId());
}
// 遍历所有树节点(一个树节点就是数据库中的一条数据)
for (Iterator<NatureTree> iterator = natureTrees.iterator(); iterator.hasNext();)
{
NatureTree natureTree = (NatureTree) iterator.next();
// 如果是顶级节点, 遍历该父节点的所有子节点
if (!tempList.contains(natureTree.getParentId()))
{
// 递归处理数据,此处传入的 natureTree 是最顶级节点,没有父节点
recursionFn(natureTrees, natureTree);
returnList.add(natureTree);
}
}
if (returnList.isEmpty())
{
returnList = natureTrees;
}
return returnList;
}
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复制代码	/**
* 递归列表
*/
private void recursionFn(List<NatureTree> list, NatureTree t)
{
// 获取此节点的子节点列表
List<NatureTree> childList = getChildList(list, t);
// 将子节点 List set 进去
t.setChildren(childList);
for (NatureTree tChild : childList)
{
if (hasChild(list, tChild))
{
// 判断是否有子节点
Iterator<NatureTree> it = childList.iterator();
while (it.hasNext())
{
NatureTree n = (NatureTree) it.next();
// 递归
recursionFn(list, n);
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码	/**
* 获取子节点列表
*/
private List<NatureTree> getChildList(List<NatureTree> list, NatureTree t)
{
List<NatureTree> tlist = new ArrayList<NatureTree>();
Iterator<NatureTree> it = list.iterator();
while (it.hasNext())
{
NatureTree n = (NatureTree) it.next();
if (StringUtils.isNotNull(n.getParentId()) && n.getParentId().intValue() == t.getNatureTreeId().intValue())
{
tlist.add(n);
}
}
return tlist;
}
1
2
3
4
5
6
7
java复制代码/**
* 判断是否有子节点
*/
private boolean hasChild(List<NatureTree> list, NatureTree t)
{
return getChildList(list, t).size() > 0 ? true : false;
}
1
2
3
4
java复制代码// 经过 buildNatureTree 处理过的 listNatureTrees 中的 NatureTree,children 属性就都有数据了
List<NatureTree> listNatureTrees = buildNatureTree(natureTrees);
// 然后就是将 listNatureTrees 处理成最终格式的数据
listNatureTrees.stream().map(TreeSelect::new).collect(Collectors.toList());

TreeSelect 类

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
java复制代码/**
* Treeselect树结构实体类
*
* @author wangpsh
*/
public class TreeSelect implements Serializable
{
private static final long serialVersionUID = 1L;

/** 节点ID */
private Long id;

/** 节点名称 */
private String label;

/** 子节点 */
@JsonInclude(JsonInclude.Include.NON_EMPTY)
private List<TreeSelect> children;

public TreeSelect()
{

}

public TreeSelect(NatureTree natureTree)
{
this.id = (long) natureTree.getNatureTreeId();
this.label = natureTree.getNatureTreeName();
this.children = natureTree.getChildren().stream().map(TreeSelect::new).collect(Collectors.toList());
}

public Long getId()
{
return id;
}

public void setId(Long id)
{
this.id = id;
}

public String getLabel()
{
return label;
}

public void setLabel(String label)
{
this.label = label;
}

public List<TreeSelect> getChildren()
{
return children;
}

public void setChildren(List<TreeSelect> children)
{
this.children = children;
}
}

上面贴出了最主要的代码,用到了 java 8 的 stream 流。至此,我们的数据结构就构建完成了

  1. 数据初始化

数据库中初始化一些数据,其中的停电性质就是顶级节点
在这里插入图片描述

三、总结

以上树结构的数据是提前在数据库中初始化好的,也可以将树节点添加上增删改功能,这个相对来说就比较简单了。

本文转载自: 掘金

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

“中台”消失了?

发表于 2021-07-11

中台缘起阿里

近几年,“中台”这个词是火的一塌糊涂。导致好多公司也是盲目跟从,建设自己的中台,这个词又是怎么来的呢。


其实,这个词起源于阿里,也是最早落实到阿里的。随着17年逍遥子的一声令下,阿里开始了技术中台的建设,也正是如此,“中台”这个词也逐渐走到了大家面前。


至于,阿里为什么要建设中台,可以说是众说纷云。比较可信的是,Jack Ma带着阿里一众高管,拜访了荷兰一家游戏公司Supercell(号称世界上最成功的游戏公司)。当时他们的员工只有200-300人左右,却能在很短的时间内不断发布新品。究其原因就是他们将游戏中公用的模块抽象出来,有专门的团队来维护。而其他人分成小团队(10人左右),只需要接入公用的部分,就可以不断去开发新的游戏,从而迅速的推出新的产品。这一点深深的影响到了拜访的阿里一众人,所以回去后,他们就开始建设新的架构体系。

浅谈中台

既然要做中台,那么何为中台?如何去做。所谓中台,就是将通用的能力沉淀到一个模块,然后对于业务方或者需求方提供输出的能力,避免重复性工作,以此来提升效率。这样的描述可能让读者觉得有点抽象,难以理解。


下面举个例子,来进行解释:比如我们的要开发一个APP 【X】,是一个偏社交类的产品,要做一个类似于朋友圈的功能,它就涉及到发布内容,评论,点赞,分享等功能。而公司有好几个产品,都有类似点赞,分享,评论的功能。而每个团队都是自己开发一套相同功能的服务。其架构如下:

对于这样的场景,我们是否可以把点赞,评论,分享的服务抽象出一个服务中台,任何业务方需要这样的需求,直接来接入即可,不用自己单独来开发,避免重复性的工作。这就是中台化,为多个业务方提供业务能力。因此,我们是不是可以进一步优化:

这样以来,我们每次开发只需要快速的接入评论中台,然后就能完成相应的业务,效率会大大提升。当然,中台也分为数据中台,业务中台等。业务中台也可以分为移动中台,管理中台,技术中台等等。

中台现状

从上面的介绍,我们不难看出,中台建设的场景出现在整个公司层面,每个部门存在共用某部分业务的诉求。所以对于中小型公司,这样的场景就少了很多。如果去强行建设中台,反倒是一种资源浪费。对于大公司而言,有强烈的建设中台诉求,这也就是为什么阿里,腾讯,百度,字节等公司会建设中台。

关于中台的未来

我们常听到的一句话:“大前台,小中台”,前台服务直接对接前端,会汇聚多个中台以及自己本身的业务,中台就只专注于某部分公共服务。当然这么说也不是说中台不好,各有所专嘛。随着国内互联网的极速发展,各垂直业务都有比较大的巨头,比如出行的滴滴,游戏的腾讯和网易,短视频的快手和字节,电商的阿里和拼夕夕,要想在这个大环境生存,就必须有快速验证的新业务的能力,这就需要我们有基础公共能力的支撑。在未来很长一段时间,我相信中台都是我们所要不断完善和建设的。

本文转载自: 掘金

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

Cobra + Client-go实现K8s 自定义插件开发

发表于 2021-07-10

一 背景

在我们使用kubectl查看k8s资源的时候,想直接查看对应资源的容器名称和镜像名称,目前kubectl还不支持该选型,需要我们describe然后来查看,对于集群自己比较多,不是很方便,因此萌生了自己开发kubectl 插件来实现该功能。

二 相关技术

2.1 Cobra

Cobra是一个命令行程序库,其是一个用来编写命令行的神器,提供了一个脚手架,用于快速生成基于Cobra应用程序框架。我们可以利用Cobra快速的去开发出我们想要的命令行工具,非常的方便快捷。

详细可参考:Golang 开发之Cobra初探

2.2 Client-go

在K8s运维中,我们可以使用kubectl、客户端库或者REST请求来访问K8S API。而实际上,无论是kubectl还是客户端库,都是封装了REST请求的工具。client-go作为一个客户端库,能够调用K8S API,实现对K8S集群中资源对象(包括deployment、service、ingress、replicaSet、pod、namespace、node等)的增删改查等操作。

详细可参考:K8s二开之 client-go 初探

2.3 k8s 插件krew

Krew 是 类似于系统的apt、dnf或者brew的 kubectl插件包管理工具,利用其可以轻松的完成kubectl 插件的全上面周期管理,包括搜索、下载、卸载等。

kubectl 其工具已经比较完善,但是对于一些个性化的命令,其宗旨是希望开发者能以独立而紧张形式发布自定义的kubectl子命令,插件的开发语言不限,需要将最终的脚步或二进制可执行程序以kubectl- 的前缀命名,然后放到PATH中即可,可以使用kubectl plugin list查看目前已经安装的插件。

详细可参考:k8s 插件管理工具之krew使用

三 插件规划

  • 插件命名为:kubeimg。
  • 目前仅简单实现一个image命令,用于查看不同资源对象(deployments/daemonsets/statefulsets/jobs/cronjobs)的名称,和对应容器名称,镜像名称。
  • 支持json格式输出。
  • 最后将其作为krew插件使用。
  • 可以直接根据名称空间来进行查看对应资源。

四 实战开发

4.1 项目初始化

  • 安装cobra
1
shell复制代码go get -v github.com/spf13/cobra/cobra
  • 初始化项目
1
2
3
4
5
6
7
8
9
10
11
12
13
shell复制代码 $ ~/workspace/goworkspace/src/github.com/kaliarch/kubeimg  /Users/xuel/workspace/goworkspace/bin/cobra init --pkg-name kubeimg
Your Cobra application is ready at
/Users/xuel/workspace/goworkspace/src/github.com/kaliarch/kubeimg
$ ~/workspace/goworkspace/src/github.com/kaliarch/kubeimg  ls
LICENSE cmd main.go
$ ~/workspace/goworkspace/src/github.com/kaliarch/kubeimg  tree

├── LICENSE
├── cmd
│   └── root.go
└── main.go

1 directory, 3 files
  • 创建go mod,下载相关包
1
shell复制代码go mod init kubeimg

4.2 增加子命令

增加一个子命令image。

1
2
shell复制代码$ /Users/xuel/workspace/goworkspace/bin/cobra add image
image created at /Users/xuel/workspace/goworkspace/src/github.com/kaliarch/kubeimg

4.3 添加参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
go复制代码
// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
cobra.CheckErr(rootCmd.Execute())
}

func init() {
KubernetesConfigFlags = genericclioptions.NewConfigFlags(true)
imageCmd.Flags().BoolP("deployments", "d", false, "show deployments image")
imageCmd.Flags().BoolP("daemonsets", "e", false, "show daemonsets image")
imageCmd.Flags().BoolP("statefulsets", "f", false, "show statefulsets image")
imageCmd.Flags().BoolP("jobs", "o", false, "show jobs image")
imageCmd.Flags().BoolP("cronjobs", "b", false, "show cronjobs image")
imageCmd.Flags().BoolP("json", "j", false, "show json format")
KubernetesConfigFlags.AddFlags(rootCmd.PersistentFlags())
}

4.4 实现image命令

1
2
3
4
5
6
7
8
9
10
go复制代码var imageCmd = &cobra.Command{
Use: "image",
Short: "show resource image",
Long: `show k8s resource image`,
RunE: image,
}

func init() {
rootCmd.AddCommand(imageCmd)
}

4.5 初始化clientset

1
2
3
4
5
6
7
8
9
10
11
12
13
go复制代码// ClientSet k8s clientset
func ClientSet(configFlags *genericclioptions.ConfigFlags) *kubernetes.Clientset {
config, err := configFlags.ToRESTConfig()
if err != nil {
panic("kube config load error")
}
clientSet, err := kubernetes.NewForConfig(config)
if err != nil {

panic("gen kube config error")
}
return clientSet
}

4.6 实现查看资源对象

利用反射实现根据不同资源类型查看具体对应资源镜像及镜像名称功能。

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
go复制代码func image(cmd *cobra.Command, args []string) error {

clientSet := kube.ClientSet(KubernetesConfigFlags)
ns, _ := rootCmd.Flags().GetString("namespace")
// 生命一个全局资源列表
var rList []interface{}

if flag, _ := cmd.Flags().GetBool("deployments"); flag {
deployList, err := clientSet.AppsV1().Deployments(ns).List(context.Background(), v1.ListOptions{})
if err != nil {
fmt.Printf("list deployments error: %s", err.Error())
}
rList = append(rList, deployList)
}
...
deployMapList := make([]map[string]string, 0)
for i := 0; i < len(rList); i++ {
switch t := rList[i].(type) {
case *kv1.DeploymentList:
for k := 0; k < len(t.Items); k++ {
for j := 0; j < len(t.Items[k].Spec.Template.Spec.Containers); j++ {
deployMap := make(map[string]string)
deployMap["NAMESPACE"] = ns
deployMap["TYPE"] = "deployment"
deployMap["RESOURCE_NAME"] = t.Items[k].GetName()
deployMap["CONTAINER_NAME"] = t.Items[k].Spec.Template.Spec.Containers[j].Name
deployMap["IMAGE"] = t.Items[k].Spec.Template.Spec.Containers[j].Image
deployMapList = append(deployMapList, deployMap)
}
}

4.6 实现输出

利用tabel来对结果进行输出

1
2
3
4
5
6
7
8
9
10
go复制代码
func GenTable(mapList []map[string]string) *table.Table {
t, err := gotable.Create(title...)
if err != nil {
fmt.Printf("create table error: %s", err.Error())
return nil
}
t.AddRows(mapList)
return t
}

最终项目结构:

五 测试

对完成的插件进行测试,编译go build生成kubeimg二进制可执行文件。

5.1 查看帮助

  • 查看所有帮助

其中可以看到cobra帮我们自动生成了help和completion两个命令,可以快速实现table补全,支持bash/fish/zsh/powershell

1
go复制代码./kubeimg --help

  • 查看image命令flags
1
go复制代码./kubeimg image --help

5.1 查看deployment资源

不知地ing名称名称空间,默认查看所有,名称空间下的资源

1
shell复制代码./kubeimg image -d

5.2 查看某个名称空间下资源

1
shell复制代码./kubeimg image -d -n kube-system

5.3 查看所有资源

可以看到imlc-operator-controller-manager 一个pod中有两个container。

1
shell复制代码./kubeimg image -b -e -d -o -f

5.4 json格式输出

1
shell复制代码./kubeimg image -o -j

六 作为krew插件使用

需要将最终的脚步或二进制可执行程序以kubectl- 的前缀命名,然后放到PATH中即可,可以使用kubectl plugin list查看目前已经安装的插件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
shell复制代码$ kubectl plugin list
The following compatible plugins are available:=
/usr/local/bin/kubectl-debug
- warning: kubectl-debug overwrites existing command: "kubectl debug"
/usr/local/bin/kubectl-v1.10.11
/usr/local/bin/kubectl-v1.20.0
/Users/xuel/.krew/bin/kubectl-df_pv
/Users/xuel/.krew/bin/kubectl-krew

# 将自己开发的插件重新命名为kubectl-img放到可执行路基下
$ cp kubeimg /Users/xuel/.krew/bin/kubectl-img

$ kubectl plugin list
The following compatible plugins are available:=
/usr/local/bin/kubectl-debug
- warning: kubectl-debug overwrites existing command: "kubectl debug"
/usr/local/bin/kubectl-v1.10.11
/usr/local/bin/kubectl-v1.20.0
/Users/xuel/.krew/bin/kubectl-df_pv
/Users/xuel/.krew/bin/kubectl-krew
/Users/xuel/.krew/bin/kubectl-img

$ cp kubeimg /Users/xuel/.krew/bin/kubectl-img

之后就可以想使用kubectl插件一样使用了。


其他
==

目前实现的比较简单,以此来抛砖引玉的功能,后期可以进行更多功能或其他插件的开发,自己动手丰衣足食。

后期待再完善开源到github,项目地址:kubeclt-img,以供大家学习交流。

参考链接

  • juejin.cn/post/696918…
  • juejin.cn/post/696286…
  • github.com/spf13/cobra

本文转载自: 掘金

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

SpringBoot:实现PC端微信扫码支付-超详细实战!

发表于 2021-07-10

开篇前絮叨两句,也算是我个人的一个记录吧,整体实现了微信扫码支付,但还有很多细节和提升的点,为了简介明了,只将整体过程给大家串一下,如果有大佬看到也要多多指点,不懂的也可以私信我。如果想了解微信登陆的,在分栏微信相关也有文章,大家可以看一下

实现应用微信支付,你需要有微信商户平台:pay.weixin.qq.com。申请公众号(服务号)认证费300,才能开通微信支付。在微信支付中需要有公众号id和密钥还有商户id和密钥,如果你没有上线应用把整体流程明白就可以,当然,朋友公司之类有的话更好。

先将一些用到的链接地址放在这里,方便大家查看

微信支付申请流程:pay.weixin.qq.com/guide/qrcod…

常用支付方式文档:pay.weixin.qq.com/wiki/doc/ap…

案例演示:pay.weixin.qq.com/guide/webba…

扫码支付文档:pay.weixin.qq.com/wiki/doc/ap…

微信支付时序图:pay.weixin.qq.com/wiki/doc/ap…

统一下单文档:pay.weixin.qq.com/wiki/doc/ap…

签名算法规范:pay.weixin.qq.com/wiki/doc/ap…

签名校验工具:pay.weixin.qq.com/wiki/doc/ap…

NatAPP内网穿透:natapp.cn/

上代码之前需要给大家讲解一些必要知识,不然直接来代码你还是一头雾水,完成了功能但不明白这个过程也是白费

Step1:微信网站扫码支付介绍

Stpe1.1:名词理解

appid:公众号唯一表示

appsecret:公众号密钥

mch_id:商户号,申请微信支付的时候分配的

key:支付交易过程中生成签名的密钥, 设置路径: 微信商户平台(pay.wexin.qq.com)–>账户中心–>账户设置–>API安全–>密钥设置

Step1.2:微信支付交互方式

  • POST方式提交
  • XML格式的协议
  • 签名算法MD5
  • 交互业务规则 先判断协议字段返回,再判断业务返回,最后判断交易状态
    ​ 接口交易单位 分
    ​ 交易类型:JSAPI– 公众号支付、NATIVE–原生扫码支付、APP-app支付
  • 商户订单号规则:商户支付的订单号由商户自定义生成,仅支持使用字母、数字、中划线-、下划线_、竖线|、星号*这些英文半角字符的组合,请勿使用汉字或全角等特殊字符,微信支付要求商户订单号保持唯一性

Step1.3:时序图讲解(重点!)

顶部有微信官方的时序图链接。这个图一定要明白,因为下面我上代码会告诉这是时序中的第几步,看的图就容易明白代码了

时序图说白了就是你一个操作的流程,这个过程中会经过哪个对象的方法,返回什么操作等的过程

顺序的时序图:就是交互流程图(把大象装进冰箱分几步)

对象(Object)、生命线(Lifeline)、激活(Activation)、消息(Message)

对象:时序图中的对象在交互中扮演的角色就是对象,使用矩形将对象名称包含起来, 名称下有下划线

生命线:生命线是一条垂直的虚线, 这条虚线表示对象的存在, 在时序图中, 每个对象都有生命线

激活:代表时序图中对象执行一项操作的时期, 表示该对象被占用以完成某个任务,当对象处于激活时期, 生命线可以拓宽为矩形

消息:对象之间的交互是通过相互发消息来实现的,箭头上面标出消息名,一个对象可以请求(要求)另一个对象做某件事件。消息从源对象指向目标对象,消息一旦发送便将控制从源对象转移到目标对象,息的阅读顺序是严格自上而下的

消息交互中的实线:请求消息

消息交互中的虚线:响应返回消息

自己调用自己的方法:反身消息

用我的白话给大家讲一下:

1.用户下单,进入购买页面,点击购买进入后台

2.后台收到请求,生成订单。大家肯定都用淘宝买过东西对吧,你购买东西但发现钱不够,这个订单在一段时间内都存在等待你支付,但这个订单在数据库中已经申城了,之后你支付后订单才会修改状态

3.后台调微信统一下单,不光你后台生成订单,微信也生成预订单号

4.微信返回code_url支付交易链接,通过这个值生成二维码图片

5.将这个二维码返回给前台用户,用户进行扫一扫支付。这里是直接和微信交互的

6.微信支付系统验证有效性,验证后返回用户是否确认支付

7.用户确认,输密码。返回给微信支付系统授权

8.验证授权,完成支付交易

9.返回支付结果,发送短信和微信消息提示。这里是并行处理,一个通知用户,一个通知后台

10.异步通知后台支付结果,会携带一些参数,订单号等。收到结果后告知接收情况

11.如果后台宕机,微信会定时发送通知,后台可以做定时任务,用户支付了但订单状态未修改,定时调微信的接口,调API有没有成功做操作

下面的一些流程都是看业务情况
在这里插入图片描述

Step2:统一下单

商户系统先调用该接口在微信支付服务后台生成预支付交易订单,返回正确的预支付交易会标识后再按扫码、JSAPI、APP等不同场景生成交易串调起支付。这里是时序图第二步

顶部链接文档:pay.weixin.qq.com/wiki/doc/ap…

这里上个统一下单的流程图

1.告诉微信支付你要下单

2.微信支付系统数据库生成一条订单,但未支付。你的后台也是生成一条未支付订单

3.用户支付后微信支付将订单更新为已支付

4.微信调后台告诉我们已经支付了

5.后台再返回确认信息等

时序图和统一下单的流程基本都在这了,一定要明白,一定要清楚!
在这里插入图片描述

Step2.1:统一下单请求

向微信支付系统发送http请求,我们需要组成一个xml格式的数据消息,里面包括一些必须的参数
在这里插入图片描述

官方例子
在这里插入图片描述

大部分人在做微信支付都是错在这里,签名方式不对,或者传输的一些信息不符合规范等,这里只是先给大家讲解一下,下面实战都会说清楚的。

Stpe2.2:统一下单返回消息

返回一些我们需要的参数,也就是code_url,如果你发送的xml不正确会返回错误提示
在这里插入图片描述

在这里插入图片描述

Step3:战前准备

Step3.1:数据库设计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
sql复制代码视频表 也可以认为是商品表 里面的一些字段是按照我项目需求来的,有一些你感觉用不到的可以不加,如果你有自己的数据库设计更好
CREATE TABLE `video` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键id',
`title` varchar(255) DEFAULT NULL COMMENT '视频标题',
`summary` varchar(255) DEFAULT NULL COMMENT '详情',
`cover_img` varchar(255) DEFAULT NULL COMMENT '封面图',
`price` int(11) DEFAULT NULL COMMENT '价格-最小单位分',
`c_id` int(10) DEFAULT NULL COMMENT '分类id',
`point` double(11,2) DEFAULT NULL COMMENT '评分-最长11保留两位小数',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`view_num` int(10) DEFAULT NULL COMMENT '观看数',
`online` int(11) DEFAULT '1' COMMENT '0表示未上线,1表示上线',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=49 DEFAULT CHARSET=utf8;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
less复制代码订单表	用户表就不给大家上了 无非就是跟了个用户主键 自己创一个就可以了
del字段采用逻辑删除 避免订单出现问题 不删除信息
CREATE TABLE `video_order` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键id',
`out_trade_no` varchar(64) DEFAULT NULL COMMENT '订单流水号唯一标识',
`state` int(11) DEFAULT NULL COMMENT '订单状态(1支付-0未支付)',
`create_time` datetime DEFAULT NULL COMMENT '订单创建时间',
`total_fee` int(11) DEFAULT NULL COMMENT '订单金额',
`video_id` int(11) DEFAULT NULL COMMENT '视频主键id',
`video_title` varchar(128) DEFAULT NULL COMMENT '标题字段冗余',
`video_img` varchar(255) DEFAULT NULL COMMENT '图片字段冗余',
`user_id` int(11) DEFAULT NULL COMMENT '用户主键id',
`ip` varchar(64) DEFAULT NULL COMMENT '用户ip地址',
`openid` varchar(64) DEFAULT NULL COMMENT '用户标示',
`notify_time` datetime DEFAULT NULL COMMENT '支付回调时间',
`nickname` varchar(32) DEFAULT NULL COMMENT '微信昵称',
`head_img` varchar(128) DEFAULT NULL COMMENT '微信头像',
`del` int(11) DEFAULT '0' COMMENT '0表示未删除,1表示已经删除',
PRIMARY KEY (`id`),
UNIQUE KEY `out_trade_no` (`out_trade_no`)
) ENGINE=InnoDB AUTO_INCREMENT=35 DEFAULT CHARSET=utf8;

Step3.2:实体类

自行加上get set方法

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
swift复制代码/**
* 视频实体
*/
public class Video implements Serializable {

private Integer id;

/**
* 视频标题
*/
private String title;

/**
* 描述
*/
private String summary;

/**
* 封面图路径
*/
@JsonProperty("cover_img")
private String coverImg;

/**
* 价格
*/
private Integer price;

/**
* 视频分类
*/
@JsonProperty("c_id")
private Integer cId;

/**
* 评分
*/
private Double point;

/**
* 视频创建时间
*/
@JsonFormat(pattern = "yyyy-MM-dd hh:mm:ss", locale = "zh", timezone = "GMT+8")
@JsonProperty("create_time")
private Date createTime;

/**
* 观看数
*/
@JsonProperty("view_num")
private Integer viewNum;

/**
* 0表示未上线,1表示上线
*/
private Integer online;
@JsonProperty("chapter_list")
private List<Chapter> chapterList;
}
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
scss复制代码/**
* 视频订单实体
*/
public class VideoOrder implements Serializable {

private Integer id;

/**
* 订单流水号
*/
@JsonProperty("out_trade_no")
private String outTradeNo;

/**
* 订单状态
*/
private Integer state;

/**
* 订单创建时间
*/
@JsonProperty("cover_img")
@JsonFormat(pattern = "yyyy-MM-dd hh:mm:ss", locale = "zh", timezone = "GMT+8")
private Date createTime;

/**
* 订单金额
*/
@JsonProperty("total_fee")
private Integer totalFee;

/**
* 视频id
*/
@JsonProperty("video_id")
private Integer videoId;

/**
* 视频荣誉字段-标题
*/
@JsonProperty("video_title")
private String videoTitle;

/**
* 视频冗余字段-图片
*/
@JsonProperty("video_img")
private String videoImg;

/**
* 用户id
*/
@JsonProperty("user_id")
private Integer userId;

/**
* 用户ip地址
*/
private String ip;

@JsonProperty("open_id")
private String openId;

/**
* 支付回调时间2
*/
@JsonProperty("notify_time")
@JsonFormat(pattern = "yyyy-MM-dd hh:mm:ss", locale = "zh", timezone = "GMT+8")
private Date notifyTime;

/**
* 冗余字段:微信昵称
*/
@JsonProperty("nick_name")
private String nickName;

/**
* 冗余字段:微信头像
*/
@JsonProperty("head_img")
private String headImg;

/**
* 0表示未删除,1表示已经删除
*/
private Integer del;
}
1
2
3
4
5
scala复制代码/**
* 订单数据传输对象
*/
public class VideoOrderDto extends VideoOrder {
}

Step3.3:配置文件

******代表用自己的微信配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
ini复制代码server.port=8089

#==============================数据库相关配置==========================================================
spring.datasource.driver-class-name =com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/educationapp?useUnicode=true&characterEncoding=utf-8&useSSL=false
spring.datasource.username=root
spring.datasource.password=root
#使用阿里巴巴druid数据源,默认使用自带的(com.zaxxer,hikari.HikariDataSource)如果使用默认的这里就不用写
spring.datasource.type =com.alibaba.druid.pool.DruidDataSource


#=============================MyBatis相关配置
#开启控制台打印sql
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

# mybatis 下划线转驼峰配置,两者都可以
#mybatis.configuration.mapUnderscoreToCamelCase=true
mybatis.configuration.map-underscore-to-camel-case=true
#mapper配置扫描
mybatis.mapper-locations=classpath:mapper/*.xml
#配置xml的结果别名 resultType:去掉前缀
mybatis.type-aliases-package=net.jhclass.online_jhclass.model.pojo


#======================================微信相关
#公众号
wxpay.appid=*********
wxpay.appsecret=***********


#微信商户平台商户号
wxpay.mer_id=********
#密钥
wxpay.key=**************
#回调地址
wxpay.callback=http://sj6e6c.natappfree.cc/api/v2/wechat/order/callback

Step3.4:微信配置类

自行加上get、set方法

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
kotlin复制代码/**
* 微信配置类
*/
@Configuration
@PropertySource(value = "classpath:application.properties")
public class WeChatConfig {

/**
* 公众号appid
*/
@Value("${wxpay.appid}")
private String appId;

/**
* 公众号密钥
*/
@Value("${wxpay.appsecret}")
private String appsecret;
/**
* 商户号ID
*/
@Value("${wxpay.mer_id}")
private String mchId;

/**
* 支付key
*/
@Value("${wxpay.key}")
private String key;

/**
* 微信支付回调URL
*/
@Value("${wxpay.callback}")
private String payCallbackUrl;

/**
* 微信统一下单url地址
*/
private final static String UNIFIED_ORDER_URL="https://api.mch.weixin.qq.com/pay/unifiedorder";

Step3.5:封装http、get、post方法

相关依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
xml复制代码<!--HttpClient-->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.3</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpmime</artifactId>
<version>4.5.2</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.1.1</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpcore</artifactId>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
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
scss复制代码/**
* 封装http get post方法
*/
public class HttpUtils {

private static final Gson gson = new Gson();

/**
* get方法
* @param url
* @return
*/
public static Map<String,Object> doGet(String url){

Map<String,Object> map = new HashMap<>();

CloseableHttpClient httpClient = HttpClients.createDefault();


RequestConfig requestConfig = RequestConfig.custom().setConnectTimeout(5000)//连接超时
.setConnectionRequestTimeout(5000)//请求连接超时
.setSocketTimeout(5000)
.setRedirectsEnabled(true)//允许重定向
.build();

HttpGet httpGet = new HttpGet(url);
httpGet.setConfig(requestConfig);

try {
HttpResponse httpResponse = httpClient.execute(httpGet);
if(httpResponse.getStatusLine().getStatusCode() == 200){

String jsonResult = EntityUtils.toString(httpResponse.getEntity());
//转换key value形式
map = gson.fromJson(jsonResult,map.getClass());
}
}catch (Exception e){
e.printStackTrace();
}finally {
//关闭请求
try {
httpClient.close();
}catch (Exception e){
e.printStackTrace();
}
}

return map;
}


public static String doPost(String url,String data,int timeout){

CloseableHttpClient httpClient = HttpClients.createDefault();

//超时设置
RequestConfig requestConfig = RequestConfig.custom().setConnectTimeout(timeout)//连接超时
.setConnectionRequestTimeout(timeout)//请求连接超时
.setSocketTimeout(timeout)
.setRedirectsEnabled(true)//允许重定向
.build();
HttpPost httpPost = new HttpPost(url);
httpPost.setConfig(requestConfig);
//增加头信息
httpPost.addHeader("Content-Type","text/html;chartset=UTF-8");
if(data != null && data instanceof String){//使用字符窜传参
StringEntity stringEntity = new StringEntity(data,"UTF-8");
httpPost.setEntity(stringEntity);
}

try {

CloseableHttpResponse httpResponse = httpClient.execute(httpPost);
HttpEntity httpEntity = httpResponse.getEntity();
if(httpResponse.getStatusLine().getStatusCode() == 200){
String result = EntityUtils.toString(httpEntity);
return result;
}
}catch (Exception e){
e.printStackTrace();
}finally {
try {
httpClient.close();
}catch (Exception e){
e.printStackTrace();
}
}
return null;
}
}

Step3.6:微信支付工具类 xml转map mao转xml 生成签名

微信官方也有java相关的工具类,基本给大家的无差别,这里我就直接给大家上代码用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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
ini复制代码public class WXPayUtil {

/**
* XML格式字符串转换为Map
*
* @param strXML XML字符串
* @return XML数据转换后的Map
* @throws Exception
*/
public static Map<String, String> xmlToMap(String strXML) throws Exception {
try {
Map<String, String> data = new HashMap<String, String>();
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
InputStream stream = new ByteArrayInputStream(strXML.getBytes("UTF-8"));
org.w3c.dom.Document doc = documentBuilder.parse(stream);
doc.getDocumentElement().normalize();
NodeList nodeList = doc.getDocumentElement().getChildNodes();
for (int idx = 0; idx < nodeList.getLength(); ++idx) {
Node node = nodeList.item(idx);
if (node.getNodeType() == Node.ELEMENT_NODE) {
org.w3c.dom.Element element = (org.w3c.dom.Element) node;
data.put(element.getNodeName(), element.getTextContent());
}
}
try {
stream.close();
} catch (Exception ex) {
// do nothing
}
return data;
} catch (Exception ex) {
throw ex;
}

}

/**
* 将Map转换为XML格式的字符串
*
* @param data Map类型数据
* @return XML格式的字符串
* @throws Exception
*/
public static String mapToXml(Map<String, String> data) throws Exception {
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder documentBuilder= documentBuilderFactory.newDocumentBuilder();
org.w3c.dom.Document document = documentBuilder.newDocument();
org.w3c.dom.Element root = document.createElement("xml");
document.appendChild(root);
for (String key: data.keySet()) {
String value = data.get(key);
if (value == null) {
value = "";
}
value = value.trim();
org.w3c.dom.Element filed = document.createElement(key);
filed.appendChild(document.createTextNode(value));
root.appendChild(filed);
}
TransformerFactory tf = TransformerFactory.newInstance();
Transformer transformer = tf.newTransformer();
DOMSource source = new DOMSource(document);
transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
StringWriter writer = new StringWriter();
StreamResult result = new StreamResult(writer);
transformer.transform(source, result);
String output = writer.getBuffer().toString(); //.replaceAll("\n|\r", "");
try {
writer.close();
}
catch (Exception ex) {
}
return output;
}


/**
* 生成微信支付sign
* @return
*/
public static String createSign(SortedMap<String, String> params, String key){
StringBuilder sb = new StringBuilder();
Set<Map.Entry<String, String>> es = params.entrySet();
Iterator<Map.Entry<String,String>> it = es.iterator();

//生成 stringA="appid=wxd930ea5d5a258f4f&body=test&device_info=1000&mch_id=10000100&nonce_str=ibuaiVcKdpRxkhJA";
while (it.hasNext()){
Map.Entry<String,String> entry = (Map.Entry<String,String>)it.next();
String k = (String)entry.getKey();
String v = (String)entry.getValue();
if(null != v && !"".equals(v) && !"sign".equals(k) && !"key".equals(k)){
sb.append(k+"="+v+"&");
}
}

sb.append("key=").append(key);
String sign = CommonUtils.MD5(sb.toString()).toUpperCase();
return sign;
}


/**
* 校验签名
* @param params
* @param key
* @return
*/
public static boolean isCorrectSign(SortedMap<String, String> params, String key){
String sign = createSign(params,key);

String weixinPaySign = params.get("sign").toUpperCase();

return weixinPaySign.equals(sign);
}


/**
* Map转SortedMap 获取有序map
* @param map
* @return
*/
public static SortedMap<String,String> getSortedMap(Map<String,String> map){
SortedMap<String,String> sortedMap = new TreeMap<>();
Iterator<String> it = map.keySet().iterator();
while (it.hasNext()){
String key = (String)it.next();
String value = map.get(key);
//定义临时变量
String temp="";
if(null!=value){
temp = value.trim();
}
sortedMap.put(key,temp);
}


return sortedMap;
}


}

Step3.7:返回工具类

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
typescript复制代码/**
* 数据传输对象(后端输出对象)
* @param <T>
* Created by hanlu on 2017/5/7.
*/
public class Dto<T>{
private String success; //判断系统是否出错做出相应的true或者false的返回,与业务无关,出现的各种异常
private String errorCode;//该错误码为自定义,一般0表示无错
private String msg;//对应的提示信息
private T data;//具体返回数据内容(pojo、自定义VO、其他)
private int count; // 数据的数量

public int getCount() {
return count;
}

public void setCount(int count) {
this.count = count;
}

public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
public String getSuccess() {
return success;
}
public void setSuccess(String success) {
this.success = success;
}
public String getErrorCode() {
return errorCode;
}
public void setErrorCode(String errorCode) {
this.errorCode = errorCode;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}

}
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
scss复制代码
/**
* 用于返回Dto的工具类
* Created by XX on 17-5-8.
*/
public class DtoUtil {

public static String success="true";

public static String fail="false";

public static String errorCode="0";
/***
* 统一返回成功的DTO
*/
public static Dto returnSuccess(){
Dto dto=new Dto();
dto.setSuccess(success);
return dto;
}
/***
* 统一返回成功的DTO 带数据
*/
public static Dto returnSuccess(String message,Object data){
Dto dto=new Dto();
dto.setSuccess(success);
dto.setMsg(message);
dto.setErrorCode(errorCode);
dto.setData(data);
return dto;
}
/***
* 统一返回成功的DTO 不带数据
*/
public static Dto returnSuccess(String message){
Dto dto=new Dto();
dto.setSuccess(success);
dto.setMsg(message);
dto.setErrorCode(errorCode);
return dto;
}
/***
* 统一返回成功的DTO 带数据 没有消息
*/
public static Dto returnDataSuccess(Object data){
Dto dto=new Dto();
dto.setSuccess(success);
dto.setErrorCode(errorCode);
dto.setData(data);
return dto;
}

/**
* 请求失败,返回错误语句及错误码
* @param message
* @param errorCode
* @return
*/
public static Dto returnFail(String message,String errorCode){
Dto dto=new Dto();
dto.setSuccess(fail);
dto.setMsg(message);
dto.setErrorCode(errorCode);
return dto;
}

/**
* 返回数据 并返回数据数量
* @param data
* @param count
* @return
*/
public static Dto returnPage(Object data,int count){
Dto dto=new Dto();
dto.setSuccess(success);
dto.setErrorCode(errorCode);
dto.setData(data);
dto.setCount(count);

return dto;
}
}

Step4:生成订单

一些其它像查询用户信息和查询视频信息的操作就不给大家上了,就是简单查询,这里主要做订单的,避免大家看不懂下面代码的一些方法

Step4.1:Service层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
java复制代码public interface VideoOrderService {

/**
* 下单操作 你会问 不应该是int返回么 为什么String? 你dao层写int service就写String,因为需要拿微信返回的code_url 所以这里写String
* @return
*/
String saveVideoOrder(VideoOrderDto videoOrderDto) throws Exception;

/**
* 查询用户订单列表
* @param userId 用户id
* @return
*/
List<VideoOrder> listOrderByUserId(Integer userId);

/**
* 根据订单流水号查找订单对象
* @param outTradeNo
* @return
*/
VideoOrder findByOutTradeNo(String outTradeNo);

/**
* 根据流水号更新订单状态
* @param videoOrder
* @return
*/
int updateVideoOrderByOutTradeNo(VideoOrder videoOrder);
}

Step4.2:mapper文件

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" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >

<mapper namespace="net.jhclass.online_jhclass.mapper.VideoOrderMapper">

<!--检查用户订单状态-->
<select id="findByUserIdAndVideoIdAndState" resultType="VideoOrder">
select * from `video_order` where user_id=#{user_id} and video_id=#{video_id} and state=#{state}
</select>

<!--下单-->
<insert id="saveVideoOrder" useGeneratedKeys="true" keyColumn="id" keyProperty="id" parameterType="VideoOrder">
insert into `video_order` (out_trade_no,state,create_time,total_fee,video_id,video_title,video_img,user_id,ip,openid,notify_time,nickname,head_img,del)
values (#{outTradeNo},#{state},#{createTime},#{totalFee},#{videoId},#{videoTitle},#{videoImg},#{userId},#{ip},#{openId},#{notifyTime},#{nickName},#{headImg},#{del})
</insert>

<!--查询订单列表-->
<select id="listOrderByUserId" resultType="VideoOrder">
select * from `video_order` where user_id=#{user_id}
</select>

<!--根据订单id 查询订单信息-->
<select id="findById" resultType="VideoOrder">
select * from `video_order` where id=#{order_id} and del=0
</select>

<!--根据订单流水号 查询订单信息-->
<select id="findByOutTradeNo" resultType="VideoOrder">
select * from `video_order` where out_trade_no=#{out_trade_no} and del=0
</select>

<!--逻辑删除订单-->
<update id="del">
update video_order set del=0 where id=#{id} and user_id=#{userId}
</update>

<!--
微信回调更新订单状态
根据订单流水号更新
-->
<update id="updateVideoOrderByOutTradeNo" parameterType="VideoOrder">
update video_order set state=#{state},notify_time=#{notifyTime},openid=#{openId}
where
out_trade_no=#{outTradeNo} and state=0 and del=0
</update>
</mapper>

Step5:控制层

一切准备工作完成后重点要来了!

控制层先这么 稍后再更新

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
less复制代码/**
* 视频订单控制层
*/
@RestController
@RequestMapping("api/v1/pri/order")

public class VideoOrderController {

@Autowired
private VideoOrderService videoOrderService;

/**
* 下单接口
* @param videoId 视频id
* @param request 用户信息
* @return
* @throws Exception
*/
@GetMapping("saveOrder")
public void saveOrder(@RequestParam(value = "video_id",required = true) int videoId,
HttpServletRequest request,
HttpServletResponse response) throws Exception {

//记录用户下单ip
//如果使用reques去拿ip不严谨,容易出现拿不到的情况,会过滤一些http头信息

//获取ip 模拟一个假的ip
//String ip = IpUtils.getIpAddr(request);
String ip = "120.25.1.43";

//获取用户id 这里是我的项目里加了jwt登陆 你可以直接写一次参数传
Integer userId = (Integer)request.getAttribute("user_id");

VideoOrderDto videoOrderDto = new VideoOrderDto();
videoOrderDto.setVideoId(videoId);
videoOrderDto.setUserId(userId);
videoOrderDto.setIp(ip);

videoOrderService.saveVideoOrder(videoOrderDto);

return DtoUtil.returnSuccess("下单成功");
}

}

Step6:ServiceImpl实现

这里的操作是先保证下单成功,数据库能生成数据

注释的地方都是需要更新的

完成这一步可以先启动一下,调一下接口 看看数据能否添加成功

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
scss复制代码@Service
public class VideoOrderServiceImpl implements VideoOrderService {

@Autowired
private VideoOrderMapper videoOrderMapper;

@Autowired
private UserMapper userMapper;

@Autowired
private VideoMapper videoMapper;

@Autowired
private WeChatConfig weChatConfig;

/**
* 下单操作
* 未来版本 优惠卷功能、微信支付、风控用户检查、生成订单基础信息、生成支付信息
* @return
*/
@Override
@Transactional(propagation = Propagation.REQUIRED)//默认隔离级别
public String saveVideoOrder(VideoOrderDto videoOrderDto) throws Exception {

int videoId = videoOrderDto.getVideoId();
int userId = videoOrderDto.getUserId();
String ip = videoOrderDto.getIp();

//判断是否已经购买 订单状态码1
VideoOrder videoOrder = videoOrderMapper.findByUserIdAndVideoIdAndState(userId,videoId,1);

if(videoOrder!=null){
//已经支付过了,订单存在
return null;
}

//查询视频信息
Video video = videoMapper.findById(videoId);
//查询用户信息
User user = userMapper.findByUserId(userId);

//生成订单
//构造订单实体 根据用户购买哪个视频做处理
VideoOrder newvideoOrder = new VideoOrder();
newvideoOrder.setCreateTime(new Date());//订单创建时间
newvideoOrder.setOutTradeNo(CommonUtils.generateUUID());//唯一流水号
newvideoOrder.setTotalFee(video.getPrice());//价格

newvideoOrder.setState(0);//支付状态
newvideoOrder.setUserId(userId);//用户id
newvideoOrder.setVideoId(video.getId());//视频id
newvideoOrder.setHeadImg(user.getHeadImg());//微信头像
newvideoOrder.setNickName(user.getUsername());//微信昵称
newvideoOrder.setVideoImg(video.getCoverImg());//冗余字段
newvideoOrder.setVideoTitle(video.getTitle());//冗余字段

newvideoOrder.setDel(0);
newvideoOrder.setIp(ip);

//保存订单
int num = videoOrderMapper.saveVideoOrder(newvideoOrder);


//生成签名
String codeUrl = unifiedOrder(newvideoOrder);

//统一下单

//获取code_url

//生成二维码

return codeUrl;
}

}

Step6.1:签名开发

orderserviceimpl下再写一个统一下单方法 生成签名

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
csharp复制代码/**
* 统一下单
* @return
*/
private String unifiedOrder(VideoOrder videoOrder) throws Exception {

//生成签名
SortedMap<String,String> params = new TreeMap<>();
params.put("appid",weChatConfig.getAppId());//公众号AppId
params.put("mch_id",weChatConfig.getMchId());//商户ID
params.put("nonce_str", CommonUtils.generateUUID());
params.put("body",videoOrder.getVideoTitle());//商品描述
params.put("out_trade_no",videoOrder.getOutTradeNo());//订单流水号
params.put("total_fee",videoOrder.getTotalFee().toString());//商品金额
params.put("spbill_create_ip",videoOrder.getIp());//终端IP
params.put("notify_url",weChatConfig.getPayCallbackUrl());//通知地址
params.put("trade_type","NATIVE");//交易类型 扫码支付

//sign签名 调用工具类
String sign = WXPayUtil.createSign(params,weChatConfig.getKey());
params.put("sign",sign);

//生成签名后转map 进行校验 map>xml
String payXml = WXPayUtil.mapToXml(params);
System.out.println(payXml);
System.out.println(sign);

//统一下单
//发送post请求
String orderStr = HttpUtils.doPost(WeChatConfig.getUnifiedOrderUrl(),payXml,4000);
if(orderStr == null){
return null;
}
//接收返回结果 将微信返回的结果xml转map
Map<String,String> unifiedOrderMap = WXPayUtil.xmlToMap(orderStr);
if(unifiedOrderMap != null){
return unifiedOrderMap.get("code_url");

}
return null;
}

打断点调试一下

这里很重要,如果你签名生成的不对,下面是无法进行的
在这里插入图片描述

得到payXml值之后复制一下 去微信支付文档签名校验一下,如果能通过,那么恭喜你,重要的第一步完成了。链接在顶部!
在这里插入图片描述

Step6.2:发送请求

签名校验通过后给微信发送请求。这里都是时序图的第二步
在这里插入图片描述

orderStr就是微信返回给我们的信息,如果提示SUCCESS表示成功下单

Step6.3:拿取code_url

主要是说明一下,时序图第三步,微信生成订单后返回这样的值,里面包含code_url,就是二维码生成链接,我们需要这个值来生成二维码

Step7:更新控制层生成二维码

Step7.1:添加google二维码依赖

1
2
3
4
5
6
7
8
9
10
11
xml复制代码		<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>javase</artifactId>
<version>3.3.0</version>
</dependency>

<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>core</artifactId>
<version>2.0</version>
</dependency>

Step7.2:更新控制层

现在就明白service为什么使用String的返回了吧 就是需要拿到code_url这个值

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
ini复制代码@GetMapping("saveOrder")
public void saveOrder(@RequestParam(value = "video_id",required = true) int videoId,
HttpServletRequest request,
HttpServletResponse response) throws Exception {

//记录用户下单ip
//如果使用reques去拿ip不严谨,容易出现拿不到的情况,会过滤一些http头信息

//获取ip
//String ip = IpUtils.getIpAddr(request);
String ip = "120.25.1.43";

//获取用户id
Integer userId = (Integer)request.getAttribute("user_id");

VideoOrderDto videoOrderDto = new VideoOrderDto();
videoOrderDto.setVideoId(videoId);
videoOrderDto.setUserId(userId);
videoOrderDto.setIp(ip);

//统一下单拿支付交易链接codeUrl
String codeUrl = videoOrderService.saveVideoOrder(videoOrderDto);
if(codeUrl == null){
throw new NullPointerException();
}

try {
//生成二维码配置
Map<EncodeHintType,Object> hints = new HashMap<>();
//设置纠错等级
hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.L);
//设置编码类型
hints.put(EncodeHintType.CHARACTER_SET,"UTF-8");
//构造图片对象
BitMatrix bitMatrix = new MultiFormatWriter().encode(codeUrl, BarcodeFormat.QR_CODE,400,400,hints);
//输出流
OutputStream out = response.getOutputStream();

MatrixToImageWriter.writeToStream(bitMatrix,"png",out);
}catch (Exception e){
e.printStackTrace();
}


}

Step7.3:生成二维码

重新启动项目,使用postman测试,老版的不显示二维码,生成的是乱码,需要去浏览器访问,新版的可以显示二维码
在这里插入图片描述

出现下面的二维码你就可以打开手机扫码了!
在这里插入图片描述

之后的步骤大家都明白吧,就是用户和微信交互了,确认支付输密码之类的。直接到时序图的第八步

Step8:内网穿透接收消息

微信完成预支付信息后,给用户发消息的同时,还给我们后台发消息,告诉我们支付成功了,我们拿到这个信息后修改订单状态就完事了,但问题是我们是本地开发, 怎样接收发来的信息呢?

使用工具NatApp,顶部有链接,使用方法非常简单,使用免费隧道,但每次启动都是随机隧道,所以每次需要改配置文件
在这里插入图片描述

前面的域名: rrgdbr.natappfree.cc 就代理了本地 像我这样就能正常访问本地项目

rrgdbr.natappfree.cc/api/v1/pri/…

在这里插入图片描述

注意配置文件也要修改,可能有些懵,我这个值是在什么时候告诉微信支付系统的呢?就是在我们生成签名第一次给微信发统一下单微信那边就记录了

Step9:接收微信确认消息

路径一定要对啊,别你写回调地址和你控制层接收消息的路径不一致,不然怎么你也收不到消息,第一次我就脑瘫了,路径写错了,打断点试了半天也没进到控制层,浪费了好几毛钱。。。

下面的代码简单说几句,都有注释,流程就是收到请求后验证一下签名,有没有错误信息什么的,之后更新订单状态,完事再告诉微信,我这里OK了!就行了。如果不告诉微信它会一直给你发消息,直到你告诉他。

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
scss复制代码@Controller
@RequestMapping("api/v2/wechat")
public class WechatController {

@Autowired
private WeChatConfig weChatConfig;

@Autowired
private VideoOrderService videoOrderService;

/**
* 微信支付回调
*/
@RequestMapping ("/order/callback")
public void orderCallback(HttpServletResponse response, HttpServletRequest request) throws Exception{

//获取流信息
InputStream inputStream = request.getInputStream();

//转换 比inputStream更快 包装设计模式 性能更高
BufferedReader in = new BufferedReader(new InputStreamReader(inputStream,"UTF-8"));
//进行缓冲
StringBuffer sb = new StringBuffer();
String line;
while ((line = in.readLine())!=null){
sb.append(line);
}
in.close();
inputStream.close();
Map<String,String> callbackMap = WXPayUtil.xmlToMap(sb.toString());

//map转sortedMap
SortedMap<String,String> sortedMap = WXPayUtil.getSortedMap(callbackMap);

//判断签名是否正确
if(WXPayUtil.isCorrectSign(sortedMap,weChatConfig.getKey())){
System.out.println("OK");
//判断业务状态是否正确
if("SUCCESS".equals(sortedMap.get("result_code"))){

String outTradeNo = sortedMap.get("out_trade_no");

//使用队列方式提高性能
VideoOrder dbVideoOrder = videoOrderService.findByOutTradeNo(outTradeNo);

//更新订单状态
if(dbVideoOrder !=null && dbVideoOrder.getState()==0){//判断逻辑看业务场景
VideoOrder videoOrder = new VideoOrder();
videoOrder.setOpenId(sortedMap.get("openid"));
videoOrder.setOutTradeNo(outTradeNo);
videoOrder.setNotifyTime(new Date());
videoOrder.setState(1);
int rows = videoOrderService.updateVideoOrderByOutTradeNo(videoOrder);
System.out.println("受影响行数:"+rows);
//判断影响行数 row==1 或者row==0 响应微信成功或者失败
if(rows==1){//通知微信订单处理成功
response.setContentType("text/xml");
response.getWriter().println("success");
return;
}
}
}
}
//都处理失败
response.setContentType("text/xml");
response.getWriter().println("fail");
}
}

序言:整体微信支付就是这样,但还有细节的地方,验证订单、或者某笔交易出现问题都没有做。还有小伙伴会问。那我前台支付成功后前台是怎么给用户显示支付OK了,你这里方法也没告诉前台的啊。其实这个操作是前台来发请求的,循环的向后台发请求,查询用户这笔订单状态,变成1就不发请求了,然后给用户显示支付OK了。如果有小伙伴需要源码的可以私信,感谢!

WechatIMG244.jpeg
感觉不错的大佬点个赞呗~ 手敲截图演示不易

本文转载自: 掘金

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

Golang 开发之Cobra初探 一 背景 二 功能特性

发表于 2021-07-10

一 背景

在云原生如日中天的当下,相信很多人对Kubernetes/etcd等都有所听闻,当我们看其源码或对其进行二次开发的时候,可以发现其均使用了一个命令行程序库Cobra,其是一个用来编写命令行的神器,提供了一个脚手架,用于快速生成基于Cobra应用程序框架。

其作者是非常有名的spf13,相信大家对vim都有所了解,可以去使用vim的终极终端spf13-vim,可以一键配置,非常的方便,其还有作品viper是一个完整的配置解决方案,支持JSON/YAML/TOML/HCL/envFile等配置文件,还可以热加载,配置保存等,hugo也是其的作品。

我们可以利用Cobra快速的去开发出我们想要的命令行工具,非常的方便快捷。

二 功能特性

  • 简易的子命令行模式,如 app server, app fetch等等
  • 完全兼容posix命令行模式
  • 嵌套子命令subcommand
  • 支持全局,局部,串联flags
  • 使用Cobra很容易的生成应用程序和命令,使用cobra create appname和cobra add cmdname
  • 如果命令输入错误,将提供智能建议,如 app srver,将提示srver没有,是否是app server
  • 自动生成commands和flags的帮助信息
  • 自动生成详细的help信息,如app help
  • 自动识别-h,–help帮助flag
  • 自动生成应用程序在bash下命令自动完成功能
  • 自动生成应用程序的man手册
  • 命令行别名
  • 自定义help和usage信息
  • 可选的紧密集成的viper apps

三 使用Cobra

3.1 安装

Cobra安装非常简单,可以使用go get获取即可,安装完成后,打开GOPATH目录,bin目录下应该有已经编译好的cobra,当然也可以使用源码编译安装。

在使用cobra之前需要了解三个概念,其也是命令行的三部分内容,command、flag和args

  • 命令自身的一些基本信息,用command表示,具体对象是 cobra.Command
  • 命令的一些标致或者选项,用flag表示,具体对象是 flag.FlagSet
  • 最后的参数,用args表示,通常是[]string

对应如下例子:

1
shell复制代码go get -u test.com/a/b

这里get 就是commond(这里比较特殊), -u 就是flag, test.com/a/b 就是args

3.2 生成应用程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
shell复制代码$ /Users/xuel/workspace/goworkspace/bin/cobra init --pkg-name smartant-cli
Your Cobra application is ready at
/Users/xuel/workspace/goworkspace/src/github.com/kaliarch/smartant-cli
$ ls
LICENSE cmd go.mod go.sum main.go
$ tree
.
├── LICENSE
├── cmd
│   └── root.go
├── go.mod
├── go.sum
└── main.go

1 directory, 5 files

3.3 设计cls程序

在smartant-cli 目录下创建imp目录,器重编写utils.go文件,内入如下

1
2
3
4
5
6
7
go复制代码package utils

import "fmt"

func Show(name string, age int) {
fmt.Printf("name is %s, age is %d", name, age)
}
  • main.go
1
2
3
4
5
6
7
go复制代码package main

import "github.com/kaliarch/smartant-cli/cmd"

func main() {
cmd.Execute()
}

可以看出main函数执行cmd包,所以我们只需要在cmd包内调用utils包就能实现smartant-cli程序的需求。接着打开root.go文件查看:

  • root.go
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
go复制代码/*
Copyright © 2021 NAME HERE <EMAIL ADDRESS>

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cmd

import (
"fmt"
"github.com/spf13/cobra"
"os"

homedir "github.com/mitchellh/go-homedir"
"github.com/spf13/viper"
)

var cfgFile string

// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
Use: "smartant-cli",
Short: "SmartAnt linux agent cli",
Long: `
smartant-cli is a CLI for SmartAnt applications.
This application is a tool to migrations linux system.`,
// Uncomment the following line if your bare application
// has an action associated with it:
// Run: func(cmd *cobra.Command, args []string) { },
}

// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}

func init() {
cobra.OnInitialize(initConfig)

// Here you will define your flags and configuration settings.
// Cobra supports persistent flags, which, if defined here,
// will be global for your application.

rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.smartant-cli.yaml)")

// Cobra also supports local flags, which will only run
// when this action is called directly.
rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}

// initConfig reads in config file and ENV variables if set.
func initConfig() {
if cfgFile != "" {
// Use config file from the flag.
viper.SetConfigFile(cfgFile)
} else {
// Find home directory.
home, err := homedir.Dir()
if err != nil {
fmt.Println(err)
os.Exit(1)
}

// Search config in home directory with name ".smartant-cli" (without extension).
viper.AddConfigPath(home)
viper.SetConfigName(".smartant-cli")
}

viper.AutomaticEnv() // read in environment variables that match

// If a config file is found, read it in.
if err := viper.ReadInConfig(); err == nil {
fmt.Println("Using config file:", viper.ConfigFileUsed())
}
}

从源代码来看cmd包进行了一些初始化操作并提供了Execute接口。十分简单,其中viper是cobra集成的配置文件读取的库,这里不需要使用,我们可以注释掉(不注释可能生成的应用程序很大约10M,这里没哟用到最好是注释掉)。cobra的所有命令都是通过cobra.Command这个结构体实现的。为了实现smartant-cli功能,显然我们需要修改RootCmd。修改后的代码如下:

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
go复制代码/*
Copyright © 2021 NAME HERE <EMAIL ADDRESS>

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cmd

import (
"fmt"
"github.com/spf13/cobra"
//"github.com/spf13/viper"
"github.com/kaliarch/cobra-demo/utils"
"os"
)

var cfgFile string

//var name string
//var age int
var command string

// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
Use: "cobra-demo",
Short: "A brief description of your application",
Long: `A longer description that spans multiple lines and likely contains
examples and usage of using your application. For example:

Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.`,
// Uncomment the following line if your bare application
// has an action associated with it:
// Run: func(cmd *cobra.Command, args []string) { },
Run: func(cmd *cobra.Command, args []string) {
//if len(name) == 0 {
// cmd.Help()
// return
//}
//imp.Show(name, age)
if len(command) == 0 {
cmd.Help()
return
}
utils.Cmd(command)

},
}

// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(-1)
}
}

func init() {
//cobra.OnInitialize(initConfig)

// Here you will define your flags and configuration settings.
// Cobra supports persistent flags, which, if defined here,
// will be global for your application.

rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.smartant-agent.yaml)")

// Cobra also supports local flags, which will only run
// when this action is called directly.
//rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
//rootCmd.PersistentFlags().StringVarP(&name, "name", "n", "", "person name")
//rootCmd.PersistentFlags().IntVarP(&age, "age", "a", 0, "person age")
rootCmd.PersistentFlags().StringVarP(&command, "command", "o", "", "execute command context")

}

// initConfig reads in config file and ENV variables if set.
//func initConfig() {
// if cfgFile != "" {
// // Use config file from the flag.
// viper.SetConfigFile(cfgFile)
// } else {
// // Find home directory.
// home, err := homedir.Dir()
// if err != nil {
// fmt.Println(err)
// os.Exit(1)
// }
//
// // Search config in home directory with name ".cobra-demo" (without extension).
// viper.AddConfigPath(home)
// viper.SetConfigName(".cobra-demo")
// }
//
// viper.AutomaticEnv() // read in environment variables that match
//
// // If a config file is found, read it in.
// if err := viper.ReadInConfig(); err == nil {
// fmt.Println("Using config file:", viper.ConfigFileUsed())
// }
//}

3.4 执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
python复制代码# 编译
$ go build -o smartant-cli
$ ./smartant-cli

smartant-cli is a CLI for SmartAnt applications.
This application is a tool to migrations linux system.

Usage:
smartant-cli [flags]

Flags:
-a, --age int persons age
-h, --help help for smartant-cli
-n, --name string persons name
$ ./smartant-cli -a 11 -n "xuel"
name is xuel, age is 11%

四 实现带有子命令的clis

在执行cobra.exe init demo之后,继续使用cobra为demo添加子命令test:

4.1 生成sysinfo子命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
go复制代码$ /Users/xuel/workspace/goworkspace/bin/cobra add sysinfo
sysinfo created at /Users/xuel/workspace/goworkspace/src/github.com/kaliarch/smartant-cli
$ tree
.
├── LICENSE
├── cmd
│ ├── root.go
│ └── sysinfo.go
├── go.mod
├── go.sum
├── main.go
├── smartant-cli
└── utils
└── utils.go

4.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
vbnet复制代码$ go build -o smartant-cli 
$ ./smartant-cli

smartant-cli is a CLI for SmartAnt applications.
This application is a tool to migrations linux system.

Usage:
smartant-cli [flags]
smartant-cli [command]

Available Commands:
help Help about any command
sysinfo A brief description of your command

Flags:
-a, --age int persons age
-h, --help help for smartant-cli
-n, --name string persons name

Use "smartant-cli [command] --help" for more information about a command.
$ ./smartant-cli sysinfo -h
A longer description that spans multiple lines and likely contains examples
and usage of using your command. For example:

Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.

Usage:
smartant-cli sysinfo [flags]

Flags:
-h, --help help for sysinfo

4.3 编写子命令

  • sysinfo.go
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
go复制代码/*
Copyright © 2021 NAME HERE <EMAIL ADDRESS>

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cmd

import (
"fmt"
"github.com/kaliarch/smartant-cli/utils"

"github.com/spf13/cobra"
)

var (
host, pwd, username string
port int
command string
)

// sysinfoCmd represents the sysinfo command
var sysinfoCmd = &cobra.Command{
Use: "sysinfo",
Short: "check sys info message",
Long: `A longer description that spans multiple lines and likely contains examples
and usage of using your command. For example:

Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.`,
Run: func(cmd *cobra.Command, args []string) {
if len(host) == 0 || len(pwd) == 0 {
cmd.Help()
return
}
fmt.Println("sysinfo called")
utils.Sysinfo(host, pwd, username, port, command)
fmt.Println("sysinfo called commpled")
},
}

func init() {
rootCmd.AddCommand(sysinfoCmd)

// Here you will define your flags and configuration settings.

// Cobra supports Persistent Flags which will work for this command
// and all subcommands, e.g.:
// sysinfoCmd.PersistentFlags().String("foo", "", "A help for foo")

// Cobra supports local flags which will only run when this command
// is called directly, e.g.:
// sysinfoCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
sysinfoCmd.Flags().StringVarP(&host, "host", "i", "", "host ip addr")
sysinfoCmd.Flags().StringVarP(&username, "username", "u", "", "host username")
sysinfoCmd.Flags().StringVarP(&command, "command", "c", "", "command")
sysinfoCmd.Flags().StringVarP(&pwd, "pwd", "p", "", "host password")
sysinfoCmd.Flags().IntVarP(&port, "port", "P", 0, "host port")
}
  • utils.go
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
go复制代码package utils

import (
"bytes"
"fmt"
"golang.org/x/crypto/ssh"
"net"
"strings"
//"strconv"
"log"
)

// smartant-cli
func Show(name string, age int) {
fmt.Printf("name is %s, age is %d", name, age)
}

func sshConnect(user, pwd, host string, port int) (*ssh.Session, error) {
var (
auth []ssh.AuthMethod
addr string
clientConfig *ssh.ClientConfig
client *ssh.Client
session *ssh.Session
err error
)
// get auth method
auth = make([]ssh.AuthMethod, 0)
auth = append(auth, ssh.Password(pwd))

// host key callbk
hostKeyCallbk := func(host string, remote net.Addr, key ssh.PublicKey) error {
return nil
}
clientConfig = &ssh.ClientConfig{
User: user,
Auth: auth,
HostKeyCallback: hostKeyCallbk,
BannerCallback: nil,
//ClientVersion: "",
//HostKeyAlgorithms: nil,
//Timeout: 10000000,
}

// connet to ssh
addr = fmt.Sprintf("%s:%d", host, port)

if client, err = ssh.Dial("tcp", addr, clientConfig); err != nil {
return nil, err
}

// create session
if session, err = client.NewSession(); err != nil {
return nil, err
}
return session, nil
}

func Sysinfo(host, pwd, username string, port int, cmd string) {
var stdOut, stdErr bytes.Buffer
// 使用用户名,密码登陆
session, err := sshConnect(username, pwd, host, port)
if err != nil {
log.Fatal(err)
}
defer session.Close()

session.Stdout = &stdOut
session.Stderr = &stdErr

session.Run(cmd)
fmt.Println(strings.Replace(stdOut.String(), "\n", " ", -1))
}
  • 执行测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
shell复制代码$ ./smartant-cli sysinfo
A longer description that spans multiple lines and likely contains examples
and usage of using your command. For example:

Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.

Usage:
smartant-cli sysinfo [flags]

Flags:
-c, --command string command
-h, --help help for sysinfo
-i, --host string host ip addr
-P, --port int host port
-p, --pwd string host password
-u, --username string host username

$ ./smartant-cli sysinfo -i 121.3.10.55 -u root -P 22 -p xxxxxxx -c "cat /etc/hosts"
sysinfo called
::1 localhost localhost.localdomain localhost6 localhost6.localdomain6 127.0.0.1 localhost localhost.localdomain localhost4 localhost4.localdomain4 127.0.0.1 localhost localhost 127.0.0.1 hw-server hw-server
sysinfo called commpled

五 其他

Cobra非常强大,能够帮助我们快速创建命令行工具,但是如果直接仅仅写一个非常简单的命令行工具,flag选项非常少,golang built-in的flag库够了。当然使用看个人选择,Cobra更适合复杂的命令行工具。

参考链接

  • github.com/spf13/cobra
  • www.huweihuang.com/golang-note…
  • juejin.cn/post/692454…
  • o-my-chenjian.com/2017/09/20/…

本文转载自: 掘金

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

1…613614615…956

开发者博客

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