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, "獲取報告數據成功,准備上傳~", ""); }

如果只需生成離線文檔或者是上傳到服務器啥的,可以使用后面這種,支持並發,安全,還支持進度查詢以及執行的結果查詢;

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM