FreeMarker
前言
freeMarker是用来干啥的?
FreeMarker 是一款模板引擎: 即一种基于模板和要改变的数据, 并用来生成输出文本(HTML网页,电子邮件,配置文件,源代码等)的通用工具。
if标签
目标变量后面连续两个??判断对象是否为存在(null)
<#if target??>
xxxx
<# else>
yyyy
</#if>
判断相等和不等(数字和字符串的判断是一样的)
<#if target == "xxx">xxx</#if>
<#if target !== "xxx">xxx</#if>>
判断集合的长度
<!--必须括起来,否则会报错-->
<#if (fields?size>0) ></#if>
<!--或者-->
<#if fields?size gt 0 ></#if>
使用if判断后端传参和freeMarker中assign定义的变量
<#assign name="张三">
<#if user.name == name></#if>
<!--错误的写法-->
<#if user.name == ${name}></#if>
list标签
如何遍历list?如何获取list的索引值?
通过_index就可以获取的索引值,通过.xxx就可以获取lists中元素的xxx属性值。
<#list lists as res>
<tr>
<td>${res_index}</td>
<td>${res.xxx}</td>
</tr>
</#list>
如何获取集合的长度?
${fields?size}
如何遍历map类型的集合?
<#list stuMap?keys as k>
<tr>
<#--_index:得到循环的下标,使用方法是在stu后边加"_index",它的值是从0开始-->
<td>${k_index + 1}</td>
<td>${stuMap[k].name}</td>
<td>${stuMap[k].age}</td>
<td >${stuMap[k].money}</td>
</tr>
</#list>
如何跳出循环?
<#list table.columns as c>
<#if c.isPK>
<#break>
</#if>
</#list>
function标签
定义一个方法,让数据表里两位小数
<#setting number_format="computer">
<#function numFormat t>
<#if t==0>
<#return "0.00">
<#else>
<#return t?string('0.00')>
</#if>
</#function>
<!-- 调用方法-->
${numFormat(user.salary)}
巧用?
<!--以时间的格式展示 15:23:05-->
${nowDate?time}
<!--以时期的格式展示 2011-4-28(date的格式能够在freemarker.properties文件里配置)-->
${nowDate?date}
<!--获取当前时间-->
${.now?string('yyyy-MM')}
<!--freemarker数字当超过4为就会用逗号分隔,使用?c可以去除逗号【1,111---1111】-->
${user.salary?c}
<!--保留两位小数,不足自动补0【string是小写的】-->
${num?string('0.00')}
<!--判断字符串是否包含-->
"a,b,c,"?contains("a")
巧用!
<!--如果为空就赋值为x-->
${num!"x"}
示例一:xhtmlrenderer
xhtmlrenderer工具链接:百度网盘 请输入提取码 提取码:s8yz
该案例展示了在SpringBoot中使用freeMarker模板和xhtmlrenderer生成pdf报告
maven
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
<version>2.2.7.RELEASE</version>
</dependency>
<dependency>
<groupId>org.xhtmlrenderer</groupId>
<artifactId>flying-saucer-pdf</artifactId>
<version>9.1.12</version>
</dependency>
application.yml配置
#静态pdf配置
pdf:
template:
#根路径
rootPath: /usr/local/freeMarker/upload/static
#存放pdf的路径
pdfPath: /pdfFile/html/
#linux下存放wkhtmltopdf软件的路径
linuxCmdPath: /usr/local/bin/wkhtmltopdf
author: 懒鑫人
配置类
写一个配置类用于读取yml文件的配置
@Data
@Configuration
@ConfigurationProperties(prefix="pdf.template")
@PropertySource("classpath:application.yml")
public class PdfTemplateConfig {
private String rootPath;
private String pdfPath;
private String linuxCmdPath;
private String author;
}
写一个配置类用来创建线程池用于后续的多线程任务
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
@Configuration
public class ThreadPoolCreate {
@Bean
public ThreadPoolExecutor createExecutor() {
ThreadPoolExecutor pool= new ThreadPoolExecutor(2, 6, 3, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(4));
pool.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy ());
return pool;
}
}
实体类
生成的html和pdf模板我们可以将它的信息存储到数据库,这样第二次下载的时候通过数据库就能找到模板路径。
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PdfMessage {
private Integer id;
private String pdfName;
private String url;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
Integer id;
String userName;
Integer age;
Integer sex;
String createTime;
}
mapper
此处省略mapper.xml
@Mapper
@Repository
public interface FreeMarkerMapper {
//根据id查询pdf信息
PdfMessage getPdfMessage(Integer pdfId);
//保存pdf信息
int savePdfMessage(PdfMessage pdfMessage);
}
service
public interface FreeMarkerService {
PdfMessage getPdfMessage(Integer pdfId);
}
-
查询数据库中是否已有pdf模板的信息
-
如果没有或者服务器中模板已经不存在就重新下载模板
-
我们这里使用了线程池来执行任务,提高效率
import javax.annotation.Resource;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ThreadPoolExecutor;
@Service
@Slf4j
public class FreeMarkerServiceImpl implements FreeMarkerService {
@Resource
FreeMarkerMapper freeMarkerMapper;
@Resource
private ThreadPoolCreate poolCreate;
@Resource
private PdfTemplateConfig pdfConfig;
/**返回一个pdf模板的信息*/
@Override
public PdfMessage getPdfMessage(Integer pdfId) {
PdfMessage pdfMessage=freeMarkerMapper.getPdfMessage(pdfId);
if (pdfMessage==null||!new File(pdfMessage.getUrl()).exists()){
pdfMessage=downTemplate(pdfId);
//将pdf信息存入数据库
freeMarkerMapper.savePdfMessage(pdfMessage);
}
return pdfMessage;
}
/**生成pdf和html模板*/
public PdfMessage downTemplate(Integer pdfId) {
PdfMessage pdfMessage=new PdfMessage();
pdfMessage.setPdfName("Freemarker案例"+new Date().getTime());
//我们通过线程池来执行任务:这样可以提升效率
ThreadPoolExecutor pool = poolCreate.createExecutor();
//定义一个map来存储模板需要的数据 ConcurrentHashMap是线程安全的
Map<String, Object> map = new ConcurrentHashMap();
map.put("author",pdfConfig.getAuthor());
//CountDownLatch允许一个或者多个线程去等待其他线程完成操作。接收一个int型参数,表示要等待的工作线程的个数。
CountDownLatch latch = new CountDownLatch(1);
PdfTask pdfTask=new PdfTask(map,latch);
pool.execute(pdfTask);
try {
latch.await();
} catch (Exception e) {
log.error("企业信息线程池查询错误|" + e.getMessage());
}
//生成html
String html = getHtml(map, pdfId.toString());
//生成pdf
String pdfPath = null;
if (html.equals("")) {
log.error("静态化企业模板出错!");
} else {
pdfPath = createPdf(html, pdfId.toString());
pdfMessage.setUrl(pdfPath);
}
return pdfMessage;
}
/**生成静态的html*/
public String getHtml(Map<String, Object> map, String taxId) {
String charSet = "UTF-8";
//html模板保存路径
String htmlRootPath = pdfConfig.getRootPath() + pdfConfig.getPdfPath();
//html生成时间
String pdfDate = '/' + LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMM"));
map.put("pdfDate", pdfDate.substring(1));
//根据月份生成保存html的目录
File packagePath = new File(htmlRootPath + pdfDate + '/');
if (!packagePath.exists()) {
packagePath.mkdirs();
}
//Configuration负责模版(Template)实例的创建以及缓存
Configuration cfg = new Configuration(Configuration.VERSION_2_3_22);
//模板目录 你想使用ClassLoader的方式来加载模版的时候,你就可以使用这种方式,这种方式将会调用来寻找模版文件
cfg.setClassForTemplateLoading(this.getClass(), "/templates");
cfg.setDefaultEncoding(charSet);
String newsHtmlPath = null;
try {
//获得模板
Template newsTemplate = cfg.getTemplate("pdfTemplate.ftl");
//生成的内容
newsHtmlPath = packagePath.getAbsolutePath() + '/' + taxId.trim() + ".html";
File htmlFile = new File(newsHtmlPath);
if (!htmlFile.exists()) {
boolean flag = htmlFile.createNewFile();
if (!flag)
log.info("模板生成失败!");
}
Writer wr = new OutputStreamWriter(new FileOutputStream(htmlFile), charSet);
newsTemplate.process(map, wr);
wr.flush();
wr.close();
} catch (Exception e) {
log.error("模板静态化出错", e);
}
return newsHtmlPath;
}
/**生成静态的pdf*/
public String createPdf(String htmlPath, String leftFooter) {
final String htmlStr = ".html";
final String pdfStr = ".pdf";
StringBuilder cmdStr = new StringBuilder();
cmdStr.append("--enable-local-file-access").append(" ")
.append("--footer-left " + leftFooter).append(" ")
.append("--footer-right 第[page]页/共[topage]页 --footer-font-size 8").append(" ")
.append("--footer-line --footer-spacing 2 --page-size A4").append(" ")
.append(htmlPath).append(" ")
.append(htmlPath.replace(htmlStr, pdfStr));
Runtime runtime = Runtime.getRuntime();
String osName = System.getProperty("os.name").toLowerCase();
Process process = null;
String pdfPath = null;
String cmd = null;
try {
if (osName.indexOf("windows") > -1) {
cmd = "E:\\wkhtmltopdf\\bin\\wkhtmltopdf.exe " + cmdStr.toString();
} else {
cmd = pdfConfig.getLinuxCmdPath() + " " + cmdStr.toString();
}
process = runtime.exec(cmd);
ProcessInputThread errInput = new ProcessInputThread(process.getErrorStream(), "err");
ProcessInputThread infoInput = new ProcessInputThread(process.getInputStream(), "info");
errInput.start();
infoInput.start();
process.waitFor();
pdfPath = htmlPath.replace(htmlStr, pdfStr);
} catch (Exception e) {
log.error(leftFooter + "--企业报告PDF转化出错", e);
}
return pdfPath;
}
/**pdf下载进程*/
@Data
@EqualsAndHashCode(callSuper = true)
@AllArgsConstructor
class ProcessInputThread extends Thread {
InputStream is;
String type;
@Override
public void run() {
try (InputStreamReader inRead = new InputStreamReader(getIs(), StandardCharsets.UTF_8);
BufferedReader bfread = new BufferedReader(inRead)) {
String len = null;
if ("info".equals(getType())) {
while ((len = bfread.readLine()) != null)
log.info(len);
} else {
while ((len = bfread.readLine()) != null)
log.warn(len);
}
} catch (Exception e) {
log.error("PDF转化进程中死锁错误" + e.getMessage());
}
}
}
@Data
@AllArgsConstructor
static class PdfTask implements Runnable, Serializable{
private static final long serialVersionUID = 112L;
private transient Map<String, Object> map;
private transient CountDownLatch latch;
@Override
public void run() {
List<User> users=new ArrayList<>();
User user=new User();
for (int i = 0; i < 4; i++) {
user.setId(i+1);
user.setAge(i+18);
user.setUserName("测试"+i);
users.add(user);
}
map.put("pdfTask",users);
latch.countDown();
}
}
}
resource/templates/pdfTemplate.ftl
-
模板数据来源于service中提供的map
-
模板还演示了在模板中如何使用echarts图表
-
因为在yml中配置了根路径,所以echars需要依赖和图片我们要存放到
-
usr\local\freeMarker\upload\static\pdfFile\html\static\js\echarts.min.js
-
usr\local\freeMarker\upload\static\pdfFile\html\static\img\custom-gauge-panel.png
-
提取码:o4b8
-
<!DOCTYPE html>
<html>
<head lang="en">
<title>freeMarker模板</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<style>
#container {
font-family: "microsoft yahei", serif;
width: 800px;
margin: 0 auto;
text-align: center;
font-size: 14px;
}
p {
font-size: 20px;
font-weight: bold;
}
table {
width: 800px;
border-collapse: collapse;
text-align: left;
margin-bottom: 20px;
}
table caption{
text-align: left;
font-size: 16px;
padding-bottom: 2px;
font-weight: bold;
}
table tr {
width: 100%;
padding: 5px 0;
page-break-before: always;
page-break-after: always;
page-break-inside: avoid;
}
table td {
border: 1px black solid;
}
table .title_table_tip {
width: 120px;
background-color: rgba(200, 205, 210, 0.52);
text-align: center;
}
.user_message,
.echarts_img
{
page-break-inside: avoid;
page-break-before: left;
}
.first_page_center {
width: 380px;
text-align: left;
font-size: 20px;
margin: 0 auto 360px;
}
.first_page_center div {
margin-bottom: 20px;
}
</style>
<script type="text/javascript" src="../static/js/echarts.min.js"></script>
</head>
<body>
<div id="container">
<#--第一部分:报告封面页-->
<div class="page_header" style=" text-align: center">
<div style="height: 300px;"></div>
<p style="margin-bottom: 140px;font-size: 48px;">FreeMarker模板的使用</p>
<div class="first_page_center">
<div>
<span>评估日期:</span>
<span>${pdfDate}</span>
</div>
<div>
<span>作者:</span>
<span>${author}</span>
</div>
</div>
</div>
<#--第二部分: 用户信息-->
<div class="user_message">
<p>一、用户信息</p>
<table>
<caption>基本信息</caption>
<tr>
<td class="title_table_tip">编号</td>
<td class="title_table_tip">用户名</td>
<td class="title_table_tip">年龄</td>
</tr>
<#list pdfTask as pt>
<tr>
<td class="collapse_td">${pt.id}</td>
<td class="collapse_td">${pt.userName}</td>
<td class="collapse_td">${pt.age}</td>
</tr>
</#list>
</table>
</div>
<#--第三部分:echart图-->
<div class="echarts_img">
<div id="echarts_img_demo" style="height: 200px;width: 800px"></div>
<script type="text/javascript">
//初始化图案
var myChart = echarts.init(document.getElementById('echarts_img_demo'), null, {
renderer: 'canvas',
useDirtyRect: false
});
var _panelImageURL = '../static/img/custom-gauge-panel.png';
var app = {};
var option;
//echart底层通过_valOnRadianMax/valOnRadian计算环形图的占比
var _valOnRadianMax = 100;
//设置外部圆直径
var _outerRadius = 80;
//设置中间圆直径
var _innerRadius = 70;
var _pointerInnerRadius = 40;
//设置内部圆直径
var _insidePanelRadius = 60;
var _currentDataIndex = 0;
function renderItem(params, api) {
var valOnRadian = 70;
var coords = api.coord([api.value(0), valOnRadian]);
var polarEndRadian = coords[3];
var imageStyle = {
image: _panelImageURL,
x: params.coordSys.cx - _outerRadius,
y: params.coordSys.cy - _outerRadius,
width: _outerRadius * 2,
height: _outerRadius * 2
};
return {
type: 'group',
children: [
{
type: 'image',
style: imageStyle,
clipPath: {
type: 'sector',
shape: {
cx: params.coordSys.cx,
cy: params.coordSys.cy,
r: _outerRadius,
r0: _innerRadius,
startAngle: 0,
endAngle: -polarEndRadian,
transition: 'endAngle',
enterFrom: {endAngle: 0}
}
}
},
{
type: 'image',
style: imageStyle,
clipPath: {
type: 'polygon',
shape: {
points: makePionterPoints(params, polarEndRadian)
},
extra: {
polarEndRadian: polarEndRadian,
transition: 'polarEndRadian',
enterFrom: {polarEndRadian: 0}
},
during: function (apiDuring) {
apiDuring.setShape(
'points',
makePionterPoints(params, apiDuring.getExtra('polarEndRadian'))
);
}
}
},
{
type: 'circle',
shape: {
cx: params.coordSys.cx,
cy: params.coordSys.cy,
r: _insidePanelRadius
},
style: {
fill: '#fff',
shadowBlur: 25,
shadowOffsetX: 0,
shadowOffsetY: 0,
shadowColor: 'rgba(76,107,167,0.4)'
}
},
{
type: 'text',
style: {
text: makeText(70),
fontSize: 16,
fontWeight: 700,
x: params.coordSys.cx,
y: params.coordSys.cy,
fill: 'rgb(0,50,190)',
align: 'center',
verticalAlign: 'middle',
enterFrom: {opacity: 0}
}
}
]
};
}
function convertToPolarPoint(renderItemParams, radius, radian) {
return [
Math.cos(radian) * radius + renderItemParams.coordSys.cx,
-Math.sin(radian) * radius + renderItemParams.coordSys.cy
];
}
function makePionterPoints(renderItemParams, polarEndRadian) {
return [
convertToPolarPoint(renderItemParams, _outerRadius, polarEndRadian),
convertToPolarPoint(
renderItemParams,
_outerRadius,
polarEndRadian + Math.PI * 0.03
),
convertToPolarPoint(renderItemParams, _pointerInnerRadius, polarEndRadian)
];
}
function makeText(valOnRadian) {
return "" + valOnRadian;
}
option = {
animation:false,
dataset: {
source: [[1, 190]]
},
tooltip: {
//取消鼠标悬浮时的提示框
show: false
},
angleAxis: {
type: 'value',
startAngle: 0,
show: false,
min: 0,
max: _valOnRadianMax
},
radiusAxis: {
type: 'value',
show: false
},
polar: {},
series: [
{
type: 'custom',
radius: '50%',
coordinateSystem: 'polar',
renderItem: renderItem,
}
],
};
if (option && typeof option === 'object') {
myChart.setOption(option);
}
</script>
</div>
</div>
</body>
</html>
controller类
-
拿到服务器中pdf模板报告的地址,根据模板下载报告
-
下载的报告加上水印
@RestController
@RequestMapping("/free")
@Slf4j
public class FreeMarKerController {
@Resource
private PdfTemplateConfig pdfConfig;
@Resource
FreeMarkerService freeMarkerService;
/**
* 根据url地址下载pdf文件
*/
@GetMapping("/downPdf/{pdfId}" )
public void downPdf(@PathVariable(required = true) Integer pdfId, HttpServletResponse response) {
/**
* 1.为了效率更高:我们采用的是先提前将pdf生成好上传到服务器。
* 2.然后我们再通过上传到服务器的地址来下载文件
* 真实开发中我们可以通过定时任务将资源生成下载上传到服务器,然后通过id查找到对应的pdf路径进行下载
*/
PdfMessage pdfMessage = freeMarkerService.getPdfMessage(pdfId);
//将作者名作为水印
String watermark =pdfConfig.getAuthor();
//获取到生成pdf模板的路径
String pdfPath = pdfMessage.getUrl();
//判断当前程序是在什么服务器上运行 linux/windows
String osName = System.getProperty("os.name").toLowerCase();
String fileName;
//根据不同的服务器来制定文件名的命名方式
if (osName.contains("windows")) {
fileName = pdfPath.substring(pdfPath.lastIndexOf('\\'));
} else {
fileName = pdfPath.substring(pdfPath.lastIndexOf('/'));
}
//通过 PdfReader 读取pdf 文件
PdfReader read = null;
//将内容添加到PDF文件
PdfStamper stamper = null;
try {
response.setHeader("Content-Disposition", "attachment;fileName=\"" + URLEncoder.encode(fileName, "utf-8") + "\"");
response.addHeader("Content-Type", "application/pdf;charset=UTF-8");
read = new PdfReader(pdfPath);
stamper = new PdfStamper(read, response.getOutputStream());
int pageTotal = read.getNumberOfPages();
//PdfContentByte可以进行文本的绝对定位和字体的加粗、导入图片、画指定位置线条、创建下一页。
PdfContentByte content;
//设置字体
BaseFont base = BaseFont.createFont("STSong-Light", "UniGB-UCS2-H", BaseFont.EMBEDDED);
//添加水印
PdfGState gs = new PdfGState();
for (int i = 1; i <= pageTotal; i++) {
//内容下方添加水印
content = stamper.getUnderContent(i);
//设置透明度
gs.setFillOpacity(0.5f);
//加载文本
content.beginText();
content.setColorFill(BaseColor.LIGHT_GRAY);
content.setFontAndSize(base, 30);
//设置文本绝对坐标
content.setTextMatrix(70, 200);
//添加文字水印
content.showTextAligned(Element.ALIGN_CENTER, watermark, 300, 150, 55);
//接收设置
content.endText();
}
} catch (DocumentException | IOException e) {
log.error("导出PDF出错,请联系管理员", e);
} finally {
try {
if (stamper != null) {
stamper.close();
}
if (read != null) {
read.close();
}
} catch (DocumentException | IOException e) {
log.error("导出PDF数据流关闭出错", e);
}
}
}
}
注意事项
-
如果我们是使用wkhtmltopdf生成pdf的话,freeMarker中就不能够使用ES6语法(如let关键子和Map集合等),否则会报错。
-
我们在freeMarker中使用echars的时候,最好关闭动态效果,不然可能会因为加载延迟导致图片加载不出来。
option={ animation:false,//echarts图标关闭动态效果 }
-
我们可以通过 cmd wkhtmltopdf\bin 回车 ,然后通过下面命令来看我们的html模板能否成功转换成pdf【将1.html转换成1.pdf】
wkhtmltopdf\bin>wkhtmltopdf.exe 1.html 1.pdf Loading pages (1/6) Warning: Failed to load https://raw.githubusercontent.com/apache/echarts-website/asf-site/examples/data/asset/img/custom-gauge-panel.png (ignore) Counting pages (2/6) Resolving links (4/6) Loading headers and footers (5/6) Printing pages (6/6) Done
-
在将模板转换为pdf的时候,freeMarker中的容器必须要有宽和高,否则html模板能够显示出来内容,pdf显示不出来。