起站福島 東奧聖火傳遞路線輻射量仍超標

整理:鄒敏惠(環境資訊中心記者)

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

【其他文章推薦】

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

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

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

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

※專營大陸快遞台灣服務

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

再生能源將取代石油、電動車將讓油價再次崩跌?從技術觀點分析其可能性

彭博(Bloomberg)於 2015 年 10 月報導稱,替代能源與節能科技發威,全球將步入電力富足年代;2016 年 2 月更進一步報導稱,電動車將造成下個石油崩跌危機,甚至指出石化燃料會愈來愈不重要、再生能源價格會持續下降、人類的能源不再是問題?但這些可能成真嗎?

原文中有許多樂觀的預測,包括:石化燃料生產成本低,原油價格將為每桶 50 美元而非 90 美元、10 年內太陽能的價格就能和石化燃料競爭、電動車將讓石油走上末路。最後的結論是:科技不斷創新,未來有望步入「電力富足年代」,電動車會造成下一次油價崩跌。   不過真會如此順利嗎?就現實的技術面來考量,可能未必。因為再生能源發展最大的困難在於成本太高、穩定度太低,目前都必須依靠政策補貼,否則難在市場中競爭,依照目前的技術發展現況,恐怕未來 10 年仍然難以改變,以下分別從技術觀點把目前市場上最被看好的幾種再生能源做簡單的介紹分析,並簡述其取代傳統能源的困難點。  
太陽電池與模組:價格已經接近成本價,難有大幅下降空間   太陽電池是目前最成熟的再生能源,上中下游已經形成完整的產業鏈,其中單晶矽晶圓能量轉換效率較高,但是長單晶的成本也高;多晶矽薄片能量轉換效率有待提升,成本仍然不夠低;非晶矽薄膜能量轉換效率太低,許多廠商開發多年仍然沒有進展,甚至有的已經倒閉。   因此說「10 年之內,太陽能的價格就能和石化燃料競爭」恐怕是不可能的事,目前的太陽電池與模組價格已經接近成本價,再降就得虧錢賣,即使廠商願意流血殺價競爭,一個虧錢賣的產業能支持多久呢?未來太陽能價格的確有機會和石化燃料競爭,但原因不是太陽能價格下跌,而是石化燃料價格上漲。  
生質能:成本過高,更會影響糧食供應   「生質能(Biomass energy)」是指利用生物所產生的有機物質,來轉換成可以利用的電能或熱能,目前主要是以動物或植物所提煉出來的油脂,經由轉脂化反應產生的「生質柴油(Biodiesel)」,麻瘋樹果實提煉的生質柴油是目前被業界認為最可行的方法之一。   此外,以植物(醣類)經由微生物發酵反應產生的「生質酒精(Bioethanol)」,玉米與甘蔗提煉的生質酒精是目前被業界認為最可行的方法之一,但是這些生質能的價格仍然遠高於石化燃料,而且一旦大量使用一定會影響到糧食供應,所以有人說:生質能是搶窮人的糧食做為富人的燃料(對生質能的原理有興趣的人可以參考這裡)。  
鋰電池:成熟商品,但充電是個大問題   可移動的能源除了生質能,使用鋰電池是目前被認為最可行的方法,由於鋰電池的能量密度夠高,因此才有美國 Tesla 公司設計與製造的電動汽車與台灣 Gogoro 公司設計與製造的電動機車。   鋰電池如何充電是個大問題,因為電動汽車的電池重達 200 公斤,很難由使用者如 Gogoro 機車那樣自行更換充電,只能以定點充電的方式進行,即使目前的規格要求在 1 小時內完成充電,使用者是否能在加「電」站等 1 小時卻是個問題。   如果必須把車開回家在停車場充電,最大的問題是目前的電力基礎建設不足,假設大樓停車場有 100 個停車位,每個都設置插座,當 100 輛電動車同時充電時,大樓的變壓器無法承受如此巨大的電流,整個電力基礎建設,包括變壓器、變電所、高壓電塔都必須重新設計才能達成,聽起來就不是短期內可以做到的事,就更別說這些電還是來自發電廠,結局還是要用火力、核能、水力發電來提供電力的。  
氫能與燃料電池:終極環保的再生能源?氫氣來源是個大問題   傳統電池直接使用化學反應產生能量,優點是能量轉換效率很高(80% 以上),但充電需要比較長的時間;而使用燃料以內燃機(汽車引擊)進行燃燒反應產生能量,優點是可以直接補充燃料,但使用內燃機的能量轉換效率很低(30% 以下),科學家開始思考,有沒有一種方法同時具有「電池」與「燃料」的優點呢?於是燃料電池就此誕生了。   燃料電池和傳統電池的原理相同,都是將活性物質的化學能轉換成電能,但是傳統電池的電極本身是活性物質,會參與化學反應;而燃料電池的電極本身只是儲存容器而已,並不會參與化學反應(觸媒只用來引發化學反應),必須將活性物質(氫氣或含氫原子的化合物)加入電池內(就好像我們的汽車補充燃料一樣),才能產生化學反應形成電能,是一種要補充燃料的電池,故稱為「燃料電池(Fuel cell)」。   燃料電池使用氫氣與氧氣反應產生水,反應後排放的氮化物或硫化物極少,幾乎沒有任何污染,因此被視為終極環保的再生能源,但是燃料電池必須使用氫氣做為燃料,卻成為最大的問題。   高壓儲氫技術如何把又大又重又危險的氫氣鋼瓶放在車上是個大問題;因此有國外公司開發出可以承受 700 大氣壓的航太複合材料儲氫瓶,能取代氫氣鋼瓶;Toyota 公司更在去年推出氫燃料電池車款 Mirai 創下單次加滿氫氣可行駛 500 公里的紀錄,已經是成功的商品了,那麼它的問題到底在那裡呢?  

▲ 複合材料儲氫瓶。(Source:)  

▲ 氫燃料電池車款 Mirai。(Source: CC BY 2.0)   首先車上放了一個壓力這麼大的儲氫瓶是否安全是個問題,氫氣的來源則是更大的問題,大家都知道電解水可以產生氫氣與氧氣,問題是電解水產生氫氣的成本很高,而且這些電來自發電廠,結局還是要用火力、核能、水力發電來提供電力,為了降於成本目前工業上主要是將碳氫化合物(石油)以「蒸氣重組(Steam reforming)」的方式分解生產氫氣,搞了半天還是要以石油做為原料,這樣石油的價格怎麼可能「崩跌」?   此外,低壓儲氫技術的平衡壓力低於 10 大氣壓,安全性比高壓儲氫技術高,但必須使用貴重或稀有金屬(例如鑭鎳合金)做為儲氫合金,因此成本偏高。如果使用甲醇取代氫氣做為燃料的轉換效率很低,觸媒通常含有貴重或稀有金屬,所以價格偏高,依照目前燃料電池發展的情況,還是以高壓複合材料儲氫瓶的可行性最高(對燃料電池的原理有興趣的人可以參考這裡;對燃料電池的儲氫技術原理有興趣的人可以參考這裡)。  
目前石化燃料低價在於市場競爭供過於求   每次原油價格大跌,市場往往會出現「石油不重要」的言論,2001 年知識經濟興起就有「實質資產價值將不再,能源對經濟的影響轉弱」的說法,沒想到接下來油價在人為炒作下一路走高到 2008 年每桶 148 美元,並且成為金融海嘯爆發的間接原因之一;如今油價跌到每桶 30 美元,立刻又有人提出了「石化燃料生產成本低,全球將步入電力富足年代」的說法。   目前油價偏低是人為操作所造成的,石油輸出國家組織(Organization of Petroleum Exporting Countries,OPEC)為了打擊美國業岩油產業,不惜殺價競爭,目的就是要以低油價迫使美國業岩油廠商破產倒閉以搶回石油產業的主導權。   若真的美國業岩油廠商倒光了,OPEC 還會讓石油維持在這麼低的價格嗎?因此低油價是短期的現象,或許就是這幾年的時間而已,未來太陽能的價格能和石化燃料競爭,不是因為太陽能價格下跌,而是因為石化燃料價格上漲。  
人類會繼續依賴石油,但程度可能緩慢降低   目前工程上還沒有出現終極的技術足以取代石油,因此未來人類仍然繼續依賴石油或其他可以燃燒的東西(天然氣、煤碳),但是依賴程度可能會隨著環保意識與新能源技術的發展而緩慢減低,要完全取代石油為時尚遠。  
結論   第一,石化燃料最近處於低價不是因為生產成本低,而是因為市場競爭供過於求,未來 OPEC 發動的價格戰結束,油價就會回到 50 美元以上,再長遠看未來可低價開採的石油儲存量枯竭,原油價格上漲是必然的趨勢,不要說 90 美元,超過 100 美元都是有可能的。   第二,2040 年再生能源市場蓬渤發展是很有可能的,但是要取代石化燃料主導電力供應的可能性不高,而且主要不是因為再生能源價格下跌,而是石化燃料價格上漲,當油價回到 100 美元以上,再生能原價格就會很有競爭力了。   第三,省電科技與各種節能技術減少了電力需求是確定的,但這樣就說未來有望步入「電力富足年代」是過於誇大了;電動車或燃料電池汽車的發展確實可以減少空氣污染與石化燃料的消耗,但是要完全取代石化燃料是不可能的。   目前最大的問題在於:電價太便宜造成使用者沒有節約用電的習慣,各種價格較高的家庭能源管理系統(Home Energy Management System,HEMS)乏人問津;而電價如果真的大漲又會造成物價波動,受限於選舉與政治因素,要讓電價上漲也是困難重重,「電力富足年代」之說恐怕倍受考驗。   (首圖來源: CC BY 2.0)   (本文授權轉載自《》─〈〉

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

【其他文章推薦】

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

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

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

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

※專營大陸快遞台灣服務

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

Geometry 判斷幾何是否被另一個幾何/線段分割成多段

如下圖,如何判斷幾何多邊形A被多邊形B,切割為多段幾何?

 幾何A被幾何B切割

1. 獲取幾何A與幾何B的交集C

 var intersectGeometry = new CombinedGeometry(GeometryCombineMode.Intersect, geometry1, geometry2); 

 

 

2.幾何A排除交集C,得到餘下空白區域D

 var combinedGeometry = new CombinedGeometry(GeometryCombineMode.Exclude, geometry1, intersectGeometry); 

 

 3.判斷幾何D區域是否包含多段幾何

幾何D區分為倆段,獲取域的邊框近似點集,發現含有倆段線條的描述(倆段M->z的文本),與真實幾何分段對應。

所以,可以通過線條終止字符”z”個數,來判斷幾何的分段數量。

  • 獲取幾何的近似多邊形值
  • 獲取其路徑內的點集
  • 判斷點集中是否含有2個及以上的線條繪製結束字符”z”
1     var flattenedPathGeometry = combinedGeometry.GetFlattenedPathGeometry();
2     var outerPointsString = flattenedPathGeometry.Figures.ToString();
3     if (outerPointsString.Length > 2
4         && outerPointsString.Replace("z", string.Empty).Length == outerPointsString.Length - 2)
5     {
6         return true;
7     }

 完整函數見下方代碼

 1     /// <summary>
 2     /// 檢查幾何是否被另一個幾何分割成多段
 3     /// </summary>
 4     /// <param name="geometry1"></param>
 5     /// <param name="geometry2"></param>
 6     /// <returns></returns>
 7     private bool CheckGeometryIsDividedByAnotherGeometry(PathGeometry geometry1, Geometry geometry2)
 8     {
 9         var intersectGeometry = new CombinedGeometry(GeometryCombineMode.Intersect, geometry1, geometry2);
10         var combinedGeometry = new CombinedGeometry(GeometryCombineMode.Exclude, geometry1, intersectGeometry);
11         var flattenedPathGeometry = combinedGeometry.GetFlattenedPathGeometry();
12         var outerPointsString = flattenedPathGeometry.Figures.ToString();
13         var geometryList = outerPointsString.Split(new[] { 'M' }, StringSplitOptions.RemoveEmptyEntries).Where(i => i.Contains("z")).Select(i => $"M{i}").ToList();
14         if (geometryList.Count >= 2 && HintStrokePath.Data == null)
15         {
16             var a = Geometry.Parse(geometryList[0]); ;
17             var b = Geometry.Parse(geometryList[1]); ;
18         }
19         if (outerPointsString.Length > 2
20             && outerPointsString.Replace("z", string.Empty).Length == outerPointsString.Length - 2)
21         {
22             return true;
23         }
24         return false;
25     }

View Code

4. 獲取幾何被分割后的多段幾何內容

解析”M”、”z”,分別獲取倆段幾何數據

1     var geometryList = outerPointsString.Split(new[] { 'M' }, StringSplitOptions.RemoveEmptyEntries).Where(i => i.Contains("z")).Select(i => $"M{i}").ToList();
2     if (geometryList.Count >= 2)
3     {
4         var geometry1 = Geometry.Parse(geometryList[0]); ;
5         var geometry2 = Geometry.Parse(geometryList[1]); ;
6     }

幾何被直線分割

幾何被線段分割,如何判斷或者獲取分割后的多段幾何?

直接用線段與幾何重複上面的步驟,是有問題的。

線段類似“M150,130L150,1300 150,170z”去與幾何去交集,CombinedGeometry中的數據是空的

需要給線條添加1的粗細:

  var geometry2 = lineGeometry.GetWidenedPathGeometry(new System.Windows.Media.Pen(System.Windows.Media.Brushes.Black, 1)); 

結果如下圖:

 

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

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

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

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

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

※專營大陸快遞台灣服務

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

強颱哈吉貝沖走91袋核災污染廢棄物 日官方估:12存放所有相同危機

摘錄自2020年3月17日自由時報報導

去年第19號颱風哈吉貝重創日本關東地區,更衝刷走核災污染物臨時存放所中的污染物,引發擔憂。經日本環境省調查,在同樣降雨量下,部分臨存放所都隱藏著廢棄物流出危機;今(17日)內閣會議之後,環境大臣小泉進次郎對此做出回應。

環境省就存放所週邊河流與潛在淹水地區的322個地點進行調查,按強颱哈吉貝的降雨強度標準評估,共有12處臨時存放所,都有污染廢棄物流出的可能性,甚至還有土石流撕破塑膠袋等潛藏危機。

小泉進次郎在記者會上表示,鑑於近年暴雨的危機逐漸高升,望盡可能對此提早採取應變措施。對此,環境省將以搬運污染物至臨時存儲設施中存放、加裝圍欄以防流出等方式作為5月底以前的應變對策。

公害污染
廢棄物
核能
能源議題
國際新聞
日本
核災

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

【其他文章推薦】

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

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

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

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

※專營大陸快遞台灣服務

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

第六屆中國國際新能源汽車論壇2016即將在下周隆重開幕!

2016年4月20-22日∣中國·上海安亭穎奕皇冠假日酒店
電池、電控電機、充電設施、汽車電子
車聯網、車後市、創新核心技術、無線充電

由希邁商務諮詢(上海)有限公司主辦的2016年第六屆中國國際新能源汽車論壇即將于下周4月20日-4月22日在上海隆重舉行。此次論壇獲得了亞太電動車學會、國家新能源機動車產品品質監督檢驗中心、上海交大密西根學院、賽迪顧問及中國綠色能源產業技術創新戰略聯盟的大力支持。截止3月14日,論壇已經確認42位演講嘉賓出席本次論壇並做高品質學術演講。演講嘉賓分別來自菲律賓電動車協會、國家新能源機動車產品品質監督檢驗中心、美國國家能源局、中國工程院等在內的政府單位與研究機構,以及包括寶馬、通用汽車、特斯拉、長安汽車、宇通客車、奇瑞、比亞迪戴姆勒、上汽、北汽、觀致等在內的多個知名整車商,將在論壇上共同研討新能源汽車行業發展趨勢、技術路線及難點、基礎設施建設、商業模式,延續以往的豐碩成果,繼續為新能源汽車行業作出貢獻。

本屆得到行業內世界百強企業麥格納動力總成與格特拉克聯合作為論壇的鉑金贊助,同時也獲得了寧波招寶磁業有限公司作為論壇的銀牌贊助,廈門宏發電力電器有限公司作為論壇的銅牌贊助及北京易微行科技有限公司作為論壇的晚宴贊助的大力支持。此外,論壇還獲得了30多家來自全世界各地的知名媒體進行宣傳,並且將會在現場為部分企業進行專題訪問,將在為期3天的論壇上進行全方位的即時報導。

本屆論壇相比歷屆舉辦規模最大的第六屆新能源汽車論壇,涉及主論壇及三個分論壇、考察活動、頒獎典禮和交流晚宴。屆時將有全球範圍內的整車製造商、電網電力公司、電池廠商、零部件供應商、核心技術提供商和政府官員一起,對新能源汽車產業面臨的挑戰,機遇與對策各方面進行為期三天更深層次並具有建設和戰略性的探討。歡迎各位行業內人士積極參與,如有意向請聯繫組委會,期待與您下周共同參與!

會議結構

大會連絡人:Hill ZENG(曾先生)
聯繫電話:0086 21-6045 1760 或郵箱
我們期待與貴單位一起出席於2016年4月20-22日在上海舉辦的第六屆中國國際新能源汽車論壇2016,以利決策!

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

【其他文章推薦】

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

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

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

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

※專營大陸快遞台灣服務

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

在 ASP.NET Core 項目中使用 MediatR 實現中介者模式

 一、前言

   最近有在看 DDD 的相關資料以及微軟的  這個項目中基於 DDD 的架構設計,在  這個示例服務中,可以看到各層之間的代碼調用與我們之前傳統的調用方式似乎差異很大,整個項目各個層之間的代碼全部是通過注入 IMediator 進行調用的,F12 查看源碼后可以看到該接口是屬於  這個組件的。既然要照葫蘆畫瓢,那我們就先來了解下如何在 ASP.NET Core 項目中使用 。

  代碼倉儲:

 二、Step by Step

    從 github 的項目主頁上可以看到作者對於這個項目的描述是基於中介者模式的 .NET 實現,是一種基於進程內的數據傳遞。也就是說這個組件主要實現的是在一個應用中實現數據傳遞,如果想要實現多個應用間的數據傳遞就不太適合了。從作者的 github 個人主頁上可以看到,他還是  這個 OOM 組件的作者,PS,如果你想要了解如何在 ASP.NET Core 項目中使用 AutoMapper,你可以查看我之前寫的這一篇文章()。而對於 MediatR 來說,在具體的學習使用之前,我們先來了解下什麼是中介者模式。

  1、什麼是中介者模式

  很多舶來詞的中文翻譯其實最終都會與實際的含義相匹配,例如軟件開發過程中的 23 種設計模式的中文名稱,我們其實可以比較容易的從中文名稱中得知出該設計模式具體想要實現的作用,就像這裏介紹的中介者模式。

  在我們通過代碼實現實際的業務邏輯時,如果涉及到多個對象類之間的交互,通常我們都是會採用直接引用的形式,隨着業務邏輯變的越來越複雜,對於一個簡單的業務抽象出的實現方法中,可能會被我們添加上各種判斷的邏輯或是對於數據的業務邏輯處理方法。

  例如一個簡單的用戶登錄事件,我們可能最終會抽象出如下的業務流程實現。

public bool Login(AppUserLoginDto dto, out string msg)
{
    bool flag = false;
    try
    {
        // 1、驗證碼是否正確
        flag = _redisLogic.GetValueByKey(dto.VerificationCode);
        if (!flag)
        {
            msg = "驗證碼錯誤,請重試";
            return false;
        }

        // 2、驗證賬戶密碼是否正確
        flag = _userLogic.GetAppUser(dto.Account.Trim(), dto.Password.Trim(), out AppUserDetailDto appUser);
        if (!flag)
        {
            msg = "賬戶或密碼錯誤,請重試";
            return false;
        }

        // 3、驗證賬戶是否可以登錄當前的站點(未被鎖定 or 具有登錄當前系統的權限...)
        flag = _authLogic.CheckIsAvailable(appUser);
        if (!flag)
        {
            msg = "用戶被禁止登錄當前系統,請重試";
            return false;
        }

        // 4、設置當前登錄用戶信息
        _authLogic.SetCurrentUser(appUser);

        // 5、記錄登錄記錄
        _userLogic.SaveLoginRecord(appUser);

        msg = "";
        return true;
    }
    catch (Exception ex)
    {
        // 記錄錯誤信息
        msg = $"用戶登錄失敗:{ex.Message}";
        return false;
    }
}

  這裏我們假設對於登錄事件的實現方法存在於 UserAppService 這個類中,對於 redis 資源的操作在 RedisLogic 類中,對於用戶相關資源的操作在 UserLogic 中,而對於權限校驗相關的資源操作位於 AuthLogic 類中。

  可以看到,為了實現 UserAppService 類中定義的登錄方法,我們至少需要依賴於 RedisLogic、UserLogic 以及 AuthLogic,甚至在某些情況下可能在 UserLogic 和 AuthLogic 之間也存在着某種依賴關係,因此我們可以從中得到如下圖所示的類之間的依賴關係。

  一個簡單的登錄業務尚且如此,如果我們需要對登錄業務添加新的需求,例如現在很多網站的登錄和註冊其實是放在一起的,當登錄時如果判斷沒有當前的用戶信息,其實會催生創建新用戶的流程,那麼,對於原本的登錄功能實現,是不是會存在繼續添加新的依賴關係的情況。同時對於很多本身就很複雜的業務,最終實現出來的方法是不是會有更多的對象類之間存在各種的依賴關係,牽一發而動全身,後期修改測試的成本會不會變得更高。

  那麼,中介者模式是如何解決這個問題呢?

  在上文有提到,對於舶來詞的中文名稱,中文更多的會根據實際的含義進行命名,試想一下我們在現實生活中提到中介,是不是更多的會想到房屋中介這一角色。當我們來到一個新的城市,面臨着租房的問題,絕大多數的情況下,我們最終需要通過中介去達成我們租房的目的。在租房這個案例中,房屋中介其實就是一个中介者,他承接我們對於想要租的房子的各種需求,從自己的房屋數據庫中去尋找符合條件的,最終以一個橋樑的形式,連接我們與房東,最終就房屋的租住達成一致。

  而在軟件開發中,中介者模式則是要求我們根據實際的業務去定義一個包含各種對象之間交互關係的對象類,之後,所有涉及到該業務的對象都只關聯於這一个中介對象類,不再顯式的調用其它類。採用了中介者模式之後設計的登錄功能所涉及到的類依賴如下圖所示,這裏的 AppUserLoginEventHandler 其實就是我們的中介類。

  當然,任何事都會有利有弊,不會存在百分百完美的事情,就像我們通過房租中介去尋找合適的房屋,最終我們需要付給中介一筆費用去作為酬勞,採用中介者模式設計的代碼架構也會存在別的問題。因為在代碼中引入了中介者這一對象,勢必會增加我們代碼的複雜度,可能會使原本很輕鬆就實現的代碼變得複雜。同時,我們引入中介者模式的初衷是為了解決各個對象類之間複雜的引用關係,對於某些業務來說,本身就很複雜,最終必定會導致這个中介者對象異常複雜。

  畢竟,軟件開發的過程中不會存在銀彈去幫我們解決所有的問題。

  那麼,在本篇文章的示例代碼中,我將使用 MediatR 這一組件,通過引入中介者模式的思想來完成上面的用戶登錄這一案例。

  2、組件加載

  在使用 MediatR 之前,這裏簡單介紹下這篇文章的示例 demo 項目。這個示例項目的架構分層可以看成是介於傳統的多層架構與採用 DDD 的思想的架構分層。嗯,你可以理解成四不像,屬於那種傳統模式下的開發人員在往 DDD 思想上進行遷移的成品,具體的代碼分層說明解釋如下。

  01_Infrastructure:基礎架構層,這層會包含一些對於基礎組件的配置或是幫助類的代碼,對於每個新建的服務來說,該層的代碼幾乎都是差不多的,所以對於基礎架構層的代碼其實最好是發布到公有 or 私有的 Nuget 倉庫中,然後我們直接在項目中通過 Nuget 去引用。

  對於採用 DDD 的思想構建的項目來說,很多人可能習慣將一些實體的配置也放置在基礎架構層,我的個人理解還是應該置於領域層,對於基礎架構層,只做一些基礎組件的封裝。如果有什麼不對的地方,歡迎在評論區提出。

  02_Domain:領域層,這層會包含我們根據業務劃分出的領域的幾乎所有重要的部分,有領域對象(Domain Object)、值對象(Value Object)、領域事件(Domain Event)、以及倉儲(Repository)等等領域組件。

  這裏雖然我創建了 AggregateModels(聚合實體)這個文件夾,其實在這個項目中,我創建的還是不包含任何業務邏輯的貧血模型。同時,對於倉儲(Repository)在領域分層中是置於 Infrastructure(基礎架構層)還是位於 Domain(領域層),每個人都會有自己的理解,這裏我還是更傾向於放在 Domain 層中更符合其定位。

  03_Application:應用層,這一層會包含我們基於領域所封裝出的各種實際的業務邏輯,每個封裝出的服務應用之間並不會出現互相調用的情況。

  Sample.Api:API 接口層,這層就很簡單了,主要是通過 API 接口暴露出我們基於領域對外提供的各種服務。

  整個示例項目的分層結構如下圖所示。

  與使用其它的第三方組件的使用方式相同,在使用之前,我們需要在項目中通過 Nuget 添加對於 MediatR 的程序集引用。

  這裏需要注意,因為我們主要是通過引用 MediatR 來實現中介者模式,所以我們只需要在領域層和應用層加載 MediatR 即可。而對於 Sample.Api 這個 Web API 項目,因為需要通過依賴注入的方式來使用我們基於 MediatR 所構建出的各種服務,所以這裏我們還要添加 MediatR.Extensions.Microsoft.DependencyInjection 這個程序集到 Sample.Api 中。

Install-Package MediatR
Install-Package MediatR.Extensions.Microsoft.DependencyInjection

  3、案例實現

  首先我們在 Sample.Domain 這個類庫的 AggregateModels 文件夾下添加 AppUser(用戶信息)類 和 Address(地址信息) 類,這裏雖然並沒有採用 DDD 的思想去劃分領域對象和值對象,我們創建出來的都是不含任何業務邏輯的貧血模型。但是在用戶管理這個業務中,對於用戶所包含的聯繫地址信息,其實是一種無狀態的數據。也就是說對於同一個地址信息,不會因為置於多個用戶中而出現數據的二義性。因此,對於地址信息來說,是不需要唯一的標識就可以區分出這個數據的,所以這裏的 Address 類就不需要添加主鍵,其實也就是對應於領域建模中的值對象。

  這裏我是使用的 EF Core 作為項目的 ORM 組件,當創建好需要使用實體之後,我們在 Sample.Domain 這個類庫下面新建一個 SeedWorks 文件夾,添加自定義的 DbContext 對象和用於執行 EF Core 第一次生成數據庫時寫入預置種子數據的信息類。

  這裏需要注意,在 EF Core 中,當我們需要將編寫的 C# 類通過 Code First 創建出數據庫表時,我們的 C# 類必須包含主鍵信息。而對應到我們這裏的 Address 類來說,它更多的是作為 AppUser 類中的屬性信息來展示的,所以這裏我們需要對 EF Core 生成數據庫表的過程進行重寫。

  這裏我們在 SeedWorks 文件夾下創建一個新的文件夾 EntityConfigurations,在這裏用來存放我們自定義的 EF Core 創建表的規則。新建一個繼承於 IEntityTypeConfiguration<AppUser> 接口的 AppUserConfiguration 配置類,在接口默認 Configure 方法中,我們需要編寫映射規則,將 Address 類作為 AppUser 類中的字段進行显示,最終實現后的代碼如下所示。 

public class AppUserConfiguration : IEntityTypeConfiguration<AppUser>
{
    public void Configure(EntityTypeBuilder<AppUser> builder)
    {
        // 表名稱
        builder.ToTable("appuser");

        // 實體屬性配置
        builder.OwnsOne(i => i.Address, n =>
        {
            n.Property(p => p.Province).HasMaxLength(50)
                .HasColumnName("Province")
                .HasDefaultValue("");

            n.Property(p => p.City).HasMaxLength(50)
                .HasColumnName("City")
                .HasDefaultValue("");

            n.Property(p => p.Street).HasMaxLength(50)
                .HasColumnName("Street")
                .HasDefaultValue("");

            n.Property(p => p.ZipCode).HasMaxLength(50)
                .HasColumnName("ZipCode")
                .HasDefaultValue("");
        });
    }
}

  當創建表的映射規則編寫完成后,我們就可以對 UserApplicationDbContext 類進行重寫 OnModelCreating 方法。在這個方法中,我們就可以去應用我們自定義設置的實體映射規則,從而讓 EF Core 按照我們的想法去創建數據庫,最終實現的代碼如下所示。

public class UserApplicationDbContext : DbContext
{
    public DbSet<AppUser> AppUsers { get; set; }

    public UserApplicationDbContext(DbContextOptions<UserApplicationDbContext> options)
        : base(options)
    {
    }

    /// <summary>
    ///
    /// </summary>
    /// <param name="modelBuilder"></param>
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // 自定義 AppUser 表創建規則
        modelBuilder.ApplyConfiguration(new AppUserConfiguration());
    }
}

  當我們創建好 DbContext 后,我們需要在 Startup 類的 ConfigureServices 方法中進行注入。在示例代碼中,我使用的是 MySQL 8.0 數據庫,將配置文件寫入到 appsettings.json 文件中,最終注入 DbContext 的代碼如下所示。

public void ConfigureServices(IServiceCollection services)
{
    // 配置數據庫連接字符串
    services.AddDbContext<UserApplicationDbContext>(options =>
        options.UseMySql(Configuration.GetConnectionString("SampleConnection")));
}

  數據庫的連接字符串配置如下。

{
  "ConnectionStrings": {
    "SampleConnection": "server=127.0.0.1;database=sample.application;user=root;password=123456@sql;port=3306;persistsecurityinfo=True;"
  }
}

  在上文有提到,除了創建一個 DbContext 對象,我們還創建了一個 DbInitializer 類用於在 EF Core 第一次執行創建數據庫操作時將我們預置的信息寫入到對應的數據庫表中。這裏我們只是簡單的判斷下 AppUser 這張表是否存在數據,如果沒有數據,我們就添加一條新的記錄,最終實現的代碼如下所示。

public class DbInitializer
{
    public static void Initialize(UserApplicationDbContext context)
    {
        context.Database.EnsureCreated();

        if (context.AppUsers.Any())
            return;

        AppUser admin = new AppUser()
        {
            Id = Guid.NewGuid(),
            Name = "墨墨墨墨小宇",
            Account = "danvic.wang",
            Phone = "13912345678",
            Age = 12,
            Password = "123456",
            Gender = true,
            IsEnabled = true,
            Address = new Address("啦啦啦啦街道", "啦啦啦市", "啦啦啦省", "12345"),
            Email = "danvic.wang@yuiter.com",
        };

        context.AppUsers.Add(admin);
        context.SaveChanges();
    }
}

  當我們完成種子數據植入的代碼,我們需要在程序啟動之前就去執行我們的代碼。因此我們需要修改 Program 類中的 Main 方法,實現在運行 web 程序之前去執行種子數據的植入。

public class Program
{
    public static void Main(string[] args)
    {
        var host = CreateWebHostBuilder(args).Build();

        using (var scope = host.Services.CreateScope())
        {
            // 執行種子數據植入
            //
            var services = scope.ServiceProvider;
            var context = services.GetRequiredService<UserApplicationDbContext>();
            DbInitializer.Initialize(context);
        }
    }

    public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
        WebHost.CreateDefaultBuilder(args)
            .UseStartup<Startup>();
}

  這時,運行我們的項目,程序就會自動執行創建數據庫的操作,同時會將我們預設好的種子數據寫入到數據庫表中,最終實現的效果如下圖所示。

  基礎的項目代碼已經完成之後,我們就可以開始學習如何通過 MediatR 來實現中介者模式。在這一章的示例項目中,我們會使用到 MediatR 中兩個很重要的接口類型:IRequest 和 INotification。

  在 Github 上,作者針對這兩個接口做了如下的,這裏我會按照我的理解去進行使用。同時,為了防止我的理解出現了偏差,從而對各位造成影響,這裏貼上作者回復解釋的原文。

Requests are for:
1 request to 1 handler. Handler may or may not return a value
Notifications are for:
1 notification to n handlers. Handler may not return a value.


In practical terms, requests are "commands", notifications are "events".
Command would be directing MediatR to do something like "ApproveInvoiceCommand -> ApproveInvoiceHandler". Event would be
notifications, like "InvoiceApprovedEvent -> SendThankYouEmailToCustomerHandler"

 

  對於繼承於 IRequest 接口的類來說,一個請求(request)只會有一個針對這個請求的處理程序(requestHandler),它可以返回值或者不返回任何信息;

  而對於繼承於 INotification 接口的類來說,一個通知(notification)會對應多個針對這個通知的處理程序(notificationHandlers),而它們不會返回任何的數據。

  請求(request)更像是一種命令(command),而通知(notification)更像是一種事件(event)。嗯,可能看起來更暈了,jbogard 這裏給了一個案例給我們進一步的解釋了 request 與 notification 之間的差異性。

  雙十一剛過,很多人都會瘋狂剁手,對於購買大件來說,為了能夠更好地擁有售後服務,我們在購買后肯定會期望商家給我們提供發票,這裏的要求商家提供發票就是一種 request,而針對我們的這個請求,商家會做出回應,不管能否開出來發票,商家都應當通知到我們,這裏的通知用戶就是一種 notification。

  對於提供發票這個 request 來說,不管最終的結果如何,它只會存在一種處理方式;而對於通知用戶這個 notification 來說,商家可以通過短信通知,可以通過公眾號推送,也可以通過郵件通知,不管採用什麼方式,只要完成了通知,對於這個事件來說也就已經完成了。    

  而對應於用戶登錄這個業務來說,用戶的登錄行為其實就是一個 request,對於這個 request 來說,我們可能會去數據庫查詢賬戶是否存在,判斷是不是具有登錄系統的權限等等。而不管我們在這個過程中做了多少的邏輯判斷,它只會有兩種結果,登錄成功或登錄失敗。而對於用戶登錄系統之後可能需要設置當前登錄人員信息,記錄用戶登錄日誌這些行為來說,則是歸屬於 notification 的。

  弄清楚了用戶登錄事件中的 request 和 notification 劃分,那麼接下來我們就可以通過代碼來實現我們的功能。這裏對於示例項目中的一些基礎組件的配置我就跳過了,如果你想要具體的了解這裏使用到的一些組件的使用方法,你可以查閱我之前的文章。

  首先,我們在 Sample.Application 這個類庫下面創建一個 Commands 文件夾,在下面存放用戶的請求信息。現在我們創建一個用於映射用戶登錄請求的 UserLoginCommand 類,它需要繼承於 IRequest<T> 這個泛型接口。因為對於用戶登錄這個請求來說,只會有可以或不可以這兩個結果,所以對於這個請求的響應的結果是 bool 類型的,也就是說,我們具體應該繼承的是 IRequest<bool>。

  對於用戶發起的各種請求來說,它其實只是包含了對於這次請求的一些基本信息,而對於 UserLoginCommand 這個用戶登錄請求類來說,它可能只會有賬號、密碼、驗證碼這三個信息,請求類代碼如下所示。

public class UserLoginCommand : IRequest<bool>
{
    /// <summary>
    /// 賬戶
    /// </summary>
    public string Account { get; private set; }

    /// <summary>
    /// 密碼
    /// </summary>
    public string Password { get; private set; }

    /// <summary>
    /// 驗證碼
    /// </summary>
    public string VerificationCode { get; private set; }

    /// <summary>
    /// ctor
    /// </summary>
    /// <param name="account">賬戶</param>
    /// <param name="password">密碼</param>
    /// <param name="verificationCode">驗證碼</param>
    public UserLoginCommand(string account, string password, string verificationCode)
    {
        Account = account;
        Password = password;
        VerificationCode = verificationCode;
    }
}

  當我們擁有了存儲用戶登錄請求信息的類之後,我們就需要對用戶的登錄請求進行處理。這裏,我們在 Sample.Application 這個類庫下面新建一個 CommandHandlers 文件夾用來存放用戶請求的處理類。

  現在我們創建一個繼承於 IRequestHandler 接口的 UserLoginCommandHandler 類用來實現對於用戶登錄請求的處理。IRequestHandler 是一個泛型的接口,它需要我們在繼承時聲明我們需要實現的請求,以及該請求的返回信息。因此,對於 UserLoginCommand 這個請求來說,UserLoginCommandHandler 這個請求的處理類,最終需要繼承於 IRequestHandler<UserLoginCommand, bool>。

  就像上面提到的一樣,我們需要在這個請求的處理類中對用戶請求的信息進行處理,在 UserLoginCommandHandler 類中,我們應該在 Handle 方法中去執行我們的判斷邏輯,這裏我們會引用到倉儲來獲取用戶的相關信息。倉儲中的代碼這裏我就不展示了,最終我們實現后的代碼如下所示。

public class UserLoginCommandHandler : IRequestHandler<UserLoginCommand, bool>
{
    #region Initizalize

    /// <summary>
    /// 倉儲實例
    /// </summary>
    private readonly IUserRepository _userRepository;

    /// <summary>
    /// ctor
    /// </summary>
    /// <param name="userRepository"></param>
    public UserLoginCommandHandler(IUserRepository userRepository)
    {
        _userRepository = userRepository ?? throw new ArgumentNullException(nameof(userRepository));
    }

    #endregion Initizalize

    /// <summary>
    /// Command Handler
    /// </summary>
    /// <param name="request"></param>
    /// <param name="cancellationToken"></param>
    /// <returns></returns>
    public async Task<bool> Handle(UserLoginCommand request, CancellationToken cancellationToken)
    {
        // 1、判斷驗證碼是否正確
        if (string.IsNullOrEmpty(request.VerificationCode))
            return false;

        // 2、驗證登錄密碼是否正確
        var appUser = await _userRepository.GetAppUserInfo(request.Account.Trim(), request.Password.Trim());
        if (appUser == null)
            return false;

        return true;
    }
}

  當我們完成了對於請求的處理代碼后,就可以在 controller 中提供用戶訪問的入口。當然,因為我們需要採用依賴注入的方式去使用 MediatR,所以在使用之前,我們需要將請求的對應處理關係注入到依賴注入容器中。

  在通過依賴注入的方式使用 MediatR 時,我們需要將所有的事件(請求以及通知)注入到容器中,而 MediatR 則會自動尋找對應事件的處理類,除此之外,我們也需要將通過依賴注入使用到的 IMediator 接口的實現類注入到容器中。而在這個示例項目中,我們主要是在 Sample.Domain、Sample.Application 以及我們的 Web Api 項目中使用到了 MediatR,因此,我們需要將這三個項目中使用到 MediatR 的類全部注入到容器中。

  一個個的注入會比較的麻煩,所以這裏我還是採用對指定的程序集進行反射操作,去獲取需要加載的信息批量的進行注入操作,最終實現后的代碼如下。

public static IServiceCollection AddCustomMediatR(this IServiceCollection services, MediatorDescriptionOptions options)
{
    // 獲取 Startup 類的 type 類型
    var mediators = new List<Type> { options.StartupClassType };

    // IRequest<T> 接口的 type 類型
    var parentRequestType = typeof(IRequest<>);

    // INotification 接口的 type 類型
    var parentNotificationType = typeof(INotification);

    foreach (var item in options.Assembly)
    {
        var instances = Assembly.Load(item).GetTypes();

        foreach (var instance in instances)
        {
            // 判斷是否繼承了接口
            //
            var baseInterfaces = instance.GetInterfaces();
            if (baseInterfaces.Count() == 0 || !baseInterfaces.Any())
                continue;

            // 判斷是否繼承了 IRequest<T> 接口
            //
            var requestTypes = baseInterfaces.Where(i => i.IsGenericType
                && i.GetGenericTypeDefinition() == parentRequestType);

            if (requestTypes.Count() != 0 || requestTypes.Any())
                mediators.Add(instance);

            // 判斷是否繼承了 INotification 接口
            //
            var notificationTypes = baseInterfaces.Where(i => i.FullName == parentNotificationType.FullName);

            if (notificationTypes.Count() != 0 || notificationTypes.Any())
                mediators.Add(instance);
        }
    }

    // 添加到依賴注入容器中
    services.AddMediatR(mediators.ToArray());

    return services;
}

  因為需要知道哪些程序集應該進行反射獲取信息,而對於 Web Api 這個項目來說,它只會通過依賴注入使用到 IMediator 這一個接口,所以這裏需要採用不同的參數的形式去確定具體需要通過反射加載哪些程序集。

public class MediatorDescriptionOptions
{
    /// <summary>
    /// Startup 類的 type 類型
    /// </summary>
    public Type StartupClassType { get; set; }

    /// <summary>
    /// 包含使用到 MediatR 組件的程序集
    /// </summary>
    public IEnumerable<string> Assembly { get; set; }
}

  最終,我們就可以在 Startup 類中通過擴展方法的信息進行快速的注入,實際使用的代碼如下,這裏我是將需要加載的程序集信息放在 appsetting 這個配置文件中的,你可以根據你的喜好進行調整。

public class Startup
{
    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        // Config mediatr
        services.AddCustomMediatR(new MediatorDescriptionOptions
        {
            StartupClassType = typeof(Startup),
            Assembly = Configuration["Assembly:Mediator"].Split("|", StringSplitOptions.RemoveEmptyEntries)
        });
    }
}

  在這個示例項目中的配置信息如下所示。

{
  "Assembly": {
    "Function": "Sample.Domain",
    "Mapper": "Sample.Application",
    "Mediator": "Sample.Application|Sample.Domain"
  }
}

  當我們注入完成后,就可以直接在 controller 中進行使用。對於繼承了 IRequest 的方法,可以直接通過 Send 方法進行調用請求信息,MediatR 會幫我們找到對應請求的處理方法,最終登錄 action 中的代碼如下。

[ApiVersion("1.0")]
[ApiController]
[Route("api/v{version:apiVersion}/[controller]")]
public class UsersController : ControllerBase
{
    #region Initizalize

    /// <summary>
    ///
    /// </summary>
    private readonly IMediator _mediator;

    /// <summary>
    /// ctor
    /// </summary>
    /// <param name="mediator"></param>
    public UsersController(IMediator mediator)
    {
        _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
    }

    #endregion Initizalize

    #region APIs

    /// <summary>
    /// 用戶登錄
    /// </summary>
    /// <param name="login">用戶登錄數據傳輸對象</param>
    /// <returns></returns>
    [HttpPost("login")]
    [ProducesResponseType(StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
    public async Task<IActionResult> Post([FromBody] AppUserLoginDto login)
    {
        // 實體映射轉換
        var command = new UserLoginCommand(login.Account, login.Password, login.VerificationCode);

        bool flag = await _mediator.Send(command);

        if (flag)
            return Ok(new
            {
                code = 20001,
                msg = $"{login.Account} 用戶登錄成功",
                data = login
            });
        else
            return Unauthorized(new
            {
                code = 40101,
                msg = $"{login.Account} 用戶登錄失敗",
                data = login
            });
    }

    #endregion APIs
}

  當我們完成了對於用戶登錄請求的處理之後,就可以去執行後續的“通知類”的事件。與用戶登錄的請求信息類相似,對於用戶登錄事件的通知類也只是包含一些通知的基礎信息。在 Smaple.Domain 這個類庫下面,創建一個 Events 文件用來存放我們的事件,我們來新建一個繼承於 INotification 接口的 AppUserLoginEvent 類,用來對用戶登錄事件進行相關的處理。

public class AppUserLoginEvent : INotification
{
    /// <summary>
    /// 賬戶
    /// </summary>
    public string Account { get; }

    /// <summary>
    /// ctor
    /// </summary>
    /// <param name="account"></param>
    public AppUserLoginEvent(string account)
    {
        Account = account;
    }
}

  在上文中有提到過,對於一個通知事件可能會存在着多種處理方式,所以這裏我們在 Smaple.Application 這個類庫的 DomainEventHandlers 文件夾下面會按照事件去創建對應的文件夾去存放實際處理方法。

  對於繼承了 INotification 接口的通知類來說,在 MediatR 中我們可以通過創建繼承於 INotificationHandler 接口的類去處理對應的事件。因為一個 notification 可以有多個的處理程序,所以我們可以創建多個的 NotificationHandler 類去處理同一個 notification。一個示例的 NotificationHandler 類如下所示。

public class SetCurrentUserEventHandler : INotificationHandler<AppUserLoginEvent>
{
    #region Initizalize

    /// <summary>
    ///
    /// </summary>
    private readonly ILogger<SetCurrentUserEventHandler> _logger;

    /// <summary>
    ///
    /// </summary>
    /// <param name="logger"></param>
    public SetCurrentUserEventHandler(ILogger<SetCurrentUserEventHandler> logger)
    {
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
    }

    #endregion Initizalize

    /// <summary>
    /// Notification handler
    /// </summary>
    /// <param name="notification"></param>
    /// <param name="cancellationToken"></param>
    /// <returns></returns>
    public Task Handle(AppUserLoginEvent notification, CancellationToken cancellationToken)
    {
        _logger.LogInformation($"CurrentUser with Account: {notification.Account} has been successfully setup");

        return Task.FromResult(true);
    }
}

  如何去引發這個事件,對於領域驅動設計的架構來說,一個更好的方法是將各種領域事件添加到事件的集合中,然後在提交事務之前或之後立即調度這些域事件,而對於我們這個項目來說,因為這不在這篇文章考慮的範圍內,只是演示如何去使用 MediatR 這個組件,所以這裏我就採取在請求邏輯處理完成后直接觸發事件的方式。

  在 UserLoginCommandHandler 類中,修改我們的代碼,在確認登錄成功后,通過調用 AppUser 類的 SetUserLoginRecord 方法來觸發我們的通知事件,修改后的代碼如下所示。

public class UserLoginCommandHandler : IRequestHandler<UserLoginCommand, bool>
{
    #region Initizalize

    /// <summary>
    /// 倉儲實例
    /// </summary>
    private readonly IUserRepository _userRepository;

    /// <summary>
    ///
    /// </summary>
    private readonly IMediator _mediator;

    /// <summary>
    /// ctor
    /// </summary>
    /// <param name="userRepository"></param>
    /// <param name="mediator"></param>
    public UserLoginCommandHandler(IUserRepository userRepository, IMediator mediator)
    {
        _userRepository = userRepository ?? throw new ArgumentNullException(nameof(userRepository));
        _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
    }

    #endregion Initizalize

    /// <summary>
    /// Command Handler
    /// </summary>
    /// <param name="request"></param>
    /// <param name="cancellationToken"></param>
    /// <returns></returns>
    public async Task<bool> Handle(UserLoginCommand request, CancellationToken cancellationToken)
    {
        // 1、判斷驗證碼是否正確
        if (string.IsNullOrEmpty(request.VerificationCode))
            return false;

        // 2、驗證登錄密碼是否正確
        var appUser = await _userRepository.GetAppUserInfo(request.Account.Trim(), request.Password.Trim());
        if (appUser == null)
            return false;

        // 3、觸發登錄事件
        appUser.SetUserLoginRecord(_mediator);

        return true;
    }
}

  與使用 Send 方法去調用 request 類的請求不同,對於繼承於 INotification 接口的事件通知類,我們需要採用 Publish 的方法去調用。至此,對於一個採用中介者模式設計的登錄流程就結束了,SetUserLoginRecord 方法的定義,以及最終我們實現的效果如下所示。

public void SetUserLoginRecord(IMediator mediator)
{
    mediator.Publish(new AppUserLoginEvent(Account));
}

 三、總結

  這一章主要是介紹了如何通過 MediatR 來實現中介者模式,因為自己也是第一次接觸這種思想,對於 MediatR 這個組件也是第一次使用,所以僅僅是採用案例分享的方式對中介者模式的使用方法進行了一個解釋。如果你想要對中介者模式的具體定義與基礎的概念進行進一步的了解的話,可能需要你自己去找資料去弄明白具體的定義。因為初次接觸,難免會有遺漏或錯誤,如果從文章中發現有不對的地方,歡迎在評論區中指出,先行感謝。

 四、參考

  1、

  2、

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

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

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

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

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

※專營大陸快遞台灣服務

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

SpringBoot基本配置詳解

SpringBoot項目有一些基本的配置,比如啟動圖案(banner),比如默認配置文件application.properties,以及相關的默認配置項。

示例項目代碼在:

一、啟動圖案banner

編寫banner.txt放入resources文件夾下,然後啟動項目即可修改默認圖案。

關於banner的生成,可以去一些專門的網站。

比如:https://www.bootschool.net/ascii

二、配置文件application

2.1 application.properties/yml

resources下通常會默認生成一個application.properties文件,這個文件包含了SpringBoot項目的全局配置文件。裏面的配置項通常是這樣的:

server.port=8080

在這個文件里我們可以添加框架支持的配置項,比如項目端口號、JDBC連接的數據源、日誌級別等等。

現在比較流行的是將properties文件改為yml文件。yml文件的格式yaml是這樣的:

server:
    port: 8080

yml和properties的作用是一樣的。而yml的好處是顯而易見的——更易寫易讀。

屬性之間互相調用使用${name}:

eknown:
    email: eknown@163.com
    uri: http://www.eknown.cn
    title: 'hello, link to ${eknown.uri} or email to ${eknown.email}'

鏈接:

2.2 多環境配置文件

通常開發一個應用會有多個環境,常見如dev/prod,也會有test,甚至其他一些自定義的環境,SpringBoot支持配置文件的靈活切換。

定義新配置文件需要遵循以下格式:application-{profile}.properties 或者application-{profile}.yml

比如現在有dev和prod兩個環境,我需要在application.yml文件之外新建兩個文件:

  1. application-dev.yml

    server:
       port: 8080
  2. application-prod.yml

    server:
      port: 8081

然後在application.yml中通過application.profiles.active={profile}指明啟用那個配置:

application:
    profiles:
      active: dev

除了在application.yml中指定配置文件外,還可以通過啟動命令指定:java -jar xxx.jar --spring.profiles.active=dev

2.2 自定義配置項並獲取它

主要介紹兩種方式,獲取單個配置項和獲取多個配置項。

舉例:

eknown:
    email: eknown@163.com
    uri: http://www.eknown.cn

2.2.1 使用@Value註解獲取單個配置項

@Value("${eknown.email}")
private String email;

@Value("${eknown.uri}")
private String url;

注意:使用@Value註解的時候,所在類必須被Spring容器管理,也就是被@Component、@Controller、@Service等註解定義的類。

2.2.2 獲取多個配置項

第一種,定義一個bean類,通過@Value獲取多個配置項:

@Component
public class MyConfigBean {
  
}

然後我們通過get方法來獲取這些值:

@RestController
public class BasicAction {
  
  @Autowired
  private MyConfigBean myConfigBean;

}

第二種,使用註解@ConfigurationProperties:

@Component
@ConfigurationProperties(perfix="eknown")
public class MyConfigBean {

  private String email;
  private String uri;
}

這裏只需要通過prefix指定前綴即可,後面的值自動匹配。

這裏我們還使用了@Component註解來讓spring容器管理這個MyConfigBean。

此外,我們可以不需要引入@Component,轉而在Application啟動類上加上@EnableConfigurationProperties({MyConfigBean.class})來啟動這個配置。

注意:我們這裡是從主配置文件,也就是SpringBoot默認的application-profile文件中獲取配置數據的。

而從自定義的配置文件,比如test.yml這種形式中獲取配置項時,情況是有點不大一樣的。

三、自定義配置文件

上面介紹的配置文件都是springboot默認的application開頭的文件。如果要自定義一個配置文件呢,比如test.yml或test.properties,怎麼獲取其中的配置項呢?

使用@PageResource註解即可。

首先我們來看一下讀取自定義的properties文件里的內容:

test.properties

hello.time=2019.11.19
hello.name=eknown

定義Configuration類:

@Configuration
@PropertySource("classpath:test.properties")
//@PropertySource("classpath:test.yml") // 注意,yml文件不能直接這樣寫,會讀不出數據
@ConfigurationProperties(prefix = "hello")
public class TestConfiguration {
    private String name;
    private String time;

    // hide get and set methods
}

測試一下:

@RestController
@RequestMapping(value = "test")
public class TestAction {

    @Autowired
    private TestConfiguration testConfiguration;

    @GetMapping(value = "config")
    public String test() {
        return testConfiguration.getName() + "<br/>" + testConfiguration.getTime();
    }
}

如果將properties文件換成yml文件呢?

我們嘗試一下,發現:

讀不出數據?

分析一下@PropertySource註解,發現其使用的PropertySourceFactory是DefaultPropertySourceFactory.

這個類的源碼如下:

public class DefaultPropertySourceFactory implements PropertySourceFactory {
    public DefaultPropertySourceFactory() {
    }

    public PropertySource<?> createPropertySource(@Nullable String name, EncodedResource resource) throws IOException {
        return name != null ? new ResourcePropertySource(name, resource) : new ResourcePropertySource(resource);
    }
}

這個類只能處理properties文件,無法處理yml文件。所以我們需要自定義一個YmlSourceFactory。

public class YamlSourceFactory extends DefaultPropertySourceFactory {

    @Override
    public PropertySource<?> createPropertySource(String name, EncodedResource resource) throws IOException {
        return new YamlPropertySourceLoader().load(resource.getResource().getFilename()
                , resource.getResource()).get(0);
    }
}

然後定義test.yml文件的config類:

@Configuration
@PropertySource(value = "classpath:test.yml", encoding = "utf-8", factory = YamlSourceFactory.class)
@ConfigurationProperties(prefix = "yml.hello")
public class TestYamlConfiguration {
    private String name;
    private String time;

    // hide get and set methods
}

注:為了區分test.properties和test.yml,這裏的test.yml中的屬性以yml.hello開頭。

編寫一下測試:

    @Autowired
    private TestYamlConfiguration ymlConfiguration;

    @GetMapping(value = "yml")
    public String testYml() {
        return "yml config: <br/>" + ymlConfiguration.getName() + "<br/>" + ymlConfiguration.getTime();
    }

訪問:

四、補充@ConfigurationProperties

網上一些資料中,為配合使用@ConfigurationProperties,還使用了@EnableConfigurationProperties註解。

經過測試發現:

  1. 從SpringBoot默認配置文件讀取配置信息,使用@ConfigurationProperties + @Component/@Configuration,或者@ConfigurationProperties + 在啟動類添加@EnableConfigurationProperties({class})。這兩種方式都能解決問題

  2. 從非默認配置文件讀取配置信息,需要利用@PropertySource註解。同樣兩種方式:

    2.1 @PropertySource + @ConfigurationProperties + @Component/@Configuration

    2.2 @PropertySource + @ConfigurationProperties + @Component/@Configuration + @EnableConfigurationProperties,第二種方式存在一個問題,即還是必須要使用@Component註解,如果不使用,則會導致讀取配置信息為null,但程序不會報錯;而如果採用了,則會導致bean類的set方法被執行兩次(也就是生成了兩個同樣類型的bean類)。這種方式不建議!

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

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

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

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

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

※專營大陸快遞台灣服務

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

Hadoop壓縮的圖文教程

近期由於Hadoop集群機器硬盤資源緊張,有需求讓把 Hadoop 集群上的歷史數據進行下壓縮,開始從網上查找的都是關於各種壓縮機制的對比,很少有關於怎麼壓縮的教程(我沒找到。。),再此特記錄下本次壓縮的過程,方便以後查閱,利己利人。

 

本文涉及的所有 jar包、腳本、native lib 見文末的相關下載 ~

 

我的壓縮版本: 

Jdk 1.7及以上

Hadoop-2.2.0 版本

 

壓縮前環境準備:

關於壓縮算法對比,網上資料很多,這裏我用的是 Bzip2 的壓縮方式,比較中庸,由於是Hadoop自帶的壓縮機制,也不需要額外下載別的東西,只需要在 Hadoop根目錄下 lib/native 文件下有如下文件即可:

 

 

 

 

壓縮之前要檢查 Hadoop 集群支持的壓縮算法: hadoop checknative

每台機器都要檢查一下,都显示如圖 true 則說明 集群支持 bzip2 壓縮,

如果显示false 則需要將上圖的文件下載拷貝到 Hadoop根目錄下 lib/native

 

 

 

壓縮程序介紹:

 

壓縮程序用到的類 getFileList(獲取文件路徑) 、 FileHdfsCompress(壓縮類)、FileHdfsDeCompress(解壓縮類) ,只用到這三個類即可完成壓縮/解壓縮操作。

 

 

 

 

getFileList 作用:遞歸打印 傳入文件目錄下文件的根路徑,包括子目錄下的文件。開始想直接輸出到文件中,後來打包放到集群上運行時,發現文件沒有內容,可能是由於分佈式運行的關係,所以就把路徑打印出來,人工在放到文件中。

 

核心代碼:
public static void listAllFiles(String path, List<String> strings) throws IOException {
        FileSystem hdfs = getHdfs(path);
        Path[] filesAndDirs = getFilesAndDirs(path);

        for(Path p : filesAndDirs){
            if(hdfs.getFileStatus(p).isFile()){
                if(!p.getName().contains("SUCCESS")){
                    System.out.println(p);
                }
            }else{
                listAllFiles(p.toString(),strings);
            }
        }
       // FileUtils.writeLines(new File(FILE_LIST_OUTPUT_PATH), strings,true);

    }

public static FileSystem getHdfs(String path) throws IOException {
        Configuration conf = new Configuration();
        return FileSystem.get(URI.create(path),conf);
    }


public static Path[] getFilesAndDirs(String path) throws IOException { FileStatus[] fs = getHdfs(path).listStatus(new Path(path)); return FileUtil.stat2Paths(fs); }

  

 FileHdfsCompress:壓縮程序非常簡單,對應程序里的 FileHdfsCompress 類,(解壓縮是 FileHdfsDeCompress),採用的是Hadoop 原生API  ,將Hadoop集群上原文件讀入作為輸入流,將壓縮路徑的輸入流作為輸出,再使用相關的壓縮算法即,代碼如下:

 

核心代碼:

 //指定壓縮方式
            Class<?> codecClass = Class.forName(COMPRESS_CLASS_NAME);

            Configuration conf = new Configuration();
            CompressionCodec codec = (CompressionCodec)ReflectionUtils.newInstance(codecClass, conf);
            // FileSystem fs = FileSystem.get(conf);
            FileSystem fs = FileSystem.get(URI.create(inputPath),conf);

            //原文件路徑 txt 用原本的輸入流讀入
            FSDataInputStream in = fs.open(new Path(inputPath));

            //創建 HDFS 上的輸出流,壓縮路徑
            //通過文件系統獲取輸出流
            OutputStream out = fs.create(new Path(FILE_OUTPUT_PATH));
            //對輸出流的數據壓縮
            CompressionOutputStream compressOut = codec.createOutputStream(out);
            //讀入原文件 壓縮到HDFS上 輸入--普通流  輸出-壓縮流
            IOUtils.copyBytes(in, compressOut, 4096,true);

  

以下是代碼優化的過程,不涉及壓縮程序使用,不感興趣同學可以跳過 ~ :

 

在實際編碼中,我其實是走了彎路的,一開始並沒有想到用 Hadoop API 就能實現壓縮解壓縮功能,代碼到此其實是經歷了優化迭代的過程。

 

最開始時壓縮的思路就是 將文件讀進來,再壓縮出去,一開始使用了 MapReduce 的方式,在編碼過程中,由於對生成壓縮文件的路徑還有要求,又在 Hadoop 輸出時自定義了輸出類來使的輸出文件的名字符合要求,不是 part-r-0000.txt ,而是時間戳.txt 的格式,至此符合原線上路徑的要求。

 

而在實際運行過程中發現,MR 程序需要啟動 Yarn,並佔用Yarn 資源,由於壓縮時間較長,有可能會長時間佔用 集群資源不釋放,後來發現 MR 程序的初衷是用來做并行計算的,而壓縮僅僅是 map 任務讀取一條就寫一條,不涉及計算,就是內容的簡單搬運。所以這裏放棄了使用 MR 想着可不可以就用簡單的 Hadoop API 就完成壓縮功能,經過一番嘗試后,發現真的可行! 使用了 Hadoop API 釋放了集群資源,壓縮速度也還可以,這樣就把這個壓縮程序當做一個後台進程跑就行了也不用考慮集群資源分配的問題

 

實測壓縮步驟:

 

1 將項目打包,上傳到hadoop 集群任一節點即可,準備好相應的腳本,輸入數據文件,日誌文件,如下圖:

 

 

 

 

2 使用獲取文件路徑腳本,打印路徑: 

 

 

 

getFileLish.sh 腳本內容如下,就是簡單調用,傳入參數為 hadoop集群上 HDFS 上目錄路徑

#!/bin/sh

echo "begin get fileList"

echo "第一個參數$1"

if [ ! -n "$1" ]; then
echo "check param!"
fi

#original file
hadoop jar hadoop-compress-1.0.jar com.people.util.getFileList  $1

  

3 將 打印的路徑 粘貼到 compress.txt 中,第 2 步中會把目錄的文件路徑包括子目錄路徑都打印出來,將其粘貼進  compress.txt 中即可,注意 文件名可隨意定

 

4 使用壓縮腳本即可,sh compress.sh /data/new_compress/compress.txt  ,加粗的部分是腳本的參數意思是 第3步中文件的路徑,注意:這裏只能是絕對路徑,不然可能報找不到文件的異常。

 

 

 compress.sh 腳本內容如下,就是簡單調用,傳入參數為 第3步中文件的絕對路徑

 

#!/bin/sh
echo "begin compress"

echo "第一個參數$1"

if [ ! -n "$1" ]; then
echo "check param!"
fi


hadoop jar hadoop-compress-1.0.jar com.people.compress.FileHdfsCompress  $1 >> /data/new_compress/compress.log 2>&1 &

  

 

5 查看壓縮日誌,發現後台程序已經開始壓縮了!,tail  -f  compress.log

 

6 如果感覺壓縮速度不夠,可以多台機器執行腳本,也可以一台機器執行多個任務,因為這個腳本任務是一個後台進程,不會佔用集群 Yarn 資源。

 

 相關下載:

 

程序源碼下載: git@github.com:fanpengyi/hadoop-compress.git

 

Hadoop 壓縮相關需要的 腳本、jar包、lib 下載: 關注公眾號 “大數據江湖”,後台回復 “hadoop壓縮”,即可下載

 

長按即可關注

 

 — The End —

 

 

 

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

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

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

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

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

※專營大陸快遞台灣服務

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

每年聯合國大會召開之際,多國元首和政府領袖同時舉行的「氣候週」今天(25日)開跑

每年聯合國大會召開之際,多國元首和政府領袖同時舉行的「氣候週」今天(25日)開跑,他們敦促世界領袖緊急採取行動降低全球暖化。

波蘭12月將主辦聯合國氣候變化綱要公約第24次締約方會議(COP24),聯合國氣候首長艾斯皮諾薩(Patricia Espinosa)呼籲各國團結,支持2015年巴黎協定所訂規定,將全球暖化升溫限制在攝氏兩度以下。

艾斯皮諾薩表示,各國並未實現他們的承諾。並說:「各國目前依據巴黎協定做出的承諾,將使得全球溫度在2100年升高約三度。」

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

【其他文章推薦】

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

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

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

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

※專營大陸快遞台灣服務

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

中國電動車銷量,首季跌九成

2016年第一季,中國純電動車銷量因政策問題而較前一季跌了近九成。今年北京政府預計將推出新的補貼政策,但由於政策尚未公布,市場呈現觀望氣氛。

根據中國大陸工業與信息化部的統計,今年首季,中國純電動家用汽車的銷量較去年第四季減少了89%,為6,265輛;中國媒體披露,業界普遍認為,如此巨大的銷量減低是對新能源汽車的補貼政策遲未推出而引發的觀望氣氛所致。

自2015年開始,中國大陸就出現新能源車企業透過捏造數據來騙取補貼的「騙補」現象,並已引發政府關注。政府已表示今年將調整新能源車補貼制度,且預計在2020年撤出對新能源車的補貼。現行對新能源車補助的對象,也已從原先3,412款減少到只剩247款。

據報導,「騙補」可使有關係的買家不花一毛錢買到輕型純電動車,排擠到真正有需求的人。

今年第一季,中國純電動車的產量年成長率84%,比去年第一季的年成長率365%大幅下滑。不過,中國全國乘用車信息聯席會秘書長崔東樹表示,今年的數字才是正常狀況。

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

【其他文章推薦】

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

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

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

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

※專營大陸快遞台灣服務

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