Beetl Java模板引擎生成word excel
之前项目中使用freemarker和POI进行word以及excel的模板导出,在使用的过程中为了解决一些小问题,意外的接触了Beetl这款模板生成导出方案,解决了一些痛点,所以这次就进行总结分享下。
maven依赖
<dependency>
<groupId>com.ibeetl</groupId>
<artifactId>beetl</artifactId>
<version>2.9.3</version>
</dependency>
模板制作
创建模板 doc或者xls,设置好样式以及在需要动态加载的地方填充好一些数据(替换变量的时候方便定位),然后另存为xml格式文件;用文本编辑器打开进行模板制作。
根据beetl的语法,在早先设置的数据处替换为变量,然后修改文件后缀名为btl,即制作好了模板。java项目的话,则放在templates目录下。
beetl工具类
import org.beetl.core.Configuration;
import org.beetl.core.GroupTemplate;
import org.beetl.core.Template;
import org.beetl.core.resource.ClasspathResourceLoader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.http.HttpServletResponse;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.Map;
/**
* Created on 2018/10/16.
* Beetl生成doc文档工具类
*/
public class BeetlUtils {
private static final Logger LOGGER = LoggerFactory.getLogger(BeetlUtils.class);
/** 模板路径 */
private static final String TEMPLATE_PATH = "templates/";
/** 模板组 */
private static GroupTemplate gt ;
/**
* 获取到模板
* @param templateName 模板名称
* @return beetle模板
* @throws Exception 异常
*/
public static Template getTemplate(String templateName)throws Exception{
if (gt==null) {
ClasspathResourceLoader resourceLoader = new ClasspathResourceLoader(TEMPLATE_PATH);
Configuration cfg = Configuration.defaultConfiguration();
gt = new GroupTemplate(resourceLoader, cfg);
}
return gt.getTemplate(templateName+".btl");
}
public static String renderToString(Map<String, Object> data,String templateName) throws Exception{
Template template =getTemplate(templateName);
template.binding(data);
return template.render();
}
/**
* 下载doc文件
* @param dataMap 模板数据
* @param template 模板
* @throws Exception 异常
*/
public static void exportWord(String fileName, Map<String, Object> dataMap,Template template,HttpServletResponse response) throws Exception {
setFileDownloadHeader(response, fileName);
template.binding(dataMap);
template.renderTo(response.getOutputStream());
}
/**
* 下载doc文件
* @param dataMap 模板数据
* @param template 模板
* @throws Exception 异常
*/
public static void exportWord(Map<String, Object> dataMap,Template template,OutputStream out) throws Exception {
template.binding(dataMap);
template.renderTo(out);
}
/**
* 设置让浏览器弹出下载对话框的Header
* @param response web响应
* @param fileName 文件名
*/
public static void setFileDownloadHeader(HttpServletResponse response, String fileName) {
try {
// 中文文件名支持 ContentType 根据下载的文件不同而不同
String encodedFileName = URLEncoder.encode(fileName, "UTF-8");
response.setContentType("application/x-xls");
response.setHeader("Content-Disposition", "attachment; filename=" + encodedFileName);
} catch (UnsupportedEncodingException e) {
LOGGER.error(e.getLocalizedMessage(),e);
}
}
/**
* 将模板渲染到指定文件
* @param filePath 文件全路径
* @param dataMap 数据
* @param templateName 模板名称
* @throws Exception 异常
*/
public static void renderToFile(String filePath,Map<String, Object> dataMap,String templateName) throws Exception{
Template template =getTemplate(templateName);
template.binding(dataMap);
try ( FileOutputStream fileOutputStream = new FileOutputStream(filePath)) {
template.renderTo(fileOutputStream);
}
}
}
调用生成
在controller层或者service层,将查询出的数据放进map中,传递给模板;然后写给流导出。
Map<String, Object> data = new HashMap<>();
List<Map> list = new ArrayList<>();
data.put("listPro",list);
data.put("count",list.size()+12);
try {
Template t = BeetlUtils.getTemplate("product");
BeetlUtils.exportWord("信息统计表.xls",data,t,response);
} catch (Exception e) {
e.printStackTrace();
}
无论是生成word 还是 excel,都是通过一样的思路:
- 先制作模板,然后保存为xml格式。制作模板的时候,可以先预填一些数据,以便在添加变量的时候好找到需要动态替换的位置,然后将预填的固定值改为变量。
- 编辑xml文件,通过beetl的语法,将模板内需要动态填充的地方进行变量赋值;需要循环追加元素的地方也是一样。循环的时候需要找到正确的循环节点,即xml里面的,比如word表格里面的行节点等。
注:如果只是需要动态的替换掉某些位置的值,只需要在制作模板的时候设置个好记的初始值,生成另存为xml后打开替换为相应的变量即可;如果是需要动态的填充列表或者段落,道理也是一样,唯一需要注意的是,在xml里面找到预设值的地方后,需要找到合适的xml节点(循环节点),可通过生成测试是否正确,例如 word模板里,<w:p></w:p> 是段落,<w:tr></w:tr>是行,循环的时候则需要包裹它。
- 将添加好变量的模板 后缀名改为btl,即Beetl识别的格式。至此模板就做好了,下一步就是动态的填充数据进去生成。
beetl 官网
使用文档 http://ibeetl.com/guide/#/beetl
其他使用
在项目中遇到过需要编写 各种类型的统计jsp页面,根据选择的不同类型的条件以及限定条件,展示不同的统计页面以及筛选条件项;因为页面是根据选择的统计方式而变化的,所以编写固定的jsp页面就很难实现需求。
这时候找到的解决方法就是:制作jsp生成模板,模板内根据传入的参数进行代码判断,生成不同的元素,执行不同的查询和展示;即动态的生成 ‘代码页面’。后续可以加上一些判断,如将生成的jsp页面进行有效的命名,跳转页面的时候进行判断,如果已经生成(生成路径下存在 xx.jsp),则直接返回jsp路径,否则就先调用通过模板生成页面的方法,再返回jsp路径;
<${jsp_char_start} page language="java" pageEncoding="UTF-8"${jsp_char_end}>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<${jsp_char_start} taglib prefix="ta" tagdir="/WEB-INF/tags/tatags"${jsp_char_end}>
<html>
<head>
<title></title>
<${jsp_char_start} include file="/ta/inc.jsp"${jsp_char_end}>
<script type="text/javascript" src="<${jsp_char_end}=basePath${jsp_char_end}>js/reportout.js"></script>
<script type="text/javascript" src="<${jsp_char_end}=basePath${jsp_char_end}>js/inputTree.js"></script>
<style type="text/css">
.my-tree .fielddiv2{
border-radius: 10px;
}
.my-tree .fielddiv{
margin-top: 0px !important;
}
</style>
</head>
<%
var length = report_item.~size;
var is_4 = length%4==0;
%>
<body class="no-scrollbar" >
<ta:pageloading/>
<ta:box cssStyle="padding:10px;" fit="true">
<ta:text id="raqFile" display="false" value="${raq_file}"/>
<ta:text id="saveAsName" display="false" value="${report_name}"/>
<ta:text id="reportId" display="false" value="${report_id}"/>
<% if(length>0){%>
<ta:fieldset cols="${is_4?3:4}" id="reportQuery" key="检索条件" >
<%
for (item in report_item){
%>
<%
if (item.condition_type=='select' && item.condition_mb == '2'){
%>
<ta:text id="${item.condition_field}" placeholder="请选择" key="${item.condition_name}" span="1"></ta:text>
<ta:box cssClass="my-tree" cssStyle="height: 300px;width:69%; overflow: auto;position: absolute;z-index: 99999;display:none;left: -235px;
top: 30px;padding-top:2px; background-color: #ffffff;border: 1px solid #dddddd;" span="1" >
<ta:fieldset cols="3">
<ta:text id="treeSearch-in" span="2" placeholder="搜索"></ta:text>
<ta:button id="treeSearch-btn" key="检索"></ta:button>
</ta:fieldset>
<ta:tree showIcon="true" id="${item.condition_field+'_tree'}" showLine="true" nameKey="name" childKey="id" parentKey="pid" checkable="true"></ta:tree>
</ta:box>
<%
}
else {
var dataTypeStr = item.condition_type;
var selectType = item.condition_mb;
var dateCondition = item.condition_date;
var taType = "text";
var collectionValue;
var selectCol = "sqlType";
var dateShowType = '';
if(dataTypeStr == "date"){
taType = "date";
if(item.condition_date!='' && item.condition_date != 'date'){
var words = strutil.split(dateCondition,"_");
if(array.contain(words,"issue")){
dateShowType = ' issue ="true"';
}else{
dateShowType = words[0]+'="true"' ;
}
}
}
if(dataTypeStr == "number"){
taType = "number";
}
if(dataTypeStr == "select" && selectType == '1'){
taType = "selectInput";
selectCol = "colType";
collectionValue = ' collection="'+item.condition_code+'"' ;
}
if(dataTypeStr == "select" && selectType == '0'){
taType = "selectInput";
}
%>
<ta:${taType} id="${item.condition_field}"${selectCol=="colType"?collectionValue:null} ${taType=="date"? dateShowType:null}${taType=="date"?' showSelectPanel="true"':null} key="${item.condition_name}" span="1" />
<%
}
}
%>
<ta:buttonLayout span="1" align="left" >
<ta:button key="查询" onClick="search()"/>
<ta:button key="导出" onClick="execute()"/>
</ta:buttonLayout>
</ta:fieldset>
<%}%>
<ta:box id="son_box" cssStyle="width:100%;" fit="true">
<ta:panel >
<% if(length==0){%>
<ta:buttonLayout id="_runqian_daochu" span="1" align="left" cssStyle="">
<ta:button key="导出" onClick="execute()"/>
</ta:buttonLayout>
<%}%>
<iframe src="" frameborder="0" id="showReportIframe"></iframe>
</ta:panel>
</ta:box>
</ta:box>
</body>
</html>
<script type="text/javascript">
var paramAll = [];
$(document).ready(function () {
$("body").taLayout();
$("#showReportIframe").attr("width",$("#son_box").width());
$("#showReportIframe").attr("height",$("#son_box").height()-($("#_runqian_daochu").height()?$("#_runqian_daochu").height():0));
initTreeData();
noFilter();
changeStyle();
});
function initTreeData() {
<%
for (item in report_item){
if (item.condition_type=='select' && item.condition_mb == '2'){
%>
var param={};
param["dto['condition_field']"] = '${item.condition_field}';
param["dto['report_id']"] = Base.getValue("reportId");
Base.submit("" , "toReport!getSqlSelectTree.do" ,param ,null ,null ,function (data) {
var ${item.condition_field+'_Tree'};
${item.condition_field+'_Tree'}=Object.create(InputTree);
${item.condition_field+'_Tree'}.init("${item.condition_field}","${item.condition_field+'_tree'}",data.fieldData.treeData,500)
});
<%
}
}
%>
}
function changeStyle() {
<%
for (item in report_item){
if (item.condition_name=='至'){
%>
$('label[for="${item.condition_field}"]').css('width','60px');
<%
}
}
%>
}
//无参报表,自动展示
function noFilter(){
<% if(length==0){%>
search();
<%}%>
}
function search() {
if (Base.getValue('raqFile') == ''){
parent.Base.alert("报表文件不存在!" , "error");
return;
}
Base.submit("raqFile,reportId","toReport!checkRaqFileExist.do",null,null,null,function (data) {
var file_flag = data.fieldData.dataMap.file_flag;
if(file_flag){
Base.submit("raqFile,saveAsName,reportId,reportQuery","toReport!showReportData.do",null,null,null,function (data) {
//获取到动态生成的jsp文件路径
var fileUrl = data.fieldData.dataMap.fileUrl;
if(fileUrl){
parent.Base.showMask();
document.getElementById('showReportIframe').src = "<${jsp_char_end}=basePath${jsp_char_end}>reportnew"+ "/toReport!toSonIframePage.do" + "?dto.fileUrl="+fileUrl;
}
});
}else{
parent.Base.alert("报表文件不存在!" , "error");
return;
}
});
}
/*导出*/
function execute(){
var allParam = [];
var flag = true;
var raqFile = Base.getValue('raqFile');
var saveName = Base.getValue('saveAsName');
if (raqFile == ''){
parent.Base.alert("报表文件不存在!" , "error");
return;
}
<%
for (var i=0;i<report_item.~size;i++){
var conditionField = report_item[i].condition_field;
var baseValue = 'Base.getValue("'+conditionField+'")' ;
%>
var ${conditionField} = ${baseValue};
allParam.push({key:"${conditionField}",value:${baseValue}});
<%}%>
if (flag) {
var options = {
params: allParam,
raq: raqFile,
url: "<${jsp_char_end}=basePath${jsp_char_end}>",
saveAsName: saveName
};
download(options);
}
}
</script>
<${jsp_char_start} include file="/ta/incfooter.jsp"${jsp_char_end}>
注:导出生成后的word或者excel,一般常见的问题有:wps可以正常打开而office却打开报错。
一.制作excel模板生成的 xls wps可以打开,office打开报错
参考博文
解决测试:
- ExpandedRowCount 设置的行数值小于实际行数则报错,修改成 xml文件里默认的ExpandedRowCount+追加的数据行数,但是还是office打不开
- 发现另存的xml里还有ActiveRow节点 跟 ExpandedRowCount一致,也修改成默认的ActiveRow数+追加的数据行数,测试office可以打开
二. 生成word数据 设置 ContentType 错误, response.setContentType;需要针对导出 word、还是xls 选择不同的ContentType,可以网上搜索
要点:
- 生成的模板最好使用office来进行制作,避免一些未知的错误(适当使用不是最新版本的),
- 语法参考Beetl的使用文档
网上关于beetl的生成模板的使用文档比较少,自己最开始使用的时候也是找了很久,踩过很多的坑,当然模板生成的解决方案有很多,freemarker以及poi都可以,综合使用后,还是觉得beetl比较方便,对于生成出的文档的格式,兼容性也不错。有时间的话可以试一试哟。