有時候我需要查看已經部署到服務器上的應用程序的日志,每次都要遠程登錄服務器感覺很麻煩,所以一般我會把log文件的目錄用apache做個網站,這樣通過IE就可以訪問到了。但是有時要看即時輸出情況,就要不斷的F5,很麻煩。所以就有個想法,不如用DWR2.0的反轉Ajax來做個程序,讓日志有變化時自動的發送到客戶端,這樣就我一個勁的按F5了。

我下面就把這個程序分享給大家,希望大家提提意見。

首先是環境:
DWR 2.0.rc1
Sun JDK 1.5

先看張運行起來的圖吧,大家一看就知道這是個什么東西了。
logviewer.png
你可以制定要監視的log文件,當然有哪些文件文件可以被監視,你必須在服務端的xml配置中文件設置,當然你也可以監視一個目錄里的log文件,這對于而log文件是每天生成一個的情況很有用。你可以設定在瀏覽器上顯示的行數,操作行數,屏幕會自動滾動。你還可以添加一些過濾器,過濾掉不想看見的行,我目前只做了到了過濾掉一些信息,當然如果你有興趣,你也再添加一些更復雜的過濾器。過濾器的模式是用正則表達式表示的。

下面是點擊“開始監聽”,運行后樣子
logviewer2.png
如果服務器上的catalina.2006-12-09.log文件發生變化,客戶端的瀏覽器上log顯示區也會自動的向上滾動。


下面我就大致的介紹一下如何用DWR2.0來實現這樣的功能。在這里介紹的可能不是很詳細,不清楚的地方請看我提供的源碼。

先來介紹一下目錄結構
├─lib? -- 編譯和測試用的第三方類庫
├─webapp -- 部署目錄
├─test -- 測試程序
├─java -- 主程序
└─build.xml -- ant構建文件

webapp下的文件和目錄
│? style.css? -- 樣式表文件
│? index.html -- 主畫面文件

├─WEB-INF
│? │? web.xml -- 部署配置文件
│? │? dwr.xml -- dwr的配置文件
│? │? conf.xml -- 我們這個應用程序配置文件,主要是配置log文件
│? │
│? ├─classes
│? │
│? └─lib

└─script -- javascript文件


index.html中就是我們上面的圖片上能看到的頁面元素。其中的控件的事件處理都寫在\script\logviewer.js文件中。

當頁面加載時執行startPoll()方法,復雜開始與服務器的通信,并且把log文件選擇框初始化,把已經添加過濾器列表顯示出來。

function ?startPoll()?{
????DWREngine.setActiveReverseAjax(
true );

????LogManager.getLogFileNames(
function ?(data)?{
????????DWRUtil.removeAllOptions(
" log_file " );
????????DWRUtil.addOptions(
" log_file " ,?data);
????});

????LogManager.getFilters(
function (data)?{
????????
for ?( var ?i? = ? 0 ;?i? < ?data.length;?i ++ )?{
????????????addFilterDiv(data[i].pattern,?data[i].id);
????????}
????});
}


當點擊“開始監聽”按鈕時調用服務端的LogManager的send方法,服務端開啟監聽線程,開始監聽做為參數傳遞的文件,如果文件有變動就會把最近增加的行發送到瀏覽器上來。

var ?startWatch? = ? function ()?{
????clearLog();
????LogManager.send(DWRUtil.getValue(
" log_file " ));
}


當點擊“結束監聽”按鈕,調用LogManager的stop()方法,結束掉監聽線程。

function ?stopWatch()?{
????LogManager.stop();
}


當點擊“清空日志”按鈕,清除mainPanel中的所有子元素

function ?clearLog()?{
????
var ?mainPanel? = ?$( " main_panel " );
????
while ?(mainPanel.hasChildNodes())?{
????????mainPanel.removeChild(mainPanel.firstChild);
????}
}

當點擊“添加過濾器”,填充輸入框,要求輸入做為過濾器的正則表達式,輸入完成后,要做兩件事:
1、LogManager.addFilter方法,把輸入的正則表達式傳送給服務端。
2、把這個正則表達式添加到頁面上。

function ?addFilter()?{
????
var ?regex? = ?prompt( " 輸入正則表達式 " ,? "" );
????
if ?(regex? != ? null ? && ?regex? != ? "" )?{
????????LogManager.addFilter(regex,?
function ?(filterId)?{
????????????addFilterDiv(regex,?filterId);
????????});
????}
}
注意這里,我們用到了DWR的回調模式,在調用服務端方法LogManager.addFilter成功后我們才調用客戶端的addFilterDiv方法把這個輸入的正則表達式顯示到頁面上。

如果你足夠細心的話,應該會發現在這個js文件中有一個叫做addNewLine的方法在index.html中是沒有被調用的。這個方法其實是給服務端的LogManager.send函數調用的。

上面這些內容就是服務端腳本的主要內容了,其實很簡單。主要負責通過DWR與服務端通信和處理頁面顯示。

下面介紹服務端的核心類:LogManager

這個類主要就這樣幾個方法:
???/**
?????*?停止監控
?????
*/
????
public?void?stop()?{
????????
if?(watcher?!=?null)?{
????????????watcher.halt();
????????}
????}

????
/**
?????*?發送log信息
?????
*/
????
public?void?send(String?filename)?{
????????WebContext?wctx?
=?WebContextFactory.get();
????????
final?ScriptSession?scriptSession?=?wctx.getScriptSession();
????????
if?(watcher?!=?null)?{
????????????watcher.halt();
????????}

????????
try?{
????????????watcher?
=?new?LogFileWatcher(filename);
????????????watcher.addListener(
new?LogUpdateListener()?{
????????????????
public?void?onLogUpdate(List<String>?lines)?{
????????????????????
for?(String?line?:?lines)?{
????????????????????????
if?(checkFilters(line))?{
????????????????????????????ScriptBuffer?scriptBuffer?
=?new?ScriptBuffer();
????????????????????????????scriptBuffer.appendScript(
"addNewLine(")
????????????????????????????????????.appendData(line)
????????????????????????????????????.appendScript(
");");
????????????????????????????scriptSession.addScript(scriptBuffer);
????????????????????????}
????????????????????}
????????????????}
????????????});
????????????watcher.start();
????????}?
catch?(IOException?e)?{
????????????ScriptBuffer?scriptBuffer?
=?new?ScriptBuffer();
????????????scriptBuffer.appendScript(
"addNewLine(")
????????????????????.appendData(e.getMessage())
????????????????????.appendScript(
");");
????????????scriptSession.addScript(scriptBuffer);
????????????log.warn(e);
????????}
????}

????
/**
?????*?取得指定的日志文件路徑
?????*
?????*?
@return?指定的日志文件路徑
?????
*/
????
public?List<String>?getLogFileNames()?{
????????List
<String>?filenames?=?new?ArrayList<String>();
????????
try?{
????????????XMLConfiguration?config?
=?getConfiguration();
????????????List?logfiles?
=?config.getList("log-files.file");
????????????
for?(Object?o?:?logfiles)?{
????????????????filenames.add((String)?o);
????????????}
????????}?
catch?(ConfigurationException?e)?{
????????????log.warn(e);
????????}

????????
return?filenames;
????}

????
/**
?????*?取得指定的日志目錄下的文件
?????*
?????*?
@return?指定的日志目錄下的文件
?????
*/
????
public?List<String>?getLogFileNamesFromDir()?{
????????List
<String>?filenames?=?new?ArrayList<String>();
????????
try?{
????????????XMLConfiguration?config?
=?getConfiguration();
????????????String?dir?
=?config.getString("log-dir.dir");
????????????
if?(dir?!=?null)?{
????????????????File?rootDir?
=?new?File(dir);
????????????????
if?(rootDir.exists())?{
????????????????????
if?(rootDir.isFile())?{
????????????????????????filenames.add(rootDir.getPath().replace(
'\\',?'/'));
????????????????????}?
else?if?(rootDir.isDirectory())?{
????????????????????????String?patternString?
=?config.getString("log-dir.filter");
????????????????????????File[]?files;
????????????????????????
if?(patternString?!=?null?&&?!patternString.equals(""))?{
????????????????????????????files?
=?rootDir.listFiles(new?LogFileFilter(patternString));
????????????????????????}?
else?{
????????????????????????????files?
=?rootDir.listFiles();
????????????????????????}

????????????????????????
for?(File?file?:?files)?{
????????????????????????????filenames.add(file.getPath().replace(
'\\',?'/'));
????????????????????????}
????????????????????}
????????????????}
????????????}
????????}?
catch?(ConfigurationException?e)?{
????????????log.warn(e);
????????}

????????
return?filenames;
????}

????
/**
?????*?添加一個過濾器,返回過濾器的id
?????
*/
????
public?int?addFilter(String?regex)?{
????????
synchronized?(filters)?{
????????????Filter?filter?
=?new?Filter(regex,?SequenceGenerator.getInstance().next(),?FilterType.INCLUDE);
????????????filters.add(filter);
????????????
return?filter.getId();
????????}

????}

????
/**
?????*?根據id刪除一個過濾器
?????
*/
????
public?void?removeFilter(int?id)?{
????????
synchronized?(filters)?{
????????????filters.remove(
new?Filter(id));
????????}
????}

????
/**
?????*?取得現在所有的過濾器列表
?????
*/
????
public?List<Map<String,?Object>>?getFilters()?{
????????List
<Map<String,?Object>>?result?=?new?ArrayList<Map<String,?Object>>();
????????
synchronized?(filters)?{
????????????
for?(Filter?filter?:?filters)?{
????????????????Map
<String,?Object>?filterItem?=?new?HashMap<String,?Object>();
????????????????filterItem.put(
"id",?filter.getId());
????????????????filterItem.put(
"pattern",?filter.getPattern().pattern());
????????????????result.add(filterItem);
????????????}
????????}
????????
return?result;
????}

對于大家都做過Java的朋友來說,這些代碼應該很容易就能看懂,我就不多說了。大家主要注意一下ScriptSession類,這個類就是起到主要功能的類了。

其中的LogFileWatcher是一個Thread類,它是用來監視log文件的。

SequenceGenerator.java是用來生成過濾器的id的。

LogUpdateListener.java是一個接口,用于實現事件回調的。

然后看一個dwr的配置文件
<dwr>
????
<allow>
????????
<create?creator="new"?javascript="LogManager"?scope="session">
????????????
<param?name="class"?value="org.devside.logviewer.LogManager"/>
????????????
<include?method="send"/>
????????????
<include?method="stop"/>
????????????
<include?method="getLogFileNames"/>
????????????
<include?method="getLogFileNamesFromDir"/>
????????????
<include?method="addFilter"/>
????????????
<include?method="removeFilter"/>
????????????
<include?method="getFilters"/>
????????
</create>
????
</allow>
</dwr>

這里的配置文件和1.x幾乎沒什么兩樣,就是scope我這里設置成了session范圍的。這樣就可以多個人同時監視不同的log文件了。

web.xml文件也基本上是老樣子
<?xml?version="1.0"?encoding="UTF-8"?>
<web-app?id="LogViewer"?version="2.4"
?????????xmlns
="http://java.sun.com/xml/ns/j2ee"
?????????xmlns:xsi
="http://www.w3.org/2001/XMLSchema-instance"
?????????xsi:schemaLocation
="http://java.sun.com/xml/ns/j2ee?http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
????
<display-name>Web?Log?Viewer</display-name>

????
<servlet>
????????
<description>Direct?Web?Remoter?Servlet</description>
????????
<display-name>DWR?Servlet</display-name>
????????
<servlet-name>dwr-invoker</servlet-name>
????????
<servlet-class>org.directwebremoting.servlet.DwrServlet</servlet-class>
????????
<init-param>
????????????
<param-name>debug</param-name>
????????????
<param-value>true</param-value>
????????
</init-param>
????????
<init-param>
????????????
<param-name>pollAndCometEnabled</param-name>
????????????
<param-value>true</param-value>
????????
</init-param>
????????
<load-on-startup>1</load-on-startup>
????
</servlet>

????
<servlet-mapping>
????????
<servlet-name>dwr-invoker</servlet-name>
????????
<url-pattern>/dwr/*</url-pattern>
????
</servlet-mapping>

????
<welcome-file-list>
????????
<welcome-file>index.html</welcome-file>
????
</welcome-file-list>
</web-app>
dwr的包名發生了變化,并且要開啟反轉ajax,就要把pollAndCometEnabled參數設置為true。

總結,總體來說DWR2.0中的反轉ajax還是很容易使用的,這也是dwr的一貫風格,不用知道過多的細節就能容易的實現ajax。dwr絕對是Java開發者的首選ajax框架。
另外我這個程序其實還是為了演示用的,如果想要用戶實際開發可能還需要修改,比如安全性上面,性能上面。而性能上面的主要問題是客戶端瀏覽器,如果服務端的log文件過大,而瀏覽器有不能即時的回收內存,就會造成客戶端瀏覽器內存占用過大而死掉的問題。而服務端由于java的內容回收機制已經比較成熟應該不會有什么問題。我在ie6和firefox2都試過了,firefox效果能好一些。

源碼下載:
http://www.tkk7.com/Files/mstar/LogViewer.part1.rar
http://www.tkk7.com/Files/mstar/LogViewer.part2.rar