記錄項目中通過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, "獲取報告數據成功,准備上傳~", "");
}
如果只需生成離線文檔或者是上傳到服務器啥的,可以使用后面這種,支持並發,安全,還支持進度查詢以及執行的結果查詢;
