背景
页面上有一份表单,包含基本信息和列表信息,用户填写完信息后,点击预览/导出,可以预览/导出word、pdf、excel文档。
因为代码重构过,所以看流程可能会有点绕,为方便能看懂,这里直接提供了项目的地址
文件生成项目
期望你在本文能收获什么
一套比较完整的文件生成的解决方案和思路
一些代码构建的思路和细节,文中代码的一些组成是我参考在用项目的结构,重构改过来的,用上了一些设计模式,扩展性、规范性应该也还是可以的。
效果示例
doc文件模板
doc生成效果
pdf效果一样
excel文件模板
excel生成效果
文件word、pdf、excel生成/预览的基本流程
- 前端页面,用户填写用户信息,可以包含基本信息和列表信息,用户提交数据
- 后端接收数据后依靠第三方的组件/工具,把文件生成到本地
- 把生成到本地的文件上传到文件服务器并返回文件id/文件访问地址
- 删除本地生成的文件
- 把文件id/文件访问地址返回给前端
- 有的项目组做项目喜欢上传/生成文件再上传后在数据库存文件信息,然后返回文件地址给前端,前端拿到文件地址就直接处理
- 有的项目组做项目喜欢上传/生成文件再上传后在数据库存文件信息,然后返回文件id给前端,前端默认是要知道文件服务器的基础访问地址,所以让前端拿到文件id后他们就会处理了(拼接字符串为文件访问地址(文件id为生成文件名)如 “http://文件地址/文件id”,或者再发个请求到数据库查文件具体信息(拿到地址)都可以)
- 本文使用的是第二种。单独有文件服务器做文件上传,然后把上传成功的文件的基本信息保存到数据库,然后返回文件id给前端。
- 本文主要是介绍如何生成各种类型的文档,所以不包含如何上传文件部分。当然市面上常用的文件上传服务实现这么多,相信你也是可以找到合适的解决方案的。
- 如果以后有空,再给补一个通用的文件上传服务好了,哈哈
主要用到的工具
- FreeMarker(生成word)、aspose(将word转换成pdf)、easyExcel(生成excel)
代码实现
基础工具类及依赖
- maven项目
- spring boot 项目
- 基础依赖
maven依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>org.wwt.file</groupId><artifactId>FileHandle</artifactId><version>1.0-SNAPSHOT</version><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.1.5.RELEASE</version></parent><properties><java.version>1.8</java.version></properties><dependencies><!--mysql运行时依赖--><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><scope>runtime</scope></dependency><!--web--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!--测试的起步依赖--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><!--lombok用来简化实体类--><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><!--用来生成word文档--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-freemarker</artifactId></dependency><!--hutool--><dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.5.2</version></dependency><!-- 转化pdf start --><dependency><groupId>aspose-words</groupId><artifactId>aspose-words</artifactId><version>15.8.0</version><scope>system</scope><systemPath>${project.basedir}/src/main/resources/lib/aspose-words-15.8.0-jdk16.jar</systemPath></dependency><!--转excel--><dependency><groupId>com.alibaba</groupId><artifactId>easyexcel</artifactId><version>3.1.1</version></dependency><!--这个注意要加,我在看官网的时候没看到,只加了上面那个,结果后来实测就少了东西报错了--><dependency><groupId>com.alibaba</groupId><artifactId>easyexcel-core</artifactId><version>3.1.1</version></dependency></dependencies></project>
application.yml
#mysql数据库连接
spring:datasource:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/xxx?serverTimezone=GMT%2B8username: rootpassword: 123456
这里看到我用了mysql,其实我这里数据是由前端传过来的,不是查出来的,所以没用到数据库。只是加个配置项目启动就不会找默认配置报错。当然你可以的通过加个注解的方式解决,这里不提了。
项目结构
4. 基本类
我把包名也复制进去,希望尽量能让你还原项目
实体类
User - 数据实体类
package com.wwt.file.entity;import lombok.Data;@Data
public class User {private Long id;private String name;private Integer age;private String email;
}
FileVO - 文件返回信息
package com.wwt.file.entity.vo;import lombok.Data;/*** 封装文件返回信息**/
@Data
public class FileVO {/*** 文件名*/private String fileName;/*** 文件id*/private Long fileId;public FileVO(String fileName, Long fileId) {this.fileName = fileName;this.fileId = fileId;}
}
UserExportDTO
package com.wwt.file.entity.dto;import cn.hutool.core.map.MapUtil;
import com.wwt.file.entity.User;
import lombok.Data;import java.util.HashMap;
import java.util.List;
import java.util.Map;/*** 封装前端传过来的用户请求信息*/
@Data
public class UserExportDTO {private User user1;private User user2;private List<User> users;/**** @return*/public Map<String, Object> structDataMap() {Map<String, Object> map = MapUtil.createMap(HashMap.class);map.put("user1",this.user1);map.put("user2",this.user2);map.put("users",this.users);return map;}
}
在controller中的使用示例
枚举类
FileTypeEnum - 文件类型的枚举类
package com.wwt.file.enums;/*** 文件类型*/
public enum FileTypeEnum {WORD("word",".doc"),PDF("pdf",".pdf"),EXCEL("excel",".xlsx"),XML("xml",".xml");/*** 类型*/private String type;/*** 后缀*/private String suffix;FileTypeEnum(String type, String suffix) {this.type = type;this.suffix = suffix;}public String getType() {return type;}public String getSuffix() {return suffix;}
}
UserTemplate - 用户模板
package com.wwt.file.enums;/*** 用户模板*/
public enum UserTemplate {USER_TEMPLATE_XML("user_template","用户信息","这是一个用户模板",FileTypeEnum.XML),USER_TEMPLATE_EXCEL("user_template","用户信息","这是一个用户模板",FileTypeEnum.EXCEL);/*** 模板名称*/private String template;/*** 模板中文名*/private String chName;/*** 详情*/private String des;/*** 模板类型*/private String type;/*** 模板后缀*/private String suffix;UserTemplate(String template, String chName, String des, FileTypeEnum fileTypeEnum) {this.chName = chName;this.des = des;this.template = template + fileTypeEnum.getSuffix();this.type = fileTypeEnum.getType();this.suffix = fileTypeEnum.getSuffix();}public String getTemplate() {return template;}public String getChName() {return chName;}public String getDes() {return des;}public String getType() {return type;}public String getSuffix() {return suffix;}
}
这两个枚举类的关联关系
FileTypeEnum :该枚举是单一职责的,只表示文件的类型信息
UserTemplate :通过构造方法引入 FileTypeEnum ,在构造方法中接入 fileTypeEnum 实现
UserTemplate 基本信息的初始化,创建 UserTemplate 时更加清晰明了。
不太好的实现是这样的:
USER_TEMPLATE_XML("user_template","用户信息","这是一个用户模板",FileTypeEnum.XML.getSuffix(),FileTypeEnum.XML.getType()),
USER_TEMPLATE_EXCEL("user_template","用户信息","这是一个用户模板",FileTypeEnum.EXCEL.getSuffix(),FileTypeEnum.EXCEL.getType());UserTemplate(String template, String chName, String des, String suffix,String type) {this.chName = chName;this.des = des;this.template = template + suffix;this.type = type;this.suffix = suffix;}
//又或者不创建 FileTypeEnum ,直接在这些显示文件类型信息的地方都用字符串写死
统一响应结果
Result - 一个抽象的返回结果类
package com.wwt.file.result;import com.wwt.file.result.enums.ResultCodeEnum;
import lombok.Data;@Data
public abstract class Result<T> {public static Result success(){Result r = new Result(){};r.setSuccess(ResultCodeEnum.SUCCESS.getSuccess());r.setCode(ResultCodeEnum.SUCCESS.getCode());r.setMessage(ResultCodeEnum.SUCCESS.getMessage());return r;}public static Result error(){Result r = new Result(){};r.setSuccess(ResultCodeEnum.UNKNOWN_REASON.getSuccess());r.setCode(ResultCodeEnum.UNKNOWN_REASON.getCode());r.setMessage(ResultCodeEnum.UNKNOWN_REASON.getMessage());return r;}/*** 是否成功*/private Boolean success;/*** 响应码*/private Integer code;/*** 响应信息*/private String message;/*** 响应数据*/private T data;public Result data(T data){this.setData(data);return this;}public Result(){}
}
DefaultResult - Result的实现,默认的响应结果类,平时用这个类做响应结果就可以了
package com.wwt.file.result;import com.wwt.file.result.enums.ResultCodeEnum;/*** 默认响应结果* @param <T>*/
public class DefaultResult<T> extends Result<T> {public static DefaultResult setResult(ResultCodeEnum resultCodeEnum){DefaultResult r = new DefaultResult();r.setSuccess(resultCodeEnum.getSuccess());r.setCode(resultCodeEnum.getCode());r.setMessage(resultCodeEnum.getMessage());return r;}
}
ResultCodeEnum - 常用响应结果,有什么想用的响应结果,在这里增加枚举结果对象即可
package com.wwt.file.result.enums;import lombok.Getter;
import lombok.ToString;//这个类是在尚硅谷的相关资料里面抄过来的
@Getter
@ToString
public enum ResultCodeEnum {SUCCESS(true, 20000,"成功"),UNKNOWN_REASON(false, 20001, "未知错误"),BAD_SQL_GRAMMAR(false, 21001, "sql语法错误"),JSON_PARSE_ERROR(false, 21002, "json解析异常"),PARAM_ERROR(false, 21003, "参数不正确"),FILE_UPLOAD_ERROR(false, 21004, "文件上传错误"),FILE_DELETE_ERROR(false, 21005, "文件刪除错误"),EXCEL_DATA_IMPORT_ERROR(false, 21006, "Excel数据导入错误"),VIDEO_UPLOAD_ALIYUN_ERROR(false, 22001, "视频上传至阿里云失败"),VIDEO_UPLOAD_TOMCAT_ERROR(false, 22002, "视频上传至业务服务器失败"),VIDEO_DELETE_ALIYUN_ERROR(false, 22003, "阿里云视频文件删除失败"),FETCH_VIDEO_UPLOADAUTH_ERROR(false, 22004, "获取上传地址和凭证失败"),REFRESH_VIDEO_UPLOADAUTH_ERROR(false, 22005, "刷新上传地址和凭证失败"),FETCH_PLAYAUTH_ERROR(false, 22006, "获取播放凭证失败"),URL_ENCODE_ERROR(false, 23001, "URL编码失败"),ILLEGAL_CALLBACK_REQUEST_ERROR(false, 23002, "非法回调请求"),FETCH_ACCESSTOKEN_FAILD(false, 23003, "获取accessToken失败"),FETCH_USERINFO_ERROR(false, 23004, "获取用户信息失败"),LOGIN_ERROR(false, 23005, "登录失败"),COMMENT_EMPTY(false, 24006, "评论内容必须填写"),PAY_RUN(false, 25000, "支付中"),PAY_UNIFIEDORDER_ERROR(false, 25001, "统一下单错误"),PAY_ORDERQUERY_ERROR(false, 25002, "查询支付结果错误"),ORDER_EXIST_ERROR(false, 25003, "课程已购买"),GATEWAY_ERROR(false, 26000, "服务不能访问"),CODE_ERROR(false, 28000, "验证码错误"),LOGIN_PHONE_ERROR(false, 28009, "手机号码不正确"),LOGIN_MOBILE_ERROR(false, 28001, "账号不正确"),LOGIN_PASSWORD_ERROR(false, 28008, "密码不正确"),LOGIN_DISABLED_ERROR(false, 28002, "该用户已被禁用"),REGISTER_MOBLE_ERROR(false, 28003, "手机号已被注册"),LOGIN_AUTH(false, 28004, "需要登录"),LOGIN_ACL(false, 28005, "没有权限"),SMS_SEND_ERROR(false, 28006, "短信发送失败"),SMS_SEND_ERROR_BUSINESS_LIMIT_CONTROL(false, 28007, "短信发送过于频繁");private Boolean success;private Integer code;private String message;ResultCodeEnum(Boolean success, Integer code, String message) {this.success = success;this.code = code;this.message = message;}
}
正因为感觉把所有的响应结果都封装在一个类里面,后期类就太长了,所以我才把响应结果类 Result 做抽象化,让其实现类的 setResult() 方法来实现不同的响应结果类的入参。当然怕麻烦的话,就直接用 DefaultResult 和 ResultCodeEnum 基本就够用了。
我们这里用到的,扩展其他的响应结果枚举类,以及返回结果类的示例如下:
FileResult - 文件结果类
package com.wwt.file.result.file;import com.wwt.file.result.Result;
import com.wwt.file.result.enums.FileResultEnum;/*** 文件响应结果* @param <T>*/
public class FileResult<T> extends Result<T> {public static FileResult setResult(FileResultEnum FileResultEnum){FileResult r = new FileResult();r.setSuccess(FileResultEnum.getSuccess());r.setCode(FileResultEnum.getCode());r.setMessage(FileResultEnum.getMessage());return r;}}
FileResultEnum - 文件返回结果枚举对象 - 从 ResultCodeEnum 中分离出来了
package com.wwt.file.result.enums;import lombok.Getter;
import lombok.ToString;@Getter
@ToString
public enum FileResultEnum {//只要不是20000都是失败SUCCESS(true, 20000,"成功"),UNKNOWN_REASON(false, 20001, "未知错误"),UPLOAD_SUCCESS(true, 20000, "文件上传成功"),UPLOAD_FAIL(false, 20002, "文件上传失败");private Boolean success;private Integer code;private String message;FileResultEnum(Boolean success, Integer code, String message) {this.success = success;this.code = code;this.message = message;}
}
只要新的响应结果类和新的响应结果枚举类能用 setResult()方法使其一对一关联起来即可
新的响应结果类继承 Result 类即可
使用示例
工具类
FileUtil
package com.wwt.file.util;import cn.hutool.core.io.resource.ClassPathResource;
import cn.hutool.core.lang.UUID;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.wwt.file.enums.FileTypeEnum;/*** 文件处理工具类*/
public class FileUtil {/*** 获取项目根路径* pathStr : D:\code\javaCode\FileHandle\target\classes* 如果是在ide中运行,则和target同级目录,如果是jar部署到服务器,则默认和jar包同级* pathStr : D:\code\javaCode\FileHandle\ FileHandle是我的项目名*/public static String getResourceBasePath() {ClassPathResource classPathResource = new ClassPathResource("");String pathStr = classPathResource.getFile().getAbsolutePath();pathStr = pathStr.replace("target\\classes", "");return pathStr;}/*** 如果父目录不存在,则创建* 保证父目录一定存在* 否则文件可能创建失败* 支持创建多级目录* @Param filePath 文件绝对路径*/public static void mkParentDirIfNotExist(String filePath) {cn.hutool.core.io.FileUtil.mkParentDirs(filePath);}/*** 删除文件或目录及其下文件* @param filePath 文件绝对路径*/public static void delDir(String filePath){cn.hutool.core.io.FileUtil.del(filePath);}/*** 生成随机文件名* @param fileTypeEnum 指定文件类型* @return*/public static String generateFileName(FileTypeEnum fileTypeEnum){if (ObjectUtil.isNull(fileTypeEnum)){return null;}//System.currentTimeMillis() 如果你不想用uuid,用时间也是可以的return StrUtil.concat(true, UUID.randomUUID().toString(true), fileTypeEnum.getSuffix());}}
生成word文档
原理
- word文档生成主要用到的工具是freemarker
- 提供一份word文档模板,在需要插入数据的地方填写占位符,代码中将数据和文档绑定时,会用实际数据替换掉占位符
步骤
依赖
<!--用来生成word文档-->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
新建项目模板目录
新建项目模板
另存为xml文件
把文件模板放进template目录
用notepad++之类的工具类打开并修改模板文件
实体类数据
小提示:如果文件内容不是格式化的,你复制内容到idea里的随便一个xml文档,ctrl+alt+L代码格式化之后再复制回来也是可以的。
为什么先预写数据,然后再查找替换。而不是在word文档里面直接写${xxx.xxx},然后生成xml文件呢?因为实测过可能$符号会因为一些格式问题,预写替换符再成xml的话会出奇怪的问题,比如说$符号和属性分开了,导致数据填不进去,或者报错。
list数据
全部替换后,转为word文档打开应该是如下面这个样子的。但是因为加了list标签的原因,可能替换完就打不开了。不过还是再看看替换完应该是长什么样的吧。
前端数据
{"user1": {"id": 1, "name": "张三", "age": 77, "email": "22667344.@qq.com"}, "user2": {"id": 19, "name": "李四", "age": 68, "email": "33447899@qq.com"}, "users": [{"id": 33, "name": "帅哥1", "age": 18, "email": "23217744@qq.com"}, {"id": 34, "name": "帅哥2", "age": 18, "email": "23217744@qq.com"}, {"id": 35, "name": "帅哥3", "age": 18, "email": "23217744@qq.com"}, {"id": 36, "name": "帅哥4", "age": 18, "email": "23217744@qq.com"}, {"id": 37, "name": "帅哥5", "age": 18, "email": "23217744@qq.com"}, {"id": 38, "name": "帅哥6", "age": 18, "email": "23217744@qq.com"}, {"id": 39, "name": "帅哥7", "age": 18, "email": "23217744@qq.com"}, {"id": 40, "name": "帅哥8", "age": 18, "email": "23217744@qq.com"}]
}
后端接收数据
controller写请求接口
package com.wwt.file.controller;import com.wwt.file.entity.User;
import com.wwt.file.entity.dto.UserExportDTO;
import com.wwt.file.entity.vo.FileVO;
import com.wwt.file.result.DefaultResult;
import com.wwt.file.result.Result;
import com.wwt.file.result.enums.FileResultEnum;
import com.wwt.file.result.file.FileResult;
import com.wwt.file.service.IUserService;
import org.springframework.web.bind.annotation.*;import javax.annotation.Resource;
import java.io.IOException;@RestController
@RequestMapping("/user")
public class UserController {@Resourceprivate IUserService userService;@PostMapping("/export")public Result<FileVO> export(@RequestBody UserExportDTO upd, String type){return FileResult.setResult(FileResultEnum.UPLOAD_SUCCESS).data(userService.export(upd,type));}}
service
IUserService接口
package com.wwt.file.service;import com.wwt.file.entity.dto.UserExportDTO;
import com.wwt.file.entity.vo.FileVO;public interface IUserService {public FileVO export(UserExportDTO upd, String type);}
UserService实现类
package com.wwt.file.service.impl;import cn.hutool.core.util.StrUtil;
import com.wwt.file.entity.dto.UserExportDTO;
import com.wwt.file.entity.vo.FileVO;
import com.wwt.file.enums.FileTypeEnum;
import com.wwt.file.enums.UserTemplate;
import com.wwt.file.service.ExportService;
import com.wwt.file.service.IUserService;
import com.wwt.file.util.excel.UserExcelDataWriter;
import org.springframework.stereotype.Service;import javax.annotation.Resource;@Service
public class UserService implements IUserService {@Resourcepublic ExportService exportService;@Overridepublic FileVO export(UserExportDTO upd, String type){//用工具类生成word文件if (StrUtil.equals(FileTypeEnum.WORD.getType(),type) || StrUtil.equals(FileTypeEnum.PDF.getType(),type)){return exportService.export(upd.structDataMap(), type, UserTemplate.USER_TEMPLATE_XML.getTemplate(),null);}//生成excel的话要提供excelwriterreturn exportService.export(upd.structDataMap(), type, UserTemplate.USER_TEMPLATE_EXCEL.getTemplate(),new UserExcelDataWriter());}
}
解析
ExportService
package com.wwt.file.service;import cn.hutool.core.util.StrUtil;
import com.wwt.file.entity.vo.FileVO;
import com.wwt.file.enums.FileTypeEnum;
import com.wwt.file.enums.UserTemplate;
import com.wwt.file.util.DocUtil;
import com.wwt.file.util.excel.ExcelDataWriter;
import com.wwt.file.util.FileUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;import java.io.File;
import java.util.Map;@Slf4j
@Service
public class ExportService {/*** 临时生成文件的路径*/private static final String TEMP_FILE_DIR = "tmp\\";public FileVO export(Map<String, Object> dataMap, String type ,String tempFileName,ExcelDataWriter writer){if (StrUtil.equals(FileTypeEnum.WORD.getType(),type)){return new FileVO(UserTemplate.USER_TEMPLATE_XML.getChName(),exportWord(dataMap,tempFileName));}if (StrUtil.equals(FileTypeEnum.PDF.getType(),type)){return new FileVO(UserTemplate.USER_TEMPLATE_XML.getChName(),exportPdf(dataMap,tempFileName));}if (StrUtil.equals(FileTypeEnum.EXCEL.getType(),type)){return new FileVO(UserTemplate.USER_TEMPLATE_EXCEL.getChName(),exportExcel(dataMap,writer));}return new FileVO(null,null);}/*** @param dataMap 填充的数据,每个值都不能为null,可以为empty* @param tempFileName 模板文件名,放置在template目录下* @return 文件服务器返回的fileId* @description 模板导出word**/public Long exportWord(Map<String, Object> dataMap, String tempFileName) {//生成文件名 - 随机文件名,防止重名String fileName = FileUtil.generateFileName(FileTypeEnum.WORD);DocUtil.saveWord(tempFileName, fileName, dataMap);//获取文件绝对路径String filePath = StrUtil.concat(true,FileUtil.getResourceBasePath(),TEMP_FILE_DIR,fileName);//上传文件到文件服务器//Long fileId = fileManagerService.uploadFile(filePath, fileName, "temp", ttl);//删除生成的文件//FileUtil.delDir(filePath);//模拟上传成功返回的id return 1234L; //fileId}/*** @param dataMap 填充的数据,每个值都不能为null,可以为empty* @param tempFileName 模板文件名,放置在template目录下* @return 文件服务器返回的fileId* @description 模板导出pdf**/public Long exportPdf(Map<String, Object> dataMap, String tempFileName) {//生成文件名String fileName = FileUtil.generateFileName(FileTypeEnum.PDF);DocUtil.savePdf(tempFileName, fileName, dataMap);String filePath = new File(FileUtil.getResourceBasePath(), TEMP_FILE_DIR + fileName).getAbsolutePath();//上传文件到文件服务器//Long fileId = fileManagerService.uploadFile(filePath, fileName, "temp", ttl);//FileUtil.delDir(filePath);return 1234L; //fileId}/*** 导出excel* 包括生成文档和上传文件* @param dataMap 数据* @param writer 不同的writer实现类处理不同的模板写入* @return*/public Long exportExcel(Map<String, Object> dataMap,ExcelDataWriter writer){writer.export(dataMap);return 1234L;}}
DocUtil 为实现具体文件生成到本地的操作。因为 ExportService 中还涉及到文件上传的操作,所以又把这个生成操作单独提取了出来,封装到DocUtil 中,实现上传和生成的逻辑解耦。
DocUtil
主要看这个 saveWord 方法
package com.wwt.file.util;import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.system.OsInfo;
import cn.hutool.system.SystemUtil;
import com.aspose.words.Document;
import com.aspose.words.FontSettings;
import com.aspose.words.License;
import com.aspose.words.SaveFormat;
import com.wwt.file.enums.FileTypeEnum;
import freemarker.template.Template;
import lombok.extern.slf4j.Slf4j;import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.Map;@Slf4j
public class DocUtil {/*** 临时生成文件的路径*/private static final String TEMP_FILE_DIR = "tmp\\";private static final String LICENSE_FILENAME = "aspose_license.xml";/*** @param templateFileName 模板文件名称* @param fileName 生成的文件名.doc结尾* @param dataMap 数据,* @description 用模板生成word, 模板存放路径在resources/template下,生成的文件路径在jar包的同级目录/tmp路径下* @attention dataMap中的数据不能为null, 可用空值代替* @other 导出的doc文件其实是文本文件, 而docx文件是二进制文件(其实是一个压缩包).关于如何导出docx可参考 https://blog.csdn.net/wantLight/article/details/106105416**/public static void saveWord(String templateFileName, String fileName, Map<String, Object> dataMap) {Writer out = null;try {//根据配置获取FreeMarker模板对象Template template = FreeMarkerTemplateFactory.getTemplate(templateFileName);//拼接出即将生成文件的绝对路径 /xxx/xxx/xxx.docString filePath = StrUtil.concat(true,FileUtil.getResourceBasePath(),TEMP_FILE_DIR,fileName);//如果文件父目录不存在,则创建父目录FileUtil.mkParentDirIfNotExist(filePath);// 创建一个Word文档的输出流out = new BufferedWriter(new OutputStreamWriter(Files.newOutputStream(new File(filePath).toPath()), StandardCharsets.UTF_8));template.process(dataMap, out);out.flush();} catch (Exception e) {log.error("saveWord error:{}", e.getMessage(), e);} finally {IoUtil.close(out);}}/*** @param templateFileName 模板文件名称* @param fileName 生成的文件名.pdf结尾* @param dataMap 数据,* @description 用模板先生成生成word, 再转成pdf,模板存放路径在resources/template下,生成的文件路径在jar包的同级目录/tmp路径下* @attention dataMap中的数据不能为null, 可用空值代替**/public static void savePdf(String templateFileName, String fileName, Map<String, Object> dataMap) {InputStream docInputStream = null;OutputStream outputStream = null;try {String docFileName = fileName.replace(FileTypeEnum.PDF.getSuffix(), FileTypeEnum.WORD.getSuffix());//先生成wordsaveWord(templateFileName, docFileName, dataMap);String docFilePath = StrUtil.concat(true,FileUtil.getResourceBasePath(),TEMP_FILE_DIR,docFileName);//aspose将word转成pdfif (!getLicense()) {// 验证License 若不验证则转化出的pdf文档会有水印产生return;}OsInfo osInfo = SystemUtil.getOsInfo();//linux环境的字体,需要将c:\windows\fonts下的字体文件放到指定目录下,否则会乱码if (osInfo.isLinux()) {FontSettings.setFontsFolder("/usr/share/fonts/chinese", true);}docInputStream = Files.newInputStream(new File(docFilePath).toPath());File outputFile = new File(FileUtil.getResourceBasePath(), "tmp/" + fileName);outputStream = Files.newOutputStream(outputFile.toPath());Document doc = new Document(docInputStream); //sourcerFile是将要被转化的word文档doc.save(outputStream, SaveFormat.PDF);//全面支持DOC, DOCX, OOXML, RTF HTML, OpenDocument, PDF, EPUB, XPS, SWF 相互转换//删除生成的临时word文件FileUtil.delDir(docFilePath);} catch (Exception e) {log.error("savePdf error:{}", e.getMessage(), e);} finally {IoUtil.close(docInputStream);IoUtil.close(outputStream);}}/*** 判断是否有授权文件 如果没有则会认为是试用版,转换的文件会有水印** @return*/public static boolean getLicense() {boolean result = false;try {InputStream is = DocUtil.class.getClassLoader().getResourceAsStream(LICENSE_FILENAME);License asposeLic = new License();asposeLic.setLicense(is);result = true;} catch (Exception e) {log.error("getLicense error:{}", e.getMessage());}return result;}}
如果在测试word,还没用到pdf的功能,代码报错,先把报错的注释掉等会再测试就好了
FreeMarkerTemplateFactory - 用来创建 template对象
package com.wwt.file.util;import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.Version;import java.io.IOException;/*** FreeMarker 创建模板对象的工厂*/
public class FreeMarkerTemplateFactory {/*** FreeMarker的版本*/private static final String FREEMARKER_VERSION = "2.3.28";/*** 模板存放目录*/private static final String TEMPLATE_DIR = "/template";/*** 编码格式*/private static final String CHARSET = "utf-8";/*** 获取模板对象* @param templateFileName* @return* @throws IOException*/public static Template getTemplate(String templateFileName) throws IOException {Configuration configuration = new Configuration(new Version(FREEMARKER_VERSION));configuration.setDefaultEncoding("utf-8");//设置模板位置,默认是classpath下的指定目录下,idea中运行时为resources下的指定目录下configuration.setClassForTemplateLoading(DocUtil.class, TEMPLATE_DIR);Template template = configuration.getTemplate(templateFileName, CHARSET);return template;}}
测试
请求及数据
请求地址:http://localhost:8080/user/export?type=word
请求数据:
{"user1": {"id": 1, "name": "张三", "age": 77, "email": "22667344.@qq.com"}, "user2": {"id": 19, "name": "李四", "age": 68, "email": "33447899@qq.com"}, "users": [{"id": 33, "name": "帅哥1", "age": 18, "email": "23217744@qq.com"}, {"id": 34, "name": "帅哥2", "age": 18, "email": "23217744@qq.com"}, {"id": 35, "name": "帅哥3", "age": 18, "email": "23217744@qq.com"}, {"id": 36, "name": "帅哥4", "age": 18, "email": "23217744@qq.com"}, {"id": 37, "name": "帅哥5", "age": 18, "email": "23217744@qq.com"}, {"id": 38, "name": "帅哥6", "age": 18, "email": "23217744@qq.com"}, {"id": 39, "name": "帅哥7", "age": 18, "email": "23217744@qq.com"}, {"id": 40, "name": "帅哥8", "age": 18, "email": "23217744@qq.com"}]
}
可能会遇到的问题
- 文件生成到本地不成功,没权限(window下)
Could not open/create prefs root node Software\JavaSoft\Prefs at root
参考:https://blog.csdn.net/yxlzfx/article/details/117767879 - 数据填不进去word文档,但是代码执行没报错。这个一方面你要看看你模板中替换符的名字是否和你构建的map的key对应得上。另一方面,你是不是用过idea在启动项目的情况下修改了xml的模板文件,如果是,你看看target/class/template是不是没有替换符。我改用nodpad来修改xml就是这个意思,可能会修改不成功。打包打不进去修改了的内容。所以还是停止项目,用nodpad等工具来改比较好。
生成PDF文档
原理
- 生成pdf主要用的是 aspose
- 流程就是先生成word文档到临时文件目录,然后用 aspose-word 将word文档转换成pdf,再上传到文件服务器返回文件id,再删除pdf文件,返回文件id给前端。
- 生成word的工作调用上面已实现接口,即可。
步骤
下载jar包
地址: https://repository.aspose.com/repo/com/aspose/
进去后选一个版本下即可
把jar包添加到项目中
添加依赖
<!-- 转化pdf start --><dependency><groupId>aspose-words</groupId><artifactId>aspose-words</artifactId><version>15.8.0</version><scope>system</scope><systemPath>${project.basedir}/src/main/resources/lib/aspose-words-15.8.0-jdk16.jar</systemPath></dependency>
注意版本和jar包名字要对应的上
添加认证文件
<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>
Service
UserService
没有改变,因为都是通过 ExportService 的export 接口根据类型判断统一实现的。
package com.wwt.file.service.impl;import cn.hutool.core.util.StrUtil;
import com.wwt.file.entity.dto.UserExportDTO;
import com.wwt.file.entity.vo.FileVO;
import com.wwt.file.enums.FileTypeEnum;
import com.wwt.file.enums.UserTemplate;
import com.wwt.file.service.ExportService;
import com.wwt.file.service.IUserService;
import com.wwt.file.util.excel.UserExcelDataWriter;
import org.springframework.stereotype.Service;import javax.annotation.Resource;@Servicepublic class UserService implements IUserService {@Resourcepublic ExportService exportService;@Overridepublic FileVO export(UserExportDTO upd, String type){//用工具类生成word文件if (StrUtil.equals(FileTypeEnum.WORD.getType(),type) || StrUtil.equals(FileTypeEnum.PDF.getType(),type)){return exportService.export(upd.structDataMap(), type, UserTemplate.USER_TEMPLATE_XML.getTemplate(),null);}return exportService.export(upd.structDataMap(), type, UserTemplate.USER_TEMPLATE_EXCEL.getTemplate(),new UserExcelDataWriter());}}
ExportService
package com.wwt.file.service;import cn.hutool.core.util.StrUtil;
import com.wwt.file.entity.vo.FileVO;
import com.wwt.file.enums.FileTypeEnum;
import com.wwt.file.enums.UserTemplate;
import com.wwt.file.util.DocUtil;
import com.wwt.file.util.excel.ExcelDataWriter;
import com.wwt.file.util.FileUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;import java.io.File;
import java.util.Map;@Slf4j
@Service
public class ExportService {/*** 临时生成文件的路径*/private static final String TEMP_FILE_DIR = "tmp\\";public FileVO export(Map<String, Object> dataMap, String type ,String tempFileName,ExcelDataWriter writer){if (StrUtil.equals(FileTypeEnum.WORD.getType(),type)){return new FileVO(UserTemplate.USER_TEMPLATE_XML.getChName(),exportWord(dataMap,tempFileName));}if (StrUtil.equals(FileTypeEnum.PDF.getType(),type)){return new FileVO(UserTemplate.USER_TEMPLATE_XML.getChName(),exportPdf(dataMap,tempFileName));}if (StrUtil.equals(FileTypeEnum.EXCEL.getType(),type)){return new FileVO(UserTemplate.USER_TEMPLATE_EXCEL.getChName(),exportExcel(dataMap,writer));}return new FileVO(null,null);}/*** @param dataMap 填充的数据,每个值都不能为null,可以为empty* @param tempFileName 模板文件名,放置在template目录下* @return 文件服务器返回的fileId* @description 模板导出word**/public Long exportWord(Map<String, Object> dataMap, String tempFileName) {//生成文件名String fileName = FileUtil.generateFileName(FileTypeEnum.WORD);DocUtil.saveWord(tempFileName, fileName, dataMap);//获取文件绝对路径String filePath = StrUtil.concat(true,FileUtil.getResourceBasePath(),TEMP_FILE_DIR,fileName);//上传文件到文件服务器//Long fileId = fileManagerService.uploadFile(filePath, fileName, "temp", ttl);//删除生成的文件//FileUtil.delDir(filePath);//模拟上传成功返回的idreturn 1234L; //fileId}/*** @param dataMap 填充的数据,每个值都不能为null,可以为empty* @param tempFileName 模板文件名,放置在template目录下* @return 文件服务器返回的fileId* @description 模板导出pdf**/public Long exportPdf(Map<String, Object> dataMap, String tempFileName) {//生成文件名String fileName = FileUtil.generateFileName(FileTypeEnum.PDF);DocUtil.savePdf(tempFileName, fileName, dataMap);String filePath = new File(FileUtil.getResourceBasePath(), TEMP_FILE_DIR + fileName).getAbsolutePath();//上传文件到文件服务器//Long fileId = fileManagerService.uploadFile(filePath, fileName, "temp", ttl);//FileUtil.delDir(filePath);return 1234L; //fileId}/*** 导出excel* 包括生成文档和上传文件* @param dataMap 数据* @param writer 不同的writer实现类处理不同的模板写入* @return*/public Long exportExcel(Map<String, Object> dataMap,ExcelDataWriter writer){writer.export(dataMap);return 1234L;}}
DocUtil
package com.wwt.file.util;import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.system.OsInfo;
import cn.hutool.system.SystemUtil;
import com.aspose.words.Document;
import com.aspose.words.FontSettings;
import com.aspose.words.License;
import com.aspose.words.SaveFormat;
import com.wwt.file.enums.FileTypeEnum;
import freemarker.template.Template;
import lombok.extern.slf4j.Slf4j;import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.Map;@Slf4j
public class DocUtil {/*** 临时生成文件的路径*/private static final String TEMP_FILE_DIR = "tmp\\";private static final String LICENSE_FILENAME = "aspose_license.xml";/*** @param templateFileName 模板文件名称* @param fileName 生成的文件名.doc结尾* @param dataMap 数据,* @description 用模板生成word, 模板存放路径在resources/template下,生成的文件路径在jar包的同级目录/tmp路径下* @attention dataMap中的数据不能为null, 可用空值代替* @other 导出的doc文件其实是文本文件, 而docx文件是二进制文件(其实是一个压缩包).关于如何导出docx可参考 https://blog.csdn.net/wantLight/article/details/106105416**/public static void saveWord(String templateFileName, String fileName, Map<String, Object> dataMap) {Writer out = null;try {//根据配置获取FreeMarker模板对象Template template = FreeMarkerTemplateFactory.getTemplate(templateFileName);String filePath = StrUtil.concat(true,FileUtil.getResourceBasePath(),TEMP_FILE_DIR,fileName);//如果文件父目录不存在,则创建父目录FileUtil.mkParentDirIfNotExist(filePath);// 创建一个Word文档的输出流out = new BufferedWriter(new OutputStreamWriter(Files.newOutputStream(new File(filePath).toPath()), StandardCharsets.UTF_8));template.process(dataMap, out);out.flush();} catch (Exception e) {log.error("saveWord error:{}", e.getMessage(), e);} finally {IoUtil.close(out);}}/*** @param templateFileName 模板文件名称* @param fileName 生成的文件名.pdf结尾* @param dataMap 数据,* @description 用模板先生成生成word, 再转成pdf,模板存放路径在resources/template下,生成的文件路径在jar包的同级目录/tmp路径下* @attention dataMap中的数据不能为null, 可用空值代替**/public static void savePdf(String templateFileName, String fileName, Map<String, Object> dataMap) {InputStream docInputStream = null;OutputStream outputStream = null;try {String docFileName = fileName.replace(FileTypeEnum.PDF.getSuffix(), FileTypeEnum.WORD.getSuffix());//先生成wordsaveWord(templateFileName, docFileName, dataMap);String docFilePath = StrUtil.concat(true,FileUtil.getResourceBasePath(),TEMP_FILE_DIR,docFileName);//aspose将word转成pdfif (!getLicense()) {// 验证License 若不验证则转化出的pdf文档会有水印产生return;}OsInfo osInfo = SystemUtil.getOsInfo();//linux环境的字体,需要将c:\windows\fonts下的字体文件放到指定目录下,否则会乱码if (osInfo.isLinux()) {FontSettings.setFontsFolder("/usr/share/fonts/chinese", true);}docInputStream = Files.newInputStream(new File(docFilePath).toPath());File outputFile = new File(FileUtil.getResourceBasePath(), "tmp/" + fileName);outputStream = Files.newOutputStream(outputFile.toPath());Document doc = new Document(docInputStream); //sourcerFile是将要被转化的word文档doc.save(outputStream, SaveFormat.PDF);//全面支持DOC, DOCX, OOXML, RTF HTML, OpenDocument, PDF, EPUB, XPS, SWF 相互转换//删除生成的临时word文件FileUtil.delDir(docFilePath);} catch (Exception e) {log.error("savePdf error:{}", e.getMessage(), e);} finally {IoUtil.close(docInputStream);IoUtil.close(outputStream);}}/*** 判断是否有授权文件 如果没有则会认为是试用版,转换的文件会有水印** @return*/public static boolean getLicense() {boolean result = false;try {InputStream is = DocUtil.class.getClassLoader().getResourceAsStream(LICENSE_FILENAME);License asposeLic = new License();asposeLic.setLicense(is);result = true;} catch (Exception e) {log.error("getLicense error:{}", e.getMessage());}return result;}}
测试
请求地址: http://localhost:8080/user/export?type=pdf
数据同word
生成excel
原理
- 使用的是easyExcel
- 原理也是生成生成Excel文件,然后上传文件服务器,然后返回文件id,再删除临时文件
- 因为excel的处理不再是模板不同,构建不同的dataMap即可生成。而是对于每种模板,都要相应地设计不同的写入方式,所以这里我引入了自定义的数据处理抽象类 ExcelDataWriter 。我们处理不同的模板,就构建其子类,实现抽象方法即可。
- 写数据的时候,基本对象信息,列表信息是要分次写入的,列表数据如果过大,还能分批写入,easyExcel会提供文件缓存。
步骤
依赖
<!--转excel--><dependency><groupId>com.alibaba</groupId><artifactId>easyexcel</artifactId><version>3.1.1</version></dependency><!--这个注意要加,我在看官网的时候没看到,只加了上面那个,结果后来实测就少了东西报错了--><dependency><groupId>com.alibaba</groupId><artifactId>easyexcel-core</artifactId><version>3.1.1</version></dependency>
添加excel模板
Service
UserService
@Servicepublic class UserService implements IUserService {@Resourcepublic ExportService exportService;@Overridepublic FileVO export(UserExportDTO upd, String type){//用工具类生成word文件if (StrUtil.equals(FileTypeEnum.WORD.getType(),type) || StrUtil.equals(FileTypeEnum.PDF.getType(),type)){return exportService.export(upd.structDataMap(), type, UserTemplate.USER_TEMPLATE_XML.getTemplate(),null);}return exportService.export(upd.structDataMap(), type, UserTemplate.USER_TEMPLATE_EXCEL.getTemplate(),new UserExcelDataWriter());}}
ExportService
package com.wwt.file.service;import cn.hutool.core.util.StrUtil;
import com.wwt.file.entity.vo.FileVO;
import com.wwt.file.enums.FileTypeEnum;
import com.wwt.file.enums.UserTemplate;
import com.wwt.file.util.DocUtil;
import com.wwt.file.util.excel.ExcelDataWriter;
import com.wwt.file.util.FileUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;import java.io.File;
import java.util.Map;@Slf4j
@Service
public class ExportService {/*** 临时生成文件的路径*/private static final String TEMP_FILE_DIR = "tmp\\";public FileVO export(Map<String, Object> dataMap, String type ,String tempFileName,ExcelDataWriter writer){if (StrUtil.equals(FileTypeEnum.WORD.getType(),type)){return new FileVO(UserTemplate.USER_TEMPLATE_XML.getChName(),exportWord(dataMap,tempFileName));}if (StrUtil.equals(FileTypeEnum.PDF.getType(),type)){return new FileVO(UserTemplate.USER_TEMPLATE_XML.getChName(),exportPdf(dataMap,tempFileName));}if (StrUtil.equals(FileTypeEnum.EXCEL.getType(),type)){return new FileVO(UserTemplate.USER_TEMPLATE_EXCEL.getChName(),exportExcel(dataMap,writer));}return new FileVO(null,null);}/*** @param dataMap 填充的数据,每个值都不能为null,可以为empty* @param tempFileName 模板文件名,放置在template目录下* @return 文件服务器返回的fileId* @description 模板导出word**/public Long exportWord(Map<String, Object> dataMap, String tempFileName) {//生成文件名String fileName = FileUtil.generateFileName(FileTypeEnum.WORD);DocUtil.saveWord(tempFileName, fileName, dataMap);//获取文件绝对路径String filePath = StrUtil.concat(true,FileUtil.getResourceBasePath(),TEMP_FILE_DIR,fileName);//上传文件到文件服务器//Long fileId = fileManagerService.uploadFile(filePath, fileName, "temp", ttl);//删除生成的文件//FileUtil.delDir(filePath);//模拟上传成功返回的idreturn 1234L; //fileId}/*** @param dataMap 填充的数据,每个值都不能为null,可以为empty* @param tempFileName 模板文件名,放置在template目录下* @return 文件服务器返回的fileId* @description 模板导出pdf**/public Long exportPdf(Map<String, Object> dataMap, String tempFileName) {//生成文件名String fileName = FileUtil.generateFileName(FileTypeEnum.PDF);DocUtil.savePdf(tempFileName, fileName, dataMap);String filePath = new File(FileUtil.getResourceBasePath(), TEMP_FILE_DIR + fileName).getAbsolutePath();//上传文件到文件服务器//Long fileId = fileManagerService.uploadFile(filePath, fileName, "temp", ttl);//FileUtil.delDir(filePath);return 1234L; //fileId}/*** 导出excel* 包括生成文档和上传文件* @param dataMap 数据* @param writer 不同的writer实现类处理不同的模板写入* @return*/public Long exportExcel(Map<String, Object> dataMap,ExcelDataWriter writer){String fileName = writer.export(dataMap);String filePath = StrUtil.concat(true,FileUtil.getResourceBasePath(),TEMP_FILE_DIR,fileName);//文件上传逻辑自己实现//Long fileId = fileManagerService.uploadFile(filePath, fileName, "temp", ttl);//FileUtil.delDir(filePath);return 1234L;}}
excel的处理逻辑类
ExcelDataWriter
package com.wwt.file.util.excel;import cn.hutool.core.io.resource.ClassPathResource;
import cn.hutool.core.util.StrUtil;
import com.wwt.file.enums.FileTypeEnum;
import com.wwt.file.util.FileUtil;
import lombok.Data;import java.util.Map;@Data
public abstract class ExcelDataWriter {/*** 模板名* template.xlsx* 子类构造方法去定义*/private String template;/*** 模板所在目录路径* /xxx/xxx/xxx/target/classes/template/*/private String tempBasePath;/*** 模板绝对路径* /xxx/xxx/xxx/target/classes/template/template.xlsx*/private String templatePath;/*** 生成的文件名* aaa.xlsx*/private String fileName;/*** 生成的文件的绝对路径* /xxx/xxx/xxx/aaa.xlsx*/private String filePath;//抽象方法,让子类实现怎么根据dataMap去写数据生成文件,返回文件名字public abstract String export(Map<String, Object> dataMap);/*** 初始化参数*/public ExcelDataWriter(String template) {//D:\code\javaCode\FileHandle\target\classesClassPathResource classPathResource = new ClassPathResource("");String classPath = classPathResource.getFile().getAbsolutePath();this.template = template;//D:\code\javaCode\FileHandle\target\classes\template\this.tempBasePath = StrUtil.concat(true,classPath,"\\template\\");//D:\code\javaCode\FileHandle\target\classes\template\xxx.xlsxthis.templatePath = StrUtil.concat(true,tempBasePath,template);this.fileName = FileUtil.generateFileName(FileTypeEnum.EXCEL);//D:\code\javaCode\FileHandle\tmp\123456.xlsxthis.filePath = StrUtil.concat(true,FileUtil.getResourceBasePath(),"tmp\\",this.fileName);//写数据前要保证父目录一定要存在FileUtil.mkParentDirIfNotExist(this.filePath);}
}
UserExcelDataWriter
package com.wwt.file.util.excel;import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ObjectUtil;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.enums.WriteDirectionEnum;
import com.alibaba.excel.write.metadata.WriteSheet;
import com.alibaba.excel.write.metadata.fill.FillConfig;
import com.alibaba.excel.write.metadata.fill.FillWrapper;
import com.wwt.file.entity.User;
import com.wwt.file.enums.UserTemplate;
import lombok.extern.slf4j.Slf4j;import java.util.HashMap;
import java.util.List;
import java.util.Map;/*** 用户excel模板生成处理类*/
@Slf4j
public class UserExcelDataWriter extends ExcelDataWriter{//指定模板名private final String template = UserTemplate.USER_TEMPLATE_EXCEL.getTemplate();/*** 初始化参数**/public UserExcelDataWriter() {super(UserTemplate.USER_TEMPLATE_EXCEL.getTemplate());}/*** 导出文件到本地* 返回文件的绝对路径* @param dataMap* @return*/@Overridepublic String export(Map<String, Object> dataMap) {User user1 = MapUtil.get(dataMap, "user1", User.class);User user2 = MapUtil.get(dataMap, "user2", User.class);List<User> user3 = MapUtil.get(dataMap, "users", List.class);List<User> user4 = MapUtil.get(dataMap, "users", List.class);List<User> user5 = MapUtil.get(dataMap, "users", List.class);String templateFileName = super.getTemplatePath();String filePath = super.getFilePath();ExcelWriter excelWriter = null;String fileName = null;try{//技巧,数据分段写入excelWriter = EasyExcel.write(filePath).withTemplate(templateFileName).build();WriteSheet writeSheet = EasyExcel.writerSheet().build();HashMap<String, Object> baseMessMap = MapUtil.newHashMap();HashMap<String, Object> user3ListMap = MapUtil.newHashMap();HashMap<String, Object> user4ListMap = MapUtil.newHashMap();//简单模板数据写入baseMessMap.put("name1",user1.getName());baseMessMap.put("age1",user1.getAge());baseMessMap.put("email1",user1.getEmail());baseMessMap.put("id1",user1.getId());baseMessMap.put("name2",user2.getName());baseMessMap.put("age2",user2.getAge());baseMessMap.put("email2",user2.getEmail());baseMessMap.put("id2",user2.getId());excelWriter.fill(baseMessMap, writeSheet);//横向列表数据写入FillConfig fillConfig = FillConfig.builder().direction(WriteDirectionEnum.HORIZONTAL).build();// 如果有多个list 模板上必须有{前缀.} 这里的前缀就是 data5,然后多个list必须用 FillWrapper包裹excelWriter.fill(new FillWrapper("user5", user5), fillConfig, writeSheet);//纵向列表数据写入 如果数据量很大,可以看看官网,有让你分段查询插入数据的办法,会用到缓存,效率还是很高的excelWriter.fill(new FillWrapper("user3", user3), writeSheet);excelWriter.fill(new FillWrapper("user4", user4), writeSheet);fileName = super.getFileName();}catch (Exception e){log.error("excel create error : ",e.getMessage(), e);}finally {if (ObjectUtil.isNotNull(excelWriter)){excelWriter.finish();}}return fileName;}
}
测试
请求地址:http://localhost:8080/user/export?type=excel
数据同word,只是构建方法不一样,看上面的 UserExcelDataWriter 的 export