文章出處

前面有一篇講解如何在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管理控制臺上,一直處于部署中的狀態,也沒有任何輸出,讓人一頭霧水,折騰半天才能定位錯誤,很浪費時間,如果是線上生產環境,是要粗事情的。


文章列表




Avast logo

Avast 防毒軟體已檢查此封電子郵件的病毒。
www.avast.com


arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()