使用freemarker,导出制作好的ftl模板,并写入数据
一、背景
1.1 项目背景
- 最近在开发一个项目,需要导出一些数据,然后写入到word文档中,然后再导出到本地,这个需求是比较常见的,但是我在网上找了很多资料,都没有找到一个比较好的解决方案,所以就自己写了一个,这里分享给大家,希望能帮助到大家。
- 项目中使用的技术栈:freemarker
- 项目中使用的依赖:
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.28</version>
</dependency>
二、实现
2.1 代码实现
- 代码实现比较简单,就是先制作好ftl模板,然后在模板中标记好需要写入的数据的位置,然后在代码中将数据写入到模板中,然后再导出到本地,具体代码如下:
- 1.首先创建一个ftl模板,这里我创建了一个test.ftl模板,模板中的内容如下:
<html>
<head>
<title>Welcome!</title>
</head>
<body>
<h1>Welcome ${user}!</h1>
<p>We have there animals:
<ul>
<list animals as animal>
<li>${animal.name} for ${animal.price} Euros
</list>
</ul>
<include "common_footer.html">
</body>
</html>
- 2.然后在查询数据中定义数据对象,使用@ExportWordField注解标记需要写入的数据,具体代码如下:
@Documented
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface ExportWordField {
String fieldName() default "";
boolean isFileNameFiled() default false;
int fileNameFiledSort() default 0;
}
@Data
public class Something{
@ExportWordField(fieldName = "user",isFileNameFiled = true,fileNameFiledSort = 1)
private String user;
@ExportWordField(fieldName = "animals")
private List<Animal> animals;
}
@Data
public class Animal{
@ExportWordField(fieldName = "name")
private String name;
@ExportWordField(fieldName = "price")
private String price;
}
- 3.然后在代码中将数据写入到模板中,具体代码如下:
@Slf4j
public class ExportTemplateUtil {
private static final String DOC_SUFFIX = ".doc";
public static <T>void exportListFileOnZip(HttpServletResponse response, String zipName, String templateName, List<T> dataList,Class<T> clazz){
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ZipOutputStream zipOutputStream = null;
try {
response.setContentType("application/octet-stream;charset=UTF-8");
response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(zipName, "UTF-8") + ".zip");
zipOutputStream = new ZipOutputStream(response.getOutputStream());
Template template = getTemplate(templateName,clazz);
Map<String,Integer> nameMap = new HashMap<>();
for (T data : dataList) {
String fileName = getFileName(data);
if (nameMap.containsKey(fileName)) {
nameMap.put(fileName,nameMap.get(fileName) +1);
fileName = fileName+"(" + nameMap.get(fileName) +")";
}else {
nameMap.put(fileName,0);
}
ByteArrayOutputStream byteArrayOutputStream = generateOneFile(template,data);
outputStream.write(byteArrayOutputStream.toByteArray());
ZipEntry zipEntry = new ZipEntry(fileName+DOC_SUFFIX);
zipOutputStream.putNextEntry(zipEntry);
zipOutputStream.write(byteArrayOutputStream.toByteArray());
zipOutputStream.closeEntry();
}
zipOutputStream.write(outputStream.toByteArray());
zipOutputStream.closeEntry();
zipOutputStream.flush();
zipOutputStream.close();
} catch (Exception e) {
log.error("导出word文件放在zip里面异常",e);
}finally {
try {
outputStream.close();
zipOutputStream.close();
} catch (IOException e) {
log.error("导出word文件放在zip里面异常",e);
}
}
}
public static <T> void exportListOnOneFile(HttpServletResponse response,String fileName,String templateName, List<T> dataList,Class<T> clazz){
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
try {
response.setContentType("application/msword;charset=UTF-8");
response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(fileName, "UTF-8") + ".doc");
Template template = getTemplate(templateName,clazz);
for (T data : dataList) {
ByteArrayOutputStream byteArrayOutputStream = generateOneFile(template,data);
outputStream.write(byteArrayOutputStream.toByteArray());
}
response.getOutputStream().write(outputStream.toByteArray());
response.getOutputStream().flush();
response.getOutputStream().close();
} catch (Exception e) {
log.error("导出word文件失败",e);
}finally {
try {
outputStream.close();
} catch (IOException e) {
log.error("关闭流失败",e);
}
}
}
public static <T> void exportOneFile(HttpServletResponse response, String fileName, String templateName, T data, Class<T> clazz) throws Exception{
Template template = getTemplate(templateName,clazz);
ByteArrayOutputStream outputStream = generateOneFile(template,data);
response.setContentType("application/msword;charset=UTF-8");
response.setHeader("Content-Disposition", "attachment;filename=" + new String(fileName.getBytes("UTF-8"), "ISO8859-1") + ".doc");
response.getOutputStream().write(outputStream.toByteArray());
response.getOutputStream().flush();
response.getOutputStream().close();
}
private static <T> ByteArrayOutputStream generateOneFile(Template template,T data) throws Exception{
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
OutputStreamWriter writer = new OutputStreamWriter(outputStream);
HashMap<String, Object> dataMap = convertDataToHashMap(data);
if (null == dataMap || dataMap.size() == 0) {
log.error("数据为空");
throw new RuntimeException("数据为空");
}
template.process(dataMap, writer);
return outputStream;
}
private static <T>HashMap<String,Object> convertDataToHashMap(T data){
Class<?> myClass = data.getClass();
Field[] fields = myClass.getDeclaredFields();
HashMap<String,Object> dataMap = new HashMap<>();
for (Field field : fields) {
if (field.isAnnotationPresent(ExportWordField.class)) {
field.setAccessible(true);
if (field.getType().equals(List.class)) {
List<?> list = null;
try {
list = convertListDataToHashMap((List<?>) field.get(data));
} catch (IllegalAccessException e) {
log.error("获取属性值失败",e);
throw new RuntimeException("获取属性值失败");
}
dataMap.put(field.getName(),list);
continue;
}
ExportWordField exportWordField = field.getAnnotation(ExportWordField.class);
String fieldName = exportWordField.fieldName();
try {
if (fieldName != null && !"".equals(fieldName)) {
dataMap.put(fieldName, field.get(data) == null ? "" : field.get(data));
}else {
dataMap.put(field.getName(), field.get(data) == null ? "" : field.get(data));
}
} catch (IllegalAccessException e) {
log.error("获取属性值失败",e);
throw new RuntimeException("获取属性值失败");
}
}
}
return dataMap;
}
private static <T> List<HashMap<String, Object>> convertListDataToHashMap(List<T> list) {
List<HashMap<String, Object>> listMap = new ArrayList<>();
if (!CollectionUtils.isEmpty(list)) {
for (T t : list) {
HashMap<String, Object> hashMap = convertDataToHashMap(t);
listMap.add(hashMap);
}
}
return listMap;
}
private static <T>Template getTemplate(String templateName,Class<T> clazz) throws IOException {
Configuration configuration = new Configuration(Configuration.getVersion());
configuration.setDefaultEncoding("UTF-8");
configuration.setClassForTemplateLoading(clazz, "/templates");
return configuration.getTemplate(templateName);
}
private static <T> String getFileName(T data){
Field[] fields = data.getClass().getDeclaredFields();
StringBuilder fileName = new StringBuilder();
Arrays.stream(fields).filter(field -> field.getAnnotation(ExportWordField.class) != null
&& field.getAnnotation(ExportWordField.class).isFileNameFiled()).sorted((o1, o2) -> {
ExportWordField annotation1 = o1.getAnnotation(ExportWordField.class);
ExportWordField annotation2 = o2.getAnnotation(ExportWordField.class);
return annotation1.fileNameFiledSort() - annotation2.fileNameFiledSort();
}).forEach(field -> {
field.setAccessible(true);
try {
fileName.append(field.get(data)+"-");
} catch (IllegalAccessException e) {
log.error("获取文件名称异常",e);
}
});
return fileName.toString();
}
}
3.使用
ExportTemplateUtil.exportListFileOnZip(response,"测试导出","template2.0.ftl",somethingObj,Something.class);
ExportTemplateUtil.exportOneFile(response,"测试导出","template2.0.ftl",somethingObj,Something.class);
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?