Redis詳解(十三)—— Redis布隆過濾器_網頁設計公司

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

以設計的實用美學觀點,規劃出舒適、美觀的視覺畫面,有效提昇使用者的心理期待,營造出輕鬆、愉悅的網站瀏覽體驗。

  本篇博客我們主要介紹如何用Redis實現布隆過濾器,但是在介紹布隆過濾器之前,我們首先介紹一下,為啥要使用布隆過濾器。

1、布隆過濾器使用場景

  比如有如下幾個需求:

  ①、原本有10億個號碼,現在又來了10萬個號碼,要快速準確判斷這10萬個號碼是否在10億個號碼庫中?

  解決辦法一:將10億個號碼存入數據庫中,進行數據庫查詢,準確性有了,但是速度會比較慢。

  解決辦法二:將10億號碼放入內存中,比如Redis緩存中,這裏我們算一下佔用內存大小:10億*8字節=8GB,通過內存查詢,準確性和速度都有了,但是大約8gb的內存空間,挺浪費內存空間的。

  ②、接觸過爬蟲的,應該有這麼一個需求,需要爬蟲的網站千千萬萬,對於一個新的網站url,我們如何判斷這個url我們是否已經爬過了?

  解決辦法還是上面的兩種,很顯然,都不太好。

  ③、同理還有垃圾郵箱的過濾。

  那麼對於類似這種,大數據量集合,如何準確快速的判斷某個數據是否在大數據量集合中,並且不佔用內存,布隆過濾器應運而生了。

2、布隆過濾器簡介

  帶着上面的幾個疑問,我們來看看到底什麼是布隆過濾器。

  布隆過濾器:一種數據結構,是由一串很長的二進制向量組成,可以將其看成一個二進制數組。既然是二進制,那麼裏面存放的不是0,就是1,但是初始默認值都是0。

  如下所示:

  

  ①、添加數據

  介紹概念的時候,我們說可以將布隆過濾器看成一個容器,那麼如何向布隆過濾器中添加一個數據呢?

  如下圖所示:當要向布隆過濾器中添加一個元素key時,我們通過多個hash函數,算出一個值,然後將這個值所在的方格置為1。

  比如,下圖hash1(key)=1,那麼在第2個格子將0變為1(數組是從0開始計數的),hash2(key)=7,那麼將第8個格子置位1,依次類推。

  

 

  ②、判斷數據是否存在?

  知道了如何向布隆過濾器中添加一個數據,那麼新來一個數據,我們如何判斷其是否存在於這個布隆過濾器中呢?

  很簡單,我們只需要將這個新的數據通過上面自定義的幾個哈希函數,分別算出各個值,然後看其對應的地方是否都是1,如果存在一個不是1的情況,那麼我們可以說,該新數據一定不存在於這個布隆過濾器中。

  反過來說,如果通過哈希函數算出來的值,對應的地方都是1,那麼我們能夠肯定的得出:這個數據一定存在於這個布隆過濾器中嗎?

  答案是否定的,因為多個不同的數據通過hash函數算出來的結果是會有重複的,所以會存在某個位置是別的數據通過hash函數置為的1。

  我們可以得到一個結論:布隆過濾器可以判斷某個數據一定不存在,但是無法判斷一定存在

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

台中景泰電動車行只是一個單純的理由,將來台灣的環境,出門可以自由放心的深呼吸,讓空氣回歸自然的乾淨,減少污染,留給我們下一代有好品質無空污的優質環境

  ③、布隆過濾器優缺點

  優點:優點很明顯,二進制組成的數組,佔用內存極少,並且插入和查詢速度都足夠快。

  缺點:隨着數據的增加,誤判率會增加;還有無法判斷數據一定存在;另外還有一個重要缺點,無法刪除數據。

3、Redis實現布隆過濾器

①、bitmaps

  我們知道計算機是以二進制位作為底層存儲的基礎單位,一個字節等於8位。

  比如“big”字符串是由三個字符組成的,這三個字符對應的ASCII碼分為是98、105、103,對應的二進制存儲如下:

  

 

 

  在Redis中,Bitmaps 提供了一套命令用來操作類似上面字符串中的每一個位。

  一、設置值

setbit key offset value

  

 

   我們知道”b”的二進製表示為0110 0010,我們將第7位(從0開始)設置為1,那0110 0011 表示的就是字符“c”,所以最後的字符 “big”變成了“cig”。

  二、獲取值

gitbit key offset

  

   三、獲取位圖指定範圍值為1的個數

bitcount key [start end]

  如果不指定,那就是獲取全部值為1的個數。

  注意:start和end指定的是字節的個數,而不是位數組下標。

  

②、Redisson

  Redis 實現布隆過濾器的底層就是通過 bitmap 這種數據結構,至於如何實現,這裏就不重複造輪子了,介紹業界比較好用的一個客戶端工具——Redisson。

  Redisson 是用於在 Java 程序中操作 Redis 的庫,利用Redisson 我們可以在程序中輕鬆地使用 Redis。

  下面我們就通過 Redisson 來構造布隆過濾器。

 1 package com.ys.rediscluster.bloomfilter.redisson;
 2 
 3 import org.redisson.Redisson;
 4 import org.redisson.api.RBloomFilter;
 5 import org.redisson.api.RedissonClient;
 6 import org.redisson.config.Config;
 7 
 8 public class RedissonBloomFilter {
 9 
10     public static void main(String[] args) {
11         Config config = new Config();
12         config.useSingleServer().setAddress("redis://192.168.14.104:6379");
13         config.useSingleServer().setPassword("123");
14         //構造Redisson
15         RedissonClient redisson = Redisson.create(config);
16 
17         RBloomFilter<String> bloomFilter = redisson.getBloomFilter("phoneList");
18         //初始化布隆過濾器:預計元素為100000000L,誤差率為3%
19         bloomFilter.tryInit(100000000L,0.03);
20         //將號碼10086插入到布隆過濾器中
21         bloomFilter.add("10086");
22 
23         //判斷下面號碼是否在布隆過濾器中
24         System.out.println(bloomFilter.contains("123456"));//false
25         System.out.println(bloomFilter.contains("10086"));//true
26     }
27 }

  這是單節點的Redis實現方式,如果數據量比較大,期望的誤差率又很低,那單節點所提供的內存是無法滿足的,這時候可以使用分佈式布隆過濾器,同樣也可以用 Redisson 來實現,這裏我就不做代碼演示了,大家有興趣可以試試。

4、guava 工具

  最後提一下不用Redis如何來實現布隆過濾器。

  guava 工具包相信大家都用過,這是谷歌公司提供的,裏面也提供了布隆過濾器的實現。

 1 package com.ys.rediscluster.bloomfilter;
 2 
 3 import com.google.common.base.Charsets;
 4 import com.google.common.hash.BloomFilter;
 5 import com.google.common.hash.Funnel;
 6 import com.google.common.hash.Funnels;
 7 
 8 public class GuavaBloomFilter {
 9     public static void main(String[] args) {
10         BloomFilter<String> bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8),100000,0.01);
11 
12         bloomFilter.put("10086");
13 
14         System.out.println(bloomFilter.mightContain("123456"));
15         System.out.println(bloomFilter.mightContain("10086"));
16     }
17 }

 

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

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

網站的第一印象網頁設計,決定了客戶是否繼續瀏覽的意願。台北網動廣告製作的RWD網頁設計,採用精簡與質感的CSS語法,提升企業的專業形象與簡約舒適的瀏覽體驗,讓瀏覽者第一眼就愛上它。

一本正經的聊數據結構(7):哈弗曼編碼_如何寫文案

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

什麼是銷售文案服務?A就是幫你撰寫適合的廣告文案。當您需要販售商品、宣傳活動、建立個人品牌,撰寫廣告文案都是必須的工作。

前文傳送門:

「一本正經的聊數據結構(1):時間複雜度」

「一本正經的聊數據結構(2):數組與向量」

「一本正經的聊數據結構(3):棧和隊列」

「一本正經的聊數據結構(4):樹」

「一本正經的聊數據結構(5):二叉樹的存儲結構與遍歷」

「一本正經的聊數據結構(6):最優二叉樹 —— 哈夫曼樹」

引言

在上一期,我們介紹了什麼是哈夫曼樹以及哈夫曼樹的構建過程,本期我們接着介紹哈夫曼樹的用途。

字符編碼壓縮

哈夫曼樹的應用很廣,哈夫曼編碼就是其在電訊通信中的應用之一。廣泛地用於數據文件壓縮的十分有效的編碼方法,其壓縮率通常在 20% ~ 90% 之間。

在電訊通信業務中,通常用二進制編碼來表示字母或其他字符,並用這樣的編碼來表示字符序列。

在計算機當中,因為計算機不是人,不能識別圖像、聲音、視頻等內容,對於計算機來講,它只能認識二進制的 0 和 1 ,在数字电子電路中,邏輯門的實現直接應用了二進制,因此現代的計算機和依賴計算機的設備里都用到二進制。

我們在計算機上看到的一切的圖像、聲音、視頻等內容,都是由二進制的方式進行存儲的。

簡單來講,我們把信息轉化為二進制的過程可以稱之為編碼,在計算機的世界里,編碼有很多種格式,比如我們常見的: ASCII 、 Unicode 、 GBK 、 GB2312 、 GB18030 、 UTF-8 、 UTF-16 等等。

編碼方式從長度上來分大致可以分為兩個大類:

  • 定長編碼:定長僅表明段與段之間長度相同,但沒說明是多長。
  • 變長編碼:變長就是段與段之間的長度不相同,同樣也不定義具體有多長。

在最初的設計中, ASCII 編碼就是採用的定長編碼的方案,使用定長一字節來表示一個字符。

舉個栗子,假如我們對 「hello」 進行編碼,使用定長編碼,為了方便,採用了十進制,主要是因為我懶,原理與二進制是一樣的。

字符 編碼
h 00
e 01
l 02
o 03

假設我們現在有個文件,內容是 00000001 ,假如定長 2 位(這裏的位指十進制的位)是唯一的編碼方案,用它去解碼,就會得到 「hhhe」 (可以對比上面的編碼, 00 代表 h ,所以前 6 個 0 轉化成 3 個 h ,後面的 01 則轉化成 e )。

但是,如果定長 2 位不是唯一的編碼方案呢?如上圖中的定長 4 位方案,如果我們誤用定長 4 位去解碼,結果就只能得到「he」( 0000 轉化為 h , 0001 轉化為 e )

隨着時代的發展,不僅老美要對他們的英文進行編碼,我們中國人也要對漢字進行編碼,而早期的 ASCII 碼的方案只有一個字節,對我們漢字而言是遠遠不夠的,所以在我們的漢字編碼方案 GB2312 中,漢字是使用兩個字節來表示的(這也是迫不得已的事,一字節壓根不夠用) 。

再多說一句,實際上我們的 GB2312 是一種變長的編碼方案,主要是為了兼容一個字節的 ASCII 碼。

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

擁有後台管理系統的網站,將擁有強大的資料管理與更新功能,幫助您隨時新增網站的內容並節省網站開發的成本。

隨着計算機在全世界的推廣,各種編碼方案都出來了,彼此之間的轉換也帶來了諸多的問題。採用某種統一的標準就勢在必行了,於是乎天上一聲霹靂, Unicode 粉墨登場!

不過 Unicode 對於只需要使用到一個字節的 ASCII 碼來講,讓他們使用 Unicode ,多少還是不是很願意的。

比如 「he」 兩個字符,用 ASCII 只需要保存成 6865 ( 16 進制),現在則成了 00680065 ,前面多的那兩個 0 (用作站位) ,基本上可以說是毫無意義,用 Unicode 編碼的文本,原來可能只有 1KB 大小,現在要變成 2KB ,體積成倍的往上漲。

最終, Unicode 編碼方案逐漸演化成了變長的 UTF-8 編碼方案,並且 UTF-8 是可以和 ASCII 碼進行兼容。

UTF-8 因為能兼容 ASCII 而受到廣泛歡迎,但在保存中文方面,要用 3 個字節,有的甚至要 4 個字節,所以在保存中文方面效率並不算太好,與此相對, GB2312 , GBK 之類用兩字節保存中文字符效率上會高,同時它們也都兼容 ASCII ,所以在中英混合的情況下還是比 UTF-8 要好,但在國際化方面及可擴展空間上則不如 UTF-8 了。

所以如果有進軍國際的想法,那麼最好還是使用 UTF-8 編碼。

哈弗曼編碼

哈弗曼編碼是一種不定長的編碼方式,是由麻省理工學院的哈夫曼博所發明,這種編碼方式實現了兩個重要目標:

  • 任何一個字符編碼,都不是其他字符編碼的前綴。

  • 信息編碼的總長度最小。

乾巴巴的,還是接着舉例子:

如果我們對 「ABACCDA」 進行編碼,假設 A, B, C, D 的編碼分別為 00, 01,10, 11。

那麼 「ABACCDA」 編碼后的結果是 「00010010101100」 (共 14 位),我們解碼的時候只需要每兩位進行拆分,就可以恢復編碼前的信息了。

那我們如果用哈弗曼編碼的方式進行編碼呢?

第一件事情是要確定每個字母的權值(出現頻率), 「ABACCDA」 這個字符串中 A, B, C, D 的權值(出現的頻率)分別為 0.43, 0.14, 0.29, 0.14 。

有了權值,我們可以構造一個哈弗曼樹了,感興趣的同學可以自己畫一下,下面這個是我畫的:

編碼的結果就顯而易見了: A:0, C:10, B:110, D:111 。

剛才那個 「ABACCDA」 編碼后的結果就是 「0110010101110」 (共 13 位)。

上面我們知道了哈夫曼編碼如何編碼,那麼我們拿到了一個經過哈弗曼編碼后的代碼,如何進行譯碼工作呢?

首先還是要知道每個字母的權重是多少,然後畫出來這個哈弗曼樹,接下來,就可以對照着這個哈弗曼樹進行譯碼工作了。

在譯碼的過程中,若編碼是 「0」 ,則向左走。若編碼是 「1」 ,則向右走,一旦到達恭弘=叶 恭弘子結點,則譯出一個字符。然後不停的重複,直到這個編碼的結束,就是我們需要的原內容了。

參考

https://www.cnblogs.com/wkfvawl/p/9783271.html

https://my.oschina.net/goldenshaw/blog/307708

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

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

銷售文案是什麼?A文案是廣告用的文字。舉凡任何宣傳、行銷、販賣商品時所用到的文字都是文案。在網路時代,文案成為行銷中最重要的宣傳方式,好的文案可節省大量宣傳資源,達成行銷目的。

重學 Java 設計模式:實戰適配器模式_網頁設計公司

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

RWD(響應式網頁設計)是透過瀏覽器的解析度來判斷要給使用者看到的樣貌

作者:小傅哥
博客:https://bugstack.cn

沉澱、分享、成長,讓自己和他人都能有所收穫!

一、前言

擦屁屁紙80%的面積都是保護手的!

工作到3年左右很大一部分程序員都想提升自己的技術棧,開始嘗試去閱讀一些源碼,例如SpringMybaitsDubbo等,但讀着讀着發現越來越難懂,一會從這過來一會跑到那去。甚至懷疑自己技術太差,慢慢也就不願意再觸碰這部分知識。

而這主要的原因是一個框架隨着時間的發展,它的複雜程度是越來越高的,從最開始只有一個非常核心的點到最後開枝散恭弘=叶 恭弘。這就像你自己開發的業務代碼或者某個組件一樣,最開始的那部分核心代碼也許只能佔到20%,而其他大部分代碼都是為了保證核心流程能正常運行的。所以這也是你讀源碼費勁的一部分原因。

框架中用到了設計模式嗎?

框架中不僅用到設計模式還用了很多,而且有些時候根本不是一個模式的單獨使用,而是多種設計模式的綜合運用。與大部分小夥伴平時開發的CRUD可就不一樣了,如果都是if語句從上到下,也就算得不上什麼框架了。就像你到Spring的源碼中搜關鍵字Adapter,就會出現很多實現類,例如;UserCredentialsDataSourceAdapter。而這種設計模式就是我們本文要介紹的適配器模式。

適配器在生活里隨處可見

如果提到在日常生活中就很多適配器的存在你會想到什麼?在沒有看後文之前可以先思考下。

二、開發環境

  1. JDK 1.8
  2. Idea + Maven
  3. 涉及工程三個,可以通過關注公眾號bugstack蟲洞棧,回復源碼下載獲取(打開獲取的鏈接,找到序號18)
工程 描述
itstack-demo-design-6-00 場景模擬工程;模擬多個MQ消息體
itstack-demo-design-6-01 使用一坨代碼實現業務需求
itstack-demo-design-6-02 通過設計模式優化改造代碼,產生對比性從而學習

三、適配器模式介紹

適配器模式的主要作用就是把原本不兼容的接口,通過適配修改做到統一。使得用戶方便使用,就像我們提到的萬能充、數據線、MAC筆記本的轉換頭、出國旅遊買個插座等等,他們都是為了適配各種不同的,做的兼容。。

除了我們生活中出現的各種適配的場景,那麼在業務開發中呢?

在業務開發中我們會經常的需要做不同接口的兼容,尤其是中台服務,中台需要把各個業務線的各種類型服務做統一包裝,再對外提供接口進行使用。而這在我們平常的開發中也是非常常見的。

四、案例場景模擬

隨着公司的業務的不斷髮展,當基礎的系統逐步成型以後。業務運營就需要開始做用戶的拉新和促活,從而保障DAU的增速以及最終ROI轉換。

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

透過資料庫的網站架設建置,建立公司的形象或購物系統,並提供最人性化的使用介面,讓使用者能即時接收到相關的資訊

而這時候就會需要做一些營銷系統,大部分常見的都是裂變、拉客,例如;你邀請一個用戶開戶、或者邀請一個用戶下單,那麼平台就會給你返利,多邀多得。同時隨着拉新的量越來越多開始設置每月下單都會給首單獎勵,等等,各種營銷場景。

那麼這個時候做這樣一個系統就會接收各種各樣的MQ消息或者接口,如果一個個的去開發,就會耗費很大的成本,同時對於後期的拓展也有一定的難度。此時就會希望有一個系統可以配置一下就把外部的MQ接入進行,這些MQ就像上面提到的可能是一些註冊開戶消息、商品下單消息等等。

而適配器的思想方式也恰恰可以運用到這裏,並且我想強調一下,適配器不只是可以適配接口往往還可以適配一些屬性信息。

1. 場景模擬工程

itstack-demo-design-6-00
└── src
    └── main
        └── java
            └── org.itstack.demo.design
                ├── mq
                │   ├── create_account.java
                │   ├── OrderMq.java
                │   └── POPOrderDelivered.java
                └── service
                    ├── OrderServicejava
                    └── POPOrderService.java
  • 這裏模擬了三個不同類型的MQ消息,而在消息體中都有一些必要的字段,比如;用戶ID、時間、業務ID,但是每個MQ的字段屬性並不一樣。就像用戶ID在不同的MQ里也有不同的字段:uId、userId等。
  • 同時還提供了兩個不同類型的接口,一個用於查詢內部訂單訂單下單數量,一個用於查詢第三方是否首單。
  • 後面會把這些不同類型的MQ和接口做適配兼容。

2. 場景簡述

1.1 註冊開戶MQ

public class create_account {

    private String number;      // 開戶編號
    private String address;     // 開戶地
    private Date accountDate;   // 開戶時間
    private String desc;        // 開戶描述

    // ... get/set     
}

1.2 內部訂單MQ

public class OrderMq {

    private String uid;           // 用戶ID
    private String sku;           // 商品
    private String orderId;       // 訂單ID
    private Date createOrderTime; // 下單時間     

    // ... get/set      
}

1.3 第三方訂單MQ

public class POPOrderDelivered {

    private String uId;     // 用戶ID
    private String orderId; // 訂單號
    private Date orderTime; // 下單時間
    private Date sku;       // 商品
    private Date skuName;   // 商品名稱
    private BigDecimal decimal; // 金額

    // ... get/set      
}

1.4 查詢用戶內部下單數量接口

public class OrderService {

    private Logger logger = LoggerFactory.getLogger(POPOrderService.class);

    public long queryUserOrderCount(String userId){
        logger.info("自營商家,查詢用戶的訂單是否為首單:{}", userId);
        return 10L;
    }

}

1.5 查詢用戶第三方下單首單接口

public class POPOrderService {

    private Logger logger = LoggerFactory.getLogger(POPOrderService.class);

    public boolean isFirstOrder(String uId) {
        logger.info("POP商家,查詢用戶的訂單是否為首單:{}", uId);
        return true;
    }

}
  • 以上這幾項就是不同的MQ以及不同的接口的一個體現,後面我們將使用這樣的MQ消息和接口,給它們做相應的適配。

五、用一坨坨代碼實現

其實大部分時候接MQ消息都是創建一個類用於消費,通過轉換他的MQ消息屬性給自己的方法。

我們接下來也是先體現一下這種方式的實現模擬,但是這樣的實現有一個很大的問題就是,當MQ消息越來越多后,甚至幾十幾百以後,你作為中台要怎麼優化呢?

1. 工程結構

itstack-demo-design-6-01
└── src
    └── main
        └── java
            └── org.itstack.demo.design
                └── create_accountMqService.java
                └── OrderMqService.java
                └── POPOrderDeliveredService.java
  • 目前需要接收三個MQ消息,所有就有了三個對應的類,和我們平時的代碼幾乎一樣。如果你的MQ量不多,這樣的寫法也沒什麼問題,但是隨着數量的增加,就需要考慮用一些設計模式來解決。

2. Mq接收消息實現

public class create_accountMqService {

    public void onMessage(String message) {

        create_account mq = JSON.parseObject(message, create_account.class);

        mq.getNumber();
        mq.getAccountDate();

        // ... 處理自己的業務
    }

}
  • 三組MQ的消息都是一樣模擬使用,就不一一展示了。可以獲取源碼後學習。

六、適配器模式重構代碼

接下來使用適配器模式來進行代碼優化,也算是一次很小的重構。

適配器模式要解決的主要問題就是多種差異化類型的接口做統一輸出,這在我們學習工廠方法模式中也有所提到不同種類的獎品處理,其實那也是適配器的應用。

在本文中我們還會再另外體現出一個多種MQ接收,使用MQ的場景。來把不同類型的消息做統一的處理,便於減少後續對MQ接收。

在這裏如果你之前沒要開發過接收MQ消息,可能聽上去會有些不理解這樣的場景。對此,我個人建議先了解下MQ。另外就算不了解也沒關係,不會影響對思路的體會。

再者,本文所展示的MQ兼容的核心部分,也就是處理適配不同的類型字段。而如果我們接收MQ后,在配置不同的消費類時,如果不希望一個個開發類,那麼可以使用代理類的方式進行處理。

1. 工程結構

itstack-demo-design-6-02
└── src
    └── main
        └── java
            └── org.itstack.demo.design
                ├── impl
                │   ├── InsideOrderService.java
                │   └── POPOrderAdapterServiceImpl.java
                ├── MQAdapter,java
                ├── OrderAdapterService,java
                └── RebateInfo,java

適配器模型結構

  • 這裏包括了兩個類型的適配;接口適配、MQ適配。之所以不只是模擬接口適配,因為很多時候大家都很常見了,所以把適配的思想換一下到MQ消息體上,增加大家多設計模式的認知。
  • 先是做MQ適配,接收各種各樣的MQ消息。當業務發展的很快,需要對下單用戶首單才給獎勵,在這樣的場景下再增加對接口的適配操作。

2. 代碼實現(MQ消息適配)

2.1 統一的MQ消息體

public class RebateInfo {

    private String userId;  // 用戶ID
    private String bizId;   // 業務ID
    private Date bizTime;   // 業務時間
    private String desc;    // 業務描述
    
    // ... get/set
}
  • MQ消息中會有多種多樣的類型屬性,雖然他們都有同樣的值提供給使用方,但是如果都這樣接入那麼當MQ消息特別多時候就會很麻煩。
  • 所以在這個案例中我們定義了通用的MQ消息體,後續把所有接入進來的消息進行統一的處理。

2.2 MQ消息體適配類

public class MQAdapter {

    public static RebateInfo filter(String strJson, Map<String, String> link) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        return filter(JSON.parseObject(strJson, Map.class), link);
    }

    public static RebateInfo filter(Map obj, Map<String, String> link) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        RebateInfo rebateInfo = new RebateInfo();
        for (String key : link.keySet()) {
            Object val = obj.get(link.get(key));
            RebateInfo.class.getMethod("set" + key.substring(0, 1).toUpperCase() + key.substring(1), String.class).invoke(rebateInfo, val.toString());
        }
        return rebateInfo;
    }

}
  • 這個類里的方法非常重要,主要用於把不同類型MQ種的各種屬性,映射成我們需要的屬性並返回。就像一個屬性中有用戶ID;uId,映射到我們需要的;userId,做統一處理。
  • 而在這個處理過程中需要把映射管理傳遞給Map<String, String> link,也就是準確的描述了,當前MQ中某個屬性名稱,映射為我們的某個屬性名稱。
  • 最終因為我們接收到的mq消息基本都是json格式,可以轉換為MAP結構。最後使用反射調用的方式給我們的類型賦值。

2.3 測試適配類

2.3.1 編寫單元測試類
@Test
public void test_MQAdapter() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
    create_account create_account = new create_account();
    create_account.setNumber("100001");
    create_account.setAddress("河北省.廊坊市.廣陽區.大學里職業技術學院");
    create_account.setAccountDate(new Date());
    create_account.setDesc("在校開戶");          

    HashMap<String, String> link01 = new HashMap<String, String>();
    link01.put("userId", "number");
    link01.put("bizId", "number");
    link01.put("bizTime", "accountDate");
    link01.put("desc", "desc");
    RebateInfo rebateInfo01 = MQAdapter.filter(create_account.toString(), link01);
    System.out.println("mq.create_account(適配前)" + create_account.toString());
    System.out.println("mq.create_account(適配后)" + JSON.toJSONString(rebateInfo01));

    System.out.println("");

    OrderMq orderMq = new OrderMq();
    orderMq.setUid("100001");
    orderMq.setSku("10928092093111123");
    orderMq.setOrderId("100000890193847111");
    orderMq.setCreateOrderTime(new Date()); 

    HashMap<String, String> link02 = new HashMap<String, String>();
    link02.put("userId", "uid");
    link02.put("bizId", "orderId");
    link02.put("bizTime", "createOrderTime");
    RebateInfo rebateInfo02 = MQAdapter.filter(orderMq.toString(), link02);

    System.out.println("mq.orderMq(適配前)" + orderMq.toString());
    System.out.println("mq.orderMq(適配后)" + JSON.toJSONString(rebateInfo02));
}
  • 在這裏我們分別模擬傳入了兩個不同的MQ消息,並設置字段的映射關係。
  • 等真的業務場景開發中,就可以配這種映射配置關係交給配置文件或者數據庫後台配置,減少編碼。
2.3.2 測試結果
mq.create_account(適配前){"accountDate":1591024816000,"address":"河北省.廊坊市.廣陽區.大學里職業技術學院","desc":"在校開戶","number":"100001"}
mq.create_account(適配后){"bizId":"100001","bizTime":1591077840669,"desc":"在校開戶","userId":"100001"}

mq.orderMq(適配前){"createOrderTime":1591024816000,"orderId":"100000890193847111","sku":"10928092093111123","uid":"100001"}
mq.orderMq(適配后){"bizId":"100000890193847111","bizTime":1591077840669,"userId":"100001"}

Process finished with exit code 0
  • 從上面可以看到,同樣的字段值在做了適配前後分別有統一的字段屬性,進行處理。這樣業務開發中也就非常簡單了。
  • 另外有一個非常重要的地方,在實際業務開發中,除了反射的使用外,還可以加入代理類把映射的配置交給它。這樣就可以不需要每一個mq都手動創建類了。

3. 代碼實現(接口使用適配)

就像我們前面提到隨着業務的發展,營銷活動本身要修改,不能只是接了MQ就發獎勵。因為此時已經拉新的越來越多了,需要做一些限制。

因為增加了只有首單用戶才給獎勵,也就是你一年或者新人或者一個月的第一單才給你獎勵,而不是你之前每一次下單都給獎勵。

那麼就需要對此種方式進行限制,而此時MQ中並沒有判斷首單的屬性。只能通過接口進行查詢,而拿到的接口如下;

接口 描述
org.itstack.demo.design.service.OrderService.queryUserOrderCount(String userId) 出參long,查詢訂單數量
org.itstack.demo.design.service.OrderService.POPOrderService.isFirstOrder(String uId) 出參boolean,判斷是否首單
  • 兩個接口的判斷邏輯和使用方式都不同,不同的接口提供方,也有不同的出參。一個是直接判斷是否首單,另外一個需要根據訂單數量判斷。
  • 因此這裏需要使用到適配器的模式來實現,當然如果你去編寫if語句也是可以實現的,但是我們經常會提到這樣的代碼很難維護。

3.1 定義統一適配接口

public interface OrderAdapterService {

    boolean isFirst(String uId);

}
  • 後面的實現類都需要完成此接口,並把具體的邏輯包裝到指定的類中,滿足單一職責。

3.2 分別實現兩個不同的接口

內部商品接口

public class InsideOrderService implements OrderAdapterService {

    private OrderService orderService = new OrderService();

    public boolean isFirst(String uId) {
        return orderService.queryUserOrderCount(uId) <= 1;
    }

}

第三方商品接口

public class POPOrderAdapterServiceImpl implements OrderAdapterService {

    private POPOrderService popOrderService = new POPOrderService();

    public boolean isFirst(String uId) {
        return popOrderService.isFirstOrder(uId);
    }

}
  • 在這兩個接口中都實現了各自的判斷方式,尤其像是提供訂單數量的接口,需要自己判斷當前接到mq時訂單數量是否<= 1,以此判斷是否為首單。

3.3 測試適配類

3.3.1 編寫單元測試類
@Test
public void test_itfAdapter() {
    OrderAdapterService popOrderAdapterService = new POPOrderAdapterServiceImpl();
    System.out.println("判斷首單,接口適配(POP):" + popOrderAdapterService.isFirst("100001"));   

    OrderAdapterService insideOrderService = new InsideOrderService();
    System.out.println("判斷首單,接口適配(自營):" + insideOrderService.isFirst("100001"));
}
3.3.2 測試結果
23:25:47.076 [main] INFO  o.i.d.design.service.POPOrderService - POP商家,查詢用戶的訂單是否為首單:100001
判斷首單,接口適配(POP):true
23:25:47.079 [main] INFO  o.i.d.design.service.POPOrderService - 自營商家,查詢用戶的訂單是否為首單:100001
判斷首單,接口適配(自營):false

Process finished with exit code 0
  • 從測試結果上來看,此時已經的接口已經做了統一的包裝,外部使用時候就不需要關心內部的具體邏輯了。而且在調用的時候只需要傳入統一的參數即可,這樣就滿足了適配的作用。

七、總結

  • 從上文可以看到不使用適配器模式這些功能同樣可以實現,但是使用了適配器模式就可以讓代碼:乾淨整潔易於維護、減少大量重複的判斷和使用、讓代碼更加易於維護和拓展。
  • 尤其是我們對MQ這樣的多種消息體中不同屬性同類的值,進行適配再加上代理類,就可以使用簡單的配置方式接入對方提供的MQ消息,而不需要大量重複的開發。非常利於拓展。
  • 設計模式的學習學習過程可能會在一些章節中涉及到其他設計模式的體現,只不過不會重點講解,避免喧賓奪主。但在實際的使用中,往往很多設計模式是綜合使用的,並不會單一出現。

八、推薦閱讀

  • 1. 重學 Java 設計模式:實戰工廠方法模式(多種類型商品發獎場景)
  • 2. 重學 Java 設計模式:實戰抽象工廠模式(替換Redis雙集群升級場景)
  • 3. 重學 Java 設計模式:實戰建造者模式(裝修物料組合套餐選配場景)
  • 4. 重學 Java 設計模式:實戰原型模式(多套試每人題目和答案亂序場景)
  • 5. 重學 Java 設計模式:實戰單例模式(Effective Java 作者推薦枚舉單例模式)

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

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

當全世界的人們隨著網路時代而改變向上時您還停留在『網站美醜不重要』的舊有思維嗎?機會是留給努力改變現況的人們,別再浪費一分一秒可以接觸商機的寶貴時間!

吐血整理全網最全的單例模式_網頁設計

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

窩窩以「數位行銷」「品牌經營」「網站與應用程式」「印刷品設計」等四大主軸,為每一位客戶客製建立行銷脈絡及洞燭市場先機。

前言

之前文章已經說過了設計模式的七大原則,即接口屏蔽原則,開閉原則,依賴倒轉原則,迪米特原則,里氏替換原則,單一職責原則,合成復用原則,不明白的,可以移至萬字總結之設計模式七大原則(https://www.cnblogs.com/chenchen0618/p/12434603.html)。從今天開始我們就要學習一些常見的設計模式,方便我們以後看源碼使用,當然,也可以指導我們平常的編碼任務。

我們常見的設計模式主要有23種,分為3種類型,咱也不全說,只寫重要的幾個把。

創建型:單例模式,工廠模式,原型模式

結構型:適配器模式,裝飾模式,代理模式

行為型:模板模式,觀察者模式,狀態模式,責任鏈模式

單例模式的概念和作用

概念

系統中只需要一個全局的實例,比如一些工具類,Converter,SqlSession等。

為什麼要用單例模式?

  • 只有一個全局的實例,減少了內存開支,特別是某個對象需要頻繁的創建和銷毀的時候,而創建和銷毀的過程由jvm執行,我們無法對其進行優化,所以單例模式的優勢就顯現出來啦。
  • 單例模式可以避免對資源的多重佔用,避免出現多線程的複雜問題。

單例模式的寫法重點

構造方法私有化

我們需要將構造方法私有化,而默認不寫的話,是公有的構造方法,外部可以顯式的調用來創建對象,我們的目的是讓外部不能創建對象。

提供獲取實例的公有方法

對外只提供一個公有的的方法,用來獲取實例,而這個實例是否是唯一的,單例的,由方法決定,外部無需關心。

單例模式的常見寫法(如下,重點)

餓漢式和懶漢式的區別

餓漢式

餓漢式,從名字上也很好理解,就是“比較餓”,迫不及待的想吃飯,實例在初始化的時候就已經建好了,不管你有沒有用到,都先建好了再說。

懶漢式

餓漢式,從名字上也很好理解,就是“比較懶”,不想吃飯,等餓的時候再吃。在初始化的時候先不建好對象,如果之後用到了,再創建對象。

1.餓漢式(靜態變量)–可以使用

A類

public class A {
    //私有的構造方法
    private A(){}
    //私有的靜態變量
    private final static A a=new A();
    //對外的公有方法
    public static A getInstance(){
        return a;
    }
}

 

測試類

public class test {
    public static void main(String[] args){
        A a1=A.getInstance();
        System.out.println(a1.hashCode());

        A a2=A.getInstance();
        System.out.println(a2.hashCode());
    }
}

 

運行結果

說明

該方法採用的靜態常量的方法來生成對應的實例,其只在類加載的時候就生成了,後續並不會再生成,所以其為單例的。

優點

在類加載的時候,就完成實例化,避免線程同步問題。

缺點

沒有達到懶加載的效果,如果從始到終都沒有用到這個實例,可能會導致內存的浪費。

2.餓漢式(靜態代碼塊)–可以使用

A類

public class A { 
    //私有的構造方法
     private A(){}
    //私有的靜態變量
     private final static A a; 
    //靜態代碼塊 
    static{ a=new A(); } 
    //對外的公有方法
    public static A getInstance(){
     return a; 
    }
}

 

測試類

public class test {
    public static void main(String[] args){
        A a1=A.getInstance();
        System.out.println(a1.hashCode());

        A a2=A.getInstance();
        System.out.println(a2.hashCode());
    }
}

 

運行結果

說明

該靜態代碼塊的餓漢式單例模式與靜態變量的餓漢式模式大同小異,只是將初始化過程移到了靜態代碼塊中。

優點缺點

與靜態變量餓漢式的優缺點類似。

3.懶漢式

A類

public class A {
    //私有的構造方法
    private A(){}
    //私有的靜態變量
    private  static A a;
    //對外的公有方法
    public static A getInstance(){
        if(a==null){
            a=new A();
        }
        return a;
    }
}

 

測試類和運行結果

同上。

優點

該方法的確做到了用到即加載,也就是當調用getInstance的時候,才判斷是否有該對象,如果不為空,則直接放回,如果為空,則新建一個對象並返回,達到了懶加載的效果。

缺點

當多線程的時候,可能會產生多個實例。比如我有兩個線程,同時調用getInstance方法,並都到了if語句,他們都新建了對象,那這裏就不是單例的啦。

4.懶漢式(線程安全,同步方法)–可以使用

public class A {
    //私有的構造方法
    private A(){}
    //私有的靜態變量
    private  static A a;
    //對外的公有方法
    public synchronized static A getInstance(){
        if(a==null){
            a=new A();
        }
        return a;
    }
}

 

測試類和運行結果

同上。

優點

通過synchronize關鍵字,解決了線程不安全的問題。如果兩個線程同時調用getInstance方法時,那就先執行一個線程,另一個等待,等第一個線程運行結束了,另一個等待的開始執行。

缺點

這種方法是解決了線程不安全的問題,卻給性能帶來了很大的問題,效率太低了,getInstance經常發生,每一次都要同步這個方法。

我們想着既然是方法同步導致了性能的問題,我們核心的代碼就是新建對象的過程,也就是new A();的過程,我們能不能只對部分代碼進行同步呢?

那就是方法5啦。

5.懶漢式(線程不安全)

A類

public class A {
    //私有的構造方法
    private A(){}
    //私有的靜態變量
    private  static A a;
    public  static A getInstance(){
        if(a==null){
            synchronized (A.class){
                a=new A();
            }
        }
        return a;
    }
} 

 

測試類和運行結果

如上。

優點

懶漢式的通用優點,用到才創建,達到懶加載的效果。

缺點

這個沒有意義,並沒有解決多線程的問題。我們可以看到如果兩個線程同時調用getInstance方法,並且都已經進入了if語句,即synchronized的位置,即便同步了,第一個線程先執行,進入synchronized同步的代碼塊,創建了對象,另一個進入等待狀態,等第一個線程執行結束,第二個線程還是會進入synchronized同步的代碼塊,創建對象。這個時候我們可以發現,對這代碼塊加了synchronized沒有任何意義,還是創建了多個對象,並不符合單例。

6.雙重檢查 –強烈推薦使用

A類

public class A {
    //私有的構造方法
    private A() {
    }

    //私有的靜態變量
    private volatile static A a;

    //對外的公有方法
    public static A getInstance() {
        if (a == null) {
            synchronized (A.class) {
                if (a == null) {
                    a = new A();
                }
            }
        }
        return a;
    }
} 

 

測試類和運行結果

同上。

優點

強烈推薦使用,這種寫法既避免了在多線程中出現線程不安全的情況,也能提高性能。

咱具體來說,如果兩個線程同時調用了getInstance方法,並且都已到達了if語句之後,synchronized語句之前,此時第一個線程進入synchronized之中,先判斷是否為空,很顯然第一次肯定為空,那麼則新建了對象。等到第二個線程進入synchronized之中,先判斷是否為空,顯然第一個已經創建了,所以即不新建對象。下次,不管是一個線程或者多個線程,在第一個if語句那就判斷出有對象了,便直接返回啦,根本進不了裏面的代碼。

缺點

就是這麼完美,沒有缺點,哈哈哈。

volatile(插曲)

咱先來看一個概念,重排序,也就是語句的執行順序會被重新安排。其主要分為三種:

1.編譯器優化的重排序:可以重新安排語句的執行順序。

2.指令級并行的重排序:現代處理器採用指令級并行技術,將多條指令重疊執行。

3.內存系統的重排序:由於處理器使用緩存和讀寫緩衝區,所以看上去可能是亂序的。

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

網動是一群專業、熱情、向前行的工作團隊,我們擁有靈活的組織與溝通的能力,能傾聽客戶聲音,激發創意的火花,呈現完美的作品

上面代碼中的a = new A();可能被被JVM分解成如下代碼:

// 可以分解為以下三個步驟
1 memory=allocate();// 分配內存 相當於c的malloc
2 ctorInstanc(memory) //初始化對象
3 s=memory //設置s指向剛分配的地址
 // 上述三個步驟可能會被重排序為 1-3-2,也就是:
1 memory=allocate();// 分配內存 相當於c的malloc
3 s=memory //設置s指向剛分配的地址
2 ctorInstanc(memory) //初始化對象

 

一旦假設發生了這樣的重排序,比如線程A在執行了步驟1和步驟3,但是步驟2還沒有執行完。這個時候線程B有進入了第一個if語句,它會判斷a不為空,即直接返回了a。其實這是一個未初始化完成的a,即會出現問題。

所以我們會將入volatile關鍵字,來禁止這樣的重排序,即可正常運行。

7.靜態內部類 –強烈推薦使用

A類

public class A {
    //私有構造函數
    private A() {
    }

    //私有的靜態內部類
    private static class B {
        //私有的靜態變量
        private static A a = new A();
    }

    //對外的公有方法
    public static A getInstance() {
        return B.a;
    }
}

 

 

優點

B在A裝載的時候並不會裝載,而是會在調用getInstance的時候裝載,這利用了JVM的裝載機制。這樣一來,優點有兩點,其一就是沒有A加載的時候,就裝載了a對象,而是在調用的時候才裝載,避免了資源的浪費。其二是多線程狀態下,沒有線程安全性的問題。

缺點

沒有缺點,太完美啦。

8.枚舉 –Java粑粑強烈推薦使用

問題1:私有構造器並不安全

如果不明白反射,可以查看我之前的文章,傳送門,萬字總結之反射(框架之魂)。

如果我們的對象是通過反射方法invoke出來,這樣新建的對象與通過調用getInstance新建的對象是不一樣的,具體咱來看代碼。

 

public class test {
    public static void main(String[] args) throws Exception {
        A a=A.getInstance();
        A b=A.getInstance();
        System.out.println("a的hash:"+a.hashCode()+",b的hash:"+b.hashCode());

        Constructor<A> constructor=A.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        A c=constructor.newInstance();
        System.out.println("a的hash:"+a.hashCode()+",c的hash:"+c.hashCode());

    }
}

 

我們來看下運行結果:

我們可以看到c的hashcode是和a,b不一樣,因為c是通過構造器反射出來的,由此可以證明私有構造器所組成的單例模式並不是十分安全的。

問題2:序列化問題

我們先將A類實現一個Serializable接口,具體代碼如下,跟之前的雙重if檢查一樣,只是多了個接口。

 

public class A implements Serializable {
    //私有的構造方法
    private A() {
    }

    //私有的靜態變量
    private volatile static A a;

    //對外的公有方法
    public static A getInstance() {
        if (a == null) {
            synchronized (A.class) {
                if (a == null) {
                    a = new A();
                }
            }
        }
        return a;
    }
} 

 

測試類:

public class test {
    public static void main(String[] args) throws Exception {
        A s = A.getInstance();

        //
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("學習Java的小姐姐"));
        oos.writeObject(s);
        oos.flush();
        oos.close();
        //
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("學習Java的小姐姐"));
        A s1 = (A)ois.readObject();
        ois.close();

        System.out.println(s+"\n"+s1);
        System.out.println("序列化前後兩個是否同一個:"+(s==s1));
    }
}

 

我們來看下運行結果,很顯然序列化前後兩個對象並不相等。為什麼會出現這種問題呢?這個講起來,又可以寫一篇文章了。簡單來說,任何一個readObject方法,不管是顯式的還是默認的,它都會返回一個新建的實例,這個新建的實例不同於該類初始化時創建的實例。

A類

public enum A {
  a;
  public A getInstance(){
      return a;
  }
}

 

看着代碼量很少,我們將其編譯下,代碼如下:

public final class  A extends Enum< A> {      
public static final A a;
public static A[] values();
public static AvalueOf(String s);
static {}; }

 

如何解決問題1?

public class test {
    public static void main(String[] args) throws Exception {
        A a1 = A.a;
        A a2 = A.a;
        System.out.println("正常情況下,實例化兩個實例是否相同:" + (a1 == a2));

        Constructor<A> constructor = null;
        constructor = A.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        A a3 = null;
        a3 = constructor.newInstance();
        System.out.println("a1的hash:" + a1.hashCode() + ",a2的hash:" + a2.hashCode() + ",a3的hash:" + a3.hashCode());
        System.out.println("通過反射攻擊單例模式情況下,實例化兩個實例是否相同:" + (a1 == a3));
    }
}

 

運行結果:

我們看到報錯了,是在尋找構造函數的時候報錯的,即沒有無參的構造方法,那我們看下他繼承的父類ENUM有沒有構造函數,看下源碼,發現有個兩個參數String和int類型的構造方法,我們再看下是不是構造方法的問題。

我們再用父類的有參構造方法試下,代碼如下:

public class test {
    public static void main(String[] args) throws Exception {
        A a1 = A.a;
        A a2 = A.a;
        System.out.println("正常情況下,實例化兩個實例是否相同:" + (a1 == a2));
        Constructor<A> constructor = null;
        constructor = A.class.getDeclaredConstructor(String.class,int.class);//其父類的構造器
        constructor.setAccessible(true);
        A a3 = null;
        a3 = constructor.newInstance("學習Java的小姐姐",1);
        System.out.println("a1的hash:" + a1.hashCode() + ",a2的hash:" + a2.hashCode() + ",a3的hash:" + a3.hashCode());
        System.out.println("通過反射攻擊單例模式情況下,實例化兩個實例是否相同:" + (a1 == a3));
    }
}

運行結果如下:

我們發現報錯信息的位置已經換了,現在是已經有構造方法,而是在newInstance方法的時候報錯了,我們跟下源碼發現,人家已經明確寫明了如果是枚舉類型,直接拋出異常,代碼如下,所以是無法使用反射來操作枚舉類型的數據的。

如何解決問題2?

public class test {
    public static void main(String[] args) throws Exception {
        A s = A.a;

        //
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("學習Java的小姐姐"));
        oos.writeObject(s);
        oos.flush();
        oos.close();
        //
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("學習Java的小姐姐"));
        A s1 = (A)ois.readObject();
        ois.close();

        System.out.println(s+"\n"+s1);
        System.out.println("序列化前後兩個是否同一個:"+(s==s1));
    }
}

 

運行結果;

優點

避免了反射帶來的對象不一致問題和反序列問題,簡單來說,就是簡單高效沒問題。

結語

看到這裏的都是真愛的,在這裏先謝謝各位大佬啦。

單例模式是最簡單的一種設計模式,主要包括八種形式,分別是餓漢式靜態變量,餓漢式靜態代碼塊,懶漢式線程不安全,懶漢式線程安全,懶漢式線程不安全(沒啥意義),懶漢式雙重否定線程安全,內部靜態類,枚舉類型。

這幾種最優的是枚舉類型和內部靜態類,其次是懶漢式雙重否定,剩下的都差不多啦。

如果有說的不對的地方,還請各位指正,我好繼續學習去。

參考資料

一個單例模式中volatile關鍵字引發的思考

 

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

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

擁有專業的維修技術團隊,同時聘請資深iphone手機維修專家,現場說明手機問題,快速修理,沒修好不收錢

Angular 從入坑到挖坑 – 路由守衛連連看_貨運

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

搬家價格與搬家費用透明合理,不亂收費。本公司提供下列三種搬家計費方案,由資深專業組長到府估價,替客戶量身規劃選擇最經濟節省的計費方式

一、Overview

Angular 入坑記錄的筆記第六篇,介紹 Angular 路由模塊中關於路由守衛的相關知識點,了解常用到的路由守衛接口,知道如何通過實現路由守衛接口來實現特定的功能需求,以及實現對於特性模塊的惰性加載

對應官方文檔地址:

  • 路由與導航

配套代碼地址:angular-practice/src/router-combat

二、Contents

  1. Angular 從入坑到棄坑 – Angular 使用入門
  2. Angular 從入坑到挖坑 – 組件食用指南
  3. Angular 從入坑到挖坑 – 表單控件概覽
  4. Angular 從入坑到挖坑 – HTTP 請求概覽
  5. Angular 從入坑到挖坑 – Router 路由使用入門指北
  6. Angular 從入坑到挖坑 – 路由守衛連連看

三、Knowledge Graph

四、Step by Step

4.1、基礎準備

重複上一篇筆記的內容,搭建一個包含路由配置的 Angualr 項目

新建四個組件,分別對應於三個實際使用到的頁面與一個設置為通配路由的 404 頁面

-- 危機中心頁面
ng g component crisis-list

-- 英雄中心頁面
ng g component hero-list

-- 英雄相親頁面
ng g component hero-detail

-- 404 頁面
ng g component page-not-found 

在 app-routing.module.ts 文件中完成對於項目路由的定義,這裏包含了對於路由的重定向、通配路由,以及通過動態路由進行參數傳遞的使用

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

// 引入組件
import { CrisisListComponent } from './crisis-list/crisis-list.component';
import { HeroListComponent } from './hero-list/hero-list.component';
import { HeroDetailComponent } from './hero-detail/hero-detail.component';
import { PageNotFoundComponent } from './page-not-found/page-not-found.component';

const routes: Routes = [
  {
    path: 'crisis-center',
    component: CrisisListComponent,
  },
  {
    path: 'heroes',
    component: HeroListComponent,
  },
  {
    path: 'hero/:id',
    component: HeroDetailComponent,
  },
  {
    path: '',
    redirectTo: '/heroes',
    pathMatch: 'full',
  },
  {
    path: '**',
    component: PageNotFoundComponent,
  }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule],
})
export class AppRoutingModule { }

之後,在根組件中,添加 router-outlet 標籤用來聲明路由在頁面上渲染的出口

<h1>Angular Router</h1>
<nav>
  <a routerLink="/crisis-center" routerLinkActive="active">Crisis Center</a> &nbsp;&nbsp;
  <a routerLink="/heroes" routerLinkActive="active">Heroes</a>
</nav>
<router-outlet></router-outlet>

4.2、路由守衛

在 Angular 中,路由守衛主要可以解決以下的問題

  • 對於用戶訪問頁面的權限校驗(是否已經登錄?已經登錄的角色是否有權限進入?)
  • 在跳轉到組件前獲取某些必須的數據
  • 離開頁面時,提示用戶是否保存未提交的修改

Angular 路由模塊提供了如下的幾個接口用來幫助我們解決上面的問題

  • CanActivate:用來處理系統跳轉到到某個路由地址的操作(判斷是否可以進行訪問)
  • CanActivateChild:功能同 CanActivate,只不過針對的是子路由
  • CanDeactivate:用來處理從當前路由離開的情況(判斷是否存在未提交的信息)
  • CanLoad:是否允許通過延遲加載的方式加載某個模塊

在添加了路由守衛之後,通過路由守衛返回的值,從而達到我們控制路由的目的

  • true:導航將會繼續
  • false:導航將會中斷,用戶停留在當前的頁面或者是跳轉到指定的頁面
  • UrlTree:取消當前的導航,並導航到路由守衛返回的這個 UrlTree 上(一個新的路由信息)
4.2.1、CanActivate:認證授權

在實現路由守衛之前,可以通過 Angular CLI 來生成路由守衛的接口實現類,通過命令行,在 app/auth 路徑下生成一個授權守衛類,CLI 會提示我們選擇繼承的路由守衛接口,這裏選擇 CanActivate 即可

ng g guard auth/auth

在 AuthGuard 這個路由守衛類中,我們模擬了是否允許訪問一個路由地址的認證授權。首先判斷是否已經登錄,如果登錄后再判斷當前登錄人是否具有當前路由地址的訪問權限

import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree, Router } from '@angular/router';
import { Observable } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class AuthGuard implements CanActivate {

  /**
   * ctor
   * @param router 路由
   */
  constructor(private router: Router) { }

  canActivate(
    next: ActivatedRouteSnapshot,
    state: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {

    // 判斷是否有 token 信息
    let token = localStorage.getItem('auth-token') || '';
    if (token === '') {
      this.router.navigate(['/login']);
      return false;
    }

    // 判斷是否可以訪問當前連接
    let url: string = state.url;
    if (token === 'admin' && url === '/crisis-center') {
      return true;
    }

    this.router.navigate(['/login']);
    return false;
  }
}

之後我們就可以在 app-routing.module.ts 文件中引入 AuthGuard 類,針對需要保護的路由進行路由守衛的配置

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

// 引入組件
import { CrisisListComponent } from './crisis-list/crisis-list.component';

// 引入路由守衛
import { AuthGuard } from './auth/auth.guard';

const routes: Routes = [
  {
    path: 'crisis-center',
    component: CrisisListComponent,
    canActivate: [AuthGuard], // 添加針對當前路由的 canActivate 路由守衛
  }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule],
})
export class AppRoutingModule { }

4.2.2、CanActivateChild:針對子路由的認證授權

與繼承 CanActivate 接口進行路由守衛的方式相似,針對子路由的認證授權可以通過繼承 CanActivateChild 接口來實現,因為授權的邏輯很相似,這裏通過多重繼承的方式,擴展 AuthGuard 的功能,從而達到同時針對路由和子路由的路由守衛

改造下原先 canActivate 方法的實現,將認證邏輯修改為用戶的 token 信息中包含 admin 即可訪問 crisis-center 頁面,在針對子路由進行認證授權的 canActivateChild 方法中,通過判斷 token 信息是否為 admin-master 模擬完成對於子路由的訪問認證

import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree, Router, CanActivateChild } from '@angular/router';
import { Observable } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class AuthGuard implements CanActivate, CanActivateChild {

  /**
   * ctor
   * @param router 路由
   */
  constructor(private router: Router) { }

  canActivate(
    next: ActivatedRouteSnapshot,
    state: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {

    // 判斷是否有 token 信息
    let token = localStorage.getItem('auth-token') || '';
    if (token === '') {
      this.router.navigate(['/login']);
      return false;
    }

    // 判斷是否可以訪問當前連接
    let url: string = state.url;
    if (token.indexOf('admin') !== -1 && url.indexOf('/crisis-center') !== -1) {
      return true;
    }

    this.router.navigate(['/login']);
    return false;
  }

  canActivateChild(
    childRoute: ActivatedRouteSnapshot,
    state: RouterStateSnapshot): boolean | UrlTree | Observable<boolean | UrlTree> | Promise<boolean | UrlTree> {
    let token = localStorage.getItem('auth-token') || '';
    if (token === '') {
      this.router.navigate(['/login']);
      return false;
    }

    return token === 'admin-master';
  }
}

通過 Angular CLI 新增一個 crisis-detail 組件,作為 crisis-list 的子組件

ng g component crisis-detail

接下來在 crisis-list 中添加 router-outlet 標籤,用來定義子路由的渲染出口

<h2>危機中心</h2>

<ul class="crises">
  <li *ngFor="let crisis of crisisList">
    <a [routerLink]="[crisis.id]">
      <span class="badge">{{ crisis.id }}</span>{{ crisis.name }}
    </a>
  </li>
</ul>

<!-- 定義子路由的渲染出口 -->
<router-outlet></router-outlet>

在針對子路由的認證授權配置時,我們可以選擇針對每個子路由添加 canActivateChild 屬性,也可以定義一個空地址的子路由,將所有歸屬於 crisis-list 的子路由作為這個空路由的子路由,通過針對這個空路徑添加 canActivateChild 屬性,從而實現將守護規則應用到所有的子路由上

※回頭車貨運收費標準

宇安交通關係企業,自成立迄今,即秉持著「以誠待人」、「以實處事」的企業信念

這裏其實相當於將原先兩級的路由模式(父:crisis-list,子:crisis-detail)改成了三級(父:crisis-list,子:’ ‘(空路徑),孫:crisis-detail)

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

// 引入組件
import { CrisisListComponent } from './crisis-list/crisis-list.component';
import { CrisisDetailComponent } from './crisis-detail/crisis-detail.component';

// 引入路由守衛
import { AuthGuard } from './auth/auth.guard';

const routes: Routes = [
  {
    path: 'crisis-center',
    component: CrisisListComponent,
    canActivate: [AuthGuard], // 添加針對當前路由的 canActivate 路由守衛
    children: [{
      path: '',
      canActivateChild: [AuthGuard], // 添加針對子路由的 canActivate 路由守衛
      children: [{
        path: 'detail',
        component: CrisisDetailComponent
      }]
    }]
  }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule],
})
export class AppRoutingModule { }

4.2.3、CanDeactivate:處理用戶未提交的修改

當進行表單填報之類的操作時,因為會涉及到一個提交的動作,當用戶沒有點擊保存按鈕就離開時,最好能暫停,對用戶進行一個友好性的提示,由用戶選擇後續的操作

創建一個路由守衛,繼承於 CanDeactivate 接口

ng g guard hero-list/guards/hero-can-deactivate

與上面的 CanActivate、CanActivateChild 路由守衛的使用方式不同,對於 CanDeactivate 守衛來說,我們需要將參數中的 unknown 替換成我們實際需要進行路由守衛的組件

import { Injectable } from '@angular/core';
import { CanDeactivate, ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree } from '@angular/router';
import { Observable } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class HeroCanDeactivateGuard implements CanDeactivate<unknown> {
  canDeactivate(
    component: unknown,
    currentRoute: ActivatedRouteSnapshot,
    currentState: RouterStateSnapshot,
    nextState?: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
    return true;
  }
  
}

例如,這裏針對的是 HeroListComponent 這個組件,因此我們需要將泛型的參數 unknown 改為 HeroListComponent,通過 component 參數,就可以獲得需要進行路由守衛的組件的相關信息

import { Injectable } from '@angular/core';
import {
  CanDeactivate,
  ActivatedRouteSnapshot,
  RouterStateSnapshot,
  UrlTree,
} from '@angular/router';
import { Observable } from 'rxjs';

// 引入需要進行路由守衛的組件
import { HeroListComponent } from '../hero-list.component';

@Injectable({
  providedIn: 'root',
})
export class HeroCanDeactivateGuard
  implements CanDeactivate<HeroListComponent> {
  canDeactivate(
    component: HeroListComponent,
    currentRoute: ActivatedRouteSnapshot,
    currentState: RouterStateSnapshot,
    nextState?: RouterStateSnapshot
  ):
    | Observable<boolean | UrlTree>
    | Promise<boolean | UrlTree>
    | boolean
    | UrlTree {

    // 判斷是否修改了原始數據
    //
    const data = component.hero;
    if (data === undefined) {
      return true;
    }
    const origin = component.heroList.find(hero => hero.id === data.id);
    if (data.name === origin.name) {
      return true;
    }

    return window.confirm('內容未提交,確認離開?');
  }
}

這裏模擬判斷用戶有沒有修改原始的數據,當用戶修改了數據並移動到別的頁面時,觸發路由守衛,提示用戶是否保存后再離開當前頁面

4.3、異步路由

4.3.1、惰性加載

當應用逐漸擴大,使用現有的加載方式會造成應用在第一次訪問時就加載了全部的組件,從而導致系統首次渲染過慢。因此這裏可以使用惰性加載的方式在請求具體的模塊時才加載對應的組件

惰性加載只針對於特性模塊(NgModule),因此為了使用惰性加載這個功能點,我們需要將系統按照功能劃分,拆分出一個個獨立的模塊

首先通過 Angular CLI 創建一個危機中心模塊(crisis 模塊)

-- 查看創建模塊的相關參數
ng g module --help

-- 創建危機中心模塊(自動在 app.moudule.ts 中引入新創建的 CrisisModule、添加當前模塊的路由配置)
ng g module crisis --module app --routing

將 crisis-list、crisis-detail 組件全部移動到 crisis 模塊下面,並在 CrisisModule 中添加對於 crisis-list、crisis-detail 組件的聲明,同時將原來在 app.module.ts 中聲明的組件代碼移除

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';

import { CrisisRoutingModule } from './crisis-routing.module';

import { FormsModule } from '@angular/forms';

// 引入模塊中使用到的組件
import { CrisisListComponent } from './crisis-list/crisis-list.component';
import { CrisisDetailComponent } from './crisis-detail/crisis-detail.component';


@NgModule({
  declarations: [
    CrisisListComponent,
    CrisisDetailComponent
  ],
  imports: [
    CommonModule,
    FormsModule,
    CrisisRoutingModule
  ]
})
export class CrisisModule { }

同樣的,將當前模塊的路由配置移動到專門的路由配置文件 crisis-routing.module.ts 中,並將 app-routing.module.ts 中相關的路由配置刪除

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

// 引入組件
import { CrisisListComponent } from './crisis-list/crisis-list.component';
import { CrisisDetailComponent } from './crisis-detail/crisis-detail.component';

// 引入路由守衛
import { AuthGuard } from '../auth/auth.guard';

const routes: Routes = [{
  path: '',
  component: CrisisListComponent,
  canActivate: [AuthGuard], // 添加針對當前路由的 canActivate 路由守衛
  children: [{
    path: '',
    canActivateChild: [AuthGuard], // 添加針對子路由的 canActivate 路由守衛
    children: [{
      path: 'detail',
      component: CrisisDetailComponent
    }]
  }]
}];

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule]
})
export class CrisisRoutingModule { }

重新運行項目,如果你在創建模塊的命令中設置了自動引入當前模塊到 app.module.ts 文件中,大概率會遇到下面的問題

這裏的問題與配置通配路由需要放到最後的原因相似,因為腳手架在幫我們將創建的模塊導入到 app.module.ts 中時,是添加到整個數組的最後,同時因為我們已經將 crisis 模塊的路由配置移動到專門的 crisis-routing.module.ts 中了,框架在進行路由匹配時會預先匹配上 app-routing.module.ts 中設置的通配路由,從而導致無法找到實際應該對應的組件,因此這裏我們需要將 AppRoutingModule 放到聲明的最後

當問題解決后,就可以針對 crisis 模塊設置惰性加載

在配置惰性路由時,我們需要以一種類似於子路由的方式進行配置,通過路由的 loadChildren 屬性來加載對應的模塊,而不是具體的組件,修改后的 AppRoutingModule 代碼如下

import { HeroCanDeactivateGuard } from './hero-list/guards/hero-can-deactivate.guard';
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

const routes: Routes = [
  {
    path: 'crisis-center',
    loadChildren: () => import('./crisis/crisis.module').then(m => m.CrisisModule)
  }
];

@NgModule({
  imports: [RouterModule.forRoot(routes, { enableTracing: true })],
  exports: [RouterModule],
})
export class AppRoutingModule { }

當導航到這個 /crisis-center 路由時,框架會通過 loadChildren 字符串來動態加載 CrisisModule,然後把 CrisisModule 添加到當前的路由配置中,而惰性加載和重新配置工作只會發生一次,也就是在該路由首次被請求時執行,在後續請求時,該模塊和路由都是立即可用的

4.3.2、CanLoad:杜絕未通過認證授權的組件加載

在上面的代碼中,對於 CrisisModule 模塊我們已經使用 CanActivate、CanActivateChild 路由守衛來進行路由的認證授權,但是當我們並沒有權限訪問該路由的權限,卻依然點擊了鏈接時,此時框架路由仍會加載該模塊。為了杜絕這種授權未通過仍加載模塊的問題發生,這裏需要使用到 CanLoad 守衛

因為這裏的判斷邏輯與認證授權的邏輯相同,因此在 AuthGuard 中,繼承 CanLoad 接口即可,修改后的 AuthGuard 代碼如下

import { Injectable } from '@angular/core';
import {
  CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree, Router, CanActivateChild, CanLoad, Route, UrlSegment
} from '@angular/router';
import { Observable } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class AuthGuard implements CanActivate, CanActivateChild, CanLoad {

  /**
   * ctor
   * @param router 路由
   */
  constructor(private router: Router) { }


  canActivate(
    next: ActivatedRouteSnapshot,
    state: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {

    // 判斷是否有 token 信息
    let token = localStorage.getItem('auth-token') || '';
    if (token === '') {
      this.router.navigate(['/login']);
      return false;
    }

    // 判斷是否可以訪問當前連接
    let url: string = state.url;
    if (token.indexOf('admin') !== -1 && url.indexOf('/crisis-center') !== -1) {
      return true;
    }

    this.router.navigate(['/login']);
    return false;
  }

  canActivateChild(
    childRoute: ActivatedRouteSnapshot,
    state: RouterStateSnapshot): boolean | UrlTree | Observable<boolean | UrlTree> | Promise<boolean | UrlTree> {
    let token = localStorage.getItem('auth-token') || '';
    if (token === '') {
      this.router.navigate(['/login']);
      return false;
    }

    return token === 'admin-master';
  }

  canLoad(route: Route, segments: UrlSegment[]): boolean | Observable<boolean> | Promise<boolean> {
    let token = localStorage.getItem('auth-token') || '';
    if (token === '') {
      this.router.navigate(['/login']);
      return false;
    }

    let url = `/${route.path}`;

    if (token.indexOf('admin') !== -1 && url.indexOf('/crisis-center') !== -1) {
      return true;
    }
  }
}

同樣的,針對路由守衛的實現完成后,將需要使用到的路由守衛添加到 crisis-center 路由的 canLoad 數組中即可實現授權認證不通過時不加載模塊

import { HeroCanDeactivateGuard } from './hero-list/guards/hero-can-deactivate.guard';
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

const routes: Routes = [
  {
    path: 'crisis-center',
    loadChildren: () => import('./crisis/crisis.module').then(m => m.CrisisModule)
  }
];

@NgModule({
  imports: [RouterModule.forRoot(routes, { enableTracing: true })],
  exports: [RouterModule],
})
export class AppRoutingModule { }

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

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

網動結合了許多網際網路業界的菁英共同研發簡單易操作的架站工具,及時性的更新,為客戶創造出更多的網路商機。

從聚合支付業務的設計來聊聊策略模式_網頁設計公司

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

台中景泰電動車行只是一個單純的理由,將來台灣的環境,出門可以自由放心的深呼吸,讓空氣回歸自然的乾淨,減少污染,留給我們下一代有好品質無空污的優質環境

六月福利

2020年6月公眾號碼農小胖哥原創文章轉發第一名將送全新《Spring Boot實戰》實體書一本,該書是學習熱門框架 Spring Boot的經典之作。你不再需要依靠運氣,而是勤奮。截止統計日期2020年6月30日,統計數據以官方公眾號工具為準,運營人員不參加活動,本次活動圖書由掘金社區贊助。

1. 前言

前幾天講了設計模式中的命令模式,今天來看看另一個模式。移動支付目前在國內已經是非常普及了,連樓下早餐攤的七十多歲大媽也使用支付寶和微信支付賣雞蛋餅。如果讓你做一個App你肯定要考慮多個渠道支付,以保證獲客渠道。如果讓你來接入多種支付渠道你會怎麼設計?

2. 通常寫法

一般下面這種寫法很容易被創造出來:

    public boolean pay(BigDecimal amount){
        
        boolean ret =false;
        if (alipay){
            //todo 支付寶的邏輯
        }else if (wechatpay){
            //todo 微信支付的邏輯
        }else if (ooxx){
           // …… 
        }
        return ret;
    }

如果集成了四五種支付,這個代碼就沒法看了少說幾千行,而且改動某個支付的邏輯很容易改了其它支付的邏輯。因此需要合理的設計來避免這種風險。

3. 策略模式

大部分的支付可以簡化為這個流程:

中間的發起支付前邏輯支付后處理邏輯是客戶端的自定義業務邏輯,向支付服務器發送的請求只會攜帶對應支付服務器特定要求的參數調用不同的支付SDK。所以我們分別建立對應支付方式的策略來隔離區分它們,降低它們的耦合度。當準備支付時我們只需要選擇對應的策略就可以了。

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

以設計的實用美學觀點,規劃出舒適、美觀的視覺畫面,有效提昇使用者的心理期待,營造出輕鬆、愉悅的網站瀏覽體驗。

這就用到了設計模式中的策略模式:

結合上面的類圖,我們就來結合著需求來聊聊策略模式中的主要幾個角色。

  • Strategy接口。這個接口用來聲明每一種方式的獨立執行策略,用來封裝具體策略的特有算法邏輯。
  • ConcreteStrategyStrategy的實現類。實現了不同策略的算法邏輯。比如每種支付的調用細節等等。
  • Context上下文。它通過策略接口來引用了具體的策略並使用具體的策略來執行邏輯,同時所有策略的共性也可以在該類中進行統一處理。在聚合支付需求中我們傳入一個策略,先執行支付前的邏輯,然後使用策略,策略執行完畢后,再執行後置的共性邏輯。
  • Client客戶端。創建策略對象並傳遞給上下文Context,然後由上下文運行具體的策略。

結合業務邏輯是這樣的:請求到達客戶端,客戶端根據請求中包含的支付渠道來構建對應的策略對象並把它交給上下文對象去執行支付流程。

然後我們就可以分別為支付寶、微信支付、銀聯支付構造三個策略對象 AliPayStrategyWechatPayStrategyUnionPayStrategy ,我們來模擬一下執行策略:

public class Client {

    public static void main(String[] args) {
        // 獲取請求中的支付渠道標識
        String code = "p01";
        PayStrategy payStrategy = null;
        PayRequest request = null;
        
        if (PayType.ALI.getCode().equals(code)) {
            //組裝為支付寶支付策略
            payStrategy = new AliPayStrategy();
            // 構造支付寶請求參數
            request = new AliPayRequest();
        }
        if (PayType.WECHAT.getCode().equals(code)) {
            //組裝為微信支付策略
            payStrategy = new AliPayStrategy();
            // 構造微信支付請求參數
            request = new WechatPayRequest();
        }

        if (PayType.UNION.getCode().equals(code)) {
            //組裝為銀聯支付策略
            payStrategy = new UnionPayStrategy();
            // 構造銀聯支付請求參數
            request = new UnionPayRequest();
        }

        if (Objects.nonNull(payStrategy)) {
            PayContext payContext = new PayContext();
            payContext.setPayStrategy(payStrategy);
            payContext.pay(request);
        }
    }
}

我們拿到請求中的支付標識,然後初始化對應的支付策略,封裝指定的請求參數,交給上下文對象PayContext 來執行請求。如果你再增加什麼ApplePay之類的去實現ApplePayStrategy就行了。

4. 優缺點

策略模式並不都帶來正面的作用。

4.1 優點

  • 我們將算法的實現和算法的使用進行了隔離,算法實現只關心算法邏輯,使用算法只關心什麼條件下使用什麼算法。
  • 開閉原則,無需修改上下文對象,想擴展只需要引入新的策略。
  • 運行時根據條件動態的去切換算法。
  • 適應個性的同時也可以兼容共性。

4.2 缺點

  • 客戶端必須明確知道激活策略的條件。
  • 引入太多的策略類。
  • 可被一些函數式接口所代替。偽代碼Pay.request(data).strategy(data->{ })

5. 總結

策略模式也是很常見而且有着廣泛使用場景的設計模式。今天我們從聚合支付來學習了策略模式,對它的優缺點也進行了一個分析。隨着函數式編程的普及,策略模式開始被逐漸的代替,但是它依然值得我們去學習。感謝你的閱讀,多多關注:碼農小胖哥,更多編程乾貨奉上。

關注公眾號:Felordcn 獲取更多資訊

個人博客:https://felord.cn

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

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

網站的第一印象網頁設計,決定了客戶是否繼續瀏覽的意願。台北網動廣告製作的RWD網頁設計,採用精簡與質感的CSS語法,提升企業的專業形象與簡約舒適的瀏覽體驗,讓瀏覽者第一眼就愛上它。

Spring Boot入門系列(十四)使用JdbcTemplate操作數據庫,配置多數據源!_如何寫文案

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

什麼是銷售文案服務?A就是幫你撰寫適合的廣告文案。當您需要販售商品、宣傳活動、建立個人品牌,撰寫廣告文案都是必須的工作。

前面介紹了Spring Boot 中的整合Mybatis並實現增刪改查、如何實現事物控制。不清楚的朋友可以看看之前的文章:https://www.cnblogs.com/zhangweizhong/category/1657780.html。

Spring Boot 除了Mybatis數據庫框架,還有JdbcTemplate等數據庫操作框架,同樣也比較簡單實用,如果是一般簡單的項目,用JdbcTemplate完全可以實現相關的數據庫操作。它雖然沒有MyBatis功能強大,但使用比較簡單,JdbcTemplate應該算是最簡單的數據持久化方案,所以下面就來給大家介紹Spring Boot 使用JdbcTemplate操作數據庫,配置多數據源!

 

一、JDBC簡介

JDBC(Java Data Base Connectivity, Java 數據庫連接)是一種用於執行各種數據庫操作的 API,可以為多種數據庫提供統一訪問接口。所以,JDBC 就像是一套 Java 訪問數據庫的 API 規範,利用這套規範屏蔽了各種數據庫 API 調用的差異性。當應用程序需要訪問數據庫時,調用 JDBC API 相關代碼進新操作,再由JDBC調用各類數據庫的驅動包進行數據操作,最後數據庫驅動包和對應的數據庫通訊協議完成對應的數據庫操作。

在Java領域,數據持久化有幾個常見的方案,有Spring Boot自帶的JdbcTemplate、有MyBatis,還有JPA,在這些方案中,最簡單的就是Spring Boot自帶的JdbcTemplate,雖然沒有MyBatis功能強大,但是,使用比較簡單,事實上,JdbcTemplate應該算是最簡單的數據持久化方案。

 

二、快速開始

開始之前,需要創建一個Spring Boot項目,JdbcTemplate的引用很簡單,開發者在創建一個SpringBoot項目時,選上Jdbc以及數據庫驅動依賴即可。之前介紹過如何創建項目這裏就不介紹,直接使用之前創建的項目工程。

1、依賴配置

1、pom添加依賴

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
  <groupId>mysql</groupId>
  <artifactId>mysql-connector-java</artifactId>
</dependency>

需要注意的是

如果是用數據庫連接池,記得添加Druid數據庫連接池依賴。

這裏可以添加專門為Spring Boot打造的druid-spring-boot-starter,JdbcTemplate默認使用Hikari 連接池,如果需要使用druid,需要另外配置。

 

2、application.properties配置數據源

接下來需要在application.properties中提供數據的基本配置即可,如下:

spring.datasource.url=jdbc:mysql://localhost:3306/zwz_test
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

注意:在 Spring Boot 2.1.0 中, com.mysql.jdbc.Driver 已經過期,推薦使用com.mysql.cj.jdbc.Driver

如此之後,所有的配置就算完成了,接下來就可以直接使用JdbcTemplate了,是不是特別方便。其實這就是SpringBoot的自動化配置帶來的好處。

 

2、數據庫和實體類

1、數據庫表

DROP TABLE IF EXISTS `products`;
CREATE TABLE `products` (
    `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主鍵id',
    `name` varchar(32) DEFAULT NULL COMMENT '名稱',
    `code` varchar(32) DEFAULT NULL COMMENT '編碼',
    `price` int DEFAULT NULL,
    PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

 

2、實體類

package com.weiz.pojo;

public class Product {
    private Long id;
    private String name;
    private String code;
    private int price;
    public Product(String name, String code, int price) {
        this.name = name;
        this.code = code;
        this.price = price;
    }
    // 省略 getter setter
}

實體類的數據類型要和數據庫字段一一對應,否則會有問題。

3、Serverice封裝

創建ProductService和ProductServiceImpl類

1、創建 UserService 定義我們常用的增刪改查接口

package com.weiz.service;

import com.weiz.pojo.Product;

public interface ProductService {
    int save(Product product);

    int update(Product product);

    int delete(long id);

    Product findById(long id);
}

2、創建 ProductServiceImpl 類實現 ProductService 類接口

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

擁有後台管理系統的網站,將擁有強大的資料管理與更新功能,幫助您隨時新增網站的內容並節省網站開發的成本。

package com.weiz.service.impl;

import com.weiz.pojo.Product;
import com.weiz.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;


@Service
public class ProductServiceImpl implements ProductService  {
    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Override
    public int save(Product product) {
        return jdbcTemplate.update("INSERT INTO products(name, code, price) values(?, ? , ?)",
                product.getName(), product.getCode(), product.getPrice());
    }

    @Override
    public int update(Product product) {
        return jdbcTemplate.update("UPDATE products SET name = ? , code = ? , price = ? WHERE id=?",
                product.getName(), product.getCode(), product.getPrice(), product.getId());
    }

    @Override
    public int delete(long id) {
        return jdbcTemplate.update("DELETE FROM products where id = ? ",id);
    }

    @Override
    public Product findById(long id) {
        return jdbcTemplate.queryForObject("SELECT * FROM products WHERE id=?", new Object[] { id }, new BeanPropertyRowMapper<Product>(Product.class));
    }

}

代碼說明:

UserServiceImpl類上使用 @Service 註解用於標註數據訪問組件,@Autowired 在類中注入 JdbcTemplate,JdbcTemplate是 Spring Boot操作JDBC 提供的工具類 。

除了以上這些基本用法之外,JdbcTemplate也支持其他用法,例如調用存儲過程等,這些都比較容易,而且和Jdbc本身都比較相似,這裏也就不做介紹了,有興趣可以留言討論。

 

三、調用測試

 接下來我們對jdbc操作數據庫的功能進行測試。

1、創建ProductController 

package com.weiz.controller;

import com.weiz.pojo.Product;
import com.weiz.service.ProductService;
import com.weiz.utils.JSONResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("product")
public class ProductController {
    @Autowired
    private ProductService productService;


    @RequestMapping("/save")
    public JSONResult save() {
        Product product = new Product();
        product.setCode("iphone 11");
        product.setName("iphone 11");
        product.setPrice(100);
        productService.save(product);
        return JSONResult.ok("保存成功");
    }

    @RequestMapping("/update")
    public JSONResult update() {
        long pid = 1;
        Product product = new Product();
        product.setCode("iphone 12");
        product.setName("iphone 12");
        product.setPrice(200);
        product.setId(pid);
        productService.update(product);
        return JSONResult.ok("修改成功");
    }

    @RequestMapping("/delete")
    public JSONResult delete(long pid) {
        productService.delete(pid);
        return JSONResult.ok("刪除成功");
    }

    @RequestMapping("/findbyId")
    public JSONResult findById(long pid) {
        Product product =  productService.findById(pid);
        return JSONResult.ok(product);
    }
}

2、啟動項目,在瀏覽器分別輸入增刪改查對應的地址,測試對應的方法是不是正確即可。

 

四、多數據源的使用

在實際項目中,經常會碰到使用多個數據源的情況, 比如:需要使用多個host、需要使用多種數據庫(MySql、Oracle、SqlServer…)。SpringBoot中,對此都有相應的解決方案,不過一般來說,如果有多數據源的需求,我還是建議首選分佈式數據庫中間件MyCat。這些都是比較成熟的框架,不需要自己重新寫一套。當然如果一些簡單的需求,還是可以使用多數據源的,Spring Boot中,JdbcTemplate、MyBatis以及Jpa都可以配置多數據源。接下來,就在上面的項目的基礎上進行改造,給大家介紹JdbcTemplate 如何配置多數據源。

1、配置多數據源

application.properties配置多個數據源:

spring.datasource.primary.jdbc-url=jdbc:mysql://localhost:3306/zwz_test
spring.datasource.primary.username=root
spring.datasource.primary.password=root
spring.datasource.primary.driver-class-name=com.mysql.cj.jdbc.Driver

spring.datasource.secondary.jdbc-url=jdbc:mysql://localhost:3306/zwz_test2
spring.datasource.secondary.username=root
spring.datasource.secondary.password=root
spring.datasource.secondary.driver-class-name=com.mysql.cj.jdbc.Driver

上面的配置文件,添加了兩個數據源,一個是 zwz_test 庫,鈴個是 zwz_test2 庫。

注意:之前單個數據源的數據庫連接是:spring.datasource.url,這裏多個數據源使用的是 spring.datasource.*.jdbc-url,因為JdbcTemplate默認使用Hikari 連接池,而 HikariCP 讀取的是 jdbc-url 。

 

2、配置JDBC初始化

創建DataSourceConfig,在項目啟動的時候讀取配置文件中的數據庫信息,並對 JDBC 初始化。  

package com.weiz.config;

import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.core.JdbcTemplate;

import javax.sql.DataSource;

@Configuration
public class DataSourceConfig {
    @Primary
    @Bean(name = "primaryDataSource")
    @Qualifier("primaryDataSource")
    @ConfigurationProperties(prefix="spring.datasource.primary")
    public DataSource primaryDataSource() {
        return DataSourceBuilder.create().build();
    }
    @Bean(name = "secondaryDataSource")
    @Qualifier("secondaryDataSource")
    @ConfigurationProperties(prefix="spring.datasource.secondary")
    public DataSource secondaryDataSource() {
        return DataSourceBuilder.create().build();
    }
    @Bean(name="primaryJdbcTemplate")
    public JdbcTemplate primaryJdbcTemplate (
            @Qualifier("primaryDataSource") DataSource dataSource ) {
        return new JdbcTemplate(dataSource);
    }
    @Bean(name="secondaryJdbcTemplate")
    public JdbcTemplate secondaryJdbcTemplate(
            @Qualifier("secondaryDataSource") DataSource dataSource) {
        return new JdbcTemplate(dataSource);
    }
}

DataSourceConfig類的作用是在項目啟動的時候根據特定的前綴加載不同的數據源,再根據構建好的數據源創建不同的 JDBC。   
注意事項:使用多個數據源時,需要添加@Primary註解,@Primary:自動裝配時當出現多個Bean候選者時,被註解為@Primary的Bean將作為首選者。Primary 意味着”主要的”,類似與SQL語句中的”primary key”,有且只能有一個,否則會報錯。     3、修改Serverice封裝 需要對 ProductServerice 中的所有方法法進行改造,增加一個傳入參數 JdbcTemplate,根據調用方傳入的JdbcTemplate 進行操作。

// ProductService 接口
public interface ProductService {
    int save(Product product, JdbcTemplate jdbcTemplate);
    // 省略其他方法  
}

// ProductServiceImpl 
@Service
public class ProductServiceImpl implements ProductService  {
    @Override
    public int save(Product product,JdbcTemplate jdbcTemplate) {
        return jdbcTemplate.update("INSERT INTO products(name, code, price) values(?, ? , ?)",
                product.getName(), product.getCode(), product.getPrice());
    }
    // 省略其他方法 
}

 

4、調用測試

同樣,將之前的ProductController 修改如下:

@RestController
@RequestMapping("product")
public class ProductController {
    @Autowired
    private ProductService productService;
    @Autowired
    private JdbcTemplate primaryJdbcTemplate;
    @Autowired
    private JdbcTemplate secondaryJdbcTemplate;

    @RequestMapping("/save")
    public JSONResult save() {
        Product product = new Product();
        product.setCode("iphone 11");
        product.setName("iphone 11");
        product.setPrice(100);
        productService.save(product,primaryJdbcTemplate);
        productService.save(product,secondaryJdbcTemplate);
        return JSONResult.ok("保存成功");
    }

    // 省略其他方法
}

在瀏覽器中輸入:/save 地址后,查看zwz_test 和 zwz_test2數據庫中的products表,都存入一條數據,說明多數據源插入數據成功,其他方方法也是一樣的。這樣在實際項目中,我們通過傳入不同的JdbcTemplate 實例,就可以操作多個數據庫。

 

最後

以上,就把Spring Boot 使用jdbcTemplate 操作數據庫介紹完了。同時也介紹了如何配置使用多數據源。Spring Boot 項目中 JDBC 操作數據庫是不是非常簡單。

這個系列課程的完整源碼,也會提供給大家。大家關注我的微信公眾號(架構師精進),回復:springboot源碼。獲取這個系列課程的完整源碼。

 

 

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

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

銷售文案是什麼?A文案是廣告用的文字。舉凡任何宣傳、行銷、販賣商品時所用到的文字都是文案。在網路時代,文案成為行銷中最重要的宣傳方式,好的文案可節省大量宣傳資源,達成行銷目的。

LeetCode 73,為什麼第一反應想到的解法很有可能是個坑?_網頁設計公司

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

透過資料庫的網站架設建置,建立公司的形象或購物系統,並提供最人性化的使用介面,讓使用者能即時接收到相關的資訊

本文始發於個人公眾號:TechFlow,原創不易,求個關注

今天是LeetCode第42篇文章,我們來看看LeetCode第73題矩陣置零,set matrix zeroes。

這題的難度是Medium,通過率在43%左右,從通過率上可以看出這題的難度並不大。但是這題的解法不少,從易到難,有很多種方法。而且解法和它的推導過程都挺有意思,我們一起來看下。

題意

首先我們來看題意,這題的題意很簡單,給定一個二維數組。要求我們對這個數組當中的元素做如下修改,如果數組的i行j列為0,那麼將同行和同列的元素全部置為0。要求我們直接在原數組上進行修改,而不是返回一個新的數組。

言下之意,要求我們實現一個in-place的方法,而避免額外開闢新的內存。

樣例

Input: 
[
  [0,1,2,0],
  [3,4,5,2],
  [1,3,1,5]
]
Output: 
[
  [0,0,0,0],
  [0,4,5,0],
  [0,3,1,0]
]

近在眼前的解法原來是坑

這題的題意非常簡單,解法也非常明顯,以至於很多人拿到它都會當做模擬題來解決。即遍歷一下數組,如果找到0,那麼將它所在的行和列賦值為0,然後繼續遍歷。

這段邏輯並不難寫,我們很容易寫出來:

class Solution:
    def setZeroes(self, matrix: List[List[int]]) -> None:
        """  Do not return anything, modify matrix in-place instead.  """
        n = len(matrix)
        if n == 0:
            return
        
        m = len(matrix[0])
        for i in range(n):
            for j in range(m):
                # 當我們找到為0的位置之後,將所在的行和列置為0
                if matrix[i][j] == 0:
                    for k in range(m):
                        matrix[i][k] = 0
                    for k in range(n):
                        matrix[k][j] = 0

但是很遺憾的是,這樣的做法是錯誤的,實際上連樣例都無法通過。通不過樣例的原因也很簡單, 因為0具有傳遞性。

舉個簡單的例子,假設第0行當中有一個0,那麼最後的結果一定是第0行全部被置為0。但問題是我們是在遍歷到0的時候來進行的set操作,這樣會將第0行的其他元素也置為0。這樣當我們遍歷到後面的位置之後,會繼續傳遞,從而將一些不該置為0的地方也置為0了。

舉個簡單的例子,比如:第0行是1 0 0 1。顯然由於第0行存在0,所以操作之後的結果一定是全為0。但問題是matrix[0][3]這個位置原本並不為0,但是如果我們在發現matrix[0][1]為0的時候,將它置為0的話,那麼當我們後面遍歷到matrix[0][3]得到0的時候,會無法判斷究竟是這個位置原本就是0,還是前面出現了0導致這一行全部變成了0。這兩者的操作是不同的。

眼看着目標就在眼前,好像一伸手就碰得到,但是偏偏好像這一步就是咫尺天涯,怎麼也碰不到。這種感覺想想都很難受,我想,當你試着用這種方法去解這道題然後發現不行的時候,一定會有這樣的感覺。並且你會發現好像也沒有什麼很好的辦法來優化。

這種情況在正式的算法比賽當中經常遇到,所以專業的競賽选手有了經驗(吃過虧)之後,想出思路的第一時間就會立即轉向思考,這樣做是不是會有什麼坑,或者是考慮不到的情況。嚴謹一點的同學還會構思幾組不同的測試數據進行測試,或者是腦海中模擬算法的運算。

剛不過去只能繞

以前我年輕的時候總是不信邪,有時候明知道這個方法並不好,或者是存在反例,但是仍會堅持想要通過自己的努力想出一個方案來解決它,而不是更換方法。

我不知道有多少人有同樣的想法,但是一般來說頭鐵的毛病最後總是會被治好的。這題算是一個不錯的例子,如果你堅持使用模擬的方法來做這道題,只有一種方案就是再創建一個同樣大小的數組來作為緩存。當我們遇到0的時候,我們不直接修改原數組中的結果,而是修改緩存,將同行和同列緩存數組中的元素置為0,最後再將緩存數組與原數組合併。

但是顯然這不是一種好的方法,因為題目要求in-place的目的就是為了節約空間,我們另外創建了一個同樣大小的數組顯然違背了題目的本意

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

當全世界的人們隨著網路時代而改變向上時您還停留在『網站美醜不重要』的舊有思維嗎?機會是留給努力改變現況的人們,別再浪費一分一秒可以接觸商機的寶貴時間!

所以頭鐵到最後還是得認清現狀,這個方法不適合這道題,需要更換解法。如果是在比賽當中得出的這個結論,那麼很有可能獎牌已經和你沒什麼關係了。堅持和固執本身也許沒有太大的區別, 可能只是出現的場景不一樣。

進階解法

回到這道題本身,我們已經證明了模擬的思路是行不通的,除了一邊遍歷一邊操作可能帶來的混亂之外,還有一個點是這樣的複雜度很高。因為如果原數據當中如果本身0就很多的話,那麼我們會需要不停地操作,極端情況下,如果所有元素都是0,那麼我們每一個位置都需要操作一下行列,整體的複雜度會達到

既然如此,還有什麼好的辦法嗎?

當然是有的,其實也挺明顯的,因為對於一個出現的0來說它會影響的範圍是固定的,就是所在的行和列,那我們是不是記錄下會全部置為0的行和列,最後再遍歷一遍數據,看下當前元素是不是出在置為0的範圍當中就可以了。這種方法需要我們再創建兩個數組,用來存儲行和列是否被置為0。

這個解法也很直觀,想到了代碼應該不難寫:

class Solution:
    def setZeroes(self, matrix: List[List[int]]) -> None:
        """  Do not return anything, modify matrix in-place instead.  """
        n = len(matrix)
        if n == 0:
            return
        
        m = len(matrix[0])
        rows = [0 for _ in range(n)]
        cols = [0 for _ in range(m)]
        
        # 記錄置為0的行和列
        for i in range(n):
            for j in range(m):
                if matrix[i][j] == 0:
                    rows[i], cols[j] = 1, 1
                    
        # 如果所在行或者列置為0,那麼當前位置為0
        for i in range(n):
            for j in range(m):
                if rows[i] or cols[j]:
                    matrix[i][j] = 0
                    

終極解法

上面的做法雖然通過之後的戰績不太光彩,沒能戰勝90%以上的提交,但是能夠通過,而且算法沒有數量級的差距,也算是可以的。如果讓我來做,我可能就想到這種方法為止了。但是題目當中明確說了,還有空間複雜度為O(1)的算法,逼得我進一步思考了一下。

一般來說我們都是優化時間複雜度,很少會優化空間複雜度。相比於優化時間,優化空間有時候更加困難。因為有些時候我們可以空間換時間,可以預處理,可以離線計算……方法相對比較多。但優化空間的方法則很少,尤其是很多時候還不能犧牲時間,所以一般來說只能從算法本身來優化,很少有什麼套路可以套用。

在這個問題當中,要優化空間複雜度到常數級,那麼說明我們連數組都不能用。也就是說不能記錄行和列的信息,但是我們也不能用模擬的方法來進行,那麼應該怎麼辦呢?

干想是很難想出來的, 但是我們換個思路,問題就完全不一樣了。上面的算法時間複雜度是最優的,空間複雜度不太行,那麼有沒有辦法既使用同樣的算法,又能節省空間呢?看起來似乎不可能,但是其實可以,方法說穿了也並不值錢,就是將數據想辦法存在已有的地方,而不是另外開闢空間。在這個問題當中,已有的地方當然就只有一個就是原數組。也就是說我們要把每一行和列是否為0的信息記錄在原數組當中,比如我們可以把第0行和第0列用來做這個事情。

但這樣又會帶來另外一個問題,如果第0行和第0列本身當中也有0出現該怎麼辦?沒辦法,只能特判了。我們單獨用變量來記錄第0行和第0列是否被置為0,這樣我們就最大化地利用了空間,將空間複雜度降低到了常數級。

代碼邏輯和上面一脈相承,只是多了一點騷操作。

class Solution:
    def setZeroes(self, matrix: List[List[int]]) -> None:
        """  Do not return anything, modify matrix in-place instead.  """
        n = len(matrix)
        if n == 0:
            return
        
        m = len(matrix[0])
        
        row, col = False, False
        
        # 特判0,0的位置
        if matrix[0][0] == 0:
            row, col = True, True
        
        # 特判第0列是否含0
        for i in range(n):
            if matrix[i][0] == 0:
                col = True
                break
                
        # 特判第0行是否含0
        for i in range(m):
            if matrix[0][i] == 0:
                row = True
                break
                
        # 將i行,j列是否為0的信息存入matrix當中
        for i in range(0, n):
            for j in range(0, m):
                if matrix[i][j] == 0:
                    matrix[i][0] = 0
                    matrix[0][j] = 0
                    
        for i in range(1, n):
            for j in range(1, m):
                # 根據第0行與第0列數據還原
                if matrix[i][0] == 0 or matrix[0][j] == 0:
                    matrix[i][j] = 0
                  
        # 最後處理第0行與第0列
        if row:
            for i in range(m):
                matrix[0][i] = 0
                
        if col:
            for i in range(n):
                matrix[i][0] = 0

總結

到這裏,這道題就算是分享完了,它的題意簡單,但是解法挺多的,我個人感覺也許還存在更好的解法也不一定。

我個人做完這題最大的感受不是這題的思路如何,也不是它涉及的算法如何,而是想到了很多和算法題無關的事情。比如我們生活當中有沒有這樣看似簡單,但是做起來發現一點也不簡單的事情?有沒有眼看着目標就在眼前,卻發現選擇的路一開始就是錯的呢?

帶着這樣的思路來做題,會發現題目也變得有意思多了。

今天的內容就是這些,如果喜歡本文,可以的話,請點個關注,給我一點鼓勵,也方便獲取更多文章。

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

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

RWD(響應式網頁設計)是透過瀏覽器的解析度來判斷要給使用者看到的樣貌

從一年前的1200多人優化到現在200多人,待在這樣的技術團隊是一種什麼體驗?_網頁設計

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

窩窩以「數位行銷」「品牌經營」「網站與應用程式」「印刷品設計」等四大主軸,為每一位客戶客製建立行銷脈絡及洞燭市場先機。

 聲明:

感覺之前可能沒表達清楚,導致評論區很多夥伴誤會了,我依然在職,只是身邊的朋友一個個的離開讓我有很大的觸動,所以才寫下這篇文章,以下所有內容均為個人感受與意見,與公司無關

1.寫點情懷

  平日里都是分享技術的,但是最近做的最多的一件事卻是送別,挺悲傷的一個詞,我個人不太喜歡,但是現在整個大的環境都不太景氣,眾多企業紛紛倒閉,一批批工人,白領被迫失去了工作,其中也不乏我自己所在的職業:程序員。特別的從去年年底至今,依舊活着的公司大部分也是苟延殘喘,大幅度的“優化”員工,而我自己身邊的朋友,送走了一批又一批,自己的感觸挺深的,所以想把這段時間的經歷寫成文字,當做是解壓了。

 

2.親身經歷

  在現在這家公司幹了接近三年了,剛進來時,整個IT部分是700多人,當時業務發展速度很快,很多業務線需要技術做數據支撐,那時公司處在上升期,所以不斷的在擴張技術團隊去滿足各種業務需求,頂峰時期,技術團隊達到了1200多人,各種項目也是琳琅滿目(姑且這樣形容吧,算是對好的一種嚮往),自己也有參与到不同的項目中,也學到了挺多東西,交到了很多朋友,樂此不疲。但是好景不長,伴隨着互聯網寒冬以及疫情的到來,從去年年底到現在,身邊很多朋友陸陸續續都離開了,舉一個印象比較深刻的例子:

  去年五月份開始,我們領導找我組建一個新的項目團隊,做3D應用相關的開發,當時結合了公司的業務和市場反饋,經過簡短溝通后說干就干,然後就開始對相關技術做調研(因為在這之前完全沒做過3D應用的開發),剛開始只招聘了一個技術,之前有做過相關工作,來了之後形成了一個三人小組,開始做硬件選型,做競品分析(當然也在學習別人的優勢),做關鍵技術的攻克,兩個月後領導拿着我們費了九年二虎之力的一個demo去找老總,估計就是一頓前(連)景(哄)展(帶)示(騙)了,項目被公司看好,覺得可以投入人力和更多的資源,將其形成真正的產品。然後就開始招聘技術,產品,3D建模師,UI設計師等等搭建技術團隊,到9月份形成了一個9人組成的項目小組開始立項,走產品化流程,從一個demo到真正形成一個產品確實也需要走很多路,由於應用較為特殊,我們從硬件開始準備,期間用到的技術棧(包含但又不僅僅是):電機,樹莓派,Python,Nodejs,.NetCore,Unity3D,Aws眾多雲服務(MySQL,Redis,SQS,負載均衡器,CDN,S3,EC2等等),過程可以用過五關斬六將來形容,解決了很多技術上的難點,也做了一些以前沒有做過的嘗試,直到12月份項目上線可第一個版本,大家都覺得鬆了一口氣,畢竟過程雖然很波折,但是大家都很充實,一起擼代碼,一起加班,一起解決困難,一起喝下午茶,一起打鬧,有時會為一個問題爭執不休而鬧情緒,有時也會因為一點點小的突破而哈哈大笑,確實留下了很多珍貴的回憶。上線后也收到了很多反饋,後續也在對線上版本進行不斷的迭代優化,從上線到過年一個多月的時間里,一直頻繁的在發版本,雖然大家都覺得被各方大佬孽的很慘,但是依然幹勁十足。年後由於疫情,大家都開始在家遠程辦公,但是面對一次次的延遲上班的通知,大家可能心裏多少都有些想法,但是該來的總會來,遠程辦公的第二周,領導就通知說,現在疫情給公司帶來了較大的影響,為了維護公司的正常運轉只保留核心技術團隊,而我們剛上線兩個多月的項目也被迫就此夭折。而我也親自參与到了項目的下線整個過程,心裏很不是滋味,畢竟就像是十月懷胎后剛出生的嬰兒,還希望它能健康長大的,但是深深的明白,在職場沒有人會去談這些感情感性的東西,都是以結果,收益為導向。

  項目被砍了,人員自然也是公司關注的焦點,各種名義開始大幅度的優化人員,而我所在的項目組,除了我和我領導兩個人轉到了另外一個團隊,其他所有的小夥伴都開始了離開的行程,收到了很多工作交接,送他們一個個的離開,這種場景雖然嘴上會說沒關係,走到哪裡都是朋友,但是內心裏多少還是有點不舒服,畢竟一起留下了諸多回憶,難免會有所傷感。而這個例子只是N多個項目小組的一個縮影,由於在公司做過很多項目,認識的人也不少,最近收到他們的消息,陸陸續續的都離開了,再放眼望去,三年在技術團隊認識的一些人現在依然在的,寥寥無幾了,也是最觸動自己的地方。

 

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

網動是一群專業、熱情、向前行的工作團隊,我們擁有靈活的組織與溝通的能力,能傾聽客戶聲音,激發創意的火花,呈現完美的作品

3.大環境不好,怎麼破局?

  3.1 不管程序員是不是吃青春飯,相對其他行業來說,程序員的薪資還是有優勢的,就我們公司,開發年薪50W的不在少數,由開發上升為管理崗的年薪過100W的也是存在的,哪怕是青春飯,那麼是否能在有限的時間裏面提升自己,讓自己更有價值,也能為以後打下好的基礎。

  3.2 互聯網更新迭代的速度是很快的,幾年就會興起一股潮流趨勢,一會人工智能,一會區塊鏈,一會中台等等,很多開發者會去追隨這些比較流行的物種,當然了解前沿知識是好事,但是不在少數的開發者會盲目的去跟風,一會做前端,一會做後端,一會小程序,一會人工智能,各種技術棧說起來都會,都用過,但是不精,知其然,不知其所以然。並不是說了解這些東西不好,只是我們要有個度,不能盲目,一味追隨可能讓自己浮躁,忽略了知識體系的積累,從而使自己失去了核心競爭力。

  3.3 之前面試我自己有個習慣,在結束時一般面試官會問自己有沒有什麼是想了解的,這個時候我通常會問:咱們公司的核心競爭力是什麼?其實這句話用在我們自己身上也很合適,我們自己的核心競爭力,優勢又是什麼呢。我覺得很重要的一點就是不斷學習,快速成長,只有當我們自己保持一個較快的成長速度,那就可以跟別人拉開差距,並持續將差距最大化,這樣我們才能在競爭中保證自身的優勢,最差也能立於不敗之地。

  3.4 如果你現在的工作很安逸,通常都不是什麼好事,這意味着一切按部就班,然後慢慢在消耗我們的上進心和鬥志,扼殺我們的創造力,這也是經常大家都懂的溫水煮了青蛙的道理;同時也意味着我們慢慢失去了競爭力,也會因為太安逸,讓我們變得膽怯,以至於在遇到更好的機會時,我們會猶豫不決… 職場上得懂得居安思危。

 

4.共勉

  希望所有的程序員們也包括自己,做一個有擔當,有理想,有抱負的好青年,帶着寫程序的初心,從“hello world”寫到”change the world”,一起加油吧!

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

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

擁有專業的維修技術團隊,同時聘請資深iphone手機維修專家,現場說明手機問題,快速修理,沒修好不收錢

【大廠面試02期】Redis過期key是怎麼樣清理的?_貨運

※回頭車貨運收費標準

宇安交通關係企業,自成立迄今,即秉持著「以誠待人」、「以實處事」的企業信念

【大廠面試02期】Redis過期key是怎麼樣清理的?

在Redis中,對於過期key的清理主要有惰性清除,定時清理,內存不夠時清理三種方法,下面我們就來具體看看這三種清理方法。

(1)惰性清除

在訪問key時,如果發現key已經過期,那麼會將key刪除。

(2)定時清理

Redis配置項hz定義了serverCron任務的執行周期,默認每次清理時間為25ms,每次清理會依次遍歷所有DB,從db隨機取出20個key,如果過期就刪除,如果其中有5個key過期,那麼就繼續對這個db進行清理,否則開始清理下一個db。

(3)內存不夠時清理

當執行寫入命令時,如果發現內存不夠,那麼就會按照配置的淘汰策略清理內存,淘汰策略一般有6種,Redis4.0版本后又增加了2種,主要由分為三類

  • 第一類 不處理,等報錯(默認的配置)

    • noeviction,發現內存不夠時,不刪除key,執行寫入命令時直接返回錯誤信息。(Redis默認的配置就是noeviction)
  • 第二類 從所有結果集中的key中挑選,進行淘汰

    • allkeys-random 就是從所有的key中隨機挑選key,進行淘汰
    • allkeys-lru 就是從所有的key中挑選最近使用時間距離現在最遠的key,進行淘汰
    • allkeys-lfu 就是從所有的key中挑選使用頻率最低的key,進行淘汰。(這是Redis 4.0版本后新增的策略)
  • 第三類 從設置了過期時間的key中挑選,進行淘汰

    這種就是從設置了expires過期時間的結果集中選出一部分key淘汰,挑選的算法有:

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

    網動結合了許多網際網路業界的菁英共同研發簡單易操作的架站工具,及時性的更新,為客戶創造出更多的網路商機。

    • volatile-random 從設置了過期時間的結果集中隨機挑選key刪除。

    • volatile-lru 從設置了過期時間的結果集中挑選上次使用時間距離現在最久的key開始刪除

    • volatile-ttl 從設置了過期時間的結果集中挑選可存活時間最短的key開始刪除(也就是從哪些快要過期的key中先刪除)

    • volatile-lfu 從過期時間的結果集中選擇使用頻率最低的key開始刪除(這是Redis 4.0版本后新增的策略)

LRU算法

LRU算法的設計原則是如果一個數據近期沒有被訪問到,那麼之後一段時間都不會被訪問到。所以當元素個數達到限制的值時,優先移除距離上次使用時間最久的元素。

可以使用雙向鏈表Node+HashMap<String, Node>來實現,每次訪問元素后,將元素移動到鏈表頭部,當元素滿了時,將鏈表尾部的元素移除,HashMap主要用於根據key獲得Node以及添加時判斷節點是否已存在和刪除時快速找到節點。

PS:使用單向鏈表能不能實現呢,也可以,單向鏈表的節點雖然獲取不到pre節點的信息,但是可以將下一個節點的key和value設置在當前節點上,然後把當前節點的next指針指向下下個節點,這樣相當於把下一個節點刪除了

//雙向鏈表
    public static class ListNode {
        String key;//這裏存儲key便於元素滿時,刪除尾節點時可以快速從HashMap刪除鍵值對
        Integer value;
        ListNode pre = null;
        ListNode next = null;
        ListNode(String key, Integer value) {
            this.key = key;
            this.value = value;
        }
    }

    ListNode head;
    ListNode last;
    int limit=4;
    
    HashMap<String, ListNode> hashMap = new HashMap<String, ListNode>();

    public void add(String key, Integer val) {
        ListNode existNode = hashMap.get(key);
        if (existNode!=null) {
            //從鏈表中刪除這個元素
            ListNode pre = existNode.pre;
            ListNode next = existNode.next;
            if (pre!=null) {
               pre.next = next;
            }
            if (next!=null) {
               next.pre = pre;
            }
            //更新尾節點
            if (last==existNode) {
                last = existNode.pre;
            }
            //移動到最前面
            head.pre = existNode;
            existNode.next = head;
            head = existNode;
            //更新值
            existNode.value = val;
        } else {
            //達到限制,先刪除尾節點
            if (hashMap.size() == limit) {
                ListNode deleteNode = last;
                hashMap.remove(deleteNode.key);
              //正是因為需要刪除,所以才需要每個ListNode保存key
                last = deleteNode.pre;
                deleteNode.pre = null;
                last.next = null;
            }
            ListNode node = new ListNode(key,val);
            hashMap.put(key,node);
            if (head==null) {
                head = node;
                last = node;
            } else {
                //插入頭結點
                node.next = head;
                head.pre = node;
                head = node;
            }
        }

    }

    public ListNode get(String key) {
        return hashMap.get(key);
    }

    public void remove(String key) {
        ListNode deleteNode = hashMap.get(key);
        ListNode preNode = deleteNode.pre;
        ListNode nextNode = deleteNode.next;
        if (preNode!=null) {
            preNode.next = nextNode;
        }
        if (nextNode!=null) {
            nextNode.pre = preNode;
        }
        if (head==deleteNode) {
            head = nextNode;
        }
        if (last == deleteNode) {
            last = preNode;
        }
        hashMap.remove(key);
    }

LFU算法

LFU算法的設計原則時,如果一個數據在最近一段時間被訪問的時次數越多,那麼之後被訪問的概率會越大,基本實現是每個數據都有一個引用計數,每次數據被訪問后,引用計數加1,需要淘汰數據時,淘汰引用計數最小的數據。在Redis的實現中,每次key被訪問后,引用計數是加一個介於0到1之間的數p,並且訪問越頻繁p值越大,而且在一定的時間間隔內,
如果key沒有被訪問,引用計數會減少。

最後

大家有什麼想法,歡迎進群一起討論(因為大群已經滿200人了,大家可以掃碼進這個小群,我拉大家進大群)!本文已收錄到1.1K Star數開源學習指南——《大廠面試指北》,如果想要了解更多大廠面試相關的內容,了解更多可以看
http://notfound9.github.io/interviewGuide/#/docs/BATInterview

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

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

搬家價格與搬家費用透明合理,不亂收費。本公司提供下列三種搬家計費方案,由資深專業組長到府估價,替客戶量身規劃選擇最經濟節省的計費方式