Java使用poi-tl实现word模版渲染文本/图片

poi-tl参考文档:https://deepoove.com/poi-tl/

摘要: 案例主要涉及了 文本、图片、多张图片 渲染的功能,如需其他功能可参看poi-tl官方文档。

一、制作word模版

  1. 使用WPS或者office制作出word模版。示例:模版放在 resources 目录下,图片可放在 resources/static 目录下。
  • resources:SpringBoot中的类路径加载,通过ClassPathResource或类加载器可以轻松获取文件流,无需编写绝对路径。
  • resources/static:SpringBoot的默认静态资源目录,通常用来存放CSS、JS和图片。
  1. 数据渲染后导出的word文档示例:

二、编写代码

  1. 首先创建一个SpringBoot项目。

  2. pom文件引入相关依赖。

java 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>3.2.5</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.example</groupId>
	<artifactId>ExportWord</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<packaging>jar</packaging>
    <name>ExportWord</name>
	<description>ExportWord</description>

	<properties>
		<java.version>17</java.version>
		<poi-tl.version>1.12.2</poi-tl.version>
	</properties>

	<dependencies>
		<!-- Spring Boot Web -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

		<!-- MyBatis Plus (通常不含 POI,但为保险排除) -->
		<dependency>
			<groupId>com.baomidou</groupId>
			<artifactId>mybatis-plus-boot-starter</artifactId>
			<version>3.4.1</version>
		</dependency>
		<dependency>
			<groupId>com.deepoove</groupId>
			<artifactId>poi-tl</artifactId>
			<version>${poi-tl.version}</version>
		</dependency>

		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<version>1.18.42</version>
		</dependency>
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<version>8.0.33</version>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-compiler-plugin</artifactId>
				<configuration>
					<source>${java.version}</source>
					<target>${java.version}</target>
					<encoding>UTF-8</encoding>
				</configuration>
			</plugin>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>
  1. 编写工具类------Response 响应类。
java 复制代码
@Data
public class Response<T> implements Serializable
{
    private static final long serialVersionUID = 1L;

    /** 成功 */
    public static final int SUCCESS = 200;

    /** 失败 */
    public static final int FAIL = 500;

    private int code;

    private String msg;

    private T data;

    public static <T> Response<T> ok()
    {
        return restResult(null, SUCCESS, null);
    }

    public static <T> Response<T> ok(T data)
    {
        return restResult(data, SUCCESS, null);
    }

    public static <T> Response<T> ok(T data, String msg)
    {
        return restResult(data, SUCCESS, msg);
    }

    public static <T> Response<T> fail()
    {
        return restResult(null, FAIL, null);
    }

    public static <T> Response<T> fail(String msg)
    {
        return restResult(null, FAIL, msg);
    }

    public static <T> Response<T> fail(T data)
    {
        return restResult(data, FAIL, null);
    }

    public static <T> Response<T> fail(T data, String msg)
    {
        return restResult(data, FAIL, msg);
    }

    public static <T> Response<T> fail(int code, String msg)
    {
        return restResult(null, code, msg);
    }

    private static <T> Response<T> restResult(T data, int code, String msg)
    {
        Response<T> apiResult = new Response<>();
        apiResult.setCode(code);
        apiResult.setData(data);
        apiResult.setMsg(msg);
        return apiResult;
    }

    public static <T> Boolean isError(Response<T> ret)
    {
        return !isSuccess(ret);
    }

    public static <T> Boolean isSuccess(Response<T> ret)
    {
        return Response.SUCCESS == ret.getCode();
    }
}
  1. 编写实体类。
java 复制代码
@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("safety_briefing")
public class SafetyBriefing {

    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;

    // 单位名称
    private String unitName;
    // 作业名称
    private String jobName;
    // 编号
    private String sno;
    // 作业地点
    private String jobPlace;
    // 组织者
    private String organizer;
    // 交底时间
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private LocalDateTime briefingTime;
    // 交底地点
    private String place;
    // 作业计划
    private String contentPart1;
    // 安全交底内容
    private String contentPart2;
    // 公司员工签字
    private String companyStaffList;
    // 作业负责人
    private String projectManager;
    // 签字日期
    private LocalDate signDate;
    // 创建时间
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private LocalDateTime createTime;
    // 更新时间
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private LocalDateTime updateTime;
}
  1. 编写 controller 类。
java 复制代码
@RestController
@RequestMapping("/export")
@RequiredArgsConstructor
public class ExportWordController {

    private final SafetyBriefingWordService wordService;


    /**
     * 动态生成word文档
     * 1. 获取word文件模版
     * 2. 使用 poi-tl 渲染并生成新的 word 文档
     * 3. 返回回显数据与下载链接
     * @return
     */
    @PostMapping("/word/generate")
    public Response<?> generateWord(@RequestBody SafetyBriefing generateForm) {
        GenerateWordResult generate = wordService.generate(generateForm);
        return Response.ok(generate);
    }

    /**
     * 下载生成后的文档(下载成功后会清理对应临时文件)
     */
    @GetMapping("/word/download/{fileId}")
    public void download(@PathVariable String fileId, HttpServletResponse response) throws IOException {
        response.setContentType("application/vnd.openxmlformats-officedocument.wordprocessingml.document");
        response.setHeader("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0");
        response.setHeader("Pragma", "no-cache");
        response.setCharacterEncoding("UTF-8");
        wordService.writeToResponseAndCleanup(fileId, response);
    }
}
  1. 编写 service 类。
java 复制代码
@Service
@Slf4j
@RequiredArgsConstructor
public class SafetyBriefingWordService {

    private static final String TEMPLATE_CLASSPATH = "classpath:/交底记录表.docx";
    private static final String STATIC_CLASSPATH_PREFIX = "classpath:/static/";
    private static final DateTimeFormatter DT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");

    private final ResourceLoader resourceLoader;
    private final WordFileStore store = new WordFileStore();

    public GenerateWordResult generate(SafetyBriefing form) {
        String fileId = UUID.randomUUID().toString().replace("-", "");
        Path outPath = createTempDocxPath(fileId);

        Resource templateResource = resourceLoader.getResource(TEMPLATE_CLASSPATH);
        if (!templateResource.exists()) {
            throw new IllegalStateException("找不到模板:" + TEMPLATE_CLASSPATH + ",请将交底记录表.docx放到resources根目录");
        }

        Map<String, Object> data = toRenderData(form);

        try (XWPFTemplate template = XWPFTemplate.compile(templateResource.getInputStream()).render(data);
             FileOutputStream fos = new FileOutputStream(outPath.toFile())) {
            template.write(fos);
        } catch (Exception e) {
            log.error("word渲染失败,companyStaffList={}", form.getCompanyStaffList(), e);
            throw new IllegalStateException("生成word失败:" + e.getMessage(), e);
        }

        store.put(fileId, outPath);
        return new GenerateWordResult(fileId, "/export/word/download/" + fileId, form);
    }

    private Map<String, Object> toRenderData(SafetyBriefing form) {
        Map<String, Object> map = new HashMap<>();
        map.put("unitName", form.getUnitName());
        map.put("jobName", form.getJobName());
        map.put("sno", form.getSno());
        map.put("jobPlace", form.getJobPlace());
        map.put("organizer", form.getOrganizer());
        map.put("briefingTime", form.getBriefingTime() == null ? "" : DT.format(form.getBriefingTime()));
        map.put("place", form.getPlace());
        map.put("contentPart1", form.getContentPart1());
        map.put("contentPart2", form.getContentPart2());
        map.put("projectManager", loadStaffSignature(form.getProjectManager()));

        // 动态员工签名(poi-tl循环标签数据)
        List<Map<String, Object>> staffRows = buildStaffRows(form.getCompanyStaffList());
        map.put("signImageList", staffRows);

        // 向后兼容:若模板是固定占位符写法,仍可显示前两张
        map.put("companyStaff", staffRows.isEmpty() ? null : staffRows.get(0).get("companyStaffSign"));
        map.put("companyStaff1", staffRows.size() > 0 ? staffRows.get(0).get("companyStaffSign") : null);
        map.put("companyStaff2", staffRows.size() > 1 ? staffRows.get(1).get("companyStaffSign") : null);

        LocalDate signDate = form.getSignDate();
        if (signDate != null) {
            map.put("year", signDate.getYear());
            map.put("month", signDate.getMonthValue());
            map.put("day", signDate.getDayOfMonth());
        } else {
            map.put("year", "");
            map.put("month", "");
            map.put("day", "");
        }
        return map;
    }

    private List<Map<String, Object>> buildStaffRows(String companyStaffListStr) {
        List<String> staffNames = parseSignNames(companyStaffListStr);
        if (staffNames.isEmpty()) {
            return Collections.emptyList();
        }

        List<Map<String, Object>> rows = new ArrayList<>();
        for (String staffName : staffNames) {
            Map<String, Object> row = new HashMap<>();
            row.put("staffName", staffName);
            PictureRenderData sign = loadStaffSignature(staffName);
            row.put("companyStaffSign", sign);
            rows.add(row);
        }
        return rows;
    }
    
    private List<String> parseSignNames(String companyStaff) {
        if (companyStaff == null || companyStaff.isBlank()) {
            return Collections.emptyList();
        }
        return Arrays.stream(companyStaff.split("[,;|,;]"))
                .map(String::trim)
                .filter(s -> !s.isEmpty())
                .toList();
    }

    private PictureRenderData loadStaffSignature(String staffName) {
        String filename = normalizePngName(staffName);
        Resource resource = resourceLoader.getResource(STATIC_CLASSPATH_PREFIX + filename);
        if (!resource.exists()) {
            log.warn("员工签名图片不存在:{}", filename);
            return null;
        }

        try (InputStream is = resource.getInputStream()) {
            return Pictures.ofStream(is, PictureType.PNG)
                    .size(80, 40)
                    .create();
        } catch (Exception e) {
            log.error("读取员工签名图片失败:{}", filename, e);
            return null;
        }
    }

    private String normalizePngName(String rawName) {
        String name = rawName == null ? "" : rawName.trim();
        if (name.isEmpty()) {
            return "";
        }
        return name.endsWith(".png") ? name : name + ".png";
    }

    private static Path createTempDocxPath(String fileId) {
        try {
            String tmpDir = System.getProperty("java.io.tmpdir");
            Path dir = Path.of(tmpDir, "ExportWord");
            Files.createDirectories(dir);
            return dir.resolve(fileId + ".docx");
        } catch (IOException e) {
            throw new IllegalStateException("创建临时目录失败:" + e.getMessage(), e);
        }
    }

    public void writeToResponseAndCleanup(String fileId, HttpServletResponse response) throws IOException {
        Path path = store.remove(fileId)
                .orElseThrow(() -> new FileNotFoundException("未找到可下载文件,可能已下载或已过期。fileId=" + fileId));

        String filename = "safety-briefing-" + fileId + ".docx";
        String encoded = URLEncoder.encode(filename, StandardCharsets.UTF_8).replace("+", "%20");
        response.setHeader("Content-Disposition", "attachment; filename*=UTF-8''" + encoded);

        try (FileInputStream fis = new FileInputStream(path.toFile());
             ServletOutputStream os = response.getOutputStream()) {
            fis.transferTo(os);
            os.flush();
        } finally {
            Files.deleteIfExists(path);
        }
    }
}
  1. 编写 GenerateWordResult 类。
java 复制代码
@Data
@NoArgsConstructor
@AllArgsConstructor
public class GenerateWordResult {
    private String fileId;
    private String downloadUrl;
    private SafetyBriefing data;

}
  1. 使用接口测试工具如PostMan或Apifox测试接口。
相关推荐
tkevinjd3 小时前
Redis主从复制
数据库·redis·后端·缓存·面试
庞轩px3 小时前
ThreadLocal 源码分析与内存泄漏问题
java·jvm·线程·threadlocal·内存泄露·key-value
小江的记录本3 小时前
【Logback】Logback 日志框架 与 SLF4J绑定、三层模块、MDC链路追踪、异步日志、滚动策略
java·spring boot·后端·spring·log4j·maven·logback
随风,奔跑3 小时前
Spring Boot笔记
java·spring boot·笔记·后端
studyForMokey3 小时前
【Android面试】Handler专题
android·java·面试
大鹏说大话3 小时前
构建铜墙铁壁:Laravel 中间件实现基于 Redis 滑动窗口的速率限制
数据库
ruiang3 小时前
Spring Boot 3.3.4 升级导致 Logback 之前回滚策略配置不兼容问题解决
java·spring boot·logback
yoothey3 小时前
我对Java Web开发中多线程的困惑
java·开发语言·前端
xiufeia3 小时前
JMeter
java·jmeter·tomcat·高并发