前言
先说下实际需求,由于业务需要,使用定时任务来生产离线报表(包含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>