RALM: 實時 Look-alike 算法在微信看一看中的應用

嘉賓:劉雨丹 騰訊 高級研究員

整理:Jane Zhang

來源:DataFunTalk

出品:DataFun

注:歡迎關注DataFunTalk同名公眾號,收看第一手原創技術文章。

導讀:本次分享是微信看一看團隊在 KDD2019 上發表的一篇論文。長尾問題是推薦系統中的經典問題,但現今流行的點擊率預估方法無法從根本上解決這個問題。文章在 look-alike 方法基礎上,針對微信看一看的應用場景設計了一套實時 look-alike 框架,在解決長尾問題的同時也滿足了資訊推薦的高時效性要求。

▌背景

微信大家可能都用過,微信中的“看一看”是 feed 推薦流的形式,涵蓋了騰訊整個生態鏈的內容分發平台,包括騰訊新聞、公眾號文章、騰訊視頻等。每天總分發量在千萬級以上,面對如此大的分發量,要滿足不同興趣偏好的用戶需求,使用傳統的方法時遇到了一些問題。我們針對發現的問題做了優化和改進,接下來分享下我們優化的過程。

▌未緩解的馬太效應

馬太效應,簡單解釋,在內容的生態系統中,自然分髮狀態會造成一種現象:頭部10%的內容佔據了系統90%的流量、曝光量or點擊量,剩下90%的內容,集中在長尾的10%里。這對於內容的生產方、內容系統的生態和使用系統的用戶來說,都是不健康的狀態。造成這種現象的原因,是因為系統分發能力不夠強,無法處理信息過載的現象,推薦系統設計的初衷就是為了解決馬太效應問題。

回顧推薦系統的發展,從最開始的規則匹配 -> 協同過濾 -> 線性模型 -> deep learning,逐步緩解了馬太效應現象,但沒有完全解決。

造成這個現象的原因是傳統模型、CTR 預估和 deep model,都對部分特徵有依賴,沒有把特徵完全發掘出來,導致模型推薦結果是趨熱的,使生態系統內優質長尾內容投放依然困難。因為 CTR model 最終趨向於行為特徵,或者后驗結果較好的數據,對於優質長尾內容,如小眾興趣的音樂、電影、深度報道的新聞專題等,獲得的相應曝光依舊困難,處於馬太效應 long tail 90%的部分,這會影響推薦系統的生態,導致推薦系統內容越來越窄。

▌為什麼無法準確投放長尾?

怎樣解決這個問題?這個問題歸根結底是對內容的建模不夠完整。我們嘗試分析下問題出在哪:

先看下推薦系統建模流程。首先得到原始樣本,這是業務下的訓練數據,形式是三元組:userid,itemid 和 label。如果是 timeline 的樣本,那就是點擊或者不點擊。原始樣本中,一條樣本可以完整表示一個用戶在某個時間點對一個 item 產生了一次行為,把這個三元組當作信息的最完整形式。對於這個完整形式,直接建模很簡單,如傳統的 item CF,或者協同過濾。協同過濾是最初級的方法,直接對 uid,itemid,label 做擬合,因為可以完全利用初始樣本的信息,擬合的準確性非常好。弱點也很明顯,對原始樣本中沒有包含的 userid 或者 itemid,沒有泛化推理能力,後續新曝光的 user 和 item 是無法處理的。這個問題,就是我們要做的第二步驟,對原始樣本做抽象。既然無法獲取所有的 userid 和 itemid,那就要對 user 或者 item 做一層抽象,如 user 抽象成基礎畫像:年齡、性別或所處地域;item 抽象成語義特徵:topic、tag 等;item 歷史行為特徵,簡單做統計:過去一段時間的點擊率、曝光率、曝光次數。最後基於泛化過的特徵做擬合,得到最終模型。

問題出在哪?做原始特徵抽象,抽象意味着發生了信息損失,這部分信息損失導致模型擬合時走向了比較偏的道路。舉個簡單的例子:同一個 item,有相同的 topic tag,歷史點擊率和歷史曝光次數和點擊次數也相同,可以說這兩個 item 是相同的嗎?顯然有可能是不同的。使用統計特徵無法完整表達,同樣的 item 點擊都是0.5,PV 都是1000 or 2000。有些 item 被這群用戶看過,有些 item 被那群用戶看過。儘管語義特徵和行為特徵都相同,但兩群 user 不同,Item 的受眾也不同。這裏說的抽象的方式,是不完整的 item 行為建模,也是對 item 歷史行為不完整的刻畫,這就導致了整個 model,對 item 后驗數據十分依賴,導致推薦結果趨向於 CTR 表現好或者 PV 表現好的 item。最終后驗數據表現好的數據又會更進一步被模型推薦且曝光,這樣會造成惡性循環:一方面,加劇了頭部效應的影響,使模型陷入局部最優;另一方面,整個推薦系統邊界效應收窄,用戶趨向於看之前表現好的數據,很少看到能拓寬推薦系統邊界或者用戶視野的長尾數據。

▌Look-alike 模型

問題就是這樣產生的, 可以思考一下,問題的本質是什麼?就是因為模型無法對 item 行為完整建模,這一步信息損失太大,怎麼解決這個問題呢?我們首先想到了一種方案:look-alike。

這是廣告領域的經典方案,這類模型的方法也很簡單,首先可以有一個候選集合的 item,我們要推這部分 item,怎麼推呢?第一個步驟:找到歷史上已知的、廣告主提供的對 item 表達過興趣的用戶,這部分用戶稱為種子用戶。然後使用用戶相似度法方法,找到和種子用戶最相似的目標人群,稱為目標用戶,把這部分 item 直接推給目標用戶。這個方法在廣告系統中,是用來做定向投放的,效果很好。為什麼呢?我們來看下模型的整體思路。

把相關的 item 找到對它發生過歷史行為的種子用戶,直接用種子用戶的特徵,作為模型的輸入,這是正樣本;從全局用戶中負採樣一部分用戶作為負樣本。用歷史行為的用戶的特徵來學習 item 的歷史行為,相當於把不同用戶看過的 item 區分開,其實是對 item 的歷史行為特徵的完整建模。之前提到,行為樣本是信息量最大的樣本,它們沒有經過抽象,如果能完整的用受眾用戶的行為來計算 item 的特徵,可以說是最完整的 item 歷史特徵的建模。

Look-alike 在廣告領域的應用已經很完善,也有很多方式。可以把 look-alike 相關的研究分成兩個方向:第一種是基於相似度的 look-alike,這種 look-alike 比較簡單,大體思路是把所有用戶做 user embedding,映射到低維的向量中,對它做基於 k-means 或者局部敏感 hash 做聚類,根據當前用戶屬於哪個聚類,把這個種子用戶的類感興趣的內容推給目標用戶。這種方法的特點:性能強。因為簡單,只需要找簇中心,或者向量相似度的計算,因為簡單、性能好,模型準確性低。

第二種是和第一種相反的,基於回歸。包括 LR,或者樹模型,或者 DNN or deep model 的方法,主要思路是直接建模種子用戶的特徵。把種子用戶當做模型的正樣本, 針對每個 item 訓練一個回歸模型,做二分類,得出種子用戶的特徵規律。這種方法的優點是:準確性高,因為會針對每個 item 建模。缺點也明顯:訓練開銷大,針對每個 item 都要單獨訓練一個模型。對於廣告來說,可以接受,因為廣告的候選集沒有那麼大,更新頻率也沒那麼高。

但是對於我們的推薦場景,有一些問題:1. 對內容時效性要求高,如推薦的新聞專題,必須在5分鐘或10分鐘內要觸達用戶;2. 候選集更新頻率高,我們每天的候選集上千萬,每分鐘、每一秒都有新內容,如果新內容無法進入推薦池,會影響推薦效果。

▌核心需求

在我們的場景下,如果還用廣告領域的經典的 look-alike,是無法解決的。如果要對每個候選集建模,採用 regression-base 的方法,如每分鐘都要對新加進來的候選集做建模,包括積累種子用戶、做負採樣、訓練,等模型收斂后離線預測 target user 的相似分,這對於線上的時效性是不能接受的。

對於 similarity base 的方法,它的問題是計算過於簡單,如果直接和 CTR 模型 PK,核心指標會下降,得出來的結論是:傳統的 look-alike 不能直接照搬到我們的系統中。

針對我們的需求,我們整理出來了應該滿足的3點核心需求:

  1. 實時。新 item 分發不需要重新訓練模型,要能實時完成種子用戶的擴展;

  2. 高效。因為線上加到 rank 模型 CTR 的後面, 要保持模型核心指標 CTR 的前提下,再去加強長尾內容分發,這樣模型才有意義。要學習準確性和多樣性的用戶表達方式。

  3. 快速。Look-alike 模型要部署到線上,實時預測種子用戶和目標用戶群體的相似度,要滿足線上實時計算的耗時性能要求,也要精簡模型預測的計算次數。

▌RALM:Real-time Attention based Look-alike Model

基於這三個核心需求,我們提出了一個新的方法,全稱是 real-time attention based look-alike model,簡稱 RALM。我先簡單講下 RALM 核心的三個點。

  1. 核心點

① 模型可總結為 user-users 的 model。回想下經典的 CTR 預估模型,是 user2item 的 point-wise 的處理流程建模。User、item、label,我們做的最大的變化,是借鑒了 look-alike 的思想,把 item 替換成種子用戶。用種子用戶的用戶特徵,代替 item 的行為特徵。所以模型從 user2item 的 model,變成 user2user 的 model。圖中右側是 target user,左側是 seeds。

② 完善的 seeds representation。用種子用戶代替 item 行為特徵。這樣面臨的問題是:怎樣更好地表達一個人群。這個 seeds representation,是我們研究中的核心步驟,要得到一個高效、自適應更新的種子用戶的表達方式。

③ real-time。最終目標是部署在線上,實時預測種子用戶群體相似度,需要是能夠實現 real-time 的框架。

上述是模型表達的思路。I 是一個 item,把 item 用 seeds 的 embedding 的集合來表示,seeds embedding,是組成這個種子用戶的每個用戶的 embedding 的函數。學習了 seeds representation,就是這個函數 f。

  1. 整體結構

接下來看下離線訓練部分,這是離線訓練的整體結構。

模型離線訓練分成兩個階段:右側 user representation learning, 左側第二階段是 look alike learning。user representation learning 模型結構,最後的目標是通過一個用戶在不同領域的行為,學習到用戶在所有領域的多樣性且兼顧準確性的用戶興趣的高階畫像。這個畫像在這個位置是低維特徵,向量特徵通過 user presentation learning 的目標學到了所有用戶的 embedding 之後,第二階段是 look alike learning。Look alike learning 模型,是一個 user to user 的 model,右側是目標用戶的特徵輸入,左側是種子用戶人群的 embedding 輸入,左邊種子用戶是一群用戶的 embedding 堆疊到一起,輸入其實是一個矩陣。這兩邊的輸入來源都是第一階段 representation learning 輸出的 embedding。Look alike 的目標是學習目標用戶和候選 item 種子用戶的相似度,最上面是學習兩次相似分的,最後完成種子用戶的擴展。

▌User Representation Learning

按順序來分析下,第一階段,是用戶的表示學習,user representation learning。

這個模型大家看着會比較眼熟,它是用 Youtube 的 representation model 中演化過來的。Youbute 的基礎模型很簡單,下面是用戶在不同領域的行為,下面的基礎特徵可能會有離散值,也可能是連續值。如果是離散值,可以通過 embedding lookup,再過一個 pooling,再和所有領域的特徵做 merge,上面過一個全連接,最後輸出 embedding。右側是感興趣的 item,也會做一些 embedding lookup,整個做 sce loss,或者是多分類。要預測的是:用戶在點擊了這麼多 item 之後,下一個要點擊的 item,最後要預測的就是表達用戶興趣的 embedding。這層 merge layer,最初 Youtube 的版本是用一個 concat。可以看到最初版模型在訓練時遇到了一個問題,最下層是用到了用戶很多個域 ( 每個 field 稱為一個域,可能是每個用戶在每個分佈下的行為,如電商購物下行為,或者是公眾號閱讀的行為 )。

訓練時看到一個現象,有些域的行為學的非常強,參數來看學的非常充分,某些 field 參數分佈不大,最後的權重值較小,對最終預估的分數沒有影響。這裡有兩個名詞:強關聯和弱關聯。最終預估結果關係比較大的 field、參數學習較強的,稱為強關聯特徵域;相反,學的不充分的、對最終結果影響小的,稱為弱關聯特徵域。對於強關聯和弱關聯,如果看到參數分佈是這樣的,是不是就表明弱關聯特徵不重要呢?並不是。舉例來說,representation learning 如果訓練目標是在“看一看”中的閱讀行為,對於某些經常使用微信公眾號、或者閱讀的用戶來說,他們在公眾號平台的閱讀歷史就是非常強的關聯特徵,能夠決定再看一看中的興趣。對於這些特徵來說,這些特徵是很強的,對於其他的如在電商中的購物或者是在搜索中的 query,這些是比較弱的,對看一看的影響很有限。再思考另一種 case,比如,看一看通過某種形式,吸引了很多新用戶。新用戶進來之後,沒有在公眾號平台的閱讀歷史,但是他們在購物或者搜索中有歷史行為,此時這些歷史行為會影響他下一次閱讀的文章,或者感興趣的 item。這些特徵對這些用戶來說是非常重要的。但目前,顯然這些用戶是沒有學到這些變化的。

排查了下模型訓練的過程,可以把結果集中在這一點上,就是這個 merge layer,其實是負責把用戶不同域的特徵 merge 到一起。Merge layer,可以看到右側的圖,原始的 deep model 用的是左側的實現方法,直接用 concat。Concat 的優點是,可以學到所有 field 的參數,缺點是,無法根據輸入的不同分佈,來調整權重值。也就是說,如果80%的用戶的閱讀歷史都是看一看的種子用戶,閱讀歷史都是很豐富的,就很有可能對所有用戶都把這個特徵學的很強。如果是少量用戶,就學不到了,少量用戶關注對其它特徵的啟發作用,concat layer 是學不到的。因為它對於大部分用戶來說,已經把參數學的非常重了,小部分用戶不足以對它產生影響。所以需要一個機制,針對不同用戶的特徵域的輸入動態調整 merge layer 的方式,我們想到的最好的辦法是 attention。Attention 是最近在 NLP 中非常火的,很多模型都會用到。為什麼要用 attetnion?

右下角的結構,就是 attention。我們用到的 attention 是把用戶的輸入的所有的域當做 attention 的 query,key 和 value 都是自身 field 的本身。這是一個典型的 self-attetnion,我們最後要做的是,讓模型根據用戶自己的輸入領域的情況,動態調整不同領域的融合方式,相對於之前的 concat 的方式來說,concat 其實是把所有領域的 field 強行放在同一個向量空間中來學習,自然會有學習不充分的情況。Self-attenion merge 是讓不同的域在自己的向量空間中學習充分,再通過不同的權重組合在一起。其實是相當於讓用戶能有屬於自己的表達,而不是被歷史豐富的用戶帶着走。這是一個優化,可以明顯改善強弱特徵、訓練不均衡的問題。

可以看一下這是我之前訓練的時候在某個特徵域用 tensorboard 打出來的參數分佈的情況,可以看到 attention merge layer 前後,訓練參數有很大變化,之前這些參數基本上都是0,之後會激活出一些值,這個是最後 user presentat learning 的值:precession、recall、auc。也可以看到模型加完 attention 之後,在 auc 和 loss 上都有所優化。

經過 user representation learning 之後,我們現在擁有了所有用戶的兼顧多樣性和準確性的 embedding 表達。接下來要做的是怎麼用 embedding 來表達種子用戶人群?

▌Look-alike learning

Look-alike 要做的第一步就是如何表達 seeds user。

一個種子用戶應該包含什麼信息,這裏我們做兩點假設:

  1. 每個用戶都有自己的興趣,但對整個群體的人群信息存在不同的貢獻度,我們稱為群體的共性信息:global info。共性信息和目標用戶無關,只和用戶群體自身有關。

  2. 種子用戶群體的個性信息。種子群體中一定存在一小部分用戶和 target 用戶興趣相似,這時,當 target 人群變化時,信息會變化,稱為 local info。

種子用戶的相對表達=個性信息+共性信息。怎樣學習 local info 和 global info 呢?我們想到的是用不同的 attention 機制,學習出兩個 embedding:local & global embedding,分別表示這兩種信息。對於 local embedding,是右上角的圖,稱為 local attention unit,這個 attention,是一個乘法開始,它的公式是把種子用戶的矩陣乘以 w,再乘以 target user 的 embedding,再做一層 softmax,再乘以種子用戶自己,這是一個典型的乘法 attention。它的作用是提取種子用戶群體中和 target user 相關的部分。捕獲種子用戶的 local info。

第二部分是 global info,用 global attention,只和 user 相關,和 attention merge 的方法類似,也是一個 self-attention。作用是把種子用戶乘以矩陣轉換,再乘以種子用戶自己,所做的就是捕捉用戶群體自身內部的興趣分佈。得到的這兩種 local & global embedding 之後,進行加權和,這就是種子用戶群體的全部信息。另一個問題來了,採用兩種 attention union 來捕獲信息,這意味着要計算很多次矩陣乘法,對線上開銷很大。兩個 embedding 需要多少次計算?這裡有個表達公式,這個 h 是 embedding 的維度,K 是種子用戶用戶的數量,總的計算次數 = h * h * K * 2。對於線上耗時,一次預測超過 1000ms,無法接受。

優化耗時,第一個方法是減少種子用戶的數量,這樣會影響種子用戶的表達完整性;另一種是我們線上採取的方式,使用聚類。找到種子用戶內部比較相似的,把它們聚在一起。這種方法:1. 減少 key 的數量,2. 保持種子用戶的全部信息。聚類的方式比較簡單,用的是 k-means。

簡單看下這個模型,右側是 target user embedding,經過全連接,左邊是 series user embedding 矩陣,兩邊都經過 embedding 之後,首先對種子用戶的 embedding 做聚類,得到 k 個聚類中心,把種子用戶的向量根據 k 個聚類中心做聚和,在類似中心內部做類似於 average 的聚和,然後得到 k 個向量,在這 k 個向量之上,一邊做 global embedding,另一邊和 target user 做 local embedding。有了這兩個 embedding 之後,通過加權和的方式,做 cosine,再去擬合 user 到 item 的 label。這裏的 label 用的是點擊。

細節:

聚類的過程需要迭代,比較耗時,並非每個 batch 都去更新聚類中心,而是採取迭代更新的方式,比如把1000個 batch 一輪,訓練完1000個 batch 之後,這1000個 batch 中,不更新聚類中心;到了第二輪,根據全連接參數的變化,再去更新種子用戶的聚類中心,每通過一輪更新一次聚類中心,保證和核心參數是同步的。這樣既保證了訓練的效率,也保證了訓練的準確性。聚類的優化,使線上的計算次數減小到了 k/K 中,之前 K 是萬級別的數量,現在 k 是百級別的數量,耗時也下降了很多。

根據實驗結果,確定不同聚類中心數 k 帶來的影響,選擇了合適的 k。實驗中,k=20,線上 k 是100左右。模型訓練的 label 優化方式,是一個多分類。對不同的種子用戶人群選擇最相似的用戶。多分類的優化方式和 deep model 相似,採用 negative sampling 的方式。

▌系統架構

線上需要實現實時預測,系統實際部署到線上,需要整套系統架構。簡單介紹下 RALM 的配套體系。

大體過程,分成三個模塊,從最底下的離線訓練,到在線異步處理,到在線服務,接下來分別講一下。

  1. 離線訓練

離線訓練,就是兩個階段的訓練,representation learning,look alike learning,需要一提的是,進行完 look alike learning 之後,可以把 user 經過全連接層的 user 表達緩存起來。全量用戶,有10多億,可以 catch 到 KV 中。可以提供給在線服務做緩存,線上不用做實時全量傳播。

2.在線異步處理

離線訓練結束后,是在線異步處理,主要作用是某些可以離線且和線上請求無關的計算,可以先計算完,如更新種子用戶。每個 item 候選集都會對應一個種子用戶列表,更新種子用戶列表,可以每一分鐘更新一次,這和訪問用戶無關,只和候選集的 item 有關。可以實時拉取用戶的點擊日誌,更新點擊某個候選集的種子列表。

① 可以把 global embedding 預計算 ( gl 只和種子用戶有關,是 self-attenion,可在線做異步處理,如每隔一分鐘算一次 )。

② 計算 k-means 聚類中心,也是只和種子用戶有關,可以提前計算好,如推到推薦系統內存中。

③ 所有的東西都是定時更新,不需要線上實時計算。

3.在線服務

線上把聚類中心、global embedding 和所有用戶的 embedding 都已緩存好,只需要拉取 user embedding,和候選集的 global embedding 和聚類中心。線上只需要計算 local embedding,是 target user 到種子用戶的 attention,這需要根據線上請求的 urn 來實時計算。再計算一次 cosine,就可以得到相似度,這個計算量很小。

▌實驗結果

算出 look alike 相似度之後,相似度的分數,可直接給到排序服務,做曝光依據。這是當時寫論文之前做的 ABtest,對比的是用戶畫像匹配推送的策略,上線之後,在擴大曝光規模的前提下,CTR 基本取向穩定,而且有微小提升,多樣性也提升了很多,這都是相對提升。

▌一些細節和思考

特徵:

為什麼要用第一階段的 user representation learning 得到用戶的高階畫像?高階畫像的作用:包含了用戶在某個領域的全部信息,信息量很大,結合 look alike learning 中的行為,需要去學習用戶群體的特徵。不用高階特徵,怎樣學習用戶群體?比較簡單的方法是通過統計的方式:平均年齡分佈和平均閱讀傾向。這些都是基於離散的統計,信息損失很大。如果有了高階的特徵,高階特徵也是從低階特徵,如基礎畫像、年月分佈,這些都是可以學到高階特徵中。如果能夠直接輸出所有領域的高階特徵,之後的利用、或者作為召回、作為 CTR 特徵,都很方便。

模型調優:

① 防止模型過擬合。look alike 的結構很簡單,這樣做的原因: 直接使用用戶的高階特徵,使用了用戶特徵,如果模型不做處理,容易對高階特徵過擬合。採取了2種方式:

  1. 盡量保證 look alike learning 結構簡單;

  2. 全連接層做 dropout。

② 採用 stacking model 的形式。看一看閱讀、電商、新聞、音樂領域都做一次 user representation learning,這些特徵用 stacking 的模式都放到 look alike model 中學習,這就是不同特徵根據不同目標來訓練的,更加減少了在同一個模型中過擬合的防線。

冷啟動曝光:

Look alike model 中用了種子用戶的表達,如果線上有新的 item,怎樣做曝光?

  1. 初始投放策略。使用基於 user item 的語義特徵做線性模型的預測,當做冷啟動 item 的初始投放。這個初始投放不需要積累很多種子用戶,大概到百級別的種子用戶就可以切到 look alike 邏輯了。

  2. Look alike 出來的相似度分數,怎麼做曝光的依據?如果直接用相似度分數,需要確定曝光閾值,如對於某個 item,高於多少分才曝光。我們使用的是線上試探曝光機制:最初給1000條流量,做曝光,這次曝光后,收集在用戶側的打分,取打分的分佈統計,根據不同業務的要求,曝光 top 5% 或者 top10%, 來砍一個閾值分數,最後取曝光閾值。

本次分享就到這裏,謝謝大家。

▌Q & A

Q:這個算法有沒有在召迴環節用,曝光該如何理解?

A:目前的策略有兩種方式:

  1. 直接採用召回的方式,定一個曝光閾值,直接確定是否曝光;

  2. 把相似分數給到下游的 CTR model 作為參考。

Q:能否將兩階段學習合併成一個端到端學習?

A:End-to-End 方式存在兩個問題:

  1. 整個模型參數量很大,結構比較複雜,採用 End-to-End 方式不一定能學習到或者學習的很充分;

  2. 剛剛講到的 stacking 方式,我們最後需要的是盡可能全的表達用戶的方式,所以右側的 user representation learning 並不是從單一業務領域得出的結果,有可能是在多個領域得到的結果,比如在看一看訓練一版 user representation learning,然後用社交或者電商上的行為,再做一版用戶的表示,最後用 stacking 的方式把它們拼接起來,作為特徵輸入,這樣達到的效果會更好。

Q:如果將第一階段用戶表徵學習換成其他通用能學習表徵用戶向量的模型,效果會有什麼影響?

A:我們單獨用 user representation learning 和其它模型做過對比,比如 CTR 中的 user embedding,是針對當前業務比較精準化的表達,所在在泛化性上沒有 user representation learning 效果好。

▌參考資料

Real-time Attention Based Look-alike Model for Recommender System

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

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

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

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

結合源碼,重溫 Android View 的事件處理知多少 ?

前言

  • Android View 的 事件處理在我們的編程中,可謂是無處不在了。但對於大多數人而言,一直都是簡單的使用,對其原理缺乏深入地認識。
  • 學 Android 有一段時間了,最近發現,很多基礎知識開始有些遺忘了,所以從新複習了 View 的事件分發。特地整理成了這篇文章分享給大家。
  • 本文不難,可以作為大家茶餘飯後的休閑。

祝大家閱讀愉快!

方便大家學習,我在 GitHub 上建立個 倉庫

  • 倉庫內容與博客同步更新。由於我在 稀土掘金 簡書 CSDN 博客園 等站點,都有新內容發布。所以大家可以直接關注該倉庫,即使獲得精彩內容

  • 倉庫地址:

一、View 的事件回調

  • 我們結合源碼看看 View 的事件分發是個怎樣的過程,首先我們建立一個類 MyButton 類繼承 AppCompatButton 用於測試:
public class MyButton extends AppCompatButton {

    private final String TAG = "DeBugMyButton";
        public MyButton(Context context) {
        super(context);
    }

    public MyButton(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public MyButton(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

}

1.1 事件分發流程

  • 我們都知道有一個方法叫做 public boolean dispatchTouchEvent(MotionEvent event) 。首先我們要知道,對於我們這個自定義控件,他的觸摸事件都是從我們 dispatchTouchEvent 這個方法開始往下去分發的。所以可以說:這個方法是一個入口方法。

1.1.1 onTouchEvent 作用

  • 現在我們重寫該方法和另一個方法:onTouchEvent ,並且打印一行日誌:
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
    Log.d(TAG, "----on dispatch Touch Event----");
    return super.dispatchTouchEvent(event);
}

@Override
public boolean onTouchEvent(MotionEvent event) {
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            Log.d(TAG, "----on touch event----");
    }
    return super.onTouchEvent(event);
}
  • 然後我們在 MainActivity 中,設置一個實例化一個 MyButton 控件對象用於測試,並且給他添加一個 onClickListentersetOnTouchListener
public class MainActivity extends AppCompatActivity {

    private final String TAG = "DeBugMainActivity";

    /**
     * 自定義控件 MyButton
     */
    private MyButton mMyButton;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        iniView();
    }

    /**
     * 實例化控件
     */
    private void iniView() {
        mMyButton = findViewById(R.id.my_button);

    mMyButton.setOnTouchListener(new View.OnTouchListener() {
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    Log.d(TAG, "----on touch----");
                    break;
                default:
                    break;
            }
            return false;
        }
    });
    
    mMyButton.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            Log.d(TAG, "----on click----");
        }
    });
    }
}
  • 然後我們運行這個 Demo ,點擊 MyButton 按鈕,會的到如下日誌:
  • 我們可以看到首先回調了這個 dispatchTouchEvent ,然後是它的監聽器 OnTouch ,接着是它的 onTouchEvent,最後又執行了 dispatchTouchEvent ,那麼這是為什麼呢?

  • 這是因為我們這兒只監聽了 ACTION_DOWN 而當手指抬起時它同樣還回去回調 dispatchTouchEvent ,最後我們打印 OnClick 的回調。

  • 總結一下就是:
    dispatchTouchEvent -> setOnTouchListener -> onTouchEvent -> setOnClickListener

  • 說明我們 setOnClickListener 是通過 onTouchEvent 處理,產生了 OnClick 。一會我們再來看看其中的原理。

  • 既然說 dispatchTouchEvent 像一個入口,就先讓我們來看下它是怎麼處理和操作的: 首先,既然我們調用了 super.dispatchTouchEvent(event) ,那麼我們就來看看它父類中是怎麼實現該方法的。不信的是,它的父類 AppCompatButton 也沒有實現該方法 ,最後經過層層搜尋,我們發現這個方法是屬於 View 的方法。

1.1.2 dispatchTouchEvent 的實現

  • 那麼現在我們來看看 ViewdispatchTouchEvent 怎麼實現的:
public boolean dispatchTouchEvent(MotionEvent event) {
    ......
        //noinspection SimplifiableIfStatement
        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnTouchListener != null
                && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event)) {
            result = true;
        }

        if (!result && onTouchEvent(event)) {
            result = true;
        }
    }

    if (!result && mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
    }

    // Clean up after nested scrolls if this is the end of a gesture;
    // also cancel it if we tried an ACTION_DOWN but we didn't want the rest
    // of the gesture.
    if (actionMasked == MotionEvent.ACTION_UP ||
            actionMasked == MotionEvent.ACTION_CANCEL ||
            (actionMasked == MotionEvent.ACTION_DOWN && !result)) {
        stopNestedScroll();
    }

    return result;
}
  • dispatchTouchEvent 中,我們可以發現下面這樣一個代碼塊
if (li != null && li.mOnTouchListener != null
        && (mViewFlags & ENABLED_MASK) == ENABLED
        && li.mOnTouchListener.onTouch(this, event)) {
    result = true;
}
  • 不難看出:如果執行了這個代碼段,那麼後面的方法就不會執行了,並且 dispatchTouchEvent 會返回 true 。我們再仔細觀察下其中的條件:在 if 條件中我們發現:只有當其滿足 li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event)) 時才會執行 if 內的操作

  • 經過上面分析,我們可以知道: onTouch 事件必須返回 true 時,才會執行該方法塊。那麼我們就回到 MainActivity 中。我們發現 setOnTouchListeneronTouch 默認返回值是 false( 不滿足返回值為 true ), 這就表明他會繼續去執行下一個代碼塊:

if (!result && onTouchEvent(event)) {
    result = true;
}
  • 執行這個 if 語句的過程中。首先調用了 onTouchEvent 方法。這就解釋了,為什麼它先執行了 mOnTouchListener ,然後再執行 onTouchEvent

  • 現在我們就可以總結一下:首先我們回調了 dispatchTouchEvent ,然後回調 OnTouchListener 。這個時候,如果 TouchListener 沒有 return true ,那麼就會接着去運行 onTouchEvent ( 當然,如果 return true 後面的層級就不會執行了 。一句話說就是:到那個層級 return true 那麼哪個層級就消費掉了這個事件 )。

1.1.3 onTouchEvent 的處理

  • 同時我們還有一個結果:我們 onClick ( 包括我們的 onLongClick ) 是來自於我們 onTouchEvent 這個方法的處理。那麼下面我們就來看看 View 中是怎麼處理 onTouchEvent 的:
public boolean onTouchEvent(MotionEvent event) {
    。。。

    if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
        switch (action) {
            case MotionEvent.ACTION_UP:
                。。。
                break;

            case MotionEvent.ACTION_DOWN:
                if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
                    mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
                }
                mHasPerformedLongPress = false;

                if (!clickable) {
                    checkForLongClick(0, x, y);
                    break;
                }

                if (performButtonActionOnTouchDown(event)) {
                    break;
                }

                // Walk up the hierarchy to determine if we're inside a scrolling container.
                boolean isInScrollingContainer = isInScrollingContainer();

                // For views inside a scrolling container, delay the pressed feedback for
                // a short period in case this is a scroll.
                if (isInScrollingContainer) {
                    mPrivateFlags |= PFLAG_PREPRESSED;
                    if (mPendingCheckForTap == null) {
                        mPendingCheckForTap = new CheckForTap();
                    }
                    mPendingCheckForTap.x = event.getX();
                    mPendingCheckForTap.y = event.getY();
                    postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                } else {
                    // Not inside a scrolling container, so show the feedback right away
                    setPressed(true, x, y);
                    checkForLongClick(0, x, y);
                }
                break;

            case MotionEvent.ACTION_CANCEL:
                。。。
                break;

            case MotionEvent.ACTION_MOVE:
                if (clickable) {
                    drawableHotspotChanged(x, y);
                }

                // Be lenient about moving outside of buttons
                if (!pointInView(x, y, mTouchSlop)) {
                    // Outside button
                    // Remove any future long press/tap checks
                    removeTapCallback();
                    removeLongPressCallback();
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                        setPressed(false);
                    }
                    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                }
                break;
        }

        return true;
    }

    return false;
}

二、onClick 和 OnLongClick

  • 因為我們是拿 ACTION_DOWN 作為舉例的。那麼我們先來分析一下 case MotionEvent.ACTION_DOWN : 中 onTouchEvent 是怎麼執行的,以及 onClickOnLongClick 是如何產生的:

2.1 onClick 和 OnLongClick 的產生

  • 首先,當我們手指按下時,有一個 mHasPerformedLongPress 標識會先被設為 false 。再往下會執行一行 postDelayed(mPendingCheckForTapViewConfiguration.getTapTimeout()); 我們來看看這一行的作用:

  • 首先,從名字我們就可以猜測,這是個延時執行的方法。我們進一步閱讀發現 mPendingCheckForTap 是一個 Runnable 動作; ViewConfiguration.getTapTimeout() 是一個 100mm 的延時。也就是說延時 100mm 後去執行 mPendingCheckForTap 中的動作。那麼我們就來看看 mPendingCheckForTap 中做了什麼:

private final class CheckForTap implements Runnable {
    public float x;
    public float y;

    @Override
    public void run() {
        mPrivateFlags &= ~PFLAG_PREPRESSED;
        setPressed(true, x, y);
        checkForLongClick(ViewConfiguration.getTapTimeout(), x, y);
    }
}
  • 也就是說,停一百秒后就開始檢查,用戶的手指是否離開了屏幕。( 就是當前 ACTION_DOWN 之後,有沒有觸發了 ACTION_UP 這個環節 ),但是 ACTION_DOWN 后,我們還有一個 ACTION_MOVE 過程。在這個 ACTION_MOVE 中,如果 100mm 內離開了屏幕、或者離開了這個控件就會觸發 ACTION_UP ,那麼就認為這是一個點擊事件 onClick 。如果沒有觸發 ACTION_UP 的話,就會再延時 400mm

2.2 ACTION_DOWN 之後流程

  • ACTION_DOWN 之後,會先等 100mm
  • 如果沒有離開屏幕或者離開控件,就是沒有觸發 ACTION_UP 的話,就會再延時 400mm。
  • 500mm 后就會觸發 onLongClick 事件。

2.3 那麼我們現在來驗證一下 onLongClick :

  • 首先再 MainActivity 中加上:
mMyButton.setOnLongClickListener(new View.OnLongClickListener() {
    @Override
    public boolean onLongClick(View v) {

        return true;
    }
});
  • 接着,我們發現 OnLongClick 是有返回值的,如果返回值是 false 還會接着去觸發 onClick 事件,如果返回 true 的話,那麼這個長按事件就直接被消費掉了( 也就是這個點擊事件就不會完後傳遞到 OnClickListener 中去了 )。

2.4 總結

  • 100mm 時為點擊,500mm 時為長按,接着觸髮長按事件。
  • 再看長按事件的返回值,如果時 true 就結束。
  • 如果時 false 那麼 OnClickListener 就同樣也被執行。
  • 這就是由 obTouchEvent 產生出來的 onClick/onLongClick 的來龍去脈。

總結

  • 我們 View 的事件方法,基本上就是這麼一個思路,從 dispatchTouchEventOnTouchListener 監聽器,再到 onTouchEvent,接着 onTouchEvent 由產生了 onClick/onLongClick
  • 如果大家感興趣的話可以更深入的去閱讀源碼。
  • 重點:學 Android 有一段時間了,我打算好好的梳理一下所學知識,包括 ActivityServiceBroadcastRecevier 事件分發、滑動衝突、新能優化等所有重要模塊,歡迎大家關注 ,方便及時接收更新
  • 如果有可以補充的知識點,歡迎大家在評論區指出。

碼字不易,你的點贊是我總結的最大動力!

  • 由於我在「稀土掘金」「簡書」「CSDN」「博客園」等站點,都有新內容發布。所以大家可以直接關注我的 GitHub 倉庫,以免錯過精彩內容!

  • 倉庫地址:

  • 一萬多字長文,加上精美思維導圖,記得點贊哦,歡迎關注 ,我們下篇文章見!

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

.NET core3.0 使用Jwt保護api

摘要:

本文演示如何向有效用戶提供jwt,以及如何在webapi中使用該token通過JwtBearerMiddleware中間件對用戶進行身份認證。

認證和授權區別?

首先我們要弄清楚認證(Authentication)和授權(Authorization)的區別,以免混淆了。認證是確認的過程中你是誰,而授權圍繞是你被允許做什麼,即權限。顯然,在確認允許用戶做什麼之前,你需要知道他們是誰,因此,在需要授權時,還必須以某種方式對用戶進行身份驗證。 

什麼是JWT?

根據維基百科的定義,JSON WEB Token(JWT),是一種基於JSON的、用於在網絡上聲明某種主張的令牌(token)。JWT通常由三部分組成:頭信息(header),消息體(payload)和簽名(signature)。

頭信息指定了該JWT使用的簽名算法:

header = '{"alg":"HS256","typ":"JWT"}'

HS256表示使用了HMAC-SHA256來生成簽名。

消息體包含了JWT的意圖:

payload = '{"loggedInAs":"admin","iat":1422779638}'//iat表示令牌生成的時間

未簽名的令牌由base64url編碼的頭信息和消息體拼接而成(使用”.”分隔),簽名則通過私有的key計算而成:

key = 'secretkey'  
unsignedToken = encodeBase64(header) + '.' + encodeBase64(payload)  
signature = HMAC-SHA256(key, unsignedToken)

最後在未簽名的令牌尾部拼接上base64url編碼的簽名(同樣使用”.”分隔)就是JWT了:

token = encodeBase64(header) + '.' + encodeBase64(payload) + '.' + encodeBase64(signature)

# token看起來像這樣: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2dnZWRJbkFzIjoiYWRtaW4iLCJpYXQiOjE0MjI3Nzk2Mzh9.gzSraSYS8EXBxLN_oWnFSRgCzcmJmMjLiuyu5CSpyHI

JWT常常被用作保護服務端的資源(resource),客戶端通常將JWT通過HTTP的Authorization header發送給服務端,服務端使用自己保存的key計算、驗證簽名以判斷該JWT是否可信:

Authorization: Bearer eyJhbGci*...<snip>...*yu5CSpyHI

準備工作

使用vs2019創建webapi項目,並且安裝nuget包

Microsoft.AspNetCore.Authentication.JwtBearer

Startup類
  • ConfigureServices 添加認證服務

services.AddAuthentication(options =>
            {
                options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
                options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
            }).AddJwtBearer(options =>
            {
                options.SaveToken = true;
                options.RequireHttpsMetadata = false;
                options.TokenValidationParameters = new TokenValidationParameters()
                {
                    ValidateIssuer = true,
                    ValidateAudience = true,
                    ValidAudience = "https://www.cnblogs.com/chengtian",
                    ValidIssuer = "https://www.cnblogs.com/chengtian",
                    IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("SecureKeySecureKeySecureKeySecureKeySecureKeySecureKey"))
                };
            });
  • Configure 配置認證中間件

 app.UseAuthentication();//認證中間件

創建一個token

  • 添加一個登錄model命名為LoginInput

public class LoginInput
    {

        public string Username { get; set; }

        public string Password { get; set; }
    }
  • 添加一個認證控制器命名為AuthenticateController

[Route("api/[controller]")]
    public class AuthenticateController : Controller
    {
        [HttpPost]
        [Route("login")]
        public IActionResult Login([FromBody]LoginInput input)
        {
            //從數據庫驗證用戶名,密碼 
            //驗證通過 否則 返回Unauthorized

            //創建claim
            var authClaims = new[] {
                new Claim(JwtRegisteredClaimNames.Sub,input.Username),
                new Claim(JwtRegisteredClaimNames.Jti,Guid.NewGuid().ToString())
            };
            IdentityModelEventSource.ShowPII = true;
            //簽名秘鑰 可以放到json文件中
            var authSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("SecureKeySecureKeySecureKeySecureKeySecureKeySecureKey"));

            var token = new JwtSecurityToken(
                   issuer: "https://www.cnblogs.com/chengtian",
                   audience: "https://www.cnblogs.com/chengtian",
                   expires: DateTime.Now.AddHours(2),
                   claims: authClaims,
                   signingCredentials: new SigningCredentials(authSigningKey, SecurityAlgorithms.HmacSha256)
                   );

            //返回token和過期時間
            return Ok(new
            {
                token = new JwtSecurityTokenHandler().WriteToken(token),
                expiration = token.ValidTo
            });
        }
    }
添加api資源

利用默認的控制器WeatherForecastController

    • 添加個Authorize標籤

    • 路由調整為:[Route(“api/[controller]”)] 代碼如下

 [Authorize]
    [ApiController]
    [Route("api/[controller]")]
    public class WeatherForecastController : ControllerBase

到此所有的代碼都已經准好了,下面進行運行測試

運行項目

使用postman進行模擬

  • 輸入url:https://localhost:44364/api/weatherforecast

     

     發現返回時401未認證,下面獲取token

  • 通過用戶和密碼獲取token

    如果我們的憑證正確,將會返回一個token和過期日期,然後利用該令牌進行訪問

  • 利用token進行請求

    ok,最後發現請求狀態200!

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

※評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

Java多線程系列——多線程方法詳解

Java多線程系列文章是Java多線程的詳解介紹,對多線程還不熟悉的同學可以先去看一下我的這篇博客,這篇博客從宏觀層面介紹了多線程的整體概況,接下來的幾篇文章是對多線程的深入剖析。

 

多線程的常用方法

1、currentThread()方法:

介紹:currentThread()方法可返回該代碼正在被哪個線程調用的信息。

示例

例1:

public class Test01 {

	public static void main(String[] args) {
		System.out.println(Thread.currentThread().getName());
	}
	
}

結果:
main

  

結果說明,main方法被名為main的線程調用

 

例2:

class Mythread extends Thread{
	
	public Mythread() {
		System.out.println("構造方法的打印:"+Thread.currentThread().getName());
	}
	
	@Override
	public void run() {
		System.out.println("run方法的打印:"+Thread.currentThread().getName());
	}
}

public class Test01 {

	public static void main(String[] args) {
		Mythread t=new Mythread();
		t.start();//①
	}
	
}

結果:
構造方法的打印:main
run方法的打印:Thread-0

  

從結果可知:Mythread的構造方法是被main線程調用的,而run方法是被名稱為Thread-0的線程調用的,run方法是線程自動調用的

現在我們將①處的代碼改為t.run(),現在的輸出結果如下:

構造方法的打印:main
run方法的打印:main

  

從結果中我們可以看到兩次的結果显示都是main線程調用了方法,因為當你使用t.start()方法的時候是線程自動調用的run()方法,所以輸出的是Thread-0,當你直接調用run()方法時,和調用普通方法沒有什麼區別,所以是main線程調用run()

 

2、isAlive()方法:

介紹:isAlive()方法的功能是判斷當前的線程是否處於活動狀態

示例:

例1:

class Mythread extends Thread{
	
	@Override
	public void run() {
		System.out.println("run =="+this.isAlive());
	}
	
}

public class Test01 {

	public static void main(String[] args) {
		Mythread thread=new Mythread();
		System.out.println("begin =="+thread.isAlive());//①
		thread.start();//②
		System.out.println("end =="+thread.isAlive());//③
	}
	
}

結果:
begin ==false
end ==true
run ==true

  

方法isAlive()的作用是測試線程是否處於活動狀態。那麼什麼情況下是活動狀態呢?活動狀態就是線程已經啟動且尚未停止。線程處於正在運行或準備開始運行的狀態,就認為線程是存活的

①處代碼的結果為false,因為此時線程還未啟動;

②處代碼調用了run()方法輸出結果為run ==true,此時線程處於活動狀態;

③處代碼的結果為true,有的同學看到這個輸出可能會不理解,不是說線程處於活動狀態isAlive()方法的結果才是true,現在程序都已經運行結束了為什麼還是true?這裏的輸出結果是不確定的,我們再來看下面一段代碼

我們將例1中的代碼稍做修改,代碼如下:

public class Test01 {

	public static void main(String[] args) throws InterruptedException {
		Mythread thread=new Mythread();
		System.out.println("begin =="+thread.isAlive());//①
		thread.start();//②
		Thread.sleep(1000);//這裏加了一行代碼,讓當前線程沉睡1秒
		System.out.println("end =="+thread.isAlive());//③
	}
	
}

結果:
begin ==false
run ==true
end ==false

  

現在我們看到③處的代碼結果為end ==false,因為thread對象已經在1秒內執行完畢,而上面代碼輸出結果為true是因為thread線程未執行完畢。

 

3、sleep()方法:

介紹:

方法sleep()的作用是在指定的毫秒數內讓當前“正在執行的線程”休眠(暫停執行),這個“正在執行的線程”是指this.currentThread()返回的線程。

示例:

class Mythread extends Thread{
	
	@Override
	public void run() {
		
		try {
			System.out.println("run threadName="+this.currentThread().getName()+" begin");
			Thread.sleep(2000);
			System.out.println("run threadName="+this.currentThread().getName()+" end");
			
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
	}
	
}

public class Test01 {

	public static void main(String[] args) throws InterruptedException {
		Mythread thread=new Mythread();
		System.out.println("begin ="+System.currentTimeMillis());
		thread.run();//①
		System.out.println("end ="+System.currentTimeMillis());
	}
	
}

結果:
begin =1574660731663
run threadName=main begin
run threadName=main end
end =1574660733665

  

從結果中可以看出main線程暫停了2秒(因為這裏調用的是thread.run())

下面我們將①處的代碼改成thread.start(),再來看下運行結果:

begin =1574661491412
end =1574661491412
run threadName=Thread-0 begin
run threadName=Thread-0 end

  

由於main線程與thread線程是異步執行的,所以首先打印的信息為begin和end,而thread線程是隨後運行的,在最後兩行打印run begin和run end的信息。

 

4、getId()方法:

介紹:getId()方法的作用是取得線程的唯一標識

示例

public class Test01 {

	public static void main(String[] args) throws InterruptedException {
		Thread thread=Thread.currentThread();
		System.out.println(thread.getName()+" "+thread.getId());
	}
	
}

結果:main 1

  

從運行結果可以看出,當前執行代碼的線程名稱是main,線程id值為1

 

5、停止線程:

介紹:停止線程是在多線程開發時很重要的技術點,掌握此技術可以對線程的停止進行有效的處理。停止線程在Java語言中並不像break語句那樣乾脆,需要一些技巧性的處理。

在java中有三種方法可以停止線程

  1. 使用退出標誌,讓線程正常退出,也就是當run方法執行完之後終止
  2. 使用stop方法強制終止線程,但是不推薦使用,因為stop和suspend及resume一樣,是java廢棄的方法
  3. 使用interrupt方法中斷線程(推薦使用)

示例:

例1:

class Mythread extends Thread{
	
	@Override
	public void run() {
		
		for(int i=0;i<5000;i++) {
			System.out.println("i="+(i+1));
		}
	}
	
}

public class Test01 {

	public static void main(String[] args) throws InterruptedException {
		Mythread thread=new Mythread();
		thread.start();
		Thread.sleep(2000);
		thread.interrupt();
	}
	
}

  

運行結果:

 

從運行結果我們可以看出最後i=500000,調用interrupt方法沒有停止線程,那麼該如何停止線程呢?

在介紹如何停止線程時,我們先來介紹一下如何判斷線程是否處於停止狀態

Thread類中提供了兩種方法用來判斷線程是否停止:

1、this.interrupted():測試當前線程是否已經中斷,執行后具有將狀態標誌清除為false的功能

public static boolean interrupted() {
        return currentThread().isInterrupted(true);
}

  

2、this.isInterrupted():測試線程Thread對象是否已經中斷,但是不清除狀態標誌

public boolean isInterrupted() {
        return isInterrupted(false);
}

  

讀者可以仔細觀看一下這兩個方法的聲明有什麼不同?

 

例2:

class Mythread extends Thread{
	
	@Override
	public void run() {
		
		for(int i=0;i<5000;i++) {
			System.out.println("i="+(i+1));
		}
	}
	
}

public class Test01 {

	public static void main(String[] args) throws InterruptedException {
		Mythread thread=new Mythread();
		thread.start();
		Thread.sleep(1000);
		thread.interrupt();
		System.out.println("是否停止1?="+thread.interrupted());
		System.out.println("是否停止2?="+thread.interrupted());
		System.out.println("end!");
	}
	
}

  

結果:

 

 

 輸出結果显示調用了thread.interrupt()方法后線程並未停止,這也就證明了interrupted()方法的解釋:測試當前線程是否已經中斷。這個當前線程是main,它從未斷過,所以打印的結果是兩個false。

如果想讓main線程結束該怎麼做?

將main方法改成如下:

public class Test01 {

	public static void main(String[] args) throws InterruptedException {
		Thread.currentThread().interrupt();
		System.out.println("是否停止1?="+Thread.interrupted());
		System.out.println("是否停止2?="+Thread.interrupted());
		System.out.println("end!");
	}
	
}

結果:
是否停止1?=true
是否停止2?=false
end!

  

從輸出結果我們可以看出,方法interrupted()的確判斷出當前線程是否是停止狀態。但為什麼第2個值是false?

查看一下官方文檔的介紹:

測試當前線程是否已經中斷。線程的中斷狀態由該方法清除。換句話說,如果連續兩次調用該方法,則第二次調用將返回false(在第一次調用已清除了其中斷狀態之後,且第二次調用檢驗完中斷狀態前,當前線程再次中斷的情況除外)。

 文檔中說明的非常清楚,interrupted()方法具有清除狀態的功能,所以第二次調用interrupted方法返回的值時false。

下面我們來看一下isInterrupted()方法,將main方法改成如下代碼:

public static void main(String[] args) throws InterruptedException {
		Mythread thread=new Mythread();
		thread.start();
		thread.interrupt();
		Thread.sleep(1000);
		System.out.println("是否停止1?="+thread.isInterrupted());
		System.out.println("是否停止2?="+thread.isInterrupted());
		System.out.println("end");
}
結果:

是否停止1?=true
是否停止2?=true
end

  

從結果可以看出,方法isInterrrupted()並未清除狀態,所以結果為兩個true。

 

例3:在沉睡中停止

當線程調用sleep()方法后再調用interrupt()方法後會有什麼結果:

class Mythread extends Thread{
	
	@Override
	public void run() {
		
		try {
			System.out.println("run begin");
			Thread.sleep(200000);
			System.out.println("run end");
		} catch (InterruptedException e) {
			System.out.println("在沉睡中被停止,進入catch!"+this.isInterrupted());
			e.printStackTrace();
		}
	}
	
}

public class Test01 {

	public static void main(String[] args) throws InterruptedException {
		try {
			Mythread thread=new Mythread();
			thread.start();
			Thread.sleep(200);
			thread.interrupt();
		}catch(Exception e) {
			System.out.println("main catch");
			e.printStackTrace();
		}
		System.out.println("end!");
	}
	
}

  

 

 

6、暫停線程:

暫停線程意味着此線程還可以恢復運行。在java多線程中,可以使用suspend()方法暫停線程,使用resume()方法恢複線程的執行

例1:

class Mythread extends Thread{
	
	private long i=0;
	public long getI() {
		return i;
	}
	
    public void setI(long i) {
		this.i = i;
	}
	
	@Override
	public void run() {
		while(true) {
			i++;
		}
	}
	
}

public class Test01 {

	public static void main(String[] args) throws InterruptedException {
		
		Mythread thread=new Mythread();
		thread.start();
		Thread.sleep(5000);
		//A段
		thread.suspend();
		System.out.println("A= "+System.currentTimeMillis()+" i="+thread.getI());
		Thread.sleep(5000);
		System.out.println("A= "+System.currentTimeMillis()+" i="+thread.getI());
		
		//B段
		thread.resume();
		Thread.sleep(5000);
		
		//C段
		thread.suspend();
		System.out.println("B= "+System.currentTimeMillis()+" i="+thread.getI());
		Thread.sleep(5000);
		System.out.println("B= "+System.currentTimeMillis()+" i="+thread.getI());
		
	}
	
}

  

結果:

 


從控制台打印的時間上來看,線程的確被暫停了,而且還可以恢復成運行狀態。

 

7、yield方法:

介紹:yield()方法的作用是放棄當前的CPU資源,將它讓給其他的任務去佔用CPU執行時間。但放棄的時間不確定,有可能剛剛放棄,馬上又獲得CPU時間片

示例:

class Mythread extends Thread{
	
	@Override
	public void run() {
		long beginTime=System.currentTimeMillis();
		int count=0;
		for(int i=0;i<500000;i++) {
               //Thread.yield();① count=count+(i+1); } long endTime=System.currentTimeMillis(); System.out.println("用時:"+(endTime-beginTime)+"毫秒!"); } } public class Test01 { public static void main(String[] args) throws InterruptedException { Mythread thread=new Mythread(); thread.start(); } }

結果:用時:2毫秒!

  

現在將①處的代碼取消註釋,我們再來看一下運行結果:

用時:213毫秒!

  

將CPU讓給其他資源導致速度變慢

 

8、線程優先級:

介紹:

在操作系統中,線程可以劃分優先級,優先級較高的線程得到的CPU資源較多,也就是CPU優先執行優先級較高的線程對象中的任務。

設置線程優先級有助於幫“線程規劃器”確定在下一次選擇哪一個線程來優先執行。

設置線程的優先級使用setPriority()方法,此方法在JDK的源代碼如下:

public final void setPriority(int newPriority) {
        ThreadGroup g;
        checkAccess();
        if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) {
            throw new IllegalArgumentException();
        }
        if((g = getThreadGroup()) != null) {
            if (newPriority > g.getMaxPriority()) {
                newPriority = g.getMaxPriority();
            }
            setPriority0(priority = newPriority);
        }
}

  

在Java中,線程的優先級為1-10這10個等級,如果小於1或大於10,則JDK拋出異常throw new IllegalArgumentException()。

通常高優先級的線程總是先執行完,但是並不是一定的,高優先級和低優先級的線程會交替進行,高優先級執行的次數多一些

 

線程優先級的繼承特性:

在Java中,線程的優先級具有繼承性,比如A線程啟動B線程,則B線程的優先級與A是一樣的。

class Mythread2 extends Thread{
	@Override
	public void run() {
		System.out.println("Mythread2 run priority="+this.getPriority());
	}
}


class Mythread1 extends Thread{
	
	@Override
	public void run() {
		System.out.println("Mythread run priority="+this.getPriority());
		Mythread2 thread2=new Mythread2();
		thread2.start();
	}
	
}

public class Test01 {

	public static void main(String[] args) throws InterruptedException {
		
		System.out.println("main thread begin priority="+Thread.currentThread().getPriority());
    	        //Thread.currentThread().setPriority(6);①
		System.out.println("main thread end priority="+Thread.currentThread().getPriority());
		Mythread1 thread1=new Mythread1();
		thread1.start();
	}
	
}

結果:
main thread begin priority=5
main thread end priority=5
Mythread run priority=5
Mythread2 run priority=5

  

可以看到上面幾個線程的優先級都為5

現在將①處的代碼註釋掉后的結果是:

main thread begin priority=5
main thread end priority=6
Mythread run priority=6
Mythread2 run priority=6

  

優先級被更改后再繼續繼承

 

9、守護線程:

在java中有兩種線程,一種是用戶線程,另一種是守護線程。

守護線程是一種特殊的線程,它的特性有“陪伴”的含義,當進程中不存在非守護線程了,則守護線程自動銷毀。典型的守護線程就是垃圾回收線程,當進程中沒有非守護線程了,則垃圾回收線程也就沒有存在的必要了,自動銷毀。用個比較通俗的比喻來解釋一下:“守護線程”:任何一個守護線程都是整個JVM中所有非守護線程的“保姆”,只要當前JVM實例中存在任何一個非守護線程沒有結束,守護線程就在工作,只有當最後一個非守護線程結束時,守護線程才隨着JVM一同結束工作。Daemon的作用是為其他線程的運行提供便利服務,守護線程最典型的應用就是GC(垃圾回收器),它就是一個很稱職的守護者。

 

class Mythread extends Thread{
	
	private int i=0;
	
	
	@Override
	public void run() {
		
		try {
			while(true) {
				i++;
				System.out.println("i="+(i));
				Thread.sleep(1000);
			}
		}catch(Exception e) {
			e.printStackTrace();
		}
		
	}
	
}

public class Test01 {

	public static void main(String[] args) throws InterruptedException {
		
		try {
			Mythread thread=new Mythread();
			thread.setDaemon(true);
			thread.start();
			Thread.sleep(5000);
			System.out.println("我離開Thread對象就不再打印了,也就是停止了");
		}catch(Exception e) {
			e.printStackTrace();
		}
	}
	
}

  

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

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

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

徹底搞懂CSS偽類選擇器:is、not

本文介紹一下Css偽類:is和:not,並解釋一下is、not、matches、any之前的關係

:not

The :not() CSS pseudo-class represents elements that do not match a list of selectors. Since it prevents specific items from being selected, it is known as the negation pseudo-class.

以上是MDN對not的解釋

單從名字上我們應該能對它有大概的認知,非選擇,排除括號內的其它元素

最簡單的例子,用CSS將div內,在不改變html的前提下,除了P標籤,其它的字體顏色變成藍色,

<div>
    <span>我是藍色</span>
    <p>我是黑色</p>
    <h1>我是藍色</h2>
    <h2>我是藍色</h2>
    <h3>我是藍色</h3>
    <h4>我是藍色</h4>
    <h5>我是藍色</h5>
</div>

之前的做法

div span,div h2,div h3, div h4,{
  color: blue;
}

not寫法

div:not(p){
  color: blue;
}

從上面的例子可以明顯體會到not偽類選擇器的作用

下面升級一下,問:將div內除了span和p,其它字體顏色變藍色

div:not(p):not(span){
  color: blue;
}

還有更為簡潔的方法,如下,但是目前兼容不太好,不建議使用

div:not(p,span){
  color: blue;
}

兼容

除IE8,目前所有主流瀏覽器都支持,可以放心使用

:is

The :is() CSS pseudo-class function takes a selector list as its argument, and selects any element that can be selected by one of the selectors in that list. This is useful for writing large selectors in a more compact form.

以上是MDN的解釋

在說is前,需要先了解一下matches

matches跟is是什麼關係?

matches是is的前世,但是本質上確實一個東西,用法完全一樣

matches這個單詞意思跟它的作用非常匹配,但是它跟not作用恰好相反,作為not的對立面,matches這個次看起來確實格格不入,而且單詞不夠簡潔,所以它被改名了,這裏還有一個issue

好了,現在知道matches和is其實是一個東西,那麼is的用法是怎樣的呢?

舉例:將header和main下的p標籤,在鼠標hover時文字變藍色

<header>
  <ul>
    <li><p>鼠標放上去變藍色</p></li>
    <li><p>鼠標放上去變藍色</p></li>
  </ul>
  <p>正常字體</p>
</header>

<main>
  <ul>
    <li><p>鼠標放上去變藍色</p></li>
    <li><p>鼠標放上去變藍色</p></li>
    <p>正常字體</p>
  </ul>
</main>

<footer>
  <ul>
    <li><p>正常字體</p></li>
    <li><p>正常字體</p></li>
  </ul>
</footer>

之前的做法

header ul p:hover,main ul p:hover{
  color: blue;
}

is寫法

:is(header, main) ul p:hover{
  color: blue;
}

從上面的例子大概能看出is的左右,但是並沒有完全體現出is的強大之處,但是當選擇的內容變多之後,特別是那種層級較多的,會發現is的寫法有多簡潔,拿MDN的一個例子看下

之前的寫法

/* Level 0 */
h1 {
  font-size: 30px;
}
/* Level 1 */
section h1, article h1, aside h1, nav h1 {
  font-size: 25px;
}
/* Level 2 */
section section h1, section article h1, section aside h1, section nav h1,
article section h1, article article h1, article aside h1, article nav h1,
aside section h1, aside article h1, aside aside h1, aside nav h1,
nav section h1, nav article h1, nav aside h1, nav nav h1 {
  font-size: 20px;
}

is寫法

/* Level 0 */
h1 {
  font-size: 30px;
}
/* Level 1 */
:is(section, article, aside, nav) h1 {
  font-size: 25px;
}
/* Level 2 */
:is(section, article, aside, nav)
:is(section, article, aside, nav) h1 {
  font-size: 20px;
}

可以看出,隨着嵌套層級的增加,is的優勢越來越明顯

說完了is,那就必須認識一下any,前面說到is是matches的替代者,

any跟is又是什麼關係呢?

是的,is也是any的替代品,它解決了any的一些弊端,比如瀏覽器前綴、選擇性能等

any作用跟is完全一樣,唯一不同的是它需要加瀏覽器前綴,用法如下

:-moz-any(.b, .c) {

}
:-webkit-any(.b, .c) {
    
}

結論

通過上面的介紹大概講述了css偽類is,not,matches,any它們三者的關係

is+not組合是大勢所趨

最後附上我的個人網站 ,轉載請著名出處

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

※想知道網站建置網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計後台網頁設計

※不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

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

※帶您來看台北網站建置台北網頁設計,各種案例分享

【故障公告】docker swarm 集群問題造成新版博客後台故障

非常抱歉,今天下午 16:55~17:05 左右,由於 docker swarm 集群的突發不穩定問題造成(目前處於灰度發布階段)無法正常使用,由此給您帶來麻煩,請您諒解。

出故障期時,新版博客後台的2個容器都無法正常啟動。

AME                NODE                DESIRED STATE       CURRENT STATE 
i_web.1             prod-swarm-w3       Running             Assigned 5 minutes ago                       
i_web.2             prod-swarm-w4       Running             Assigned 2 hours ago       

發現問題后,我們進行了刪除 stack 並重新部署的操作。

docker stack rm i
./deploy-production.sh 2.0.6
NAME                NODE                DESIRED STATE       CURRENT STATE
i_web.1             prod-swarm-w3       Running             Assigned 42 seconds ago                       
i_web.2             prod-swarm-w7       Running             Starting 42 seconds ago

重新部署后發現 prod-swarm-w7  節點上的容器可以正常啟動,而 prod-swarm-w3 節點上的容器問題依舊,由此確認是 prod-swarm-w3 節點出了問題,於是立即卸載該節點。

docker node update --availability drain prod-swarm-w3

卸載后,新版博客後台很快恢復了正常。

我們已經決定用 k8s 取代 docker swarm ,但目前 k8s 集群還沒部署好,在這即將與 docker swarm 說 88 的時刻,又被 docker swarm 坑了一次,都怪我們當時貪圖省事,選對了集裝箱(docker 容器)卻上錯了船(docker swarm),我們會深刻吸取這次上錯船的教訓。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

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

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

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

BloomFilter在Hudi中的應用

Bloom Filter在Hudi中的應用

介紹

Bloom Filter可以用於檢索一個元素是否在一個集合中。它的優點是空間效率和查詢時間都遠遠超過一般的算法,主要缺點是存在一定的誤判率:當其判斷元素存在時,實際上元素可能並不存在。而當判定不存在時,則元素一定不存在,Bloom Filter在對精確度要求不太嚴格的大數據量場景下運用十分廣泛。

引入

為何要引入Bloom Filter?這是Hudi為加快數據upsert採用的一種解決方案,即判斷record是否已經在文件中存在,若存在,則更新,若不存在,則插入。對於upsert顯然無法容忍出現誤判,否則可能會出現應該插入和變成了更新的錯誤,那麼Hudi是如何解決誤判問題的呢?一種簡單辦法是當Bloom Filter判斷該元素存在時,再去文件里二次確認該元素是否真的存在;而當Bloom Filter判斷該元素不存在時,則無需讀文件,通過二次確認的方法來規避Bloom Filter的誤判問題,實際上這也是Hudi採取的方案,值得一提的是,現在Delta暫時還不支持Bloom Filter,其判斷一條記錄是否存在是直接通過一次全表join來實現,效率比較低下。接下來我們來分析Bloom Filter在Hudi中的應用。

流程

Hudi從上游系統(Kafka、DFS等)消費一批數據后,會根據用戶配置的寫入模式(insert、upsert、bulkinsert)寫入Hudi數據集。而當配置為upsert時,意味着需要將數據插入更新至Hudi數據集,而第一步是需要標記哪些記錄已經存在,哪些記錄不存在,然後,對於存在的記錄進行更新,不存在記錄進行插入。

HoodieWriteClient中提供了對應三種寫入模式的方法(#insert、#upsert、#bulkinsert),對於使用了Bloom Filter的#upsert方法而言,其核心源代碼如下

public JavaRDD<WriteStatus> upsert(JavaRDD<HoodieRecord<T>> records, final String commitTime) {
    ...
    // perform index loop up to get existing location of records
    JavaRDD<HoodieRecord<T>> taggedRecords = index.tagLocation(dedupedRecords, jsc, table);
    ...
    return upsertRecordsInternal(taggedRecords, commitTime, table, true);
}

可以看到首先利用索引給記錄打標籤,然後再進行更新,下面主要分析打標籤的過程。

對於索引,Hudi提供了四種索引方式的實現:HBaseIndexHoodieBloomIndexHoodieGlobalBloomIndexInMemoryHashIndex,默認使用HoodieBloomIndex。其中HoodieGlobalBloomIndex與HoodieBloomIndex的區別是前者會讀取所有分區文件,而後者只讀取記錄所存在的分區下的文件。下面以HoodieBloomIndex為例進行分析。

HoodieBloomIndex#tagLocation核心代碼如下

public JavaRDD<HoodieRecord<T>> tagLocation(JavaRDD<HoodieRecord<T>> recordRDD, JavaSparkContext jsc,
      HoodieTable<T> hoodieTable) {

    // Step 0: cache the input record RDD
    if (config.getBloomIndexUseCaching()) {
      recordRDD.persist(config.getBloomIndexInputStorageLevel());
    }

    // Step 1: Extract out thinner JavaPairRDD of (partitionPath, recordKey)
    JavaPairRDD<String, String> partitionRecordKeyPairRDD =
        recordRDD.mapToPair(record -> new Tuple2<>(record.getPartitionPath(), record.getRecordKey()));

    // Lookup indexes for all the partition/recordkey pair
    JavaPairRDD<HoodieKey, HoodieRecordLocation> keyFilenamePairRDD =
        lookupIndex(partitionRecordKeyPairRDD, jsc, hoodieTable);

    // Cache the result, for subsequent stages.
    if (config.getBloomIndexUseCaching()) {
      keyFilenamePairRDD.persist(StorageLevel.MEMORY_AND_DISK_SER());
    }

    // Step 4: Tag the incoming records, as inserts or updates, by joining with existing record keys
    // Cost: 4 sec.
    JavaRDD<HoodieRecord<T>> taggedRecordRDD = tagLocationBacktoRecords(keyFilenamePairRDD, recordRDD);

    if (config.getBloomIndexUseCaching()) {
      recordRDD.unpersist(); // unpersist the input Record RDD
      keyFilenamePairRDD.unpersist();
    }

    return taggedRecordRDD;
  }

該過程會緩存記錄以便優化數據的加載。首先從記錄中解析出對應的分區路徑 -> key,接着查看索引,然後將位置信息(存在於哪個文件)回推到記錄中。

HoodieBloomIndex#lookup核心代碼如下

private JavaPairRDD<HoodieKey, HoodieRecordLocation> lookupIndex(
      JavaPairRDD<String, String> partitionRecordKeyPairRDD, final JavaSparkContext jsc,
      final HoodieTable hoodieTable) {
    // Obtain records per partition, in the incoming records
    Map<String, Long> recordsPerPartition = partitionRecordKeyPairRDD.countByKey();
    List<String> affectedPartitionPathList = new ArrayList<>(recordsPerPartition.keySet());

    // Step 2: Load all involved files as <Partition, filename> pairs
    List<Tuple2<String, BloomIndexFileInfo>> fileInfoList =
        loadInvolvedFiles(affectedPartitionPathList, jsc, hoodieTable);
    final Map<String, List<BloomIndexFileInfo>> partitionToFileInfo =
        fileInfoList.stream().collect(groupingBy(Tuple2::_1, mapping(Tuple2::_2, toList())));

    // Step 3: Obtain a RDD, for each incoming record, that already exists, with the file id,
    // that contains it.
    Map<String, Long> comparisonsPerFileGroup =
        computeComparisonsPerFileGroup(recordsPerPartition, partitionToFileInfo, partitionRecordKeyPairRDD);
    int safeParallelism = computeSafeParallelism(recordsPerPartition, comparisonsPerFileGroup);
    int joinParallelism = determineParallelism(partitionRecordKeyPairRDD.partitions().size(), safeParallelism);
    return findMatchingFilesForRecordKeys(partitionToFileInfo, partitionRecordKeyPairRDD, joinParallelism, hoodieTable,
        comparisonsPerFileGroup);
  }

該方法首先會計算出每個分區有多少條記錄和影響的分區有哪些,然後加載影響的分區的文件,最後計算并行度后,開始找記錄真正存在的文件。

對於#loadInvolvedFiles方法而言,其會查詢指定分區分區下所有的數據文件(parquet格式),並且如果開啟了hoodie.bloom.index.prune.by.ranges,還會讀取文件中的最小key和最大key(為加速後續的查找)。

HoodieBloomIndex#findMatchingFilesForRecordKeys核心代碼如下

JavaPairRDD<HoodieKey, HoodieRecordLocation> findMatchingFilesForRecordKeys(
      final Map<String, List<BloomIndexFileInfo>> partitionToFileIndexInfo,
      JavaPairRDD<String, String> partitionRecordKeyPairRDD, int shuffleParallelism, HoodieTable hoodieTable,
      Map<String, Long> fileGroupToComparisons) {
    JavaRDD<Tuple2<String, HoodieKey>> fileComparisonsRDD =
        explodeRecordRDDWithFileComparisons(partitionToFileIndexInfo, partitionRecordKeyPairRDD);

    if (config.useBloomIndexBucketizedChecking()) {
      Partitioner partitioner = new BucketizedBloomCheckPartitioner(shuffleParallelism, fileGroupToComparisons,
          config.getBloomIndexKeysPerBucket());

      fileComparisonsRDD = fileComparisonsRDD.mapToPair(t -> new Tuple2<>(Pair.of(t._1, t._2.getRecordKey()), t))
          .repartitionAndSortWithinPartitions(partitioner).map(Tuple2::_2);
    } else {
      fileComparisonsRDD = fileComparisonsRDD.sortBy(Tuple2::_1, true, shuffleParallelism);
    }

    return fileComparisonsRDD.mapPartitionsWithIndex(new HoodieBloomIndexCheckFunction(hoodieTable, config), true)
        .flatMap(List::iterator).filter(lr -> lr.getMatchingRecordKeys().size() > 0)
        .flatMapToPair(lookupResult -> lookupResult.getMatchingRecordKeys().stream()
            .map(recordKey -> new Tuple2<>(new HoodieKey(recordKey, lookupResult.getPartitionPath()),
                new HoodieRecordLocation(lookupResult.getBaseInstantTime(), lookupResult.getFileId())))
            .collect(Collectors.toList()).iterator());
  }

該方法首先會查找記錄需要進行比對的文件,然後再查詢的記錄的位置信息。

其中,對於#explodeRecordRDDWithFileComparisons方法而言,其會藉助樹/鏈表結構構造的文件過濾器來加速記錄對應文件的查找(每個record可能會對應多個文件)。

而使用Bloom Filter的核心邏輯承載在HoodieBloomIndexCheckFunction,HoodieBloomIndexCheckFunction$LazyKeyCheckIterator該迭代器完成了記錄對應文件的實際查找過程,查詢的核心邏輯在computeNext`中,其核心代碼如下

protected List<HoodieKeyLookupHandle.KeyLookupResult> computeNext() {

      List<HoodieKeyLookupHandle.KeyLookupResult> ret = new ArrayList<>();
      try {
        // process one file in each go.
        while (inputItr.hasNext()) {
          Tuple2<String, HoodieKey> currentTuple = inputItr.next();
          String fileId = currentTuple._1;
          String partitionPath = currentTuple._2.getPartitionPath();
          String recordKey = currentTuple._2.getRecordKey();
          Pair<String, String> partitionPathFilePair = Pair.of(partitionPath, fileId);

          // lazily init state
          if (keyLookupHandle == null) {
            keyLookupHandle = new HoodieKeyLookupHandle(config, hoodieTable, partitionPathFilePair);
          }

          // if continue on current file
          if (keyLookupHandle.getPartitionPathFilePair().equals(partitionPathFilePair)) {
            keyLookupHandle.addKey(recordKey);
          } else {
            // do the actual checking of file & break out
            ret.add(keyLookupHandle.getLookupResult());
            keyLookupHandle = new HoodieKeyLookupHandle(config, hoodieTable, partitionPathFilePair);
            keyLookupHandle.addKey(recordKey);
            break;
          }
        }

        // handle case, where we ran out of input, close pending work, update return val
        if (!inputItr.hasNext()) {
          ret.add(keyLookupHandle.getLookupResult());
        }
      } catch (Throwable e) {
        if (e instanceof HoodieException) {
          throw e;
        }
        throw new HoodieIndexException("Error checking bloom filter index. ", e);
      }

      return ret;
    }

該方法每次迭代只會處理一個文件,每次處理時都會生成HoodieKeyLookupHandle,然後會添加recordKey,處理完后再獲取查詢結果。

其中HoodieKeyLookupHandle#addKey方法核心代碼如下

public void addKey(String recordKey) {
    // check record key against bloom filter of current file & add to possible keys if needed
    if (bloomFilter.mightContain(recordKey)) {
      ...
      candidateRecordKeys.add(recordKey);
    }
    totalKeysChecked++;
  }

可以看到,這裏使用到了Bloom Filter來判斷該記錄是否存在,如果存在,則加入到候選隊列中,等待進一步判斷;若不存在,則無需額外處理,其中Bloom Filter會在創建HoodieKeyLookupHandle實例時初始化(從指定文件中讀取Bloom Filter)。

HoodieKeyLookupHandle#getLookupResult方法核心代碼如下

public KeyLookupResult getLookupResult() {
    ...
    HoodieDataFile dataFile = getLatestDataFile();
    List<String> matchingKeys =
        checkCandidatesAgainstFile(hoodieTable.getHadoopConf(), candidateRecordKeys, new Path(dataFile.getPath()));
    ...
    return new KeyLookupResult(partitionPathFilePair.getRight(), partitionPathFilePair.getLeft(),
        dataFile.getCommitTime(), matchingKeys);
  }

該方法首先獲取指定分區下的最新數據文件,然後判斷數據文件存在哪些recordKey,並將其封裝進KeyLookupResult后返回。其中#checkCandidatesAgainstFile會讀取文件中所有的recordKey,判斷是否存在於candidateRecordKeys,這便完成了進一步確認。

到這裏即完成了record存在於哪些文件的所有查找,查找完後會進行進一步處理,後續再給出分析。

總結

Hudi引入Bloom Filter是為了加速upsert過程,並將其存入parquet數據文件中的Footer中,在讀取文件時會從Footer中讀取該BloomFilter。在利用Bloom Filter來判斷記錄是否存在時,會採用二次確認的方式規避Bloom Filter的誤判問題。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

Java開發中常用jar包整理及使用

本文整理了我自己在Java開發中常用的jar包以及常用的API記錄。

<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.8</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/commons-io/commons-io -->
        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>2.6</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.8</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.17</version>
        </dependency>

common-lang3

簡介

一個現在最為常用的jar包,封裝了許多常用的工具包

依賴:

<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.4</version>
</dependency>

主要常見的類如下:

  • 數組工具類 ArrayUtils
  • 日期工具類 DateUtils DateFormatUtils
  • 字符串工具類 StringUtils
  • 数字工具類 NumberUtils
  • 布爾工具類 BooleanUtils
  • 反射相關工具類 FieldUtils、MethodUtils、MemberUtils、TypeUtils、ConstructorUtils
  • 對象工具類 ObjectUtils
  • 序列化工具類 SerializationUtils

API介紹

這裏我只介紹經常使用的幾個工具類及方法,ArrayUtils,StringUtils,NumberUtils,DateUtils,其他的請查看官方API文檔吧

1.ArrayUtils

方法名 說明
add
remove
clone 複製數組
addAll
removeAll 第二個參數傳入需要刪除的下標(可以指定多個下標)
toObject 把數值(int[],double[])轉為包裝類(Int[],Double[])
indexOf 在數組按順序查找,找到第一個滿足對應的數值的下標
lastIndexOf 在數組按順序查找,找到最後一個滿足對應的數值的下標
contains 數組是否包含某個值
isEmpty 判斷數組是否為空
isNotEmpty 判斷數組是否不為空
reverse 數組反轉
subarray 指定區間截取數組,區間為半開區間,不包含末尾
toArray 接收一個多個對象,把這幾個對象轉為對應類型的數組
toMap 將一個二維數組轉為Map

2.NumberUtils

方法名 說明
min 比較三個數,返回最小值 或比較指定的幾個數,返回最小值
max 比較三個數,返回最大值 或比較指定的幾個數,返回最大值
createInt 從傳入的String中創建對應類型的數值,createDouble,createFloat…
toInt 將指定字符串轉為Int類型,可以選擇指定默認數值,如果字符串為null則返回默認數值,除此之外,還有toDouble,toLong…等轉為不同類型的方法
compare 比較兩個同類型數值的大小
isDigits 判斷字符串是否只包含数字
isParsable 判斷字符串是否可轉換為Long,Int等類型
isNumber 判斷字符串是否為數值(0x,0X開頭等進制數值)

3.DateUtils

方法名 說明
parseDate 將Date對象轉為字符串
isSameDay 判斷兩個Dated對象是否為同一天
isSameDay 判斷兩個Dated對象是否為同一天
addHour 將指定的Date對象加上指定小時,除此之外,還有addMonth,addDay..等

DateFormatUtils正如其名,是用來把時間轉為字符串,這裏就不再多說

4.StringUtils

方法名 說明
join 將指定的數組連接成字符串,並添加指定的分割字符
containOnly 字符串是否只包含某個字符串
substringBefore 截取指定字符串前面的內容
substringAfter 截取指定字符串後面的內容(不包括指定字符串)
substringBetween 截取字符串某區間內容,如substringBetween(“abcde”,”a”,”e”)=”bcd”
difference 比較兩個字符串,返回兩個字符串不同的內容,具體可以看API文檔給出的示例
isBlank 判斷字符串是否為空白,null,””,” “這三個結果都是為true
isEmpty 判斷字符串是否為空(只要不為null,或傳入的String對象的長度不為0即為true)
countMatches 判斷指定的字符串在某個字符串中出現的次數
deleteWhitespace 刪除字符串中的空格
defaultIfBlank 如果字符串為空白,則返回一個指定的默認值(null或某個String)
defaultIfEmpty 如果字符串為空,則返回一個指定的默認值(null或某個String)
capitalize 將指定字符串首字母大寫
abbreviate 將指定字符串的後面三位轉為…
swapCase 將字符串中的字母大小寫反轉,如aBc變為AbC
lowerCase 將字符串的字母全部轉為小寫
upperCase 將字符串的字母全部轉為大寫
left 取字符串左邊幾個字符,如left(“hello”,3)=”hel”,right與此相反
leftPad 字符串的長度不夠,則使用指定字符填充指定字符串,如leftPad(“hel”,5,”z”)=”zzhel”,rightPad方法與此相反
prependIfMissing 指定字符串不以某段字符串開頭,則自動添加開頭,如prependIfMissing(“hello”,”li”)=”lihello”
prependIfMissing 指定字符串不以某段字符串開頭(忽略大小寫),則自動添加開頭
getCommonPrefix 獲得多個字符串相同的開頭內容,接收參數為多個字符串
removeEnd 刪除字符串中結尾(滿足是以某段內容結尾),如removeEnd(“hello”,”llo”)=”he”
removeEndIgnoreCase 與上面一樣,忽略大小寫
removeStart 與上面的相反
remove 刪除字符串中的指定內容,如remove(“hello”,”l”)=”heo”
removeIgnoreCase 刪除字符串中的指定內容,如remove(“hello”,”l”)=”heo”
strip 清除字符串開頭和末尾指定的字符(第二個參數為null,用來清除字符串開頭和末尾的空格),如strip(” abcxy”,”xy”)=” abc”,strip(” abcxy”,”yx”)=” abc”
stripStart 清除字符串開頭指定字符
stripEnd 清除字符串末尾指定的字符

common-io

簡介

常用的IO流工具包

<!-- https://mvnrepository.com/artifact/commons-io/commons-io -->
<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>2.6</version>
</dependency>

API

我們主要關心的就是Utils後綴的那幾個類即可,可以看到,common-io庫提供了FileUtils,FileSystemUtils,FileNameUtils,FileFilterUtils,IOUtils

FileUtils

  • 寫出文件
  • 讀取文件
  • 創建一個有父級文件夾的文件夾
  • 複製文件和文件夾
  • 刪除文件和文件夾
  • URL轉文件
  • 通過過濾器和擴展名來篩選文件和文件夾
  • 比較文件內容
  • 文件最後修改時間
  • 文件校驗

FileSystemUtils

關於文件系統的相關操作,如查看C盤的大小,剩餘大小等操作

IOUtils

字面意思,是封裝了IO流的各種操作的工具類

Log4j

簡介

Log4J 是 Apache 的一個開源項目,通過在項目中使用 Log4J,我們可以控制日誌信息輸出到控制台、文件、GUI 組件、甚至是數據庫中。

我們可以控制每一條日誌的輸出格式,通過定義日誌的輸出級別,可以更靈活的控制日誌的輸出過程,方便項目的調試。

依賴:

<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
</dependency>

結構

Log4J 主要由 Loggers (日誌記錄器)、Appenders(輸出端)和 Layout(日誌格式化器)組成。

其中Loggers 控制日誌的輸出級別與日誌是否輸出;
Appenders 指定日誌的輸出方式(輸出到控制台、文件等);
Layout 控制日誌信息的輸出格式。

日誌級別:

級別 說明
OFF 最高日誌級別,關閉左右日誌
FATAL 將會導致應用程序退出的錯誤
ERROR 發生錯誤事件,但仍不影響系統的繼續運行
WARN 警告,即潛在的錯誤情形
INFO 一般和在粗粒度級別上,強調應用程序的運行全程
DEBUG 一般用於細粒度級別上,對調試應用程序非常有幫助
ALL 最低等級,打開所有日誌記錄

我們主要使用這四個:Error>Warn>Info>Debug

使用

我們可以使用兩種方式來運行Log4j,一種是java代碼方式,另外一種則是配置文件方式

例子(Java方式)

public class Log4JTest {
    public static void main(String[] args) {   
        //獲取Logger對象的實例(傳入當前類)         
        Logger logger = Logger.getLogger(Log4JTest.class);
        //使用默認的配置信息,不需要寫log4j.properties
        BasicConfigurator.configure();
        //設置日誌輸出級別為WARN,這將覆蓋配置文件中設置的級別,只有日誌級別低於WARN的日誌才輸出
        logger.setLevel(Level.WARN);
        logger.debug("這是debug");
        logger.info("這是info");
        logger.warn("這是warn");
        logger.error("這是error");
        logger.fatal("這是fatal");
    }
}

例子(配置文件方式)

上面的例子,我們想要實現打印Log,但是每次都要寫一遍,浪費時間和精力,所以,Log4j提供了另外一種方式來配置好我們的信息

創建一個名為log4j.properties的文件,此文件需要放在項目的根目錄(約定),如果是maven項目,直接放在resources文件夾中即可

log4j.properties

#控制台
log4j.appender.Console=org.apache.log4j.ConsoleAppender
log4j.appender.Console.layout=org.apache.log4j.PatternLayout
log4j.appender.Console.layout.ConversionPattern=%d [%t] %-5p [%c] - %m%n

#log jdbc
log4j.logger.java.sql.ResultSet=INFO
log4j.logger.org.apache=WARN
log4j.logger.java.sql.Connection=DEBUG
log4j.logger.java.sql.Statement=DEBUG
log4j.logger.java.sql.PreparedStatement=DEBUG

#log mybatis設置
#log4j.logger.org.apache.ibatis=DEBUG
log4j.logger.org.apache.ibatis.jdbc=error
log4j.logger.org.apache.ibatis.io=info
log4j.logger.org.apache.ibatis.datasource=info

#springMVC日誌
log4j.logger.org.springframework.web=WARN

# 文件輸出配置
log4j.appender.A = org.apache.log4j.DailyRollingFileAppender
log4j.appender.A.File = D:/log.txt #指定日誌的輸出路徑
log4j.appender.A.Append = true
log4j.appender.A.Threshold = DEBUG
log4j.appender.A.layout = org.apache.log4j.PatternLayout #使用自定義日誌格式化器
log4j.appender.A.layout.ConversionPattern = %-d{yyyy-MM-dd HH:mm:ss}  [ %t:%r ] - [ %p ]  %m%n #指定日誌的輸出格式
log4j.appender.A.encoding=UTF-8 #指定日誌的文件編碼

#指定日誌的輸出級別與輸出端
log4j.rootLogger=DEBUG,Console,A

#指定某個包名日誌級別(不能超過上面定義的級別,否則日誌不會輸出)
log4j.logger.com.wan=DEBUG

之後使用的話就比較簡單了

//Logger的初始化(這個推薦定義為全局變量,方便使用)
Logger logger = Logger.getLogger(Log4JTest.class);
//輸出Log
logger.info("這是info");

參考鏈接:

lombok

簡介

平常我們創建實體類的時候,需要get/set方法,極其麻煩,雖然IDEA等IDE都是有提供了快捷生成,不過,最好的解決方法還是省略不寫

而lombok就是這樣的一個框架,實現省略get/set方法,當然,lombok的功能不只有此,還有equal,toString方法也可以由此框架自動生成

lombok的原理是使用註解,之後就會在編譯過程中,給Class文件自動加上get/set等方法

不過IDEA似乎無法識別,代碼檢查還是會報錯,所以,使用IDEA的時候還得安裝一個插件,在plugin搜索lombok,之後安裝重啟即可,如圖

之後為Java項目添加依賴

<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.8</version>
    <scope>provided</scope>
</dependency>

使用示例

1.實體類省略get/set
估計Kotlin中的data關鍵字就是參照着lombok實現的

//這裏我們只需要為類添加Data註解,就會自動生成對應屬性的get/set方法,toString,equal等方法
@Data
public class User {
    private String username;
    private String password;
}

2.需要無參構造以及get/set方法

@Getter
@Setter
@NoArgsConstructor
public class User {
    private String username;
    private String password;
}

3.鏈式調用set方法

@Data
@Accessors(chain = true)
public class User {
    private String username;
    private String password;
}

//使用
User user = new User();
user.setUsername("helo").setPassword("123");

4.參數不為空

//如果調用此方法,就會抱一個空指針錯誤
public String print(@NotNull String str){
    ...
}

5.只需要toString

@ToString(callSuper=true, includeFieldNames=true)
public class User {
    private String username;
    private String password;
    //省略的get/set方法
}

6.builder模式創建實體類對象

@Data
@Builder
public class User {
    private String username;
    private String password;
}
//使用
User user1 = User.builder().username("user1").password("123").build();

7.工具類

@UtilityClass
public class MyUtils{
    //會將此方法自動轉為靜態方法
    public void print(String str){
        ...
    }
}
//使用
MyUtils.print("hello");

8.自動關閉流

public static void main(String[] args) throws Exception {
    //使用Cleanup會自動調用close方法
    @Cleanup InputStream in = new FileInputStream(args[0]);
    @Cleanup OutputStream out = new FileOutputStream(args[1]);
    byte[] b = new byte[1024];
    while (true) {
        int r = in.read(b);
        if (r == -1) break;
        out.write(b, 0, r);
    }
}

9.省略Logger時的初始化

@Log4j
@Log
public class User{
    //會自動添加此語句
    //Logger logger = Logger.getLogger(User.class);
    ...
}

參考:

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

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

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

坑~夏令時冬令時引發的時間換算問題

 

起因

最近接觸到一些國外的項目,由於國內外有時差這個東西,對於某些基礎數據存到數據庫的時候需要記錄時間,為了方便,這裏採用了時間戳(int或者timestamp)記錄。由於時間戳全球都是一樣的,需要的時候根據時區進行轉換就能夠拿到當地的時間。

嗯~ o(* ̄▽ ̄*)o,這樣看起來確實沒什麼毛病。眾所周知,一天有24小時,換算成秒就是:24*60*60=86400秒。

然而,我在某次使用 MySql 的 FROM_UNIXTIME 發現一個問題,兩個時間相差86400秒,但是格式化之後卻不是相差一天!!!

假設北京時間2019年11月25日 12:00:00,對應的時間戳是:1574654400,照理說這個時間戳加上一天86400秒,理論上就是北京時間2019年11月26日 12:00:00,事實上確實如此,國內的話這麼算確實沒什麼問題,但是如果是國外時區的話,那可能會出問題。

由於國外部分國家有夏令時冬令時之分(具體下面會細說),直接加上86400秒可能會有問題。

感興趣的可以拿1572764400(太平洋時間2019-11-03 00:00:00,單位:)這個時間戳驗證下

拿代碼演示下:

PHP:

<?php

echo "PST時區的時間\n";
date_default_timezone_set('PST8PDT');
echo date('Y-m-d H:i:s',1572764400);
echo "\n";
echo date('Y-m-d H:i:s',1572764400+86400);
echo "\n";

//換個時區
echo "換成上海時區看看\n";
date_default_timezone_set('Asia/Shanghai');
echo date('Y-m-d H:i:s',1572764400);
echo "\n";
echo date('Y-m-d H:i:s',1572764400+86400);
echo "\n";

運行結果:

PST時區的時間
2019-11-03 00:00:00
2019-11-03 23:00:00
換成上海時區看看
2019-11-03 15:00:00
2019-11-04 15:00:00

明明是同一個時間戳,都是加上86400(一天),為什麼在上海這個時區是第二天,而在PST(美國太平洋時區)只加了23小時?神不神奇!意不意外!

為了弄清楚這個問題,首先得先了解下什麼是夏令時,什麼是冬令時

 

夏令時

夏令時,表示為了節約能源,人為規定時間的意思。也叫夏時制,夏時令(Daylight Saving Time:DST),又稱“日光節約時制”和“夏令時間”,在這一制度實行期間所採用的統一時間稱為“夏令時間”。

一般在天亮早的夏季人為將時間調快一小時,可以使人早起早睡,減少照明量,以充分利用光照資源,從而節約照明用電。各個採納夏時制的國家具體規定不同。目前全世界有近110個國家每年要實行夏令時。[1]

 

冬令時

有夏令時就會有冬令時。高緯度和中緯度的許多國家在夏季到來前,把時針撥快一小時,新的時間就是夏令時,到下半季秋季來臨前,再把時針撥回一小時,即形成冬令時。 [2] 

 

夏令時和冬令時的影響

拿美國來說,美國各個地區的時間都不同,不像中國一樣統一使用北京時間,美國一般以三月份第二個周日凌晨兩點當成夏季的開始,十一月份第一個周日的凌晨兩點當成冬季的開始。

所以在每年的三月份第二個周日凌晨兩點過後,時間就會往前調快一個小時;同理,十一月份第一個周日把這一個小時調回來

你也可以理解成美國那邊,一年裡面有一天只有23小時(夏天開始那一天),有一天有25小時(冬天開始那一天),其他時間每天都是24小時。

所以你會發現,夏天的時候,中國的北京時間(東八區)與美國太平洋時區(西八區)的時差是15小時,而到了冬天卻變成16小時

 

解決方案

回到開頭那個問題,如果我們想直接算第二天,直接加上86400(一天)可能在其他國家就會有我上面那個夏令時和冬令時時間換算的問題,要如何避免呢?首先能夠確定的是,直接加上86400是不可取的,如果加上一天能否行得通

PHP:

<?php

echo "PST時區的時間\n";
date_default_timezone_set('PST8PDT');
echo date('Y-m-d H:i:s',1572764400);
echo "\n";
echo date('Y-m-d H:i:s',1572764400+86400);
echo "\n";

echo "--------------------------\n";
echo date('Y-m-d H:i:s',1572764400);
echo "\n";
echo date('Y-m-d H:i:s',strtotime('+1 day',1572764400));
echo "\n";

運行結果:

PST時區的時間
2019-11-03 00:00:00
2019-11-03 23:00:00
--------------------------
2019-11-03 00:00:00
2019-11-04 00:00:00

可以看出,不直接加上86400,直接在日期上加上一天是完全沒問題的。

JavaScript:

var date = new Date(1572764400*1000);
date.setDate(date.getDate()+1);
var timestamp = Math.round(date.getTime()/1000);

注意:JS的時間戳是毫秒!!!

 

結論

在經濟全球化快速發展的今天,在軟件開發的過程中,盡量養成習慣,由於夏令時和冬令時不是固定的,開發在時間計算上應該慎用86400進行加減運算,時間計算請直接對日期進行加減,展示時間給用戶看的時候盡量結合當地時間,結合夏令時和冬令時計算出準確的當地時間,避免產生不必要的分歧。

 

參考:

[1]. 

[2]. 

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

※想知道網站建置網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計後台網頁設計

※不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

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

※帶您來看台北網站建置台北網頁設計,各種案例分享

.NET高級特性-Emit(2)類的定義,.NET高級特性-Emit(1)

  在上一篇博文發了一天左右的時間,就收到了博客園許多讀者的評論和推薦,非常感謝,我也會及時回復讀者的評論。之後我也將繼續撰寫博文,梳理相關.NET的知識,希望.NET的圈子能越來越大,開發者能了解/深入.NET的本質,將工作做的簡單又高效,拒絕重複勞動,拒絕CRUD。

  ok,咱們開始繼續Emit的探索。在這之前,我先放一下我往期關於Emit的文章,方便讀者閱讀。

  《》

一、基礎知識

  既然C#作為一門面向對象的語言,所以首當其沖的我們需要讓Emit為我們動態構建類。

  廢話不多說,首先,我們先來回顧一下C#類的內部由什麼東西組成:

  (1) 字段-C#類中保存數據的地方,由訪問修飾符、類型和名稱組成;

  (2) 屬性-C#類中特有的東西,由訪問修飾符、類型、名稱和get/set訪問器組成,屬性的是用來控制類中字段數據的訪問,以實現類的封裝性;在Java當中寫作getXXX()和setXXX(val),C#當中將其變成了屬性這種語法糖;

  (3) 方法-C#類中對邏輯進行操作的基本單元,由訪問修飾符、方法名、泛型參數、入參、出參構成;

  (4) 構造器-C#類中一種特殊的方法,該方法是專門用來創建對象的方法,由訪問修飾符、與類名相同的方法名、入參構成。

  接着,我們再觀察C#類本身又具備哪些東西:

  (1) 訪問修飾符-實現對C#類的訪問控制

  (2) 繼承-C#類可以繼承一個父類,並需要實現父類當中所有抽象的方法以及選擇實現父類的虛方法,還有就是子類需要調用父類的構造器以實現對象的創建

  (3) 實現-C#類可以實現多個接口,並實現接口中的所有方法

  (4) 泛型-C#類可以包含泛型參數,此外,類還可以對泛型實現約束

  以上就是C#類所具備的一些元素,以下為樣例:

public abstract class Bar
{
    public abstract void PrintName();
}
public interface IFoo<T> { public T Name { get; set; } } //繼承Bar基類,實現IFoo接口,泛型參數T
public class Foo<T> : Bar, IFoo<T>
  //泛型約束
  where T : struct {
//構造器 public Foo(T name):base() { _name = name; } //字段 private T _name; //屬性 public T Name { get => _name; set => _name = value; } //方法 public override void PrintName() {
    Console.WriteLine(_name.ToString()); }
}

  在探索完了C#類及其定義后,我們要來了解C#的項目結構組成。我們知道C#的一個csproj項目最終會對應生成一個dll文件或者exe文件,這一個文件我們稱之為程序集Assembly;而在一個程序集中,我們內部包含和定義了許多命名空間,這些命令空間在C#當中被稱為模塊Module,而模塊正是由一個一個的C#類Type組成。

 

 

 

   所以,當我們需要定義C#類時,就必須首先定義Assembly以及Module,如此才能進行下一步工作。

二、IL概覽

   由於Emit實質是通過IL來生成C#代碼,故我們可以反向生成,先將寫好的目標代碼寫成cs文件,通過編譯器生成dll,再通過ildasm查看IL代碼,即可依葫蘆畫瓢的編寫出Emit代碼。所以我們來查看以下上節Foo所生成的IL代碼。

  

 

 

   從上圖我們可以很清晰的看到.NET的層級結構,位於樹頂層淺藍色圓點表示一個程序集Assembly,第二層藍色表示模塊Module,在模塊下的均為我們所定義的類,類中包含類的泛型參數、繼承類信息、實現接口信息,類的內部包含構造器、方法、字段、屬性以及它的get/set方法,由此,我們可以開始編寫Emit代碼了

三、Emit編寫

  有了以上的對C#類的解讀和IL的解讀,我們知道了C#類本身所需要哪些元素,我們就開始根據這些元素來開始編寫Emit代碼了。這裏的代碼量會比較大,請讀者慢慢閱讀,也可以參照以上我寫的類生成il代碼進行比對。

  在Emit當中所有創建類型的幫助類均以Builder結尾,從下錶中我們可以看的非常清楚

元素中文 元素名稱 對應Emit構建器名稱
程序集  Assembly AssemblyBuilder
模塊  Module ModuleBuilder
 Type TypeBuilder
構造器  Constructor ConstructorBuilder
屬性  Property PropertyBuilder
字段  Field FieldBuilder
方法  Method MethodBuilder

  由於創建類需要從Assembly開始創建,所以我們的入口是AssemblyBuilder

  (1) 首先,我們先引入命名空間,我們以上節Foo類為樣例進行編寫

using System.Reflection.Emit;

  (2) 獲取基類和接口的類型

var barType = typeof(Bar);
var interfaceType = typeof(IFoo<>);

  (3) 定義Foo類型,我們可以看到在定義類之前我們需要創建Assembly和Module

//定義類
var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName("Edwin.Blog.Emit"), AssemblyBuilderAccess.Run);
var moduleBuilder = assemblyBuilder.DefineDynamicModule("Edwin.Blog.Emit");
var typeBuilder = moduleBuilder.DefineType("Foo", TypeAttributes.Public | TypeAttributes.Class | TypeAttributes.AutoClass | TypeAttributes.AnsiClass | TypeAttributes.BeforeFieldInit);

  (4) 定義泛型參數T,並添加約束

//定義泛型參數
var genericTypeBuilder = typeBuilder.DefineGenericParameters("T")[0];
//設置泛型約束
genericTypeBuilder.SetGenericParameterAttributes(GenericParameterAttributes.NotNullableValueTypeConstraint);

  (5) 繼承和實現接口,注意當實現類的泛型參數需傳遞給接口時,需要將泛型接口添加泛型參數后再調用AddInterfaceImplementation方法

//繼承基類
typeBuilder.SetParent(barType);
//實現接口
typeBuilder.AddInterfaceImplementation(interfaceType.MakeGenericType(genericTypeBuilder));

  (6) 定義字段,因為字段在構造器值需要使用,故先創建

//定義字段
var fieldBuilder = typeBuilder.DefineField("_name", genericTypeBuilder, FieldAttributes.Private);

  (7) 定義構造器,並編寫內部邏輯

//定義構造器
var ctorBuilder = typeBuilder.DefineConstructor(MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName, CallingConventions.Standard, new Type[] { genericTypeBuilder });
var ctorIL = ctorBuilder.GetILGenerator();
//Ldarg_0在實例方法中表示this,在靜態方法中表示第一個參數
ctorIL.Emit(OpCodes.Ldarg_0);
ctorIL.Emit(OpCodes.Ldarg_1);
//為field賦值
ctorIL.Emit(OpCodes.Stfld, fieldBuilder);
ctorIL.Emit(OpCodes.Ret);

  (8) 定義Name屬性

//定義屬性
var propertyBuilder = typeBuilder.DefineProperty("Name", PropertyAttributes.None, genericTypeBuilder, Type.EmptyTypes);

  (9) 編寫Name屬性的get/set訪問器

//定義get方法
var getMethodBuilder = typeBuilder.DefineMethod("get_Name", MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.NewSlot | MethodAttributes.SpecialName | MethodAttributes.Virtual, CallingConventions.Standard, genericTypeBuilder, Type.EmptyTypes);
var getIL = getMethodBuilder.GetILGenerator();
getIL.Emit(OpCodes.Ldarg_0);
getIL.Emit(OpCodes.Ldfld, fieldBuilder);
getIL.Emit(OpCodes.Ret);
typeBuilder.DefineMethodOverride(getMethodBuilder, interfaceType.GetProperty("Name").GetGetMethod()); //實現對接口方法的重載
propertyBuilder.SetGetMethod(getMethodBuilder); //設置為屬性的get方法
//定義set方法
var setMethodBuilder = typeBuilder.DefineMethod("set_Name", MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.NewSlot | MethodAttributes.SpecialName | MethodAttributes.Virtual, CallingConventions.Standard, null, new Type[] { genericTypeBuilder });
var setIL = setMethodBuilder.GetILGenerator();
setIL.Emit(OpCodes.Ldarg_0);
setIL.Emit(OpCodes.Ldarg_1);
setIL.Emit(OpCodes.Stfld, fieldBuilder);
setIL.Emit(OpCodes.Ret);
typeBuilder.DefineMethodOverride(setMethodBuilder, interfaceType.GetProperty("Name").GetSetMethod()); //實現對接口方法的重載
propertyBuilder.SetSetMethod(setMethodBuilder); //設置為屬性的set方法

   (10) 定義並實現PrintName方法

//定義方法
var printMethodBuilder = typeBuilder.DefineMethod("PrintName", MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.Virtual, CallingConventions.Standard, null, Type.EmptyTypes);
var printIL = printMethodBuilder.GetILGenerator();
printIL.Emit(OpCodes.Ldarg_0);
printIL.Emit(OpCodes.Ldflda, fieldBuilder);
printIL.Emit(OpCodes.Constrained, genericTypeBuilder);
printIL.Emit(OpCodes.Callvirt, typeof(object).GetMethod("ToString", Type.EmptyTypes));
printIL.Emit(OpCodes.Call, typeof(Console).GetMethod("WriteLine", new Type[] { typeof(string) }));
printIL.Emit(OpCodes.Ret);
//實現對基類方法的重載
typeBuilder.DefineMethodOverride(printMethodBuilder, barType.GetMethod("PrintName", Type.EmptyTypes));

  (11) 創建類

var type = typeBuilder.CreateType(); //netstandard中請使用CreateTypeInfo().AsType()

  (12) 調用

var obj = Activator.CreateInstance(type.MakeGenericType(typeof(DateTime)), DateTime.Now);
(obj as Bar).PrintName();
Console.WriteLine((obj as IFoo<DateTime>).Name);

四、應用

  上面的樣例僅供學習只用,無法運用在實際項目當中,那麼,Emit構建類在實際項目中我們可以有什麼應用,提高我們的編碼效率

  (1) 動態DTO-當我們需要將實體映射到某個DTO時,可以用動態DTO來代替你手寫的DTO,選擇你需要的字段回傳給前端,或者前端把他想要的字段傳給後端

  (2) DynamicLinq-我的第一篇博文有個讀者提到了表達式樹,而linq使用的正是表達式樹,當表達式樹+Emit時,我們就可以用像SQL或者GraphQL那樣的查詢語句實現動態查詢

  (3) 對象合併-我們可以編寫實現一個像js當中Object.assign()一樣的方法,實現對兩個實體的合併

  (4) AOP動態代理-AOP的核心就是代理模式,但是與其對應的是需要手寫代理類,而Emit就可以幫你動態創建代理類,實現切面編程

  (5) …

五、小結

  對於Emit,確實初學者會對其感到複雜和難以學習,但是只要搞懂其中的原理,其實最終就是C#和.NET語言的本質所在,在學習Emit的同時,也是在鍛煉你的基本功是否紮實,你是否對這門語言精通,是否有各種簡化代碼的應用。

  保持學習,勇於實踐;Write Less,Do More;作者之後還會繼續.NET高級特性系列,感謝閱讀!

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

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

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

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