分類: 3C資訊

  • 淑女?劈蕉佬?這些雷人車型名字你了解幾個?

    淑女?劈蕉佬?這些雷人車型名字你了解幾個?

    有誰想到這款車當年竟然是名棄嬰,四處找人收留。Jimny的型號的原本來自一間叫希望汽車的生產廠家,該公司研製了一部希望之星ON360,但鑒於公司規模,老闆小野定良計劃向三菱汽車推銷代工制權,但三菱礙於希望汽車的往績,並沒有接受這單生意,最後只能硬着頭皮自行生產,並於1967年底上市,可惜銷量不如預期。

    每部車都有着自己的型號名,一般來說就是字母加数字的組合,或單純以文字作為型號名,在當中有的更隱藏別具意義的故事,今天就和大家分享幾個有趣的故事。

    淑女的由來

    “惡魔Z”、“大魔王”這些都是我們對Fairlady的尊稱,日產經典跑車系列Fairlady這個名字的靈感來源於百老匯歌舞劇My Fair Lady, 1961年時任社長川又克二正在美國公幹,機緣巧合下觀看了這套長壽歌舞劇,同期美國即將推出新款跑車SLp213,一下子就決定新車冠以Fairlady之名,希望它像該歌劇一樣受歡迎,並成為經典。

    380萬中挑一

    Sunny(陽光)於1966年推出,曾經是日產主力家庭用車,當年日產在報紙廣告上預告推出一升的新車,並進行募集車名活動,在緊接一輪廣告攻勢逐步發布新車細節后,接連一個月宣傳吸引了848萬人參與,收到共380萬個名字推薦,最後選定了切合車型概念的Sunny為名。

    南美洲“劈蕉佬”

    不少越野發燒友及工程人員鍾情的三菱pajero(帕傑羅),因其英文發音和粵語里的“劈蕉佬”有着99%的相似度,瞬間顯得親民又剛強。至於pajero名稱的起源,則是取自於生活在阿根廷南部高原地帶的一種山貓,它活躍於崎嶇山間,正好吻合pajero的強勁野外走破性能,更讓這隻猛獸衝出南美洲,穿梭於全球各地山野。

    人棄我取,終成經典

    硬派越野車一直給人一種高大上的感覺,但也有例外,鈴木Jimny(吉姆尼)就是這樣一款小巧見稱的越野車,即使到了今天仍然是不少越野愛好者的寵兒。有誰想到這款車當年竟然是名棄嬰,四處找人收留。Jimny的型號的原本來自一間叫希望汽車的生產廠家,該公司研製了一部希望之星ON360,但鑒於公司規模,老闆小野定良計劃向三菱汽車推銷代工制權,但三菱礙於希望汽車的往績,並沒有接受這單生意,最後只能硬着頭皮自行生產,並於1967年底上市,可惜銷量不如預期。於是小野定良改去向鈴木汽車負責人鈴木修推銷此車,鈴木修對此車一見鍾情,即使公司上下都反對仍堅持一己之見,最終以1,200萬日元向希望汽車買斷整個設計,並於1970年4月推出初代Jimny,造就這部鈴木的招牌車款。

    買回來的名字

    在Integra未面世之前,prelude(廣東及港澳地區稱“披露”)就是本田coupe車款的代表,也是展現本田各種黑科技的試驗田。DOHC VTEC、雙搖臂懸架及四輪轉向系統等配置早早就出現在prelude之上,中文意謂“前奏曲”的prelude名字原為豐田擁有,當本田於70年代末期決定以此為新車型號后才發現早已被豐田註冊了名字。本來最簡單的方法就是改名,但當年本田執意鍾情於prelude這個名字,於是主動向豐田提出轉讓要求,結果一輪商議后prelude終於歸入本田名下。雖然雙方未有提到當中細節,不過相信離不開用金錢來解決,而這個買回來的名字就一直使用到2001年第五代prelude停產為止,歷時23年。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

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

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

    ※回頭車貨運收費標準

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

    ※超省錢租車方案

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

  • 近5米長,舒適度同級無敵,這款18.99萬起的車值不值?

    近5米長,舒適度同級無敵,這款18.99萬起的車值不值?

    雪鐵龍C6上配備的1。8T發動機是pSA集團在中國最高階的發動機。這台發動機的賬面數據和實際表現當然比不上對手最高階的發動機,但是比起他們中階發動機,倒也不至於落後。傳動系統是來自愛信的6AT變速箱,這台變速箱可以說拖了後腿,競爭對手大多數用上了8、9AT或者7速雙離合的產品。

    在2008年,C6曾經以中大型車的身份引進中國。當時的雪鐵龍C6是C級車的身份,售價高達64萬。時過境遷,當C6再次回到國人視線里時,已經是B級車的身份,而且售價也僅需18.99-27.99萬。

    這一代的C6是一款中國特供車型,專為國人設計。中國人喜歡的元素,超大後排空間、肉眼可見的豪華感、豐富高端的配置、運動流暢的車身造型,在這台C6上一樣不缺。然而,C6的銷量非常慘淡。好在,今天我的任務不是解答為什麼C6銷量慘淡,而是要告訴你這台車到底有多棒。

    雪鐵龍C6的外觀以沉穩為主,前臉的雪鐵龍雙人標識延伸到整個車頭,車頭的線條以水平為主,搭配上大面積的鍍鉻格柵,鑽晶型日間行車燈,彰顯了C6行政級轎車的定位。尾部造型飽滿,銀色的鍍鉻裝飾條連接了兩旁的回字形尾燈,盡顯格調。C6側麵線條流暢動感,缺少了一絲行政級轎車應有的厚重感,而且影響了頭部空間的表現。

    雪鐵龍C6的車身尺寸比市面上大多數B級車都要大,甚至可以列入C級車的行列。

    上帝在製造法國人的時候,只給了他們優秀的設計能力,卻沒有把將設計付諸現實的能力一併安裝上去。C6車身漆面左右厚薄不一,而且這款白色車漆容易沾灰且難以清潔。

    雪鐵龍C6的內飾比外觀做得還好。C6的內飾採用了最新家族式對稱式設計,造型設計更加註重檔次感。材質方面啊,C6的內飾採用了大量的Nappa真皮,實木飾板,同級再也找不到更好的。全液晶儀錶盤的显示效果清晰,可調節的選項豐富,UI設計出色。

    C6座椅方面採用了Nappa真皮包裹,座椅寬大,填充物支撐性足。這台C6的頂配車型,前後座椅均有座椅通風/加熱/按摩功能,這種級別的配置也是同級所不具備的。

    C6的乘坐空間只有一個字——大。1米73的乘客在前排調好位置后,在後排能夠獲得3拳2指左右的腿部空間。可惜,受流線型車身的影響,頭部空間不容樂觀,前排和後排分別只有3指和4指的表現。

    C6的後備廂空間非常大,有523L的容積。不過後排座椅不能放倒,所以沒有擴充的空間。

    雪鐵龍C6上配備的1.8T發動機是pSA集團在中國最高階的發動機。這台發動機的賬面數據和實際表現當然比不上對手最高階的發動機,但是比起他們中階發動機,倒也不至於落後。傳動系統是來自愛信的6AT變速箱,這台變速箱可以說拖了後腿,競爭對手大多數用上了8、9AT或者7速雙離合的產品。這款6AT的擋位數就顯得落後了。從匹配上來說,整套動力系統也不夠線性,起步時需要很小心地控制油門才不會突然被“踢”一腳。如果C6能來個ECO模式,相信會好很多。

    雪鐵龍C6的0-100km/h實測百公里加速時間為9.4秒。當天有小雨,地面略微有積水,但是加速的過程245mm寬的輪胎幾乎沒有打滑,很平順地就衝出去了。這數據雖然不能跟競爭對手的高階動力系統對比,但是跟他們的1.5T或1.8T的動力對比,還是有競爭力的。

    既然雪鐵龍C6作為一款行政級定位的轎車,底盤肯定是以舒適為主的。雪鐵龍C6的減振器和避震彈簧採用斜置的布置方式,而且減振器和避震彈簧另一端連接的是副車架,而不是車身。這麼做的好處,就是減少減振器上垂直方向的力和避免力直接作用於車身,最終達到乘坐舒適的效果。實際體驗下來,雪鐵龍C6確實能很好地將路面顛簸過濾掉,比起同級對手單薄的濾震能力要好上不少,甚至可以說,這是C級車上會有的濾震能力。

    雪鐵龍C6的方向盤採用電動液壓助力,路感非常清晰,沒有什麼虛位,回正的力度也線性,只是手感比較重,更加適合男性駕駛。

    C6的隔音水平也是行政級水準的。C6四門車窗都使用了雙層隔音玻璃。C6在怠速時幾乎聽不到發動機的聲音,高速跑起來的時候輪胎噪音會相對較大,而風噪不明顯。

    從市場定位上來說,與C6有相似定位的B級車,只有別克君越這一款。他們都有舒適的底盤表現,寬敞的乘坐空間,高檔的內飾氛圍。兩款拿出來比較,C6的配置更高,後期保養更便宜,而君越的動力會更完善。如果不考慮品牌知名度,C6會更加合適。當然,如果雪鐵龍的品牌知名度能夠打出來,C6肯定不會是現在這個定價。所以,各位觀眾朋友,要撿便宜趁現在了。

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

    【其他文章推薦】

    ※超省錢租車方案

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

    ※回頭車貨運收費標準

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

    FB行銷專家,教你從零開始的技巧

  • mingw32 exception在sjlj與dwarf差別-反彙編分析

    mingw32 exception在sjlj與dwarf差別-反彙編分析

    sjlj (setjump/longjump)與dwarf-2為mingw32兩種異常處理模型的實現。sjlj有着開銷,而隨linux發行的mingw32開發庫包都是用sjlj版編譯的,而Qt卻採用dwarf-2版,那麼兩者之間有多少差異,本文就這問題對兩版的異常代碼的反彙編進行分析比較。

    我使用mingw-w65-i686-810的sjlj與dwarf-2兩個版本對下面異常代碼編譯。

    __attribute__((dllimport)) int dllfunc();
    int main()
    {
        dllfunc();
        //_create_locale(LC_ALL, "C");
        printf("abc");
        //return 0;
    
        try
        {
            try
            {
                throw std::exception();
            }
            catch(std::exception&)
          {
                std::rethrow_exception(std::current_exception());
          }
            
        }
        catch(int)
        {
            
        }
        catch(std::exception& e)
        {
            std::cout << e.what() << std::endl;
        }
        catch(...)
        {
            std::cout << "unknown" << std::endl;
        }
        return 0;
    }

    代碼邏輯:

    兩層 try/catch,

    1. 裡層 try/catch

    1.1 try塊, throw 異常

    1.2 catch塊, rethrow

    2. 外層 try/catch

    2.1 有三catch分支。

     

    開刷前,先定義一下。

    如果將 try/catch 去除 c++語言特性后,基本就是一種由c++庫還有c++編譯器共同管理的 goto。

    throw相當於goto, catch相當於label(一種以類型區分的)。

    那麼c++編譯器與c++庫為我們提供了什麼樣的管理呢?

    c++編譯器

    0. 利用c++支持對象析構進行try塊保護。

    1. 將 throw 關鍵字生成彙編 call __cxa_throw,調用 c++庫的函數。

    2. 為每個catch塊生成代碼片斷,只能通過jmp跳轉進來。

    2.1 開頭 call __cxa_begin_catch。

    2.2 結尾 call __cxa_end_catch。

    2.3 最後跳出到 try/catch塊邏輯代碼的下條執行指令。

    3. 為同一try/catch塊的所有catch塊產生分支控制代碼。

    4. 為try塊的析構代碼產生跳轉入口。

    5. 為每一層try/catch塊生成 uncaught 代碼塊,調用 _Unwind_Resume。

    c++庫:

    1. __cxa_throw,馬上_Unwind_RaiseException。跳轉到當前最裏面一層 try/catch的支路控制代碼片斷。

    2. _Unwind_Resume,向上繼續展開。

    3. std::rethrow_exception,調用 __gcclibcxx_demangle_callback,

    3.1 要麼有 catch可達跳回到原來代碼的控制流,直接離開std::rethrow_exception的調用上下文。

    3.2 要麼從__gcclibcxx_demangle_callback返回,執行terminate結束進程。

     

    sjlj 版的反彙編代碼比 dwarf-2 版的多了50行。

    先來看dwarf-2的反彙編代碼 

      1  <+0>:    lea    0x4(%esp),%ecx
      2  <+4>:    and    $0xfffffff0,%esp
      3  <+7>:    pushl  -0x4(%ecx)
      4  <+10>:    push   %ebp
      5  <+11>:    mov    %esp,%ebp
      6  <+13>:    push   %esi
      7  <+14>:    push   %ebx
      8  <+15>:    push   %ecx
      9  <+16>:    sub    $0x2c,%esp
     10  <+19>:    call   0x401890 <__main>
     11  <+24>:    mov    0x4071a4,%eax
     12  <+29>:    call   *%eax
     13  <+31>:    movl   $0x404045,(%esp)
     14  <+38>:    call   0x4027c4 <printf>
     15  <+43>:    movl   $0x4,(%esp)
     16  <+50>:    call   0x4017ac <__cxa_allocate_exception>
     17  <+55>:    mov    %eax,%ebx
     18  <+57>:    mov    %ebx,%ecx
     19  <+59>:    call   0x402890 <std::exception::exception()>
     20  <+64>:    movl   $0x4017d4,0x8(%esp)
     21  <+72>:    movl   $0x4042a8,0x4(%esp)
     22  <+80>:    mov    %ebx,(%esp)
     23  <+83>:    call   0x401794 <__cxa_throw>
     24  <+88>:    mov    $0x0,%eax
     25  <+93>:    jmp    0x401723 <main()+355>
     26  <+98>:    mov    %edx,%ecx
     27  <+100>:    cmp    $0x2,%ecx
     28  <+103>:    je     0x40162b <main()+107>
     29  <+105>:    jmp    0x401663 <main()+163>
     30  <+107>:    mov    %eax,(%esp)
     31  <+110>:    call   0x4017a4 <__cxa_begin_catch>
     32  <+115>:    mov    %eax,-0x1c(%ebp)
     33  <+118>:    lea    -0x28(%ebp),%eax
     34  <+121>:    mov    %eax,(%esp)
     35  <+124>:    call   0x4017cc <_ZSt17current_exceptionv>
     36  <+129>:    lea    -0x28(%ebp),%eax
     37  <+132>:    mov    %eax,(%esp)
     38  <+135>:    call   0x4017c4 <_ZSt17rethrow_exceptionNSt15__exception_ptr13exception_ptrE>
     39  <+140>:    mov    %eax,%esi
     40  <+142>:    mov    %edx,%ebx
     41  <+144>:    lea    -0x28(%ebp),%eax
     42  <+147>:    mov    %eax,%ecx
     43  <+149>:    call   0x4017ec <_ZNSt15__exception_ptr13exception_ptrD1Ev>
     44  <+154>:    call   0x40179c <__cxa_end_catch>
     45  <+159>:    mov    %esi,%eax
     46  <+161>:    mov    %ebx,%edx
     47  <+163>:    cmp    $0x1,%edx
     48  <+166>:    je     0x40166f <main()+175>
     49  <+168>:    cmp    $0x2,%edx
     50  <+171>:    je     0x401683 <main()+195>
     51  <+173>:    jmp    0x4016ca <main()+266>
     52  <+175>:    mov    %eax,(%esp)
     53  <+178>:    call   0x4017a4 <__cxa_begin_catch>
     54  <+183>:    mov    (%eax),%eax
     55  <+185>:    mov    %eax,-0x24(%ebp)
     56  <+188>:    call   0x40179c <__cxa_end_catch>
     57  <+193>:    jmp    0x401618 <main()+88>
     58  <+195>:    mov    %eax,(%esp)
     59  <+198>:    call   0x4017a4 <__cxa_begin_catch>
     60  <+203>:    mov    %eax,-0x20(%ebp)
     61  <+206>:    mov    -0x20(%ebp),%eax
     62  <+209>:    mov    (%eax),%eax
     63  <+211>:    add    $0x8,%eax
     64  <+214>:    mov    (%eax),%eax
     65  <+216>:    mov    -0x20(%ebp),%edx
     66  <+219>:    mov    %edx,%ecx
     67  <+221>:    call   *%eax
     68  <+223>:    mov    %eax,0x4(%esp)
     69  <+227>:    movl   $0x6ff07a00,(%esp)
     70  <+234>:    call   0x4017b4 <_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc>
     71  <+239>:    movl   $0x4017bc,(%esp)
     72  <+246>:    mov    %eax,%ecx
     73  <+248>:    call   0x4017f4 <_ZNSolsEPFRSoS_E>
     74  <+253>:    sub    $0x4,%esp
     75  <+256>:    call   0x40179c <__cxa_end_catch>
     76  <+261>:    jmp    0x401618 <main()+88>
     77  <+266>:    mov    %eax,(%esp)
     78  <+269>:    call   0x4017a4 <__cxa_begin_catch>
     79  <+274>:    movl   $0x404049,0x4(%esp)
     80  <+282>:    movl   $0x6ff07a00,(%esp)
     81  <+289>:    call   0x4017b4 <_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc>
     82  <+294>:    movl   $0x4017bc,(%esp)
     83  <+301>:    mov    %eax,%ecx
     84  <+303>:    call   0x4017f4 <_ZNSolsEPFRSoS_E>
     85  <+308>:    sub    $0x4,%esp
     86  <+311>:    call   0x40179c <__cxa_end_catch>
     87  <+316>:    jmp    0x401618 <main()+88>
     88  <+321>:    mov    %eax,%ebx
     89  <+323>:    call   0x40179c <__cxa_end_catch>
     90  <+328>:    mov    %ebx,%eax
     91  <+330>:    mov    %eax,(%esp)
     92  <+333>:    call   0x402770 <_Unwind_Resume>
     93  <+338>:    mov    %eax,%ebx
     94  <+340>:    call   0x40179c <__cxa_end_catch>
     95  <+345>:    mov    %ebx,%eax
     96  <+347>:    mov    %eax,(%esp)
     97  <+350>:    call   0x402770 <_Unwind_Resume>
     98  <+355>:    lea    -0xc(%ebp),%esp
     99  <+358>:    pop    %ecx
    100  <+359>:    pop    %ebx
    101  <+360>:    pop    %esi
    102  <+361>:    pop    %ebp
    103  <+362>:    lea    -0x4(%ecx),%esp
    104  <+365>:    ret    

    我們的主要代碼邏輯只有20-30條指令

     

     當 throw時,__cxa_throw函數是不會返回的, 如同goto最後是跳轉到他處,若被本層catch處理完才會跳轉回來<+88>。

    然後看c++編譯器為我們生成的異常代碼 。

     

     

     

     

     

     對於沒有發生異常時,代碼執行路徑基本不會去涉及到異常代碼支路,開銷幾近為0,只是代碼量增大。

    下面來看 sjlj 版的彙編代碼,

      1 function main():
      2  <+0>:    lea    0x4(%esp),%ecx
      3  <+4>:    and    $0xfffffff0,%esp
      4  <+7>:    pushl  -0x4(%ecx)
      5  <+10>:    push   %ebp
      6  <+11>:    mov    %esp,%ebp
      7  <+13>:    push   %edi
      8  <+14>:    push   %esi
      9  <+15>:    push   %ebx
     10  <+16>:    push   %ecx
     11  <+17>:    sub    $0x68,%esp
     12  <+20>:    movl   $0x4017ac,-0x44(%ebp)
     13  <+27>:    movl   $0x402958,-0x40(%ebp)
     14  <+34>:    lea    -0x3c(%ebp),%eax
     15  <+37>:    lea    -0x18(%ebp),%ebx
     16  <+40>:    mov    %ebx,(%eax)
     17  <+42>:    mov    $0x4015b4,%edx
     18  <+47>:    mov    %edx,0x4(%eax)
     19  <+50>:    mov    %esp,0x8(%eax)
     20  <+53>:    lea    -0x5c(%ebp),%eax
     21  <+56>:    mov    %eax,(%esp)
     22  <+59>:    call   0x402790 <_Unwind_SjLj_Register>
     23  <+64>:    call   0x4018b0 <__main>
     24  <+69>:    mov    0x406194,%eax
     25  <+74>:    movl   $0xffffffff,-0x58(%ebp)
     26  <+81>:    call   *%eax
     27  <+83>:    movl   $0x404001,(%esp)
     28  <+90>:    call   0x4027e4 <printf>
     29  <+95>:    movl   $0x4,(%esp)
     30  <+102>:    call   0x4017cc <__cxa_allocate_exception>
     31  <+107>:    mov    %eax,-0x60(%ebp)
     32  <+110>:    mov    %eax,%ecx
     33  <+112>:    call   0x4028b0 <std::exception::exception()>
     34  <+117>:    movl   $0x4017f4,0x8(%esp)
     35  <+125>:    movl   $0x404264,0x4(%esp)
     36  <+133>:    mov    -0x60(%ebp),%eax
     37  <+136>:    mov    %eax,(%esp)
     38  <+139>:    movl   $0x1,-0x58(%ebp)
     39  <+146>:    call   0x4017b4 <__cxa_throw>
     40  <+151>:    mov    $0x0,%eax
     41  <+156>:    mov    %eax,-0x60(%ebp)
     42  <+159>:    jmp    0x401733 <main()+547>
     43  <+164>:    lea    0x18(%ebp),%ebp
     44  <+167>:    mov    -0x54(%ebp),%edx
     45  <+170>:    mov    -0x50(%ebp),%ecx
     46  <+173>:    mov    -0x58(%ebp),%eax
     47  <+176>:    test   %eax,%eax
     48  <+178>:    je     0x4015e6 <main()+214>
     49  <+180>:    sub    $0x1,%eax
     50  <+183>:    test   %eax,%eax
     51  <+185>:    je     0x40161b <main()+267>
     52  <+187>:    sub    $0x1,%eax
     53  <+190>:    test   %eax,%eax
     54  <+192>:    je     0x4016f8 <main()+488>
     55  <+198>:    sub    $0x1,%eax
     56  <+201>:    test   %eax,%eax
     57  <+203>:    je     0x401712 <main()+514>
     58  <+209>:    sub    $0x1,%eax
     59  <+212>:    ud2    
     60  <+214>:    mov    %edx,%eax
     61  <+216>:    mov    %ecx,%edx
     62  <+218>:    mov    %edx,%ecx
     63  <+220>:    cmp    $0x2,%ecx
     64  <+223>:    je     0x4015f3 <main()+227>
     65  <+225>:    jmp    0x401642 <main()+306>
     66  <+227>:    mov    %eax,(%esp)
     67  <+230>:    call   0x4017c4 <__cxa_begin_catch>
     68  <+235>:    mov    %eax,-0x1c(%ebp)
     69  <+238>:    lea    -0x28(%ebp),%eax
     70  <+241>:    mov    %eax,(%esp)
     71  <+244>:    call   0x4017ec <_ZSt17current_exceptionv>
     72  <+249>:    lea    -0x28(%ebp),%eax
     73  <+252>:    mov    %eax,(%esp)
     74  <+255>:    movl   $0x2,-0x58(%ebp)
     75  <+262>:    call   0x4017e4 <_ZSt17rethrow_exceptionNSt15__exception_ptr13exception_ptrE>
     76  <+267>:    mov    %edx,-0x60(%ebp)
     77  <+270>:    mov    %ecx,-0x64(%ebp)
     78  <+273>:    lea    -0x28(%ebp),%eax
     79  <+276>:    mov    %eax,%ecx
     80  <+278>:    call   0x40180c <_ZNSt15__exception_ptr13exception_ptrD1Ev>
     81  <+283>:    mov    -0x60(%ebp),%eax
     82  <+286>:    mov    %eax,-0x60(%ebp)
     83  <+289>:    mov    -0x64(%ebp),%esi
     84  <+292>:    mov    %esi,-0x64(%ebp)
     85  <+295>:    call   0x4017bc <__cxa_end_catch>
     86  <+300>:    mov    -0x60(%ebp),%eax
     87  <+303>:    mov    -0x64(%ebp),%edx
     88  <+306>:    cmp    $0x1,%edx
     89  <+309>:    je     0x40164e <main()+318>
     90  <+311>:    cmp    $0x2,%edx
     91  <+314>:    je     0x401665 <main()+341>
     92  <+316>:    jmp    0x4016b3 <main()+419>
     93  <+318>:    mov    %eax,(%esp)
     94  <+321>:    call   0x4017c4 <__cxa_begin_catch>
     95  <+326>:    mov    (%eax),%eax
     96  <+328>:    mov    %eax,-0x20(%ebp)
     97  <+331>:    call   0x4017bc <__cxa_end_catch>
     98  <+336>:    jmp    0x4015a7 <main()+151>
     99  <+341>:    mov    %eax,(%esp)
    100  <+344>:    call   0x4017c4 <__cxa_begin_catch>
    101  <+349>:    mov    %eax,-0x24(%ebp)
    102  <+352>:    mov    -0x24(%ebp),%eax
    103  <+355>:    mov    (%eax),%eax
    104  <+357>:    add    $0x8,%eax
    105  <+360>:    mov    (%eax),%eax
    106  <+362>:    mov    -0x24(%ebp),%edx
    107  <+365>:    mov    %edx,%ecx
    108  <+367>:    call   *%eax
    109  <+369>:    mov    %eax,0x4(%esp)
    110  <+373>:    movl   $0x6ff29a00,(%esp)
    111  <+380>:    movl   $0x3,-0x58(%ebp)
    112  <+387>:    call   0x4017d4 <_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc>
    113  <+392>:    movl   $0x4017dc,(%esp)
    114  <+399>:    mov    %eax,%ecx
    115  <+401>:    call   0x401814 <_ZNSolsEPFRSoS_E>
    116  <+406>:    sub    $0x4,%esp
    117  <+409>:    call   0x4017bc <__cxa_end_catch>
    118  <+414>:    jmp    0x4015a7 <main()+151>
    119  <+419>:    mov    %eax,(%esp)
    120  <+422>:    call   0x4017c4 <__cxa_begin_catch>
    121  <+427>:    movl   $0x404005,0x4(%esp)
    122  <+435>:    movl   $0x6ff29a00,(%esp)
    123  <+442>:    movl   $0x4,-0x58(%ebp)
    124  <+449>:    call   0x4017d4 <_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc>
    125  <+454>:    movl   $0x4017dc,(%esp)
    126  <+461>:    mov    %eax,%ecx
    127  <+463>:    call   0x401814 <_ZNSolsEPFRSoS_E>
    128  <+468>:    sub    $0x4,%esp
    129  <+471>:    movl   $0xffffffff,-0x58(%ebp)
    130  <+478>:    call   0x4017bc <__cxa_end_catch>
    131  <+483>:    jmp    0x4015a7 <main()+151>
    132  <+488>:    mov    %edx,-0x60(%ebp)
    133  <+491>:    call   0x4017bc <__cxa_end_catch>
    134  <+496>:    mov    -0x60(%ebp),%eax
    135  <+499>:    mov    %eax,(%esp)
    136  <+502>:    movl   $0xffffffff,-0x58(%ebp)
    137  <+509>:    call   0x402788 <_Unwind_SjLj_Resume>
    138  <+514>:    mov    %edx,-0x60(%ebp)
    139  <+517>:    movl   $0x0,-0x58(%ebp)
    140  <+524>:    call   0x4017bc <__cxa_end_catch>
    141  <+529>:    mov    -0x60(%ebp),%eax
    142  <+532>:    mov    %eax,(%esp)
    143  <+535>:    movl   $0xffffffff,-0x58(%ebp)
    144  <+542>:    call   0x402788 <_Unwind_SjLj_Resume>
    145  <+547>:    lea    -0x5c(%ebp),%eax
    146  <+550>:    mov    %eax,(%esp)
    147  <+553>:    call   0x402780 <_Unwind_SjLj_Unregister>
    148  <+558>:    mov    -0x60(%ebp),%eax
    149  <+561>:    lea    -0x10(%ebp),%esp
    150  <+564>:    pop    %ecx
    151  <+565>:    pop    %ebx
    152  <+566>:    pop    %esi
    153  <+567>:    pop    %edi
    154  <+568>:    pop    %ebp
    155  <+569>:    lea    -0x4(%ecx),%esp
    156  <+572>:    ret    

    下面的分析只列出不同的地方 

     上圖的註釋有誤沒有勘誤過,lea是不訪問內存,通常代替add指令做加法,應該是6條指令要訪問內存。

    支路控制代碼:

     

     

     

     

     可以看出,支路選路控制指令多而且複雜,還有就是跳轉多。

    最後是函數結束前。

     

     

     

     可以看出在 sjlj 版本中,即使代碼不發生異常,函數在進入與離開時都要為登記維護付出一此成本,當涉及異常代碼時,支路選路控制更加複雜更多跳轉。這裡有一個成本比例,你的函數邏輯簡單,上面的開銷比重就越大,如果是頻繁調用的輕量函數就要考慮不用exception這樣的error handle。

    還有就是當發生異常時,需要交給c++庫去管理,不同異常處理模型的實現,有着不同的開銷,本文並沒有涉及到。只是單純從c++庫以外的代碼進行分析,也足夠看出他們之間有着一定的差別。

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

    【其他文章推薦】

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

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

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

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

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

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

  • 【Flutter實戰】圖片組件及四大案例

    【Flutter實戰】圖片組件及四大案例

    老孟導讀:大家好,這是【Flutter實戰】系列文章的第三篇,這一篇講解圖片組件,Image有很多高級用法,希望對您有所幫助。

    圖片組件是Flutter基礎組件之一,和文本組件一樣必不可少。圖片組件包含Image和Icon兩個組件,本質上Icon不屬於圖片組件,但其外形效果上類似於圖片。

    在項目中建議優先使用Icon組件,Icon本質上是一種字體,只不過显示的不是文字,而是圖標,而Image組件先通過圖片解碼器將圖片解碼,所以Icon有如下優點:

    • 通常情況下,圖標比圖片體積更小,顯著的減少App包體積。
    • 圖標不會出現失真或者模糊的現象,例如將20×20的圖片,渲染在200×200的屏幕上,圖片會失真或模糊,而圖標是矢量圖,不會失真,就像字體一樣。
    • 多個圖標可以存放在一個文件中,方便管理。
    • 全平台通用。

    Image

    Image組件用於显示圖片,圖片的來源可以是網絡、項目中圖片或者設備上的圖片。

    加載網絡圖片:

    Image.network(
      'http://pic1.win4000.com/pic/c/cf/cdc983699c.jpg',
    )
    

    加載項目中圖片:

    首先將圖片拷貝到項目中,通常情況下,拷貝到assets/images/目錄下,assets/images/目錄為手動創建,新建的項目默認是沒有此目錄的。

    設置pubspec.yaml配置文件:

    assets:
      - assets/images/
    

    或者指定具體圖片的名稱:

    assets:
      - assets/images/aa.jpg
    

    通常情況下,使用第一種方式,因為圖片會有很多張,增加一張就這裏配置一個太麻煩。

    注意:assets前面的空格問題,極容易引發編譯異常,正確格式如下:

    加載圖片:

    Image.asset('assets/images/aa.jpg')
    

    加載設備上的圖片:

    要加載設備(手機)上的圖片首先需要獲取設備圖片的路徑,由於不同平台的路徑不同,因此路徑的獲取必須依靠原生支持,如果了解原生(Android和iOS)開發,可以直接使用MethodChannel獲取路徑,如果不懂原生(Android和iOS)開發,可以使用第三方插件獲取路徑,這裏推薦官方的path_provider

    加載設備上的圖片:

    Image.file(File('path'))
    

    設置圖片的大小:

    Image.asset('assets/images/aa.jpg',width: 100,height: 200,),
    

    當Image的大小和圖片大小不匹配時,需要設置填充模式fit,設置組件大小為150×150,

    Container(
      color: Colors.red.withOpacity(.3),
      child: Image.asset('assets/images/aa.jpg',width: 150,height: 150),
    )
    

    看到,圖片左右兩邊有空白區域(淺紅色填充的區域),如果想要圖片充滿整個區域,設置如下:

    Container(
      color: Colors.red.withOpacity(.3),
      child: Image.asset('assets/images/aa.jpg',width: 150,height: 150,fit: BoxFit.fill,),
    )
    

    雖然圖片充滿整個區域,但圖片變形了,使圖片等比拉伸,直到兩邊都充滿區域:

    Container(
      color: Colors.red.withOpacity(.3),
      child: Image.asset('assets/images/aa.jpg',width: 150,height: 150,fit: BoxFit.cover,),
    )
    

    此時,圖片未變形且兩邊都充滿區域,不過圖片被裁減了一部分。

    fit參數就是設置填充方式,其值介紹如下:

    • fill:完全填充,寬高比可能會變。
    • contain:等比拉伸,直到一邊填充滿。
    • cover:等比拉伸,直到2邊都填充滿,此時一邊可能超出範圍。
    • fitWidth:等比拉伸,寬填充滿。
    • fitHeight:等比拉伸,高填充滿。
    • none:當組件比圖片小時,不拉伸,超出範圍截取。
    • scaleDown:當組件比圖片小時,圖片等比縮小,效果和contain一樣。

    BoxFit.none的裁減和alignment相關,默認居中,

    Image.asset(
      'assets/images/aa.jpg',
      width: 150,
      height: 150,
      fit: BoxFit.none,
      alignment: Alignment.centerRight,
    ),
    

    左邊為原圖。

    設置對齊方式:

    Container(
      color: Colors.red.withOpacity(.3),
      child: Image.asset(
        'assets/images/aa.jpg',
        width: 150,
        height: 150,
        alignment: Alignment.centerLeft,
      ),
    ),
    

    colorcolorBlendMode用於將顏色和圖片進行顏色混合,colorBlendMode表示混合模式,下面介紹的混合模式比較多,瀏覽一遍即可,此屬性可以用於簡單的濾鏡效果。

    • clear:清楚源圖像和目標圖像。
    • color:獲取源圖像的色相和飽和度以及目標圖像的光度。
    • colorBurn:將目標的倒數除以源,然後將結果倒數。
    • colorDodge:將目標除以源的倒數。
    • darken:通過從每個顏色通道中選擇最小值來合成源圖像和目標圖像。
    • difference:從每個通道的較大值中減去較小的值。合成黑色沒有效果。合成白色會使另一張圖像的顏色反轉。
    • dst:僅繪製目標圖像。
    • dstATop:將目標圖像合成到源圖像上,但僅在與源圖像重疊的位置合成。
    • dstIn:显示目標圖像,但僅显示兩個圖像重疊的位置。不渲染源圖像,僅將其視為蒙版。源的顏色通道將被忽略,只有不透明度才起作用。
    • dstOut:显示目標圖像,但僅显示兩個圖像不重疊的位置。不渲染源圖像,僅將其視為蒙版。源的顏色通道將被忽略,只有不透明度才起作用。
    • dstOver:將源圖像合成到目標圖像下。
    • exclusion:從兩個圖像的總和中減去兩個圖像的乘積的兩倍。
    • hardLight:調整源圖像和目標圖像的成分以使其適合源圖像之後,將它們相乘。
    • hue:獲取源圖像的色相,以及目標圖像的飽和度和光度。
    • lighten:通過從每個顏色通道中選擇最大值來合成源圖像和目標圖像。
    • luminosity:獲取源圖像的亮度,以及目標圖像的色相和飽和度。
    • modulate:將源圖像和目標圖像的顏色分量相乘。
    • multiply:將源圖像和目標圖像的分量相乘,包括alpha通道。
    • overlay:調整源圖像和目標圖像的分量以使其適合目標后,將它們相乘。
    • plus:對源圖像和目標圖像的組成部分求和。
    • saturation:獲取源圖像的飽和度以及目標圖像的色相和亮度。
    • screen:將源圖像和目標圖像的分量的逆值相乘,然後對結果求逆。
    • softLight:對於低於0.5的源值使用colorDodge,對於高於0.5的源值使用colorBurn。
    • src:放置目標圖像,僅繪製源圖像。
    • srcATop:將源圖像合成到目標圖像上,但僅在與目標圖像重疊的位置合成。
    • srcIn:显示源圖像,但僅显示兩個圖像重疊的位置。目標圖像未渲染,僅被視為蒙版。目標的顏色通道將被忽略,只有不透明度才起作用。
    • srcOut:显示源圖像,但僅显示兩個圖像不重疊的位置。
    • srcOver:將源圖像合成到目標圖像上。
    • xor:將按位異或運算符應用於源圖像和目標圖像。

    是不是感覺看了和沒看差不多,看了也看不懂。正常,估計只有學過視覺算法的才能看懂吧,直接看下各個屬性的效果吧:

    repeat表示當組件有空餘位置時,將會重複显示圖片

    Image.asset(
      'assets/images/aa.jpg',
      width: double.infinity,
      height: 150,
      repeat: ImageRepeat.repeatX,
    )
    

    重複的模式有:

    • repeat:x,y方向都充滿。
    • repeatX:x方向充滿。
    • repeatY:y方向充滿。
    • noRepeat:不重複。

    matchTextDirection設置為true時,圖片的繪製方向為TextDirection設置的方向,其父組件必須為Directionality

    Directionality(
        textDirection: TextDirection.rtl,
        child: Image.asset(
          'assets/images/logo.png',
          height: 150,
          matchTextDirection: true,
        )),
    

    左邊為原圖,效果是左右鏡像。

    filterQuality表示繪製圖像的質量,從高到低為:high->medium->low->none。越高效果越好,越平滑,當然性能損耗越大,默認是low,如果發現圖片有鋸齒,可以設置此參數。

    當加載圖片的時候回調frameBuilder,當此參數為null時,此控件將會在圖片加載完成后显示,未加載完成時显示空白,尤其在加載網絡圖片時會更明顯。因此此參數可以用於處理圖片加載時显示佔位圖片和加載圖片的過渡效果,比如淡入淡出效果。

    下面的案例是淡入淡出效果:

    Image.network(
      'https://flutter.github.io/assets-for-api-docs/assets/widgets/puffin.jpg',
      frameBuilder: (BuildContext context, Widget child, int frame,
          bool wasSynchronouslyLoaded) {
        if (wasSynchronouslyLoaded) {
          return child;
        }
        return AnimatedOpacity(
          child: child,
          opacity: frame == null ? 0 : 1,
          duration: const Duration(seconds: 2),
          curve: Curves.easeOut,
        );
      },
    )
    

    loadingBuilder參數比frameBuilder控制的力度更細,可以獲取圖片加載的進度,下面的案例显示了加載進度條:

    Image.network(
        'https://flutter.github.io/assets-for-api-docs/assets/widgets/puffin.jpg',
        loadingBuilder: (BuildContext context, Widget child,
            ImageChunkEvent loadingProgress) {
      if (loadingProgress == null) {
        return child;
      }
      return Center(
        child: CircularProgressIndicator(
          value: loadingProgress.expectedTotalBytes != null
              ? loadingProgress.cumulativeBytesLoaded /
                  loadingProgress.expectedTotalBytes
              : null,
        ),
      );
    })
    

    centerSlice用於.9圖,.9圖用於拉伸圖片的特定區域,centerSlice設置的區域(Rect)就是拉伸的區域。.9圖通常用於控件大小、寬高比不固定的場景,比如聊天背景圖片等。

    Container(
        width: 250,
        height: 300,
        decoration: BoxDecoration(
            image: DecorationImage(
                centerSlice: Rect.fromLTWH(20, 20, 10, 10),
                image: AssetImage(
                  'assets/images/abc.jpg',
                ),
                fit: BoxFit.fill))),
    

    上面為原圖,下面為拉伸的圖片。

    在使用時大概率會出現如下異常:

    這是由於圖片比組件的尺寸大,如果使用centerSlice屬性,圖片必須比組件的尺寸小,一般情況下,.9圖的尺寸都非常小。

    Icon

    Icon是圖標組件,Icon不具有交互屬性,如果想要交互,可以使用IconButton。

    Icon(Icons.add),
    

    設置圖標的大小和顏色:

    Icon(
      Icons.add,
      size: 40,
      color: Colors.red,
    )
    

    上面的黑色為默認大小和顏色。

    Icons.add是系統提供的圖標,創建Flutter項目的時候,pubspec.yaml中默認有如下配置:

    所有的圖標在Icons中已經定義,可以直接在源代碼中查看,也可以到官網查看所有圖標。

    所有圖標效果如下:

    案例

    聊天背景(.9圖實現)

    Container(
      width: 200,
      padding: EdgeInsets.only(left: 8,top: 8,right: 20,bottom: 8),
      decoration: BoxDecoration(
          image: DecorationImage(
              centerSlice: Rect.fromLTWH(20, 20, 1, 1),
              image: AssetImage(
                'assets/images/chat.png',
              ),
              fit: BoxFit.fill)),
      child: Text('老孟,專註分享Flutter技術和應用實戰。'
          '老孟,專註分享Flutter技術和應用實戰。'
          '老孟,專註分享Flutter技術和應用實戰。',),
    )
    

    背景圖片大小是57×80:

    右側三角已經不在中間了,如果想讓其一直保持居中,修改拉伸區域:

    centerSlice: Rect.fromLTWH(20, 10, 1, 60),
    

    圓形帶邊框的頭像

    Container(
      width: 100,
      height: 100,
      padding: EdgeInsets.all(3),
      decoration: BoxDecoration(shape: BoxShape.circle, color: Colors.blue),
      child: Container(
        decoration: BoxDecoration(
            shape: BoxShape.circle,
            image: DecorationImage(
                image: AssetImage('assets/images/aa.jpg'), fit: BoxFit.cover)),
      ),
    )
    

    圖片佔位符:

    Image.network(
      'https://flutter.github.io/assets-for-api-docs/assets/widgets/puffin.jpg',
      height: 150,
      width: 150,
      fit: BoxFit.cover,
      frameBuilder: (
        BuildContext context,
        Widget child,
        int frame,
        bool wasSynchronouslyLoaded,
      ) {
        if (frame == null) {
          return Image.asset(
            'assets/images/place.png',
            height: 150,
            width: 150,
            fit: BoxFit.cover,
          );
        }
        return child;
      },
    )
    

    添加自己的圖標庫

    如果系統提供的圖標沒有我們想要的圖標,這時需要引入第三方庫的圖標,下面以阿里巴巴的圖標庫為例。

    打開阿里巴巴的圖標官網,找到自己想要的圖標后,將鼠標放置到圖標上,加入購物車:

    點擊右上角的購物車,然後點擊添加至項目:

    如果沒有添加過項目,需要創建一個新項目:

    創建好后加入此項目,跳轉到我的項目頁面,點擊下載:

    解壓下載的文件,解壓出來的文件有好幾個,如下圖:

    選擇iconfont.ttf文件拷貝到 Flutter 項目的assets/fonts目錄下,assets/fonts目錄默認是沒有的,需要手動創建,在pubspec.yaml設置如下:

    千萬注意紅框內開頭的空格問題,否則編譯不通過,family後面跟的字符串最好有意義,後面用圖標的時候需要用到。

    用法如下:

    Icon(IconData(0xe613,fontFamily: 'appIconFonts')
    

    0xe613在下載圖標時已經標註,將&#替換為0,如下圖:

    fontFamily是在pubspec.yaml中設置的family屬性,第三方的圖標和系統圖標一樣,可以設置其顏色和大小。

    交流

    老孟Flutter博客地址(330個控件用法):http://laomengit.com

    歡迎加入Flutter交流群(微信:laomengit)、關注公眾號【老孟Flutter】:

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

    【其他文章推薦】

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

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

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

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

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

  • 使用Apache commons email發送郵件

    使用Apache commons email發送郵件

    今天研究了一下怎麼用java代碼發送郵件,用的是Apache的commons-email包。

    據說這個包是對javamail進行了封裝,簡化了操作。 這裏講一下具體用法吧

     

    一.首先你需要有郵箱賬號和一個授權碼。

    需要進入到QQ郵箱或者是網易郵箱裏面去獲取。在郵箱的設置->賬戶裏面,開啟如下服務,就能得到一個授權碼,這個授權碼要好好保管。有了這兩個東西就能夠通過第三方客戶端發送郵件了。

     

    二.導入commons-email的maven依賴。

    我用的是1.4,也可以去maven倉庫網站(https://mvnrepository.com)上面找別的版本。

    <dependency>
         <groupId>org.apache.commons</groupId>
         <artifactId>commons-email</artifactId>
         <version>1.4</version>
     </dependency>

    三.然後就可以寫發送郵件的代碼了。

    我在網上找了幾個案例,如下。

     1.發送簡單文本郵件。這是最簡單也是最常用的。

        /**
         * @describe 發送內容為簡單文本的郵件
         * @throws EmailException
         */
        public static void sendSimpleTextEmail() throws EmailException {
             Email email = new SimpleEmail();
             //設置主機名,QQ郵箱是"smtp.qq.com",網易郵箱是"smtp.163.com"
             email.setHostName("smtp.163.com");
             // 用戶名和密碼為郵箱的賬號和授權碼(不需要進行base64編碼)
             email.setAuthenticator(new DefaultAuthenticator("myemailaddress@163.com", "myshouquanma"));
             //設置SSL連接,這樣寫就對了
             email.setSSLOnConnect(true);
             //設置來源,就是發送方的郵箱地址
             email.setFrom("myemailaddress@163.com");
             //設置主題,可以不設置
             email.setSubject("java發送郵件");
             //設置信息,就是內容,這個必須要有
             email.setMsg("這是測試郵件 ... :-)");
             //接收人郵箱地址
             email.addTo("receiveeraddress@qq.com");
             email.send();
        }

     

    2.發送包含附件的郵件(附件為本地資源),這裏用到了一個EmailAttachment對象,也就是附件的意思

        /**
         * @describe 發送包含附件的郵件(附件為本地資源)
         * @throws EmailException
         */
        public static void sendEmailsWithAttachments() throws EmailException {
            // 創建一個attachment(附件)對象
            EmailAttachment attachment = new EmailAttachment();
            //設置上傳附件的地址
            attachment.setPath("C:\\Users\\Administrator\\Pictures\\Saved Pictures\\conti.png");
            attachment.setDisposition(EmailAttachment.ATTACHMENT);
            //這個描述可以隨便寫
            attachment.setDescription("Picture of conti");
            //這個名稱要注意和文件格式一致,這將是接收人下載下來的文件名稱
            attachment.setName("conti.png");
    
            //因為要上傳附件,所以用MultiPartEmail()方法創建一個email對象,固定步驟都是一樣的
            MultiPartEmail email = new MultiPartEmail();
            email.setHostName("smtp.163.com");
            email.setAuthenticator(new DefaultAuthenticator("myemailaddress@163.com", "myshouquanma"));
            email.setSSLOnConnect(true);
            email.addTo("receiveemail@qq.com", "Conti Zhang");
            email.setFrom("myemailaddress@163.com", "Me");
            email.setSubject("圖片");
            email.setMsg("這是發送給你的圖片");
            //將附件添加到郵件
            email.attach(attachment);
    
            email.send();
        }

     

    3.發送包含附近的郵件(附件為在線資源),這個與上傳本地附件稍有區別,注意一下就行

       /**
         * @describe 發送包含附件的郵件(附件為在線資源)
         * @throws EmailException
         * @throws MalformedURLException
         */
        public static void sendEmailsWithOnlineAttachments() throws EmailException, MalformedURLException {
            EmailAttachment attachment = new EmailAttachment();
            //設置在線資源路徑,和上傳本地附件的唯一區別
            attachment.setURL(new URL("http://www.apache.org/images/asf_logo_wide.gif"));
            attachment.setDisposition(EmailAttachment.ATTACHMENT);
            attachment.setDescription("Apache logo");
            attachment.setName("Apache logo.gif");
    
            MultiPartEmail email = new MultiPartEmail();
            email.setHostName("smtp.163.com");
            email.setAuthenticator(new DefaultAuthenticator("myemailaddress@163.com", "myshouquanma"));
            email.setSSLOnConnect(true);
            email.addTo("receiveemail@qq.com", "Conti Zhang");
            email.setFrom("myemailaddress@163.com", "Me");
            email.setSubject("The logo");
            email.setMsg("Here is Apache's logo");
            email.attach(attachment);
            email.send();
        }

     

    4.發送內容為HTML格式的郵件,有些郵件直接打開就是一個HTML頁面。雖然不一定用到,可以了解一下

       /**
         * @describe 發送內容為HTML格式的郵件
         * @throws EmailException
         * @throws MalformedURLException
         */
        public static void sendHTMLFormattedEmail() throws EmailException, MalformedURLException {
            // 這裏需要使用HtmlEmail創建一個email對象
            HtmlEmail email = new HtmlEmail();
            email.setHostName("smtp.163.com");
            email.setAuthenticator(new DefaultAuthenticator("myemailaddresss@163.com", "myshouquanma"));
            email.addTo("receiveemail@qq.com", "Conti Zhang");
            email.setFrom("myemailaddress@163.com", "Me");
            email.setSubject("Test email with inline image");
    
            // 嵌入圖像並獲取內容id,雖然案例這樣寫,但我感覺直接在html內容裏面寫圖片網絡地址也可以
            URL url = new URL("http://www.apache.org/images/asf_logo_wide.gif");
            String cid = email.embed(url, "Apache logo");
    
            // 設置html內容
            email.setHtmlMsg("<html>The apache logo - <img src=\"cid:" + cid + "\"></html>");
    
            // 設置替代內容,如果不支持html
            email.setTextMsg("你的郵件客戶端不支持html郵件");
            email.send();
        }

     

    5.發送內容為HTML格式的郵件(嵌入圖片更方便)

    這裏用到了DataSourceFileResolver對象,和DataSourceUrlResolver對象,前者可以解析本地文件路徑,後者可以解析網絡路徑

    具體用法如下

        /**
         * @describe 發送內容為HTML格式的郵件(嵌入圖片更方便)
         * @throws MalformedURLException
         * @throws EmailException
         */
        public static void sendHTMLFormattedEmailWithEmbeddedImages() throws MalformedURLException, EmailException {
            //html郵件模板
    String htmlEmailTemplate = "<img src=\"http://www.conti.com/images/1.jpg\">"; DataSourceResolver[] dataSourceResolvers =new DataSourceResolver[]{new DataSourceFileResolver(),new DataSourceUrlResolver(new URL("http://"))}; email.setDataSourceResolver(new DataSourceCompositeResolver(dataSourceResolvers)); email.setHostName("smtp.qq.com"); email.setAuthenticator(new DefaultAuthenticator("myemailaddress@qq.com", "myshouquanma")); email.addTo("receiveemail@qq.com", "Conti Zhang"); email.setFrom("myemailaddress@qq.com", "Me"); email.setSubject("Test email with inline image"); email.setHtmlMsg(htmlEmailTemplate); email.setTextMsg("你的郵件客戶端不支持html郵件"); email.send(); }

    此種方式可能會報錯,會被郵箱認為是有害郵件不接收而導致發送失敗。解決方法就是,網易郵箱不行就換QQ郵箱,我就是這樣做的

     

    好了,就這麼多,歡迎討論!

     

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

    【其他文章推薦】

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

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

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

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

    ※回頭車貨運收費標準

  • [源碼解析] GroupReduce,GroupCombine 和 Flink SQL group by

    [源碼解析] GroupReduce,GroupCombine和Flink SQL group by

    目錄

    • [源碼解析] GroupReduce,GroupCombine和Flink SQL group by
      • 0x00 摘要
      • 0x01 緣由
      • 0x02 概念
        • 2.1 GroupReduce
        • 2.2 GroupCombine
        • 2.3 例子
      • 0x03 代碼
      • 0x04 Flink SQL內部翻譯
      • 0x05 JobGraph
      • 0x06 Runtime
        • 6.1 ChainedFlatMapDriver
        • 6.2 GroupReduceCombineDriver
        • 6.3 GroupReduceDriver & ChainedFlatMapDriver
      • 0x07 總結
      • 0x08 參考

    0x00 摘要

    本文從源碼和實例入手,為大家解析 Flink 中 GroupReduce 和 GroupCombine 的用途。也涉及到了 Flink SQL group by 的內部實現。

    0x01 緣由

    在前文[源碼解析] Flink的Groupby和reduce究竟做了什麼中,我們剖析了Group和reduce都做了些什麼,也對combine有了一些了解。但是總感覺意猶未盡,因為在Flink還提出了若干新算子,比如GroupReduce和GroupCombine。這幾個算子不搞定,總覺得如鯁在喉,但沒有找到一個良好的例子來進行剖析說明。

    本文是筆者在探究Flink SQL UDF問題的一個副產品。起初是為了調試一段sql代碼,結果發現Flink本身給出了一個GroupReduce和GroupCombine使用的完美例子。於是就拿出來和大家共享,一起分析看看究竟如何使用這兩個算子。

    請注意:這個例子是Flink SQL,所以本文中將涉及Flink SQL goup by內部實現的知識。

    0x02 概念

    Flink官方對於這兩個算子的使用說明如下:

    2.1 GroupReduce

    GroupReduce算子應用在一個已經分組了的DataSet上,其會對每個分組都調用到用戶定義的group-reduce函數。它與Reduce的區別在於用戶定義的函數會立即獲得整個組。

    Flink將在組的所有元素上使用Iterable調用用戶自定義函數,並且可以返回任意數量的結果元素。

    2.2 GroupCombine

    GroupCombine轉換是可組合GroupReduceFunction中組合步驟的通用形式。它在某種意義上被概括為允許將輸入類型 I 組合到任意輸出類型O。與此相對的是,GroupReduce中的組合步驟僅允許從輸入類型 I 到輸出類型 I 的組合。這是因為GroupReduceFunction的 “reduce步驟” 期望自己的輸入類型為 I。

    在一些應用中,我們期望在執行附加變換(例如,減小數據大小)之前將DataSet組合成中間格式。這可以通過CombineGroup轉換能以非常低的成本實現。

    注意:分組數據集上的GroupCombine在內存中使用貪婪策略執行,該策略可能不會一次處理所有數據,而是以多個步驟處理。它也可以在各個分區上執行,而無需像GroupReduce轉換那樣進行數據交換。這可能會導致輸出的是部分結果,所以GroupCombine是不能替代GroupReduce操作的,儘管它們的操作內容可能看起來都一樣。

    2.3 例子

    是不是有點暈?還是直接讓代碼來說話吧。以下官方示例演示了如何將CombineGroup和GroupReduce轉換用於WordCount實現。即通過combine操作先對單詞數目進行初步排序,然後通過reduceGroup對combine產生的結果進行最終排序。因為combine進行了初步排序,所以在算子之間傳輸的數據量就少多了

    DataSet<String> input = [..] // The words received as input
    
    // 這裏通過combine操作先對單詞數目進行初步排序,其優勢在於用戶定義的combine函數只調用一次,因為runtime已經把輸入數據一次性都提供給了自定義函數。  
    DataSet<Tuple2<String, Integer>> combinedWords = input
      .groupBy(0) // group identical words
      .combineGroup(new GroupCombineFunction<String, Tuple2<String, Integer>() {
    
        public void combine(Iterable<String> words, Collector<Tuple2<String, Integer>>) { // combine
            String key = null;
            int count = 0;
    
            for (String word : words) {
                key = word;
                count++;
            }
            // emit tuple with word and count
            out.collect(new Tuple2(key, count));
        }
    });
    
    // 這裏對combine的結果進行第二次排序,其優勢在於用戶定義的reduce函數只調用一次,因為runtime已經把輸入數據一次性都提供給了自定義函數。  
    DataSet<Tuple2<String, Integer>> output = combinedWords
      .groupBy(0)                              // group by words again
      .reduceGroup(new GroupReduceFunction() { // group reduce with full data exchange
    
        public void reduce(Iterable<Tuple2<String, Integer>>, Collector<Tuple2<String, Integer>>) {
            String key = null;
            int count = 0;
    
            for (Tuple2<String, Integer> word : words) {
                key = word;
                count++;
            }
            // emit tuple with word and count
            out.collect(new Tuple2(key, count));
        }
    });
    

    看到這裏,有的兄弟已經明白了,這和mapPartition很類似啊,都是runtime做了大量工作。為了讓大家這兩個算子的使用情形有深刻的認識,我們再通過一個sql的例子,向大家展示Flink內部是怎麼應用這兩個算子的,也能看出來他們的強大之處

    0x03 代碼

    下面代碼主要參考自 flink 使用問題匯總。我們可以看到這裏通過groupby進行了聚合操作。其中collect方法,類似於mysql的group_concat。

    public class UdfExample {
        public static class MapToString extends ScalarFunction {
    
            public String eval(Map<String, Integer> map) {
                if(map==null || map.size()==0) {
                    return "";
                }
                StringBuffer sb=new StringBuffer();
                for(Map.Entry<String, Integer> entity : map.entrySet()) {
                    sb.append(entity.getKey()+",");
                }
                String result=sb.toString();
                return result.substring(0, result.length()-1);
            }
        }
    
        public static void main(String[] args) throws Exception {
            MemSourceBatchOp src = new MemSourceBatchOp(new Object[][]{
                    new Object[]{"1", "a", 1L},
                    new Object[]{"2", "b33", 2L},
                    new Object[]{"2", "CCC", 2L},
                    new Object[]{"2", "xyz", 2L},
                    new Object[]{"1", "u", 1L}
            }, new String[]{"f0", "f1", "f2"});
    
            BatchTableEnvironment environment = MLEnvironmentFactory.getDefault().getBatchTableEnvironment();
            Table table = environment.fromDataSet(src.getDataSet());
            environment.registerTable("myTable", table);
            BatchOperator.registerFunction("MapToString",  new MapToString());
            BatchOperator.sqlQuery("select f0, mapToString(collect(f1)) as type from myTable group by f0").print();
        }
    }
    

    程序輸出是

    f0|type
    --|----
    1|a,u
    2|CCC,b33,xyz
    

    0x04 Flink SQL內部翻譯

    這個SQL語句的重點是group by。這個是程序猿經常使用的操作。但是大家有沒有想過這個group by在真實運行起來時候是怎麼操作的呢?針對大數據環境有沒有做了什麼優化呢?其實,Flink正是使用了GroupReduce和GroupCombine來實現並且優化了group by的功能。優化之處在於:

    • GroupReduce和GroupCombine的函數調用次數要遠低於正常的reduce算子,如果reduce操作中涉及到頻繁創建額外的對象或者外部資源操作,則會相當省時間。
    • 因為combine進行了初步排序,所以在算子之間傳輸的數據量就少多了。

    SQL生成Flink的過程十分錯綜複雜,所以我們只能找最關鍵處。其是在 DataSetAggregate.translateToPlan 完成的。我們可以看到,對於SQL語句 “select f0, mapToString(collect(f1)) as type from myTable group by f0”,Flink系統把它翻譯成如下階段,即

    • pre-aggregation :排序 + combine;
    • final aggregation :排序 + reduce;

    從之前的文章我們可以知道,groupBy這個其實不是一個算子,它只是排序過程中的一個輔助步驟而已,所以我們重點還是要看combineGroup和reduceGroup。這恰恰是我們想要的完美例子。

    input ----> (groupBy + combineGroup) ----> (groupBy + reduceGroup) ----> output
    

    SQL生成的Scala代碼如下,其中 combineGroup在後續中將生成GroupCombineOperator,reduceGroup將生成GroupReduceOperator。

      override def translateToPlan(
          tableEnv: BatchTableEnvImpl,
          queryConfig: BatchQueryConfig): DataSet[Row] = {
    
        if (grouping.length > 0) {
          // grouped aggregation
          ...... 
          if (preAgg.isDefined) { // 我們的例子是在這裏
            inputDS          
              // pre-aggregation
              .groupBy(grouping: _*)
              .combineGroup(preAgg.get) // 將生成GroupCombineOperator算子
              .returns(preAggType.get)
              .name(aggOpName)
              // final aggregation
              .groupBy(grouping.indices: _*) //將生成GroupReduceOperator算子。
              .reduceGroup(finalAgg.right.get)
              .returns(rowTypeInfo)
              .name(aggOpName)
          } else {
            ......
          }
        }
        else {
          ......
        }
      }
    }
    
    // 程序變量打印如下
    this = {DataSetAggregate@5207} "Aggregate(groupBy: (f0), select: (f0, COLLECT(f1) AS $f1))"
     cluster = {RelOptCluster@5220} 
    

    0x05 JobGraph

    LocalExecutor.execute中會生成JobGraph。JobGraph是提交給 JobManager 的數據結構,是唯一被Flink的數據流引擎所識別的表述作業的數據結構,也正是這一共同的抽象體現了流處理和批處理在運行時的統一。

    在生成JobGraph時候,系統得到如下JobVertex。

    jobGraph = {JobGraph@5652} "JobGraph(jobId: 6aae8b5e5ad32f588136bef26f8b65f6)"
     taskVertices = {LinkedHashMap@5655}  size = 4
    
    {JobVertexID@5677} "c625209bb7fb9a098807551840aeaa99" -> {InputOutputFormatVertex@5678} "CHAIN DataSource (at initializeDataSource(MemSourceBatchOp.java:98) (org.apache.flink.api.java.io.CollectionInputFormat)) -> FlatMap (select: (f0, f1)) (org.apache.flink.runtime.operators.DataSourceTask)"
    
    {JobVertexID@5679} "b56ace4acd7a2f69ea110a9f262ff80a" -> {JobVertex@5680} "CHAIN GroupReduce (groupBy: (f0), select: (f0, COLLECT(f1) AS $f1)) -> FlatMap (select: (f0, mapToString($f1) AS type)) -> Map (Map at linkFrom(MapBatchOp.java:35)) (org.apache.flink.runtime.operators.BatchTask)"
     
    {JobVertexID@5681} "3f5e2a0f700421d80ce85e02a6d9db73" -> {InputOutputFormatVertex@5682} "DataSink (collect()) (org.apache.flink.runtime.operators.DataSinkTask)"
     
    {JobVertexID@5683} "ad29dc5b2e0a39ad2cd1d164b6f859f7" -> {JobVertex@5684} "GroupCombine (groupBy: (f0), select: (f0, COLLECT(f1) AS $f1)) (org.apache.flink.runtime.operators.BatchTask)"
    

    我們可以看到,在JobGraph中就生成了對應的兩個算子。其中這裏的FlatMap就是用戶的UDF函數MapToString的映射生成。

    GroupCombine (groupBy: (f0), select: (f0, COLLECT(f1) AS $f1))  
      
    CHAIN GroupReduce (groupBy: (f0), select: (f0, COLLECT(f1) AS $f1)) -> FlatMap (select: (f0, mapToString($f1) AS type)) -> Map 
    

    0x06 Runtime

    最後,讓我們看看runtime會如何處理這兩個算子。

    6.1 ChainedFlatMapDriver

    首先,Flink會在ChainedFlatMapDriver.collect中對record進行處理,這是從Table中提取數據所必須經歷的,與後續的group by關係不大。

    @Override
    public void collect(IT record) {
       try {
          this.numRecordsIn.inc();
          this.mapper.flatMap(record, this.outputCollector);
       } catch (Exception ex) {
          throw new ExceptionInChainedStubException(this.taskName, ex);
       }
    }
    
    // 這裡能夠看出來,我們獲取了第一列記錄
    record = {Row@9317} "1,a,1"
     fields = {Object[3]@9330} 
    this.taskName = "FlatMap (select: (f0, f1))"
    
    // 程序堆棧打印如下
    collect:80, ChainedFlatMapDriver (org.apache.flink.runtime.operators.chaining)
    collect:35, CountingCollector (org.apache.flink.runtime.operators.util.metrics)
    invoke:196, DataSourceTask (org.apache.flink.runtime.operators)
    doRun:707, Task (org.apache.flink.runtime.taskmanager)
    run:532, Task (org.apache.flink.runtime.taskmanager)
    run:748, Thread (java.lang)
    

    6.2 GroupReduceCombineDriver

    其次,GroupReduceCombineDriver.run()中會進行combine操作。

    1. 會通過this.sorter.write(value)把數據寫到排序緩衝區。
    2. 會通過sortAndCombineAndRetryWrite(value)進行實際的排序,合併。

    因為是系統實現,所以Combine的用戶自定義函數就是由Table API提供的,比如org.apache.flink.table.functions.aggfunctions.CollectAccumulator.accumulate

    @Override
    public void run() throws Exception {
       final MutableObjectIterator<IN> in = this.taskContext.getInput(0);
       final TypeSerializer<IN> serializer = this.serializer;
    
       if (objectReuseEnabled) {
        .....
       }
       else {
          IN value;
          while (running && (value = in.next()) != null) {
             // try writing to the sorter first
             if (this.sorter.write(value)) {
                continue;
             }
    
             // do the actual sorting, combining, and data writing
             sortAndCombineAndRetryWrite(value);
          }
       }
    
       // sort, combine, and send the final batch
       if (running) {
          sortAndCombine();
       }
    }
    
    // 程序變量如下
    value = {Row@9494} "1,a"
     fields = {Object[2]@9503} 
    

    sortAndCombine是具體排序/合併的過程。

    1. 排序是通過 org.apache.flink.runtime.operators.sort.QuickSort 完成的。
    2. 合併是通過 org.apache.flink.table.functions.aggfunctions.CollectAccumulator.accumulate 完成的。
    3. 給下游是由 org.apache.flink.table.runtime.aggregate.DataSetPreAggFunction.combine 調用 out.collect(output) 完成的。
    private void sortAndCombine() throws Exception {
       final InMemorySorter<IN> sorter = this.sorter;
       // 這裏進行實際的排序
       this.sortAlgo.sort(sorter);
       final GroupCombineFunction<IN, OUT> combiner = this.combiner;
       final Collector<OUT> output = this.output;
    
       // iterate over key groups
       if (objectReuseEnabled) {
    			......		
       } else {
          final NonReusingKeyGroupedIterator<IN> keyIter = 
                new NonReusingKeyGroupedIterator<IN>(sorter.getIterator(), this.groupingComparator);
          // 這裡是歸併操作
          while (this.running && keyIter.nextKey()) {
             // combiner.combiner 是用戶定義操作,runtime把某key對應的數據一次性傳給它
             combiner.combine(keyIter.getValues(), output);
          }
       }
    }
    

    具體調用棧如下:

    accumulate:57, CollectAggFunction (org.apache.flink.table.functions.aggfunctions)
    accumulate:-1, DataSetAggregatePrepareMapHelper$5
    combine:71, DataSetPreAggFunction (org.apache.flink.table.runtime.aggregate)
    sortAndCombine:213, GroupReduceCombineDriver (org.apache.flink.runtime.operators)
    run:188, GroupReduceCombineDriver (org.apache.flink.runtime.operators)
    run:504, BatchTask (org.apache.flink.runtime.operators)
    invoke:369, BatchTask (org.apache.flink.runtime.operators)
    doRun:707, Task (org.apache.flink.runtime.taskmanager)
    run:532, Task (org.apache.flink.runtime.taskmanager)
    run:748, Thread (java.lang)
    

    6.3 GroupReduceDriver & ChainedFlatMapDriver

    這兩個放在一起,是因為他們組成了Operator Chain。

    GroupReduceDriver.run中完成了reduce。具體reduce 操作是在 org.apache.flink.table.runtime.aggregate.DataSetFinalAggFunction.reduce 完成的,然後在其中直接發送給下游 out.collect(output)

    @Override
    public void run() throws Exception {
       // cache references on the stack
       final GroupReduceFunction<IT, OT> stub = this.taskContext.getStub();
     
       if (objectReuseEnabled) {
           ......	
       }
       else {
          final NonReusingKeyGroupedIterator<IT> iter = new NonReusingKeyGroupedIterator<IT>(this.input, this.comparator);
          // run stub implementation
          while (this.running && iter.nextKey()) {
             // stub.reduce 是用戶定義操作,runtime把某key對應的數據一次性傳給它
             stub.reduce(iter.getValues(), output);
          }
       }
    }
    

    從前文我們可以,這裏已經配置成了Operator Chain,所以out.collect(output)會調用到CountingCollector。CountingCollector的成員變量collector已經配置成了ChainedFlatMapDriver。

    public void collect(OUT record) {
       this.numRecordsOut.inc();
       this.collector.collect(record);
    }
    
    this.collector = {ChainedFlatMapDriver@9643} 
     mapper = {FlatMapRunner@9610} 
     config = {TaskConfig@9655} 
     taskName = "FlatMap (select: (f0, mapToString($f1) AS type))"
    

    於是程序就調用到了 ChainedFlatMapDriver.collect

    public void collect(IT record) {
       try {
          this.numRecordsIn.inc();
          this.mapper.flatMap(record, this.outputCollector);
       } catch (Exception ex) {
          throw new ExceptionInChainedStubException(this.taskName, ex);
       }
    }
    

    最終調用棧如如下:

    eval:21, UdfExample$MapToString (com.alibaba.alink)
    flatMap:-1, DataSetCalcRule$14
    flatMap:52, FlatMapRunner (org.apache.flink.table.runtime)
    flatMap:31, FlatMapRunner (org.apache.flink.table.runtime)
    collect:80, ChainedFlatMapDriver (org.apache.flink.runtime.operators.chaining)
    collect:35, CountingCollector (org.apache.flink.runtime.operators.util.metrics)
    reduce:80, DataSetFinalAggFunction (org.apache.flink.table.runtime.aggregate)
    run:131, GroupReduceDriver (org.apache.flink.runtime.operators)
    run:504, BatchTask (org.apache.flink.runtime.operators)
    invoke:369, BatchTask (org.apache.flink.runtime.operators)
    doRun:707, Task (org.apache.flink.runtime.taskmanager)
    run:532, Task (org.apache.flink.runtime.taskmanager)
    run:748, Thread (java.lang)
    

    0x07 總結

    由此我們可以看到:

    • GroupReduce,GroupCombine和mapPartition十分類似,都是從系統層面對算子進行優化,把循環操作放到用戶自定義函數來處理。
    • 對於group by這個SQL語句,Flink將其翻譯成 GroupReduce + GroupCombine,採用兩階段優化的方式來完成了對大數據下的處理。

    0x08 參考

    flink 使用問題匯總

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

    【其他文章推薦】

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

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

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

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

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

    ※超省錢租車方案

  • 漲姿勢了解一下Kafka消費位移可好?

    漲姿勢了解一下Kafka消費位移可好?

    摘要:Kafka中的位移是個極其重要的概念,因為數據一致性、準確性是一個很重要的語義,我們都不希望消息重複消費或者丟失。而位移就是控制消費進度的大佬。本文就詳細聊聊kafka消費位移的那些事,包括:

    概念剖析

    kafka的兩種位移

    關於位移(Offset),其實在kafka的世界里有兩種位移:

    • 分區位移:生產者向分區寫入消息,每條消息在分區中的位置信息由一個叫offset的數據來表徵。假設一個生產者向一個空分區寫入了 10 條消息,那麼這 10 條消息的位移依次是 0、1、…、9;

    • 消費位移:消費者需要記錄消費進度,即消費到了哪個分區的哪個位置上,這是消費者位移(Consumer Offset)。

    注意,這和上面所說的消息在分區上的位移完全不是一個概念。上面的“位移”表徵的是分區內的消息位置,它是不變的,即一旦消息被成功寫入到一個分區上,它的位移值就是固定的了。而消費者位移則不同,它可能是隨時變化的,畢竟它是消費者消費進度的指示器。

    消費位移

    消費位移,記錄的是 Consumer 要消費的下一條消息的位移,切記,是下一條消息的位移! 而不是目前最新消費消息的位移

    假設一個分區中有 10 條消息,位移分別是 0 到 9。某個 Consumer 應用已消費了 5 條消息,這就說明該 Consumer 消費了位移為 0 到 4 的 5 條消息,此時 Consumer 的位移是 5,指向了下一條消息的位移。

    至於為什麼要有消費位移,很好理解,當 Consumer 發生故障重啟之後,就能夠從 Kafka 中讀取之前提交的位移值,然後從相應的位移處繼續消費,從而避免整個消費過程重來一遍。就好像書籤一樣,需要書籤你才可以快速找到你上次讀書的位置。

    那麼了解了位移是什麼以及它的重要性,我們自然而然會有一個疑問,kafka是怎麼記錄、怎麼保存、怎麼管理位移的呢?

    位移的提交

    Consumer 需要上報自己的位移數據,這個彙報過程被稱為位移提交。因為 Consumer 能夠同時消費多個分區的數據,所以位移的提交實際上是在分區粒度上進行的,即Consumer 需要為分配給它的每個分區提交各自的位移數據。

    鑒於位移提交甚至是位移管理對 Consumer 端的巨大影響,KafkaConsumer API提供了多種提交位移的方法,每一種都有各自的用途,這些都是本文將要談到的方案。

    void commitSync(Duration timeout);
    void commitSync(Map<TopicPartition, OffsetAndMetadata> offsets);
    void commitSync(final Map<TopicPartition, OffsetAndMetadata> offsets, final Duration timeout);
    void commitAsync();
    void commitAsync(OffsetCommitCallback callback);
    void commitAsync(Map<TopicPartition, OffsetAndMetadata> offsets, OffsetCommitCallback callback);
    

    先粗略的總結一下。位移提交分為自動提交和手動提交;而手動提交又分為同步提交和異步提交。

    自動提交

    當消費配置enable.auto.commit=true的時候代表自動提交位移。

    自動提交位移是發生在什麼時候呢?auto.commit.interval.ms默認值是50000ms。即kafka每隔5s會幫你自動提交一次位移。自動位移提交的動作是在 poll()方法的邏輯里完成的,在每次真正向服務端發起拉取請求之前會檢查是否可以進行位移提交,如果可以,那麼就會提交上一次輪詢的位移。假如消費數據量特別大,可以設置的短一點。

    越簡單的東西功能越不足,自動提交位移省事的同時肯定會帶來一些問題。自動提交帶來重複消費和消息丟失的問題:

    • 重複消費: 在默認情況下,Consumer 每 5 秒自動提交一次位移。現在,我們假設提交位移之後的 3 秒發生了 Rebalance 操作。在 Rebalance 之後,所有 Consumer 從上一次提交的位移處繼續消費,但該位移已經是 3 秒前的位移數據了,故在 Rebalance 發生前 3 秒消費的所有數據都要重新再消費一次。雖然你能夠通過減少 auto.commit.interval.ms 的值來提高提交頻率,但這麼做只能縮小重複消費的時間窗口,不可能完全消除它。這是自動提交機制的一個缺陷。

    • 消息丟失: 假設拉取了100條消息,正在處理第50條消息的時候,到達了自動提交窗口期,自動提交線程將拉取到的每個分區的最大消息位移進行提交,如果此時消費服務掛掉,消息並未處理結束,但卻提交了最大位移,下次重啟就從100條那消費,即發生了50-100條的消息丟失。

    手動提交

    當消費配置enable.auto.commit=false的時候代表手動提交位移。用戶必須在適當的時機(一般是處理完業務邏輯后),手動的調用相關api方法提交位移。比如在下面的案例中,我需要確認我的業務邏輯返回true之後再手動提交位移

     while (true) {
         try {
             ConsumerRecords<String, String> consumerRecords = consumer.poll(Duration.ofMinutes(KafkaConfig.pollTimeoutOfMinutes));
             if (!consumerRecords.isEmpty()) {
                 for (ConsumerRecord<String, String> record : consumerRecords) {
                     KafkaMessage kafkaMessage = JSON.parseObject(record.value(), KafkaMessage.class);
                     // 處理業務
                     boolean handleResult = handle(kafkaMessage);
                     if (handleResult) {
                         log.info(" handle success, kafkaMessage={}" ,kafkaMessage);
                     } else {
                         log.info(" handle failed, kafkaMessage={}" ,kafkaMessage);
                     }
                 }
                 // 手動提交offset
                 consumer.commitSync(Duration.ofMinutes(KafkaConfig.pollTimeoutOfMinutes));
            
             } 
         } catch (Exception e) {
             log.info("kafka consume error." ,e);
         }
     }
    

    手動提交明顯能解決消息丟失的問題,因為你是處理完業務邏輯后再提交的,假如此時消費服務掛掉,消息並未處理結束,那麼重啟的時候還會重新消費。

    但是對於業務層面的失敗導致消息未消費成功,是無法處理的。因為業務層的邏輯千變萬化、比如格式不正確,你叫Kafka消費端程序怎麼去處理?應該要業務層面自己處理,記錄失敗日誌做好監控等。

    但是手動提交不能解決消息重複的問題,也很好理解,假如消費0-100條消息,50條時掛了,重啟後由於沒有提交這一批消息的offset,是會從0開始重新消費。至於如何避免重複消費的問題,在這篇文章有說。

    手動提交又分為異步提交和同步提交。

    同步提交

    上面案例代碼使用的是commitSync() ,顧名思義,是同步提交位移的方法。同步提交位移Consumer 程序會處於阻塞狀態,等待 Broker 返回提交結果。同步模式下提交失敗的時候一直嘗試提交,直到遇到無法重試的情況下才會結束。在任何系統中,因為程序而非資源限制而導致的阻塞都可能是系統的瓶頸,會影響整個應用程序的 TPS。當然,你可以選擇拉長提交間隔,但這樣做的後果是 Consumer 的提交頻率下降,在下次 Consumer 重啟回來后,會有更多的消息被重新消費。因此,為了解決這些不足,kafka還提供了異步提交方法。

    異步提交

    異步提交會立即返回,不會阻塞,因此不會影響 Consumer 應用的 TPS。由於它是異步的,Kafka 提供了回調函數,供你實現提交之後的邏輯,比如記錄日誌或處理異常等。下面這段代碼展示了調用 commitAsync() 的方法

     consumer.commitAsync((offsets, exception) -> {
     if (exception != null)
         handleException(exception);
     });
    

    但是異步提交會有一個問題,那就是它沒有重試機制,不過一般情況下,針對偶爾出現的提交失敗,不進行重試不會有太大問題,因為如果提交失敗是因為臨時問題導致的,那麼後續的提交總會有成功的。所以消息也是不會丟失和重複消費的。
    但如果這是發生在關閉消費者或再均衡前的最後一次提交,就要確保能夠提交成功。因此,組合使用commitAsync()commitSync()是最佳的方式。

    try {
        while (true) {
            ConsumerRecords<String, String> consumerRecords = consumer.poll(Duration.ofMinutes(KafkaConfig.pollTimeoutOfMinutes));
            if (!consumerRecords.isEmpty()) {
                 for (ConsumerRecord<String, String> record : consumerRecords) {
                    KafkaMessage kafkaMessage = JSON.parseObject(record.value(), KafkaMessage.class);
                    boolean handleResult = handle(kafkaMessage);             
                 }
                 //異步提交位移               
                 consumer.commitAsync((offsets, exception) -> {
                 if (exception != null)
                     handleException(exception);
                 });
               
            }
        }
    } catch (Exception e) {
        System.out.println("kafka consumer error:" + e.toString());
    } finally {
        try {
            //最後同步提交位移
            consumer.commitSync();
        } finally {
            consumer.close();
        }
    }
    

    讓位移提交更加靈活和可控

    如果細心的閱讀了上面所有demo的代碼,那麼你會發現這樣幾個問題:

    1、所有的提交,都是提交 poll 方法返回的所有消息的位移,poll 方法一次返回1000 條消息,則一次性地將這 1000 條消息的位移一併提交。可這樣一旦中間出現問題,位移沒有提交,下次會重新消費已經處理成功的數據。所以我想做到細粒度控制,比如每次提交100條,該怎麼辦?

    答:可以通過commitSync(Map<TopicPartition, OffsetAndMetadata>)commitAsync(Map<TopicPartition, OffsetAndMetadata>)對位移進行精確控制。

    2、poll和commit方法對於普通的開發人員而言是一個黑盒,無法精確地掌控其消費的具體位置。我都不知道這次的提交,是針對哪個partition,提交上去的offset是多少。

    答:可以通過record.topic()獲取topic信息, record.partition()獲取分區信息,record.offset() + 1獲取消費位移,記住消費位移是指示下一條消費的位移,所以要加一。

    3、我想自己管理offset怎麼辦?一方面更加保險,一方面下次重啟之後可以精準的從數據庫讀取最後的offset就不存在丟失和重複消費了。
    答:可以將消費位移保存在數據庫中。消費端程序使用comsumer.seek方法指定從某個位移開始消費。

    綜合以上幾個可優化點,並結合全文,可以給出一個比較完美且完整的demo:聯合異步提交和同步提交,對處理過程中所有的異常都進行了處理。細粒度的控制了消費位移的提交,並且保守的將消費位移記錄到了數據庫中,重新啟動消費端程序的時候會從數據庫讀取位移。這也是我們消費端程序位移提交的最佳實踐方案。你只要繼承這個抽象類,實現你具體的業務邏輯就可以了。

    public abstract class PrefectCosumer {
        private Map<TopicPartition, OffsetAndMetadata> offsets = new HashMap<>();
        int count = 0;
        public final void consume() {
            Properties properties = PropertiesConfig.getConsumerProperties();
            properties.put("group.id", getGroupId());
            Consumer<String, String> consumer = new KafkaConsumer<>(properties);
            consumer.subscribe(getTopics());
            consumer.poll(0);
            // 把offset記錄到數據庫中 從指定的offset處消費 
            consumer.partitionsFor(getTopics()).stream().map(info ->
            new TopicPartition(getTopics(), info.partition()))
            .forEach(tp -> {
                   consumer.seek(tp, JdbcUtils.queryOffset().get(tp.partition()));   
             });
            try {
                while (true) {
                    ConsumerRecords<String, String> consumerRecords = consumer.poll(Duration.ofMinutes(KafkaConfig.pollTimeoutOfMinutes));
                    if (!consumerRecords.isEmpty()) {
                        for (ConsumerRecord<String, String> record : consumerRecords) {
    
                            KafkaMessage kafkaMessage = JSON.parseObject(record.value(), KafkaMessage.class);
                            boolean handleResult = handle(kafkaMessage);
                            if (handleResult) {
                                //注意:提交的是下一條消息的位移。所以OffsetAndMetadata 對象時,必須使用當前消息位移加 1。
                                offsets.put(new TopicPartition(record.topic(), record.partition()),
                                        new OffsetAndMetadata(record.offset() + 1));
    
                                // 細粒度控制提交 每10條提交一次offset
                                if (count % 10 == 0) {
                                    // 異步提交offset
                                    consumer.commitAsync(offsets, (offsets, exception) -> {
                                        if (exception != null) {
                                            handleException(exception);
                                        }
                                        // 將消費位移再記錄一份到數據庫中
                                        offsets.forEach((k, v) -> {
                                            String s = "insert into kafka_offset(`topic`,`group_id`,`partition_id`,`offset`) values" +
                                                    " ('" + k.topic() + "','" + getGroupId() + "'," + k.partition() + "," + v.offset() + ")" +
                                                    " on duplicate key update offset=values(offset);";
                                            JdbcUtils.insertTable(s);
                                        });
    
    
                                    });
                                }
                                count++;
                            } else {         
                                System.out.println("消費消息失敗 kafkaMessage={}" + getTopics() + getGroupId() + kafkaMessage.toString());                         
                            }
                        }
    
    
                    }
                }
            } catch (Exception e) {
                System.out.println("kafka consumer error:" + e.toString());
            } finally {
                try {
                    // 最後一次提交 使用同步提交offset
                    consumer.commitSync();
                } finally {
                    consumer.close();
                }
    
    
            }
        }
    
    
        /**
         * 具體的業務邏輯
         *
         * @param kafkaMessage
         * @return
         */
        public abstract boolean handle(KafkaMessage kafkaMessage);
    
        public abstract List<String> getTopics();
    
        public abstract String getGroupId();
    
        void handleException(Exception e) {
            //異常處理
        }
    }
    

    控制位移提交的N種方式

    剛剛我們說自己控制位移,使用seek方法可以指定offset消費。那到底怎麼控制位移?怎麼重設消費組位移?seek是什麼?現在就來仔細說說。

    並不是所有的消息隊列都可以重設消費者組位移達到重新消費的目的。比如傳統的RabbitMq,它們處理消息是一次性的,即一旦消息被成功消費,就會被刪除。而Kafka消費消息是可以重演的,因為它是基於日誌結構(log-based)的消息引擎,消費者在消費消息時,僅僅是從磁盤文件上讀取數據而已,所以消費者不會刪除消息數據。同時,由於位移數據是由消費者控制的,因此它能夠很容易地修改位移的值,實現重複消費歷史數據的功能。

    了解如何重設位移是很重要的。假設這麼一個場景,我已經消費了1000條消息后,我發現處理邏輯錯了,所以我需要重新消費一下,可是位移已經提交了,我到底該怎麼重新消費這1000條呢??假設我想從某個時間點開始消費,我又該如何處理呢?

    首先說個誤區:auto.offset.reset=earliest/latest這個參數大家都很熟悉,但是初學者很容易誤會它。大部分朋友都覺得在任何情況下把這兩個值設置為earliest或者latest ,消費者就可以從最早或者最新的offset開始消費,但實際上並不是那麼回事,他們生效都有一個前提條件,那就是對於同一個groupid的消費者,如果這個topic某個分區有已經提交的offset,那麼無論是把auto.offset.reset=earliest還是latest,都將失效,消費者會從已經提交的offset開始消費。因此這個參數並不能解決用戶想重設消費位移的需求。

    kafka有七種控制消費組消費offset的策略,主要分為位移維度和時間維度,包括:

    • 位移維度。這是指根據位移值來重設。也就是說,直接把消費者的位移值重設成我們給定的位移值。包括Earliest/Latest/Current/Specified-Offset/Shift-By-N策略

    • 時間維度。我們可以給定一個時間,讓消費者把位移調整成大於該時間的最小位移;也可以給出一段時間間隔,比如 30 分鐘前,然後讓消費者直接將位移調回 30 分鐘之前的位移值。包括DateTime和Duration策略

    說完了重設策略,我們就來看一下具體應該如何實現,可以從兩個角度,API方式和命令行方式。

    重設位移的方法之API方式

    API方式只要記住用seek方法就可以了,包括seek,seekToBeginning 和 seekToEnd。

    void seek(TopicPartition partition, long offset);    
    void seek(TopicPartition partition, OffsetAndMetadata offsetAndMetadata);    
    void seekToBeginning(Collection<TopicPartition> partitions);    
    void seekToEnd(Collection<TopicPartition> partitions);    
    

    從方法簽名我們可以看出seekToBeginningseekToEnd是可以一次性重設n個分區的位移,而seek 只允許重設指定分區的位移,即為每個分區都單獨設置位移,因為不難得出,如果要自定義每個分區的位移值則用seek,如果希望kafka幫你批量重設所有分區位移,比如從最新數據消費或者從最早數據消費,那麼用seekToEnd和seekToBeginning。

    Earliest 策略:從最早的數據開始消費

    從主題當前最早位移處開始消費,這個最早位移不一定就是 0 ,因為很久遠的消息會被 Kafka 自動刪除,主要取決於你的刪除配置。

    代碼如下:

    Properties properties = PropertiesConfig.getConsumerProperties();
    properties.put("group.id", getGroupId());
    Consumer<String, String> consumer = new KafkaConsumer<>(properties);
    consumer.subscribe(getTopics());
    consumer.poll(0);
    consumer.seekToBeginning(
    consumer.partitionsFor(getTopics()).stream().map(partitionInfo ->
       new TopicPartition(getTopics(), partitionInfo.partition()))
       .collect(Collectors.toList()));
    

    首先是構造consumer對象,這樣我們可以通過partitionsFor獲取到分區的信息,然後我們就可以構造出TopicPartition集合,傳給seekToBegining方法。需要注意的一個地方是:需要用consumer.poll(0),而不能用consumer.poll(Duration.ofMillis(0))

    在poll(0)中consumer會一直阻塞直到它成功獲取了所需的元數據信息,之後它才會發起fetch請求去獲取數據。而poll(Duration)會把元數據獲取也計入整個超時時間。由於本例中使用的是0,即瞬時超時,因此consumer根本無法在這麼短的時間內連接上coordinator,所以只能趕在超時前返回一個空集合。

    Latest策略:從最新的數據開始消費

        consumer.seekToEnd(
            consumer.partitionsFor(getTopics().get(0)).stream().map(partitionInfo ->
                new TopicPartition(getTopics().get(0), partitionInfo.partition()))
                  .collect(Collectors.toList()));
    
    

    Current策略:從當前已經提交的offset處消費

    consumer.partitionsFor(getTopics().get(0)).stream().map(info ->
            new TopicPartition(getTopics().get(0), info.partition()))
            .forEach(tp -> {
                long committedOffset = consumer.committed(tp).offset();
                consumer.seek(tp, committedOffset);
            });
    

    **Special-offset策略:從指定的offset處消費 **

    該策略使用的方法和current策略一樣,區別在於,current策略是直接從kafka元信息中讀取中已經提交的offset值,而special策略需要用戶自己為每一個分區指定offset值,我們一般是把offset記錄到數據庫中然後可以從數據庫去讀取這個值

        consumer.partitionsFor(getTopics().get(0)).stream().map(info ->
                    new TopicPartition(getTopics().get(0), info.partition()))
                    .forEach(tp -> {
                        try {
                            consumer.seek(tp, JdbcUtils.queryOffset().get(tp.partition()));
                        } catch (SQLException e) {
                            e.printStackTrace();
                        }
                    });
    
    

    以上演示了用API方式重設位移,演示了四種常見策略的代碼,另外三種沒有演示,一方面是大同小異,另一方面在實際生產中,用API的方式不太可能去做時間維度的重設,而基本都是用命令行方式。

    重設位移的方法之命令行方式

    命令行方式重設位移是通過 kafka-consumer-groups 腳本。比起 API 的方式,用命令行重設位移要簡單得多。

    Earliest 策略指定–to-earliest。

    bin/kafka-consumer-groups.sh --bootstrap-server kafka-host:port --group test-group --reset-offsets --all-topics --to-earliest –execute
    

    Latest 策略指定–to-latest。

    bin/kafka-consumer-groups.sh --bootstrap-server kafka-host:port --group test-group --reset-offsets --all-topics --to-latest --execute
    

    Current 策略指定–to-current。

    bin/kafka-consumer-groups.sh --bootstrap-server kafka-host:port --group test-group --reset-offsets --all-topics --to-current --execute
    

    Specified-Offset 策略指定–to-offset。

    bin/kafka-consumer-groups.sh --bootstrap-server kafka-host:port --group test-group --reset-offsets --all-topics --to-offset <offset> --execute
    

    Shift-By-N 策略指定–shift-by N。

    bin/kafka-consumer-groups.sh --bootstrap-server kafka-host:port --group test-group --reset-offsets --shift-by <offset_N> --execute
    

    DateTime 策略指定–to-datetime。

    DateTime 允許你指定一個時間,然後將位移重置到該時間之後的最早位移處。常見的使用場景是,你想重新消費昨天的數據,那麼你可以使用該策略重設位移到昨天 0 點。

    bin/kafka-consumer-groups.sh --bootstrap-server kafka-host:port --group test-group --reset-offsets --to-datetime 2019-06-20T20:00:00.000 --execute
    

    Duration 策略指定–by-duration。
    Duration 策略則是指給定相對的時間間隔,然後將位移調整到距離當前給定時間間隔的位移處,具體格式是 PnDTnHnMnS。如果你熟悉 Java 8 引入的 Duration 類的話,你應該不會對這個格式感到陌生。它就是一個符合 ISO-8601 規範的 Duration 格式,以字母 P 開頭,後面由 4 部分組成,即 D、H、M 和 S,分別表示天、小時、分鐘和秒。舉個例子,如果你想將位移調回到 15 分鐘前,那麼你就可以指定 PT0H15M0S

    bin/kafka-consumer-groups.sh --bootstrap-server kafka-host:port --group test-group --reset-offsets --by-duration PT0H30M0S --execute
    

    提交的位移都去哪了?

    通過上面那幾部分的內容,我們已經搞懂了位移提交的方方面面,那麼提交的位移它保存在哪裡呢?這就要去位移主題的的世界里一探究竟了。kafka把位移保存在一個叫做__consumer_offsets的內部主題中,叫做位移主題。

    注意:老版本的kafka其實是把位移保存在zookeeper中的,但是zookeeper並不適合這種高頻寫的場景。所以新版本已經是改進了這個方案,直接保存到kafka。畢竟kafka本身就適合高頻寫的場景,並且kafka也可以保證高可用性和高持久性。

    既然它也是主題,那麼離不開分區和副本這兩個機制。我們並沒有手動創建這個主題並且指定,所以是kafka自動創建的, 分區的數量取決於Broker 端參數 offsets.topic.num.partitions,默認是50個分區,而副本參數取決於offsets.topic.replication.factor,默認是3。

    既然也是主題,肯定會有消息,那麼消息格式是什麼呢?參考前面我們手動設計將位移寫入數據庫的方案,我們保存了topic,group_id,partition,offset四個字段。topic,group_id,partition無疑是數據表中的聯合主鍵,而offset是不斷更新的。無疑kafka的位移主題消息也是類似這種設計。key也是那三個字段,而消息體其實很複雜,你可以先簡單理解為就是offset。

    既然也是主題,肯定也會有刪除策略,否則消息會無限膨脹。但是位移主題的刪除策略和其他主題刪除策略又不太一樣。我們知道普通主題的刪除是可以通過配置刪除時間或者大小的。而位移主題的刪除,叫做 Compaction。Kafka 使用Compact 策略來刪除位移主題中的過期消息,對於同一個 Key 的兩條消息 M1 和 M2,如果 M1 的發送時間早於 M2,那麼 M1 就是過期消息。Compact 的過程就是掃描日誌的所有消息,剔除那些過期的消息,然後把剩下的消息整理在一起。

    Kafka 提供了專門的後台線程定期地巡檢待 Compact 的主題,看看是否存在滿足條件的可刪除數據。這個後台線程叫 Log Cleaner。很多實際生產環境中都出現過位移主題無限膨脹佔用過多磁盤空間的問題,如果你的環境中也有這個問題,我建議你去檢查一下 Log Cleaner 線程的狀態,通常都是這個線程掛掉了導致的。

    總結

    kafka的位移是個極其重要的概念,控制着消費進度,也即控制着消費的準確性,完整性,為了保證消息不重複和不丟失。我們最好做到以下幾點:

    • 手動提交位移。

    • 手動提交有異步提交和同步提交兩種方式,既然兩者有利也有弊,那麼我們可以結合起來使用。

    • 細粒度的控制消費位移的提交,這樣可以避免重複消費的問題。

    • 保守的將消費位移再記錄到了數據庫中,重新啟動消費端程序的時候從數據庫讀取位移。

    獲取Kafka全套原創學習資料及思維導圖,關注【胖滾豬學編程】公眾號,回復”kafka”。

    本文來源於公眾號:【胖滾豬學編程】。一枚集顏值與才華於一身,不算聰明卻足夠努力的女程序媛。用漫畫形式讓編程so easy and interesting!求關注!

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

    【其他文章推薦】

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

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

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

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

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

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

  • VSCode + WSL 2 + Ruby環境搭建詳解

    VSCode + WSL 2 + Ruby環境搭建詳解

    vscode配置ruby開發環境

    vscode近年來發展迅速,幾乎在3年之間就搶佔了原來vim、sublime text的很多份額,猶記得在2015-2016年的時候,ruby推薦的開發環境基本上都是vim和sublime text,然而,隨着vscode的發展,vscode下ruby的開發體驗已經非常不錯。現在基本上使用win 10 wsl2 + vscode + windows terminal的體驗已經不遜於mac + vim (sublime) + item 2的體驗了

    總體步驟

    使用win10專業版配置ruby開發環境大致分為以下幾步:

    1. 開啟win10 wsl功能
    2. 升級wsl2
    3. 安裝ubuntu
    4. 安裝ruby(rvm)
    5. 安裝vscode
    6. 安裝vscode wsl擴展
    7. 安裝vscode ruby相關擴展

    經過以上7步就可以開始愉悅的ruby開發了,再開始之前,可以先看個效果圖。

    1. 開啟win10 wsl功能

    ruby對Linux和Mac比較友好,在windows下很多第三方庫要配合mingw或msys2才能安裝,不過好在windows 10提供了Linux子系統,在win10 2004版本中wsl也升級到了wsl2,速度更快,功能更完善。

    要使用wsl2需要先在控制面板中開啟wsl功能:

    • 適用於Linux的Windows子系統
    • 虛擬機平台

    2. 升級wsl2

    目前wsl2還需要安裝一個內核升級包,具體可參考微軟說明:

    • wsl2安裝說明
    • wsl2 update包

    更新包安裝完成后,輸入命令

    wsl --set-default-version 2
    

    3. 安裝Ubuntu

    在微軟應用商店安裝Ubuntu,當前Ubuntu版本為20.04 LTS

    安裝完成以後,配置Ubuntu默認為wsl2

    # 查看
    wsl --list --verbose
    
    # 設置
    wsl --set-version Ubuntu 2
    

    4. 安裝ruby

    在Linux下安裝ruby有多種方法,比較主流的方法是RVM,不過為了簡單起見,我直接通過ubuntu的apt工具進行了安裝。

    關於RVM的安裝可參考如下網站:

    • RVM官網
    • RVM實用指南

    通過APT安裝,輸入下列命令即可

    sudo apt install ruby ruby-dev ri ruby-bundle
    

    安裝完成以後需要配置gem國內鏡像,參考如下網址:

    • gem中文鏡像

    輸入下列命令

    # 設置gem source
    gem sources --add https://gems.ruby-china.com/ --remove https://rubygems.org/
    
    # 查看gem source
    gem sources -l
    
    # 設置bundle
    bundle config mirror.https://rubygems.org https://gems.ruby-china.com
    

    5. 安裝vscode

    vscode直接在官網下載安裝即可,這裏我選擇了System Installer

    • vscode官網下載頁面

    6. 安裝vscode wsl擴展

    vscode安裝完成以後,可以在plugin中找到Remote – WSL擴展,點擊安裝即可

    7. 安裝vscode ruby相關擴展

    直接在plugin中搜索ruby在wsl中安裝下列五個擴展即可

    • Peng Lv/Ruby
    • Castwide/Ruby Solargraph(Language Server)
    • misogi/ruby-rubocop(Lint)
    • Simple Ruby ERB
    • endwise

    其中,ruby solargraphrubocop除了安裝擴展,還需要通過gem安裝第三方包

    sudo gem install rubocop
    sudo gem install solargraph
    

    重新加載vscode-wsl就可以愉快的使用ruby language進行開發了

    vscode使用

    在使用上基本只要require了相應的庫,就solargraph就會對require的庫中涉及的類和模塊進行提示,非常方便。唯一有問題的地方就是require的時候沒有提示,這可能就需要自己記一下庫的名稱,不過相比於原來已經好太多了,應該說在可以接受的範圍內。

    1. 如果安裝了新的第三方庫會提示嗎?

    如果安裝了sinatra這樣的庫,vscode-ruby如何給出提示呢?只需要Ctrl + Shift + P,選擇solargraph: build new gem documention即可

    2. rubocop如何使用?

    rubocop是一個Ruby Lint工具,可以進行Ruby代碼風格檢查,並能夠自動修復,只需要Ctrl + Shift + P,選擇Ruby: autocorrect by rubocop即可

    3. 常用類型註釋

    ruby是動態強類型語言,由於不需要指定函數返回值類型,這導致IDE無法自動推斷一些變量的類型。目前Python、PHP、TypeScript都在不斷的強化類型以方便IDE進行靜態檢查。IDE只有在知道類型的情況下才能準確地進行智能提示。

    在ruby 2當中,我們可以通過類型註釋的方式增強IDE推斷能力。常見的類型註釋可參考YARD項目

    下面代碼給出了一些示例。

    require 'socket'
    
    server = TCPServer.new 2000
    loop do
      # 代碼塊參數類型註釋
      # @param {TCPSocket} client
      Thread.start(server.accept) do |client|
        client.puts 'hello !'
        client.puts "Time is #{Time.now}"
        client.close
      end
    end
    
    server = TCPServer.new 2000
    loop do
      # 變量註釋
      # @type {TCPSocket} client
      client = server.accept
    end
    
    # 函數參數和返回值註釋,數組類型
    # @param {Array(Integer)} nums
    # @param {Integer} target
    # @return {Array(Integer)}
    def two_sum(nums, target)
      hash_nums = {}
      result = []
      nums.each_with_index do |num, index|
        hash_nums[num] = index
      end
    
      nums.each_with_index do |num, index|
        another = target - num
        if hash_nums[another] && hash_nums[another] != index
          result.push(index, hash_nums[another])
          break
        end
      end
    
      result
    end
    

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

    【其他文章推薦】

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

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

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

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

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

  • SpringMVC-攔截器

    SpringMVC-攔截器

    概述

    Java 里的攔截器是動態攔截 action 調用的對象。

    可以在Controller 中的方法執行之前與執行之後,及頁面显示完畢后,執行指定的方法,自定義的攔截器必須實現HandlerInterceptor 接口。

    方法介紹

    preHandle

    在業務處理器處理請求之前被調用

    postHandle

    在業務處理器處理完請求后

    afterCompletion

    在 DispatcherServlet 完全處理完請求后被調用

    SpringMVC攔截器使用

    攔截所有請求

    創建一個類實現 HandlerInterceptor 接口

     

     配置文件當中添加攔截器

     

     內部源碼分析

     

     

     

     

    攔截指定請求

     

     

    配置多個攔截器執行順序

     

     

     

     

     

     

     

    第 2 個返回 false

     

     

     

     

     

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

    【其他文章推薦】

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

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

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

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

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

  • salesforce零基礎學習(九十八)Type淺談

    salesforce零基礎學習(九十八)Type淺談

    在Salesforce的世界,凡事皆Metadata。

    先通過一句經常使用的代碼帶入一下:

    Account accountItem = (Account)JSON.deserialize(accountString,Account.class);

    這種代碼相信大部分開發都會寫過,前台將數據序列化,通過字符串的形參傳遞給後台,後台將數據進行反序列化,從而獲取到這個表或者這個自定義類的實例。所以問題來了,為啥第二個參數是 Account.class?我們通過官方的API描述可能更好的進行了解。

     這裏我們引出了 Type的概念,他是apex 預定的類型,包括 基礎數據類型(Integer等) , 集合, sObject類型以及 用戶定義的類。基礎數據類型等等都是 object類型,所以當我們理解salesforce裏面的類型時,可以簡單的分成兩個大類:Object & sObject。所以Type概念引入完了,它用來幹嘛?怎麼聲明?什麼時候用呢?

    Type t1 = Integer.class;
    Type t2 = Type.forName('Integer');
    system.debug(t1.equals(t2));

    上面的簡單的demo中提供了兩種聲明Type的方式,一種是根據 object | sObject使用 .class聲明,另外一種是使用 Type的方法forName來實例化變量。既然變量可以聲明出來,我們就可以看看他的方法考慮如何搞事情了。

     Type的方法非常少,所以我們想要查看其對應的方法描述以及使用很容易就看完。這裏針對幾個重要的方法進行描述:

    • forName(fullyQualifiedName):返回與指定的完全限定的類名相對應的類型。這裏的類名包括salesforce系統預製的類,自定義的類以及sObject名稱;
    • isAssignableFrom(sourceType):如果object指定類型的引用可以從子類型分配,則返回true,否則返回false。這個方法我們可能首先會先想到 instanceof,因為都是來判斷類型是否相兼容匹配,但是 instanceof必須是初始化這個類或者對象的變量,才能使用 instanceof 來進行判斷,使用 此方法可以省去了靜態編譯時的依賴,所以如果想判斷依賴性,可以優先考慮此方法。
    • newInstance():此方法用來實例化一個指定Type的對象,返回類型是一個object對象,我們可以強制轉換成我們需要的對象類型。因為apex也是面向對象的語言,封裝,繼承,多態三大特性,我們可以通過 newInstance實現父子類型的輕鬆轉換調用相關的方法從而實現動態配置。

    基礎信息介紹完畢,此篇淺入淺出,介紹兩種可能用到的場景。

    1.  JSON序列化與反序列化

    這個我們經常使用,一筆帶過:通過字符串以及指定的 Type類型可以轉換成指定的數據類型。

    Account accountItem = (Account)JSON.deserialize(accountString,Account.class);

    2. 針對Custom Setting等根據配置的動態的類調用動態方法的使用

    ParentClass是一個父類,有兩個變量以及一個虛擬的方法,繼承的子類可以進行實現

    public abstract class ParentClass {
        public String param1 { get; set; }
        public String param2 { get; set; }
    
        public ParentClass() {
            this.param1 = 'value1';
            this.param2 = 'value2';
        }
    
        public virtual String joinParam() {
            return param1 + param2;
        }
    }

    SonClass1繼承了它並實現了它相關的方法

    public class SonClass1 extends ParentClass {
        public SonClass1() {
            super();
            this.param1 = 'son value1';
            this.param2 = 'son value2';
        }
    
        public override String joinParam() {
            return super.joinParam();
        }
    }

    還有其他的SonClassn繼承了它並實現了它的相關的方法,我們在custom setting中配置了不同的場景應該調用的不同的子類,公共方法中,我們只需要使用以下的代碼來判斷和調用即可。

    public String executeJoin(String className) {
        Type t = Type.forName(className);
        Type t2 = Type.forName('ParentClass');
        if(!t2.isAssignableFrom(t)) {
            throw new CustomException('should be son class of ParentClass');
        }
        ParentClass newObj = (ParentClass)t.newInstance();
        return newObj.joinParam();
    }

    總結:篇中簡單的介紹了salesforce中的 Type的使用,拋磚引玉,想要深入了解還要自行查看官方文檔。篇中有錯誤的地方歡迎指出,有不懂的歡迎留言。

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

    【其他文章推薦】

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

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

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

    ※超省錢租車方案

    FB行銷專家,教你從零開始的技巧