在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的状态时,并不代表这个页面完全的渲染完成(在这里指的是绘图完成)。如果在页面没有渲染完成的时候,获得的页面内容就是这样的:

在grails(spring mvc)中如何定时发送动态生成的报告_第1张图片
test.png

但是我需要的是这样的:

在grails(spring mvc)中如何定时发送动态生成的报告_第2张图片
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 = '[email protected]'
        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 "[email protected]"

                    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的绝对路径。

你可能感兴趣的:(在grails(spring mvc)中如何定时发送动态生成的报告)