想買德系SUV的考慮一下是不是把錢留到2017年?

SUV陣營上汽大眾途觀L新款途觀的上市可以說是眾多合資SUV潛在買家“千呼萬喚始出來”的角色,繼全進口的tiguan公布售價之後,合資的國產途觀也緊隨着將在2017年年初上市。新平台,新外觀,跨越級別的車身尺寸是途觀L的最大武器,2791mm的軸距已然超越中型SUV的標杆車型漢蘭達,儘管是僅僅1mm的差距,但也足以看出途觀意圖跨級競爭的決心。

不知從什麼時候開始,“買車就買德系車”,成為了很多家庭和個人的購車第一主觀反應,也不得不承認德系車擁有着相當大的一波粉絲。

轎車陣營

一汽大眾CC

現款大眾CC也確實接近於全新換代的時間臨界點,雖然是每年都有小改動,但都是同一代車型,預計將在明年年中,據悉,一汽大眾CC的全新換代車型將在2017年上半年正式下線。

一汽大眾全新CC將基於MQB平台生產,從國外媒體所透露出來的渲染圖可見,溜背式的轎跑造型依然還是大眾CC的主體語言,全新一台CC設計方面借鑒了很多大眾sport Coupe GTE概念車的元素;根據之前已經得到的消息,新CC將在2017年實現國產。

寶馬1系三廂

三廂版本的國產寶馬1系可以說是寶馬品牌寄予厚望的一跨車型,該車亮相於今年十一月的廣州車展,預計將於2017年年初就正式上市。

1系三廂版本基於寶馬UKL前驅平台生產,儘管是前驅,但它依然是一款強調着運動感和操控性的緊湊型三廂轎車,至於未來價格和實際動態表現究竟如何,着實令人期待。

SUV陣營

上汽大眾途觀L

新款途觀的上市可以說是眾多合資SUV潛在買家“千呼萬喚始出來”的角色,繼全進口的tiguan公布售價之後,合資的國產途觀也緊隨着將在2017年年初上市。

新平台,新外觀,跨越級別的車身尺寸是途觀L的最大武器,2791mm的軸距已然超越中型SUV的標杆車型漢蘭達,儘管是僅僅1mm的差距,但也足以看出途觀意圖跨級競爭的決心。

上汽大眾Teramont

Teramont是上汽大眾進軍中大型SUV市場的強力車型,車身長度超過5米,定位比途觀還要高半個級別,儘管沒有正式發布,但不出意外的話,這款SUV將於明年上半年跟公眾見面。

至於價格,小編猜測這麼大的一台大眾,怎麼著也要四十多萬吧。

斯柯達科迪亞克

儘管斯柯達是一個捷克斯洛伐克的品牌,但畢竟是基於大眾的生產技術,所以把它歸類於德系車也不為過。

科迪亞克是斯柯達的第一台擁有七座的SUV,產品定位與途觀L一樣,生產平台也共享自大眾集團MQB橫置模塊箱平台,由於品牌定位原因,很多人猜測科迪亞克的定價應該會比較親民,承載了相當高關注度的科迪亞克,屆時上市價格希望不會令人失望才好。

本次介紹的車型僅是德系車的一部分在國內可以購買到合資的車型,價格在正式上市之後比全進口的德系車要便宜不少,相信在SUV極度火爆的中國市場,那幾款SUV的正式亮相才是廣大消費者更為關注的新聞,如果在今年尋覓不到心中所想,或許耐心等到明年,這些產品力進一步提升的新款車型上市之後,就可以“找到真愛”啦~本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※產品缺大量曝光嗎?你需要的是一流包裝設計!

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

※回頭車貨運收費標準

※推薦評價好的iphone維修中心

※超省錢租車方案

虎眼看中國中國SUV新標杆瑞虎7華南試駕會精彩大趴

0DVVT+CVT、1。5T+DCT三種動力組合。1。5T高性能渦輪增壓發動機搭配愛信6速手動變速箱,油耗低至百公里6。3L。瑞虎7后懸採用多連桿獨立懸架+副車架結構,令整車的操控樂趣與駕乘舒適性完美統一。科技配置及安全性方面,瑞虎7搭載了Cloudrive2。

2016年12月18日,奇瑞瑞虎7千人交車儀式暨試駕會在花園城市東莞隆重舉行。近三百輛嶄新的瑞虎7正式交付東莞車主。

12月18日對於奇瑞汽車來說,一個非常特別的日子,1999年12月18日奇瑞汽車第一台整車下線,時至今天—2016年12月18日,奇瑞公司在大家的陪伴下走過了整整19個年頭,以榮獲日內瓦國際車展設計大獎TX概念車為原型的瑞虎7,瑞虎7自9月20日上市以來,上市首月客戶訂單突破2萬台,受到廣大車友的一致追捧,其前瞻性動感造型,出色動力及精準操控,滿足多樣化駕控需求、科技智慧帶來便捷行車生活。瑞虎7的上市熱銷,標志著奇瑞汽車創下中國車企造車水平的又一新高度。

伴隨着動感的音樂,瑞虎7千人交車儀式正式開始,華南大區總經理翟小兵先生為今天活動致辭,一起見證瑞虎7千人交車活動的珍貴瞬間。

瑞虎7不僅是奇瑞歷史上最好的產品,更是中國品牌標杆產品之一。作為奇瑞進入戰略2.0后的首款SUV車型,瑞虎7是在“大奇瑞”國際化團隊協作下,融合捷豹路虎、觀致技術流程開發的專業SUV平台——奇瑞T1X平台開發的首款產品。以未來派前瞻美學的設計風格、自主領先,超越合資的整體性能,實現了品質與品位的完美統一。

在全廣東媒體和現場上百人的見證下,奇瑞汽車廠家領導將瑞虎7車鑰匙交給首批東莞瑞虎7車主,恭喜進300名瑞虎7車主“金”日提車。

瑞虎7上市熱銷,兼具顏值和實力 榮耀之作樹立中國SUV新標杆。

外形方面,轎跑風的“水流”車身設計、側身極速破浪式躍動腰線、超凡奪目的三叉戟式氙氣大燈配虎爪矩陣式LED尾燈,盡顯霸氣與精緻。

動力操控方面,瑞虎7搭載了1.5T+6MT、2.0DVVT+CVT、1.5T+DCT三種動力組合。1.5T高性能渦輪增壓發動機搭配愛信6速手動變速箱,油耗低至百公里6.3L。瑞虎7后懸採用多連桿獨立懸架+副車架結構,令整車的操控樂趣與駕乘舒適性完美統一。

科技配置及安全性方面,瑞虎7搭載了Cloudrive2.0智雲互聯行車系統,採用9英寸超大電容觸摸真彩高清屏,內置4G無線網卡,為全車用戶提供高速流暢的互聯體驗,打造全時在線生活。

瑞虎7採用6D縱梁框架和3R-Body的一體化籠式高強度車身結構,有效提高正側面撞擊的抗擊能力。全系標配ESp电子車身穩定系統、HHC上坡起步輔助系統、TpMS智能胎壓監測系統、前後8探頭雷達,高配車型還裝備了360°全景式影像系統、實時監測車身周邊障礙物,由內而外保護車內乘員的人身安全。

瑞虎7堪稱瑞虎家族“十年磨一劍”的蛻變之作,是一款具有划時代意義的精品SUV。市場對瑞虎7充滿了期待,瑞虎7上市熱銷也給出了完美的交代。瑞虎7在高速增長又競爭激烈的SUV市場中扮演了一名狠角色。已成為中國SUV市場新標杆,征服華南區SUV市場。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※回頭車貨運收費標準

※產品缺大量曝光嗎?你需要的是一流包裝設計!

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

※推薦評價好的iphone維修中心

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

經內蒙古自治區動物疫病預防控制中心確診

根據中國農業農村部官網今(30日)公告,經內蒙古自治區動物疫病預防控制中心確診,鄂爾多斯市一養殖戶從外省違規調入的一批仔豬發生非洲豬瘟疫情,該批仔豬共200頭,死亡92頭。

此外,12日、13日、28日,四川省發現3起、河南省發現1起非洲豬瘟疫情,都是從外省調運仔豬或生豬中排查出來。其中,河南省三門峽市案例運送的仔豬有364隻,發現時已有252隻死亡。

【其他文章推薦】

※新北市探針選用參考標準?

※如何知道自已的電腦cpu支不支持AVX指令集?

※如何正確使用飲水機?

※攻戰消費者第一視覺,包裝設計很重要!

※滑鼠墊適用各種文宣活動廣告曝光,專業客製服務

※封口機購物網-不怕你比價,就怕你買貴!

※高溫殺菌機最多可達多少溫度?

高雄區農改場推廣百香果 促進南部檳榔園轉作

摘錄自2020年3月31日中央社報導

高屏地區的百香果目前已上市,高雄區農業改良場表示,高屏地區的百香果產期正好與中部的百香果產期互補,三年前開始輔導高屏農民種植百香果,已促進不少檳榔園轉作。

高雄區農業改良場長戴順發表示,春季早收百香果是高屏地區新興產業,農改場三年前開始輔導轄區農友種植,因巧妙運用高屏地區冬季溫暖少雨的特色氣候,成功克服了雨季果實好發的疫病。

戴順發說,農改場也輔導農民透過網室栽培有效阻絕東方果實蠅,搭配友善資材的使用,大幅降低化學農藥用量,甚至已有農民開始投入生產有機百香果,春季早收百香果生產模式為9月至10月定植,經燈照促進開花,於隔年2月至6月採收,生產過程完全避開7月至9月颱風天候風險,讓所生產百香果品質佳且產量更穩定。

【其他文章推薦】

※無塵擦拭布各大品牌廠商販售比價網!

※如何正確使用飲水機?

※如何知道自已的電腦cpu支不支持AVX指令集?

※掌握產品行銷策略,帶你認識商品包裝設計基本要素

※精密CNC 自動車床設備介紹

※空壓機這裡買最划算!

接到通報的台中市文化資產處長李智富

接到通報的台中市文化資產處長李智富、文化部文化資產局古蹟聚落組長張祐創等官員與文史團體趕到現場,與工頭協商時卻聽到3樓頂有機具拆除聲音,質疑是「偷偷拆除」,雙方爆發言語衝突。台中市警局三分局擔心施工掉落物傷人,也擔心衝突愈演愈烈,派員把所有工人帶到一樓,禁止施工。

文化部緊急介入下,立刻將天外天指定為暫訂古蹟,派員於中午從台北搭乘高鐵把公文送達現場,公文中強調,毀損者可處6月至5年有期徒刑,得併科50萬元至2000萬元罰金,立委黃國書也居間協調,先暫緩拆除,工頭同意暫時罷手。此一「暫訂古蹟」期限為半年,得延長半年。

【其他文章推薦】

※客製專屬滑鼠墊、可愛造型L夾、L型資料夾、透明證件套、手提袋,專業印刷設計廠商!  

※示波器鮮為人知的使用技巧?

※掌握產品行銷策略,帶你認識商品包裝設計基本要素

※買不起高檔茶葉,精緻包裝茶葉罐,也能撐場面!

※貨梯使用安全與保養

※高效率洗滌塔活性碳設備有哪些?

※專業模具開發-五金製品代工製作廠

秋行軍蟲為害再擴大!台東首見小米受災

摘錄自2020年4月1日聯合報報導

「秋行軍蟲災害緊急應變小組」表示,台東縣政府及農委會台東區農業改良場首度通報轄內小米受害,國內秋行軍蟲再度新增寄主範圍。

農委會公告9種小米秋行軍蟲緊急防治藥劑,以供強制施藥防治。農委會為掌握全國栽植小米受害現況,動員小米主要產區的農改場及地方政府擴大田區巡查,目前僅台東縣南迴公路一帶主要產區零星發生,受害田區將進行施藥及加強防治宣導,屏東縣境內小米則無發現受害情形。

1日,台東農改場會同台東縣政府共同巡查轄內自太麻里鄉至金峰鄉等小米田區,植株生長期介於苗期至採收期,均發現有秋行軍蟲零星危害。農委會今已公告賜諾特、蘇力菌等9種緊急防治藥劑,並積極輔導種植農戶加強巡田及防治,發現後落實通報。

【其他文章推薦】

※貨梯使用安全與保養

※如何知道自已的電腦cpu支不支持AVX指令集?

※臭氧機推薦

※高效率洗滌塔活性碳設備有哪些?

※無塵擦拭布各大品牌廠商販售比價網!

※掌握產品行銷策略,帶你認識商品包裝設計基本要素

天外天劇場驚傳突拆爆對峙!暫定古蹟公文高鐵急送搶救

摘錄自2020年4月2日自由時報報導

天外天劇場啟用於1936年,是台中市第一座電影院與劇場,今天(2日)上午7時許,竟有包商、工人10多人以及大型怪手等機具進駐,工頭並宣稱,如果不動工,會立刻損失20萬元,一定要進行拆除作業。

接到通報的台中市文化資產處長李智富、文化部文化資產局古蹟聚落組長張祐創等官員與文史團體趕到現場,與工頭協商時卻聽到3樓頂有機具拆除聲音,質疑是「偷偷拆除」,雙方爆發言語衝突。台中市警局三分局擔心施工掉落物傷人,也擔心衝突愈演愈烈,派員把所有工人帶到一樓,禁止施工。

文化部緊急介入下,立刻將天外天指定為暫訂古蹟,派員於中午從台北搭乘高鐵把公文送達現場,公文中強調,毀損者可處6月至5年有期徒刑,得併科50萬元至2000萬元罰金,立委黃國書也居間協調,先暫緩拆除,工頭同意暫時罷手。此一「暫訂古蹟」期限為半年,得延長半年。

【其他文章推薦】

※如何知道自已的電腦cpu支不支持AVX指令集?

※無塵擦拭布各大品牌廠商販售比價網!

※臭氧機推薦

※貨梯使用安全與保養

※高效率洗滌塔活性碳設備有哪些?

※掌握產品行銷策略,帶你認識商品包裝設計基本要素

面試官:說說Redis的Hash底層 我:……(來自閱文的面試題)

redis源碼分析系列文章

[Redis源碼系列]在Liunx安裝和常見API 

為什麼要從Redis源碼分析 

String底層實現——動態字符串SDS 

Redis的雙向鏈表一文全知道

前言

hello,各位小可愛們,又見面了。今天這篇文章來自去年面試閱文的面試題,結果被虐了。這一part不說了,下次專門開一篇,寫下我面試被虐的名場面,尷尬的不行,全程尬聊。哈哈哈哈,話不多說,開始把。

 

今天要寫Redis的Hash類型,如果有對Redis不熟悉,或者對其他數據類型感興趣的,可以移步上面的系列文章。(最上面的最上面最上面,重要的事情說三遍)

在Redis中Hash類型的應用非常廣泛,其中key到value的映射就通過字典結構來維護的。記筆記,此處要考。

​

API使用

API的使用比較簡單,所以以下就粗略的寫了。

插入數據hset

使用hset命令往myhash中插入兩個key,value的鍵值對,分別是(name,zhangsan)和(age,20),返回值當前的myhash的長度。

獲取數據hget

使用hget命令獲取myhash中key為name的value值。

獲取所有數據hgetall

使用hgetall命令獲取myhash中所有的key和value值。

獲取所有key

使用hkeys命令獲取myhash中所有的key值。

獲取長度

使用hlen命令獲取myhash的長度。

獲取所有value

使用hvals命令獲取myhash中所有的value值。

具體邏輯圖

hash的底層主要是採用字典dict的結構,整體呈現層層封裝。

首先dict有四個部分組成,分別是dictType(類型,不咋重要),dictht(核心),rehashidx(漸進式hash的標誌),iterators(迭代器),這裏面最重要的就是dictht和rehashidx。

接下來是dictht,其有兩個數組構成,一個是真正的數據存儲位置,還有一個用於hash過程,包括的變量分別是真正的數據table和一些常見變量。

最後數據節點,和上篇說的雙向鏈表一樣,每個節點都有next指針,方便指向下一個節點,這樣目的是為了解決hash碰撞。具體的可以看下圖。

這邊看不懂沒關係,後面會針對每個模塊詳細說明。(千萬不要看到這裏就跳過啦)

​

雙向鏈表的定義

字典結構體dict

我們先看字典結構體dict,其包括四個部分,重點是dictht[2](真正的數據)和rehashidx(漸進式hash的標誌)。具體圖如下。

具體代碼如下:

//字典結構體 
 typedef struct dict {
    dictType *type;//類型,包括一些自定義函數,這些函數使得key和value能夠存儲 
    void *privdata;//私有數據 
    dictht ht[2];//兩張hash表 
    long rehashidx; //漸進式hash標記,如果為-1,說明沒在進行hash
    unsigned long iterators; //正在迭代的迭代器數量
} dict;

 

數組結構體dictht

dictht主要包括四個部分,1是真正的數據dictEntry類型的數組,裏面存放的是數據節點;2是數組長度size;3是進行hash運算的參數sizemask,這個不咋重要,只要記住等於size-1;4是數據節點數量used,當前有多少個數據節點。

具體代碼如下:

//hash結構體 
typedef struct dictht {
    dictEntry **table;//真正數據的數組 
    unsigned long size;//數組的大小 
    unsigned long sizemask;//用戶將hash映射到table的位置索引,他的值總是等於size-1 
    unsigned long used;//已用節點數量 
} dictht;

 

數據節點dictEntry

dictEntry為真正的數據節點,包括key,value和next節點。

//每個節點的結構體  
typedef struct dictEntry {
    void *key; //key
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;//value
    struct dictEntry *next; //下一個數據節點的地址
} dictEntry;

 

 

 

擴容過程和漸進式Hash圖解

我們先來第一個部分,dictht[2]為什麼會要2個數組存放,真正的數據只要一個數組就夠了?

這其實和Java的HashMap相似,都是數據加鏈表的結構,隨着數據量的增加,hash碰撞發生的就越頻繁,每個數組後面的鏈表就越長,整個鏈表顯得非常累贅。如果業務需要大量查詢操作,因為是鏈表,只能從頭部開始查詢,等一個數組的鏈表全部查詢完才能開始下一個數組,這樣查詢時間將無線拉長。

這無疑是要進行擴容,所以第一個數組存放真正的數據,第二個數組用於擴容用。第一個數組中的節點經過hash運算映射到第二個數組上,然後依次進行。那麼過程中還能對外提供服務嗎?答案是可以的,因為他可以隨時停止,這就到了下一個變量rehashidx。(一點都不生硬的轉場,哈哈哈)

rehashidx其實是一個標誌量,如果為-1說明當前沒有擴容,如果不為-1則表示當前擴容到哪個下標位置,方便下次進行從該下標位置繼續擴容。

這樣說是不是太抽象了,還是一臉懵逼,貼心的送上擴容過程全解,一定要點贊評論多誇誇我哦。(越來越不要臉了。。。)

步驟1

首先是未擴容前,rehashidx為-1,表示未擴容,第一個數組的dictEntry長度為4,一共有5個節點,所以used為5。

步驟2

當發生擴容了,rahashidx為第一個數組的第一個下標位置,即0。擴容之後的大小為大於used*2的2的n次方的最小值,即能包含這些節點*2的2的倍數的最小值。因為當前為5個數據節點,所以used*2=10,擴容后的數組大小為大於10的2的次方的最小值,為16。從第一個數組0下標位置開始,查找第一個元素,找到key為name,value為張三的節點,將其hash過,找到在第二個數組的下標為1的位置,將節點移過去,其實是指針的移動。這邊就簡單說了。

 

步驟3

key為name,value為張三的節點移動結束后,繼續移動第一個數組dictht[0]的下標為0的後續節點,移動步驟和上面相同。

步驟4

繼續移動第一個數組dictht[0]的下標為0的後續節點都移動完了,開始移動下標為1的節點,發現其沒有數據,所以移動下標為2的節點,同時修改rehashidx為2,移動步驟和上面相同。

整個過程的重點在於rehashidx,其為第一個數組正在移動的下標位置,如果當前內存不夠,或者操作系統繁忙,擴容的過程可以隨時停止。

停止之後如果對該對象進行操作,那是什麼樣子的呢?

  • 如果是新增,則直接新增后第二個數組,因為如果新增到第一個數組,以後還是要移過來,沒必要浪費時間
  • 如果是刪除,更新,查詢,則先查找第一個數組,如果沒找到,則再查詢第二個數組。

字典的實現(源碼分析)

創建並初始化字典

首先分配內存,接着調用初始化方法_dictInit,主要是賦值操作,重點看下rehashidx賦值為-1(這驗證了剛才的圖解,-1表示未進行hash擴容),最後返回是否創建成功。

/* 創建並初始化字典 */
dict *dictCreate(dictType *type,
        void *privDataPtr)
{
    dict *d = zmalloc(sizeof(*d));
    _dictInit(d,type,privDataPtr);
    return d;
}

/* Initialize the hash table */
int _dictInit(dict *d, dictType *type,
        void *privDataPtr)
{
    _dictReset(&d->ht[0]);
    _dictReset(&d->ht[1]);
    d->type = type;
    d->privdata = privDataPtr;
    d->rehashidx = -1;//賦值為-1,表示未進行hash
    d->iterators = 0;
    return DICT_OK;
}

 

擴容

dict裏面有一個靜態方法_dictExpandIfNeed,判斷是否需要擴容。

首先判斷通過dictIsRehashing方法,判斷是否處於hash狀態,其調用的是宏常量#define dictIsRehashing(d) ((d)->rehashidx != -1),即判斷rehashidx是否為-1,如果為-1,即不處於hash狀態,if條件為false,可以進行擴容,如果不為-1,即處於hash狀態,if條件為true,不可以進行擴容,直接返回常量DICT_OK。

接着判斷第一個數組的size是否為0,如果為0,則擴容為默認大小4,如果不為0,則執行下面的代碼。

再接着判斷是否需要擴容,if中有三個條件,具體的分析如下。

最後就是調用dictExpand擴容方法了,參數為數據節點的雙倍大小ht[0].used*2。此處驗證了上面擴容過程的數組大小16。

擴容方法比較簡單點,獲取擴容后的大小,將第二個設置新的大小。

這樣講感覺有點空,看下流程圖。

擴容流程圖

具體代碼:

static int _dictExpandIfNeeded(dict *d)
{
    //判斷是否處於擴容狀態中,通過調用宏常量#define dictIsRehashing(d) ((d)->rehashidx != -1)
    //來判斷是否可以擴容
    if (dictIsRehashing(d)) return DICT_OK;

    //判斷第一個數組size是否為0,如果為0,則調用擴容方法,大小為宏常量
    //#define DICT_HT_INITIAL_SIZE     4
    if (d->ht[0].size == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE);

    //下面先列出if條件中所使用到的參數 
    // static int dict_can_resize = 1;數值為1表示可以擴容
    //static unsigned int dict_force_resize_ratio = 5;
    //我們來分析if條件,如果第一個數組的所有節點數量大於等於第一個數組的大小(表示節點數據已經有些多)
    //並且可用擴容(數值為1)或者所有節點數量除以數組大小大於5
    //這個條件表示擴容那個的條件,第一個就是節點必要大於等於數組長度,
    //第二點就再可以擴容和數據太多,超過5兩个中選其一
    if (d->ht[0].used >= d->ht[0].size &&
        (dict_can_resize ||
         d->ht[0].used/d->ht[0].size > dict_force_resize_ratio))
    {
        //調用擴容方法
        return dictExpand(d, d->ht[0].used*2);
    }
    return DICT_OK;
}

int dictExpand(dict *d, unsigned long size)
{
    dictht n;
    //獲取擴容后真正的大小,找到比size大的最小值,且是2的倍數
    unsigned long realsize = _dictNextPower(size);

    //一些判斷條件
    if (dictIsRehashing(d) || d->ht[0].used > size)
        return DICT_ERR;

    if (realsize == d->ht[0].size) return DICT_ERR;

    n.size = realsize;
    n.sizemask = realsize-1;
    n.table = zcalloc(realsize*sizeof(dictEntry*));
    n.used = 0;

    //第一個hash為null,說明在初始化 
    if (d->ht[0].table == NULL) {
        d->ht[0] = n;
        return DICT_OK;
    }
    //正在hash,給第二個hash的長度設置新的, 
    d->ht[1] = n;
    d->rehashidx = 0;//設置當前正在hash 
    return DICT_OK;
}

/* 找到比size大的最小值,且是2的倍數 */
static unsigned long _dictNextPower(unsigned long size)
{
    unsigned long i = DICT_HT_INITIAL_SIZE;

    if (size >= LONG_MAX) return LONG_MAX;
    while(1) {
        if (i >= size)
            return i;
        i *= 2;
    }
}

 

 

漸進式hash

漸進式hash過程已經通過上面圖解說明,以下主要看下代碼是如何實現的,以及過程是不是對的。

擴容之後就是執行dictRehash方法,參數包括待移動的哈希表d和步驟数字n。

首先判斷標誌量rehashidx是否等於-1,如果等於-1,則表示hash完成,如果不等於-1,則執行下面的代碼。

接着進行循環,遍歷第一個數組上的每個下標,每次移動下標位置,都需要更新rehashidx值,每次加1。

再接着進行第二個循環,遍歷下標的鏈表每個節點,完成數據的遷移,主要是指針的移動和一些參數的修改。

最後,返回int數值,如果為0表示整個數據全部hash完成,如果返回1則表示部分hash結束,並沒有全部完成,下次可以通過rehashidx值繼續hash。

具體代碼如下:

//重新hash這個哈希表
 // Redis的哈希表結構公有兩個table數組,t0和t1,平常只使用一個t0,當需要重hash時則重hash到另一個table數組中
 //參數列表
 // 1. d: 待移動的哈希表,結構中存有目前已經重hash到哪個桶了
  //  2. n: N步進行rehash 
// 返回值 返回0說明整個表都重hash完成了,返回1代表未完成
int dictRehash(dict *d, int n) {
    int empty_visits = n*10; 
    //如果當前rehashidx=-1,則返回0,表示hash完成 
    if (!dictIsRehashing(d)) return 0;
    //分n步,而且ht[0]還有沒有移動的節點 
    while(n-- && d->ht[0].used != 0) {
        dictEntry *de, *nextde;
        assert(d->ht[0].size > (unsigned long)d->rehashidx);
        //第一個循環用來更新 rehashidx 的值,因為有些桶為空,所以 rehashidx並非每次都比原來前進一個位置,而是有可能前進幾個位置,但最多不超過 10。
        //將rehashidx移動到ht[0]有節點的下標,也就是table[d->rehashidx]非空
        while(d->ht[0].table[d->rehashidx] == NULL) {
            d->rehashidx++;
            if (--empty_visits == 0) return 1;
        }
        de = d->ht[0].table[d->rehashidx];  //第二個循環用來將ht[0]表中每次找到的非空桶中的鏈表(或者就是單個節點)拷貝到ht[1]中

        /* 利用循環講數據節點移過去 */
        while(de) {
            unsigned int h;

            nextde = de->next;
            /* Get the index in the new hash table */
            h = dictHashKey(d, de->key) & d->ht[1].sizemask;
            de->next = d->ht[1].table[h];
            d->ht[1].table[h] = de;
            d->ht[0].used--;
            d->ht[1].used++;
            de = nextde;
        }
        d->ht[0].table[d->rehashidx] = NULL;
        d->rehashidx++;
    }

    if (d->ht[0].used == 0) {
        zfree(d->ht[0].table);
        d->ht[0] = d->ht[1];
        _dictReset(&d->ht[1]);
        d->rehashidx = -1;
        return 0;
    }

    return 1;
}

總結

該篇主要講了Redis的Hash數據類型的底層實現字典結構Dict,先從Hash的一些API使用,引出字典結構Dict,剖析了其三個主要組成部分,字典結構體Dict,數組結構體Dictht,數據節點結構體DictEntry,進而通過多幅過程圖解釋了擴容過程和rehash過程,最後結合源碼對字典進行描述,如創建過程,擴容過程,漸進式hash過程,中間穿插流程圖講解。

如果覺得寫得還行,麻煩給個贊,您的認可才是我寫作的動力!

如果覺得有說的不對的地方,歡迎評論指出。

好了,拜拜咯。

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

【其他文章推薦】

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

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

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

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

※產品缺大量曝光嗎?你需要的是一流包裝設計!

從linux源碼看socket(tcp)的timeout

從linux源碼看socket(tcp)的timeout

前言

網絡編程中超時時間是一個重要但又容易被忽略的問題,對其的設置需要仔細斟酌。在經歷了數次物理機宕機之後,筆者詳細的考察了在網絡編程(tcp)中的各種超時設置,於是就有了本篇博文。本文大部分討論的是socket設置為block的情況,即setNonblock(false),僅在最後提及了nonblock socket(本文基於linux 2.6.32-431內核)。

connectTimeout

在討論connectTimeout之前,讓我們先看下java和C語言對於socket connect調用的函數簽名:

java:
 // 函數調用中攜帶有超時時間
 public void connect(SocketAddress endpoint, int timeout) ;
C語言:
 // 函數調用中並不攜帶超時時間
 int connect(int sockfd, const struct sockaddr * sockaddr, socklen_t socklent) 	 

操作系統提供的connect系統調用並沒有提供timeout的參數設置而java卻有,我們先考察一下原生系統調用的超時策略。

connect系統調用

我們觀察一下此系統調用的kernel源碼,調用棧如下所示:

connect[用戶態]
	|->SYSCALL_DEFINE3(connect)[內核態]
			|->sock->ops->connect

由於我們考察的是tcp的connect,其socket的內部結構如下圖所示:

最終調用的是tcp_connect,代碼如下所示:

int tcp_connect(struct sock *sk) {
	......
	// 發送SYN
	err = tcp_transmit_skb(sk, buff, 1, sk->sk_allocation);
	...
	/* Timer for repeating the SYN until an answer. */
	// 由於是剛建立連接,所以其rto是TCP_TIMEOUT_INIT
	inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS,
				inet_csk(sk)->icsk_rto, TCP_RTO_MAX);
	return 0;	
}

又上面代碼可知,在tcp_connect設置了重傳定時器之後return回了tcp_v4_connect再return到inet_stream_connect。我們繼續考察:

int inet_stream_connect(struct socket *sock, struct sockaddr *uaddr,
			int addr_len, int flags)
{
	......
	// tcp_v4_connect=>tcp_connect
	err = sk->sk_prot->connect(sk, uaddr, addr_len);
	// 這邊用的是sk->sk_sndtimeo
	timeo = sock_sndtimeo(sk, flags & O_NONBLOCK);
	......
	inet_wait_for_connect(sk, timeo));
	......
out:
	release_sock(sk);
	return err;

sock_error:
	err = sock_error(sk) ? : -ECONNABORTED;
	sock->state = SS_UNCONNECTED;
	if (sk->sk_prot->disconnect(sk, flags))
		sock->state = SS_DISCONNECTING;
	goto out
}

由上面代碼可見,可以採用設置SO_SNDTIMEO來控制connect系統調用的超時,如下所示:

setsockopt(sockfd, SOL_SOCKET, SO_SNDTIMEO, &timeout, len);

不設置SO_SNDTIMEO

如果不設置SO_SNDTIMEO,那麼會由tcp重傳定時器在重傳超過設置的時候后超時,如下圖所示:

這個syn重傳的次數由:

cat /proc/sys/net/ipv4/tcp_syn_retries 筆者機器上是5 

來決定。那麼我們就來看一下這個重傳到底是多長時間:

tcp_connect中:
		// 設置的初始超時時間為icsk_rto=TCP_TIMEOUT_INIT為1s
		inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS,
				inet_csk(sk)->icsk_rto, TCP_RTO_MAX);

其重傳定時器的回掉函數為tcp_retransmit_timer:

void tcp_retransmit_timer(struct sock *sk)
{
	......
	// 檢測是否超時
	if (tcp_write_timeout(sk))
		goto out;
	......
	// icsk_rto = icsk_rto * 2,由於syn階段,所以isck_rto不會由於網絡傳輸而改變
	// 重傳的時候會以1,2,4,8指數遞增
	icsk->icsk_rto = min(icsk->icsk_rto << 1, TCP_RTO_MAX);
	// 重設timer
	inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS, icsk->icsk_rto, TCP_RTO_MAX);
out:;		
}

而計算tcp_write_timeout的邏輯則是在這篇blog中已經詳細描述過,

https://my.oschina.net/alchemystar/blog/1936433

只不過在connect時刻,重傳的計算以TCP_TIMEOUT_INIT為單位進行計算。而ESTABLISHED(read/write)時刻,重傳以TCP_RTO_MIN進行計算。那麼根據這段重傳邏輯,我們就可以計算出不同tcp_syn_retries最終表現的超時時間。如下圖所示:

那麼整理下錶格,對於系統調用,connect的超時時間為:

tcp_syn_retries timeout
1 min(so_sndtimeo,3s)
2 min(so_sndtimeo,7s)
3 min(so_sndtimeo,15s)
4 min(so_sndtimeo,31s)
5 min(so_sndtimeo,63s)

上述超時時間和筆者的實測一致。

kernel代碼版本細微變化

值得注意的是,linux本身官方發布的2.6.32源碼對於tcp_syn_retries2的解釋和RFC並不一致(至少筆者閱讀的代碼如此,這個細微的變化困擾了筆者好久,筆者下載了和機器對應的內核版本后才發現代碼改了)。而redhat發布的2.6.32-431已經修復了這個問題(不清楚具體哪個小版本修改的),並將初始RTO設置為1s(官方2.6.32為3s)。這也是,不同內核小版本上的實驗會有不同的connect timeout表現的原因(有的抓包到的重傳SYN時間間隔為3,6,12……)。以下為代碼對比:

========================>linux 內核版本2.6.32-431<========================
#define TCP_TIMEOUT_INIT ((unsigned)(1*HZ))	/* RFC2988bis initial RTO value	*/

static inline bool retransmits_timed_out(struct sock *sk,
					 unsigned int boundary,
					 unsigned int timeout,
					 bool syn_set)
{
	......
	unsigned int rto_base = syn_set ? TCP_TIMEOUT_INIT : TCP_RTO_MIN;
	......
	timeout = ((2 << boundary) - 1) * rto_base;
	......

}
========================>linux 內核版本2.6.32.63<========================
#define TCP_TIMEOUT_INIT ((unsigned)(3*HZ))	/* RFC 1122 initial RTO value	*/

static inline bool retransmits_timed_out(struct sock *sk,
					 unsigned int boundary
{
	......
	timeout = ((2 << boundary) - 1) * TCP_RTO_MIN;
	......
}

另外,tcp_syn_retries重傳次數可以在單個socket中通過setsockopt設置。

JAVA connect API

現在我們考察下java的connect api,其connect最終調用下面的代碼:

Java_java_net_PlainSocketImpl_socketConnect(...){

    if (timeout <= 0) {
    	 ......
        connect_rv = NET_Connect(fd, (struct sockaddr *)&him, len);
    	 .....
    }else{
    	 // 如果timeout > 0 ,則設置為nonblock模式
        SET_NONBLOCKING(fd);
        /* no need to use NET_Connect as non-blocking */
        connect_rv = connect(fd, (struct sockaddr *)&him, len);
        /*
         * 這邊用系統調用select來模擬阻塞調用超時
         */
        while (1) {
            ......
            struct timeval t;
            t.tv_sec = timeout / 1000;
            t.tv_usec = (timeout % 1000) * 1000;
            connect_rv = NET_Select(fd+1, 0, &wr, &ex, &t);
            ......
        }
        ......
        // 重新設置為阻塞模式
        SET_BLOCKING(fd);
        ......
    }
}

其和connect系統調用的不同點是,在timeout為0的時候,走默認的系統調用不設置超時時間的邏輯。在timeout>0時,將socket設置為非阻塞,然後用select系統調用去模擬超時,而沒有走linux本身的超時邏輯,如下圖所示:

由於沒有java並沒有設置so_sndtimeo的選項,所以在timeout為0的時候,直接就通過重傳次數來控制超時時間。而在調用connect時設置了timeout(不為0)的時候,超時時間如下錶格所示:

tcp_syn_retries timeout
1 min(timeout,3s)
2 min(timeout,7s)
3 min(timeout,15s)
4 min(timeout,31s)
5 min(timeout,63s)

socketTimeout

write系統調用的超時時間

socket的write系統調用最後調用的是tcp_sendmsg,源碼如下所示:

int tcp_sendmsg(struct kiocb *iocb, struct socket *sock, struct msghdr *msg,
		size_t size){
	......
	timeo = sock_sndtimeo(sk, flags & MSG_DONTWAIT);
	......
	while (--iovlen >= 0) {
		......
		// 此種情況是buffer不夠了
		if (copy <= 0) {
	new_segment:
		  ......
		  if (!sk_stream_memory_free(sk))
			  goto wait_for_sndbuf;

		  skb = sk_stream_alloc_skb(sk, select_size(sk),sk->sk_allocation);
		  if (!skb)
			  goto wait_for_memory;
		}
		......
	}
	......
	// 這邊等待write buffer有空間
wait_for_sndbuf:
		set_bit(SOCK_NOSPACE, &sk->sk_socket->flags);
wait_for_memory:
		if (copied)
			tcp_push(sk, flags & ~MSG_MORE, mss_now, TCP_NAGLE_PUSH);
			// 這邊等待timeo長的時間
		if ((err = sk_stream_wait_memory(sk, &timeo)) != 0)
			goto do_error;
		......
out:
	// 如果拷貝了數據,則返回
	if (copied)
		tcp_push(sk, flags, mss_now, tp->nonagle);
	TCP_CHECK_TIMER(sk);
	release_sock(sk);
	return copied;		
out_err:
	// error的處理
	err = sk_stream_error(sk, flags, err);
	TCP_CHECK_TIMER(sk);
	release_sock(sk);
	return err;		
}

從上面的內核代碼看出,如果socket的write buffer依舊有空間的時候,會立馬返回,並不會有timeout。但是write buffer不夠的時候,會等待SO_SNDTIMEO的時間(nonblock時候為0)。但是如果SO_SNDTIMEO沒有設置的時候,默認初始化為MAX_SCHEDULE_TIMEOUT,可以認為其超時時間為無限。那麼其超時時間會有另一個條件來決定,我們看下sk_stream_wait_memory的源碼:

int sk_stream_wait_memory(struct sock *sk, long *timeo_p){
		// 等待socket shutdown或者socket出現err
		sk_wait_event(sk, &current_timeo, sk->sk_err ||
						  (sk->sk_shutdown & SEND_SHUTDOWN) ||
						  (sk_stream_memory_free(sk) &&
						  !vm_wait));
}						 

在write等待的時候,如果出現socket被shutdown或者socket出現錯誤的時候,則會跳出wait進而返回錯誤。在不考慮對端shutdown的情況下,出現sk_err的時間其實就是其write的timeout時間,那麼我們看下什麼時候出現sk->sk_err。

SO_SNDTIMEO不設置,write buffer滿之後ack一直不返回的情況(例如,物理機宕機)

物理機宕機后,tcp發送msg的時候,ack不會返回,則會在重傳定時器tcp_retransmit_timer到期后timeout,其重傳到期時間通過tcp_retries2以及TCP_RTO_MIN計算出來。其源碼可見筆者的blog:

https://my.oschina.net/alchemystar/blog/1936433

tcp_retries2的設置位置為:

cat /proc/sys/net/ipv4/tcp_retries2 筆者機器上是5,默認是15

SO_SNDTIMEO不設置,write buffer滿之後對端不消費,導致buffer一直滿的情況

和上面ack超時有些許不一樣的是,一個邏輯是用TCP_RTO_MIN通過tcp_retries2計算出來的時間。另一個是真的通過重傳超過tcp_retries2次數來time_out,兩者的區別和rto的動態計算有關。但是可以大致認為是一致的。

上述邏輯如下圖所示:

write_timeout表格

tcp_retries2 buffer未滿 buffer滿
5 立即返回 min(SO_SNDTIMEO,(25.6s-51.2s)根據動態rto定
15 立即返回 min(SO_SNDTIMEO,(924.6s-1044.6s)根據動態rto定

java的SocketOutputStream的sockWrite0超時時間

java的sockWrite0沒有設置超時時間的地方,同時也沒有設置過SO_SNDTIMEOUT,其直接調用了系統調用,所以其超時時間和write系統調用保持一致。

readTimeout

ReadTimeout可能是最容易導致問題的地方。我們先看下系統調用的源碼:

read系統調用

socket的read系統調用最終調用的是tcp_recvmsg, 其源碼如下:

int tcp_recvmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg,
		size_t len, int nonblock, int flags, int *addr_len)
{
	......
	// 這邊timeo=SO_RCVTIMEO
	timeo = sock_rcvtimeo(sk, nonblock);
	......
	do{
		......
		// 下面這一堆判斷表明,如果出現錯誤,或者已經被CLOSE/SHUTDOWN則跳出循環
		if(copied) {
			if (sk->sk_err ||
			    sk->sk_state == TCP_CLOSE ||
			    (sk->sk_shutdown & RCV_SHUTDOWN) ||
			    !timeo ||
			    signal_pending(current))
				break;
		} else {
			if (sock_flag(sk, SOCK_DONE))
				break;

			if (sk->sk_err) {
				copied = sock_error(sk);
				break;
			}
			// 如果socket shudown跳出
			if (sk->sk_shutdown & RCV_SHUTDOWN)
				break;
			// 如果socket close跳出
			if (sk->sk_state == TCP_CLOSE) {
				if (!sock_flag(sk, SOCK_DONE)) {
					/* This occurs when user tries to read
					 * from never connected socket.
					 */
					copied = -ENOTCONN;
					break;
				}
				break;
			}
			.......
		}
		.......

		if (copied >= target) {
			/* Do not sleep, just process backlog. */
			release_sock(sk);
			lock_sock(sk);
		} else /* 如果沒有讀到target自己數(和水位有關,可以暫認為是1),則等待SO_RCVTIMEO的時間 */
			sk_wait_data(sk, &timeo);	
	} while (len > 0);
	......
}

上面的邏輯如下圖所示:

重傳以及探測定時器timeout事件的觸發時機如下圖所示:

如果內核層面ack正常返回而且對端窗口不為0,僅僅應用層不返回任何數據,那麼就會無限等待,直到對端有數據或者socket close/shutdown為止,如下圖所示:

很多應用就是基於這個無限超時來設計的,例如activemq的消費者邏輯。

java的SocketInputStream的sockRead0超時時間

java的超時時間由SO_TIMOUT決定,而linux的socket並沒有這個選項。其sockRead0和上面的java connect一樣,在SO_TIMEOUT>0的時候依舊是由nonblock socket模擬,在此就不再贅述了。

ReadTimeout超時表格

C系統調用:

tcp_retries2 對端無響應 對端內核響應正常
5 min(SO_RCVTIMEO,(25.6s-51.2s)根據動態rto定 SO_RCVTIMEO==0?無限,SO_RCVTIMEO)
15 min(SO_RCVTIMEO,(924.6s-1044.6s)根據動態rto定 SO_RCVTIMEO==0?無限,SO_RCVTIMEO)

Java系統調用

tcp_retries2 對端無響應 對端內核響應正常
5 min(SO_TIMEOUT,(25.6s-51.2s)根據動態rto定 SO_TIMEOUT==0?無限,SO_RCVTIMEO
15 min(SO_TIMEOUT,(924.6s-1044.6s)根據動態rto定 SO_TIMEOUT==0?無限,SO_RCVTIMEO

對端物理機宕機之後的timeout

對端物理機宕機后還依舊有數據發送

對端物理機宕機時對端內核也gg了(不會發出任何包通知宕機),那麼本端發送任何數據給對端都不會有響應。其超時時間就由上面討論的
min(設置的socket超時[例如SO_TIMEOUT],內核內部的定時器超時來決定)。

對端物理機宕機后沒有數據發送,但在read等待

這時候如果設置了超時時間timeout,則在timeout后返回。但是,如果僅僅是在read等待,由於底層沒有數據交互,那麼其無法知道對端是否宕機,所以會一直等待。但是,內核會在一個socket兩個小時都沒有數據交互情況下(可設置)啟動keepalive定時器來探測對端的socket。如下圖所示:

大概是2小時11分鐘之後會超時返回。keepalive的設置由內核參數指定:

cat /proc/sys/net/ipv4/tcp_keepalive_time 7200 即兩個小時后開始探測
cat /proc/sys/net/ipv4/tcp_keepalive_intvl 75 即每次探測間隔為75s
cat /proc/sys/net/ipv4/tcp_keepalve_probes 9 即一共探測9次

可以在setsockops中對單獨的socket指定是否啟用keepalive定時器(java也可以)。

對端物理機宕機后沒有數據發送,也沒有read等待

和上面同理,也是在keepalive定時器超時之後,將連接close。所以我們可以看到一個不活躍的socket在對端物理機突然宕機之後,依舊是ESTABLISHED狀態,過很長一段時間之後才會關閉。

進程宕后的超時

如果僅僅是對端進程宕機的話(進程所在內核會close其所擁有的所有socket),由於fin包的發送,本端內核可以立刻知道當前socket的狀態。如果socket是阻塞的,那麼將會在當前或者下一次write/read系統調用的時候返回給應用層相應的錯誤。如果是nonblock,那麼會在select/epoll中觸發出對應的事件通知應用層去處理。
如果fin包沒發送到對端,那麼在下一次write/read的時候內核會發送reset包作為回應。

nonblock

設置為nonblock=true后,由於read/write都是立刻返回,且通過select/epoll等處理重傳超時/probe超時/keep alive超時/socket close等事件,所以根據應用層代碼決定其超時特性。定時器超時事件發生的時間如上面幾小節所述,和是否nonblock無關。nonblock的編程模式可以讓應用層對這些事件做出響應。

總結

網絡編程中超時時間是個重要但又容易被忽略的問題,這個問題只有在遇到物理機宕機等平時遇不到的現象時候才會凸顯。筆者在經曆數次物理機宕機之後才好好的研究了一番,希望本篇文章可以對讀者在以後遇到類似超時問題時有所幫助。

公眾號

關注筆者公眾號,獲取更多乾貨文章:

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

【其他文章推薦】

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

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

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

※超省錢租車方案

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

※產品缺大量曝光嗎?你需要的是一流包裝設計!

【String註解驅動開發】如何按照條件向Spring容器中註冊bean?這次我懂了!!

寫在前面

當bean是單實例,並且沒有設置懶加載時,Spring容器啟動時,就會實例化bean,並將bean註冊到IOC容器中,以後每次從IOC容器中獲取bean時,直接返回IOC容器中的bean,不再創建新的bean。

如果bean是單實例,並且使用@Lazy註解設置了懶加載,則Spring容器啟動時,不會實例化bean,也不會將bean註冊到IOC容器中,只有第一次獲取bean的時候,才會實例化bean,並且將bean註冊到IOC容器中。

如果bean是多實例,則Spring容器啟動時,不會實例化bean,也不會將bean註冊到IOC容器中,以後每次從IOC容器中獲取bean時,都會創建一個新的bean返回。

Spring支持按照條件向IOC容器中註冊bean,滿足條件的bean就會被註冊到IOC容器中,不滿足條件的bean就不會被註冊到IOC容器中。接下來,我們就一起來探討Spring中如何實現按照條件向IOC容器中註冊bean。

項目工程源碼已經提交到GitHub:https://github.com/sunshinelyz/spring-annotation

@Conditional註解概述

@Conditional註解可以按照一定的條件進行判斷,滿足條件向容器中註冊bean,不滿足條件就不向容器中註冊bean。

@Conditional註解是由 SpringFramework 提供的一個註解,位於 org.springframework.context.annotation 包內,定義如下。

package org.springframework.context.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Conditional {
	Class<? extends Condition>[] value();
}

從@Conditional註解的源碼來看,@Conditional註解可以添加到類上,也可以添加到方法上。在@Conditional註解中,存在一個Condition類型或者其子類型的Class對象數組,Condition是個啥?我們點進去看一下。

package org.springframework.context.annotation;

import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.core.type.AnnotatedTypeMetadata;
@FunctionalInterface
public interface Condition {
	boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
}

可以看到,Condition是一個函數式接口,對於函數式接口不了解的同學可以參見【Java8新特性】中的《【Java8新特性】還沒搞懂函數式接口?趕快過來看看吧!》一文。也可以直接查看《Java8新特性專欄》來系統學習Java8的新特性。

所以,我們使用@Conditional註解時,需要一個類實現Spring提供的Condition接口,它會匹配@Conditional所符合的方法,然後我們可以使用我們在@Conditional註解中定義的類來檢查。

@Conditional註解的使用場景如下所示。

  • 可以作為類級別的註解直接或者間接的與@Component相關聯,包括@Configuration類;
  • 可以作為元註解,用於自動編寫構造性註解;
  • 作為方法級別的註解,作用在任何@Bean方法上。

向Spring容器註冊bean

不帶條件註冊bean

我們在PersonConfig2類中新增person01()方法和person02()方法,併為兩個方法添加@Bean註解,如下所示。

@Bean("binghe001")
public Person person01(){
    return new Person("binghe001", 18);
}

@Bean("binghe002")
public Person person02(){
    return new Person("binghe002", 20);
}

那麼,這兩個bean默認是否會被註冊到Spring容器中呢,我們新建一個測試用例來測試一下。在SpringBeanTest類中新建testAnnotationConfig6()方法,如下所示。

@Test
public void testAnnotationConfig6(){
    ApplicationContext context = new AnnotationConfigApplicationContext(PersonConfig2.class);
    String[] names = context.getBeanNamesForType(Person.class);
    Arrays.stream(names).forEach(System.out::println);
}

我們運行testAnnotationConfig6()方法,輸出的結果信息如下所示。

person
binghe001
binghe002

從輸出結果可以看出,同時輸出了binghe001和binghe002。說明默認情況下,Spring容器會將單實例並且非懶加載的bean註冊到IOC容器中。

接下來,我們再輸出bean的名稱和bean實例對象信息,此時我們在testAnnotationConfig6()方法中添加相應的代碼片段,如下所示。

@Test
public void testAnnotationConfig6(){
    ApplicationContext context = new AnnotationConfigApplicationContext(PersonConfig2.class);
    String[] names = context.getBeanNamesForType(Person.class);
    Arrays.stream(names).forEach(System.out::println);

    Map<String, Person> beans = context.getBeansOfType(Person.class);
    System.out.println(beans);
}

再次運行SpringBeanTest類中的testAnnotationConfig6()方法,輸出結果如下所示。

person
binghe001
binghe002
給容器中添加Person....
{person=Person(name=binghe002, age=18), binghe001=Person(name=binghe001, age=18), binghe002=Person(name=binghe002, age=20)}

可以看到,輸出了註冊到容器的bean。

帶條件註冊bean

現在,我們就要提出新的需求了,比如,如果當前操作系統是Windows操作系統,則向Spring容器中註冊binghe001;如果當前操作系統是Linux操作系統,則向Spring容器中註冊binghe002。此時,我們就需要使用@Conditional註解了。

這裏,有小夥伴可能會問:如何獲取操作系統的類型呢,別急,這個問題很簡單,我們繼續向下看。

使用Spring的ApplicationContext接口就能夠獲取到當前操作系統的類型,如下所示。

ApplicationContext context = new AnnotationConfigApplicationContext(PersonConfig2.class);
Environment environment = context.getEnvironment();
String osName = environment.getProperty("os.name");
System.out.println(osName);

我們將上述代碼整合到SpringBeanTest類中的testAnnotationConfig6()方法中,如下所示。

@Test
public void testAnnotationConfig6(){
    ApplicationContext context = new AnnotationConfigApplicationContext(PersonConfig2.class);
    Environment environment = context.getEnvironment();
    String osName = environment.getProperty("os.name");
    System.out.println(osName);

    String[] names = context.getBeanNamesForType(Person.class);
    Arrays.stream(names).forEach(System.out::println);

    Map<String, Person> beans = context.getBeansOfType(Person.class);
    System.out.println(beans);
}

接下來,我們運行SpringBeanTest類中的testAnnotationConfig6()方法,輸出的結果信息如下所示。

Windows 10
person
binghe001
binghe002
給容器中添加Person....
{person=Person(name=binghe002, age=18), binghe001=Person(name=binghe001, age=18), binghe002=Person(name=binghe002, age=20)}

由於我使用的操作系統是Windows 10操作系統,所以在結果信息中輸出了Windows 10。

到這裏,我們成功獲取到了操作系統的類型,接下來,就可以實現:如果當前操作系統是Windows操作系統,則向Spring容器中註冊binghe001;如果當前操作系統是Linux操作系統,則向Spring容器中註冊binghe002的需求了。此時,我們就需要藉助Spring的@Conditional註解來實現了。

要想使用@Conditional註解,我們需要實現Condition接口來為@Conditional註解設置條件,所以,這裏,我們創建了兩個實現Condition接口的類,分別為WindowsCondition和LinuxCondition,如下所示。

  • WindowsCondition
package io.mykit.spring.plugins.register.condition;


import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.env.Environment;
import org.springframework.core.type.AnnotatedTypeMetadata;

/**
 * @author binghe
 * @version 1.0.0
 * @description Windows條件,判斷操作系統是否是Windows
 */
public class WindowsCondition implements Condition {
    /**
     * ConditionContext:判斷條件使用的上下文環境
     * AnnotatedTypeMetadata:註釋信息
     */
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        //判斷是否是Linux系統
        //1.獲取到IOC容器使用的BeanFactory
        ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
        //2.獲取類加載器
        ClassLoader classLoader = context.getClassLoader();
        //3.獲取當前的環境信息
        Environment environment = context.getEnvironment();
        //4.獲取bean定義的註冊類,我們可以通過BeanDefinitionRegistry對象查看
        //Spring容器中註冊了哪些bean,也可以通過BeanDefinitionRegistry對象向
        //Spring容器中註冊bean,移除bean,查看bean的定義,查看是否包含某個bean的定義
        BeanDefinitionRegistry registry = context.getRegistry();
        String property = environment.getProperty("os.name");
        return property.contains("Windows");
    }
}
  • LinuxCondition
package io.mykit.spring.plugins.register.condition;

import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.env.Environment;
import org.springframework.core.type.AnnotatedTypeMetadata;

/**
 * @author binghe
 * @version 1.0.0
 * @description Linux條件,判斷操作系統是否是Linux
 */
public class LinuxCondition implements Condition {
    /**
     * ConditionContext:判斷條件使用的上下文環境
     * AnnotatedTypeMetadata:註釋信息
     */
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        //判斷是否是Linux系統
        //1.獲取到IOC容器使用的BeanFactory
        ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
        //2.獲取類加載器
        ClassLoader classLoader = context.getClassLoader();
        //3.獲取當前的環境信息
        Environment environment = context.getEnvironment();
        //4.獲取bean定義的註冊類,我們可以通過BeanDefinitionRegistry對象查看
        //Spring容器中註冊了哪些bean,也可以通過BeanDefinitionRegistry對象向
        //Spring容器中註冊bean,移除bean,查看bean的定義,查看是否包含某個bean的定義
        BeanDefinitionRegistry registry = context.getRegistry();
        String property = environment.getProperty("os.name");
        return property.contains("linux");
    }
}

接下來,我們就需要在PersonConfig2類中使用@Conditional註解添加條件了。添加註解后的方法如下所示。

@Conditional({WindowsCondition.class})
@Bean("binghe001")
public Person person01(){
    return new Person("binghe001", 18);
}

@Conditional({LinuxCondition.class})
@Bean("binghe002")
public Person person02(){
    return new Person("binghe002", 20);
}

此時,我們再次運行SpringBeanTest類中的testAnnotationConfig6()方法,輸出的結果信息如下所示。

Windows 10
person
binghe001
給容器中添加Person....
{person=Person(name=binghe002, age=18), binghe001=Person(name=binghe001, age=18)}

可以看到,輸出結果中不再含有名稱為binghe002的bean了,說明程序中檢測到當前操作系統為Windows10,沒有向Spring容器中註冊名稱為binghe002的bean。

@Conditional註解也可以標註在類上,標註在類上含義為:滿足當前條件,這個類中配置的所有bean註冊才能生效,大家可以自行驗證@Conditional註解標註在類上的情況

@Conditional的擴展註解

@ConditionalOnBean:僅僅在當前上下文中存在某個對象時,才會實例化一個Bean。
@ConditionalOnClass:某個class位於類路徑上,才會實例化一個Bean。
@ConditionalOnExpression:當表達式為true的時候,才會實例化一個Bean。
@ConditionalOnMissingBean:僅僅在當前上下文中不存在某個對象時,才會實例化一個Bean。
@ConditionalOnMissingClass:某個class類路徑上不存在的時候,才會實例化一個Bean。
@ConditionalOnNotWebApplication:不是web應用,才會實例化一個Bean。
@ConditionalOnBean:當容器中有指定Bean的條件下進行實例化。
@ConditionalOnMissingBean:當容器里沒有指定Bean的條件下進行實例化。
@ConditionalOnClass:當classpath類路徑下有指定類的條件下進行實例化。
@ConditionalOnMissingClass:當類路徑下沒有指定類的條件下進行實例化。
@ConditionalOnWebApplication:當項目是一個Web項目時進行實例化。
@ConditionalOnNotWebApplication:當項目不是一個Web項目時進行實例化。
@ConditionalOnProperty:當指定的屬性有指定的值時進行實例化。
@ConditionalOnExpression:基於SpEL表達式的條件判斷。
@ConditionalOnJava:當JVM版本為指定的版本範圍時觸發實例化。
@ConditionalOnResource:當類路徑下有指定的資源時觸發實例化。
@ConditionalOnJndi:在JNDI存在的條件下觸發實例化。
@ConditionalOnSingleCandidate:當指定的Bean在容器中只有一個,或者有多個但是指定了首選的Bean時觸發實例化。

@Conditional 與@Profile 的對比

Spring3.0 也有一些和@Conditional 相似的註解,它們是Spring SPEL 表達式和Spring Profiles 註解 Spring4.0的@Conditional 註解要比@Profile 註解更加高級。@Profile 註解用來加載應用程序的環境。@Profile註解僅限於根據預定義屬性編寫條件檢查。 @Conditional註釋則沒有此限制。

Spring中的@Profile 和 @Conditional 註解用來檢查”If…then…else”的語義。然而,Spring4 @Conditional是@Profile 註解的更通用法。

  • Spring 3中的 @Profile僅用於編寫基於Environment變量的條件檢查。 配置文件可用於基於環境加載應用程序配置。
  • Spring 4 @Conditional註解允許開發人員為條件檢查定義用戶定義的策略。 @Conditional可用於條件bean註冊。

好了,咱們今天就聊到這兒吧!別忘了給個在看和轉發,讓更多的人看到,一起學習一起進步!!

項目工程源碼已經提交到GitHub:https://github.com/sunshinelyz/spring-annotation

寫在最後

如果覺得文章對你有點幫助,請微信搜索並關注「 冰河技術 」微信公眾號,跟冰河學習Spring註解驅動開發。公眾號回復“spring註解”關鍵字,領取Spring註解驅動開發核心知識圖,讓Spring註解驅動開發不再迷茫。

參考:
https://www.cnblogs.com/cxuanBlog/p/10960575.html

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

【其他文章推薦】

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

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

※超省錢租車方案

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

※網頁設計最專業,超強功能平台可客製化

※產品缺大量曝光嗎?你需要的是一流包裝設計!