在grails(spring mvc)中如何定时发送动态生成的报告

项目背景

基于Grails + groovy 框架开发了一个web系统,因为groovy是基于Java的脚本语言,所以这个方案在Java中也是可行的。
现在有这样的需求,需要每天定时生成HTML报告,并发送到固定邮件组。这份HTML报告不是静态的,数据会通过ajax获取,图表通过highchart.js来渲染。

需求点分析

这项需求的难点是,后端如何获得js代码运行之后的HTML页面内容。

  • 刚开始想走Java引擎解析HTML这类的方案,就是写一个template,然后将数据填充进去,但是js代码如何编译呢?朝这个方向去搜资料,并没有整理出一个可执行的方案。
  • 后来考虑到,首先将报告写成一个页面形式,然后在groovy中访问这个URL不就可以了吗? 但是没有考虑到在浏览器打开一个URL,和使用curl(或者说使用java中httpClient包请求一个URL)的差别。那么这两者的区别在哪里?前者会运行js代码,而后者不会。问题来了,怎么在groovy(或者Java)中打开一个URL能有浏览器的效果呢?偶然间查到了phantomjs,一个据说就是一个没有界面的浏览器程序。

方案

phantomjs的使用方法这里不详细描述。感兴趣的可以参考这个链接phantomjs教程

那么首先编写执行脚本executeJs.js啦。希望这个脚本能完成如下功能---当页面加载完成之后,能获得报告的HTML源码。

为什么在这里要强调页面加载完成之后。因为这个报告页面,是有highchat.js绘制图表的,还有ajax发送请求。当我打开这个页面的时候,当phantomjs 给我返回status为success的状态时,并不代表这个页面完全的渲染完成(在这里指的是绘图完成)。如果在页面没有渲染完成的时候,获得的页面内容就是这样的:

test.png

但是我需要的是这样的:

demo.png

那么,在executeJs.js脚本中,如何得知页面已经渲染完成呢?当然有非常偷懒的做法。打开URL之后,等待10秒钟,一般情况下,页面肯定已经渲染完成了。

但是,我想处理得更精细一点,不想傻等。在被请求的URL这个页面要画四幅图,四幅图都完成了,这个页面也就渲染完成了。那么我怎么知道,highchart 画图完成了呢?进一步的,我怎么知道最后一副完成的图是哪一个呢?

第一个问题---highchart 的series属性有这样一个方法

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

afterAnimate 被调用时,说明图片已经渲染完成了。

第二个问题---我确实不知道最后一幅图是哪一个?不妨换一个思路,定义一个全局变量,每一幅图画完之后,给这个全局变量+1 ,当全局变量等于4时,代表四幅图全部渲染完成。

Linux下phantomjs的安装

在Linux环境下安装phantomjs之前需要安装如下三个依赖

  • libstdc++.so.6
  • glibc
  • fontconfig
    前面两个使用yum来安装。后面一个下载fontconfig的压缩包使用make安装。

安装libstdc++.so.6

> yum provides libstdc++.so.6 //查看哪个安装包包含该库.结果显示
libstdc++-4.4.7-16.el6.i686 : GNU Standard C++ Library 
> yum install  libstdc++-4.4.7-4.el6.i686  //安装这个包,即可

安装glibc

> yum install glibc 

如果yum源没有问题的话,应该就可以安装成功,但是我执行这个命令的时候报如下错误

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的官方文档,执行安装步骤如下所示

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

依赖项安装成功。然后在phantomjs官网上,根据系统的类型和版本,选择对应的包下载,解压。进入bin目录下直接执行即可

> phantomjs test.js

实现细节

那么再加上一些异常处理的代码,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") {
      //计算一下是否能读到值
      
      var maxTimes = 0;
      var timer = setInterval(function() {
        maxTimes++ 
        var chartHasDone = page.evaluate(function() {
             return chartHasDone  //这个是被打开页面中记录渲染完成的图表数的全局变量。
          });

        //重试1分钟,若1分钟还没有结束,自动结束进程。返回false
        if (maxTimes >= 5) {
            clearInterval(timer);
            //保存结果
            console.log(false)
            phantom.exit();
        }

        //chartHasDone变为5,说明图表渲染完成
        if (chartHasDone  == 5) {
            clearInterval(timer);
            
            var content = page.evaluate(function() {
              return document.getElementById('reportDetail').innerHTML;
            });

            console.log(content)

            phantom.exit();
        }
      }, 2000)

  }
});

同时,groovy(java)中代码如下所示:

    def sendEmail() {
        def mailTo = 'zangsan@xx.com'
        def mailtitle = "日报-${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(); //等待命令执行完成
        if (exitStatus != 0) {
            log.error("EXIT-STATUS - " + process.toString());
            return [success: false, message: "执行phantomjs文件出错: ${process.toString()}"]
        }

        def content = process.text

        if (content?.trim() == 'false') {
            log.error "请求URL超时"

            return [success: false, message: '请求URL超时']
        } 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 = "邮件发送失败: " + ex.message
            }

            return result
        }
    }

然后在Grails的job中,定时调用sendEmail 函数,即可。

遇到的坑

phantomjs 与浏览器的差别

phantomjs 声称是一个没有界面的浏览器。虽然它可以执行js代码,但是和在chrome中访问页面还有差别的。我跳进去的这个坑就是--phantomjs 无法解析 多行字符串的反引号
在chrome上如下一段代码是可以正常执行

var  tmpl = `
hello
world
`

但是,在 phantomjs中上面一段代码会出现错误。关键是还不提示错误信息。最开始的时候都没法排查!!!后来将那个页面的js代码一段段注释,才找到出错的原因。

执行脚本出现问题

在Grails中执行executeJs.js的命令如下所示:

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

但是该命令在测试环境下并没有执行成功。本地调试时,该命令是成功的。后来发现区别是,在测试环境下是以root用户执行该命令的。以root用户执行命令的话,${System.properties['user.home']}的值和以普通用户执行命令时的值是不一样的。前者是/root/,后者是/home/www。所以,phantomjs程序的目录需要发生变更。

优化代码

我将executeJs.jsphantomjs放在同一个本地目录下。如果万一executeJs.js发生变更的话,那我还得去机器上更新代码。为了修改方便,决定将executeJs.js放在了Grails 工程中。相应的,上一段代码也要发生如下变更,现在的问题是,如何在Grails代码中找Grails工程中的资源文件。executeJs.js放在grails-app/src/main/resource中。代码修改如下所示


       ....
       def yesterDay = yesterday()
        def mailtitle = "ZEUS-持续集成日报-${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的绝对路径
        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 "执行content的命令是 ${getHtmlContentCmd}"
        ...

使用this.class.classLoader.getResource('executeJs.js').path来获取executeJs.js的绝对路径。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,236评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,867评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,715评论 0 340
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,899评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,895评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,733评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,085评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,722评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,025评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,696评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,816评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,447评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,057评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,254评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,204评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,561评论 2 343

推荐阅读更多精彩内容