來勢兇猛 看T60如何演繹皮卡新時代

在傳統思維上,也許你覺得智能化和皮卡根本毫無瓜葛。T60打破了這種陳建,首創在皮卡上開發了Normal/Eco/power三種駕駛模式。在輕載或行駛在平坦路面時,選擇ECO模式,以達到更好的燃油經濟性。在山路、工地等複雜路況或重載時,選擇power模式,帶來更強勁的動力。

在中國,皮卡一直以邊緣車型處在一個尷尬境地,各類歧視性政策更是讓實用價值很高的皮卡被人們敬而遠之,和國外濃厚的皮卡文化相比,我們仍然處於一個嬰兒般的市場狀態,如何培育,如何增長,如何推廣,都成了皮卡的廠商一個大難題,而且中國皮卡造型多以中庸的外觀出現,講究皮實耐用,與國內80后、90后追求外觀、個性的性格有些格格不入,所以,皮卡車企如何適應當前社會潮流,推出適合當下年輕人喜歡的中高端皮卡,將是刺激當前皮卡市場的一劑強心劑,也是一個對於廠商來說一個最大的突破口。

縱觀國內皮卡市場,雖然每年有着40萬輛左右的銷量,但產品的各方面整體較落後,市場上缺少一款性能卓越,讓用戶滿意的產品。在此背景下,上汽T60的誕生就是為了引領並推動國內皮卡市場。

12月21日,上汽大通邀經銷商夥伴聚集海南博鰲,回顧輝煌業績,共贏未來。回望2016年,在今年的廣州車展上,上汽大通推出了“划時代皮卡”——T60,當時只是公布了售價區間為9.98-19.98萬元。

與會期間,上汽大通為大家揭開了最後的謎底,官方公布T60各款車型的價格

T60柴油兩驅低底盤價格表

T60柴油兩驅高底盤價格表

T60柴油四驅高底盤價格表

突破傳統,時尚觀感遇見智能互聯

T60在外觀造型上有了重大突破,在我們之前的印象中,皮卡一向講求“內修”只要實用,空間大,力量足就行,但是T60確實是做到了“內外兼修”摒棄此前皮卡車過於單調、落後的特點,用迎合年輕一代用車群體的需求作為設計出發點,這款T60皮卡有着眾多獨特個性與亮點,稜角分明的前臉造型線條十分豐富,直瀑式進氣格柵突出了皮卡車型硬朗風格的同時也融入現代SUV的時尚元素。多邊形前大燈組造型,近光燈帶有透鏡結構,保險杠底部的霧燈則顯得十分秀氣。直觀的感受,T60帶來一款讓人耳目一新的皮卡。

在內飾設計上,T60整體造型顯得十分簡潔,全車黑色搭配銀色鍍鉻飾條,T形對稱式中控檯布局,大尺寸的液晶显示屏搭載了上汽與阿里共同開發的YunOS車聯網系統,提供智能車管家、在線互聯、遠程控制、語音識別等智能功能,這些科技的配置完全不輸一台B級車的配置了,而且皮卡上配備了pEpS無鑰匙進入一鍵啟動功能,這樣的皮卡配置確實有些無敵了。

作為一款皮卡,動力方面是其必須具備的,畢竟它的主要角色擔當是裝載及運輸,所以對於動力的要求會很高,那T60搭載了來自意大利VM的2.8T柴油發動機,其最大功率為110千瓦,最大扭矩360牛米,這樣的動力是足夠消費者輕鬆應對各種繁重任務了。

值得一提的,T60首次在皮卡上配備了6AT自動變速箱,這讓其在彎道或變速方面更加應付自如。

在傳統思維上,也許你覺得智能化和皮卡根本毫無瓜葛。T60打破了這種陳建,首創在皮卡上開發了Normal/Eco/power三種駕駛模式。在輕載或行駛在平坦路面時,選擇ECO模式,以達到更好的燃油經濟性;在山路、工地等複雜路況或重載時,選擇power模式,帶來更強勁的動力。這種改變,大大滿足了越來越多的年輕用戶希望有更輕鬆的駕駛體驗的需求。

最後,不得不提是T60在安全配置方面的專家級表現,T60以澳洲A-NCAp五星碰撞安全標準進行設計,首次將熱成型技術運用於皮卡,並通過激光焊接,大幅提升車身強度;T60還率先採用6安全氣囊,為駕乘人員提供全方位的保護。

T60首次在皮卡上配備,AFS隨動轉向LED大燈,根據車輛轉向,自動調節大燈照射角度,為用戶夜晚行車,提供更寬闊的照明視野。

在面對雨雪泥濘、崎嶇山路行駛時出現甩尾、側滑等險情時, T60採用BOSCH 9.0,ESp車身穩定控制系統,給皮卡用戶在各種工況下,帶來更好的行車穩定性。並把360環視影像、LDW車道偏離預警、疲勞駕駛提醒,等多項安全配置,首次應用在皮卡上,提供全方位的安全保障。

在美國,皮卡代表了張揚、自由與個性,涵蓋了美國英雄主義的西部文化,駕駛皮卡犹如之前的騎馬一樣輕鬆自然,而在中國恰恰缺少美國一樣的皮卡文化。

隨着皮卡市場的春天來臨,大家對皮卡的關注與討論與日俱增。同時,上汽大通T60更是憑藉行業首創的C2B理念,提供最全面的產品型譜和組合,上汽大通T60作為一輛外觀時尚、性能卓越、注重安全的皮卡,將會引領皮卡市場走向宜商宜家的高端路線。這樣一款划時代的皮卡,讓我們更加期待其精彩的市場表現。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

※超省錢租車方案

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

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

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

車輪上的家 上汽大通發布全新房車RV80

98萬的價格。而且RV80型譜包含B型和C型房車,擁有短軸/長軸/加長軸,低頂/中頂/高頂多種車身形式,用戶可通過C2B線上交互平台,可直接在線訂製房車,OTD高效透明,消費者可參与試駕、試住等全體驗式互動,享受真正的價值和樂趣。

買一輛房車,開到郊外,給自己一個遠離城市堵車和污染的樂土,傍晚一邊欣賞湖邊那一抹落日一邊和家人烹制一頓可口的晚餐;晚上透過大大的天窗看沒有污染的星空;亦或把車停在海邊,換上泳衣下海衝浪,回來后在寬敞的洗澡間沖個熱水澡這份嚮往其實不僅僅是一輛車,而是一種生活方式更是一種房車生活的文化,這才是一種說走就走的旅行,因為不用規劃路線,不用訂酒店,風景在那,走到那,家在路上。

近日,一場融新款房車上市發布、房車露營大會、草地音樂狂歡節、美酒佳肴於一體的“全體驗式”上市發布會登陸海南博鰲,由“中國房車第一品牌”上汽大通打造的2016“新旅程·心享受”房車之夜暨房車新品上市發布會亮相南海之濱,全新房車RV80在萬眾期待中揭開神秘面紗。

當晚,配合現場炫目的燈光秀,國內首款滿足國五排放標準的高端房車——上汽大通全新房車RV80正式發布。

新款房車憑藉六大產品優勢,定義房車行業新標杆,其搭載源自歐洲的VM柴油發動機,最高功率110KW,綜合油耗百公里僅為8.3L;全系國5排放標準;在國內房車行業中率先使用AMT技術;而且全車座椅是達乘用級安全標準。

消費者最為關心的價格也在上市會上正式揭曉,售:26.98萬至51.98萬的價格。

而且RV80型譜包含B型和C型房車,擁有短軸/長軸/加長軸,低頂/中頂/高頂多種車身形式,用戶可通過C2B線上交互平台,可直接在線訂製房車,OTD高效透明,消費者可參与試駕、試住等全體驗式互動,享受真正的價值和樂趣。

雖然國內房車起步晚,房車少、營地少,在心往來說,移動露營來說比較難實現,但隨着房車市場近幾年的發展,房車露營地的逐漸增多,露營地的規劃與擬建逐漸完善,移動露營的實現,在今天已經變的越來越便利了。

房車,又稱“車輪上的家”,兼具“房”與“車”兩大功能,但其屬性還是車,相比普通汽車,房車上的居家設施有爐具、冰箱、櫥櫃、沙發、餐桌椅、盥洗設施、空調、電視、音響等傢具和電器等,隨心而行,隨景而歇,這是一種房車的生活狀態。

購買房車的理由可以很多,可能是為了帶着父母去旅行,也可能是為了滿足夫妻雙方共同的旅行夢想,也可以一群朋友同事用懷揣探索的心踏上未知的旅途,無論是兩個人駕駛房車出行,要過一次久違的二人世界或帶着一家人遠離都市的喧鬧,這都是我們嚮往的生活,而房車是對於這種嚮往的一個載體,一個最適合的載體。

國內消費者對待房車的消費觀念正在發生轉變,讓目前的房車市場顯得更為火爆,現在很多人的旅遊觀念都在轉變,更加傾向於自駕出遊,而非報團旅行。而自駕的長途旅行,使得房車成為了旅行者的首選。

但相應的配套設施卻略顯單薄,最大的制約就是目前國內房車露營地較少,無法為房車提供停放場所。畢竟在房車內生活,做飯、取暖、照明等都需要電力和水源,雖然房車裡自帶有蓄電池和水源儲蓄,但容量的限制最多只能保證一兩個晚上的生活需求,這也是制約房車發展最重要的一個條件。

對於現在房車的各方面使用性來說,可能有些人覺的買了回來,使用率比較低,上汽大通也在2017年投入超過1200輛房車,試水房車租賃市場,與運營夥伴在全國鋪開租賃網絡。

讓用戶輕鬆體驗到大通房車落地自駕的樂趣;開發了“房車生活家”平台,為房車用戶和潛客用戶提供“@房車”、“@生活”、“@家”三個方面的服務,構建多個使用和體驗場景,實現在線房車自駕游線路推薦和預訂、營地推薦和預訂,閱讀攻略遊記、預約試駕體驗、參加自駕活動等功能。

也可掃二維碼了解更多房車生活及房車租賃信息,而且還有很多優惠信息喔!

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

【其他文章推薦】

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

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

※回頭車貨運收費標準

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

※超省錢租車方案

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

別唧唧歪歪了!年輕人第一台SUV就該選TA!

但相信2765mm的軸距將會給用戶一個滿意的乘坐空間。後排無論在橫向還是縱向空間都是足夠大而舒適,頭部空間則因車頂後排頭頂位置為內凹設計,也是很讓人滿意。“低”價格,高配置啟辰T90的內飾給人的感覺跟外觀還是有些不一樣,整體的風格偏向於穩重,但穩重中也沒有忽略運動感的塑造。

在目前的國內市場,SUV尤其的火爆,可以這麼說,當人們有需求的時候,市場就會自動做出調整生產符合消費者要求的產品。在這種情況下,很多廠家也紛紛都將矛頭指向了緊湊型的SUV,以及二胎政策捧紅的7座中型SUV。然而,啟辰獨辟蹊徑,瞄準了85后消費群體,打造了溜背式的中型SUV啟辰T90,憑藉媲美合資車的生產品質、顛覆傳統的驚艷設計、充滿競爭力的價格,勢必成為中型SUV現有市場格局的全新標杆。

12月25日,啟辰全新中型SUV啟辰T90正式上市,售價區間為10.98—15.48萬元,共推出6款車型。

個性時尚的外觀

啟辰T90最大的亮點可以說是外觀造型,無論是前臉還是性感的“背影”,開這麼一款SUV在路上,回頭率都會是極高的。其採用了全新的家族式前臉和溜背式SUV的風雕美學設計。不是華麗的簡單堆砌,而是經過設計師無數次對細節完美追求的結果。總體來看,不管是在研發設計還是產品定位上,啟辰T90的設計感並不亞於一些合資品牌。

寬敞舒適的空間

說到這裏,也許有人會問,採用溜背造型的設計,其後排空間會不會就因此受到影響,要說完全沒有,這個有點不現實。但相信2765mm的軸距將會給用戶一個滿意的乘坐空間。後排無論在橫向還是縱向空間都是足夠大而舒適,頭部空間則因車頂後排頭頂位置為內凹設計,也是很讓人滿意。

“低”價格,高配置

啟辰T90的內飾給人的感覺跟外觀還是有些不一樣,整體的風格偏向於穩重,但穩重中也沒有忽略運動感的塑造。值得一提的還有,中控台上12.3英寸的大屏,從布局上就可以看得出設計的用心,其呈環抱式位置分佈,主駕駛席無需起身即可完成所有操作;真皮座椅,搭配着藍色的氛圍燈,精密的雙縫線,各處接縫的高度契合,使內飾整體更有質感,是現代年輕一代受眾所青睞的。

除了以上提到的,還有智能互聯、高科技配置和安全配置等方面,啟辰T90也展示出非凡的實力,可以說是智超所值。IT-COMMANDER旋鈕式信息控制系統堪稱同級SUV罕有的“鼠標”式車載控制系統,集便利性、簡易性、精美性於一體,觸控隨心所欲;全彩3D平視信息显示系統以駕駛者為中心,操控簡潔。啟辰T90還具備駕駛提醒、胎壓監測、泊車輔助等功能,在白色高亮質感照明和多功能方向盤按鍵控制下,能夠帶來更清晰、更科技、更好操作的显示效果。

總結:如今中型SUV市場,早已是一片紅海,啟辰T90無論是獨樹一幟的外觀,還是空間、配置,以及親民的價格,都可以說,將會讓它在這片紅海中,逆襲上位、脫穎而出,成為業內的一個新標杆。期待在未來,啟辰T90成為下一个中型SUV爆款。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※超省錢租車方案

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

※回頭車貨運收費標準

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

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

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

不只Apple碳中和全球頂尖企業都靠這個建立市場區隔

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

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

【其他文章推薦】

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

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

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

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

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

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

川普將大砍「犀牛和老虎保育基金」 全球不到100隻蘇門答臘犀牛告急

環境資訊中心綜合外電;黃鈺婷 翻譯;林大利 審校;稿源:Mongabay

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

【其他文章推薦】

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

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

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

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

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

※超省錢租車方案

小師妹學JavaIO之:NIO中Channel的妙用

目錄

  • 簡介
  • Channel的分類
    • FileChannel
    • Selector和Channel
    • DatagramChannel
    • SocketChannel
    • ServerSocketChannel
    • AsynchronousSocketChannel
  • 使用Channel
  • 總結

簡介

小師妹,你還記得我們使用IO和NIO的初心嗎?

小師妹:F師兄,使用IO和NIO不就是為了讓生活更美好,世界充滿愛嗎?讓我等程序員可以優雅的將數據從一個地方搬運到另外一個地方。利其器,善其事,才有更多的時間去享受生活呀。

善,如果將數據比做人,IO,NIO的目的就是把人運到美國。

小師妹:F師兄,為什麼要運到美國呀,美國現在新冠太嚴重了,還是待在中國吧。中國是世界上最安全的國家!

好吧,為了保險起見,我們要把人運到上海。人就是數據,怎麼運過去呢?可以坐飛機,坐汽車,坐火車,這些什麼飛機,汽車,火車就可以看做是一個一個的Buffer。

最後飛機的航線,汽車的公路和火車的軌道就可以看做是一個個的channel。

更多精彩內容且看:

  • 區塊鏈從入門到放棄系列教程-涵蓋密碼學,超級賬本,以太坊,Libra,比特幣等持續更新
  • Spring Boot 2.X系列教程:七天從無到有掌握Spring Boot-持續更新
  • Spring 5.X系列教程:滿足你對Spring5的一切想象-持續更新
  • java程序員從小工到專家成神之路(2020版)-持續更新中,附詳細文章教程

簡單點講,channel就是負責運送Buffer的通道。

IO按源頭來分,可以分為兩種,從文件來的File IO,從Stream來的Stream IO。不管哪種IO,都可以通過channel來運送數據。

Channel的分類

雖然數據的來源只有兩種,但是JDK中Channel的分類可不少,如下圖所示:

先來看看最基本的,也是最頂層的接口Channel:

public interface Channel extends Closeable {
    public boolean isOpen();
    public void close() throws IOException;

}

最頂層的Channel很簡單,繼承了Closeable接口,需要實現兩個方法isOpen和close。

一個用來判斷channel是否打開,一個用來關閉channel。

小師妹:F師兄,頂層的Channel怎麼這麼簡單,完全不符合Channel很複雜的人設啊。

別急,JDK這麼做其實也是有道理的,因為是頂層的接口,必須要更加抽象更加通用,結果,一通用就發現還真的就只有這麼兩個方法是通用的。

所以為了應對這個問題,Channel中定義了很多種不同的類型。

最最底層的Channel有5大類型,分別是:

FileChannel

這5大channel中,和文件File有關的就是這個FileChannel了。

FileChannel可以從RandomAccessFile, FileInputStream或者FileOutputStream中通過調用getChannel()來得到。

也可以直接調用FileChannel中的open方法傳入Path創建。

public abstract class FileChannel
    extends AbstractInterruptibleChannel
    implements SeekableByteChannel, GatheringByteChannel, ScatteringByteChannel

我們看下FileChannel繼承或者實現的接口和類。

AbstractInterruptibleChannel實現了InterruptibleChannel接口,interrupt大家都知道吧,用來中斷線程執行的利器。來看一下下面一段非常玄妙的代碼:

protected final void begin() {
        if (interruptor == null) {
            interruptor = new Interruptible() {
                    public void interrupt(Thread target) {
                        synchronized (closeLock) {
                            if (closed)
                                return;
                            closed = true;
                            interrupted = target;
                            try {
                                AbstractInterruptibleChannel.this.implCloseChannel();
                            } catch (IOException x) { }
                        }
                    }};
        }
        blockedOn(interruptor);
        Thread me = Thread.currentThread();
        if (me.isInterrupted())
            interruptor.interrupt(me);
    }

上面這段代碼就是AbstractInterruptibleChannel的核心所在。

首先定義了一個Interruptible的實例,這個實例中有一個interrupt方法,用來關閉Channel。

然後獲得當前線程的實例,判斷當前線程是否Interrupted,如果是的話,就調用Interruptible的interrupt方法將當前channel關閉。

SeekableByteChannel用來連接Entry或者File。它有一個獨特的屬性叫做position,表示當前讀取的位置。可以被修改。

GatheringByteChannel和ScatteringByteChannel表示可以一次讀寫一個Buffer序列結合(Buffer Array):

public long write(ByteBuffer[] srcs, int offset, int length)
        throws IOException;
public long read(ByteBuffer[] dsts, int offset, int length)
        throws IOException;

Selector和Channel

在講其他幾個Channel之前,我們看一個和下面幾個channel相關的Selector:

這裏要介紹一個新的Channel類型叫做SelectableChannel,之前的FileChannel的連接是一對一的,也就是說一個channel要對應一個處理的線程。而SelectableChannel則是一對多的,也就是說一個處理線程可以通過Selector來對應處理多個channel。

SelectableChannel通過註冊不同的SelectionKey,實現對多個Channel的監聽。後面我們會具體的講解Selector的使用,敬請期待。

DatagramChannel

DatagramChannel是用來處理UDP的Channel。它自帶了Open方法來創建實例。

來看看DatagramChannel的定義:

public abstract class DatagramChannel
    extends AbstractSelectableChannel
    implements ByteChannel, ScatteringByteChannel, GatheringByteChannel, MulticastChannel

ByteChannel表示它同時是ReadableByteChannel也是WritableByteChannel,可以同時寫入和讀取。

MulticastChannel代表的是一種多播協議。正好和UDP對應。

SocketChannel

SocketChannel是用來處理TCP的channel。它也是通過Open方法來創建的。

public abstract class SocketChannel
    extends AbstractSelectableChannel
    implements ByteChannel, ScatteringByteChannel, GatheringByteChannel, NetworkChannel

SocketChannel跟DatagramChannel的唯一不同之處就是實現的是NetworkChannel借口。

NetworkChannel提供了一些network socket的操作,比如綁定地址等。

ServerSocketChannel

ServerSocketChannel也是一個NetworkChannel,它主要用在服務器端的監聽。

public abstract class ServerSocketChannel
    extends AbstractSelectableChannel
    implements NetworkChannel

AsynchronousSocketChannel

最後AsynchronousSocketChannel是一種異步的Channel:

public abstract class AsynchronousSocketChannel
    implements AsynchronousByteChannel, NetworkChannel

為什麼是異步呢?我們看一個方法:

public abstract Future<Integer> read(ByteBuffer dst);

可以看到返回值是一個Future,所以read方法可以立刻返回,只在我們需要的時候從Future中取值即可。

使用Channel

小師妹:F師兄,講了這麼多種類的Channel,看得我眼花繚亂,能不能講一個Channel的具體例子呢?

好的小師妹,我們現在講一個使用Channel進行文件拷貝的例子,雖然Channel提供了transferTo的方法可以非常簡單的進行拷貝,但是為了能夠看清楚Channel的通用使用,我們選擇一個更加常規的例子:

public void useChannelCopy() throws IOException {
        FileInputStream input = new FileInputStream ("src/main/resources/www.flydean.com");
        FileOutputStream output = new FileOutputStream ("src/main/resources/www.flydean.com.txt");
        try(ReadableByteChannel source = input.getChannel(); WritableByteChannel dest = output.getChannel()){
            ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
            while (source.read(buffer) != -1)
            {
                // flip buffer,準備寫入
                buffer.flip();
                // 查看是否有更多的內容
                while (buffer.hasRemaining())
                {
                    dest.write(buffer);
                }
                // clear buffer,供下一次使用
                buffer.clear();
            }
        }
    }

上面的例子中我們從InputStream中讀取Buffer,然後寫入到FileOutputStream。

總結

今天講解了Channel的具體分類,和一個簡單的例子,後面我們會再體驗一下Channel的其他例子,敬請期待。

本文的例子https://github.com/ddean2009/learn-java-io-nio

本文作者:flydean程序那些事

本文鏈接:http://www.flydean.com/java-io-nio-channel/

本文來源:flydean的博客

歡迎關注我的公眾號:程序那些事,更多精彩等着您!

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

【其他文章推薦】

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

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

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

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

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

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

數據源管理 | Kafka集群環境搭建,消息存儲機制詳解

本文源碼:GitHub·點這裏 || GitEE·點這裏

一、Kafka集群環境

1、環境版本

版本:kafka2.11,zookeeper3.4

注意:這裏zookeeper3.4也是基於集群模式部署。

2、解壓重命名

tar -zxvf kafka_2.11-0.11.0.0.tgz
mv kafka_2.11-0.11.0.0 kafka2.11

創建日誌目錄

[root@en-master kafka2.11]# mkdir logs

注意:以上操作需要同步到集群下其他服務上。

3、添加環境變量

vim /etc/profile
export KAFKA_HOME=/opt/kafka2.11
export PATH=$PATH:$KAFKA_HOME/bin
source /etc/profile

4、修改核心配置

[root@en-master /opt/kafka2.11/config]# vim server.properties
-- 核心修改如下
# 唯一編號
broker.id=0
# 開啟topic刪除
delete.topic.enable=true
# 日誌地址
log.dirs=/opt/kafka2.11/logs
# zk集群
zookeeper.connect=zk01:2181,zk02:2181,zk03:2181

注意:broker.id安裝集群服務個數編排即可,集群下不能重複。

5、啟動kafka集群

# 啟動命令
[root@node02 kafka2.11]# bin/kafka-server-start.sh -daemon config/server.properties
# 停止命令
[root@node02 kafka2.11]# bin/kafka-server-stop.sh
# 進程查看
[root@node02 kafka2.11]# jps

注意:這裏默認啟動了zookeeper集群服務,並且集群下的kafka分別啟動。

6、基礎管理命令

創建topic

bin/kafka-topics.sh --zookeeper zk01:2181 \
--create --replication-factor 3 --partitions 1 --topic one-topic

參數說明:

  • replication-factor 定義副本個數
  • partitions 定義分區個數
  • topic:定義topic名稱

查看topic列表

bin/kafka-topics.sh --zookeeper zk01:2181 --list

修改topic分區

bin/kafka-topics.sh --zookeeper zk01:2181 --alter --topic one-topic --partitions 5

查看topic

bin/kafka-topics.sh --zookeeper zk01:2181 \
--describe --topic one-topic

發送消息

bin/kafka-console-producer.sh \
--broker-list 192.168.72.133:9092 --topic one-topic

消費消息

bin/kafka-console-consumer.sh \
--bootstrap-server 192.168.72.133:9092 --from-beginning --topic one-topic

刪除topic

bin/kafka-topics.sh --zookeeper zk01:2181 \
--delete --topic first

7、Zk集群用處

Kafka集群中有一個broker會被選舉為Controller,Controller依賴Zookeeper環境,管理集群broker的上下線,所有topic的分區副本分配和leader選舉等工作。

二、消息攔截案例

1、攔截器簡介

Kafka中間件的Producer攔截器主要用於實現消息發送的自定義控制邏輯。用戶可以在消息發送前以及回調邏輯執行前有機會對消息做一些自定義,比如消息修改等,發送狀態監控等,用戶可以指定多個攔截器按順序執行攔截。

核心方法

  • configure:獲取配置信息和初始化數據時調用;
  • onSend:消息被序列化以及和計算分區前調用該方法,可以對消息做操作;
  • onAcknowledgement:消息發送到Broker之後,或發送過程失敗時調用;
  • close:關閉攔截器調用,執行一些資源清理工作;

注意:這裏說的攔截器是針對消息發送流程。

2、自定義攔截

定義方式:實現ProducerInterceptor接口即可。

攔截器一:在onSend方法中,對攔截的消息進行修改。

@Component
public class SendStartInterceptor implements ProducerInterceptor<String, String> {

    private final Logger LOGGER = LoggerFactory.getLogger("SendStartInterceptor");
    @Override
    public void configure(Map<String, ?> configs) {
        LOGGER.info("configs...");
    }
    @Override
    public ProducerRecord<String, String> onSend(ProducerRecord<String, String> record) {
        // 修改消息內容
        return new ProducerRecord<>(record.topic(), record.partition(),
                                    record.timestamp(), record.key(),
                              "onSend:{" + record.value()+"}");
    }
    @Override
    public void onAcknowledgement(RecordMetadata metadata, Exception exception) {
        LOGGER.info("onAcknowledgement...");
    }
    @Override
    public void close() {
        LOGGER.info("SendStart close...");
    }
}

攔截器二:在onAcknowledgement方法中,判斷消息是否發送成功。

@Component
public class SendOverInterceptor implements ProducerInterceptor<String, String> {

    private final Logger LOGGER = LoggerFactory.getLogger("SendOverInterceptor");
    @Override
    public void configure(Map<String, ?> configs) {
        LOGGER.info("configs...");
    }

    @Override
    public ProducerRecord<String, String> onSend(ProducerRecord<String, String> record) {
        LOGGER.info("record...{}", record.value());
        return record ;
    }

    @Override
    public void onAcknowledgement(RecordMetadata metadata, Exception exception) {
        if (exception != null){
            LOGGER.info("Send Fail...exe-msg",exception.getMessage());
        }
        LOGGER.info("Send success...");
    }

    @Override
    public void close() {
        LOGGER.info("SendOver close...");
    }
}

加載攔截器:基於一個KafkaProducer配置Bean,加入攔截器。

@Configuration
public class KafkaConfig {

    @Bean
    public Producer producer (){
        Properties props = new Properties();
        // 省略其他配置...
        // 添加攔截器
        List<String> interceptors = new ArrayList<>();
        interceptors.add("com.kafka.cluster.interceptor.SendStartInterceptor");
        interceptors.add("com.kafka.cluster.interceptor.SendOverInterceptor");
        props.put(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG, interceptors);
        return new KafkaProducer<>(props) ;
    }
}

3、代碼案例

@RestController
public class SendMsgWeb {
    @Resource
    private KafkaProducer<String,String> producer ;
    @GetMapping("/sendMsg")
    public String sendMsg (){
        producer.send(new ProducerRecord<>("one-topic", "msgKey", "msgValue"));
        return "success" ;
    }
}

基於上述自定義Bean類型,進行消息發送,關注攔截器中打印日誌信息。

三、Kafka存儲分析

說明:該過程基於上述案例producer.send方法追蹤的源碼執行流程,源碼中的過程相對清楚,涉及的核心流程如下。

1、消息生成過程

Producer發送消息採用的是異步發送的方式,消息發送過程如下:

  • Producer發送消息之後,經過攔截器,序列化,事務判斷;
  • 流程執行后,消息內容放入容器中;
  • 容器在指定時間內如果裝滿(size),會喚醒Sender線程;
  • 容器如果在指定時間內沒有裝滿,也會執行一次Sender線程喚醒;
  • 喚醒Sender線程之後,把容器數據拉取到topic中;

絮叨一句:讀這些中間件的源碼,不僅能開闊思維,也會讓自己意識到平時寫的代碼可能真的叫搬磚。

2、存儲機制

Kafka中消息是以topic進行標識分類,生產者面向topic生產消息,topic分區(partition)是物理上的存儲,基於消息日誌文件的方式。

  • 每個partition對應於一個log文件,發送的消息不斷追加到該log文件末端;
  • log文件中存儲的就是producer生產的消息數據,採用分片和索引機制;
  • partition分為多個segment。每個segment對應兩個(.index)和(.log)文件;
  • index文件類型存儲的索引信息;
  • log文件存儲消息的數據;
  • 索引文件中的元數據指向對應數據文件中message的物理偏移地址;
  • 消費者組中的每個消費者,都會實時記錄消費的消息offset位置;
  • 當然消息消費出錯時,恢復是從上次的記錄位置繼續消費;

3、事務控制機制

Kafka支持消息的事務控制

Producer事務

跨分區跨會話的事務原理,引入全局唯一的TransactionID,並將Producer獲得的PID和TransactionID綁定。Producer重啟后可以通過正在進行的TransactionID獲得原來的PID。
Kafka基於TransactionCoordinator組件管理Transaction,Producer通過和TransactionCoordinator交互獲得TransactionID對應的任務狀態。TransactionCoordinator將事務狀態寫入Kafka的內部Topic,即使整個服務重啟,進行中的事務狀態可以得到恢復。

Consumer事務

Consumer消息消費,事務的保證強度很低,無法保證消息被精確消費,因為同一事務的消息可能會出現重啟后已經被刪除的情況。

四、源代碼地址

GitHub·地址
https://github.com/cicadasmile/data-manage-parent
GitEE·地址
https://gitee.com/cicadasmile/data-manage-parent

推薦關聯閱讀:數據源管理系列

序號 標題
01 數據源管理:主從庫動態路由,AOP模式讀寫分離
02 數據源管理:基於JDBC模式,適配和管理動態數據源
03 數據源管理:動態權限校驗,表結構和數據遷移流程
04 數據源管理:關係型分庫分表,列式庫分佈式計算
05 數據源管理:PostGreSQL環境整合,JSON類型應用
06 數據源管理:基於DataX組件,同步數據和源碼分析
07 數據源管理:OLAP查詢引擎,ClickHouse集群化管理

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

【其他文章推薦】

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

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

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

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

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

※超省錢租車方案

實時標籤開發——從零開始搭建實時用戶畫像(五)

數據接入

數據的接入可以通過將數據實時寫入Kafka進行接入,不管是直接的寫入還是通過oracle和mysql的實時接入方式,比如oracle的ogg,mysql的binlog

ogg

Golden Gate(簡稱OGG)提供異構環境下交易數據的實時捕捉、變換、投遞。

通過OGG可以實時的將oracle中的數據寫入Kafka中。

對生產系統影響小:實時讀取交易日誌,以低資源佔用實現大交易量數據實時複製

以交易為單位複製,保證交易一致性:只同步已提交的數據

高性能

  • 智能的交易重組和操作合併
  • 使用數據庫本地接口訪問
  • 并行處理體系

binlog

MySQL 的二進制日誌 binlog 可以說是 MySQL 最重要的日誌,它記錄了所有的 DDLDML 語句(除了數據查詢語句select、show等),以事件形式記錄,還包含語句所執行的消耗的時間,MySQL的二進制日誌是事務安全型的。binlog 的主要目的是複製和恢復

通過這些手段,可以將數據同步到kafka也就是我們的實時系統中來。

Flink接入Kafka數據

Apache Kafka Connector可以方便對kafka數據的接入。

依賴

<dependency>  <groupId>org.apache.flink</groupId>  <artifactId>flink-connector-kafka_2.11</artifactId>  <version>1.9.0</version></dependency>
構建FlinkKafkaConsumer

必須有的:

1.topic名稱

2.用於反序列化Kafka數據的DeserializationSchema / KafkaDeserializationSchema

3.配置參數:“bootstrap.servers” “group.id” (kafka0.8還需要 “zookeeper.connect”)

val properties = new Properties()properties.setProperty("bootstrap.servers", "localhost:9092")// only required for Kafka 0.8properties.setProperty("zookeeper.connect", "localhost:2181")properties.setProperty("group.id", "test")stream = env    .addSource(new FlinkKafkaConsumer[String]("topic", new SimpleStringSchema(), properties))    .print()
時間戳和水印

在許多情況下,記錄的時間戳(顯式或隱式)嵌入記錄本身。另外,用戶可能想要周期性地或以不規則的方式發出水印。

我們可以定義好Timestamp Extractors / Watermark Emitters,通過以下方式將其傳遞給消費者

val env = StreamExecutionEnvironment.getExecutionEnvironment()val myConsumer = new FlinkKafkaConsumer[String](...)myConsumer.setStartFromEarliest()      // start from the earliest record possiblemyConsumer.setStartFromLatest()        // start from the latest recordmyConsumer.setStartFromTimestamp(...)  // start from specified epoch timestamp (milliseconds)myConsumer.setStartFromGroupOffsets()  // the default behaviour//指定位置//val specificStartOffsets = new java.util.HashMap[KafkaTopicPartition, java.lang.Long]()//specificStartOffsets.put(new KafkaTopicPartition("myTopic", 0), 23L)//myConsumer.setStartFromSpecificOffsets(specificStartOffsets)val stream = env.addSource(myConsumer)
檢查點

啟用Flink的檢查點后,Flink Kafka Consumer將使用主題中的記錄,並以一致的方式定期檢查其所有Kafka偏移以及其他操作的狀態。如果作業失敗,Flink會將流式程序恢復到最新檢查點的狀態,並從存儲在檢查點中的偏移量開始重新使用Kafka的記錄。

如果禁用了檢查點,則Flink Kafka Consumer依賴於內部使用的Kafka客戶端的自動定期偏移提交功能。

如果啟用了檢查點,則Flink Kafka Consumer將在檢查點完成時提交存儲在檢查點狀態中的偏移量。

val env = StreamExecutionEnvironment.getExecutionEnvironment()env.enableCheckpointing(5000) // checkpoint every 5000 msecs

Flink消費Kafka完整代碼:

import org.apache.flink.api.common.serialization.SimpleStringSchema;import org.apache.flink.streaming.api.datastream.DataStream;import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;import org.apache.flink.streaming.connectors.kafka.FlinkKafkaConsumer;import java.util.Properties;public class KafkaConsumer {    public static void main(String[] args) throws Exception {        final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();        Properties properties = new Properties();        properties.setProperty("bootstrap.servers", "localhost:9092");        properties.setProperty("group.id", "test");        //構建FlinkKafkaConsumer        FlinkKafkaConsumer<String> myConsumer = new FlinkKafkaConsumer<>("topic", new SimpleStringSchema(), properties);        //指定偏移量        myConsumer.setStartFromEarliest();        DataStream<String> stream = env                .addSource(myConsumer);        env.enableCheckpointing(5000);        stream.print();        env.execute("Flink Streaming Java API Skeleton");    }

這樣數據已經實時的接入我們系統中,可以在Flink中對數據進行處理了,那麼如何對標籤進行計算呢? 標籤的計算過程極大的依賴於數據倉庫的能力,所以擁有了一個好的數據倉庫,標籤也就很容易計算出來了。

數據倉庫基礎知識

數據倉庫是指一個面向主題的、集成的、穩定的、隨時間變化的數據的集合,以用於支持管理決策的過程。

(1)面向主題
業務數據庫中的數據主要針對事物處理任務,各個業務系統之間是各自分離的。而數據倉庫中的數據是按照一定的主題進行組織的

(2)集成
數據倉庫中存儲的數據是從業務數據庫中提取出來的,但並不是原有數據的簡單複製,而是經過了抽取、清理、轉換(ETL)等工作。
業務數據庫記錄的是每一項業務處理的流水賬,這些數據不適合於分析處理,進入數據倉庫之前需要經過系列計算,同時拋棄一些分析處理不需要的數據。

(3)穩定
操作型數據庫系統中一般只存儲短期數據,因此其數據是不穩定的,記錄的是系統中數據變化的瞬態。
數據倉庫中的數據大多表示過去某一時刻的數據,主要用於查詢、分析,不像業務系統中數據庫一樣經常修改。一般數據倉庫構建完成,主要用於訪問

OLTP 聯機事務處理
OLTP是傳統關係型數據庫的主要應用,主要用於日常事物、交易系統的處理
1、數據量存儲相對來說不大
2、實時性要求高,需要支持事物
3、數據一般存儲在關係型數據庫(oracle或mysql中)

OLAP 聯機分析處理
OLAP是數據倉庫的主要應用,支持複雜的分析查詢,側重決策支持
1、實時性要求不是很高,ETL一般都是T+1的數據;
2、數據量很大;
3、主要用於分析決策;

星形模型是最常用的數據倉庫設計結構。由一個事實表和一組維表組成,每個維表都有一個維主鍵。
該模式核心是事實表,通過事實表將各種不同的維表連接起來,各個維表中的對象通過事實表與另一個維表中的對象相關聯,這樣建立各個維表對象之間的聯繫
維表:用於存放維度信息,包括維的屬性和層次結構;
事實表:是用來記錄業務事實並做相應指標統計的表。同維表相比,事實表記錄數量很多

雪花模型是對星形模型的擴展,每一個維表都可以向外連接多個詳細類別表。除了具有星形模式中維表的功能外,還連接對事實表進行詳細描述的維度,可進一步細化查看數據的粒度
例如:地點維表包含屬性集{location_id,街道,城市,省,國家}。這種模式通過地點維度表的city_id與城市維度表的city_id相關聯,得到如{101,“解放大道10號”,“武漢”,“湖北省”,“中國”}、{255,“解放大道85號”,“武漢”,“湖北省”,“中國”}這樣的記錄。
星形模型是最基本的模式,一個星形模型有多個維表,只存在一個事實表。在星形模式的基礎上,用多個表來描述一個複雜維,構造維表的多層結構,就得到雪花模型

清晰數據結構:每一個數據分層都有它的作用域,這樣我們在使用表的時候能更方便地定位和理解
臟數據清洗:屏蔽原始數據的異常
屏蔽業務影響:不必改一次業務就需要重新接入數據
數據血緣追蹤:簡單來講可以這樣理解,我們最終給業務呈現的是能直接使用的一張業務表,但是它的來源有很多,如果有一張來源表出問題了,我們希望能夠快速準確地定位到問題,並清楚它的危害範圍。
減少重複開發:規範數據分層,開發一些通用的中間層數據,能夠減少極大的重複計算。
把複雜問題簡單化。將一個複雜的任務分解成多個步驟來完成,每一層只處理單一的步驟,比較簡單和容易理解。便於維護數據的準確性,當數據出現問題之後,可以不用修復所有的數據,只需要從有問題的步驟開始修復。

數據倉庫的數據直接對接OLAP或日誌類數據,
用戶畫像只是站在用戶的角度,對數據倉庫數據做進一步的建模加工。因此每天畫像標籤相關數據的調度依賴上游數據倉庫相關任務執行完成。

在了解了數據倉庫以後,我們就可以進行標籤的計算了。在開發好標籤的邏輯以後,將數據寫入hive和druid中,完成實時與離線的標籤開發工作。

Flink Hive Druid

flink hive

Flink從1.9開始支持集成Hive,不過1.9版本為beta版,不推薦在生產環境中使用。在最新版Flink1.10版本,標志著對 Blink的整合宣告完成,隨着對 Hive 的生產級別集成,Hive作為數據倉庫系統的絕對核心,承擔著絕大多數的離線數據ETL計算和數據管理,期待Flink未來對Hive的完美支持。

而 HiveCatalog 會與一個 Hive Metastore 的實例連接,提供元數據持久化的能力。要使用 Flink 與 Hive 進行交互,用戶需要配置一個 HiveCatalog,並通過 HiveCatalog 訪問 Hive 中的元數據。

添加依賴

要與Hive集成,需要在Flink的lib目錄下添加額外的依賴jar包,以使集成在Table API程序或SQL Client中的SQL中起作用。或者,可以將這些依賴項放在文件夾中,並分別使用Table API程序或SQL Client 的-C-l選項將它們添加到classpath中。本文使用第一種方式,即將jar包直接複製到$FLINK_HOME/lib目錄下。本文使用的Hive版本為2.3.4(對於不同版本的Hive,可以參照官網選擇不同的jar包依賴),總共需要3個jar包,如下:

  • flink-connector-hive_2.11-1.10.0.jar
  • flink-shaded-hadoop-2-uber-2.7.5-8.0.jar
  • hive-exec-2.3.4.jar

添加Maven依賴

<!-- Flink Dependency -->
<dependency>
  <groupId>org.apache.flink</groupId>
  <artifactId>flink-connector-hive_2.11</artifactId>
  <version>1.10.0</version>
  <scope>provided</scope>
</dependency>

<dependency>
  <groupId>org.apache.flink</groupId>
  <artifactId>flink-table-api-java-bridge_2.11</artifactId>
  <version>1.10.0</version>
  <scope>provided</scope>
</dependency>

<!-- Hive Dependency -->
<dependency>
    <groupId>org.apache.hive</groupId>
    <artifactId>hive-exec</artifactId>
    <version>${hive.version}</version>
    <scope>provided</scope>
</dependency>

實例代碼

package com.flink.sql.hiveintegration;

import org.apache.flink.table.api.EnvironmentSettings;
import org.apache.flink.table.api.TableEnvironment;
import org.apache.flink.table.catalog.hive.HiveCatalog;


public class FlinkHiveIntegration {

    public static void main(String[] args) throws Exception {

        EnvironmentSettings settings = EnvironmentSettings
                .newInstance()
                .useBlinkPlanner() // 使用BlinkPlanner
                .inBatchMode() // Batch模式,默認為StreamingMode
                .build();

        //使用StreamingMode
       /* EnvironmentSettings settings = EnvironmentSettings
                .newInstance()
                .useBlinkPlanner() // 使用BlinkPlanner
                .inStreamingMode() // StreamingMode
                .build();*/

        TableEnvironment tableEnv = TableEnvironment.create(settings);

        String name = "myhive";      // Catalog名稱,定義一個唯一的名稱表示
        String defaultDatabase = "qfbap_ods";  // 默認數據庫名稱
        String hiveConfDir = "/opt/modules/apache-hive-2.3.4-bin/conf";  // hive-site.xml路徑
        String version = "2.3.4";       // Hive版本號

        HiveCatalog hive = new HiveCatalog(name, defaultDatabase, hiveConfDir, version);

        tableEnv.registerCatalog("myhive", hive);
        tableEnv.useCatalog("myhive");
        // 創建數據庫,目前不支持創建hive表
        String createDbSql = "CREATE DATABASE IF NOT EXISTS myhive.test123";

        tableEnv.sqlUpdate(createDbSql);  

    }
}

Flink Druid

可以將Flink分析好的數據寫回kafka,然後在druid中接入數據,也可以將數據直接寫入druid,以下為示例代碼:

依賴

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.8.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.flinkdruid</groupId>
    <artifactId>FlinkDruid</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>FlinkDruid</name>
    <description>Flink Druid Connection</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>

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

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

        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-core</artifactId>
            <version>1.9.0</version>
        </dependency>

        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-clients_2.12</artifactId>
            <version>1.9.0</version>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

示例代碼

@SpringBootApplication
public class FlinkDruidApp {


    private static String url = "http://localhost:8200/v1/post/wikipedia";

    private static RestTemplate template;

    private static HttpHeaders headers;


    FlinkDruidApp() {

        template = new RestTemplate();
        headers = new HttpHeaders();
        headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON));
        headers.setContentType(MediaType.APPLICATION_JSON);

    }

    public static void main(String[] args) throws Exception {

        SpringApplication.run(FlinkDruidApp.class, args);

        // Creating Flink Execution Environment
        ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment();

        //Define data source
        DataSet<String> data = env.readTextFile("/wikiticker-2015-09-12-sampled.json");

        // Trasformation on the data 
        data.map(x -> {

            return httpsPost(x).toString();
        }).print();


    }

    // http post method to post data in Druid
    private static ResponseEntity<String> httpsPost(String json) {

        HttpEntity<String> requestEntity =
                new HttpEntity<>(json, headers);
        ResponseEntity<String> response =
                template.exchange("http://localhost:8200/v1/post/wikipedia", HttpMethod.POST, requestEntity,
                        String.class);

        return response;
    }

    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    } 

}

標籤的開發工作繁瑣,需要不斷的開發並且優化,但是如何將做好的標籤提供出去產生真正的價值呢? 下一章,我們將介紹用戶畫像產品化,未完待續~

參考文獻

《用戶畫像:方法論與工程化解決方案》

更多實時數據分析相關博文與科技資訊,歡迎關注 “實時流式計算” 獲取用戶畫像相關資料 請關注 “實時流式計算” 回復 “用戶畫像”

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

【其他文章推薦】

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

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

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

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

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

前端也要懂物理 —— 慣性滾動篇

作者:凹凸曼-吖偉

我們在平時編程開發時,除了需要關注技術實現、算法、代碼效率等因素之外,更要把所學到的學科知識(如物理學、理論數學等等)靈活應用,畢竟理論和實踐相輔相成、密不可分,這無論是對於我們的方案選型、還是技術實踐理解都有非常大的幫助。今天就讓我們一起來回顧中學物理知識,並靈活運用到慣性滾動的動效實現當中。

慣性滾動(也叫 滾動回彈momentum-based scrolling)最早是出現在 iOS 系統中,是指 當用戶在終端上滑動頁面然後把手指挪開,頁面不會馬上停下而是繼續保持一定時間的滾動效果,並且滾動的速度和持續時間是與滑動手勢的強烈程度成正比。抽象地理解,就像高速行駛的列車制動后依然會往前行駛一段距離才會最終停下。而且在 iOS 系統中,當頁面滾動到頂/底部時,還有可能觸發 “回彈” 的效果。這裏錄製了微信 APP 【賬單】頁面中的 iOS 原生時間選擇器的慣性滾動效果:

熟悉 CSS 開發的同學或許會知道,在 Safari 瀏覽器中有這樣一條 CSS 規則:

-webkit-overflow-scrolling: touch;

當其樣式值為 touch 時,瀏覽器會使用具有回彈效果的滾動, 即“當手指從觸摸屏上移開,內容會繼續保持一段時間的滾動效果”。除此之外,在豐富多姿的 web 前端生態中,很多經典組件的交互都一定程度地沿用了慣性滾動的效果,譬如下面提到的幾個流行 H5 組件庫中的例子。

流行 UI 庫效果

為了方便對比,我們先來看看一個 H5 普通長列表在 iOS 系統下(開啟了滾動回彈)的滾動表現:

  • weui 的 picker 組件

    明顯可見,weui 選擇器的慣性滾動效果非常弱,基本上手從屏幕上移開后滾動就很快停止了,體驗較為不好。

  • vant 的 picker 組件

    相比之下,vant 選擇器的慣性滾動效果則明顯清晰得多,但是由於觸頂/底回彈時依然維持了普通滾動時的係數或持續時間,導致整體來說回彈的效果有點脫節。

應用物理學模型

慣性 一詞來源於物理學中的慣性定律(即 牛頓第一定律):一切物體在沒有受到力的作用的時候,運動狀態不會發生改變,物體所擁有的這種性質就被稱為慣性。可想而知,慣性滾動的本質就是物理學中的慣性現象,因此,我們可以恰當利用中學物理上的 滑塊模型 來描述慣性滾動全過程。

為了方便描述,我們把瀏覽器慣性滾動效果中的滾動目標(如瀏覽器中的頁面元素)模擬成滑塊模型中的 滑塊。而且分析得出,慣性滾動的全過程可以模擬為(人)使滑塊滑動一定距離然後釋放的過程,那麼,全流程可以拆解為以下兩個階段:

  • 第一階段,滑動滑塊使其從靜止開始做加速運動;

    在此階段,滑塊受到的 F拉 大於 F摩 使其從左到右勻加速前進。

    需要注意的是,對於瀏覽器的慣性滾動來說,我們一般關注的是用戶即將釋放手指前的一小階段,而非滾動的全流程(全流程意義不大),這一瞬間階段可以簡單模擬為滑塊均衡受力做 勻加速運動。

  • 第二階段,釋放滑塊使其在只受摩擦力的作用下繼續滑動,直至最終靜止;

    在此階段,滑塊只受到反向的摩擦力,會維持從左到右的運動方向減速前進然後停下。

基於滑塊模型,我們需要找到適合的量化指標來建立慣性滾動的計算體系。結合模型和具體實現,我們需要關注 滾動距離速度曲線 以及 滾動時長 這幾個關鍵指標,下面會一一展開解析。

滾動距離

對於滑動模型的第一階段,滑塊做勻加速運動,我們不妨設滑塊的滑動距離為 s1,滑動的時間為 t1,結束時的臨界點速度(末速度)為 v1 ,根據位移公式

可以得出速度關係

對於第二階段,滑塊受摩擦力 F拉 做勻減速運動,我們不妨設滑動距離為 s2,滑動的時間為 t2,滑動加速度為 a,另外初速度為 v1,末速度為 0m/s,結合位移公式和加速度公式

可以推算出滑動距離 s2

由於勻減速運動的加速度為負(即 a < 0),不妨設一個加速度常量 A,使其滿足 A = -2a 的關係,那麼滑動距離

然而在瀏覽器實際應用時,v1 算平方會導致最終計算出的慣性滾動距離太大(即對滾動手勢的強度感應過於靈敏),我們不妨把平方運算去掉

所以,求慣性滾動的距離(即 s2)時,我們只需要記錄用戶滾動的 距離 s1滾動時長 t1,並設置一個合適的 加速度常量 A 即可。

經大量測試得出,加速度常量 A 的合適值為 0.003

另外,需要注意的是,對於真正的瀏覽器慣性滾動效果來說,這裏討論的滾動距離和時長是指能夠作用於慣性滾動的範圍內的距離和時長,而非用戶滾動頁面元素的全流程,詳細的可以看【啟停條件】這一節內容。

慣性滾動速度曲線

針對慣性滾動階段,也就是第二階段中的勻減速運動,根據位移公式可以得到位移差和時間間距 T 的關係

不難得出,在同等時間間距條件下,相鄰兩段位移差會越來越小,換句話說就是慣性滾動的偏移量增加速度會越來越小。這與 CSS3 transition-timing-function 中的 ease-out 速度曲線非常吻合,ease-out (即 cubic-bezier(0, 0, .58, 1))的貝塞爾曲線為

曲線圖來自 在線繪製貝塞爾曲線網站。

其中,圖表中的縱坐標是指 動畫推進的進程,橫坐標是指 時間,原點坐標為 (0, 0),終點坐標為 (1, 1),假設動畫持續時間為 2 秒,(1, 1) 坐標點則代表動畫啟動后 2 秒時動畫執行完畢(100%)。根據圖表可以得出,時間越往後動畫進程的推進速度越慢,符合勻減速運動的特性。

我們試試實踐應用 ease-out 速度曲線:

很明顯,這樣的速度曲線過於線性平滑,減速效果不明顯。我們參考 iOS 滾動回彈的效果重複測試,調整貝塞爾曲線的參數為 cubic-bezier(.17, .89, .45, 1)

調整曲線后的效果理想很多:

回彈

接下來模擬慣性滾動時觸碰到容器邊界觸發回彈的情況。

我們基於滑塊模型來模擬這樣的場景:滑塊左端與一根彈簧連接,彈簧另一端固定在牆體上,在滑塊向右滑動的過程中,當滑塊到達臨界點(彈簧即將發生形變時)而速度還沒有降到 0m/s 時,滑塊會繼續滑動並拉動彈簧使其發生形變,同時滑塊會受到彈簧的反拉力作減速運動(動能轉化為內能);當滑塊速度降為 0m/s 時,此時彈簧的形變量最大,由於彈性特質彈簧會恢復原狀(內能轉化成動能),並拉動滑塊反向(左)運動。

類似地,回彈過程也可以分為下面兩個階段:

  • 滑塊拉動彈簧往右做變減速運動;

此階段滑塊受到摩擦力 F摩 和越來越大的彈簧拉力 F彈 共同作用,加速度越來越大,導致速度降為 0m/s 的時間會非常短。

  • 彈簧恢復原狀,拉動滑塊向左做先變加速后變減速運動;

    此階段滑塊受到的摩擦力 F摩 和越來越小的彈簧拉力 F彈 相互抵消,剛開始 F彈 > F摩,滑塊做加速度越來越小的變加速運動;隨後 F彈 < F摩,滑塊做加速度越來越大的變減速運動,直至最終靜止。這裏為了方便實際計算,我們不妨假設一個理想狀態:當滑塊靜止時彈簧剛好恢復形變

回彈距離

根據上面的模型分析,回彈的第一階段做加速度越來越大的變減速直線運動,不妨設此階段的初速度為 v0,末速度為 v1,那麼可以與滑塊位移建立關係:

其中 a 為加速度變量,這裏暫不展開討論。那麼,根據物理學的彈性模型,第二階段的回彈距離為

微積分都來了,簡直沒法計算……

然而,我們可以根據運動模型適當簡化 S回彈 值的計算。由於 回彈第二階段的加速度 是大於 非回彈慣性滾動階段的加速度F彈 + F摩 > F摩)的,不妨設非回彈慣性滾動階段的總距離為 S滑,那麼

因此,我們可以設置一個較為合理的常量 B,使其滿足這樣的等式:

經大量實踐得出,常量 B 的合理值為 10。

回彈速度曲線

觸發回彈的整個慣性滾動軌跡可以拆分成三個運動階段:

然而,如果要把階段 a 和階段 b 準確描繪成 CSS 動畫是有很高的複雜度的:

  • 階段 b 中的變減速運動難以準確描繪;
  • 這兩個階段雖運動方向相同但動畫速度曲線不連貫,很容易造成用戶體驗斷層;

為了簡化流程,我們把階段 ab 合併成一個運動階段,那麼簡化后的軌跡就變成:

鑒於在階段 a 末端的反向加速度會越來越大,所以此階段滑塊的速度驟減同比非回彈慣性滾動更快,對應的貝塞爾曲線末端就會更陡。我們選擇一條較為合理的曲線 cubic-bezier(.25, .46, .45, .94)

對於階段 b,滑塊先變加速后變減速,與 ease-in-out 的曲線有點類似,實踐嘗試:

仔細觀察,我們發現階段 a 和階段 b 的銜接不夠流暢,這是由於 ease-in-out 曲線的前半段緩入導致的。所以,為了突出效果我們選擇只描繪變減速運動的階段 b 末段。貝塞爾曲線調整為 cubic-bezier(.165, .84, .44, 1)

實踐效果:

由於 gif 轉格式導致部分掉幀,示例效果看起來會有點卡頓,建議直接體驗 demo

CSS 動效時長

我們對 iOS 的滾動回彈效果做多次測量,定義出體驗良好的動效時長參數。在一次慣性滾動中,可能會出現下面兩種情況,對應的動效時間也不一樣:

  • 沒有觸發回彈

    慣性滾動的合理持續時間為 2500ms

  • 觸發回彈

    對於階段 a,當 S回彈 大於某個關鍵閾值時定義為 強回彈,動效時長為 400ms;反之則定義為 弱回彈,動效時長為 800ms

    而對於階段 b,反彈的持續時間為 500ms 較為合理。

啟停條件

前文中有提到,如果把用戶滾動頁面元素的整個過程都納入計算範圍是非常不合理的。不難想象,當用戶以非常緩慢的速度使元素滾動比較大的距離,這種情況下元素動量非常小,理應不觸發慣性滾動。因此,慣性滾動的觸發是有條件的。

  • 啟動條件

    慣性滾動的啟動需要有足夠的動量。我們可以簡單地認為,當用戶滾動的距離足夠大(大於 15px)和持續時間足夠短(小於 300ms)時,即可產生慣性滾動。換成編程語言就是,最後一次 touchmove 事件觸發的時間和 touchend 事件觸發的時間間隔小於 300ms,且兩者產生的距離差大於 15px 時認為可啟動慣性滾動。

  • 暫停時機

    當慣性滾動未結束(包括處於回彈過程),用戶再次觸碰滾動元素時,我們應該暫停元素的滾動。在實現原理上,我們需要通過 getComputedStylegetPropertyValue 方法獲取當前的 transform: matrix() 矩陣值,抽離出元素的水平 y 軸偏移量后重新調整 translate 的位置。

示例代碼

基於 vuejs 提供了部分關鍵代碼,也可以直接訪問 codepen demo 體驗效果(完整代碼)。

<html>
  <body>
    <div id="app"></div>
    <template id="tpl">
      <div
        ref="wrapper"
        @touchstart.prevent="onStart"
        @touchmove.prevent="onMove"
        @touchend.prevent="onEnd"
        @touchcancel.prevent="onEnd"
        @transitionend="onTransitionEnd">
        <ul ref="scroller" :style="scrollerStyle">
          <li v-for="item in list">{{item}}</li>
        </ul>
      </div>
    </template>
    <script>
      new Vue({
        el: '#app',
        template: '#tpl',
        computed: {
          list() {},
          scrollerStyle() {
            return {
              'transform': `translate3d(0, ${this.offsetY}px, 0)`,
              'transition-duration': `${this.duration}ms`,
              'transition-timing-function': this.bezier,
            };
          },
        },
        data() {
          return {
            minY: 0,
            maxY: 0,
            wrapperHeight: 0,
            duration: 0,
            bezier: 'linear',
            pointY: 0,                    // touchStart 手勢 y 坐標
            startY: 0,                    // touchStart 元素 y 偏移值
            offsetY: 0,                   // 元素實時 y 偏移值
            startTime: 0,                 // 慣性滑動範圍內的 startTime
            momentumStartY: 0,            // 慣性滑動範圍內的 startY
            momentumTimeThreshold: 300,   // 慣性滑動的啟動 時間閾值
            momentumYThreshold: 15,       // 慣性滑動的啟動 距離閾值
            isStarted: false,             // start鎖
          };
        },
        mounted() {
          this.$nextTick(() => {
            this.wrapperHeight = this.$refs.wrapper.getBoundingClientRect().height;
            this.minY = this.wrapperHeight - this.$refs.scroller.getBoundingClientRect().height;
          });
        },
        methods: {
          onStart(e) {
            const point = e.touches ? e.touches[0] : e;
            this.isStarted = true;
            this.duration = 0;
            this.stop();
            this.pointY = point.pageY;
            this.momentumStartY = this.startY = this.offsetY;
            this.startTime = new Date().getTime();
          },
          onMove(e) {
            if (!this.isStarted) return;
            const point = e.touches ? e.touches[0] : e;
            const deltaY = point.pageY - this.pointY;
            this.offsetY = Math.round(this.startY + deltaY);
            const now = new Date().getTime();
            // 記錄在觸發慣性滑動條件下的偏移值和時間
            if (now - this.startTime > this.momentumTimeThreshold) {
              this.momentumStartY = this.offsetY;
              this.startTime = now;
            }
          },
          onEnd(e) {
            if (!this.isStarted) return;
            this.isStarted = false;
            if (this.isNeedReset()) return;
            const absDeltaY = Math.abs(this.offsetY - this.momentumStartY);
            const duration = new Date().getTime() - this.startTime;
            // 啟動慣性滑動
            if (duration < this.momentumTimeThreshold && absDeltaY > this.momentumYThreshold) {
              const momentum = this.momentum(this.offsetY, this.momentumStartY, duration);
              this.offsetY = Math.round(momentum.destination);
              this.duration = momentum.duration;
              this.bezier = momentum.bezier;
            }
          },
          onTransitionEnd() {
            this.isNeedReset();
          },
          momentum(current, start, duration) {
            const durationMap = {
              'noBounce': 2500,
              'weekBounce': 800,
              'strongBounce': 400,
            };
            const bezierMap = {
              'noBounce': 'cubic-bezier(.17, .89, .45, 1)',
              'weekBounce': 'cubic-bezier(.25, .46, .45, .94)',
              'strongBounce': 'cubic-bezier(.25, .46, .45, .94)',
            };
            let type = 'noBounce';
            // 慣性滑動加速度
            const deceleration = 0.003;
            // 回彈阻力
            const bounceRate = 10;
            // 強弱回彈的分割值
            const bounceThreshold = 300;
            // 回彈的最大限度
            const maxOverflowY = this.wrapperHeight / 6;
            let overflowY;

            const distance = current - start;
            const speed = 2 * Math.abs(distance) / duration;
            let destination = current + speed / deceleration * (distance < 0 ? -1 : 1);
            if (destination < this.minY) {
              overflowY = this.minY - destination;
              type = overflowY > bounceThreshold ? 'strongBounce' : 'weekBounce';
              destination = Math.max(this.minY - maxOverflowY, this.minY - overflowY / bounceRate);
            } else if (destination > this.maxY) {
              overflowY = destination - this.maxY;
              type = overflowY > bounceThreshold ? 'strongBounce' : 'weekBounce';
              destination = Math.min(this.maxY + maxOverflowY, this.maxY + overflowY / bounceRate);
            }

            return {
              destination,
              duration: durationMap[type],
              bezier: bezierMap[type],
            };
          },
          // 超出邊界時需要重置位置
          isNeedReset() {
            let offsetY;
            if (this.offsetY < this.minY) {
              offsetY = this.minY;
            } else if (this.offsetY > this.maxY) {
              offsetY = this.maxY;
            }
            if (typeof offsetY !== 'undefined') {
              this.offsetY = offsetY;
              this.duration = 500;
              this.bezier = 'cubic-bezier(.165, .84, .44, 1)';
              return true;
            }
            return false;
          },
          // 停止滾動
          stop() {
            const matrix = window.getComputedStyle(this.$refs.scroller).getPropertyValue('transform');
            this.offsetY = Math.round(+matrix.split(')')[0].split(', ')[5]);
          },
        },
      });
    </script>
  </body>
</html>

參考資料

  • weui-picker
  • better-scroll

歡迎關注凹凸實驗室博客:aotu.io

或者關注凹凸實驗室公眾號(AOTULabs),不定時推送文章:

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

【其他文章推薦】

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

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

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

※超省錢租車方案

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

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

【優雅寫代碼系統】springboot+mybatis+pagehelper+mybatisplus+druid教你如何優雅寫代碼

目錄

  • spring基本搭建
  • 整合mybatis
  • pom配置
  • mybatis配置
    • 設置數據源
    • 設置sqlsessionfactory
    • 設置掃描
    • 設置開啟事務
    • 資源放行
    • 測試
    • 結果
  • 思考&&疑問
  • 使用通用mapper功能
  • 使用mybatis-plus
  • 使用分頁插件
    • mybatis-plus自帶分頁
    • github分頁插件
      • 使用常規版本
  • 總結
  • # 加入戰隊
    • 微信公眾號
  • 主題

springboot 融合了很多插件。springboot相比spring來說有一下有點

  • 自動配置: 針對很多spring的應用程序,springboot提供了很多自動配置
  • 起步依賴: 告訴springboot你需要什麼,他就會引入需要的庫
  • 命令行界面:springboot的可選特性
  • Autuator: 監控springboot項目的運行情況

spring基本搭建

  • springboot的配置很簡單。在pom中繼承springboot的pom .然後依賴一下pom就可以繼承所需的jar了

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.0.3.RELEASE</version>
</parent>


<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

  • 出了jar外。我們pom中配置一個插件就行了

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

整合mybatis

【詳見feature/0002/spring-mybatis 分支】

  • 之前我們梳理過mybatis的運行機制及注意點。javaweb的開發時離不開spring的。一個完整的框架是離不開spirng的。所以spring整合mybatis勢在必行。我們下面實現如何在spring下融合mybatis及其優秀的一些插件搭建架構

pom配置

  • 我們在springboot項目的pom中繼續添加我們mybatis的jar就完成了第一步。
  • 一個是mybatis與spring的整合jar 。 開啟springboot加載mybatis項目
  • 一個是spring的aopjar包。主要是實現mybatis的事務問題
  • 一個是mysql的jar包。這裏主要看你自己的需求。如果你的項目中使用oracle那麼久加入oracle的坐標就行了
  • druid是管理數據的數據源

<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>1.3.1</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.1.10</version>
</dependency>

mybatis配置

  • 上面的pom設置已經準備了mybatis開發階段的基本jar。 有了上述的包就可以搭建mybatis . 正常我們在我們的springboot項目中配置一個bean配置的類 MybatisConfig

設置數據源


@Bean
@ConfigurationProperties("spring.datasource")
public DataSource primaryDataSource() {
    // 這裏為了演示,暫時寫死 。 實際上是可以通過autoConfigure裝配參數的
    DruidDataSource dataSource = new DruidDataSource();
    dataSource.setUsername("root");
    dataSource.setPassword("123456");
    dataSource.setDriverClassName("com.mysql.jdbc.Driver");
    dataSource.setUrl("jdbc:mysql://192.168.44.130:3306/others?useUnicode=true&amp;characterEncoding=utf8");
    return dataSource;
}

  • 在springboot中我們只需要在方法上添加Bean註解,就相當於我們在spring的xml中配置的bean標籤。在java代碼中我們可以進行我們業務處理。個人理解感覺更加的可控。 因為我們使用的mysql。所以這裏我們簡單的使用druid的數據源

設置sqlsessionfactory


@Bean
public SqlSessionFactory primarySqlSessionFactory() {
    SqlSessionFactory factory = null;
    try {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(primaryDataSource());
        sqlSessionFactoryBean.setConfigLocation(new DefaultResourceLoader().getResource("classpath:mybatis-primary.xml"));
        sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:/com/github/zxhtom.**./*.xml"));
        factory = sqlSessionFactoryBean.getObject();
    } catch (Exception e) {
        e.printStackTrace();
    }
    return factory;
}

  • 在mybatis中sqlsesson是我們操作數據庫最直接的java類。管理sqlsession就是上面的sqlsessionFactory 。 所以我們配置它也是必須的。因為和spring整合。Mybatis的SqlsessionFactory交由給spring的SqlsessionfactoryBean管理。所以我們先構建SqlSessionFactoryBean。然後配置必須的數據源、Configuration、mybatis的xml路徑等等信息
  • SqlSessionFactoryBean 設置出了spring的FactoryBean屬性意外。最重要的是可以設置Mybatis的sqlsessionfactory的屬性。上面我們只是簡單的設置了。後期可以根據架構的需求進行不斷的完善。
  • 可以設置插件、緩存、別名、映射處理器、事務、環境等等所有mybatis的配置

設置掃描


@Bean
public MapperScannerConfigurer mapperScannerConfigurer() {
    MapperScannerConfigurer mapperScannerConfigurer = new MapperScannerConfigurer();
    mapperScannerConfigurer.setSqlSessionFactory(primarySqlSessionFactory());
    //每張表對應的*.java,interface類型的Java文件
    mapperScannerConfigurer.setBasePackage("com.github.zxhtom.**.mapper");
    return mapperScannerConfigurer;
}

  • springboot 中我們的mapper接口也是交由spring來掃描的。之前mybatis程序我們是一個一個手動加入的。spring的特性可以直接掃描指定路徑的所有java類。這裏我們就設置了一下mapper的路徑。

設置開啟事務

  • 優秀的架構沒有事務是能使用的。我們配置事務也是很簡單的。在springboot中我們推薦使用java代碼來配置事務的。下面的配置我們為了容易閱讀。配置在TransactionConfig類中

@Bean
public DataSourceTransactionManager primaryTransactionManager() {
    return new DataSourceTransactionManager(dataSource);
}

  • 首先是配置事務管理器。事務管理的是數據源。所以這裏我們需要將MybatisConfig中的DataSource加載進來。這裏不多說
  • 然後配置我們的通知點

@Bean
public TransactionInterceptor txAdvice() {
    DefaultTransactionAttribute txAttr_REQUIRED = new DefaultTransactionAttribute();
    txAttr_REQUIRED.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);

    DefaultTransactionAttribute txAttr_REQUIRED_READONLY = new DefaultTransactionAttribute();
    txAttr_REQUIRED_READONLY.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
    txAttr_REQUIRED_READONLY.setReadOnly(true);

    NameMatchTransactionAttributeSource source = new NameMatchTransactionAttributeSource();
    source.addTransactionalMethod("add*", txAttr_REQUIRED);
    source.addTransactionalMethod("save*", txAttr_REQUIRED);
    source.addTransactionalMethod("delete*", txAttr_REQUIRED);
    source.addTransactionalMethod("update*", txAttr_REQUIRED);
    source.addTransactionalMethod("exec*", txAttr_REQUIRED);
    source.addTransactionalMethod("set*", txAttr_REQUIRED);
    source.addTransactionalMethod("get*", txAttr_REQUIRED_READONLY);
    source.addTransactionalMethod("query*", txAttr_REQUIRED_READONLY);
    source.addTransactionalMethod("find*", txAttr_REQUIRED_READONLY);
    source.addTransactionalMethod("list*", txAttr_REQUIRED_READONLY);
    source.addTransactionalMethod("count*", txAttr_REQUIRED_READONLY);
    source.addTransactionalMethod("is*", txAttr_REQUIRED_READONLY);
    return new TransactionInterceptor(primaryTransactionManager(), source);
}

  • 最後我們配置一下我們的切面、切點

@Bean
public Advisor txAdviceAdvisor() {
    AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
    pointcut.setExpression(AOP_POINTCUT_EXPRESSION);
    return new DefaultPointcutAdvisor(pointcut, txAdvice());
}@Bean
public Advisor txAdviceAdvisor() {
    AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
    pointcut.setExpression(AOP_POINTCUT_EXPRESSION);
    return new DefaultPointcutAdvisor(pointcut, txAdvice());
}

  • 最後說明下。TransactionConfig這個類因為配置事務、攔截所以我們需要加入如下註解在雷傷

@Configuration
@Aspect
@EnableTransactionManagement

資源放行

  • 以上就是mybatis基本的一個配置了。但是這個時候還是不能使用的。因為我們的mapper對應的xml是放在java目錄下的。正常src下都是java。xml文件在maven編譯時不放行的。我們需要特殊處理下

  • 在pom的build下加入放行配置


<resources>
    <resource>
        <directory>src/main/java</directory>
        <includes>
            <include>**/*.xml</include>
            <include>**/*.properties</include>
            <include>**/*.yaml</include>
        </includes>
        <filtering>true</filtering>
    </resource>
    <resource>
        <directory>src/main/resources</directory>
        <includes>
            <include>**/*.*</include>
        </includes>
        <filtering>false</filtering>
    </resource>
</resources>

  • 加入如下配置后別忘記了clean一下在運行。防止緩存。clean之後看看target下文件

測試

  • 為了方便測試我們需要編寫測試類。

@SpringBootTest(classes = Application.class)
@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
public class TestDataSource {
  
    @Autowired
    private TestMapper testMapper;
    
    @Test
    public void test() {
        List<Map<String, Object>> test = testMapper.test();
        System.out.println(test);
    }
}

結果

  • 這裡是我查詢數據庫中的一條數據。主要是測試springboot整合mybatis的流程。到這裏mybatis就整合完了。

思考&&疑問

  • 上面的配置我們是接入mybatis了。但是在筆者其他架構上測試過。在MybatisConfig這個類中配置數據源的時候可以直接new出DruidDataSource就可以了。springboot會自動通過DataSourceProperties這個類獲取到數據源信息的。但是筆者這裏一直沒有嘗試成功。至於原理更是沒有搞懂了。這裏留下一個彩蛋希望知道的朋友能說說這事怎麼回事

@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource primaryDataSource() {
    return new DruidDataSource();
}

  • 上述代碼和本案例中相比就少了很多配置。雖然有的朋友說可以通過映射實體來做。但是直接new對象對我們而言更簡單。疑問

使用通用mapper功能

【詳見feature/0002/spring-mybatis-tk-page 分支】


<dependency>
    <groupId>tk.mybatis</groupId>
    <artifactId>mapper</artifactId>
    <version>3.3.9</version>
</dependency>

  • 這些插件其實就是改寫我們之前學習的myabtis四大組件中的某一個組件而實現的。所以不管是通用mapper還是後面要說的myabtis-plus都是需要重新改寫我們的myabtis配置類的。

  • 首先我們想接入通用mapper時,我們需要改用tk提供的掃包配置


@Bean
public MapperScannerConfigurer mapperScannerConfigurer() {
    tk.mybatis.spring.mapper.MapperScannerConfigurer mapperScannerConfigurer = new tk.mybatis.spring.mapper.MapperScannerConfigurer();
    //MapperScannerConfigurer mapperScannerConfigurer = new MapperScannerConfigurer();
    mapperScannerConfigurer.setSqlSessionFactory(primarySqlSessionFactory());
    //每張表對應的*.java,interface類型的Java文件
    mapperScannerConfigurer.setBasePackage("com.github.zxhtom.**.mapper");
    return mapperScannerConfigurer;
}

  • 然後添加通用mapper配置

@Bean()
public MapperHelper primaryMapperHelper() throws Exception {
    MapperHelper mapperHelper = new MapperHelper();

    Config config = mapperHelper.getConfig();
    //表名字段名使用駝峰轉下劃線大寫形式
    config.setStyle(Style.camelhumpAndUppercase);

    mapperHelper.registerMapper(Mapper.class);
    mapperHelper.processConfiguration(primarySqlSessionFactory().getConfiguration());
    return mapperHelper;
}

  • 測試代碼就很簡單了。

User user = tkMapper.selectByPrimaryKey(1);

使用mybatis-plus

【詳見feature/0002/spring-mybatisplus 分支】

  • mybatis-plus 實際上就是通用mapper 。這裏可以理解就是不同的實現。接入mybatis-plus其實很簡單。首先我們引入坐標


<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>2.2.0</version>
</dependency>

  • 然後我們創建實體

@Data
@TableName(value = "person_info_large")
public class User {
    @TableId(value = "id",type = IdType.AUTO)
    private Integer id;
    private String account;
    private String name;
    private String area;
    private String title;
    private String motto;
}

  • 然後就是編寫我們的Mapper 。需要繼承BaseMapper

public interface PlusMapper extends BaseMapper<User> {
}

  • 上述的編寫之後基本就可以查詢了。但是注意一下我們需要修改上面的myabtisCOnfog這個類。因為接入mybatisplus后需要我們用mybatisplus中的sqlsessionbean。

@Bean
public SqlSessionFactory primarySqlSessionFactory() {
    SqlSessionFactory factory = null;
    try {
        MybatisSqlSessionFactoryBean sqlSessionFactoryBean = new MybatisSqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(primaryDataSource());
        sqlSessionFactoryBean.setConfigLocation(new DefaultResourceLoader().getResource("classpath:mybatis-primary.xml"));
        sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:com/github/zxhtom.**./*.xml"));
        sqlSessionFactoryBean.setTypeAliasesPackage("com.github.zxhtom.**.model");
        factory = sqlSessionFactoryBean.getObject();
    } catch (Exception e) {
        e.printStackTrace();
    }
    return factory;
}

  • 和通用mapper一樣。就這樣我們就可以調用BaseMapper為我們提供的方法操作數據庫了。

@Test
public void plusTest() {
    User user = plusMapper.selectById(1);
    System.out.println(user);
}

使用分頁插件

mybatis-plus自帶分頁

【詳見feature/0002/spring-mybatisplus 分支】

  • 首先我們使用mybatis plus自帶的分頁插件,將插件類注入到容器中

@Bean
public PaginationInterceptor paginationInterceptor() {
    return new PaginationInterceptor();
}

  • 然後我們查詢的構造一個Page對象就行了。參數分別是第幾頁、展示條數

return plusMapper.selectPage(page, null);

  • 就是這麼簡單。使用簡單原理才是我們學習的重點。喜歡myabtis插件原理的可以留言。抽空可以研究

github分頁插件

【詳見feature/0002/spring-mybatis-tk-page 分支】

  • 因為myatis-plus自帶了分頁插件。上面也展示了如何使用myabtis-plus插件。還有一個github上開元的分頁插件。這裏就和通用mapper進行組合使用

  • pagehelper官方更新還是挺勤奮的。提供了pagehelper 和springboot auto裝配兩種。 筆者這裏測試了pagehelper-spring-boot-starter這個包不適合本案例中。因為本案例中掃描mapper路徑是通過MapperScannerConfigurer註冊的。如果使用pagehelper-spring-boot-starter的話就會導致分頁攔截器失效。我們看看源碼

  • 這是因為這版本提供了springboot自動裝配。但是自動裝配的代碼中進行添加攔截器的時候sqlsessionfactory這個時候還沒有進行掃描mapper.也就沒有進行addMapper 。 所以這個時候添加的攔截器攔截不到我們的mapper . 如果非要使用這個版本的話。我們掃描mapper就不能通過MapperScannerConfigurer . 經過筆者測試。需要在MybatisConfig類上添加掃描註解@MapperScan(basePackages = {"com.github.zxhtom.**.mapper"}, sqlSessionFactoryRef = "primarySqlSessionFactory")

使用常規版本

  • 為了符合本版本宗旨 。 我們這裏使用如下坐標

<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper</artifactId>
    <version>5.1.7</version>
</dependency>

  • 使用這個版本pagehelper。 沒有了springboot的自動裝配了。我們可以自己添加插件。添加插件有兩種方式。想自動裝配版本一樣通過Configuration進行添加不過我們通過Condition條件選擇添加的時機 。
  • 還有一個簡單的方式就是通過SqlSessionFactoryBean設置sqlSessionFactoryBean.setPlugins(new Interceptor[]{new PageInterceptor()});
  • 因為通過sqlsessionFactoryBean添加的插件和在settings文件中添加是一樣的。在通過XmlFactory.build Configuration對象是會自動將插件裝在上。這個時候mapper也都掃描過了。

@Bean
public SqlSessionFactory primarySqlSessionFactory() {
    SqlSessionFactory factory = null;
    try {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(primaryDataSource());
        sqlSessionFactoryBean.setConfigLocation(new DefaultResourceLoader().getResource("classpath:mybatis-primary.xml"));
        sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:com/github/zxhtom.**./*.xml"));
        sqlSessionFactoryBean.setTypeAliasesPackage("com.github.zxhtom.**.model");
        sqlSessionFactoryBean.setPlugins(new Interceptor[]{new PageInterceptor()});
        factory = sqlSessionFactoryBean.getObject();
    } catch (Exception e) {
        e.printStackTrace();
    }
    return factory;
}

總結

  • 這裏簡單闡述下為什麼不適用註解掃描mapper、或者不在配置文件中配置掃包路徑。因為通過MapperScannerConfigurer我們可以動態控制路徑。這樣顯得比較有逼格。在實際開發中筆者推薦使用註解方式掃描。因為這樣可以避免不必要的坑。

  • 到這裏我們整理了【springboot整合mybaits】、【mybatis-plus】、【通用mapper】、【pagehelper插件整合】 這四個模塊的整理中我們發現離不開myabtis的四大組件。springboot其實我們還未接觸到深的內容。這篇文章主要偏向myabtis . 後續還會繼續衍生探索 【myabtis+druid】監控數據信息 。

  • 喜歡研究源碼和開元框架小試牛刀的歡迎關注我

加入戰隊

# 加入戰隊

微信公眾號

主題

非xml配置springboot+mybatis事務管理

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

【其他文章推薦】

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

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

※超省錢租車方案

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

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

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