根据文件模板实现预览、生成word、pdf、excel(后端-项目)

背景

页面上有一份表单,包含基本信息和列表信息,用户填写完信息后,点击预览/导出,可以预览/导出word、pdf、excel文档。

因为代码重构过,所以看流程可能会有点绕,为方便能看懂,这里直接提供了项目的地址
文件生成项目

期望你在本文能收获什么
一套比较完整的文件生成的解决方案和思路
一些代码构建的思路和细节,文中代码的一些组成是我参考在用项目的结构,重构改过来的,用上了一些设计模式,扩展性、规范性应该也还是可以的。

效果示例

doc文件模板

根据文件模板实现预览、生成word、pdf、excel(后端-项目)_第1张图片

doc生成效果

根据文件模板实现预览、生成word、pdf、excel(后端-项目)_第2张图片

pdf效果一样

excel文件模板

根据文件模板实现预览、生成word、pdf、excel(后端-项目)_第3张图片

excel生成效果

根据文件模板实现预览、生成word、pdf、excel(后端-项目)_第4张图片

文件word、pdf、excel生成/预览的基本流程

  1. 前端页面,用户填写用户信息,可以包含基本信息和列表信息,用户提交数据
  2. 后端接收数据后依靠第三方的组件/工具,把文件生成到本地
  3. 把生成到本地的文件上传到文件服务器并返回文件id/文件访问地址
  4. 删除本地生成的文件
  5. 把文件id/文件访问地址返回给前端
  1. 有的项目组做项目喜欢上传/生成文件再上传后在数据库存文件信息,然后返回文件地址给前端,前端拿到文件地址就直接处理
  2. 有的项目组做项目喜欢上传/生成文件再上传后在数据库存文件信息,然后返回文件id给前端,前端默认是要知道文件服务器的基础访问地址,所以让前端拿到文件id后他们就会处理了(拼接字符串为文件访问地址(文件id为生成文件名)如 “http://文件地址/文件id”,或者再发个请求到数据库查文件具体信息(拿到地址)都可以)
  3. 本文使用的是第二种。单独有文件服务器做文件上传,然后把上传成功的文件的基本信息保存到数据库,然后返回文件id给前端。
  4. 本文主要是介绍如何生成各种类型的文档,所以不包含如何上传文件部分。当然市面上常用的文件上传服务实现这么多,相信你也是可以找到合适的解决方案的。
  5. 如果以后有空,再给补一个通用的文件上传服务好了,哈哈

主要用到的工具

  1. FreeMarker(生成word)、aspose(将word转换成pdf)、easyExcel(生成excel)

代码实现

基础工具类及依赖

  1. maven项目
  2. spring boot 项目
  3. 基础依赖

maven依赖


<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0modelVersion>

    <groupId>org.wwt.filegroupId>
    <artifactId>FileHandleartifactId>
    <version>1.0-SNAPSHOTversion>
    <parent>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-parentartifactId>
        <version>2.1.5.RELEASEversion>
    parent>
    <properties>
        <java.version>1.8java.version>
    properties>

    <dependencies>
        
        <dependency>
            <groupId>mysqlgroupId>
            <artifactId>mysql-connector-javaartifactId>
            <scope>runtimescope>
        dependency>
        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>
        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-testartifactId>
            <scope>testscope>
        dependency>
        
        <dependency>
            <groupId>org.projectlombokgroupId>
            <artifactId>lombokartifactId>
            <optional>trueoptional>
        dependency>
        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-freemarkerartifactId>
        dependency>
        
        <dependency>
            <groupId>cn.hutoolgroupId>
            <artifactId>hutool-allartifactId>
            <version>5.5.2version>
        dependency>
        
        <dependency>
            <groupId>aspose-wordsgroupId>
            <artifactId>aspose-wordsartifactId>
            <version>15.8.0version>
            <scope>systemscope>
            <systemPath>${project.basedir}/src/main/resources/lib/aspose-words-15.8.0-jdk16.jarsystemPath>
        dependency>

        
        <dependency>
            <groupId>com.alibabagroupId>
            <artifactId>easyexcelartifactId>
            <version>3.1.1version>
        dependency>
        
        <dependency>
            <groupId>com.alibabagroupId>
            <artifactId>easyexcel-coreartifactId>
            <version>3.1.1version>
        dependency>
    dependencies>

project>

application.yml

#mysql数据库连接
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/xxx?serverTimezone=GMT%2B8
    username: root
    password: 123456

这里看到我用了mysql,其实我这里数据是由前端传过来的,不是查出来的,所以没用到数据库。只是加个配置项目启动就不会找默认配置报错。当然你可以的通过加个注解的方式解决,这里不提了。

项目结构

根据文件模板实现预览、生成word、pdf、excel(后端-项目)_第5张图片
4. 基本类

我把包名也复制进去,希望尽量能让你还原项目

实体类

根据文件模板实现预览、生成word、pdf、excel(后端-项目)_第6张图片
User - 数据实体类

package com.wwt.file.entity;

import lombok.Data;

@Data
public class User {
    private Long id;
    private String name;
    private Integer age;
    private String email;
}

FileVO - 文件返回信息

package com.wwt.file.entity.vo;

import lombok.Data;

/**
 * 封装文件返回信息
 *
 */
@Data
public class FileVO {

    /**
     * 文件名
     */
    private String fileName;

    /**
     * 文件id
     */
    private Long fileId;

    public FileVO(String fileName, Long fileId) {
        this.fileName = fileName;
        this.fileId = fileId;
    }
}

UserExportDTO

package com.wwt.file.entity.dto;

import cn.hutool.core.map.MapUtil;
import com.wwt.file.entity.User;
import lombok.Data;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 封装前端传过来的用户请求信息
 */
@Data
public class UserExportDTO {

    private User user1;

    private User user2;

    private List<User> users;

    /**
     *
     * @return
     */
    public Map<String, Object> structDataMap() {
        Map<String, Object> map = MapUtil.createMap(HashMap.class);
        map.put("user1",this.user1);
        map.put("user2",this.user2);
        map.put("users",this.users);
        return map;
    }
}

在controller中的使用示例
根据文件模板实现预览、生成word、pdf、excel(后端-项目)_第7张图片

枚举类

根据文件模板实现预览、生成word、pdf、excel(后端-项目)_第8张图片
FileTypeEnum - 文件类型的枚举类

package com.wwt.file.enums;

/**
 * 文件类型
 */
public enum  FileTypeEnum {
    WORD("word",".doc"),
    PDF("pdf",".pdf"),
    EXCEL("excel",".xlsx"),
    XML("xml",".xml")
    ;

    /**
     * 类型
     */
    private String type;

    /**
     * 后缀
     */
    private String suffix;

    FileTypeEnum(String type, String suffix) {
        this.type = type;
        this.suffix = suffix;
    }

    public String getType() {
        return type;
    }

    public String getSuffix() {
        return suffix;
    }
}

UserTemplate - 用户模板

package com.wwt.file.enums;

/**
 * 用户模板
 */
public enum UserTemplate {
    USER_TEMPLATE_XML("user_template","用户信息","这是一个用户模板",FileTypeEnum.XML),
    USER_TEMPLATE_EXCEL("user_template","用户信息","这是一个用户模板",FileTypeEnum.EXCEL);

    /**
     * 模板名称
     */
    private String template;

    /**
     * 模板中文名
     */
    private String chName;

    /**
     * 详情
     */
    private String des;

    /**
     * 模板类型
     */
    private String type;

    /**
     * 模板后缀
     */
    private String suffix;


    UserTemplate(String template, String chName, String des, FileTypeEnum fileTypeEnum) {
        this.chName = chName;
        this.des = des;
        this.template = template + fileTypeEnum.getSuffix();
        this.type = fileTypeEnum.getType();
        this.suffix = fileTypeEnum.getSuffix();
    }

    public String getTemplate() {
        return template;
    }

    public String getChName() {
        return chName;
    }

    public String getDes() {
        return des;
    }

    public String getType() {
        return type;
    }

    public String getSuffix() {
        return suffix;
    }
}

这两个枚举类的关联关系

FileTypeEnum :该枚举是单一职责的,只表示文件的类型信息
UserTemplate :通过构造方法引入 FileTypeEnum ,在构造方法中接入 fileTypeEnum 实现
UserTemplate 基本信息的初始化,创建 UserTemplate 时更加清晰明了。

不太好的实现是这样的:

USER_TEMPLATE_XML("user_template","用户信息","这是一个用户模板",FileTypeEnum.XML.getSuffix(),FileTypeEnum.XML.getType()),
USER_TEMPLATE_EXCEL("user_template","用户信息","这是一个用户模板",FileTypeEnum.EXCEL.getSuffix(),FileTypeEnum.EXCEL.getType());

UserTemplate(String template, String chName, String des, String suffix,String type) {
        this.chName = chName;
        this.des = des;
        this.template = template + suffix;
        this.type = type;
        this.suffix = suffix;
    }
//又或者不创建 FileTypeEnum ,直接在这些显示文件类型信息的地方都用字符串写死

统一响应结果

根据文件模板实现预览、生成word、pdf、excel(后端-项目)_第9张图片
Result - 一个抽象的返回结果类

package com.wwt.file.result;

import com.wwt.file.result.enums.ResultCodeEnum;
import lombok.Data;

@Data
public abstract class Result<T> {

    public static Result success(){
        Result r = new Result(){};
        r.setSuccess(ResultCodeEnum.SUCCESS.getSuccess());
        r.setCode(ResultCodeEnum.SUCCESS.getCode());
        r.setMessage(ResultCodeEnum.SUCCESS.getMessage());
        return r;
    }

    public static Result error(){
        Result r = new Result(){};
        r.setSuccess(ResultCodeEnum.UNKNOWN_REASON.getSuccess());
        r.setCode(ResultCodeEnum.UNKNOWN_REASON.getCode());
        r.setMessage(ResultCodeEnum.UNKNOWN_REASON.getMessage());
        return r;
    }

    /**
     * 是否成功
     */
    private Boolean success;

    /**
     * 响应码
     */
    private Integer code;

    /**
     * 响应信息
     */
    private String message;

    /**
     * 响应数据
     */
    private T data;

    public Result data(T data){
        this.setData(data);
        return this;
    }

    public Result(){

    }
}

DefaultResult - Result的实现,默认的响应结果类,平时用这个类做响应结果就可以了

package com.wwt.file.result;

import com.wwt.file.result.enums.ResultCodeEnum;

/**
 * 默认响应结果
 * @param 
 */
public class DefaultResult<T> extends Result<T> {

    public static DefaultResult setResult(ResultCodeEnum resultCodeEnum){
        DefaultResult r = new DefaultResult();
        r.setSuccess(resultCodeEnum.getSuccess());
        r.setCode(resultCodeEnum.getCode());
        r.setMessage(resultCodeEnum.getMessage());
        return r;
    }
}

ResultCodeEnum - 常用响应结果,有什么想用的响应结果,在这里增加枚举结果对象即可

package com.wwt.file.result.enums;

import lombok.Getter;
import lombok.ToString;

//这个类是在尚硅谷的相关资料里面抄过来的
@Getter
@ToString
public enum ResultCodeEnum {

    SUCCESS(true, 20000,"成功"),
    UNKNOWN_REASON(false, 20001, "未知错误"),

    BAD_SQL_GRAMMAR(false, 21001, "sql语法错误"),
    JSON_PARSE_ERROR(false, 21002, "json解析异常"),
    PARAM_ERROR(false, 21003, "参数不正确"),

    FILE_UPLOAD_ERROR(false, 21004, "文件上传错误"),
    FILE_DELETE_ERROR(false, 21005, "文件刪除错误"),
    EXCEL_DATA_IMPORT_ERROR(false, 21006, "Excel数据导入错误"),

    VIDEO_UPLOAD_ALIYUN_ERROR(false, 22001, "视频上传至阿里云失败"),
    VIDEO_UPLOAD_TOMCAT_ERROR(false, 22002, "视频上传至业务服务器失败"),
    VIDEO_DELETE_ALIYUN_ERROR(false, 22003, "阿里云视频文件删除失败"),
    FETCH_VIDEO_UPLOADAUTH_ERROR(false, 22004, "获取上传地址和凭证失败"),
    REFRESH_VIDEO_UPLOADAUTH_ERROR(false, 22005, "刷新上传地址和凭证失败"),
    FETCH_PLAYAUTH_ERROR(false, 22006, "获取播放凭证失败"),

    URL_ENCODE_ERROR(false, 23001, "URL编码失败"),
    ILLEGAL_CALLBACK_REQUEST_ERROR(false, 23002, "非法回调请求"),
    FETCH_ACCESSTOKEN_FAILD(false, 23003, "获取accessToken失败"),
    FETCH_USERINFO_ERROR(false, 23004, "获取用户信息失败"),
    LOGIN_ERROR(false, 23005, "登录失败"),

    COMMENT_EMPTY(false, 24006, "评论内容必须填写"),

    PAY_RUN(false, 25000, "支付中"),
    PAY_UNIFIEDORDER_ERROR(false, 25001, "统一下单错误"),
    PAY_ORDERQUERY_ERROR(false, 25002, "查询支付结果错误"),

    ORDER_EXIST_ERROR(false, 25003, "课程已购买"),

    GATEWAY_ERROR(false, 26000, "服务不能访问"),

    CODE_ERROR(false, 28000, "验证码错误"),

    LOGIN_PHONE_ERROR(false, 28009, "手机号码不正确"),
    LOGIN_MOBILE_ERROR(false, 28001, "账号不正确"),
    LOGIN_PASSWORD_ERROR(false, 28008, "密码不正确"),
    LOGIN_DISABLED_ERROR(false, 28002, "该用户已被禁用"),
    REGISTER_MOBLE_ERROR(false, 28003, "手机号已被注册"),
    LOGIN_AUTH(false, 28004, "需要登录"),
    LOGIN_ACL(false, 28005, "没有权限"),
    SMS_SEND_ERROR(false, 28006, "短信发送失败"),
    SMS_SEND_ERROR_BUSINESS_LIMIT_CONTROL(false, 28007, "短信发送过于频繁");


    private Boolean success;

    private Integer code;

    private String message;

    ResultCodeEnum(Boolean success, Integer code, String message) {
        this.success = success;
        this.code = code;
        this.message = message;
    }
}

正因为感觉把所有的响应结果都封装在一个类里面,后期类就太长了,所以我才把响应结果类 Result 做抽象化,让其实现类的 setResult() 方法来实现不同的响应结果类的入参。当然怕麻烦的话,就直接用 DefaultResult 和 ResultCodeEnum 基本就够用了。

我们这里用到的,扩展其他的响应结果枚举类,以及返回结果类的示例如下:
FileResult - 文件结果类

package com.wwt.file.result.file;

import com.wwt.file.result.Result;
import com.wwt.file.result.enums.FileResultEnum;

/**
 * 文件响应结果
 * @param 
 */
public class FileResult<T> extends Result<T> {

    public static FileResult setResult(FileResultEnum FileResultEnum){
        FileResult r = new FileResult();
        r.setSuccess(FileResultEnum.getSuccess());
        r.setCode(FileResultEnum.getCode());
        r.setMessage(FileResultEnum.getMessage());
        return r;
    }

}

FileResultEnum - 文件返回结果枚举对象 - 从 ResultCodeEnum 中分离出来了

package com.wwt.file.result.enums;

import lombok.Getter;
import lombok.ToString;

@Getter
@ToString
public enum FileResultEnum {

    //只要不是20000都是失败
    SUCCESS(true, 20000,"成功"),
    UNKNOWN_REASON(false, 20001, "未知错误"),
    UPLOAD_SUCCESS(true, 20000, "文件上传成功"),
    UPLOAD_FAIL(false, 20002, "文件上传失败");

    private Boolean success;

    private Integer code;

    private String message;

    FileResultEnum(Boolean success, Integer code, String message) {
        this.success = success;
        this.code = code;
        this.message = message;
    }
}

只要新的响应结果类和新的响应结果枚举类能用 setResult()方法使其一对一关联起来即可
新的响应结果类继承 Result 类即可

使用示例
根据文件模板实现预览、生成word、pdf、excel(后端-项目)_第10张图片

工具类

根据文件模板实现预览、生成word、pdf、excel(后端-项目)_第11张图片
FileUtil

package com.wwt.file.util;

import cn.hutool.core.io.resource.ClassPathResource;
import cn.hutool.core.lang.UUID;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.wwt.file.enums.FileTypeEnum;

/**
 * 文件处理工具类
 */
public class FileUtil {

    /**
     * 获取项目根路径
     * pathStr : D:\code\javaCode\FileHandle\target\classes
     * 如果是在ide中运行,则和target同级目录,如果是jar部署到服务器,则默认和jar包同级
     * pathStr : D:\code\javaCode\FileHandle\         FileHandle是我的项目名
     */
    public static String getResourceBasePath() {
        ClassPathResource classPathResource = new ClassPathResource("");
        String pathStr = classPathResource.getFile().getAbsolutePath();
        pathStr = pathStr.replace("target\\classes", "");
        return pathStr;
    }

    /**
     * 如果父目录不存在,则创建
     * 保证父目录一定存在
     * 否则文件可能创建失败
     * 支持创建多级目录
     * @Param filePath 文件绝对路径
     */
    public static void mkParentDirIfNotExist(String filePath) {
        cn.hutool.core.io.FileUtil.mkParentDirs(filePath);
    }

    /**
     * 删除文件或目录及其下文件
     * @param filePath 文件绝对路径
     */
    public static void delDir(String filePath){
        cn.hutool.core.io.FileUtil.del(filePath);
    }

    /**
     * 生成随机文件名
     * @param fileTypeEnum 指定文件类型
     * @return
     */
    public static String generateFileName(FileTypeEnum fileTypeEnum){
        if (ObjectUtil.isNull(fileTypeEnum)){
            return null;
        }
        //System.currentTimeMillis() 如果你不想用uuid,用时间也是可以的
        return StrUtil.concat(true, UUID.randomUUID().toString(true), fileTypeEnum.getSuffix());
    }

}

生成word文档

原理

  1. word文档生成主要用到的工具是freemarker
  2. 提供一份word文档模板,在需要插入数据的地方填写占位符,代码中将数据和文档绑定时,会用实际数据替换掉占位符

步骤

依赖


<dependency>
  <groupId>org.springframework.bootgroupId>
  <artifactId>spring-boot-starter-freemarkerartifactId>
dependency>

新建项目模板目录

根据文件模板实现预览、生成word、pdf、excel(后端-项目)_第12张图片

新建项目模板

根据文件模板实现预览、生成word、pdf、excel(后端-项目)_第13张图片
根据文件模板实现预览、生成word、pdf、excel(后端-项目)_第14张图片另存为xml文件
根据文件模板实现预览、生成word、pdf、excel(后端-项目)_第15张图片
把文件模板放进template目录
根据文件模板实现预览、生成word、pdf、excel(后端-项目)_第16张图片
用notepad++之类的工具类打开并修改模板文件
实体类数据
根据文件模板实现预览、生成word、pdf、excel(后端-项目)_第17张图片

小提示:如果文件内容不是格式化的,你复制内容到idea里的随便一个xml文档,ctrl+alt+L代码格式化之后再复制回来也是可以的。
为什么先预写数据,然后再查找替换。而不是在word文档里面直接写${xxx.xxx},然后生成xml文件呢?因为实测过可能$符号会因为一些格式问题,预写替换符再成xml的话会出奇怪的问题,比如说$符号和属性分开了,导致数据填不进去,或者报错。

list数据
根据文件模板实现预览、生成word、pdf、excel(后端-项目)_第18张图片
根据文件模板实现预览、生成word、pdf、excel(后端-项目)_第19张图片
全部替换后,转为word文档打开应该是如下面这个样子的。但是因为加了list标签的原因,可能替换完就打不开了。不过还是再看看替换完应该是长什么样的吧。
根据文件模板实现预览、生成word、pdf、excel(后端-项目)_第20张图片

前端数据

{
    "user1": {
        "id": 1, 
        "name": "张三", 
        "age": 77, 
        "email": "[email protected]"
    }, 
    "user2": {
        "id": 19, 
        "name": "李四", 
        "age": 68, 
        "email": "[email protected]"
    }, 
    "users": [
        {
            "id": 33, 
            "name": "帅哥1", 
            "age": 18, 
            "email": "[email protected]"
        }, 
        {
            "id": 34, 
            "name": "帅哥2", 
            "age": 18, 
            "email": "[email protected]"
        }, 
        {
            "id": 35, 
            "name": "帅哥3", 
            "age": 18, 
            "email": "[email protected]"
        }, 
        {
            "id": 36, 
            "name": "帅哥4", 
            "age": 18, 
            "email": "[email protected]"
        }, 
        {
            "id": 37, 
            "name": "帅哥5", 
            "age": 18, 
            "email": "[email protected]"
        }, 
        {
            "id": 38, 
            "name": "帅哥6", 
            "age": 18, 
            "email": "[email protected]"
        }, 
        {
            "id": 39, 
            "name": "帅哥7", 
            "age": 18, 
            "email": "[email protected]"
        }, 
        {
            "id": 40, 
            "name": "帅哥8", 
            "age": 18, 
            "email": "[email protected]"
        }
    ]
}

后端接收数据

根据文件模板实现预览、生成word、pdf、excel(后端-项目)_第21张图片

controller写请求接口

package com.wwt.file.controller;

import com.wwt.file.entity.User;
import com.wwt.file.entity.dto.UserExportDTO;
import com.wwt.file.entity.vo.FileVO;
import com.wwt.file.result.DefaultResult;
import com.wwt.file.result.Result;
import com.wwt.file.result.enums.FileResultEnum;
import com.wwt.file.result.file.FileResult;
import com.wwt.file.service.IUserService;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;
import java.io.IOException;

@RestController
@RequestMapping("/user")
public class UserController {

    @Resource
    private IUserService userService;


    @PostMapping("/export")
    public Result<FileVO> export(@RequestBody UserExportDTO upd, String type){
        return FileResult.setResult(FileResultEnum.UPLOAD_SUCCESS).data(userService.export(upd,type));
    }

}

service

IUserService接口
package com.wwt.file.service;

import com.wwt.file.entity.dto.UserExportDTO;
import com.wwt.file.entity.vo.FileVO;

public interface IUserService {

    public FileVO export(UserExportDTO upd, String type);

}
UserService实现类
package com.wwt.file.service.impl;

import cn.hutool.core.util.StrUtil;
import com.wwt.file.entity.dto.UserExportDTO;
import com.wwt.file.entity.vo.FileVO;
import com.wwt.file.enums.FileTypeEnum;
import com.wwt.file.enums.UserTemplate;
import com.wwt.file.service.ExportService;
import com.wwt.file.service.IUserService;
import com.wwt.file.util.excel.UserExcelDataWriter;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;

@Service
public class UserService implements IUserService {

    @Resource
    public ExportService exportService;


    @Override
    public FileVO export(UserExportDTO upd, String type){
        //用工具类生成word文件
        if (StrUtil.equals(FileTypeEnum.WORD.getType(),type) || StrUtil.equals(FileTypeEnum.PDF.getType(),type)){
            return exportService.export(upd.structDataMap(), type, UserTemplate.USER_TEMPLATE_XML.getTemplate(),null);
        }
        //生成excel的话要提供excelwriter
        return exportService.export(upd.structDataMap(), type, UserTemplate.USER_TEMPLATE_EXCEL.getTemplate(),new UserExcelDataWriter());
    }
}

解析
根据文件模板实现预览、生成word、pdf、excel(后端-项目)_第22张图片

ExportService

根据文件模板实现预览、生成word、pdf、excel(后端-项目)_第23张图片
根据文件模板实现预览、生成word、pdf、excel(后端-项目)_第24张图片
根据文件模板实现预览、生成word、pdf、excel(后端-项目)_第25张图片

package com.wwt.file.service;

import cn.hutool.core.util.StrUtil;
import com.wwt.file.entity.vo.FileVO;
import com.wwt.file.enums.FileTypeEnum;
import com.wwt.file.enums.UserTemplate;
import com.wwt.file.util.DocUtil;
import com.wwt.file.util.excel.ExcelDataWriter;
import com.wwt.file.util.FileUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.io.File;
import java.util.Map;

@Slf4j
@Service
public class ExportService {

    /**
     * 临时生成文件的路径
     */
    private static final String TEMP_FILE_DIR = "tmp\\";

    public FileVO export(Map<String, Object> dataMap, String type ,String tempFileName,ExcelDataWriter writer){
        if (StrUtil.equals(FileTypeEnum.WORD.getType(),type)){
            return new FileVO(UserTemplate.USER_TEMPLATE_XML.getChName(),exportWord(dataMap,tempFileName));
        }
        if (StrUtil.equals(FileTypeEnum.PDF.getType(),type)){
            return new FileVO(UserTemplate.USER_TEMPLATE_XML.getChName(),exportPdf(dataMap,tempFileName));
        }
        if (StrUtil.equals(FileTypeEnum.EXCEL.getType(),type)){
            return new FileVO(UserTemplate.USER_TEMPLATE_EXCEL.getChName(),exportExcel(dataMap,writer));
        }
        return new FileVO(null,null);
    }

    /**
     * @param dataMap        填充的数据,每个值都不能为null,可以为empty
     * @param tempFileName   模板文件名,放置在template目录下
     * @return 文件服务器返回的fileId
     * @description 模板导出word
     **/
    public Long exportWord(Map<String, Object> dataMap, String tempFileName) {

        //生成文件名 - 随机文件名,防止重名
        String fileName = FileUtil.generateFileName(FileTypeEnum.WORD);
        DocUtil.saveWord(tempFileName, fileName, dataMap);
        //获取文件绝对路径
        String filePath = StrUtil.concat(true,FileUtil.getResourceBasePath(),TEMP_FILE_DIR,fileName);
        //上传文件到文件服务器
        //Long fileId = fileManagerService.uploadFile(filePath, fileName, "temp", ttl);
        //删除生成的文件
        //FileUtil.delDir(filePath);
        //模拟上传成功返回的id 
        return 1234L; //fileId
    }

    /**
     * @param dataMap        填充的数据,每个值都不能为null,可以为empty
     * @param tempFileName   模板文件名,放置在template目录下
     * @return 文件服务器返回的fileId
     * @description 模板导出pdf
     **/
    public Long exportPdf(Map<String, Object> dataMap, String tempFileName) {
        //生成文件名
        String fileName = FileUtil.generateFileName(FileTypeEnum.PDF);
        DocUtil.savePdf(tempFileName, fileName, dataMap);
        String filePath = new File(FileUtil.getResourceBasePath(), TEMP_FILE_DIR + fileName).getAbsolutePath();
        //上传文件到文件服务器
        //Long fileId = fileManagerService.uploadFile(filePath, fileName, "temp", ttl);
        //FileUtil.delDir(filePath);
        return 1234L; //fileId
    }

    /**
     * 导出excel
     * 包括生成文档和上传文件
     * @param dataMap 数据
     * @param writer 不同的writer实现类处理不同的模板写入
     * @return
     */
    public Long exportExcel(Map<String, Object> dataMap,ExcelDataWriter writer){
        writer.export(dataMap);
        return 1234L;
    }

}

DocUtil 为实现具体文件生成到本地的操作。因为 ExportService 中还涉及到文件上传的操作,所以又把这个生成操作单独提取了出来,封装到DocUtil 中,实现上传和生成的逻辑解耦。

DocUtil

主要看这个 saveWord 方法
根据文件模板实现预览、生成word、pdf、excel(后端-项目)_第26张图片

package com.wwt.file.util;

import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.system.OsInfo;
import cn.hutool.system.SystemUtil;
import com.aspose.words.Document;
import com.aspose.words.FontSettings;
import com.aspose.words.License;
import com.aspose.words.SaveFormat;
import com.wwt.file.enums.FileTypeEnum;
import freemarker.template.Template;
import lombok.extern.slf4j.Slf4j;

import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.Map;

@Slf4j
public class DocUtil {

    /**
     * 临时生成文件的路径
     */
    private static final String TEMP_FILE_DIR = "tmp\\";

    private static final String LICENSE_FILENAME = "aspose_license.xml";

    /**
     * @param templateFileName 模板文件名称
     * @param fileName      生成的文件名.doc结尾
     * @param dataMap          数据,
     * @description 用模板生成word, 模板存放路径在resources/template下,生成的文件路径在jar包的同级目录/tmp路径下
     * @attention dataMap中的数据不能为null, 可用空值代替
     * @other 导出的doc文件其实是文本文件, 而docx文件是二进制文件(其实是一个压缩包).关于如何导出docx可参考 https://blog.csdn.net/wantLight/article/details/106105416
     **/
    public static void saveWord(String templateFileName, String fileName, Map<String, Object> dataMap) {
        Writer out = null;
        try {
            //根据配置获取FreeMarker模板对象
            Template template = FreeMarkerTemplateFactory.getTemplate(templateFileName);
            //拼接出即将生成文件的绝对路径 /xxx/xxx/xxx.doc
            String filePath = StrUtil.concat(true,FileUtil.getResourceBasePath(),TEMP_FILE_DIR,fileName);
            //如果文件父目录不存在,则创建父目录
            FileUtil.mkParentDirIfNotExist(filePath);
            // 创建一个Word文档的输出流
            out = new BufferedWriter(new OutputStreamWriter(Files.newOutputStream(new File(filePath).toPath()), StandardCharsets.UTF_8));
            template.process(dataMap, out);

            out.flush();
        } catch (Exception e) {
            log.error("saveWord error:{}", e.getMessage(), e);
        } finally {
            IoUtil.close(out);
        }
    }

    /**
     * @param templateFileName 模板文件名称
     * @param fileName         生成的文件名.pdf结尾
     * @param dataMap          数据,
     * @description 用模板先生成生成word, 再转成pdf,模板存放路径在resources/template下,生成的文件路径在jar包的同级目录/tmp路径下
     * @attention dataMap中的数据不能为null, 可用空值代替
     **/
    public static void savePdf(String templateFileName, String fileName, Map<String, Object> dataMap) {
        InputStream docInputStream = null;
        OutputStream outputStream = null;
        try {
            String docFileName = fileName.replace(FileTypeEnum.PDF.getSuffix(), FileTypeEnum.WORD.getSuffix());
            //先生成word
            saveWord(templateFileName, docFileName, dataMap);
            String docFilePath = StrUtil.concat(true,FileUtil.getResourceBasePath(),TEMP_FILE_DIR,docFileName);
            //aspose将word转成pdf
            if (!getLicense()) {
                // 验证License 若不验证则转化出的pdf文档会有水印产生
                return;
            }
            OsInfo osInfo = SystemUtil.getOsInfo();
            //linux环境的字体,需要将c:\windows\fonts下的字体文件放到指定目录下,否则会乱码
            if (osInfo.isLinux()) {
                FontSettings.setFontsFolder("/usr/share/fonts/chinese", true);
            }
            docInputStream = Files.newInputStream(new File(docFilePath).toPath());
            File outputFile = new File(FileUtil.getResourceBasePath(), "tmp/" + fileName);
            outputStream = Files.newOutputStream(outputFile.toPath());
            Document doc = new Document(docInputStream);                    //sourcerFile是将要被转化的word文档
            doc.save(outputStream, SaveFormat.PDF);//全面支持DOC, DOCX, OOXML, RTF HTML, OpenDocument, PDF, EPUB, XPS, SWF 相互转换
            //删除生成的临时word文件
            FileUtil.delDir(docFilePath);
        } catch (Exception e) {
            log.error("savePdf error:{}", e.getMessage(), e);
        } finally {
            IoUtil.close(docInputStream);
            IoUtil.close(outputStream);
        }
    }

    /**
     * 判断是否有授权文件 如果没有则会认为是试用版,转换的文件会有水印
     *
     * @return
     */
    public static boolean getLicense() {
        boolean result = false;
        try {
            InputStream is = DocUtil.class.getClassLoader().getResourceAsStream(LICENSE_FILENAME);
            License asposeLic = new License();
            asposeLic.setLicense(is);
            result = true;
        } catch (Exception e) {
            log.error("getLicense error:{}", e.getMessage());
        }
        return result;
    }

}

如果在测试word,还没用到pdf的功能,代码报错,先把报错的注释掉等会再测试就好了

FreeMarkerTemplateFactory - 用来创建 template对象
根据文件模板实现预览、生成word、pdf、excel(后端-项目)_第27张图片

package com.wwt.file.util;

import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.Version;

import java.io.IOException;

/**
 * FreeMarker 创建模板对象的工厂
 */
public class FreeMarkerTemplateFactory {

    /**
     * FreeMarker的版本
     */
    private static final String FREEMARKER_VERSION = "2.3.28";

    /**
     * 模板存放目录
     */
    private static final String TEMPLATE_DIR = "/template";

    /**
     * 编码格式
     */
    private static final String CHARSET = "utf-8";


    /**
     * 获取模板对象
     * @param templateFileName
     * @return
     * @throws IOException
     */
    public static Template getTemplate(String templateFileName) throws IOException {
        Configuration configuration = new Configuration(new Version(FREEMARKER_VERSION));
        configuration.setDefaultEncoding("utf-8");
        //设置模板位置,默认是classpath下的指定目录下,idea中运行时为resources下的指定目录下
        configuration.setClassForTemplateLoading(DocUtil.class, TEMPLATE_DIR);
        Template template = configuration.getTemplate(templateFileName, CHARSET);
        return template;
    }

}

测试

请求及数据

根据文件模板实现预览、生成word、pdf、excel(后端-项目)_第28张图片
请求地址:http://localhost:8080/user/export?type=word
请求数据:

{
    "user1": {
        "id": 1, 
        "name": "张三", 
        "age": 77, 
        "email": "[email protected]"
    }, 
    "user2": {
        "id": 19, 
        "name": "李四", 
        "age": 68, 
        "email": "[email protected]"
    }, 
    "users": [
        {
            "id": 33, 
            "name": "帅哥1", 
            "age": 18, 
            "email": "[email protected]"
        }, 
        {
            "id": 34, 
            "name": "帅哥2", 
            "age": 18, 
            "email": "[email protected]"
        }, 
        {
            "id": 35, 
            "name": "帅哥3", 
            "age": 18, 
            "email": "[email protected]"
        }, 
        {
            "id": 36, 
            "name": "帅哥4", 
            "age": 18, 
            "email": "[email protected]"
        }, 
        {
            "id": 37, 
            "name": "帅哥5", 
            "age": 18, 
            "email": "[email protected]"
        }, 
        {
            "id": 38, 
            "name": "帅哥6", 
            "age": 18, 
            "email": "[email protected]"
        }, 
        {
            "id": 39, 
            "name": "帅哥7", 
            "age": 18, 
            "email": "[email protected]"
        }, 
        {
            "id": 40, 
            "name": "帅哥8", 
            "age": 18, 
            "email": "[email protected]"
        }
    ]
}

根据文件模板实现预览、生成word、pdf、excel(后端-项目)_第29张图片
根据文件模板实现预览、生成word、pdf、excel(后端-项目)_第30张图片

可能会遇到的问题

  1. 文件生成到本地不成功,没权限(window下)
    Could not open/create prefs root node Software\JavaSoft\Prefs at root
    参考:https://blog.csdn.net/yxlzfx/article/details/117767879
  2. 数据填不进去word文档,但是代码执行没报错。这个一方面你要看看你模板中替换符的名字是否和你构建的map的key对应得上。另一方面,你是不是用过idea在启动项目的情况下修改了xml的模板文件,如果是,你看看target/class/template是不是没有替换符。我改用nodpad来修改xml就是这个意思,可能会修改不成功。打包打不进去修改了的内容。所以还是停止项目,用nodpad等工具来改比较好。

生成PDF文档

原理

  1. 生成pdf主要用的是 aspose
  2. 流程就是先生成word文档到临时文件目录,然后用 aspose-word 将word文档转换成pdf,再上传到文件服务器返回文件id,再删除pdf文件,返回文件id给前端。
  3. 生成word的工作调用上面已实现接口,即可。

步骤

下载jar包

地址: https://repository.aspose.com/repo/com/aspose/
根据文件模板实现预览、生成word、pdf、excel(后端-项目)_第31张图片
进去后选一个版本下即可
把jar包添加到项目中
根据文件模板实现预览、生成word、pdf、excel(后端-项目)_第32张图片

添加依赖


        <dependency>
            <groupId>aspose-wordsgroupId>
            <artifactId>aspose-wordsartifactId>
            <version>15.8.0version>
            <scope>systemscope>
            <systemPath>${project.basedir}/src/main/resources/lib/aspose-words-15.8.0-jdk16.jarsystemPath>
        dependency>

注意版本和jar包名字要对应的上

添加认证文件

根据文件模板实现预览、生成word、pdf、excel(后端-项目)_第33张图片

<License>
  <Data>
    <Products>
      <Product>Aspose.Total for JavaProduct>
      <Product>Aspose.Words for JavaProduct>
    Products>
    <EditionType>EnterpriseEditionType>
    <SubscriptionExpiry>20991231SubscriptionExpiry>
    <LicenseExpiry>20991231LicenseExpiry>
    <SerialNumber>8bfe198c-7f0c-4ef8-8ff0-acc3237bf0d7SerialNumber>
  Data>
  <Signature>
    sNLLKGMUdF0r8O1kKilWAGdgfs2BvJb/2Xp8p5iuDVfZXmhppo+d0Ran1P9TKdjV4ABwAgKXxJ3jcQTqE/2IRfqwnPf8itN8aFZlV3TJPYeD3yWE7IT55Gz6EijUpC7aKeoohTb4w2fpox58wWoF3SNp6sK6jDfiAUGEHYJ9pjU=
  Signature>
License>

Service

UserService

没有改变,因为都是通过 ExportService 的export 接口根据类型判断统一实现的。

package com.wwt.file.service.impl;

import cn.hutool.core.util.StrUtil;
import com.wwt.file.entity.dto.UserExportDTO;
import com.wwt.file.entity.vo.FileVO;
import com.wwt.file.enums.FileTypeEnum;
import com.wwt.file.enums.UserTemplate;
import com.wwt.file.service.ExportService;
import com.wwt.file.service.IUserService;
import com.wwt.file.util.excel.UserExcelDataWriter;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;

@Service
    public class UserService implements IUserService {

        @Resource
        public ExportService exportService;


        @Override
        public FileVO export(UserExportDTO upd, String type){
            //用工具类生成word文件
            if (StrUtil.equals(FileTypeEnum.WORD.getType(),type) || StrUtil.equals(FileTypeEnum.PDF.getType(),type)){
                return exportService.export(upd.structDataMap(), type, UserTemplate.USER_TEMPLATE_XML.getTemplate(),null);
            }
            return exportService.export(upd.structDataMap(), type, UserTemplate.USER_TEMPLATE_EXCEL.getTemplate(),new UserExcelDataWriter());
        }
    }

ExportService

根据文件模板实现预览、生成word、pdf、excel(后端-项目)_第34张图片

package com.wwt.file.service;

import cn.hutool.core.util.StrUtil;
import com.wwt.file.entity.vo.FileVO;
import com.wwt.file.enums.FileTypeEnum;
import com.wwt.file.enums.UserTemplate;
import com.wwt.file.util.DocUtil;
import com.wwt.file.util.excel.ExcelDataWriter;
import com.wwt.file.util.FileUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.io.File;
import java.util.Map;

@Slf4j
@Service
public class ExportService {

    /**
     * 临时生成文件的路径
     */
    private static final String TEMP_FILE_DIR = "tmp\\";

    public FileVO export(Map<String, Object> dataMap, String type ,String tempFileName,ExcelDataWriter writer){
        if (StrUtil.equals(FileTypeEnum.WORD.getType(),type)){
            return new FileVO(UserTemplate.USER_TEMPLATE_XML.getChName(),exportWord(dataMap,tempFileName));
        }
        if (StrUtil.equals(FileTypeEnum.PDF.getType(),type)){
            return new FileVO(UserTemplate.USER_TEMPLATE_XML.getChName(),exportPdf(dataMap,tempFileName));
        }
        if (StrUtil.equals(FileTypeEnum.EXCEL.getType(),type)){
            return new FileVO(UserTemplate.USER_TEMPLATE_EXCEL.getChName(),exportExcel(dataMap,writer));
        }
        return new FileVO(null,null);
    }

    /**
     * @param dataMap        填充的数据,每个值都不能为null,可以为empty
     * @param tempFileName   模板文件名,放置在template目录下
     * @return 文件服务器返回的fileId
     * @description 模板导出word
     **/
    public Long exportWord(Map<String, Object> dataMap, String tempFileName) {

        //生成文件名
        String fileName = FileUtil.generateFileName(FileTypeEnum.WORD);
        DocUtil.saveWord(tempFileName, fileName, dataMap);
        //获取文件绝对路径
        String filePath = StrUtil.concat(true,FileUtil.getResourceBasePath(),TEMP_FILE_DIR,fileName);
        //上传文件到文件服务器
        //Long fileId = fileManagerService.uploadFile(filePath, fileName, "temp", ttl);
        //删除生成的文件
        //FileUtil.delDir(filePath);
        //模拟上传成功返回的id
        return 1234L; //fileId
    }

    /**
     * @param dataMap        填充的数据,每个值都不能为null,可以为empty
     * @param tempFileName   模板文件名,放置在template目录下
     * @return 文件服务器返回的fileId
     * @description 模板导出pdf
     **/
    public Long exportPdf(Map<String, Object> dataMap, String tempFileName) {
        //生成文件名
        String fileName = FileUtil.generateFileName(FileTypeEnum.PDF);
        DocUtil.savePdf(tempFileName, fileName, dataMap);
        String filePath = new File(FileUtil.getResourceBasePath(), TEMP_FILE_DIR + fileName).getAbsolutePath();
        //上传文件到文件服务器
        //Long fileId = fileManagerService.uploadFile(filePath, fileName, "temp", ttl);
        //FileUtil.delDir(filePath);
        return 1234L; //fileId
    }

    /**
     * 导出excel
     * 包括生成文档和上传文件
     * @param dataMap 数据
     * @param writer 不同的writer实现类处理不同的模板写入
     * @return
     */
    public Long exportExcel(Map<String, Object> dataMap,ExcelDataWriter writer){
        writer.export(dataMap);
        return 1234L;
    }

}
DocUtil

根据文件模板实现预览、生成word、pdf、excel(后端-项目)_第35张图片
根据文件模板实现预览、生成word、pdf、excel(后端-项目)_第36张图片

package com.wwt.file.util;

import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.system.OsInfo;
import cn.hutool.system.SystemUtil;
import com.aspose.words.Document;
import com.aspose.words.FontSettings;
import com.aspose.words.License;
import com.aspose.words.SaveFormat;
import com.wwt.file.enums.FileTypeEnum;
import freemarker.template.Template;
import lombok.extern.slf4j.Slf4j;

import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.Map;

@Slf4j
public class DocUtil {

    /**
     * 临时生成文件的路径
     */
    private static final String TEMP_FILE_DIR = "tmp\\";

    private static final String LICENSE_FILENAME = "aspose_license.xml";

    /**
     * @param templateFileName 模板文件名称
     * @param fileName      生成的文件名.doc结尾
     * @param dataMap          数据,
     * @description 用模板生成word, 模板存放路径在resources/template下,生成的文件路径在jar包的同级目录/tmp路径下
     * @attention dataMap中的数据不能为null, 可用空值代替
     * @other 导出的doc文件其实是文本文件, 而docx文件是二进制文件(其实是一个压缩包).关于如何导出docx可参考 https://blog.csdn.net/wantLight/article/details/106105416
     **/
    public static void saveWord(String templateFileName, String fileName, Map<String, Object> dataMap) {
        Writer out = null;
        try {
            //根据配置获取FreeMarker模板对象
            Template template = FreeMarkerTemplateFactory.getTemplate(templateFileName);
            String filePath = StrUtil.concat(true,FileUtil.getResourceBasePath(),TEMP_FILE_DIR,fileName);
            //如果文件父目录不存在,则创建父目录
            FileUtil.mkParentDirIfNotExist(filePath);
            // 创建一个Word文档的输出流
            out = new BufferedWriter(new OutputStreamWriter(Files.newOutputStream(new File(filePath).toPath()), StandardCharsets.UTF_8));
            template.process(dataMap, out);

            out.flush();
        } catch (Exception e) {
            log.error("saveWord error:{}", e.getMessage(), e);
        } finally {
            IoUtil.close(out);
        }
    }

    /**
     * @param templateFileName 模板文件名称
     * @param fileName         生成的文件名.pdf结尾
     * @param dataMap          数据,
     * @description 用模板先生成生成word, 再转成pdf,模板存放路径在resources/template下,生成的文件路径在jar包的同级目录/tmp路径下
     * @attention dataMap中的数据不能为null, 可用空值代替
     **/
    public static void savePdf(String templateFileName, String fileName, Map<String, Object> dataMap) {
        InputStream docInputStream = null;
        OutputStream outputStream = null;
        try {
            String docFileName = fileName.replace(FileTypeEnum.PDF.getSuffix(), FileTypeEnum.WORD.getSuffix());
            //先生成word
            saveWord(templateFileName, docFileName, dataMap);
            String docFilePath = StrUtil.concat(true,FileUtil.getResourceBasePath(),TEMP_FILE_DIR,docFileName);
            //aspose将word转成pdf
            if (!getLicense()) {
                // 验证License 若不验证则转化出的pdf文档会有水印产生
                return;
            }
            OsInfo osInfo = SystemUtil.getOsInfo();
            //linux环境的字体,需要将c:\windows\fonts下的字体文件放到指定目录下,否则会乱码
            if (osInfo.isLinux()) {
                FontSettings.setFontsFolder("/usr/share/fonts/chinese", true);
            }
            docInputStream = Files.newInputStream(new File(docFilePath).toPath());
            File outputFile = new File(FileUtil.getResourceBasePath(), "tmp/" + fileName);
            outputStream = Files.newOutputStream(outputFile.toPath());
            Document doc = new Document(docInputStream);                    //sourcerFile是将要被转化的word文档
            doc.save(outputStream, SaveFormat.PDF);//全面支持DOC, DOCX, OOXML, RTF HTML, OpenDocument, PDF, EPUB, XPS, SWF 相互转换
            //删除生成的临时word文件
            FileUtil.delDir(docFilePath);
        } catch (Exception e) {
            log.error("savePdf error:{}", e.getMessage(), e);
        } finally {
            IoUtil.close(docInputStream);
            IoUtil.close(outputStream);
        }
    }

    /**
     * 判断是否有授权文件 如果没有则会认为是试用版,转换的文件会有水印
     *
     * @return
     */
    public static boolean getLicense() {
        boolean result = false;
        try {
            InputStream is = DocUtil.class.getClassLoader().getResourceAsStream(LICENSE_FILENAME);
            License asposeLic = new License();
            asposeLic.setLicense(is);
            result = true;
        } catch (Exception e) {
            log.error("getLicense error:{}", e.getMessage());
        }
        return result;
    }

}

测试

请求地址: http://localhost:8080/user/export?type=pdf
数据同word
根据文件模板实现预览、生成word、pdf、excel(后端-项目)_第37张图片

生成excel

原理

  1. 使用的是easyExcel
  2. 原理也是生成生成Excel文件,然后上传文件服务器,然后返回文件id,再删除临时文件
  3. 因为excel的处理不再是模板不同,构建不同的dataMap即可生成。而是对于每种模板,都要相应地设计不同的写入方式,所以这里我引入了自定义的数据处理抽象类 ExcelDataWriter 。我们处理不同的模板,就构建其子类,实现抽象方法即可。
  4. 写数据的时候,基本对象信息,列表信息是要分次写入的,列表数据如果过大,还能分批写入,easyExcel会提供文件缓存。

步骤

依赖


        <dependency>
            <groupId>com.alibabagroupId>
            <artifactId>easyexcelartifactId>
            <version>3.1.1version>
        dependency>
        
        <dependency>
            <groupId>com.alibabagroupId>
            <artifactId>easyexcel-coreartifactId>
            <version>3.1.1version>
        dependency>

添加excel模板

根据文件模板实现预览、生成word、pdf、excel(后端-项目)_第38张图片
根据文件模板实现预览、生成word、pdf、excel(后端-项目)_第39张图片

Service

UserService

根据文件模板实现预览、生成word、pdf、excel(后端-项目)_第40张图片

@Service
    public class UserService implements IUserService {

        @Resource
        public ExportService exportService;


        @Override
        public FileVO export(UserExportDTO upd, String type){
            //用工具类生成word文件
            if (StrUtil.equals(FileTypeEnum.WORD.getType(),type) || StrUtil.equals(FileTypeEnum.PDF.getType(),type)){
                return exportService.export(upd.structDataMap(), type, UserTemplate.USER_TEMPLATE_XML.getTemplate(),null);
            }
            return exportService.export(upd.structDataMap(), type, UserTemplate.USER_TEMPLATE_EXCEL.getTemplate(),new UserExcelDataWriter());
        }
    }
ExportService

根据文件模板实现预览、生成word、pdf、excel(后端-项目)_第41张图片

package com.wwt.file.service;

import cn.hutool.core.util.StrUtil;
import com.wwt.file.entity.vo.FileVO;
import com.wwt.file.enums.FileTypeEnum;
import com.wwt.file.enums.UserTemplate;
import com.wwt.file.util.DocUtil;
import com.wwt.file.util.excel.ExcelDataWriter;
import com.wwt.file.util.FileUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.io.File;
import java.util.Map;

@Slf4j
@Service
public class ExportService {

    /**
     * 临时生成文件的路径
     */
    private static final String TEMP_FILE_DIR = "tmp\\";

    public FileVO export(Map<String, Object> dataMap, String type ,String tempFileName,ExcelDataWriter writer){
        if (StrUtil.equals(FileTypeEnum.WORD.getType(),type)){
            return new FileVO(UserTemplate.USER_TEMPLATE_XML.getChName(),exportWord(dataMap,tempFileName));
        }
        if (StrUtil.equals(FileTypeEnum.PDF.getType(),type)){
            return new FileVO(UserTemplate.USER_TEMPLATE_XML.getChName(),exportPdf(dataMap,tempFileName));
        }
        if (StrUtil.equals(FileTypeEnum.EXCEL.getType(),type)){
            return new FileVO(UserTemplate.USER_TEMPLATE_EXCEL.getChName(),exportExcel(dataMap,writer));
        }
        return new FileVO(null,null);
    }

    /**
     * @param dataMap        填充的数据,每个值都不能为null,可以为empty
     * @param tempFileName   模板文件名,放置在template目录下
     * @return 文件服务器返回的fileId
     * @description 模板导出word
     **/
    public Long exportWord(Map<String, Object> dataMap, String tempFileName) {

        //生成文件名
        String fileName = FileUtil.generateFileName(FileTypeEnum.WORD);
        DocUtil.saveWord(tempFileName, fileName, dataMap);
        //获取文件绝对路径
        String filePath = StrUtil.concat(true,FileUtil.getResourceBasePath(),TEMP_FILE_DIR,fileName);
        //上传文件到文件服务器
        //Long fileId = fileManagerService.uploadFile(filePath, fileName, "temp", ttl);
        //删除生成的文件
        //FileUtil.delDir(filePath);
        //模拟上传成功返回的id
        return 1234L; //fileId
    }

    /**
     * @param dataMap        填充的数据,每个值都不能为null,可以为empty
     * @param tempFileName   模板文件名,放置在template目录下
     * @return 文件服务器返回的fileId
     * @description 模板导出pdf
     **/
    public Long exportPdf(Map<String, Object> dataMap, String tempFileName) {
        //生成文件名
        String fileName = FileUtil.generateFileName(FileTypeEnum.PDF);
        DocUtil.savePdf(tempFileName, fileName, dataMap);
        String filePath = new File(FileUtil.getResourceBasePath(), TEMP_FILE_DIR + fileName).getAbsolutePath();
        //上传文件到文件服务器
        //Long fileId = fileManagerService.uploadFile(filePath, fileName, "temp", ttl);
        //FileUtil.delDir(filePath);
        return 1234L; //fileId
    }

    /**
     * 导出excel
     * 包括生成文档和上传文件
     * @param dataMap 数据
     * @param writer 不同的writer实现类处理不同的模板写入
     * @return
     */
    public Long exportExcel(Map<String, Object> dataMap,ExcelDataWriter writer){
        String fileName = writer.export(dataMap);
        String filePath = StrUtil.concat(true,FileUtil.getResourceBasePath(),TEMP_FILE_DIR,fileName);
        //文件上传逻辑自己实现
        //Long fileId = fileManagerService.uploadFile(filePath, fileName, "temp", ttl);
        //FileUtil.delDir(filePath);
        return 1234L;
    }

}

excel的处理逻辑类

根据文件模板实现预览、生成word、pdf、excel(后端-项目)_第42张图片

ExcelDataWriter
package com.wwt.file.util.excel;

import cn.hutool.core.io.resource.ClassPathResource;
import cn.hutool.core.util.StrUtil;
import com.wwt.file.enums.FileTypeEnum;
import com.wwt.file.util.FileUtil;
import lombok.Data;

import java.util.Map;

@Data
public abstract class ExcelDataWriter {

    /**
     * 模板名
     * template.xlsx
     * 子类构造方法去定义
     */
    private String template;

    /**
     * 模板所在目录路径
     * /xxx/xxx/xxx/target/classes/template/
     */
    private String tempBasePath;

    /**
     * 模板绝对路径
     * /xxx/xxx/xxx/target/classes/template/template.xlsx
     */
    private String templatePath;

    /**
     * 生成的文件名
     * aaa.xlsx
     */
    private String fileName;

    /**
     * 生成的文件的绝对路径
     * /xxx/xxx/xxx/aaa.xlsx
     */
    private String filePath;


	//抽象方法,让子类实现怎么根据dataMap去写数据生成文件,返回文件名字
    public abstract String export(Map<String, Object> dataMap);

    /**
     * 初始化参数
     */
    public ExcelDataWriter(String template) {
        //D:\code\javaCode\FileHandle\target\classes
        ClassPathResource classPathResource = new ClassPathResource("");
        String classPath = classPathResource.getFile().getAbsolutePath();

        this.template = template;
        //D:\code\javaCode\FileHandle\target\classes\template\
        this.tempBasePath =  StrUtil.concat(true,classPath,"\\template\\");
        //D:\code\javaCode\FileHandle\target\classes\template\xxx.xlsx
        this.templatePath = StrUtil.concat(true,tempBasePath,template);

        this.fileName = FileUtil.generateFileName(FileTypeEnum.EXCEL);
        //D:\code\javaCode\FileHandle\tmp\123456.xlsx
        this.filePath = StrUtil.concat(true,FileUtil.getResourceBasePath(),"tmp\\",this.fileName);
        //写数据前要保证父目录一定要存在
        FileUtil.mkParentDirIfNotExist(this.filePath);
    }
}
UserExcelDataWriter
package com.wwt.file.util.excel;

import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ObjectUtil;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.enums.WriteDirectionEnum;
import com.alibaba.excel.write.metadata.WriteSheet;
import com.alibaba.excel.write.metadata.fill.FillConfig;
import com.alibaba.excel.write.metadata.fill.FillWrapper;
import com.wwt.file.entity.User;
import com.wwt.file.enums.UserTemplate;
import lombok.extern.slf4j.Slf4j;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 用户excel模板生成处理类
 */
@Slf4j
public class UserExcelDataWriter extends ExcelDataWriter{

    //指定模板名
    private final String template = UserTemplate.USER_TEMPLATE_EXCEL.getTemplate();

    /**
     * 初始化参数
     *
     */
    public UserExcelDataWriter() {
        super(UserTemplate.USER_TEMPLATE_EXCEL.getTemplate());
    }

    /**
     * 导出文件到本地
     * 返回文件的绝对路径
     * @param dataMap
     * @return
     */
    @Override
    public String export(Map<String, Object> dataMap) {
        User user1 = MapUtil.get(dataMap, "user1", User.class);
        User user2 = MapUtil.get(dataMap, "user2", User.class);
        List<User> user3 = MapUtil.get(dataMap, "users", List.class);
        List<User> user4 = MapUtil.get(dataMap, "users", List.class);
        List<User> user5 = MapUtil.get(dataMap, "users", List.class);
        String templateFileName = super.getTemplatePath();
        String filePath = super.getFilePath();

        ExcelWriter excelWriter = null;
        String fileName = null;
        try{
            //技巧,数据分段写入
            excelWriter = EasyExcel.write(filePath).withTemplate(templateFileName).build();
            WriteSheet writeSheet = EasyExcel.writerSheet().build();
            HashMap<String, Object> baseMessMap = MapUtil.newHashMap();
            HashMap<String, Object> user3ListMap = MapUtil.newHashMap();
            HashMap<String, Object> user4ListMap = MapUtil.newHashMap();

            //简单模板数据写入
            baseMessMap.put("name1",user1.getName());
            baseMessMap.put("age1",user1.getAge());
            baseMessMap.put("email1",user1.getEmail());
            baseMessMap.put("id1",user1.getId());

            baseMessMap.put("name2",user2.getName());
            baseMessMap.put("age2",user2.getAge());
            baseMessMap.put("email2",user2.getEmail());
            baseMessMap.put("id2",user2.getId());

            excelWriter.fill(baseMessMap, writeSheet);

            //横向列表数据写入
            FillConfig fillConfig = FillConfig.builder().direction(WriteDirectionEnum.HORIZONTAL).build();
            // 如果有多个list 模板上必须有{前缀.} 这里的前缀就是 data5,然后多个list必须用 FillWrapper包裹
            excelWriter.fill(new FillWrapper("user5", user5), fillConfig, writeSheet);
            //纵向列表数据写入 如果数据量很大,可以看看官网,有让你分段查询插入数据的办法,会用到缓存,效率还是很高的
            excelWriter.fill(new FillWrapper("user3", user3), writeSheet);
            excelWriter.fill(new FillWrapper("user4", user4), writeSheet);
            fileName = super.getFileName();
        }catch (Exception e){
            log.error("excel create error : ",e.getMessage(), e);
        }finally {
            if (ObjectUtil.isNotNull(excelWriter)){
                excelWriter.finish();
            }
        }
        return fileName;
    }
}

测试

请求地址:http://localhost:8080/user/export?type=excel
数据同word,只是构建方法不一样,看上面的 UserExcelDataWriter 的 export
根据文件模板实现预览、生成word、pdf、excel(后端-项目)_第43张图片
根据文件模板实现预览、生成word、pdf、excel(后端-项目)_第44张图片

你可能感兴趣的:(工作常见场景,word,excel,pdf)