標籤: iphone維修

  • PBFT共識算法

    PBFT共識算法

    拜占庭將軍問題

    我們已知的共識算法,Paxos、Raft解決的都是非拜占庭問題,也就是可以容忍節點故障,消息丟失、延時、亂序等,但節點不能有惡意節點。但如何在有惡意節點存在的情況下達成共識呢?BFT共識算法就是解決這一問題的。即不但能容忍節點故障,還能容忍一定的惡意節點或者說拜占庭節點的存在。我們下面就學習一下BFT算法中的PBFT(Practical Byzantine Fault Tolerance)。BFT算法有非常多的變種,這裏只學習PBFT,其他的可以舉一反三。

    PBFT

    PBFT核心由3個協議組成:一致性協議、檢查點協議、視圖更換協議。系統正常運行在一致性協議和檢查點協議下,只有當主節點出錯或者運行緩慢的情況下才會啟動視圖更換協議,以維持系統繼續響應客戶端的請求。下面詳解這3個子協議。在講一致性協議之前,我們屏蔽算法細節先看一下正常情況下大致是怎麼工作的,大致流程如下:

    1. 客戶端發送請求給主節點(如果請求發送給了從節點,從節點會將該請求轉發給主節點或者將主節點的信息告知客戶端,讓客戶端發送給主節點)。
    2. 主節點將請求廣播給從節點。
    3. 主從節點經過2輪投票后執行客戶端的請求並響應客戶端。(協議細節見下面的一致性協議)
    4. 客戶端收集到來着\(f+1\)個不同節點的相同的響應后,確認請求執行成功。(因為最多有\(f\)個惡意節點,\(f+1\)個相同即能保證正確性)。

    一致性協議

    一致性協議的目標是使來自客戶端的請求在每個服務器上都按照一個確定的順序執行。 在協議中,一般有一個服務器被稱作主節點,負責將客戶端的請求排序;其餘的服務器稱作從節點,按照主節點提供的順序執行請求。所有的服務器都在相同的配置信息下工作,這個配置信息稱作視圖view,每更換一次主節點,視圖view就會隨之變化。協議主要分pre-preparepreparecommit三階段,如下圖所示:

    REQUEST:

    首先是客戶端發起請求, 請求<REQUEST,o,t,c>中時間戳t主要用來保證exactly-once語義,也就是說對同一客戶端請求不能有執行2次的情況,具體實現時也不一定非是時間戳,也可以是邏輯時鐘或者其他,只要能唯一標識這個請求就可以了。

    PRE-PREPARE:

    【1】 收到客戶端的請求消息后,先判斷當前正在處理的消息數量是否超出限制,如果超出限制,則先緩存起來,後面再打包一起處理。否則的話(當然,沒超過也可以緩存處理),對請求分配序列號n,並附加視圖號v等信息生成PRE-PREPARE消息<<PRE-PREPARE,v,n,d>,m>,廣播給其他節點。簡而言之就是對請求分配序號並告知所有節點。

    【2】 收到PRE-PREPARE的消息後進行如下處理:

    • 消息合法性檢查,消息簽名是否正確,消息摘要是否正確。
    • 視圖檢查,檢查是否是同一個視圖號v
    • 水線檢查,判斷n是否在hH之間。(h一般是系統穩定檢查點,H是上限,會隨着h的不斷提高而提高)

    如果都通過的話,就廣播PREPARE消息<PREPARE,v,n,d,i>給其他節點,表示自己收到並認可[n,v]這個請求,進入prepare階段。如果沒有通過,則忽略該消息。

    這裏想一個問題,從節點能不能收到PRE-PREPARE消息就執行請求呢?答案顯然是不能的,因為不能確認本節點與其他節點收到的是相同的請求消息,此時不能確定主節點是不是正常節點,如果主節點是惡意節點呢?比如,發送給從節點1的消息是m,而發送給從節點2的消息是m',如果直接執行就會出現從節點的不一致。因為不能確認本節點與其他節點收到的是相同的請求消息,所以要通過從節點與從節點交互的方式互相告知收到了請求消息,好讓後面階段對比一下,是否一致。

    PREPARE:
    收到PREPARE消息<PREPARE,v,n,d,i>后,進行如下處理:

    • 消息合法性檢查,消息簽名是否正確,消息摘要是否正確。
    • 視圖檢查,檢查是否是同一個視圖號v
    • 水線檢查,判斷n是否在hH之間。

    如果上面都通過,就將PREPARE消息加入到日誌中,並繼續收集PREPARE消息,如果收到正確的\(2f\)張(包括自己)PREPARE消息,這裏如何驗證是否正確呢?主要是收到的PREPARE要與PRE-PREPARE中的vnd等信息要匹配,就進入COMMIT階段,廣播COMMIT消息<COMMIT,v,n,D(m),i>

    這一階段一般也可以稱為第一輪投票,目的是什麼呢?論文中是這麼說的:The pre-prepare and prepare phases of the algorithm guarantee that non-faulty replicas agree on a total order for the requests within a view. 濃縮為兩個字就是定序,確定在同一視圖下足額的正常的節點都對來自客戶端的請求有相同的定序。再說的直白點,就是解決上面提到的,無法確認本節點與其他節點收到的消息是否一致的問題。通過檢查相同視圖號v及同一序號n下的消息摘要d是否一致來判斷同一視圖配置下的同一個序號請求的消息是否一致。同時也確保了有足夠數量的節點收到了一致的消息請求。

    可以再想一個問題,此時可以直接執行請求嗎?答案是不可以,因為此時,你只能確認自己收到了\(2f\)個一致的PREPARE消息,你無法確認其他節點是否也收到了\(2f\)個一致的PREPARE消息。也就是說,當前,你只能確認自己準備好了去執行序號為n的請求,但是你不能確認其他節點有沒有準備好,所以,還要再進行一次節點間的消息交互,互相告訴大家,我準備好了。

    COMMIT:

    在上一階段,節點收到足額PREPARE投票後會廣播COMMIT投票,過程類似,當節點收到其他節點的COMMIT投票消息后,會進行如下檢查:

    • 消息合法性檢查,檢查消息簽名是否正確,消息摘要正不正確有沒有被篡改。
    • 視圖檢查,view是否匹配。
    • 水線檢查,判斷n是否在hH之間。

    如果都通過則把收到的投票消息寫入日誌log中,如果收到的合法的COMMIT投票消息大於等於\(2f+1\)個(包括自己),意思就是,已經確認大多數節點都準備好了執行請求,就執行請求並回復REPLY消息給客戶端。這裏如同上面一樣,也是檢查視圖,序號及消息是否匹配。

    REPLY:

    客戶端收到REPLY后,會進行統計,如果收到\(f+1\)個相同時間戳t和響應值r,則認為請求響應成功。如果在規定的時間內沒有收到回應或者沒有收到足額回應怎麼辦?可以將該請求廣播給所有節點,節點收到請求后,如果該請求已經被狀態機執行了,則再次回復客戶端REPLY消息,如果沒有被狀態機執行,如果節點不是主節點,就將該請求轉發給主節點。如果主節點沒有正常的將該請求廣播給其他節點,則將會被懷疑是主節點故障或惡意節點,當有足夠的節點都懷疑時將會觸發視圖變更協議,更換視圖。

    我們進行進一步的分析,可以看到,如果是客戶端沒有收到任何回應,很有可能是主節點故障或主節點是惡意節點(我就故意不執行你的請求),沒有將請求足額廣播給其他節點,(當然還有消息丟失等原因,這裏不在詳細分析),這時,客戶端因一直沒有響應,所以將請求廣播給了所有節點,所有節點收到請求后,轉發給主節點后發現主節點怎麼什麼都不幹呀,懷疑主節點有問題,然後觸發視圖更換協議,換掉主節點。當然,客戶端沒有收到足額回應的一個原因還可能是消息丟失,那麼如果是已經執行了該請求的節點再次收到該請求後會再次回應REPLY,前提是該請求是在水線範圍內的合法請求,否則被拒絕。

    檢查點協議

    在上面的一致性協議中可以看到,系統每執行一個請求,服務器都需要記錄日誌(包括,request、pre-prepare、prepare、commit等消息)。如果日誌得不到及時的清理,就會導致系統資源被大量的日誌所佔用,影響系統性能及可用性。另一方面,由於拜占庭節點的存在,一致性協議並不能保證每一台服務器都執行了相同的請求,所以,不同服務器狀態可能不一致。例如,某些服務器可能由於網絡延時導致從某個序號開始之後的請求都沒有執行。因此,設置周期性的檢查點協議,將系統中的服務器同步到某一個相同的狀態。簡言之,主要作用有2個:1、同步服務器的狀態;2、定期清理日誌。

    同步服務器的狀態,比較容易理解與做到。比如在區塊鏈系統中,同步服務器的狀態,實際上就是追塊,即服務器節點會通過鏈定時廣播的鏈世界狀態或其他消息獲知到自己區塊落後了,然後啟動追塊流程。

    定期清理日誌,怎麼做呢?首先要明確哪些日誌可以被清理,哪些日誌仍然需要保留。如果一個請求已經被\(f+1\)台非拜占庭節點執行,並且某一服務器節點i可以向其他服務器節點證明這一點,那麼該i節點就可以將關於這個請求的日誌刪除。協議一般採用的方式是服務器節點每執行一定數量的請求就將自己的狀態發送給所有服務器並且執行一個該協議,如果某台服務器節點收到\(2f+1\)台服務器節點的狀態,那麼其中一致的部分就是至少有\(f+1\)台非拜占庭服務器節點經歷過的狀態,因此,這部分的日誌就可以刪除,同時更新為較新狀態。

    具體實現時可以聯想到上面的一致性協議總的水線檢查。上面的低水線h值等同於穩定檢查點,穩定檢查點之前的日誌都可被清理掉。高水線H=h+k,也就是接收請求序號上限值,因為穩定檢查點往往是間隔很多的序號才觸發一次,所以k一般要設置的足夠大。例如,每間隔100個請求就觸發一次檢查點協議,提升水線,k可以設置為200。

    這裏解釋一下穩定檢查點的概念,可以理解為當\(2f+1\)個節點都達到了某個請求序號,該請求序號就是穩定檢查點。所有穩定檢查點之前的消息都可以被丟棄,減少資源佔用。 對比Raft,Raft是通過快照的方式壓縮日誌,都需要一個清理日誌的機制,不然日誌無限增長下去會造成系統不可用

    視圖更換協議

    在一致性協議里,已經知道主節點在整個系統中擁有序號分配,請求轉發等核心能力,支配着這個系統的運行行為。然而一旦主節點自身發生錯誤,就可能導致從節點接收到具有相同序號的不同請求,或者同一個請求被分配多個序號等問題,這將直接導致請求不能被正確執行。視圖更換協議的作用就是在主節點不能繼續履行職責時,將其用一個從節點替換掉,並且保證已經被非拜占庭服務器執行的請求不會被篡改。即,核心有2點:1,主節點故障時,可能造成系統不可用,要更換主節點;2,當主節點是惡意節點時,要更換為誠實節點,不能讓作惡節點作為主節點。

    當檢測到主節點故障或為惡意節點觸發視圖更換時,下一任主節點應該選誰呢?PBFT的辦法是採用“輪流上崗”的方式,通過\((v+1) \ mod \ N\),其中\(v\)為當前視圖號,\(N\)為節點總數,通過這一方式確定下一個視圖的主節點。還有個更關鍵的問題,什麼時候觸發視圖更換協議呢?我們繼續往下討論。

    如果是主節點故障的情況,這種情況一般較好處理。具體實現時,一般從節點都會維護一個定時器,如果長時間沒有收到來自主節點的消息,就會認為主節點發生故障。此時可觸發視圖更換協議,當然具體實現時,細節可能會不同,比如,也可以是這種情況,客戶端發送請求給故障主節點必然導致長時間收不到響應,所以,客戶端將請求發送給了系統中所有從節點,從節點將請求轉發給主節點並啟動定時器,如果主節點長時間沒有將該請求分配序號發送PRE-PREPARE消息,認為主節點故障,觸發視圖更換協議。這2種情況比較好理解,但就這2種情況嗎?其實還有以下幾種情況也會觸發視圖更換協議:

    • 從節點廣播PREPARE消息后,在約定的時間內未收到來自其他節點的\(2f\)個一致合法消息。
    • 從節點廣播COMMIT消息后,在約定的時間內未收到來自其他節點的\(2f\)個一致合法消息。
    • 從節點收到異常消息,比如視圖、序號一致,但消息不一致。
      這三點,都有可能是主節點作惡導致的,但也有可能是消息丟失等原因導致的。雖然不一定是因為主節點異常導致的,但從另一個角度看,解決了從節點不能無限等待其他節點投票消息的問題。

    這裏補充一點,觸發視圖更換協議后,將不再接收除檢查點消息、VIEW-CHANGE消息、NEW-VIEW消息之外的消息。也就是視圖更換期間,不再接收客戶端請求,暫停服務。

    解決了什麼時候觸發的問題后,下一個問題就是具體怎麼實現呢?當因上面的情況觸發視圖更換協議時,從節點i就會廣播一個VIEW-CHANGE消息<VIEW-CHANGE,v+1,n,C,P,i>,序號n是節點i的最新穩定檢查點sC\(2f+1\)個有效檢查點消息,是為了證明穩定檢查點s的正確性,P是位於序號n之後的一系列消息的結合,這裏要包含這些信息可以理解為是證據,也就是說,從節點不能隨便就發送一個VIEW-CHANGE,什麼證據都沒有,別人怎麼能認同你更換視圖呢?。上面我們提到過下一任主節點是誰的問題?通過\((v+1) \ mod \ N\)確定的一下任主節點p(在圖中就是節點1),在收到\(2f\)個有效的VIEW-CHANGE消息后,就廣播<NEW-VIEW,v+1,V,O>消息,這裏VO具體的生成方法參考原論文,主要是VIEW-CHANGEPRE-PREPARE等消息構成的集合,主要目的是為了讓從節點去驗證當前新的主節點的合法性以及解決下面這個問題,還有要處理未確認消息和投票消息。

    視圖更換協議需要解決的問題是如何保證已經被非拜占庭服務器執行的請求不被更改。由於系統達成一致性之後至少有\(f+1\)台非拜占庭服務器節點執行了請求,所以目前採用的方法是:由新的主節點收集至少\(2f+1\)台服務器節點的狀態信息(也就是上面在構造消息時所需的各種消息集合),這些狀態信息中一定包含所有執行過的請求;然後,新主節點將這些狀態信息發送給所有的服務器,服務器按照相同的原則將在上一個主節點完成的請求同步一遍,同步之後,所有的節點都處於相同的狀態,這時就可以開始執行新的請求。

    若干細節問題的思考

    在3階段協議中,對收到的消息都要進行消息合法性檢查、視圖檢查、水線檢查這3項檢查,為什麼呢?

    這3項檢查是十分有必要的,添加消息簽名是為了驗證投票是否合法,正確統計合法票數,不能是隨便一個不知道的節點都能投票,那我怎麼驗證到底是誰投的呀。也就是說,要通過消息簽名的方式確認消息來源,通過消息摘要的方式,確認消息沒有被篡改。當然,考慮到性能因素,也可以使用消息認證碼(MAC),以節省大量加解密的性能開銷。PBFT算法,可以容忍節點作惡,消息丟失、延時、亂序,但消息不能被篡改。

    視圖檢查比較容易理解,所有節點必須在同一個配置下才能正常工作。如果節點的視圖配置不一致,比如主節點不一致、節點數量不一致,那統計合法票數的時候,真沒法幹了。

    水線檢查,是檢查點協議的一部分,在工程實現時,不是所有的請求我都有處理,比如,你收到一個歷史投票信息,你還有必要處理嗎?當然,它的作用不止於此,還可以防止惡意節點選擇一個非常大的序列號而耗盡序列號空間,例如,當一個節點分配了超過H上限的序列號,這時,正常節點會拒絕這個請求從而阻止了惡意節點分配的遠超過H的序列號。

    3階段協議中每一階段的意義是什麼?

    論文中有如下錶述:

    The three phases are pre-prepare, prepare, and commit.The pre-prepare and prepare phases are used to totally order requests sent in the same view even when the primary, which proposes the ordering of requests, is faulty. The prepare and commit phases are used to ensure that requests that commit are totally ordered across views.

    即,pre-prepareprepare階段,主要的作用就是定序,個人理解就是要確認有足夠數量的節點收到同一請求,並且與自己所收到的請求相一致。prepare以及commit階段是確認大家執行的同一請求。

    為什麼是\(3f+1\)

    我們知道PBFT的容錯能力為不超過三分之一,即\(n=3f+1\)\(f\)為拜占庭節點數量。但這個公式是怎麼來的呢?論文中有這麼一段論述可以幫助我們去理解:

    The resiliency of our algorithm is optimal: \(3f+1\) is the minimum number of replicas that allow an asynchronous system to provide the safety and liveness properties when up to \(f\) replicas are faulty. This many replicas are needed because it must be possible to proceed after communicating with \(n-f\) replicas, since \(f\) replicas might be faulty and not responding. However, it is possible that the \(f\) replicas that did not respond are not faulty and, therefore, \(f\) of those that responded might be faulty. Even so, there must still be enough responses that those from non-faulty replicas outnumber those from faulty ones, i.e., \(n-2f>f\). Therefore \(n>3f\).

    意思就是,在一個容忍\(f\)個錯誤節點的系統中,系統至少要\(3f+1\)個節點才能保證系統安全可靠。為什麼呢?因為在所有\(n\)個節點中,有\(f\)個節點可能因故障而沒有回應(或者投票),而在回應的\(n-f\)中又有可能有\(f\)個是惡意節點的回應,即使如此,也要保證正常節點的投票要多於惡意節點的投票數量,即\(n-f-f>f\),推出\(n>3f\)

    PBFT對比Raft

    PBFT對比Raft,最大的不同在於解決的問題不一樣,雖然都是共識算法,但一個解決的拜占庭問題,另一個則解決的非拜占庭問題。從算法細節上來看,Raft中的領導者是強領導者,即,一切領導者說了算,但PBFT中對應的主節點卻不是,因為不能保證主節點不是拜占庭節點,萬一主節點作惡,從節點要有發現主節點是惡意節點的能力,並及時觸發視圖更換協議更換主節點。從算法消耗的資源來看,明顯PBFT要更複雜,投票數明顯多於Raft,不但要主從節點交互,還有從節點與從節點互相交互,所以,其性能也一定比Raft低,這是肯定的,因為PBFT解決的問題比Raft更複雜,一定程度上可以認為Raft是PBFT的子集,如果你把PBFT三階段協議中從節點與從節點交互的那部分去掉,只保留主節點與從節點交互的那部分,你會發現,好像還蠻像的。從另一個方面說,Raft算法,因為沒有拜占庭節點的存在,領導者節點一定是對的,從節點一切聽領導的就是。但是在PBFT中,從節點就不能光聽主節點的,萬一主節點也是壞人咋辦?怎麼解決這個問題呢?顯然,只聽主節點肯定是不行的,我還要看看其他節點的意見,如果有足額的節點認為是對的,就同意。怎麼確定足額節點數到底是多少呢?上面有講到過。所以,相比Raft,PBFT多了從節點與從節點的消息交互。

    PBFT的時間複雜度分析

    PBFT有比較明顯的兩輪投票,所以時間複雜度\(O(n^2)\),節點數量較大時,一次共識協商所需的消息太多,這也決定了PBFT只能適用於節點數量不大的系統中,比如區塊鏈中的許可鏈,公鏈節點數量太多,並不適用PBFT算法。

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

    【其他文章推薦】

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

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

    ※回頭車貨運收費標準

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

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

  • 8000字長文讓你徹底了解 Java 8 的 Lambda、函數式接口、Stream 用法和原理

    8000字長文讓你徹底了解 Java 8 的 Lambda、函數式接口、Stream 用法和原理

    我是風箏,公眾號「古時的風箏」。一個兼具深度與廣度的程序員鼓勵師,一個本打算寫詩卻寫起了代碼的田園碼農!
    文章會收錄在 JavaNewBee 中,更有 Java 後端知識圖譜,從小白到大牛要走的路都在裏面。公眾號回復『666』獲取高清大圖。

    就在今年 Java 25周歲了,可能比在座的各位中的一些少年年齡還大,但令人遺憾的是,竟然沒有我大,不禁感嘆,Java 還是太小了。(難道我會說是因為我老了?)

    而就在上個月,Java 15 的試驗版悄悄發布了,但是在 Java 界一直有個神秘現象,那就是「你發你發任你發,我的最愛 Java 8」.

    據 Snyk 和 The Java Magazine 聯合推出發布的 2020 JVM 生態調查報告显示,在所有的 Java 版本中,仍然有 64% 的開發者使用 Java 8。另外一些開發者可能已經開始用 Java 9、Java 11、Java 13 了,當然還有一些神仙開發者還在堅持使用 JDK 1.6 和 1.7。

    儘管 Java 8 發布多年,使用者眾多,可神奇的是竟然有很多同學沒有用過 Java 8 的新特性,比如 Lambda表達式、比如方法引用,再比如今天要說的 Stream。其實 Stream 就是以 Lambda 和方法引用為基礎,封裝的簡單易用、函數式風格的 API。

    Java 8 是在 2014 年發布的,實話說,風箏我也是在 Java 8 發布后很長一段時間才用的 Stream,因為 Java 8 發布的時候我還在 C# 的世界中掙扎,而使用 Lambda 表達式卻很早了,因為 Python 中用 Lambda 很方便,沒錯,我寫 Python 的時間要比 Java 的時間還長。

    要講 Stream ,那就不得不先說一下它的左膀右臂 Lambda 和方法引用,你用的 Stream API 其實就是函數式的編程風格,其中的「函數」就是方法引用,「式」就是 Lambda 表達式。

    Lambda 表達式

    Lambda 表達式是一個匿名函數,Lambda表達式基於數學中的λ演算得名,直接對應於其中的lambda抽象,是一個匿名函數,即沒有函數名的函數。Lambda表達式可以表示閉包。

    在 Java 中,Lambda 表達式的格式是像下面這樣

    // 無參數,無返回值
    () -> log.info("Lambda")
    
     // 有參數,有返回值
    (int a, int b) -> { a+b }
    

    其等價於

    log.info("Lambda");
    
    private int plus(int a, int b){
      	return a+b;
    }
    

    最常見的一個例子就是新建線程,有時候為了省事,會用下面的方法創建並啟動一個線程,這是匿名內部類的寫法,new Thread需要一個 implements 自Runnable類型的對象實例作為參數,比較好的方式是創建一個新類,這個類 implements Runnable,然後 new 出這個新類的實例作為參數傳給 Thread。而匿名內部類不用找對象接收,直接當做參數。

    new Thread(new Runnable() {
        @Override
        public void run() {
            System.out.println("快速新建並啟動一個線程");
        }
    }).run();
    

    但是這樣寫是不是感覺看上去很亂、很土,而這時候,換上 Lambda 表達式就是另外一種感覺了。

    new Thread(()->{
        System.out.println("快速新建並啟動一個線程");
    }).run();
    

    怎麼樣,這樣一改,瞬間感覺清新脫俗了不少,簡潔優雅了不少。

    Lambda 表達式簡化了匿名內部類的形式,可以達到同樣的效果,但是 Lambda 要優雅的多。雖然最終達到的目的是一樣的,但其實內部的實現原理卻不相同。

    匿名內部類在編譯之後會創建一個新的匿名內部類出來,而 Lambda 是調用 JVM invokedynamic指令實現的,並不會產生新類。

    方法引用

    方法引用的出現,使得我們可以將一個方法賦給一個變量或者作為參數傳遞給另外一個方法。::雙冒號作為方法引用的符號,比如下面這兩行語句,引用 Integer類的 parseInt方法。

    Function<String, Integer> s = Integer::parseInt;
    Integer i = s.apply("10");
    

    或者下面這兩行,引用 Integer類的 compare方法。

    Comparator<Integer> comparator = Integer::compare;
    int result = comparator.compare(100,10);
    

    再比如,下面這兩行代碼,同樣是引用 Integer類的 compare方法,但是返回類型卻不一樣,但卻都能正常執行,並正確返回。

    IntBinaryOperator intBinaryOperator = Integer::compare;
    int result = intBinaryOperator.applyAsInt(10,100);
    

    相信有的同學看到這裏恐怕是下面這個狀態,完全不可理喻嗎,也太隨便了吧,返回給誰都能接盤。

    先別激動,來來來,現在咱們就來解惑,解除蒙圈臉。

    Q:什麼樣的方法可以被引用?

    A:這麼說吧,任何你有辦法訪問到的方法都可以被引用。

    Q:返回值到底是什麼類型?

    A:這就問到點兒上了,上面又是 Function、又是Comparator、又是 IntBinaryOperator的,看上去好像沒有規律,其實不然。

    返回的類型是 Java 8 專門定義的函數式接口,這類接口用 @FunctionalInterface 註解。

    比如 Function這個函數式接口的定義如下:

    @FunctionalInterface
    public interface Function<T, R> {
        R apply(T t);
    }
    

    還有很關鍵的一點,你的引用方法的參數個數、類型,返回值類型要和函數式接口中的方法聲明一一對應才行。

    比如 Integer.parseInt方法定義如下:

    public static int parseInt(String s) throws NumberFormatException {
        return parseInt(s,10);
    }
    

    首先parseInt方法的參數個數是 1 個,而 Function中的 apply方法參數個數也是 1 個,參數個數對應上了,再來,apply方法的參數類型和返回類型是泛型類型,所以肯定能和 parseInt方法對應上。

    這樣一來,就可以正確的接收Integer::parseInt的方法引用,並可以調用Funcitonapply方法,這時候,調用到的其實就是對應的 Integer.parseInt方法了。

    用這套標準套到 Integer::compare方法上,就不難理解為什麼即可以用 Comparator<Integer>接收,又可以用 IntBinaryOperator接收了,而且調用它們各自的方法都能正確的返回結果。

    Integer.compare方法定義如下:

    public static int compare(int x, int y) {
        return (x < y) ? -1 : ((x == y) ? 0 : 1);
    }
    

    返回值類型 int,兩個參數,並且參數類型都是 int

    然後來看ComparatorIntBinaryOperator它們兩個的函數式接口定義和其中對應的方法:

    @FunctionalInterface
    public interface Comparator<T> {
        int compare(T o1, T o2);
    }
    
    @FunctionalInterface
    public interface IntBinaryOperator {
        int applyAsInt(int left, int right);
    }
    

    對不對,都能正確的匹配上,所以前面示例中用這兩個函數式接口都能正常接收。其實不止這兩個,只要是在某個函數式接口中聲明了這樣的方法:兩個參數,參數類型是 int或者泛型,並且返回值是 int或者泛型的,都可以完美接收。

    JDK 中定義了很多函數式接口,主要在 java.util.function包下,還有 java.util.Comparator 專門用作定製比較器。另外,前面說的 Runnable也是一個函數式接口。

    自己動手實現一個例子

    1. 定義一個函數式接口,並添加一個方法

    定義了名稱為 KiteFunction 的函數式接口,使用 @FunctionalInterface註解,然後聲明了具有兩個參數的方法 run,都是泛型類型,返回結果也是泛型。

    還有一點很重要,函數式接口中只能聲明一個可被實現的方法,你不能聲明了一個 run方法,又聲明一個 start方法,到時候編譯器就不知道用哪個接收了。而用default 關鍵字修飾的方法則沒有影響。

    @FunctionalInterface
    public interface KiteFunction<T, R, S> {
    
        /**
         * 定義一個雙參數的方法
         * @param t
         * @param s
         * @return
         */
        R run(T t,S s);
    }
    

    2. 定義一個與 KiteFunction 中 run 方法對應的方法

    在 FunctionTest 類中定義了方法 DateFormat,一個將 LocalDateTime類型格式化為字符串類型的方法。

    public class FunctionTest {
        public static String DateFormat(LocalDateTime dateTime, String partten) {
            DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(partten);
            return dateTime.format(dateTimeFormatter);
        }
    }
    

    3.用方法引用的方式調用

    正常情況下我們直接使用 FunctionTest.DateFormat()就可以了。

    而用函數式方式,是這樣的。

    KiteFunction<LocalDateTime,String,String> functionDateFormat = FunctionTest::DateFormat;
    String dateString = functionDateFormat.run(LocalDateTime.now(),"yyyy-MM-dd HH:mm:ss");
    

    而其實我可以不專門在外面定義 DateFormat這個方法,而是像下面這樣,使用匿名內部類。

    public static void main(String[] args) throws Exception {
      
        String dateString = new KiteFunction<LocalDateTime, String, String>() {
            @Override
            public String run(LocalDateTime localDateTime, String s) {
                DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(s);
                return localDateTime.format(dateTimeFormatter);
            }
        }.run(LocalDateTime.now(), "yyyy-MM-dd HH:mm:ss");
        System.out.println(dateString);
    }
    

    前面第一個 Runnable的例子也提到了,這樣的匿名內部類可以用 Lambda 表達式的形式簡寫,簡寫后的代碼如下:

    public static void main(String[] args) throws Exception {
    
            KiteFunction<LocalDateTime, String, String> functionDateFormat = (LocalDateTime dateTime, String partten) -> {
                DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(partten);
                return dateTime.format(dateTimeFormatter);
            };
            String dateString = functionDateFormat.run(LocalDateTime.now(), "yyyy-MM-dd HH:mm:ss");
            System.out.println(dateString);
    }
    

    使用(LocalDateTime dateTime, String partten) -> { } 這樣的 Lambda 表達式直接返回方法引用。

    Stream API

    為了說一下 Stream API 的使用,可以說是大費周章啊,知其然,也要知其所以然嗎,追求技術的態度和姿勢要正確。

    當然 Stream 也不只是 Lambda 表達式就厲害了,真正厲害的還是它的功能,Stream 是 Java 8 中集合數據處理的利器,很多本來複雜、需要寫很多代碼的方法,比如過濾、分組等操作,往往使用 Stream 就可以在一行代碼搞定,當然也因為 Stream 都是鏈式操作,一行代碼可能會調用好幾個方法。

    Collection接口提供了 stream()方法,讓我們可以在一個集合方便的使用 Stream API 來進行各種操作。值得注意的是,我們執行的任何操作都不會對源集合造成影響,你可以同時在一個集合上提取出多個 stream 進行操作。

    我們看 Stream 接口的定義,繼承自 BaseStream,機會所有的接口聲明都是接收方法引用類型的參數,比如 filter方法,接收了一個 Predicate類型的參數,它就是一個函數式接口,常用來作為條件比較、篩選、過濾用,JPA中也使用了這個函數式接口用來做查詢條件拼接。

    public interface Stream<T> extends BaseStream<T, Stream<T>> {
      
      Stream<T> filter(Predicate<? super T> predicate);
      
      // 其他接口
    }  
    

    下面就來看看 Stream 常用 API。

    of

    可接收一個泛型對象或可變成泛型集合,構造一個 Stream 對象。

    private static void createStream(){
        Stream<String> stringStream = Stream.of("a","b","c");
    }
    

    empty

    創建一個空的 Stream 對象。

    concat

    連接兩個 Stream ,不改變其中任何一個 Steam 對象,返回一個新的 Stream 對象。

    private static void concatStream(){
        Stream<String> a = Stream.of("a","b","c");
        Stream<String> b = Stream.of("d","e");
        Stream<String> c = Stream.concat(a,b);
    }
    

    max

    一般用於求数字集合中的最大值,或者按實體中数字類型的屬性比較,擁有最大值的那個實體。它接收一個 Comparator<T>,上面也舉到這個例子了,它是一個函數式接口類型,專門用作定義兩個對象之間的比較,例如下面這個方法使用了 Integer::compareTo這個方法引用。

    private static void max(){
        Stream<Integer> integerStream = Stream.of(2, 2, 100, 5);
        Integer max = integerStream.max(Integer::compareTo).get();
        System.out.println(max);
    }
    

    當然,我們也可以自己定製一個 Comparator,順便複習一下 Lambda 表達式形式的方法引用。

    private static void max(){
        Stream<Integer> integerStream = Stream.of(2, 2, 100, 5);
        Comparator<Integer> comparator =  (x, y) -> (x.intValue() < y.intValue()) ? -1 : ((x.equals(y)) ? 0 : 1);
        Integer max = integerStream.max(comparator).get();
        System.out.println(max);
    }
    

    min

    與 max 用法一樣,只不過是求最小值。

    findFirst

    獲取 Stream 中的第一個元素。

    findAny

    獲取 Stream 中的某個元素,如果是串行情況下,一般都會返回第一個元素,并行情況下就不一定了。

    count

    返回元素個數。

    Stream<String> a = Stream.of("a", "b", "c");
    long x = a.count();
    

    peek

    建立一個通道,在這個通道中對 Stream 的每個元素執行對應的操作,對應 Consumer<T>的函數式接口,這是一個消費者函數式接口,顧名思義,它是用來消費 Stream 元素的,比如下面這個方法,把每個元素轉換成對應的大寫字母並輸出。

    private static void peek() {
        Stream<String> a = Stream.of("a", "b", "c");
        List<String> list = a.peek(e->System.out.println(e.toUpperCase())).collect(Collectors.toList());
    }
    

    forEach

    和 peek 方法類似,都接收一個消費者函數式接口,可以對每個元素進行對應的操作,但是和 peek 不同的是,forEach 執行之後,這個 Stream 就真的被消費掉了,之後這個 Stream 流就沒有了,不可以再對它進行後續操作了,而 peek操作完之後,還是一個可操作的 Stream 對象。

    正好藉著這個說一下,我們在使用 Stream API 的時候,都是一串鏈式操作,這是因為很多方法,比如接下來要說到的 filter方法等,返回值還是這個 Stream 類型的,也就是被當前方法處理過的 Stream 對象,所以 Stream API 仍然可以使用。

    private static void forEach() {
        Stream<String> a = Stream.of("a", "b", "c");
        a.forEach(e->System.out.println(e.toUpperCase()));
    }
    

    forEachOrdered

    功能與 forEach是一樣的,不同的是,forEachOrdered是有順序保證的,也就是對 Stream 中元素按插入時的順序進行消費。為什麼這麼說呢,當開啟并行的時候,forEachforEachOrdered的效果就不一樣了。

    Stream<String> a = Stream.of("a", "b", "c");
    a.parallel().forEach(e->System.out.println(e.toUpperCase()));
    

    當使用上面的代碼時,輸出的結果可能是 B、A、C 或者 A、C、B或者A、B、C,而使用下面的代碼,則每次都是 A、 B、C

    Stream<String> a = Stream.of("a", "b", "c");
    a.parallel().forEachOrdered(e->System.out.println(e.toUpperCase()));
    

    limit

    獲取前 n 條數據,類似於 MySQL 的limit,只不過只能接收一個參數,就是數據條數。

    private static void limit() {
        Stream<String> a = Stream.of("a", "b", "c");
        a.limit(2).forEach(e->System.out.println(e));
    }
    

    上述代碼打印的結果是 a、b。

    skip

    跳過前 n 條數據,例如下面代碼,返回結果是 c。

    private static void skip() {
        Stream<String> a = Stream.of("a", "b", "c");
        a.skip(2).forEach(e->System.out.println(e));
    }
    

    distinct

    元素去重,例如下面方法返回元素是 a、b、c,將重複的 b 只保留了一個。

    private static void distinct() {
        Stream<String> a = Stream.of("a", "b", "c","b");
        a.distinct().forEach(e->System.out.println(e));
    }
    

    sorted

    有兩個重載,一個無參數,另外一個有個 Comparator類型的參數。

    無參類型的按照自然順序進行排序,只適合比較單純的元素,比如数字、字母等。

    private static void sorted() {
        Stream<String> a = Stream.of("a", "c", "b");
        a.sorted().forEach(e->System.out.println(e));
    }
    

    有參數的需要自定義排序規則,例如下面這個方法,按照第二個字母的大小順序排序,最後輸出的結果是 a1、b3、c6。

    private static void sortedWithComparator() {
        Stream<String> a = Stream.of("a1", "c6", "b3");
        a.sorted((x,y)->Integer.parseInt(x.substring(1))>Integer.parseInt(y.substring(1))?1:-1).forEach(e->System.out.println(e));
    }
    

    為了更好的說明接下來的幾個 API ,我模擬了幾條項目中經常用到的類似數據,10條用戶信息。

    private static List<User> getUserData() {
        Random random = new Random();
        List<User> users = new ArrayList<>();
        for (int i = 1; i <= 10; i++) {
            User user = new User();
            user.setUserId(i);
            user.setUserName(String.format("古時的風箏 %s 號", i));
            user.setAge(random.nextInt(100));
            user.setGender(i % 2);
            user.setPhone("18812021111");
            user.setAddress("無");
            users.add(user);
        }
        return users;
    }
    

    filter

    用於條件篩選過濾,篩選出符合條件的數據。例如下面這個方法,篩選出性別為 0,年齡大於 50 的記錄。

    private static void filter(){
        List<User> users = getUserData();
        Stream<User> stream = users.stream();
        stream.filter(user -> user.getGender().equals(0) && user.getAge()>50).forEach(e->System.out.println(e));
    
        /**
         *等同於下面這種形式 匿名內部類
         */
    //    stream.filter(new Predicate<User>() {
    //        @Override
    //        public boolean test(User user) {
    //            return user.getGender().equals(0) && user.getAge()>50;
    //        }
    //    }).forEach(e->System.out.println(e));
    }
    

    map

    map方法的接口方法聲明如下,接受一個 Function函數式接口,把它翻譯成映射最合適了,通過原始數據元素,映射出新的類型。

    <R> Stream<R> map(Function<? super T, ? extends R> mapper);
    

    Function的聲明是這樣的,觀察 apply方法,接受一個 T 型參數,返回一個 R 型參數。用於將一個類型轉換成另外一個類型正合適,這也是 map的初衷所在,用於改變當前元素的類型,例如將 Integer 轉為 String類型,將 DAO 實體類型,轉換為 DTO 實例類型。

    當然了,T 和 R 的類型也可以一樣,這樣的話,就和 peek方法沒什麼不同了。

    @FunctionalInterface
    public interface Function<T, R> {
    
        /**
         * Applies this function to the given argument.
         *
         * @param t the function argument
         * @return the function result
         */
        R apply(T t);
    }
    

    例如下面這個方法,應該是業務系統的常用需求,將 User 轉換為 API 輸出的數據格式。

    private static void map(){
        List<User> users = getUserData();
        Stream<User> stream = users.stream();
        List<UserDto> userDtos = stream.map(user -> dao2Dto(user)).collect(Collectors.toList());
    }
    
    private static UserDto dao2Dto(User user){
        UserDto dto = new UserDto();
        BeanUtils.copyProperties(user, dto);
        //其他額外處理
        return dto;
    }
    

    mapToInt

    將元素轉換成 int 類型,在 map方法的基礎上進行封裝。

    mapToLong

    將元素轉換成 Long 類型,在 map方法的基礎上進行封裝。

    mapToDouble

    將元素轉換成 Double 類型,在 map方法的基礎上進行封裝。

    flatMap

    這是用在一些比較特別的場景下,當你的 Stream 是以下這幾種結構的時候,需要用到 flatMap方法,用於將原有二維結構扁平化。

    1. Stream<String[]>
    2. Stream<Set<String>>
    3. Stream<List<String>>

    以上這三類結構,通過 flatMap方法,可以將結果轉化為 Stream<String>這種形式,方便之後的其他操作。

    比如下面這個方法,將List<List<User>>扁平處理,然後再使用 map或其他方法進行操作。

    private static void flatMap(){
        List<User> users = getUserData();
        List<User> users1 = getUserData();
        List<List<User>> userList = new ArrayList<>();
        userList.add(users);
        userList.add(users1);
        Stream<List<User>> stream = userList.stream();
        List<UserDto> userDtos = stream.flatMap(subUserList->subUserList.stream()).map(user -> dao2Dto(user)).collect(Collectors.toList());
    }
    

    flatMapToInt

    用法參考 flatMap,將元素扁平為 int 類型,在 flatMap方法的基礎上進行封裝。

    flatMapToLong

    用法參考 flatMap,將元素扁平為 Long 類型,在 flatMap方法的基礎上進行封裝。

    flatMapToDouble

    用法參考 flatMap,將元素扁平為 Double 類型,在 flatMap方法的基礎上進行封裝。

    collection

    在進行了一系列操作之後,我們最終的結果大多數時候並不是為了獲取 Stream 類型的數據,而是要把結果變為 List、Map 這樣的常用數據結構,而 collection就是為了實現這個目的。

    就拿 map 方法的那個例子說明,將對象類型進行轉換后,最終我們需要的結果集是一個 List<UserDto >類型的,使用 collect方法將 Stream 轉換為我們需要的類型。

    下面是 collect接口方法的定義:

    <R, A> R collect(Collector<? super T, A, R> collector);
    

    下面這個例子演示了將一個簡單的 Integer Stream 過濾出大於 7 的值,然後轉換成 List<Integer>集合,用的是 Collectors.toList()這個收集器。

    private static void collect(){
        Stream<Integer> integerStream = Stream.of(1,2,5,7,8,12,33);
        List<Integer> list = integerStream.filter(s -> s.intValue()>7).collect(Collectors.toList());
    }
    

    很多同學表示看不太懂這個 Collector是怎麼一個意思,來,我們看下面這段代碼,這是 collect的另一個重載方法,你可以理解為它的參數是按順序執行的,這樣就清楚了,這就是個 ArrayList 從創建到調用 addAll方法的一個過程。

    private static void collect(){
        Stream<Integer> integerStream = Stream.of(1,2,5,7,8,12,33);
        List<Integer> list = integerStream.filter(s -> s.intValue()>7).collect(ArrayList::new, ArrayList::add,
                ArrayList::addAll);
    }
    

    我們在自定義 Collector的時候其實也是這個邏輯,不過我們根本不用自定義, Collectors已經為我們提供了很多拿來即用的收集器。比如我們經常用到Collectors.toList()Collectors.toSet()Collectors.toMap()。另外還有比如Collectors.groupingBy()用來分組,比如下面這個例子,按照 userId 字段分組,返回以 userId 為key,List 為value 的 Map,或者返回每個 key 的個數。

    // 返回 userId:List<User>
    Map<String,List<User>> map = user.stream().collect(Collectors.groupingBy(User::getUserId));
    
    // 返回 userId:每組個數
    Map<String,Long> map = user.stream().collect(Collectors.groupingBy(User::getUserId,Collectors.counting()));
    

    toArray

    collection是返回列表、map 等,toArray是返回數組,有兩個重載,一個空參數,返回的是 Object[]

    另一個接收一個 IntFunction<R>類型參數。

    @FunctionalInterface
    public interface IntFunction<R> {
    
        /**
         * Applies this function to the given argument.
         *
         * @param value the function argument
         * @return the function result
         */
        R apply(int value);
    }
    

    比如像下面這樣使用,參數是 User[]::new也就是new 一個 User 數組,長度為最後的 Stream 長度。

    private static void toArray() {
        List<User> users = getUserData();
        Stream<User> stream = users.stream();
        User[] userArray = stream.filter(user -> user.getGender().equals(0) && user.getAge() > 50).toArray(User[]::new);
    }
    

    reduce

    它的作用是每次計算的時候都用到上一次的計算結果,比如求和操作,前兩個數的和加上第三個數的和,再加上第四個數,一直加到最後一個數位置,最後返回結果,就是 reduce的工作過程。

    private static void reduce(){
        Stream<Integer> integerStream = Stream.of(1,2,5,7,8,12,33);
        Integer sum = integerStream.reduce(0,(x,y)->x+y);
        System.out.println(sum);
    }
    

    另外 Collectors好多方法都用到了 reduce,比如 groupingByminBymaxBy等等。

    并行 Stream

    Stream 本質上來說就是用來做數據處理的,為了加快處理速度,Stream API 提供了并行處理 Stream 的方式。通過 users.parallelStream()或者users.stream().parallel() 的方式來創建并行 Stream 對象,支持的 API 和普通 Stream 幾乎是一致的。

    并行 Stream 默認使用 ForkJoinPool線程池,當然也支持自定義,不過一般情況下沒有必要。ForkJoin 框架的分治策略與并行流處理正好契合。

    雖然并行這個詞聽上去很厲害,但並不是所有情況使用并行流都是正確的,很多時候完全沒這個必要。

    什麼情況下使用或不應使用并行流操作呢?

    1. 必須在多核 CPU 下才使用并行 Stream,聽上去好像是廢話。
    2. 在數據量不大的情況下使用普通串行 Stream 就可以了,使用并行 Stream 對性能影響不大。
    3. CPU 密集型計算適合使用并行 Stream,而 IO 密集型使用并行 Stream 反而會更慢。
    4. 雖然計算是并行的可能很快,但最後大多數時候還是要使用 collect合併的,如果合併代價很大,也不適合用并行 Stream。
    5. 有些操作,比如 limit、 findFirst、forEachOrdered 等依賴於元素順序的操作,都不適合用并行 Stream。

    最後

    Java 25 周歲了,有多少同學跟我一樣在用 Java 8,還有多少同學再用更早的版本,請說出你的故事。

    壯士且慢,先給點個贊吧,總是被白嫖,身體吃不消!

    我是風箏,公眾號「古時的風箏」。一個兼具深度與廣度的程序員鼓勵師,一個本打算寫詩卻寫起了代碼的田園碼農!你可選擇現在就關注我,或者看看歷史文章再關注也不遲。

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

    【其他文章推薦】

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

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

    ※回頭車貨運收費標準

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

    ※超省錢租車方案

  • 日企研發寒天製的透明可食薄膜 成塑膠減量新選擇

    文:宋瑞文

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

    【其他文章推薦】

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

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

    ※台北網頁設計公司全省服務真心推薦

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

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

  • MySql輕鬆入門系列——第二站 使用visual studio 對mysql進行源碼級調試

    MySql輕鬆入門系列——第二站 使用visual studio 對mysql進行源碼級調試

    一:背景

    1. 講故事

    上一篇說了mysql的架構圖,很多同學反饋說不過癮,畢竟還是聽我講故事,那這篇就來說一說怎麼利用visual studio 對 mysql進行源碼級調試,畢竟源碼面前,不談隱私,聖人面前,皆為螻蟻。

    二:工具合集

    mysql是C++寫的,要想在windows上編譯,還需要下載幾個必備小工具。

    • mysql-5.7.12.zip
    • cmake-3.17.3-win64-x64.msi
    • boost_1_59_0.tar.gz
    • bison-2.4.1-setup.exe
    • windows 10 x64

    這裏簡單說一下:可以用 cmake 將源碼生成 *.sln 可打開的解決方案,比如可以通過它最終生成 MySQL.sln。boost 是C++中非常強大的基礎庫,bison 一個流行的語法分析器程序,用於給mysql提供語法分析,最後就是下載正確的mysql版本5.7.12。

    三. 詳細安裝

    我會寫的比較細,畢竟我也花了一下午時間,寒酸(┬_┬)

    1. cmake-3.17.3-win64-x64.msi 和 bison-2.4.1-setup.exe

    cmake 和 bison 安裝起來比較方便,一鍵安裝就可以了,不過這裡有一個大坑注意了,在安裝Bison的時候,千萬不要使用默認路徑,因為默認路徑有空格,會導致你後面vs編譯的時候卡住,又不显示什麼原因,可氣!!! 所以我換成自定義的: C:\2\GnuWin32。

    最後確保 cmake 和 bison 的bin文件都在 環境變量中即可。

    2. mysql-5.7.12.zip

    這裏我用 C:\2作為根文件夾,所有的小工具都在這裏,如圖:

    接下來將 mysql-5.7.12.zip 解壓一下,然後進入解壓后的文件夾,新建一個boost文件夾,將boost_1_59_0.tar.gz放入其中,然後再新建一個 brelease 文件夾可用於存放最終生成的MySql.sln。。

    3. cmake編譯

    都準備好了之後,可以開始cmake編譯了。

    
    PS C:\2\mysql-5.7.12\brelease> cmake ..  -DDOWNLOAD_BOOST=1 -DWITH_BOOST="C:\2\mysql-5.7.12\boost\boost_1_59_0.tar.gz"
    -- Building for: Visual Studio 16 2019
    CMake Deprecation Warning at CMakeLists.txt:26 (CMAKE_POLICY):
      The OLD behavior for policy CMP0018 will be removed from a future version
      of CMake.
    -- Cannot find wix 3, installer project will not be generated
    -- COMPILE_DEFINITIONS: _WIN32_WINNT=0x0601;WIN32_LEAN_AND_MEAN;NOGDI;NOMINMAX;HAVE_CONFIG_H
    -- CMAKE_C_FLAGS: /DWIN32 /D_WINDOWS /W3 /MP /wd4800 /wd4805 /wd4996
    -- CMAKE_CXX_FLAGS: /DWIN32 /D_WINDOWS /W3 /GR /EHsc /MP /wd4800 /wd4805 /wd4996 /we4099
    -- CMAKE_C_FLAGS_DEBUG: /MTd /Z7 /Ob1 /Od /RTC1 /EHsc -DENABLED_DEBUG_SYNC -DSAFE_MUTEX
    -- CMAKE_CXX_FLAGS_DEBUG: /MTd /Z7 /Ob1 /Od /RTC1 /EHsc -DENABLED_DEBUG_SYNC -DSAFE_MUTEX
    -- CMAKE_C_FLAGS_RELWITHDEBINFO: /MT /Z7 /O2 /Ob1 /DNDEBUG /EHsc -DDBUG_OFF
    -- CMAKE_CXX_FLAGS_RELWITHDEBINFO: /MT /Z7 /O2 /Ob1 /DNDEBUG /EHsc -DDBUG_OFF
    -- Configuring done
    -- Generating done
    -- Build files have been written to: C:/2/mysql-5.7.12/brelease
    
    

    當看到最後一句 Build files have been written to: C:/2/mysql-5.7.12/brelease,恭喜你,MySQL.sln生成好了。

    4. 打開 MySQL.sln 編譯項目

    我的電腦安裝的是visual studio 2019,接下來打開MySql.Sln整體編譯,需要等個十幾分鐘,看到下面的輸出就算安裝成功。

    三: 啟動mysql並調試insert

    1. mysql的初始化

    這裏要做兩件事情,第一件事是將mysql的調試模式打開,第二件事就是附加 --initialize 啟動參數。

    <1> mysql 調試模式打開

    修改C:\2\mysql-5.7.12\sql\mysqld.cc中的 test_lc_time_sz方法中的 DBUG_ASSERT(0); 改成 DBUG_ASSERT(1); 如下圖:

    <2> vs的command增加啟動參數

    上一篇大家都知道了,mysqld項目是mysql的啟動項目,main函數也在其中,在F5調試之前增加初始化參數 --console --initialize,如下圖:

    2. 繼續入坑出坑

    啟動之後,有103個報錯,氣人呀。。。看錯誤信息應該是編碼問題,如下圖:

    修改起來也很簡單,將 C:\2\mysql-5.7.12\sqlsql_locale.cc 用 [utf-8 + BOM] 格式保存一下,然後對mysqld項目Rebuild再Ctrl+F5直接運行,終於謝天謝地,從輸出可以看到,搞定啦。。。太不容易啦。

    從上圖中可以看到,默認密碼是:zJDE>IC5o+ya,先記錄下這個密碼,然後再把CommandLine Arguments 中的–initialize去掉再重啟Console。

    可以看到,3306端口已開啟,然後用剛才的 zJDE>IC5o+ya 連接即可,這裏我使用navicat。

    連接上去後會提示修改默認密碼,設置我就設置為:123456 ,嘿嘿,一切搞定~~~

    3. 繼續追蹤 write_row

    上一篇我們追蹤到了 write_row 就斷掉了,我當時說它是一個虛方法,由底層具體的存儲引擎去調用,代碼如下:

    
    int handler::ha_write_row(uchar *buf)
    {
        MYSQL_TABLE_IO_WAIT(m_psi, PSI_TABLE_WRITE_ROW, MAX_KEY, 0,{ error= write_row(buf); })
    }
    
    //這是一個虛方法
    virtual int write_row(uchar *buf __attribute__((unused)))
    {
        return HA_ERR_WRONG_COMMAND;
    }
    
    

    到底這話虛不虛,這次我親自調試一下給大家看看,證據先行哈。。。為了方便,我生成一條創表sql。

    
    drop database if exists `datamip`;
    create database `datamip`;
    drop table if exists `datamip`.`customer`;
    create table `datamip`.`customer` (
     `customerID` int NOT NULL AUTO_INCREMENT COMMENT '自增主鍵',
     `customerName` varchar(50) COMMENT '用戶姓名',
     `email` varchar(50) COMMENT '郵箱地址',
     `desc` varchar(50) COMMENT '描述',
     primary key (`customerID`)
    ) ENGINE=InnoDB charset=utf8 collate=utf8_bin;
    
    

    接下來,大家看仔細了,在源碼 int handler::ha_write_row(uchar *buf) 方法處下一個斷點,然後F5調試應用程序。

    接下來可以執行insert操作,這地方會命中斷點的。

    
    insert into  `datamip`.`customer`(customerName,email,`desc`) values('mary','123456789@qq.com','vip');
    
    

    可以看到,斷點命中了,然後進行單步調試,最終你會看到代碼會進入到 C:\2\mysql-5.7.12\storage\innobase\handler\ha_innodb.cc中的 int ha_innobase::write_row 方法,如下圖:

    然後找幾個局部變量和調用堆棧看看。。。

    四: 總結

    這就是我花了一下午的時間總結出的進坑出坑指南,希望能幫助大家節省時間,還是那句話,源碼面前,不談隱私,若還能進行調試,那一切皆為螻蟻!

    如您有更多問題與我互動,掃描下方進來吧~

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

    【其他文章推薦】

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

    台北網頁設計公司這麼多該如何選擇?

    ※智慧手機時代的來臨,RWD網頁設計為架站首選

    ※評比南投搬家公司費用收費行情懶人包大公開

    ※回頭車貨運收費標準

  • 心痛!抹香鯨遭漁網纏住痛苦掙扎 潛水員花3天仍割不完

    摘錄自2020年7月22日自由時報報導

    義大利當地時間18日,一條抹香鯨在利帕里島(Lipari)附近海域被廢棄漁網纏住,義大利海警花了數天時間企圖割開漁網,但由於抹香鯨躁動,導致進度緩慢。

    綜合外媒報導,一隻抹香鯨被發現受困於義大利利帕里島海域,牠被廢棄漁網困住無法游離,義大利海警獲報後,即刻出動潛水員救援,他們企圖割開漁網,但由於抹香鯨情緒不穩,相當躁動,令潛水員相當困擾,也因此把牠命為「Fury」(憤怒之意),潛水員花了3天時間才將部分漁網割下。

    不料恢復行動力的「Fury」開始下潛失去蹤影,儘管纏在「Fury」身上的大部分漁網已經割開,不過牠的尾巴仍有魚網纏繞住,救難人員正積極尋找其下落。

    生物多樣性
    海洋
    國際新聞
    抹香鯨
    漁網

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

    【其他文章推薦】

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

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

    ※台北網頁設計公司全省服務真心推薦

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

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

  • 【Spring註解驅動開發】使用@Import註解給容器中快速導入一個組件

    寫在前面

    我們可以將一些bean組件交由Spring管理,並且Spring支持單實例bean和多實例bean。我們自己寫的類,可以通過包掃描+標註註解(@Controller、@Servcie、@Repository、@Component)的形式將其註冊到IOC容器中,如果不是我們自己寫的類,比如,我們在項目中引入了一些第三方的類庫,此時,我們需要將這些第三方類庫中的類註冊到Spring容器中,該怎麼辦呢?此時,我們就可以使用@Bean和@Import註解將這些類快速的導入Spring容器中。接下來,我們來一起探討下如何使用@Import註解給容器中快速導入一個組件。

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

    註冊bean的方式

    向Spring容器中註冊bean通常有以下幾種方式:

    • 包掃描+標註註解(@Controller、@Servcie、@Repository、@Component),通常用於自己寫的類。
    • @Bean註解,通常用於導入第三方包中的組件。
    • @Import註解,快速向Spring容器中導入組件。

    @Import註解概述

    Spring 3.0之前,創建Bean可以通過xml配置文件與掃描特定包下面的類來將類注入到Spring IOC容器內。而在Spring 3.0之後提供了JavaConfig的方式,也就是將IOC容器里Bean的元信息以java代碼的方式進行描述。我們可以通過@Configuration與@Bean這兩個註解配合使用來將原來配置在xml文件里的bean通過java代碼的方式進行描述

    @Import註解提供了@Bean註解的功能,同時還有xml配置文件里 標籤組織多個分散的xml文件的功能,當然在這裡是組織多個分散的@Configuration

    先看一下@Import註解的源碼:

    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface Import {
        /**
          * {@link Configuration}, {@link ImportSelector}, {@link ImportBeanDefinitionRegistrar}
          * or regular component classes to import.
          */
         Class<?>[] value();
    }
    

    從源碼里可以看出@Import可以配合 Configuration , ImportSelector, ImportBeanDefinitionRegistrar 來使用,下面的or表示也可以把Import當成普通的Bean使用。

    @Import只允許放到類上面,不能放到方法上。下面我們來看具體的使用方式。

    @Import註解的使用方式

    @Import註解的三種用法主要包括:

    • 直接填class數組方式
    • ImportSelector方式【重點】
    • ImportBeanDefinitionRegistrar方式

    注意:我們先來看第一種方法:直接填class數組的方式,其他的兩種方式我們後面繼續講。

    @Import導入組件的簡單示例

    沒有使用@Import註解的效果

    首先,我們創建一個Department類,這個類是一個空類,沒有成員變量和方法,如下所示。

    package io.mykit.spring.plugins.register.bean;
    
    /**
     * @author binghe
     * @version 1.0.0
     * @description 測試@Import註解的bean
     */
    public class Department {
    }
    

    接下來,我們先在SpringBeanTest類中創建testAnnotationConfig7()方法,輸出Spring容器中所有的bean,來查看是否存在Department類對應的bean實例,以此來判斷Spring容器中是否註冊有Department類對應的bean實例。

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

    運行SpringBeanTest類的testAnnotationConfig7()方法,輸出的結果信息如下所示。

    org.springframework.context.annotation.internalConfigurationAnnotationProcessor
    org.springframework.context.annotation.internalAutowiredAnnotationProcessor
    org.springframework.context.annotation.internalCommonAnnotationProcessor
    org.springframework.context.event.internalEventListenerProcessor
    org.springframework.context.event.internalEventListenerFactory
    personConfig2
    person
    binghe001
    

    可以看到Spring容器中並沒有Department類對應的bean實例。

    使用@Import註解的效果

    我們在PersonConfig2類上添加@Import註解,並將Department類標註到註解中,如下所示。

    @Configuration
    @Import(Department.class)
    public class PersonConfig2 {
    

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

    org.springframework.context.annotation.internalConfigurationAnnotationProcessor
    org.springframework.context.annotation.internalAutowiredAnnotationProcessor
    org.springframework.context.annotation.internalCommonAnnotationProcessor
    org.springframework.context.event.internalEventListenerProcessor
    org.springframework.context.event.internalEventListenerFactory
    personConfig2
    io.mykit.spring.plugins.register.bean.Department
    person
    binghe001
    

    可以看到,輸出結果中打印了io.mykit.spring.plugins.register.bean.Department,說明使用@Import導入bean時,id默認是組件的全類名。

    @Import註解支持同時導入多個類,例如,我們再次創建一個Employee類,如下所示。

    package io.mykit.spring.plugins.register.bean;
    /**
     * @author binghe
     * @version 1.0.0
     * @description 測試@Import註解的bean
     */
    public class Employee {
    }
    

    接下來,我們也將Employee類添加到@Import註解中,如下所示。

    @Configuration
    @Import({Department.class, Employee.class})
    public class PersonConfig2 {
    

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

    org.springframework.context.annotation.internalConfigurationAnnotationProcessor
    org.springframework.context.annotation.internalAutowiredAnnotationProcessor
    org.springframework.context.annotation.internalCommonAnnotationProcessor
    org.springframework.context.event.internalEventListenerProcessor
    org.springframework.context.event.internalEventListenerFactory
    personConfig2
    io.mykit.spring.plugins.register.bean.Department
    io.mykit.spring.plugins.register.bean.Employee
    person
    binghe001
    

    可以看到,結果信息中同時輸出了io.mykit.spring.plugins.register.bean.Department和io.mykit.spring.plugins.register.bean.Employee,說明Department類的bean實例和Employee類的bean實例都導入到Spring容器中了。

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

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

    寫在最後

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

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

    【其他文章推薦】

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

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

    ※台北網頁設計公司全省服務真心推薦

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

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

  • 不到15萬,軸距超過3米還有誰!

    不到15萬,軸距超過3米還有誰!

    而依維柯的中控 則採用了深灰色的內飾配色,整體看起來略微有些沉悶。並且空調的出風口以及手套箱的設計相對過於緊湊,讓人感覺整个中控台略微有些凌亂。四輻式的方向盤造型也難以提升駕駛員的駕駛興趣。馬兒跑得快還要不吃草作為全新一代輕客的代表之作,光有光鮮的外表、時尚的造型可不夠,選擇這類車型的消費者們更為看重的必然是它們的內在能力。

    拼家底誰更厚?

    全順:全順在輕客領域摸爬滾打也有將近五十年歷史了。它的原型車可以追溯到1953年福特生產的一款輕型商用車FK1000,它於1961被稱作Ford Taunus Transit。1965年,福特汽車英國公司推出第一代福特Transit全順,迅速取得了輕型貨車領域的主導地位,並在此後一直霸佔着歐洲輕型客貨車銷量冠軍,成為了輕型客貨車的代名詞。

    依維柯:作為歐洲的輕型商用車之一,依維柯在歐洲市場佔據很大的市場份額。在1975年的時候依維柯公司正式成立,1978年第一代依維柯Daily誕生。並且一直發展到今日,依維柯一共經歷了六次換代。

    誰才是真正的“顏值帝”

    外觀設計各花入各眼,但就目前的眼光來看,新全順外觀設計相比起老款可謂是翻天覆地,時尚動感的外型設計,微微收緊的車頭、熏黑的大燈以及一條筆直斜向上一直延伸到車尾的線條設計,彷彿讓新全順一躍跳出了輕客這個領域。

    而依維柯則是傳統的造型設計,菱形的前大燈以及方方正正的車頭設計讓它很難與時尚設計扯上關係,保持了輕客一貫的傳統印象,但沒能給人眼前一亮的感覺。

    內飾誰更前衛

    新全順的內飾設計採用了福特最新家族的設計語言,非對稱式的中控設計,外加黑色內飾加銀色鍍鉻配色,整體顯得更有檔次感,中控面板和門把手對於細節的處理也非常細緻,非常符合新全順的車型定位。

    而依維柯的中控 則採用了深灰色的內飾配色,整體看起來略微有些沉悶。並且空調的出風口以及手套箱的設計相對過於緊湊,讓人感覺整个中控台略微有些凌亂。四輻式的方向盤造型也難以提升駕駛員的駕駛興趣。

    馬兒跑得快還要不吃草

    作為全新一代輕客的代表之作,光有光鮮的外表、時尚的造型可不夠,選擇這類車型的消費者們更為看重的必然是它們的內在能力。就這方面來看,全順的表現還是極為優秀的。從動力上看新全順所搭載的2.0T柴油渦輪增壓發動機雖在排量上比起依維柯的2.5T渦輪發動機稍顯劣勢,但由於全順採用了新的渦輪技術使得在排量吃虧的情況下,依然在能在功率上追上對手,並且在扭矩上還略微有優勢。

    江鈴福特-全順

    南京依維柯-power Daily

    【聽宣判pK結果】:兩位老對手的定位雖然相近,但是結合兩輛車的表現,新全順更加出色,在各個項目的表現上都要更勝一籌。而依維柯則保留了純粹的商用車氣息,缺乏了一點新時代的便利性以及通用性。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

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

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

    ※台北網頁設計公司全省服務真心推薦

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

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

  • 【Java Spring Cloud 實戰之路】添加一個SpringBootAdmin監控

    【Java Spring Cloud 實戰之路】添加一個SpringBootAdmin監控

    0. 前言

    在之前的幾章中,我們先搭建了一個項目骨架,又搭建了一個使用nacos的gateway網關項目,網關項目中並沒有配置太多的東西。現在我們就接着搭建在Spring Cloud 微服務中另一個重要的項目 – Spring boot admin.

    1. Spring Boot Admin 介紹

    Spring Boot Admin 用來監控基於Spring Boot的應用,在Spring Boot Actuator的基礎上提供了簡潔的可視化Web UI。Spring Boot Admin 提供了以下功能:

    • 显示應用的健康狀態
    • 显示應用的細節內容: JVM和內存信息,micrometer信息, 數據源信息,緩存信息等
    • 显示 編譯版本
    • 查看和下載日誌
    • 查看jvm參數和環境變量值
    • 查看Spring Boot項目配置
    • 显示 thread dump
    • 显示 http-traces

    ……

    等一系列內容。

    2. 創建一個 Spring Boot Admin項目

    那麼,我們就來創建一個Spring Boot Admin 項目吧。

    2.1 創建 Spring Boot Admin 服務端

    在manager 目錄下,創建一個 monitor目錄,並在monitor目錄下創建一個pom.xml 文件,添加以下內容:

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
    
        <artifactId>monitor</artifactId>
        <version>${revision}</version>
        <packaging>jar</packaging>
        <parent>
            <artifactId>manager</artifactId>
            <groupId>club.attachie</groupId>
            <version>${revision}</version>
        </parent>
    
    </project>
    

    在 manager/pom.xml 註冊我們新建的項目模塊:

    <modules>
        <module>gateway</module>
        <module>monitor</module>
    </modules>
    

    在 monitor 創建如下目錄:

    .
    ├── pom.xml
    └── src
        └── main
            ├── java
            └── resources
    

    在根目錄的pom.xml 添加 Spring Boot Admin 依賴:

    先添加spring-boot-admin版本號變量:

    <spring-boot-admin.version>2.2.3</spring-boot-admin.version>
    

    並在dependencyManagement > dependencies 下添加:

    <dependency>
        <groupId>de.codecentric</groupId>
        <artifactId>spring-boot-admin-starter-server</artifactId>
        <version>${spring-boot-admin.version}</version>
    </dependency>
    

    在monitor/pom.xml文件中添加:

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>de.codecentric</groupId>
            <artifactId>spring-boot-admin-starter-server</artifactId>
        </dependency>
    </dependencies>
    

    運行

    mvn clean install

    檢查並刷mvn引用緩存。

    創建MonitorApplication類:

    package club.attachie.nature.monitor;
    
    import de.codecentric.boot.admin.server.config.EnableAdminServer;
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    
    @SpringBootApplication
    @EnableAdminServer
    public class MonitorApplication {
        public static void main(String[] args) {
            SpringApplication.run(MonitorApplication.class, args);
        }
    }
    

    啟動后能看到如下界面:

    3 與網關服務進行互通

    在上一篇中,我們添加了Spring Cloud Gateway項目,到目前為止兩個項目之間完全割裂沒有關聯。在這一節,我們在兩者之間建立關聯。也就是說,將gateway 項目引入Spring Admin Boot監聽。

    在 manager/gateway 的pom.xml 文件中加入如下引用:

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    

    然後修改 gateway項目的啟動端口,在resources/bootstrap.yml 添加:

    server:
      port: 8070
    

    在 monitor中加入nacos引用:

    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>      
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    

    修改MonitorApplication 為:

    package club.attachie.nature.monitor;
    
    import de.codecentric.boot.admin.server.config.EnableAdminServer;
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.cloud.context.config.annotation.RefreshScope;
    
    @SpringBootApplication
    @EnableAdminServer
    @RefreshScope
    public class MonitorApplication {
        public static void main(String[] args) {
            SpringApplication.run(MonitorApplication.class, args);
        }
    }
    

    創建monitor項目的bootsrap.yml:

    spring:
      application:
        name: monitor
      
      cloud:
      	nacos:
          discovery:
            server-addr: 127.0.0.1:8848
    

    關於這裏的配置 在上一篇 中有個錯誤,應該是 discovery > server-addr,不是 config > server-addr。兩者有區別,discovery表示設置nacos為服務發現中心,config表示nacos為配置中心。

    啟動 gateway 項目和 monitor項目查看效果, 訪問 8080端口:

    可以看到兩個應用可以被發現,如果沒有設置monitor項目把nacos當做服務發現中心,將無法獲取到具體在線的應用。點擊 gateway 進去后可以查看到:

    4. 總結

    我們搭建了一個Spring Boot Admin 項目作為一個監控系統,後續會在這裏添加更多的內容。

    更多內容煩請關注我的博客《高先生小屋》

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

    【其他文章推薦】

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

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

    ※台北網頁設計公司全省服務真心推薦

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

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

  • 7.98萬起產品力不輸哈弗H6,這款SUV車主是怎樣評價的?

    7.98萬起產品力不輸哈弗H6,這款SUV車主是怎樣評價的?

    最不滿意的地方:起步有點肉,而且這個時候發動機聲音是比較大的。所以感覺上這個發動機實際動力還是比較一般的,而且發動機倉隔音也是比較一般。車主:老兵哥購買車型:北汽幻速S6 2017款 1。5T CVT樂享型裸車購買價:10。

    前言

    作為國內的造車大戶,北汽奉行着“多生孩兒多掙錢”的政策,有注重一般家用市場的北汽紳寶、有注重硬派SUV市場的北京汽車、還有着北汽幻速以及北汽比速這兩個入門品牌。那麼作為一個比較年輕的品牌,北汽幻速的口碑究竟怎樣呢?今天筆者就搜集了幾位北汽幻速S6車主的意見,這款7.98萬起售的緊湊型SUV有着該價位中不俗的競爭力。全系標配的是1.5T發動機,而且有着CVT變速箱作為自動擋的選擇。

    那麼多孩子,打起群架肯定贏

    北汽銀翔幻速S6

    官方指導價:7.98-11.68萬

    編者意見:

    性價比較高,動力表現在同價位中表現比較優秀。不過全系沒能標配ESp車身穩定系統以及电子助力轉向比較可惜。

    車主:BY2000

    購買車型:北汽幻速S6 2017款 1.5T CVT尊享型

    裸車購買價11.68 萬元

    最滿意的地方:整體都比較滿意,但是最滿意的的是價格,性價比很高。能在這個價格買到這樣配置的緊湊型SUV還是不錯的,而且是渦輪增壓發動機。

    最不滿意的地方:起步有點肉,而且這個時候發動機聲音是比較大的。所以感覺上這個發動機實際動力還是比較一般的,而且發動機倉隔音也是比較一般。

    車主:老兵哥

    購買車型:北汽幻速S6 2017款 1.5T CVT樂享型

    裸車購買價:10.68 萬元

    最滿意的地方:乘坐空間,沒有想到這個價格的車還能有着那麼大的空間,滿載的情況下也不是很擁擠。而且後備箱容積也是相當可觀,大天窗還有那麼多的配置,買這款車真的是比較值。

    最不滿意的地方:裝配工藝有待加強,有些部分的縫隙是比較大的,就如尾門的縫隙,看着很掉價,而且方向盤塑料感太強了。

    車主:smg20900

    購買車型:北汽幻速S6 2017款 1.5T CVT尊享型

    裸車購買價:10.68 萬元

    最滿意的地方:外觀,看着更像是二十多萬的SUV。能給人更多的面子,而且在動力方面感覺還是不錯的,一直都可以維持在較低轉速,120km/h時速下轉速也只是2200rpm左右,這個是最為滿意的,所以綜合油耗上也是9L左右,對於一款SUV來說是滿意了。

    最不滿意的地方:儲物空間實在是少得可憐,中間只有一個杯架,不夠用。其次是噪音的問題,不過對於如此便宜的車來說,還是可以接受的。

    編者總結:

    北汽雖然是個歷史悠長的品牌,但事實基本是為別人“代工”,自身在工藝方面以及控製成本方面還是需要向合資學習,所以在做工以及隔音用料上表現一般。不過在主要的發動機上表現卻是相當不錯,雖然渦輪遲滯現象還是有的,但是油耗表現卻是令人信服的。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

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

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

    ※台北網頁設計公司全省服務真心推薦

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

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

  • javascript 面向對象學習(三)——this,bind、apply 和 call

    this 是 js 里繞不開的話題,也是非常容易混淆的概念,今天試着把它理一理。

    this 在非嚴格模式下,總是指向一個對象,在嚴格模式下可以是任意值,本文僅考慮非嚴格模式。記住它總是指向一個對象對於理解它的意義很重要。this 在實際使用中,大致分為以下幾種情況:

    1. 函數作為對象的方法調用時,this 指向調用該函數的對象

    var obj = {
        name: 'jack',
        getName: function() {
            console.log(this === obj) // true
            console.log(this.name)  // jack
        }
    }
    obj.getName()

    這個應該很好理解,不多說了。

    2. 函數作為普通函數被調用時,this 指向全局對象。在瀏覽器中,全局對象是window。

    var name = 'global'
    function getName() {
        console.log(this === window) // true
        console.log(this.name) // global
    }
    getName()

    我的理解是上面的代碼可以改寫為

    window.name = 'global'
    window.getName = function() {
        console.log(this === window) // true
        console.log(this.name) // global
    }
    window.getName()

    這樣其實與情況1是一樣的,相當於函數作為對象的方法調用,只不過這裏的對象是全局對象。

    《Javascript 設計模式與開發實踐》一書中有個例子如下:

    window.name = 'globalName';
    var myObject = {
        name: 'seven',
        getName: function(){
            return this.name
        } 
    }
    
    var getName = myObject.getName
    console.log(getName())  // globalName

    getName 是定義在myObject 對象中的方法,在調用getName 方法時,打印出的卻是全局對象的name,而不是myObject對象的name,這再次證明了 this 並非指向函數被聲明時的環境對象,而是指向函數被調用時的環境對象

    3. 函數作為構造函數調用時,指向構造出的新對象

    function Person(name) {
        this.name = name  
    }
    
    var jack = new Person('Jack')
    console.log(jack.name) // Jack
    var rose = new Person('Rose')
    console.log(rose.name) // Rose

    這裏創建了兩個不同名字的對象,打印出的name也是不一樣的,說明構造函數的 this 會根據創建對象的不同而變化。需要注意的是,如果構造函數里返回了一個Object類型的對象,那麼this會指向這個對象,而不是利用構造函數創建出的對象。我們在構造函數一章里也提到過,new 操作符所做的最後一步就是返回新對象,而如果我們顯式地返回一個對象,就會覆蓋這步操作,this也就不再指向新對象。

    4. 函數作為事件處理函數調用時,指向觸發事件的元素

    document.getElementById("myBtn").addEventListener("click", function(e){
        console.log(this === e.currentTarget) // true
    });

    5. 箭頭函數

    由於箭頭函數沒有this,它的 this 是繼承父執行上下文裏面的 this。執行上下文後面再討論,現在只要知道簡單對象(非函數)是沒有執行上下文的。

    var obj = {
        name:  'obj',
        getName: function() {
    console.log(this) // 執行上下文里的 this
    return (()=>{ console.log(this.name) }) } } var fn = obj.getName() fn() // obj

    按照情況2來處理的話,this 指向全局對象,應該輸出 undefined,結果並不是。與普通函數不同,箭頭函數的 this 是在函數被聲明時決定的,而不是函數被調用時。在這裏,父執行上下文是 getName 函數,也就繼承了 getName 的 this,即 obj。

    利用 bind、apply、call 改變 this 指向

    bind、apply、call 都是定義在 Function 原型對象上的方法,所有函數對象都能繼承這個方法,三者都能用來改變 this 指向,我們來看看它們的聯繫與區別。

    function fn() {
        console.log(this.name)
    }
    
    // bind
    var bindfn = fn.bind({name: 'bind'})
    bindfn() // bind // apply
    fn.apply({name: 'apply'}) // apply // call
    fn.call({name: 'call'}) // call

    我們定義了一個函數fn,然後分別調用了它的 bind、apply、call 方法,並傳入一個對象參數,通過打印出的內容可以看到 this 被綁定到了參數對象上。bind 似乎有些不同,多了一步 bindfn() 調用,這是因為 bind 方法返回的是一個函數,不會立即執行,而調用 apply 和 call 方法會立即執行。

    下面再來看一下 fn 函數存在參數的情況:

    function fn(a, b, c) {
        console.log(a, b, c)
    }
    
    var bindfn = fn.bind(null, 'bind');
    bindfn('A', 'B', 'C');           // bind A B
    
    fn.apply(null, ['apply', 'A']) // apply A undefined
    
    fn.call(null, 'call', 'A');  // bind A undefined

    bindfn 打印出的結果是fn調用bind方法時的傳遞的參數加上bindfn傳遞的參數,參數 ‘C’ 被捨棄掉了。調用 apply 和 call 方法打印出的則是傳遞給它們的參數,不一樣的是,apply 的參數是一個數組(或類數組),call 則是把參數依次傳入函數。這時候再看它們的定義應該會好理解很多:

    bind() 方法創建一個新的函數,在 bind() 被調用時,這個新函數的 this 被指定為 bind() 的第一個參數,而其餘參數將作為新函數的參數,供調用時使用。

    apply() 方法調用一個具有給定 this 值的函數,以及作為一個數組(或類數組對象)提供的參數。

    call() 方法使用一個指定的 this 值和單獨給出的一個或多個參數來調用一個函數。

    我們可以利用它們來借用其他對象的方法。已知函數的參數列表 arguments 是一個類數組對象,比如上例中函數 fn 的參數 a, b, c,因為它不是一個真正的數組,不能調用數組方法,這時借用 apply/call 方法(bind 也可以,就是用得比較少)將 this 指向 arguments 就能借用數組方法:

    (function(){
        Array.prototype.push.call(arguments, 'c')
        console.log(arguments) // ['a', 'b', 'c']
    })('a','b')

    值得一提的是,push 方法並不是只有數組才能調用,一個對象只要滿足1.可讀寫 length 屬性;2.對象本身可存取屬性. 就可以利用 call / apply 調用 push 方法。

     

    參考:

    《Javascript 設計模式與開發實踐》

    https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/this

    http://www.imooc.com/article/80117

    https://blog.csdn.net/weixin_42519137/article/details/88053339

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

    【其他文章推薦】

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

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

    ※台北網頁設計公司全省服務真心推薦

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

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