電動車生意難做?電池廠三星SDI淨損飆、股價遭熊吻

三星集團的電池大廠三星SDI(Samsung SDI)公布2015年第4季財報,虧損擴大。不過三星SDI的化學部門即將分拆出售給樂天集團(Lotte),或許有助未來獲利。   韓聯社報導,三星SDI 25日盤後公布,去年Q4營收年減2.53%至1.86兆韓圜,營損為808億韓圜。淨損為2,313億韓圜(1.94億美元),虧損高於2014年同期的淨損1,288億美元,也不及Q3出現淨利5,024億韓圜。   此外,三星SDI股東25日同意,將分拆化學部門成立獨立公司,暫名為「SDI Chemical」,其中90%股權將在今年上半之前,出售給Lotte Chemical。去年十月樂天集團以3兆韓圜(25億美元)入股三星SDI化學部門,並購買Samsung Fine Chemicals的31.5%股權。   嘉實XQ全球贏家系統報價顯示,三星SDI 25日上漲0.49%收在102,500韓圜。和去年12月16日波段收盤高點(129,000韓圜),重挫21%之多。   三星集團積極卡位汽車市場,旗下電池大廠三星SDI (Samsung SDI)展示最新的車用電池,充電一次可跑600公里,預定2020年量產。   韓聯社11日報導,三星SDI在底特律車展秀出新的車用電池原型,充電一次可跑600公里,高於先前的500公里,預定2020年量產。該公司估計,2020年全球電動車銷量將增至116萬輛,遠高於2015年的27.8萬輛。   三星SDI已成為純粹的車用電池廠,該公司目前在南韓與中國擁有五座生產線、總產能多達4.5GWh。到了2020年,該公司還打算把產能擴充到35GWh。barron`s.com 去年11月4日報導,Bernstein Research分析師Mark Newman發表研究報告指出,三星SDI在2020年的車用電池產能,正好與特斯拉預定的Gigafactory產能完全一致。   (本文內容由授權使用)

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

【其他文章推薦】

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

※評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

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

通用汽車組建團隊加速無人駕駛和電動汽車開發

通用汽車本1月21號表示,公司將組建一個團隊加速無人駕駛汽車和電動汽車的開發,通用汽車還會擴充該類產品線。

通用汽車全球產品專案副總裁道格•帕克斯(Doug Parks)將成為無人駕駛技術和汽車業務執行副總裁,他將向全球產品研發主管羅伊斯(Mark Reuss)彙報工作。帕克斯將負責新電動和電池系統、汽車軟體的開發。

在聲明中通用汽車還表示,謝裡•希考克(Sheri Hickok)將擔任無人駕駛合資和車隊實施方面的執行首席工程師,在任命之前希考克是下一代皮卡的首席工程師。此外,通用汽車電動汽車首席設計工程師帕姆•弗雷切(Pam Fletcher)將承擔更多的職責,他將負責無人駕駛和電動汽車的戰略規劃。
 

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

【其他文章推薦】

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

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

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

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

常隆客車和AESC公司達成了電動客車錳酸鋰離子電池供應協定

2016年1月14日, 江蘇常隆客車有限公司(江蘇省江陰市)和AESC公司(神奈川縣座間市)達成了電動客車錳酸鋰離子電池供應協定,雙方高層出席了隆重的簽字儀式。本次合作協定初期電池供給規模為30MWh,可以滿足100台12米電動客車的錳酸鋰電池。雙方還預計到2017年可以持續供貨900MWh規模的錳酸鋰電池,滿足3000輛純電動客車生產需求。

通過雙方合作,常隆電動客車將更加安全可靠、經久耐用,努力為市場提供最好的電動客車、最優質的服務、最值得信賴的品牌。同時雙方約定今後持續探討本次合作的動力電池在儲能領域應用的可行性。雙方計畫同步開拓中國市場和國際市場。

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

【其他文章推薦】

※想知道網站建置網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計後台網頁設計

※不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

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

※帶您來看台北網站建置台北網頁設計,各種案例分享

北汽新能源將推5款新車 全新品牌「眾創」4月發佈

近日, 北京新能源汽車股份有限公司黨委書記、總經理鄭剛表示,北汽新能源規劃推出5款全新車型,其中全新品牌“眾創”車型將於4月份正式發佈。即將推出的5款車型覆蓋A00級、A0級、A級轎車及A級 SUV 領域。

北汽新能源將推出5款全新車型,其中純電動A級車EU260(續航里程260公里)將於2-3月正式上市,接下來純電動A0級SUV—EX200(續航200公里)將於3-4月登陸市場,其後續推出的高里程版續航可達到300公里。在今年的9-10月份,旗下全新A00級“國民車”將上市,該車將進一步拉低入門售價;今年年底,全新“眾創”品牌旗下車型將上市,進一步豐富產品陣容。2017年,北汽新能源將推出全新D級高性能純電動轎車,其續航里程將超過500公里。

據悉,北汽新能源推動的“眾創”專案,將在4月舉行的北京車展上,正式發佈該品牌LOGO、車型及命名等細節。“眾創”品牌的首款車型將於今年底正式上市,該車將採用超輕量化的全鋁框架車身結構設計。據介紹,北汽新能源將彙集全球“電粉”,全面參與到新產品的名稱定義、標識設計甚至軟硬選裝件的創意開發中,打造全球首款眾創汽車,該項目也將作為北汽新能源“衛藍眾創”計畫一部分。

在北汽新能源的規劃中,在2017年將推出更具產品實力的全新高性能D級純電動轎車,新車也將成為北汽新能源的旗艦產品,其續航里程將達到500公里。從定位來看,該車將與特斯拉MODEL S處同一級別。按照此前消息,北汽新能源與Atieva聯合開展的高性能電動汽車設計研發工作正在矽谷緊鑼密鼓地推進之中,新車有望是該款D級轎車。

多款熱銷車型的出現將促使北汽新能源加快對於產業佈局的建設。據悉,北汽新能源將以“1(北京采育基地)+2(常州基地、青島基地)+I(北汽集團內部傳統乘用車生產基地)+P(社會合作夥伴生產資源)>80”為基礎,在十三五末期形成80萬輛以上生產能力,年產銷50萬輛規模,打造年營業收入600億元、上市市值1000億元的企業。

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

【其他文章推薦】

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

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

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

南投搬家前需注意的眉眉角角,別等搬了再說!

電動車五日環台之旅,充電設備是最大考驗

由台灣電動車經銷、維修商綠動未來所主辦的第一屆電動車環台Party活動,歷經五日純電動行程,於2月1日順利抵台北101大樓。雖然台灣各地的充電設備分布不均,但這次活動仍證明電動車環島的可行性,主辦單位希望能藉此鼓勵更多民眾加入電動車行列。

綠動未來創辦人劉小麟表示,參加本次環島活動的車款分別是Tesla Model S、Luxgen M7 EV+ 以及Nissan Leaf 輕巧通勤車,皆為純電動車,且Luxgen、Nissan的車款都是都市用車,充飽電後的續航力只有150公里,在環島過程中也因此面臨較大的充電考驗。而Tesla Model S 每次充飽電可行駛480公里,影響相對輕微。

台灣各地充電設備空間分布不均。劉小麟紀錄環台過程的充電狀況,統計台灣東部的宜蘭、花蓮、台東三縣市均無充電設備,西部的新竹、苗栗、雲林、嘉義、高雄、屏東則沒有公共充電站。環台期間,須商借個人充電站協助充電,或者到汽車廠、警察局與消防隊,甚至在投宿的民宿充電。不過,用一般電源充電所需時間長,也因此限制電動車環島之旅的機動性;相較之下,專為電動車設計的快充充電站僅需幾十分鐘就能充飽電池再上路。

在成功完成純電環島之旅後,劉小麟也規劃開始募款,希望能帶著國產的Luxgen M7 EV 到西班牙參加六月即將展開的世界電動車環球賽,挑戰80天、20國、25,000公里的長征之旅。

(照片來源:臉書專頁)

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

【其他文章推薦】

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

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

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

類型擦除真的能完全擦除一切信息嗎?java 泛型揭秘

背景

我們都知道泛型本質上是提供類型的”類型參數”,它們也被稱為參數化類型(parameterized type)或參量多態(parametric polymorphism)。其實泛型思想並不是 Java 最先引入的,C++ 中的模板就是一個運用泛型的例子。

GJ(Generic Java)是對 Java 語言的一種擴展,是一種帶有參數化類型的 Java 語言。用 GJ 編寫的程序看起來和普通的 Java 程序基本相同,只不過多了一些參數化的類型同時少了一些類型轉換。實際上,這些 GJ 程序也是首先被轉化成一般的不帶泛型的 Java 程序后再進行處理的,編譯器自動完成了從 Generic Java 到普通 Java 的翻譯。

 

什麼是真實的java泛型

我們都知道編譯器會進行泛型擦除,編譯器可以在對源程序(帶有泛型的 Java 代碼)進行編譯時使用泛型類型信息保證類型安全,對大量如果沒有泛型就不會去驗證的類型安全約束進行驗證,同時在生成的字節碼當中,將這些類型信息清除掉。下面我們先驗證一下:

public static void main(String[] args) {
 ArrayList<Integer> ints = new ArrayList<Integer>();
 ints.add(1); 
 ints.add(2);
 ints.add(3);
 
 ArrayList<String> sts = new ArrayList<String>();
 sts.add("a");
 sts.add("b");
 sts.add("c");
 
 System.out.println(ints.getClass() == sts.getClass()); 
 }
上面打印的結果是true,原因是:

按照理解,泛型擦除后將不能找回原來的類型,都是Object形式的,真的如此嗎?

看一下如下代碼:

import java.lang.reflect.ParameterizedType;
import java.util.ArrayList;
import java.util.List;

public class ClassTest {
     public static void main(String[] args) throws Exception {
     ParameterizedType type = (ParameterizedType) 
     Bar.class.getGenericSuperclass();
     System.out.println(type.getActualTypeArguments()[0]);
     
     ParameterizedType fieldType = (ParameterizedType) 
     Foo.class.getField("children").getGenericType();
     System.out.println(fieldType.getActualTypeArguments()[0]);
     
     ParameterizedType paramType = (ParameterizedType) 
     Foo.class.getMethod("foo", List.class)
     .getGenericParameterTypes()[0];
     System.out.println(paramType.getActualTypeArguments()[0]);
     
     System.out.println(Foo.class.getTypeParameters()[0]
     .getBounds()[0]);
     }
     
     class Foo<E extends CharSequence> {
     public List<Bar> children = new ArrayList<Bar>();
     public List<StringBuilder> foo(List<String> foo) {return null; }
     public void bar(List<? extends String> param) {}
     }
     
     class Bar extends Foo<String> {}
    }

打印出

class java.lang.String
class com.javapuzzle.davidwang456.ClassTest$Bar
class java.lang.String
interface java.lang.CharSequence

你會發現每一個類型參數都被保留了,而且在運行期可以通過反射機制獲取到。那麼到底什麼是“類型擦除”?至少某些東西被擦除了吧?是的。事實上,除了結構化信息外的所有東西都被擦除了 —— 這裏結構化信息是指與類結構相關的信息,而不是與程序執行流程有關的。換言之,與類及其字段和方法的類型參數相關的元數據都會被保留下來,可以通過反射獲取到。

參考資料

【1】http://techblog.bozho.net/on-java-generics-and-erasure/

【2】https://www.ibm.com/developerworks/cn/java/j-lo-gj/?mhsrc=ibmsearch_a&mhq=%E7%B1%BB%E5%9E%8B%E6%93%A6%E9%99%A4

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

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

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

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

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

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

Vue躬行記(8)——Vue Router

  雖然Vue.js未提供路由功能,但是官方推出了Vue Router(即vue-router庫),以插件的形式支持。它與Vue.js深度集成,可快速的創建單頁應用(Single Page Application,SPA)。

一、基本用法

  首先需要引入vue和vue-router兩個庫,如果像下面這樣全局引用(即存在全局變量Vue),那麼vue-router會自動調用Vue.use()方法註冊其自身;但如果以模塊的方式引用,那麼就得顯式地調用Vue.use()。

<script src="js/vue.js"></script>
<script src="js/vue-router.js"></script>

  然後添加兩個內置的組件,第一個是導航用的router-link組件,它默認會被渲染成<a>元素,如果要渲染成其它元素,那麼可以使用它的tag屬性;第二個是路徑匹配時用於渲染視圖的router-view組件,它還是一個函數式組件,如下所示。

<div id="container">
  <router-link to="/main">首頁</router-link>
  <router-link to="/list">列表</router-link>
  <router-view></router-view>
</div>

  再聲明要渲染的組件,這些組件會與指定的路由映射,下面是兩個非常簡單的自定義組件。

const Main = { template: '<div>首頁</div>' };
const List = { template: '<div>列表</div>' };

  接着定義路由配置,添加一組路徑以及對應的組件,如下所示。

const routes = [
  { path: '/main', component: Main },
  { path: '/list', component: List }
];

  最後創建路由器實例,並將之前的路由配置傳遞進來,在掛載根實例時,需要將它注入,從而讓整個應用都有路由功能,如下所示。

const router = new VueRouter({
  routes: routes
});
var vm = new Vue({
  el: "#container",
  router: router
});

  注意,router實例包含三個導航方法:push()、replace()和go(),可以用編程的方式進行導航。

  頁面在渲染完成后,得到的DOM結構如下所示,此時router-view組件所佔的位置無任何內容。

<div id="container">
  <a href="#/main" class="">首頁</a>
  <a href="#/list" class="">列表</a>
</div>

  當router-link組件所對應的路由匹配成功時,其渲染出的元素會被自動添加一個CSS類:router-link-active。例如點擊首頁鏈接,得到的結構如下所示,注意,此時router-view組件被替換成了Main組件中的內容。

<div id="container">
  <a href="#/main" class="router-link-active">首頁</a>
  <a href="#/list" class="">列表</a>
  <div>首頁</div>
</div>

二、路由模式

  Vue Router默認採用URL hash模式來保持頁面和URL的同步,其創建的URL格式需要包含井號(#),如下所示。

http://pwstrick.com/#/main

  Vue Router還有另外一種history模式,利用HTML5 History來保持頁面和URL的同步,其創建的URL格式在視覺上更為簡潔清晰,如下所示。

http://pwstrick.com/main

  如果要開啟history模式,那麼需要在路由配置時添加mode選項,並賦予history關鍵字,如下所示(直接修改了上一節中的示例)。

const router = new VueRouter({
  mode: "history",
  routes: routes
});

  注意,當直接訪問history模式創建的URL時,會返回一個404頁面。為了避免這種情況的發生,建議在服務器上配置一個默認的候選頁面。

三、路由

  本節不僅會介紹動態路由的概念,還會分析路由的命名和嵌套等操作。

1)動態路由

  Vue Router採用的路徑匹配引擎是path-to-regexp,它支持動態路由的匹配,例如有一個List組件,需要根據路徑中的頁碼參數把組件渲染成不同的內容,可以像下面這樣配置。

const List = { template: '<div>{{$route.params.page}}</div>' };
const routes = [
  { path: '/list/:page', component: List }
];

  在路由配置中,以冒號開頭的:page是路徑參數,它的值會被保存到路由對象$route的params屬性中。路由對象不僅包含解析URL得到的信息(例如路徑、查詢字符串、錨點等),還有匹配到的路由信息(例如路由名稱、路由記錄等)。當路徑是/list/1時,$route.params的值為{page: 1},params是一個對象,其鍵值與路徑參數對應。注意,一條路徑可以包含多個不同的路徑參數。

  當一個路徑能匹配多個路由時,匹配優先級會按照路由的定義順序來排,即先定義的,優先級高。下面的配置包含兩個路由,當路徑是/list/1時,只會渲染List組件。

const routes = [
  { path: '/list/:page', component: List },
  { path: '/list/1', component: Main }
];

2)命名路由

  可以在配置時為路由標識一個名稱,從而就能在使用時省略路徑了,如下所示,name屬性的值就是路由名稱。

const routes = [
  { path: '/list/:page', component: List, name: "list" }
];

  如果要導航到一個命名路由,那麼需要動態地綁定to屬性,併為其傳入路由的名稱以及可選的URL參數,如下所示。

<router-link :to="{ name: 'list', params: {page: 3} }">第三頁</router-link>

3)嵌套路由

  Vue Router允許嵌套路由,可通過多層嵌套的組件實現。下面是一個使用示例,首先在一個組件中聲明router-view視圖組件,如下所示。

const List = { template: '<router-view></router-view>' };
const Detail = { template: '<div>詳情</div>' };

  然後在路由配置中,添加children選項,並定義子路由。

const routes = [
  {
    path: "/list",
    component: List,
    children: [{ path: "detail", component: Detail }]
  }
];

  在經過這一系列操作后,當路徑是/list/detail時,Detail組件就會被渲染到List組件的視圖中。

  嵌套路由可用來渲染多個不同層級的視圖,而利用命名視圖可渲染多個同級的視圖。在下面的代碼中,為第二個router-view組件添加了name屬性,賦予它一個名稱,而第一個router-view組件的默認名稱為default。

<router-view></router-view>
<router-view name="detail"></router-view>

  在路由配置時,新增用於記錄各個命名組件的components選項,如下所示。

const routes = [
  {
    path: "/named",
    components: {
      default: Main,
      detail: Detail
    }
  }
];

  當路徑是/named時,Main和Detail兩個組件會被同時渲染。

四、重定向和別名

1)重定向

  在路由配置中,通過redirect選項可重定向到一個新的頁面,該選項可取的值有三種,分別是路徑、命名路由和回調函數,如下所示,其中函數中的to參數表示目標路由對象。

const routes = [
  { path: "/list/1", redirect: "/main" },
  { path: "/list/1", redirect: {name: "main"} },
  { path: "/list/1", redirect: to => "/main" }
];

  當訪問的路徑是/list/1時,瀏覽器的地址欄會變成/main,匹配的將是路徑為/main的路由。

2)別名

  在路由配置中,通過alias選項可定義路由的別名,如下所示。

const routes = [
  { path: "/list/4", component: Detail, alias: "/detail" }
];

  當訪問的路徑是/main時,瀏覽器的地址欄保持為/main,但匹配的將是路徑為/list/4的路由。

五、組件傳參

  除了在組件中通過$route.params獲取路由參數之外,還能將路由參數作為組件的props傳入,從而將組件和路由解耦,如下所示。

const List = {
  props: ["page"],
  template: "<div>{{page}}</div>"
};
const routes = [
  { path: "/list/:page", component: List, props: true }
];

  需要注意的是,在路由配置時要添加props選項,並賦予true后,才會讓路由參數和props關聯。

六、導航守衛

  導航守衛就是路由發生變化時的鈎子函數,Vue Router提供了三類守衛:全局、路由獨享和組件級。

1)全局守衛

  在路由器實例上可註冊三個全局守衛,分別是beforeEach()、beforeResolve()和afterEach()。

  beforeEach()是一個全局前置守衛,包含三個參數(如下所示),其中to是目標路由對象、from是來源路由對象,next是一個回調函數,用於解析(resolve)當前守衛。

router.beforeEach((to, from, next) => {
  next();
});

  由於導航在所有守衛解析之前會處於等待狀態(即不會改變路徑,也不會渲染對應的組件),因此在回調中不能省略next()函數。注意,該函數不僅能處理導航,還能將其中斷或重定向,甚至註冊一個錯誤。

  beforeResolve()是一個全局解析守衛,其參數和beforeEach()的相同,在導航被確認前,並且組件內的守衛和異步路由組件被解析之後調用。afterEach()是一個全局後置守衛,它只有to和from兩個參數。

2)路由守衛

  在路由配置中可直接定義beforeEnter()守衛,作用於某個單獨的路由而非全局,其參數和beforeEach()的相同,如下所示。

const routes = [
  {
    path: "/list",
    component: List,
    beforeEnter: (to, from, next) => {
      next();
    }
  }
];

3)組件守衛

  組件內的導航守衛包含三個,如下所列,它們的參數都和beforeEach()的相同,也是to、from和next。

  (1)beforeRouteEnter():由於該組件在導航確認前被調用,因此組件還未被創建,從而訪問this無法得到組件實例。但next()函數能接收一個參數為組件實例的回調,如下所示。

const List = {
  beforeRouteEnter: function(to, from, next) {
    next(vm => { 
      //vm是組件實例
    });
  }
};

  (2)beforeRouteUpdate():可通過this得到組件實例,當路由發生變化,但復用了一個組件時,會調用該守衛。例如一個路徑為/list/:page的路由(如下配置所示),在將/list/1導航至list/2時,由於渲染的都是List組件,因此會調用beforeRouteUpdate()守衛。

const routes = [
  {
    path: "/list/:page",
    component: List
  }
];

  (3)beforeRouteLeave():也可通過this得到組件實例,當路由改變,並且渲染的組件不同時,會調用該守衛。例如有兩條路徑/main和/list,分別對應Main和List組件,在List組件中聲明了beforeRouteLeave()守衛(如下所示),當從/list導航至/main時,會彈出確認框,判斷是否需要取消本次導航。

const List = {
  beforeRouteLeave: function(to, from, next) {
    const answer = window.confirm("是否離開當前頁?");
    answer ? next() : next(false);
  }
};

4)解析流程

  完整的導航解析流程如下所列。

(1)導航被觸發。

(2)在失活的組件里調用beforeRouteLeave()守衛。

(3)調用全局的beforeEach()守衛。

(4)在重用的組件里調用beforeRouteUpdate()守衛。

(5)在路由配置里調用beforeEnter()守衛。

(6)解析異步路由組件。

(7)在被激活的組件里調用beforeRouteEnter()守衛。

(8)調用全局的beforeResolve()守衛。

(9)導航被確認。

(10)調用全局的afterEach()守衛。

(11)觸發DOM更新。

(12)用創建好的實例調用beforeRouteEnter()守衛中傳給next參數的回調函數。

 

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

【其他文章推薦】

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

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

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

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

手把手教你實現熱更新功能,帶你了解 Arthas 熱更新背後的原理

文章來源:

一、前言

一天下午正在摸魚的時候,測試小姐姐走了過來求助,說是需要改動測試環境 mock 應用。但是這個應用一時半會又找不到源代碼存在何處。但是測試小姐姐的活還是一定要幫,突然想起了 Arthas 可以熱更新應用代碼,按照網上的步驟,反編譯應用代碼,加上需要改動的邏輯,最後熱更新成功。對此,測試小姐姐很滿意,並表示下次會少提 Bug。

嘿嘿,以前一直對熱更新背後原理很好奇,藉著這個機會,研究一下熱更新的原理。

二、Arthas 熱更新

我們先來看下 Arthas 是如何熱更新的。

詳情參考:

假設我們現在有一個 HelloService 類,邏輯如下,現在我們使用 Arthas 熱更新代碼,讓其輸出 hello arthas

public class HelloService {

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

        while (true){
            TimeUnit.SECONDS.sleep(1);
            hello();
        }
    }

    public static void hello(){
        System.out.println("hello world");
    }

}

2.1、jad 反編譯代碼

首先運行 jad 命令反編譯 class 文件獲取源代碼,運行命令如下:。

jad --source-only com.andyxh.HelloService > /tmp/HelloService.java

2.2、修改反編譯之後的代碼

拿到源代碼之後,使用 VIM 等文本編輯工具編輯源代碼,加入需要改動的邏輯。

2.3、查找 ClassLoader

然後使用 sc 命令查找加載修改類的 ClassLoader,運行命令如下:

$ sc -d  com.andyxh.HelloService | grep classLoaderHash
 classLoaderHash   4f8e5cde

這裏運行之後將會得到 ClassLoader 哈希值。

2.4、 mc 內存編譯源代碼

使用 mc 命令編譯上一步修改保存的源代碼,生成最終 class 文件。

$ mc -c 4f8e5cde  /tmp/HelloService.java  -d /tmp
Memory compiler output:
/tmp/com/andyxh/HelloService.class
Affect(row-cnt:1) cost in 463 ms.

2.5、redefine 熱更新代碼

運行 redefine 命令:

$ redefine /tmp/com/andyxh/HelloService.class
redefine success, size: 1

熱更新成功之後,程序輸出結果如下:

一般情況下,我們本地將會有源代碼,上面的步驟我們可以進一步省略,我們可以先在自己 IDE 上改動代碼,編譯生成 class 文件。這樣我們只需要運行 redefine 命令即可。也就是說實際上起到作用只是 redefine

三、 Instrumentation 與 attach 機制

Arthas 熱更新功能看起來很神奇,實際上離不開 JDK 一些 API,分別為 instrument API 與 attach API。

3.1 Instrumentation

Java Instrumentation 是 JDK5 之後提供接口。使用這組接口,我們可以獲取到正在運行 JVM 相關信息,使用這些信息我們構建相關監控程序檢測 JVM。另外, 最重要我們可以替換修改類的,這樣就實現了熱更新。

Instrumentation 存在兩種使用方式,一種為 pre-main 方式,這種方式需要在虛擬機參數指定 Instrumentation 程序,然後程序啟動之前將會完成修改或替換類。使用方式如下:

java -javaagent:jar Instrumentation_jar -jar xxx.jar

有沒有覺得這種啟動方式很熟悉,仔細觀察一下 IDEA 運行輸出窗口。

另外很多應用監控工具,如:zipkin、pinpoint、skywalking。

這種方式只能在應用啟動之前生效,存在一定的局限性。

JDK6 針對這種情況作出了改進,增加 agent-main 方式。我們可以在應用啟動之後,再運行 Instrumentation 程序。啟動之後,只有連接上相應的應用,我們才能做出相應改動,這裏我們就需要使用 Java 提供 attach API。

3.2 Attach API

Attach API 位於 tools.jar 包,可以用來連接目標 JVM。Attach API 非常簡單,內部只有兩個主要的類,VirtualMachineVirtualMachineDescriptor

VirtualMachine 代表一個 JVM 實例, 使用它提供 attach 方法,我們就可以連接上目標 JVM。

 VirtualMachine vm = VirtualMachine.attach(pid);

VirtualMachineDescriptor 則是一個描述虛擬機的容器類,通過該實例我們可以獲取到 JVM PID(進程 ID),該實例主要通過 VirtualMachine#list 方法獲取。

        for (VirtualMachineDescriptor descriptor : VirtualMachine.list()){

            System.out.println(descriptor.id());
        }

介紹完熱更新涉及的相關原理,接下去使用上面 API 實現熱更新功能。

四、實現熱更新功能

這裏我們使用 Instrumentation agent-main 方式。

4.1、實現 agent-main

首先需要編寫一個類,包含以下兩個方法:

public static void agentmain (String agentArgs, Instrumentation inst);          [1]
public static void agentmain (String agentArgs);            [2]

上面的方法只需要實現一個即可。若兩個都實現, [1] 優先級大於 [2],將會被優先執行。

接着讀取外部傳入 class 文件,調用 Instrumentation#redefineClasses,這個方法將會使用新 class 替換當前正在運行的 class,這樣我們就完成了類的修改。

public class AgentMain {
    /**
     *
     * @param agentArgs 外部傳入的參數,類似於 main 函數 args
     * @param inst
     */
    public static void agentmain(String agentArgs, Instrumentation inst) {
        // 從 agentArgs 獲取外部參數
        System.out.println("開始熱更新代碼");
        // 這裏將會傳入 class 文件路徑
        String path = agentArgs;
        try {
            // 讀取 class 文件字節碼
            RandomAccessFile f = new RandomAccessFile(path, "r");
            final byte[] bytes = new byte[(int) f.length()];
            f.readFully(bytes);
            // 使用 asm 框架獲取類名
            final String clazzName = readClassName(bytes);

            // inst.getAllLoadedClasses 方法將會獲取所有已加載的 class
            for (Class clazz : inst.getAllLoadedClasses()) {
                // 匹配需要替換 class
                if (clazz.getName().equals(clazzName)) {
                    ClassDefinition definition = new ClassDefinition(clazz, bytes);
                    // 使用指定的 class 替換當前系統正在使用 class
                    inst.redefineClasses(definition);
                }
            }

        } catch (UnmodifiableClassException | IOException | ClassNotFoundException e) {
            System.out.println("熱更新數據失敗");
        }


    }

    /**
     * 使用 asm 讀取類名
     *
     * @param bytes
     * @return
     */
    private static String readClassName(final byte[] bytes) {
        return new ClassReader(bytes).getClassName().replace("/", ".");
    }
}

完成代碼之後,我們還需要往 jar 包 manifest 寫入以下屬性。

## 指定 agent-main 全名
Agent-Class: com.andyxh.AgentMain
## 設置權限,默認為 false,沒有權限替換 class
Can-Redefine-Classes: true

我們使用 maven-assembly-plugin,將上面的屬性寫入文件中。

<plugin>
    <artifactId>maven-assembly-plugin</artifactId>
    <version>3.1.0</version>
    <configuration>
        <!--指定最後產生 jar 名字-->
        <finalName>hotswap-jdk</finalName>
        <appendAssemblyId>false</appendAssemblyId>
        <descriptorRefs>
            <!--將工程依賴 jar 一塊打包-->
            <descriptorRef>jar-with-dependencies</descriptorRef>
        </descriptorRefs>
        <archive>
            <manifestEntries>
                <!--指定 class 名字-->
                <Agent-Class>
                    com.andyxh.AgentMain
                </Agent-Class>
                <Can-Redefine-Classes>
                    true
                </Can-Redefine-Classes>
            </manifestEntries>
            <manifest>
                <!--指定 mian 類名字,下面將會使用到-->
                <mainClass>com.andyxh.JvmAttachMain</mainClass>
            </manifest>
        </archive>
    </configuration>
    <executions>
        <execution>
            <id>make-assembly</id> <!-- this is used for inheritance merges -->
            <phase>package</phase> <!-- bind to the packaging phase -->
            <goals>
                <goal>single</goal>
            </goals>
        </execution>
    </executions>
</plugin>

到這裏我們就完成熱更新主要代碼,接着使用 Attach API,連接目標虛擬機,觸發熱更新的代碼。

public class JvmAttachMain {
    public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
        // 輸入參數,第一個參數為需要 Attach jvm pid 第二參數為 class 路徑
        if(args==null||args.length<2){
            System.out.println("請輸入必要參數,第一個參數為 pid,第二參數為 class 絕對路徑");
            return;
        }
        String pid=args[0];
        String classPath=args[1];
        System.out.println("當前需要熱更新 jvm pid 為 "+pid);
        System.out.println("更換 class 絕對路徑為 "+classPath);
        // 獲取當前 jar 路徑
        URL jarUrl=JvmAttachMain.class.getProtectionDomain().getCodeSource().getLocation();
        String jarPath=jarUrl.getPath();

        System.out.println("當前熱更新工具 jar 路徑為 "+jarPath);
        VirtualMachine vm = VirtualMachine.attach(pid);//7997是待綁定的jvm進程的pid號
        // 運行最終 AgentMain 中方法
        vm.loadAgent(jarPath, classPath);
    }
}

在這個啟動類,我們最終調用 VirtualMachine#loadAgent,JVM 將會使用上面 AgentMain 方法使用傳入 class 文件替換正在運行 class。

4.2、運行

這裏我們繼續開頭使用的例子,不過這裏加入一個方法獲取 JVM 運行進程 ID。

public class HelloService {

    public static void main(String[] args) throws InterruptedException {
        System.out.println(getPid());
        while (true){
            TimeUnit.SECONDS.sleep(1);
            hello();
        }
    }

    public static void hello(){
        System.out.println("hello world");
    }

    /**
     * 獲取當前運行 JVM PID
     * @return
     */
    private static String getPid() {
        // get name representing the running Java virtual machine.
        String name = ManagementFactory.getRuntimeMXBean().getName();
        System.out.println(name);
        // get pid
        return name.split("@")[0];
    }

}

首先運行 HelloService,獲取當前 PID,接着複製 HelloService 代碼到另一個工程,修改 hello 方法輸出 hello agent,重新編譯生成新的 class 文件。

最後在命令行運行生成的 jar 包。

HelloService 輸出效果如下所示:

4.3、調試技巧

普通的應用我們可以在 IDE 直接使用 Debug 模式調試程序,但是上面的程序無法直接使用 Debug。剛開始運行的程序碰到很多問題,無奈之下,只能選擇最原始的辦法,打印錯誤日誌。後來查看 arthas 的文檔,發現上面一篇文章介紹使用 IDEA Remote Debug 模式調試程序。

首先我們需要在 HelloService JVM 參數加入以下參數:

-Xrunjdwp:transport=dt_socket,server=y,address=8001  

此時程序將會被阻塞,直到遠程調試程序連接上 8001 端口,輸出如下:

然後在 Agent-main 這個工程增加一個 remote 調試。

圖中參數如下:

-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8001

Agent-main 工程打上斷點,運行遠程調試, HelloService 程序將會被啟動。

最後在命令行窗口運行 Agent-main 程序,遠程調試將會暫停到相應斷點處,接下來調試就跟普通 Debug 模式一樣,不再敘述。

4.4、相關問題

由於 Attach API 位於 tools.jar 中,而在 JDK8 之前 tools.jar 與我們常用JDK jar 包並不在同一個位置,所以編譯與運行過程可能找不到該 jar 包,從而導致報錯。

如果 maven 編譯與運行都使用 JDK9 之後,不用擔心下面問題。

maven 編譯問題

maven 編譯過程可能發生如下錯誤。

解決辦法為在 pom 下加入 tools.jar 。

        <dependency>
            <groupId>jdk.tools</groupId>
            <artifactId>jdk.tools</artifactId>
            <scope>system</scope>
            <version>1.6</version>
            <systemPath>${java.home}/../lib/tools.jar</systemPath>
        </dependency>

或者使用下面依賴。

        <dependency>
            <groupId>com.github.olivergondza</groupId>
            <artifactId>maven-jdk-tools-wrapper</artifactId>
            <version>0.1</version>
            <scope>provided</scope>
            <optional>true</optional>
        </dependency>

程序運行過程 tools.jar 找不到

運行程序時拋出 java.lang.NoClassDefFoundError,主要原因還是系統未找到 tools.jar 導致。

在運行參數加入 -Xbootclasspath/a:${java_home}/lib/tools.jar,完整運行命令如下:

4.5、熱更新存在一些限制

並不是所有改動熱更新都將會成功,當前使用 Instrumentation#redefineClasses 還是存在一些限制。我們僅只能修改方法內部邏輯,屬性值等,不能添加,刪除方法或字段,也不能更改方法的簽名或繼承關係。

五、彩蛋

寫完熱更新代碼,收到一封系統郵件提示 xxx bug 待修復。恩,說好的少提 Bug 呢 o(╥﹏╥)o。

六、幫助

歡迎關注我的公眾號:程序通事,獲得日常乾貨推送。如果您對我的專題內容感興趣,也可以關注我的博客:

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

【其他文章推薦】

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

※評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

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

一個excel(20M)就能幹趴你的poi,你信嗎?

  自從上一篇:后,很巧的是這次又發現一個問題,所以有了這篇文章,還是想沿用上篇的”流水帳“的方式查找問題和解決問題。這篇文章主要是因為使用POI導入一個20M的excel發生了OOM(OutOfMemoryError)異常。說來也慚愧,工作了這麼多年,還真沒導入過這種大小的文件,並且還發生了內存溢出。如果你百度下基本上清一色的告訴你:POI導入excel文件有兩種方式,第一種是用戶模式,這種模式用起來很簡單直觀,可以類比xml的dom方式解析(這裏只針對excel2007,因為2003本身就有最大條數限制並且目前基本用的很少,這裏直接忽略),第二種是event模式,這種通常是網上說的解決POI導入大excel的”萬金油“方法,可以類比為xml的sax解析方式。呵呵,我這篇文章首先就是要干趴這種方法(JVM使用-Xms512m -Xmx512m)。不信你隨便寫一個導入接口,導入如下20M大小的execl看看:鏈接: https://pan.baidu.com/s/1DUrS8ctLPp7Z6imOc1aIUQ 提取碼: hd79 。

  首先,既然要導入大點的excel2007,那麼我們應該稍微了解一下這種文件如何存儲數據,我們百度上可以發現,2007其實就是一個壓縮包,可以直接修改後綴成zip然後解壓打開文件看看,如下

 

  上圖可以看到最大的兩個文件就兩個:sharedStrings.xml和sheet1.xml。其中sheet2.xml這個可以不關注,直接從excel刪掉都沒事,這裏沒刪除主要是沒多大關係,這個excel文件也是測試直接提供給我的。由於sheet2比較小,與這個文章說到的內存溢出並無關係,請不要胡思亂想,被分散了注意。

  直接用大文本編輯工具打開上圖兩個大文件,可以發現sharedString.xml里內容其實就是excel中每個單元格里的字符串內容(数字類型除外),sheet.xml就是每個sheet里的結構xml,了解到這裏基本上就了解了本文章說到問題的基本知識,然後下面進入正題。

  先使用百度中查到的提供的event方式導入excel,代碼如下:

package com.example.utils;

import org.apache.poi.openxml4j.opc.OPCPackage;
import org.apache.poi.xssf.eventusermodel.ReadOnlySharedStringsTable;
import org.apache.poi.xssf.eventusermodel.XSSFReader;
import org.apache.poi.xssf.usermodel.XSSFRichTextString;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;
import org.xml.sax.helpers.XMLReaderFactory;

import java.io.File;
import java.io.InputStream;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

/**
 * 百度上直接copy過來的
 * XSSF and SAX (Event API)
 */
public abstract class BigDataParseExcelUtil extends DefaultHandler {
    private ReadOnlySharedStringsTable sst;
    private String lastContents;
    private boolean nextIsString;
    private int sheetIndex = -1;
    private List<String> rowlist = new ArrayList<String>();
    private int curRow = 0; // 當前行
    private int curCol = 0; // 當前列索引
    private int preCol = 0; // 上一列列索引
    private int titleRow = 0; // 標題行,一般情況下為0
    private int rowsize = 0; // 列數
    private List excelList = new ArrayList();  //excel全部轉換為list

    // excel記錄行操作方法,以sheet索引,行索引和行元素列表為參數,對sheet的一行元素進行操作,元素為String類型

    public abstract void optRows(int sheetIndex, int curRow,
                                 List<String> rowlist, List excelList) throws SQLException, Exception;

    // 只遍歷一個sheet,其中sheetId為要遍歷的sheet索引,從1開始,1-3

    /**
     * @param filename
     * @param sheetId  sheetId為要遍歷的sheet索引,從1開始,1-3
     * @throws Exception
     */
    public void processOneSheet(String filename, int sheetId) throws Exception {
        OPCPackage pkg = OPCPackage.open(filename);
        XSSFReader r = new XSSFReader(pkg);
        ReadOnlySharedStringsTable strings = new ReadOnlySharedStringsTable(pkg);
        XMLReader parser = fetchSheetParser(strings);
        // rId2 found by processing the Workbook
        // 根據 rId# 或 rSheet# 查找sheet
        InputStream sheet2 = r.getSheet("rId" + sheetId);
        sheetIndex++;
        InputSource sheetSource = new InputSource(sheet2);
        parser.parse(sheetSource);
        sheet2.close();
    }

    @Override
    public void characters(char[] ch, int start, int length)
        throws SAXException {
        // 得到單元格內容的值
        lastContents += new String(ch, start, length);
    }

    public void process(InputStream inputStream) throws Exception {
        OPCPackage pkg = OPCPackage.open(inputStream);
        XSSFReader r = new XSSFReader(pkg);
        ReadOnlySharedStringsTable strings = new ReadOnlySharedStringsTable(pkg);
        XMLReader parser = fetchSheetParser(strings);
        Iterator<InputStream> sheets = r.getSheetsData();
        while (sheets.hasNext()) {
            curRow = 0;
            sheetIndex++;
            InputStream sheet = sheets.next();
            InputSource sheetSource = new InputSource(sheet);
            parser.parse(sheetSource);
            sheet.close();
        }
    }

    /**
     * 遍歷 excel 文件
     */
    public void process(File file) throws Exception {
        OPCPackage pkg = OPCPackage.open(file);
        XSSFReader r = new XSSFReader(pkg);
        ReadOnlySharedStringsTable strings = new ReadOnlySharedStringsTable(pkg);
        XMLReader parser = fetchSheetParser(strings);
        Iterator<InputStream> sheets = r.getSheetsData();
        while (sheets.hasNext()) {
            curRow = 0;
            sheetIndex++;
            InputStream sheet = sheets.next();
            InputSource sheetSource = new InputSource(sheet);
            parser.parse(sheetSource);
            sheet.close();
        }
    }

    public XMLReader fetchSheetParser(ReadOnlySharedStringsTable sst)
        throws SAXException {
        XMLReader parser = XMLReaderFactory.createXMLReader();
        // .createXMLReader("org.apache.xerces.parsers.SAXParser");
        this.sst = sst;
        parser.setContentHandler(this);
        return parser;
    }

    @Override
    public void startElement(String uri, String localName, String name,
                             Attributes attributes) throws SAXException {
        // c => 單元格
        if (name.equals("c")) {
            // 如果下一個元素是 SST 的索引,則將nextIsString標記為true
            String cellType = attributes.getValue("t");
            String rowStr = attributes.getValue("r");
            curCol = this.getRowIndex(rowStr);
            if (cellType != null && cellType.equals("s")) {
                nextIsString = true;
            } else {
                nextIsString = false;
            }
        }
        // 置空
        lastContents = "";
    }

    @Override
    public void endElement(String uri, String localName, String name)
        throws SAXException {
        // 根據SST的索引值的到單元格的真正要存儲的字符串
        // 這時characters()方法可能會被調用多次
        if (nextIsString) {
            try {
                int idx = Integer.parseInt(lastContents);
                lastContents = new XSSFRichTextString(sst.getEntryAt(idx))
                    .toString();
            } catch (Exception e) {
            }
        }
        // v => 單元格的值,如果單元格是字符串則v標籤的值為該字符串在SST中的索引
        // 將單元格內容加入rowlist中,在這之前先去掉字符串前後的空白符
        if (name.equals("v")) {
            String value = lastContents.trim();
            value = value.equals("") ? " " : value;
            int cols = curCol - preCol;
            if (cols > 1) {
                for (int i = 0; i < cols - 1; i++) {
                    rowlist.add(preCol, "");
                }
            }
            preCol = curCol;
            rowlist.add(curCol - 1, value);
        } else {
            // 如果標籤名稱為 row ,這說明已到行尾,調用 optRows() 方法
            if (name.equals("row")) {
                int tmpCols = rowlist.size();
                if (curRow > this.titleRow && tmpCols < this.rowsize) {
                    for (int i = 0; i < this.rowsize - tmpCols; i++) {
                        rowlist.add(rowlist.size(), "");
                    }
                }
                try {
                    optRows(sheetIndex, curRow, rowlist, excelList);
                } catch (SQLException e) {
                    e.printStackTrace();
                } catch (Exception e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
                if (curRow == this.titleRow) {
                    this.rowsize = rowlist.size();
                }
                rowlist.clear();
                curRow++;
                curCol = 0;
                preCol = 0;
            }
        }
    }

    // 得到列索引,每一列c元素的r屬性構成為字母加数字的形式,字母組合為列索引,数字組合為行索引,
    // 如AB45,表示為第(A-A+1)*26+(B-A+1)*26列,45行
    public int getRowIndex(String rowStr) {
        rowStr = rowStr.replaceAll("[^A-Z]", "");
        byte[] rowAbc = rowStr.getBytes();
        int len = rowAbc.length;
        float num = 0;
        for (int i = 0; i < len; i++) {
            num += (rowAbc[i] - 'A' + 1) * Math.pow(26, len - i - 1);
        }
        return (int) num;
    }


}
package com.example.service;

import com.example.utils.BigDataParseExcelUtil;
import org.springframework.stereotype.Service;

import java.io.InputStream;
import java.sql.SQLException;
import java.util.List;

/**
 * @author: rongdi
 * @date:
 */
@Service
public class ExcelService {

    public void import1(InputStream inputStream) throws Exception {

        BigDataParseExcelUtil xlx = new BigDataParseExcelUtil() {
            @Override
            public void optRows(int sheetIndex, int curRow, List<String> rowlist, List excelList)
                throws SQLException {
                System.out.println(rowlist);
            }
        };
        xlx.process(inputStream);
    }


}
package com.example.controller;

import com.example.service.ExcelService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;

/**
 * @author: rongdi
 * @date:
 */
@Controller
public class ExcelController {

    @Autowired
    private ExcelService excelService;

    @RequestMapping("/excel/import1")
    @ResponseBody
    public String import1(@RequestParam("file") MultipartFile multipartFile) throws Exception {
        excelService.import1(multipartFile.getInputStream());
        return "ok";
    }

}

  使用postman等工具,導入上面說的20M的文件,報錯如下:

   那我們優化一下不使用inputStream,直接使用一個File傳入看看

    public void import2(File file) throws Exception {
        BigDataParseExcelUtil xlx = new BigDataParseExcelUtil() {
            @Override
            public void optRows(int sheetIndex, int curRow, List<String> rowlist, List excelList)
                throws SQLException {
                System.out.println(rowlist);
            }
        };
        xlx.process(file);
    }

    @RequestMapping("/excel/import2")
    @ResponseBody
    public String import2(@RequestParam("file") MultipartFile multipartFile) throws Exception {
        // 延遲解析比率
        ZipSecureFile.setMinInflateRatio(-1.0d);
        File tmp = Files.createTempFile("tmp-", ".xlsx").toFile();
        Files.copy(multipartFile.getInputStream(), Paths.get(tmp.getPath()), StandardCopyOption.REPLACE_EXISTING);
        excelService.import2(tmp);
        return "ok";
    }

  使用postman上傳文件運行效果如下:

 

   這時候就發現很尷尬了,難道是POI自己代碼里就有bug,我們可以使用斷點調試確認一下這個strings里到底是不是全部放了sharedStrings.xml里的內容。

   由上證明就是這個strings裝多了導致內存溢出了,從這裏可以看出網上說使用event事件解析excel的方案基本都是行不通的,哎,我也不懂為啥百度上都是這種答案,難道他們壓根都沒遇到過大數據導入嗎?當然也有可能我冤枉了他們,因為sharedStrings.xml中存放的是每個單元格的字符串內容,這個存放是排重過的,如果雖然excel里單元格很多,但是大多都是整型或者大多都是重複的,那確實可以跳過這一步一路之後會一路暢通了,因為畢竟sax解析xml確實可以節省很多內存。

  從上分析可以看到POI就兩種方式導入:一種是用戶方式寫代碼簡單,基本按順序數格子就好,但是類比dom方式解析xml,很耗內存。第二種事件方式,類比sax方式解析xml確實很省內存,但是POI提供的類里把解析出的大量字符串放入了集合中,還是會導致內存溢出。那麼我們怎麼解決這個問題,這裏很常規的想法是到底這個strings是用來幹啥的,怎麼用的,如果可以保持strings相同邏輯功能的前提下,修改了ReadOnlySharedStringsTable這個類的邏輯,就可以解決這裏的內存溢出了。那麼我們可以直接搜索ReadOnlySharedStringsTable類里所有用到strings的方法上打上斷點,特別是從strings里取值的方法上,然後調大jvm內存避免內存溢出的情況下斷點調試如下

   我們是不是可以直接往strings里添加字符串和獲取字符串的方法那裡替換掉,不要使用strings這個集合存儲所有字符串。但是既然excel里設計成使用一個sharedStrings.xml存放公共的字符串,而不是像csv格式那樣,每次讀一行取一行數據就好了。那麼這個sharedStrings.xml中的數據總要解析出來,總要有個地方存儲裏面的數據,不然怎麼結合sheet.xml的格式獲取到每一行的數據呢?所以這裏就很尷尬了,不能每次解析sharedStrings.xml時不保存每次需要獲取strings的時候,再去解析一下這個xm吧,如果從本文章的xml上來看,要重複解析25W次,效率極其低。現在問題可以簡化成我們需要把sharedStrings.xml解析出的所有字符串放在一個地方,還能方便解析,由於怕內存溢出,肯定不能放在內存中了。那麼這裏就有一些選擇,比如解析出的字符串按加入strings集合的順序放入數據庫,文件,外部存儲或者緩存(限制內存大小,多餘寫入文件)存儲中。然後使用的時候按照索引位置idx去一一取出。本文章先使用臨時文件來放這些數據,因為不想搞那麼複雜,導入任務不管再多複雜的系統中,最終執行的都會是一個單節點,在單節點中先使用本機資源這種就近資源是最方便的。如下直接先複製源碼,然後修改上述說的兩個地方。

package com.example.utils;

import org.apache.poi.ooxml.util.SAXHelper;
import org.apache.poi.openxml4j.opc.OPCPackage;
import org.apache.poi.openxml4j.opc.PackagePart;
import org.apache.poi.ss.usermodel.RichTextString;
import org.apache.poi.util.Removal;
import org.apache.poi.xssf.model.SharedStrings;
import org.apache.poi.xssf.usermodel.XSSFRelation;
import org.apache.poi.xssf.usermodel.XSSFRichTextString;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;

import javax.xml.parsers.ParserConfigurationException;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.LineNumberReader;
import java.io.PushbackInputStream;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import static org.apache.poi.xssf.usermodel.XSSFRelation.NS_SPREADSHEETML;

public class ReadOnlySharedStringsTable extends DefaultHandler implements SharedStrings {

    protected final boolean includePhoneticRuns;

    /**
     * An integer representing the total count of strings in the workbook. This count does not
     * include any numbers, it counts only the total of text strings in the workbook.
     */
    protected int count;

    /**
     * An integer representing the total count of unique strings in the Shared String Table.
     * A string is unique even if it is a copy of another string, but has different formatting applied
     * at the character level.
     */
    protected int uniqueCount;

    /**
     * The shared strings table.
     */
    private List<String> strings;

    private File tmp = null;

    FileOutputStream fos = null;

    private int counts;

    private Map<Integer,String> map = new LinkedHashMap<Integer,String>();

    public ReadOnlySharedStringsTable(OPCPackage pkg)
            throws IOException, SAXException {
        this(pkg, true);
    }

    public ReadOnlySharedStringsTable(OPCPackage pkg, boolean includePhoneticRuns)
            throws IOException, SAXException {
        this.includePhoneticRuns = includePhoneticRuns;
        ArrayList<PackagePart> parts =
                pkg.getPartsByContentType(XSSFRelation.SHARED_STRINGS.getContentType());

        // Some workbooks have no shared strings table.
        if (parts.size() > 0) {
            PackagePart sstPart = parts.get(0);
            readFrom(sstPart.getInputStream());
        }
    }

    /**
     * Like POIXMLDocumentPart constructor
     *
     * Calls {@link #ReadOnlySharedStringsTable(PackagePart, boolean)}, with a
     * value of <code>true</code> to include phonetic runs.
     *
     * @since POI 3.14-Beta1
     */
    public ReadOnlySharedStringsTable(PackagePart part) throws IOException, SAXException {
        this(part, true);
    }

    /**
     * Like POIXMLDocumentPart constructor
     *
     * @since POI 3.14-Beta3
     */
    public ReadOnlySharedStringsTable(PackagePart part, boolean includePhoneticRuns)
        throws IOException, SAXException {
        this.includePhoneticRuns = includePhoneticRuns;
        readFrom(part.getInputStream());
    }
    
    /**
     * Read this shared strings table from an XML file.
     *
     * @param is The input stream containing the XML document.
     * @throws IOException if an error occurs while reading.
     * @throws SAXException if parsing the XML data fails.
     */
    public void readFrom(InputStream is) throws IOException, SAXException {
        // test if the file is empty, otherwise parse it
        PushbackInputStream pis = new PushbackInputStream(is, 1);
        int emptyTest = pis.read();
        if (emptyTest > -1) {
            pis.unread(emptyTest);
            InputSource sheetSource = new InputSource(pis);
            try {
                XMLReader sheetParser = SAXHelper.newXMLReader();
                sheetParser.setContentHandler(this);
                sheetParser.parse(sheetSource);
            } catch(ParserConfigurationException e) {
                throw new RuntimeException("SAX parser appears to be broken - " + e.getMessage());
            }
        }
    }

    /**
     * Return an integer representing the total count of strings in the workbook. This count does not
     * include any numbers, it counts only the total of text strings in the workbook.
     *
     * @return the total count of strings in the workbook
     */
    @Override
    public int getCount() {
        return this.count;
    }

    /**
     * Returns an integer representing the total count of unique strings in the Shared String Table.
     * A string is unique even if it is a copy of another string, but has different formatting applied
     * at the character level.
     *
     * @return the total count of unique strings in the workbook
     */
    @Override
    public int getUniqueCount() {
        return this.uniqueCount;
    }

    /**
     * Return the string at a given index.
     * Formatting is ignored.
     *
     * @param idx index of item to return.
     * @return the item at the specified position in this Shared String table.
     * @deprecated use <code>getItemAt</code> instead
     */
    @Removal(version = "4.2")
    @Deprecated
    public String getEntryAt(int idx) {
        /**
         * 這裏就是修改部分了,直接從按行存儲的臨時文件讀取需要的字符串
         */
        String value = map.get(idx + 1);
        if(value == null) {

            return readString(idx,1000,this.uniqueCount);
        } else {
            return value;
        }

    }

    /**
     * 從指定位置讀取size個字符串,這裡是使用局部性原理,每次讀取size個字符串,
     * 以免每次需要讀取文件,性能極低
     * @return
     */
    private String readString(int idx,int size,int numbers) {
        map.clear();
        int currNumber = idx + 1;
        if (currNumber < 0 || currNumber > numbers) {
            return null;
        }
        try {
            FileReader in = new FileReader(tmp);
            LineNumberReader reader = new LineNumberReader(in);
            try {
                String line = "";
                for(int i = 1;i <= numbers;i ++) {
                    line = reader.readLine();
                    if(i >= currNumber && i < currNumber + size) {
                        map.put(i, line);
                    }
                }
            } finally {
                reader.close();
                in.close();
            }
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }
        return map.get(idx + 1);
    }


    /**
     * Returns all the strings.
     * Formatting is ignored.
     *
     * @return a list with all the strings
     * @deprecated use <code>getItemAt</code> instead
     */
    @Removal(version = "4.2")
    @Deprecated
    public List<String> getItems() {
        return strings;
    }

    @Override
    public RichTextString getItemAt(int idx) {
        return new XSSFRichTextString(getEntryAt(idx));
    }

    //// ContentHandler methods ////

    private StringBuilder characters;
    private boolean tIsOpen;
    private boolean inRPh;

    @Override
    public void startElement(String uri, String localName, String name,
                             Attributes attributes) throws SAXException {
        if (uri != null && ! uri.equals(NS_SPREADSHEETML)) {
            return;
        }

        if ("sst".equals(localName)) {
            String count = attributes.getValue("count");
            if(count != null) this.count = Integer.parseInt(count);
            String uniqueCount = attributes.getValue("uniqueCount");
            if(uniqueCount != null) this.uniqueCount = Integer.parseInt(uniqueCount);
            try {
                tmp = Files.createTempFile("tmp-", ".xlsx").toFile();
            } catch (IOException e) {
                e.printStackTrace();
            }
            //    this.strings = new ArrayList<>(this.uniqueCount);
            characters = new StringBuilder(64);
            try {
                fos = new FileOutputStream(tmp,true);
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            }
        } else if ("si".equals(localName)) {
            characters.setLength(0);
        } else if ("t".equals(localName)) {
            tIsOpen = true;
        } else if ("rPh".equals(localName)) {
            inRPh = true;
            //append space...this assumes that rPh always comes after regular <t>
            if (includePhoneticRuns && characters.length() > 0) {
                characters.append(" ");
            }
        }
    }

    @Override
    public void endElement(String uri, String localName, String name) throws SAXException {
        if (uri != null && ! uri.equals(NS_SPREADSHEETML)) {
            return;
        }

        if ("si".equals(localName)) {
         //   strings.add(characters.toString().intern());
            try {
                /**
                 * 這裏就是修改的一部分,這裏直接把字符串按行存入臨時文件
                 */
                counts ++;
                fos.write((characters.toString() + "\n").getBytes());
                if(counts == this.uniqueCount) {
                    fos.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        } else if ("t".equals(localName)) {
            tIsOpen = false;
        } else if ("rPh".equals(localName)) {
            inRPh = false;
        }
    }

    /**
     * Captures characters only if a t(ext) element is open.
     */
    @Override
    public void characters(char[] ch, int start, int length) throws SAXException {
        if (tIsOpen) {
            if (inRPh && includePhoneticRuns) {
                characters.append(ch, start, length);
            } else if (! inRPh){
                characters.append(ch, start, length);
            }
        }
    }

}

  然後在自己代碼里把包換成自己的包,替換POI里該類的包,運行JVM堆情況如下毫無壓力

  自此內存溢出問題大功告成!針對使用POI導入大Excel遇到的問題總結如下:

  1)網上給出的方案不管是用戶模式還是事件模式,往往都不能支持大excel的導入

  2)excel本質上是一堆excel的壓縮包(這裏只考慮2007忽略2003)改了個後綴名成xlsx

  3)使用事件導入時應先將上傳文件存入文件,再使用文件OPCPackage.open(file),如果直接傳入輸入流,由於裏面邏輯會將輸入流的所有內容先存入ByteArrayOutputStream 中,這個輸出流實際上是一個內存中的字節流,所以也會導致內存溢出。

  4)用戶模式不用考慮,事件模式會先將sharedString.xml這個大xml解析出來放入一個List中,不管通過什麼方式都繞不開需要解析這個類,因為每個單元格的字符串都放在這個xml中,而要解析這個xml最常規的方法就是保存在內存使用list和map之內的容器存放我相信不會有人會想剛解析出一個xml還要存迴文件中把,這裏基本就繞不開ReadOnlySharedStringsTable或者SharedStringsTable,就算你僥倖繞開了,想自己解析,或許還是重複這兩個類的悲劇,這就是另外一種內存溢出的根源。

  回顧一下上述實現直接把sharedStrings.xml中的內容粗略的保存到文件中,然後再從文件中獲取是屬於很低劣的實現,只能說能滿足不內存溢出,性能方面堪憂!下面直接借鑒easyexcel源碼中用到的ReadCache來實現保存sharedStrings.xml中的內容

package com.example.advanceevent;

import com.example.utils.FileUtils;
import org.ehcache.Cache;
import org.ehcache.CacheManager;
import org.ehcache.config.CacheConfiguration;
import org.ehcache.config.builders.CacheConfigurationBuilder;
import org.ehcache.config.builders.CacheManagerBuilder;
import org.ehcache.config.builders.ResourcePoolsBuilder;
import org.ehcache.config.units.MemoryUnit;
import org.ehcache.core.Ehcache;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.util.HashMap;
import java.util.UUID;

/**
 * @author: rongdi
 * @date:
 */
public class ReadCache {

    private static final Logger LOGGER = LoggerFactory.getLogger(Ehcache.class);
    private int index = 0;
    private HashMap<Integer, String> dataMap = new HashMap(1334);
    private static CacheManager fileCacheManager;
    private static CacheConfiguration<Integer, HashMap> fileCacheConfiguration;
    private static CacheManager activeCacheManager;
    private CacheConfiguration<Integer, HashMap> activeCacheConfiguration;
    private Cache<Integer, HashMap> fileCache;
    private Cache<Integer, HashMap> activeCache;
    private String cacheAlias;
    private int cacheMiss = 0;

    public ReadCache(int maxCacheActivateSize) {
        this.activeCacheConfiguration = CacheConfigurationBuilder.newCacheConfigurationBuilder(Integer.class, HashMap.class, ResourcePoolsBuilder.newResourcePoolsBuilder().heap((long)maxCacheActivateSize, MemoryUnit.MB)).withSizeOfMaxObjectGraph(1000000L).withSizeOfMaxObjectSize((long)maxCacheActivateSize, MemoryUnit.MB).build();
        init();
    }

    private void init() {
        this.cacheAlias = UUID.randomUUID().toString();
        this.fileCache = fileCacheManager.createCache(this.cacheAlias, fileCacheConfiguration);
        this.activeCache = activeCacheManager.createCache(this.cacheAlias, this.activeCacheConfiguration);
    }

    public void put(String value) {
        this.dataMap.put(this.index, value);
        if ((this.index + 1) % 1000 == 0) {
            this.fileCache.put(this.index / 1000, this.dataMap);
            this.dataMap = new HashMap(1334);
        }

        ++this.index;
        if (LOGGER.isDebugEnabled() && this.index % 1000000 == 0) {
            LOGGER.debug("Already put :{}", this.index);
        }

    }

    public String get(Integer key) {
        if (key != null && key >= 0) {
            int route = key / 1000;
            HashMap<Integer, String> dataMap = (HashMap)this.activeCache.get(route);
            if (dataMap == null) {
                dataMap = (HashMap)this.fileCache.get(route);
                this.activeCache.put(route, dataMap);
                if (LOGGER.isDebugEnabled() && this.cacheMiss++ % 1000 == 0) {
                    LOGGER.debug("Cache misses count:{}", this.cacheMiss);
                }
            }

            return (String)dataMap.get(key);
        } else {
            return null;
        }
    }

    public void putFinished() {
        if (this.dataMap != null) {
            this.fileCache.put(this.index / 1000, this.dataMap);
        }
    }

    public void destroy() {
        fileCacheManager.removeCache(this.cacheAlias);
        activeCacheManager.removeCache(this.cacheAlias);
    }

    static {
        File cacheFile = FileUtils.createCacheTmpFile();
        fileCacheManager = CacheManagerBuilder.newCacheManagerBuilder().with(CacheManagerBuilder.persistence(cacheFile)).build(true);
        activeCacheManager = CacheManagerBuilder.newCacheManagerBuilder().build(true);
        fileCacheConfiguration = CacheConfigurationBuilder.newCacheConfigurationBuilder(Integer.class, HashMap.class, ResourcePoolsBuilder.newResourcePoolsBuilder().disk(10L, MemoryUnit.GB)).withSizeOfMaxObjectGraph(1000000L).withSizeOfMaxObjectSize(10L, MemoryUnit.GB).build();
    }

}
package com.example.advanceevent;

import org.apache.poi.ooxml.util.SAXHelper;
import org.apache.poi.openxml4j.opc.OPCPackage;
import org.apache.poi.openxml4j.opc.PackagePart;
import org.apache.poi.ss.usermodel.RichTextString;
import org.apache.poi.util.Removal;
import org.apache.poi.xssf.model.SharedStrings;
import org.apache.poi.xssf.usermodel.XSSFRelation;
import org.apache.poi.xssf.usermodel.XSSFRichTextString;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;

import javax.xml.parsers.ParserConfigurationException;
import java.io.IOException;
import java.io.InputStream;
import java.io.PushbackInputStream;
import java.util.ArrayList;
import java.util.List;

import static org.apache.poi.xssf.usermodel.XSSFRelation.NS_SPREADSHEETML;

public class ReadOnlySharedStringsTable extends DefaultHandler implements SharedStrings {

    protected final boolean includePhoneticRuns;

    /**
     * An integer representing the total count of strings in the workbook. This count does not
     * include any numbers, it counts only the total of text strings in the workbook.
     */
    protected int count;

    /**
     * An integer representing the total count of unique strings in the Shared String Table.
     * A string is unique even if it is a copy of another string, but has different formatting applied
     * at the character level.
     */
    protected int uniqueCount;

    /**
     * 緩存
     */
    ReadCache readCache = new ReadCache(100);

    private int counts;


    public ReadOnlySharedStringsTable(OPCPackage pkg)
            throws IOException, SAXException {
        this(pkg, true);
    }

    public ReadOnlySharedStringsTable(OPCPackage pkg, boolean includePhoneticRuns)
            throws IOException, SAXException {
        this.includePhoneticRuns = includePhoneticRuns;
        ArrayList<PackagePart> parts =
                pkg.getPartsByContentType(XSSFRelation.SHARED_STRINGS.getContentType());

        // Some workbooks have no shared strings table.
        if (parts.size() > 0) {
            PackagePart sstPart = parts.get(0);
            readFrom(sstPart.getInputStream());
        }
    }

    /**
     * Like POIXMLDocumentPart constructor
     *
     * Calls {@link #ReadOnlySharedStringsTable(PackagePart, boolean)}, with a
     * value of <code>true</code> to include phonetic runs.
     *
     * @since POI 3.14-Beta1
     */
    public ReadOnlySharedStringsTable(PackagePart part) throws IOException, SAXException {
        this(part, true);
    }

    /**
     * Like POIXMLDocumentPart constructor
     *
     * @since POI 3.14-Beta3
     */
    public ReadOnlySharedStringsTable(PackagePart part, boolean includePhoneticRuns)
        throws IOException, SAXException {
        this.includePhoneticRuns = includePhoneticRuns;
        readFrom(part.getInputStream());
    }
    
    /**
     * Read this shared strings table from an XML file.
     *
     * @param is The input stream containing the XML document.
     * @throws IOException if an error occurs while reading.
     * @throws SAXException if parsing the XML data fails.
     */
    public void readFrom(InputStream is) throws IOException, SAXException {
        // test if the file is empty, otherwise parse it
        PushbackInputStream pis = new PushbackInputStream(is, 1);
        int emptyTest = pis.read();
        if (emptyTest > -1) {
            pis.unread(emptyTest);
            InputSource sheetSource = new InputSource(pis);
            try {
                XMLReader sheetParser = SAXHelper.newXMLReader();
                sheetParser.setContentHandler(this);
                sheetParser.parse(sheetSource);
            } catch(ParserConfigurationException e) {
                throw new RuntimeException("SAX parser appears to be broken - " + e.getMessage());
            }
        }
    }

    /**
     * Return an integer representing the total count of strings in the workbook. This count does not
     * include any numbers, it counts only the total of text strings in the workbook.
     *
     * @return the total count of strings in the workbook
     */
    @Override
    public int getCount() {
        return this.count;
    }

    /**
     * Returns an integer representing the total count of unique strings in the Shared String Table.
     * A string is unique even if it is a copy of another string, but has different formatting applied
     * at the character level.
     *
     * @return the total count of unique strings in the workbook
     */
    @Override
    public int getUniqueCount() {
        return this.uniqueCount;
    }

    /**
     * Return the string at a given index.
     * Formatting is ignored.
     *
     * @param idx index of item to return.
     * @return the item at the specified position in this Shared String table.
     * @deprecated use <code>getItemAt</code> instead
     */
    @Removal(version = "4.2")
    @Deprecated
    public String getEntryAt(int idx) {
        /**
         * 這裏就是修改部分了,直接從按行存儲的臨時文件讀取需要的字符串
         */
        return readCache.get(idx);

    }

    /**
     * Returns all the strings.
     * Formatting is ignored.
     *
     * @return a list with all the strings
     * @deprecated use <code>getItemAt</code> instead
     */
    @Removal(version = "4.2")
    @Deprecated
    public List<String> getItems() {
        return null;
    }

    @Override
    public RichTextString getItemAt(int idx) {
        return new XSSFRichTextString(getEntryAt(idx));
    }

    //// ContentHandler methods ////

    private StringBuilder characters;
    private boolean tIsOpen;
    private boolean inRPh;

    @Override
    public void startElement(String uri, String localName, String name,
                             Attributes attributes) throws SAXException {
        if (uri != null && ! uri.equals(NS_SPREADSHEETML)) {
            return;
        }

        if ("sst".equals(localName)) {
            String count = attributes.getValue("count");
            if(count != null) this.count = Integer.parseInt(count);
            String uniqueCount = attributes.getValue("uniqueCount");
            if(uniqueCount != null) this.uniqueCount = Integer.parseInt(uniqueCount);
            //    this.strings = new ArrayList<>(this.uniqueCount);
            characters = new StringBuilder(64);
        } else if ("si".equals(localName)) {
            characters.setLength(0);
        } else if ("t".equals(localName)) {
            tIsOpen = true;
        } else if ("rPh".equals(localName)) {
            inRPh = true;
            //append space...this assumes that rPh always comes after regular <t>
            if (includePhoneticRuns && characters.length() > 0) {
                characters.append(" ");
            }
        }
    }

    @Override
    public void endElement(String uri, String localName, String name) throws SAXException {
        if (uri != null && ! uri.equals(NS_SPREADSHEETML)) {
            return;
        }

        if ("si".equals(localName)) {
         //   strings.add(characters.toString().intern());
            readCache.put(characters.toString());
            /**
             * 這裏就是修改的一部分,這裏直接把字符串按行存入臨時文件
             */
            counts ++;
            if(counts == this.uniqueCount) {
                readCache.putFinished();
            }
        } else if ("t".equals(localName)) {
            tIsOpen = false;
        } else if ("rPh".equals(localName)) {
            inRPh = false;
        }
    }

    /**
     * Captures characters only if a t(ext) element is open.
     */
    @Override
    public void characters(char[] ch, int start, int length) throws SAXException {
        if (tIsOpen) {
            if (inRPh && includePhoneticRuns) {
                characters.append(ch, start, length);
            } else if (! inRPh){
                characters.append(ch, start, length);
            }
        }
    }

}

  至此代碼效率有了相當大的提高,而且內存溢出問題也得到解決。詳細測試代碼:https://github.com/rongdi/poi-example.git

  

  

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

【其他文章推薦】

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

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

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

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

MySQL InnoDB 事務

MySQL 原理篇

MySQL5.5 及以後版本的默認存儲引擎使用的是 InnoDB,接下來針對事務的講解都是基於 InnoDB 存儲引擎的。

事務的定義

事務:數據庫操作的最小工作單元,是作為單個邏輯工作單元執行的一系列操作; 事務是一組不可再分割的操作集合(工作邏輯單元)。

典型事務使用場景:轉賬

update user_account set balance = balance - 1000 where userID = 3;
update user_account set balance = balance + 1000 where userID = 1;

MySQL 開啟事務

/*
  BEGIN / START TRANSACTION        --手工
  COMMIT / ROLLBACK                --事務提交或回滾
  SET SESSION autocommit = ON/OFF  --設定會話級別事務是否自動開啟
*/

MySQL 默認是開啟事務的,通過 SHOW VARIABLES like 'autocommit'; 可以查看 MySQL 的事務開啟情況。

  • 在 autocommit = ON(自動提交事務)的情況下,可以執行 BEGIN; 或者 START TRANSACTION; 命令,改為手動提交事務,執行完 SQL 語句后,需要通過 COMMIT 命令提交事務,或者通過 ROLLBACK 命令回滾事務。
  • 在 autocommit = OFF(手動提交事務)的情況下,執行完 SQL 語句后,需要通過 COMMIT 命令提交事務,或者通過 ROLLBACK 命令回滾事務。

JDBC 編程

connection.setAutoCommit(boolean);

測試命令

SHOW VARIABLES like 'autocommit';

/* autocommit為ON */
update teacher set name ='seven' where id =1;
insert teacher (name,age) value ('james',22);
delete from teacher where name = 'james';

/* autocommit為ON如何開啟事務 */
BEGIN;
START TRANSACTION;

update teacher set name ='seven' where id =1;
insert teacher (name,age) value ('james',22);
delete from teacher where name = 'james';

COMMIT;
ROLLBACK;

/* 將autocommit改成OFF */
set session autocommit = OFF;
update teacher set name ='seven' where id =1;
insert teacher (name,age) value ('james',22);
delete from teacher where name = 'james';

COMMIT;
ROLLBACK;

事務 ACID 特性

原子性(Atomicity):最小的工作單元,整個工作單元要麼一起提交成功,要麼全部失敗回滾。

一致性(Consistency):事務中操作的數據及狀態改變是一致的,即寫入資料的結果必須完全符合預設的規則,不會因為出現系統意外等原因導致狀態的不一致。

隔離型(Isolation):一個事務所操作的數據在提交之前,對其他事務的可見性設定(一般設定為不可見)。

持久性(Durability):事務所做的修改就會永久保存,不會因為系統意外導致數據的丟失。

參考()這篇博文,講了一下原子性和一致性的區別。

事務併發帶來了哪些問題

臟讀

比如 user 表中有一條用戶數據,執行了如下操作:

  1. 事務B更新 id=1 的數據,age 更新為18,不提交事務
  2. 事務A查詢 id=1 的數據
  3. 事務B回滾剛才的更新操作

這個時候,事務A中查詢出的 id=1 的數據,age 的值是16還是18?

不可重複讀

 

比如 user 表中有一條用戶數據,執行了如下操作:

  1. 事務A查詢 id=1 的數據
  2. 事務B更新 id=1 的數據,age 更新為18,並提交事務
  3. 事務A再次查詢 id=1 的數據

這個時候,事務A兩次查詢出的 id=1 的數據,age 的值是16還是18?

幻讀

比如在 user 表中執行了如下操作:

  1. 事務A查詢 age>15 的數據
  2. 事務B新增一條數據,age=22,並提交事務
  3. 事務A再次查詢 age>15 的數據

這個時候,事務A兩次查詢出的數據,數量是1條還是2條?

事務四種隔離級別

SQL92 ANSI/ISO標準:http://www.contrib.andrew.cmu.edu/~shadow/sql/sql1992.txt

Read Uncommitted(未提交讀未解決併發問題

事務未提交對其他事務也是可見的,臟讀(dirty read)

Read Committed(提交讀)解決臟讀問題

一個事務開始之後,只能看到自己提交的事務所做的修改,不可重複讀(non repeatable read)

Repeatable Read(可重複讀解決不可重複讀問題

在同一個事務中多次讀取同樣的數據結果是一樣的,這種隔離級別未定義解決幻讀的問題

Serializable(串行化)解決所有問題

最高的隔離級別,通過強制事務的串行執行

InnoDB 引擎對隔離級別的支持程度

事務隔離級別

臟讀

不可重複讀

幻讀

未提交讀(Read Uncommitted)

可能

可能

可能

已提交讀(Read Committed)

不可能

可能

可能

可重複讀(Repeatable Read)

不可能

不可能

對 InnoDB 不可能

串行化(Serializable)

不可能

不可能

不可能

事務隔離級別的併發能力:未提交讀 > 已提交讀 > 可重複讀 > 串行化

InnDB 引擎默認的事務隔離級別是可重複讀(Repeatable Read),在該級別中它把幻讀的問題也解決了。InnDB 中事務隔離級別通過鎖、MVCC 實現。

通過如下語句查看 InnoDB 的默認事務隔離級別:

/* 全局、當前會話的隔離級別 */
SELECT @@global.tx_isolation, @@tx_isolation;

通過如下語句設置 InnoDB 的事務隔離級別:

/* 設置全局隔離級別 */
set global transaction isolation level read committed;
/* 設置當前會話的隔離級別 */
set session transaction isolation level read committed;

接下來我們來測試一下 InnoDB 的默認事務隔離級別(Repeatable Read)是否解決了臟讀、不可重複讀、幻讀的問題。

數據準備:

CREATE TABLE `user` (
  `id` int(11) NOT NULL,
  `name` varchar(32) NOT NULL,
  `age` int(11) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

insert into `user` (`id`, `name`, `age`) values('1','Ada','16');

是否解決了臟讀的問題?

開啟兩個 MySQL 客戶端,模擬事務A和事務B的操作,執行步驟如下:

  1. 事務B開啟手動事務,更新 id=1 的數據,age 更新為18,不提交事務
  2. 事務A查詢 id=1 的數據
  3. 事務B回滾剛才的更新操作

圖中的数字是執行步驟,通過下圖可以看出事務A的執行結果是16,InnDB 的默認事務隔離級別完美的解決了臟讀的問題。

是否解決了不可重複讀的問題?

開啟兩個 MySQL 客戶端,模擬事務A和事務B的操作,執行步驟如下:

  1. 事務A開啟手動事務,查詢 id=1 的數據
  2. 事務B更新 id=1 的數據,age 更新為18,並提交事務
  3. 事務A再次查詢 id=1 的數據

圖中的数字是執行步驟,通過下圖可以看出事務A兩次的執行結果都是16,沒有受事務B更新操作的影響,InnDB 的默認事務隔離級別完美的解決了不可重複讀的問題。

是否解決了幻讀的問題?

開啟兩個 MySQL 客戶端,模擬事務A和事務B的操作,執行步驟如下:

  1. 事務A開啟手動事務,查詢 age>15 的數據
  2. 事務B新增一條數據,age=22,並提交事務
  3. 事務A再次查詢 age>15 的數據

圖中的数字是執行步驟,通過下圖可以看出事務A兩次的執行結果都是一條數據,沒有受事務B新增操作的影響,InnDB 的默認事務隔離級別完美的解決了幻讀的問題。

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

【其他文章推薦】

※想知道網站建置網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計後台網頁設計

※不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

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

※帶您來看台北網站建置台北網頁設計,各種案例分享