帶你漲姿勢的認識一下 Kafka 消費者

之前我們介紹過了 Kafka 整體架構,Kafka 生產者,Kafka 生產的消息最終流向哪裡呢?當然是需要消費了,要不只產生一系列數據沒有任何作用啊,如果把 Kafka 比作餐廳的話,那麼生產者就是廚師的角色,消費者就是客人,只有廚師的話,那麼炒出來的菜沒有人吃也沒有意義,如果只有客人沒有廚師的話,誰會去這個店吃飯呢?!所以如果你看完前面的文章意猶未盡的話,可以繼續讓你爽一爽。如果你沒看過前面的文章,那就從現在開始讓你爽。

Kafka 消費者概念

應用程序使用 KafkaConsumer 從 Kafka 中訂閱主題並接收來自這些主題的消息,然後再把他們保存起來。應用程序首先需要創建一個 KafkaConsumer 對象,訂閱主題並開始接受消息,驗證消息並保存結果。一段時間后,生產者往主題寫入的速度超過了應用程序驗證數據的速度,這時候該如何處理?如果只使用單個消費者的話,應用程序會跟不上消息生成的速度,就像多個生產者像相同的主題寫入消息一樣,這時候就需要多個消費者共同參与消費主題中的消息,對消息進行分流處理。

Kafka 消費者從屬於消費者群組。一個群組中的消費者訂閱的都是相同的主題,每個消費者接收主題一部分分區的消息。下面是一個 Kafka 分區消費示意圖

上圖中的主題 T1 有四個分區,分別是分區0、分區1、分區2、分區3,我們創建一個消費者群組1,消費者群組中只有一個消費者,它訂閱主題T1,接收到 T1 中的全部消息。由於一個消費者處理四個生產者發送到分區的消息,壓力有些大,需要幫手來幫忙分擔任務,於是就演變為下圖

這樣一來,消費者的消費能力就大大提高了,但是在某些環境下比如用戶產生消息特別多的時候,生產者產生的消息仍舊讓消費者吃不消,那就繼續增加消費者。

如上圖所示,每個分區所產生的消息能夠被每個消費者群組中的消費者消費,如果向消費者群組中增加更多的消費者,那麼多餘的消費者將會閑置,如下圖所示

向群組中增加消費者是橫向伸縮消費能力的主要方式。總而言之,我們可以通過增加消費組的消費者來進行水平擴展提升消費能力。這也是為什麼建議創建主題時使用比較多的分區數,這樣可以在消費負載高的情況下增加消費者來提升性能。另外,消費者的數量不應該比分區數多,因為多出來的消費者是空閑的,沒有任何幫助。

Kafka 一個很重要的特性就是,只需寫入一次消息,可以支持任意多的應用讀取這個消息。換句話說,每個應用都可以讀到全量的消息。為了使得每個應用都能讀到全量消息,應用需要有不同的消費組。對於上面的例子,假如我們新增了一個新的消費組 G2,而這個消費組有兩個消費者,那麼就演變為下圖這樣

在這個場景中,消費組 G1 和消費組 G2 都能收到 T1 主題的全量消息,在邏輯意義上來說它們屬於不同的應用。

總結起來就是如果應用需要讀取全量消息,那麼請為該應用設置一個消費組;如果該應用消費能力不足,那麼可以考慮在這個消費組裡增加消費者

消費者組和分區重平衡

消費者組是什麼

消費者組(Consumer Group)是由一個或多個消費者實例(Consumer Instance)組成的群組,具有可擴展性和可容錯性的一種機制。消費者組內的消費者共享一個消費者組ID,這個ID 也叫做 Group ID,組內的消費者共同對一個主題進行訂閱和消費,同一個組中的消費者只能消費一個分區的消息,多餘的消費者會閑置,派不上用場。

我們在上面提到了兩種消費方式

  • 一個消費者群組消費一個主題中的消息,這種消費模式又稱為點對點的消費方式,點對點的消費方式又被稱為消息隊列
  • 一個主題中的消息被多個消費者群組共同消費,這種消費模式又稱為發布-訂閱模式

消費者重平衡

我們從上面的消費者演變圖中可以知道這麼一個過程:最初是一個消費者訂閱一個主題並消費其全部分區的消息,後來有一個消費者加入群組,隨後又有更多的消費者加入群組,而新加入的消費者實例分攤了最初消費者的部分消息,這種把分區的所有權通過一個消費者轉到其他消費者的行為稱為重平衡,英文名也叫做 Rebalance 。如下圖所示

重平衡非常重要,它為消費者群組帶來了高可用性伸縮性,我們可以放心的添加消費者或移除消費者,不過在正常情況下我們並不希望發生這樣的行為。在重平衡期間,消費者無法讀取消息,造成整個消費者組在重平衡的期間都不可用。另外,當分區被重新分配給另一個消費者時,消息當前的讀取狀態會丟失,它有可能還需要去刷新緩存,在它重新恢復狀態之前會拖慢應用程序。

消費者通過向組織協調者(Kafka Broker)發送心跳來維護自己是消費者組的一員並確認其擁有的分區。對於不同不的消費群體來說,其組織協調者可以是不同的。只要消費者定期發送心跳,就會認為消費者是存活的並處理其分區中的消息。當消費者檢索記錄或者提交它所消費的記錄時就會發送心跳。

如果過了一段時間 Kafka 停止發送心跳了,會話(Session)就會過期,組織協調者就會認為這個 Consumer 已經死亡,就會觸發一次重平衡。如果消費者宕機並且停止發送消息,組織協調者會等待幾秒鐘,確認它死亡了才會觸發重平衡。在這段時間里,死亡的消費者將不處理任何消息。在清理消費者時,消費者將通知協調者它要離開群組,組織協調者會觸發一次重平衡,盡量降低處理停頓。

重平衡是一把雙刃劍,它為消費者群組帶來高可用性和伸縮性的同時,還有有一些明顯的缺點(bug),而這些 bug 到現在社區還無法修改。

重平衡的過程對消費者組有極大的影響。因為每次重平衡過程中都會導致萬物靜止,參考 JVM 中的垃圾回收機制,也就是 Stop The World ,STW,(引用自《深入理解 Java 虛擬機》中 p76 關於 Serial 收集器的描述):

更重要的是它在進行垃圾收集時,必須暫停其他所有的工作線程。直到它收集結束。Stop The World 這個名字聽起來很帥,但這項工作實際上是由虛擬機在後台自動發起並完成的,在用戶不可見的情況下把用戶正常工作的線程全部停掉,這對很多應用來說都是難以接受的。

也就是說,在重平衡期間,消費者組中的消費者實例都會停止消費,等待重平衡的完成。而且重平衡這個過程很慢……

創建消費者

上面的理論說的有點多,下面就通過代碼來講解一下消費者是如何消費的

在讀取消息之前,需要先創建一個 KafkaConsumer 對象。創建 KafkaConsumer 對象與創建 KafkaProducer 對象十分相似 — 把需要傳遞給消費者的屬性放在 properties 對象中,後面我們會着重討論 Kafka 的一些配置,這裏我們先簡單的創建一下,使用3個屬性就足矣,分別是 bootstrap.serverkey.deserializervalue.deserializer

這三個屬性我們已經用過很多次了,如果你還不是很清楚的話,可以參考

還有一個屬性是 group.id 這個屬性不是必須的,它指定了 KafkaConsumer 是屬於哪個消費者群組。創建不屬於任何一個群組的消費者也是可以的

Properties properties = new Properties();
        properties.put("bootstrap.server","192.168.1.9:9092");     properties.put("key.serializer","org.apache.kafka.common.serialization.StringSerializer");   properties.put("value.serializer","org.apache.kafka.common.serialization.StringSerializer");
KafkaConsumer<String,String> consumer = new KafkaConsumer<>(properties);

主題訂閱

創建好消費者之後,下一步就開始訂閱主題了。subscribe() 方法接受一個主題列表作為參數,使用起來比較簡單

consumer.subscribe(Collections.singletonList("customerTopic"));

為了簡單我們只訂閱了一個主題 customerTopic,參數傳入的是一個正則表達式,正則表達式可以匹配多個主題,如果有人創建了新的主題,並且主題的名字與正則表達式相匹配,那麼會立即觸發一次重平衡,消費者就可以讀取新的主題。

要訂閱所有與 test 相關的主題,可以這樣做

consumer.subscribe("test.*");

輪詢

我們知道,Kafka 是支持訂閱/發布模式的,生產者發送數據給 Kafka Broker,那麼消費者是如何知道生產者發送了數據呢?其實生產者產生的數據消費者是不知道的,KafkaConsumer 採用輪詢的方式定期去 Kafka Broker 中進行數據的檢索,如果有數據就用來消費,如果沒有就再繼續輪詢等待,下面是輪詢等待的具體實現

try {
  while (true) {
    ConsumerRecords<String, String> records = consumer.poll(Duration.ofSeconds(100));
    for (ConsumerRecord<String, String> record : records) {
      int updateCount = 1;
      if (map.containsKey(record.value())) {
        updateCount = (int) map.get(record.value() + 1);
      }
      map.put(record.value(), updateCount);
    }
  }
}finally {
  consumer.close();
}
  • 這是一個無限循環。消費者實際上是一個長期運行的應用程序,它通過輪詢的方式向 Kafka 請求數據。
  • 第三行代碼非常重要,Kafka 必須定期循環請求數據,否則就會認為該 Consumer 已經掛了,會觸發重平衡,它的分區會移交給群組中的其它消費者。傳給 poll() 方法的是一個超市時間,用 java.time.Duration 類來表示,如果該參數被設置為 0 ,poll() 方法會立刻返回,否則就會在指定的毫秒數內一直等待 broker 返回數據。
  • poll() 方法會返回一個記錄列表。每條記錄都包含了記錄所屬主題的信息,記錄所在分區的信息、記錄在分區中的偏移量,以及記錄的鍵值對。我們一般會遍歷這個列表,逐條處理每條記錄。
  • 在退出應用程序之前使用 close() 方法關閉消費者。網絡連接和 socket 也會隨之關閉,並立即觸發一次重平衡,而不是等待群組協調器發現它不再發送心跳並認定它已經死亡。

線程安全性

在同一個群組中,我們無法讓一個線程運行多個消費者,也無法讓多個線程安全的共享一個消費者。按照規則,一個消費者使用一個線程,如果一個消費者群組中多個消費者都想要運行的話,那麼必須讓每個消費者在自己的線程中運行,可以使用 Java 中的 ExecutorService 啟動多個消費者進行進行處理。

消費者配置

到目前為止,我們學習了如何使用消費者 API,不過只介紹了幾個最基本的屬性,Kafka 文檔列出了所有與消費者相關的配置說明。大部分參數都有合理的默認值,一般不需要修改它們,下面我們就來介紹一下這些參數。

  • fetch.min.bytes

該屬性指定了消費者從服務器獲取記錄的最小字節數。broker 在收到消費者的數據請求時,如果可用的數據量小於 fetch.min.bytes 指定的大小,那麼它會等到有足夠的可用數據時才把它返回給消費者。這樣可以降低消費者和 broker 的工作負載,因為它們在主題使用頻率不是很高的時候就不用來回處理消息。如果沒有很多可用數據,但消費者的 CPU 使用率很高,那麼就需要把該屬性的值設得比默認值大。如果消費者的數量比較多,把該屬性的值調大可以降低 broker 的工作負載。

  • fetch.max.wait.ms

我們通過上面的 fetch.min.bytes 告訴 Kafka,等到有足夠的數據時才會把它返回給消費者。而 fetch.max.wait.ms 則用於指定 broker 的等待時間,默認是 500 毫秒。如果沒有足夠的數據流入 kafka 的話,消費者獲取的最小數據量要求就得不到滿足,最終導致 500 毫秒的延遲。如果要降低潛在的延遲,就可以把參數值設置的小一些。如果 fetch.max.wait.ms 被設置為 100 毫秒的延遲,而 fetch.min.bytes 的值設置為 1MB,那麼 Kafka 在收到消費者請求后,要麼返回 1MB 的數據,要麼在 100 ms 后返回所有可用的數據。就看哪個條件首先被滿足。

  • max.partition.fetch.bytes

該屬性指定了服務器從每個分區里返回給消費者的最大字節數。它的默認值時 1MB,也就是說,KafkaConsumer.poll() 方法從每個分區里返回的記錄最多不超過 max.partition.fetch.bytes 指定的字節。如果一個主題有20個分區和5個消費者,那麼每個消費者需要至少4 MB的可用內存來接收記錄。在為消費者分配內存時,可以給它們多分配一些,因為如果群組裡有消費者發生崩潰,剩下的消費者需要處理更多的分區。max.partition.fetch.bytes 的值必須比 broker 能夠接收的最大消息的字節數(通過 max.message.size 屬性配置大),否則消費者可能無法讀取這些消息,導致消費者一直掛起重試。 在設置該屬性時,另外一個考量的因素是消費者處理數據的時間。消費者需要頻繁的調用 poll() 方法來避免會話過期和發生分區再平衡,如果單次調用poll() 返回的數據太多,消費者需要更多的時間進行處理,可能無法及時進行下一個輪詢來避免會話過期。如果出現這種情況,可以把 max.partition.fetch.bytes 值改小,或者延長會話過期時間。

  • session.timeout.ms

這個屬性指定了消費者在被認為死亡之前可以與服務器斷開連接的時間,默認是 3s。如果消費者沒有在 session.timeout.ms 指定的時間內發送心跳給群組協調器,就會被認定為死亡,協調器就會觸發重平衡。把它的分區分配給消費者群組中的其它消費者,此屬性與 heartbeat.interval.ms 緊密相關。heartbeat.interval.ms 指定了 poll() 方法向群組協調器發送心跳的頻率,session.timeout.ms 則指定了消費者可以多久不發送心跳。所以,這兩個屬性一般需要同時修改,heartbeat.interval.ms 必須比 session.timeout.ms 小,一般是 session.timeout.ms 的三分之一。如果 session.timeout.ms 是 3s,那麼 heartbeat.interval.ms 應該是 1s。把 session.timeout.ms 值設置的比默認值小,可以更快地檢測和恢復崩憤的節點,不過長時間的輪詢或垃圾收集可能導致非預期的重平衡。把該屬性的值設置得大一些,可以減少意外的重平衡,不過檢測節點崩潰需要更長的時間。

  • auto.offset.reset

該屬性指定了消費者在讀取一個沒有偏移量的分區或者偏移量無效的情況下的該如何處理。它的默認值是 latest,意思指的是,在偏移量無效的情況下,消費者將從最新的記錄開始讀取數據。另一個值是 earliest,意思指的是在偏移量無效的情況下,消費者將從起始位置處開始讀取分區的記錄。

  • enable.auto.commit

我們稍後將介紹幾種不同的提交偏移量的方式。該屬性指定了消費者是否自動提交偏移量,默認值是 true,為了盡量避免出現重複數據和數據丟失,可以把它設置為 false,由自己控制何時提交偏移量。如果把它設置為 true,還可以通過 auto.commit.interval.ms 屬性來控制提交的頻率

  • partition.assignment.strategy

我們知道,分區會分配給群組中的消費者。PartitionAssignor 會根據給定的消費者和主題,決定哪些分區應該被分配給哪個消費者,Kafka 有兩個默認的分配策略RangeRoundRobin

  • client.id

該屬性可以是任意字符串,broker 用他來標識從客戶端發送過來的消息,通常被用在日誌、度量指標和配額中

  • max.poll.records

該屬性用於控制單次調用 call() 方法能夠返回的記錄數量,可以幫你控制在輪詢中需要處理的數據量。

  • receive.buffer.bytes 和 send.buffer.bytes

socket 在讀寫數據時用到的 TCP 緩衝區也可以設置大小。如果它們被設置為 -1,就使用操作系統默認值。如果生產者或消費者與 broker 處於不同的數據中心內,可以適當增大這些值,因為跨數據中心的網絡一般都有比較高的延遲和比較低的帶寬。

提交和偏移量的概念

特殊偏移

我們上面提到,消費者在每次調用poll() 方法進行定時輪詢的時候,會返回由生產者寫入 Kafka 但是還沒有被消費者消費的記錄,因此我們可以追蹤到哪些記錄是被群組裡的哪個消費者讀取的。消費者可以使用 Kafka 來追蹤消息在分區中的位置(偏移量)

消費者會向一個叫做 _consumer_offset 的特殊主題中發送消息,這個主題會保存每次所發送消息中的分區偏移量,這個主題的主要作用就是消費者觸發重平衡後記錄偏移使用的,消費者每次向這個主題發送消息,正常情況下不觸發重平衡,這個主題是不起作用的,當觸發重平衡后,消費者停止工作,每個消費者可能會分到對應的分區,這個主題就是讓消費者能夠繼續處理消息所設置的。

如果提交的偏移量小於客戶端最後一次處理的偏移量,那麼位於兩個偏移量之間的消息就會被重複處理

如果提交的偏移量大於最後一次消費時的偏移量,那麼處於兩個偏移量中間的消息將會丟失

既然_consumer_offset 如此重要,那麼它的提交方式是怎樣的呢?下面我們就來說一下

提交方式

KafkaConsumer API 提供了多種方式來提交偏移量

自動提交

最簡單的方式就是讓消費者自動提交偏移量。如果 enable.auto.commit 被設置為true,那麼每過 5s,消費者會自動把從 poll() 方法輪詢到的最大偏移量提交上去。提交時間間隔由 auto.commit.interval.ms 控制,默認是 5s。與消費者里的其他東西一樣,自動提交也是在輪詢中進行的。消費者在每次輪詢中會檢查是否提交該偏移量了,如果是,那麼就會提交從上一次輪詢中返回的偏移量。

提交當前偏移量

auto.commit.offset 設置為 false,可以讓應用程序決定何時提交偏移量。使用 commitSync() 提交偏移量。這個 API 會提交由 poll() 方法返回的最新偏移量,提交成功后馬上返回,如果提交失敗就拋出異常。

commitSync() 將會提交由 poll() 返回的最新偏移量,如果處理完所有記錄后要確保調用了 commitSync(),否則還是會有丟失消息的風險,如果發生了在均衡,從最近一批消息到發生在均衡之間的所有消息都將被重複處理。

異步提交

異步提交 commitAsync() 與同步提交 commitSync() 最大的區別在於異步提交不會進行重試,同步提交會一致進行重試。

同步和異步組合提交

一般情況下,針對偶爾出現的提交失敗,不進行重試不會有太大的問題,因為如果提交失敗是因為臨時問題導致的,那麼後續的提交總會有成功的。但是如果在關閉消費者或再均衡前的最後一次提交,就要確保提交成功。

因此,在消費者關閉之前一般會組合使用commitAsync和commitSync提交偏移量

提交特定的偏移量

消費者API允許調用 commitSync() 和 commitAsync() 方法時傳入希望提交的 partition 和 offset 的 map,即提交特定的偏移量。

文章參考:

《極客時間-Kafka核心技術與實戰》

《Kafka 權威指南》

關注公眾號獲取更多優質电子書,關注一下你就知道資源是有多好了

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

收購3c,收購IPHONE,收購蘋果電腦-詳細收購流程一覽表

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

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

※公開收購3c價格,不怕被賤賣!

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

SpringSecurity動態加載用戶角色權限實現登錄及鑒權

很多人覺得Spring Security實現登錄驗證很難,我最開始學習的時候也這樣覺得。因為我好久都沒看懂我該怎麼樣將自己寫的用於接收用戶名密碼的Controller與Spring Security結合使用,這是一個先入為主的誤區。後來我搞懂了:根本不用你自己去寫Controller。你只需要告訴Spring Security用戶信息、角色信息、權限信息、登錄頁是什麼?登陸成功頁是什麼?或者其他有關登錄的一切信息。具體的登錄驗證邏輯它來幫你實現。

一、動態數據登錄驗證的基礎知識

在本號之前的文章中,已經介紹了Spring Security的formLogin登錄認證模式,RBAC的權限控制管理模型,並且針對Spring Security的登錄認證邏輯源碼進行了解析等等。我們所有的用戶、角色、權限信息都是在配置文件裏面寫死的,然而在實際的業務系統中,這些信息通常是存放在RBAC權限模型的數據庫表中的。下面我們來回顧一下其中的核心概念:

  • RBAC的權限模型可以從用戶獲取為用戶分配的一個或多個角色,從用戶的角色又可以獲取該角色的多種權限。通過關聯查詢可以獲取某個用戶的角色信息和權限信息。
  • 在源碼解析的文章中,我們知道如果我們不希望用戶、角色、權限信息寫死在配置裏面。我們應該實現UserDetails與UserDetailsService接口,從而從數據庫或者其他的存儲上動態的加載這些信息。

以上是對一些核心的基礎知識的總結,如果您對這些知識還不是很清晰,建議您先往下讀本文。如果看完本文仍然理解困難,建議您翻看本號之前的文章。

二、UserDetails與UserDetailsService接口

  • UserDetailsService接口有一個方法叫做loadUserByUsername,我們實現動態加載用戶、角色、權限信息就是通過實現該方法。函數見名知義:通過用戶名加載用戶。該方法的返回值就是UserDetails。
  • UserDetails就是用戶信息,即:用戶名、密碼、該用戶所具有的權限。

下面我們來看一下UserDetails接口都有哪些方法。

public interface UserDetails extends Serializable {
    //獲取用戶的權限集合
    Collection<? extends GrantedAuthority> getAuthorities();

    //獲取密碼
    String getPassword();

    //獲取用戶名
    String getUsername();

    //賬號是否沒過期
    boolean isAccountNonExpired();

    //賬號是否沒被鎖定
    boolean isAccountNonLocked();

    //密碼是否沒過期
    boolean isCredentialsNonExpired();

    //賬戶是否可用
    boolean isEnabled();
}

現在,我們明白了,只要我們把這些信息提供給Spring Security,Spring Security就知道怎麼做登錄驗證了,根本不需要我們自己寫Controller實現登錄驗證邏輯。

三、實現UserDetails 接口

public class SysUser implements UserDetails{
    
    String password();  //密碼
    String username();  //用戶名
    boolean accountNonExpired;   //是否沒過期
    boolean accountNonLocked;   //是否沒被鎖定
    boolean credentialsNonExpired;  //是否沒過期
    boolean enabled;  //賬號是否可用
    Collection<? extends GrantedAuthority> authorities;  //用戶的權限集合

    //省略構造方法
    //省略set方法
    //省略get方法(即接口UserDetails的方法)
}

我們就是寫了一個適應於UserDetails的java POJO類,所謂的 UserDetails接口實現就是一些get方法。get方法由Spring Security調用,我們通過set方法或構造函數為 Spring Security提供UserDetails數據。

四、實現UserDetailsService接口

@Component
public class MyUserDetailsService implements UserDetailsService{

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            
       //這裏從數據庫sys_user表裡面查詢實體類對象。loadUser方法可使用Mybatis或JDBC或JPA自行實現。
       SysUser sysUser =  loadUser(username);   

        // 判斷用戶是否存在 
       if(user == null)  {  throw  new  UsernameNotFoundException("用戶名不存在");  }

       //從數據庫該用戶所有的角色信息,所有的權限標誌
       //遍歷所有的ROLE角色及所有的Authority權限(菜單、按鈕)。
       //用逗號分隔他們的唯一標誌,具體過程自行實現。
       sysUser.setAuthorities(
               AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_AMIN , system:user:delete"));
        
        //sysUser.setAccountNonLocked(true或false);
        return sysUser;
    }
}
  • 通常數據庫表sys_user字段要和SysUser屬性一一對應,比如username、password、enabled。但是比如accountNonLocked字段用於登錄多次錯誤鎖定,但我們一般不會在表裡存是否鎖定,而是存一個鎖定時間字段。通過鎖定時間是否大於當前時間判斷賬號是否鎖定,所以實現過程中可以靈活做判斷並用好set方法,不必拘泥於一一對應的形式。
  • 角色是一種特殊的權限,在Spring Security我們可以使用hasRole(角色標識)表達式判斷用戶是否具有某個角色,決定他是否可以做某個操作;通過hasAuthority(權限標識)表達式判斷是否具有某個操作權限。

五、最後說明

至此,我們將系統裏面的所有的用戶、角色、權限信息都通過UserDetailsService和UserDetails告知了Spring Security。但是多數朋友可能仍然不知道該怎樣實現登錄的功能,其實剩下的事情很簡單了:

  • 寫一個登錄界面,寫一個登錄表單,表單使用post方法提交到默認的/login路徑
  • 表單的用戶名、密碼字段名稱默認是username、password。
  • 寫一個登錄成功之後的跳轉頁面,比如index.html

然後把這些信息通過配置方式告知Spring Security ,以上的配置信息名稱都可以靈活修改。如果您不知道如何配置請參考本號之前的文章《formLogin登錄認證模式》。

期待您的關注

  • 向您推薦博主的系列文檔:
  • 本文轉載註明出處(必須帶連接,不能只轉文字):。

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

※高價收購3C產品,價格不怕你比較

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

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

3c收購,鏡頭 收購有可能以全新價回收嗎?

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

看好台灣電動車發展,日本住友商事繼投資 Gogoro 後再入股電動巴士廠商華德動能

因應世界環保趨勢,期望達到未來零排放的標準,日商住友商事看上台灣電動車產業發展,繼之前投資電動機車大廠 Gogoro 之後,再次宣布投資台灣電動巴士大廠華德動能,為台日雙方在電動車方面的合作奠下基礎。

上櫃企業車王電子公司華德動能於 12 日上午召開臨時董事會,通過私募普通股定價相關事宜,應募人為日本住友商事株式會社 (以下簡稱日本住友商事),本次參與私募價格每股新台幣 25 元,總投資金額為日幣 4.5 億元 (約新台幣 1.27 億元),佔華德動能持股比率約 7%。華德動能表示,目前該項投資案仍須送投審會審議,預計最快將在 2020 年第 1 季可以得知審議結果。

華德動能指出,簽約儀式由日本住友商事由本部長岩波與華德動能董事長蔡裕慶代表雙方簽約。而日本住友商事為初次參與華德動能私募,著眼於雙方合作電動巴士製造及服務,未來將持續在智慧移動載具、汰役電池二次應用業務合作發展。基於鋰電池成本將逐年大幅降低,全球電動巴士及智慧載具市場在未來數年內將有突破性增長,華德動能將藉由日本住友商事之全球行銷服務據點協助華德動能拓展全球市場。

華德動能董事長蔡裕慶強調,多年來雙方一直簽有相關備忘錄,並進行進一步的合作。也就是過去華德動能透過住友商事向日本取得相關電動車電池使用於其所生產的電動巴士上,而華德動能也透過與住友商事的合作,藉由旗下全球 60 多個據點與子公司進行市場銷售。目前在東南亞及南美洲都有相關計畫發展,預計短期內就有成果。而住友商事由本部長岩波則是指出,雖然目前雙方的合作在於電動巴士的銷售,但未來仍會在售後服務與能源管理上合作,而且投資華德動能的持股比率還希望增加到 20%。

蔡裕慶進一步指出,華德動能是台灣唯一獲得交通部車輛安全審驗中心電動巴士「自主設計能力」資格審核通過的公司。其國產附加價值率超過 60%,所生産的電動巴士擁有優異的電池管理技術,採用零電池事故之日產 Leaf 同款電池及自主開發之專利電池主動平衡技術,大幅提升電池安全及壽命。另外,華德動能也是全球唯一採用六段電子自動變速箱之電動巴士廠商,並結合芬蘭設計東元電機生產之超高效能馬達,大幅降低能耗並保有超高爬坡力及高速行駛能力,華德電動巴士領先業界採用 10.1 吋智慧化觸控面板及雲端後台管理系統,並藉由大數據達到車輛異常偵測及預防保養,及時掌握車輛運行及異常資訊。

而為了因應目前市場對電動巴士的需求,華德動能 11 日也宣布在中港加工區投資新台幣 25 億元打造中港新廠。其規劃為 4 層樓建築,建物面積約 12,600 坪,預計 2021 年初完工,2021 年 7 月正式投產的新廠區,1 樓供華德動能生產電動巴士及底盤三電之用,2 至 4 樓則分別規劃為車王電子生產線、倉儲等用途。而新廠區的產能為年產整車 1,700 部,與 6,000 部底盤加三電系統,未來將以大多數供應外銷為主。

(合作媒體:。首圖來源:攝)

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

【其他文章推薦】

※公開收購3c價格,不怕被賤賣!

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

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

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

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

特斯拉發表電動卡車 Cybertruck,一台從科幻電影走出來的鋼鐵車

今天的 Elon Musk 看起來不像鋼鐵人,更像蝙蝠俠,因為他們的新車 Cybertruck,不但外型酷似蝙蝠車,同時還能防彈防撞,並且擁有超越保時捷的加速度,跟勝過市面上卡車的拖吊能力,更驚人的是,售價只要 39,900 美元起。

眾所期待的特斯拉新車 Cybertruck 今日正式發表,和之前流出的影像不同,Cybertruck 酷似隱形戰機 F-117 的設計,讓人聯想到蝙蝠車,甚至懷疑這是不是一台防雷達偵測的戰車?

Tesla Cybertruck 全車採用冷鑄鋼板,能夠抵擋 9mm 口徑手槍的射擊,現場展示用重鎚敲擊也毫髮無傷;車窗玻璃同樣採用防彈設計,然而有趣的是,現場展示時,被大鐵球砸出了一片雪花。「至少,它沒被打穿,你坐在裡面很安全。」Elon Musk 笑著說。

Cybertruck 為了因應負重,搭載了適應性氣壓懸吊系統,針對高速公路,或是越野泥巴路,能夠自動調整懸吊高度,同時也順便使用這個氣壓系統,做了一個高壓出力裝置,使用者可以自行加裝不同氣壓工具,像是高壓水槍或是電鑽等。

當重裝電動機車開上後廂時,懸吊系統會自動調整車尾高度,讓車身保持平衡。

車尾與其他皮卡車開放式貨斗不同,Cybertruck 採用封閉式貨斗,並有升降式尾門,現場展示時,將這台電動機車 ATV 直接騎上貨斗後,還能直接充電,顯然是在致敬蝙蝠車跟蝙蝠機車。

Cybertruck 如同其他皮卡車,車尾裝有釣鉤,能夠充當拖車使用,而歸功於它的強力馬達,拖車能力屌打了皮卡車霸主 Ford F-150,在現場展示的影片中,特斯拉讓 Cybertruck 跟 F-150 互相拖住對方,進行拔河測試,結果 F-150 整台被 Cybertruck 拖走。

F-150 慘遭 Cybertruck 拖走。

馬斯克強調,一般皮卡車需要另外裝載發電機才能使用電動工具,Cybertruck 直接提供了電源,因此省下不少空間,同時還提供強大的拖力。

此外,做為一台卡車,Cybertruck 莫名其妙地擁有超越保時捷的加速度,根據現場公布數據,最頂級版的 0-100 公里加速時間不到 3 秒。現場展示了 Cybertruck 與 Porsche 911 賽跑的影片,起步雖然小輸一點,但隨後就超越了 911。

現場展示競速影片,大約 1 秒後,Cybertruck 就超過了 911。

Tesla Cybertruck 共有 3 種版本,依照馬達數量來分別,最低價 39,900 美元起,最高 69,900 美元。Cybertruck 從今天起在美國開放預購,實際交車時間預計要等到 2021 年底。頂級的三馬達款,更預計要等到 2022 年底才會開始生產。

如同馬斯克開場所說,卡車在過去幾十年來都長得差不多,特斯拉要打造一台完全不一樣的卡車,同時還要保持零排放,跟超高性能,從今天的現場展示來看,特斯拉再次完成一個不可能的任務。在興奮之餘也別忘了,這一切都是現場展示,實際上如何,就有待實際交車後驗證了!

(合作媒體:。圖片來源:)

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

【其他文章推薦】

收購3c,收購IPHONE,收購蘋果電腦-詳細收購流程一覽表

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

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

※公開收購3c價格,不怕被賤賣!

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

【其他文章推薦】

收購3c,收購IPHONE,收購蘋果電腦-詳細收購流程一覽表

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

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

※公開收購3c價格,不怕被賤賣!

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

特斯拉 Cybertruck 的 6 個設計原則與 5 個未公開的疑問

特斯拉上週發表了最新車種 Cybertruck,破格的外型與性能讓全球傻眼,本篇文章將告訴你 Cybertruck 設計成這樣的 6 個理由,以及 5 個它尚未說清楚的疑點。

老派科幻電影的線條、全車冷鑄不鏽鋼、跑得比保時捷還快、拉得比福特還給力,特斯拉 Cybertruck 一出場就奪走全世界目光,如同特斯拉執行長馬斯克(Elon Musk)說的,他要改變皮卡車一成不變的外觀。改變歸改變,改成這樣確實是有點誇張了,但 Cybertruck 在設計時,是遵循著以下 6 個原則,才會長成這樣。

  1. 原則一:強悍外表
  2. 原則二:將低風阻,提高效率
  3. 原則三:內在舒適大空間
  4. 原則四:適應不同路面駕駛
  5. 原則五:大載貨量與拉力
  6. 原則六:大電池容量

這些原則(或說是目標),部分來自於馬斯克個人的期待,一部分來自於市場調查的回饋,相信收到這個提案要求的專案,當下應該很想離職。但從發表會的成果來看,他們確實做到了。

為了看起來更強悍,Cybertruck 全車採用 301 不鏽鋼包覆。

皮卡車外表強悍,幾乎是一種固定形象,方方正正的大塊頭是百年傳統。但這樣的外表牴觸了原則二的低風阻要求,同時別忘了,低風阻的跑車,可不會配備原則三的車內大空間。

最終,結合這三項要求的最大公約數,原來就存在於 80 年代的科幻電影跟電玩中,當然這一切若沒有電動馬達是不可能達成的。燃油車的引擎通常配置於車頭,這是為了讓後方留出寬敞的乘坐空間,犧牲了車頭的流線設計,也在傳輸過程中浪費了部分動力。因此追求速度的跑車,通常將車頭壓低,引擎後置,這樣子減低了風阻,也讓動力傳導更直接,但是乘坐空間就非常狹窄,更別提燃油皮卡車後面的大型貨斗了。

Cybertruck 和目前越來越多的電動車一樣,將馬達、電池組整合在底盤,離輪胎更近,也讓上部空間更有彈性,Cybertruck 進一步運用了大眾對皮卡車車身較高的印象,讓它的底盤空間能塞進更多電池,還有其他黑科技,來滿足這些設計原則,像是適應性氣壓懸吊、龐大的電池容量以及可容納六人乘坐的車廂和一個可開閉的貨斗。

從試乘影片中,可以看到後座乘客(身高約 180 公分)在入座後,頭頂上仍然有兩個拳頭左右的空間,算是相當舒適,然而腿部空間似乎就比較侷促,後座寬度看起來和一般房車差不多,塞進兩位大漢剛剛好,整體來說,算是勉強達成了原則三的大空間要求。(官方數據,能容納 6 個成人)

但是,在完成這樣的設計之後,我們會發現,這種外觀線條看起來並不是那麼強悍,因為斜切的車頭讓人聯想到的是跑車,不是皮卡車或悍馬車這種硬漢風格。解決的辦法,就如同我們現在看到的,捨棄一般車輛的鈑金與烤漆,改用與姊妹公司 SpaceX 的太空船同樣材料的 301 不鏽鋼板,而且厚度達到 3 公厘,足以抵擋 9mm 口徑手槍的射擊。代價是,每一扇車門重達 27 公斤,萬一被車門夾到,後果可是不得了。

Cybertruck 內部空間比想像中寬闊,前方只有一塊 17 吋觸控螢幕作為主控台。

5 個等待揭開的疑問

發表會後隔天,馬斯克宣布訂單數量已經突破 14 萬,換句話說,這個發表會已經幫特斯拉賺進了 1,400 萬美元,然而這位愛做夢、愛說大話的老闆,心中也明白最後成交的數量可能不到一半,因為這台科幻裝甲車,仍然有幾個問題需要釐清。

問題一:後照鏡

許多人都發現了,發表會上的 Cybertruck 並沒有裝備車側後照鏡,官網上的影片與照片也都沒有看到,推測是使用攝影機來取代了傳統後照鏡,然而並非所有國家都開放電子後照鏡上路,包含美國跟台灣都還沒開放。在 Cybertruck 試乘的過程中,也沒看到螢幕上有播放左右後側的畫面,只有裝設數位正後方後照鏡,究竟 Cybertruck 怎麼解決車側後照鏡的問題,尚有待解答。

問題二:外觀塗裝

全不鏽鋼車身肯定會吸引到一部分的客群,但是正式販售時會不會提供不同款式的彩繪或是塗裝,並未說明,畢竟許多車主仍然希望自己的愛車能保有一些特殊性,至少花了 7 萬美元購買頂級款的車主,會希望自己的車看起來跟 4 萬美元的有些不同。

而提到外觀就不能不提那一整片乾乾淨淨的車頭,一般車輛會有車頭 Logo、進氣壩等,即使特斯拉其他車款,也都留有進氣孔,或許 Cybertruck 可以不用 Logo 來辨識,但是令人好奇的是它怎麼解決進氣的問題,又或者 Cybertruck 有了不同的方式來處理散熱和車內空氣流通問題

問題三:上下車

一般的皮卡車,車身都較高,對於家有老小的家庭來說,上下車會比較辛苦。從 Cybertruck 的照片中來看,底盤高度約在成年女子的膝蓋,算是有一點點難度。

在發表會上,Cybertruck 展示了它的適應性氣壓懸吊系統,在貨斗載重時,能夠自動調整車身高度;特斯拉官方也表示,當行駛於不同路面時,Cybertruck 會自動調整懸吊,在高速公路時會降低車身,在路面崎嶇時,會提高車身。這讓人不免想知道,當車主或乘客要上下車的時候,車身是否也會自動降低,方便進出呢?

問題四:貨斗操縱方式

市面上的皮卡車,後方貨斗大都是開放式的,也有車主會加裝棚罩,除了保護行李外,最重要是不讓路人亂丟垃圾。Cybertruck 的貨斗是機械式的,在發表會中,可以看到機車騎士走到車尾按了一些隱藏的開關,開啟貨斗,接著再用手拉開尾門,放下斜板。

特斯拉是否會將貨斗開閉設計得更容易使用呢?

對於一般卡車司機來說,這些動作稀鬆平常,但對於標榜高科技的 Cybertruck 來說,似乎不太「性感」,因此我們不免好奇,正式上市後,特斯拉是否會將操作貨斗開閉的功能,放到他們的手機 App 裡,讓車主可以更輕鬆地開關?

問題五:交車時間

最後的問題,也是特斯拉在過去幾年最為人詬病的問題,真的能夠如期交車嗎?特斯拉官網上寫得非常保守,「生產日期接近 2021 年底」,最高規格的三馬達版本,更是標明「預計在 2022 年底開始生產」。這等於是在說,我沒有說什麼時候交車,我只說了什麼時候開始製造,從生產到交車要多久,沒有人知道。

(合作媒體:。圖片來源:)

延伸閱讀:

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

【其他文章推薦】

※公開收購3c價格,不怕被賤賣!

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

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

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

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

深入理解static關鍵字

在開始講static之前,我想讓各位看一段有意思的代碼:

public class Test {
     
    static{
        System.out.println("test static 1");
    }
  
    static{
        System.out.println("test static 2");
    }
    
    public static void main(String[] args) {
         
    }
}

看完程序,小白童鞋發話了:啥玩意?main方法中啥都沒有,能運行啥?博主你個星星星…

運行結果:
test static 1
test static 2

小白童鞋:那啥…那啥…博主我說啥了,我啥都沒說…

其實,上面的代碼懂的自然懂,不懂的自然就不懂了,因為上面的代碼涉及到JVM的類加載了!當然不在本篇博客文章的範疇內,如果有興趣理解上面的程序,這篇文章可能會對你有所幫助

1、static存在的主要意義

static的主要意義是在於創建獨立於具體對象的域變量或者方法。以致於即使沒有創建對象,也能使用屬性和調用方法

static關鍵字還有一個比較關鍵的作用就是 用來形成靜態代碼塊以優化程序性能。static塊可以置於類中的任何地方,類中可以有多個static塊。在類初次被加載的時候,會按照static塊的順序來執行每個static塊,並且只會執行一次。

  為什麼說static塊可以用來優化程序性能,是因為它的特性:只會在類加載的時候執行一次。因此,很多時候會將一些只需要進行一次的初始化操作都放在static代碼塊中進行。

2、static的獨特之處

1、被static修飾的變量或者方法是獨立於該類的任何對象,也就是說,這些變量和方法不屬於任何一個實例對象,而是被類的實例對象所共享

怎麼理解 “被類的實例對象所共享” 這句話呢?就是說,一個類的靜態成員,它是屬於大夥的【大夥指的是這個類的多個對象實例,我們都知道一個類可以創建多個實例!】,所有的類對象共享的,不像成員變量是自個的【自個指的是這個類的單個實例對象】…我覺得我已經講的很通俗了,你明白了咩?

2、在該類被第一次加載的時候,就會去加載被static修飾的部分,而且只在類第一次使用時加載並進行初始化,注意這是第一次用就要初始化,後面根據需要是可以再次賦值的。

3、static變量值在類加載的時候分配空間,以後創建類對象的時候不會重新分配。賦值的話,是可以任意賦值的!

4、被static修飾的變量或者方法是優先於對象存在的,也就是說當一個類加載完畢之後,即便沒有創建對象,也可以去訪問。

3、static應用場景

因為static是被類的實例對象所共享,因此如果某個成員變量是被所有對象所共享的,那麼這個成員變量就應該定義為靜態變量

因此比較常見的static應用場景有:

1、修飾成員變量
2、修飾成員方法
3、靜態代碼塊
4、修飾類【只能修飾內部類也就是靜態內部類】
5、靜態導包

以上的應用場景將會在下文陸續講到…

4、靜態變量和實例變量的概念

靜態變量:
static修飾的成員變量叫做靜態變量【也叫做類變量】,靜態變量是屬於這個類,而不是屬於是對象。

實例變量:
沒有被static修飾的成員變量叫做實例變量,實例變量是屬於這個類的實例對象。

還有一點需要注意的是:static是不允許用來修飾局部變量,不要問我問什麼,因為java規定的!

5、靜態變量和實例變量區別【重點常用】

靜態變量:
靜態變量由於不屬於任何實例對象,屬於類的,所以在內存中只會有一份,在類的加載過程中,JVM只為靜態變量分配一次內存空間。

實例變量:
每次創建對象,都會為每個對象分配成員變量內存空間,實例變量是屬於實例對象的,在內存中,創建幾次對象,就有幾份成員變量。

我相信各位智商都比宜春智商要高,應該都能理解上面的話。下面舉了例子完全出於娛樂,理解了大可不必看,下面的例子僅供參考,僅供娛樂一下下氣氛,趕時間的熊dei大可略過!

怎麼理解呢?打個比喻吧…就比方說程序員小王是一個比較溫柔陽光的男孩子,這1024的這一天,老闆閑的沒事,非要拉着程序員小王來玩耍,怎麼個玩法呢?老闆和小王一人拿着一把菜刀,規則很簡單,互相傷害,一人一刀,你一刀,我一刀….遊戲一開始,老闆二話不說,跳起來就是一刀,程序員小王二話也沒說反手就是一菜刀回去,這個時候老闆發飆了,雙眼瞪得忒大,跳起來又是一刀,這個時候程序員小王不敢還手了,就沒動手。沒想到老闆越來越生猛,左一刀右一刀全程下來差不多砍個半個時….程序員小王一直沒有還過手,因為小王知道他是老闆…

這個程序員小王只會在老闆第一次揮刀的時候,回老闆一刀,之後就不還手了,這個時候我們把程序員小王看做是靜態變量,把老闆第一次向小王揮刀看做是類加載,把小王回老闆一刀看出是分配內存空間,而一人一刀這個回合過程看成是類加載的過程,之後老闆的每一刀都看成是創建一次對象。

連貫起來就是static變量值在類第一次加載的時候分配空間,以後創建類對象的時候不會重新分配

之後這個老闆挨了一刀之後躺醫院了一年,一出院回到公司第一件事就是拉程序員宜春出來玩耍,老闆殊不知其然,這個博主程序員宜春性格異常暴躁,老闆遞給程序員宜春一把菜刀,博主宜春一接過菜刀,猝不及防的被老闆跳起來就是一刀,程序員宜春痛的嗷了一聲,暴躁的程序員宜春還沒嗷完,在嗷的同時跳起來就是給老闆一刀,接着老闆跳起來又是一刀,程序員宜春嗷的一聲又是回一刀,老闆跳起來又一刀,程序員宜春嗷的一聲又是回一刀,只要老闆沒停程序員宜春就沒停,因為程序員宜春知道,就自己這曝脾氣,暴躁起來si都敢摸,肯定有幾個老鐵知道….

程序員宜春就類似實例變量,每次創建對象,都會為每個對象分配成員變量內存空間,就像老闆來一刀,程序員宜春都會回一刀這樣子的…

6、訪問靜態變量和實例變量的兩種方式

我們都知道靜態變量是屬於這個類,而不是屬於是對象,static獨立於對象。

但是各位有木有想過:靜態成員變量雖然獨立於對象,但是不代表不可以通過對象去訪問,所有的靜態方法和靜態變量都可以通過對象訪問【只要訪問權限足夠允許就行】,不理解沒關係,來個代碼就理解了

public class StaticDemo {

        static int value = 666;

        public static void main(String[] args) throws Exception{
            new StaticDemo().method();
        }

        private void method(){
            int value = 123;
            System.out.println(this.value);
        }

}

猜想一下結果,我猜你的結果是123,哈哈是咩?其實

運行結果: 666

回過頭再去品味一下上面的那段話,你就能非常客觀明了了,這個思想概念要有隻是這種用法不推薦!

因此小結一下訪問靜態變量和實例變量的兩種方法:

靜態變量:

類名.靜態變量

對象.靜態變量(不推薦)

靜態方法:

類名.靜態方法

對象.靜態方法(不推薦)

7、static靜態方法

static修飾的方法也叫做靜態方法,不知道各位發現咩有,其實我們最熟悉的static靜態方法就是main方法了~小白童鞋:喔好像真的是哦~。由於對於靜態方法來說是不屬於任何實例對象的,this指的是當前對象,因為static靜態方法不屬於任何對象,所以就談不上this了。

還有一點就是:構造方法不是靜態方法

8、static靜態代碼塊

先看個程序吧,看看自個是否掌握了static代碼塊,下面程序代碼繼承關係為 BaseThree——> BaseTwo——> BaseOne

BaseOne類

package com.gx.initializationblock;

public class BaseOne {

    public BaseOne() {
        System.out.println("BaseOne構造器");
    }

    {
        System.out.println("BaseOne初始化塊");
        System.out.println();
    }

    static {
        System.out.println("BaseOne靜態初始化塊");

    }

}

BaseTwo類

package com.gx.initializationblock;

public class BaseTwo extends BaseOne {
    public BaseTwo() {
        System.out.println("BaseTwo構造器");
    }

    {
        System.out.println("BaseTwo初始化塊");
    }

    static {
        System.out.println("BaseTwo靜態初始化塊");
    }
}

BaseThree 類

package com.gx.initializationblock;

public class BaseThree extends BaseTwo {
    public BaseThree() {
        System.out.println("BaseThree構造器");
    }

    {
        System.out.println("BaseThree初始化塊");
    }

    static {
        System.out.println("BaseThree靜態初始化塊");
    }
}

測試demo2類

package com.gx.initializationblock;

/*
     注:這裏的ABC對應BaseOne、BaseTwo、BaseThree 
 * 多個類的繼承中初始化塊、靜態初始化塊、構造器的執行順序
     在繼承中,先後執行父類A的靜態塊,父類B的靜態塊,最後子類的靜態塊,
     然後再執行父類A的非靜態塊和構造器,然後是B類的非靜態塊和構造器,最後執行子類的非靜態塊和構造器
 */
public class Demo2 {
    public static void main(String[] args) {
        BaseThree baseThree = new BaseThree();
        System.out.println("-----");
        BaseThree baseThree2 = new BaseThree();

    }
}

運行結果

BaseOne靜態初始化塊
BaseTwo靜態初始化塊
BaseThree靜態初始化塊
BaseOne初始化塊

BaseOne構造器
BaseTwo初始化塊
BaseTwo構造器
BaseThree初始化塊
BaseThree構造器
-----
BaseOne初始化塊

BaseOne構造器
BaseTwo初始化塊
BaseTwo構造器
BaseThree初始化塊
BaseThree構造器

至於static代碼塊運行結果不是很清晰的童鞋,詳細講解請看這篇

以上僅僅是讓各位明確代碼塊之間的運行順序,顯然還是不夠的,靜態代碼塊通常用來對靜態變量進行一些初始化操作,比如定義枚舉類,代碼如下:

public enum WeekDayEnum {
    MONDAY(1,"周一"),
    TUESDAY(2, "周二"),
    WEDNESDAY(3, "周三"),
    THURSDAY(4, "周四"),
    FRIDAY(5, "周五"),
    SATURDAY(6, "周六"),
    SUNDAY(7, "周日");
 
    private int code;
    private String desc;
 
    WeekDayEnum(int code, String desc) {
        this.code = code;
        this.desc = desc;
    }
 
    private static final Map<Integer, WeekDayEnum> WEEK_ENUM_MAP = new HashMap<Integer, WeekDayEnum>();
 
    // 對map進行初始化
    static {
        for (WeekDayEnum weekDay : WeekDayEnum.values()) {
            WEEK_ENUM_MAP.put(weekDay.getCode(), weekDay);
        }
    }
 
    public static WeekDayEnum findByCode(int code) {
        return WEEK_ENUM_MAP.get(code);
    }
 
    public int getCode() {
        return code;
    }
 
    public void setCode(int code) {
        this.code = code;
    }
 
    public String getDesc() {
        return desc;
    }
 
    public void setDesc(String desc) {
        this.desc = desc;
    }
} 

當然不僅僅是枚舉這一方面,還有我們熟悉的單例模式同樣也用到了靜態代碼塊,如下:

public class Singleton {
    private static Singleton instance;
 
    static {
        instance = new Singleton();
    }
 
    private Singleton() {}
 
    public static Singleton getInstance() {
        return instance;
    }
}

9、static變量與普通變量區別

static變量也稱作靜態變量,靜態變量和非靜態變量的區別是:靜態變量被所有的對象所共享,在內存中只有一個副本,它當且僅當在類初次加載時會被初始化。而非靜態變量是對象所擁有的,在創建對象的時候被初始化,存在多個副本,各個對象擁有的副本互不影響。

還有一點就是static成員變量的初始化順序按照定義的順序進行初始化。

10、靜態內部類

靜態內部類與非靜態內部類之間存在一個最大的區別,我們知道非靜態內部類在編譯完成之後會隱含地保存着一個引用,該引用是指向創建它的外圍內,但是靜態內部類卻沒有。沒有這個引用就意味着:

1、它的創建是不需要依賴外圍類的創建。
2、它不能使用任何外圍類的非static成員變量和方法。

代碼舉例(靜態內部類實現單例模式)

public class Singleton {
    
   // 聲明為 private 避免調用默認構造方法創建對象
    private Singleton() {
    }
    
   // 聲明為 private 表明靜態內部該類只能在該 Singleton 類中被訪問
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getUniqueInstance() {
        return SingletonHolder.INSTANCE;
    }
}

Singleton 類加載時,靜態內部類 SingletonHolder 沒有被加載進內存。只有當調用 getUniqueInstance()方法從而觸發 SingletonHolder.INSTANCESingletonHolder 才會被加載,此時初始化 INSTANCE 實例,並且 JVM 能確保 INSTANCE 只被實例化一次。

這種方式不僅具有延遲初始化的好處,而且由 JVM 提供了對線程安全的支持。

11、靜態導包

靜態導包格式:import static

這兩個關鍵字連用可以指定導入某個類中的指定靜態資源,並且不需要使用類名調用類中靜態成員,可以直接使用類中靜態成員變量和成員方法

//  Math. --- 將Math中的所有靜態資源導入,這時候可以直接使用裏面的靜態方法,而不用通過類名進行調用
//  如果只想導入單一某個靜態方法,只需要將換成對應的方法名即可
 
import static java.lang.Math.;
//  換成import static java.lang.Math.max;具有一樣的效果
 
public class Demo {
    public static void main(String[] args) {
 
        int max = max(1,2);
        System.out.println(max);
    }
}

靜態導包在書寫代碼的時候確實能省一點代碼,可以直接調用裏面的靜態成員,但是會影響代碼可讀性,所以開發中一般情況下不建議這麼使用。

12、static注意事項

1、靜態只能訪問靜態。
2、非靜態既可以訪問非靜態的,也可以訪問靜態的。

13、final與static的藕斷絲連

到這裏文章本該結束了的,但是static的使用始終離不開final字眼,二者可謂藕斷絲連,常常繁見,我覺得還是很有必要講講,那麼一起來看看下面這個程序吧。

package Demo;

class FinalDemo {
    public final double i = Math.random();
    public static double t = Math.random();
}

public class DemoDemo {
    public static void main(String[] args) {

        FinalDemo demo1 = new FinalDemo();
        FinalDemo demo2 = new FinalDemo();
        System.out.println("final修飾的  i=" + demo1.i);
        System.out.println("static修飾的 t=" + demo1.t);
        System.out.println("final修飾的  i=" + demo2.i);
        System.out.println("static修飾的 t=" + demo2.t);

        System.out.println("t+1= "+ ++demo2.t );
//      System.out.println( ++demo2.i );//編譯失敗
      }
}
運行結果:
    final修飾的  i=0.7282093281367935
    static修飾的 t=0.30720545678577604
    final修飾的  i=0.8106990945706758
    static修飾的 t=0.30720545678577604
    t+1= 1.307205456785776

static修飾的變量沒有發生變化是因為static作用於成員變量只是用來表示保存一份副本,其不會發生變化。怎麼理解這個副本呢?其實static修飾的在類加載的時候就加載完成了(初始化),而且只會加載一次也就是說初始化一次,所以不會發生變化!

至於final修飾的反而發生變化了?是不是巔覆你對final的看法?關於final詳細講解博主也準備好了一篇文章

ok,文章就先到這裏了,希望這篇文章能夠幫助到你對static的認識,若有不足或者不正之處,希望諒解並歡迎批評指正!

如果本文章對你有幫助,哪怕是一點點,那就請點一個讚唄,謝謝~

參考:
《java編程思想》

如果本文對你有一點點幫助,那麼請點個讚唄,謝謝~

最後,若有不足或者不正之處,歡迎指正批評,感激不盡!如果有疑問歡迎留言,絕對第一時間回復!

歡迎各位關注我的公眾號,一起探討技術,嚮往技術,追求技術,說好了來了就是盆友喔…

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

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

平板收購,iphone手機收購,二手筆電回收,二手iphone收購-全台皆可收購

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

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

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

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

快速搭建 SpringCloud 微服務開發環境的腳手架

本文適合有 SpringBoot 和 SpringCloud 基礎知識的人群,跟着本文可使用和快速搭建 SpringCloud 項目。

本文作者:HelloGitHub-秦人

HelloGitHub 推出的系列,今天給大家帶來一款基於 SpringCloud2.1 的微服務開發腳手開源項目——SpringCloud

項目源碼地址:

一、微服務的簡介

微服務是可以獨立部署、水平擴展、獨立訪問的服務單元。Java 中常見最小的微服務單元就是基於 SpringBoot 框架的一個獨立項目。一個微服務只做一件事(單一職責),多個微服務組合才能稱之為一個完整的項目或產品。那麼多個微服務的就需要來管理,而 SpringCloud 就是統籌這些微服務的大管家。它是一系列有序框架的集合,簡單易懂、易部署易維護的分佈式系統開發工具包。

今天介紹的開源項目就是基於 SpringCloud2.1 的腳手架,讓項目開發快速進入業務開發,而不需過多時間花費在架構搭建上,下面就讓我們一起來看看這個項目的使用吧。

二、項目結構

這裏以一個網關(gateway-admin)微服務來說明。

項目目錄結構如下圖:

目錄說明:

  1. db:項目初始化數據庫腳本。
  2. docker:Docker 配置文件目錄,將微服務打包為 docker 鏡像(image)。
  3. config:項目配置信息目錄,包括數據庫配置,消息轉化配置等。
  4. dao:數據庫操作目錄,主要對底層數據進行增刪查改。
  5. entity:項目實體類目錄。
  6. events:事件處理目錄。
  7. exception:異常處理目錄,通過面向切面處理全局異常。
  8. rest:微服務控制器目錄,也就是對外提供的接口。
  9. service:微服務業務層目錄。
  10. GatewayAdminApplication:微服務 SpringBoot 入口類。
  11. resources:項目配置文件目錄。
  12. test:項目單元測試目錄。
  13. pom.xml:maven 項目對象模型文件。

三、實戰操作

3.1 前提

  • 確保本地安裝 Git、Java8、Maven。
  • 懂一些 SpringMVC 的知識,因為 SpringBoot 是基於 SpringMVC 演化而來的。
  • 懂一些應用容器引擎 Docker、Docker-compose 的知識。

3.2 微服務架構說明

一個完整的項目,微服務架構一般包括下面這些服務:

  • 註冊中心(常用的框架 Nacos、Eureka)
  • 統一網關(常用的框架 Gateway、Zuul)
  • 認證中心(常用技術實現方案 Jwt、OAuth)
  • 分佈式事務(常用的框架 Txlcn、Seata)
  • 文件服務
  • 業務服務

3.3 運行項目

下面介紹了三種運行的方式:

第一種:一鍵運行

Linux 和 Mac 系統下可在項目根目錄下執行 ./install.sh 快速搭建開發環境。

第二種:本地環境運行

不推薦此方法,但還是簡單介紹下。

  1. 基礎環境安裝:mysql、redis,rabbitmq

  2. 環境運行:
    git clone https://github.com/zhoutaoo/SpringCloud.git #克隆項目

  3. 安裝認證公共包到本地 maven 倉庫,執行如下命令:
    cd common mvn clean install #安裝認證公共包到本地 maven 倉庫

  4. 安裝註冊中心 Nacos
    • 下載
    • 執行如下命令:

      unzip nacos-server-0.9.0.zip  OR tar -xvf nacos-server-0.9.0.tar.gz
      cd nacos/bin
      bash startup.sh -m standalone # Linux 啟動命令
      cmd startup.cmd # Windows 啟動命令
  5. 運行網關服務、認證服務、業務服務等

這裏以網關服務為例:執行 GatewayAdminApplication.java

注意:認證服務(auth)、網關服務(gateway)、組織管理服務(sysadmin)需要執行數據庫初始化腳本。

可通過 swager 接口: 測試是否搭建成功,如果能正常訪問表示服務啟動成功。

說明:

  • application.yml 文件主要配置 rabbitmq,redis, mysql 的連接信息。

    spring:
      rabbitmq:
        host: ${RABBIT_MQ_HOST:localhost}
        port: ${RABBIT_MQ_PORT:5672}
        username: ${RABBIT_MQ_USERNAME:guest}
        password: ${RABBIT_MQ_PASSWORD:guest}
      redis:
        host: ${REDIS_HOST:localhost}
        port: ${REDIS_PORT:6379}
        #password: ${REDIS_PASSWORD:}
        lettuce:
          pool:
            max-active: 300
    
      datasource:
        driver-class-name: com.mysql.jdbc.Driver
        url: jdbc:${DATASOURCE_DBTYPE:mysql}://${DATASOURCE_HOST:localhost}:${DATASOURCE_PORT:3306}/sc_gateway?characterEncoding=UTF-8&useUnicode=true&useSSL=false
        username: ${DATASOURCE_USERNAME:root}
        password: ${DATASOURCE_PASSWORD:root123}
  • bootstrap.yml 文件主要配置服務基本信息(端口,服務名稱),註冊中心地址等。

    server:
      port: ${SERVER_PORT:8445}
    spring:
      application:
        name: gateway-admin
      cloud:
        nacos:
          discovery:
            server-addr: ${REGISTER_HOST:localhost}:${REGISTER_PORT:8848}
          config:
            server-addr: ${REGISTER_HOST:localhost}:${REGISTER_PORT:8848}
            file-extension: yml
        sentinel:
          transport:
            dashboard: ${SENTINEL_DASHBOARD_HOST:localhost}:${SENTINEL_DASHBOARD_PORT:8021}

第三種:Docker 環境運行

  1. 基礎環境安裝
    • 通過 docker 命令安裝

      # 安裝redis
      docker run -p 6379:6379 --name redis -d docker.io/redis:latest --requirepass "123456" 
      # 安裝mysql
      docker run --name mysql5.7 -p 3306:3306 -e MYSQL_ROOT_PASSWORD=root123 -d docker.io/mysql:5.7
      # 安裝rabbitmq 
      docker run -d -p 15672:15672 -p 5672:5672 -e RABBITMQ_DEFAULT_USER=admin -e RABBITMQ_DEFAULT_PASS=admin --name rabbitmq docker.io/rabbitmq:latest
    • 也可以通過 docker-compose 命令安裝

      cd docker-compose
      docker-compose up -d  #docker-compose 安裝mysql,redis,rabbitmq 服務
  2. 下載項目到本地
    git clone https://github.com/zhoutaoo/SpringCloud.git #克隆項目

  3. 安裝認證公共包到本地 maven 倉庫執行如下命令:
    cd common && mvn install #安裝認證公共包到本地maven倉庫

  4. docker-compose 運行 Nacos
    cd docker-compose docker-compose -f docker-compose.yml -f docker-compose.nacos.yml up -d nacos #啟動註冊中心

  5. 構建消息中心鏡像
    cd ./center/bus mvn package && mvn docker:build cd docker-compose #啟動消息中心 docker-compose -f docker-compose.yml -f docker-compose.center.yml up -d bus-server

需要構建鏡像的其他服務有:(注:操作和消息中心鏡像構建方式類似)

  • 網關管理服務 (gateway-admin、gateway-web)

  • 組織服務(sysadmin/organization)

  • 認證服務 (auth/authentication-server)

  • 授權服務(auth authorization-server)

  • 管理台服務(monitor/admin)

3.4 運行效果

Nacos 服務中心

所有服務都正常啟動,在 nacos 管理中心可查看,實例數表示運行此服務的個數,值為 1 可以理解為服務正常啟動。

查看後台服務

命令行執行:docker ps -a 查看 docker 所有進程信息

通過訪問微服務對外暴露的接口(swagger)檢測服務是否可用。

swager 接口地址:

測試如下圖:

四、最後

微服務(SpringBoot、SpringCloud、Docker)現在吵得特別火,它並不是一門新的技術,而是在老技術的基礎上衍生出來的,增加了一些新的特性。

教程至此,你應該能夠通過 SpringCloud 這項目快速搭建微服務了。那麼就可以開始你的微服務學習之旅了,是時候更新一下自己的技能樹了,讓我們一起來學習微服務吧!

五、參考資料

『講解開源項目系列』——讓對開源項目感興趣的人不再畏懼、讓開源項目的發起者不再孤單。跟着我們的文章,你會發現編程的樂趣、使用和發現參与開源項目如此簡單。歡迎留言聯繫我們、加入我們,讓更多人愛上開源、貢獻開源~

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

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

收購3c,收購IPHONE,收購蘋果電腦-詳細收購流程一覽表

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

※想要讓你的商品在網路上成為最夯、最多人討論的話題?

※高價收購3C產品,價格不怕你比較

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

讀寫分離很難嗎?springboot結合aop簡單就實現了

目錄

前言

入職新公司到現在也有一個月了,完成了手頭的工作,前幾天終於有時間研究下公司舊項目的代碼。在研究代碼的過程中,發現項目里用到了Spring Aop來實現數據庫的讀寫分離,本着自己愛學習(我自己都不信…)的性格,決定寫個實例工程來實現spring aop讀寫分離的效果。

環境部署

數據庫:MySql

庫數量:2個,一主一從

關於mysql的主從環境部署之前已經寫過文章介紹過了,這裏就不再贅述,參考

開始項目

首先,毫無疑問,先開始搭建一個SpringBoot工程,然後在pom文件中引入如下依賴:

<dependencies>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.10</version>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.3.2</version>
        </dependency>
        <dependency>
            <groupId>tk.mybatis</groupId>
            <artifactId>mapper-spring-boot-starter</artifactId>
            <version>2.1.5</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.16</version>
        </dependency>
        <!-- 動態數據源 所需依賴 ### start-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
            <scope>provided</scope>
        </dependency>
        <!-- 動態數據源 所需依賴 ### end-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.4</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
    </dependencies>

目錄結構

引入基本的依賴后,整理一下目錄結構,完成后的項目骨架大致如下:

建表

創建一張表user,在主庫執行sql語句同時在從庫生成對應的表數據

DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
  `user_id` bigint(20) NOT NULL COMMENT '用戶id',
  `user_name` varchar(255) DEFAULT '' COMMENT '用戶名稱',
  `user_phone` varchar(50) DEFAULT '' COMMENT '用戶手機',
  `address` varchar(255) DEFAULT '' COMMENT '住址',
  `weight` int(3) NOT NULL DEFAULT '1' COMMENT '權重,大者優先',
  `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '創建時間',
  `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新時間',
  PRIMARY KEY (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO `user` VALUES ('1196978513958141952', '測試1', '18826334748', '廣州市海珠區', '1', '2019-11-20 10:28:51', '2019-11-22 14:28:26');
INSERT INTO `user` VALUES ('1196978513958141953', '測試2', '18826274230', '廣州市天河區', '2', '2019-11-20 10:29:37', '2019-11-22 14:28:14');
INSERT INTO `user` VALUES ('1196978513958141954', '測試3', '18826273900', '廣州市天河區', '1', '2019-11-20 10:30:19', '2019-11-22 14:28:30');

主從數據源配置

application.yml,主要信息是主從庫的數據源配置

server:
  port: 8001
spring:
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: GMT+8
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    master:
      url: jdbc:mysql://127.0.0.1:3307/user?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&failOverReadOnly=false&useSSL=false&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true
      username: root
      password:
    slave:
      url: jdbc:mysql://127.0.0.1:3308/user?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&failOverReadOnly=false&useSSL=false&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true
      username: root
      password:

因為有一主一從兩個數據源,我們用枚舉類來代替,方便我們使用時能對應

@Getter
public enum DynamicDataSourceEnum {
    MASTER("master"),
    SLAVE("slave");
    private String dataSourceName;
    DynamicDataSourceEnum(String dataSourceName) {
        this.dataSourceName = dataSourceName;
    }
}

數據源配置信息類 DataSourceConfig,這裏配置了兩個數據源,masterDb和slaveDb

@Configuration
@MapperScan(basePackages = "com.xjt.proxy.mapper", sqlSessionTemplateRef = "sqlTemplate")
public class DataSourceConfig {
    
     // 主庫
      @Bean
      @ConfigurationProperties(prefix = "spring.datasource.master")
      public DataSource masterDb() {
  return DruidDataSourceBuilder.create().build();
      }

    /**
     * 從庫
     */
    @Bean
    @ConditionalOnProperty(prefix = "spring.datasource", name = "slave", matchIfMissing = true)
    @ConfigurationProperties(prefix = "spring.datasource.slave")
    public DataSource slaveDb() {
        return DruidDataSourceBuilder.create().build();
    }

    /**
     * 主從動態配置
     */
    @Bean
    public DynamicDataSource dynamicDb(@Qualifier("masterDb") DataSource masterDataSource,
        @Autowired(required = false) @Qualifier("slaveDb") DataSource slaveDataSource) {
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put(DynamicDataSourceEnum.MASTER.getDataSourceName(), masterDataSource);
        if (slaveDataSource != null) {
            targetDataSources.put(DynamicDataSourceEnum.SLAVE.getDataSourceName(), slaveDataSource);
        }
        dynamicDataSource.setTargetDataSources(targetDataSources);
        dynamicDataSource.setDefaultTargetDataSource(masterDataSource);
        return dynamicDataSource;
    }
    @Bean
    public SqlSessionFactory sessionFactory(@Qualifier("dynamicDb") DataSource dynamicDataSource) throws Exception {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setMapperLocations(
            new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/*Mapper.xml"));
        bean.setDataSource(dynamicDataSource);
        return bean.getObject();
    }
    @Bean
    public SqlSessionTemplate sqlTemplate(@Qualifier("sessionFactory") SqlSessionFactory sqlSessionFactory) {
        return new SqlSessionTemplate(sqlSessionFactory);
    }
    @Bean(name = "dataSourceTx")
    public DataSourceTransactionManager dataSourceTx(@Qualifier("dynamicDb") DataSource dynamicDataSource) {
        DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
        dataSourceTransactionManager.setDataSource(dynamicDataSource);
        return dataSourceTransactionManager;
    }
}

設置路由

設置路由的目的為了方便查找對應的數據源,我們可以用ThreadLocal保存數據源的信息到每個線程中,方便我們需要時獲取

public class DataSourceContextHolder {
    private static final ThreadLocal<String> DYNAMIC_DATASOURCE_CONTEXT = new ThreadLocal<>();
    public static void set(String datasourceType) {
        DYNAMIC_DATASOURCE_CONTEXT.set(datasourceType);
    }
    public static String get() {
        return DYNAMIC_DATASOURCE_CONTEXT.get();
    }
    public static void clear() {
        DYNAMIC_DATASOURCE_CONTEXT.remove();
    }
}

獲取路由

public class DynamicDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceContextHolder.get();
    }
}

AbstractRoutingDataSource的作用是基於查找key路由到對應的數據源,它內部維護了一組目標數據源,並且做了路由key與目標數據源之間的映射,提供基於key查找數據源的方法。

數據源的註解

為了可以方便切換數據源,我們可以寫一個註解,註解中包含數據源對應的枚舉值,默認是主庫,

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface DataSourceSelector {

    DynamicDataSourceEnum value() default DynamicDataSourceEnum.MASTER;
    boolean clear() default true;
}

aop切換數據源

到這裏,aop終於可以現身出場了,這裏我們定義一個aop類,對有註解的方法做切換數據源的操作,具體代碼如下:

@Slf4j
@Aspect
@Order(value = 1)
@Component
public class DataSourceContextAop {

 @Around("@annotation(com.xjt.proxy.dynamicdatasource.DataSourceSelector)")
    public Object setDynamicDataSource(ProceedingJoinPoint pjp) throws Throwable {
        boolean clear = true;
        try {
            Method method = this.getMethod(pjp);
            DataSourceSelector dataSourceImport = method.getAnnotation(DataSourceSelector.class);
            clear = dataSourceImport.clear();
            DataSourceContextHolder.set(dataSourceImport.value().getDataSourceName());
            log.info("========數據源切換至:{}", dataSourceImport.value().getDataSourceName());
            return pjp.proceed();
        } finally {
            if (clear) {
                DataSourceContextHolder.clear();
            }

        }
    }
    private Method getMethod(JoinPoint pjp) {
        MethodSignature signature = (MethodSignature)pjp.getSignature();
        return signature.getMethod();
    }

}

到這一步,我們的準備配置工作就完成了,下面開始測試效果。

先寫好Service文件,包含讀取和更新兩個方法,

@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    @DataSourceSelector(value = DynamicDataSourceEnum.SLAVE)
    public List<User> listUser() {
        List<User> users = userMapper.selectAll();
        return users;
    }

    @DataSourceSelector(value = DynamicDataSourceEnum.MASTER)
    public int update() {
        User user = new User();
        user.setUserId(Long.parseLong("1196978513958141952"));
        user.setUserName("修改后的名字2");
        return userMapper.updateByPrimaryKeySelective(user);
    }

    @DataSourceSelector(value = DynamicDataSourceEnum.SLAVE)
    public User find() {
        User user = new User();
        user.setUserId(Long.parseLong("1196978513958141952"));
        return userMapper.selectByPrimaryKey(user);
    }
}

根據方法上的註解可以看出,讀的方法走從庫,更新的方法走主庫,更新的對象是userId為1196978513958141953 的數據,

然後我們寫個測試類測試下是否能達到效果,

@RunWith(SpringRunner.class)
@SpringBootTest
class UserServiceTest {

    @Autowired
    UserService userService;

    @Test
    void listUser() {
        List<User> users = userService.listUser();
        for (User user : users) {
            System.out.println(user.getUserId());
            System.out.println(user.getUserName());
            System.out.println(user.getUserPhone());
        }
    }
    @Test
    void update() {
        userService.update();
        User user = userService.find();
        System.out.println(user.getUserName());
    }
}

測試結果:

1、讀取方法

2、更新方法

執行之後,比對數據庫就可以發現主從庫都修改了數據,說明我們的讀寫分離是成功的。當然,更新方法可以指向從庫,這樣一來就只會修改到從庫的數據,而不會涉及到主庫。

注意

上面測試的例子雖然比較簡單,但也符合常規的讀寫分離配置。值得說明的是,讀寫分離的作用是為了緩解寫庫,也就是主庫的壓力,但一定要基於數據一致性的原則,就是保證主從庫之間的數據一定要一致。如果一個方法涉及到寫的邏輯,那麼該方法里所有的數據庫操作都要走主庫

假設寫的操作執行完后數據有可能還沒同步到從庫,然後讀的操作也開始執行了,如果這個讀取的程序走的依然是從庫的話,那麼就會出現數據不一致的現象了,這是我們不允許的。

最後發一下項目的github地址,有興趣的同學可以看下,記得給個star哦

地址:

參考:

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

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

※高價3c回收,收購空拍機,收購鏡頭,收購 MACBOOK-更多收購平台討論專區

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

收購3c瘋!各款手機、筆電、相機、平板,歡迎來詢價!

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

京東物流出問題了?褥了30塊羊毛 & 淺析系統架構

本人親身經歷,但後續的流程分析都是個人猜測的,畢竟沒有實際做過這塊的業務。

訂單物流阻塞經過

火熱的雙11剛剛退去,截止今日,我在京東購買的礦泉水終於到貨啦,下單兩箱還只收到了一箱 🙁 ,從下單到收到貨過去了14天,足足兩周的時間。

我從11-20號開始與京東客服聯繫,直到11-25整個購物體驗才完成,也因為京東沒有按照約定重新發貨,算是補償了我3000個京豆。

朋友們,不會不知道京豆是幹啥的吧,100個京豆相當於一塊錢,1000個京豆相當於10塊錢,3000個京豆就是30塊錢。

可那不是現金有啥卵用,你不會不在京東購物吧,下單的時候就可以選擇用京豆來抵用一部分下單金額了。

所以一般購買商品后鼓勵你去評價,文字超過一定字數且上傳了購買商品的圖片,就能得到比如20個京豆。京豆積少成多,就可以下單抵用現金了。

廢話不多說,回到正題!

雙11我在京東下單,自營商品的訂單一般都是次日達,因為雙11物流緊張,所以下單后提示11-13日送達。

11-13日:

遺憾的是,11-13日並沒有如期送達,查看訂單物流,增加了一段溫馨提示:「由於銷售火爆,根據目前情況,訂單預計11月16日送達到您的手中」,額~,當然大家都能理解,原來京東商品這麼「火爆」,畢竟雙十一累積銷量2000億呢。

11-20日 周三:

問題是到了11-16日並沒有送達,我把這個訂單差點忘記了,11-20號突然想起來了這件事,上京東確認了訂單,才發現還是那個「銷售火爆…」的提示呀!竟然沒有給我送貨。。。

然後在線聯繫人工客服,說了一下情況,客服態度很好,兩箱水拆分下單后因已經拆分為了兩個訂單,有兩個訂單號,為了表示歉意,每個訂單號補了500個京豆,1000個京豆到手了。

然後,客服跟我說,已經給我催促倉儲發貨了,讓耐心等待一下,預計第二天就能到了。

11-21號 周四:

可惜到了第二天,並沒有像客服MM所說的那樣如期送達,反正已經晚了,心想也不差這一天兒,還贈送了京豆,再等等了…… 。

11-22號 周五:

然而,到了11-22號還是沒有配送物流通知,訂單中的分揀流程沒有完成,這是什麼操作??

當天繼續聯繫客服,問了是什麼原因,又來「話術」:小妹已為您催促正在發貨中,此時我有點懷疑了,可能這個流程本身就中斷了,需要人工來協助處理補單流程。

此刻,「客服的嘴,騙人的鬼」終於用到這了~

同時,京東客服升級來了個電話溝通,誠摯的表示歉意,說是倉儲這邊發貨有點問題,正在重新補貨中… ,預計明日就能送到,請注意查收!

11-23日 周六:

「客服的嘴,騙人的鬼」再一次用到這了~

周六仍然沒有收到貨,而且訂單里的物流配送流程一動也沒動~

11-23日 周日:

周日仍然沒有收到貨,而且訂單里的物流配送流程一動也沒動~

看來沒很好的注重用戶體驗嘛,再次在線聯繫客服,每次接線的不是同一個客服,所以每次都要求提供一下訂單號,很煩,此時很無語了,本用戶表示很生氣啊,自己查!

然後呢,客服又說已經重新補發貨了,並且這次竟然不給我大概的送貨時間點了,因為他不相信到底有沒有真的去補發貨操作了,補發貨這個操作多半客服是沒有權限的。

另外,解釋到因訂單延遲時間過長,又一次非常的抱歉,給申請了1000京豆,不過這次京豆並不是實時到賬的,需要經過審核流程。

11-25日 周一:

早上已經在地鐵上了,收到了京東快遞小哥的來電,但是只到了一個訂單的貨。查了一下另外一個訂單物流狀態仍然一動沒動 :(。

最後,客服專員再次電話聯繫,解釋到這個訂單給疏忽了,建議我重新下單,然後這個訂單走退款流程。並且再一次給予了1000京豆的補償 :)。

物流系統異常分析:

上述物流配送異常流程中,想了解故障原因,電話中我也有意識的去問一下客服,是不是某個環節有這樣的問題,但是從客服那裡只能給到說倉配流程有問題 ,具體他們也不是很清楚了,全都是針對用戶的話術,避免說錯話。

作為個技術人,通常得思考一下問題背後的原因:

  • 到底是哪個環節出現的問題
  • 出現這樣問題的原因
  • 對用戶的影響及應對方案
  • 如何能避免類似的問題發生
物流系統介紹

由此次問題引出,我還特意去查資料看了一下京東物流的系統架構演進過程。記得當時京東物流招人非常猛,作為一個內部非常重量級的項目投入了很多研發人力。

在2012年的時候京東內部開始對物流系統進行設計改造,那時訪問量應該還不算高,最初的系統還沒那麼複雜。新改造的物流系統:「青龍系統

青龍系統演進過程如上圖所示 ,它的系統發展至今,已經包含了分揀中心,運輸路由,終端,對外拓展,運營支持等五十餘個核心子系統,構建了完善的電商物流體系。

並且青龍系統中總結了一些最佳的實戰原則,如下所示:

這些系統設計原則我認為對任何系統都是通用的,值得我們一起學習的:

  • 高可用

選擇合適的架構方案;大系統小做,服務拆分;併發控制,服務隔離;灰度發布;全方位監控報警;核心服務,平滑降級。

  • 高性能

緩存和異步化,同步接口異步化設計;接口數據緩存化。

接口數據緩存化是非常重要的手段,對Redis緩存系統的很好的利用,構建了具有自己特色的緩存體系,很好的支撐了業務發展。同時,還發展了基於Redis的分佈式調度系統

  • 數據一致性

高實時性/高一致性,高實時性/低一致性,低實時性/高一致性,低實時性/低一致性。

針對具體的業務,可以匹配到具體的數據場景,找到對應的解決方案。要客觀的結合業務分析,選擇最適合的一致性方案,並不是高實時性/高一致性就好,成本是很更貴的。

  • 用戶體驗

東哥要求過任何人不能對用戶體驗提升的建議說No。用戶體驗主要遵循MVP原則和動態運營的原則。

MVP原則:也就是敏捷開發中的迭代思路。即快速迭代,核心需求線上,及時的反饋和改進。

動態運營:跟MVP原則強關聯,上線后收集並分析用戶數據,使得產品落地的設計符合用戶的需求,不符合設計要求的就要不斷的持續調整,是一個動態持續的過程。

物流分揀系統

簡單介紹完了物流系統的演進過程及架構原則,還是回到主題,到底是哪個環節出現了問題?

需要了解整個購物鏈路的各個環節:

用戶整個購物流程經過以上幾個關鍵的流程,已經生成訂單號並且已經支付了,流程到了訂單中心。

各個系統都是分佈式部署的,訂單中心會發送一個MQ消息給各個下游系統,積分系統增加積分京豆等,促銷系統發放優惠券等,倉儲系統接收到MQ消息進行處理,調用物流系統生成物流單,通知到配送站,由配送員送貨。

結合一個火爆的訂單,看一下訂單跟蹤過程:

該訂單在倉庫處理中已經打包完成,訂單在京東【北京李橋接貨倉庫】分揀完成。注意到了「分揀」二字,順便看了一下正常的訂單流程,會經過多個貨倉的分揀過程,最終會分揀到離用戶最近的貨倉。

所以,猜測,這筆訂單的問題就是在配送前的分揀系統處理過程中出現了異常情況。

青龍物流系統其中就包括了預分揀流程,如下所示:

當用戶下單后,首先必須是經過預分揀環節,但是根據最新的訂單跟蹤過程看,是先進行了倉庫打包處理,然後進入分揀流程。

分揀系統接收到訂單,根據不同的訂單進行規則匹配,分配站點,處理成功後生產包裹打印標籤。

訂單無法被正常分揀完成,將無法生成訂單:

想必我的訂單大概率就是在分揀環節出現了問題 ~

分揀系統的目標:

其中可用性要求是達到99.99%,4個9的可用率呢,看來很不幸啊,不可用的0.01%小概率事件偶發在了我的訂單上。

預分揀算法:

1、經驗值

只適用於同一個地址多次購買,依賴於第一妥投地址。

2、特徵值

需要提前人工維護關鍵字,依賴於關鍵字的準確性。

3、特殊配置

需要提前人工配置,依賴於該區域是否有特殊配置。

4、GIS

通過GIS技術精準的匹配地理位置。

上述都沒有匹配到,那麼只能走人工處理流程了。

預分揀系統架構:

訂單系統下發服務,默認會進入到預分揀系統,不同的訂單有不同的匹配規則,匹配規則使用開源的Drools來實現的,規則匹配完成,會按照預分揀算法匹配,優先匹配到離用戶最近的地址,返回自動預分揀的結果。

一旦回傳失敗,應該會有預警,需要人工介入來協助完成預分揀,將結果返回給訂單服務。

預分揀服務系統交互流程:

分揀服務使用Tomcat分佈式部署的Worker進程,完成后,將結果寫入到任務庫,回傳服務從人物庫抓取分揀結果回傳站點。

下圖來源於網絡,不是很清晰了:

其中預分揀服務接受訂單服務都是分佈式部署的,並且針對不同的訂單做了服務隔離,使用應用服務器是Tomcat;全文檢索使用的Solr,可能目前已改進為流行的ElasticSerach架構了;分佈式緩存使用了Redis集群;預分揀算法中的地址庫、特徵值、配置都對應了自己的Worker集群,也是做了服務隔離,每個服務分佈式部署,最終將結果寫入到MySQL數據庫中;預分揀回傳站點單獨的Worker集群,用來從數據庫抓取分揀數據,返回給用戶站點。

小結

經過以上過程猜測性分析,基本就清楚了自己的訂單問題出現的位置了。

大概率就是預分揀服務在某一個站點因為流量洪峰或異常出現了故障,可能服務恢復后沒有及時完成自動分揀數據校對。

與客服的溝通結果來看,當分揀過程出現問題后,可能並沒有及時預警並人工及時的去干預處理,導致分揀流程被阻塞,遲遲無法進入到分揀恢復階段。或許也是考慮到這種小概率事件,就由用戶來直接反饋,然後由人工介入處理。

但是,很明顯,客服用話術告知用戶結果,讓用戶耐心等待的同時。在後續的分揀系統訂單恢複流程並不是那麼順暢的,不一定是那麼簡單的人工直接快速處理,會經過一些校驗核對、人工審核等一系列流程,又或者讓技術人員協助恢復的,導致分揀流程流轉下去很慢,也進而影響了用戶體驗。

在線話術告知用戶結果算是A方案。

人工處理的第一筆訂單跟蹤:

而第二個訂單,客服根據情況執行了B方案,將問題升級到專員,電話聯繫用戶,建議用戶重新下單,並給予一定的補償。當你重新下單,分揀系統接收到新的訂單,就是進入了自動預分揀訂單處理過程了,自動化流程當然是很快的,無需人工干預。

總體來說,京東客服的做法可圈可點,整體售後服務流程較以前值得肯定,越來越完善。

同時,系統架構在未來方向上,肯定更趨向於更加的智能化,使用機器學習、人工智能等手段持續不斷優化物流的各環節,減少或避免小概率的事件發生。

ps:文章前半段真實發生,後半段僅作為問題分析參考。

歡迎關注我的公眾號,掃二維碼關注獲得更多精彩文章,與你一同成長~

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

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

平板收購,iphone手機收購,二手筆電回收,二手iphone收購-全台皆可收購

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

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

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

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

Web Scraper 翻頁——利用 Link 選擇器翻頁 | 簡易數據分析 14

這是簡易數據分析系列的第 14 篇文章。

今天我們還來聊聊 Web Scraper 翻頁的技巧。

這次的更新是受一位讀者啟發的,他當時想用 Web scraper 爬取一個分頁器分頁的網頁,卻發現我之前介紹的方法不管用。我研究了一下才發現我漏講了一種很常見的翻頁場景。

在 的文章里,我們講了如何利用 Element Click 選擇器模擬鼠標點擊分頁器進行翻頁,但是把同樣的方法放在 上,翻頁到第二頁時抓取窗口就會自動退出,一條數據都抓不到。

其實主要原因是我沒有講清楚這種方法的適用邊界。

通過 Element Click 點擊分頁器翻頁,只適用於網頁沒有刷新的情況,我在那篇文章里舉了蔡徐坤微博評論的例子,翻頁時網頁是沒有刷新的:

仔細看下圖,鏈接發生了變化,但是刷新按鈕並沒有變化,說明網頁並沒有刷新,只是內容變了

而在 豆瓣 TOP 250 的網頁里,每次翻頁都會重新加載網頁:

仔細看下圖,鏈接發生變化的同時網頁刷新了,有很明顯的 loading 轉圈動畫

其實這個原理從技術規範上很好解釋:當一個 URL 鏈接是 # 字符后數據變化時,網頁不會刷新;當鏈接其他部分變化時,網頁會刷新。當然這個只是隨口提一下,感興趣的同學可以去研究一下,不感興趣可以直接跳過。

1.創建 Sitemap

本篇文章就來講解一下,如何利用 Web Scraper 抓取翻頁時會刷新網頁的分頁器網站。

這次的網頁我們選用練手 Web Scraper 的網站——,換個姿勢練習 Web Scraper 翻頁技巧。

像這種類型的網站,我們要藉助 Link 選擇器來輔助我們翻頁。Link 標籤我們在介紹過了,我們可以利用這個標籤跳轉網頁,抓取另一個網頁的數據。這裏我們利用 Link 標籤跳轉到分頁網站的下一頁

首先我們用 Link 選擇器選擇下一頁按鈕,具體的配置可以見下圖:

這裡有一個比較特殊的地方:Parent Selectors ——父選擇器。

之前我們都沒有碰過這個選擇框的內容,**next_page 這次要有兩個父節點——_root 和 next_page**,鍵盤按 shift 再鼠標點選就可以多選了,先按我說的做,後面我會解釋這樣做的理由。

保存 next_page 選擇器后,在它的同級下再創建 container 節點,用來抓取電影數據:

這裏要注意:翻頁選擇器節點 next_page 和數據選擇器節點 container 是同一級,兩個節點的父節點都是兩個:_root 和 next_page:

因為重點是 web scraper 翻頁技巧,抓取的數據上我只簡單的抓取標題和排名:

然後我們點擊 Selector graph 查看我們編寫的爬蟲結構:

可以很清晰的看到這個爬蟲的結構,可以無限的嵌套下去:

點擊 Scrape,爬取一下試試,你會發現所有的數據都爬取下來了:

2.分析原理

按照上面的流程下來,你可能還會比較困擾,數據是抓下來了,但是為什麼這樣操作就可以呢,**為什麼 next_page 和 container 要同級,為什麼他們要同時選擇兩個父節點:_root 和 next_page?**

產生困擾的原因是因為我們是倒敘的講法,從結果倒推步驟;下面我們從正向的思維分步講解。

首先我們要知道,我們抓取的數據是一個樹狀結構,_root 表示根節點,就是我們的抓取的第一個網頁,我們在這個網頁要選擇什麼東西呢?

1.一個是下一頁的節點,在這個例子里就是用 Link 選擇器選擇的 next_page

2.一個是數據節點,在這個例子里就是用 Element 選擇器選擇的 container

因為 next_page 節點是會跳轉的,會跳到第二頁。第二頁除了數據不一樣,結構和第一頁還是一樣的,為了持續跳轉,我們還要選擇下一頁,為了抓取數據,還得選擇數據節點:

如果我們把箭頭反轉一下,就會發現真相就在眼前,next_page 的父節點,不正好就是 _root 和 next_page  嗎?container 的父節點,也是 _root 和 next_page!

到這裏基本就真相大白了,不理解的同學可以再多看幾遍。像 next_page 這種我調用我自己的形式,在編程里有個術語——遞歸,在計算機領域里也算一種比較抽象的概念,感興趣的同學可以自行搜索了解一下。

3.sitemap 分享

下面是這次實戰的 Sitemap,同學們可以導入到自己的 web scraper 中進行研究:

{"_id":"douban_movie_top_250","startUrl":["https://movie.douban.com/top250?start=0&filter="],"selectors":[{"id":"next_page","type":"SelectorLink","parentSelectors":["_root","next_page"],"selector":".next a","multiple":true,"delay":0},{"id":"container","type":"SelectorElement","parentSelectors":["_root","next_page"],"selector":".grid_view li","multiple":true,"delay":0}]}

4.推薦閱讀

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

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

收購3c,收購IPHONE,收購蘋果電腦-詳細收購流程一覽表

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

※想要讓你的商品在網路上成為最夯、最多人討論的話題?

※高價收購3C產品,價格不怕你比較

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