亞洲碳中和浪潮下 澳洲總理仍裹足不前捍衛煤炭

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

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

【其他文章推薦】

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

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

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

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

※回頭車貨運收費標準

全球僅五例! 黃金烏龜身世之謎曝光

摘錄自2020年11月4日聯合新聞網報導

美國媒體《紐約郵報》(New York Post)報導,尼泊爾村莊上周二(27日)發現一隻外貌奇特、金黃色的緣板鱉 (Indian flapshell turtle)。《紐約郵報》指出,這隻緣板鱉是因為基因突變的白變(Leucism),導致體內色素減少,皮膚白皙,蒼白或斑駁,而黃色色素細胞通常會變成主導顏色,與白化症不同。

發現這隻緣板鱉的爬行動物專家德沃柯塔(Kamal Devkota)表示他第一次看到這種特殊顏色的烏龜。牠也是尼泊爾史上第一隻、全世界第五隻患白變的烏龜。研究人員已放生這隻緣板鱉回野外,但德沃柯塔擔心牠會面臨生存問題:「在大自然裡這樣的色差非常罕見,可能會在環境中處於不利地位。」

德沃柯塔補充,烏龜在尼泊爾有重要的宗教和文化價值:「人們相信毗濕奴(印度教最著名的神靈之一)變身烏龜以拯救宇宙免受破壞。」

生物多樣性
國際新聞
尼泊爾
烏龜

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

【其他文章推薦】

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

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

※回頭車貨運收費標準

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

※超省錢租車方案

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

民主非人類專利 這三種動物也會選舉罷免

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

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

【其他文章推薦】

※超省錢租車方案

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

※回頭車貨運收費標準

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

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

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

同級最帥的4款轎車,50萬以內沒對手,年輕人就該買這些車

68-68。38萬1、3、5、7系,大家肯定都知道,那4系是什麼鬼。基本上來說,把它“當”成3系的雙門版本准沒錯。這一代寶馬4系其實與3系關聯性非常大,應該說除了外形上的差異,動力系統與机械結構基本一致的。這時候肯定有人會說,“貴這麼多還要少兩扇門這是玩我呢。

基本上每個男生或者喜愛汽車的女性朋友都有着一個跑車夢,開着敞篷跑車在海邊公路上馳騁,或者傍晚獨自在山路享受日落的時間。但現實總是殘酷的,年輕的時候可能礙於經濟實力的原因,基本沒可能會選擇跑車,到了成家立業以後,手頭是寬裕了,可是為了家人買一台敞篷跑車似乎也不是太適合,當然如果是自己的第二第三台車的話就無所謂。那有沒有能作為第一台車,既能滿足家用,也有着想到高的顏值和動感的駕駛體驗呢?答案是有,而且還有4部給閣下選擇。

梅賽德斯-奔馳(進口)- C級

指導價:41.68萬-48.98萬

每次提起奔馳,用優雅這個形容詞最為合適,好像就是它與生俱來的氣質融入到每一款車中,而加上Coupe這個詞之後,這份優雅更能發揮到極致。性感的外形總能給路人留下深刻的印象,經過徹底的革新之後,完全不同於三廂版C級。如果閣下看到這樣一台陌生的屁股,絕對就是Coupe。

C級Coupe不管是300還是200,對應的都是個性化雙門轎跑風格的中型車市場,它們都擁有凌厲而運動的外觀設計,毫不保留的展現出自己的個性,時刻讓你記住一句話,你開的可是奔馳。

英菲尼迪(進口)- Q60

指導價38.98-40.98萬

也許是品牌關係,大家一說到英菲尼迪第一時間想到的就是Q50,然後,就沒有然後了。那現在大家不妨認識一下Q50的Coupe版,Q60,一台詮釋了什麼叫“曲線美”雙門轎跑。各種流暢似水的線條勾勒出絕美的車身輪廓,一副攻擊性十足的前臉相信能瞬間俘獲閣下的心,不說了,先去擦一下口水。

外觀融合了Q50 EauRouge和概念車Q80 Inspiration的設計新元素,使車頭更有衝擊力。內飾相比動感的外觀則溫柔很多,甚至略帶平淡。動力方面目前在售的版本均匹配2.0T發動機搭配7速手自一體變速箱,整體動態跟Q50L相似。

寶馬(進口)- 4系

指導價:41.68-68.38萬

1、3、5、7系,大家肯定都知道,那4系是什麼鬼?基本上來說,把它“當”成3系的雙門版本准沒錯。這一代寶馬4系其實與3系關聯性非常大,應該說除了外形上的差異,動力系統與机械結構基本一致的。這時候肯定有人會說,“貴這麼多還要少兩扇門這是玩我呢?”,這個問題一開始也是這麼想的,直到開上去的那瞬間,那操控手感,那底盤素質。值,非常值。

總結起來4系的特點就是:外觀比3系更寬更低,視覺效果也更震撼,內飾就是一台“寶馬”,沒有驚喜,沒有意外。要說4系最吸引的,還是跟3系的外觀差異,以及紮實的底盤功底。

雷克薩斯-RC

指導價:48.80-58.60萬

雷克薩斯這車在今天講的4部車裡算是比較特別的存在,很多人以為它是IS的雙門版本,但實際上RC可以完全看作單獨專門開發的跑車,而不是衍生車型。

日本紳士最厲害是什麼?當然就是那個固執起來又挺能跟上潮流的性格,RC霸氣的外觀設計會很容易讓人誤以為它有着強大的性能,事實上它對舒適性的妥協較多,很舒服的座椅,很舒服的車內氛圍,在高速上會開到困也不奇怪。性能也是今天講的4部車裡最不“跑車”的,但如果閣下就喜歡跑車造型設計和超級舒適的內在,那麼RC也許就是最適合閣下的那台愛車

結尾

50萬可以買一部思域然後改得“飛天遁地”,也可以買坐上去就舒服到馬上睡着的行政車,但能在外形和內部豪華感上達到平衡且高水準的,目前還是比較少見的。雖然4部都是2.0T汽油發動機,跟“性能”都相去甚遠,但日常在道路找找激情撒撒野還是沒任何問題的。如果閣下是“絕對顏控”,Q60肯定是第一選擇;如果說雙門跑車一定要德國車的話,C級Coupe和4系都不會讓閣下失望,甚至能有點小興奮;至於RC,它除了能給閣下“我很快”的感覺之外,雷克薩斯的匠人品質以及豪華感還能讓圍觀的路人都覺得“你好棒”。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

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

※回頭車貨運收費標準

淑女?劈蕉佬?這些雷人車型名字你了解幾個?

有誰想到這款車當年竟然是名棄嬰,四處找人收留。Jimny的型號的原本來自一間叫希望汽車的生產廠家,該公司研製了一部希望之星ON360,但鑒於公司規模,老闆小野定良計劃向三菱汽車推銷代工制權,但三菱礙於希望汽車的往績,並沒有接受這單生意,最後只能硬着頭皮自行生產,並於1967年底上市,可惜銷量不如預期。

每部車都有着自己的型號名,一般來說就是字母加数字的組合,或單純以文字作為型號名,在當中有的更隱藏別具意義的故事,今天就和大家分享幾個有趣的故事。

淑女的由來

“惡魔Z”、“大魔王”這些都是我們對Fairlady的尊稱,日產經典跑車系列Fairlady這個名字的靈感來源於百老匯歌舞劇My Fair Lady, 1961年時任社長川又克二正在美國公幹,機緣巧合下觀看了這套長壽歌舞劇,同期美國即將推出新款跑車SLp213,一下子就決定新車冠以Fairlady之名,希望它像該歌劇一樣受歡迎,並成為經典。

380萬中挑一

Sunny(陽光)於1966年推出,曾經是日產主力家庭用車,當年日產在報紙廣告上預告推出一升的新車,並進行募集車名活動,在緊接一輪廣告攻勢逐步發布新車細節后,接連一個月宣傳吸引了848萬人參與,收到共380萬個名字推薦,最後選定了切合車型概念的Sunny為名。

南美洲“劈蕉佬”

不少越野發燒友及工程人員鍾情的三菱pajero(帕傑羅),因其英文發音和粵語里的“劈蕉佬”有着99%的相似度,瞬間顯得親民又剛強。至於pajero名稱的起源,則是取自於生活在阿根廷南部高原地帶的一種山貓,它活躍於崎嶇山間,正好吻合pajero的強勁野外走破性能,更讓這隻猛獸衝出南美洲,穿梭於全球各地山野。

人棄我取,終成經典

硬派越野車一直給人一種高大上的感覺,但也有例外,鈴木Jimny(吉姆尼)就是這樣一款小巧見稱的越野車,即使到了今天仍然是不少越野愛好者的寵兒。有誰想到這款車當年竟然是名棄嬰,四處找人收留。Jimny的型號的原本來自一間叫希望汽車的生產廠家,該公司研製了一部希望之星ON360,但鑒於公司規模,老闆小野定良計劃向三菱汽車推銷代工制權,但三菱礙於希望汽車的往績,並沒有接受這單生意,最後只能硬着頭皮自行生產,並於1967年底上市,可惜銷量不如預期。於是小野定良改去向鈴木汽車負責人鈴木修推銷此車,鈴木修對此車一見鍾情,即使公司上下都反對仍堅持一己之見,最終以1,200萬日元向希望汽車買斷整個設計,並於1970年4月推出初代Jimny,造就這部鈴木的招牌車款。

買回來的名字

在Integra未面世之前,prelude(廣東及港澳地區稱“披露”)就是本田coupe車款的代表,也是展現本田各種黑科技的試驗田。DOHC VTEC、雙搖臂懸架及四輪轉向系統等配置早早就出現在prelude之上,中文意謂“前奏曲”的prelude名字原為豐田擁有,當本田於70年代末期決定以此為新車型號后才發現早已被豐田註冊了名字。本來最簡單的方法就是改名,但當年本田執意鍾情於prelude這個名字,於是主動向豐田提出轉讓要求,結果一輪商議后prelude終於歸入本田名下。雖然雙方未有提到當中細節,不過相信離不開用金錢來解決,而這個買回來的名字就一直使用到2001年第五代prelude停產為止,歷時23年。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

※回頭車貨運收費標準

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

※超省錢租車方案

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

近5米長,舒適度同級無敵,這款18.99萬起的車值不值?

雪鐵龍C6上配備的1。8T發動機是pSA集團在中國最高階的發動機。這台發動機的賬面數據和實際表現當然比不上對手最高階的發動機,但是比起他們中階發動機,倒也不至於落後。傳動系統是來自愛信的6AT變速箱,這台變速箱可以說拖了後腿,競爭對手大多數用上了8、9AT或者7速雙離合的產品。

在2008年,C6曾經以中大型車的身份引進中國。當時的雪鐵龍C6是C級車的身份,售價高達64萬。時過境遷,當C6再次回到國人視線里時,已經是B級車的身份,而且售價也僅需18.99-27.99萬。

這一代的C6是一款中國特供車型,專為國人設計。中國人喜歡的元素,超大後排空間、肉眼可見的豪華感、豐富高端的配置、運動流暢的車身造型,在這台C6上一樣不缺。然而,C6的銷量非常慘淡。好在,今天我的任務不是解答為什麼C6銷量慘淡,而是要告訴你這台車到底有多棒。

雪鐵龍C6的外觀以沉穩為主,前臉的雪鐵龍雙人標識延伸到整個車頭,車頭的線條以水平為主,搭配上大面積的鍍鉻格柵,鑽晶型日間行車燈,彰顯了C6行政級轎車的定位。尾部造型飽滿,銀色的鍍鉻裝飾條連接了兩旁的回字形尾燈,盡顯格調。C6側麵線條流暢動感,缺少了一絲行政級轎車應有的厚重感,而且影響了頭部空間的表現。

雪鐵龍C6的車身尺寸比市面上大多數B級車都要大,甚至可以列入C級車的行列。

上帝在製造法國人的時候,只給了他們優秀的設計能力,卻沒有把將設計付諸現實的能力一併安裝上去。C6車身漆面左右厚薄不一,而且這款白色車漆容易沾灰且難以清潔。

雪鐵龍C6的內飾比外觀做得還好。C6的內飾採用了最新家族式對稱式設計,造型設計更加註重檔次感。材質方面啊,C6的內飾採用了大量的Nappa真皮,實木飾板,同級再也找不到更好的。全液晶儀錶盤的显示效果清晰,可調節的選項豐富,UI設計出色。

C6座椅方面採用了Nappa真皮包裹,座椅寬大,填充物支撐性足。這台C6的頂配車型,前後座椅均有座椅通風/加熱/按摩功能,這種級別的配置也是同級所不具備的。

C6的乘坐空間只有一個字——大。1米73的乘客在前排調好位置后,在後排能夠獲得3拳2指左右的腿部空間。可惜,受流線型車身的影響,頭部空間不容樂觀,前排和後排分別只有3指和4指的表現。

C6的後備廂空間非常大,有523L的容積。不過後排座椅不能放倒,所以沒有擴充的空間。

雪鐵龍C6上配備的1.8T發動機是pSA集團在中國最高階的發動機。這台發動機的賬面數據和實際表現當然比不上對手最高階的發動機,但是比起他們中階發動機,倒也不至於落後。傳動系統是來自愛信的6AT變速箱,這台變速箱可以說拖了後腿,競爭對手大多數用上了8、9AT或者7速雙離合的產品。這款6AT的擋位數就顯得落後了。從匹配上來說,整套動力系統也不夠線性,起步時需要很小心地控制油門才不會突然被“踢”一腳。如果C6能來個ECO模式,相信會好很多。

雪鐵龍C6的0-100km/h實測百公里加速時間為9.4秒。當天有小雨,地面略微有積水,但是加速的過程245mm寬的輪胎幾乎沒有打滑,很平順地就衝出去了。這數據雖然不能跟競爭對手的高階動力系統對比,但是跟他們的1.5T或1.8T的動力對比,還是有競爭力的。

既然雪鐵龍C6作為一款行政級定位的轎車,底盤肯定是以舒適為主的。雪鐵龍C6的減振器和避震彈簧採用斜置的布置方式,而且減振器和避震彈簧另一端連接的是副車架,而不是車身。這麼做的好處,就是減少減振器上垂直方向的力和避免力直接作用於車身,最終達到乘坐舒適的效果。實際體驗下來,雪鐵龍C6確實能很好地將路面顛簸過濾掉,比起同級對手單薄的濾震能力要好上不少,甚至可以說,這是C級車上會有的濾震能力。

雪鐵龍C6的方向盤採用電動液壓助力,路感非常清晰,沒有什麼虛位,回正的力度也線性,只是手感比較重,更加適合男性駕駛。

C6的隔音水平也是行政級水準的。C6四門車窗都使用了雙層隔音玻璃。C6在怠速時幾乎聽不到發動機的聲音,高速跑起來的時候輪胎噪音會相對較大,而風噪不明顯。

從市場定位上來說,與C6有相似定位的B級車,只有別克君越這一款。他們都有舒適的底盤表現,寬敞的乘坐空間,高檔的內飾氛圍。兩款拿出來比較,C6的配置更高,後期保養更便宜,而君越的動力會更完善。如果不考慮品牌知名度,C6會更加合適。當然,如果雪鐵龍的品牌知名度能夠打出來,C6肯定不會是現在這個定價。所以,各位觀眾朋友,要撿便宜趁現在了。

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

【其他文章推薦】

※超省錢租車方案

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

※回頭車貨運收費標準

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

FB行銷專家,教你從零開始的技巧

使用Apache commons email發送郵件

今天研究了一下怎麼用java代碼發送郵件,用的是Apache的commons-email包。

據說這個包是對javamail進行了封裝,簡化了操作。 這裏講一下具體用法吧

 

一.首先你需要有郵箱賬號和一個授權碼。

需要進入到QQ郵箱或者是網易郵箱裏面去獲取。在郵箱的設置->賬戶裏面,開啟如下服務,就能得到一個授權碼,這個授權碼要好好保管。有了這兩個東西就能夠通過第三方客戶端發送郵件了。

 

二.導入commons-email的maven依賴。

我用的是1.4,也可以去maven倉庫網站(https://mvnrepository.com)上面找別的版本。

<dependency>
     <groupId>org.apache.commons</groupId>
     <artifactId>commons-email</artifactId>
     <version>1.4</version>
 </dependency>

三.然後就可以寫發送郵件的代碼了。

我在網上找了幾個案例,如下。

 1.發送簡單文本郵件。這是最簡單也是最常用的。

    /**
     * @describe 發送內容為簡單文本的郵件
     * @throws EmailException
     */
    public static void sendSimpleTextEmail() throws EmailException {
         Email email = new SimpleEmail();
         //設置主機名,QQ郵箱是"smtp.qq.com",網易郵箱是"smtp.163.com"
         email.setHostName("smtp.163.com");
         // 用戶名和密碼為郵箱的賬號和授權碼(不需要進行base64編碼)
         email.setAuthenticator(new DefaultAuthenticator("myemailaddress@163.com", "myshouquanma"));
         //設置SSL連接,這樣寫就對了
         email.setSSLOnConnect(true);
         //設置來源,就是發送方的郵箱地址
         email.setFrom("myemailaddress@163.com");
         //設置主題,可以不設置
         email.setSubject("java發送郵件");
         //設置信息,就是內容,這個必須要有
         email.setMsg("這是測試郵件 ... :-)");
         //接收人郵箱地址
         email.addTo("receiveeraddress@qq.com");
         email.send();
    }

 

2.發送包含附件的郵件(附件為本地資源),這裏用到了一個EmailAttachment對象,也就是附件的意思

    /**
     * @describe 發送包含附件的郵件(附件為本地資源)
     * @throws EmailException
     */
    public static void sendEmailsWithAttachments() throws EmailException {
        // 創建一個attachment(附件)對象
        EmailAttachment attachment = new EmailAttachment();
        //設置上傳附件的地址
        attachment.setPath("C:\\Users\\Administrator\\Pictures\\Saved Pictures\\conti.png");
        attachment.setDisposition(EmailAttachment.ATTACHMENT);
        //這個描述可以隨便寫
        attachment.setDescription("Picture of conti");
        //這個名稱要注意和文件格式一致,這將是接收人下載下來的文件名稱
        attachment.setName("conti.png");

        //因為要上傳附件,所以用MultiPartEmail()方法創建一個email對象,固定步驟都是一樣的
        MultiPartEmail email = new MultiPartEmail();
        email.setHostName("smtp.163.com");
        email.setAuthenticator(new DefaultAuthenticator("myemailaddress@163.com", "myshouquanma"));
        email.setSSLOnConnect(true);
        email.addTo("receiveemail@qq.com", "Conti Zhang");
        email.setFrom("myemailaddress@163.com", "Me");
        email.setSubject("圖片");
        email.setMsg("這是發送給你的圖片");
        //將附件添加到郵件
        email.attach(attachment);

        email.send();
    }

 

3.發送包含附近的郵件(附件為在線資源),這個與上傳本地附件稍有區別,注意一下就行

   /**
     * @describe 發送包含附件的郵件(附件為在線資源)
     * @throws EmailException
     * @throws MalformedURLException
     */
    public static void sendEmailsWithOnlineAttachments() throws EmailException, MalformedURLException {
        EmailAttachment attachment = new EmailAttachment();
        //設置在線資源路徑,和上傳本地附件的唯一區別
        attachment.setURL(new URL("http://www.apache.org/images/asf_logo_wide.gif"));
        attachment.setDisposition(EmailAttachment.ATTACHMENT);
        attachment.setDescription("Apache logo");
        attachment.setName("Apache logo.gif");

        MultiPartEmail email = new MultiPartEmail();
        email.setHostName("smtp.163.com");
        email.setAuthenticator(new DefaultAuthenticator("myemailaddress@163.com", "myshouquanma"));
        email.setSSLOnConnect(true);
        email.addTo("receiveemail@qq.com", "Conti Zhang");
        email.setFrom("myemailaddress@163.com", "Me");
        email.setSubject("The logo");
        email.setMsg("Here is Apache's logo");
        email.attach(attachment);
        email.send();
    }

 

4.發送內容為HTML格式的郵件,有些郵件直接打開就是一個HTML頁面。雖然不一定用到,可以了解一下

   /**
     * @describe 發送內容為HTML格式的郵件
     * @throws EmailException
     * @throws MalformedURLException
     */
    public static void sendHTMLFormattedEmail() throws EmailException, MalformedURLException {
        // 這裏需要使用HtmlEmail創建一個email對象
        HtmlEmail email = new HtmlEmail();
        email.setHostName("smtp.163.com");
        email.setAuthenticator(new DefaultAuthenticator("myemailaddresss@163.com", "myshouquanma"));
        email.addTo("receiveemail@qq.com", "Conti Zhang");
        email.setFrom("myemailaddress@163.com", "Me");
        email.setSubject("Test email with inline image");

        // 嵌入圖像並獲取內容id,雖然案例這樣寫,但我感覺直接在html內容裏面寫圖片網絡地址也可以
        URL url = new URL("http://www.apache.org/images/asf_logo_wide.gif");
        String cid = email.embed(url, "Apache logo");

        // 設置html內容
        email.setHtmlMsg("<html>The apache logo - <img src=\"cid:" + cid + "\"></html>");

        // 設置替代內容,如果不支持html
        email.setTextMsg("你的郵件客戶端不支持html郵件");
        email.send();
    }

 

5.發送內容為HTML格式的郵件(嵌入圖片更方便)

這裏用到了DataSourceFileResolver對象,和DataSourceUrlResolver對象,前者可以解析本地文件路徑,後者可以解析網絡路徑

具體用法如下

    /**
     * @describe 發送內容為HTML格式的郵件(嵌入圖片更方便)
     * @throws MalformedURLException
     * @throws EmailException
     */
    public static void sendHTMLFormattedEmailWithEmbeddedImages() throws MalformedURLException, EmailException {
        //html郵件模板
String htmlEmailTemplate = "<img src=\"http://www.conti.com/images/1.jpg\">"; DataSourceResolver[] dataSourceResolvers =new DataSourceResolver[]{new DataSourceFileResolver(),new DataSourceUrlResolver(new URL("http://"))}; email.setDataSourceResolver(new DataSourceCompositeResolver(dataSourceResolvers)); email.setHostName("smtp.qq.com"); email.setAuthenticator(new DefaultAuthenticator("myemailaddress@qq.com", "myshouquanma")); email.addTo("receiveemail@qq.com", "Conti Zhang"); email.setFrom("myemailaddress@qq.com", "Me"); email.setSubject("Test email with inline image"); email.setHtmlMsg(htmlEmailTemplate); email.setTextMsg("你的郵件客戶端不支持html郵件"); email.send(); }

此種方式可能會報錯,會被郵箱認為是有害郵件不接收而導致發送失敗。解決方法就是,網易郵箱不行就換QQ郵箱,我就是這樣做的

 

好了,就這麼多,歡迎討論!

 

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

【其他文章推薦】

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

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

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

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

※回頭車貨運收費標準

drf之框架基礎

(一)drf基礎

全稱:django-rest framework

接口:什麼是接口、restful接口規範(協議)

CBV(基於FBV的基礎上形成)、CBV生命周期源碼—-基於restful規範下的CBV接口

請求生命周期:請求組件、解析組件、響應組件

序列化組件(序列化、反序列化簡單來說就是對象轉為字符串、字符串轉為對象,目的是為傳輸數據(傳給別的語言或者存儲))

三大認證(重中之重):認證(用戶是否合法)、權限(管理員、普通用戶)、頻率(次數過多限制)

其他組件(過濾、篩選、排序、分頁、路由)

 

1.接口

概念:聯繫兩個物質的媒介,完成信息的交互。在web程序中,聯繫前台頁面後台數據庫的媒介。

web接口組成:

url:長得像返回數據的url鏈接。如api.baidu.map/search

www.baidu.com不叫接口,叫url鏈接,訪問只能拿到主頁。api.baidu.map/search是接口,返回的是json數據)

請求參數:前台按照指定的key提供數據給後台。(必須是給定的,這樣後台才能以此提取數據再到數據庫查詢返回)

響應數據:後台與數據庫交互后,將數據反饋給前台。

因此,接口就是帶着請求參數訪問能夠得到響應數據的url鏈接。

接口 = url + 請求參數 + 響應數據

 

 

2.restful接口規範

接口規範:不同後台語言,使用同樣的接口返回同樣的數據。

如何寫接口:要寫url和響應數據。如果將請求參數也加上,就是在寫接口文檔。

 

兩大部分:

1url

1)用api關鍵字標識接口url。方式1api.baidu.com;方式2:www.baudu.com/api

2)接口數據安全性決定優先選用https協議

3)如果一個接口有多版本存在,需要在url中標識體現。如下的v1v2

api.baidu.com/v1/….  api.baidu.com/v2/….

4)操作中的數據稱為資源,在url中資源一般採用複數形式,一個接口可以概括對該資源的多種操作方式。(一個url對應一個類,類裏面可以有多個請求方法)

可以對操作隱藏,並且復用性更強(寫動作了,只能適用這一個動作,不寫其他動作都可以用)如api.baidu.com/books    api.baidu.com/books/(pk)

5)請求方式有多種,用一個url處理如何讓保證不混亂——通過不同的請求方式標識不同操作資源的方式

/books      get     獲取所有

/books post   增加一個(多個)

/books/(pk) delete 刪除一個

/books/(pk)  put 整體更新一個  #改一個用戶

/books/(pk)  patch 局部更新一個 #改一個用戶的密碼

 

6)資源往往涉及數據的各種操作方式:篩選、排序、限制

api.baidu.com/books/?search=寶馬&ordering=-price&limit=3

 

2)響應數據

1http請求的響應會有響應狀態碼,接口用來返回操作的資源數據,也有自己操作數據結果的資源狀態碼status 0代表操作資源成功,1代表操作失敗,2代表操作成功,但沒匹配結果)

注:資源狀態碼和http狀態碼不一樣,為前後台的約定

2)資源狀態碼的文字提示。

status ok “賬號有誤或者密碼有誤”

3)資源本身

results

:刪除資源成功不做任何數據返回(只返回空字符串,連狀態碼、狀態信息都不返回)

4)不能直接返回的資源(子資源、圖片、視頻等資源),返回該資源的url鏈接。

 

https://api.baidu.com/v1/books?limit=3
get|post|delete|put|patch   
{
  “status” : 0,
  “msg” : “ok”,
  “results”: [
{
  “title”: “三國”,
  “price”: 78,
  “img”: “https://.....”   
    }
  ]
}

 

3.django流程

1)項目準備:

 

1.分發路由

在項目文件夾的urls複製一份到應用文件夾中。然後在項目文件夾的urls分發路由給app:導入include,然後url(r’^api/’, include(‘api.urls’))。再在app文件夾的urls.py中分發路由給CBV

2.視圖

在應用中分發路由前,先寫類視圖

from django.http import JsonResponse
from django.views import View

class Book(View):

    def get(self, request, *args, **kwargs):
        return JsonResponse('get ok', safe=False)

    def post(self, request, *args, **kwargs):
        return JsonResponse('get ok', safe=False)   #safe默認為true,要返回字典。不是字典否則拋異常。

 

3.在應用urls下分發路由

from django.conf.urls import url
from . import views    #注意在應用中導入視圖都是.   從當前應用中導入

urlpatterns = [
    url(r'^books/', views.Book.as_view()),
]

 

4.定義模型類

1models.py中定義類

from django.db import models

class Book(models.Model):

    title = models.CharField(max_length=64)
    price = models.DecimalField(max_digits=5, decimal_places=2) #整數、小數位

    class Meta:    #嵌套類(給上級類添加功能或指定標準)
        
       db_table = 'book'    #自定義數據庫表名
        verbose_name = "book"   #給模型起個可讀的名字,默認是複數
        verbose_name_plural = verbose_name   #取消上面的複數

    def __str__(self):      #显示的內容
        return '<<%s>>' % self.title    

 

 

   (2)數據庫遷移

進入djangoshell環境中:Tools—-> run manage.py task

shell環境中生成遷移文件:makemigrations。然後遷移:migrate

 

5.生成admin

1)在amin.py中註冊並且導入模型

from django.contrib import admin
port models

admin.site.register(models.Book)

 

2)創建用戶

shell環境中:createsuper創建超級用戶,然後輸入用戶密碼(郵箱不用)

 

 

 2CBV的請求生命周期

請求如何到CBV下的getpost

a.請求過來,項目文件中路由分發給應用api的路由

b.應用分發路由走as_view函數。

views.Book.as_view()  保存一系列數據(requestargs**kwargs等)給Book對象,然後都給dispatch進行路由分發。

dispatch乾的事:判斷請求方式是否支持,然後返回(通過getattr)支持的這些請求方法(getpost等,在視圖中自定義getpost的返回值)的結果。

c.通過dispatch就執行了CBV下請求方式的結果,返回結果

 

 

4.django原生的接口、序列化

  六大基礎接口:獲取一個、獲取所有、增加一個、刪除一個、整體更新一個、局部更新一個

 十大接口:6大基礎、群增、群刪、整體群改、局部群改

 

1.在應用的urls.py下分發路由

url(r’^books/$’, views.Book.as_view()),   #必須要加$,否則後面匹配不到

url(r’^books/(?P<pk>.*)/$’, views.Book.as_view()),有名分組

在視圖函數中通過kwargs.get(pk)取到匹配的值

 

2.views.py里寫邏輯

class Book(View):

 

    def get(self, request, *args, **kwargs):

        pk = kwargs.get(‘pk’)  #獲取參數

        if not pk:  #群查接口

            #操作數據庫

            book_obj_list = models.Book.objects.all()

            #序列化過程

            book_list = []

            for obj in book_obj_list:   #將查到的對象序列化

                dic = {}

                dic[‘title’] = obj.title

                dic[‘price’] = obj.price

                book_list.append(dic)

            return JsonResponse({

                ‘status’ : 0,

                “msg” : “ok”,

                “results”: book_list,

            }, json_dumps_params={‘ensure_ascii’:False})

        else:    #單查接口

            book_dic = models.Book.objects.filter(pk=pk).values(

                ‘title’, ‘price’).first()

            if book_dic:

                return JsonResponse({

                    ‘status’: 0,

                    “msg”: “ok”,

                    “results”: book_dic,

                }, json_dumps_params={‘ensure_ascii’: False})

 

            return JsonResponse({

                ‘status’: 2,

                “msg”: “no results”,

            }, json_dumps_params={‘ensure_ascii’: False})

 

 

    def post(self, request, *args, **kwargs):

        #前台通過urlencoded方式提交數據

        try:

            book_obj = models.Book.objects.create(**request.POST.dict()) #create創建對象。將request.POST中存放的提交的關鍵詞參數轉化為字典以**方式傳進去。沒傳參數,這邊會報錯。

            if book_obj:

                return JsonResponse({

                ‘status’: 0,

                “msg”: “ok”,

                “results”: {‘title’:book_obj.title, “price”:book_obj.price}

            }, json_dumps_params={‘ensure_ascii’: False})

        except:    #健壯性

            return JsonResponse({

                ‘status’: 1,

                “msg”: “wrong params”,

            }, json_dumps_params={‘ensure_ascii’: False})

        return JsonResponse({    #可能操作數據庫失敗了

                ‘status’: 2,

                “msg”: “created failed”,

            }, json_dumps_params={‘ensure_ascii’: False})

           

  

JsonResponse返回時,中文會變成unicode,要加json_dumps_params={‘ensure_ascii’:False}選項。但在linux環境下的火狐瀏覽器,加了是亂碼。

 

filter返回queryset對象,對象里是個列表(表名:對象信息(有自定義str就是自定義的信息))。first取里第一個對象(相當於print(第一個對象)values展示對應的對象里的值

<QuerySet [<Book: <<三國演義>>>]>                   #直接.filter

<<三國演義>>                                    #.first()

<QuerySet [{‘title’: ‘三國演義‘, ‘price’: Decimal(‘56.00’)}]>  #.values(‘title’,’price’)

{‘title’: ‘三國演義‘, ‘price’: Decimal(‘56.00’)}             #.values.first()  是個字典

 

 上面序列化的工作很麻煩。drf就是為了方便序列化的。

 

postman可以完成不同方式的請求:getpostput

postman發送數據包有三種方式:form-dataurlencodedjson. 原生djangourlencoded數據提交兼容。

 

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

【其他文章推薦】

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

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

※回頭車貨運收費標準

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

※超省錢租車方案

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

『圖論』LCA 最近公共祖先

概述篇

LCA (Least Common Ancestors) ,即最近公共祖先,是指這樣的一個問題:在一棵有根樹中,找出某兩個節點 uv 最近的公共祖先。

LCA 可分為在線算法離線算法

  • 在線算法:指程序可以以序列化的方式一個一個處理輸入,也就是說在一開始並不需要知道所有的輸入。
  • 離線算法:指一開始就需要知道問題的所有輸入數據,而在解決一個問題后立即輸出結果。

算法篇

對於該問題,很容易想到的做法是從 u、v 分別回溯到根節點,然後這兩條路徑中的第一個交點即為 u、v 的最近公共祖先,在一棵平衡二叉樹中,該算法的時間複雜度可以達到 O(logn)O(log⁡n) ,但是對於某些退化為鏈狀的樹來說,算法的時間複雜度最壞為 O(n)O(n) ,顯然無法滿足更高頻率的查詢。

本節將介紹幾種比較高效的算法來解決這一問題,常見的算法有三種:在線 DFS + ST 算法、倍增算法、離線 Tarjan 算法。

接下來我們來一一解釋這三種 /* 看似高深,其實也不簡單 */ 的算法。

在線 DFS + ST 算法

首先看到 ST 你會想到什麼呢?(腦補許久都沒有想到它會是哪個單詞的縮寫)

看過前文 『數據結構』RMQ 問題 的話你便可以明白 ST算法 的思路啦~

So ,關於 LCA 的這種在線算法也是可以建立在 RMQ 問題的基礎上咯~

我們設 LCA(T,u,v) 為在有根樹 T 中節點 u、v 的最近公共祖先, RMQ(A,i,j) 為線性序列 A 中區間 [i,j] 上的最小(大)值。

如下圖這棵有根樹:

我們令節點編號滿足父節點編號小於子節點編號(編號條件)

可以看出 LCA(T,4,5) = 2, LCA(T,2,8) = 1, LCA(T,3,9) = 3

設線性序列 A 為有根樹 T 的中序遍歷,即 A = [4,2,5,1,8,6,9,3,7]

由中序遍歷的性質我們可以知道,任意兩點 u、v 的最近公共祖先總在以該兩點所在位置為端點的區間內,且編號最小。

舉個栗子:

假設 u = 8, v = 7 ,則該兩點所確定的一段區間為 [8,6,9,3,7] ,而區間最小值為 3 ,也就是說,節點 3u、v 的最近公共祖先。

解決區間最值問題我們可以採用 RMQ 問題中的 ST 算法

但是在有些問題中給出的節點並不一定滿足我們所說的父節點編號小於子節點編號,因此我們可以利用節點間的關係建圖,然後採用前序遍歷來為每一個節點重新編號以生成線性序列 A ,於是問題又被轉化為了區間最值的查詢,和之前一樣的做法咯~

時間複雜度: n×O(logn)n×O(log⁡n) 預處理 + O(1)O(1) 查詢

想了解 RMQ 問題 的解法可以戳上面的鏈接哦~

以上部分介紹了 LCA 如何轉化為 RMQ 問題,而在實際中這兩種方案之間可以相互轉化

類比之前的做法,我們如何將一個線性序列轉化為滿足編號條件的有根樹呢?

  1. 設序列中的最小值為 AkAk ,建立優先級為 AkAk 的根節點 TkTk
  2. 將 A[1…k−1]A[1…k−1] 遞歸建樹作為 TkTk 的左子樹
  3. 將 A[k+1…n]A[k+1…n] 遞歸建樹作為 TkTk 的右子樹

讀者可以試着利用此方法將之前的線性序列 A = [4,2,5,1,8,6,9,3,7] 構造出有根樹 T ,結果一定滿足之前所說的編號條件,但卻不一定唯一。

離線 Tarjan 算法

Tarjan 算法是一種常見的用於解決 LCA 問題的離線算法,它結合了深度優先搜索與並查集,整個算法為線性處理時間。

首先來介紹一下 Tarjan 算法的基本思路:

  1. 任選一個節點為根節點,從根節點開始
  2. 遍歷該點 u 的所有子節點 v ,並標記 v 已經被訪問過
  3. 若 v 還有子節點,返回 2 ,否則下一步
  4. 合併 v 到 u 所在集合
  5. 尋找與當前點 u 有詢問關係的點 e
  6. 若 e 已經被訪問過,則可以確定 u、e 的最近公共祖先為 e 被合併到的父親節點

偽代碼:

Tarjan(u)               // merge 和 find 為並查集合併函數和查找函數
{
    for each(u,v)       // 遍歷 u 的所有子節點 v
    {
        Tarjan(v);      // 繼續往下遍歷
        merge(u,v);     // 合併 v 到 u 這一集合
        標記 v 已被訪問過;
    }
    for each(u,e)       // 遍歷所有與 u 有查詢關係的 e
    {
        if (e 被訪問過)
            u, e 的最近公共祖先為 find(e);
    }
}
C++

感覺講到這裏已經沒有其它內容了,但是一定會有好多人沒有理解怎麼辦呢?

我們假設在如下樹中模擬 Tarjan 過程(節點數量少一點可以畫更少的圖o( ̄▽ ̄)o)

存在查詢: LCA(T,3,4)、LCA(T,4,6)、LCA(T,2,1)

注意:每個節點的顏色代表它當前屬於哪一個集合,橙色線條為搜索路徑,黑色線條為合併路徑。

當前所在位置為 u = 1 ,未遍歷孩子集合 v = {2,5} ,向下遍歷。

當前所在位置為 u = 2 ,未遍歷孩子集合 v = {3,4} ,向下遍歷。

當前所在位置為 u = 3 ,未遍歷孩子集合 v = {} ,遞歸到達最底層,遍歷所有相關查詢發現存在 LCA(T,3,4) ,但是節點 4 此時標記未訪問,因此什麼也不做,該層遞歸結束。

遞歸返回,當前所在位置 u = 2 ,合併節點 3u 所在集合,標記 vis[3] = true ,此時未遍歷孩子集合 v = {4} ,向下遍歷。

當前所在位置 u = 4 ,未遍歷孩子集合 v = {} ,遍歷所有相關查詢發現存在 LCA(T,3,4) ,且 vis[3] = true ,此時得到該查詢的解為節點 3 所在集合的首領,即 LCA(T,3,4) = 2 ;又發現存在相關查詢 LCA(T,4,6) ,但是節點 6 此時標記未訪問,因此什麼也不做。該層遞歸結束。

遞歸返回,當前所在位置 u = 2 ,合併節點 4u 所在集合,標記 vis[4] = true ,未遍歷孩子集合 v = {} ,遍歷相關查詢發現存在 LCA(T,2,1) ,但是節點 1 此時標記未訪問,因此什麼也不做,該層遞歸結束。

遞歸返回,當前所在位置 u = 1 ,合併節點 2u 所在集合,標記 vis[2] = true ,未遍歷孩子集合 v = {5} ,繼續向下遍歷。

當前所在位置 u = 5 ,未遍歷孩子集合 v = {6} ,繼續向下遍歷。

當前所在位置 u = 6 ,未遍歷孩子集合 v = {} ,遍歷相關查詢發現存在 LCA(T,4,6) ,且 vis[4] = true ,因此得到該查詢的解為節點 4 所在集合的首領,即 LCA(T,4,6) = 1 ,該層遞歸結束。

遞歸返回,當前所在位置 u = 5 ,合併節點 6u 所在集合,並標記 vis[6] = true ,未遍歷孩子集合 v = {} ,無相關查詢因此該層遞歸結束。

遞歸返回,當前所在位置 u = 1 ,合併節點 5u 所在集合,並標記 vis[5] = true ,未遍歷孩子集合 v = {} ,遍歷相關查詢發現存在 LCA(T,2,1) ,此時該查詢的解便是節點 2 所在集合的首領,即 LCA(T,2,1) = 1 ,遞歸結束。

至此整個 Tarjan 算法便結束啦~

PS:不要在意最終根節點的顏色和其他節點顏色有一點點小小差距,可能是在染色的時候沒仔細看,總之就這樣咯~

PPS:所謂的首領就是、就是首領啦~

倍增算法

哇!還有一個倍增算法以後繼續補充吧!

總結篇

對於不同的 LCA 問題我們可以選擇不同的算法。

假若一棵樹存在動態更新,此時離線算法就顯得有點力不從心了,但是在其他情況下,離線算法往往效率更高(雖然不能保證得到解的順序與輸入一致,不過我們有 sort 呀)

總之,喜歡哪種風格的 code 是我們自己的意願咯~

另外, LCA 和 RMQ 問題是兩個非常基礎的問題,很多複雜問題都可以轉化為這兩類問題來解決。(當然這兩類問題之間也可以相互轉化啦~)

參考資料

OI wiki https://oi-wiki.org/graph/lca/

https://blog.csdn.net/my_sunshine26/article/details/72717112

https://wizardforcel.gitbooks.io/the-art-of-programming-by-july/content/03.03.html

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

【其他文章推薦】

※超省錢租車方案

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

※回頭車貨運收費標準

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

FB行銷專家,教你從零開始的技巧

這一次搞懂SpringMVC原理

@

目錄

  • 前言
  • 正文
    • 請求入口
    • 組件初始化
    • 調用Controller
    • 參數、返回值解析
  • 總結

前言

前面幾篇文章,學習了Spring IOC、Bean實例化過程、AOP、事務的源碼和設計思想,了解了Spring的整體運行流程,但如果是web開發,那麼必不可少的還有Spring MVC,本篇主要分析在請求調用過程中SpringMVC的實現原理,通過本篇要搞懂它是怎麼解決請求、參數、返回值映射等問題的。

正文

請求入口

我們都知道前端調用後端接口時,都會通過Servlet進行轉發,而Servlet的聲明周期包含下面四個階段:

  • 實例化(new)
  • 初始化(init)
  • 執行(service調用doGet/doPost)
  • 銷毀(destroy)

前兩個階段在Spring啟動階段就做好了(init根據配置可能是第一次請求時才會調用),銷毀是服務關閉的時候進行,本文主要分析的就是請求執行階段。我們知道SpringMVC的核心就是DispatcherServlet,該類是對Servlet的擴展,所以直接從該類的service方法開始,但在此類中沒有service方法,那肯定是在其父類中,我們先來看看其繼承體系:

逐個往上找,在FrameworkServlet方法中就有一個service方法:

	protected void service(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {

		HttpMethod httpMethod = HttpMethod.resolve(request.getMethod());
		if (httpMethod == HttpMethod.PATCH || httpMethod == null) {
			processRequest(request, response);
		}
		else {
			super.service(request, response);
		}
	}

    protected void service(HttpServletRequest req, HttpServletResponse resp)
        throws ServletException, IOException
    {
        String method = req.getMethod();

        if (method.equals(METHOD_GET)) {
            long lastModified = getLastModified(req);
            if (lastModified == -1) {
                doGet(req, resp);
            } else {
                long ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);
                if (ifModifiedSince < lastModified) {
                    maybeSetLastModified(resp, lastModified);
                    doGet(req, resp);
                } else {
                    resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
                }
            }

        } else if (method.equals(METHOD_HEAD)) {
            long lastModified = getLastModified(req);
            maybeSetLastModified(resp, lastModified);
            doHead(req, resp);
        } else if (method.equals(METHOD_POST)) {
            doPost(req, resp);
        } else if (method.equals(METHOD_PUT)) {
            doPut(req, resp);
        } else if (method.equals(METHOD_DELETE)) {
            doDelete(req, resp);
        } else if (method.equals(METHOD_OPTIONS)) {
            doOptions(req,resp);
        } else if (method.equals(METHOD_TRACE)) {
            doTrace(req,resp);
        } else {
            String errMsg = lStrings.getString("http.method_not_implemented");
            Object[] errArgs = new Object[1];
            errArgs[0] = method;
            errMsg = MessageFormat.format(errMsg, errArgs);
            
            resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, errMsg);
        }
    }
   

但其主要還是調用父類HttpServlet中的方法,而該類又會根據不同的請求方式會調到子類中,最後的核心方法就是DispatcherServlet中的doDispatch方法:

	protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
		HttpServletRequest processedRequest = request;
		HandlerExecutionChain mappedHandler = null;
		boolean multipartRequestParsed = false;

		//異步管理
		WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

		try {
			ModelAndView mv = null;
			Exception dispatchException = null;

			try {
				//文件上傳
				processedRequest = checkMultipart(request);
				multipartRequestParsed = (processedRequest != request);

				//這個方法很重要,重點看
				// Determine handler for the current request.
				mappedHandler = getHandler(processedRequest);
				if (mappedHandler == null) {
					noHandlerFound(processedRequest, response);
					return;
				}

				//獲取跟HandlerMethod匹配的HandlerAdapter對象
				// Determine handler adapter for the current request.
				HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

				// Process last-modified header, if supported by the handler.
				String method = request.getMethod();
				boolean isGet = "GET".equals(method);
				if (isGet || "HEAD".equals(method)) {
					long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
					if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
						return;
					}
				}

				//前置過濾器,如果為false則直接返回
				if (!mappedHandler.applyPreHandle(processedRequest, response)) {
					return;
				}

				//調用到Controller具體方法,核心方法調用,重點看看
				// Actually invoke the handler.
				mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

				if (asyncManager.isConcurrentHandlingStarted()) {
					return;
				}

				applyDefaultViewName(processedRequest, mv);

				//中置過濾器
				mappedHandler.applyPostHandle(processedRequest, response, mv);
			}
			catch (Exception ex) {
				dispatchException = ex;
			}
			catch (Throwable err) {
				// As of 4.3, we're processing Errors thrown from handler methods as well,
				// making them available for @ExceptionHandler methods and other scenarios.
				dispatchException = new NestedServletException("Handler dispatch failed", err);
			}

			//視圖渲染及後置過濾器執行
			processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
		}
		catch (Exception ex) {
			triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
		}
		catch (Throwable err) {
			triggerAfterCompletion(processedRequest, response, mappedHandler,
					new NestedServletException("Handler processing failed", err));
		}
		finally {
			if (asyncManager.isConcurrentHandlingStarted()) {
				// Instead of postHandle and afterCompletion
				if (mappedHandler != null) {
					mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
				}
			}
			else {
				// Clean up any resources used by a multipart request.
				if (multipartRequestParsed) {
					cleanupMultipart(processedRequest);
				}
			}
		}
	}

MVC的所有處理邏輯都在這個方法中,先總結一下這個方法的實現邏輯,首先根據請求的url拿到緩存中的HandlerMethod對象和執行鏈對象,HandlerMethod中封裝了controller對象、方法對象和方法參數等信息,執行鏈則是包含了一個個HandlerInterceptor攔截器;然後再通過HandlerMethod拿到對應的HandlerAdapter,這個對象的作用就是去適配我們的controller;準備工作做完后,首先會執行前置過濾,如果被攔截則直接返回,否則就去調用controller中的方法執行我們的業務邏輯並返回一個ModelView對象;接着執行中置過濾器,以及處理全局異常捕獲器捕獲到異常;最後進行視圖渲染返回並執行後置過濾器進行資源釋放等工作。
以上就是MVC的整體執行流程,下面就逐個來分析,首先進入getHandler方法:

	protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
		//handlerMappering實例
		if (this.handlerMappings != null) {
			for (HandlerMapping mapping : this.handlerMappings) {
				//獲取HandlerMethod和過濾器鏈的包裝類
				HandlerExecutionChain handler = mapping.getHandler(request);
				if (handler != null) {
					return handler;
				}
			}
		}
		return null;
	}

是委託給HandlerMapping對象的,這是一個接口,主要的實現類是RequestMappingHandlerMapping,同樣先來看看其繼承體系:

這個類是管理請求和處理類之間的映射關係的,你是否疑惑它是在哪裡實例化的呢?下面先來看看MVC組件的初始化。

組件初始化

這裏我以自動化配置的註解方式說明,Spring提供了一個@EnableWebMvc,通過前面的學習我們知道在這個註解中必定導入了一個配置類,點進去可以看到是DelegatingWebMvcConfiguration,這個類就是負責MVC的組件和擴展實現的初始化,其本身我們先不看,先看其父類WebMvcConfigurationSupport,這個類我們應該不陌生,要做一些自定義擴展時就需要繼承該類(如攔截器Interceptor),同樣作用的類還有WebMvcConfigurerAdapter,這個類是對前者相對安全的擴展,為什麼是相對安全呢?因為繼承前者會導致自動配置失效,而使用後者則不必擔心此問題,只需要在類上加上@EnableWebMvc註解。
WebMvcConfigurationSupport中我們可以看到很多@Bean標註的方法,也就是mvc組件的實例化,這裏主要看看requestMappingHandlerMapping,其餘的可自行閱讀理解,也就是一些Bean的註冊:

	public RequestMappingHandlerMapping requestMappingHandlerMapping() {
		RequestMappingHandlerMapping mapping = createRequestMappingHandlerMapping();
		mapping.setOrder(0);
		mapping.setInterceptors(getInterceptors());
		mapping.setContentNegotiationManager(mvcContentNegotiationManager());
		mapping.setCorsConfigurations(getCorsConfigurations());

		......省略

		return mapping;
	}

這裏主要看getInterceptors方法如何獲取攔截器的:

	protected final Object[] getInterceptors() {
		if (this.interceptors == null) {
			InterceptorRegistry registry = new InterceptorRegistry();
			//鈎子方法,需要自己定義
			addInterceptors(registry);
			registry.addInterceptor(new ConversionServiceExposingInterceptor(mvcConversionService()));
			registry.addInterceptor(new ResourceUrlProviderExposingInterceptor(mvcResourceUrlProvider()));
			this.interceptors = registry.getInterceptors();
		}
		return this.interceptors.toArray();
	}

第一次進來會調用addInterceptors添加攔截器,這是一個模板方法,在子類DelegatingWebMvcConfiguration中實現:

	private final WebMvcConfigurerComposite configurers = new WebMvcConfigurerComposite();
	
	protected void addInterceptors(InterceptorRegistry registry) {
		this.configurers.addInterceptors(registry);
	}

	public void addInterceptors(InterceptorRegistry registry) {
		for (WebMvcConfigurer delegate : this.delegates) {
			delegate.addInterceptors(registry);
		}
	}

可以看到最終是調用WebMvcConfigureraddInterceptors方法,也就是我們對WebMvcConfigurerAdapter的自定義擴展。看到這裏我們應該明白了MVC的組件是如何添加到IOC容器中的,但是DispatcherServlet又是怎麼獲取到它們的呢?回到之前的代碼中,在DispatcherServlet這個類中有一個onRefresh方法,這個方法又調用了initStrategies方法完成了MVC九大組件的註冊:

	protected void onRefresh(ApplicationContext context) {
		initStrategies(context);
	}

	protected void initStrategies(ApplicationContext context) {
		initMultipartResolver(context);
		initLocaleResolver(context);
		initThemeResolver(context);
		initHandlerMappings(context);
		initHandlerAdapters(context);
		initHandlerExceptionResolvers(context);
		initRequestToViewNameTranslator(context);
		initViewResolvers(context);
		initFlashMapManager(context);
	}

	private void initHandlerMappings(ApplicationContext context) {
		this.handlerMappings = null;

		if (this.detectAllHandlerMappings) {
			// Find all HandlerMappings in the ApplicationContext, including ancestor contexts.
			Map<String, HandlerMapping> matchingBeans =
					BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);
			if (!matchingBeans.isEmpty()) {
				this.handlerMappings = new ArrayList<>(matchingBeans.values());
				// We keep HandlerMappings in sorted order.
				AnnotationAwareOrderComparator.sort(this.handlerMappings);
			}
		}
		else {
			try {
				HandlerMapping hm = context.getBean(HANDLER_MAPPING_BEAN_NAME, HandlerMapping.class);
				this.handlerMappings = Collections.singletonList(hm);
			}
			catch (NoSuchBeanDefinitionException ex) {
				// Ignore, we'll add a default HandlerMapping later.
			}
		}
		
		if (this.handlerMappings == null) {
			this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class);
		}
	}

initHandlerMappings為例,其它組件實現邏輯基本一樣。首先從IOC容器中拿到handlerMappings的所有實現類(WebMvcConfigurationSupport中注入的對象就在這裏被獲取到),若沒有,則從DispatcherServlet.properties配置文件中(這個配置在spring-webmvc工程下org/springframework/web/servlet/DispatcherServlet.properties)獲取默認的配置:

org.springframework.web.servlet.LocaleResolver=org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver

org.springframework.web.servlet.ThemeResolver=org.springframework.web.servlet.theme.FixedThemeResolver

org.springframework.web.servlet.HandlerMapping=org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping,\
	org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping

org.springframework.web.servlet.HandlerAdapter=org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter,\
	org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter,\
	org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter

org.springframework.web.servlet.HandlerExceptionResolver=org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver,\
	org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver,\
	org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver

org.springframework.web.servlet.RequestToViewNameTranslator=org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator

org.springframework.web.servlet.ViewResolver=org.springframework.web.servlet.view.InternalResourceViewResolver

org.springframework.web.servlet.FlashMapManager=org.springframework.web.servlet.support.SessionFlashMapManager

但是onRefresh又是在什麼時候調用的呢?有兩個地方,一個是Servlet初始化時會調用到initWebApplicationContext進行容器的初始化,這個方法中就會觸發onRefresh;另外還有一個,在FrameworkServlet中有一個onApplicationEvent方法,而這個方法又會被內部類ContextRefreshListener調用,這個類實現了ApplicationListener接口,表示會接收容器刷新事件。
以上就就是MVC HandlerMapping組件的初始化邏輯,其它組件實現邏輯相同,下面不再分析。

調用Controller

回到getHandler方法,其調用的是AbstractHandlerMapping類的方法:

	public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
		//根據請求的uri拿到對應的HandlerMethod對象
		Object handler = getHandlerInternal(request);
		if (handler == null) {
			handler = getDefaultHandler();
		}
		if (handler == null) {
			return null;
		}
		// Bean name or resolved handler?
		if (handler instanceof String) {
			String handlerName = (String) handler;
			handler = obtainApplicationContext().getBean(handlerName);
		}

		//獲取HandlerMethod和過濾器鏈的包裝類
		HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request);

		if (logger.isTraceEnabled()) {
			logger.trace("Mapped to " + handler);
		}
		else if (logger.isDebugEnabled() && !request.getDispatcherType().equals(DispatcherType.ASYNC)) {
			logger.debug("Mapped to " + executionChain.getHandler());
		}

		//是否是跨域請求,就是查看request請求頭中是否有Origin屬性
		if (CorsUtils.isCorsRequest(request)) {
			//自定義的鈎子方法獲取跨域配置
			CorsConfiguration globalConfig = this.corsConfigurationSource.getCorsConfiguration(request);
			//註解獲取跨域配置
			CorsConfiguration handlerConfig = getCorsConfiguration(handler, request);
			CorsConfiguration config = (globalConfig != null ? globalConfig.combine(handlerConfig) : handlerConfig);
			//這裏設置了跨域的過濾器CorsInterceptor
			executionChain = getCorsHandlerExecutionChain(request, executionChain, config);
		}

		return executionChain;
	}

先看AbstractHandlerMethodMapping.getHandlerInternal

	protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {
		//從request對象中獲取uri,/common/query2
		String lookupPath = getUrlPathHelper().getLookupPathForRequest(request);
		this.mappingRegistry.acquireReadLock();
		try {
			//根據uri從映射關係中找到對應的HandlerMethod對象
			HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request);
			//把Controller類實例化
			return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null);
		}
		finally {
			this.mappingRegistry.releaseReadLock();
		}
	}

	protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
		List<Match> matches = new ArrayList<>();
		// 根據url拿到對應的RequestMappingInfo
		List<T> directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath);
		if (directPathMatches != null) {
			addMatchingMappings(directPathMatches, matches, request);
		}
		if (matches.isEmpty()) {
			// No choice but to go through all mappings...
			addMatchingMappings(this.mappingRegistry.getMappings().keySet(), matches, request);
		}

		if (!matches.isEmpty()) {
			Comparator<Match> comparator = new MatchComparator(getMappingComparator(request));
			matches.sort(comparator);
			Match bestMatch = matches.get(0);
			if (matches.size() > 1) {
				if (logger.isTraceEnabled()) {
					logger.trace(matches.size() + " matching mappings: " + matches);
				}
				if (CorsUtils.isPreFlightRequest(request)) {
					return PREFLIGHT_AMBIGUOUS_MATCH;
				}
				Match secondBestMatch = matches.get(1);
				//如果兩個RequestMappinginfo什麼都相同,報錯
				if (comparator.compare(bestMatch, secondBestMatch) == 0) {
					Method m1 = bestMatch.handlerMethod.getMethod();
					Method m2 = secondBestMatch.handlerMethod.getMethod();
					String uri = request.getRequestURI();
					throw new IllegalStateException(
							"Ambiguous handler methods mapped for '" + uri + "': {" + m1 + ", " + m2 + "}");
				}
			}
			request.setAttribute(BEST_MATCHING_HANDLER_ATTRIBUTE, bestMatch.handlerMethod);
			handleMatch(bestMatch.mapping, lookupPath, request);
			return bestMatch.handlerMethod;
		}
		else {
			return handleNoMatch(this.mappingRegistry.getMappings().keySet(), lookupPath, request);
		}
	}

	private void addMatchingMappings(Collection<T> mappings, List<Match> matches, HttpServletRequest request) {
		for (T mapping : mappings) {
			// 拿到匹配的RequestMappingInfo對象,有可能url相同,@RequestMapping的屬性(請求方式、參數等)匹配不上
			T match = getMatchingMapping(mapping, request);
			if (match != null) {
				//RequestMappingInfo對象和HandlerMethod對象封裝到Match對象中,其實就是註解屬性和Method對象的映射
				matches.add(new Match(match, this.mappingRegistry.getMappings().get(mapping)));
			}
		}
	}

這裏邏輯很簡單,就是通過請求url從urlLookup中拿到對應的RequestMappingInfo(每一個 @RequestMapping對應一個RequestMappingInfo對象)對象,再根據RequestMappingInfo對象從mappingLookup拿到對應的HandlerMethod並返回。
但這裏你可能會比較好奇urlLookupmappingLookup從哪裡來的,仔細觀察你會發現當前這個類實現了一個接口InitializingBean,實現了這個接口的類會在該類的Bean實例化完成后調用afterPropertiesSet方法,上面的映射關係就是在這個方法中做的。實際上這個方法不止完成了上面兩個映射關係,還有下面兩個:

  • corsLookup:handlerMethod -> corsConfig
  • registry:RequestMappingInfo -> MappingRegistration(包含url、handlerMethod、RequestMappingInfo、name等信息)

這裏就不展開分析了,奉上一張時序圖,讀者可根據下面的時序圖自行分析:

拿到HandlerMethod對象后,又會通過getHandlerExecutionChain方法去獲取到所有的HandlerInterceptor攔截器對象,並連同HandlerMethod對象一起封裝為HandlerExecutionChain。之後是獲取跨域配置,這裏不詳細分析。
拿到HandlerExecutionChain對象后返回到doDispatch方法,又調用了getHandlerAdapter
方法拿到HandlerAdapter

	protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {
		//根據handlerMethod對象,找到合適的HandlerAdapter對象,這裏用到了策略模式
		if (this.handlerAdapters != null) {
			for (HandlerAdapter adapter : this.handlerAdapters) {
				if (adapter.supports(handler)) {
					return adapter;
				}
			}
		}
	}

這裏的handlerAdapters變量值從哪裡來?相信不用我再分析,主要看這裏的設計思想,典型的策略模式
之後調用完前置過濾器后,才是真正調用我們controller方法的邏輯,通過HandlerAdapter.handle去調用,最終會調用到ServletInvocableHandlerMethod.invokeAndHandle

	public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer,
			Object... providedArgs) throws Exception {

		//具體調用邏輯,重點看
		Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
		setResponseStatus(webRequest);

		if (returnValue == null) {
			if (isRequestNotModified(webRequest) || getResponseStatus() != null || mavContainer.isRequestHandled()) {
				mavContainer.setRequestHandled(true);
				return;
			}
		}
		else if (StringUtils.hasText(getResponseStatusReason())) {
			mavContainer.setRequestHandled(true);
			return;
		}

		mavContainer.setRequestHandled(false);
		Assert.state(this.returnValueHandlers != null, "No return value handlers");
		try {
			//返回值處理
			this.returnValueHandlers.handleReturnValue(
					returnValue, getReturnValueType(returnValue), mavContainer, webRequest);
		}
		catch (Exception ex) {
			if (logger.isTraceEnabled()) {
				logger.trace(formatErrorForReturnValue(returnValue), ex);
			}
			throw ex;
		}
	}

這個方法裏面主要看invokeForRequesthandleReturnValue的調用,前者是完成參數綁定並調用controller,後者則是對返回值進行處理並封裝到ModelAndViewContainer中。先來看invokeForRequest

	public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
			Object... providedArgs) throws Exception {

		//獲取參數數組
		Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
		if (logger.isTraceEnabled()) {
			logger.trace("Arguments: " + Arrays.toString(args));
		}
		return doInvoke(args);
	}

doInvoke就是完成反射調用,主要還是看參數綁定的實現邏輯,在getMethodArgumentValues方法中:

	protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
			Object... providedArgs) throws Exception {

		if (ObjectUtils.isEmpty(getMethodParameters())) {
			return EMPTY_ARGS;
		}
		//入參的包裝類,裡面包裝了參數類型,參數名稱,參數註解等等信息
		MethodParameter[] parameters = getMethodParameters();
		Object[] args = new Object[parameters.length];
		for (int i = 0; i < parameters.length; i++) {
			MethodParameter parameter = parameters[i];
			//設置參數名稱解析器
			parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
			args[i] = findProvidedArgument(parameter, providedArgs);
			if (args[i] != null) {
				continue;
			}
			//典型的策略模式,根據parameter能否找到對應參數的處理類,能找到就返回true
			if (!this.resolvers.supportsParameter(parameter)) {
				throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver"));
			}
			try {
				//具體參數值解析過程,重點看看
				args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
			}
			catch (Exception ex) {
				// Leave stack trace for later, exception may actually be resolved and handled..
				if (logger.isDebugEnabled()) {
					String error = ex.getMessage();
					if (error != null && !error.contains(parameter.getExecutable().toGenericString())) {
						logger.debug(formatArgumentError(parameter, error));
					}
				}
				throw ex;
			}
		}
		return args;
	}

參數、返回值解析

因為參數類型非常多,同時還會伴隨各種註解,如:@RequestBody、@RequestParam、@PathVariable等,所以參數解析的工作是非常繁雜的,同時還要考慮到擴展性,所以SpringMVC依然採用了策略模式來完成對各種參數類型的解析綁定,其頂層接口就是HandlerMethodArgumentResolver,而默認SpringMVC提供的解析方式就高達20多種:

上面是類圖,讀者可根據自己熟悉的參數類型找到對應的類進行分析,最核心的還是要掌握這裏的設計思想。
接着方法調用完成后就是對返回值的處理,同樣的,返回值類型也是非常多,也可以使用各種註解標註,所以也是使用策略模式實現,其頂層接口是HandlerMethodReturnValueHandler,實現類如下:

調用完成之後就是執行後續操作了:執行中置過濾器、處理全局異常、視圖渲染以及執行後置過濾器,這些與主流程沒有太大關係,本篇不展開分析了,最後是MVC的執行時序圖:

總結

本篇是Spring核心原理系列的最後一篇,前前後后花了一個月時間,終於從宏觀上大致上理解了Spring的實現原理和運行機制,明白了之前項目中一些坑是如何產生的,最主要的是學到設計模式的運用以及如何利用Spring的一些常用的擴展點進行自定義擴展。但對於Spring這個龐大的體系來說,還有很多是要去理解學習的,尤其是設計思想,只有長期琢磨才能深刻的理解掌握。在我之前的文章中包括本篇還有很多沒分析到的細節,在後面我會不定期分享出來。

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

【其他文章推薦】

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

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

※回頭車貨運收費標準

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

※超省錢租車方案

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