循序漸進VUE+Element 前端應用開發(7)— 介紹一些常規的JS處理函數

在我們使用VUE+Element 處理界面的時候,往往碰到需要利用JS集合處理的各種方法,如Filter、Map、reduce等方法,也可以涉及到一些對象屬性賦值等常規的處理或者遞歸的處理方法,以前對於這些不是很在意,但往往真正使用的時候,需要了解清楚,否則很容易腦袋出現短路的情況。本篇隨筆列出一些在VUE+Element 前端開發中經常碰到的JS處理場景,供參考學習。

1、常規集合的filter、map、reduce處理方法

filter函數的主要用途是對數組元素進行過濾,並返回一個符合條件的元素的數組

const nums = [10,20,30,111,222,333]
let newNums=nums.filter(function(n){
    return n<100
})

輸出:[10,20,30]

 

map函數是對數組每個元素的映射操作,並返回一個新數組,原數組不會改變將newNums中每個数字乘2

const nums = [10,20,30,111,222,333]
let newNums=nums.filter(function(n){
    return n*2
})

輸出:[20,40,60,222,666]

 

reduce函數主要用於對數組所有元素的匯總操作,如全部相加、相乘等

const nums = [10,20,30,111,222,333]
let newNums=nums.reduce(function(preValue,n){
    return PreValue+n
},0)

輸出:726

 

有時候可以結合幾種處理方式一起,如下綜合案例所示。

const nums = [10,20,30,111,222,333]
let newNums=nums.filter(function(n){
    return n<100
}).map(function(n){
    return n*2
}).reduce(function(preValue,n){
    return preValue+n
},0)

結果:120

 

另外還有一個數組集合的find方法,和filter方法類似。

find()方法主要用來返回數組中符合條件的第一個元素(沒有的話,返回undefined)

 var Array = [1,2,3,4,5,6,7];
 var result = Array.find(function(value){
     return value > 5;   //條件
 });
 console.log(result);//6
 console.log(Array);//[1,2,3,4,5,6,7]

 

同樣我們也可以在vue裏面,利用require.context的處理機制,遍歷文件進行處理,也需要用到了filter,如下代碼所示。

下面代碼是我對某個文件夾裏面的文件進行一個過濾處理操作

const req = require.context('vue-awesome/icons', true, /\.js$/)
const requireAll = requireContext => requireContext.keys()

const re = /\.\/(.*)\.js/

const vueAwesomeIcons = requireAll(req).filter((key) => { return key.indexOf('index.js') < 0 }).map(i => {
  return i.match(re)[1]
})

export default vueAwesomeIcons

 

2、遞歸處理

有時候,我們需要從一個JSON集合裏面,由於集合是嵌套的,如children裏面還有chilren集合,根據某個關鍵屬性進行查詢,這種處理方式就要用到遞歸了。

例如我定義的一個菜單集合裏面,就是這樣一個嵌套的結構,需要根據名稱來獲得對應的對象的時候,就涉及到了一個遞歸處理函數。

首先我們來看看菜單的JSON集合。

// 此菜單數據一般由服務器端返回
export const asyncMenus = [
  {
    id: '1',
    pid: '-1',
    text: '首頁',
    icon: 'dashboard',
    name: 'dashboard'
  },
  {
    id: '2',
    pid: '-1',
    text: '產品信息',
    icon: 'table',
    children: [
      {
        id: '2-1',
        pid: '2',
        text: '產品展示',
        name: 'product-show',
        icon: 'table'
      }]
  },
  {
    id: '3',
    pid: '-1',
    text: '雜項管理',
    icon: 'example',
    children: [
      {
        id: '3-1',
        pid: '3',
        text: '圖標管理',
        name: 'icon',
        icon: 'example'
      },
      {
        id: '3-3',
        pid: '3',
        text: '樹功能展示',
        name: 'tree',
        icon: 'tree'
      },
      {
        id: '3-2',
        pid: '3',
        text: '二級菜單2',
        icon: 'tree',
        children: [
          {
            id: '3-2-2',
            pid: '3-2',
            text: '三級菜單2',
            name: 'menu1-1',
            icon: 'form'
          }
        ]
      }
    ]
  }
]

如果我們需要根據ID來遍歷查詢,就是一個典型的遞歸查詢處理。

    // 根據菜單id來獲取對應菜單對象
    FindMenuById(menuList, menuid) {
      for (var i = 0; i < menuList.length; i++) {
        var item = menuList[i];
        if (item.id && item.id === menuid) {
          return item
        } else if (item.children) {
          var foundItem = this.FindMenuById(item.children, menuid)
          if (foundItem) { // 只有找到才返回
            return foundItem
          }
        }
      }
    }

這裏值得注意的是,不能在遞歸的時候,使用下面直接返回


return this.FindMenuById(item.children, menuid)

而需要判斷是否有結果在進行返回,否則嵌套遞歸就可能返回undefined類型

  var foundItem = this.FindMenuById(item.children, menuid)
  if (foundItem) { // 只有找到才返回
    return foundItem
  }

 

3、forEach遍歷集合處理

在很多場合,我們也需要對集合進行一個forEach的遍歷處理,如下根據它的鍵值進行處理,註冊全局過濾器的處理操作

// 導入全局過濾器
import * as filters from './filters'
// 註冊全局過濾器
Object.keys(filters).forEach(key => {
  Vue.filter(key, filters[key])
})

或者我們在通過API方式獲取數據后,對集合進行處理的操作

    // 獲取產品類型,用於綁定字典等用途
    GetProductType().then(data => {
      if (data) {
        this.treedata = [];// 樹列表清空
        data.forEach(item => {
          this.productTypes.set(item.id, item.name)
          this.typeList.push({ key: item.id, value: item.name })

          var node = { id: item.id, label: item.name }
          this.treedata.push(node)
        })

        // 獲取列表信息
        this.getlist()
      }
    });

又或者請求字典數據的時候,進行一個非空值的判斷處理。

      // 使用字典類型,從服務器請求數據
      GetDictData(this.typeName).then(data => {
        if (data) {
          data.forEach(item => {
            if (item && typeof (item.Value) !== 'undefined' && item.Value !== '') {
              that.dictItems.push(item)
            }
          });
        }
      })

forEach()方法也是用於對數組中的每一個元素執行一次回調函數,但它沒有返回值(或者說它的返回值為undefined,即便我們在回調函數中寫了return語句,返回值依然為undefined)

注意: 如果forEach里有兩個參數,則第一個參數為該集合里的元素,第二個參數為集合的索引;

 

4、Object.assign賦值方法

在有些場合,我們需要把全新的集合,複製到另一個對象上,替換原來對象的屬性值,那麼我們可以利用Object對象的assign方法。

如在編輯界面展示的時候,把請求到的對象屬性複製到表單對象上。

      var param = { id: id }
      GetProductDetail(param).then(data => {
        Object.assign(this.editForm, data);
      })

或者查詢的時候,獲得查詢條件,進行部分替換

      // 構造常規的分頁查詢條件
      var param = {
        type: this.producttype === 'all' ? '' : this.producttype,
        pageindex: this.pageinfo.pageindex,
        pagesize: this.pageinfo.pagesize
      };

      // 把SearchForm的條件加入到param裏面,進行提交查詢
      param.type = this.searchForm.ProductType // 轉換為對應屬性
      Object.assign(param, this.searchForm);

 

5、slice() 方法

slice() 方法可從已有的數組中返回選定的元素。

語法如下所示。

arrayObject.slice(start,end)

如下案例所示。

let red = parseInt(color.slice(0, 2), 16)
let green = parseInt(color.slice(2, 4), 16)
let blue = parseInt(color.slice(4, 6), 16)

或者我們結合filter函數對圖標集合進行獲取部分處理

vueAwesomeIconsFiltered: function() {
  const that = this
  var list = that.vueAwesomeIcons.filter(item => { return item.indexOf(that.searchForm.label) >= 0 })
  if (that.searchForm.pagesize > 0) {
    return list.slice(0, that.searchForm.pagesize)
  } else {
    return list;
  }
}

 

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

【其他文章推薦】

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

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

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

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

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

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

全球最大煤業投資者貝萊德倒戈 宣布投資組合將排除煤

摘錄自2020年2月4日科技新報報導

許多投資機構在這樣的潮流下宣示不再投資碳排放量高的產業,其中以煤業為指標。貝萊德(BlackRock)原本是全球投資煤業規模最大的投資機構,如今倒戈加入全球 117 個宣示投資組合排除煤業的大型投資機構行列,宣示投資煤業不僅可能造成商譽受損,更可能直接造成財務風險。

貝萊德管理 7 兆美元資產,在煤業有 170 億美元,如今覺得煤業資產越來越像燙手山芋,2020 年 1 月,執行長  Larry Fink 撰寫公開信,表示氣候變遷已成為企業長期表現的決定因素,過去市場對此反應緩慢,但如今投資者越來越開始認知到氣候變遷對投資的影響。

在這樣的壓力下,貝萊德順應客戶,進行投資組合調整,將有相當龐大的資本重新配置,貝萊德已於 2020 年 1 月初加入「氣候行動百強」(Climate Action 100+)宣示加入綠色投資的潮流,如今更明確提出減碳投資方案,首先就是針對煤。

貝萊德轉向顯示,綠色投資成主流,不進行綠色投資、迴避高碳排資產的投資機構,將面臨客戶與市場越來越高的質疑,造成資金成本提升,直接造成財務風險。

環境經濟
循環經濟
國際新聞
氣候變遷
綠色金融
煤炭

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

【其他文章推薦】

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

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

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

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

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

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

「一帶一路」再受挫 泰國喊停中國湄公河開發計畫

摘錄自2020年2月6日聯合新聞網報導

泰國宣布停止由中國主導的湄公河開發計畫,理由是當地民眾反對,且北京不願提高經費在要開發的地區進行進一步調查。泰國政府副發言人特薩拉薩納庫爾(Trisulee Trisaranakul)表示,泰國政府內閣會議4日決定放棄這個項目。

路透報導,中國在2001年提出計畫,對湄公河進行疏浚,以方便大型貨船從中國的雲南省沿湄公河把商品運往泰國、老撾的港口和東南亞其他地區。

但這個計畫遭到湄公河沿岸的泰國社區民眾和環保人士的反對。他們擔心疏浚計畫會影響環境,而且只能讓中國獲利。一份泰國政府內閣的文件顯示,中國去年通知湄公河流沿岸的國家說,中國不再計畫繼續推動這個項目,可老撾和緬甸境內的疏浚工作依然繼續進行。

土地水文
生物多樣性
土地利用
國際新聞
泰國
湄公河
一帶一路
商業開發
水文

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

【其他文章推薦】

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

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

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

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

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

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

防疫比環保重要?新罕布什爾州暫時重推塑膠袋

摘錄自2020年03月22日中央通訊社美國報導

美國新罕布什爾州長蘇努努今天(22日)敦促州內民眾,將可重複使用的環保袋留在家中暫時不用。他說,為對抗武漢肺炎,在此過渡期,購物宜用店家提供的新塑膠袋或紙袋。

此舉主要是愈發擔心賣場員工面對店內人潮擁擠,加上新型冠狀病毒疾病(COVID-19,武漢肺炎)具高度傳染性,且可能附著於各種物件表面,增加感染風險。州長蘇努努(Christopher Sununu)今天推文說:「由於確認(武漢肺炎病毒)社區傳染,顧及賣場裝袋人員、雜貨商和顧客潛在風險,購物者將環保袋暫留家中,這很重要。」

本週在新英格蘭醫學期刊(New England Journal of Medicine)發表的研究顯示,武漢肺炎病毒可在空氣中存活數小時,而在不同的物件表面甚至可存活數天之久。

公害污染
污染治理
國際新聞
美國
環保袋
武漢肺炎
疫情下的食衣住行
廢棄物

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

【其他文章推薦】

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

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

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

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

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

※超省錢租車方案

※回頭車貨運收費標準

疫情衝擊泰國觀光業 大象賺不到生活費恐餓死

摘錄自2020年3月31日中央社報導

隨著武漢肺炎大流行讓觀光客銳減,倡議人士警告,在泰國旅遊業工作的許多飢餓並遭長時間鍊住的大象,可能會因沒有收入而餓死、被賣給動物園,或轉入非法伐木業。

在疫情爆發前,在泰國旅遊業工作的約2000頭大象生活已不容易,時常傳出虐待情事,好馴化牠們提供乘載服務或在動物秀上表演雜耍賺錢。但因全球旅遊業停擺,大象現在甚至賺不到一天必須的300公斤食物錢。

大象生態營與保育人士警告,再不獲得緊急援助,象群即將面臨飢餓及新的剝削威脅。泰國大象聯盟協會(Thai Elephant Alliance Association)主席提拉帕(Theerapat Trungprakan)表示,隨著病毒重挫泰國觀光業,約2000頭大象現在失業了。

國際新聞
泰國
大象
觀光業
動物與大環境變遷
武漢肺炎
展示動物
動物福利

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

【其他文章推薦】

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

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

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

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

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

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

※回頭車貨運收費標準

分解塑膠救星?科學家在垃圾場中發現吃聚氨酯的微生物

環境資訊中心綜合外電;姜唯 編譯;林大利 審校

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

【其他文章推薦】

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

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

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

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

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

※回頭車貨運收費標準

台中搬家公司費用怎麼算?

Java多線程之深入解析ThreadLocal和ThreadLocalMap

ThreadLocal概述

ThreadLocal是線程變量,ThreadLocal中填充的變量屬於當前線程,該變量對其他線程而言是隔離的。ThreadLocal為變量在每個線程中都創建了一個副本,那麼每個線程可以訪問自己內部的副本變量。

它具有3個特性:

  1. 線程併發:在多線程併發場景下使用。
  2. 傳遞數據:可以通過ThreadLocal在同一線程,不同組件中傳遞公共變量。
  3. 線程隔離:每個線程變量都是獨立的,不會相互影響。

在不使用ThreadLocal的情況下,變量不隔離,得到的結果具有隨機性。

public class Demo {
    private String variable;

    public String getVariable() {
        return variable;
    }

    public void setVariable(String variable) {
        this.variable = variable;
    }

    public static void main(String[] args) {
        Demo demo = new Demo();
        for (int i = 0; i < 5; i++) {
            new Thread(()->{
                demo.setVariable(Thread.currentThread().getName());
                System.out.println(Thread.currentThread().getName()+" "+demo.getVariable());
            }).start();
        }
    }
}

輸出結果:

Thread-2 Thread-2
Thread-4 Thread-4
Thread-1 Thread-2
Thread-0 Thread-2
Thread-3 Thread-3

View Code

在不使用ThreadLocal的情況下,變量隔離,每個線程有自己專屬的本地變量variable,線程綁定了自己的variable,只對自己綁定的變量進行讀寫操作。

public class Demo {
    private ThreadLocal<String> variable = new ThreadLocal<>();

    public String getVariable() {
        return variable.get();
    }

    public void setVariable(String variable) {
        this.variable.set(variable);
    }

    public static void main(String[] args) {
        Demo demo = new Demo();
        for (int i = 0; i < 5; i++) {
            new Thread(()->{
                demo.setVariable(Thread.currentThread().getName());
                System.out.println(Thread.currentThread().getName()+" "+demo.getVariable());
            }).start();
        }
    }
}

輸出結果:

Thread-0 Thread-0
Thread-1 Thread-1
Thread-2 Thread-2
Thread-3 Thread-3
Thread-4 Thread-4

View Code

synchronized和ThreadLocal的比較

上述需求,通過synchronized加鎖同樣也能實現。但是加鎖對性能和併發性有一定的影響,線程訪問變量只能排隊等候依次操作。TreadLocal不加鎖,多個線程可以併發對變量進行操作。

public class Demo {
    private String variable;
    public String getVariable() {
        return variable;
    }

    public void setVariable(String variable) {
        this.variable = variable;
    }

    public static void main(String[] args) {
        Demo demo = new Demo1();
        for (int i = 0; i < 5; i++) {
            new Thread(()->{
                synchronized (Demo.class){
                    demo.setVariable(Thread.currentThread().getName());
                    System.out.println(Thread.currentThread().getName()+" "+demo.getVariable());
                }
            }).start();
        }
    }
}

ThreadLocal和synchronized都是用於處理多線程併發訪問資源的問題。ThreadLocal是以空間換時間的思路,每個線程都擁有一份變量的拷貝,從而實現變量隔離,互相不干擾。關注的重點是線程之間數據的相互隔離關係。synchronized是以時間換空間的思路,只提供一個變量,線程只能通過排隊訪問。關注的是線程之間訪問資源的同步性。ThreadLocal可以帶來更好的併發性,在多線程、高併發的環境中更為合適一些。

ThreadLocal使用場景

轉賬事務的例子

JDBC對於事務原子性的控制可以通過setAutoCommit(false)設置為事務手動提交,成功后commit,失敗后rollback。在多線程的場景下,在service層開啟事務時用的connection和在dao層訪問數據庫的connection應該要保持一致,所以併發時,線程只能隔離操作自已的connection。

解決方案1:service層的connection對象作為參數傳遞給dao層使用,事務操作放在同步代碼塊中。

存在問題:傳參提高了代碼的耦合程度,加鎖降低了程序的性能。

解決方案2:當需要獲取connection對象的時候,通過ThreadLocal對象的get方法直接獲取當前線程綁定的連接對象使用,如果連接對象是空的,則去連接池獲取連接,並通過ThreadLocal對象的set方法綁定到當前線程。使用完之後調用ThreadLocal對象的remove方法解綁連接對象。

ThreadLocal的優勢:

  1. 可以方便地傳遞數據:保存每個線程綁定的數據,需要的時候可以直接獲取,避免了傳參帶來的耦合。
  2. 可以保持線程間隔離:數據的隔離在併發的情況下也能保持一致性,避免了同步的性能損失。

ThreadLocal的原理

每個ThreadLocal維護一個ThreadLocalMap,Map的Key是ThreadLocal實例本身,value是要存儲的值。

每個線程內部都有一個ThreadLocalMap,Map裏面存放的是ThreadLocal對象和線程的變量副本。Thread內部的Map通過ThreadLocal對象來維護,向map獲取和設置變量副本的值。不同的線程,每次獲取變量值時,只能獲取自己對象的副本的值。實現了線程之間的數據隔離。

JDK1.8的設計相比於之前的設計(通過ThreadMap維護了多個線程和線程變量的對應關係,key是Thread對象,value是線程變量)的好處在於,每個Map存儲的Entry數量變少了,線程越多鍵值對越多。現在的鍵值對的數量是由ThreadLocal的數量決定的,一般情況下ThreadLocal的數量少於線程的數量,而且並不是每個線程都需要創建ThreadLocal變量。當Thread銷毀時,ThreadLocal也會隨之銷毀,減少了內存的使用,之前的方案中線程銷毀后,ThreadLocalMap仍然存在。

ThreadLocal源碼解析

set方法

首先獲取線程,然後獲取線程的Map。如果Map不為空則將當前ThreadLocal的引用作為key設置到Map中。如果Map為空,則創建一個Map並設置初始值。

get方法

首先獲取當前線程,然後獲取Map。如果Map不為空,則Map根據ThreadLocal的引用來獲取Entry,如果Entry不為空,則獲取到value值,返回。如果Map為空或者Entry為空,則初始化並獲取初始值value,然後用ThreadLocal引用和value作為key和value創建一個新的Map。

 

remove方法

刪除當前線程中保存的ThreadLocal對應的實體entry。

initialValue方法

該方法的第一次調用發生在當線程通過get方法訪問線程的ThreadLocal值時。除非線程先調用了set方法,在這種情況下,initialValue才不會被這個線程調用。每個線程最多調用依次這個方法。

該方法只返回一個null,如果想要線程變量有初始值需要通過子類繼承ThreadLocal的方式去重寫此方法,通常可以通過匿名內部類的方式實現。這個方法是protected修飾的,是為了讓子類覆蓋而設計的。

ThreadLocalMap源碼分析

ThreadLocalMap是ThreadLocal的靜態內部類,沒有實現Map接口,獨立實現了Map的功能,內部的Entry也是獨立實現的。

與HashMap類似,初始容量默認是16,初始容量必須是2的整數冪。通過Entry類的數據table存放數據。size是存放的數量,threshold是擴容閾值。

 Entry繼承自WeakReference,key是弱引用,其目的是將ThreadLocal對象的生命周期和線程生命周期解綁。

弱引用和內存泄漏

內存溢出:沒有足夠的內存供申請者提供

內存泄漏:程序中已動態分配的堆內存由於某種原因程序未釋放或無法釋放,造成系統內存的浪費,導致程序運行速度減慢甚至系統崩潰等驗證后溝。內存泄漏的堆積會導致內存溢出。

弱引用:垃圾回收器一旦發現了弱引用的對象,不管內存是否足夠,都會回收它的內存。

內存泄漏的根源是ThreadLocalMap和Thread的生命周期是一樣長的。

如果在ThreadLocalMap的key使用強引用還是無法完全避免內存泄漏,ThreadLocal使用完后,ThreadLocal Reference被回收,但是Map的Entry強引用了ThreadLocal,ThreadLocal就無法被回收,因為強引用鏈的存在,Entry無法被回收,最後會內存泄漏。

在實際情況中,ThreadLocalMap中使用的key為ThreadLocal的弱引用,value是強引用。如果ThreadLocal沒有被外部強引用的話,在垃圾回收的時候,key會被清理,value不會。這樣ThreadLocalMap就出現了為null的Entry。如果不做任何措施,value永遠不會被GC回收,就會產生內存泄漏。

ThreadLocalMap中考慮到這個情況,在set、get、remove操作后,會清理掉key為null的記錄(將value也置為null)。使用完ThreadLocal后最後手動調用remove方法(刪除Entry)。

也就是說,使用完ThreadLocal后,線程仍然運行,如果忘記調用remove方法,弱引用比強引用可以多一層保障,弱引用的ThreadLocal會被回收,對應的value會在下一次ThreadLocalMap調用get、set、remove方法的時候被清除,從而避免了內存泄漏。

Hash衝突的解決

ThreadLocalMap的構造方法

構造函數創建一個長隊為16的Entry數組,然後計算firstKey的索引,存儲到table中,設置size和threshold。

firstKey.threadLocalHashCode & (INITIAL_CAPACITY-1)用來計算索引,nextHashCode是Atomicinteger類型的,Atomicinteger類是提供原子操作的Integer類,通過線程安全的方式來加減,適合高併發使用。

每次在當前值上加上一個HASH_INCREMENT值,這個值和斐波拉契數列有關,主要目的是為了讓哈希碼可以均勻的分佈在2的n次方的數組裡,從而盡量的避免衝突。

當size為2的冪次的時候,hashCode & (size – 1)相當於取模運算hashCode % size,位運算比取模更高效一些。為了使用這種取模運算, 所有size必須是2的冪次。這樣一來,在保證索引不越界的情況下,減少衝突的次數。

ThreadLocalMap的set方法

ThreadLocalMao使用了線性探測法來解決衝突。線性探測法探測下一個地址,找到空的地址則插入,若整個空間都沒有空餘地址,則產生溢出。例如:長度為8的數組中,當前key的hash值是6,6的位置已經被佔用了,則hash值加一,尋找7的位置,7的位置也被佔用了,回到0的位置。直到可以插入為止,可以將這個數組看成一個環形數組

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

【其他文章推薦】

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

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

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

※超省錢租車方案

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

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

※回頭車貨運收費標準

【Spring註解開發】組件註冊-使用@Configuration和@Bean給容器中註冊組件

寫在前面

在之前的Spring版本中,我們只能通過寫XML配置文件來定義我們的Bean,XML配置不僅繁瑣,而且很容易出錯,稍有不慎就會導致編寫的應用程序各種報錯,排查半天,發現是XML文件配置不對!另外,每個項目編寫大量的XML文件來配置Spring,也大大增加了項目維護的複雜度,往往很多個項目的Spring XML文件的配置大部分是相同的,只有很少量的配置不同,這也造成了配置文件上的冗餘。

項目工程源碼已經提交到GitHub:https://github.com/sunshinelyz/spring-annotation

Spring IOC和DI

在Spring容器的底層,最重要的功能就是IOC和DI,也就是控制反轉和依賴注入。

IOC:控制反轉,將類的對象的創建交給Spring類管理創建。
DI:依賴注入,將類裏面的屬性在創建類的過程中給屬性賦值。
DI和IOC的關係:DI不能單獨存在,DI需要在IOC的基礎上來完成。

在Spring內部,所有的組件都會放到IOC容器中,組件之間的關係通過IOC容器來自動裝配,也就是我們所說的依賴注入。接下來,我們就使用註解的方式來完成容器組件的註冊、管理及依賴、注入等功能。

在介紹使用註解完成容器組件的註冊、管理及依賴、注入等功能之前,我們先來看看使用XML文件是如何注入Bean的。

通過XML文件注入JavaBean

首先,我們在工程的io.mykit.spring.bean包下創建Person類,作為測試的JavaBean,代碼如下所示。

package io.mykit.spring.bean;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
import java.io.Serializable;

/**
 * @author binghe
 * @version 1.0.0
 * @description 測試實體類
 */
@Data
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class Person implements Serializable {
    private static final long serialVersionUID = 7387479910468805194L;
    private String name;
    private Integer age;
}

接下來,我們在工程的resources目錄下創建Spring的配置文件beans.xml,通過beans.xml文件將Person類注入到Spring的IOC容器中,配置如下所示。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id = "person" class="io.mykit.spring.bean.Person">
        <property name="name" value="binghe"></property>
        <property name="age" value="18"></property>
    </bean>
</beans>

到此,我們使用XML方式注入JavaBean就配置完成了。接下來,我們創建一個SpringBeanTest類來進行測試,這裏,我使用的是Junit進行測試,測試方法如下所示。

@Test
public void testXmlConfig(){
    ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
    Person person = (Person) context.getBean("person");
    System.out.println(person);
}

運行testXmlConfig()方法,輸出的結果信息如下。

Person(name=binghe, age=18)

從輸出結果中,我們可以看出,Person類通過beans.xml文件的配置,已經注入到Spring的IOC容器中了。

通過註解注入JavaBean

通過XML文件,我們可以將JavaBean注入到Spring的IOC容器中。那使用註解又該如何實現呢?別急,其實使用註解比使用XML文件要簡單的多,我們在項目的io.mykit.spring.plugins.register.config包下創建PersonConfig類,並在PersonConfig類上添加@Configuration註解來標註PersonConfig類是一個Spring的配置類,通過@Bean註解將Person類注入到Spring的IOC容器中。

package io.mykit.spring.plugins.register.config;

import io.mykit.spring.bean.Person;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author binghe
 * @version 1.0.0
 * @description 以註解的形式來配置Person
 */
@Configuration
public class PersonConfig {
     @Bean
    public Person person(){
        return new Person("binghe001", 18);
    }
}

沒錯,通過PersonConfig類我們就能夠將Person類注入到Spring的IOC容器中,是不是很Nice!!主要我們在類上加上@Configuration註解,並在方法上加上@Bean註解,就能夠將方法中創建的JavaBean注入到Spring的IOC容器中。

接下來,我們在SpringBeanTest類中創建一個testAnnotationConfig()方法來測試通過註解注入的Person類,如下所示。

@Test
public void testAnnotationConfig(){
    ApplicationContext context = new AnnotationConfigApplicationContext(PersonConfig.class);
    Person person = context.getBean(Person.class);
    System.out.println(person);
}

運行testAnnotationConfig()方法,輸出的結果信息如下所示。

Person(name=binghe001, age=18)

可以看出,通過註解將Person類注入到了Spring的IOC容器中。

到這裏,我們已經明確,通過XML文件和註解兩種方式都可以將JavaBean注入到Spring的IOC容器中。那麼,使用註解將JavaBean注入到IOC容器中時,使用的bean的名稱是什麼呢? 我們可以在testAnnotationConfig()方法中添加如下代碼來獲取Person類型下的註解名稱。

//按照類型找到對應的bean名稱數組
String[] names = context.getBeanNamesForType(Person.class);
Arrays.stream(names).forEach(System.out::println);

完整的testAnnotationConfig()方法的代碼如下所示。

@Test
public void testAnnotationConfig(){
    ApplicationContext context = new AnnotationConfigApplicationContext(PersonConfig.class);
    Person person = context.getBean(Person.class);
    System.out.println(person);

    //按照類型找到對應的bean名稱數組
    String[] names = context.getBeanNamesForType(Person.class);
    Arrays.stream(names).forEach(System.out::println);
}

運行testAnnotationConfig()方法輸出的結果信息如下所示。

Person(name=binghe001, age=18)
person

那這裏的person是啥?我們修改下PersonConfig類中的person()方法,將person()方法修改成person01()方法,如下所示。

package io.mykit.spring.plugins.register.config;

import io.mykit.spring.bean.Person;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author binghe
 * @version 1.0.0
 * @description 以註解的形式來配置Person
 */
@Configuration
public class PersonConfig {

    @Bean
    public Person person01(){
        return new Person("binghe001", 18);
    }
}

此時,我們再次運行testAnnotationConfig()方法,輸出的結果信息如下所示。

Person(name=binghe001, age=18)
person01

看到這裏,大家應該有種豁然開朗的感覺了,沒錯!!使用註解注入Javabean時,bean在IOC中的名稱就是使用@Bean註解標註的方法名稱。我們可不可以為bean單獨指定名稱呢?那必須可以啊!只要在@Bean註解中明確指定名稱就可以了。比如下面的PersonConfig類的代碼,我們將person01()方法上的@Bean註解修改成@Bean(“person”)註解,如下所示。

package io.mykit.spring.plugins.register.config;

import io.mykit.spring.bean.Person;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author binghe
 * @version 1.0.0
 * @description 以註解的形式來配置Person
 */
@Configuration
public class PersonConfig {

    @Bean("person")
    public Person person01(){
        return new Person("binghe001", 18);
    }
}

此時,我們再次運行testAnnotationConfig()方法,輸出的結果信息如下所示。

Person(name=binghe001, age=18)
person

可以看到,此時,輸出的JavaBean的名稱為person。

結論:我們在使用註解方式向Spring的IOC容器中注入JavaBean時,如果沒有在@Bean註解中明確指定bean的名稱,就使用當前方法的名稱來作為bean的名稱;如果在@Bean註解中明確指定了bean的名稱,則使用@Bean註解中指定的名稱來作為bean的名稱。

好了,咱們今天就聊到這兒吧!別忘了給個在看和轉發,讓更多的人看到,一起學習一起進步!!

項目工程源碼已經提交到GitHub:https://github.com/sunshinelyz/spring-annotation

寫在最後

如果覺得文章對你有點幫助,請微信搜索並關注「 冰河技術 」微信公眾號,跟冰河學習Spring註解驅動開發。公眾號回復“spring註解”關鍵字,領取Spring註解驅動開發核心知識圖,讓Spring註解驅動開發不再迷茫。

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

【其他文章推薦】

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

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

※回頭車貨運收費標準

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

※超省錢租車方案

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

※推薦台中搬家公司優質服務,可到府估價

容器技術之Dockerfile(三)

  前面我們聊到了dockerfile的 FROM、COPY 、ADD、LABEL、MAINTAINER、ENV、ARG、WORKDIR、VOLUME、EXPOSE、RUN、CMD、ENTRYPOINT指令的使用和說明,回顧請參考https://www.cnblogs.com/qiuhom-1874/tag/Dockerfile/;今天我們來聊聊剩下的dockerfile指令的使用和說明;

  1、USER:該指令用於指定運行image時的或運行dockerfile中任何RUN、CMD或ENTRYPOINT指令指定的程序時的用戶名或UID;默認情況下,container的運行身份為root用戶;語法格式 USER <UID>|<UserName>; 需要注意的是,<UID>可以為任意数字,但實踐中其必須為/etc/passwd中某用戶的有效UID,否則,docker run命令將運行失敗;

  示例: 

[root@node1 test]# cat Dockerfile 
FROM centos:7

LABEL maintainer="qiuhom <qiuhom@linux-1874.com>"

LABEL version="1.0"

LABEL description="this is test file \ that label-values can span multiple lines."

RUN useradd nginx

USER nginx

CMD ["sleep","3000"]

[root@node1 test]# 

  提示:以上dockerfile表示在鏡像運行成容器時,以nginx用戶運行 sleep 3000

  驗證:編譯成鏡像,啟動為容器,然後進入到容器里看看sleep 3000 是否是nginx用戶在運行?

[root@node1 test]# docker build . -t test:v1
Sending build context to Docker daemon  1.051MB
Step 1/7 : FROM centos:7
 ---> b5b4d78bc90c
Step 2/7 : LABEL maintainer="qiuhom <qiuhom@linux-1874.com>"
 ---> Running in 0f503dae4448
Removing intermediate container 0f503dae4448
 ---> d31363b96f38
Step 3/7 : LABEL version="1.0"
 ---> Running in 8dad05999903
Removing intermediate container 8dad05999903
 ---> 2281f36d7c3c
Step 4/7 : LABEL description="this is test file \ that label-values can span multiple lines."
 ---> Running in d2be9ed44aee
Removing intermediate container d2be9ed44aee
 ---> 8de872e222fb
Step 5/7 : RUN useradd nginx
 ---> Running in 37bda6ba6b60
Removing intermediate container 37bda6ba6b60
 ---> dc681f95f5ca
Step 6/7 : USER nginx
 ---> Running in 97d2357826f9
Removing intermediate container 97d2357826f9
 ---> ed277ac0c482
Step 7/7 : CMD ["sleep","3000"]
 ---> Running in 0ea578fa10bc
Removing intermediate container 0ea578fa10bc
 ---> 461f6ceabc88
Successfully built 461f6ceabc88
Successfully tagged test:v1
[root@node1 test]# docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
test                v1                  461f6ceabc88        3 seconds ago       204MB
centos              7                   b5b4d78bc90c        4 weeks ago         203MB
[root@node1 test]# docker run --name t1 --rm -d test:v1
37e46346d6ca0ab05b67f5350d4c2a7b6b86b8d34c8d1622d78ef70b7d3dff86
[root@node1 test]# docker ps 
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
37e46346d6ca        test:v1             "sleep 3000"        3 seconds ago       Up 2 seconds                            t1
[root@node1 test]# docker exec -it t1 /bin/bash
[nginx@37e46346d6ca /]$ ps aux
USER        PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
nginx         1  0.1  0.0   4364   352 ?        Ss   10:02   0:00 sleep 3000
nginx         6  0.4  0.0  11828  1808 pts/0    Ss   10:02   0:00 /bin/bash
nginx        23  0.0  0.0  51756  1708 pts/0    R+   10:02   0:00 ps aux
[nginx@37e46346d6ca /]$ exit
exit
[root@node1 test]#

  提示:可以看到基於上面的dockerfile構建的鏡像運行為容器,裏面默認跑的進程就是我們在dockerfile中指定用戶運行的進程;使用USER指定用戶運行容器里的進程,需要注意該用戶要對運行進程所需資源的所有權限;否則容器運行不起來;

  2、HEALTHCHECK:該指令用於定義如何對容器做健康狀態檢測;運行為容器后,容器里的進程不掛掉,當然容器也就不會掛掉,但是存在一種情況,容器沒有掛掉,容器里的進程無法正常提供服務了,這個時候我們就需要通過一定的手段,第一時間知道容器里的進程是否健康(是否能夠正常提供服務);healthcheck指令就是用來定義如果去檢測容器內部進程是否健康;語法格式HEALTHCHECK [OPTIONS] CMD command;其中CMD是固定格式,而後面的command是對容器里的進程做健康狀態檢查的命令;而options是用來指定對容器做健康狀態檢查的周期時間相關信息;–interval=DURATION (default: 30s),該選項用於指定對容器做健康狀態檢查的頻率,默認是30s一次;–timeout=DURATION (default: 30s),該選項用於指定對容器內部的進程做健康狀態檢查的超時時長,默認是30秒;–start-period=DURATION (default: 0s)指定對容器中的進程做健康狀態檢查延遲時間,默認0表示不延遲;這裏補充一點,之所以要延遲多少秒做健康狀態檢查是因為,docker運行為容器以後,會立刻把該容器的狀態標記為running狀態,而對於有些初始化比較慢的容器,如果馬上對它做健康狀態檢查,可能是不健康的狀態,這樣一來我們對了解容器是否健康就不是很準確了;如果配合某些工具,很可能存在檢測到容器不健康就把該容器刪除,然後重新創建,以此重複;這樣就會導致我們的容器啟動不起來; –retries=N (default: 3)表示指定對容器做健康狀態檢查的重試次數,默認是3次;也就是說檢查到容器不健康的前提或健康的前提,它都會檢查3次,如果3次檢查都是失敗狀態那麼就標記該容器不健康;而對於我們指定的命令來講,命令的返回值就決定了容器是否健康,通常命令返回值為0表示我們執行的命令正常退出,也就意味着容器是健康狀態;命令返回值為1表示容器不健康;返回值為2我們通常都是保留不使用;HEALTHCHECK NONE就表示不對容器做健康狀態檢查;

  示例:

[root@node1 test]# cat Dockerfile 
FROM centos:7

LABEL maintainer="qiuhom <qiuhom@linux-1874.com>"

LABEL version="1.0"

LABEL description="this is test file \ that label-values can span multiple lines."

RUN yum install -y httpd 

ADD ok.html /var/www/html/

CMD ["/usr/sbin/httpd","-DFOREGROUND"]

HEALTHCHECK --interval=5s --timeout=5s --start-period=5s --retries=2 \
        CMD curl -f http://localhost/ok.html || exit 1

[root@node1 test]# 

  提示:以上HEALTHCHECK指令表示每5秒檢查一次,超時時長為5秒,延遲5秒開始檢查,重試2次;如果curl -f http://localhost/ok.html這條命令正常返回0,那麼就表示容器健康,否則就返回1,表示容器不健康;

  驗證:把以上dockerfile構建成鏡像啟動為容器,我們把ok.html刪除或移動到別的目錄,看看容器是否標記為不健康?

[root@node1 test]# docker build . -t test:v1.1
Sending build context to Docker daemon  1.052MB
Step 1/8 : FROM centos:7
 ---> b5b4d78bc90c
Step 2/8 : LABEL maintainer="qiuhom <qiuhom@linux-1874.com>"
 ---> Using cache
 ---> d31363b96f38
Step 3/8 : LABEL version="1.0"
 ---> Using cache
 ---> 2281f36d7c3c
Step 4/8 : LABEL description="this is test file \ that label-values can span multiple lines."
 ---> Using cache
 ---> 8de872e222fb
Step 5/8 : RUN yum install -y httpd
 ---> Running in 9964718a2c3e
Loaded plugins: fastestmirror, ovl
Determining fastest mirrors
 * base: mirrors.bfsu.edu.cn
 * extras: mirrors.aliyun.com
 * updates: mirrors.aliyun.com
Resolving Dependencies
--> Running transaction check
---> Package httpd.x86_64 0:2.4.6-93.el7.centos will be installed
--> Processing Dependency: httpd-tools = 2.4.6-93.el7.centos for package: httpd-2.4.6-93.el7.centos.x86_64
--> Processing Dependency: system-logos >= 7.92.1-1 for package: httpd-2.4.6-93.el7.centos.x86_64
--> Processing Dependency: /etc/mime.types for package: httpd-2.4.6-93.el7.centos.x86_64
--> Processing Dependency: libaprutil-1.so.0()(64bit) for package: httpd-2.4.6-93.el7.centos.x86_64
--> Processing Dependency: libapr-1.so.0()(64bit) for package: httpd-2.4.6-93.el7.centos.x86_64
--> Running transaction check
---> Package apr.x86_64 0:1.4.8-5.el7 will be installed
---> Package apr-util.x86_64 0:1.5.2-6.el7 will be installed
---> Package centos-logos.noarch 0:70.0.6-3.el7.centos will be installed
---> Package httpd-tools.x86_64 0:2.4.6-93.el7.centos will be installed
---> Package mailcap.noarch 0:2.1.41-2.el7 will be installed
--> Finished Dependency Resolution

Dependencies Resolved

================================================================================
 Package             Arch          Version                    Repository   Size
================================================================================
Installing:
 httpd               x86_64        2.4.6-93.el7.centos        base        2.7 M
Installing for dependencies:
 apr                 x86_64        1.4.8-5.el7                base        103 k
 apr-util            x86_64        1.5.2-6.el7                base         92 k
 centos-logos        noarch        70.0.6-3.el7.centos        base         21 M
 httpd-tools         x86_64        2.4.6-93.el7.centos        base         92 k
 mailcap             noarch        2.1.41-2.el7               base         31 k

Transaction Summary
================================================================================
Install  1 Package (+5 Dependent packages)

Total download size: 24 M
Installed size: 32 M
Downloading packages:
warning: /var/cache/yum/x86_64/7/base/packages/apr-1.4.8-5.el7.x86_64.rpm: Header V3 RSA/SHA256 Signature, key ID f4a80eb5: NOKEY
Public key for apr-1.4.8-5.el7.x86_64.rpm is not installed
--------------------------------------------------------------------------------
Total                                              2.0 MB/s |  24 MB  00:12     
Retrieving key from file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7
Importing GPG key 0xF4A80EB5:
 Userid     : "CentOS-7 Key (CentOS 7 Official Signing Key) <security@centos.org>"
 Fingerprint: 6341 ab27 53d7 8a78 a7c2 7bb1 24c6 a8a7 f4a8 0eb5
 Package    : centos-release-7-8.2003.0.el7.centos.x86_64 (@CentOS)
 From       : /etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7
Running transaction check
Running transaction test
Transaction test succeeded
Running transaction
  Installing : apr-1.4.8-5.el7.x86_64                                       1/6 
  Installing : apr-util-1.5.2-6.el7.x86_64                                  2/6 
  Installing : httpd-tools-2.4.6-93.el7.centos.x86_64                       3/6 
  Installing : centos-logos-70.0.6-3.el7.centos.noarch                      4/6 
  Installing : mailcap-2.1.41-2.el7.noarch                                  5/6 
  Installing : httpd-2.4.6-93.el7.centos.x86_64                             6/6 
  Verifying  : mailcap-2.1.41-2.el7.noarch                                  1/6 
  Verifying  : apr-util-1.5.2-6.el7.x86_64                                  2/6 
  Verifying  : httpd-2.4.6-93.el7.centos.x86_64                             3/6 
  Verifying  : apr-1.4.8-5.el7.x86_64                                       4/6 
  Verifying  : httpd-tools-2.4.6-93.el7.centos.x86_64                       5/6 
  Verifying  : centos-logos-70.0.6-3.el7.centos.noarch                      6/6 

Installed:
  httpd.x86_64 0:2.4.6-93.el7.centos                                            

Dependency Installed:
  apr.x86_64 0:1.4.8-5.el7                                                      
  apr-util.x86_64 0:1.5.2-6.el7                                                 
  centos-logos.noarch 0:70.0.6-3.el7.centos                                     
  httpd-tools.x86_64 0:2.4.6-93.el7.centos                                      
  mailcap.noarch 0:2.1.41-2.el7                                                 

Complete!
Removing intermediate container 9964718a2c3e
 ---> a931e93eea06
Step 6/8 : ADD ok.html /var/www/html/
 ---> 97e61f41911d
Step 7/8 : CMD ["/usr/sbin/httpd","-DFOREGROUND"]
 ---> Running in e91ccdef90c2
Removing intermediate container e91ccdef90c2
 ---> 7c8af9bb7eb3
Step 8/8 : HEALTHCHECK --interval=5s --timeout=5s --start-period=5s --retries=2         CMD curl -f http://localhost/ok.html || exit 1
 ---> Running in 80682ab087d3
Removing intermediate container 80682ab087d3
 ---> aa53cba15046
Successfully built aa53cba15046
Successfully tagged test:v1.1
[root@node1 test]# docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
test                v1.1                aa53cba15046        8 seconds ago       312MB
test                v1                  461f6ceabc88        57 minutes ago      204MB
centos              7                   b5b4d78bc90c        4 weeks ago         203MB
[root@node1 test]# docker run --name t1 --rm -d test:v1.1
332590e683fcb29f60a28703548fce7aa83df715cbb840e1283472834867d6a1
[root@node1 test]# docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS                            PORTS               NAMES
332590e683fc        test:v1.1           "/usr/sbin/httpd -DF…"   3 seconds ago       Up 2 seconds (health: starting)                       t1
[root@node1 test]# docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS                   PORTS               NAMES
332590e683fc        test:v1.1           "/usr/sbin/httpd -DF…"   7 seconds ago       Up 6 seconds (healthy)                       t1
[root@node1 test]# 

  提示:可以看到基於我們寫的dockerfile構建的鏡像已經成功運行為容器,並且標記為healthy;接下來我們進入容器把ok.html幹掉,然後在看看容器是否標記為不健康狀態?

  提示:從上面的信息可以看到我們把ok.html移除后,容器狀態就變成不健康狀態了;我們再把ok.html還原到原有位置,看看容器是否會從不健康轉換為健康呢?

  提示:可以看到把ok.html還原到/var/www/html/目錄后,容器從不健康狀態變為了健康狀態;

  3、SHELL:該指令用於指定默認shell,該指令開始到下一個SHELL中間的命令都是SHELL指定的shell 運行,所以SHELL指令在dockerfile中可出現多次,後面的SHELL指令指定的shell會覆蓋前面所有SHELL指令指定的shell;默認在Linux上是[“/bin/sh”,”-c”]在Windows上述[“cmd”,”/s”,”/c”];SHELL指令必須是以json數組的格式定義;語法SHELL [“executable”, “parameters”];

  4、STOPSIGNAL:該指令用於定義停止容器的信號;默認停止容器是15號信號 SIGTERM;語法STOPSIGNAL signal

  5、ONBUILD:該指令用於在Dockerfile中定義一個觸發器;Dockerfile用於build映像文件,此映像文件亦可作為base image被另一個Dockerfile用作FROM指令的參數,並以之構建新的映像文件;在後面的這個Dockerfile中的FROM指令在build過程中被執行時,將會“觸發”創建其base image的Dockerfile文件中的ONBUILD指令定義的觸發器;用法格式ONBUILD <INSTRUCTION>;儘管任何指令都可註冊成為觸發器指令,但ONBUILD不能自我嵌套,且不會觸發FROM和MAINTAINER指令;使用包含ONBUILD指令的Dockerfile構建的鏡像應該使用特殊的標籤,例如ruby:2.0-onbuild;在ONBUILD指令中使用ADD或COPY指令應該格外小心,因為新構建過程的上下文在缺少指定的源文件時會失敗;

  示例:

[root@node1 test]# cat Dockerfile
FROM centos:7

LABEL maintainer="qiuhom <qiuhom@linux-1874.com>"

ONBUILD RUN yum install -y httpd




[root@node1 test]# 

  提示:以上dockerfile表示在本次構建鏡像中不運行yum install -y httpd這條命令,而是在後面的dockerfile中以本dockerfile製作的進行作為基礎繼續時,yum install -y httpd這條命令就會被觸發執行;簡單講onbuild就是指定dockerfile指令延遲執行;這裏一定要記住一點onbuild指令後面一定是跟的是dockerfile指令;

  驗證:將上面的dockerfile編譯鏡像,看看yum install -y httpd 是否執行了?

[root@node1 test]# docker build . -t test:v1.5
Sending build context to Docker daemon  1.052MB
Step 1/3 : FROM centos:7
 ---> b5b4d78bc90c
Step 2/3 : LABEL maintainer="qiuhom <qiuhom@linux-1874.com>"
 ---> Using cache
 ---> d31363b96f38
Step 3/3 : ONBUILD RUN yum install -y httpd
 ---> Running in d3601fa1c3b7
Removing intermediate container d3601fa1c3b7
 ---> 370e3a843c3c
Successfully built 370e3a843c3c
Successfully tagged test:v1.5
[root@node1 test]# 

  提示:可以看到yum install -y httpd 這條命令並沒有執行;

  驗證:將我們上面製作好的鏡像作為基礎鏡像,再來製作其他鏡像,看看yum install -y httpd 被執行?

[root@node1 aaa]# pwd
/root/test/aaa
[root@node1 aaa]# ls
Dockerfile
[root@node1 aaa]# cat Dockerfile 
FROM test:v1.5

LABEL maintainer="qiuhom <admin@admin.com>"
[root@node1 aaa]# docker build . -t myweb:v1
Sending build context to Docker daemon  2.048kB
Step 1/2 : FROM test:v1.5
# Executing 1 build trigger
 ---> Running in cf93e9f03e89
Loaded plugins: fastestmirror, ovl
Determining fastest mirrors
 * base: mirrors.huaweicloud.com
 * extras: mirrors.aliyun.com
 * updates: mirrors.aliyun.com
Resolving Dependencies
--> Running transaction check
---> Package httpd.x86_64 0:2.4.6-93.el7.centos will be installed
--> Processing Dependency: httpd-tools = 2.4.6-93.el7.centos for package: httpd-2.4.6-93.el7.centos.x86_64
--> Processing Dependency: system-logos >= 7.92.1-1 for package: httpd-2.4.6-93.el7.centos.x86_64
--> Processing Dependency: /etc/mime.types for package: httpd-2.4.6-93.el7.centos.x86_64
--> Processing Dependency: libaprutil-1.so.0()(64bit) for package: httpd-2.4.6-93.el7.centos.x86_64
--> Processing Dependency: libapr-1.so.0()(64bit) for package: httpd-2.4.6-93.el7.centos.x86_64
--> Running transaction check
---> Package apr.x86_64 0:1.4.8-5.el7 will be installed
---> Package apr-util.x86_64 0:1.5.2-6.el7 will be installed
---> Package centos-logos.noarch 0:70.0.6-3.el7.centos will be installed
---> Package httpd-tools.x86_64 0:2.4.6-93.el7.centos will be installed
---> Package mailcap.noarch 0:2.1.41-2.el7 will be installed
--> Finished Dependency Resolution

Dependencies Resolved

================================================================================
 Package             Arch          Version                    Repository   Size
================================================================================
Installing:
 httpd               x86_64        2.4.6-93.el7.centos        base        2.7 M
Installing for dependencies:
 apr                 x86_64        1.4.8-5.el7                base        103 k
 apr-util            x86_64        1.5.2-6.el7                base         92 k
 centos-logos        noarch        70.0.6-3.el7.centos        base         21 M
 httpd-tools         x86_64        2.4.6-93.el7.centos        base         92 k
 mailcap             noarch        2.1.41-2.el7               base         31 k

Transaction Summary
================================================================================
Install  1 Package (+5 Dependent packages)

Total download size: 24 M
Installed size: 32 M
Downloading packages:
warning: /var/cache/yum/x86_64/7/base/packages/apr-1.4.8-5.el7.x86_64.rpm: Header V3 RSA/SHA256 Signature, key ID f4a80eb5: NOKEY
Public key for apr-1.4.8-5.el7.x86_64.rpm is not installed
--------------------------------------------------------------------------------
Total                                              7.2 MB/s |  24 MB  00:03     
Retrieving key from file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7
Importing GPG key 0xF4A80EB5:
 Userid     : "CentOS-7 Key (CentOS 7 Official Signing Key) <security@centos.org>"
 Fingerprint: 6341 ab27 53d7 8a78 a7c2 7bb1 24c6 a8a7 f4a8 0eb5
 Package    : centos-release-7-8.2003.0.el7.centos.x86_64 (@CentOS)
 From       : /etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7
Running transaction check
Running transaction test
Transaction test succeeded
Running transaction
  Installing : apr-1.4.8-5.el7.x86_64                                       1/6 
  Installing : apr-util-1.5.2-6.el7.x86_64                                  2/6 
  Installing : httpd-tools-2.4.6-93.el7.centos.x86_64                       3/6 
  Installing : centos-logos-70.0.6-3.el7.centos.noarch                      4/6 
  Installing : mailcap-2.1.41-2.el7.noarch                                  5/6 
  Installing : httpd-2.4.6-93.el7.centos.x86_64                             6/6 
  Verifying  : mailcap-2.1.41-2.el7.noarch                                  1/6 
  Verifying  : apr-util-1.5.2-6.el7.x86_64                                  2/6 
  Verifying  : httpd-2.4.6-93.el7.centos.x86_64                             3/6 
  Verifying  : apr-1.4.8-5.el7.x86_64                                       4/6 
  Verifying  : httpd-tools-2.4.6-93.el7.centos.x86_64                       5/6 
  Verifying  : centos-logos-70.0.6-3.el7.centos.noarch                      6/6 

Installed:
  httpd.x86_64 0:2.4.6-93.el7.centos                                            

Dependency Installed:
  apr.x86_64 0:1.4.8-5.el7                                                      
  apr-util.x86_64 0:1.5.2-6.el7                                                 
  centos-logos.noarch 0:70.0.6-3.el7.centos                                     
  httpd-tools.x86_64 0:2.4.6-93.el7.centos                                      
  mailcap.noarch 0:2.1.41-2.el7                                                 

Complete!
Removing intermediate container cf93e9f03e89
 ---> a89914bda4b5
Step 2/2 : LABEL maintainer="qiuhom <admin@admin.com>"
 ---> Running in e175e0542b5e
Removing intermediate container e175e0542b5e
 ---> 4f406abeaab7
Successfully built 4f406abeaab7
Successfully tagged myweb:v1
[root@node1 aaa]#

  提示:可以看到在我們的dockerfile中並沒有寫 RUN  yum install -y httpd  ,但build時卻執行了 yum install -y httpd ;這是因為onbuild指令被觸發了;我們可以理解為如果我們製作的鏡像有onbuild指令指定的命令,那麼該鏡像被其他dockerfile 作為基礎鏡像時(或者被其他docker FROM指令引用時)onbuild指定就會被激活,被執行;

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

【其他文章推薦】

※超省錢租車方案

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

※回頭車貨運收費標準

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

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

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

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

“造輪運動”之 ORM框架系列(三)~ 乾貨呈上

   這一趴裏面,我就來正式介紹一下CoffeeSQL的乾貨。

    首先要給CoffeeSQL來個定位:最開始就是由於本人想要了解ORM框架內部的原理,所以就四處搜尋有關的博客與學習資料,就是在那個夏天,在博客園上看到了一位7tiny老哥的博客(https://www.cnblogs.com/7tiny/p/9575230.html),裏面基本上包含了我所想要了解的全套內容。幸得7tiny老哥的博客和代碼都寫的非常清晰,所以沒花多久時間就看完了源碼並洞悉其中奧妙,於是自己就有個想法:在7tiny的開源代碼的基礎上歸納自己的ORM框架。於是出於學習與自我使用的目的就開始了擴展功能的道路,到現在為止,自己已經在公司的一個項目中用上了,效果還不錯。在這裏也感謝7tiny老哥對我提出的一些問題及時的回復和指導,真心感謝。

一、框架模塊介紹

  根據CoffeeSQL的功能模塊組成來劃分,可以分為:數據庫連接管理、SQL命令執行入口、SQL命令生成器、SQL查詢引擎、ORM緩存機制、實體數據驗證 這六個部分,CoffeeSQL的操作入口與其他的ORM框架一樣,都是以數據庫上下文(DBContext)的方式進行操作。整體結構圖如下:

 

下面就大致地介紹一下每一個模塊的具體功能與實現的思路:

1、數據庫連接管理(DBConnectionManagement)

   數據庫連接的管理實際上就是對數據庫連接字符串與其對應的數據庫連接對象的管理機制,它可以保證在進行一主多從的數據庫部署時ORM幫助我們自動地切換連接的數據庫,而且還支持 <最小使用>與 <輪詢>兩種數據庫連接切換策略。

 

2、SQL命令執行入口(QueryExecute)

   QueryExecute是CoffeeSQL生成的所有sql語句執行的入口,執行sql語句並返回結果,貫穿整個CoffeeSQL最核心的功能就是映射sql查詢結果到實體,這裏採用的是構建表達式樹的技術,性能大大優於反射獲取實體的方式,具體的兩者速度對比的實驗在7tiny的博客中有詳細介紹,大家可以移步觀看(https://www.cnblogs.com/7tiny/p/9861166.html),在我的博客(https://www.cnblogs.com/MaMaNongNong/p/12173620.html)中我使用表達式樹的技術造了個簡練版的OOM框架。

   這裏貼出核心代碼,方便查看:

   

  1     /// <summary>
  2     /// Auto Fill Adapter
  3     /// => Fill DataRow to Entity
  4     /// </summary>
  5     public class EntityFillAdapter<Entity>
  6     {
  7         private static readonly Func<DataRow, Entity> funcCache = GetFactory();
  8 
  9         public static Entity AutoFill(DataRow row)
 10         {
 11             return funcCache(row);
 12         }
 13 
 14         private static Func<DataRow, Entity> GetFactory()
 15         {
 16             #region get Info through Reflection
 17             var entityType = typeof(Entity);
 18             var rowType = typeof(DataRow);
 19             var convertType = typeof(Convert);
 20             var typeType = typeof(Type);
 21             var columnCollectionType = typeof(DataColumnCollection);
 22             var getTypeMethod = typeType.GetMethod("GetType", BindingFlags.Static | BindingFlags.Public, null, new[] { typeof(string) }, null);
 23             var changeTypeMethod = convertType.GetMethod("ChangeType", BindingFlags.Static | BindingFlags.Public, null, new[] { typeof(object), typeof(Type) }, null);
 24             var containsMethod = columnCollectionType.GetMethod("Contains");
 25             var rowIndexerGetMethod = rowType.GetMethod("get_Item", BindingFlags.Instance | BindingFlags.Public, null, new[] { typeof(string) }, new[] { new ParameterModifier(1) });
 26             var columnCollectionIndexerGetMethod = columnCollectionType.GetMethod("get_Item", BindingFlags.Instance | BindingFlags.Public, null, new[] { typeof(int) }, new[] { new ParameterModifier(1) });
 27             var entityIndexerSetMethod = entityType.GetMethod("set_Item", BindingFlags.Instance | BindingFlags.NonPublic, null, new[] { typeof(string), typeof(object) }, null);
 28             var properties = entityType.GetProperties(BindingFlags.Instance | BindingFlags.Public);
 29             #endregion
 30 
 31             #region some Expression class that can be repeat used
 32             //DataRow row
 33             var rowDeclare = Expression.Parameter(rowType, "row");
 34             //Student entity
 35             var entityDeclare = Expression.Parameter(entityType, "entity");
 36             //Type propertyType
 37             var propertyTypeDeclare = Expression.Parameter(typeof(Type), "propertyType");
 38             //new Student()
 39             var newEntityExpression = Expression.New(entityType);
 40             //row == null
 41             var rowEqualnullExpression = Expression.Equal(rowDeclare, Expression.Constant(null));
 42             //row.Table.Columns
 43             var rowTableColumns = Expression.Property(Expression.Property(rowDeclare, "Table"), "Columns");
 44             //int loopIndex
 45             var loopIndexDeclare = Expression.Parameter(typeof(int), "loopIndex");
 46             //row.Table.Columns[loopIndex].ColumnName
 47             var columnNameExpression = Expression.Property(Expression.Call(rowTableColumns, columnCollectionIndexerGetMethod, loopIndexDeclare), "ColumnName");
 48             //break;
 49             LabelTarget labelBreak = Expression.Label();
 50             //default(Student)
 51             var defaultEntityValue = Expression.Default(entityType);
 52             #endregion
 53 
 54             var setRowNotNullBlockExpressions = new List<Expression>();
 55                         
 56             #region entity = new Student();loopIndex = 0;
 57             setRowNotNullBlockExpressions.Add(Expression.Assign(entityDeclare, newEntityExpression));
 58             setRowNotNullBlockExpressions.Add(Expression.Assign(loopIndexDeclare, Expression.Constant(0)));
 59 
 60             #endregion
 61 
 62             #region loop Fill DataRow's field to Entity Indexer
 63             /*
 64              * while (true)
 65              * {
 66              *     if (loopIndex < row.Table.Columns.Count)
 67              *     {
 68              *         entity[row.Table.Columns[loopIndex].ColumnName] = row[row.Table.Columns[loopIndex].ColumnName];
 69              *         loopIndex++;
 70              *     }
 71              *     else break;
 72              * } 
 73              */
 74 
 75             setRowNotNullBlockExpressions.Add(
 76 
 77                 Expression.Loop(
 78                     Expression.IfThenElse(
 79                         Expression.LessThan(loopIndexDeclare, Expression.Property(rowTableColumns, "Count")),
 80                         Expression.Block(
 81                             Expression.Call(entityDeclare, entityIndexerSetMethod, columnNameExpression, Expression.Call(rowDeclare, rowIndexerGetMethod, columnNameExpression)),
 82                             Expression.PostIncrementAssign(loopIndexDeclare)
 83                         ),
 84                         Expression.Break(labelBreak)
 85                     ),
 86                     labelBreak
 87                 )
 88             );
 89             #endregion
 90 
 91             #region assign for Entity property
 92             foreach (var propertyInfo in properties)
 93             {
 94                 var columnAttr = propertyInfo.GetCustomAttribute(typeof(ColumnAttribute), true) as ColumnAttribute;
 95 
 96                 // no column , no translation
 97                 if (null == columnAttr) continue;
 98 
 99                 if (propertyInfo.CanWrite)
100                 {
101                     var columnName = Expression.Constant(columnAttr.GetName(propertyInfo.Name), typeof(string));
102 
103                     //entity.Id
104                     var propertyExpression = Expression.Property(entityDeclare, propertyInfo);
105                     //row["Id"]
106                     var value = Expression.Call(rowDeclare, rowIndexerGetMethod, columnName);
107                     //default(string)
108                     var defaultValue = Expression.Default(propertyInfo.PropertyType);
109                     //row.Table.Columns.Contains("Id")
110                     var checkIfContainsColumn = Expression.Call(rowTableColumns, containsMethod, columnName);
111                     //!row["Id"].Equals(DBNull.Value)
112                     var checkDBNull = Expression.NotEqual(value, Expression.Constant(System.DBNull.Value));
113                     
114                     var propertyTypeName = Expression.Constant(propertyInfo.PropertyType.ToString(), typeof(string));
115 
116                     /*
117                      * if (row.Table.Columns.Contains("Id") && !row["Id"].Equals(DBNull.Value))
118                      * {
119                      *     propertyType = Type.GetType("System.String");
120                      *     entity.Id = (string)Convert.ChangeType(row["Id"], propertyType);
121                      * }
122                      * else
123                      *     entity.Id = default(string);
124                      */
125                     setRowNotNullBlockExpressions.Add(
126 
127                         Expression.IfThenElse(
128                             Expression.AndAlso(checkIfContainsColumn, checkDBNull),
129                             Expression.Block(
130                                 Expression.Assign(propertyTypeDeclare, Expression.Call(getTypeMethod, propertyTypeName)),
131                                 Expression.Assign(propertyExpression, Expression.Convert(Expression.Call(changeTypeMethod, value, propertyTypeDeclare), propertyInfo.PropertyType))
132                             ),
133                             Expression.Assign(propertyExpression, defaultValue)
134                         )
135                     );
136                 }
137             }
138 
139             #endregion
140 
141             var checkIfRowIsNull = Expression.IfThenElse(
142                 rowEqualnullExpression,
143                 Expression.Assign(entityDeclare, defaultEntityValue),
144                 Expression.Block(setRowNotNullBlockExpressions)
145             );
146 
147             var body = Expression.Block(
148 
149                 new[] { entityDeclare, loopIndexDeclare, propertyTypeDeclare },
150                 checkIfRowIsNull,
151                 entityDeclare   //return Student;
152             );
153 
154             return Expression.Lambda<Func<DataRow, Entity>>(body, rowDeclare).Compile();
155         }
156     }
157 
158     #region
159     //public class Student : EntityDesign.EntityBase
160     //{
161     //    [Column]
162     //    public string Id { get; set; }
163 
164     //    [Column("StudentName")]
165     //    public string Name { get; set; }
166     //}
167     ////this is the template of "GetFactory()" created.
168     //public static Student StudentFillAdapter(DataRow row)
169     //{
170     //    Student entity;
171     //    int loopIndex;
172     //    Type propertyType;
173 
174     //    if (row == null)
175     //        entity = default(Student);
176     //    else
177     //    {
178     //        entity = new Student();
179     //        loopIndex = 0;
180 
181     //        while (true)
182     //        {
183     //            if (loopIndex < row.Table.Columns.Count)
184     //            {
185     //                entity[row.Table.Columns[loopIndex].ColumnName] = row[row.Table.Columns[loopIndex].ColumnName];
186     //                loopIndex++;
187     //            }
188     //            else break;
189     //        }
190 
191     //        if (row.Table.Columns.Contains("Id") && !row["Id"].Equals(DBNull.Value))
192     //        {
193     //            propertyType = Type.GetType("System.String");
194     //            entity.Id = (string)Convert.ChangeType(row["Id"], propertyType);
195     //        }
196     //        else
197     //            entity.Id = default(string);
198 
199     //        if (row.Table.Columns.Contains("StudentName") && !row["StudentName"].Equals(DBNull.Value))
200     //        {
201     //            propertyType = Type.GetType("System.String");
202     //            entity.Name = (string)Convert.ChangeType(row["StudentName"], propertyType);
203     //        }
204     //        else
205     //            entity.Name = default(string);
206     //    }
207 
208     //    return entity;
209     //}
210     #endregion

EntityFillAdapter(表達式樹技術)

 

3、SQL查詢引擎(QueryEngine)

  SQL查詢引擎的功能主要就是以函數的形式來構建查詢SQL的結構。將sql語句使用高級語言的函數來進行構建能大大減輕程序員必須一絲不苟編寫sql語句的壓力。特別是在使用強類型查詢引擎時以Lambda表達式的方式編寫程序,相當舒適的體驗;對於稍微複雜的sql,建議使用弱類型查詢引擎來構建sql查詢語句,同時也提供方便的分頁功能,用法與Dapper類似;再複雜一點的數據庫查詢邏輯可能你就要考慮使用存儲過程查詢引擎了,總之,有了這三個查詢引擎,所有的查詢需求都能滿足了。最後一個是update的執行引擎,它被用來構建update的語句。

 

4、實體數據驗證(EntityValidation)

  實體數據驗證是完全獨立的一部分,主要用來檢驗實體類中字段值的合法性,相當於在高級語言層面對即將持久化到數據庫表中的數據進行預先的字段合法性校驗,避免在持久化過程中發生不必要的字段格式不合法的錯誤。

 

5、ORM緩存機制(ORMCache)

  這裏的ORM緩存主要分為兩級緩存,一級緩存為以sql語句為緩存鍵的緩存,緩存的內容就是當前執行的sql語句的執行結果;而二級緩存則是以表名為緩存鍵的表緩存,就是會把一整個表的數據全部存入緩存中,所以表緩存最適合那些數據量不大且查詢頻繁的表

 

6、SQL命令生成器【強類型】(CommandTextGenerator)

  在使用諸如強類型查詢引擎、Update執行引擎等進行了強類型的SQL語句構造后,相應的sql構造信息都要通過SQL命令生成器來生成最終可由數據庫執行的sql語句。SQL命令生成器扮演的就是類似於翻譯官的角色,將高級語言中的語句轉化為數據庫中的sql語句。在實際的應用場景中還可以根據不同的數據庫類型將SQL命令生成器擴展成諸如Mysql-SQL命令生成器或者Oracle-SQL命令生成器以符合不同類型數據庫的不同sql語法。

 

7、數據庫上下文(DBContext)

  作為整個CoffeeSQL的操作入口,DBContext類涵蓋了各種配置參数字段與增刪改查的API調用函數。其中在事務處理中,由於寫操作都是通過對主庫的操作,所以在事務處理中是以主庫作為事務處理的對象。

二、使用方式

  下載CoffeeSql源碼進行編譯,你會得到 CoffeeSql.Core.dll、CoffeeSql.Oracle.dll、CoffeeSql.Mysql.dll 三個dll文件,其中CoffeeSql.Core.dll為必選,然後根據你的數據庫類型選擇是CoffeeSql.Oracle.dll或者CoffeeSql.Mysql.dll,目前還只支持這兩種數據庫,後續會支持更多數據庫。

 

 

三、展望

  路漫漫其修遠兮,吾將上下而求索,對比市面上火熱的ORM框架,CoffeeSQL還是缺少了一些實用的功能,對這個ORM框架的展望中我會考慮以下一些功能:

    1、CodeFirst、DbFirst功能的支持,可以快捷方便地進行實體類與數據庫建表sql的生成;

    2、批量插入操作的實現,可以提高批量插入數據的性能;

    3、對多表聯合查詢的lambda語法支持;

  

  介紹的再多都不如讀一遍源碼來的實在,有想深入了解orm原理的小夥伴可以閱讀一下源碼,真的SO EASY!

   源碼地址:https://gitee.com/xiaosen123/CoffeeSqlORM

   本文為作者原創,轉載請註明出處:https://www.cnblogs.com/MaMaNongNong/p/12896787.html

 

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

【其他文章推薦】

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

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

※回頭車貨運收費標準

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

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

台中搬家公司教你幾個打包小技巧,輕鬆整理裝箱!

台中搬家公司費用怎麼算?