分類: 3C資訊

  • 極*Java速成教程 – (1)

    序言

    眾所周知,程序員需要快速學習新知識,所以就有了《21天精通C++》和《MySQL-從刪庫到跑路》這樣的書籍,Java作為更“高級”的語言也不應該落後,所以我就寫一個《極·Java速成教程》,讓想學的人能夠快速學習Java(手斜)。
    本文大概分三大部分:

    • 編程基礎知識
      • 面向對象思想
      • 數據類型
      • 運算符
      • 流程控制
    • Java語言基礎
    • Java高級特性

    本文請逐字理解文本內容,本文僅供理解和學習Java編程,不可作為概念性參考

    本文是閱讀《Java編程思想》后的讀後感
    本文着重於理解某種思想,所以些許描述顯得晦澀,意會即可

    面向對象思想

    何為面向對象

    Java是一個從設計之初就遵循面向對象思想的語言,因此而有着諸多面向對象的特性,那麼問題來了,面向對象指的是什麼?
    面向對象,不是指的臉超女朋友,而是說編程的時候,你所操作的一切事物都是“對象”,假如說Java程序員是個搬磚工,那Java操作的對象就是搬磚工手裡的磚,一塊磚就是一個對象,一堆轉就是一堆對象,諸多同樣的對象可以用一個概念來概括,這個概念就是“”。
    先在我們可以思考磚是個什麼東西,也就是磚的“概念(類)”:磚,由土燒制而成,可以用來砌牆、鋪地和墊桌角,有一定的重量和體積,能被搬被砌。仔細分析這句定義,可以發現能分為4小句:

    • 磚是由土燒制而成。也就是說,一個類都是從另一個或多個類繼承發展而來,也就是“繼承(extend)和組合自另一個或多個類”,在Java中,所有類都繼承和擴展自Object類,在自己寫的程序中,一個類可以繼承和擴展自官方的類,別人寫的類和自己寫的類。
    • 磚可以用來砌牆、鋪地和墊桌角。也就是同一個類可以根據不同的需求去加工處理,修改為不同的“形狀”,添加不同的屬性,在不同的地方起到不同的作用。
    • 磚由一定的重量和體積。也就是說每個類都有其屬性,但是對於同一種屬性,他們的值未必相同,正如每個磚的重量和體積都可以不同。
    • 磚能被搬被砌。也就是說每個類都有其功能,屬性是一個類靜態的描述,而功能(或者說方法)則是一個類動態的描述。

    將許多個“磚”一樣的對象搭配組合,堆壘到一起,就可以構建起程序的這棟房屋,實現整體的功能。

    Java中類的幾個特點

    Java中的類有幾個特點

    1. 單根繼承,也就是說某一個類只能直接繼承自另一個類,就像磚只能由土燒制而成,即土→磚,然而除了土,磚的燒制過程中還需要加其他的配料比如外面要燒上去一層琉璃,那就得在磚本身的屬性以上添加一個琉璃的屬性,琉璃的屬性是額外的,獨立無關的,但是他們組合起來實現了更多的功能。也就是說除了直接繼承,還可以添加其他類到一個類中,這種方法叫通過接口(interface)進行繼承。
    2. 訪問控制。對於一個類來說,你只能看到一個類想讓你看到的部分。就像一個磚,你只能看到磚的表面,不能看到它的內里,除非敲開他,但是在Java中,這是不被允許的。一個類想讓你看到的部分和不想讓你看到的部分由不同的關鍵詞進行控制:
      • public 對於所有對象都公開的內容
      • private 只有自己可見的內容
      • protected 只有自己和自己的子孫可以看到的內容
      • default 只有自己的親戚(同一個包)可以看到的內容
        我們不用關心他們內部什麼樣子,我們只需要關心我們需要的,暴露在外面的東西,以及對象能正常工作就行。
    3. “像”就是“是”。對於一個類來說,如果長得像一個東西,那就可以看作是一個東西。就像兒子像父親,父親能幹的兒子也能幹一樣,一塊防火轉是一個磚,那他也有磚的所有屬性,也能被壘起來。而一個具有壘起來方法的對象看上去能壘起來,那他就能像磚一樣被壘起來。
    4. static屬性。對於一個類來說,有的屬性和方法可以是static也就是靜態的,這種屬性和方法不是描述一個類的對象的,而是描述一個類的。就比如可以對所有的磚執行統計數量操作,對於一個磚頭讓他統計同類的數量是不恰當的,但是對於一堆轉,讓這堆磚告訴你有多少塊是可行的。
    5. 對象生命周期管理。Java語言的使用者不用關心Java對象是怎麼誕生的,怎麼消亡的,只需要在需要時new一下,創建一個對象即可,使用完以後的垃圾丟到哪裡,那是Java高級特性需要研究的東西。
    6. Java中的類之上具有包(package)的結構,包是具有層次的,如java.lang.Math就表示java下的lang包下的Math類,這個層次結構可以有好多層,一層一層深入,所以可能這個名字會很長。在使用某一個包中的某一個類的某些內容時,需要使用import進行導入,如果需要導入某個包下的全部,可以使用*通配符。,如java.lang.*。

    Java編程需要關心的點

    只要想好這個類和它的對象應該怎麼設計,怎麼正常運行即可,其他語言的包袱,甩掉即可

    數據類型

    一切都是對象,而根基只需要幾個

    當Java使用者需要一個對象時,這個對象在內存中的位置時我們不用管也沒法管的。但這個對象是怎麼來的,卻是可以思考的。就像房子是磚壘成的,磚是由土燒成的,那土是什麼構成的呢,這麼追究下去,追究到最後就成了重子和輕子了,但是我們不必追求這麼細,就像Java中我們知道一切對象都有Object這個“祖宗”,但是我們不需要凡事都請動這個祖宗,它有幾個子女是我們常用的,也就是“基本類型”:

    類型 大小 包裝器 初始值
    boolean true/false Boolean false
    char 16 bit Character ‘\u0000’
    byte 8 bit Byte (byte)0
    short 16 bit Short (short)0
    int 32 bit Integer 0
    long 64 bit Long 0L
    float 32 bit Float 0.0f
    double 64 bit Double 0.0d
    void Void void

    這些基本類型的大小都與其位數n有關,最小值為-2n-1,最大值為2n-1-1,float和double遵循IEEE754標準。
    除了基本類型,還有字符串String,高精度整數BigInteger和高精度浮點數BigDecimal等常見類。

    還有一些方便的類型可以使用

    String

    字符串是一個常用的類型,可以用來儲存一個字符序列。字符串類型在內存中是一個不可變對象,也就是說當一個String s="ABC"字符串在內存中創建后,它就不可以被修改了,如果執行了s+="DEF"的操作,那這個s引用實際上是指向了一個新的”ABCDEF”對象,它與原來的”ABC”對象已經沒有關係了。
    字符串變量的比較是不可以直接用==判斷的。==是用來進行引用對比的運算符,而兩個內容一樣的字符串引用未必在內存中指向同一個對象。因此,String對象的比較要使用equals()方法進行。

    enum

    enum是枚舉對象,是一個有限的可以n選1的對象的集合,可以在switch語句內使用。形如:

    public enum Spiciness{
        NOT,MILD,MEDIUM,HOT,FLAMING
    }

    枚舉類型的實例都是常量,可以使用枚舉類型名.枚舉常量的方法選擇常量的值。它內部創建了toString()方法,所以可以很方便地打印枚舉常量的值,還提供了ordinal()方法,可以獲取某個枚舉常量在枚舉類型中的順序。以及靜態的 values()方法,可以按照枚舉常量的聲明順序獲取由這些常量值構成的數組。

    對象應當被管理

    一個對象,給他個稱號,就可以根據這個名字喊他做各種事,就像家裡人可以喊你小名讓你幹活一樣。但是當出了家裡,可能有好多人跟你重名,這會帶來困擾,因此是不被允許的,而在外面叫你的小名,這也會帶來尷尬,也就是說,這個名字超出了作用域,在Java中,一個稱號(或者說引用)的作用域是從這個命名的聲明到所在花括號的結尾。當然如果不關心這一點也沒問題,因為IDE會提醒你這裏出了問題。
    當一個對象有了它的稱呼以後,就可以安排他做好多的事情了,無論誰喊,只要是喊到,就可以用。在作用域範圍內的所有調用都可以通過這個名字實行,而你也可以再給這個對象起另一個名字,這多個名字都是指的同一個對象,當處理不當時,這會帶來一些問題,比如一個喊你張三的人給你圖了個花臉,別人喊你李四把你喊過來以後也會看到一個花臉。
    對於一堆相同類型的對象,可以用一個容器裝住他們,方便我們管理和使用,Java中提供了多種類型的容器,有最基本的數組,也就是一串連續的空間去裝一樣的對象,還有其他的諸如set和map之類的容器可以裝,這一點可以在後面進行詳細討論。

    運算符

    運算符按照字面語義理解就好

    運算符有其優先級,也有短路的特徵。優先級就是說先乘除后加減,短路就是邏輯運算時當直接可以產生結果時就不再繼續運算。
    運算符有以下幾個:
    +。加號,数字的累加,字符串的連接,將較小類型的数字提升為int類型。
    -。減號,数字相減。負號。
    *。乘號,数字相乘。
    /。除號,数字相除,給出結果的整數部分,直接截斷小數,不會四舍五入。
    %。求余,数字相除求餘數。
    ++自增,–自減,當出現在對象前面時先運算再輸出值,當出現在對象後面時先輸出值再運算。
    >,<,>=,<=。大於,小於,大於等於,小於等於。顧名思義。
    ==判斷相等,對於Java來說,這是容易使人困擾的一點,因為除了在判斷基本類型對象是否相等時以外,==僅僅為判斷別名所指向的對象是否為同一個,而不關心對象的屬性是否相等。如果要判斷String等官方封裝好的類型,就要用到equals()方法,但是對於自己定義的類來說,如果沒有重新定義equals()方法,就會和==一樣,對引用對象進行判斷,而不是對對象的屬性進行判斷。
    &&,||,!。邏輯運算符與或非,對boolean值進行判斷,兩邊都true時&&結果為真,兩邊有一個為true時||結果為真,!是對boolean對象的邏輯值進行顛倒。
    &,|,^,~。按位操作符,對数字類型進行按位的與,或,異或和非操作,與,或,異或是對兩個對象進行操作,非是對一個對象進行操作。
    <<,>>,>>>。移位操作符,就是將操作數按位進行移位,A※※B就是將A往箭頭方向移B位。如果是負數,>>在左側補1,>>>在左側補0。
    =。数字賦值,或者是給一個對象起別名,或者是將另一個別名添加到一個對象上。
    +=,-=,*=,/=,&=,|=,^=,>>=,>>>=,<<=。比較類似,a※=b就是a=a※b。當>>>=byte和short時,轉化成int進行處理然後截斷,可能產生不正確的結果。
    指數計數法:AeB代表了Ax10B,如1.39e-43f代表float的1.39×10-43
    ?:。這是個三元運算符,如A?B:C就表示如果A的邏輯結果為真,就執行B,否則就執行C。
    直接常量,就是直接的數,如200D,0x200。其規則是如果後面跟D表示double,F表示float,L表示long,前面跟0表示8進制數,0x表示16進制數。

    操作符使用時容易出現的問題

    類型轉換

    (A)B可以將B強制轉化為A類型,如果是從較小的類型轉化為較大的類型,將不會帶來問題,但如果是從較大的類型轉化為較小的類型,則會帶來信息丟失的問題。自定義類的轉換同理。

    截尾和舍入

    對於浮點數來說,強制轉化為整形時,會捨棄小數部分,如果要獲取捨入的結果,需要使用java.lang.Math中的round()方法。

    流程控制

    Java是一種面向對象的語言,但其執行過程依舊時順序的,在順序執行的程序中,會進行一些選擇分支和循環操作。

    選擇

    Java中可以使用if-else和switch進行分支選擇。

    if-else

    形如

    if(A){
        B
    }else{
        C
    }

    就是如果判斷條件A為真,則執行B,否則執行C。

    switch

    形如

    switch(A){
        case 1: B;
        case 2: C;break;
        case 3: D;break;
        case 4: E;break;
    ···
    }

    即如果判斷條件A為1,則執行B然後執行C然後中斷該代碼塊,如果判斷條件為2則執行C然後中斷該代碼塊,如果判斷條件為3則執行D然後中斷該代碼塊,等等。

    循環

    循環有3種形式:while,do-while和for。

    while

    形如

    while(A){
        B
    }
    對判斷條件A進行判斷,當判斷條件為A時,多次執行B代碼。

    do-while

    形如

    do{
        B
    }while(A)

    先執行一次B代碼,然後對判斷條件A進行判斷,當判斷條件A為真時,循環至花括號頭部執行B代碼。

    for

    形如

    for(int i=0,j=0;i<100;i++,j++){
        B
    }

    首先對第一個;前面的部分執行,常見於循環控制對象進行初始化,多個對象可以使用,進行區分,然後對;間的循環控制條件進行判斷,如果條件為真則執行下方代碼塊,最後對;后的第三部分執行,比如對循環對象進行自增操作等。實例中就是先對i和j進行初始化為0,然後判斷i是否小於100,如果為真則執行B代碼,最後執行i自增和j自增,然後判斷i是否小於100,如果為真繼續循環,該代碼最後將執行B代碼段100次。
    在編程時,循環常在數組等可迭代對象上進行。可迭代就是可以從前到后順序走一遍。對此,Java提供了一些便利的語法方便開發。形如

    容器/數組 A;
    for(int i:A){
        B
    }

    就是說對於容器或者數組A,迭代遍歷一遍,i會在每次循環中引用到A中的每個對象,然後執行B代碼塊。假如容器/數組A中有100個對象,那B就會被執行100次。

    返回

    return

    結束該語句所在函數,返回返回值。
    返回值類型由函數定義決定,如果不匹配且無法兼容則會IDE報錯。

    Java的與眾不同

    Java的流程控制中沒有goto語句,但是通過break和continue語句,實現了從深層循環中跳出的功能。通過在循環前標記label:,然後使用break labelcontinue label,即可跳出循環到指定位置或者是跳出本次循環到指定位置。

    label1:
    while(true){
        label2:
        while(true){
            continue label1;//結束本次循環,跳轉到外循環繼續循環
            break label1://結束外層循環
        }
    }

    感慨

    Java的基本語法,與其他的語言有相似之處,也有不同之處,但編程本來就是一種實踐的技能,紙上得來終覺淺,絕知代碼要手擼,還是實打實的寫代碼才可以將知識轉化為能力。以及,文章真難寫。

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

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

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

    ※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

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

    ※專營大陸快遞台灣服務

    台灣快遞大陸的貨運公司有哪些呢?

  • 前端每周學習分享–第12期

    1.VuePress

    大家看過不少Vue.js及其子項目的文檔,一定發現了它們風格完全一致,界面清爽,讀起來很舒服,它們都使用了vuepress。

    VuePress是尤大為了支持 Vue 及其子項目的文檔需求而寫的一個靜態網站生成工具,廣泛用於編寫技術文檔 ,可以部署在github上做個人博客。

    原理:

    在構建過程中,會創建應用程序的服務器渲染版本,通過訪問每個路由,來渲染相應的 HTML。

    其中, 每個 markdown 文件都使用 編譯為 HTML,然後作為 Vue 組件的模板進行處理。這允許你直接在 markdown 文件中使用 Vue,在需要嵌入動態內容時,這種使用方式非常有用。

    十分實用的特性:

    • md文件可內嵌vue代碼
    • 可自定義主題
    • 利用service worker做離線緩存
    • 多語言支持
    • 基於git的最近更新

    官方文檔:

    快速搭建:

    2.WebWorker

    web worker是運行在後台的jacvascript,利用類似線程的消息傳遞實現并行,獨立於其他腳本,不會影響頁面的性能。

    web worker能夠長時間運行,有理想的啟動性能以及理想的內存消耗。

    worker 創建后,它可以向它的創建者指定的事件監聽函數傳遞消息,這樣該worker生成的所有任務都會接收到這些消息。

    webworker有專用線程dedicated worker(單窗口專用),sharedWorker(可多窗口共享),以及後來的service worker(目前瀏覽器支持程度還不高)。

    2.1.dedicated worker

    使用方法:

    worker線程里監聽onmessage,

    頁面線程里創建worker對象:const myworker = new Worker("worker.js")

    發送消息:postMessage(msg)

    接受消息:onmessage = function(e){const msg = e.data}

    msg的數據格式自行定義。

    終止worker:myworker.terminate()

    例如下面的示例,worker會接收頁面上輸入的兩個数字,計算出乘積后返回結果。

    worker.js

    onmessage = function(e) {
      console.log('Worker: Message received from main script');
      let result = e.data[0] * e.data[1];
      if (isNaN(result)) {
        postMessage('Please write two numbers');
      } else {
        let workerResult = 'Result: ' + result;
        console.log('Worker: Posting message back to main script');
        postMessage(workerResult);
      }
    }

    index.html里

    const first = document.querySelector('#number1');
    const second = document.querySelector('#number2');
    const result = document.querySelector('.result');
    if (window.Worker) {
        const myWorker = new Worker("worker.js");
    
        first.onchange = function() {
          myWorker.postMessage([first.value, second.value]);
          console.log('Message posted to worker');
        }
    
        second.onchange = function() {
          myWorker.postMessage([first.value, second.value]);
          console.log('Message posted to worker');
        }
    
        myWorker.onmessage = function(e) {
            result.textContent = e.data;
            console.log('Message received from worker');
        }
    } else {
        console.log('Your browser doesn\'t support web workers.')
    }

    2.2.shared worker

    共享進程可以連接到多個不同的頁面,這些頁面必須屬於相同的域(相同的協議,主機以及端口)

    在火狐中,共享進程不能在私有與公共文檔間進行共享。

    SharedWorker.port返回一個MessagePort對象,用來進行通信和對共享進程進行控制。

    創建共享進程對象:const myWorker = new SharedWorker("worker.js");

    獲取端口:

    發送消息:myWorker.port.postMessage(msg)

    接收消息:myWorker.port.onmessage = function(e) {const msg = e.data}

    worker線程獲取端口:onconnect = function(e) {const port = e.ports[0]}

    啟動端口:port.start()

    2.3.service worker

    Service Worker 可以理解為一個介於客戶端和服務器之間的一個代理服務器 ,常用於做離線資源緩存

    出於對安全問題的考慮,Service Worker 只能被使用在 https 或者本地的 localhost 環境下。

    暫時沒有仔細學這塊,可以閱讀。

    參考文章:

    3.代碼相關

    3.1.元素內文本垂直居中

    已知元素高度的話,可以設置line-height:元素高度.

    如果元素高度未知,就不能使用line-height了。

    有人會想使用line-height:100%,會發現這是不行的,這個百分比是相對當前字體尺寸,而不是元素高度。

    我使用了flex布局實現

        display: flex;
        align-items:center;
        justify-content:center;

    還可以設置padding來使文本看起來垂直居中

    padding: 50px 20px;

    3.2.微信小程序自定義placeholder的隱藏時機

    在一個searchBar組件中,有一個自定的placeholder如下:

    <!-- <view
    ​        wx:if="{{!inputValue.length}}"
    ​        class="placeholder" >
    ​        {{placeholder}}
    ​     </view> -->

    原生的placeholder不是在觸發bindinput時隱藏,而是在輸入鍵盤按鈕點擊時。使用inputValue.length來判斷显示自定義的placeholder會在某些輸入法中導致拼音預覽和自定義placeholder重疊(因為拼音显示的時候value值還沒變)

    最後選擇棄用這個自定義placeholder,使用input組件的placeholder屬性,並使用placeholder-class來設置它的樣式。

    3.3.關於微信小程序原生組件的坑

    原生組件有camera、canvas、input (僅在focus時表現為原生組件) 、live-player、live-pusher、map、textarea、video、cover-view、cover-image。

    所以當你用canvas畫圖表、使用地圖、播放視頻甚至做文本輸入時,都是可能遇到相關坑點的。

    1. 關於原生組件、組件之間的層級關係、

    ​ 原生組件的層級始終高於普通組件,不論普通組件的z-index設置了多少。

    ​ 后插入的原生組件可以覆蓋之前的原生組件。

    ​ 原生組件之間的相對層級關係可以通過z-index來調整。

    ​ 原生組件會遮擋vconsole彈出的調試面板。

    ​ cover-view和cover-image可以覆蓋在部分原生組件上。

    1. cover-view的使用

    ​ cover-view在做地圖、畫布、視頻上的彈出層時是會用到的,但它有很多使用限制。

    ​ cover-view只能內嵌cover-view、cover-image、button,其他元素在真機上就會被cover-view給覆蓋住,如果想內嵌radio、picker等就只能自己用這3個可內嵌的元素來實現。

    ​ cover-view不支持iconfont,也不支持單邊border、background-imageshadowoverflow: visible等。

    1. input的使用

    ​ input在不聚焦時是佔位元素,會被原生組件遮擋,聚焦時才使用原生組件渲染。這就會出現input設置了更高的z-index,不聚焦時仍會被其他原生組件遮住。

    ​ 要解決這個問題,可以使用textarea來代替input。

    ​ 我的一個解決方案是,加一個標誌位來記錄input是否聚焦,當不聚焦時,显示一個承載value值的cover-view(它需要綁定一個觸發聚焦的點擊事件),聚焦時,就显示input組件。

    3.4.多個標籤頁之間的通信方案

    1. 使用websocket

    2. 使用localstorage或者cookie

    3. 使用sharedworker

    我遇到的問題是需要在新窗口打開當前網站的新窗口時,能繼承上一個窗口的vuex的狀態樹里的某些數據。這不需要和服務器打交道,最好就在本地。

    最後使用localstorage來做,在跳轉新窗口前更新localstorage,在新窗口根組件掛載時取出數據。

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

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

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

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

    台灣海運大陸貨務運送流程

    兩岸物流進出口一站式服務

  • 2.NioEventLoop的創建

    2.NioEventLoop的創建

    NioEventLoop的創建

    NioEventLoop是netty及其重要的組成部件,它的首要職責就是為註冊在它上的channels服務,發現這些channels上發生的新連接、讀寫等I/O事件,然後將事件轉交 channel 流水線處理。使用netty時,我們首先要做的就是創建NioEventLoopGroup,這是一組NioEventLoop的集合,類似線程與線程池。通常,服務端會創建2個group,一個叫做bossGroup,一個叫做workerGroup。bossGroup負責監聽綁定的端口,接受請求並創建新連接,初始化后交由workerGroup處理後續IO事件。

    NioEventLoop和NioEventLoopGroup的類圖

    首先看看NioEventLoop和NioEventLoopGroup的類關係圖

    類多但不亂,可以發現三個特點:

    1. 兩者都繼承了ExecutorService,從而與線程池建立了聯繫
    2. NioEventLoop繼承的都是SingleThread,NioEventLoop繼承的是MultiThread
    3. NioEventLoop還繼承了AbstractScheduledEventExecutor,不難猜出這是個和定時任務調度有關的線程池

    NioEventLoopGroup的創建

    EventLoopGroup bossGroup = new NioEventLoopGroup(1);
    EventLoopGroup workerGroup = new NioEventLoopGroup();

    我們先看看bossGroup和workerGroup的構造方法。

    public NioEventLoopGroup() {
        this(0);
    }
    public NioEventLoopGroup(int nThreads) {
        this(nThreads, (Executor) null);
    }
    除此之外,還有多達8種構造方法,這些構造方法可以指定5種參數:
    1、最大線程數量。如果指定為0,那麼Netty會將線程數量設置為CPU邏輯處理器數量的2倍
    2、線程工廠。要求線程工廠類必須實現java.util.concurrent.ThreadFactory接口。如果沒有指定線程工廠,那麼默認DefaultThreadFactory。
    3、SelectorProvider。如果沒有指定SelectorProvider,那麼默認的SelectorProvider為SelectorProvider.provider()。
    4、SelectStrategyFactory。如果沒有指定則默認為DefaultSelectStrategyFactory.INSTANCE
    5、RejectedExecutionHandler。拒絕策略處理類,如果這個EventLoopGroup已被關閉,那麼之後提交的Runnable任務會默認調用RejectedExecutionHandler的reject方法進行處理。如果沒有指定,則默認調用拒絕策略。

    最終,NioEventLoopGroup會重載到父類MultiThreadEventExecutorGroup的構造方法上,這裏省略了一些健壯性代碼。

    protected MultithreadEventExecutorGroup(int nThreads, Executor executor,EventExecutorChooserFactory chooserFactory, Object... args) {
        // 步驟1
        if (executor == null) {
            executor = new ThreadPerTaskExecutor(newDefaultThreadFactory());
        }
        
        // 步驟2
        children = new EventExecutor[nThreads];
        for (int i = 0; i < nThreads; i ++) {
            children[i] = newChild(executor, args);
        }    
    
        // 步驟3
        chooser = chooserFactory.newChooser(children);
    
        // 步驟4
        final FutureListener<Object> terminationListener = future -> {
            if (terminatedChildren.incrementAndGet() == children.length) {
                terminationFuture.setSuccess(null);
            }
        };
        for (EventExecutor e: children) {
            e.terminationFuture().addListener(terminationListener);
        }
    
        // 步驟5
        Set<EventExecutor> childrenSet = new LinkedHashSet<>(children.length);
        Collections.addAll(childrenSet, children);
        readonlyChildren = Collections.unmodifiableSet(childrenSet);
        }

    這裏可以分解為5個步驟,下面一步步講解

    步驟1

    第一個步驟是創建線程池executor。從workerGroup構造方法可知,默認傳進來的executor為null,所以首先創建executor。newDefaultThreadFactory的作用是設置線程的前綴名和線程優先級,默認情況下,前綴名是nioEventLoopGroup-x-y這樣的命名規則,而線程優先級則是5,處於中間位置。
    創建完newDefaultThreadFactory后,進入到ThreadPerTaskExecutor。它直接實現了juc包的線程池頂級接口,從構造方法可以看到它只是簡單的把factory賦值給自己的成員變量。而它實現的接口方法調用了threadFactory的newThread方法。從名字可以看出,它構造了一個thread,並立即啟動thread。

    public ThreadPerTaskExecutor(ThreadFactory threadFactory) {
        this.threadFactory = threadFactory;
    }
    @Override
    public void execute(Runnable command) {
        threadFactory.newThread(command).start();
    }    

    那麼我們回過頭來看下DefaultThreadFactory的newThread方法,發現他創建了一個FastThreadLocalThread。這是netty自定義的一個線程類,顧名思義,netty認為它的性能更快。關於它的解析留待以後。這裏步驟1創建線程池就完成了。總的來說他與我們通常使用的線程池不太一樣,不設置線程池的線程數和任務隊列,而是來一個任務啟動一個線程。(問題:那任務一多豈不是直接線程爆炸?)

    @Override
    public Thread newThread(Runnable r) {
        Thread t = newThread(FastThreadLocalRunnable.wrap(r), prefix + nextId.incrementAndGet());        
        return t;
    }
    protected Thread newThread(Runnable r, String name) {
        return new FastThreadLocalThread(threadGroup, r, name);
    }

    步驟2

    步驟2是創建workerGroup中的NioEventLoop。在示例代碼中,傳進來的線程數是0,顯然不可能真的只創建0個nioEventLoop線程。在調用父類MultithreadEventLoopGroup構造函數時,對線程數進行了判斷,若為0,則傳入默認線程數,該值默認為2倍CPU核心數

    protected MultithreadEventLoopGroup(int nThreads, Executor executor, Object... args) {
        super(nThreads == 0 ? DEFAULT_EVENT_LOOP_THREADS : nThreads, executor, args);
    }
    // 靜態代碼塊初始化DEFAULT_EVENT_LOOP_THREADS
    static {
        DEFAULT_EVENT_LOOP_THREADS = Math.max(1, SystemPropertyUtil.getInt("io.netty.eventLoopThreads",     NettyRuntime.availableProcessors() * 2));
    }    

    接下來是通過newChild方法為每一個EventExecutor創建一個對應的NioEventLoop。這個方法傳入了一些args到NioEventLoop中,前三個是在NioEventLoopGroup創建時傳過來的參數。默認值見上文

    1. SlectorProvider.provider, 用於創建 Java NIO Selector 對象;
    2. SelectStrategyFactory, 選擇策略工廠;
    3. RejectedExecutionHandlers, 拒絕執行處理器;
    4. EventLoopTaskQueueFactory,任務隊列工廠,默認為null;

    進入NioEventLoop的構造函數,如下:

    NioEventLoop構造函數
    NioEventLoop(NioEventLoopGroup parent, Executor executor, SelectorProvider selectorProvider,
                     SelectStrategy strategy, RejectedExecutionHandler rejectedExecutionHandler,
                     EventLoopTaskQueueFactory queueFactory) {
            super(parent, executor, false, newTaskQueue(queueFactory), newTaskQueue(queueFactory),
                    rejectedExecutionHandler);
            if (selectorProvider == null) {
                throw new NullPointerException("selectorProvider");
            }
            if (strategy == null) {
                throw new NullPointerException("selectStrategy");
            }
            provider = selectorProvider;
            final SelectorTuple selectorTuple = openSelector();
            selector = selectorTuple.selector;
            unwrappedSelector = selectorTuple.unwrappedSelector;
            selectStrategy = strategy;
        }
    // 父類構造函數    
    protected SingleThreadEventExecutor(EventExecutorGroup parent, Executor executor,
                                            boolean addTaskWakesUp, Queue<Runnable> taskQueue,
                                            RejectedExecutionHandler rejectedHandler) {
        super(parent);
        this.addTaskWakesUp = addTaskWakesUp;
        this.maxPendingTasks = DEFAULT_MAX_PENDING_EXECUTOR_TASKS;
        this.executor = ThreadExecutorMap.apply(executor, this);
        this.taskQueue = ObjectUtil.checkNotNull(taskQueue, taskQueue");
        rejectedExecutionHandler = ObjectUtil.checkNotNullrejectedHandler, "rejectedHandler");
    }    

    首先調用一個newTaskQueue方法創建一個任務隊列。這是一個mpsc即多生產者單消費者的無鎖隊列。之後調用父類的構造函數,在父類的構造函數中,將NioEventLoopGroup設置為自己的parent,並通過匿名內部類創建了這樣一個Executor————通過ThreadPerTaskExecutor執行傳進來的任務,並且在執行時將當前線程與NioEventLoop綁定。其他屬性也一一設置。
    在nioEventLoop構造函數中,我們發現創建了一個selector,不妨看一看netty對它的包裝。

    unwrappedSelector = provider.openSelector();
    if (DISABLE_KEY_SET_OPTIMIZATION) {
        return new SelectorTuple(unwrappedSelector);
    }

    首先看到netty定義了一個常量DISABLE_KEY_SET_OPTIMIZATION,如果這個常量設置為true,也即不對keyset進行優化,則直接返回未包裝的selector。那麼netty對selector進行了哪些優化?

    final SelectedSelectionKeySet selectedKeySet = new SelectedSelectionKeySet();
    
    final class SelectedSelectionKeySet extends AbstractSet<SelectionKey> {
    
        SelectionKey[] keys;
        int size;
    
        SelectedSelectionKeySet() {
            keys = new SelectionKey[1024];
        }
    } 

    往下,我們看到了一個叫做selectedSelectionKeySet的類,點進去可以看到,它繼承了AbstractSet,然而它的成員變量卻讓我們想到了ArrayList,再看看它定義的方法,除了不支持remove和contains外,活脫脫一個簡化版的ArrayList,甚至也支持擴容。
    沒錯,netty確實通過反射的方式,將selectionKey從Set替換為了ArrayList。仔細一想,卻又覺得此番做法有些道理。眾所周知,雖然HashSet和ArrayList隨機查找的時間複雜度都是o(1),但相比數組直接通過偏移量定位,HashSet由於需要Hash運算,時間消耗上又稍稍遜色了些。再加上使用場景上,都是獲取selectionKey集合然後遍歷,Set去重的特性完全用不上,也無怪乎追求性能的netty想要替換它了。

    步驟3

    創建完workerGroup的NioEventLoop后,如何挑選一個nioEventLoop進行工作是netty接下來想要做的事。一般來說輪詢是一個很容易想到的方案,為此需要創建一個類似負載均衡作用的線程選擇器。當然追求性能到喪心病狂的netty是不會輕易滿足的。我們看看netty在這樣常見的方案里又做了哪些操作。

    public EventExecutorChooser newChooser(EventExecutor[] executors) {
        if (isPowerOfTwo(executors.length)) {
            return new PowerOfTwoEventExecutorChooser(executors);
        } else {
            return new GenericEventExecutorChooser(executors);
        }
    }
    // PowerOfTwo
    public EventExecutor next() {
        return executors[idx.getAndIncrement() & executors.length - 1];
    }
    // Generic
    public EventExecutor next() {
        return executors[Math.abs(idx.getAndIncrement() % executors.length)];
    }

    可以看到,netty根據workerGroup內線程的數量採取了2種不同的線程選擇器,當線程數x是2的冪次方時,可以通過&(x-1)來達到對x取模的效果,其他情況則需要直接取模。這與hashmap強制設置容量為2的冪次方有異曲同工之妙。

    步驟4

    步驟4就是添加一些保證健壯性而添加的監聽器了,這些監聽器會在EventLoop被關閉后得到通知。

    步驟5

    創建一個只讀的NioEventLoop線程組

    到此NioEventLoopGroup及其包含的NioEventLoop組就創建完成了

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

    台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包”嚨底家”

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

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

    小三通海運與一般國際貿易有何不同?

    小三通快遞通關作業有哪些?

  • 理解Spark SQL(一)—— CLI和ThriftServer

    理解Spark SQL(一)—— CLI和ThriftServer

    Spark SQL主要提供了兩個工具來訪問hive中的數據,即CLI和ThriftServer。前提是需要Spark支持Hive,即編譯Spark時需要帶上hive和hive-thriftserver選項,同時需要確保在$SPARK_HOME/conf目錄下有hive-site.xml配置文件(可以從hive中拷貝過來)。在該配置文件中主要是配置hive metastore的URI(Spark的CLI和ThriftServer都需要)以及ThriftServer相關配置項(如hive.server2.thrift.bind.host、hive.server2.thrift.port等)。注意如果該台機器上同時運行有Hive ThriftServer和Spark ThriftServer,則hive中的hive.server2.thrift.port配置的端口與spark中的hive.server2.thrift.port配置的端口要不一樣,避免同時啟動時發生端口衝突。

    啟動CLI和ThriftServer之前都需要先啟動hive metastore。執行如下命令啟動:

    [root@BruceCentOS ~]# nohup hive –service metastore &

    成功啟動后,會出現一個RunJar的進程,同時會監聽端口9083(hive metastore的默認端口)。

     

     

     先來看CLI,通過spark-sql腳本來使用CLI。執行如下命令:

    [root@BruceCentOS4 spark]# $SPARK_HOME/bin/spark-sql –master yarn

    上述命令執行後會啟動一個yarn client模式的Spark程序,如下圖所示:

      同時它會連接到hive metastore,可以在隨後出現的spark-sql>提示符下運行hive sql語句,比如:

      其中每輸入並執行一個SQL語句相當於執行了一個Spark的Job,如圖所示:

      也就是說執行spark-sql腳本會啟動一個yarn clien模式的Spark Application,而後出現spark-sql>提示符,在提示符下的每個SQL語句都會在Spark中執行一個Job,但是對應的都是同一個Application。這個Application會一直運行,可以持續輸入SQL語句執行Job,直到輸入“quit;”,然後就會退出spark-sql,即Spark Application執行完畢。

     

    另外一種更好地使用Spark SQL的方法是通過ThriftServer,首先需要啟動Spark的ThriftServer,然後通過Spark下的beeline或者自行編寫程序通過JDBC方式使用Spark SQL。

    通過如下命令啟動Spark ThriftServer:

    [root@BruceCentOS4 spark]# $SPARK_HOME/sbin/start-thriftserver.sh –master yarn

    執行上面的命令后,會生成一個SparkSubmit進程,實際上是啟動一個yarn client模式的Spark Application,如下圖所示:

      而且它提供一個JDBC/ODBC接口,用戶可以通過JDBC/ODBC接口連接ThriftServer來訪問Spark SQL的數據。具體可以通過Spark提供的beeline或者在程序中使用JDBC連接ThriftServer。例如在啟動Spark ThriftServer后,可以通過如下命令使用beeline來訪問Spark SQL的數據。

    [root@BruceCentOS3 spark]# $SPARK_HOME/bin/beeline -n root -u jdbc:hive2://BruceCentOS4.Hadoop:10003

     上述beeline連接到了BruceCentOS4上的10003端口,也就是Spark ThriftServer。所有連接到ThriftServer的客戶端beeline或者JDBC程序共享同一個Spark Application,通過beeline或者JDBC程序執行SQL相當於向這個Application提交並執行一個Job。在提示符下輸入“!exit”命令可以退出beeline。

    最後,如果要停止ThriftServer(即停止Spark Application),需要執行如下命令:

    [root@BruceCentOS4 spark]# $SPARK_HOME/sbin/stop-thriftserver.sh

     

     綜上所述,在Spark SQL的CLI和ThriftServer中,比較推薦使用後者,因為後者更加輕量,只需要啟動一個ThriftServer(對應一個Spark Application)就可以給多個beeline客戶端或者JDBC程序客戶端使用SQL,而前者啟動一個CLI就啟動了一個Spark Application,它只能給一個用戶使用。

     

     

     

     

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

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

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

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

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

    小三通物流營運型態?

    ※快速運回,大陸空運推薦?

  • Github PageHelper 原理解析

    任何服務對數據庫的日常操作,都離不開增刪改查。如果一次查詢的紀錄很多,那我們必須採用分頁的方式。對於一個Springboot項目,訪問和查詢MySQL數據庫,持久化框架可以使用MyBatis,分頁工具可以使用github的 PageHelper。我們來看一下PageHelper的使用方法:

     1 // 組裝查詢條件
     2 ArticleVO articleVO = new ArticleVO();
     3 articleVO.setAuthor("劉慈欣");
     4 
     5 // 初始化返回類
     6 // ResponsePages類是這樣一種返回類,其中包括返回代碼code和返回消息msg
     7 // 還包括返回的數據和分頁信息
     8 // 其中,分頁信息就是 com.github.pagehelper.Page<?> 類型
     9 ResponsePages<List<ArticleVO>> responsePages = new ResponsePages<>();
    10 
    11 // 這裏為了簡單,寫死分頁參數。正確的做法是從查詢條件中獲取
    12 // 假設需要獲取第1頁的數據,每頁20條記錄
    13 // com.github.pagehelper.Page<?> 類的基本字段如下
    14 // pageNum: 當前頁
    15 // pageSize: 每頁條數
    16 // total: 總記錄數
    17 // pages: 總頁數
    18 com.github.pagehelper.Page<?> page = PageHelper.startPage(1, 20);
    19 
    20 // 根據條件獲取文章列表
    21 List<ArticleVO> articleList = articleMapper.getArticleListByCondition(articleVO);
    22 
    23 // 設置返回數據
    24 responsePages.setData(articleList);
    25 
    26 // 設置分頁信息
    27 responsePages.setPage(page);

      

    如代碼所示,page 是組裝好的分頁參數,即每頁显示20條記錄,並且显示第1頁。然後我們執行mapper的獲取文章列表的方法,返回了結果。此時我們查看 responsePages 的內容,可以看到 articleList 中有20條記錄,page中包括當前頁,每頁條數,總記錄數,總頁數等信息。   使用方法就是這麼簡單,但是僅僅知道如何使用還不夠,還需要對原理有所了解。下面就來看看,PageHelper 實現分頁的原理。   我們先來看看 startPage 方法。進入此方法,發現一堆方法重載,最後進入真正的 startPage 方法,有5個參數,如下所示:

     1 /**
     2  * 開始分頁
     3  *
     4  * @param pageNum      頁碼
     5  * @param pageSize     每頁显示數量
     6  * @param count        是否進行count查詢
     7  * @param reasonable   分頁合理化,null時用默認配置
     8  * @param pageSizeZero true 且 pageSize=0 時返回全部結果,false時分頁, null時用默認配置
     9  */
    10 public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) {
    11     Page<E> page = new Page<E>(pageNum, pageSize, count);
    12     page.setReasonable(reasonable);
    13     page.setPageSizeZero(pageSizeZero);
    14     // 當已經執行過orderBy的時候
    15     Page<E> oldPage = SqlUtil.getLocalPage();
    16     if (oldPage != null && oldPage.isOrderByOnly()) {
    17         page.setOrderBy(oldPage.getOrderBy());
    18     }
    19     SqlUtil.setLocalPage(page);
    20     return page;
    21 }

      

    getLocalPage 和 setLocalPage 方法做了什麼操作?我們進入基類 BaseSqlUtil 看一下:

     1 package com.github.pagehelper.util;
     2 ...
     3 
     4 public class BaseSqlUtil {
     5     // 省略其他代碼
     6 
     7     private static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<Page>();
     8     
     9     /**
    10      * 從 ThreadLocal<Page> 中獲取 page
    11      */ 
    12     public static <T> Page<T> getLocalPage() {
    13         return LOCAL_PAGE.get();
    14     }
    15     
    16     /**
    17      * 將 page 設置到 ThreadLocal<Page>
    18      */
    19     public static void setLocalPage(Page page) {
    20         LOCAL_PAGE.set(page);
    21     }
    22 
    23     // 省略其他代碼
    24 }

     

    原來是將 page 放入了 ThreadLocal<Page> 中。ThreadLocal 是每個線程獨有的變量,與其他線程不影響,是放置 page 的好地方。 setLocalPage 之後,一定有地方 getLocalPage,我們跟蹤進入代碼來看。   有了MyBatis動態代理的知識后,我們知道最終執行SQL的地方是 MapperMethod 的 execute 方法,作為回顧,我們來看一下:

     1 package org.apache.ibatis.binding;
     2 ...
     3 
     4 public class MapperMethod {
     5 
     6     public Object execute(SqlSession sqlSession, Object[] args) {
     7         Object result;
     8         if (SqlCommandType.INSERT == command.getType()) {
     9             // 省略
    10         } else if (SqlCommandType.UPDATE == command.getType()) {
    11             // 省略
    12         } else if (SqlCommandType.DELETE == command.getType()) {
    13             // 省略
    14         } else if (SqlCommandType.SELECT == command.getType()) {
    15             if (method.returnsVoid() && method.hasResultHandler()) {
    16                 executeWithResultHandler(sqlSession, args);
    17                 result = null;
    18             } else if (method.returnsMany()) {
    19                 /**
    20                  * 獲取多條記錄
    21                  */
    22                 result = executeForMany(sqlSession, args);
    23             } else if ...
    24                 // 省略
    25         } else if (SqlCommandType.FLUSH == command.getType()) {
    26             // 省略
    27         } else {
    28             throw new BindingException("Unknown execution method for: " + command.getName());
    29         }
    30         ...
    31         
    32         return result;
    33     }
    34 }

      

    由於執行的是select操作,並且需要查詢多條紀錄,所以我們進入 executeForMany 這個方法中,然後進入 selectList 方法,然後是 executor.query 方法。再然後突然進入到了 mybatis 的 Plugin 類的 invoke 方法,這是為什麼?   這裏就必須提到 mybatis 提供的 Interceptor 接口。
    Intercept 機制讓我們可以將自己製作的分頁插件 intercept 到查詢語句執行的地方,這是MyBatis對外提供的標準接口。藉助於Java的動態代理,標準的攔截器可以攔截在指定的數據庫訪問流程中,執行攔截器自定義的邏輯,比如在執行SQL之前攔截,拼裝一個分頁的SQL並執行。   讓我們回到MyBatis初始化的時候,我們發現 MyBatis 為我們組裝了 sqlSessionFactory,所有的 sqlSession 都是生成自這個 Factory。在這篇文章中,我們將重點放在 interceptorChain 上。程序啟動時,MyBatis 或者是 mybatis-spring 會掃描代碼中所有實現了 interceptor 接口的插件,並將它們以【攔截器集合】的方式,存儲在 interceptorChain 中。如下所示:

    # sqlSessionFactory 中的重要信息
    
    sqlSessionFactory
        configuration
            environment        
            mapperRegistry
                config         
                knownMappers   
            mappedStatements   
            resultMaps         
            sqlFragments       
            interceptorChain   # MyBatis攔截器調用鏈
                interceptors   # 攔截器集合,記錄了所有實現了Interceptor接口,並且使用了invocation變量的類

      

    如果MyBatis檢測到有攔截器,它就會在攔截器指定的執行點,首先執行 Plugin 的 invoke 方法,喚醒攔截器,然後執行攔截器定義的邏輯。因此,當 query 方法即將執行的時候,其實執行的是攔截器的邏輯。   MyBatis官網的說明: MyBatis 允許你在已映射語句執行過程中的某一點進行攔截調用。默認情況下,MyBatis 允許使用插件來攔截的方法調用包括:

    • Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
    • ParameterHandler (getParameterObject, setParameters)
    • ResultSetHandler (handleResultSets, handleOutputParameters)
    • StatementHandler (prepare, parameterize, batch, update, query)

      如果想了解更多攔截器的知識,可以看文末的參考資料。   我們回到主線,繼續看Plugin類的invoke方法:

     1 package org.apache.ibatis.plugin;
     2 ...
     3 
     4 public class Plugin implements InvocationHandler {
     5     ...
     6 
     7     public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
     8         try {
     9            Set<Method> methods = signatureMap.get(method.getDeclaringClass());
    10            if (methods != null && methods.contains(method)) {
    11                // 執行攔截器的邏輯
    12                return interceptor.intercept(new Invocation(target, method, args));
    13            }
    14            return method.invoke(target, args);
    15        } catch (Exception e) {
    16            throw ExceptionUtil.unwrapThrowable(e);
    17        }
    18    }
    19    ...
    20 }

     

    我們去看 intercept 方法的實現,這裏我們進入【PageHelper】類來看:

     1 package com.github.pagehelper;
     2 ...
     3 
     4 /**
     5  * Mybatis - 通用分頁攔截器
     6  */
     7 @SuppressWarnings("rawtypes")
     8 @Intercepts(@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}))
     9 public class PageHelper extends BasePageHelper implements Interceptor {
    10     private final SqlUtil sqlUtil = new SqlUtil();
    11 
    12     @Override
    13     public Object intercept(Invocation invocation) throws Throwable {
    14         // 執行 sqlUtil 的攔截邏輯
    15         return sqlUtil.intercept(invocation);
    16     }
    17 
    18     @Override
    19     public Object plugin(Object target) {
    20         return Plugin.wrap(target, this);
    21     }
    22 
    23     @Override
    24     public void setProperties(Properties properties) {
    25         sqlUtil.setProperties(properties);
    26     }
    27 }

     

    可以看到最終調用了 SqlUtil 的intercept 方法,裏面的 doIntercept 方法是 PageHelper 原理中最重要的方法。跟進來看:

      1 package com.github.pagehelper.util;
      2 ...
      3 
      4 public class SqlUtil extends BaseSqlUtil implements Constant {
      5     ...
      6     
      7     /**
      8      * 真正的攔截器方法
      9      *
     10      * @param invocation
     11      * @return
     12      * @throws Throwable
     13      */
     14     public Object intercept(Invocation invocation) throws Throwable {
     15         try {
     16             return doIntercept(invocation);  // 執行攔截
     17         } finally {
     18             clearLocalPage();  // 清空 ThreadLocal<Page>
     19         }
     20     }
     21     
     22     /**
     23      * 真正的攔截器方法
     24      *
     25      * @param invocation
     26      * @return
     27      * @throws Throwable
     28      */
     29     public Object doIntercept(Invocation invocation) throws Throwable {
     30         // 省略其他代碼
     31         
     32         // 調用方法判斷是否需要進行分頁
     33         if (!runtimeDialect.skip(ms, parameterObject, rowBounds)) {
     34             ResultHandler resultHandler = (ResultHandler) args[3];
     35             // 當前的目標對象
     36             Executor executor = (Executor) invocation.getTarget();
     37             
     38             /**
     39              * getBoundSql 方法執行后,boundSql 中保存的是沒有 limit 的sql語句
     40              */
     41             BoundSql boundSql = ms.getBoundSql(parameterObject);
     42             
     43             // 反射獲取動態參數
     44             Map<String, Object> additionalParameters = (Map<String, Object>) additionalParametersField.get(boundSql);
     45             // 判斷是否需要進行 count 查詢,默認需要
     46             if (runtimeDialect.beforeCount(ms, parameterObject, rowBounds)) {
     47                 // 省略代碼
     48                 
     49                 // 執行 count 查詢
     50                 Object countResultList = executor.query(countMs, parameterObject, RowBounds.DEFAULT, resultHandler, countKey, countBoundSql);
     51                 Long count = (Long) ((List) countResultList).get(0);
     52                 
     53                 // 處理查詢總數,從 ThreadLocal<Page> 中取出 page 並設置 total
     54                 runtimeDialect.afterCount(count, parameterObject, rowBounds);
     55                 if (count == 0L) {
     56                     // 當查詢總數為 0 時,直接返回空的結果
     57                     return runtimeDialect.afterPage(new ArrayList(), parameterObject, rowBounds);
     58                 }
     59             }
     60             // 判斷是否需要進行分頁查詢
     61             if (runtimeDialect.beforePage(ms, parameterObject, rowBounds)) {
     62                 /**
     63                  * 生成分頁的緩存 key
     64                  * pageKey變量是分頁參數存放的地方
     65                  */
     66                 CacheKey pageKey = executor.createCacheKey(ms, parameterObject, rowBounds, boundSql);
     67                 /**
     68                  * 處理參數對象,會從 ThreadLocal<Page> 中將分頁參數取出來,放入 pageKey 中
     69                  * 主要邏輯就是這樣,代碼就不再單獨貼出來了,有興趣的同學可以跟進驗證
     70                  */
     71                 parameterObject = runtimeDialect.processParameterObject(ms, parameterObject, boundSql, pageKey);
     72                 /**
     73                  * 調用方言獲取分頁 sql
     74                  * 該方法執行后,pageSql中保存的sql語句,被加上了 limit 語句
     75                  */
     76                 String pageSql = runtimeDialect.getPageSql(ms, boundSql, parameterObject, rowBounds, pageKey);
     77                 BoundSql pageBoundSql = new BoundSql(ms.getConfiguration(), pageSql, boundSql.getParameterMappings(), parameterObject);
     78                 //設置動態參數
     79                 for (String key : additionalParameters.keySet()) {
     80                     pageBoundSql.setAdditionalParameter(key, additionalParameters.get(key));
     81                 }
     82                 /**
     83                  * 執行分頁查詢
     84                  */
     85                 resultList = executor.query(ms, parameterObject, RowBounds.DEFAULT, resultHandler, pageKey, pageBoundSql);
     86             } else {
     87                 resultList = new ArrayList();
     88             }
     89         } else {
     90             args[2] = RowBounds.DEFAULT;
     91             // 不需要分頁查詢,執行原方法,不走代理
     92             resultList = (List) invocation.proceed();
     93         }
     94         /**
     95          * 主要邏輯:
     96          * 從 ThreadLocal<Page> 中取出 page
     97          * 將 resultList 塞進 page,並返回
     98          */
     99         return runtimeDialect.afterPage(resultList, parameterObject, rowBounds);
    100     }
    101     ...
    102 }

     

    Count 查詢語句 countBoundSql 被執行了,分頁查詢語句 pageBoundSql 也被執行了。然後從 ThreadLocal<Page> 中將page 取出來,設置記錄總數,每頁條數等信息,同時也將查詢到的記錄塞進page,最後返回。再之後就是mybatis的常規後續操作了。

     

    知識拓展

    我們來看看 PageHelper 支持哪些數據庫的分頁操作:

    1. Oracle
    2. Mysql
    3. MariaDB
    4. SQLite
    5. Hsqldb
    6. PostgreSQL
    7. DB2
    8. SqlServer(2005,2008)
    9. Informix
    10. H2
    11. SqlServer2012
    12. Derby
    13. Phoenix

      原來 PageHelper 支持這麼多數據庫,那麼持久化工具mybatis為什麼不一口氣把分頁也做了呢? 其實mybatis也有自帶的分頁方法:RowBounds。
    RowBounds簡單地來說包括 offset 和 limit。實現原理是將所有符合條件的記錄獲取出來,然後丟棄 offset 之前的數據,只獲取 limit 條數據。這種做法效率低下,個人猜想mybatis只想把數據庫連接和SQL執行這方面做精做強,至於如分頁之類的細節,本身提供Intercept接口,讓第三方實現該接口來完成分頁。PageHelper 就是這樣的第三方分頁插件。甚至你可以實現該接口,製作你自己的業務邏輯,攔截到任何MyBatis允許你攔截的地方。  

    總結

    PageHelper 的分頁原理,最核心的部分是實現了 MyBatis 的 Interceptor 接口,從而將分頁參數攔截在執行sql之前,拼裝出分頁sql到數據庫中執行。 初始化的時候,因為 PageHelper 的 SqlUtil 中實例化了 intercept 方法,因此MyBatis 將它視作一個攔截器,記錄在 interceptorChain 中。 執行的時候,PageHelper首先將 page 需求記錄在 ThreadLocal<Page> 中,然後在攔截的時候,從 ThreadLocal<Page> 中取出 page,拼裝出分頁sql,然後執行。 同時將結果分頁信息(包括當前頁,每頁條數,總頁數,總記錄數等)設置回page,讓業務代碼可以獲取。  

    參考資料

    • PageHelper淺析:
    • MyBatis攔截器:
    • ThreadLocal理解:

     

    創作時間:2019-11-20 21:21

     

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

    【其他文章推薦】

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

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

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

    台灣寄大陸海運貨物規則及重量限制?

    大陸寄台灣海運費用試算一覽表

    台中搬家,彰化搬家,南投搬家前需注意的眉眉角角,別等搬了再說!

  • 回聲消除中的LMS和NLMS算法與MATLAB實現

    回聲消除中的LMS和NLMS算法與MATLAB實現

      自適應濾波是数字信號處理的核心技術之一,在科學和工業上有着廣泛的應用領域。自適應濾波技術應用廣泛,包括回波抵消、自適應均衡、自適應噪聲抵消和自適應波束形成。回聲對消是當今通信系統中普遍存在的現象。聲回波引起的信號干擾會分散用戶的注意力,降低通信質量。本文重點介紹了LMS和NLMS算法的使用,以減少這種不必要的回聲,從而提高通信質量

    關鍵詞:自適應濾波器,自適應算法,回聲消除

    1  引言

      當音頻信號在真實環境中產生混響時,就會產生聲學回聲,從而導致原始信號加上信號[1]的衰減、延時圖像。本文將重點研究通信系統中聲學回波的產生。

      自適應濾波器是一種動態濾波器,它不斷地改變其特性以獲得最優的輸出。自適應濾波算法通過改變參數使期望輸出d (n)與實際輸出y (n)之間的差值最小化。該函數稱為自適應算法的代價函數(loss)。圖1显示了自適應回聲抵消系統的框圖。其中,濾波器H(n)表示聲環境的脈衝響應,W(n)表示用來抵消回波信號的自適應濾波器。自適應濾波器的目標是使輸出的y(n)與期望的d(n)(在回聲環境中混響的信號)相等。在每次迭代中,誤差信號e(n)=d (n)-y (n)被反饋回濾波器,濾波器的特性也隨之改變。

    自適應回聲消除系統

      自適應濾波器的目標是計算期望信號與自適應濾波器輸出之間的差值e(n)。該誤差信號反饋到自適應濾波器,並通過算法改變其係數,以最小化該差值的函數,即代價函數。在聲回波消除的情況下,自適應濾波器的最優輸出與不需要的回波信號等值。當自適應濾波器輸出等於期望信號時,誤差信號為零。在這種情況下,回顯信號將被完全取消,遠用戶將不會聽到他們的任何原始語音返回給他們。

    2. 最小均方(LMS)算法

      最小均方(LMS)算法是由Widrow和Hoff在1959年通過對模式識別的研究首次提出的。由此成為自適應濾波中應用最廣泛的算法之一。LMS算法是一種基於隨機梯度的自適應濾波算法,它利用濾波器權重的梯度來收斂到最優的維納解[2-4]。由於其計算簡單而廣為人知並被廣泛使用。正是這種簡單性使它成為判斷所有其他自適應濾波算法的基準。

      隨着LMS算法的每次迭代,自適應濾波器的濾波抽頭(tap)權值按照如下公式進行更新。

    $$公式1:w(n+1)=w(n)2\mu e(n)x(n)$$

      這裏x(n)是延時輸入值的輸入向量,$x(n)=[x_1(n)x_2(n)…x_N(n)]^T=[x(n)x(n-1)…x(n-N+1)]^T$。向量$w(n)=[w_0(n)w_1(n)w_2(n)…w_{N-1}(n)]^T$代表自適應FIR濾波器抽頭(tap)權向量在時刻n的係數。參數μ被稱為步長參數和小正的常數。此步長參數控制更新因子的影響。μ必須選擇一個合適的值LMS算法的性能,如果該值太小自適應濾波器的收斂時間會太長;如果μ太大自適應濾波器變得不穩定,導致其輸出發散[5 – 8]

    2.1 LMS算法的實現

    LMS算法的每次迭代都需要三個不同的步驟,順序如下:

    1. FIR濾波器的輸出y(n)用公式2計算。

    $$公式2:y(n)=\sum_{i=0}^{N-1}w(n)x(n-1)=w^T(n)x(n)$$

    2. 誤差估計的值按公式3計算。

    $$公式3:e(n)=d(n)-y(n)$$

    3.更新FIR向量的抽頭tap權值,為下一次迭代做準備,如公式4所示。

    $$公式4:w(n+1)=w(n)+2\mu e(n)x(n)$$

      LMS算法在自適應濾波中得到廣泛應用的主要原因是其計算簡單,比其他常用的自適應算法更易於實現。LMS算法每次迭代需要2N加法和2N + 1次乘法(N用於計算輸出y(N)),另一個用於通過向量乘法計算標量[9]

    clear;
    clc;
    snr=20;     % 信噪比
    order=8;    % 自適應濾波器的階數為8
    Hn =[0.8783 -0.5806 0.6537 -0.3223 0.6577 -0.0582 0.2895 -0.2710 0.1278 ...     % ...表示換行的意思
        -0.1508 0.0238 -0.1814 0.2519 -0.0396 0.0423 -0.0152 0.1664 -0.0245 ...
        0.1463 -0.0770 0.1304 -0.0148 0.0054 -0.0381 0.0374 -0.0329 0.0313 ...
        -0.0253 0.0552 -0.0369 0.0479 -0.0073 0.0305 -0.0138 0.0152 -0.0012 ...
        0.0154 -0.0092 0.0177 -0.0161 0.0070 -0.0042 0.0051 -0.0131 0.0059 ...
        -0.0041 0.0077 -0.0034 0.0074 -0.0014 0.0025 -0.0056 0.0028 -0.0005 ...
        0.0033 -0.0000 0.0022 -0.0032 0.0012 -0.0020 0.0017 -0.0022 0.0004 -0.0011 0 0];
    Hn=Hn(1:order);
    mu=0.5;             % mu表示步長
    N=1000;             % 橫坐標1000個採樣點
    Loop=150;           % 150次循環
    EE_NLMS=zeros(N,1); % 不同步長的初始化誤差
    for nn=1:Loop       % epoch=150
        % 權重初始化w
        win_NLMS=zeros(1,order);         % NLMS四種步長測試,四個權重——1
        error_NLMS=zeros(1,N)';     % 初始化誤差
        % 均勻分佈的輸入值
        r=sign(rand(N,1)-0.5);          % shape=(1000,1)的(0,1)均勻分佈-0.5,sign(n)>0=1;<0=-1
        % 輸出:輸入卷積Hn得到 輸出
        output=conv(r,Hn);              % r卷積Hn,output長度=length(u)+length(v)-1
        output=awgn(output,snr,'measured');     % 將白高斯噪聲添加到信號中
    
        % N=1000,每個採樣點
        for i=order:N         % i=81000
          input=r(i:-1:i-order+1);  % 每次迭代取8個數據進行處理
          e_NLMS = output(i)-win_NLMS*input;
          win_NLMS=win_NLMS+e_NLMS*input'/(input'*input);   % NLMS更新權重
          error_NLMS(i)=error_NLMS(i)+e_NLMS^2;
        end
        
        EE_NLMS=EE_NLMS+error_NLMS;     % 把總誤差相加
    end
    % 對總誤差求平均值
    error_NLMS=EE_NLMS/Loop;
    
    figure;
    error_NLMS=10*log10(error_NLMS(order:N));
    plot(error_NLMS,'r');       % 紅色
    axis tight;                 % 使用緊湊的坐標軸
    legend('NLMS算法');           % 圖例
    title('NLMS算法誤差曲線');     % 圖標題
    xlabel('樣本');                     % x軸標籤
    ylabel('誤差/dB');                  % y軸標籤
    grid on;                            % 網格線

    3 歸一化最小均方(NLMS)算法

      LMS算法的主要缺點之一是每次迭代都有一個固定的步長參數。這需要在開始自適應濾波操作之前了解輸入信號的統計信息。實際上,這是很難實現的。即使我們假設自適應回聲抵消系統的唯一輸入信號是語音,但仍有許多因素如信號輸入功率和振幅會影響其性能[10-12]

      歸一化最小均方算法(NLMS)是LMS算法的擴展,LMS算法通過計算最大步長值來繞過這個問題。步長值的計算公式如下

    $$Step\ size = \frac{1}{dot\ product(input\ vector,\ input\ vector)}$$

    這個步長與輸入向量x(n)的係數的瞬時值的總期望能量的倒數成正比。輸入樣本的期望能量之和也等於輸入向量與自身的點積,以及輸入向量自相關矩陣的跡R[13-15]。

    $$公式5:tr[R]=\sum_{i=0}^{N-1}E[x^2(n-i)]\\ \quad\quad =E[\sum_{i=0}^{N-1}x^2(n-i)]$$

    NLMS算法的遞歸公式如式6所示

    $$公式6:w(n+1)=w(n)+\frac{1}{x^T(n)x(n)}e(n)x(n)$$

    3.1 NLMS算法的實現

      NLMS算法已在Matlab中實現。由於步長參數是根據當前的輸入值來選擇的,因此NLMS算法在未知信號下具有更大的穩定性。該算法具有良好的收斂速度和相對簡單的計算能力,是實時自適應回波抵消系統[16]的理想算法

      由於NLMS是標準LMS算法的擴展,因此NLMS算法的實際實現與LMS算法非常相似。NLMS算法的每次迭代都需要按照以下順序執行這些步驟。

    1. 計算了自適應濾波器的輸出

    $$公式7:y(n)=\sum_{i=0}^{N-1}w(n)x(n-i)=w^T(n)x(n)$$

    2. 誤差信號等於期望信號和濾波器輸出之間的差值。

    $$公式8:e(n)=d(n)-y(n)$$

    3.計算了輸入向量的步長值。

    $$公式9:\mu(n)=\frac{1}{x^T(n)x(n)}$$

    4. 濾波器抽頭權重更新,為下一次迭代做準備。

    $$公式10:w(n+1)=w(n)+\mu(n)e(n)x(n)$$

    NLMS算法的每次迭代都需要3N+1次乘法,僅比標準LMS算法多N次。考慮到所獲得的穩定性和回波衰減增益,這是一個可接受的增加。

    clear;
    clc;
    snr=20;     % 信噪比
    order=8;    % 自適應濾波器的階數為8
    % Hn是濾波器權重
    Hn =[0.8783 -0.5806 0.6537 -0.3223 0.6577 -0.0582 0.2895 -0.2710 0.1278 ...     % ...表示換行的意思
        -0.1508 0.0238 -0.1814 0.2519 -0.0396 0.0423 -0.0152 0.1664 -0.0245 ...
        0.1463 -0.0770 0.1304 -0.0148 0.0054 -0.0381 0.0374 -0.0329 0.0313 ...
        -0.0253 0.0552 -0.0369 0.0479 -0.0073 0.0305 -0.0138 0.0152 -0.0012 ...
        0.0154 -0.0092 0.0177 -0.0161 0.0070 -0.0042 0.0051 -0.0131 0.0059 ...
        -0.0041 0.0077 -0.0034 0.0074 -0.0014 0.0025 -0.0056 0.0028 -0.0005 ...
        0.0033 -0.0000 0.0022 -0.0032 0.0012 -0.0020 0.0017 -0.0022 0.0004 -0.0011 0 0];
    Hn=Hn(1:order);
    mu=0.5;             % mu表示步長
    N=1000;             % 橫坐標1000個採樣點
    Loop=150;           % 150次循環
    % 不同步長的初始化誤差
    EE_LMS = zeros(N,1);
    EE_NLMS=zeros(N,1);
    for nn=1:Loop       % epoch=150
        win_LMS = zeros(1,order);   % 權重初始化w
        error_LMS=zeros(1,N)';      % 初始化誤差
        % 均勻分佈的語音數據輸入
        r=sign(rand(N,1)-0.5);          % shape=(1000,1)的(0,1)均勻分佈-0.5,sign(n)>0=1;<0=-1
        % 輸出:輸入卷積Hn得到 輸出
        output=conv(r,Hn);              % r卷積Hn,output長度=length(u)+length(v)-1
        output=awgn(output,snr,'measured');     % 真實輸出=將白高斯噪聲添加到信號中
    
        % N=1000,每個採樣點
        for i=order:N         % i=81000
          input=r(i:-1:i-order+1);  % 每次迭代取8個數據進行處理
          e_LMS = output(i)-win_LMS*input;
          
          mu=0.02;      % 步長
          win_LMS = win_LMS+2*mu*e_LMS*input';
          error_LMS(i)=error_LMS(i)+e_LMS^2;
        end
        % 把總誤差相加
        EE_LMS = EE_LMS+error_LMS;
    
    end
    % 對總誤差求平均值
    error_LMS = EE_LMS/Loop;
    
    figure;
    error1_LMS=10*log10(error_LMS(order:N));
    plot(error1_LMS,'b.');  % 藍色
    axis tight;         % 使用緊湊的坐標軸
    legend('LMS算法');       % 圖例
    title('LMS算法誤差曲線');  % 圖標題
    xlabel('樣本');                     % x軸標籤
    ylabel('誤差/dB');                  % y軸標籤
    grid on;                            % 網格線

    4 LMS算法的結果

      利用Matlab對LMS算法進行了仿真。圖2显示的是通過麥克風從計算機系統收集到的輸入語音信號。圖3显示了從輸入信號派生出的所需回波信號。圖4显示了自適應濾波器的輸出,它將減少輸入信號的回波信號。圖5显示了由濾波器輸出信號計算出的均方誤差信號。圖6是由回波信號對誤差信號的分割得到的衰減。

      自適應濾波器為1025階FIR濾波器。步長設置為0.02。MSE表明,隨着算法的發展,代價函數的平均值逐漸減小。

    5 NLMS算法的結果

      用Matlab對NLMS算法進行了仿真。圖7显示了輸入信號。圖8显示了所需的信號。圖9显示了自適應濾波器輸出。圖10显示了均方誤差。圖11显示了衰減。

      自適應濾波器為1025階FIR濾波器。步長設置為0.1。

     

     NLMS算法在均方誤差和平均衰減方面優於LMS算法,其性能總結如表1所示。

     

    6 結論

      由於其簡單性,LMS算法是最流行的自適應算法。然而,LMS算法存在收斂速度慢和數據依賴的問題。

      NLMS算法是LMS算法的一個同樣簡單但更健壯的變體,它在簡單性和性能之間表現出比LMS算法更好的平衡。由於其良好的性能,NLMS在實時應用中得到了廣泛的應用。

    7. 參考

    文章翻譯自論文《2011_adaptive algorithms for acoustic echo cancellation in speech processing》

    [1]. Homana, I.; Topa, M.D.; Kirei, B.S.; “Echo cancelling using adaptive algorithms”, Design and Technology of Electronics Packages, (SIITME) 15th International Symposium., pp. 317-321, Sept.2009.

    [2]. Paleologu, C.; Benesty, J.; Grant, S.L.; Osterwise, C.; “Variable step-size NLMS algorithms for echo cancellation” 2009 Conference Record of the forty-third Asilomar Conference on Signals, Systems and Computers., pp. 633-637, Nov 2009.

    [3]. Soria, E.; Calpe, J.; Chambers, J.; Martinez, M.; Camps, G.; Guerrero, J.D.M.; “A novel approach to introducing adaptive filters based on the LMS algorithm and its variants”, IEEE Transactions, vol. 47, pp. 127-133, Feb 2008.

    [4]. Tandon, A.; Ahmad, M.O.; Swamy, M.N.S.; “An efficient, low-complexity, normalized LMS algorithm for echo cancellation”, IEEE workshop on Circuits and Systems, 2004. NEWCAS 2004, pp. 161-164, June 2004.

    [5]. Eneman, K.; Moonen, M.; “Iterated partitioned block frequency-domain adaptive filtering for acoustic echo cancellation,” IEEE Transactions on Speech and Audio Processing, vol. 11, pp. 143-158, March 2003.

    [6]. Krishna, E.H.; Raghuram, M.; Madhav, K.V; Reddy, K.A; “Acoustic echo cancellation using a computationally efficient transform domain LMS adaptive filter,” 2010 10th International Conference on Information sciences signal processing and their applications (ISSPA), pp. 409-412, May 2010.

    [7]. Lee, K.A.; Gan,W.S; “Improving convergence of the NLMS algorithm using constrained subband updates,” Signal Processing Letters IEEE, vol. 11, pp. 736-739, Sept. 2004.

    [8]. S.C. Douglas, “Adaptive Filters Employing Partial Updates,” IEEE Trans.Circuits SYS.II, vol. 44, pp. 209-216, Mar 1997.

    [9]. D.L. Duttweiler, “Proportionate Normalized Least Mean Square Adaptation in Echo Cancellers,” IEEE Trans. Speech Audio Processing, vol. 8, pp. 508-518, Sept. 2000.

    [10]. E. Soria, J. Calpe, J. Guerrero, M. Martínez, and J. Espí, “An easy demonstration of the optimum value of the adaptation constant in the LMS algorithm,” IEEE Trans. Educ., vol. 41, pp. 83, Feb. 1998.

    [11]. D. Morgan and S. Kratzer, “On a class of computationally efficient rapidly converging, generalized NLMS algorithms,” IEEE Signal Processing Lett., vol. 3, pp. 245–247, Aug. 1996.

    [12]. G. Egelmeers, P. Sommen, and J. de Boer, “Realization of an acoustic echo canceller on a single DSP,” in Proc. Eur. Signal Processing Conf. (EUSIPCO96), Trieste, Italy, pp. 33–36, Sept. 1996.

    [13]. J. Shynk, “Frequency-domain and multirate adaptive filtering,” IEEE Signal Processing Mag., vol. 9, pp. 15– 37, Jan. 1992.

    [14]. Ahmed I. Sulyman and Azzedine Zerguine, “Echo Cancellation Using a Variable Step-Size NLMS Algorithm”, Electrical and Computer Engineering Department Queen’s University.

    [15]. D. L. Duttweiler, “A twelve-channel digital echo canceller,” IEEE Trans. Commun., vol. 26, no. 5, pp. 647–653, May 1978.

    [16]. J. Benesty, H. Rey, L. Rey Vega, and S. Tressens, “A nonparametric VSS NLMS algorithm,” IEEE Signal Process. Lett., vol. 13, pp. 581–584, Oct. 2006.

     

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

    【其他文章推薦】

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

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

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

    大陸寄台灣空運注意事項

    大陸海運台灣交貨時間多久?

    ※避免吃悶虧無故遭抬價!台中搬家公司免費估價,有契約讓您安心有保障!

  • SpringBoot系列教程JPA之指定id保存

    SpringBoot系列教程JPA之指定id保存

    原文鏈接:

    前幾天有位小夥伴問了一個很有意思的問題,使用 JPA 保存數據時,即便我指定了主鍵 id,但是新插入的數據主鍵卻是 mysql 自增的 id;那麼是什麼原因導致的呢?又可以如何解決呢?

    本文將介紹一下如何使用 JPA 的 AUTO 保存策略來指定數據庫主鍵 id

    I. 環境準備

    實際開始之前,需要先走一些必要的操作,如安裝測試使用 mysql,創建 SpringBoot 項目工程,設置好配置信息等,關於搭建項目的詳情可以參考前一篇文章

    下面簡單的看一下後續的代碼中,需要的配置 (我們使用的是 mysql 數據庫)

    1. 表準備

    沿用前一篇的表,結構如下

    CREATE TABLE `money` (
      `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
      `name` varchar(20) NOT NULL DEFAULT '' COMMENT '用戶名',
      `money` int(26) NOT NULL DEFAULT '0' COMMENT '錢',
      `is_deleted` tinyint(1) NOT NULL DEFAULT '0',
      `create_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '創建時間',
      `update_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新時間',
      PRIMARY KEY (`id`),
      KEY `name` (`name`)
    ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;

    2. 項目配置

    配置信息,與之前有一點點區別,我們新增了更詳細的日誌打印;本篇主要目標集中在添加記錄的使用姿勢,對於配置說明,後面單獨進行說明

    ## DataSource
    spring.datasource.url=jdbc:mysql://127.0.0.1:3306/story?useUnicode=true&characterEncoding=UTF-8&useSSL=false
    spring.datasource.driver-class-name=com.mysql.jdbc.Driver
    spring.datasource.username=root
    spring.datasource.password=
    ## jpa相關配置
    spring.jpa.database=MYSQL
    spring.jpa.hibernate.ddl-auto=none
    spring.jpa.show-sql=true
    spring.jackson.serialization.indent_output=true
    spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl

    II. Insert 教程

    首先簡單的看一下,我們一般使用默認的數據庫自增生成主鍵的使用方式,以便後面的自定義主鍵生成策略的對比

    對於 jpa 的插入數據的知識點不太清楚的同學,可以看一下之前的博文:

    1. 自增主鍵

    首先我們需要定義 PO,與數據庫中的表綁定起來

    @Data
    @DynamicUpdate
    @DynamicInsert
    @Entity
    @Table(name = "money")
    public class MoneyPO {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        @Column(name = "id")
        private Integer id;
    
        @Column(name = "name")
        private String name;
    
        @Column(name = "money")
        private Long money;
    
        @Column(name = "is_deleted")
        private Byte isDeleted;
    
        @Column(name = "create_at")
        @CreatedDate
        private Timestamp createAt;
    
        @Column(name = "update_at")
        @CreatedDate
        private Timestamp updateAt;
    }

    注意上面的主鍵生成策略用的是 GenerationType.IDENTITY,配合 mysql 的使用就是利用數據庫的自增來生成主鍵 id

    /**
     * 新增數據
     * Created by @author yihui in 11:00 19/6/12.
     */
    public interface MoneyCreateRepositoryV2 extends JpaRepository<MoneyPO, Integer> {
    }

    接下來保存數據就很簡單了

    private void addWithId() {
        MoneyPO po1 = new MoneyPO();
        po1.setId(20);
        po1.setName("jpa 一灰灰 1x");
        po1.setMoney(2200L + ((long) (Math.random() * 100)));
        po1.setIsDeleted((byte) 0x00);
        MoneyPO r1 = moneyCreateRepositoryV2.save(po1);
        System.out.println("after insert res: " + r1);
    }

    強烈建議實際的體驗一下上面的代碼執行

    首次執行確保數據庫中不存在 id 為 20 的記錄,雖然我們的 PO 對象中,指定了 id 為 20,但是執行完畢之後,新增的數據 id 卻不是 20

    Hibernate: select moneypo0_.id as id1_0_0_, moneypo0_.create_at as create_a2_0_0_, moneypo0_.is_deleted as is_delet3_0_0_, moneypo0_.money as money4_0_0_, moneypo0_.name as name5_0_0_, moneypo0_.update_at as update_a6_0_0_ from money moneypo0_ where moneypo0_.id=?
    Hibernate: insert into money (is_deleted, money, name) values (?, ?, ?)
    after insert res: MoneyPO(id=104, name=jpa 一灰灰 1x, money=2208, isDeleted=0, createAt=null, updateAt=null)

    上面是執行的 sql 日誌,注意插入的 sql,是沒有指定 id 的,所以新增的記錄的 id 就會利用 mysql 的自增策略

    當我們的 db 中存在 id 為 20 的記錄時,再次執行,查看日誌發現實際執行的是更新數據

    Hibernate: select moneypo0_.id as id1_0_0_, moneypo0_.create_at as create_a2_0_0_, moneypo0_.is_deleted as is_delet3_0_0_, moneypo0_.money as money4_0_0_, moneypo0_.name as name5_0_0_, moneypo0_.update_at as update_a6_0_0_ from money moneypo0_ where moneypo0_.id=?
    Hibernate: update money set create_at=?, money=?, name=?, update_at=? where id=?
    after insert res: MoneyPO(id=20, name=jpa 一灰灰 1x, money=2234, isDeleted=0, createAt=null, updateAt=null)

    大膽猜測,save 的執行過程邏輯如

    • 首先根據 id 到數據庫中查詢對應的數據
    • 如果數據不存在,則新增(插入 sql 不指定 id)
    • 如果數據存在,則判斷是否有變更,以確定是否需要更新

    2. 指定 id

    那麼問題來了,如果我希望當我的 po 中指定了數據庫 id 時,db 中沒有這條記錄時,就插入 id 為指定值的記錄;如果存在記錄,則更新

    要實現上面這個功能,自定義主鍵 id,那麼我們就需要修改一下主鍵的生成策略了,官方提供了四種

    取值 說明
    GenerationType.TABLE 使用一個特定的數據庫表格來保存主鍵
    GenerationType.SEQUENCE 根據底層數據庫的序列來生成主鍵,條件是數據庫支持序列
    GenerationType.IDENTITY 主鍵由數據庫自動生成(主要是自動增長型)
    GenerationType.AUTO 主鍵由程序控制

    從上面四種生成策略說明中,很明顯我們要使用的就是 AUTO 策略了,我們新增一個 PO,並指定保存策略

    @Data
    @DynamicUpdate
    @DynamicInsert
    @Entity
    @Table(name = "money")
    public class AutoMoneyPO {
        @Id
        @GeneratedValue(strategy = GenerationType.AUTO, generator = "myid")
        @GenericGenerator(name = "myid", strategy = "com.git.hui.boot.jpa.generator.ManulInsertGenerator")
        @Column(name = "id")
        private Integer id;
    
        @Column(name = "name")
        private String name;
    
        @Column(name = "money")
        private Long money;
    
        @Column(name = "is_deleted")
        private Byte isDeleted;
    
        @Column(name = "create_at")
        @CreatedDate
        private Timestamp createAt;
    
        @Column(name = "update_at")
        @CreatedDate
        private Timestamp updateAt;
    }

    採用自定義的生成策略,需要注意,@GenericGenerator(name = "myid", strategy = "com.git.hui.boot.jpa.generator.ManulInsertGenerator")這個需要有,否則執行會拋異常

    這一行代碼的意思是,主鍵 id 是由ManulInsertGenerator來生成

    /**
     *  自定義的主鍵生成策略,如果填寫了主鍵id,如果數據庫中沒有這條記錄,則新增指定id的記錄;否則更新記錄
     *
     *  如果不填寫主鍵id,則利用數據庫本身的自增策略指定id
     *
     * Created by @author yihui in 20:51 19/11/13.
     */
    public class ManulInsertGenerator extends IdentityGenerator {
    
        @Override
        public Serializable generate(SharedSessionContractImplementor s, Object obj) throws HibernateException {
            Serializable id = s.getEntityPersister(null, obj).getClassMetadata().getIdentifier(obj, s);
    
            if (id != null && Integer.valueOf(id.toString()) > 0) {
                return id;
            } else {
                return super.generate(s, obj);
            }
        }
    }

    具體的主鍵生成方式也比較簡單了,首先是判斷 PO 中有沒有主鍵,如果有則直接使用 PO 中的主鍵值;如果沒有,就利用IdentityGenerator策略來生成主鍵(而這個主鍵生成策略,正好是GenerationType.IDENTITY利用數據庫自增生成主鍵的策略)

    接下來我們再次測試插入

    // 使用自定義的主鍵生成策略
    AutoMoneyPO moneyPO = new AutoMoneyPO();
    moneyPO.setId(20);
    moneyPO.setName("jpa 一灰灰 ex");
    moneyPO.setMoney(2200L + ((long) (Math.random() * 100)));
    moneyPO.setIsDeleted((byte) 0x00);
    AutoMoneyPO res = moneyCreateRepositoryWithId.save(moneyPO);
    System.out.println("after insert res: " + res);
    
    moneyPO.setMoney(3200L + ((long) (Math.random() * 100)));
    res = moneyCreateRepositoryWithId.save(moneyPO);
    System.out.println("after insert res: " + res);
    
    moneyPO = new AutoMoneyPO();
    moneyPO.setName("jpa 一灰灰 2ex");
    moneyPO.setMoney(2200L + ((long) (Math.random() * 100)));
    moneyPO.setIsDeleted((byte) 0x00);
    res = moneyCreateRepositoryWithId.save(moneyPO);
    System.out.println("after insert res: " + res);

    上面的代碼執行時,確保數據庫中沒有主鍵為 20 的數據,輸出 sql 日誌如下

    # 第一次插入
    Hibernate: select automoneyp0_.id as id1_0_0_, automoneyp0_.create_at as create_a2_0_0_, automoneyp0_.is_deleted as is_delet3_0_0_, automoneyp0_.money as money4_0_0_, automoneyp0_.name as name5_0_0_, automoneyp0_.update_at as update_a6_0_0_ from money automoneyp0_ where automoneyp0_.id=?
    Hibernate: insert into money (is_deleted, money, name, id) values (?, ?, ?, ?)
    after insert res: AutoMoneyPO(id=20, name=jpa 一灰灰 ex, money=2238, isDeleted=0, createAt=null, updateAt=null)
    
    # 第二次指定id插入
    Hibernate: select automoneyp0_.id as id1_0_0_, automoneyp0_.create_at as create_a2_0_0_, automoneyp0_.is_deleted as is_delet3_0_0_, automoneyp0_.money as money4_0_0_, automoneyp0_.name as name5_0_0_, automoneyp0_.update_at as update_a6_0_0_ from money automoneyp0_ where automoneyp0_.id=?
    Hibernate: update money set create_at=?, money=?, update_at=? where id=?
    after insert res: AutoMoneyPO(id=20, name=jpa 一灰灰 ex, money=3228, isDeleted=0, createAt=null, updateAt=null)
    
    # 第三次無id插入
    Hibernate: insert into money (is_deleted, money, name) values (?, ?, ?)
    after insert res: AutoMoneyPO(id=107, name=jpa 一灰灰 2ex, money=2228, isDeleted=0, createAt=null, updateAt=null)

    注意上面的日誌輸出

    • 第一次插入時拼裝的寫入 sql 是包含 id 的,也就達到了我們指定 id 新增數據的要求
    • 第二次插入時,因為 id=20 的記錄存在,所以執行的是更新操作
    • 第三次插入時,因為沒有 id,所以插入的 sql 中也沒有指定 id,使用 mysql 的自增來生成主鍵 id

    II. 其他

    0. 項目&博文

    • 工程:
    • module:

    1. 一灰灰 Blog

    盡信書則不如,以上內容,純屬一家之言,因個人能力有限,難免有疏漏和錯誤之處,如發現 bug 或者有更好的建議,歡迎批評指正,不吝感激

    下面一灰灰的個人博客,記錄所有學習和工作中的博文,歡迎大家前去逛逛

    • 一灰灰 Blog 個人博客
    • 一灰灰 Blog-Spring 專題博客

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

    【其他文章推薦】

    ※專營大陸空運台灣貨物推薦

    台灣空運大陸一條龍服務

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

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

  • 工信部第285批新車公示218款新能源入選

    根據《中華人民共和國行政許可法》和《國務院對確需保留的行政審批專案設定行政許可的決定》的規定,工信部日前將許可的汽車、摩托車、三輪汽車和低速貨車生產企業及產 品(第285批)予以了公告,共有218款新能源車型入選。進入該公告的新能源汽車可開展生產銷售,但是要獲得補貼,還需再獲得《新能源汽車推廣應用推薦車型目錄》准入。

    純電動轎車/乘用車方面,北汽、長城、禦捷馬、卡威、吉利等12款車型入選。

    插電式乘用車方面,比亞迪、寶馬、之諾、上汽等7款車型入選。

    純電動客車方面,安凱、江淮、安源、北奔、北方、福田、比亞迪、白雲、五菱、陸地方舟、尼歐凱、友誼、青年、海格、開沃、依維柯、飛燕、大通、象牌、野馬、華新、金龍、金旅、宇通、黃河、中通、中植汽車、穗通等28個品牌75款車型入選。

    插電式混動客車方面, 安凱、海格、易聖達、金龍、金旅、宇通6個品牌21款車型入選。

    新能源專用車方面,福田、北京、大運、黃海、東風、華神、揚子江、東風、福建、環球、藍速、田野、中悅 、卡威、陸地方舟、青年曼、康迪、五菱、暢達、躍進、金龍、凱馬、時風、太行成功、東風、金杯、邢牛、海德、解放、神州、宇通、長帆汽車、迪馬、炫虎等33個品牌103款車型入選。

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

    【其他文章推薦】

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

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

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

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

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

    ※試算大陸海運運費!

  • 中國汽車工業三十強榜單發佈:冠軍上汽集團營收超萬億元

    中國汽車工業三十強榜單發佈:冠軍上汽集團營收超萬億元

    2016年5月26日,中國機械工業聯合會、中國汽車工業協會發佈最新中國汽車工業三十強榜單。上汽集團以2015年1.2萬億元的總營業收入繼續蟬聯冠軍,一汽和東風依然排第2、3位。

    榜單中,蓋世汽車盤點出有乘用車公司16家,商用車公司4家,零部件公司7家,摩托車公司3家。本次榜單基於“年度匯總口徑快報”統計2015年營業收入,和上市公司合併財務報表中的營業收入存在計算上的差別,後者抵消了關聯交易部分。這也是為何上汽2015年年報中營收為670,448,223,139.34元,而本榜單中為1.2萬億元的緣故。

    對比2015年排名(以2014年營業收入統計),19家企業排名沒有變化,比亞迪和威孚高科新上榜,陝西汽車和金城集團落榜。前十名中,僅長城(今年第9)和重汽(今年第10)互換了名次,其他8家企業都保持了去年的座次。提升最快的是中鼎,從第27提高到第24,上升3位。下降幅度最大的是法士特,從第24滑落至第28。

    冠軍上汽集團去年營收總額超過第2名一汽和第3名東風總和。

    以下是具體排名

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

    【其他文章推薦】

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

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

    ※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

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

    ※專營大陸快遞台灣服務

    台灣快遞大陸的貨運公司有哪些呢?

  • 菲亞特董事長公開闢謠 廣汽入主消息不實

    據《歐洲汽車新聞》報導,菲亞特克萊斯勒公司(FCA)董事長約翰•埃爾坎(John Elkann)明確指出,未與中國廣汽集團簽訂股權出售協定。

    2015年FCA與廣汽延續廣汽菲亞特的合作關係,成立了廣汽菲克合資公司,投產Jeep自由光和自由俠。近日,《義大利日報》發佈了一篇爆料新聞,除在生產方面的合作外,廣汽集團還有意入主FCA,展開資本層面的聯姻。

    埃爾坎同時也是菲亞特控股集團EXOR的董事長,25日召開了Exor股東大會。針對《義大利日報》的爆料,埃爾坎當日公開闢謠,公司與廣汽集團未簽訂股權出售協定。

    埃爾坎也被詢問標緻雪鐵龍集團(PSA集團)是否可成為FCA的良好合作夥伴,該董事長則指出其公司正在尋求的推動轉型的合作夥伴,PSA並不在公司的考慮範圍內。

    據法國《回聲報》的報導,法國政府正在考慮出售其持有的PSA 14%的部分或全部股權。

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

    【其他文章推薦】

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

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

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

    台灣海運大陸貨務運送流程

    兩岸物流進出口一站式服務