Java利用Freemark生成word文档
记录项目中通过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, jobName, list, 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", new SingleTablePolicy(1, 5)); configures.customPolicy("examines", new SingleTablePolicy(1, 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, docTempPath, dataMap); } log.info("word :" + docTempPath); return docTempPath; } } @GetMapping("archives") @ApiOperation("单份报告下载") @LogOperation("单份报告下载") @ResponseBody public Result archives(@RequestParam Map<String, Object> params, 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, sn, 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", new SingleTablePolicy(1, 5)); configures.customPolicy("bloods", new SingleTablePolicy(1, 5)); configures.customPolicy("examines", new SingleTablePolicy(1, 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, Object> params, 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, jobName, ""); 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, jobName, ""); if(StringUtils.isEmpty(existJobName)){ continue; } //2)、刷新进度条显示 if(dto.getPercertage() == 100 ){ //打包完成,下载中 dto.setRemark("下载中..."); }else{ // 进度 < 100,刷新打包中任务进度 int percent = MapUtils.getIntValue(batchJobNameCache, jobName, 0); dto.setPercertage(percent); dto.setRemark("打包中..."); } returnList.add(dto); } if(returnList.isEmpty()){ return new Result().success(201, "下载完成", returnList); } return new Result().success(200, "刷新打包进度条", returnList); } @GetMapping("monitorPackage") @ApiOperation("监听打包") @LogOperation("监听打包") public Result monitorPackage(@RequestParam Map<String, Object> params, 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, jobName, ""); 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, Object> params, 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, Map<String, 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, String outFile, 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, String outFile, Map<String, 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, String outFile, Map<String, 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, zos, sourceFile.getName(), 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, zos, zipName, KeepDirStructure); // } } else { // 文件夹 compressDir(sourceFile, zos, zipName, KeepDirStructure); } } public static void compressFile(File file, ZipOutputStream zos, String zipName, 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, ZipOutputStream zos, String zipName, 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, zos, zipName + File.separator + file.getName(), KeepDirStructure); } else { compressbyType(file, zos, 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, 0, 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流(可以想象成删主从表顺序,先删从表,再删主表);
************************************************
下面是一个批量并发执行基础框架,可以执行任何批量并发的任务,这个是大佬传授的,可以放心运用到生产环境中
涉及的类
上代码:
package com.modules.framework.vo; /** * * Created by hzm on 2019/6/13 * * @Description: 要求框架的使用者实现的任务接口 */ public interface ITaskProcesser<T, R> { TaskResult<R> taskExecute(T data); }
package com.modules.framework.vo; import java.util.concurrent.Delayed; import java.util.concurrent.TimeUnit; /** * * Created by hzm on 2019/6/13 * * @Description: 存放的延时队列的元素 */ public class ItemVo<T> implements Delayed { //到期时间,单位毫秒 private long activeTime; //业务数据,泛型 private T data; //传入过期时长,单位秒(内部转换为毫秒) public ItemVo(long expirationTime, T data) { this.activeTime = expirationTime*1000 + System.currentTimeMillis(); this.data = data; } public long getActiveTime() { return activeTime; } public T getData() { return data; } /** * 返回到激活日期的剩余时间,时间单位由单位参数指定 * */ @Override public long getDelay(TimeUnit unit) { long d = unit.convert(this.activeTime - System.currentTimeMillis(), unit); return d; } /** * Delayed接口继承了Comparable接口,按剩余时间排序 * */ @Override public int compareTo(Delayed o) { long d = getDelay(TimeUnit.MILLISECONDS) - o.getDelay(TimeUnit.MILLISECONDS); if(d == 0){ return 0; }else{ if(d < 0 ){ return -1; }else{ return 1; } } } }
package com.modules.framework.vo; import com.modules.framework.CheckJobProcesser; import java.util.LinkedList; import java.util.List; import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.atomic.AtomicInteger; /** * * Created by hzm on 2019/6/13 * * @Description: 提交给框架执行的工作实体类(本批次需要处理的同一性质的任务的集合) */ public class JobInfo<R> { //工作的名称(唯一标识) private final String jobName; //工作中任务的个数 private final int taskLength; //工作中任务的处理器 private final ITaskProcesser<?, ?> taskProcesser; //成功处理的任务数 private final AtomicInteger successCount; //已处理的任务数 private final AtomicInteger taskProcesserCount; //存放每个任务的处理结果,工查询用(拿结果从头拿,放结果从尾部放) private final LinkedBlockingDeque<TaskResult<R>> taskDetailQueue; //工作完成后,保留工作结果信息供查询的时间 private final long expireTime; //检查过期工作的处理器 // @Autowired private static CheckJobProcesser checkJobProcesser = CheckJobProcesser.getInstance(); public JobInfo(String jobName, int taskLength, ITaskProcesser<?, ?> taskProcesser, long expireTime) { this.jobName = jobName; this.taskLength = taskLength; this.taskProcesser = taskProcesser; successCount = new AtomicInteger(0); taskProcesserCount = new AtomicInteger(0); taskDetailQueue = new LinkedBlockingDeque<TaskResult<R>>(taskLength); this.expireTime = expireTime; } public AtomicInteger getSuccessCount() { return successCount; } public AtomicInteger getTaskProcesserCount() { return taskProcesserCount; } public int getTaskLength() { return taskLength; } public ITaskProcesser<?, ?> getTaskProcesser() { return taskProcesser; } //提供工作中失败的次数 public int getFailCount(){ return taskProcesserCount.get() - successCount.get(); } //提供工作的整体进度信息 public String getTotalProcess(){ return "Success [" + successCount.get() + "]/Current[" + taskProcesserCount.get() + "] Total [" + taskLength + "]"; } //取任务处理结果:提供工作中每个任务的处理结果 public List<TaskResult<R>> getTaskDetail(){ List<TaskResult<R>> taskDetailList = new LinkedList<>(); TaskResult<R> taskResult; //,每次从结果队列拿结果,直到拿不到 while ((taskResult = taskDetailQueue.pollFirst()) != null){ taskDetailList.add(taskResult); } return taskDetailList; } //放任务处理结果:每个任务处理完后,记录任务处理结果(保持最终一致性即可) public void addTaskResult(TaskResult<R> result){ if(TaskResultType.Success.equals(result.getResultType())){ successCount.getAndIncrement(); } taskDetailQueue.addLast(result); taskProcesserCount.getAndIncrement(); if(taskProcesserCount.get() == taskLength){ //推进过期检查处理器 checkJobProcesser.putJob(jobName, expireTime); } } }
package com.modules.framework.vo; /** * * Created by hzm on 2019/6/13 * * @Description: 任务返回的结果实体类 */ public class TaskResult<R> { private final TaskResultType resultType;//方法是否成功完成 private final R returnValue;//方法处理后的结果数据 private final String reason;//如果方法失败,这里可以填充原因 public TaskResult(TaskResultType resultType, R returnValue, String reason) { super(); this.resultType = resultType; this.returnValue = returnValue; this.reason = reason; } public TaskResult(TaskResultType resultType, R returnValue) { super(); this.resultType = resultType; this.returnValue = returnValue; this.reason = "Success"; } public TaskResultType getResultType() { return resultType; } public R getReturnValue() { return returnValue; } public String getReason() { return reason; } @Override public String toString() { return "TaskResult{" + "resultType=" + resultType + ", returnValue=" + returnValue + ", reason='" + reason + '\'' + '}'; } }
package com.modules.framework.vo; /** * * @Description: 方法本身运行是否正确的结果类型 */ public enum TaskResultType { /* 方法成功执行并返回了业务人员需要的结果 */ Success, /* 方法成功执行但是返回的是业务人员不需要的结果 */ Failure, /* 方法执行抛出了Exception */ Exception }
package com.modules.framework; import com.modules.framework.vo.ItemVo; import com.modules.framework.vo.JobInfo; import lombok.extern.slf4j.Slf4j; import java.util.Map; import java.util.concurrent.DelayQueue; /** * * Created by hzm on 2019/6/13 * * @Description: 任务完成后,在一定时间内共查询,之后会释放节约内存(从缓存中清除) */ //@Component @Slf4j public class CheckJobProcesser { //存放任务的队列 private static DelayQueue<ItemVo<String>> queue = new DelayQueue<ItemVo<String>>(); /*单例化*/ private static class ProcesserHolder{ public static CheckJobProcesser processer = new CheckJobProcesser(); } public static CheckJobProcesser getInstance() { return ProcesserHolder.processer; } /*单例化*/ //处理队列中到期的任务 private static class FetchJob implements Runnable{ private static DelayQueue<ItemVo<String>> queue = CheckJobProcesser.queue; private static Map<String, JobInfo<?>> jobInfoMap = PendingJobPool.getMap(); @Override public void run() { try { ItemVo<String> itemVo = queue.take(); String jobName = (String) itemVo.getData(); jobInfoMap.remove(jobName); //移除应用缓存中的工作 // batchJobNameCache.remove(jobName); log.info("Job:["+ jobName+"] is out of date,remove from JobList! "); } catch (InterruptedException e) { e.printStackTrace(); } } } //任务完成后,放入队列,到期后,从缓存中清除 public void putJob(String jobName, long expireTime){ Thread thread = new Thread(new FetchJob()); // thread.setName("outOfDate"+jobName); thread.setDaemon(true); thread.start(); log.info("开启[ " + jobName + " ]工作过期检查守护线程..........."); //包装工作,放入延时队列 ItemVo<String> itemVo = new ItemVo<String>(expireTime, jobName); queue.offer(itemVo); log.info("任务[" + jobName + "]已被放入过期检查缓存,过期时长:" + expireTime + "s"); } }
package com.modules.framework; import com.baomidou.mybatisplus.extension.api.R; import com.modules.framework.vo.ITaskProcesser; import com.modules.framework.vo.JobInfo; import com.modules.framework.vo.TaskResult; import com.modules.framework.vo.TaskResultType; import org.springframework.stereotype.Service; import java.util.List; import java.util.Map; import java.util.concurrent.*; /** * <p> * Created by hzm on 2019/6/13 * * @Description: 框架的主体类,也是调用者主要使用的类 */ @Service public class PendingJobPool { //运行的线程数,机器的CPU数相同 private static final int THREAD_COUNTS = Runtime.getRuntime().availableProcessors(); //线程池队列,用以存放待处理的任务 private static BlockingQueue<Runnable> taskQueue = new ArrayBlockingQueue<Runnable>(5000); //线程池,固定大小,有界队列 private static ExecutorService taskExecutor = new ThreadPoolExecutor(THREAD_COUNTS, THREAD_COUNTS, 60, TimeUnit.SECONDS, taskQueue); //提交给线程池的工作信息的存放容器 private static ConcurrentHashMap<String, JobInfo<?>> jobInfoMap = new ConcurrentHashMap<>(); public static Map<String, JobInfo<?>> getMap(){ return jobInfoMap; } //对工作中任务进行包装,提交给线程池使用,并处理任务结果,写入缓存供查询 private static class PendingTask<T, R> implements Runnable{ private JobInfo<R> jobInfo; private T processData; public PendingTask(JobInfo<R> jobInfo, T processData) { super(); this.jobInfo = jobInfo; this.processData = processData; } public void run() { R r = null; //取得任务的处理器 ITaskProcesser<T, R> taskProcesser = (ITaskProcesser<T, R>) jobInfo.getTaskProcesser(); TaskResult<R> result = null; try { //执行任务,获得处理结果 result = taskProcesser.taskExecute(processData); //检查处理器的返回结果,避免调用者处理不当 if (result==null) { result = new TaskResult<R>(TaskResultType.Exception, r, "result is NULL"); } if(result.getResultType()==null) { if(result.getReason()==null) { result = new TaskResult<R>(TaskResultType.Exception, r, "result is NULL"); }else { result = new TaskResult<R>(TaskResultType.Exception, r, "result is NULL,reason:"+result.getReason()); } } }catch(Exception e) { e.printStackTrace(); result = new TaskResult<R>(TaskResultType.Exception, r, e.getMessage()); } finally { //将任务的处理结果写入缓存 jobInfo.addTaskResult(result); } } } //提交工作中的任务 public <T, R> void putTask(String jobName, T t){ JobInfo<R> jobInfo = getJob(jobName); PendingTask<T, R> task = new PendingTask<>(jobInfo, t); taskExecutor.execute(task); } //根据工作名检索工作 private <R> JobInfo<R> getJob(String jobName){ JobInfo<R> jobInfo = (JobInfo<R>) jobInfoMap.get(jobName); if(null == jobInfo){ throw new RuntimeException(jobName + "是非法任务! "); } return jobInfo; } //调用者注册工作(工作标识,任务处理器等) public <R> void registerJob(String jobName, int taskLength, ITaskProcesser<?, ?> taskProcesser, long expireTime){ JobInfo<R> jobInfo = new JobInfo<>(jobName, taskLength, taskProcesser, expireTime); if(jobInfoMap.putIfAbsent(jobName, jobInfo) != null){ throw new RuntimeException(jobName + "已经注册! "); } } //获得每个任务的处理详情 public <R> List<TaskResult<R>> getTaskDetail(String jobName){ JobInfo<R> jobInfo = getJob(jobName); return jobInfo.getTaskDetail(); } //获得工作的整体处理进度 public <R> String getTaskProgess(String jobName) { JobInfo<R> jobInfo = getJob(jobName); return jobInfo.getTotalProcess(); } //获取工作中子任务个数 public int getTaskLength(String jobName){ JobInfo<R> jobInfo = getJob(jobName); return jobInfo.getTaskLength(); } //获取工作中子任务已处理的个数 public int gettaskProcesserCount(String jobName){ JobInfo<R> jobInfo = getJob(jobName); return jobInfo.getTaskProcesserCount().get(); } }
任务类
/** * Copyright 厦门感易通科技有限公司 版权所有 违者必究 2019 */ package com.modules.api.vo; import com.modules.constant.ApiConsts; import com.modules.framework.vo.ITaskProcesser; import com.modules.framework.vo.TaskResult; import com.modules.framework.vo.TaskResultType; import com.modules.heath.dto.TCTransportLogDTO; import com.modules.heath.entity.TCTransportLogEntity; import com.modules.heath.service.TCTransportLogService; import com.modules.jt.service.WordService; import com.modules.oss.cloud.OSSFactory; import freemarker.template.Template; import io.renren.common.utils.ConvertUtils; import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections.MapUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; import java.io.StringWriter; import java.nio.charset.StandardCharsets; import java.util.Date; import java.util.HashMap; import java.util.Map; /** * 批量生成文档,并返回文档url任务(支持查询进度) *@author : hongzm *@date: 2019/7/3/0003 */ @Component @Slf4j public class MyDocMakeTask implements ITaskProcesser<Map<String, Object>, String> { @Autowired private WordService wordService; @Autowired private TCTransportLogService tcTransportLogService; @Transactional @Override public TaskResult<String> taskExecute(Map<String, Object> data) { //场次 String plansCode = MapUtils.getString(data, "plansCode"); String sn = MapUtils.getString(data, "sn"); String downType = MapUtils.getString(data, "downType"); //模板template Template template = (Template) MapUtils.getObject(data, "template"); //导出文档名称("健康档案"或"体检报告") String fileName = MapUtils.getString(data, "fileName"); String userName = MapUtils.getString(data, "userName"); // 取模板填充数据 Map<String, Object> dataMap = new HashMap<>(); //下载记录 TCTransportLogDTO dto = null; Map<String, Object> mRes = wordService.getDataMap(plansCode, sn, downType); if(mRes != null){ dataMap.putAll(mRes); } String name = MapUtils.getString(dataMap, "name", ""); //上传oss的文件名称(可包含路径,用"/"拼接) String docName = sn + "_" + name + fileName; String tempPath = plansCode + "/" + docName; StringWriter out = new StringWriter(); try{ // 生成报告并上传oss template.process(dataMap, out); //上传时候做字符处理,不然下载下来部分乱码 String ossPath = OSSFactory.build().upload(out.toString().getBytes(StandardCharsets.UTF_8), tempPath); log.info(sn + "_" + name + fileName + "已上传," + downType + " link: " + tempPath); dto = new TCTransportLogDTO(); dto.setUserName(userName); dto.setBusinessType(ApiConsts.TRANSMISSION_UPLOAD);//上传 dto.setStatus(1);//有效 dto.setJobType(1);//子任务类型 //dto.setJobName(plansCode+"_"+(ApiConsts.RESIDENT_HEALTHY.equals(downType)?"档案":"报告")+"_"+userName); dto.setResultType(String.valueOf(TaskResultType.Success));//下载成功 dto.setResultReturn(ossPath);//下载存储路径 dto.setResultName(docName);//下载后文件名称 dto.setCreateDate(new Date()); //生成离线文档,并返回离线文档url return new TaskResult<String>(TaskResultType.Success, ossPath); }catch (Exception e){ dto.setResultReturn("");//下载存储路径 dto.setResultType(String.valueOf(TaskResultType.Failure)); dto.setResultReason("上传失败"); //处理失败 return new TaskResult<String>(TaskResultType.Failure, sn + "_" + name + "生成失败! ","Failure"); }finally { boolean insert = tcTransportLogService.insert(ConvertUtils.sourceToTarget(dto, TCTransportLogEntity.class)); } } }
使用方式:
@GetMapping("batchDownloadPlanscode") @ApiOperation("批量上传") @LogOperation("批量上传") @ResponseBody public Result batchDownloadPlanscode(@RequestParam Map<String, Object> params,HttpServletRequest request) throws Exception {//1、取得模板实例 String url = wordService.getTemplateUrl(planCode, downType); if(StringUtils.isEmpty(url)){ log.info("模板url为空"); return new Result().error(202, "请先指定模板!"); } Template template = FreemarkerUtil.getTemplate(url); String outPath = "xxx";// 2、根据场次查找受检者信息 List<TCPatientsDTO> list = xxxService.getByPlansCode(planCode); if(list.isEmpty()){ log.info("没有xxx数据"); return new Result().error(201, "暂无xxx数据!"); } //批量工作标识唯一 String jobName = "xxx"; try{ //使用框架第一步:注册工作 pendingJobPool.registerJob(jobName, list.size(), myDocMakeTask, 5); }catch (Exception e){ log.info("已经注册,请勿重复提交!"); return new Result().success(201, tipStr + businessTypeStr + "中,请休息一下~", ""); }
//使用框架第二步:将任务依次放进去 for (int i = 0; i < list.size(); i++) { //构造任务需要的参数 Map<String, Object> paramMap = new HashMap<>(); paramMap.put("plansCode", planCode);//场次 paramMap.put("sn", list.get(i).getSn());//序号 paramMap.put("downType", downType);//业务类型 paramMap.put("template", template);//模板--通过url生成模板 paramMap.put("outPath", outPath);// paramMap.put("fileName", fileName);//文档下载后的名称 paramMap.put("userName", phsUser.getUserName());//当前用户 //循环将任务放进去执行 pendingJobPool.putTask(jobName, paramMap); } //记录下载痕迹 TCTransportLogDTO dto = new TCTransportLogDTO(); dto.setUserName(phsUser.getUserName()); dto.setBusinessType(ApiConsts.TRANSMISSION_UPLOAD);//上传 dto.setStatus(1);//有效 dto.setJobName(jobName); dto.setJobType(0);//批次 dto.setCreateDate(new Date()); boolean insert = tcTransportLogService.insert(ConvertUtils.sourceToTarget(dto, TCTransportLogEntity.class)); batchJobNameCache.put(jobName, jobName);return new Result().success(200, "获取报告数据成功,准备上传~", ""); }
如果只需生成离线文档或者是上传到服务器啥的,可以使用后面这种,支持并发,安全,还支持进度查询以及执行的结果查询;