记录项目中通过freemark生成word文档。
freemark生成word文档一个不好的地方就是需要手动将带有占位符的.doc模板转成xml文件(另存为2003xml),不好就不好在一些占位符被分隔开,需要手动取处理(可以用notepad++格式化下并处理,比较美观:开启xml支持插件);

? ? ? ? ? ? ? ? ? ?  要吐槽的是什么先转xml再填充占位符,或者是先把占位符写在记事本里面再复制到.doc里面...全是扯淡,不符合word里面单词拼写的还是照样会被分开;
关于freemark的官方资料,自行去看官网,这边仅记录下自己项目中使用的;
网上大部分博客都是直接一个demo,扔几个占位符,然后从本地磁盘或是指定路径读取模板,再将生成word输出到指定路径,有个卵用,实际项目有多少是这样的...
?
下面记录下自己在java中利用freemark生成报告并下载:
1)数据库配置xml模板路径(存于oss)动态生成word文档,并下载到本地
2)当批量下载的时候,需打成zip包,并提供处理进度查询
3)已下载的文件支持可重复下载(文件放到oss服务器)
?
项目使用技术栈(前后端分离):vue+springBoot+mybatisPlus
项目第一版实现的是将xml模板放在resources下面,但考虑到模板的灵活性及可配置,改用上传oss;
?
直接上代码,不废话
控制层(判空啥都略过,因为业务操作部分每个项目不一样,只记录重要步骤):
package com.xxxx.modules.api.controller;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.deepoove.poi.XWPFTemplate;
import com.deepoove.poi.config.Configure;
import com.xxxx.common.utils.FreemarkerUtil;
import com.xxxx.common.utils.PoiUtil;
import com.xxxx.common.utils.ZipUtil;
import com.xxxx.modules.constant.ApiConsts;
import com.xxxx.modules.framework.PendingJobPool;
import com.xxxx.modules.framework.vo.TaskResult;
import com.xxxx.modules.framework.vo.TaskResultType;
import com.xxxx.modules.heath.dto.TCPatientsDTO;
import com.xxxx.modules.heath.dto.TCPhsUserDTO;
import com.xxxx.modules.heath.dto.TCTransportLogDTO;
import com.xxxx.modules.heath.entity.TCTransportLogEntity;
import com.xxxx.modules.heath.service.*;
import com.xxxx.modules.jt.service.SingleTablePolicy;
import com.xxxx.modules.jt.service.WordService;
import com.xxxx.modules.oss.cloud.OSSFactory;
import freemarker.template.Template;
import io.renren.common.annotation.LogOperation;
import io.renren.common.constant.Constant;
import io.renren.common.utils.ConvertUtils;
import io.renren.common.utils.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.MapUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.*;
import java.util.stream.Collectors;
import java.util.zip.Adler32;
import java.util.zip.CheckedOutputStream;
import java.util.zip.ZipOutputStream;
@Slf4j
public class ClientController {//缓存批量下载的jobName
public static Map<String,Object> batchJobNameCache = new HashMap<>();
// 取得机器的cpu数量
public static final int THREAD_COUNTS = Runtime.getRuntime().availableProcessors();
public static ExecutorService docMakePool = Executors.newFixedThreadPool(THREAD_COUNTS*2);
@GetMapping("xxxx")
@ApiOperation("批量下载")
@LogOperation("批量下载")
@ResponseBody
public Result batchDownloadPlanscode(@RequestParam Map<String,Object> params,HttpServletRequest request) throws Exception {
//参数判断
//判断任务是否已经存在(防重复)
String jobName = "xxxx";
String jobNameExist = MapUtils.getString(batchJobNameCache,jobName,"");
if(StringUtils.isNotEmpty(jobNameExist)){
log.info(tipStr + "下载正在处理中,请耐心等待~");
return new Result().error(201,tipStr + "下载正在处理中,请耐心等待~");
}
//1、数据库取模板路径,获得模板实例
String url = xxxxService.getTemplateUrl(planCode,downType);
if(StringUtils.isEmpty(url)){
log.info("模板url为空");
return new Result().error(202,"请先指定模板~");
}
//本次任务显示的中文名称(作为客户端显示)——根据实际需求,看是否需要,可以是客户端传参
String downName = "xxxx";
// 2、这个根据项目实际需求
List<TCPatientsDTO> list = xxxxxService.getByPlansCode(planCode);
if(list.isEmpty()){
log.info("没有可下载的数据");
return new Result().error(201,"暂无该场次报告数据!");
}
//本批次任务记录的主键
Long id = IdWorker.getId();
batchJobNameCache.put(jobName,0);
//3、另起一个线程处理下载任务(重要)
new Thread(new AsynMakeDoc(id,list,downType,url)).start();
//4、记录下载痕迹
TCTransportLogDTO dto = new TCTransportLogDTO();
dto.setId(id);
dto.setUserName(phsUser.getUserName());//当前用户
dto.setBusinessType(ApiConsts.TRANSMISSION_UPLOAD);//上传(生成报告-打zip包-上传oss)
dto.setStatus(1);//有效
dto.setJobName(jobName);//批次任务唯一标识
dto.setResultName(downName);//批次任务中文名称,作为下载记录的显示在客户端
dto.setJobType(0);//0-批次,1-子任务
dto.setCreateDate(new Date());
tcTransportLogService.insert(ConvertUtils.sourceToTarget(dto,TCTransportLogEntity.class));
return new Result().success(200,"添加下载新任务成功~",jobName + "," + id);
}
/**
* 异步处理word生成
*/
class AsynMakeDoc implements Runnable{
private Long id;//主键
private String jobName;//场次号_档案_用户
private List<TCPatientsDTO> list;private String templateUrl;
public AsynMakeDoc(Long id,String jobName,List<TCPatientsDTO> list,String templateUrl) {
this.jobName = jobName;
this.list = list;
this.downType = downType;
this.templateUrl = templateUrl;
this.id = id;
}
@Override
public void run() {
Object template;
//取oss模板的后缀
String templateType = wordService.getType(templateUrl);
if(ApiConsts.TEMPLATE_TYPE_XML.equals(templateType)){
templateType = ApiConsts.TEMPLATE_TYPE_XML;
template = FreemarkerUtil.getTemplate(templateUrl);
}else{
templateType = ApiConsts.TEMPLATE_TYPE_POI;
//多个自定义渲染策略
Configure configures = Configure.createDefault();
configures.customPolicy("urines",new SingleTablePolicy(1,5));
configures.customPolicy("bloods",5));
configures.customPolicy("examines",5));
template = PoiUtil.getTemplate(templateUrl,configures);
}
String fileName = "_报告.doc";
String zipName = "_报告.zip";
String plansCode = list.get(0).getPlansCode();
//报告、压缩包临时路径
String outTempPath = "";
String zipPath = "";
//更新批次下载记录状态
TCTransportLogEntity dto = new TCTransportLogEntity();
dto.setId(id);
dto.setJobType(1);//不管成功失败,批次任务变更为子任务
dto.setBusinessType(ApiConsts.TRANSMISSION_DOWNLOAD);//变更为下载
File zipFile = null;
ZipOutputStream zos = null;
//生成目标文件对象的输出流
OutputStream outputStream = null;
try {
//临时压缩包目录:本地磁盘/jobName.zip(jobName需保证多用户并发时候不会相互干扰)
outTempPath = getTempPath();
zipPath = outTempPath + jobName + ".zip";
zipFile = new File(zipPath);
log.info("temporary zip :" + zipPath);
outputStream = new FileOutputStream(zipPath);
CheckedOutputStream cos = new CheckedOutputStream(outputStream,new Adler32());
// 生成ZipOutputStream,用于写入要压缩的文件
zos = new ZipOutputStream(cos);
//1、往线程池添加任务
log.info(" start generating words...");
CompletionService<String> docCompletionService = new ExecutorCompletionService<String>(docMakePool);
for (int i = 0; i < list.size(); i++) {
docCompletionService.submit(new DocMakeTask(list.get(i),fileName,template,outTempPath + jobName,templateType));
}
//计算已打成完成数量
int zipCount = 0;
//2、从线程池取执行结果进行压缩
for (int j = 0; j < list.size(); j++) {
// 阻塞取结果
Future<String> future = docCompletionService.take();
// 判断要压缩的源文件是否存在
String path = future.get();
if (!StringUtils.isEmpty(path)) {
File sourceFile = new File(path);
if (!sourceFile.exists()) {
throw new RuntimeException("[" + sourceFile + "] is not exists ...");
}
ZipUtil.compressFile(sourceFile,zos,sourceFile.getName(),true);
if (sourceFile.exists()) {
sourceFile.delete();
}
zipCount++;
//通过应用缓存更新处理进度
log.info("压缩进度:" + zipCount + "/" + list.size() + " : " + zipCount*100/list.size());
batchJobNameCache.put(jobName,zipCount*100/list.size());
}
}
//关闭压缩流(不然上传的文件是不完整的)
zos.finish();
zos.close();
outputStream.close();
long s1 = System.currentTimeMillis();
log.info(jobName + ".zip completed,耗时:" + (s1 - start));
//删除临时文件夹
File docTempDir = new File(outTempPath + jobName);
if(docTempDir.exists()){
docTempDir.delete();
log.info(" temporary folder " + jobName + " has been deleted ");
}
log.info(" ready to upload ");
//3、压缩包上传oss,路径自定义:场次号/场次号_healthy.zip
FileInputStream inputStream = null;
String ossPathName = downType + "/" + plansCode + "/"+ System.currentTimeMillis() + "/" + plansCode + zipName;
String ossPath = "";
try{
inputStream = new FileInputStream(zipPath);
ossPath = OSSFactory.build().upload(inputStream,ossPathName);
batchJobNameCache.put(jobName + "_ossPath",ossPath);
log.info("upload complete,ossPath:" + ossPath);
dto.setResultReturn(ossPath);
dto.setResultType(String.valueOf(TaskResultType.Success));
}catch (Exception e){
dto.setResultType(String.valueOf(TaskResultType.Failure));
batchJobNameCache.put(jobName + "_ossPath",String.valueOf(TaskResultType.Failure));
// batchJobNameCache.remove(jobName);
dto.setResultReason("上传压缩包失败");
log.info(" upload zip failure ");
}finally {
if(inputStream != null){
inputStream.close();
}
}
} catch (Exception e) {
dto.setResultReason("打包失败");
batchJobNameCache.put(jobName + "_ossPath",String.valueOf(TaskResultType.Failure));
// batchJobNameCache.remove(jobName);
dto.setResultType(String.valueOf(TaskResultType.Failure));
log.info(" zip failure ");
}finally {
//删除压缩包
if (zipFile.exists()) {
zipFile.delete();
log.info(jobName + ".zip has been deleted ! ");
}
//更新批次任务为子任务状态(0->1)
tcTransportLogService.updateById(dto);
}
}
}
/**
* 生成wor并返回相应path
*/
class DocMakeTask implements Callable<String> {
private TCPatientsDTO tcPatientsDTO;
private Object template;private String fileName;
private String outPath; //生成报告的临时根目录
private String templateType;
public DocMakeTask(TCPatientsDTO tcPatientsDTO,String fileName,Object template,String outPath,String templateType) {
this.tcPatientsDTO = tcPatientsDTO;
this.fileName = fileName;
this.template = template;
this.outPath = outPath;this.templateType = templateType;
}
@Override
public String call() throws Exception {
//生成的报告的临时目录
String docTempPath = "";
// 取模板填充数据
Map<String,Object> dataMap = "";
docTempPath = outPath + File.separator + "xxx_xxx" + fileName;
// 生成报告
if(ApiConsts.TEMPLATE_TYPE_XML.equals(templateType)){
FreemarkerUtil.createWordByTemplate((Template) template,docTempPath,dataMap);
}else{
PoiUtil.writeToFileByTemplate((XWPFTemplate) template,dataMap);
}
log.info("word :" + docTempPath);
return docTempPath;
}
}
@GetMapping("archives")
@ApiOperation("单份报告下载")
@LogOperation("单份报告下载")
@ResponseBody
public Result archives(@RequestParam Map<String,HttpServletRequest request) throws Exception {//参数判空处理等都略过。。。
//根据业务类型取取模板实例(通过oss链接取模板)
String url = wordService.getTemplateUrl(plansCode,downType);
if(StringUtils.isEmpty(url)){
log.info("模板链接为空");
return new Result().error(202,"请先指定报告模板");
}
//取oss模板的后缀(项目支持POI和xml)
String templateType = wordService.getType(url);
if(ApiConsts.TEMPLATE_TYPE_XML.equals(templateType)){ //"xml"
templateType = ApiConsts.TEMPLATE_TYPE_XML;
}else{ //"poi"
templateType = ApiConsts.TEMPLATE_TYPE_POI;
}
//2、取业务类型对应数据
Map<String,Object> dataMap = new HashMap<>();
if(ApiConsts.RESIDENT_HEALTHY.equals(downType)){ //健康档案
dataMap = wordService.getDataMapPoi(plansCode,sn,ApiConsts.RESIDENT_HEALTHY,templateType);
}else if(ApiConsts.RESIDENT_REPORT.equals(downType)){ //体检报告
dataMap = wordService.getDataMapPoi(plansCode,ApiConsts.RESIDENT_REPORT,templateType);
}
String name = MapUtils.getString(dataMap,"name");
//oos名称:{downType}/{planscoe}/时间戳/{sn}_{name}_xxx.doc
String ossPathName = downType + "/" + plansCode + "/" + System.currentTimeMillis() + "/" + sn + "_" + name + "_" + fileName;
//记录下载痕迹
TCTransportLogDTO dto = null;
String userName = phsUser.getUserName();
//上传oss返回的链接
String ossPath = "";
try{
if(ApiConsts.TEMPLATE_TYPE_POI.equals(templateType)){
//临时目录
String outTempPath = wordService.getTempPath() + File.separator + sn + "_" + name + fileName;
//多个自定义渲染策略
Configure configures = Configure.createDefault();
configures.customPolicy("urines",5));
XWPFTemplate template = PoiUtil.getTemplate(url,configures);
template.render(dataMap);
template.writeToFile(outTempPath);
template.close();
ossPath = OSSFactory.build().upload(new FileInputStream(outTempPath),ossPathName);
//删除本地临时文件
ZipUtil.delFile(new File(outTempPath));
}else{
StringWriter out = new StringWriter();
Template template = FreemarkerUtil.getTemplate(url);
template.process(dataMap,out);
ossPath = OSSFactory.build().upload(out.toString().getBytes(StandardCharsets.UTF_8),ossPathName);
}
log.info(sn + "_" + name + fileName + "生成! doc link:" + ossPath);
dto = new TCTransportLogDTO();
dto.setUserName(userName);
dto.setBusinessType(ApiConsts.TRANSMISSION_DOWNLOAD);//下载
dto.setStatus(1);//有效
dto.setJobType(1);//子任务类型
dto.setResultType(String.valueOf(TaskResultType.Success));//下载成功
dto.setResultReturn(ossPath);//下载存储路径
dto.setResultName(sn + "_" + name + fileName);//下载后文件名称
dto.setCreateDate(new Date());
return new Result().success(200,tipStr + "下载完成",ossPath);
}catch (Exception e){
dto.setResultReturn("");//下载存储路径
dto.setResultType(String.valueOf(TaskResultType.Failure));
dto.setResultReason("下载失败");
log.info(sn + "_" + name + fileName + " 下载失败!");
return new Result().success(201,tipStr + "下载失败","");
}finally {
tcTransportLogService.insert(ConvertUtils.sourceToTarget(dto,TCTransportLogEntity.class));
}
}
@GetMapping("progressList")
@ApiOperation("下载完成记录列表")
@LogOperation("下载完成记录列表")
public Result getProgressList(@RequestParam Map<String,Object> params,HttpServletRequest request){//取当前用户下的所有子任务(businessType:==1表示上传完毕(待下载),==2表示已下载)
List<TCTransportLogDTO> list = DB.getxxxx(xxx);
if(list.isEmpty()){
return new Result().success(201,"暂无下载记录~",list);
}
return new Result().success(200,"获取下载记录成功",list);
}
@GetMapping("getInTransit")
@ApiOperation("获取打包中列表")
@LogOperation("获取打包中列表")
public Result getInTransit(@RequestParam Map<String,HttpServletRequest request){
Integer businessType = MapUtils.getInteger(params,"businessType",ApiConsts.TRANSMISSION_UPLOAD);
//获取所有批量任务
List<TCTransportLogDTO> list = tcTransportLogService.getInTransit(phsUser.getUserName(),0,businessType);
if(list.isEmpty()){
return new Result().success(201,"暂无下载中任务~",list);
}
//实际正在进行的列表
List<TCTransportLogDTO> returnList = new ArrayList<>();
//异常任务+已完成任务
List<TCTransportLogEntity> completeList = new ArrayList<>();
//下载任务异常列表
List<TCTransportLogEntity> updateList = new ArrayList<>();
for(TCTransportLogDTO dto : list){
String jobName = dto.getJobName();
//1、缓存中不存在(已完成或者任务没有正常结束两种)
String existJobName = MapUtils.getString(batchJobNameCache,"");
if(StringUtils.isEmpty(existJobName)){
if(StringUtils.isEmpty(dto.getResultType())){
dto.setResultType(String.valueOf(TaskResultType.Exception));
dto.setResultReason("任务没有正常结束");
dto.setJobType(1);//任务改为子任务
updateList.add(ConvertUtils.sourceToTarget(dto,TCTransportLogEntity.class));
}
completeList.add(ConvertUtils.sourceToTarget(dto,TCTransportLogEntity.class));
continue;
}
try{
//2、正在进行的工作
int percent = Integer.parseInt(existJobName);
dto.setPercertage(percent);
}catch (Exception e){
e.printStackTrace();
}
returnList.add(dto);
}
//处理打包异常
if(!updateList.isEmpty()){
log.info("任务没有正常结束:" + updateList.size());
//2、更新数据库状态为异常
tcTransportLogService.updateBatchById(updateList);
//检查已完成的列表,删除临时文件
handlerAbnormalTask(completeList);
}
if(returnList.isEmpty()){
return new Result().success(201,returnList);
}
return new Result().success(200,"获取下载任务成功",returnList);
}
/**
* 处理批量下载[下载失败/打包异常]任务
* @param completeList 已完成的列表
*/
private void handlerAbnormalTask(List<TCTransportLogEntity> completeList) {
String outTempPath = wordService.getTempPath();
for(TCTransportLogEntity entry : completeList){
File zipFile = new File(outTempPath + File.separator + entry.getJobName() + ".zip");
ZipUtil.delFile(zipFile);
File temFileDir = new File(outTempPath + File.separator + entry.getJobName());
ZipUtil.delFile(temFileDir);
}
log.info("delete complete or exception task...");
}
/**
* 以服务器的最后一个磁盘作为临时目录(返回字符串带文件分隔符)
* @return
*/
private String getTempPath() {
//本地磁盘的根路径
File[] paths = File.listRoots();
return paths[paths.length-1].getAbsolutePath();
}
@PostMapping("queryProcess")
@ApiOperation("查询打包进度")
@LogOperation("查询打包进度")
public Result queryProcess(@RequestBody Map<String,Object> params){
String taskList = MapUtils.getString(params,"jobNames","");
if(StringUtils.isEmpty(taskList)){
return new Result().success(201,"下载完成",null);
}
List<TCTransportLogDTO> jobNameList = JSONObject.parseArray(taskList,TCTransportLogDTO.class);
List<TCTransportLogDTO> returnList = new ArrayList<>();
//遍历列表,分开已经完成并过期的工作(
for(TCTransportLogDTO dto : jobNameList){
String jobName = dto.getJobName();
//1)、缓存中不存在的
String existJobName = MapUtils.getString(batchJobNameCache,"");
if(StringUtils.isEmpty(existJobName)){
continue;
}
//2)、刷新进度条显示
if(dto.getPercertage() == 100 ){ //打包完成,下载中
dto.setRemark("下载中...");
}else{ // 进度 < 100,刷新打包中任务进度
int percent = MapUtils.getIntValue(batchJobNameCache,0);
dto.setPercertage(percent);
dto.setRemark("打包中...");
}
returnList.add(dto);
}
if(returnList.isEmpty()){
return new Result().success(201,"下载完成","刷新打包进度条",returnList);
}
@GetMapping("monitorPackage")
@ApiOperation("监听打包")
@LogOperation("监听打包")
public Result monitorPackage(@RequestParam Map<String,HttpServletResponse response){
String jobName = MapUtils.getString(params,"jobName","");
if(StringUtils.isEmpty(jobName)){
log.info("[ monitorPackage ] jobName parameter is missing");
return new Result().error(202,"jobName parameter is missing");
}
String jobNameExist = MapUtils.getString(batchJobNameCache,"");
if(StringUtils.isEmpty(jobNameExist)){
log.info("[ monitorPackage ] [" + jobName + "] is not found");
return new Result().error(202,"[" + jobName + "] is not found");
}
//进度
int percentage = MapUtils.getIntValue(batchJobNameCache,jobName);
//打包完成后,判断oss链接
String ossPath = MapUtils.getString(batchJobNameCache,jobName + "_ossPath");
if(StringUtils.isEmpty(ossPath)){
log.info("zip being packaged");
return new Result().success(201,"zip being packaged",percentage);
}else{
//2、从缓存中剔除
batchJobNameCache.remove(jobName);
batchJobNameCache.remove(jobName + "_ossPath");
log.info(" remove from batchJobNameCache cache ");
if(String.valueOf(TaskResultType.Failure).equals(ossPath)){
//打包失败/上传失败==下载失败
log.info(percentage==100 ? "上传失败" : "打包失败");
return new Result().success(202,percentage==100 ? "上传失败" : "打包失败",ossPath);
}
//下载成功
log.info("package is complete,ready to download ");
return new Result().success(200,"package is complete,ready to download ",ossPath);
}
}
@GetMapping("queryDetail")
@ApiOperation("查询详情")
@LogOperation("查询详情")
public String queryDetail(@RequestParam("jobName") String jobName){
List<TaskResult<String>> taskDetail = pendingJobPool.getTaskDetail(jobName);
if(!taskDetail.isEmpty()){
return taskDetail.toString();
}
return null;
}
@GetMapping("clearMark")
@ApiOperation("清除下载记录")
@LogOperation("清除下载记录")
public Result clearMark(@RequestParam Map<String,HttpServletRequest request){
String clearIds = MapUtils.getString(params,"clearIds","");
if (StringUtils.isEmpty(clearIds)) {
log.info("传输完成记录主键为空");
return new Result().error(201,"丢失需要清除的记录主键信息");
}
List<Long> listIds = Arrays.asList(clearIds.split(",")).stream().map(s -> Long.parseLong(s.trim())).collect(Collectors.toList());
int row = tcTransportLogService.clearByIds(listIds);
if(row == listIds.size()){
log.info("清除传输记录成功");
return new Result().success(200,"清除成功",row);
}
log.info("清除传输记录失败");
return new Result().success(202,"清除失败",row);
}
}
涉及工具类:
package com.xxxx.common.utils;
import com.xxxx.modules.ftl.RemoteTemplateLoader;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;
import org.springframework.util.ResourceUtils;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.Map;
public class FreemarkerUtil {
public static Template getTemplate(String url) {
try {
// 通过Freemarker的Configuration读取相应的ftl,这里是对应的你使用jar包的版本号:<version>2.3.28</version>
Configuration configuration = new Configuration(Configuration.VERSION_2_3_28);
// 处理空值
configuration.setClassicCompatible(true);
configuration.setDefaultEncoding("UTF-8");
RemoteTemplateLoader remoteTemplateLoader = new RemoteTemplateLoader(url);
configuration.setTemplateLoader(remoteTemplateLoader);
Template template = configuration.getTemplate(url);
return template;
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
public void print(String name,Map<String,Object> root) {
// 通过Template可以将模版文件输出到相应的文件流
Template template = this.getTemplate(name);
try {
template.process(root,new PrintWriter(System.out)); // 在控制台输出内容
} catch (TemplateException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 输出HTML文件
*
* @param name
* @param root
* @param outFile
*/
public void fprint(String name,Object> root,String outFile) {
FileWriter out = null;
try {
// 通过一个文件输出流,就可以写到相应的文件中,此处用的是绝对路径
File file = new File(outFile);
if (!file.getParentFile().exists()) {
file.getParentFile().mkdirs();
}
out = new FileWriter(file);
Template temp = this.getTemplate(name);
temp.process(root,out);
} catch (IOException e) {
e.printStackTrace();
} catch (TemplateException e) {
e.printStackTrace();
} finally {
try {
if (out != null) out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void createWorldByMode(String modeName,String outFile,Object params) {
Configuration cfg = new Configuration(Configuration.VERSION_2_3_28);
Writer out = null;
try {
// 设置模板路径
cfg.setDirectoryForTemplateLoading(ResourceUtils.getFile("classpath:templates"));
cfg.setDefaultEncoding("UTF-8");
// 处理空值
cfg.setClassicCompatible(true);
File file = new File(outFile);
if (!file.getParentFile().exists()) {
file.getParentFile().mkdirs();
}
if (!file.exists()) {
file.createNewFile();
}
out = new OutputStreamWriter(new FileOutputStream(file),"UTF-8"); // 设置编码 UTF-8
Template template = cfg.getTemplate(modeName);
template.process(params,out);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (null != out) {
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**
* 根据模板创建word文档
*
* @param template 模板
* @param outFile 生成的word文档字符串
* @param params 模板填充需要的Map数据
*/
public static void createWordByTemplate(Template template,Object params) {
Writer out = null;
FileOutputStream fos = null;
try {
// 2、输出word
File wordFile = new File(outFile);
if (!wordFile.getParentFile().exists()) {
wordFile.getParentFile().mkdirs();
}
if (!wordFile.exists()) {
wordFile.createNewFile();
}
fos = new FileOutputStream(wordFile);
out = new OutputStreamWriter(fos,StandardCharsets.UTF_8);
template.process(params,out);
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (null != out) {
out.close();
}
if(fos!=null){
fos.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
package com.quiknos.modules.ftl;
import freemarker.cache.URLTemplateLoader;
import java.net.MalformedURLException;
import java.net.URL;
public class RemoteTemplateLoader extends URLTemplateLoader {
private String urlPath;
public RemoteTemplateLoader(String urlPath) {
this.urlPath = urlPath;
}
@Override
protected URL getURL(String path) {
URL url = null;
try {
url = new URL(urlPath);
} catch (MalformedURLException e) {
e.printStackTrace();
}
return url;
}
}
poi:
package com.xxxx.common.utils;
import com.deepoove.poi.XWPFTemplate;
import com.deepoove.poi.config.Configure;
import io.renren.common.exception.RenException;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Map;
/**
* Created by hzm on 2019/6/26
*
*/
@Slf4j
public final class PoiUtil {
/**
* 根据url取poi模板
* @param urlPath 模板url
* @return
*/
public static XWPFTemplate getTemplate(String urlPath,Configure configure){
if(StringUtils.isEmpty(urlPath)){
throw new RenException(" url is empty ");
}
XWPFTemplate template = null;
InputStream inputStream = null;
try {
inputStream = getInputStream(urlPath);
if(null == configure){
template = XWPFTemplate.compile(inputStream);
}else{
template = XWPFTemplate.compile(inputStream,configure);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if(inputStream != null){
inputStream.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return template;
}
/**
* 根据url从服务器获取一个输入流
* @param urlPath
* @return
*/
private static InputStream getInputStream(String urlPath) {
HttpURLConnection httpURLConnection = null;
InputStream inputStream = null;
try {
URL url = new URL(urlPath);
httpURLConnection = (HttpURLConnection) url.openConnection();
httpURLConnection.setConnectTimeout(3000);//设置连接超时
httpURLConnection.setDoInput(true);//设置应用程序要从网络连接读取数据
httpURLConnection.setRequestMethod("GET");
int responseCode = httpURLConnection.getResponseCode();
if(responseCode == 200){
//接收服务器返回的流
inputStream = httpURLConnection.getInputStream();
}
} catch (IOException e) {
e.printStackTrace();
}
return inputStream;
}
/**
* 根据doc模板和数据输出到文件流生成新文档
* @param template doc模板
* @param outFile
* @param dataMap 数据源
*/
public static void writeByTemplate(XWPFTemplate template,Object> dataMap){
//输出流
File wordFile = new File(outFile);
if (!wordFile.getParentFile().exists()) {
wordFile.getParentFile().mkdirs();
}
FileOutputStream fos = null;
try {
if (!wordFile.exists()) {
wordFile.createNewFile();
}
fos = new FileOutputStream(wordFile);
//输出到文件流
template.render(dataMap).write(fos);
fos.flush();
} catch (Exception e) {
} finally {
try {
if(fos!=null){
fos.close();
}
if(null != template){
template.close();
}
} catch (IOException e) {
log.info("报告生成异常:" + e.getStackTrace());
}
}
}
/**
* 根据doc模板和数据输出到文件
* @param template doc模板
* @param outFile 输出文件
* @param dataMap 数据源
*/
public static void writeToFileByTemplate(XWPFTemplate template,Object> dataMap){
try {
//输出到文件
template.render(dataMap).writeToFile(outFile);
template.close();
} catch (Exception e) {
log.info("报告生成异常:" + e.getStackTrace());
}
}
}
压缩工具类:
package com.xxxx.common.utils;
import java.io.*;
import java.util.zip.CRC32;
import java.util.zip.CheckedOutputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
public final class ZipUtil {
/**
* 功能描述: 压缩成Zip格式
*
* @author: hongzm
* @param: srcFilePath
* 要压缩的源文件路径
* @param: destFilePath
* 压缩后文件存放路径
* @param: KeepFileStructure
* 是否保留原来的目录结构,true:保留目录结构;
* false:所有文件跑到压缩包根目录下(注意:不保留目录结构可能会出现同名文件,会压缩失败)
*/
public static void toZip(String srcFilePath,String destFilePath,boolean KeepFileStructure) {
// 判断要压缩的源文件是否存在
File sourceFile = new File(srcFilePath);
if(!sourceFile.exists()) {
throw new RuntimeException(sourceFile + "不存在...");
}
long start = System.currentTimeMillis();
// 如果压缩文件已经存在,增加序号
String zipName = destFilePath + sourceFile.getName();
// 创建存放压缩文件的文件对象
File zipFile = new File(zipName + ".zip");
ZipOutputStream zos = null;
try {
// 生成目标文件对象的输出流
FileOutputStream fos = new FileOutputStream(zipFile);
CheckedOutputStream cos = new CheckedOutputStream(fos,new CRC32());
// 生成ZipOutputStream,用于写入要压缩的文件
zos = new ZipOutputStream(cos);
compressbyType(sourceFile,KeepFileStructure);
long end = System.currentTimeMillis();
System.out.println("压缩完成,耗时====" + (end - start) + " ms");
} catch(Exception e) {
throw new RuntimeException("zip error from ZipUtils",e);
} finally {
if(zos!=null){
try {
zos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public static void compressbyType(File sourceFile,ZipOutputStream zos,String zipName,boolean KeepDirStructure) throws Exception {
if(!sourceFile.exists())
return;
System.out.println("压缩" + sourceFile.getName());
if(sourceFile.isFile()) {
// if(!"myDir3.txt".equals(sourceFile.getName())) {
// 文件
compressFile(sourceFile,zipName,KeepDirStructure);
// }
} else {
// 文件夹
compressDir(sourceFile,KeepDirStructure);
}
}
public static void compressFile(File file,boolean keepDirStructure)
throws IOException {
// 1、向zip输出流中添加一个zip实体(压缩文件的目录),构造器中name为zip实体的文件的名字
ZipEntry entry = new ZipEntry(zipName);
zos.putNextEntry(entry);
FileInputStream fis = null;
BufferedInputStream bis = null;
// 2、 copy文件到zip输出流中
int len;
byte[] buf = new byte[1024];
try{
// 要压缩的文件对象写入文件流中
fis = new FileInputStream(file);
bis = new BufferedInputStream(fis);
while((len = bis.read(buf)) != -1) {
zos.write(buf,0,len);
zos.flush();
}
}catch (Exception e){
}finally {
// Complete the entry
if(fis != null){
fis.close();
}
// zos.closeEntry();
if(bis != null){
bis.close();
}
}
}
public static void compressDir(File dir,boolean KeepDirStructure)
throws IOException,Exception {
if(!dir.exists())
return;
File[] files = dir.listFiles();
if(files.length == 0) { // 空文件夹
// 需要保留原来的文件结构时,需要对空文件夹进行处理
if(KeepDirStructure) {
// 空文件夹的处理
zos.putNextEntry(new ZipEntry(zipName + File.separator));
// 没有文件,不需要文件的copy
zos.closeEntry();
}
} else {
for(File file : files) {
// 判断是否需要保留原来的文件结构
if(KeepDirStructure) {
// 注意:file.getName()前面需要带上父文件夹的名字加一斜杠,// 不然最后压缩包中就不能保留原来的文件结构,即:所有文件都跑到压缩包根目录下了
compressbyType(file,zipName + File.separator + file.getName(),KeepDirStructure);
} else {
compressbyType(file,file.getName(),KeepDirStructure);
}
}
}
}
/**
* 功能描述: outputStream转inputStream
*
* @author: hongzm
* @param: out 输出流
* @return: byte[]
*/
public static ByteArrayInputStream outPareIn(OutputStream out){
ByteArrayOutputStream baos = new ByteArrayOutputStream();
baos = (ByteArrayOutputStream) out;
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
return bais;
}
/**
* 功能描述: inputStream转byte[]
*
* @author: hongzm
* @param: in 输入流
* @return: byte[]
*/
public static byte[] outPareIn(InputStream in) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buff = new byte[1024];
int n = 0;
while ((n = in.read(buff)) != -1){
baos.write(buff,n);
}
byte[] buff2 = baos.toByteArray();
return buff2;
}
/**
* 删除文件或者目录
* @param file
* @return
*/
public static boolean delFile(File file){
if(!file.exists()){
return false;
}
if(file.isDirectory()){
File[] files = file.listFiles();
for(File f : files){
delFile(f);
}
}
return file.delete();
}
}
?
下面是自己开发中的笔记
下载记录表设计:

补充自己的数据推演:








?
开发中遇到的坑:
1)批量下载网上给出的大都是随便整几个几kb文件(压缩还不快吗),压缩成文件流,响应到浏览器,即可下载,要我说没卵用,实际项目会是这么几kb的文件吗,如若是几百份文件,每份生成的word好几兆呢,像我项目中每份word生成后是4-5兆,而且批量最大300-400份,要考虑客户端一个请求的超时问题,最终我选择采用了异步打包的方案;
2)有人可能会想在客户端点击下载时候,先拿到保存路径,后台将生成word放到这个路径下——告诉你:行不通,首先服务端没有这个权限,换句话说就是服务端怎么知道客户端要下载的,所以即使你拿到路径传到后台,服务器只会解析成服务器的本地路径,当然,本地项目在跑的时候,是可以实现功能的,因为项目就在你本机上;
3)本地下载报告中文不会乱码,但是服务器就不好说,所以还是要在生成word时候设置字符编码,这是开发时候遇到的问题之一;
4)还有一个要注意的,如果项目中是将模板放在resources下面,又是打成jar包,部到服务器上,ResourceUtils.getFile("classpath:templates")是取不到模板的,换句话说,项目打成jar包,而你若想把临时文件夹放到这个路径下,是行不通的;
5)异步处理任务中有几点需要注意:
①CompletionService可以了解下,一句话,先完成的先处理,并不会按先进先出的套路;(并发编程知识)
②压缩完的时候,要先关闭相关文件流,再上传,不然会就算上传了,下载下来也是不能用的zip包
文件流关闭顺序一般是:
一般情况是:先打开后关闭,后打开先关闭(可以想象成打开家门顺序);
另一种情况是:看依赖关系,如果a流依赖b流,应该是先关闭a流,再关闭b流(可以想象成删主从表顺序,先删从表,再删主表);
(编辑:李大同)
【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!
|