SpringBoot+Freemark渲染html导出PDF
PDF 导出在需要将信息纸质化存档时会使用到。这里将介绍在 spring boot 框架下使用 Freemarker + iText 将 ftl 模板渲染成 html,然后导出 PDF 文件
Freemarker 官方文档: freemarker.apache.org/docs/index.…
Maven依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>html2pdf</artifactId>
<version>4.0.3</version>
</dependency>
@GetMapping(value = "/export-pdf")
public ResponseEntity exportPDF(TopoDeviceQueryDto queryDto) {
ResponseEntity response = null;
try {
HashMap<String, Object> mapData = Maps.newHashMap();
mapData.put("topDeviceList", topoDeviceService.findByList(queryDto));
String templateContent = HtmlUtils.getTemplateContent("TopoDevice.ftl", mapData);
response = this.getDownloadResponse(HtmlUtils.html2Pdf(templateContent), "device info.pdf");
} catch (Exception e) {
log.error("error occurs when downloading file");
response = ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
return response;
@Slf4j
public class HtmlUtils {
* @return
* @throws Exception
public static String getTemplateDirectory() {
ClassLoader classLoader = HtmlUtils.class.getClassLoader();
URL resource = classLoader.getResource("templates");
try {
return Objects.requireNonNull(resource).toURI().getPath();
} catch (URISyntaxException e) {
log.error("获取模板文件夹失败,{}", e);
return null;
* 获取模板内容
* @param templateName 模板文件名
* @param paramMap 模板参数
* @return
* @throws Exception
public static String getTemplateContent(String templateName, Map<String, Object> paramMap) throws Exception {
Configuration config = ApplicationContextUtil.getBean(FreeMarkerConfigurer.class).getConfiguration();
config.setDefaultEncoding("UTF-8");
Template template = config.getTemplate(templateName, "UTF-8");
return FreeMarkerTemplateUtils.processTemplateIntoString(template, paramMap);
* HTML 转 PDF
* @param content html内容
* @param outPath 输出pdf路径
* @return 是否创建成功
public static boolean html2Pdf(String content, String outPath) {
try {
ConverterProperties converterProperties = new ConverterProperties();
converterProperties.setCharset("UTF-8");
FontProvider fontProvider = new FontProvider();
fontProvider.addSystemFonts();
converterProperties.setFontProvider(fontProvider);
HtmlConverter.convertToPdf(content, new FileOutputStream(outPath), converterProperties);
} catch (Exception e) {
log.error("生成模板内容失败,{}", e);
return false;
return true;
* HTML 转 PDF
* @param content html内容
* @return PDF字节数组
public static ByteArrayOutputStream html2Pdf(String content) {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
try {
ConverterProperties converterProperties = new ConverterProperties();
converterProperties.setCharset("UTF-8");
FontProvider fontProvider = new FontProvider();
fontProvider.addSystemFonts();
converterProperties.setFontProvider(fontProvider);
HtmlConverter.convertToPdf(content, outputStream, converterProperties);
} catch (Exception e) {
log.error("生成 PDF 失败,{}", e);
return outputStream;
@Configuration
@AutoConfigureOrder(-1)
public class ApplicationContextUtil implements ApplicationContextAware {
private static ApplicationContext applicationContext = null;
public static Object getBeanByName(String beanName) {
if (applicationContext == null) {
return null;
return applicationContext.getBean(beanName);
public static <T> T getBean(Class<T> type) {
return applicationContext.getBean(type);
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
ApplicationContextUtil.applicationContext = applicationContext;
<!DOCTYPE html>
<html lang="en">
<meta charset="UFT-8">
<title>网络拓扑结构表</title>
<style>
.vertical-line {
height: 26px;
border-right: 2px solid #d0d0d0;
float: left;
margin-top: -10px;
margin-left: -15px;
.horizontal-line {
width: 15px;
border-top: 2px solid #d0d0d0;
float: left;
margin-top: 16px;
margin-left: -15px;
</style>
</head>
<style type="text/css">
table {
border-collapse: collapse;
margin: 0 auto;
text-align: center;
.table td,
.table th {
border: 1px solid #cad9ea;
color: #666;
height: 30px;
.table thead th {
background-color: #cce8eb;
width: 100px;
.table tr:nth-child(odd) {
background: #fff;
.table tr:nth-child(even) {
background: #f5fafa;
.tableB th {
border: 1px solid #cad9ea;
color: #666;
height: 30px;
.tableB thead th {
background-color: #e7f1ef;
width: 100px;
</style>
<!-- Table goes in the document BODY -->
<table width="90%" class="table">
<caption>
<h2>网络拓扑结构表</h2>
</caption>
<thead>
<th>设备名称</th>
<th>符号</th>
<th>端口</th>
<th>地址</th>
<th>网络中的单元</th>
</thead>
<tbody>
<#if topDeviceList??>
<#list topDeviceList as top_device>
<td><#if top_device.deviceName??>${top_device.deviceName}</#if></td>
<td><#if top_device.mark??>${top_device.mark}</#if></td>
<td><#if top_device.port??>${top_device.port}</#if></td>
<td><#if top_device.ip??>${top_device.ip}</#if></td>
<td><#if top_device.unit??>${top_device.unit}</#if></td>
</#list>
</tbody>
</table>
</div>
</body>
</html>
解决Spring项目打成Jar包后Freemarker找不到模板的问题
freemarker在jar包中无法使用类加载器获取resourse目录下的templates文件,出现的问题代码如下:(本地测试正常,打包jar后无法获取模板)
@GetMapping(value = "/export-pdf")
public ResponseEntity exportPDF(TopoDeviceQueryDto queryDto) {
ResponseEntity response = null;
try {
HashMap<String, Object> mapData = Maps.newHashMap();
mapData.put("topDeviceList", topoDeviceService.findByList(queryDto));
String templateContent = HtmlUtils.getTemplateContent("TopoDevice.ftl", mapData);
response = this.getDownloadResponse(HtmlUtils.html2Pdf(templateContent), "device info.pdf");
} catch (Exception e) {
log.error("error occurs when downloading file");
response = ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
return response;
public static String getTemplateDirectory() {
ClassLoader classLoader = HtmlUtils.class.getClassLoader();
URL resource = classLoader.getResource("templates");
try {
return Objects.requireNonNull(resource).toURI().getPath();
} catch (URISyntaxException e) {
log.error("获取模板文件夹失败,{}", e);
return null;
* 获取模板内容
* @param templateDirectory 模板文件夹
* @param templateName 模板文件名
* @param paramMap 模板参数
* @return
* @throws Exception
public static String getTemplateContent(String templateDirectory, String templateName, Map<String, Object> paramMap) throws Exception {
Configuration configuration = new Configuration(Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS);
try {
configuration.setDirectoryForTemplateLoading(new File(templateDirectory));
} catch (Exception e) {
System.out.println("-- exception --");
Writer out = new StringWriter();
Template template = configuration.getTemplate(templateName, "UTF-8");
template.process(paramMap, out);
out.flush();
out.close();
return out.toString();
* HTML 转 PDF
* @param content html内容
* @return PDF字节数组
public static ByteArrayOutputStream html2Pdf(String content) {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
try {
ConverterProperties converterProperties = new ConverterProperties();
converterProperties.setCharset("UTF-8");
FontProvider fontProvider = new FontProvider();
fontProvider.addSystemFonts();
converterProperties.setFontProvider(fontProvider);
HtmlConverter.convertToPdf(content, outputStream, converterProperties);
} catch (Exception e) {
log.error("生成 PDF 失败,{}", e);
return outputStream;
修改后的代码(打包jar后正常获取模板信息)
@GetMapping(value = "/export-pdf")
public ResponseEntity exportPDF(TopoDeviceQueryDto queryDto) {
ResponseEntity response = null;
try {
HashMap<String, Object> mapData = Maps.newHashMap();
mapData.put("topDeviceList", topoDeviceService.findByList(queryDto));
String templateContent = HtmlUtils.getTemplateContent("TopoDevice.ftl", mapData);
response = this.getDownloadResponse(HtmlUtils.html2Pdf(templateContent), "device info.pdf");
} catch (Exception e) {
log.error("error occurs when downloading file");
response = ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
return response;
* 获取模板内容
* @param templateName 模板文件名
* @param paramMap 模板参数
* @return
* @throws Exception
public static String getTemplateContent(String templateName, Map<String, Object> paramMap) throws Exception {
Configuration config = ApplicationContextUtil.getBean(FreeMarkerConfigurer.class).getConfiguration();
config.setDefaultEncoding("UTF-8");
Template template = config.getTemplate(templateName, "UTF-8");
return FreeMarkerTemplateUtils.processTemplateIntoString(template, paramMap);
public static <T> T getBean(Class<T> type) {
return applicationContext.getBean(type);
* HTML 转 PDF
* @param content html内容
* @return PDF字节数组
public static ByteArrayOutputStream html2Pdf(String content) {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
try {
ConverterProperties converterProperties = new ConverterProperties();
converterProperties.setCharset("UTF-8");
FontProvider fontProvider = new FontProvider();
fontProvider.addSystemFonts();
converterProperties.setFontProvider(fontProvider);
HtmlConverter.convertToPdf(content, outputStream, converterProperties);
} catch (Exception e) {
log.error("生成 PDF 失败,{}", e);
return outputStream;
ps:.ftl模板文件放在 templates 目录下
总结:freemarker无法使用类加载器获取jar包中的resourse目录下的templates文件
解决办法:注入FreeMarkerConfigurer
配置类,因为freemarker模板的默认目录就在resourse下的templates目录下,
使用freeMarkerConfigurer.getConfiguration().getTemplate("mailTemplate.ftl")
可直接获取到对应的模板文件
解决Freemark导出Pdf部署Linux乱码问题
项目部署完之后发现Linux下个别字体丢失的问题,刚开始以为是编码问题,经排查是Linux系统中文字体缺失问题
第一步:拷贝Windows下系统的中文字体 Windows下字体的目录是在C:\Windows\Fonts
第二步:移动字体库到linux系统下的字体库文件夹/usr/share/fonts/下
第三步:让linux系统识别新的中文字体:终端输入:sudo fc-cache -fv