离线报表解决方案【centos6 + Phantomjs + Echarts + freemarker + java poi】

前言

先说下实际需求,由于业务需要,使用定时任务来生产离线报表(包含echarts 图表),为了最大限度的与前端保持一致性,所以需要离线报表也有echart图,生成要求就是:word、pdf ,当时形成了以下几种方案,我也都逐步趟过坑了,如果大家使用时遇到问题,也可以在评论区进行提问。

参考文档

#参考文档
安装: http://www.manongjc.com/detail/25-lwohnopbfuycmrv.html

#Debian编译安装【已成功】:
https://phantomjs.org/build.html

https://www.cnblogs.com/Cryan/p/15303547.html
https://www.cnblogs.com/qize/p/12522904.html
https://blog.csdn.net/xuhaogang3/article/details/90406005
https://blog.csdn.net/swag_gemmar/article/details/120267920
https://gitee.com/saintlee/echartsconvert/issues/IYDDU

1、可选方案

在这里插入图片描述

2、方案确定

经过尝试之后,还是确定了方案3,通过phantomjs(webkit内核)+JS+freemarker交互实现图表渲染,来最终得到一个渲染过的html页面 或者 一个渲染过的 base64图片,然后再生成PDF和word
在这里插入图片描述

3、开发实施

3-1、首先需要安装Phantomjs

下载地址:http://wenku.kuryun.com/docs/phantomjs/download.html

  • windows 下安装:比较简单,解压后在他的 bin目录下 执行 cmd 命令即可,网上搜索一大堆文章,不作赘述
    在这里插入图片描述
  • Linux下安装:这里安装后因为需要使用本无头浏览器进行echarts渲染和截图,所以需要配合echarts使用
    在这里插入图片描述
#安装无头浏览器-start-----------

#1、自己创建一个文件夹  yum_install

#2、把安装包考到 yum_install ,并解压
tar -xjvf phantomjs-2.1.1-linux-x86_64.tar.bz2

#3、配置环境变量

    # 编辑配置文件.
    vi /etc/profile

    # 将PhantomJS的bin目录加入到PATH环境变量中.
    export PHANTOMJS_HOME=/yum_install/phantomjs-2.1.1-linux-x86_64
    export PATH=${PHANTOMJS_HOME}/bin:$PATH

    # 退出vi编辑器,使用source命令让刚才的配置即时生效.
    source /etc/profile

# 4、安装字符集(不然生成echarts图会乱码)
yum -y install bitmap-fonts bitmap-fonts-cjk

# 5、解压后的 /yum_install/phantomjs-2.1.1-linux-x86_64/bin 目录 下放 这三个(文件包中已提供):

文件夹 module
文件夹 script
js文件  echarts-convert.js

# 6、启动(以服务启动)

 # 进入到安装目录
 cd /yum_install/phantomjs-2.1.1-linux-x86_64/bin
 # 查看是否安装成功(显示版本号既是安装成功)
 phantomjs -v

提供大神写好的三个echarts生成图的脚本, 并将三个脚本放到你安装后包文件的bin目录下
在这里插入图片描述
!!!!!脚本下载位置:(实在没积分,点个赞后留邮箱找我要 )
https://download.csdn.net/download/YL3126/85344470

 # 后台运行 phantomjs
 	#输入
    phantomjs /yum_install/phantomjs-2.1.1-linux-x86_64/bin/echarts-convert.js -s -p 6666 &
    #  --debug=yes  开启控制台日志打印
    #  -s 以服务启动
    #  -p 启动 端口  不加 默认为9090
    # 	& 后台运行

启动后其实 phantomjs 就成为了一个服务 , 我们在Java端可以通过POST 请求 传递 参数来实现让无头浏览器渲染后的base64图片返回给我们,然后就可以生成纯静态的html 了

3-2、安装繁体字库

但这里其实还有个问题,国际化的时候会出现繁体字不识别的情况,其实这是在linux 下没有繁体字库,所以我们需要安装繁体字库。
我们来到 windows 字体库的目录下:
在这里插入图片描述

#可参考
https://blog.csdn.net/weixin_44440642/article/details/119997271
https://blog.csdn.net/sdnu08gis/article/details/122960113

*****************************************************************

#1、安装字体库
yum -y install fontconfig
#2、安装索引信息
yum -y install ttmkfdir mkfontscale
#3、进入到字库文件目录
cd /usr/share/fonts
#创建文件夹
mkdir trchinese
#4、拷贝繁体字库 【就是我上面截图里红圈的那个】 到 trchinese 文件夹中

#5、在/usr/share/fonts/chinese执行命令,生成字库索引信息:
mkfontscale
mkfontdir

#6、更新字体缓存
fc-cache

#通过命令查看安装的字体
fc-list
3-3、Java实现代码

先说一下我的项目环境:spring mvc 4.3.29


/**
 * @Description: 静态模板生成
 * @author: 
 * @date: 2022/4/25 16:38
 */
@Component
public class ReportUtil {
    @Value("${report_base_path}") //基础路径
    private String report_base_path;

    @Value("${report_base_post_url}") //基础请求地址:也就是Phantomjs的服务地址,http://localhost:6666
    private String report_base_post_url;

    //注解注入配置的freemarker模板类
    @Autowired
    private FreeMarkerConfig freeMarkerConfig;

    /**
     *  生成静态文件
     * @param ftlDefault  模板位置
     * @param outHtmlName 文件名
     * @param data 数据集合
     * @return
     */
    public Boolean getItemHtml(String ftlDefault, String outHtmlName, Map data) {
        //数据处理
        try {
            Configuration configuration = freeMarkerConfig.getConfiguration();
            //配置默认模板
            Template template = configuration.getTemplate(ftlDefault);
            //输出位置及其文件名(并设置编码)
            OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(report_base_path+outHtmlName), "UTF-8");
            //文件流
            PrintWriter out = new PrintWriter(writer);
            //传入方法
            data.put("ReportFunctionUtil",new ReportFunctionUtil());
            //将数据输出到模板
            template.process(data, out);
            //关闭输出流
            out.close();
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }

    }

    //html生成word
    public boolean getHtmlToWord() throws Exception {
        //创建 POIFSFileSystem 对象
        POIFSFileSystem poifs = new POIFSFileSystem();
        //获取DirectoryEntry
        DirectoryEntry directory = poifs.getRoot();
        //创建输出流
        OutputStream out = new FileOutputStream("D:\\1.doc");
        try {
            //获取输入流
            FileInputStream inputStream = getInputStream("D:\\1.html");
            //创建文档,1.格式,2.HTML文件输入流
            directory.createDocument("WordDocument", inputStream);
            //写入
            poifs.writeFilesystem(out);
            //释放资源
            out.close();
            System.out.println("success");
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            //释放资源
            out.close();
        }
        return true;
    }

    /**
     *  执行 phantomjs 脚本 ,生成PDF
     *  cmds: 通过Phantomjs生成PDF命令
     */
    public String getHtmlToPdf(String cmds) {
        String ret=null;
        if (SystemUtils.IS_OS_LINUX) {
            String[] cmd =new String[3];
            cmd[0]="/bin/sh";
            cmd[1]="-c";
            cmd[2]=cmds;
            ret = CommonUtil.runCmd(cmd);
        }
        return ret;
    }

    /**
     * 获取 class path 中的文件流
     * @param name 名称
     * @return InputStream
     */
    public static FileInputStream getInputStream(String name) throws FileNotFoundException {
        return new FileInputStream(new File(name));
    }

    //获取echart图
    public String getEchartsBase64(Object opt) throws UnsupportedEncodingException {
        String url = this.report_base_post_url;
        Map<String,Object> optMap=(Map) opt;
        String optJson = JSON.toJSONString(optMap);
        //测试数据
        optJson="{series:[{name:'访问来源',type:'pie',data:[{value:235,name:'视频广告'},{value:274,name:'联盟广告'},{value:310,name:'邮件营销'},{value:335,name:'直接访问'},{value:400,name:'搜索引擎'}],roseType:'angle',itemStyle:{normal:{shadowBlur:200}}}]}";
        //删掉不必要的换行空格等,replaceAll("%", "%25") 这个很重要,因为本插件在JS中被转义了两次,所以需要处理一下 % 为 %25
        optJson = optJson.replaceAll("\\s+", "").replaceAll("\"", "'").replaceAll("%", "%25");
        Map<String, String> map = new HashMap<>();
        map.put("opt", optJson);
        String data=null;
        try {
            String post = post(url, map, "UTF-8");
            JSONObject jsonObject = JSON.parseObject(post);
            data = jsonObject.getString("data");//这个就是base64的图
        } catch (Exception e) {
            e.printStackTrace();
        }
        return data;
    }

    // post请求
    public static String post(String url, Map<String, String> map, String encoding) throws Exception, IOException {
        String body = "";
        // 创建httpclient对象
        CloseableHttpClient client = HttpClients.createDefault();
        // 创建post方式请求对象
        HttpPost httpPost = new HttpPost(url);
        //设置连接超时时间
        RequestConfig builder = RequestConfig.custom()
                .setConnectTimeout(10000)//连接超时10S
                .setSocketTimeout(10000)//服务连接10S
                .setConnectionRequestTimeout(10000)//响应超时
                .build();//响应超时5S
        //设置超时参数
        httpPost.setConfig(builder);

        // 装填参数
        List<NameValuePair> nvps = new ArrayList<>();
        if (map != null) {
            for (Map.Entry<String, String> entry : map.entrySet()) {
                nvps.add(new BasicNameValuePair(entry.getKey(), entry.getValue()));
            }
        }
        // 设置参数到请求对象中
        httpPost.setEntity(new UrlEncodedFormEntity(nvps, encoding));
        // 执行请求操作,并拿到结果(同步阻塞)
        CloseableHttpResponse response = client.execute(httpPost);
        // 获取结果实体
        HttpEntity entity = response.getEntity();
        if (entity != null) {
            // 按指定编码转换结果实体为String类型
            body = EntityUtils.toString(entity, encoding);
        }
        EntityUtils.consume(entity);
        // 释放链接
        response.close();
        return body;
    }

    public static boolean GenerateImage(String imgStr, String imgFilePath) {// 对字节数组字符串进行Base64解码并生成图片
        if (imgStr == null) // 图像数据为空
            return false;
        BASE64Decoder decoder = new BASE64Decoder();
        try {
            // Base64解码
            byte[] bytes = decoder.decodeBuffer(imgStr);
            for (int i = 0; i < bytes.length; ++i) {
                if (bytes[i] < 0) {// 调整异常数据
                    bytes[i] += 256;
                }
            }
            // 生成jpeg图片
            OutputStream out = new FileOutputStream(imgFilePath);
            out.write(bytes);
            out.flush();
            out.close();
            return true;
        } catch (Exception e) {
            return false;
        }
    }

}

生成的 base64 图表再通过 freemarker 生成的静态化模板 显示即可,这样就形成了HTML文件,html 转 word 直接使用Java POI 即可,html 生成 PDF 可以使用 java 调用 phantomjs 命令来完成, phantomjs 解压后 里面会提供很多实例,在 解压包的 文件夹下有个 :examples 目录 ,里面就是一些例子
在这里插入图片描述
我们通过执行里面提供好的的 rasterize.js 脚本,来生成PDF :

如我把脚本考到 /yum_install 里,所以我 cd 进入到 yum_install 目录下,执行命令:

#进入到目录
cd /yum_install

# 将 1.html 生成  4.pdf [这里要注意一下,所有的参数名不能包含“-”横杠等特殊字符]
phantomjs rasterize.js 1.html 4.pdf

然后可以看到目录下有了pdf文件:
在这里插入图片描述

3-4、html模板

需要添加以下很重要的两个东西,无法控制生成相应的word文档样式:

<!--头信息-->
<html xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office"
      xmlns:w="urn:schemas-microsoft-com:office:word" xmlns:m="http://schemas.microsoft.com/office/2004/12/omml"
      xmlns="http://www.w3.org/TR/REC-html40">


<!--head里面的信息-->
<head>
    <!--[if gte mso 9]><xml><w:WordDocument><w:View>Print</w:View><w:TrackMoves>false</w:TrackMoves><w:TrackFormatting/><w:ValidateAgainstSchemas/><w:SaveIfXMLInvalid>false</w:SaveIfXMLInvalid><w:IgnoreMixedContent>false</w:IgnoreMixedContent><w:AlwaysShowPlaceholderText>false</w:AlwaysShowPlaceholderText><w:DoNotPromoteQF/><w:LidThemeOther>EN-US</w:LidThemeOther><w:LidThemeAsian>ZH-CN</w:LidThemeAsian><w:LidThemeComplexScript>X-NONE</w:LidThemeComplexScript><w:Compatibility><w:BreakWrappedTables/><w:SnapToGridInCell/><w:WrapTextWithPunct/><w:UseAsianBreakRules/><w:DontGrowAutofit/><w:SplitPgBreakAndParaMark/><w:DontVertAlignCellWithSp/><w:DontBreakConstrainedForcedTables/><w:DontVertAlignInTxbx/><w:Word11KerningPairs/><w:CachedColBalance/><w:UseFELayout/></w:Compatibility><w:BrowserLevel>MicrosoftInternetExplorer4</w:BrowserLevel><m:mathPr><m:mathFont m:val="Cambria Math"/><m:brkBin m:val="before"/><m:brkBinSub m:val="--"/><m:smallFrac m:val="off"/><m:dispDef/><m:lMargin m:val="0"/> <m:rMargin m:val="0"/><m:defJc m:val="centerGroup"/><m:wrapIndent m:val="1440"/><m:intLim m:val="subSup"/><m:naryLim m:val="undOvr"/></m:mathPr></w:WordDocument></xml><![endif]-->
</head>
3-5、freemarker配置文件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:jee="http://www.springframework.org/schema/jee" xmlns:mvc="http://www.springframework.org/schema/mvc"
       xmlns:p="http://www.springframework.org/schema/p"
       xsi:schemaLocation="
    http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd
        http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd
        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
        http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee-4.0.xsd">

    <mvc:annotation-driven />

    <!-- FreeMarker视图解析 -->
    <bean id="viewResolver"
          class="org.springframework.web.servlet.view.freemarker.FreeMarkerViewResolver"
          abstract="false" scope="singleton" lazy-init="default" autowire="default">
        <property name="viewClass"
                  value="org.springframework.web.servlet.view.freemarker.FreeMarkerView" />
        <!-- freemarker中已经配置该属性 -->
        <property name="prefix" value="/freemarker/ftl/" />
        <property name="suffix" value=".ftl" />
        <property name="cache">
            <value>true</value>
        </property>
        <property name="contentType" value="text/html;charset=UTF-8" />
        <property name="requestContextAttribute" value="request" />
        <property name="exposeRequestAttributes" value="true" />
        <property name="allowSessionOverride" value="true" />
        <property name="exposeSessionAttributes" value="true" />
        <property name="exposeSpringMacroHelpers" value="true" />
    </bean>

    <bean id="localeResolver"
          class="org.springframework.web.servlet.i18n.SessionLocaleResolver">
        <property name="defaultLocale" value="zh" />
    </bean>

    <!-- freemarker的配置 abstract="false" scope="singleton" lazy-init="default"
    autowire="default" -->
    <bean id="freemarkerConfigurer"
          class="org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer">
        <property name="templateLoaderPath" value="/freemarker/ftl/" />
        <property name="defaultEncoding" value="UTF-8" />
        <property name="freemarkerSettings">
            <props>
                <prop key="template_update_delay">3</prop>
                <prop key="locale">zh_CN</prop>
                <prop key="datetime_format">yyyy-MM-dd HH🇲🇲ss</prop>
                <prop key="date_format">yyyy-MM-dd</prop>
                <prop key="number_format">#.##</prop>
                <prop key="defaultEncoding">UTF-8</prop>
                <prop key="classic_compatible">true</prop>
            </props>
        </property>
        <property name="freemarkerVariables">
            <map>
                <entry key="xml_escape" value-ref="fmXmlEscape" />
            </map>
        </property>
    </bean>

    <bean id="fmXmlEscape" class="freemarker.template.utility.XmlEscape" />

</beans>
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

会飞的小蜗

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值