一、背景需求
系统平台需要做一个功能,在某菜单页面输入条件参数,查询生成一个pdf报告,既可在线查看,也可导出。
如图所示:
二、具体实现
1、引入依赖包,这里技术选用freemark、aspose和selenium,freemark用来生成word和动态数据填充,aspose用于把word转换为pdf,selenium用于web自动化写入echarts的图表。
<!--freemarker 模板生成工具-->
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.23</version>
</dependency>
<!-- word转换工具-->
<dependency>
<groupId>com.aspose</groupId>
<artifactId>aspose-words</artifactId>
<version>21.11.0</version>
</dependency>
<!--selenium Web自动化工具-->
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
<version>3.141.59</version>
</dependency>
2、word模板处理,将word模板另存为xml格式,可以将其放在项目同级目录下。
3、aspose凭证,aspose转换word为pdf默认是有水印的,使用凭证去除。
<!--word转换pdf破解去水印-->
<License>
<Data>
<Products>
<Product>Aspose.Total for Java</Product>
<Product>Aspose.Words for Java</Product>
</Products>
<EditionType>Enterprise</EditionType>
<SubscriptionExpiry>20991231</SubscriptionExpiry>
<LicenseExpiry>20991231</LicenseExpiry>
<SerialNumber>8bfe198c-7f0c-4ef8-8ff0-acc3237bf0d7</SerialNumber>
</Data>
<Signature>sNLLKGMUdF0r8O1kKilWAGdgfs2BvJb/2Xp8p5iuDVfZXmhppo+d0Ran1P9TKdjV4ABwAgKXxJ3jcQTqE/2IRfqwnPf8itN8aFZlV3TJPYeD3yWE7IT55Gz6EijUpC7aKeoohTb4w2fpox58wWoF3SNp6sK6jDfiAUGEHYJ9pjU=</Signature>
</License>
4、编写工具类。
/**
* word工具
* author: xht
* date: 2023-08-31
*/
public class WordUtil {
/**
* word生成方法
*
* @param TemplateName 模版名
* @param root 填充的数据
* @param GenerateFilename 生成文件名
*/
public static void wordCreate(String TemplateName, Map<String, Object> root, String GenerateFilename) throws Exception {
//模板存放目录
String TemplatePath = getTempPath();
Configuration cfg = new Configuration(Configuration.VERSION_2_3_23);
//指定模板文件的来源目录
cfg.setDirectoryForTemplateLoading(new File(TemplatePath));
cfg.setDefaultEncoding("UTF-8");
//设置错误的显示方式(日志)
//在生产系统中:TemplateExceptionHandler.RETHROW_HANDLER 默认值
//在开发HTML模板期间:TemplateExceptionHandler.HTML_DEBUG_HANDLER
//在开发非HTML模板期间:TemplateExceptionHandler.DEBUG_HANDLER
cfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);
//获取模板文件
Template temp = cfg.getTemplate(TemplateName);
//合并模板和数据模型
File file = new File(TemplatePath + "wpFiles");
//如果文件夹不存在,则创建文件夹
if (!file.exists()) {
file.mkdirs();//多级目录
// file.mkdir();//只创建一级目录
}
//Writer out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(TemplatePath + "new-built/" + GenerateFilename)));//输出文件
FileWriter out = new FileWriter(TemplatePath + "wpFiles" + File.separator + GenerateFilename);
//Writer out = new OutputStreamWriter(System.out);//输出控制台
//StringWriter out = new StringWriter();//输出为字符串,可作为接口动态返回
temp.process(root, out);
//可不手动调用
out.flush();
}
/**
* 获取模板路径
* File.separator:兼容了win和linux的/和\
* 不同项目的结构不同,复用请修改
*/
public static String getTempPath() {
//此路径与启动路径相关,在linux中以jar命令形式启动时,获取的路径为启动命令中输入的路径
String analysisReportUrl = System.getProperty("user.dir");
return analysisReportUrl + File.separator + "yb_temp" + File.separator;
}
/**
* word转换pdf图片方法
*/
public static void wordToPdf(String wordName, String pdfName) {
// 验证License 若不验证则转化出的pdf文档会有水印产生
if (!getLicense()) {
return;
}
//模板存放目录
String TemplatePath = getTempPath();
FileOutputStream os = null;
try {
long old = System.currentTimeMillis();
// 新建一个空白pdf文档
File file = new File(TemplatePath + "wpFiles" + File.separator + pdfName);
os = new FileOutputStream(file);
// Address是将要被转化的word文档
Document doc = new Document(TemplatePath + "wpFiles" + File.separator + wordName);
// 全面支持DOC, DOCX, OOXML, RTF HTML, OpenDocument, PDF,
doc.save(os, SaveFormat.PDF);
// EPUB, XPS, SWF 相互转换
long now = System.currentTimeMillis();
// 转化用时
System.out.println("pdf转换成功,共耗时:" + ((now - old) / 1000.0) + "秒");
} catch (Exception e) {
e.printStackTrace();
} finally {
if (os != null) {
try {
os.flush();
os.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**
* 获取aspose水印凭证
*/
public static boolean getLicense() {
boolean result = false;
try {
InputStream is = Test.class.getClassLoader().getResourceAsStream(File.separator + "license.xml");
License aposeLic = new License();
aposeLic.setLicense(is);
result = true;
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
}
5、echarts图表生成策略
yml配置文件:
#selenium+ChromeDriver驱动加载位置,执行html文件位置
selenium:
#请下载当前谷歌浏览器对应版本的驱动 https://googlechromelabs.github.io/chrome-for-testing/
chromedriver:
url: C:\chromedriver\chromedriver.exe
#模板地址需要部署在服务器做web地址映射
template:
url: http://localhost:8848/Platform_nj/EchartsTemplate/echartsTemplate.html
模板文件html(注意jq和echarts的script文件引入):
<html>
<meta charset="UTF-8">
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<head>
<script src="scripts/jquery-1.8.2.min.js"></script>
<script src="scripts/echarts-5.2.2/echarts.js"></script>
</head>
<body>
<div id="rhEcharts" style="height:400px;width:550px"></div>
</body>
<script>
var rhEcharts = null;
$(function () {
// 基于准备好的dom,初始化echarts实例
rhEcharts = echarts.init(document.getElementById('rhEcharts'));
// showImg()
});
function showImg(data) {
//var option = {
// backgroundColor: '#ffffff',
// xAxis: {
// type: 'category',
// data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
// },
// yAxis: {
// type: 'value'
// },
// series: [{
// data: [820, 932, 901, 934, 1290, 1330, 1320],
// type: 'line'
// }]
//};
// option && rhEcharts.setOption(option);
// const picInfo = returnEchartImg();
rhEcharts.setOption(data);
}
function returnEchartImg() {
var url = rhEcharts.getDataURL({ backgroundColor: '#ffffff' });
//清空绘画内容,清空后实例可用
rhEcharts.clear();
return url;
}
</script>
</html>
echarts图表生成类:
/**
* 后台加载Echarts并获取echarts图片(Base64)
*/
@Component
public class EchartsToPicUtil {
private static final Logger logger = LoggerFactory.getLogger(EchartsToPicUtil.class);
/**
* chromeDriver.exe驱动位置
*/
private static String CHROME_DRIVER_URL;
/**
* 模板位置
*/
private static String TEMPLATE_URL;
@Value("${selenium.chromedriver.url}")
public void setChromeDriverUrl(String chromeDriverUrl) {
CHROME_DRIVER_URL = chromeDriverUrl;
}
@Value("${selenium.template.url}")
public void setTemplateUrl(String templateUrl) {
TEMPLATE_URL = templateUrl;
}
/**
* ChromeDriver驱动对象
*/
private static WebDriver driver;
@PostConstruct
public void EchartsToPicUtil() {
//创建driver对象
EchartsToPicUtil.driver = getWebDriver();
//获取html模板页面
driver.get(TEMPLATE_URL);
// 多线程模式,初始化对象池
// ChromeDriverPool.preload();
//shutdown时执行钩子,退出WebDriver防止内存泄露
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
if (logger.isDebugEnabled()) {
logger.debug("开始执行shutdown退出操作");
}
if (driver != null) {
driver.close();
driver.quit();
if (logger.isDebugEnabled()) {
logger.debug("退出执行成功");
}
}
// 多线程模式,销毁对象池中的所有对象
// ChromeDriverPool.destroy();
}));
}
//采用单例模式
public static WebDriver getWebDriverInstance() {
return driver;
}
/**
* 初始化ChromeDriver对象
*/
public static void initWebDriver() {
driver = getWebDriver();
}
/**
* 获取图片base64
* 使用多线程时,该方法必须加锁,否则会导致图片生成失败等问题
*/
public static synchronized String getImgBase64(String option) {
// WebDriver driver = null;
try {
// 多线程模式,执行CAS循环检查,尝试从对象池中获取对象
// while ((driver = ChromeDriverPool.borrowChrome()) == null){
//
// }
if (driver == null || driver.toString().contains("(null)")) {
throw new NullPointerException("WebDriver已关闭,请初始化WebDriver");
}
if (logger.isDebugEnabled()) {
logger.debug("当前系统环境为:" + System.getProperties().getProperty("os.name"));
}
if (logger.isDebugEnabled()) {
logger.debug("当前driver获取到的html资源url:" + driver.getCurrentUrl());
}
//执行html,开始画图
JavascriptExecutor js = (JavascriptExecutor) driver;
js.executeScript("showImg(" + option + ")");
String imgBase64 = js.executeScript("return returnEchartImg()").toString().replace("data:image/png;base64,", "");
if (logger.isDebugEnabled()) {
logger.debug("当前获取到的图片base64:" + imgBase64);
}
return imgBase64;
} catch (Exception e) {
e.printStackTrace();
} finally {
// 多线程模式,归还对象到对象池中
// ChromeDriverPool.returnChrome(driver);
}
return null;
}
/**
* 初始化WebDriver
* 设置私有化,防止外部获取webDriver对象
*
* @return
*/
public static WebDriver getWebDriver() {
// 设置ChromeDriver的路径加载驱动
System.setProperty("webdriver.chrome.driver", CHROME_DRIVER_URL);
//设置 chrome 的无头模式
ChromeOptions chromeOptions = new ChromeOptions();
//无头模式
chromeOptions.setHeadless(true);
//地址出现data:,
// chromeOptions.addArguments("--user-data-dir=C:/Users/Administrator/AppData/Local/Google/Chrome/User Data/Default");
//Chrome正在受到显示自动软件的控制 不提示语
chromeOptions.addArguments("disable-infobars");
//禁用gpu加速,没有gpu运行时会报错,但是并不会影响chrome正常运行
chromeOptions.addArguments("--disable-gpu");
//linux版本需要禁用沙盒,linux 下让Chrome在root权限下跑
chromeOptions.addArguments("--no-sandbox");
//启动一个 chrome 实例
return new ChromeDriver(chromeOptions);
}
/**
* 单根比率柱状图
* 示例
* @param title
* @param nameStr
* @param dataStr
* @return
*/
public static String getBarSimpleRatioEchartsOption(String title, String nameStr, String dataStr) {
return String.format("{title:{text:'%s',left:'center'}," +
"animation:false," +
"xAxis:{type:'category',data:[%s],axisLabel:{show:true,fontSize:13,rotate:55 }}," + //,formatter:function(value){return value.split("").join("");}
"yAxis:{type:'value',axisLabel:{show:true,interval: 0,color: '#000',fontSize:8,formatter: '{value}%%'}}," +
"series:[{data:[%s],type:'bar',label:{show:true,position:'top',formatter:'{c}%%',fontSize:10}}]}",
title, nameStr, dataStr);
}
}
6、写入数据填充模板。
/**
* 安全情况分析报告word生成
* 这个方法在我自己项目中写在了服务层,这里只是举个模板参数的示例,根据具体情况调整
*/
@Override
public String createWord(ReqAcdAnalysis reqAcdAnalysis) throws Exception {
//数据集
Map<String, Object> dataMap = new HashMap<>();
//自适应时间
dataMap.put("varTime", "2023-01-01");
//生成报告名称
String fileName = "交通安全情况分析";
//word生成
WordUtil.wordCreate("AcdAnalysisTemp.xml", dataMap, fileName + ".doc");
//生成pdf
WordUtil.wordToPdf(fileName + ".doc", fileName + ".pdf");
//图表测试
String barLineSimpleEchartsOption = EchartsToPicUtil.getBarSimpleRatioEchartsOption("测试图", "['考勤']","[100]");
dataMap.put("testPic", EchartsToPicUtil.getImgBase64(barLineSimpleEchartsOption));
return fileName;
}
对应的xml模板中的参数:
图片则替换对应的base64:
7、word生成接口和在线预览接口
/**
* 安全情况分析报告word生成接口
*/
@ApiOperation(value = "安全情况分析报告word生成接口")
@PostMapping("/createWord")
public Response createWord(@RequestBody ReqAcdAnalysis reqAcdAnalysis, HttpServletResponse response) throws Exception {
//参数验证
if (reqAcdAnalysis.getEtime() == null || reqAcdAnalysis.getStime() == null) {
return Response.error("请完整输入时间");
}
//word生成服务
String fileName = acdAnalysisService.createWord(reqAcdAnalysis);
//将word和pdf上传至mongodb(存储方式随意)
File wordFile = new File(WordUtil.getTempPath() + "wpFiles" + File.separator + fileName + ".doc");
File pdfFile = new File(WordUtil.getTempPath() + "wpFiles" + File.separator + fileName + ".pdf");
Map map = new HashMap<>();
map.put("wordUpload", upload(wordFile));
map.put("pdfUpload", upload(pdfFile));
return Response.success(map);
}
/**
* 预览安全情况分析报告pdf
* (在前端调用的时候写在word生成接口的回调里面,保证先后顺序)
*/
@GetMapping("/previewPDF")
public void previewPDF(@RequestParam String fileName, HttpServletResponse response) throws Exception {
try {
if (fileName == null) {
throw new Exception("没有找到文件");
}
File file = new File(WordUtil.getTempPath() + "wpFiles" + File.separator + fileName);
FileInputStream fileInputStream = new FileInputStream(file);
response.setHeader("Content-Type", "application/pdf");
OutputStream outputStream = response.getOutputStream();
IOUtils.write(IOUtils.toByteArray(fileInputStream), outputStream);
} catch (Exception e) {
e.printStackTrace();
}
}
三、注意事项
如果部署到linux上大概率会出现字体对应不上然后乱码的情况,需要将开发环境中(例如windows)的字体上传到linux中并且加载。
解决方案:
##查看linux目前的所有字体
fc-list
##查看Linux目前的所有中文字体
fc-list :lang=zh
##将上面字体全部拷贝到linux下的字体目录
mkdir /usr/share/fonts/win
cp /local/src/fonts/* /usr/share/fonts/win
##执行安装字体命令
cd /usr/share/fonts
sudo mkfontscale
sudo mkfontdir
sudo fc-cache -fv
##执行命令让字体生效
source /etc/profile
##如果安装失败,可以考虑修改字体权限
chmod 755 *.ttf
参考链接:https://developer.aliyun.com/article/1331552