在grails(spring mvc)中如何定時(shí)發(fā)送動(dòng)態(tài)生成的報(bào)告

項(xiàng)目背景

基于Grails + groovy 框架開發(fā)了一個(gè)web系統(tǒng)家肯,因?yàn)間roovy是基于Java的腳本語言,所以這個(gè)方案在Java中也是可行的盟猖。
現(xiàn)在有這樣的需求讨衣,需要每天定時(shí)生成HTML報(bào)告换棚,并發(fā)送到固定郵件組。這份HTML報(bào)告不是靜態(tài)的反镇,數(shù)據(jù)會(huì)通過ajax獲取固蚤,圖表通過highchart.js來渲染。

需求點(diǎn)分析

這項(xiàng)需求的難點(diǎn)是歹茶,后端如何獲得js代碼運(yùn)行之后的HTML頁面內(nèi)容夕玩。

  • 剛開始想走Java引擎解析HTML這類的方案,就是寫一個(gè)template惊豺,然后將數(shù)據(jù)填充進(jìn)去燎孟,但是js代碼如何編譯呢?朝這個(gè)方向去搜資料尸昧,并沒有整理出一個(gè)可執(zhí)行的方案揩页。
  • 后來考慮到,首先將報(bào)告寫成一個(gè)頁面形式烹俗,然后在groovy中訪問這個(gè)URL不就可以了嗎爆侣? 但是沒有考慮到在瀏覽器打開一個(gè)URL,和使用curl(或者說使用java中httpClient包請(qǐng)求一個(gè)URL)的差別幢妄。那么這兩者的區(qū)別在哪里累提?前者會(huì)運(yùn)行js代碼,而后者不會(huì)磁浇。問題來了斋陪,怎么在groovy(或者Java)中打開一個(gè)URL能有瀏覽器的效果呢?偶然間查到了phantomjs,一個(gè)據(jù)說就是一個(gè)沒有界面的瀏覽器程序置吓。

方案

phantomjs的使用方法這里不詳細(xì)描述无虚。感興趣的可以參考這個(gè)鏈接phantomjs教程

那么首先編寫執(zhí)行腳本executeJs.js啦。希望這個(gè)腳本能完成如下功能---當(dāng)頁面加載完成之后衍锚,能獲得報(bào)告的HTML源碼友题。

為什么在這里要強(qiáng)調(diào)頁面加載完成之后。因?yàn)檫@個(gè)報(bào)告頁面戴质,是有highchat.js繪制圖表的度宦,還有ajax發(fā)送請(qǐng)求。當(dāng)我打開這個(gè)頁面的時(shí)候告匠,當(dāng)phantomjs 給我返回status為success的狀態(tài)時(shí)戈抄,并不代表這個(gè)頁面完全的渲染完成(在這里指的是繪圖完成)。如果在頁面沒有渲染完成的時(shí)候后专,獲得的頁面內(nèi)容就是這樣的:

test.png

但是我需要的是這樣的:

demo.png

那么划鸽,在executeJs.js腳本中,如何得知頁面已經(jīng)渲染完成呢?當(dāng)然有非常偷懶的做法裸诽。打開URL之后嫂用,等待10秒鐘,一般情況下丈冬,頁面肯定已經(jīng)渲染完成了嘱函。

但是,我想處理得更精細(xì)一點(diǎn)埂蕊,不想傻等往弓。在被請(qǐng)求的URL這個(gè)頁面要畫四幅圖,四幅圖都完成了粒梦,這個(gè)頁面也就渲染完成了。那么我怎么知道荸实,highchart 畫圖完成了呢匀们?進(jìn)一步的,我怎么知道最后一副完成的圖是哪一個(gè)呢准给?

第一個(gè)問題---highchart 的series屬性有這樣一個(gè)方法

...
series: [{
                data: vals,
                events: {
                    afterAnimate: function() {
                        chartHasDone =  chartHasDone + 1;
                    }
                }
            }],
...

當(dāng)afterAnimate 被調(diào)用時(shí)泄朴,說明圖片已經(jīng)渲染完成了。

第二個(gè)問題---我確實(shí)不知道最后一幅圖是哪一個(gè)露氮?不妨換一個(gè)思路祖灰,定義一個(gè)全局變量,每一幅圖畫完之后畔规,給這個(gè)全局變量+1 局扶,當(dāng)全局變量等于4時(shí),代表四幅圖全部渲染完成叁扫。

Linux下phantomjs的安裝

在Linux環(huán)境下安裝phantomjs之前需要安裝如下三個(gè)依賴

  • libstdc++.so.6
  • glibc
  • fontconfig
    前面兩個(gè)使用yum來安裝三妈。后面一個(gè)下載fontconfig的壓縮包使用make安裝。

安裝libstdc++.so.6

> yum provides libstdc++.so.6 //查看哪個(gè)安裝包包含該庫.結(jié)果顯示
libstdc++-4.4.7-16.el6.i686 : GNU Standard C++ Library 
> yum install  libstdc++-4.4.7-4.el6.i686  //安裝這個(gè)包莫绣,即可

安裝glibc

> yum install glibc 

如果yum源沒有問題的話畴蒲,應(yīng)該就可以安裝成功,但是我執(zhí)行這個(gè)命令的時(shí)候報(bào)如下錯(cuò)誤

rpmdb: Thread/process 6539/140448388269824 failed: Thread died in Berkeley DB library
error: db3 error(-30974) from dbenv->failchk: DB_RUNRECOVERY: Fatal error, run database recovery
error: cannot open Packages index using db3 -  (-30974)
error: cannot open Packages database in /var/lib/rpm

然后搜到解決辦法如下所示

cd /var/lib/rpm/
for i in `ls | grep 'db.'`;do mv $i $i.bak;done
rpm --rebuilddb
yum clean all

安裝fontconfig

按照fontconfig的官方文檔对室,執(zhí)行安裝步驟如下所示

> sudo ./configure --prefix=/usr        \
            --sysconfdir=/etc    \
            --localstatedir=/var \
            --disable-docs       \
            --docdir=/usr/share/doc/fontconfig-2.12.4 &&
make

依賴項(xiàng)安裝成功模燥。然后在phantomjs官網(wǎng)上,根據(jù)系統(tǒng)的類型和版本掩宜,選擇對(duì)應(yīng)的包下載蔫骂,解壓。進(jìn)入bin目錄下直接執(zhí)行即可

> phantomjs test.js

實(shí)現(xiàn)細(xì)節(jié)

那么再加上一些異常處理的代碼牺汤,execute.js就很好寫了

var page = require('webpage').create();

page.viewportSize = { width: 1920, height: 960 }

page.open('http://localhost.zeus.vdian.net:9000/ci/dailyReport?showReportHref=true', function(status) {
  if(status === "success") {
      //計(jì)算一下是否能讀到值
      
      var maxTimes = 0;
      var timer = setInterval(function() {
        maxTimes++ 
        var chartHasDone = page.evaluate(function() {
             return chartHasDone  //這個(gè)是被打開頁面中記錄渲染完成的圖表數(shù)的全局變量纠吴。
          });

        //重試1分鐘,若1分鐘還沒有結(jié)束慧瘤,自動(dòng)結(jié)束進(jìn)程戴已。返回false
        if (maxTimes >= 5) {
            clearInterval(timer);
            //保存結(jié)果
            console.log(false)
            phantom.exit();
        }

        //chartHasDone變?yōu)?,說明圖表渲染完成
        if (chartHasDone  == 5) {
            clearInterval(timer);
            
            var content = page.evaluate(function() {
              return document.getElementById('reportDetail').innerHTML;
            });

            console.log(content)

            phantom.exit();
        }
      }, 2000)

  }
});

同時(shí)固该,groovy(java)中代碼如下所示:

    def sendEmail() {
        def mailTo = 'zangsan@xx.com'
        def mailtitle = "日?qǐng)?bào)-${yesterday()}"
        def phantomjsDir = "${System.properties['user.home']}/phantomjs"

        def phantomjsFile = new File("${phantomjsDir}/phantomjs")
        def executeJsFile = new File("${phantomjsDir}/executeJs.js")

        if (!phantomjsFile.exists() || !executeJsFile.exists()) {
            log.error("phantomjs文件不存在");
            return [success: false, message: 'phantomjs文件不存在']
        }

        def phantomjsPath = phantomjsFile.getAbsolutePath()
        def executeJsPath = executeJsFile.getAbsolutePath()
        def getHtmlContentCmd = "${phantomjsPath} ${executeJsPath}"

        Process process = getHtmlContentCmd.execute();
        int exitStatus = process.waitFor(); //等待命令執(zhí)行完成
        if (exitStatus != 0) {
            log.error("EXIT-STATUS - " + process.toString());
            return [success: false, message: "執(zhí)行phantomjs文件出錯(cuò): ${process.toString()}"]
        }

        def content = process.text

        if (content?.trim() == 'false') {
            log.error "請(qǐng)求URL超時(shí)"

            return [success: false, message: '請(qǐng)求URL超時(shí)']
        } else {
            def result = [success: true]

            try {
                mailService.sendMail {
                    to mailTo
                    from "lisi@xx.com"

                    subject mailtitle
                    html content?.trim()
                }
            } catch (ex) {
                log.error "send mail Failed: ${ex.cause} (${ex.message})"
                result.success = false
                result.message = "郵件發(fā)送失敗: " + ex.message
            }

            return result
        }
    }

然后在Grails的job中,定時(shí)調(diào)用sendEmail 函數(shù)糖儡,即可伐坏。

遇到的坑

phantomjs 與瀏覽器的差別

phantomjs 聲稱是一個(gè)沒有界面的瀏覽器。雖然它可以執(zhí)行js代碼握联,但是和在chrome中訪問頁面還有差別的桦沉。我跳進(jìn)去的這個(gè)坑就是--phantomjs 無法解析 多行字符串的反引號(hào)
在chrome上如下一段代碼是可以正常執(zhí)行

var  tmpl = `
hello
world
`

但是,在 phantomjs中上面一段代碼會(huì)出現(xiàn)錯(cuò)誤金闽。關(guān)鍵是還不提示錯(cuò)誤信息纯露。最開始的時(shí)候都沒法排查!4摺埠褪!后來將那個(gè)頁面的js代碼一段段注釋,才找到出錯(cuò)的原因挤庇。

執(zhí)行腳本出現(xiàn)問題

在Grails中執(zhí)行executeJs.js的命令如下所示:

 ${System.properties['user.home']}/phantomjs/phantomjs  ${System.properties['user.home']}/phantomjs/executeJs.js test

但是該命令在測(cè)試環(huán)境下并沒有執(zhí)行成功钞速。本地調(diào)試時(shí),該命令是成功的嫡秕。后來發(fā)現(xiàn)區(qū)別是渴语,在測(cè)試環(huán)境下是以root用戶執(zhí)行該命令的。以root用戶執(zhí)行命令的話昆咽,${System.properties['user.home']}的值和以普通用戶執(zhí)行命令時(shí)的值是不一樣的驾凶。前者是/root/,后者是/home/www掷酗。所以,phantomjs程序的目錄需要發(fā)生變更狭郑。

優(yōu)化代碼

我將executeJs.jsphantomjs放在同一個(gè)本地目錄下。如果萬一executeJs.js發(fā)生變更的話汇在,那我還得去機(jī)器上更新代碼翰萨。為了修改方便,決定將executeJs.js放在了Grails 工程中糕殉。相應(yīng)的亩鬼,上一段代碼也要發(fā)生如下變更,現(xiàn)在的問題是阿蝶,如何在Grails代碼中找Grails工程中的資源文件雳锋。executeJs.js放在grails-app/src/main/resource中。代碼修改如下所示


       ....
       def yesterDay = yesterday()
        def mailtitle = "ZEUS-持續(xù)集成日?qǐng)?bào)-${yesterDay}"
        def phantomjsFile = new File("${System.properties['user.home']}/phantomjs/phantomjs")

        log.error("phantomjsFile的位置${phantomjsFile.absolutePath}")
        if (!phantomjsFile.exists()) {
            log.error("phantomjs程序不存在");
            return [success: false, message: 'phantomjs程序不存在']
        }

        def executeJsResource = this.class.classLoader.getResource('executeJs.js')  //獲取resource中executeJs.js的絕對(duì)路徑
        def executeJsPath = executeJsResource.file

        def executeJsFile = new File("${executeJsPath}")

        if (!executeJsFile.exists()) {
            log.error("executeJs文件不存在");
            return [success: false, message: 'executeJs文件不存在']
        }

        def phantomjsPath = phantomjsFile.getAbsolutePath()
        def getHtmlContentCmd = "${phantomjsPath} ${executeJsPath} ${env}"

        log.error "執(zhí)行content的命令是 ${getHtmlContentCmd}"
        ...

使用this.class.classLoader.getResource('executeJs.js').path來獲取executeJs.js的絕對(duì)路徑羡洁。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末玷过,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌辛蚊,老刑警劉巖粤蝎,帶你破解...
    沈念sama閱讀 216,591評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異袋马,居然都是意外死亡初澎,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,448評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門虑凛,熙熙樓的掌柜王于貴愁眉苦臉地迎上來碑宴,“玉大人,你說我怎么就攤上這事桑谍⊙幽” “怎么了?”我有些...
    開封第一講書人閱讀 162,823評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵锣披,是天一觀的道長(zhǎng)贞间。 經(jīng)常有香客問我,道長(zhǎng)盈罐,這世上最難降的妖魔是什么榜跌? 我笑而不...
    開封第一講書人閱讀 58,204評(píng)論 1 292
  • 正文 為了忘掉前任闪唆,我火速辦了婚禮盅粪,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘悄蕾。我一直安慰自己票顾,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,228評(píng)論 6 388
  • 文/花漫 我一把揭開白布帆调。 她就那樣靜靜地躺著奠骄,像睡著了一般。 火紅的嫁衣襯著肌膚如雪番刊。 梳的紋絲不亂的頭發(fā)上含鳞,一...
    開封第一講書人閱讀 51,190評(píng)論 1 299
  • 那天,我揣著相機(jī)與錄音芹务,去河邊找鬼蝉绷。 笑死,一個(gè)胖子當(dāng)著我的面吹牛枣抱,可吹牛的內(nèi)容都是我干的熔吗。 我是一名探鬼主播,決...
    沈念sama閱讀 40,078評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼佳晶,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼桅狠!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,923評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤中跌,失蹤者是張志新(化名)和其女友劉穎咨堤,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體晒他,經(jīng)...
    沈念sama閱讀 45,334評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡吱型,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,550評(píng)論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了陨仅。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片津滞。...
    茶點(diǎn)故事閱讀 39,727評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖灼伤,靈堂內(nèi)的尸體忽然破棺而出触徐,到底是詐尸還是另有隱情,我是刑警寧澤狐赡,帶...
    沈念sama閱讀 35,428評(píng)論 5 343
  • 正文 年R本政府宣布撞鹉,位于F島的核電站,受9級(jí)特大地震影響颖侄,放射性物質(zhì)發(fā)生泄漏鸟雏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,022評(píng)論 3 326
  • 文/蒙蒙 一览祖、第九天 我趴在偏房一處隱蔽的房頂上張望孝鹊。 院中可真熱鬧,春花似錦展蒂、人聲如沸又活。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,672評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽柳骄。三九已至,卻和暖如春箕般,著一層夾襖步出監(jiān)牢的瞬間耐薯,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,826評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工丝里, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留曲初,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,734評(píng)論 2 368
  • 正文 我出身青樓丙者,卻偏偏與公主長(zhǎng)得像复斥,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子械媒,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,619評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容