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, "获取报告数据成功,准备上传~", ""); }

如果只需生成离线文档或者是上传到服务器啥的,可以使用后面这种,支持并发,安全,还支持进度查询以及执行的结果查询;

 

posted @ 2019-07-29 22:02  howtosay  阅读(2605)  评论(0编辑  收藏  举报