停止依賴中國稀土 美國防部提案擴大投資上限

摘錄自2020年5月19日自由時報報導

美國國防部已對國會提出一項提案,擴大對稀土的投資上限,以停止對中國的依賴程度,這些稀土可以用來製造飛彈和彈藥、極音速武器,及相關電子產品。如果美國可以重新生產稀土,中國打「稀土牌」的威脅程度將大幅降低。

根據《國防新聞》報導,美國國防部希望提高《國防生產法》的支出上限,在開採稀土上提升至最高17.5億美元(約新台幣523億),在微電子晶片上增至3.5億美元(約新台幣104億),當涉及到極音速武器時,將會沒有上限。據悉,此提案已於本月初提出,已納入國會正在起草的年度國防政策法案。

美國防部副部長洛德(Ellen Lord)去年8月曾表示,國防部正與澳洲進行談判,要求其為美軍提供稀土。澳洲Lynas公司擁有稀土礦,同時在馬來西亞也有精煉廠,可能是此計劃的核心。

環境經濟
循環經濟
國際新聞
美國
稀土
礦業

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

【其他文章推薦】

※回頭車貨運收費標準

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

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

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

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

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

台中搬家遵守搬運三大原則,讓您的家具不再被破壞!

實至名歸 COC廈門站眾泰T600六連冠

運動版全身大幅採用超高張力鋼板,並在車身關鍵部位進行了強化,安全性進一步提升。在主動安全性方面,T600運動版更將安全防護展現得淋漓盡致,安全配一應俱全。ESC車身穩定系統、HAC上坡輔助系統、前後倒車雷達及360°可視倒車影像等安全配置,與6方位安全氣囊、盲點信息系統、紅外夜視系統、TpMS智能胎壓監測、可選裝的HUD抬頭显示系統等尖端科技配備聯合上演重重壁壘,出色安全,呼之欲出,滿滿自信應對挑戰,盡享出行便利。

12月6日,中國汽車場地越野錦標賽(COC)廈門站比賽圓滿結束,也是最後一場分站賽,在各組別激烈的決賽搶分大戰中,眾泰T600越野車隊在汽油廠商組中奪得頭籌,車手鹿丙龍奪取該組冠軍,並與隊友徐瑩一起為車隊捧回了廠商杯冠軍殊榮,從而擴大了在年度積分方面的領先優勢,眾泰已經在今年分站比賽中已獲得六連冠,高歌猛進,一步步接近年度總冠軍。

作為本年度分站賽的最後一站,各個車隊之間的競爭日趨白熱化,尤其是之前比賽積分接近的車隊及隊員,比賽前已經是“劍拔弩張”,力爭本站取得更好排名和積分。眾泰車隊隊長楊逵如是向記者說道:“相對來說,我們在汽油改裝組的優勢要大一些,汽油廠商組和奇瑞車隊比較接近,由於總決賽採用雙倍積分的賽制,能否最終獲得全年總冠軍,廈門站比賽顯得尤為重要”。

【場地航拍圖】

【車隊大營】

【眾泰T600戰車】

汽油廠商組

本次比賽最大的變化就是之前因嚴重違規被禁賽的長安CS75車隊,重新回到了比賽。針對COC廈門站比賽形勢的變化,眾泰T600越野車隊對參賽陣容也進行了微調,喬旭與刁志剛攜手出擊汽油改裝組,鹿丙龍回歸汽油廠商組,和徐瑩搭檔。

在6圈的第一輪預賽中,車手們都拿出渾身解數,以求跑出好成績,從而得到決賽中最好的發車位置。眾泰車隊的鹿丙龍和徐瑩不負眾望,以小組第三、第四的成績闖進決賽,一起進入決賽的還有長安CS75車隊的文凡和孟斌。

“我們自身和車輛都調整到了最好狀態,對下午進行的決賽充滿信心”,眾泰T600越野車隊的車手鹿丙龍在決賽前向記者如是說。決賽中,鹿丙龍的表現堪稱“完美”,以絕對優勢力壓長安CS75車隊的孟斌和文凡獲得本組冠軍,其隊友徐瑩獲得本組季軍,獲得本組亞軍的是來自長安CS75車隊的孟斌,同時,鹿丙龍和徐瑩為眾泰T600越野車隊爭得了汽油廠商組的車隊團體冠軍獎盃。

汽油改裝組

眾泰T600越野車隊的喬旭在第一輪預賽中並不順利,他在第三圈的時候賽車出現失誤,賽車在幾處急彎都發生失控打橫,這極大地影響了喬旭的成績,儘管第二輪成績出色,但仍與決賽失之交臂,其隊友刁志剛以小組第三的成績征戰第二天進行的決賽。

6日下午的決賽中,車手刁志剛一人獨自面對其他三位車手的多面夾擊,面對發車位置不力的劣勢,刁志剛仍然奮起直追,最後以微小差距獲得了本小組的季軍,獲得本組冠亞軍的是來自另外兩支車隊的趙向前和童振榮。

作為“主流價值SUV”的眾泰T600,同眾泰車隊一樣,已然成為乘用車銷售市場上的佼佼者,早已進入月銷量“萬台俱樂部”,2016年1-10月份更是實現了94371台的銷量,以月均近萬台的銷量位居自主品牌中型SUV銷量榜首。

而且2016年眾泰汽車推出了更為年輕時尚的眾泰T600運動版,作為在眾泰T600優勢平台上推出的車型,眾泰T600運動版同樣以其年輕時尚又不乏沉穩的外觀、越級的配置在整個市場中還是有着普遍好評,銷量也是芝麻開花節節高。

眾泰T600運動版全系標配10寸中控彩色大屏,內容豐富。而Tye-net智控系統的優勢融入,實現手機操遠程控愛車,娛樂隨行,舒心便利。

此外,眾泰T600運動版還配備了一鍵啟動/無鑰匙進入、紅外夜視系統、腳步感應式電動尾門等尖端科技配備,讓駕乘人員充分享受科技智能帶來的便捷體驗。電動全景天窗、电子駐車系統、前排座椅分級加熱、雙區獨立自動恆溫空調、手機無線充電、方向盤/座椅/后視鏡三項聯動記憶功能、全液晶儀錶盤、定速巡航等帶來更加細緻入微的貼心關懷,讓出行一路無虞。

安全配置方面,眾泰T600運動版同級領先的安全性讓駕乘者無需前瞻後顧,無憂外出。運動版全身大幅採用超高張力鋼板,並在車身關鍵部位進行了強化,安全性進一步提升。在主動安全性方面,T600運動版更將安全防護展現得淋漓盡致,安全配一應俱全。ESC車身穩定系統、HAC上坡輔助系統、前後倒車雷達及360°可視倒車影像等安全配置,與6方位安全氣囊、盲點信息系統、紅外夜視系統、TpMS智能胎壓監測、可選裝的HUD抬頭显示系統等尖端科技配備聯合上演重重壁壘,出色安全,呼之欲出,滿滿自信應對挑戰,盡享出行便利。

而眾泰T600運動版不只是在外觀上吸引目光,在內飾的色彩搭配上,更是可圈可點,整個車內空間看起來既神秘又科技時尚。

眾泰T600運動版擁有的2807mm的傲人軸距,有效保證了車輛的駕乘空間。車內豐富的儲物空間為日常儲物提供了便利,而且後排座椅放倒後進一步拓展了後備箱空間,可以盡情享受眾泰T600運動版帶來的寬適空間。

動力方面,T600運動版提供1.5T及2.0T兩種發動機車型,1.5T渦輪增壓發動機與5速手動變速器搭配出黃金動力組合,最大功率達119KW,最大扭矩達215N·m。更加值得期待的是其2.0T車型,搭配使用旋鈕換擋式6速雙離合或5速手動變速器,最大功率140KW,最大扭矩250N·m,百公里加速只需9.26秒,充分提高了燃油的利用率,更加的節能環保,同時降低了用車成本。眾泰T600運動版,就是這樣讓你既有“面子”,又有“裡子”。

還有值得一說的是,眾泰T600在2015年J.D.power亞太公司發布的中國新車質量研究(IQS)報告,眾泰T600在中型SUV中pp100(每百車問題數)為100,優於中型SUV平均水平(pp100:106),全國綜合排名第13位,位列中型SUV中國品牌第二名。

2016年度COC總決賽將於12月中旬在廣西柳州打響,總決賽將實行雙倍積分制,各組別總決賽冠軍將收穫50分,這也讓之前積分落後並不太多的車手擁有了翻身逆轉的機會,那眾泰T600能否攜勢而來,獲得全年比賽的總冠軍,讓我們拭目以待!本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

※回頭車貨運收費標準

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

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

.NET Web應用中為什麼要使用async/await異步編程

前言

  1. 什麼是async/await?
    await和async是.NET Framework4.5框架、C#5.0語法裏面出現的技術,目的是用於簡化異步編程模型。

  2. async和await的關係?
    async和await是成對出現的。
    async出現在方法的聲明裡,用於批註一個異步方法。光有async是沒有意義的。
    await出現在方法內部,Task前面。只能在使用async關鍵字批註的方法中使用await關鍵字。

        private async Task DoSomething()
        {
            await Task.Delay(TimeSpan.FromSeconds(10));
        }
  1. async/await會創建新的線程嗎?
    不會。async/await關鍵字本身是不會創建新的線程的,但是被await的方法內部一般會創建新的線程。

  2. asp.net mvc/webapi action中使用async/await會提高請求的響應速度嗎?
    不會。

正題

我們都知道web應用不同於winform、wpf等客戶端應用,客戶端應用為了保證UI渲染的一致性往往都是採用單線程模式,這個UI線程稱為主線程,如果在主線程做耗時操作就會導致程序界面假死,所以客戶端開發中使用多線程異步編程非常必要。
可web應用本身就是多線程模式,服務器會為每個請求分配工作線程。
既然async/await不能創建新線程,又不能使提高請求的響應速度,那.NET Web應用中為什麼要使用async/await異步編程呢?

在 web 服務器上,.NET Framework 維護用於處理 ASP.NET 請求的線程池。 當請求到達時,將調度池中的線程以處理該請求。 如果以同步方式處理請求,則處理請求的線程將在處理請求時處於繁忙狀態,並且該線程無法處理其他請求。

在啟動時看到大量併發請求的 web 應用中,或具有突發負載(其中併發增長突然增加)時,使 web 服務調用異步會提高應用程序的響應能力。 異步請求與同步請求所需的處理時間相同。 如果請求發出需要兩秒鐘時間才能完成的 web 服務調用,則該請求將需要兩秒鐘,無論是同步執行還是異步執行。 但是,在異步調用期間,線程在等待第一個請求完成時不會被阻止響應其他請求。 因此,當有多個併發請求調用長時間運行的操作時,異步請求會阻止請求隊列和線程池的增長。

下面用代碼來實際測試一下:

  • 先是同步的方式,代碼很簡單,就是輸出一下請求開始和結束的時間和線程ID:
        public ActionResult Index()
        {
            DateTime startTime = DateTime.Now;//進入DoSomething方法前的時間
            var startThreadId = Thread.CurrentThread.ManagedThreadId;//進入DoSomething方法前的線程ID

            DoSomething();//耗時操作

            DateTime endTime = DateTime.Now;//完成DoSomething方法的時間
            var endThreadId = Thread.CurrentThread.ManagedThreadId;//完成DoSomething方法后的線程ID
            return Content($"startTime:{ startTime.ToString("yyyy-MM-dd HH:mm:ss:fff") } startThreadId:{ startThreadId }<br/>endTime:{ endTime.ToString("yyyy-MM-dd HH:mm:ss:fff") } endThreadId:{ endThreadId }<br/><br/>");
        }

        /// <summary>
        /// 耗時操作
        /// </summary>
        /// <returns></returns>
        private void DoSomething()
        {
            Thread.Sleep(10000);
        }

使用瀏覽器開3個標籤頁進行測試(因為瀏覽器對同一域名下的連接數有限制,一般是6個左右,所以就弄3個吧):

可以看到耗時都是10秒,開始和結束的線程ID一致。

  • 下面改造成異步的:
        public async Task<ActionResult> Index()
        {
            DateTime startTime = DateTime.Now;//進入DoSomething方法前的時間
            var startThreadId = Thread.CurrentThread.ManagedThreadId;//進入DoSomething方法前的線程ID

            await DoSomething();//耗時操作

            DateTime endTime = DateTime.Now;//完成DoSomething方法的時間
            var endThreadId = Thread.CurrentThread.ManagedThreadId;//完成DoSomething方法后的線程ID
            return Content($"startTime:{ startTime.ToString("yyyy-MM-dd HH:mm:ss:fff") } startThreadId:{ startThreadId }<br/>endTime:{ endTime.ToString("yyyy-MM-dd HH:mm:ss:fff") } endThreadId:{ endThreadId }<br/><br/>");
        }

        /// <summary>
        /// 耗時操作
        /// </summary>
        /// <returns></returns>
        private async Task DoSomething()
        {
            await Task.Run(() => Thread.Sleep(10000));
        }

結果:

可以看到3次請求中,雖然耗時都是10秒,但是出現了開始和結束的線程ID不一致的情況,ID為22的這個線程工作了多次,這意味着使用異步方式在同一時間可以處理更多的請求!(這句話不太對,3個同步的併發請求必然會分配3個工作線程,但是使用異步的話,同一個線程可以被多個請求重複利用。因為線程池的線程數量是有上限的,所以在相同數量的線程下,使用異步方式能處理更多的請求。)
IIS默認隊列長度:

await關鍵字不會阻塞線程直到任務完成。 它將方法的其餘部分註冊為任務的回調,並立即返回。 當await的任務最終完成時,它將調用該回調,並因此在其中斷時繼續執行方法。

簡單來說:就是使用同步方法時,線程會被耗時操作一直佔有,直到耗時操作完成。而使用異步方法,程序走到await關鍵字時會立即return,釋放線程,餘下的代碼會放進一個回調中(Task.GetAwaiter()的UnsafeOnCompleted(Action)回調),耗時操作完成時才會回調執行,所以async/await是語法糖,其本質是一個狀態機。

那是不是所有的action都要用async/await呢?
不是。一般的磁盤IO或者網絡請求等耗時操作才考慮使用異步,不要為了異步而異步,異步也是需要消耗性能的,使用不合理會適得其反。

結論

async/await異步編程不能提升響應速度,但是可以提升響應能力(吞吐量)。異步和同步各有優劣,要合理選擇,不要為了異步而異步。

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

【其他文章推薦】

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

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

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

※回頭車貨運收費標準

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

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

自己動手實現深度學習框架-7 RNN層–GRU, LSTM

目標

        這個階段會給cute-dl添加循環層,使之能夠支持RNN–循環神經網絡. 具體目標包括:

  1. 添加激活函數sigmoid, tanh.
  2. 添加GRU(Gate Recurrent Unit)實現.
  3. 添加LSTM(Long Short-term Memory)實現.
  4. 使用基於GRU和LSTM的RNN模型擬合一個正餘弦疊加函數.

RNN原理

原始的RNN

        RNN模型用來捕捉序列數據的特徵. 給定一個長度為T的輸入系列\(X=(x_1, x_2, .., X_T)\), RNN層輸出一個長度為T的序列\(H=(h_1, h_2, …, H_T)\), 對於任意時間步t, 可以表示為:

\[H_t = δ(X_tW_x + H_{t-1}W_h + b), \quad t = 2, 3, .., T \]

        函數δ是sigmoid函數:

\[δ = \frac{1}{1 + e^{-x}} \]

        \(H_t\)包含了前面第1到t-1步的所有信息。 和CNN層類似, CNN層在空間上共享參數, RNN層在時間步上共享參數\(W_x, W_h, b\).

        RNN層中隱藏層的數量為T-2, 如果T較大(超過10), 反向傳播是很容易出現梯度爆炸. GRU和LSTM就是為了解決這個問題而誕生, 這兩種模型,可以讓RNN能夠支持長度超過1000的輸入序列。

GRU

        GRU使用了不同功能的門控單元, 分別捕捉序列上不同時間跨度的的依賴關係。每個門控單元都會都有獨立的參數, 這些參數在時間步上共享。

        GRU的門控單元有:

        \(R_t = δ(X_tW^r_x + H_{t-1}W^r_h + b^r)\), 重置門用於捕捉短期依賴關係.

        \(U_t = δ(X_tW^u_x + H_{t-1}W^u_h + b^u)\), 更新門用於捕捉長期依賴關係

        \(\bar{H}_t = tanh(X_t\bar{W}_x + (R_t * H_{t-1})\bar{W}_h + \bar{b})\)

        除此之外, 還有一個輸出單元:

        \(H_t = U_t * H_{t-1} + (1-U_t)*\bar{H}_t\)

LSTM

        LSTM的設計思路和GRU類似, 同樣使用了多個門控單元:

        \(I_t = δ(X_tW^i_x + H_{t-1}W^i_h + b^i)\), 輸入門,過濾記憶門的輸出.

        \(F_t = δ(X_tW^f_x + H_{t-1}W^f_h + b^f)\), 遺忘門, 過濾前面時間步的記憶.

        \(O_t = δ(X_tW^o_x + H_{t-1}W^o_h + b^o)\), 輸出門, 過濾當前時間步的記憶.

        \(M_t = tanh(X_tW^m_x + H_{t-1}W^m_h + b^m)\), 記憶門.

        它還有自己獨有的記憶單元和輸出單元:

        \(\bar{M}_t = F_t * \bar{M}_{t-1} + I_t * M_t\)

        \(H_t = O_t * tanh(\bar{M}_t)\)

RNN實現

        設計要求:

  1. RNN層中的隱藏層的數量是基於序列長度的,輸入序列有多長, RNN層應生成對應數量的隱藏層。
  2. RNN層在時間步上共享參數, 從前面的描述可以看出, 只有門控單元有參數,因此門控單元應獨立實現。
  3. 任意一個時間步上的層都依賴上一個時間步的輸出,在正向傳播和反向傳播過程中都需要上一個時間步的輸出, 每個門控單元都使用棧保存上一個時間步的輸出.
  4. 默認情況下RNN層輸出所有時間步的輸出。但有時只需要最後一個時間步的輸出, 這種情況下使用過濾層, 只向下一層傳播最後一個時間步的輸出。
  5. 使用門控單元實現GRU和LSTM

RNN基礎類的實現

RNN類

        文件: cutedl/rnn_layers.py, 類名: RNN

        這個類是RNN層基類, 它主要功能是控制向前傳播和向後傳播的主流程.

        初始化參數:

  '''
  out_units 輸出單元數
  in_units 輸入單元數
  stateful 保留當前批次的最後一個時間步的狀態作為下一個批次的輸入狀態, 默認False不保留

  RNN 的輸入形狀是(m, t, in_units)
  m: batch_size
  t: 輸入系列的長度
  in_units: 輸入單元數頁是輸入向量的維數

  輸出形狀是(m, t, out_units)
  '''
  def __init__(self, out_units, in_units=None, stateful=False, activation='linear'):

        向前傳播

def forward(self, in_batch, training):
    m, T, n = in_batch.shape
    out_units = self.__out_units
    #所有時間步的輸出
    hstatus = np.zeros((m, T, out_units))
    #上一步的輸出
    pre_hs = self.__pre_hs
    if pre_hs is None:
        pre_hs = np.zeros((m, out_units))

    #隱藏層循環過程, 沿時間步執行
    for t in range(T):
        hstatus[:, t, :] = self.hiden_forward(in_batch[:,t,:], pre_hs, training)
        pre_hs = hstatus[:, t, :]

    self.__pre_hs = pre_hs
    #pdb.set_trace()
    if not self.stateful:
        self.__pre_hs = None

    return hstatus

        反向傳播

def backward(self, gradient):
      m, T, n = gradient.shape

      in_units = self.__in_units
      grad_x = np.zeros((m, T, in_units))
      #pdb.set_trace()
      #從最後一個梯度開始反向執行.
      for t in range(T-1, -1, -1):
          grad_x[:,t,:], grad_hs = self.hiden_backward(gradient[:,t,:])
          #pdb.set_trace()
          if t - 1 >= 0:
              gradient[:,t-1,:] = gradient[:,t-1,:] + grad_hs

      #pdb.set_trace()
      return grad_x

sigmoid和tanh激活函數

sigmoid及其導數

\[sigmoid = \frac{1}{1+e^{-x}} \]

\[\frac{d}{dx}sigmoid = sigmoid(1-sigmoid) \]

tanh及其導數

\[tanh = \frac{e^x – e^{-x}}{e^x + e^{-x}} \]

\[\frac{d}{dx}tanh = 1 – tanh^2 \]

門控單元實現

        文件: cutedl/rnn_layers.py, 類名: GateUint

        門控單元是RNN層基礎的參數單元. 和Dense層類似,它是Layer的子類,負責學習和使用參數。但在學習和使用參數的方式上有很大的不同:

  • Dense有兩個參數矩陣, GateUnit有3個參數矩陣.
  • Dense在一次反向傳播過程中只使用當前的梯度學習參數,而GateUnit會累積每個時間步的梯度。

        下面我們會主要看一下GateUnit特別之處的代碼.

        在__ init__方法中定義參數和棧:

    #3個參數
    self.__W = None #當前時間步in_batch權重參數
    self.__Wh = None #上一步輸出的權重參數
    self.__b = None #偏置量參數

    #輸入棧
    self.__hs = []  #上一步輸出
    self.__in_batchs = [] #當前時間步的in_batch

        正向傳播:

  def forward(self, in_batch, hs, training):
      W = self.__W.value
      b = self.__b.value
      Wh = self.__Wh.value

      out = in_batch @ W + hs @ Wh + b

      if training:
          #向前傳播訓練時把上一個時間步的輸出和當前時間步的in_batch壓棧
          self.__hs.append(hs)
          self.__in_batchs.append(in_batch)

          #確保反向傳播開始時參數的梯度為空
          self.__W.gradient = None
          self.__Wh.gradient = None
          self.__b.gradient = None

      return self.activation(out)

        反向傳播:

def backward(self, gradient):
    grad = self.activation.grad(gradient)

    W = self.__W.value
    Wh = self.__Wh.value
    pre_hs = self.__hs.pop()
    in_batch = self.__in_batchs.pop()

    grad_in_batch = grad @ W.T
    grad_W = in_batch.T @ grad
    grad_hs = grad @ Wh.T
    grad_Wh = pre_hs.T @ grad
    grad_b = grad.sum(axis=0)

    #反向傳播計算
    if self.__W.gradient is None:
        #當前批次第一次
        self.__W.gradient = grad_W
    else:
        #累積當前批次的所有梯度
        self.__W.gradient = self.__W.gradient + grad_W

    if self.__Wh.gradient is None:
        self.__Wh.gradient = grad_Wh
    else:
        self.__Wh.gradient = self.__Wh.gradient +  grad_Wh

    if self.__b.gradient is None:
        self.__b.gradient = grad_b
    else:
        self.__b.gradient = self.__b.gradient + grad_b

    return grad_in_batch, grad_hs

GRU實現

        文件: cutedl/rnn_layers.py, 類名: GRU

        隱藏單初始化:

def set_parent(self, parent):
    super().set_parent(parent)

    out_units = self.out_units
    in_units = self.in_units

    #pdb.set_trace()
    #重置門
    self.__g_reset = GateUnit(out_units, in_units)
    #更新門
    self.__g_update = GateUnit(out_units, in_units)
    #候選輸出門
    self.__g_cddout = GateUnit(out_units, in_units, activation='tanh')

    self.__g_reset.set_parent(self)
    self.__g_update.set_parent(self)
    self.__g_cddout.set_parent(self)

    #重置門乘法單元
    self.__u_gr = MultiplyUnit()
    #輸出單元
    self.__u_out = GRUOutUnit()

        向前傳播:

  def hiden_forward(self, in_batch, pre_hs, training):
      gr = self.__g_reset.forward(in_batch, pre_hs, training)
      gu = self.__g_update.forward(in_batch, pre_hs, training)
      ugr = self.__u_gr.forward(gr, pre_hs, training)
      cddo = self.__g_cddout.forward(in_batch, ugr, training)

      hs = self.__u_out.forward(gu, pre_hs, cddo, training)

      return hs

        反向傳播:

def hiden_backward(self, gradient):

    grad_gu, grad_pre_hs, grad_cddo = self.__u_out.backward(gradient)
    #pdb.set_trace()
    grad_in_batch, grad_ugr = self.__g_cddout.backward(grad_cddo)

    #計算梯度的過程中需要累積上一層輸出的梯度
    grad_gr, g_pre_hs = self.__u_gr.backward(grad_ugr)
    grad_pre_hs = grad_pre_hs + g_pre_hs

    g_in_batch, g_pre_hs = self.__g_update.backward(grad_gu)
    grad_in_batch = grad_in_batch + g_in_batch
    grad_pre_hs = grad_pre_hs + g_pre_hs

    g_in_batch, g_pre_hs = self.__g_reset.backward(grad_gr)
    grad_in_batch = grad_in_batch + g_in_batch
    grad_pre_hs = grad_pre_hs + g_pre_hs

    #pdb.set_trace()
    return grad_in_batch, grad_pre_hs    

LSTM實現

        文件: cutedl/rnn_layers.py, 類名: LSTM

        隱藏單元初始化:

def set_parent(self, layer):
    super().set_parent(layer)

    in_units = self.in_units
    out_units = self.out_units

    #輸入門
    self.__g_in = GateUnit(out_units, in_units)
    #遺忘門
    self.__g_forget = GateUnit(out_units, in_units)
    #輸出門
    self.__g_out = GateUnit(out_units, in_units)
    #記憶門
    self.__g_memory = GateUnit(out_units, in_units, activation='tanh')

    self.__g_in.set_parent(self)
    self.__g_forget.set_parent(self)
    self.__g_out.set_parent(self)
    self.__g_memory.set_parent(self)

    #記憶單元
    self.__memory_unit =LSTMMemoryUnit()
    #輸出單元
    self.__out_unit = LSTMOutUnit()

        向前傳播:

def hiden_forward(self, in_batch, hs, training):
    g_in = self.__g_in.forward(in_batch, hs, training)
    #pdb.set_trace()
    g_forget = self.__g_forget.forward(in_batch, hs, training)
    g_out = self.__g_out.forward(in_batch, hs, training)
    g_memory = self.__g_memory.forward(in_batch, hs, training)

    memory = self.__memory_unit.forward(g_forget, g_in, g_memory, training)
    cur_hs = self.__out_unit.forward(g_out, memory, training)

    return cur_hs

        反向傳播:

def hiden_backward(self, gradient):
    #pdb.set_trace()
    grad_out, grad_memory = self.__out_unit.backward(gradient)
    grad_forget, grad_in, grad_gm = self.__memory_unit.backward(grad_memory)

    grad_in_batch, grad_hs = self.__g_memory.backward(grad_gm)
    tmp1, tmp2 = self.__g_out.backward(grad_out)
    grad_in_batch += tmp1
    grad_hs += tmp2

    tmp1, tmp2 = self.__g_forget.backward(grad_forget)
    grad_in_batch += tmp1
    grad_hs += tmp2

    tmp1, tmp2 = self.__g_in.backward(grad_in)
    grad_in_batch += tmp1
    grad_hs += tmp2

    return grad_in_batch, grad_hs

驗證

        接下來, 驗證示例將會構建一個簡單的RNN模型, 使用該模型擬合一個正餘弦疊加函數:

#採樣函數
def sample_function(x):
    y = 3*np.sin(2 * x * np.pi) + np.cos(x * np.pi) + np.random.uniform(-0.05,0.05,len(x))
    return y

        訓練數據集和測試數據集在這個函數的不同定義域區間內樣. 訓練數據集的採樣區間為[1, 200.01), 測試數據集的採樣區間為[200.02, 240.002). 模型任務是預測這個函數值的序列.

        示例代碼在examples/rnn/fit_function.py文件中.

使用GRU構建的模型

def fit_gru():
    model = Model([
                rnn.GRU(32, 1),
                nn.Filter(),
                nn.Dense(32),
                nn.Dense(1, activation='linear')
            ])
    model.assemble()
    fit('gru', model)

訓練報告:

使用LSTM構建的模型

def fit_lstm():
    model = Model([
                rnn.LSTM(32, 1),
                nn.Filter(),
                nn.Dense(2),
                nn.Dense(1, activation='linear')
            ])
    model.assemble()
    fit('lstm', model)

訓練報告:

總結

        這個階段,框架新增了RNN的兩個最常見的實現:GRU和LSTM, 相應地增加了它需要的激活函數. cute-dl已經具備了構建最基礎RNN模型的能力。通過驗證發現, GRU模型和LSTM模型在簡單任務上都表現出了很好的性能。會添加嵌入層,使框架能夠構建文本分類任務的模型,然後在imdb-review(電影評價)數據集上進行驗證.

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

【其他文章推薦】

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

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

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

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

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

※超省錢租車方案

※回頭車貨運收費標準

如何優雅地停止 Spring Boot 應用?

首先來介紹下什麼是優雅地停止,簡而言之,就是對應用進程發送停止指令之後,能保證正在執行的業務操作不受影響,可以繼續完成已有請求的處理,但是停止接受新請求

在 Spring Boot 2.3 中增加了新特性優雅停止,目前 Spring Boot 內置的四個嵌入式 Web 服務器(Jetty、Reactor Netty、Tomcat 和 Undertow)以及反應式和基於 Servlet 的 Web 應用程序都支持優雅停止。

下面,我們先用新版本嘗試下:

Spring Boot 2.3 優雅停止

首先創建一個 Spring Boot 的 Web 項目,版本選擇 2.3.0.RELEASE,Spring Boot 2.3.0.RELEASE 版本內置的 Tomcat 為 9.0.35

然後需要在 application.yml 中添加一些配置來啟用優雅停止的功能:

# 開啟優雅停止 Web 容器,默認為 IMMEDIATE:立即停止
server:
  shutdown: graceful

# 最大等待時間
spring:
  lifecycle:
    timeout-per-shutdown-phase: 30s

其中,平滑關閉內置的 Web 容器(以 Tomcat 為例)的入口代碼在 org.springframework.boot.web.embedded.tomcatGracefulShutdown 里,大概邏輯就是先停止外部的所有新請求,然後再處理關閉前收到的請求,有興趣的可以自己去看下。

內嵌的 Tomcat 容器平滑關閉的配置已經完成了,那麼如何優雅關閉 Spring 容器了,就需要 Actuator 來實現 Spring 容器的關閉了。

然後加入 actuator 依賴,依賴如下所示:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

然後接着再添加一些配置來暴露 actuator 的 shutdown 接口:

# 暴露 shutdown 接口
management:
  endpoint:
    shutdown:
      enabled: true
  endpoints:
    web:
      exposure:
        include: shutdown

其中通過 Actuator 關閉 Spring 容器的入口代碼在 org.springframework.boot.actuate.context 包下 ShutdownEndpoint 類中,主要的就是執行 doClose() 方法關閉並銷毀 applicationContext,有興趣的可以自己去看下。

配置搞定后,然後在 controller 包下創建一個 WorkController 類,並有一個 work 方法,用來模擬複雜業務耗時處理流程,具體代碼如下:

@RestController
public class WorkController {

    @GetMapping("/work")
    public String work() throws InterruptedException {
        // 模擬複雜業務耗時處理流程
        Thread.sleep(10 * 1000L);
        return "success";
    }
}

然後,我們啟動項目,先用 Postman 請求 http://localhost:8080/work 處理業務:

然後在這個時候,調用 http://localhost:8080/actuator/shutdown 就可以執行優雅地停止,返回結果如下:

{
    "message": "Shutting down, bye..."
}

如果在這個時候,發起新的請求 http://localhost:8080/work,會沒有反應:

再回頭看第一個請求,返回了結果:success

其中有幾條服務日誌如下:

2020-05-20 23:05:15.163  INFO 102724 --- [     Thread-253] o.s.b.w.e.tomcat.GracefulShutdown        : Commencing graceful shutdown. Waiting for active requests to complete
2020-05-20 23:05:15.287  INFO 102724 --- [tomcat-shutdown] o.s.b.w.e.tomcat.GracefulShutdown        : Graceful shutdown complete
2020-05-20 23:05:15.295  INFO 102724 --- [     Thread-253] o.s.s.concurrent.ThreadPoolTaskExecutor  : Shutting down ExecutorService 'applicationTaskExecutor'

從日誌中也可以看出來,當調用 shutdown 接口的時候,會先等待請求處理完畢后再優雅地停止。

到此為止,Spring Boot 2.3 的優雅關閉就講解完了,是不是很簡單呢?如果是在之前不支持優雅關閉的版本如何去做呢?

Spring Boot 舊版本優雅停止

在這裏介紹 GitHub 上 issue 里 Spring Boot 開發者提供的一種方案:

選取的 Spring Boot 版本為 2.2.6.RELEASE,首先要實現 TomcatConnectorCustomizer 接口,該接口是自定義 Connector 的回調接口:

@FunctionalInterface
public interface TomcatConnectorCustomizer {

	void customize(Connector connector);
}

除了定製 Connector 的行為,還要實現 ApplicationListener<ContextClosedEvent> 接口,因為要監聽 Spring 容器的關閉事件,即當前的 ApplicationContext 執行 close() 方法,這樣我們就可以在請求處理完畢後進行 Tomcat 線程池的關閉,具體的實現代碼如下:

@Bean
public GracefulShutdown gracefulShutdown() {
    return new GracefulShutdown();
}

private static class GracefulShutdown implements TomcatConnectorCustomizer, ApplicationListener<ContextClosedEvent> {
    private static final Logger log = LoggerFactory.getLogger(GracefulShutdown.class);

    private volatile Connector connector;

    @Override
    public void customize(Connector connector) {
        this.connector = connector;
    }

    @Override
    public void onApplicationEvent(ContextClosedEvent event) {
        this.connector.pause();
        Executor executor = this.connector.getProtocolHandler().getExecutor();
        if (executor instanceof ThreadPoolExecutor) {
            try {
                ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) executor;
                threadPoolExecutor.shutdown();
                if (!threadPoolExecutor.awaitTermination(30, TimeUnit.SECONDS)) {
                    log.warn("Tomcat thread pool did not shut down gracefully within 30 seconds. Proceeding with forceful shutdown");
                }
            } catch (InterruptedException ex) {
                Thread.currentThread().interrupt();
            }
        }
    }
}

有了定製的 Connector 回調,還需要在啟動過程中添加到內嵌的 Tomcat 容器中,然後等待監聽到關閉指令時執行,addConnectorCustomizers 方法可以把定製的 Connector 行為添加到內嵌的 Tomcat 中,具體代碼如下:

@Bean
public ConfigurableServletWebServerFactory tomcatCustomizer() {
    TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory();
    factory.addConnectorCustomizers(gracefulShutdown());
    return factory;
}

到此為止,內置的 Tomcat 容器平滑關閉的操作就完成了,Spring 容器優雅停止上面已經說過了,再次就不再贅述了。

通過測試,同樣可以達到上面那樣優雅停止的效果。

總結

本文主要講解了 Spring Boot 2.3 版本和舊版本的優雅停止,避免強制停止導致正在處理的業務邏輯會被中斷,進而導致產生業務異常的情形。

另外使用 Actuator 的同時要注意安全問題,比如可以通過引入 security 依賴,打開安全限制並進行身份驗證,設置單獨的 Actuator 管理端口並配置只對內網開放等。

本文的完整代碼在 https://github.com/wupeixuan/SpringBoot-Learngraceful-shutdown 目錄下。

最好的關係就是互相成就,大家的在看、轉發、留言三連就是我創作的最大動力。

參考

https://github.com/spring-projects/spring-boot/issues/4657

https://github.com/wupeixuan/SpringBoot-Learn

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

【其他文章推薦】

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

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

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

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

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

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

※回頭車貨運收費標準

聊聊Asp.net過濾器Filter那一些事

  最近在整理優化.net代碼時,發現幾個很不友好的處理現象:登錄判斷、權限認證、日誌記錄、異常處理等通用操作,在項目中的action中到處都是。在代碼優化上,這一點是很重要着力點。這時.net中的過濾器、攔截器(Filter)就派上用場了。現在根據這幾天的實際工作,對其做了一個簡單的梳理,分享出來,以供大家參考交流,如有寫的不妥之處,多多指出,多多交流。

概述:

.net中的Filter中主要包括以下4大類:Authorize(授權),ActionFilter(自定義),HandleError(錯誤處理)。

過濾器

類名

實現接口

描述

授權

AuthorizeAttribute

IAuthorizationFilter

此類型(或過濾器)用於限制進入控制器或控制器的某個行為方法,比如:登錄、權限、訪問控制等等

異常

HandleErrorAttribute

IExceptionFilter

用於指定一個行為,這個被指定的行為處理某個行為方法或某個控制器裏面拋出的異常,比如:全局異常統一處理。

自定義

ActionFilterAttribute

IActionFilterIResultFilter

用於進入行為之前或之後的處理或返回結果的之前或之後的處理,比如:用戶請求日誌詳情日誌記錄

 

AuthorizeAttribute:認證授權

認證授權主要是對所有action的訪問第一入口認證,對用戶的訪問做第一道監管過濾攔截閘口。

實現方式:需要自定義一個類,繼承AuthorizeAttribute並重寫OnAuthorization,在OnAuthorization中能夠獲取到用戶請求的所有Request信息,其實我們做的所有認證攔截操作,其所有數據支撐都是來自Request中。

具體驗證流程設計:

IP白名單:這個主要針對的是API做IP限制,只有指定IP才可訪問,非指定IP直接返回

請求頻率控制:這個主要是控制用戶的訪問頻率,主要是針對API做,超出請求頻率直接返回。

登錄認證:登錄認證一般我們採用的是通過在請求的header中傳遞token的方式來進行驗證,這樣即使用與一般的MVC登錄認證,也使用與API接口的Auth認證,並且也不依賴於用戶前端js設置等。

授權認證:授權認證就簡單了,主要是驗證該用戶是否具有該權限,如果不具有,直接做下相應的返回處理。

MVC和API異同:

  命名空間:MVC:System.Web.Http.Filters;API:System.Web.Mvc

  注入方式:在注入方式上,主要包括:全局->控制器Controller->行為Action

  全局註冊:針對所有系統的所有Aciton都使用

  Controller:只針對該Controller下的Action起作用

  Action:只針對該Action起作用

其中全局註冊,針對MVC和API還有一些差異:

  MVC在 FilterConfig.cs中注入
    filters.Add(new XYHMVCAuthorizeAttribute());

  API 在 WebApiConfig.cs 中注入

     config.Filters.Add(new XYHAPIAuthorizeAttribute());

注意事項:在實際使用中,針對認證授權,我們一般都是添加全局認證,但是,有的action又不需要做認證,比如本來的登錄Action等等,那麼該如何排除呢?其實也很簡單,我們只需要在自定定義一個Attribute集成Attribute,或者系統的AllowAnonymousAttribute,在不需要驗證的action中只需要註冊上對於的Attribute,並在驗證前做一個過濾即可,比如:

    // 有 AllowAnonymous 屬性的接口直接開綠燈

            if (actionContext.ActionDescriptor.GetCustomAttributes<AllowAnonymousAttribute>().Any())

            {

                return;

            }

API AuthFilterAttribute實例代碼

/// <summary>
    /// 授權認證過濾器
    /// </summary>
    public class XYHAPIAuthFilterAttribute : AuthorizationFilterAttribute
    {
        /// <summary>
        /// 認證授權驗證
        /// </summary>
        /// <param name="actionContext">請求上下文</param>
        public override void OnAuthorization(HttpActionContext actionContext)
        {
            // 有 AllowAnonymous 屬性的接口直接開綠燈
            if (actionContext.ActionDescriptor.GetCustomAttributes<AllowAnonymousAttribute>().Any())
            {
                return;
            }

            // 在請求前做一層攔截,主要驗證token的有效性和驗簽
            HttpRequest httpRequest = HttpContext.Current.Request;

            // 獲取apikey
            var apikey = httpRequest.QueryString["apikey"];

            // 首先做IP白名單校驗 
            MBaseResult<string> result = new AuthCheckService().CheckIpWhitelist(FilterAttributeHelp.GetIPAddress(actionContext.Request), apikey);

            // 檢驗時間戳
            string timestamp = httpRequest.QueryString["Timestamp"];
            if (result.Code == MResultCodeEnum.successCode)
            {
                // 檢驗時間戳 
                result = new AuthCheckService().CheckTimestamp(timestamp);
            }

            if (result.Code == MResultCodeEnum.successCode)
            {
                // 做請求頻率驗證 
                string acitonName = actionContext.ActionDescriptor.ActionName;
                string controllerName = actionContext.ActionDescriptor.ControllerDescriptor.ControllerName;
                result = new AuthCheckService().CheckRequestFrequency(apikey, $"api/{controllerName.ToLower()}/{acitonName.ToLower()}");
            }

            if (result.Code == MResultCodeEnum.successCode)
            {
                // 簽名校驗

                // 獲取全部的請求參數
                Dictionary<string, string> queryParameters = httpRequest.GetAllQueryParameters();

                result = new AuthCheckService().SignCheck(queryParameters, apikey);

                if (result.Code == MResultCodeEnum.successCode)
                {
                    // 如果有NoChekokenFilterAttribute 標籤 那麼直接不做token認證
                    if (actionContext.ActionDescriptor.GetCustomAttributes<XYHAPINoChekokenFilterAttribute>().Any())
                    {
                        return;
                    }

                    // 校驗token的有效性
                    // 獲取一個 token
                    string token = httpRequest.Headers.GetValues("Token") == null ? string.Empty :
                        httpRequest.Headers.GetValues("Token")[0];

                    result = new AuthCheckService().CheckToken(token, apikey, httpRequest.FilePath);
                }
            }

            // 輸出
            if (result.Code != MResultCodeEnum.successCode)
            {
                // 一定要實例化一個response,是否最終還是會執行action中的代碼
                actionContext.Response = new HttpResponseMessage(HttpStatusCode.OK);
                //需要自己指定輸出內容和類型
                HttpContext.Current.Response.ContentType = "text/html;charset=utf-8";
                HttpContext.Current.Response.Write(JsonConvert.SerializeObject(result));
                HttpContext.Current.Response.End(); // 此處結束響應,就不會走路由系統
            }
        }
    }

 

 MVC AuthFilterAttribute實例代碼

 

/// <summary>
    /// MVC自定義授權
    /// 認證授權有兩個重寫方法
    /// 具體的認證邏輯實現:AuthorizeCore 這個裡面寫具體的認證邏輯,認證成功返回true,反之返回false
    /// 認證失敗處理邏輯:HandleUnauthorizedRequest 前一步返回 false時,就會執行到該方法中
    /// 但是,我平時在應用過程中,一般都是在AuthorizeCore根據不同的認證結果,直接做認證后的邏輯處理
    /// </summary>
    public class XYHMVCAuthorizeAttribute : AuthorizeAttribute
    {
        /// <summary>
        /// 認證邏輯
        /// </summary>
        /// <param name="filterContext">過濾器上下文</param>
        public override void OnAuthorization(AuthorizationContext filterContext)
        {

            // 此處主要寫認證授權的相關驗證邏輯
            // 該部分的驗證一般包括兩個部分
            // 登錄權限校驗
            //   --我們的一般處理方式是,通過header中傳遞一個token來進行邏輯驗證
            //   --當然不同的系統在設計上也不盡相同,有的也會採用session等方式來驗證
            //   --所以最終還是根據其項目本身的實際情況來進行對應的邏輯操作

            // 具體的頁面權限校驗
            // --該部分的驗證是具體的到頁面權限驗證
            // --我看有得小夥伴沒有做到這一個程度,直接將這一步放在前端js來驗證,這樣不是很安全,但是可以攔住小白用戶
            // --當然有的系統根本就沒有做權限控制,那就更不需要這一個邏輯了。
            // --所以最終還是根據其項目本身的實際情況來進行對應的邏輯操作

            // 現在用一個粗暴的方式來簡單模擬實現過,用系統當前時間段秒廚藝3,取餘數
            // 當餘數為0:認證授權通過
            //         1:代表為登錄,調整至登錄頁面
            //         2:代表無訪問權限,調整至無權限提示頁面

            // 當然,在這也還可以做一些IP白名單,IP黑名單驗證  請求頻率驗證等等

            // 說到這而,還有一點需要注意,如果我們選擇的是全局註冊該過濾器,那麼如果有的頁面根本不需要權限認證,比如登錄頁面,那麼我們可以給不需要權限的認證的控制器或者action添加一個特殊的註解 AllowAnonymous ,來排除

            // 獲取Request的幾個關鍵信息
            HttpRequest httpRequest = HttpContext.Current.Request;
            string acitonName = filterContext.ActionDescriptor.ActionName;
            string controllerName = filterContext.ActionDescriptor.ControllerDescriptor.ControllerName;

            // 注意:如果認證不通過,需要設置filterContext.Result的值,否則還是會執行action中的邏輯

            filterContext.Result = null;
            int thisSecond = System.DateTime.Now.Second;
            switch (thisSecond % 3)
            {
                case 0:
                    // 認證授權通過
                    break;
                case 1:
                    // 代表為登錄,調整至登錄頁面
                    // 只有設置了Result才會終結操作
                    filterContext.Result = new RedirectResult("/html/Login.html");
                    break;
                case 2:
                    // 代表無訪問權限,調整至無權限提示頁面
                    filterContext.Result = new RedirectResult("/html/NoAuth.html");
                    break;
            }
        }
    }

 

ActionFilter自定義過濾器

自定義過濾器,主要是監控action請求前後,處理結果返回前後的事件。其中API只有請求前後的兩個方法。

重新方法

方法功能描述

使用於

OnActionExecuting

一個請求在進入到aciton邏輯前執行

MVCAPI

OnActionExecuted

一個請求aciton邏輯執行后執行

MVCAPI

OnResultExecuting

對應的view視圖渲染前執行

MVC

OnResultExecuted

對應的view視圖渲染后執行

MVC

 

在這幾個方法中,我們一般主要用來記錄交互日誌,記錄每一個步驟的耗時情況,以便後續系統優化使用。具體的使用,根據自身的業務場景使用。

其中MVC和API的異同點,和上面說的認證授權的異同類似,不在詳細說明。

下面的一個實例代碼:

API定義過濾器實例DEMO代碼

 

/// <summary>
    /// Action過濾器
    /// </summary>
    public class XYHAPICustomActionFilterAttribute : ActionFilterAttribute
    {
        /// <summary>
        /// Action執行開始
        /// </summary>
        /// <param name="actionContext"></param>
        public override void OnActionExecuting(HttpActionContext actionContext)
        {

        }

        /// <summary>
        /// action執行以後
        /// </summary>
        /// <param name="actionContext"></param>
        public override void OnActionExecuted(HttpActionExecutedContext actionContext)
        {
            try
            {
                // 構建一個日誌數據模型
                MApiRequestLogs apiRequestLogsM = new MApiRequestLogs();

                // API名稱
                apiRequestLogsM.API = actionContext.Request.RequestUri.AbsolutePath;

                // apiKey
                apiRequestLogsM.API_KEY = HttpContext.Current.Request.QueryString["ApiKey"];

                // IP地址
                apiRequestLogsM.IP = FilterAttributeHelp.GetIPAddress(actionContext.Request);

                // 獲取token
                string token = HttpContext.Current.Request.Headers.GetValues("Token") == null ? string.Empty :
                              HttpContext.Current.Request.Headers.GetValues("Token")[0];
                apiRequestLogsM.TOKEN = token;

                // URL
                apiRequestLogsM.URL = actionContext.Request.RequestUri.AbsoluteUri;

                // 返回信息
                var objectContent = actionContext.Response.Content as ObjectContent;
                var returnValue = objectContent.Value;
                apiRequestLogsM.RESPONSE_INFOR = returnValue.ToString();

                // 由於數據庫中最大隻能存儲4000字符串,所以對返回值做一個截取
                if (!string.IsNullOrEmpty(apiRequestLogsM.RESPONSE_INFOR) &&
                    apiRequestLogsM.RESPONSE_INFOR.Length > 4000)
                {
                    apiRequestLogsM.RESPONSE_INFOR = apiRequestLogsM.RESPONSE_INFOR.Substring(0, 2000);
                }

                // 請求參數
                apiRequestLogsM.REQUEST_INFOR = actionContext.Request.RequestUri.Query;

                // 定義一個異步委託 ,異步記錄日誌
                //  Func<MApiRequestLogs, string> action = AddApiRequestLogs;//聲明一個委託
                // IAsyncResult ret = action.BeginInvoke(apiRequestLogsM, null, null);

            }
            catch (Exception ex)
            {

            }
        }
    }

 

HandleError錯誤處理

異常處理對於我們來說很常用,很好的利用異常處理,可以很好的避免全篇的try/catch。異常處理箱單很簡單,值需要自定義集成:ExceptionFilterAttribute,並自定義實現:OnException方法即可。

在OnException我們可以根據自身需要,做一些相應的邏輯處理,比如記錄異常日誌,便於後續問題分析跟進。

OnException還有一個很重要的處理,那就是對異常結果的統一包裝,返回一個很友好的結果給用戶,避免把一些不必要的信息返回給用戶。比如:針對MVC,那麼跟進不同異常,統一調整至友好的提示頁面等等;針對API,那麼我們可以一個統一的返回幾個封裝,便於用戶統一處理結果。

MVC 的異常處理實例代碼:

 

   /// <summary>
    /// MVC自定義異常處理機制
    /// 說道異常處理,其實我們腦海中的第一反應,也該是try/cache操作
    /// 但是在實際開發中,很有可能地址錯誤根本就進入不到try中,又或者沒有被try處理到異常
    /// 該類就發揮了作用,能夠很好的未經捕獲的異常,並做相應的邏輯處理
    /// 自定義異常機制,主要集成HandleErrorAttribute 重寫其OnException方法
    /// </summary>
    public class XYHMVCHandleError : HandleErrorAttribute
    {
        /// <summary>
        /// 處理異常
        /// </summary>
        /// <param name="filterContext">異常上下文</param>
        public override void OnException(ExceptionContext filterContext)
        {
            // 我們在平時的項目中,異常處理一般有兩個作用
            // 1:記錄異常的詳細日誌,便於事後分析日誌
            // 2:對異常的統一友好處理,比如根據異常類型重定向到友好提示頁面

            // 在這裏面既能獲取到未經處理的異常信息,也能獲取到請求信息
            // 在此可以根據實際項目需要做相應的邏輯處理
            // 下面簡單的列舉了幾個關鍵信息獲取方式

            // 控制器名稱 注意,這樣獲取出來的是一個文件的全路徑 
            string contropath = filterContext.Controller.ToString();

            // 訪問目錄的相對路徑
            string filePath = filterContext.HttpContext.Request.FilePath;

            // url完整地址
            string url = (filterContext.HttpContext.Request.Url.AbsoluteUri).ExUrlDeCode();

            // 請求方式 post get
            string httpMethod = filterContext.HttpContext.Request.HttpMethod;

            // 請求IP地址
            string ip = filterContext.HttpContext.Request.GetIPAddress();

            // 獲取全部的請求參數
            HttpRequest httpRequest = HttpContext.Current.Request;
            Dictionary<string, string> queryParameters = httpRequest.GetAllQueryParameters();

            // 獲取異常對象
            Exception ex = filterContext.Exception;

            // 異常描述信息
            string exMessage = ex.Message;

            // 異常堆棧信息
            string stackTrace = ex.StackTrace;

            // 根據實際情況記錄日誌(文本日誌、數據庫日誌,建議具體步驟採用異步方式來完成)


            filterContext.ExceptionHandled = true;

            // 模擬根據不同的做對應的邏輯處理
            int statusCode = filterContext.HttpContext.Response.StatusCode;

            if (statusCode>=400 && statusCode<500)
            {
                filterContext.Result = new RedirectResult("/html/404.html");
            }
            else 
            {
                filterContext.Result = new RedirectResult("/html/500.html");
            }
        }
    }

 

API 的異常處理實例代碼:

 

 /// <summary>
    /// API自定義異常處理機制
    /// 說道異常處理,其實我們腦海中的第一反應,也該是try/cache操作
    /// 但是在實際開發中,很有可能地址錯誤根本就進入不到try中,又或者沒有被try處理到異常
    /// 該類就發揮了作用,能夠很好的未經捕獲的異常,並做相應的邏輯處理
    /// 自定義異常機制,主要集成ExceptionFilterAttribute 重寫其OnException方法
    /// </summary>
    public class XYHAPIHandleError : ExceptionFilterAttribute
    {
        /// <summary>
        /// 處理異常
        /// </summary>
        /// <param name="actionExecutedContext">異常上下文</param>
        public override void OnException(HttpActionExecutedContext actionExecutedContext)
        {
            // 我們在平時的項目中,異常處理一般有兩個作用
            // 1:記錄異常的詳細日誌,便於事後分析日誌
            // 2:對異常的統一友好處理,比如根據異常類型重定向到友好提示頁面

            // 在這裏面既能獲取到未經處理的異常信息,也能獲取到請求信息
            // 在此可以根據實際項目需要做相應的邏輯處理
            // 下面簡單的列舉了幾個關鍵信息獲取方式

            // action名稱 
            string actionName = actionExecutedContext.ActionContext.ActionDescriptor.ActionName;

            // 控制器名稱 
            string controllerName =actionExecutedContext.ActionContext.ControllerContext.ControllerDescriptor.ControllerName;

            // url完整地址
            string url = (actionExecutedContext.Request.RequestUri.AbsoluteUri).ExUrlDeCode();

            // 請求方式 post get
            string httpMethod = actionExecutedContext.Request.Method.Method;

            // 請求IP地址
            string ip = actionExecutedContext.Request.GetIPAddress();

            // 獲取全部的請求參數
            HttpRequest httpRequest = HttpContext.Current.Request;
            Dictionary<string, string> queryParameters = httpRequest.GetAllQueryParameters();

            // 獲取異常對象
            Exception ex = actionExecutedContext.Exception;

            // 異常描述信息
            string exMessage = ex.Message;

            // 異常堆棧信息
            string stackTrace = ex.StackTrace;

            // 根據實際情況記錄日誌(文本日誌、數據庫日誌,建議具體步驟採用異步方式來完成)
            // 自己的記錄日誌落地邏輯略 ......

            // 構建統一的內部異常處理機制,相當於對異常做一層統一包裝暴露
            MBaseResult<string> result = new MBaseResult<string>()
            {
                Code = MResultCodeEnum.systemErrorCode,
                Message = MResultCodeEnum.systemError
            };

            actionExecutedContext.Response = new HttpResponseMessage(HttpStatusCode.OK);
            //需要自己指定輸出內容和類型
            HttpContext.Current.Response.ContentType = "text/html;charset=utf-8";
            HttpContext.Current.Response.Write(JsonConvert.SerializeObject(result));
            HttpContext.Current.Response.End(); // 此處結束響應,就不會走路由系統
        }
    }

 

總結

.net過濾器,我個人的一句話理解就是:對action的各個階段進行統一的監控處理等操作。.net過濾器中,其中每一個種過濾器的執行先後順序為:Authorize(授權)–>ActionFilter自定義–>HandleError(錯誤處理)

好了,就先聊到這而,如果什麼地方說的不對之處,多多指點和多多包涵。我自己寫了一個練習DEMO,裏面會有每一種情況的處理說明。有興趣的可以取下載下來看一看,謝謝。

DEMO在GitHub地址為:https://github.com/xuyuanhong0902/XYH.FilterTest.git

 

END
為了更高的交流,歡迎大家關注我的公眾號,掃描下面二維碼即可關注,謝謝:

 

 

認證授權

時間戳

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

【其他文章推薦】

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

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

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

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

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

※回頭車貨運收費標準

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

由美國普渡大學開發 友善環境的稀土開採技術獲專利

環境資訊中心外電;姜唯 翻譯;林大利 審校;稿源:ENS

美國普渡大學開發出更環境友善且具商業可行性的稀土開採技術。新技術已獲專利,有機會改寫產業生態,幫助美國創造更穩定的本土供給。

舉凡電腦、手機、DVD、充電電池、觸媒轉換器、磁鐵、風力渦輪機和日光燈、雷射測距儀、導航系統和精準武器都需要稀土金屬,但是在自然界中往往濃度不高,很難進行商業開採。

此外,目前以酸為基礎的稀土金屬分離和純化技術不利環境,世界各地的大多數公司無法進入市場。

碳酸鑭的結晶體,鑭是稀土之一。照片來源:ZEISS Microscopy(CC BY-NC-ND 2.0)

中國是全球稀土主要生產者。17種稀土金屬的全球儲藏量,有36%掌握在中國手上。目前中國已不再像1980至1990年代,以低於生產成本的價格出售稀土,其他國家也有機會成為稀土生產者。

但是當中國在2010年降低稀土金屬的出口配額時,一台風力渦輪機的稀土磁體成本從8萬美元飆升至50萬美元。18個月後中國放鬆了出口限制,價格又回到了低於2010年的水準。

中國目前仍是本土和出口市場中,用來生產電子產品的稀土金屬的主要消費國,緊接著是日本和美國。

幾乎60%的稀土金屬都用於生產磁鐵。具有永磁性的稀土磁鐵幾乎日常生活無所不在,不論是電子產品、飛機、油電混合車或風機。開發出新技術的普渡大學化學教授王念華(音譯)說:「目前稀土只有一個海外供給,如果因故供給受限,將嚴重影響人們的生活。美國也有這種資源,但需要一種更好、更乾淨的方式來處理這些稀土金屬。」

新的專利提取和純化技術使用配體輔助層析法,經證實可以安全有效地從煤灰、回收磁鐵和原礦石中分離出稀土金屬,對環境幾乎沒有不利影響。

鎦是一種銀白色金屬,稀土金屬之一。照片來源:
維基百科/Alchemist-hp(CC BY-NC-ND 3.0)

稀土金屬的生產每年全球有40億美元的市場。隨著新的電子產品、飛機、軍艦、電動汽車、磁鐵和其他需要稀土金屬的重要產品的開發,這個市場繼續成長中。每年靠稀土金屬作用的產品價值超過4兆美元。

王念華解釋:「傳統生產高純度稀土元素方法採用兩階段液相萃取法,這需要用到數千個串聯或併聯的混合沈降槽,產生大量有毒廢物。我們的兩區域配體輔助置換層析系統使用一種新的分區方法,可生產高純度(> 99%)金屬並達到高產率(> 99%)。」

王念華的配體輔助方法有機會從廢磁鐵和礦石回收高效、環保地純化稀土金屬,並有助使稀土加工成為循環永續的過程。

普渡大學化學工程教授喬.佩克尼(Joe Pekny)說,王念華的創新技術使美國能以環境友善、安全和永續的方式重新進入稀土金屬市場。佩克尼說:「美國的稀土金屬可以滿足美國和全球其他市場不斷成長的需求,減少對外國資源的依賴。」

這項研究部分由美國國防部(Department of Defense﹐DoD)資助。

現在美國國防部正在與美國稀土礦供應鏈簽訂新合約,加州沙漠中一度停產的鈾礦礦場,也是北美唯一的稀土礦開採和加工基地可望重新啟用。

Greener Process Grows U.S. Supply of Rare Earth Metals WEST LAFAYETTE, Indiana, May 11, 2020(ENS)

Mining for rare earth metals is about to become more environmentally and economically feasible though a process newly developed and patented at Purdue University.

These new environmentally-friendly technologies promise to be game-changers in this field and could enable the United States to create a more stable and reliable domestic source of these essential metals.

Used in computers, cell phones, DVDs, rechargeable batteries, catalytic converters, magnets, wind turbines, and fluorescent lights, and for defense in laser range-finders, guidance systems, and precision-guided weapons, these metals are difficult to mine because it is unusual to find them in concentrations high enough for economical extraction.

In addition, the detrimental environmental impact of current acid-based separation and purification of rare earth metals prohibits most companies everywhere in the world from entering the market.

China is currently the world leader in rare earth production, although it controls just 36 percent of the world’s reserves of these 17 metals. This provides an opportunity for other countries to become producers now that China is not selling rare earth materials below the cost of production as it did in the 1980s and ’90s.

But when China reduced the export quotas for rare earth metals in 2010, the costs of rare earth magnets for one wind turbine soared from $80,000 to $500,000. After China relaxed the export restrictions 18 months later, prices returned to lower levels than in 2010.

China is also the dominant consumer of rare earth metals for manufacturing electronics products for domestic and export markets. Today Japan and the United States are the second and third largest consumers of rare earth materials.

“About 60 percent of rare earth metals are used in magnets that are needed in almost everyone’s daily lives. These metals are used in electronics, airplanes, hybrid cars and even windmills,” said Nien-Hwa Linda Wang, the Purdue professor of chemistry who developed the new processes.

“We currently have one dominant foreign source for these metals and if the supply were to be limited for any reason, it would be devastating to people’s lives,” Wang said. “It’s not that the resource isn’t available in the U.S., but that we need a better, cleaner way to process these rare earth metals.”

The new patented extraction and purifying processes use ligand-assisted chromatography, a separation method that has been shown to remove and purify rare earth metals from coal ash, recycled magnets, and raw ore safely, efficiently and with virtually no detrimental environmental impact.

The production of rare earth metals is a global US$4 billion annual market that continues to grow as new electronics, computerized engines for aircraft, warships, electric automobiles, magnets, and other critical products are developed that require rare earth metals to perform. The value of the products using rare earth metals to function is valued at more than $4 trillion per year.

“Conventional methods for producing high-purity rare earth elements employ two-phase liquid–liquid extraction methods, which require thousands of mixer-settler units in series or in parallel and generate large amounts of toxic waste,” Wang said.

“We use a two-zone ligand-assisted displacement chromatography system with a new zone-splitting method that is producing high-purity (>99%) metals with high yields (>99%).”

Wang’s ligand assisted method has the potential for efficient and environmentally friendly purification of the rare earth metals from all sources of recyclates, such as waste magnets and ore-based sources and helps transform rare earth processing to a circular, sustainable process.

Joe Pekny, a Purdue professor of chemical engineering, said Wang’s innovation enables the United States to reenter the rare earth metals market in an earth-friendly, safe and sustainable way. “What’s exciting is that the U.S. has the rare earth metals to meet the growing demands of the U.S. market and other markets around the globe and reduces our dependence on foreign sources,” Pekny said.

This research was funded in part by the U.S. Department of Defense, DoD.

Now, the Defense Department is supporting the U.S. rare earth supply chain with a new contract to a once defunct uranium mine in the California desert that is now the only rare earth mining and processing site in North America.

※ 全文及圖片詳見:ENS

稀土金屬
開採
專利
友善環境
礦業
國際新聞
美國
生活環境
環境經濟
循環經濟

作者

姜唯

如果有一件事是重要的,如果能為孩子實現一個願望,那就是人類與大自然和諧共存。

林大利

於特有生物研究保育中心服務,小鳥和棲地是主要的研究對象。是龜毛的讀者,認為龜毛是探索世界的美德。

延伸閱讀

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

【其他文章推薦】

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

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

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

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

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

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

【Spring註解驅動開發】使用@Scope註解設置組件的作用域

寫在前面

Spring容器中的組件默認是單例的,在Spring啟動時就會實例化並初始化這些對象,將其放到Spring容器中,之後,每次獲取對象時,直接從Spring容器中獲取,而不再創建對象。如果每次從Spring容器中獲取對象時,都要創建一個新的實例對象,該如何處理呢?此時就需要使用@Scope註解設置組件的作用域。

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

本文內容概覽

  • @Scope註解概述
  • 單實例bean作用域
  • 多實例bean作用域
  • 單實例bean作用域如何創建對象?
  • 多實例bean作用域如何創建對象?
  • 單實例bean注意的事項
  • 多實例bean注意的事項
  • 自定義Scope的實現

@Scope註解概述

@Scope註解能夠設置組件的作用域,我們先來看@Scope註解類的源碼,如下所示。

package org.springframework.context.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.core.annotation.AliasFor;

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Scope {
	@AliasFor("scopeName")
	String value() default "";
    /**
	 * Specifies the name of the scope to use for the annotated component/bean.
	 * <p>Defaults to an empty string ({@code ""}) which implies
	 * {@link ConfigurableBeanFactory#SCOPE_SINGLETON SCOPE_SINGLETON}.
	 * @since 4.2
	 * @see ConfigurableBeanFactory#SCOPE_PROTOTYPE
	 * @see ConfigurableBeanFactory#SCOPE_SINGLETON
	 * @see org.springframework.web.context.WebApplicationContext#SCOPE_REQUEST
	 * @see org.springframework.web.context.WebApplicationContext#SCOPE_SESSION
	 * @see #value
	 */
	@AliasFor("value")
	String scopeName() default "";
    
	ScopedProxyMode proxyMode() default ScopedProxyMode.DEFAULT;
}

從源碼中可以看出,在@Scope註解中可以設置如下值。

ConfigurableBeanFactory#SCOPE_PROTOTYPE
ConfigurableBeanFactory#SCOPE_SINGLETON
org.springframework.web.context.WebApplicationContext#SCOPE_REQUEST
org.springframework.web.context.WebApplicationContext#SCOPE_SESSION

很明顯,在@Scope註解中可以設置的值包括ConfigurableBeanFactory接口中的SCOPE_PROTOTYPE和SCOPE_SINGLETON,以及WebApplicationContext類中SCOPE_REQUEST和SCOPE_SESSION。這些都是什麼鬼?別急,我們來一個個查看。

首先,我們進入到ConfigurableBeanFactory接口中,發現在ConfigurableBeanFactory類中存在兩個常量的定義,如下所示。

public interface ConfigurableBeanFactory extends HierarchicalBeanFactory, SingletonBeanRegistry {
	String SCOPE_SINGLETON = "singleton";
	String SCOPE_PROTOTYPE = "prototype";
    /*****************此處省略N多行代碼*******************/
}

沒錯,SCOPE_SINGLETON就是singleton,SCOPE_PROTOTYPE就是prototype。

那麼,WebApplicationContext類中SCOPE_REQUEST和SCOPE_SESSION又是什麼鬼呢?就是說,當我們使用了Web容器來運行Spring應用時,在@Scope註解中可以設置WebApplicationContext類中SCOPE_REQUEST和SCOPE_SESSION的值,而SCOPE_REQUEST的值就是request,SCOPE_SESSION的值就是session。

綜上,在@Scope註解中的取值如下所示。

  • singleton:表示組件在Spring容器中是單實例的,這個是Spring的默認值,Spring在啟動的時候會將組件進行實例化並加載到Spring容器中,之後,每次從Spring容器中獲取組件時,直接將實例對象返回,而不必再次創建實例對象。從Spring容器中獲取對象,小夥伴們可以理解為從Map對象中獲取對象。
  • prototype:表示組件在Spring容器中是多實例的,Spring在啟動的時候並不會對組件進行實例化操作,而是每次從Spring容器中獲取組件對象時,都會創建一個新的實例對象並返回。
  • request:每次請求都會創建一個新的實例對象,request作用域用在spring容器的web環境中。
  • session:在同一個session範圍內,創建一個新的實例對象,也是用在web環境中。
  • application:全局web應用級別的作用於,也是在web環境中使用的,一個web應用程序對應一個bean實例,通常情況下和singleton效果類似的,不過也有不一樣的地方,singleton是每個spring容器中只有一個bean實例,一般我們的程序只有一個spring容器,但是,一個應用程序中可以創建多個spring容器,不同的容器中可以存在同名的bean,但是sope=aplication的時候,不管應用中有多少個spring容器,這個應用中同名的bean只有一個。

其中,request和session作用域是需要Web環境支持的,這兩個值基本上使用不到,如果我們使用Web容器來運行Spring應用時,如果需要將組件的實例對象的作用域設置為request和session,我們通常會使用request.setAttribute(“key”,object)和session.setAttribute(“key”, object)的形式來將對象實例設置到request和session中,通常不會使用@Scope註解來進行設置。

單實例bean作用域

首先,我們在io.mykit.spring.plugins.register.config包下創建PersonConfig2配置類,在PersonConfig2配置類中實例化一個Person對象,並將其放置在Spring容器中,如下所示。

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 測試@Scope註解設置的作用域
 */
@Configuration
public class PersonConfig2 {

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

接下來,在SpringBeanTest類中創建testAnnotationConfig2()測試方法,在testAnnotationConfig2()方法中,創建ApplicationContext對象,創建完畢后,從Spring容器中按照id獲取兩個Person對象,並打印兩個對象是否是同一個對象,代碼如下所示。

@Test
public void testAnnotationConfig2(){
    ApplicationContext context = new AnnotationConfigApplicationContext(PersonConfig2.class);
    //從Spring容器中獲取到的對象默認是單實例的
    Object person1 = context.getBean("person");
    Object person2 = context.getBean("person");
    System.out.println(person1 == person2);
}

由於對象在Spring容器中默認是單實例的,所以,Spring容器在啟動時就會將實例對象加載到Spring容器中,之後,每次從Spring容器中獲取實例對象,直接將對象返回,而不必在創建新對象實例,所以,此時testAnnotationConfig2()方法會輸出true。如下所示。

這也驗證了我們的結論:對象在Spring容器中默認是單實例的,Spring容器在啟動時就會將實例對象加載到Spring容器中,之後,每次從Spring容器中獲取實例對象,直接將對象返回,而不必在創建新對象實例。

多實例bean作用域

修改Spring容器中組件的作用域,我們需要藉助於@Scope註解,此時,我們將PersonConfig2類中Person對象的作用域修改成prototype,如下所示。

@Configuration
public class PersonConfig2 {

    @Scope("prototype")
    @Bean("person")
    public Person person(){
        return new Person("binghe002", 18);
    }
}

其實,使用@Scope設置作用域就等同於在XML文件中為bean設置scope作用域,如下所示。

此時,我們再次運行SpringBeanTest類的testAnnotationConfig2()方法,此時,從Spring容器中獲取到的person1對象和person2對象還是同一個對象嗎?

通過輸出結果可以看出,此時,輸出的person1對象和person2對象已經不是同一個對象了。

單實例bean作用域何時創建對象?

接下來,我們驗證下在單實例作用域下,Spring是在什麼時候創建對象的呢?

首先,我們將PersonConfig2類中的Person對象的作用域修改成單實例,並在返回Person對象之前打印相關的信息,如下所示。

@Configuration
public class PersonConfig2 {
    @Scope
    @Bean("person")
    public Person person(){
        System.out.println("給容器中添加Person....");
        return new Person("binghe002", 18);
    }
}

接下來,我們在SpringBeanTest類中創建testAnnotationConfig3()方法,在testAnnotationConfig3()方法中,我們只創建Spring容器,如下所示。

@Test
public void testAnnotationConfig3(){
    ApplicationContext context = new AnnotationConfigApplicationContext(PersonConfig2.class);
}

此時,我們運行SpringBeanTest類中的testAnnotationConfig3()方法,輸出的結果信息如下所示。

從輸出的結果信息可以看出,Spring容器在創建的時候,就將@Scope註解標註為singleton的組件進行了實例化,並加載到Spring容器中。

接下來,我們運行SpringBeanTest類中的testAnnotationConfig2(),結果信息如下所示。

說明,Spring容器在啟動時,將單實例組件實例化之後,加載到Spring容器中,以後每次從容器中獲取組件實例對象,直接返回相應的對象,而不必在創建新對象。

多實例bean作用域何時創建對象?

如果我們將對象的作用域修改成多實例,那什麼時候創建對象呢?

此時,我們將PersonConfig2類的Person對象的作用域修改成多實例,如下所示。

@Configuration
public class PersonConfig2 {

    @Scope("prototype")
    @Bean("person")
    public Person person(){
        System.out.println("給容器中添加Person....");
        return new Person("binghe002", 18);
    }
}

我們再次運行SpringBeanTest類中的testAnnotationConfig3()方法,輸出的結果信息如下所示。

可以看到,終端並沒有輸出任何信息,說明在創建Spring容器時,並不會實例化和加載多實例對象,那多實例對象是什麼時候實例化的呢?接下來,我們在SpringBeanTest類中的testAnnotationConfig3()方法中添加一行獲取Person對象的代碼,如下所示。

@Test
public void testAnnotationConfig3(){
    ApplicationContext context = new AnnotationConfigApplicationContext(PersonConfig2.class);
    Object person1 = context.getBean("person");
}

此時,我們再次運行SpringBeanTest類中的testAnnotationConfig3()方法,結果信息如下所示。

從結果信息中,可以看出,當向Spring容器中獲取Person實例對象時,Spring容器實例化了Person對象,並將其加載到Spring容器中。

那麼,問題來了,此時Spring容器是否只實例化一個Person對象呢?我們在SpringBeanTest類中的testAnnotationConfig3()方法中再添加一行獲取Person對象的代碼,如下所示。

@Test
public void testAnnotationConfig3(){
    ApplicationContext context = new AnnotationConfigApplicationContext(PersonConfig2.class);
    Object person1 = context.getBean("person");
    Object person2 = context.getBean("person");
}

此時,我們再次運行SpringBeanTest類中的testAnnotationConfig3()方法,結果信息如下所示。

從輸出結果可以看出,當對象的Scope作用域為多實例時,每次向Spring容器獲取對象時,都會創建一個新的對象並返回。此時,獲取到的person1和person2就不是同一個對象了,我們也可以打印結果信息來進行驗證,此時在SpringBeanTest類中的testAnnotationConfig3()方法中打印兩個對象是否相等,如下所示。

@Test
public void testAnnotationConfig3(){
    ApplicationContext context = new AnnotationConfigApplicationContext(PersonConfig2.class);
    Object person1 = context.getBean("person");
    Object person2 = context.getBean("person");
    System.out.println(person1 == person2);
}

此時,我們再次運行SpringBeanTest類中的testAnnotationConfig3()方法,結果信息如下所示。

可以看到,當對象是多實例時,每次從Spring容器中獲取對象時,都會創建新的實例對象,並且每個實例對象都不相等。

單實例bean注意的事項

單例bean是整個應用共享的,所以需要考慮到線程安全問題,之前在玩springmvc的時候,springmvc中controller默認是單例的,有些開發者在controller中創建了一些變量,那麼這些變量實際上就變成共享的了,controller可能會被很多線程同時訪問,這些線程併發去修改controller中的共享變量,可能會出現數據錯亂的問題;所以使用的時候需要特別注意。

多實例bean注意的事項

多例bean每次獲取的時候都會重新創建,如果這個bean比較複雜,創建時間比較長,會影響系統的性能,這個地方需要注意。

自定義Scope

如果Spring內置的幾種sope都無法滿足我們的需求的時候,我們可以自定義bean的作用域。

1.如何實現自定義Scope

自定義Scope主要分為三個步驟,如下所示。

(1)實現Scope接口

我們先來看下Scope接口的定義,如下所示。

package org.springframework.beans.factory.config;

import org.springframework.beans.factory.ObjectFactory;
import org.springframework.lang.Nullable;

public interface Scope {

    /**
    * 返回當前作用域中name對應的bean對象
    * name:需要檢索的bean的名稱
    * objectFactory:如果name對應的bean在當前作用域中沒有找到,那麼可以調用這個ObjectFactory來創建這個對象
    **/
    Object get(String name, ObjectFactory<?> objectFactory);

    /**
     * 將name對應的bean從當前作用域中移除
     **/
    @Nullable
    Object remove(String name);

    /**
     * 用於註冊銷毀回調,如果想要銷毀相應的對象,則由Spring容器註冊相應的銷毀回調,而由自定義作用域選擇是不是要銷毀相應的對象
     */
    void registerDestructionCallback(String name, Runnable callback);

    /**
     * 用於解析相應的上下文數據,比如request作用域將返回request中的屬性。
     */
    @Nullable
    Object resolveContextualObject(String key);

    /**
     * 作用域的會話標識,比如session作用域將是sessionId
     */
    @Nullable
    String getConversationId();

}

(2)將Scope註冊到容器

需要調用org.springframework.beans.factory.config.ConfigurableBeanFactory#registerScope的方法,看一下這個方法的聲明

/**
* 向容器中註冊自定義的Scope
*scopeName:作用域名稱
* scope:作用域對象
**/
void registerScope(String scopeName, Scope scope);

(3)使用自定義的作用域

定義bean的時候,指定bean的scope屬性為自定義的作用域名稱。

2.自定義Scope實現案例

例如,我們來實現一個線程級別的bean作用域,同一個線程中同名的bean是同一個實例,不同的線程中的bean是不同的實例。

這裏,要求bean在線程中是共享的,所以我們可以通過ThreadLocal來實現,ThreadLocal可以實現線程中數據的共享。

此時,我們在io.mykit.spring.plugins.register.scope包下新建ThreadScope類,如下所示。

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

import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.config.Scope;
import org.springframework.lang.Nullable;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

/**
 * 自定義本地線程級別的bean作用域,不同的線程中對應的bean實例是不同的,同一個線程中同名的bean是同一個實例
 */
public class ThreadScope implements Scope {

    public static final String THREAD_SCOPE = "thread";

    private ThreadLocal<Map<String, Object>> beanMap = new ThreadLocal() {
        @Override
        protected Object initialValue() {
            return new HashMap<>();
        }
    };

    @Override
    public Object get(String name, ObjectFactory<?> objectFactory) {
        Object bean = beanMap.get().get(name);
        if (Objects.isNull(bean)) {
            bean = objectFactory.getObject();
            beanMap.get().put(name, bean);
        }
        return bean;
    }

    @Nullable
    @Override
    public Object remove(String name) {
        return this.beanMap.get().remove(name);
    }

    @Override
    public void registerDestructionCallback(String name, Runnable callback) {
        //bean作用域範圍結束的時候調用的方法,用於bean清理
        System.out.println(name);
    }

    @Nullable
    @Override
    public Object resolveContextualObject(String key) {
        return null;
    }

    @Nullable
    @Override
    public String getConversationId() {
        return Thread.currentThread().getName();
    }
}

在ThreadScope類中,我們定義了一個常量THREAD_SCOPE,在定義bean的時候給scope使用。

接下來,我們在io.mykit.spring.plugins.register.config包下創建PersonConfig3類,並使用@Scope(“thread”)註解標註Person對象的作用域為Thread範圍,如下所示。

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;
import org.springframework.context.annotation.Scope;

/**
 * @author binghe
 * @version 1.0.0
 * @description 測試@Scope註解設置的作用域
 */
@Configuration
public class PersonConfig3 {

    @Scope("thread")
    @Bean("person")
    public Person person(){
        System.out.println("給容器中添加Person....");
        return new Person("binghe002", 18);
    }
}

最後,我們在SpringBeanTest類中創建testAnnotationConfig4()方法,在testAnnotationConfig4()方法中創建Spring容器,並向Spring容器中註冊ThreadScope對象,接下來,使用循環創建兩個Thread線程,並分別在每個線程中獲取兩個Person對象,如下所示。

@Test
public void testAnnotationConfig4(){
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(PersonConfig3.class);
    //向容器中註冊自定義的scope
    context.getBeanFactory().registerScope(ThreadScope.THREAD_SCOPE, new ThreadScope());

    //使用容器獲取bean
    for (int i = 0; i < 2; i++) { 
        new Thread(() -> {
            System.out.println(Thread.currentThread() + "," + context.getBean("person"));
            System.out.println(Thread.currentThread() + "," + context.getBean("person"));
        }).start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

此時,我們運行SpringBeanTest類的testAnnotationConfig4()方法,輸出的結果信息如下所示。

從輸出中可以看到,bean在同樣的線程中獲取到的是同一個bean的實例,不同的線程中bean的實例是不同的。

注意:這裏,我將Person類進行了相應的調整,去掉Lombok的註解,手動寫構造函數和setter與getter方法,如下所示。

package io.mykit.spring.bean;

import java.io.Serializable;

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

    public Person() {
    }

    public Person(String name, Integer age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }
}

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

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

寫在最後

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

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

【其他文章推薦】

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

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

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

※超省錢租車方案

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

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

※回頭車貨運收費標準

一文講透Java序列化

 

本文目錄

  • 一、序列化是什麼
  • 二、為什麼需要序列化
  • 三、序列化怎麼用
  • 四、序列化深度探秘
    • 4.1 為什麼必須實現Serializable接口
    • 4.2 被序列化對象的字段是引用時該怎麼辦 
    • 4.3 同一個對象會被序列化多次嗎
    • 4.4 只想序列化對象的部分字段該怎麼辦
    • 4.5 被序列化對象具有繼承關係該怎麼辦
  • 五、serialVersionUID的作用及自動生成
  • 六、序列化的缺點
  • 七、參考文獻

 

前言

 

Oracle 公司計劃廢除 Java 中的古董:序列化技術,因為它帶來了許多嚴重的安全問題(如序列化存儲安全、反序列化安全、傳輸安全等),據統計,至少有3分之1的漏洞是序列化帶來的,這也是 1997 年誕生序列化技術的一個巨大錯誤。但是,序列化技術現在在 Java 應用中無處不在,特別是現在的持久化框架和分佈式技術中,都需要利用序列化來傳輸對象,如:Hibernate、Mybatis、Java RMI、Dubbo等,即對象要存儲或者傳輸都不可避免要用到序列化技術,所以刪除序列化技術將是一個長期的計劃。

 

你在實際工作中可能會很難有機會真正用到Java自帶的序列化技術了,工業界一般也會選擇一些更安全的對象編解碼方案例如Google的Protobuf等。所以,對於Java序列化,我們不必再投入過多的精力學習,你花20分鐘讀完本文所掌握的知識,對於應付日常源碼閱讀中遇到的遺留的Java序列化技術應該是足夠了。

 

一、序列化是什麼

 

序列化機制允許將實現序列化的Java對象轉換成字節序列,這些字節序列可以保存在磁盤上,或通過網絡傳輸,以備以後重新恢復成原來的對象。序列化機制使得對象可以脫離程序的運行而獨立存在。

  • 序列化:將一個Java對象寫入IO流中
  • 反序列化:從IO流中恢復該Java對象

 

本文中用序列化來簡稱整個序列化和反序列化機制。 

 

二、為什麼需要序列化

 

所有可能在網絡上傳輸的對象的類都應該是可序列化的,否則程序將會出現異常,比如RMI(Remote Method Invoke,即遠程方法調用,是JavaEE的基礎)過程中的參數和返回值;所有需要保存到磁盤裡的對象的類都必須可序列化,比如Web應用中需要保存到HttpSession或ServletContext屬性的Java對象。

 

因為序列化是RMI過程的參數和返回值都必須實現的機制,而RMI又是Java EE技術的基礎——所有的分佈式應用常常需要跨平台、跨網絡,所以要求所有傳遞的參數、返回值必須實現序列化。因此序列化機制是Java EE平台的基礎。通常建議:程序創建的每個JavaBean類都實現Serializable。

 

三、序列化怎麼用

 

如果一個類的對象需要序列化,那麼在Java語法層面,這個類需要:

  • 實現Serializable接口
  • 使用ObjectOutputStream將對象輸出到流,實現對象的序列化;使用ObjectInputStream從流中讀取對象,實現對象的反序列化

 

下面我們通過代碼示例來看看序列化最基本的用法。我們創建了Person類,其擁有兩個基本類型的屬性,並實現了Serializable接口。testSerialize方法用來測試序列化,testDeserialize方法用來測試反序列化。

 1 import org.junit.Test;
 2 
 3 import java.io.*;
 4 
 5 public class SerializableTest {
 6 
 7     @Test
 8     public void testSerialize() {
 9         Person one = new Person(12, 148.2);
10         Person two = new Person(35, 177.8);
11 
12         try (ObjectOutputStream output =
13                      new ObjectOutputStream(new FileOutputStream("Person.txt"))) {
14             output.writeObject(one);
15             output.writeObject(two);
16         } catch (IOException e) {
17             e.printStackTrace();
18         }
19     }
20 
21     @Test
22     public void testDeserialize() {
23 
24         try (ObjectInputStream input =
25                      new ObjectInputStream(new FileInputStream("Person.txt"))) {
26             Person one = (Person) input.readObject();
27             Person two = (Person) input.readObject();
28 
29             System.out.println(one);
30             System.out.println(two);
31         } catch (IOException e) {
32             e.printStackTrace();
33         } catch (ClassNotFoundException e) {
34             e.printStackTrace();
35         }
36     }
37 }
38 
39 class Person implements Serializable {
40     int age;
41     double height;
42 
43     public Person(int age, double height) {
44         this.age = age;
45         this.height = height;
46     }
47 
48     @Override
49     public String toString() {
50         return "Person{" +
51                 "age=" + age +
52                 ", height=" + height +
53                 '}';
54     }
55 }

 

四、序列化深度探秘

4.1 為什麼必須實現Serializable接口

如果某個類需要支持序列化功能,那麼它必須實現Serializable接口,否則會報 java.io.NotSerializableException。Serializable接口是一個標誌性接口(Marker Interface),也就是說,該接口並不包含任何具體的方法,是一個空接口,僅僅用來判斷該類是否能夠序列化。JDK8中Serializable接口的源碼如下:

1 package java.io;
2 
3 public interface Serializable {
4 }

 

在 ObjectOutputStream.java 的 writeObject0 方法中,我們確實可以看到對對象是否實現了 Serializable接口進行了驗證(第15行),否則會拋出 NotSerializableException 異常(第22行)。

 1     private void writeObject0(Object obj, boolean unshared)
 2         throws IOException
 3     {
 4         boolean oldMode = bout.setBlockDataMode(false);
 5         depth++;
 6         try {
 7             ...
 8             // remaining cases
 9             if (obj instanceof String) {
10                 writeString((String) obj, unshared);
11             } else if (cl.isArray()) {
12                 writeArray(obj, desc, unshared);
13             } else if (obj instanceof Enum) {
14                 writeEnum((Enum<?>) obj, desc, unshared);
15             } else if (obj instanceof Serializable) {
16                 writeOrdinaryObject(obj, desc, unshared);
17             } else {
18                 if (extendedDebugInfo) {
19                     throw new NotSerializableException(
20                         cl.getName() + "\n" + debugInfoStack.toString());
21                 } else {
22                     throw new NotSerializableException(cl.getName());
23                 }
24             }
25         } finally {
26             depth--;
27             bout.setBlockDataMode(oldMode);
28         }
29     }

 

4.2 被序列化對象的字段是引用時該怎麼辦

在第三部分“序列化怎麼用”部分的示例中,Person類的字段全都是基本類型,我們知道基本類型其地址中直接存放的就是它的值,那如果是引用類型呢?引用類型其地址中存放的是指向堆內存中的一個地址,難道序列化時就是將這個地址進行了保存嗎?顯然,這是說不通的,因為對象的內存地址是可變的,在同一系統的不同運行時刻或者是不同系統中,對象的地址肯定是不同的,因此,序列化內存地址沒有意義。

 

如果被序列化對象的字段是引用,那麼要求該引用的類型也是可序列化實現了Serializable接口的,否則無法序列化。當對某個對象進行序列化時,系統會自動把該對象的所有Field依次進行序列化,如果某個Field引用到另一個對象,則被引用的對象也會被序列化;如果被引用的對象的Field也引用了其他對象,則被引用的對象也會被序列化,這種情況被稱為遞歸序列化。

 

4.3 同一個對象會被序列化多次嗎

如果對象A和對象B同時引用了對象C,那麼,當序列化對象A和對象B時,對象C會被序列化兩次嗎?答案顯然是不會

 

要解釋這個問題,就不得不說一下Java序列化的基本算法了:

  • 所有序列化到二進制流的對象都有一個序列化編號
  • 當程序試圖序列化一個對象時,程序將先檢查該對象是否已經被序列化過,只有該對象從未(在本次虛擬機中)被序列化過,系統才會將該對象轉換成字節序列並賦予一個唯一的編號
  • 如果某個對象已經序列化過,程序將只是直接輸出其序列化編號,而不是再次重新序列化該對象

 

4.4 只想序列化對象的部分字段該怎麼辦

 在一些特殊的場景下,如果一個類里包含的某些Field值是敏感信息,例如銀行賬戶信息等,這時不希望系統將該Field值進行序列化;或者某個Field的類型是不可序列化的,因此不希望對該Field進行遞歸序列化,以避免引發java.io.NotSerializableException異常。

 

此時,我們就需要自定義序列化了。自定義序列化的常用方式有兩種:

  • 使用transient關鍵字
  • 重寫writeObject與readObject方法

 

我們先看第一種方式,使用transient關鍵字。transient關鍵字只能用於修飾Field,不可修飾Java程序中的其他成分。使用transient修飾的屬性,java序列化時,會忽略掉此字段,所以反序列化出的對象,被transient修飾的屬性是默認值。對於引用類型,值是null;基本類型,值是0;boolean類型,值是false。

 

下列代碼中,我們把People的height字段設置為transient,在反序列化時,可觀察到輸出為默認值0.0。

 1 import org.junit.Test;
 2 
 3 import java.io.*;
 4 
 5 public class SerializableTest {
 6 
 7     @Test
 8     public void testSerialize() {
 9         Person one = new Person(12, 156.6);
10         Person two = new Person(16, 177.7);
11 
12         try (ObjectOutputStream output =
13                      new ObjectOutputStream(new FileOutputStream("Person.txt"))) {
14             output.writeObject(one);
15             output.writeObject(two);
16         } catch (IOException e) {
17             e.printStackTrace();
18         }
19     }
20 
21     @Test
22     public void testDeserialize() {
23 
24         try (ObjectInputStream input =
25                      new ObjectInputStream(new FileInputStream("Person.txt"))) {
26             Person one = (Person) input.readObject();
27             Person two = (Person) input.readObject();
28 
29             System.out.println(one);
30             System.out.println(two);
31         } catch (IOException e) {
32             e.printStackTrace();
33         } catch (ClassNotFoundException e) {
34             e.printStackTrace();
35         }
36     }
37 }
38 
39 class Person implements Serializable{
40     protected int age;
41     protected transient double height;
42 
43     public Person() {
44     }
45 
46     public Person(int age, double height) {
47         this.age = age;
48         this.height = height;
49     }
50 
51     @Override
52     public String toString() {
53         return "Person{" +
54                 "age=" + age +
55                 ", height=" + height +
56                 '}';
57     }
58 }

 

 程序輸出:

Person{age=12, height=0.0}
Person{age=16, height=0.0}

Process finished with exit code 0

 

使用transient關鍵字修飾Field雖然簡單、方便,但被transient修飾的Field將被完全隔離在序列化機制之外,這樣導致在反序列化恢復Java對象時無法取得該Field值。Java還提供了一種自定義序列化機制,通過這種自定義序列化機制可以讓程序控制如何序列化各Field,甚至完全不序列化某些Field(與使用transient關鍵字的效果相同)。在序列化和反序列化過程中需要特殊處理的類應該提供如下特殊簽名的方法,這些特殊的方法用以實現自定義序列化。

 private void writeObject(java.io.ObjectOutputStream out)
     throws IOException
 private void readObject(java.io.ObjectInputStream in)
     throws IOException, ClassNotFoundException;
 private void readObjectNoData()
     throws ObjectStreamException;

 

  • writeObject()方法負責寫入特定類的實例狀態,以便相應的readObject()方法可以恢復它。通過重寫該方法,程序員可以完全獲得對序列化機制的控制,可以自主決定哪些Field需要序列化,需要怎樣序列化。在默認情況下,該方法會調用out.defaultWriteObject來保存Java對象的各Field,從而可以實現序列化Java對象狀態的目的。
  • readObject()方法負責從流中讀取並恢復對象Field,通過重寫該方法,程序員可以完全獲得對反序列化機制的控制,可以自主決定需要反序列化哪些Field,以及如何進行反序列化。在默認情況下,該方法會調用in.defaultReadObject來恢復Java對象的非靜態和非瞬態Field。在通常情況下,readObject()方法與writeObject()方法對應,如果writeObject()方法中對Java對象的Field進行了一些處理,則應該在readObject()方法中對其Field進行相應的反處理,以便正確恢復該對象。
  • 當序列化流不完整時,readObjectNoData()方法可以用來正確地初始化反序列化的對象。例如,接收方使用的反序列化類的版本不同於發送方,或者接收方版本擴展的類不是發送方版本擴展的類,或者序列化流被篡改時,系統都會調用readObjectNoData()方法來初始化反序列化的對象。

下面的示例代碼中,我們在writeObject方法中對Person的字段進行了簡單的加密處理,在readObject方法中對其進行了相應的解密。

 1 import org.junit.Test;
 2 
 3 import java.io.*;
 4 
 5 public class SerializableTest {
 6 
 7     @Test
 8     public void testSerialize() {
 9         Person one = new Person(12, 156.6);
10         Person two = new Person(16, 177.7);
11 
12         try (ObjectOutputStream output =
13                      new ObjectOutputStream(new FileOutputStream("Person.txt"))) {
14             output.writeObject(one);
15             output.writeObject(two);
16         } catch (IOException e) {
17             e.printStackTrace();
18         }
19     }
20 
21     @Test
22     public void testDeserialize() {
23 
24         try (ObjectInputStream input =
25                      new ObjectInputStream(new FileInputStream("Person.txt"))) {
26             Person one = (Person) input.readObject();
27             Person two = (Person) input.readObject();
28 
29             System.out.println(one);
30             System.out.println(two);
31         } catch (IOException e) {
32             e.printStackTrace();
33         } catch (ClassNotFoundException e) {
34             e.printStackTrace();
35         }
36     }
37 }
38 
39 class Person implements Serializable{
40     protected int age;
41     protected double height;
42 
43     public Person() {
44     }
45 
46     public Person(int age, double height) {
47         this.age = age;
48         this.height = height;
49     }
50 
51     private void writeObject(java.io.ObjectOutputStream out)
52             throws IOException {
53         System.out.println("Encryption!");
54         out.writeInt(age + 1);
55         out.writeDouble(height - 1);
56     }
57     private void readObject(java.io.ObjectInputStream in)
58             throws IOException, ClassNotFoundException {
59         System.out.println("Decryption!");
60         this.age = in.readInt() - 1;
61         this.height = in.readDouble() + 1;
62     }
63 
64     @Override
65     public String toString() {
66         return "Person{" +
67                 "age=" + age +
68                 ", height=" + height +
69                 '}';
70     }
71 }

 

4.5 被序列化對象具有繼承關係該怎麼辦

被序列化對象具有繼承關係時無非就兩種情況,第一,該類具有子類,第二,該類具有父類。

 

當該類實現了Serializable接口且具有子類時,根據官方文檔中的說明,其子類天然具有可被序列化的屬性,不需要顯式實現Serializable接口;。

 All subtypes of a serializable class are themselves serializable. 

 

當該類實現了Serializable接口且具有父類時,,該類的父類需要實現Serializable接口嗎?在JDK8中Serializable接口的官方文檔中有這樣一段話:

 1 /**
 2  * ......
 3  *
 4  * To allow subtypes of non-serializable classes to be serialized, the
 5  * subtype may assume responsibility for saving and restoring the
 6  * state of the supertype's public, protected, and (if accessible)
 7  * package fields.  The subtype may assume this responsibility only if
 8  * the class it extends has an accessible no-arg constructor to
 9  * initialize the class's state.  It is an error to declare a class
10  * Serializable if this is not the case.  The error will be detected at
11  * runtime. 
12  *
13  * During deserialization, the fields of non-serializable classes will
14  * be initialized using the public or protected no-arg constructor of
15  * the class.  A no-arg constructor must be accessible to the subclass
16  * that is serializable.  The fields of serializable subclasses will
17  * be restored from the stream. 
18  */

 

閱讀文檔我們得知,為了使得不可序列化類的子類能夠序列化,其子類必須擔負起保存和恢復其超類的public、protected 和 package(if accessible)實例域的責任,且要求其父類必須有一個可訪問的無參構造函數以使得在反序列化時能夠初始化實例域。

 

我們寫代碼驗證一下,如果父類中沒有可訪問的無參構造函數會發生什麼,注意Person類中沒有無參構造函數。

 1 import org.junit.Test;
 2 
 3 import java.io.*;
 4 
 5 public class SerializableTest {
 6 
 7     @Test
 8     public void testSerialize() {
 9         Student one = new Student(12, 156.6, "1234");
10         Student two = new Student(16, 177.7, "5678");
11 
12         try (ObjectOutputStream output =
13                      new ObjectOutputStream(new FileOutputStream("Student.txt"))) {
14             output.writeObject(one);
15             output.writeObject(two);
16         } catch (IOException e) {
17             e.printStackTrace();
18         }
19     }
20 
21     @Test
22     public void testDeserialize() {
23 
24         try (ObjectInputStream input =
25                      new ObjectInputStream(new FileInputStream("Student.txt"))) {
26             Student one = (Student) input.readObject();
27             Student two = (Student) input.readObject();
28 
29             System.out.println(one);
30             System.out.println(two);
31         } catch (IOException e) {
32             e.printStackTrace();
33         } catch (ClassNotFoundException e) {
34             e.printStackTrace();
35         }
36     }
37 }
38 
39 class Person{
40     protected int age;
41     protected double height;
42     
43     public Person(int age, double height) {
44         this.age = age;
45         this.height = height;
46     }
47 
48     @Override
49     public String toString() {
50         return "Person{" +
51                 "age=" + age +
52                 ", height=" + height +
53                 '}';
54     }
55 }
56 
57 class Student extends Person implements Serializable{
58     private String id;
59 
60     public Student(int age, double height, String id) {
61         super(age, height);
62         this.id = id;
63     }
64 
65     @Override
66     public String toString() {
67         return "Student{" +
68                 "age=" + age +
69                 ", height=" + height +
70                 ", id='" + id + '\'' +
71                 '}';
72     }
73 }

 

程序輸出產生異常:

java.io.InvalidClassException: Student; no valid constructor
    at java.io.ObjectStreamClass$ExceptionInfo.newInvalidClassException(ObjectStreamClass.java:150)
    at java.io.ObjectStreamClass.checkDeserialize(ObjectStreamClass.java:768)
    at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1775)
    at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1351)
    at java.io.ObjectInputStream.readObject(ObjectInputStream.java:371)
    at SerializableTest.testDeserialize(SerializableTest.java:26)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:497)
    ...

Process finished with exit code 0

 

當我們為Person類添加默認構造函數時:

 1 class Person{
 2     protected int age;
 3     protected double height;
 4 
 5     public Person() {
 6     }
 7 
 8     public Person(int age, double height) {
 9         this.age = age;
10         this.height = height;
11     }
12 
13     @Override
14     public String toString() {
15         return "Person{" +
16                 "age=" + age +
17                 ", height=" + height +
18                 '}';
19     }
20 }

 

程序輸出如下,我們可觀察到,父類中的字段都是默認值,只有子類中的字段得到了正確的序列化。出現這種情況的原因是子類並沒有擔負起序列化父類中字段的責任。

Student{age=0, height=0.0, id='1234'}
Student{age=0, height=0.0, id='5678'}

Process finished with exit code 0

 

為了解決上述問題,我們需要藉助上一節中學到的知識,使用自定義的序列化方法writeObject和readObject來主動將父類中的字段進行序列化。

 1 import org.junit.Test;
 2 
 3 import java.io.*;
 4 
 5 public class SerializableTest {
 6 
 7     @Test
 8     public void testSerialize() {
 9         Student one = new Student(12, 156.6, "1234");
10         Student two = new Student(16, 177.7, "5678");
11 
12         try (ObjectOutputStream output =
13                      new ObjectOutputStream(new FileOutputStream("Studnet.txt"))) {
14             output.writeObject(one);
15             output.writeObject(two);
16         } catch (IOException e) {
17             e.printStackTrace();
18         }
19     }
20 
21     @Test
22     public void testDeserialize() {
23 
24         try (ObjectInputStream input =
25                      new ObjectInputStream(new FileInputStream("Studnet.txt"))) {
26             Student one = (Student) input.readObject();
27             Student two = (Student) input.readObject();
28 
29             System.out.println(one);
30             System.out.println(two);
31         } catch (IOException e) {
32             e.printStackTrace();
33         } catch (ClassNotFoundException e) {
34             e.printStackTrace();
35         }
36     }
37 }
38 
39 class Person{
40     protected int age;
41     protected double height;
42 
43     public Person() {
44     }
45 
46     public Person(int age, double height) {
47         this.age = age;
48         this.height = height;
49     }
50 
51     @Override
52     public String toString() {
53         return "Person{" +
54                 "age=" + age +
55                 ", height=" + height +
56                 '}';
57     }
58 }
59 
60 class Student extends Person implements Serializable{
61     private String id;
62 
63     public Student(int age, double height, String id) {
64         super(age, height);
65         this.id = id;
66     }
67 
68     private void writeObject(java.io.ObjectOutputStream out)
69             throws IOException {
70         out.defaultWriteObject();
71         out.writeInt(age);
72         out.writeDouble(height);
73     }
74     
75     private void readObject(java.io.ObjectInputStream in)
76             throws IOException, ClassNotFoundException {
77         in.defaultReadObject();
78         this.age = in.readInt();
79         this.height = in.readDouble();
80     }
81 
82     @Override
83     public String toString() {
84         return "Student{" +
85                 "age=" + age +
86                 ", height=" + height +
87                 ", id='" + id + '\'' +
88                 '}';
89     }
90 }

 

程序輸出如下,可以看到完全正確。

Student{age=12, height=156.6, id='1234'}
Student{age=16, height=177.7, id='5678'}

Process finished with exit code 0

 

五、serialVersionUID的作用及自動生成

 

我們知道,反序列化必須擁有class文件,但隨着項目的升級,class文件也會升級,序列化怎麼保證升級前後的兼容性呢?

 

java序列化提供了一個private static final long serialVersionUID 的序列化版本號,只有版本號相同,即使更改了序列化屬性,對象也可以正確被反序列化回來。如果反序列化使用的class的版本號與序列化時使用的不一致,反序列化會報InvalidClassException異常。下面是JDK 8中ArrayList的源碼中的serialVersionUID。

 

 1 public class ArrayList<E> extends AbstractList<E>
 2         implements List<E>, RandomAccess, Cloneable, java.io.Serializable
 3 {
 4     private static final long serialVersionUID = 8683452581122892189L;
 5 
 6     /**
 7      * Default initial capacity.
 8      */
 9     private static final int DEFAULT_CAPACITY = 10;
10     ...  
11 }

 

序列化版本號可自由指定,如果不指定,JVM會根據類信息自己計算一個版本號,這樣隨着class的升級,就無法正確反序列化;不指定版本號另一個明顯隱患是,不利於jvm間的移植,可能class文件沒有更改,但不同jvm可能計算的規則不一樣,這樣也會導致無法反序列化。

 

什麼情況下需要修改serialVersionUID呢?分三種情況。

  • 如果只是修改了方法,反序列化不容影響,則無需修改版本號
  • 如果只是修改了靜態Field或瞬態Field,則反序列化不受任何影響
  •  如果修改類時修改了非靜態Field、非瞬態Field,則可能導致序列化版本不兼容。如果對象流中的對象和新類中包含同名的Field,而Field類型不同,則反序列化失敗,類定義應該更新serialVersionUID Field值。如果只是新增了實例變量,則反序列化回來新增的是默認值;如果減少了實例變量,反序列化時會忽略掉減少的實例變量。

 

我們在日常編程實踐中,一般會選擇使用IDE來自動生成serialVersionUID,這樣可以最大化地減少重複的可能性。對於IntelliJ IDEA,自動生成serialVersionUID有三步:

  • 修改IDEA配置:File->Setting->Editor->Inspections->Serialization issues->Serializable class without ’serialVersionUID’
  • 類實現Serializable接口
  • 在類名上執行Alt+Enter,然後選擇生成serialVersionUID即可

 

六、序列化的缺點

 

Java序列化存在四個致命缺點,導致其不適用於網絡傳輸:

  • 無法跨語言:在網絡傳輸中,經常會有異構語言的進程的交互,但Java序列化技術是Java語言內部的私有協議,其他語言無法進行反序列化。目前所有流行的RPC框架都沒有使用Java序列化作為編解碼框架。
  • 潛在風險高:不可信流的反序列化可能導致遠程代碼執行(RCE)、拒絕服務(DoS)和一系列其他攻擊。
  • 序列化后的碼流太大
  • 序列化的性能較低

 

在真正的生產環境中,一般會選擇其它編解碼框架,領先的跨平台結構化數據表示是 JSON 和 Protocol Buffers,也稱為 protobuf。JSON 由 Douglas Crockford 設計用於瀏覽器與服務器通信,Protocol Buffers 由谷歌設計用於在其服務器之間存儲和交換結構化數據。JSON 和 protobuf 之間最顯著的區別是 JSON 是基於文本的,並且是人類可讀的,而 protobuf 是二進制的,但效率更高。

 

 七、參考文獻

 

  1. 《瘋狂Java講義》第2版,李剛著,电子工業出版社
  2. 《Java核心技術》第10版,霍斯特曼等著,机械工業出版本
  3. 《Netty權威指南》第2版,李林鋒著,电子工業出版社
  4. 《Effective Java》第2版,Joshua Bloch著,机械工業出版社

 

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

【其他文章推薦】

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

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

※回頭車貨運收費標準

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

※超省錢租車方案

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

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

我們是如何做DevOps的?

一、DevOps的理解

DevOps的概念理解

DevOps 的概念在軟件開發行業中逐漸流行起來。越來越多的團隊希望實現產品的敏捷開發,DevOps 使一切成為可能。有了 DevOps ,團隊可以定期發布代碼、自動化部署、並將持續集成 / 持續交付作為發布過程的一部分。
一句話概括就是提高生產力,快速交付!

二、引入DevOps的背景

2.1 福祿技術棧介紹

  • 後端開發框架:基於C#的.netCore和Java的SpringCloud,少部分項目採用python和go開發

  • 前端開發框架:vue、react

  • 服務部署:前端站點基於ECS的nginx部署 ,後端服務統一部署在kubernetes上

  • 代碼倉庫:gitlab

  • 項目環境:目前有6套,開發、測試、壓測、集成、PRE和生產

2.2 後端服務的CICD現狀

                                                                                                 福祿後端CICD流程

CICD 流程說明

每一次的代碼push,根據創建的分支,根據在gitlab的CICD文件gitlab.yml定義構建步驟,觸發runner,從單元測試、通過dockerfile進行編譯和生成鏡像版本、將新鏡像部署到K8S生成pod,然後觸發接口自動化測試任務的執行

!!#00ffff 好像缺了點什麼 !!

  • 初次部署應用到kubernetes怎麼做的?

  • 服務的configmap在哪裡維護的?

  • 每個服務的gitlab.yml文件都不一樣,如何維護的?

  • 應用的域名解析怎麼做?

目前有6套環境進行管理,其中開發、測試、集成、壓測都是測試人員維護,預發布和生產運維人員維護;這也就要求每一個測試人員都必須對整個cicd流程和配置絕對掌握;所以當新人入職,需要掌握整個流程才能進入項目測試中,這是一個學習成本;

預發布和生產的kubernetes只有運維能夠操作,當有新的服務需要上線上述環境,或者configmap有變動,或者有時候排查問題需要查看容器日誌,我們只能通過運維的工單系統描述作業操作,中間文字描述可能存在理解差異,溝通成本和時間成本很大;

有的新應用我們去設置cicd的相關文件,比如dockerfile,我們發現應用的代碼目錄結構各種各樣,這樣往往就沒法套用一個模板快速配置完成

2.3 前端站點的CICD現狀

前端CICD流程說明

開發人員push代碼到gitlab,測試人員通過jenkins拉取最新的代碼到jenkins本地,然後通過jenkins與服務器之間的傳輸管道,將要部署的文件更新到目標服務器,並觸發UI自動化的job

完整的過程來看,也缺點內容

  • 一個新的站點部署,nginx需要做一些配置初始化工作,比如域名、路徑的配置
  • 前端的配置文件是如何管理的

跟後端應用一樣,前端的PRE和生產環境也是運維處理,所以當一個新的應用上線我們也需要發工單,描述具體操作,然後運維執行工單;配置文件一般不會變更,所以我們在jenkins推送更新文件到目標服務器的時候,將配置文件做了過濾處理。後續需要變更通過工單執行

2.3 痛點你看到了嗎

2.3.1 安全管控缺位
  • 代碼安全:CICD的起點在gitlab裏面,所以大家都有gitlab的賬號,代碼安全管控缺位
  • 線上安全:線上項目部署也是通過gitlab的cicd直接觸發,審批流程缺失
2.3.2 管理成本
  • 維護賬號多:gitlab賬號、jenkins賬號、kubernetes賬號(本地和阿里雲),每一個人員都需要上述賬號,運維管理麻煩,大家每個平台維護自己的賬號也麻煩
  • 工單溝通:工單編寫、溝通過程花費時間較多
  • 代碼規範:項目組多,微服務也多,代碼框架各自發揮,無論是流程維護還是問題排查都增加了難度

三、研發管理平台(RDMS)應運而生

3.1 如何理解這個平台

!!#ff0000 工具鏈到平台的轉變 !!

當前的cicd是對工具鏈進行了打通,但需要大家登錄各個工具平台操作,我們希望對工具集進行功能整合,打造一個系統平台,並且將CICD的技術細節進行屏蔽,開發人員能夠專心進行業務需求的開發,測試人員能夠專註到需求測試任務中,而運維人員能夠解放繁重的工單內容,投入到服務高可用的建設上!

3.2 業務功能設計

                                                                                                                                  福祿研發管理平台功能結構圖

3.2.1 功能說明
  • 項目管理:項目的創建和維護,默認提供了.netcore的api和控制台,java的api和前端站點的應用初始化代碼框架,開發人員開發新的應用直接根據應用類型選擇對應的模板就可以在git默認創建代碼倉庫和初始化框架代碼,並自動生成應用的http和https的域名
  • 構建記錄:獲取gitlab的pipeline,展示所有分支的構建記錄信息,可以一鍵跳轉到git倉庫
  • 部署管理:部署構建的鏡像到指定的環境,提供實時部署和定時部署功能
  • 容器管理:提供容器的查看功能,可以看到容器的存活狀態和容器實時日誌
  • 配置字段權限申請:針對PRE和生產環境查看配置,需要先走釘釘審批申請流程
  • 配置信息:進行配置的維護,包括新增、編輯、刪除,PRE和生產環境操作需要釘釘流程審批
  • 操作日誌:針對應用的操作日誌記錄
  • 用戶設置:在使用rdms前,需要先將用戶git倉庫的token設置在rdms上,這樣用戶在rdms操作與gitlab相關的業務才能正常使用
3.2.2 RDMS幾個核心頁面的展示

首頁-創建應用

構建記錄

部署管理

容器管理

3.3 技術架構

對接系統的說明

  • 通行證:RDMS的目標用戶是研發中心人員,這些人員在通行證中都有默認的賬戶信息,與通行證打通,可以直接登錄使用
  • GitlabAPI:目前RDMS的CI還是採用的gitlab的ci支撐,包括新應用在rdms的創建到git倉庫的代碼初始化等,都需要調用gitlab的api接口
  • 釘釘flow:安全管控的原因,PRE和生產的任何操作都會觸發釘釘審批流,所屬項目的項目經理審批通過後才會獲取到數據或者執行操作指令
  • 福祿開放平台:提供了網關相關的功能和菜單、角色等維護功能,公司所有後端服務都需要入駐開放平台
  • 蜂巢:公司的調度作業平台,rdms的定時部署功能依賴該服務的支撐
  • 運維工單系統:rdms的CD流程沒有直接與kubernets進行交互,而是通過運維的工單系統包裝了運維底層的shell腳本層,然後提供給rdms相關的api接口,也是基於安全控制的考慮
  • shell腳本層:shell腳本層會調用kubernetes的api進行kubernetes的相關操作(部署、配置更新、容器重啟、日誌查看等);調用阿里雲的dns解析接口,對應用的域名自動解析;調用oss的接口,進行前端站點文件目錄的維護

3.4 後端應用的devops實現詳解

舉個栗子進行介紹

根據模板,創建一個應用

根據名稱默認生成域名

初始化代碼倉庫,默認生成develop分支

在rdms第一次部署到對應環境(開發、測試、生產等)時,會默認讀取appsettings.Development.json的文件,並寫入kubernets的configmap

構建完成,進行部署

在kubernets生成pod

通過域名訪問接口文檔

3.5 前端站點的devops實現詳解

同樣的,舉個栗子介紹

首頁-創建前端站點

根據名稱生成域名

初始化代碼倉庫,默認生成develop分支

配置文件,默認生成幾套環境的配置文件,站點的配置維護就是維護這幾個文件

部署應用

kubernetes的nginx容器內可以看到部署的文件,實際就是掛載的oss到該pod上

四、展望

目前RDMS投產一個月左右,我們希望能將devops理念在這個系統上進行持續的優化和實踐,包括研發中心小夥伴也很踴躍參与共建,提出了很多好的方向和建議

  • 完善devops鏈條功能:通過字面來看有dev、ops兩部分,我們後期需要加入test的比重,比如在CI部分,引入靜態代碼掃描、單測覆蓋率;在CD部分集成我們的自動化測、性能測試
  • 工具平台:RDMS的初衷是整合,針對研發中心經常使用的工具或者有相關工具化的需求,我們可以整合到rdms或者在RDMS上進行開發

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

【其他文章推薦】

※超省錢租車方案

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

※回頭車貨運收費標準

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

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

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

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