前面有一篇講解如何在spring mvc web應用中一啟動就執行某些邏輯,今天無意發現如果使用不當,很容易引起記憶體洩露,測試程式碼如下:
1、定義一個類App
package com.cnblogs.yjmyzz.web.controller;
import java.util.Date;
public class App {
boolean isRun = false;
public App() {
isRun = true;
}
public void start() {
while (isRun) {
System.out.println("=======> I AM ALIVE =>" + new Date());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void stop() {
isRun = false;
}
}
程式碼裡面的內容不是重點,只是示意一下,我打算在spring mvc 應用一啟動時,就讓這個類例項化,執行其中的start方法,即:每隔一秒輸出一句話。
2、定義一個Listener
import com.cnblogs.yjmyzz.web.controller.App;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.stereotype.Component;
@Component
public class StartupListener implements ApplicationListener<ContextRefreshedEvent> {
App app;
@Override
public void onApplicationEvent(ContextRefreshedEvent evt) {
if (evt.getApplicationContext().getParent() == null) {
new Thread(new Runnable() {
@Override
public void run() {
app = new App();
app.start();
}
}).start();
}
}
}
程式碼也很簡單,應用一啟動,就開一個執行緒,例項化App,然後呼叫app.start()方法,執行一下,也跟預期的一樣,每隔一秒輸出類似下面的內容:
=======> I AM ALIVE =>Wed Sep 16 21:55:42 CST 2015
正式部署到jboss上以後,問題來了,在jboss管理控制檯上,把這個應用給disable甚至remove後,日誌裡仍然不斷有上面的類似輸出,即app的例項仍然活著,其start方法也始終在執行,換句話說,app並沒有被銷燬。
簡單分析一下:jboss的每個server啟動後,會伴隨啟動一個jvm例項,而部署在該server上的web應用,裡面建立的各種資源也在這個jvm例項中,就算把應用給停掉甚至刪除,由於程式碼中沒有任何清除app或停止start方法的處理,所以這個例項一直存在,不會被銷燬,除非server重啟。
另一個問題:如果把上面這段程式碼中,建立執行緒的部分去掉,改成直接 app = new App(); app.start(); 部署時會發現另一個現象,日誌裡仍然不斷有輸出,即程式碼在執行,但是該應用在jboss中的狀態始終是isdeploying,部署一直無法結束,始終處於『部署中』的狀態。
原因:start方法中的Thread.sleep()方法會阻塞執行緒,導致部署無法執行完畢。
解決辦法:
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.Date;
@Component
public class App {
boolean isRun = false;
@PostConstruct
public void init() {
System.out.println("init ==> " + new Date());
isRun = true;
}
public void start() {
while (isRun) {
System.out.println("=======> I AM ALIVE =>" + new Date());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void stop() {
isRun = false;
}
@PreDestroy
public void destroy() {
System.out.println("destroy ==> " + new Date());
stop();
}
}
這裡做了幾處改進:
a) 加上@Component後,App的例項將由Spring容器自動建立,即由容器來管理
b) 加上了@PreDestroy,Bean的生命週期由Spring容器來管理後,凡是Bean里加上該註解的方法,會在Bean銷燬前被執行,通常該方法用於清理資源
c) 將初始化的工作,移到了init方法中,並通過@PostConstruct註解告訴Spring,在呼叫完Bean的預設構造方法後,自動來呼叫該方法(當然這一步是可選的,並非必須)
@Component
public class StartupListener implements ApplicationListener<ContextRefreshedEvent> {
@Autowired
App app;
@Override
public void onApplicationEvent(ContextRefreshedEvent evt) {
if (evt.getApplicationContext().getParent() == null) {
new Thread(new Runnable() {
@Override
public void run() {
app.start();
}
}).start();
}
}
}
Listener中就簡單多了,直接@Autowired注入app例項就行了。
個人建議:
a) 如果要在web 應用一啟動時,就執行某些操作,特別是對資源類的長連線例項建立(比如:載入資料到快取中預熱、連線到Zookeeper監控節點變化、連線到Ftp準備取資料),最好交給Spring容器來自動建立,且務必記得在Destroy前,清理資源(即:斷開連線)
b) 在啟動的執行邏輯中,不要使用阻塞執行緒的操作(比如:Thread.sleep之類的方法),否則部署時,實際上程式碼已經在後臺執行了,jboss管理控制檯上,一直處於部署中的狀態,也沒有任何輸出,讓人一頭霧水,折騰半天才能定位錯誤,很浪費時間,如果是線上生產環境,是要粗事情的。