C#9.0 終於來了,帶你一起解讀 nint 和 Pattern matching 兩大新特性玩法

一:背景

1. 講故事

上一篇跟大家聊到了Target-typed newLambda discard parameters,看博客園和公號里的閱讀量都達到了新高,甚是欣慰,不管大家對新特性是多頭還是空頭,起碼還是對它抱有一種極為關注的態度,所以我的這個系列還得跟,那就繼續開擼吧,今天繼續帶來兩個新特性,更多新特性列表,請大家關注:新特性預覽

二:新特性研究

1. Native ints

從字面上看貌似是什麼原生類型ints,有點莫名其妙,還是看一看Issues上舉得例子吧:


Summary: nint i = 1; and nuint i2 = 2;

Shipped in preview in 16.7p1.

有點意思,還是第一次看到有nint這麼個東西,應該就是C#9新增的關鍵詞,好奇心爆棚,快來實操一下。


   static void Main(string[] args)
   {
        nint i = 10;
        Console.WriteLine($"i={i}");
   }

從圖中看,可以原樣輸出,然後用ILSpy查查底層IL代碼,發現連IL代碼都不用看。如下圖:

從圖中看原來 nint 就是 IntPtr 結構體哈,如果你玩過 C# 到 C++ 之間的互操作,我相信你會對Ptr再熟悉不過了,從這個 nint 上看,你不覺得C#團隊對指針操作是前所未有的重視嗎? 前有指針類型IntPtr,後有內存段處理集合Span,到現在直接提供關鍵詞支持,就是盡最大努力讓你在類型安全的前提下使用指針。

這就讓我想起了前些天寫的一篇互操作的文章,現在就可以用nint進行簡化了,來段代碼給大家看一下。

  • 原來的寫法:

        [DllImport("ConsoleApplication1.dll", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
        extern static IntPtr AddPerson(Person person);

        static void Main(string[] args)
        {
            var person = new Person() { username = "dotnetfly", password = "123456" };
            var ptr = AddPerson(person);
            var str = Marshal.PtrToStringAnsi(ptr);
        }

  • IntPtr -> nint 的寫法

總的來說這個關鍵詞不是最重要的,重要的是C#團隊對指針操作抱有前所未有的重視,這是一個非常积極的信號。

2. Pattern matching improvements

模式匹配這個不算是什麼新特性了,在本次C#9中也是繼續得到了完善,可能有很多朋友對模式匹配不是很熟悉,畢竟是C#7才有的新玩法,後面幾乎每一個新版本都在跟蹤完善,我先科普一下吧。

模式匹配到底解決了什麼問題

大家在編碼的過程中,不可能遇不到 if/else 嵌套 if/else 的這種情況,有時候嵌套甚至達到5,6層之多,特別影響代碼可讀性,我就來YY個例子。

現在各個地方都在發不同面值的消費券,為了實現千人千面,消費券的發放規則如下:

性別 年齡 地區 面值
<20 安徽 2000
<40 上海 4000
剩餘 剩餘 3000
<20 安徽 2500
<60 安徽 1500

如果用傳統的方式,你肯定要用各種花哨的if/else來實現,如下代碼:


        static decimal GetTicket(string sex, int age, string area)
        {
            if (sex == "男")
            {
                if (age < 20 && area == "安徽")
                {
                    return 2000;
                }
                else
                {
                    if (age < 40 && area == "上海")
                    {
                        return 4000;
                    }
                    else
                    {
                        return 3000;
                    }
                }
            }
            else
            {
                if (age < 20 && area == "安徽")
                {
                    return 2500;
                }
                if (age < 60 && area == "安徽")
                {
                    return 1500;
                }
            }

            return 0;
        }

這種代碼可讀性不是一般的差,就像大強子說的那樣:看着都想打人。。。 問題來了,這代碼還有救嗎??? 當然有了,這就需要用Pattern matching 去簡化,畢竟它就是為了這種問題而生的,修改后的代碼如下:


        static decimal GetTicket_Pattern(string sex, int age, string area)
        {
            return (sex, age, area) switch
            {
                ("男", < 20, "安徽") => 2000,
                ("男", < 40, "上海") => 4000,
                ("男", _, _) => 3000,
                ("女", < 20, "安徽") => 2500,
                ("女", < 60, "安徽") => 1500,
                _ => 0
            };
        }

看到這種化簡后的代碼是不是非常驚訝,這就是 Pattern matching 要幫你解決的場景,接下來看看底層的IL代碼是什麼樣子。

從圖中看,這反編譯后的代碼比我手工寫的還要爛,無力吐槽哈,當然 模式匹配 有各種千奇百怪的玩法,絕對讓你瞠目結舌,更多玩法可參考官方文檔:模式匹配

這個特性最重要的是你一定要明白它的客戶群在哪裡?

三: 總結

總的來說,這兩個特性都是比較實用的,尤其是 Pattern matching 化解了你多少不得不這麼寫的爛代碼,頭髮護理就靠它了,快來給它點個贊吧!

好了,先就這樣吧,感謝您的閱讀,希望本篇對你有幫助,謝謝。

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

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

【其他文章推薦】

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

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

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

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

※回頭車貨運收費標準

css3中的@font-face你真的了解嗎

css3中的自定義字體方法@font-face

@font-face屬性可以讓我們自定義網站字體屬性,然後引用到想要應用該字體的元素上。

基本語法:

@font-face {
  font-family: <font-name>;
  src: local( <family-name> ) | <url> [format("formatName")][,<url> [format("formatName")]]*;
  unicode-range: <unicode-range>;
  font-variant: <font-variant>;
  font-feature-settings: <font-feature-settings>;
  font-variation-settings: <font-variation-settings>;
  font-stretch: <font-stretch>;
  font-weight: <font-weight>;
  font-style: <font-style>;
  font-display: <font-display>;
}

屬性規則說明

font-family

給你引入的字體起一個專屬的字體名字,font-name,然後他會在元素font-family:中使用,如div{font-family:font-name}

src

用於指定加載字體文件的路徑或者加載本地字體

local

加載一個本地字體,font-name表示本地的字體名稱,比如Microsoft YaHei | 微軟雅黑;如果本地有應用此字體显示文本。

示例:

/* 加載一個本地字體 */
@font-face{
  font-family: myFont;
  src: local('Microsoft YaHei');
}
/* 加載多個本地字體 */
@font-face{
  font-family: myFont;
  src:  local(黑體), local("Microsoft YaHei"), local("HelveticaNeue-Light"), local("Helvetica Neue Light"),  local("PingFang SC"), local(sans-serif);
}
/* 應用自定義字體 */
.box{
  font-family: myFont;
}

在上邊代碼中看到,可以使用一個或多個local,多個之間用逗號分開,括號中的字體名稱可以使用單引號或者雙引號括起來,也可以不帶引號直接寫字體名稱,有空格的必須添加引號,但是只能寫一個字體名稱
上邊的寫法讓我們在定義字體的時候變得方便很多,我們只需要定義好自定義名稱然後直接引用該字體等同於下邊代碼:

.box{
  font-family: 黑體, "Microsoft YaHei", "HelveticaNeue-Light", "Helvetica Neue Light", "PingFang SC", sans-serif;
}

url

表示服務器端提供的字體地址,這個也是可以使用多個,多個之間用逗號隔開,一般寫多個是為了瀏覽器兼容加載不同格式的字體。目前web可以加載六種格式的字體:

  1. EOT:全拼:Embedded_OpenType,是由微軟開發的字體格式規範,所以只適用於IE瀏覽器。詳細介紹

兼容:

兼容詳情

  1. TTF:全拼:TrueType,是一種輪廓字體標準,最早是由蘋果公司研發,後來成為Mac OSMicrosoft Windows系統中最常用的字體格式。詳細介紹

兼容:

兼容詳情
3. OTF:全拼:OpenType,是可縮放計算機字體的格式,是由微軟和Adobe公司聯合開發。詳細介紹

兼容:

兼容詳情
4. WOFF:全拼:Web Open Font Formatweb網絡開放字體格式,他是專為網絡設計的一種字體格式,WOFF是把OpenTypeTrueType字體進行了封裝,並進行了壓縮優化,它使用了廣泛應用的zlib壓縮,並添加了XML元數據,這種字體格式體積更小,適用於網絡傳輸,可以使用戶體驗做到更好。詳細介紹

兼容:

兼容詳情
5. WOFF2:它是WOFF的升級版,它使用Brotli進行字節級壓縮,比WOFF體積更小

兼容:

兼容詳情

  1. SVG:全拼:Scalable Vector Graphics可縮放矢量圖形,是一種基於可擴展標記語言(XML)的矢量圖像格式,用於二維圖形,並支持交互性和動畫,字體中就是使用svg技術來呈現文字樣式。我測試只有蘋果Safari支持; 詳細介紹

兼容:

兼容詳情

format

可選值,表示給加載的外部字體指定字體格式,用來告訴瀏覽器讓瀏覽器能夠識別所使用的字體格式,可用的類型有 embedded-opentype, truetype, opentype, woff, woff2, svg。分別對應上邊我們介紹的字體格式。

語法:

/* 加載一種字體格式 */
@font-face{
  font-family: "myFontName";
  src:  url('font.woff') format('woff');
}

/* 加載多個字體格式,兼容更多瀏覽器 */
@font-face{
  font-family: "myFontName";
  src: url('font.eot'); /* IE9*/
  src: url('font.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
  url('font.woff2') format('woff2'),
  url('font.woff') format('woff'), /* chrome、firefox */
  url('font.ttf') format('truetype'), /* chrome、firefox、opera、Safari, Android, iOS 4.2+*/
  url('font.svg#Alibaba-PuHuiTi-Regular') format('svg'); /* iOS 4.1- */
}

從上邊語法來看我們可以加載一個格式的字體文件,也可以加載多個格式字體,之間用逗號分開,瀏覽器會優先讀取寫在前面的字體格式並且檢測是否支持,如果支持就使用該格式的字體文件。

font-weight

表示自定義字體規則的字重程度,我們可以給一個字體指定不同的粗細規則引用不同規格的字體文件。

語法:

/* Single values */
font-weight: normal;
font-weight: bold;
font-weight: 400;

/* Multiple Values */
font-weight: normal bold;
font-weight: 300 500;

取值說明:

  1. normal:默認值,表示該字體規則是在默認情況下的字體,也就是在應用改字體的元素中不規定字體的粗細情況或者font-weight: 400 | normal下應用該字體;
  2. bold:粗體,表示元素設置font-weight: bold | 700,或者使用<b><strong>元素的時候應用該字體。
  3. 400:也可以設置成數值,在CSS Fonts Level 4之前的版本只能去100-900的100倍數值,之後的數值可以去1-1000的任意數值。
  4. normal bold:可以使用多個關鍵字來定義此字體規則,多個關鍵字之間用逗號分開,表示元素字重設置為此關鍵字中的其中一個值時應用該字體。
  5. 300 500:也可以使用多個數值來定義此字體規則。

取數值情況下應該對應的每個字體:

value 對應的字體的自重名稱
100 Thin (Hairline)
200 Extra Light (Ultra Light)
300 Light
400 Normal
500 Medium
600 Semi Bold (Demi Bold)
700 Bold
800 Extra Bold (Ultra Bold)
900 Black (Heavy)

代碼示例:因為字體有版權限制,這裏我們使用阿里的免費商用字體來演示

https://codepen.io/qwguo88/full/jObgQYG

從上邊的案例我們可以看出,先自定義了一個名為FW的字體,並且使用font-weight定義不同字重使用不同的字體。在上邊的案例中定義了5中字重樣式,分別是bold阿里巴巴-普惠體-Heavy100楊任東竹石體-Bold200站酷高端黑300 600龐門正道標題體2900思源黑體-粗
然後給div設置font-family:FW;最後我們分別給這個div下的每個段落設置不同的font-weight,段落的字體就會根據不同的字重來應用不同的字體。
我們可以把自定義字體看成我們平常使用系統內置字體一樣,當我們設置字體為微軟雅黑,並且設置不同的字重他會在系統中尋找每個自重對應的字體,然後來显示。

font-style

表示自定義字體規則的樣式表現形式,我們可以給一個字體指定不同的樣式規則引用不同規格的字體文件。

語法:

font-style: normal | italic | oblique <angle>{0,2}

取值說明:

  1. normal:默認字樣式使用的字體規則,當我們不設置或者設置成此值時的字體。
  2. italic:表示字樣式設置成斜體的時候使用的字體規則。
  3. oblique:表示字樣式設置成斜體的時候使用的字體規則。

當我們同時定義italicoblique規則的字體時,寫在後邊的生效所設置的斜體字體显示。

代碼示例: https://codepen.io/qwguo88/full/RwWXONo

unicode-range

表示自定義字體規則的unicode字符範圍

語法:

/* unicode-range 取值規則 */
unicode-range: U+26;                /* 單個值 */
unicode-range: U+0-7F;              /* 字符編碼區間*/
unicode-range: U+0025-00FF;        /* 字符編碼區間 */
unicode-range: U+4??;              /* 通配符區間 */
unicode-range: U+0025-00FF, U+4??; /* 可以寫多個值,多個值之間用逗號分開 */

取值說明:
取值規則:前邊是U+後邊跟上字符的charCode

  1. 可以是單個值,表示文本中只有該字符的字應用該字體。
  2. 可以使用一個字符區間,表示文本中如果有在此區間的文字將應用改字體規則。
  3. 也可以使用通配符來設置一個區間規則其中?表示一個16進制0-F的之間的值U+4??表示 U+400U+4FF區間的字符編碼。
  4. 也可以使用多個值,多個值之間使用逗號分開。

案例:https://codepen.io/qwguo88/full/XWXWqmP

從上邊案例可以看出,unicode-range是用來規定應用當前字體規則的文字unicode碼在規則內的將以此字體規則显示字體。
他能讓我們來控制一個段落中的個別字的显示效果,一般要显示的字體規則排在最前面,將優先显示。

font-display

設置自定義字體在沒有加載完的显示方式取值如下:

語法:

font-display: auto | block | swap | fallback | optional
  1. auto:字體显示策略由用戶代理定義。
  2. block:為字體提供一個短暫的阻塞周期和無限的交換周期。也就是說等字體加載完以後字體显示效果會自動更新成改字體
  3. swap:為字體提供一個非常小的阻塞周期和無限的交換周期。也就是說等字體加載完以後字體显示效果會自動更新成改字體
  4. fallback:為字體提供一個非常小的阻塞周期和短暫的交換周期。也就是說等字體加載在過了一定的交互周期后加載完字體將不進行更新显示
  5. optional:為字體提供一個非常小的阻塞周期,並且沒有交換周期。也就是說等字體加載不進行更新显示

參考網站

  1. https://webplatform.github.io/docs/tutorials/typography/font-face/
  2. https://developer.mozilla.org/zh-CN/docs/Web/CSS/@font-face/font-display
  3. https://www.zhangxinxu.com/wordpress/2016/11/css-unicode-range-character-font-face/
  4. https://www.w3cplus.com/css/font-display-masses.html

字體下載格式轉換網站

  1. https://www.fontke.com/tool/fontface/
  2. http://www.fonts.net.cn/
  3. https://fonts.google.com/

字體壓縮工具

  1. http://www.fonts.net.cn/ 字體天下
  2. http://www.ziticq.com/ 字體傳奇
  3. https://www.hellofont.cn/ 字由
  4. http://fontstore.baidu.com/static/editor/index.html 百度在線字體編輯器
  5. https://efe.baidu.com/ 百度字體處理
  6. https://www.fontsquirrel.com/tools/webfont-generator 字體格式換
  7. https://www.fontke.com/tool/fontface/ 字體轉換

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

【其他文章推薦】

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

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

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

南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

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

※超省錢租車方案

Flutter 中由 BuildContext 引發的血案

今天和各位分享一個博主在實際開發中遇到的問題,以及解決方法。廢話不多說,我們先來看需求:
我們要做一個iOS風格的底部菜單彈出組件,具體涉及showCupertinoModalPopup()方法,該方法被執行后,會出現如下圖類似所示的菜單彈出視圖:

相信這個彈出菜單視圖都有見過吧?下面重點來了:在本次的項目需求中,該視圖的選項文字是由Server端返回的。也就是說,這些選項的內容和個數都不固定,因此不能將其在代碼中寫固定值。
為了簡化代碼以突出重點,下面放上我在一開始的實現方案:

  openActionSheet() {
    List<Widget> menuWidgets = new List();
    menuItems.forEach((element) {
      menuWidgets.add(CupertinoActionSheetAction(
        child: Text(element),
        onPressed: () {
          Navigator.pop(context);
          debugPrint("操作$element被執行“);
        },
        isDefaultAction: true,
      ));
    });

    showCupertinoModalPopup(
        context: context,
        builder: (buildContext) {
          return CupertinoActionSheet(
              title: Text('測試菜單'),
              message: Text('點擊菜單項試試吧!'),
              actions: menuWidgets);
        });
  }

如上述代碼所示,openActionSheet()是显示該組件的方法。其中,showCupertinoModalPopup()為Flutter SDK內置方法,其作用即显示這個組件;再其上面的循環以及List聲明、賦值等操作實際上就是在動態添加菜單項。menuItems類型是List<String>。
通過對代碼的解釋,相信大家能夠一目瞭然地看出,當某個菜單項被點擊時,整個菜單組件消失,並打印Debug Log(對應為真實項目要執行的操作)。
大家覺得上述代碼有問題嗎?如果有問題,問題在哪兒呢?
現在公布答案:這段代碼有問題!
上述代碼執行時,當用戶點擊菜單項后,其運行結果並非如我們預想的那樣:菜單組消失並輸出Log,而變成了:整個頁面被Pop,菜單組保留,並輸出Log!
這是什麼原因呢?
實際上,罪魁禍首就在我們循環遍歷賦值操作時的這條語句:

Navigator.pop(context);

這裏的context是整個頁面的BuildContext,而非菜單組的。這裏我們要明確一個概念——我們想Pop誰,一定要用誰的BuildContext對象。
在這裏,正確的BuildContext對象是誰呢?它在這裏:

showCupertinoModalPopup(
    context: context,
    builder: (buildContext) {
      return CupertinoActionSheet(
          title: Text('測試菜單'),
          message: Text('點擊菜單項試試吧!'),
          actions: menuWidgets);
    }
);

注意到了嗎?上面第三行括號里的buildContext才是我們真正要用的對象。因此,正確的做法是什麼呢?

  openActionSheet() {
    BuildContext tempContext;
    List<Widget> menuWidgets = new List();
    menuItems.forEach((element) {
      menuWidgets.add(CupertinoActionSheetAction(
        child: Text(element),
        onPressed: () {
          Navigator.pop(tempContext);
          debugPrint("操作$element被執行");
        },
        isDefaultAction: true,
      ));
    });

    showCupertinoModalPopup(
        context: context,
        builder: (buildContext) {
          tempContext = buildContext;
          return CupertinoActionSheet(
              title: Text('測試菜單'),
              message: Text('點擊菜單項試試吧!'),
              actions: menuWidgets);
        });
  }

如上所示,我們只需將正確的對象“帶”到其作用域外面就可以了。
好了,這就是本篇文章的全部內容,希望能夠對你有所幫助!

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

【其他文章推薦】

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

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

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

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

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

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

Python 圖像處理 OpenCV (9):圖像處理形態學開運算、閉運算以及梯度運算

前文傳送門:

「Python 圖像處理 OpenCV (1):入門」

「Python 圖像處理 OpenCV (2):像素處理與 Numpy 操作以及 Matplotlib 显示圖像」

「Python 圖像處理 OpenCV (3):圖像屬性、圖像感興趣 ROI 區域及通道處理」

「Python 圖像處理 OpenCV (4):圖像算數運算以及修改顏色空間」

「Python 圖像處理 OpenCV (5):圖像的幾何變換」

「Python 圖像處理 OpenCV (6):圖像的閾值處理」

「Python 圖像處理 OpenCV (7):圖像平滑(濾波)處理」

「Python 圖像處理 OpenCV (8):圖像腐蝕與圖像膨脹」

引言

前面介紹了圖像形態學的兩種基礎算法,圖像腐蝕和圖像膨脹,本篇接着介紹圖像形態學中的開運算、閉運算以及梯度運算。

由於內容的連貫性,請先閱讀前文「Python 圖像處理 OpenCV (8):圖像腐蝕與圖像膨脹」,了解清楚圖像的腐蝕與膨脹基礎原理。

不然真的沒辦法理解開運算和閉運算。

第一件事情還是給圖像增加噪聲,思路沿用之前加噪聲的思路,使用 Numpy 給圖片添加黑白兩種噪聲點,代碼如下:

import cv2 as cv
import numpy as np
import matplotlib.pyplot as plt

# 讀取圖片
img = cv.imread("demo.png", cv.IMREAD_UNCHANGED)
source = cv.cvtColor(img, cv.COLOR_BGR2RGB)
rows, cols, chn = source.shape

# 加噪聲-白點噪聲
for i in range(500):
    x = np.random.randint(0, rows)
    y = np.random.randint(0, cols)
    source[x, y, :] = 255

# 圖像保存 白點噪聲圖像
cv.imwrite("demo_noise_white.jpg", source)
print("白點噪聲添加完成")

# 重新讀取圖像
img1 = cv.imread("demo.png", cv.IMREAD_UNCHANGED)
source1 = cv.cvtColor(img1, cv.COLOR_BGR2RGB)

# 加噪聲-黑點噪聲
for i in range(1000):
    x = np.random.randint(0, rows)
    y = np.random.randint(0, cols)
    source1[x, y, :] = 0

# 圖像保存 黑點噪聲圖像
cv.imwrite("demo_noise_black.jpg", source1)
print("黑點噪聲添加完成")

# 显示結果
titles = ['White Img','Black Img']
images = [source, source1]

# matplotlib 繪圖
for i in range(2):
   plt.subplot(1, 2, i+1), plt.imshow(images[i],'gray')
   plt.title(titles[i])
   plt.xticks([]),plt.yticks([])

plt.show()

形態學開運算

圖像開運算實際上是一個組合運算,開運算是圖像先進行腐蝕,再進行膨脹的運算。

圖像被腐蝕后,去除了噪聲,但是也壓縮了圖像;接着對腐蝕過的圖像進行膨脹處理,使得剛才在腐蝕過程中被壓縮的圖像得以恢復原狀。

下面是一個圖像開運算的流程圖:

開運算的一些特性:

  • 開運算能夠除去孤立的小點,毛刺和小橋,而總的位置和形狀不便。
  • 開運算是一個基於幾何運算的濾波器。
  • 結構元素大小的不同將導致濾波效果的不同。
  • 不同的結構元素的選擇導致了不同的分割,即提取出不同的特徵。

我們先不管開運算 OpenCV 為我們提供的函數是什麼,先使用前面介紹過的圖像腐蝕與膨脹處理看下結果:

import cv2 as cv
import numpy as np
import matplotlib.pyplot as plt

# 讀取圖片
source = cv.imread("demo_noise_white.jpg", cv.IMREAD_GRAYSCALE)

# 設置卷積核
kernel = np.ones((5, 5),np.uint8)

# 圖像腐蝕
erode_img = cv.erode(source, kernel)

# 圖像膨脹
dilate_result = cv.dilate(erode_img, kernel)

# 显示結果
titles = ['Source Img','Erode Img','Dilate Img']
images = [source, erode_img, dilate_result]

# matplotlib 繪圖
for i in range(3):
   plt.subplot(1, 3, i+1), plt.imshow(images[i],'gray')
   plt.title(titles[i])
   plt.xticks([]),plt.yticks([])

plt.show()

可以看到降噪的效果還是不錯的。

接着看 OpenCV 為開運算提供的函數。

圖像開運算主要使用到的函數是 morphologyEx() 它是形態學擴展的一組函數,而其中的 cv.MORPH_OPEN 對應的是開運算。

使用時語法如下:

dst = cv.morphologyEx(src, cv.MORPH_OPEN, kernel)
  • src: 原圖形
  • cv2.MORPH_OPEN: 表示開運算
  • kernel: 卷積核

我們再使用 morphologyEx() 函數去重新實現下剛才的圖像開運算,看下和之前的結果有啥區別:

import cv2 as cv
import numpy as np
import matplotlib.pyplot as plt

# 讀取圖片
source = cv.imread("demo_noise_white.jpg", cv.IMREAD_GRAYSCALE)

# 設置卷積核
kernel = np.ones((5, 5),np.uint8)

#圖像開運算
dst = cv.morphologyEx(source, cv.MORPH_OPEN, kernel)

# 显示結果
titles = ['Source Img','Dst Img']
images = [source, dst]

# matplotlib 繪圖
for i in range(2):
   plt.subplot(1, 2, i+1), plt.imshow(images[i],'gray')
   plt.title(titles[i])
   plt.xticks([]),plt.yticks([])

plt.show()

至少從肉眼的角度上看不出來和之前的方式有啥區別,實際上也沒啥區別。

形態學閉運算

與開運算相反的是閉運算,閉運算是圖像先膨脹,后腐蝕,它有助於關閉前景物體內部的小孔,或物體上的小黑點。

先看下圖像閉運算的流程圖:

閉運算的一些特性:

  • 閉運算能夠填平小湖(即小孔),彌合小裂縫,而總的位置和形狀不變。
  • 閉運算是通過填充圖像的凹角來濾波圖像的。
  • 結構元素大小的不同將導致濾波效果的不同。
  • 不同結構元素的選擇導致了不同的分割。

首先還是用 dilate()erode() 函數實現一下圖像閉運算,代碼如下:

import cv2 as cv
import numpy as np
import matplotlib.pyplot as plt

# 讀取圖片
source = cv.imread("demo_noise_black.jpg", cv.IMREAD_GRAYSCALE)

# 設置卷積核
kernel = np.ones((5, 5),np.uint8)

# 圖像膨脹
dilate_result = cv.dilate(source, kernel)

# 圖像腐蝕
erode_img = cv.erode(dilate_result, kernel)

# 显示結果
titles = ['Source Img','Dilate Img','Erode Img']
images = [source, dilate_result, erode_img]

# matplotlib 繪圖
for i in range(3):
   plt.subplot(1, 3, i+1), plt.imshow(images[i],'gray')
   plt.title(titles[i])
   plt.xticks([]),plt.yticks([])

plt.show()

如果想要使用形態學擴展的函數 morphologyEx() 則需要把裏面的參數換成 MORPH_CLOSE ,同樣,既然是形態學擴展函數,那麼圖像腐蝕和圖像膨脹也有對應的參數:

  • 圖像腐蝕: MORPH_ERODE
  • 圖像膨脹: MORPH_DILATE

接着還是使用 MORPH_CLOSE 參數來實現下圖像的閉運算:

import cv2 as cv
import numpy as np
import matplotlib.pyplot as plt

# 讀取圖片
source = cv.imread("demo_noise_black.jpg", cv.IMREAD_GRAYSCALE)

# 設置卷積核
kernel = np.ones((5, 5),np.uint8)

# 圖像閉運算
dst = cv.morphologyEx(source, cv.MORPH_CLOSE, kernel)

# 显示結果
titles = ['Source Img','Dst Img']
images = [source, dst]

# matplotlib 繪圖
for i in range(2):
   plt.subplot(1, 2, i+1), plt.imshow(images[i],'gray')
   plt.title(titles[i])
   plt.xticks([]),plt.yticks([])

plt.show()

形態學梯度運算

圖像形態學的梯度運算和前面的開運算閉運算是一樣的,都是組合函數。

梯度運算實際上是圖像膨脹減去圖像腐蝕后的結果,最終我們得到的是一個類似於圖像輪廓的圖形。

梯度運算在 morphologyEx() 函數中的參數是 MORPH_GRADIENT ,示例代碼如下:

import cv2 as cv
import numpy as np
import matplotlib.pyplot as plt

# 讀取圖片
source = cv.imread("demo.png", cv.IMREAD_GRAYSCALE)

# 設置卷積核
kernel = np.ones((5, 5), np.uint8)

# 圖像梯度運算
dst = cv.morphologyEx(source, cv.MORPH_GRADIENT, kernel)

# 显示結果
titles = ['Source Img','Dst Img']
images = [source, dst]

# matplotlib 繪圖
for i in range(2):
   plt.subplot(1, 2, i+1), plt.imshow(images[i],'gray')
   plt.title(titles[i])
   plt.xticks([]),plt.yticks([])

plt.show()

示例代碼

如果有需要獲取源碼的同學可以在公眾號回復「OpenCV」進行獲取。

參考

http://www.woshicver.com/

https://blog.csdn.net/Eastmount/article/details/83651172

https://blog.csdn.net/hanshanbuleng/article/details/80657148

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

【其他文章推薦】

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

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

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

南投搬家公司費用需注意的眉眉角角,別等搬了再說!

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

Zookeeper分佈式鎖

分佈式解決方案源碼,請幫我點個star哦!
原文地址為https://www.cnblogs.com/haixiang/p/13112710.html,轉載請註明出處!

zookeeper客戶端選型

  • 原生zookeeper客戶端,有watcher一次性、無超時重連機制等一系列問題
  • ZkClient,解決了原生客戶端一些問題,一些存量老系統中還在使用
  • curator,提供了各種應用場景(封裝了分佈式鎖,計數器等),新項目首選

分佈式鎖使用場景

在單體項目中jvm中的鎖即可完成需要,但是微服務、分佈式環境下,同一個服務可能部署在多台服務器上,多個jvm之間無法通過常用的jvm鎖來完成同步操作,需要借用分佈式鎖來完成上鎖、釋放鎖。例如在訂單服務中,我們需要根據日期來生成訂單號流水,就有可能產生相同的時間日期,從而出現重複訂單號。(jdk8使用LocalDateTime線程安全,不會存在這樣的問題)

zookeeper分佈式鎖實現原理

  • zookeeper中規定,在同一時刻,不能有多個客戶端創建同一個節點,我們可以利用這個特性實現分佈式鎖。zookeeper臨時節點只在session生命周期存在,session一結束會自動銷毀。
  • watcher機制,在代表鎖資源的節點被刪除,即可以觸發watcher解除阻塞重新去獲取鎖,這也是zookeeper分佈式鎖較其他分佈式鎖方案的一大優勢。

基於臨時節點方案

第一種方案實現較為簡單,邏輯就是誰創建成功該節點,誰就持有鎖,創建失敗的自己進行阻塞,A線程先持有鎖,B線程獲取失敗就會阻塞,同時對/lockPath設置監聽,A線程執行完操作后刪除節點,觸發監聽器,B線程此時解除阻塞,重新去獲取鎖。

我們模仿原生jdk的lock接口設計,採用模板方法設計模式來編寫分佈式鎖,這樣的好處是擴展性強,我們可以快速切換到redis分佈式鎖、數據庫分佈式鎖等實現方式。

創建Lock接口

public interface Lock {
    /**
     * 獲取鎖
     */
    void getLock() throws Exception;

    /**
     * 釋放鎖
     */
    void unlock() throws Exception;
}

AbstractTemplateLock抽象類

public abstract class AbstractTemplateLock implements Lock {
    @Override
    public void getLock() {
        if (tryLock()) {
            System.out.println(Thread.currentThread().getName() + "獲取鎖成功");
        } else {
            //等待
            waitLock();//事件監聽 如果節點被刪除則可以重新獲取
            //重新獲取
            getLock();
        }
    }
    protected abstract void waitLock();
    protected abstract boolean tryLock();
    protected abstract void releaseLock();
    @Override
    public void unlock() {
        releaseLock();
    }
}

zookeeper分佈式鎖邏輯

@Slf4j
public class ZkTemplateLock extends AbstractTemplateLock {
    private static final String zkServers = "127.0.0.1:2181";
    private static final int sessionTimeout = 8000;
    private static final int connectionTimeout = 5000;

    private static final String lockPath = "/lockPath";


    private ZkClient client;

    public ZkTemplateLock() {
        client = new ZkClient(zkServers, sessionTimeout, connectionTimeout);
        log.info("zk client 連接成功:{}",zkServers);
    }

    @Override
    protected void waitLock() {
        CountDownLatch latch = new CountDownLatch(1);

        IZkDataListener listener = new IZkDataListener() {
            @Override
            public void handleDataDeleted(String dataPath) throws Exception {
                System.out.println("監聽到節點被刪除");
                latch.countDown();
            }
            @Override
            public void handleDataChange(String dataPath, Object data) throws Exception {}
        };
        //完成 watcher 註冊
        client.subscribeDataChanges(lockPath, listener);

        //阻塞自己
        if (client.exists(lockPath)) {
            try {
                latch.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //取消watcher註冊
        client.unsubscribeDataChanges(lockPath, listener);
    }

    @Override
    protected boolean tryLock() {
        try {
            client.createEphemeral(lockPath);
            System.out.println(Thread.currentThread().getName()+"獲取到鎖");
        } catch (Exception e) {
            log.error("創建失敗");
            return false;
        }
        return true;
    }

    @Override
    public void releaseLock() {
       client.delete(this.lockPath);
    }
}

缺點

每次去競爭鎖,都只會有一個線程拿到鎖,當線程數龐大時會發生“驚群”現象,zookeeper節點可能會運行緩慢甚至宕機。這是因為其他線程沒獲取到鎖時都會監聽/lockPath節點,當A線程釋放完畢,海量的線程都同時停止阻塞,去爭搶鎖,這種操作十分耗費資源,且性能大打折扣。

基於臨時順序節點方案

臨時順序節點與臨時節點不同的是產生的節點是有序的,我們可以利用這一特點,只讓當前線程監聽上一序號的線程,每次獲取鎖的時候判斷自己的序號是否為最小,最小即獲取到鎖,執行完畢就刪除當前節點繼續判斷誰為最小序號的節點。

臨時順序節點操作源碼

@Slf4j
public class ZkSequenTemplateLock extends AbstractTemplateLock {
    private static final String zkServers = "127.0.0.1:2181";
    private static final int sessionTimeout = 8000;
    private static final int connectionTimeout = 5000;
    private static final String lockPath = "/lockPath";
    private String beforePath;
    private String currentPath;
    private ZkClient client;

    public ZkSequenTemplateLock() {
        client = new ZkClient(zkServers);
        if (!client.exists(lockPath)) {
            client.createPersistent(lockPath);

        }
        log.info("zk client 連接成功:{}",zkServers);

    }

    @Override
    protected void waitLock() {
        CountDownLatch latch = new CountDownLatch(1);
        IZkDataListener listener = new IZkDataListener() {
            @Override
            public void handleDataDeleted(String dataPath) throws Exception {
                System.out.println("監聽到節點被刪除");
                latch.countDown();
            }
            @Override
            public void handleDataChange(String dataPath, Object data) throws Exception {}
        };
        //給排在前面的節點增加數據刪除的watcher,本質是啟動另一個線程去監聽上一個節點
        client.subscribeDataChanges(beforePath, listener);

        //阻塞自己
        if (client.exists(beforePath)) {
            try {
                System.out.println("阻塞"+currentPath);
                latch.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //取消watcher註冊
        client.unsubscribeDataChanges(beforePath, listener);
    }

    @Override
    protected boolean tryLock() {
        if (currentPath == null) {
            //創建一個臨時順序節點
            currentPath = client.createEphemeralSequential(lockPath + "/", "lock-data");
            System.out.println("current:" + currentPath);
        }

        //獲得所有的子節點並排序。臨時節點名稱為自增長的字符串
        List<String> childrens = client.getChildren(lockPath);
        //排序list,按自然順序排序
        Collections.sort(childrens);
        if (currentPath.equals(lockPath + "/" + childrens.get(0))) {
            return true;
        } else {
            //如果當前節點不是排第一,則獲取前面一個節點信息,賦值給beforePath
            int curIndex = childrens.indexOf(currentPath.substring(lockPath.length() + 1));
            beforePath = lockPath + "/" + childrens.get(curIndex - 1);
        }
        System.out.println("beforePath"+beforePath);
        return false;
    }

    @Override
    public void releaseLock() {
        System.out.println("delete:" + currentPath);
        client.delete(currentPath);
    }
}

Curator分佈式鎖工具

curator提供了以下種類的鎖:

  • 共享可重入鎖(Shared Reentrant Lock):全局同步鎖,同一時間不會有兩個客戶端持有一個鎖
  • 共享鎖:與共享可重入鎖類似,但是不可重入(有時候會因為這個原因造成死鎖)
  • 共享可重入讀寫鎖
  • 共享信號量
  • Multi Shared Lock:管理多種鎖的容器實體

我們採用第一種Shared Reentrant Lock中的InterProcessMutex來完成上鎖、釋放鎖的的操作

public class ZkLockWithCuratorTemplate implements Lock {
    // zk host地址
    private String host = "localhost";

    // zk自增存儲node
    private String lockPath = "/curatorLock";

    // 重試休眠時間
    private static final int SLEEP_TIME_MS = 1000;
    // 最大重試1000次
    private static final int MAX_RETRIES = 1000;
    //會話超時時間
    private static final int SESSION_TIMEOUT = 30 * 1000;
    //連接超時時間
    private static final int CONNECTION_TIMEOUT = 3 * 1000;
		//curator核心操作類
    private CuratorFramework curatorFramework;

    InterProcessMutex lock;

   public ZkLockWithCuratorTemplate() {
       curatorFramework = CuratorFrameworkFactory.builder()
               .connectString(host)
               .connectionTimeoutMs(CONNECTION_TIMEOUT)
               .sessionTimeoutMs(SESSION_TIMEOUT)
               .retryPolicy(new ExponentialBackoffRetry(SLEEP_TIME_MS, MAX_RETRIES))
               .build();
       curatorFramework.start();
       lock = new InterProcessMutex (curatorFramework, lockPath);
    }

    @Override
    public void getLock() throws Exception {
        //5s后超時釋放鎖
         lock.acquire(5, TimeUnit.SECONDS);
    }

    @Override
    public void unlock() throws Exception {
        lock.release();
    }
}

源碼以及測試類地址

https://github.com/Motianshi/distribute-tool

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

【其他文章推薦】

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

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

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

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

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

通過Nginx、Consul、Upsync實現動態負載均衡和服務平滑發布

前提

前段時間順利地把整個服務集群和中間件全部從UCloud遷移到阿里雲,筆者擔任了架構和半個運維的角色。這裏詳細記錄一下通過NginxConsulUpsync實現動態負載均衡和服務平滑發布的核心知識點和操作步驟,整個體系已經在生產環境中平穩運行。編寫本文使用的虛擬機系統為CentOS7.x,虛擬機的內網IP192.168.56.200

動態負載均衡的基本原理

一般會通過upstream配置Nginx的反向代理池:

http {
    
    upstream upstream_server{
        server 127.0.0.1:8081;
        server 127.0.0.1:8082;
    }

    server {
        listen       80;
        server_name localhost;

        location / {
            proxy_pass http://upstream_server;
        }
    }
}

現在假如8081端口的服務實例掛了需要剔除,那麼需要修改upstream為:

upstream upstream_server{
    # 添加down標記該端口的服務實例不參与負載
    server 127.0.0.1:8081 down;
    server 127.0.0.1:8082;
}

並且通過nginx -s reload重新加載配置,該upstream配置才會生效。我們知道,服務發布時候重啟過程中是處於不可用狀態,正確的服務發布過程應該是:

  • 把該服務從對應的upstream剔除,一般是置為down,告知Nginx服務upstream配置變更,需要通過nginx -s reload進行重載。
  • 服務構建、部署和重啟。
  • 通過探活腳本感知服務對應的端口能夠訪問,把該服務從對應的upstream中拉起,一般是把down去掉,告知Nginx服務upstream配置變更,需要通過nginx -s reload進行重載。

上面的步驟一則涉及到upstream配置,二則需要Nginx重新加載配置(nginx -s reload),顯得比較笨重,在高負載的情況下重新啟動Nginx並重新加載配置會進一步增加系統的負載並可能暫時降低性能。

所以,可以考慮使用分佈式緩存把upstream配置存放在緩存服務中,然後Nginx直接從這個緩存服務中讀取upstream的配置,這樣如果有upstream的配置變更就可以直接修改緩存服務中對應的屬性,而Nginx服務也不需要reload。在實戰中,這裏提到的緩存服務就選用了ConsulNginx讀取緩存中的配置屬性選用了新浪微博提供的NginxC語言模塊nginx-upsync-module。示意圖大致如下:

Consul安裝和集群搭建

ConsulHashicorp公司的一個使用Golang開發的開源項目,它是一個用於服務發現和配置的工具,具備分佈式和高度可用特性,並且具有極高的可伸縮性。Consul主要提供下面的功能:

  • 服務發現。
  • 運行狀況檢查。
  • 服務分塊/服務網格(Service Segmentation/Service Mesh)。
  • 密鑰/值存儲。
  • 多數據中心。

下面是安裝過程:

mkdir /data/consul
cd /data/consul
wget https://releases.hashicorp.com/consul/1.7.3/consul_1.7.3_linux_amd64.zip
# 注意解壓后只有一個consul執行文件
unzip consul_1.7.3_linux_amd64.zip

解壓完成后,使用命令nohup /data/consul/consul agent -server -data-dir=/tmp/consul -bootstrap -ui -advertise=192.168.56.200 -client=192.168.56.200 > /dev/null 2>&1 &即可後台啟動單機的Consul服務。啟動Consul實例后,訪問http://192.168.56.200:8500/即可打開其後台管理UI

下面基於單台虛擬機搭建一個偽集群,關於集群的一些配置屬性的含義和命令參數的解釋暫時不進行展開

# 創建集群數據目錄
mkdir /data/consul/node1 /data/consul/node2 /data/consul/node3
# 創建集群日誌目錄
mkdir /data/consul/node1/logs /data/consul/node2/logs /data/consul/node3/logs

/data/consul/node1目錄添加consul_conf.json文件,內容如下:

{
  "datacenter": "es8-dc",
  "data_dir": "/data/consul/node1",
  "log_file": "/data/consul/node1/consul.log",
  "log_level": "INFO",
  "server": true,
  "node_name": "node1",
  "ui": true,
  "bind_addr": "192.168.56.200",
  "client_addr": "192.168.56.200",
  "advertise_addr": "192.168.56.200",
  "bootstrap_expect": 3,
  "ports":{
    "http": 8510,
    "dns": 8610,
    "server": 8310,
    "serf_lan": 8311,
    "serf_wan": 8312
    }
}

/data/consul/node2目錄添加consul_conf.json文件,內容如下:

{
  "datacenter": "es8-dc",
  "data_dir": "/data/consul/node2",
  "log_file": "/data/consul/node2/consul.log",
  "log_level": "INFO",
  "server": true,
  "node_name": "node2",
  "ui": true,
  "bind_addr": "192.168.56.200",
  "client_addr": "192.168.56.200",
  "advertise_addr": "192.168.56.200",
  "bootstrap_expect": 3,
  "ports":{
    "http": 8520,
    "dns": 8620,
    "server": 8320,
    "serf_lan": 8321,
    "serf_wan": 8322
    }
}

/data/consul/node3目錄添加consul_conf.json文件,內容如下:

{
  "datacenter": "es8-dc",
  "data_dir": "/data/consul/node3",
  "log_file": "/data/consul/node3/consul.log",
  "log_level": "INFO",
  "server": true,
  "node_name": "node3",
  "ui": true,
  "bind_addr": "192.168.56.200",
  "client_addr": "192.168.56.200",
  "advertise_addr": "192.168.56.200",
  "bootstrap_expect": 3,
  "ports":{
    "http": 8530,
    "dns": 8630,
    "server": 8330,
    "serf_lan": 8331,
    "serf_wan": 8332
    }
}

新建一個集群啟動腳本:

cd /data/consul
touch service.sh
# /data/consul/service.sh內容如下:
nohup /data/consul/consul agent -config-file=/data/consul/node1/consul_conf.json > /dev/null 2>&1 &
sleep 10
nohup /data/consul/consul agent -config-file=/data/consul/node2/consul_conf.json -retry-join=192.168.56.200:8311 > /dev/null 2>&1 &
sleep 10
nohup /data/consul/consul agent -config-file=/data/consul/node3/consul_conf.json -retry-join=192.168.56.200:8311 > /dev/null 2>&1 &

如果集群啟動成功,觀察節點1中的日誌如下:

通過節點1的HTTP端點訪問後台管理頁面如下(可見當前的節點1被標記了一顆紅色的星星,說明當前節點1是Leader節點):

至此,Consul單機偽集群搭建完成(其實分佈式集群的搭建大同小異,注意集群節點所在的機器需要開放使用到的端口的訪問權限),由於Consul使用Raft作為共識算法,該算法是強領導者模型,也就是只有Leader節點可以進行寫操作,因此接下來的操作都需要使用節點1的HTTP端點,就是192.168.56.200:8510

重點筆記:如果Consul集群重啟或者重新選舉,Leader節點有可能發生更變,外部使用的時候建議把Leader節點的HTTP端點抽離到可動態更新的配置項中或者動態獲取Leader節點的IP和端口。

Nginx編譯安裝

直接從官網下載二進制的安裝包並且解壓:

mkdir /data/nginx
cd /data/nginx
wget http://nginx.org/download/nginx-1.18.0.tar.gz
tar -zxvf nginx-1.18.0.tar.gz

解壓后的所有源文件在/data/nginx/nginx-1.18.0目錄下,編譯之前需要安裝pcre-develzlib-devel依賴:

yum -y install pcre-devel
yum install -y zlib-devel

編譯命令如下:

cd /data/nginx/nginx-1.18.0
./configure --prefix=/data/nginx

如果./configure執行過程不出現問題,那麼結果如下:

接着執行make

cd /data/nginx/nginx-1.18.0
make

如果make執行過程不出現問題,那麼結果如下:

最後,如果是首次安裝,可以執行make install進行安裝(實際上只是拷貝編譯好的文件到--prefix指定的路徑下):

cd /data/nginx/nginx-1.18.0
make install

make install執行完畢后,/data/nginx目錄下新增了數個文件夾:

其中,Nginx啟動程序在sbin目錄下,logs是其日誌目錄,conf是其配置文件所在的目錄。嘗試啟動一下Nginx

/data/nginx/sbin/nginx

然後訪問虛擬機的80端口,從而驗證Nginx已經正常啟動:

通過nginx-upsync-module和nginx_upstream_check_module模塊進行編譯

上面做了一個Nginx極簡的編譯過程,實際上,在做動態負載均衡的時候需要添加nginx-upsync-modulenginx_upstream_check_module兩個模塊,兩個模塊必須提前下載源碼,並且在編譯Nginx過程中需要指定兩個模塊的物理路徑:

mkdir /data/nginx/modules
cd /data/nginx/modules
# 這裡是Github的資源,不能用wget下載,具體是:
nginx-upsync-module需要下載release裏面的最新版本:v2.1.2
nginx_upstream_check_module需要下載整個項目的源碼,主要用到靠近當前版本的補丁,使用patch命令進行補丁升級

下載完成後分別(解壓)放在/data/nginx/modules目錄下:

ll /data/nginx/modules
drwxr-xr-x. 6 root root   4096 Nov  3  2019 nginx_upstream_check_module-master
drwxrwxr-x. 5 root root     93 Dec 18 00:56 nginx-upsync-module-2.1.2

編譯前,還要先安裝一些前置依賴組件:

yum -y install libpcre3 libpcre3-dev ruby zlib1g-dev patch

接下來開始編譯安裝Nginx

cd /data/nginx/nginx-1.18.0
patch -p1 < /data/nginx/modules/nginx_upstream_check_module-master/check_1.16.1+.patch
./configure --prefix=/data/nginx --add-module=/data/nginx/modules/nginx_upstream_check_module-master --add-module=/data/nginx/modules/nginx-upsync-module-2.1.2
make
make install

上面的編譯和安裝過程無論怎麼調整,都會出現部分依賴缺失導致make異常,估計是這兩個模塊並不支持太高版本的Nginx。(生產上用了一個版本比較低的OpenResty,這裏想復原一下使用相對新版本Nginx的踩坑過程)於是嘗試降級進行編譯,下面是參考多個Issue后得到的相對比較新的可用版本組合:

  • nginx-1.14.2.tar.gz
  • xiaokai-wang/nginx_upstream_check_module,使用補丁check_1.12.1+.patch
  • nginx-upsync-module:release:v2.1.2
# 提前把/data/nginx下除了之前下載過的modules目錄外的所有文件刪除
cd /data/nginx
wget http://nginx.org/download/nginx-1.14.2.tar.gz
tar -zxvf nginx-1.14.2.tar.gz

開始編譯安裝:

cd /data/nginx/nginx-1.14.2
patch -p1 < /data/nginx/modules/nginx_upstream_check_module-master/check_1.12.1+.patch
./configure --prefix=/data/nginx --add-module=/data/nginx/modules/nginx_upstream_check_module-master --add-module=/data/nginx/modules/nginx-upsync-module-2.1.2
make && make install

安裝完成后通過/data/nginx/sbin/nginx命令啟動即可。

啟用動態負載均和健康檢查

首先編寫一個簡易的HTTP服務,因為Java比較重量級,這裏選用Golang,代碼如下:

package main

import (
	"flag"
	"fmt"
	"net/http"
)

func main() {
    var host string
    var port int
    flag.StringVar(&host, "h", "127.0.0.1", "IP地址")
    flag.IntVar(&port, "p", 9000, "端口")
    flag.Parse()
    address := fmt.Sprintf("%s:%d", host, port)
    http.HandleFunc("/ping", func(writer http.ResponseWriter, request *http.Request) {
        _, _ = fmt.Fprintln(writer, fmt.Sprintf("%s by %s", "pong", address))
    })
    http.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) {
        _, _ = fmt.Fprintln(writer, fmt.Sprintf("%s by %s", "hello world", address))
    })
    err := http.ListenAndServe(address, nil)
    if nil != err {
        panic(err)
    }
}

編譯:

cd src
set GOARCH=amd64
set GOOS=linux
go build -o ../bin/app app.go

這樣子在項目的bin目錄下就得到一個Linux下可執行的二進制文件app,分別在端口90009001啟動兩個服務實例:

# 記得先給app文件的執行權限chmod 773 app
nohup ./app -p 9000 >/dev/null 2>&1 &
nohup ./app -p 9001 >/dev/null 2>&1 &

修改一下Nginx的配置,添加upstream

# /data/nginx/conf/nginx.conf部分片段
http {
    include       mime.types;
    default_type  application/octet-stream;
    sendfile        on;
    keepalive_timeout  65;

    upstream app {
       # 這裡是consul的leader節點的HTTP端點
       upsync 192.168.56.200:8510/v1/kv/upstreams/app/ upsync_timeout=6m upsync_interval=500ms upsync_type=consul strong_dependency=off;
       # consul訪問不了的時候的備用配置
       upsync_dump_path /data/nginx/app.conf;
       # 這裡是為了兼容Nginx的語法檢查
       include /data/nginx/app.conf;
       # 下面三個配置是健康檢查的配置
       check interval=1000 rise=2 fall=2 timeout=3000 type=http default_down=false;
       check_http_send "HEAD / HTTP/1.0\r\n\r\n";
       check_http_expect_alive http_2xx http_3xx;
    }

    server {
        listen       80;
        server_name  localhost;
        location / {
            proxy_pass http://app;
        }
        # 健康檢查 - 查看負載均衡的列表
        location /upstream_list {
            upstream_show;
        }
        # 健康檢查 - 查看負載均衡的狀態
        location /upstream_status {
            check_status;
            access_log off;
        }
    }
}

# /data/nginx/app.conf
server 127.0.0.1:9000 weight=1 fail_timeout=10 max_fails=3;
server 127.0.0.1:9001 weight=1 fail_timeout=10 max_fails=3;

手動添加兩個HTTP服務進去Consul中:

curl -X PUT -d '{"weight":1, "max_fails":2, "fail_timeout":10}' http://192.168.56.200:8510/v1/kv/upstreams/app/127.0.0.1:9000
curl -X PUT -d '{"weight":1, "max_fails":2, "fail_timeout":10}' http://192.168.56.200:8510/v1/kv/upstreams/app/127.0.0.1:9001

最後重新加載Nginx的配置即可。

動態負載均衡測試

前置工作準備好,現在嘗試動態負載均衡,先從Consul下線9000端口的服務實例:

curl -X PUT -d '{"weight":1, "max_fails":2, "fail_timeout":10, "down":1}' http://192.168.56.200:8510/v1/kv/upstreams/app/127.0.0.1:9000

可見負載均衡的列表中,9000端口的服務實例已經置為down,此時瘋狂請求http://192.168.56.200,只輸出hello world by 127.0.0.1:9001,可見9000端口的服務實例已經不再參与負載。重新上線9000端口的服務實例:

curl -X PUT -d '{"weight":1, "max_fails":2, "fail_timeout":10, "down":0}' http://192.168.56.200:8510/v1/kv/upstreams/app/127.0.0.1:9000

再瘋狂請求http://192.168.56.200,發現hello world by 127.0.0.1:9000hello world by 127.0.0.1:9001交替輸出。到此可以驗證動態負載均衡是成功的。此時再測試一下服務健康監測,通過kill -9隨機殺掉其中一個服務實例,然後觀察/upstream_status端點:

瘋狂請求http://192.168.56.200,只輸出hello world by 127.0.0.1:9001,可見9000端口的服務實例已經不再參与負載,但是查看Consul9000端口的服務實例的配置,並沒有標記為down,可見是nginx_upstream_check_module為我們過濾了異常的節點,讓這些節點不再參与負載。

總的來說,這個相對完善的動態負載均衡功能需要nginx_upstream_check_modulenginx-upsync-module共同協作才能完成。

服務平滑發布

服務平滑發布依賴於前面花大量時間分析的動態負載均衡功能。筆者所在的團隊比較小,所以選用了阿里雲的雲效作為產研管理平台,通過裏面的流水線功能實現了服務平滑發布,下面是其中一個服務的生產環境部署的流水線:

其實平滑發布和平台的關係不大,整體的步驟大概如下:

步驟比較多,並且涉及到大量的shell腳本,這裏不把詳細的腳本內容列出,簡單列出一下每一步的操作(注意某些步驟之間可以插入合理的sleep n保證前一步執行完畢):

  • 代碼掃描、單元測試等等。
  • 代碼構建,生成構建后的壓縮包。
  • 壓縮包上傳到服務器X中,解壓到對應的目錄。
  • Consul發送指令,把當前發布的X_IP:PORT的負載配置更新為down=1
  • stop服務X_IP:PORT
  • start服務X_IP:PORT
  • 檢查服務X_IP:PORT的健康狀態(可以設定一個時間周期例如120秒內每10秒檢查一次),如果啟動失敗,則直接中斷返回,確保還有另一個正常的舊節點參与負載,並且人工介入處理。
  • Consul發送指令,把當前發布的X_IP:PORT的負載配置更新為down=0

上面的流程是通過hard code完成,對於不同的服務器,只需要添加一個發布流程節點並且改動一個IP的佔位符即可,不需要對Nginx進行配置重新加載。筆者所在的平台流量不大,目前每個服務部署兩個節點就能滿足生產需要,試想一下,如果要實現動態擴容,應該怎麼構建流水線?

小結

服務平滑發布是CI/CD中比較重要的一個環節,而動態負載均衡則是服務平滑發布的基礎。雖然現在很多雲平台都提供了十分便捷的持續集成工具,但是在使用這些工具和配置流程的時候,最好能夠理解背後的基本原理,這樣才能在工具不適用的時候或者出現問題的時時候,迅速地作出判斷和響應。

參考資料:

  • nginx-upsync-module
  • Nginx docs
  • Consul docs

(本文完 c-7-d e-a-20200613 感謝廣州某金融科技公司運維大佬昊哥提供的支持)

技術公眾號(《Throwable文摘》),不定期推送筆者原創技術文章(絕不抄襲或者轉載):

娛樂公眾號(《天天沙雕》),甄選奇趣沙雕圖文和視頻不定期推送,緩解生活工作壓力:

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

【其他文章推薦】

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

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

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

※超省錢租車方案

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

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

(數據科學學習手札87)利用adjustText解決matplotlib文字標籤遮擋問題

本文示例代碼、數據已上傳至我的Github倉庫https://github.com/CNFeffery/DataScienceStudyNotes

1 簡介

  在進行數據可視化時我們常常需要在可視化作品上進行一些文字標註,譬如對散點圖我們可以將每個散點對應的屬性信息標註在每個散點旁邊,但隨着散點量的增多,或圖像上的某個區域聚集了較多的散點時,疊加上的文字標註會擠在一起相互疊置,出現如圖1所示的情況:

圖1

  出現這種情況非常影響數據可視化作品的呈現效果,而我們下面要介紹的adjustText是一個輔助matplotlib所繪製的圖像自動調整文字位置以緩解遮擋現象的庫,其靈感來源於R中非常著名的輔助ggplot2解決文字遮擋問題的ggrepel

圖2

  它通過算法迭代,在一輪輪的迭代過程中逐漸消除文字遮擋現象:

圖3

  下面我們就來學習如何使用adjustText解決matplotlib圖像文字遮擋問題。

2 使用adjustText解決文字遮擋問題

2.1 從一個簡單的例子出發

  使用pip install adjustTextconda install -c conda-forge adjusttext 來安裝adjustText。安裝成功之後,首先生成隨機示例數據以方便之後的演示:

import matplotlib.pyplot as plt
from adjustText import adjust_text
import numpy as np

#解決中文显示問題
plt.rcParams['font.sans-serif'] = ['SimHei']

seed = np.random.RandomState(42) # 固定隨機數水平
x, y = seed.uniform(0, 1, [2, 100]) # 產生固定的均勻分佈隨機數
texts = [f'文字{i}' for i in range(x.__len__())]

  接着我們先不使用adjustText調整圖像,直接繪製出原始的散點+文字標籤

fig, ax = plt.subplots(figsize=(8, 8))
ax.scatter(x, y, c='SeaGreen', s=10) # 繪製散點

# 繪製所有點對應的文字標籤
for x_, y_, text in zip(x, y, texts):
    plt.text(x_, y_, text, fontsize=12)

# 美觀起見隱藏頂部與右側邊框線
ax.spines['right'].set_visible(False)
ax.spines['top'].set_visible(False)

fig.savefig('圖4.png', dpi=300, bbox_inches='tight', pad_inches=0) # 保存圖像

圖4

  可以看到,在通常的情況下,散點聚集的區域內文字標籤非常容易重疊在一起,接下來我們使用adjustText的基礎功能來消除文字重疊現象:

圖5

  這時可以看到與圖4相比,圖5中的所有文字都沒有出現彼此重疊現象,adjustText幫助我們自動微調了文字的擺放位置,並且距離原始散點偏移較大的文字還貼心的加上了連接線,至此,我們就初探了adjustText的強大功能,接下來我們來學習adjustText的更多功能。

2.2 adjust_text的用法

  adjustText中的核心功能都通過調用函數adjust_text來實現,其核心參數如下:

texts:List型,每個元素都是表示單個文字標籤對應的matplotlib.text.Text對象

ax:繪製文字標籤的目標axe對象,默認為最近一次的axe對象

lim:int型,控制迭代調整文本標籤位置的次數,默認為500次

precision:float型,用於決定迭代停止的精度,默認為0.01,即所有標籤相互遮擋部分的長和寬占所有標籤自身長寬之和的比例,addjust_text會在精度達到precision和迭代次數超過lim這兩個條件中至少有一個滿足時停止迭代

only_move:字典型,用於指定文本標籤與不同對象發生遮擋時的位移策略,鍵有'points''text''objects',對應的值可選'xy''x''y',分別代表豎直和水平方向均調整、只調整水平方向以及只調整豎直方向

arrowprops:字典型,用於設置偏移后的文字標籤與原始位置之間的連線樣式,下文會作具體演示

save_steps:bool型,用於決定是否保存記錄迭代過程中各輪的幀圖像,默認為False

save_prefix:str型,當save_steps設置為True時,用於指定中間幀保存的路徑,默認為”,即當前工作路徑

  下面我們來演示一下這些參數的使用效果,首先我們來看看only_move參數的效果,在圖6的基礎上,我們設置only_move={'text': 'x'},即當文字出現遮擋時,只在水平方向上進行偏移,這裏將save_steps設置為True以直觀地查看偏移過程:

fig, ax = plt.subplots(figsize=(8, 8))
ax.scatter(x, y, c='SeaGreen', s=10) # 繪製散點

# 使用adjustText修正文字重疊現象
new_texts = [plt.text(x_, y_, text, fontsize=12) for x_, y_, text in zip(x, y, texts)]
adjust_text(new_texts, 
            only_move={'text': 'x'},
            arrowprops=dict(arrowstyle='-', color='grey'),
            save_steps=True)

# 美觀起見隱藏頂部與右側邊框線
ax.spines['right'].set_visible(False)
ax.spines['top'].set_visible(False)

圖6

  可以看到在整個迭代微調的過程中,每個標籤只在水平方向發生位移,你可以根據自己作圖的實際需要靈活調整這裏的平移策略。接下來我們來看看arrowprops對可視化結果的影響,在之前的例子里我們設置了arrowprops={arrowstyle='-', color='grey'},其中arrowstyle用於設定連線的線型,color不用多說,接下來我們添加參數lw用於控制線的寬度,並對線型與顏色進行修改:

fig, ax = plt.subplots(figsize=(8, 8))
ax.scatter(x, y, c='SeaGreen', s=10) # 繪製散點

# 使用adjustText修正文字重疊現象
new_texts = [plt.text(x_, y_, text, fontsize=12) for x_, y_, text in zip(x, y, texts)]
adjust_text(new_texts, 
            arrowprops=dict(arrowstyle='->', 
                            color='red',
                            lw=1))

# 美觀起見隱藏頂部與右側邊框線
ax.spines['right'].set_visible(False)
ax.spines['top'].set_visible(False)

fig.savefig('圖7.png', dpi=300, bbox_inches='tight', pad_inches=0) # 保存圖像

  這時連線隨着我們自定義的設置改變到相應的樣式:

圖7

  有關adjustText的更多參數設置信息和示例可以去官方文檔(https://adjusttext.readthedocs.io/en/latest/ )查看。

  以上就是本文的全部內容,如有疑問歡迎在評論區與我們討論。

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

【其他文章推薦】

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

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

※超省錢租車方案

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

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

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

為何說要多用組合少用繼承?如何決定該用組合還是繼承?

在面向對象編程中,有一條非常經典的設計原則,那就是:組合優於繼承,多用組合少用繼承。為什麼不推薦使用繼承?組合相比繼承有哪些優勢?如何判斷該用組合還是繼承?今天,我們就圍繞着這三個問題,來詳細講解一下這條設計原則。

為什麼不推薦使用繼承?

繼承是面向對象的四大特性之一,用來表示類之間的 is-a 關係,可以解決代碼復用的問題。雖然繼承有諸多作用,但繼承層次過深、過複雜,也會影響到代碼的可維護性。所以,對於是否應該在項目中使用繼承,網上有很多爭議。很多人覺得繼承是一種反模式,應該盡量少用,甚至不用。為什麼會有這樣的爭議?我們通過一個例子來解釋一下。

假設我們要設計一個關於鳥的類。我們將“鳥類”這樣一個抽象的事物概念,定義為一個抽象類 AbstractBird。所有更細分的鳥,比如麻雀、鴿子、烏鴉等,都繼承這個抽象類。

我們知道,大部分鳥都會飛,那我們可不可以在 AbstractBird 抽象類中,定義一個 fly() 方法呢?答案是否定的。儘管大部分鳥都會飛,但也有特例,比如鴕鳥就不會飛。鴕鳥繼承具有 fly() 方法的父類,那鴕鳥就具有“飛”這樣的行為,這顯然不符合我們對現實世界中事物的認識。當然,你可能會說,我在鴕鳥這個子類中重寫(override)fly() 方法,讓它拋出 UnSupportedMethodException 異常不就可以了嗎?具體的代碼實現如下所示:

public class AbstractBird {
  //...省略其他屬性和方法...
  public void fly() { //... }
}

public class Ostrich extends AbstractBird { //鴕鳥
  //...省略其他屬性和方法...
  public void fly() {
    throw new UnSupportedMethodException("I can't fly.'");
  }
}

這種設計思路雖然可以解決問題,但不夠優美。因為除了鴕鳥之外,不會飛的鳥還有很多,比如企鵝。對於這些不會飛的鳥來說,我們都需要重寫 fly() 方法,拋出異常。這樣的設計,一方面,徒增了編碼的工作量;另一方面,也違背了我們之後要講的最小知識原則(Least Knowledge Principle,也叫最少知識原則或者迪米特法則),暴露不該暴露的接口給外部,增加了類使用過程中被誤用的概率。

可能又會說,那我們再通過 AbstractBird 類派生出兩個更加細分的抽象類:會飛的鳥類 AbstractFlyableBird 和不會飛的鳥類 AbstractUnFlyableBird,讓麻雀、烏鴉這些會飛的鳥都繼承 AbstractFlyableBird,讓鴕鳥、企鵝這些不會飛的鳥,都繼承 AbstractUnFlyableBird 類,不就可以了嗎?具體的繼承關係如下圖所示:

從圖中我們可以看出,繼承關係變成了三層。不過,整體上來講,目前的繼承關係還比較簡單,層次比較淺,也算是一種可以接受的設計思路。我們再繼續加點難度。在剛剛這個場景中,我們只關注“鳥會不會飛”,但如果我們還關注“鳥會不會叫”,那這個時候,我們又該如何設計類之間的繼承關係呢?

是否會飛?是否會叫?兩個行為搭配起來會產生四種情況:會飛會叫、不會飛會叫、會飛不會叫、不會飛不會叫。如果我們繼續沿用剛才的設計思路,那就需要再定義四個抽象類(AbstractFlyableTweetableBird、AbstractFlyableUnTweetableBird、AbstractUnFlyableTweetableBird、AbstractUnFlyableUnTweetableBird)。

如果我們還需要考慮“是否會下蛋”這樣一個行為,那估計就要組合爆炸了。類的繼承層次會越來越深、繼承關係會越來越複雜。而這種層次很深、很複雜的繼承關係,一方面,會導致代碼的可讀性變差。因為我們要搞清楚某個類具有哪些方法、屬性,必須閱讀父類的代碼、父類的父類的代碼……一直追溯到最頂層父類的代碼。另一方面,這也破壞了類的封裝特性,將父類的實現細節暴露給了子類。子類的實現依賴父類的實現,兩者高度耦合,一旦父類代碼修改,就會影響所有子類的邏輯。

總之,繼承最大的問題就在於:繼承層次過深、繼承關係過於複雜會影響到代碼的可讀性和可維護性。這也是為什麼我們不推薦使用繼承。那剛剛例子中繼承存在的問題,我們又該如何來解決呢?你可以先自己思考一下,再聽我下面的講解。

組合相比繼承有哪些優勢?

實際上,我們可以利用組合(composition)、接口、委託(delegation)三個技術手段,一塊兒來解決剛剛繼承存在的問題。

我們前面講到接口的時候說過,接口表示具有某種行為特性。針對“會飛”這樣一個行為特性,我們可以定義一個 Flyable 接口,只讓會飛的鳥去實現這個接口。對於會叫、會下蛋這些行為特性,我們可以類似地定義 Tweetable 接口、EggLayable 接口。

public interface Flyable {
  void fly();
}
public interface Tweetable {
  void tweet();
}
public interface EggLayable {
  void layEgg();
}
public class Ostrich implements Tweetable, EggLayable {//鴕鳥
  //... 省略其他屬性和方法...
  @Override
  public void tweet() { //... }
  @Override
  public void layEgg() { //... }
}
public class Sparrow impelents Flayable, Tweetable, EggLayable {//麻雀
  //... 省略其他屬性和方法...
  @Override
  public void fly() { //... }
  @Override
  public void tweet() { //... }
  @Override
  public void layEgg() { //... }
}

不過,我們知道,接口只聲明方法,不定義實現。也就是說,每個會下蛋的鳥都要實現一遍 layEgg() 方法,並且實現邏輯是一樣的,這就會導致代碼重複的問題。那這個問題又該如何解決呢?

我們可以針對三個接口再定義三個實現類,它們分別是:實現了 fly() 方法的 FlyAbility 類、實現了 tweet() 方法的 TweetAbility 類、實現了 layEgg() 方法的 EggLayAbility 類。然後,通過組合和委託技術來消除代碼重複。具體的代碼實現如下所示:

public interface Flyable {
  void fly();
}
public class FlyAbility implements Flyable {
  @Override
  public void fly() { //... }
}
//省略Tweetable/TweetAbility/EggLayable/EggLayAbility

public class Ostrich implements Tweetable, EggLayable {//鴕鳥
  private TweetAbility tweetAbility = new TweetAbility(); //組合
  private EggLayAbility eggLayAbility = new EggLayAbility(); //組合
  //... 省略其他屬性和方法...
  @Override
  public void tweet() {
    tweetAbility.tweet(); // 委託
  }
  @Override
  public void layEgg() {
    eggLayAbility.layEgg(); // 委託
  }
}

我們知道繼承主要有三個作用:表示 is-a 關係,支持多態特性,代碼復用。而這三個作用都可以通過其他技術手段來達成。比如 is-a 關係,我們可以通過組合和接口的 has-a 關係來替代;多態特性我們可以利用接口來實現;代碼復用我們可以通過組合和委託來實現。所以,從理論上講,通過組合、接口、委託三個技術手段,我們完全可以替換掉繼承,在項目中不用或者少用繼承關係,特別是一些複雜的繼承關係。

如何判斷該用組合還是繼承?

儘管我們鼓勵多用組合少用繼承,但組合也並不是完美的,繼承也並非一無是處。從上面的例子來看,繼承改寫成組合意味着要做更細粒度的類的拆分。這也就意味着,我們要定義更多的類和接口。類和接口的增多也就或多或少地增加代碼的複雜程度和維護成本。所以,在實際的項目開發中,我們還是要根據具體的情況,來具體選擇該用繼承還是組合。

如果類之間的繼承結構穩定(不會輕易改變),繼承層次比較淺(比如,最多有兩層繼承關係),繼承關係不複雜,我們就可以大膽地使用繼承。反之,系統越不穩定,繼承層次很深,繼承關係複雜,我們就盡量使用組合來替代繼承。

除此之外,還有一些設計模式會固定使用繼承或者組合。比如,裝飾者模式(decorator pattern)、策略模式(strategy pattern)、組合模式(composite pattern)等都使用了組合關係,而模板模式(template pattern)使用了繼承關係。

前面我們講到繼承可以實現代碼復用。利用繼承特性,我們把相同的屬性和方法,抽取出來,定義到父類中。子類復用父類中的屬性和方法,達到代碼復用的目的。但是,有的時候,從業務含義上,A 類和 B 類並不一定具有繼承關係。比如,Crawler 類和 PageAnalyzer 類,它們都用到了 URL 拼接和分割的功能,但並不具有繼承關係(既不是父子關係,也不是兄弟關係)。僅僅為了代碼復用,生硬地抽象出一個父類出來,會影響到代碼的可讀性。如果不熟悉背後設計思路的同事,發現 Crawler 類和 PageAnalyzer 類繼承同一個父類,而父類中定義的卻只是 URL 相關的操作,會覺得這個代碼寫得莫名其妙,理解不了。這個時候,使用組合就更加合理、更加靈活。具體的代碼實現如下所示:

public class Url {
  //...省略屬性和方法
}

public class Crawler {
  private Url url; // 組合
  public Crawler() {
    this.url = new Url();
  }
  //...
}

public class PageAnalyzer {
  private Url url; // 組合
  public PageAnalyzer() {
    this.url = new Url();
  }
  //..
}

還有一些特殊的場景要求我們必須使用繼承。如果你不能改變一個函數的入參類型,而入參又非接口,為了支持多態,只能採用繼承來實現。比如下面這樣一段代碼,其中 FeignClient 是一個外部類,我們沒有權限去修改這部分代碼,但是我們希望能重寫這個類在運行時執行的 encode() 函數。這個時候,我們只能採用繼承來實現了。

public class FeignClient { // feighn client框架代碼
  //...省略其他代碼...
  public void encode(String url) { //... }
}

public void demofunction(FeignClient feignClient) {
  //...
  feignClient.encode(url);
  //...
}

public class CustomizedFeignClient extends FeignClient {
  @Override
  public void encode(String url) { //...重寫encode的實現...}
}

// 調用
FeignClient client = new CustomizedFeignClient();
demofunction(client);

儘管有些人說,要杜絕繼承,100% 用組合代替繼承,但是我的觀點沒那麼極端!之所以“多用組合少用繼承”這個口號喊得這麼響,只是因為,長期以來,我們過度使用繼承。還是那句話,組合併不完美,繼承也不是一無是處。只要我們控制好它們的副作用、發揮它們各自的優勢,在不同的場合下,恰當地選擇使用繼承還是組合,這才是我們所追求的境界。

重點回顧

為什麼不推薦使用繼承?

繼承是面向對象的四大特性之一,用來表示類之間的 is-a 關係,可以解決代碼復用的問題。雖然繼承有諸多作用,但繼承層次過深、過複雜,也會影響到代碼的可維護性。在這種情況下,我們應該盡量少用,甚至不用繼承。

組合相比繼承有哪些優勢?

繼承主要有三個作用:表示 is-a 關係,支持多態特性,代碼復用。而這三個作用都可以通過組合、接口、委託三個技術手段來達成。除此之外,利用組合還能解決層次過深、過複雜的繼承關係影響代碼可維護性的問題。

如何判斷該用組合還是繼承?

儘管我們鼓勵多用組合少用繼承,但組合也並不是完美的,繼承也並非一無是處。在實際的項目開發中,我們還是要根據具體的情況,來選擇該用繼承還是組合。如果類之間的繼承結構穩定,層次比較淺,關係不複雜,我們就可以大膽地使用繼承。反之,我們就盡量使用組合來替代繼承。除此之外,還有一些設計模式、特殊的應用場景,會固定使用繼承或者組合。

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

【其他文章推薦】

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

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

※回頭車貨運收費標準

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

※超省錢租車方案

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

.net core3.1 abp動態菜單和動態權限(動態菜單實現和動態權限添加) (三)

我們來創建動態菜單吧 

首先,先對動態菜單的概念、操作、流程進行約束:
1.Host和各個Tenant有自己的自定義菜單
2.Host和各個Tenant的權限與自定義菜單相關聯
2.Tenant有一套默認的菜單,規定對應的TenantId=-1,在添加租戶時自動將標準菜單和標準菜單的權限初始化到添加的租戶

一、先實現菜單在數據庫中的增刪改查

第一步:創建表、實體,添加DbContext

我們需要創建一個菜單表,延續Abp的命名方法,表名叫AbpMenus吧(菜單和權限、驗證我們要關聯,所以文件盡量放在Authorization文件夾下)

把創建的實體放在AbpLearn.Core/Authorization下面,新建一個Menus文件夾,再創建Menus實體

    public class AbpMenus : Entity<int>
    {
        public string MenuName { set; get; }
        public string PageName { set; get; }
        public string Name { set; get; }
        public string Url { set; get; }
        public string Icon { set; get; }
        public int ParentId { set; get; }
        public bool IsActive { set; get; }
        public int Orders { set; get; }
        public int? TenantId { set; get; }
    }
如果翻過源碼中實體的定義,可以發現很多實體的繼承,例如:

1.繼承接口 IMayHaveTenant,繼承後生成的sql語句將自動增加TenantId的查詢條件,表中必須包含TenantId列
2.繼承接口 IPassivable,繼承后表中必須包含IsActive列
3.繼承接口 FullAuditedEntity<TPrimaryKey> TPrimaryKey可以是long、int等值類型,必須包含IsDeleted、DeleterUserId、DeletionTime,其中這個接口
還繼承了AuditedEntity<TPrimaryKey>, IFullAudited, IAudited, ICreationAudited, IHasCreationTime, IModificationAudited, IHasModificationTime, IDeletionAudited, IHasDeletionTime, ISoftDelete,這些父類型、接口的定義自己F12就可以看到

 

AbpLearn.EntityFrameworkCore/EntityFrameworkCore/AbpLearnDbContext.cs增加DbSet

public class AbpLearnDbContext : AbpZeroDbContext<Tenant, Role, User, AbpLearnDbContext>
    {
        /* Define a DbSet for each entity of the application */
        
        public AbpLearnDbContext(DbContextOptions<AbpLearnDbContext> options)
            : base(options)
        {
            
        }

        public DbSet<AbpMenus> AbpMenus { set; get; }

    }

再去數據庫中添加AbpMenus表 字段長度請自行調整

DROP TABLE IF EXISTS `AbpMenus`;
CREATE TABLE `AbpMenus` (
`Id` int NOT NULL AUTO_INCREMENT,
`MenuName` varchar(50) DEFAULT NULL,
`PageName` varchar(50) DEFAULT NULL,
`LName` varchar(50) DEFAULT NULL,
`Url` varchar(50) DEFAULT NULL,
`Icon` varchar(20) DEFAULT NULL,
`ParentId` int DEFAULT NULL,
`IsActive` bit(1) NOT NULL DEFAULT b’0′,
`Orders` int DEFAULT NULL,
`TenantId` int DEFAULT NULL,
PRIMARY KEY (`Id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

第二步:添加Service和Dto

AbpLearn.Application/Authorization下添加Menus文件夾,然後添加IMenusAppService、MenusAppService,然後添加Dto文件夾

第三步:添加控制器和前台頁面、js

Controller文件,MenusController.cs

 

 前台添加Menus及對應的js文件,可以簡單省事的把其他文件夾複製粘貼一份,然後關鍵詞修改下

這些文件太多了,我會把這套代碼上傳到github中,文章最低部會把鏈接掛出來

添加完之後我們就可以生成預覽一下Menus,因為SetNavigation中未將Menus的url加進去,我們自己手打鏈接進入

 

 

此時, 我們的菜單這一塊的crud已經做好了,我們可以看到有一個Host管理員這個部分是什麼意思哪?

我們為了在當前Host中可以控制所有租戶的菜單和權限,將當前Host、標準菜單、租戶做一個select,代碼如下

    public class ChangeModalViewModel
    {
        public int? TenantId { get; set; }

        public string TenancyName { get; set; }

        public int? TenantMenuType { get; set; }


        public List<ComboboxItemDto> TeneacyItems { get; set; }
    }
        public async Task<IActionResult> IndexAsync(int? id = 0)
        {
            var loginTenant = id <= 0 ? null : _tenantManager.GetById((int)id);

            var viewModel = new ChangeModalViewModel
            {
                TenancyName = loginTenant?.TenancyName,
                TenantId = id
            };

            viewModel.TeneacyItems = _tenantManager.Tenants
                .Select(p => new ComboboxItemDto(p.Id.ToString(), p.Name) { IsSelected = viewModel.TenancyName == p.TenancyName })
                .ToList();

            viewModel.TeneacyItems.Add(new ComboboxItemDto("0","Host管理員") { IsSelected = id == 0 });

            viewModel.TeneacyItems.Add(new ComboboxItemDto("-1", "默認菜單") { IsSelected = id == -1 });

            ViewBag.LoginInfo = await _sessionAppService.GetCurrentLoginInformations();

            return View(viewModel);
        }

然後在Index.cshtml中添加或修改

@model ChangeModalViewModel  // 添加


  @await Html.PartialAsync(“~/Views/Menus/Index.AdvancedSearch.cshtml”, Model)  //修改

  

  @await Html.PartialAsync(“~/Views/Menus/_CreateModal.cshtml”,Model.TenantId)  //修改

  

  //添加

  $(“#ChangeTenancyName”).change(function (e) {
     location.href = “/Menus/Index/” + this.options[this.selectedIndex].value;
  });

修改_CreateModal.cshtml

@using Abp.Authorization.Users
@using Abp.MultiTenancy
@using AbpLearn.MultiTenancy
@using AbpLearn.Web.Models.Common.Modals
@model int
@{
    Layout = null;
}
<div class="modal fade" id="MenuCreateModal" tabindex="-1" role="dialog" aria-labelledby="MenuCreateModalLabel" data-backdrop="static">
    <div class="modal-dialog modal-lg" role="document">
        <div class="modal-content">
            @await Html.PartialAsync("~/Views/Shared/Modals/_ModalHeader.cshtml", new ModalHeaderViewModel(L("CreateNewMenu")))
            <form name="systemMenuCreateForm" role="form" class="form-horizontal">
                <div class="modal-body">
                    <div class="form-group row required">
                        <label class="col-md-3 col-form-label">@L("MenuName")</label>
                        <div class="col-md-9">
                            <input type="text" name="MenuName" class="form-control" required minlength="2">
                        </div>
                    </div>
                    <div class="form-group row required">
                        <label class="col-md-3 col-form-label">@L("LName")</label>
                        <div class="col-md-9">
                            <input type="text" name="LName" class="form-control" required>
                        </div>
                    </div>
                    <div class="form-group row required">
                        <label class="col-md-3 col-form-label">@L("Url")</label>
                        <div class="col-md-9">
                            <input type="text" name="Url" class="form-control">
                        </div>
                    </div>
                    <div class="form-group row">
                        <label class="col-md-3 col-form-label">@L("PageName")</label>
                        <div class="col-md-9">
                            <input type="text" name="PageName" class="form-control">
                        </div>
                    </div>
                    <div class="form-group row">
                        <label class="col-md-3 col-form-label">@L("ParentId")</label>
                        <div class="col-md-9">
                            <input type="text" name="ParentId" class="form-control">
                        </div>
                    </div>
                    <div class="form-group row">
                        <label class="col-md-3 col-form-label">@L("Orders")</label>
                        <div class="col-md-9">
                            <input type="text" name="Orders" class="form-control">
                        </div>
                    </div>
                    <div class="form-group row">
                        <label class="col-md-3 col-form-label" for="CreateMenuIsActive">@L("IsActive")</label>
                        <div class="col-md-9">
                            <input id="CreateMenuIsActive" type="checkbox" name="IsActive" value="true" checked />
                        </div>
                    </div>
                </div>
                <input type="hidden" name="TenantId" value="@(Model)" />
                @await Html.PartialAsync("~/Views/Shared/Modals/_ModalFooterWithSaveAndCancel.cshtml")
            </form>
        </div>
    </div>
</div>

View Code

 

修改_EditModal.cshtml

@using AbpLearn.Authorization.Menus.Dto
@using AbpLearn.Web.Models.Common.Modals
@model MenuDto
@{
    Layout = null;
}
@await Html.PartialAsync("~/Views/Shared/Modals/_ModalHeader.cshtml", new ModalHeaderViewModel(L("EditMenu")))
<form name="MenuEditForm" role="form" class="form-horizontal">
    <input type="hidden" name="Id" value="@Model.Id" />
    <div class="modal-body">
        <div class="form-group row required">
            <label class="col-md-3 col-form-label" for="tenancy-name">@L("MenuName")</label>
            <div class="col-md-9">
                <input id="tenancy-name" type="text" class="form-control" name="MenuName" value="@Model.MenuName" required maxlength="64" minlength="2">
            </div>
        </div>
        <div class="form-group row required">
            <label class="col-md-3 col-form-label" for="name">@L("LName")</label>
            <div class="col-md-9">
                <input id="name" type="text" class="form-control" name="LName" value="@Model.LName" required maxlength="128">
            </div>
        </div>
        <div class="form-group row required">
            <label class="col-md-3 col-form-label" for="name">@L("Url")</label>
            <div class="col-md-9">
                <input id="name" type="text" class="form-control" name="Url" value="@Model.Url" required maxlength="128">
            </div>
        </div>

        <div class="form-group row required">
            <label class="col-md-3 col-form-label" for="name">@L("PageName")</label>
            <div class="col-md-9">
                <input id="name" type="text" class="form-control" name="PageName" value="@Model.PageName" required maxlength="128">
            </div>
        </div>
        <div class="form-group row required">
            <label class="col-md-3 col-form-label" for="name">@L("ParentId")</label>
            <div class="col-md-9">
                <input id="name" type="text" class="form-control" name="ParentId" value="@Model.ParentId" required maxlength="128">
            </div>
        </div>
        <div class="form-group row required">
            <label class="col-md-3 col-form-label" for="name">@L("Orders")</label>
            <div class="col-md-9">
                <input id="name" type="text" class="form-control" name="Orders" value="@Model.Orders" required maxlength="128">
            </div>
        </div>
        <div class="form-group row">
            <label class="col-md-3 col-form-label" for="isactive">@L("IsActive")</label>
            <div class="col-md-9">
                <input id="isactive" type="checkbox" name="IsActive" value="true" @(Model.IsActive ? "checked" : "") />
            </div>
        </div>
    </div>
    @await Html.PartialAsync("~/Views/Shared/Modals/_ModalFooterWithSaveAndCancel.cshtml")
</form>

<script src="~/view-resources/Views/Menus/_EditModal.js" asp-append-version="true"></script>

View Code

修改Index.AdvancedSearch.cshtml

@using AbpLearn.Web.Views.Shared.Components.TenantChange
@using Abp.Application.Services.Dto
@model ChangeModalViewModel

    <div class="abp-advanced-search">
        <form id="MenusSearchForm" class="form-horizontal">
            <input type="hidden" name="TenantId" value="@Model.TenantId" />
            </form>
            <div class="form-horizontal">
                <div class="form-group">
                    @Html.DropDownList(
                       "ChangeTenancyNames",
                       Model.TeneacyItems.Select(i => i.ToSelectListItem()),
                       new { @class = "form-control edited", id = "ChangeTenancyName" })
                </div>
            </div>
    </div>

因為在abp裏面加載當前列表調用的是abp.services.app.menus.getAll方法,我們還需要對MenusAppService中的GetAllAsync做一下修改

    [Serializable]
    public class MenusPagedResultRequestDto: PagedResultRequestDto, IPagedAndSortedResultRequest
    {
        public virtual int? TenantId { get; set; }

        public virtual string Sorting { get; set; }

        public virtual bool ShowAll { get; set; }

    }
        #region 查詢全部菜單
        /// <summary>
        /// 查詢全部菜單
        /// </summary>
        /// <param name="input"></param>
        /// <returns></returns>
        public override async Task<PagedResultDto<MenuDto>> GetAllAsync(MenusPagedResultRequestDto input)
        {
            IQueryable<AbpMenus> query;

            query = CreateFilteredQuery(input).Where(o => o.TenantId == (input.TenantId == 0 ? null : input.TenantId));

            var totalCount = await AsyncQueryableExecuter.CountAsync(query);

            query = ApplySorting(query, input);
            if (!input.ShowAll) query = ApplyPaging(query, input);

            var entities = await AsyncQueryableExecuter.ToListAsync(query);

            return new PagedResultDto<MenuDto>(
                totalCount,
                entities.Select(MapToEntityDto).ToList()
            );
        }

        #endregion

這樣,我們在選中下面中的任意一個Tenant時,將會跳到對應的菜單裏面了

 

 

 

 我們先把Host管理員菜單和默認菜單配置一下

 

 

 

 

 

 

 

 

 

 

二、實現添加租戶時,初始化標準菜單和權限

首先我們找到添加租戶的地方,去TenantAppService裏面去找,可以看到有CreateAsync的重寫

        public override async Task<TenantDto> CreateAsync(CreateTenantDto input)
        {
            CheckCreatePermission();

            // Create tenant
            var tenant = ObjectMapper.Map<Tenant>(input);
            tenant.ConnectionString = input.ConnectionString.IsNullOrEmpty()
                ? null
                : SimpleStringCipher.Instance.Encrypt(input.ConnectionString);

            var defaultEdition = await _editionManager.FindByNameAsync(EditionManager.DefaultEditionName);
            if (defaultEdition != null)
            {
                tenant.EditionId = defaultEdition.Id;
            }

            await _tenantManager.CreateAsync(tenant);
            await CurrentUnitOfWork.SaveChangesAsync(); // To get new tenant's id.

            // Create tenant database
            _abpZeroDbMigrator.CreateOrMigrateForTenant(tenant);

            // We are working entities of new tenant, so changing tenant filter
            using (CurrentUnitOfWork.SetTenantId(tenant.Id))
            {
                // Create static roles for new tenant
                CheckErrors(await _roleManager.CreateStaticRoles(tenant.Id));

                await CurrentUnitOfWork.SaveChangesAsync(); // To get static role ids

                // Grant all permissions to admin role
                var adminRole = _roleManager.Roles.Single(r => r.Name == StaticRoleNames.Tenants.Admin);
                await _roleManager.GrantAllPermissionsAsync(adminRole);

                // Create admin user for the tenant
                var adminUser = User.CreateTenantAdminUser(tenant.Id, input.AdminEmailAddress);
                await _userManager.InitializeOptionsAsync(tenant.Id);
                CheckErrors(await _userManager.CreateAsync(adminUser, User.DefaultPassword));
                await CurrentUnitOfWork.SaveChangesAsync(); // To get admin user's id

                // Assign admin user to role!
                CheckErrors(await _userManager.AddToRoleAsync(adminUser, adminRole.Name));
                await CurrentUnitOfWork.SaveChangesAsync();
            }

            return MapToEntityDto(tenant);
        }

我們需要做的是,在 using (CurrentUnitOfWork.SetTenantId(tenant.Id)) 的內部尾部添加賦予菜單和權限的方法即可

賦予菜單和權限的方法我們分開寫,都放在MenusAppService中,

    public interface IMenusAppService : IAsyncCrudAppService<MenuDto, int, MenusPagedResultRequestDto, CreateMenuDto, MenuDto>
    {
        /// <summary>
        /// 賦予默認菜單
        /// </summary>
        /// <param name="input"></param>
        /// <returns></returns>
        Task GiveMenusAsync(EntityDto<int> input);

        /// <summary>
        /// 賦予當前租戶Admin角色菜單權限
        /// </summary>
        /// <param name="input"></param>
        /// <returns></returns>
        Task GivePermissionsAsync(EntityDto<int> input);
    }
        #region 賦予默認菜單
        public async Task GiveMenusAsync(EntityDto<int> input)
        {
            if (input.Id > 0)
            {
                var tenant = await _tenantManager.GetByIdAsync(input.Id);

                using (_unitOfWorkManager.Current.SetTenantId(tenant.Id))
                {
                    var query = CreateFilteredQuery(new MenusPagedResultRequestDto()).Where(o => o.TenantId == tenant.Id);

                    var systemMenus = await AsyncQueryableExecuter.ToListAsync(query);

                    if (!systemMenus.Any())
                    {
                        query = CreateFilteredQuery(new MenusPagedResultRequestDto()).Where(o => o.TenantId == -1);

                        var defaultMenus = await AsyncQueryableExecuter.ToListAsync(query);
                        if (defaultMenus.Any())
                        {
                            List<MenusInsert> GetMenusInserts(List<AbpMenus> abpMenus,int parentId = 0)
                            {
                                List<MenusInsert> menusInserts = new List<MenusInsert>();
                                foreach (var entity in abpMenus.Where(o => o.ParentId == parentId))
                                {
                                    var insert = new MenusInsert()
                                    {
                                        LName = entity.LName,
                                        MenuName = entity.MenuName,
                                        PageName = entity.PageName,
                                        Icon = entity.Icon,
                                        Url = entity.Url,
                                        IsActive = entity.IsActive,
                                        Orders = entity.Orders,
                                        ParentId = entity.ParentId,
                                        TenantId = tenant.Id
                                    };
                                    insert.menusInserts = GetMenusInserts(abpMenus, entity.Id);
                                    menusInserts.Add(insert);
                                }
                                return menusInserts;
                            }

                            async Task InsertMenusAsync(List<MenusInsert> inserts,int parentId = 0)
                            {
                                foreach (var insert in inserts)
                                {
                                    var entity = await CreateAsync(new AbpMenus()
                                    {
                                        LName = insert.LName,
                                        MenuName = insert.MenuName,
                                        PageName = insert.PageName,
                                        Icon = insert.Icon,
                                        Url = insert.Url,
                                        IsActive = insert.IsActive,
                                        Orders = insert.Orders,
                                        ParentId = parentId,
                                        TenantId = tenant.Id
                                    });
                                    if (insert.menusInserts.Any())
                                    {
                                        await InsertMenusAsync(insert.menusInserts, entity.Id);
                                    }
                                }
                            }
                            await InsertMenusAsync(GetMenusInserts(defaultMenus));
                            
                        }
                    }
                }
            }

        }
        #endregion


        #region 賦予當前租戶Admin角色菜單權限
        /// <summary>
        /// 賦予當前租戶Admin角色菜單權限
        /// </summary>
        /// <param name="input"></param>
        /// <returns></returns>
        public async Task GivePermissionsAsync(EntityDto<int> input)
        {
            if (input.Id > 0)
            {
                var tenant = await _tenantManager.GetByIdAsync(input.Id);

                using (_unitOfWorkManager.Current.SetTenantId(tenant.Id))
                {
                    var adminRoles = await _roleRepository.GetAllListAsync(o => o.Name == StaticRoleNames.Tenants.Admin && o.TenantId == tenant.Id);
                    if (adminRoles.FirstOrDefault() != null)
                    {
                        var adminRole = adminRoles.FirstOrDefault();

                        var query = CreateFilteredQuery(new MenusPagedResultRequestDto()).Where(o => o.TenantId == tenant.Id);

                        var systemMenus = await AsyncQueryableExecuter.ToListAsync(query);

                        var permissions = ConvertTenantPermissions(systemMenus);

                        //await _roleManager.ResetAllPermissionsAsync(adminRole.FirstOrDefault()); //重置授權

                        var active_BatchCount = 10;
                        var active_permissions = ConvertTenantPermissions(systemMenus.Where(o => o.IsActive).ToList());
                        for (int i = 0; i < active_permissions.Count(); i += 10)//每次后移5位
                        {
                            //await _roleManager.SetGrantedPermissionsAsync(adminRole.FirstOrDefault().Id, active_permissions.Take(active_BatchCount).Skip(i));
                            foreach (var notActive_permission in active_permissions.Take(active_BatchCount).Skip(i))
                            {
                                await _roleManager.GrantPermissionAsync(adminRole, notActive_permission);
                            }
                            active_BatchCount += 10;//每次從數組中選出N+10位,skip前N位
                        }

                        var notActive_BatchCount = 10;
                        var notActive_permissions = ConvertTenantPermissions(systemMenus.Where(o => !o.IsActive).ToList());
                        for (int i = 0; i < notActive_permissions.Count(); i += 10)//每次后移5位
                        {
                            foreach (var notActive_permission in notActive_permissions.Take(notActive_BatchCount).Skip(i))
                            {
                                await _roleManager.ProhibitPermissionAsync(adminRole, notActive_permission);
                            }
                            notActive_BatchCount += 10;//每次從數組中選出N+10位,skip前N位
                        }
                    }
                    else
                    {
                        throw new AbpDbConcurrencyException("未獲取到當前租戶的Admin角色!");
                    }
                }
            }
            else
            {
                var adminRoles = await _roleRepository.GetAllListAsync(o => o.Name == StaticRoleNames.Tenants.Admin && o.TenantId == null);
                if (adminRoles.FirstOrDefault() != null)
                {
                    var adminRole = adminRoles.FirstOrDefault();

                    var query = CreateFilteredQuery(new MenusPagedResultRequestDto()).Where(o => o.TenantId == null || o.TenantId == 0);

                    var systemMenus = await AsyncQueryableExecuter.ToListAsync(query);

                    //await _roleManager.ResetAllPermissionsAsync(adminRole.FirstOrDefault()); //重置授權

                    var active_BatchCount = 10;
                    var active_permissions = ConvertHostPermissions(systemMenus.Where(o => o.IsActive).ToList());
                    for (int i = 0; i < active_permissions.Count(); i += 10)//每次后移5位
                    {
                        //await _roleManager.SetGrantedPermissionsAsync(adminRole.FirstOrDefault().Id, active_permissions.Take(active_BatchCount).Skip(i));
                        foreach (var notActive_permission in active_permissions.Take(active_BatchCount).Skip(i))
                        {
                            await _roleManager.GrantPermissionAsync(adminRole, notActive_permission);
                        }
                        active_BatchCount += 10;//每次從數組中選出N+10位,skip前N位
                    }

                    var notActive_BatchCount = 10;
                    var notActive_permissions = ConvertHostPermissions(systemMenus.Where(o => !o.IsActive).ToList());
                    for (int i = 0; i < notActive_permissions.Count(); i += 10)//每次后移5位
                    {
                        foreach (var notActive_permission in notActive_permissions.Take(notActive_BatchCount).Skip(i))
                        {
                            await _roleManager.ProhibitPermissionAsync(adminRole, notActive_permission);
                        }
                        notActive_BatchCount += 10;//每次從數組中選出N+10位,skip前N位
                    }
                }
            }
        }

        public IEnumerable<Permission> ConvertTenantPermissions(IReadOnlyList<AbpMenus> systemMenus)
        {
            return systemMenus.Select(o => new Permission(o.PageName, L(o.MenuName), L(o.LName), MultiTenancySides.Tenant));
        }

        public IEnumerable<Permission> ConvertHostPermissions(IReadOnlyList<AbpMenus> systemMenus)
        {
            return systemMenus.Select(o => new Permission(o.PageName, L(o.MenuName), L(o.LName), MultiTenancySides.Host));
        }
        #endregion

TenantAppService.cs中做一下修改

        public override async Task<TenantDto> CreateAsync(CreateTenantDto input)
        {
            CheckCreatePermission();

            // Create tenant
            var tenant = ObjectMapper.Map<Tenant>(input);
            tenant.ConnectionString = input.ConnectionString.IsNullOrEmpty()
                ? null
                : SimpleStringCipher.Instance.Encrypt(input.ConnectionString);

            var defaultEdition = await _editionManager.FindByNameAsync(EditionManager.DefaultEditionName);
            if (defaultEdition != null)
            {
                tenant.EditionId = defaultEdition.Id;
            }

            await _tenantManager.CreateAsync(tenant);
            await CurrentUnitOfWork.SaveChangesAsync(); // To get new tenant's id.

            // Create tenant database
            _abpZeroDbMigrator.CreateOrMigrateForTenant(tenant);

            // We are working entities of new tenant, so changing tenant filter
            using (CurrentUnitOfWork.SetTenantId(tenant.Id))
            {
                // Create static roles for new tenant
                CheckErrors(await _roleManager.CreateStaticRoles(tenant.Id));

                await CurrentUnitOfWork.SaveChangesAsync(); // To get static role ids

                // Grant all permissions to admin role
                var adminRole = _roleManager.Roles.Single(r => r.Name == StaticRoleNames.Tenants.Admin);
                await _roleManager.GrantAllPermissionsAsync(adminRole);

                // Create admin user for the tenant
                var adminUser = User.CreateTenantAdminUser(tenant.Id, input.AdminEmailAddress);
                await _userManager.InitializeOptionsAsync(tenant.Id);
                CheckErrors(await _userManager.CreateAsync(adminUser, User.DefaultPassword));
                await CurrentUnitOfWork.SaveChangesAsync(); // To get admin user's id

                // Assign admin user to role!
                CheckErrors(await _userManager.AddToRoleAsync(adminUser, adminRole.Name));
                await CurrentUnitOfWork.SaveChangesAsync();

                await _menusAppService.GiveMenusAsync(new EntityDto<int>() { Id = tenant.Id });
                await CurrentUnitOfWork.SaveChangesAsync();

                await _menusAppService.GivePermissionsAsync(new EntityDto<int>() { Id = tenant.Id });
                await CurrentUnitOfWork.SaveChangesAsync();
            }

            return MapToEntityDto(tenant);
        }

現在我們添加租戶企業1、企業2

 

 

 

 現在菜單已經同步好了,我們去數據庫看下權限的同步

 

TenantId:

null是Host

1是abp頁面第一次加載時初始化的Default租戶

2是我之前添加的舊的企業1,那個時候方法沒寫好,就把2的刪掉了

3是企業2

4是企業1

由此可以看出,我們添加的菜單對應的PageName已經作為權限添加到權限表了

 

三、實現菜單修改后,權限賦予對應租戶

這一個其實在二里面已經寫好了,前台做一個按鈕,賦予權限,調用一下就好了

例如:

Index.cshtml   //為什麼要加getCurrentLoginInformationsOutput.Tenant == null的判斷?是因為租戶在進入菜單管理的地方,我們不給他們添加、賦予權限的權限

 

 在/wwwroot/view-resources/Views/Menus/Index.js中添加

    $(document).on('click', '#GivePermissions', function (e) {
        var tenantId = $(this).attr('data-tenant-id');

        abp.message.confirm(
            abp.utils.formatString(
                "是否賦予當前租戶管理員賬號所有權限?",
                "系統"
            ),
            null,
            (isConfirmed) => {
                if (isConfirmed) {
                    _menuService
                        .givePermissions({
                            id: tenantId
                        })
                        .done(() => {
                            abp.notify.info("操作成功!");
                            _$menusTable.ajax.reload();
                        });
                }
            }
        );
    });

四、實現菜單的動態加載

在https://www.cnblogs.com/wangpengzong/p/13089690.html中我們找到了菜單生成的地方,在最底部,通過NavigationManager來獲取到Menus,這裏其實有一個初始化方法(Initialize),調用的是AbpLearnNavigationProvider的SetNavigation方法來進行本地化,然後在

NavigationManager的非靜態構造函數中去獲取已經本地化的Menus,但是本地化Menus因為是在初始化時,程序的初始化我們無法獲取到當前的Tenant信息,所以只能將獲取Menus的地方推遲,放在倒數第二個類UserNavigationManager裏面的GetMenuAsync方法中,我們來看下GetMenuAsync
        public async Task<UserMenu> GetMenuAsync(string menuName, UserIdentifier user)
        {
            var menuDefinition = _navigationManager.Menus.GetOrDefault(menuName);
            if (menuDefinition == null)
            {
                throw new AbpException("There is no menu with given name: " + menuName);
            }

            var userMenu = new UserMenu(menuDefinition, _localizationContext);
            await FillUserMenuItems(user, menuDefinition.Items, userMenu.Items);
            return userMenu;
        }

第一句話獲取menuDefinition是關鍵點,我們將menuDefinition修改為從數據庫中獲取,在AbpLearn.Application/Authorization/Menus下添加UserNavigationManager.cs

using Abp; using Abp.Application.Features; using Abp.Application.Navigation; using Abp.Authorization; using Abp.Dependency; using Abp.Localization; using Abp.MultiTenancy; using Abp.Runtime.Session; using AbpLearn.Authorization.Menus.Dto; using AbpLearn.Sessions; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace AbpLearn.Authorization.Menus { public class UserNavigationManager : IUserNavigationManager, ITransientDependency { public IAbpSession AbpSession { get; set; } private readonly INavigationManager _navigationManager; private readonly ILocalizationContext _localizationContext; private readonly IIocResolver _iocResolver; private readonly IMenusAppService _menuAppService; private readonly ISessionAppService _sessionAppService; public IDictionary<string, MenuDefinition> Menus { get; private set; } public MenuDefinition MainMenu { get { return Menus["MainMenu"]; } } public UserNavigationManager( INavigationManager navigationManager, ILocalizationContext localizationContext, IMenusAppService menuAppService, ISessionAppService sessionAppService, IIocResolver iocResolver) { _navigationManager = navigationManager; _localizationContext = localizationContext; _iocResolver = iocResolver; AbpSession = NullAbpSession.Instance; _menuAppService = menuAppService; _sessionAppService = sessionAppService; } public async Task<UserMenu> GetMenuAsync(string menuName, UserIdentifier user) { var loginInfo = await _sessionAppService.GetCurrentLoginInformations(); Menus = new Dictionary<string, MenuDefinition> { {"MainMenu", new MenuDefinition("MainMenu", new LocalizableString("MainMenu", AbpConsts.LocalizationSourceName))} }; var lists = await _menuAppService.GetAllAsync(new MenusPagedResultRequestDto() { ShowAll = true, TenantId = (loginInfo.Tenant == null ? 0 : loginInfo.Tenant.Id) }); var ParentMenu = lists.Items.Where(k => k.IsActive).ToList().Where(x => x.ParentId == 0).ToList(); if (ParentMenu.Any()) { ParentMenu.ForEach(g => { var menu = new MenuItemDefinition( g.LName, MenuL(g.MenuName), g.Icon, g.Url, false, g.Orders ); BuildSubMenu(menu, g.Id, lists.Items.Where(k => k.IsActive).ToList()); MainMenu.AddItem(menu); }); } var menuDefinition = MainMenu; if (menuDefinition == null) { throw new AbpException("There is no menu with given name: " + menuName); } var userMenu = new UserMenu(); userMenu.Name = menuDefinition.Name; userMenu.DisplayName = menuDefinition.DisplayName.Localize(_localizationContext); userMenu.CustomData = menuDefinition.CustomData; userMenu.Items = new List<UserMenuItem>(); await FillUserMenuItems(user, menuDefinition.Items, userMenu.Items); return userMenu; } public async Task<IReadOnlyList<UserMenu>> GetMenusAsync(UserIdentifier user) { var userMenus = new List<UserMenu>(); foreach (var menu in _navigationManager.Menus.Values) { userMenus.Add(await GetMenuAsync(menu.Name, user)); } return userMenus; } public void BuildSubMenu(MenuItemDefinition menu, int parentId, List<MenuDto> list) { var nList = list.Where(x => x.ParentId == parentId).ToList(); if (nList != null && nList.Count > 0) { nList.ForEach(g => { var subMenu = new MenuItemDefinition( g.PageName, MenuL(g.MenuName), g.Icon, g.Url, false, g.Orders ); menu.AddItem(subMenu); BuildSubMenu(subMenu, g.Id, list); }); } } private static ILocalizableString MenuL(string name) { return new LocalizableString(name, AbpLearnConsts.LocalizationSourceName); } private async Task<int> FillUserMenuItems(UserIdentifier user, IList<MenuItemDefinition> menuItemDefinitions, IList<UserMenuItem> userMenuItems) { //TODO: Can be optimized by re-using FeatureDependencyContext. var addedMenuItemCount = 0; using (var scope = _iocResolver.CreateScope()) { var permissionDependencyContext = scope.Resolve<PermissionDependencyContext>(); permissionDependencyContext.User = user; var featureDependencyContext = scope.Resolve<FeatureDependencyContext>(); featureDependencyContext.TenantId = user == null ? null : user.TenantId; foreach (var menuItemDefinition in menuItemDefinitions) { if (menuItemDefinition.RequiresAuthentication && user == null) { continue; } if (menuItemDefinition.PermissionDependency != null && (user == null || !(await menuItemDefinition.PermissionDependency.IsSatisfiedAsync(permissionDependencyContext)))) { continue; } if (menuItemDefinition.FeatureDependency != null && (AbpSession.MultiTenancySide == MultiTenancySides.Tenant || (user != null && user.TenantId != null)) && !(await menuItemDefinition.FeatureDependency.IsSatisfiedAsync(featureDependencyContext))) { continue; } var userMenuItem = new UserMenuItem(menuItemDefinition, _localizationContext); if (menuItemDefinition.IsLeaf || (await FillUserMenuItems(user, menuItemDefinition.Items, userMenuItem.Items)) > 0) { userMenuItems.Add(userMenuItem); ++addedMenuItemCount; } } } return addedMenuItemCount; } } }

 

然後在Mvc項目的Startup.cs/ConfigureServices下增加

            services.AddScoped<IUserNavigationManager, UserNavigationManager>();

因為在abp中菜單被做做成了模塊,在程序初始化時模塊添加進去,但是我們將菜單修改成了每次讀取數據庫加載,那麼我們就不需要加載這個模塊了

在mvc項目的AbpLearnWebMvcModule.cs註釋下面這句話

            //Configuration.Navigation.Providers.Add<AbpLearnNavigationProvider>();

將AbpLearnNavigationProvider.cs/SetNavigation方法的內容全部註釋掉

預覽一下mvc,用Host登錄一下

 

 用企業1登陸下,登錄切換Host和Tenant,是在登錄界面 Current tenant: 未選 (Change) 點擊Change,在彈框中輸入 E1(因為上面設置的企業1標識是E1),點擊save,頁面刷新后就變為了 Current tenant: E1 (Change) ,輸入賬號密碼登錄

 

 

 

 OK,我們的動態菜單已經完成了

 

添加jstree

 當然,我的菜單使用的是table來显示,你也可以使用tree來,我找到了一個jstree,下面修改一下

MenusAppService.cs

        #region 獲取當前賬戶的菜單樹
        /// <summary>
        /// 獲取當前賬戶的菜單樹
        /// </summary>
        /// <param name="input"></param>
        /// <returns></returns>
        public async Task<string> GetTreeAsync(MenusPagedResultRequestDto input)
        {
            var query = CreateFilteredQuery(new MenusPagedResultRequestDto()).Where(o => o.TenantId == input.TenantId);

            var systemMenus = await AsyncQueryableExecuter.ToListAsync(query);

            var childJObject = new JObject();
            var openJObject = new JObject();
            openJObject.Add("opened", true);
            childJObject.Add("id", 0);
            childJObject.Add("text", "根目錄");
            childJObject.Add("icon", "");
            childJObject.Add("state", openJObject);
            childJObject.Add("children", GetJArray(systemMenus, 0));
            return childJObject.ToString();
        }

        #region 獲取目錄Array
        /// <summary>
        /// 獲取目錄Array
        /// </summary>
        /// <param name="systemMenus"></param>
        /// <param name="parentdId"></param>
        /// <returns></returns>
        private JArray GetJArray(List<AbpMenus> systemMenus, int parentdId)
        {
            JArray jArray = new JArray();
            foreach (var menu in systemMenus.Where(o => o.ParentId == parentdId))
            {
                var jObject = new JObject();
                jObject.Add("id", menu.Id);
                jObject.Add("text", menu.MenuName);
                jObject.Add("icon", menu.Icon);
                //jObject.Add("state", menu.Icon);
                if (systemMenus.Any(o => o.ParentId == menu.Id))
                {
                    jObject.Add("children", GetJArray(systemMenus, menu.Id));
                }
                jArray.Add(jObject);
            }
            return jArray;
        }

        #endregion

        #endregion

 

 前端Index.cshtml  jstree去https://github.com/vakata/jstree/zipball/3.3.8下載,下載后在mvc項目的wwwroot文件夾下添加jstree文件夾,下載文件的src裏面內容全部賦值到jstree文件夾

註釋掉table標籤

添加jstree1

例如:

@section styles
{
    <link href="~/jstree/themes/default/style.css" rel="stylesheet" />
}                     

<div id="jstree1" style="width:100%;"></div> @section scripts { <environment names="Development"> <script src="~/view-resources/Views/Menus/Index.js" asp-append-version="true"></script> </environment> <environment names="Staging,Production"> <script src="~/view-resources/Views/Menus/Index.min.js" asp-append-version="true"></script> </environment> <script type="application/javascript" src="~/jstree/jstree.js"></script> <script type="application/javascript" src="~/jstree/jstree.contextmenu.js"></script> <script type="text/javascript"> $(function () { var _menuService = abp.services.app.menus; l = abp.localization.getSource('A_b_p'); $('#jstree1').jstree({ "core": { "data": function (node, callback) { var filter = $('#MenusSearchForm').serializeFormToObject(true); this, _menuService.getTree(filter).done(function (result) { callback.call(this, JSON.parse(result)); }); }, "themes": { "variant": "large",//加大 "ellipsis": true //文字多時省略 }, "check_callback": true, }, "plugins": ["contextmenu", "wholerow", "themes"],//"checkbox" "contextmenu": { select_node: false, show_at_node: true, "items": { "create": { "label": "新增子菜單", "action": function (obj) { var inst = jQuery.jstree.reference(obj.reference); var clickedNode = inst.get_node(obj.reference); if (parseInt(clickedNode.original.id) >= 0) { $("#ParentId").val(clickedNode.original.id); $("#MenuCreateModal").modal(); } else { abp.notify.info("父節點獲取出錯"); } }, }, "rename": { "label": "修改", "action": function (obj) { var inst = jQuery.jstree.reference(obj.reference); var clickedNode = inst.get_node(obj.reference); if (parseInt(clickedNode.original.id) >= 0) { abp.ajax({ url: abp.appPath + 'Menus/EditModal?menuId=' + clickedNode.original.id, type: 'POST', dataType: 'html', success: function (content) { $("#MenuEditModal").modal(); $('#MenuEditModal div.modal-content').html(content); }, error: function (e) { } }); } else { abp.notify.info("菜單獲取出錯"); } } }, "delete": { "label": "更改菜單狀態", "action": function (obj) { var inst = jQuery.jstree.reference(obj.reference); var clickedNode = inst.get_node(obj.reference); abp.message.confirm( abp.utils.formatString("是否" + (clickedNode.original.state.disabled?"啟用":"禁用") + "當前菜單:" + clickedNode.original.text + "?"), null, (isConfirmed) => { if (isConfirmed) { _menuService .delete({ id: clickedNode.original.id }) .done(() => { abp.notify.info(l('SuccessfullyDeleted')); location.reload(); }); } } ); }, } } } }).on('select_node.jstree', function (event, data) { console.log(data.node); }).on('changed.jstree', function (event, data) { console.log("-----------changed.jstree"); console.log("action:" + data.action); console.log(data.node); }); }); </script> }

 

 預覽一下吧

 

 

github地址

本文github:https://github.com/wangpengzong/AbpLearn

下一篇開始動態權限

 吐槽區域(寫的不好、不對,歡迎吐槽)

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

【其他文章推薦】

※超省錢租車方案

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

※回頭車貨運收費標準

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

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

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

虎鯨生氣了!多艘船隻罕見遭圍攻損壞 科學家憂環境破壞

摘錄自2020年9月14日自由時報報導

過去兩個月來虎鯨開始攻擊船隻,造成嚴重損害,科學家認為,這可能與虎鯨的生存壓力有關。

《衛報》報導,據傳虎鯨騷擾航行西班牙與葡萄牙之間的船隻,讓科學家感到困惑,過去兩個月,多艘船隻發出求救訊息,至少有一艘船因為嚴重損壞回港。船員莫里斯(Victoria Morris)表示,他所在的船隻被9頭虎鯨包圍,這些重達6噸的虎鯨不斷撞擊船隻達一小時,導致船隻轉了180度,發動機也關閉,同時虎鯨們還發出巨大的「口哨聲」。

報導表示,科學家指出虎鯨是高度群聚且好奇的動物,跟隨小船並嬉戲不是太罕見的事,但這件事奇怪的地方在於,虎鯨表現出侵略性,一般而言牠們不太會蓄意攻擊。科學家認為,這可能代表直布羅陀海峽的虎鯨有巨大的生存壓力,牠們要與漁船爭搶食物,且繁忙的航線對環境造成破壞,當地的虎鯨已經瀕臨滅絕,只剩下大約30頭成體。

海洋
國際新聞
虎鯨

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

【其他文章推薦】

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

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

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

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

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

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