springboot导出带水印文字的xlsx

重要提示:xls和csv均不可生成水印

引入maven,4.x和5.x部分方法api不一样,如果代码报错,可以找ai调整

复制代码
 <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi-ooxml</artifactId>
            <version>5.4.0</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.apache.poi/poi -->
        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi</artifactId>
            <version>5.4.0</version>
        </dependency>

字体

微软雅黑不是免费商用的,我用的是更纱黑体,你也可以选择别的字体

下载地址:https://github.com/be5invis/Sarasa-Gothic(我的资源里上传的也有)

将字体放到如下目录,部署到服务器上后记得测试功能是否正常,防止jar包下字体资源找不到

效果一

文件无法编辑,无法选中

复制代码
package com.heart;

import lombok.Data;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.stereotype.Component;

import java.io.FileOutputStream;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;

@Component
public class ExcelExportDemo {

    public void m1(){
        // 1. 准备模拟数据
        List<User> dataList = new ArrayList<>();

        for (int i = 0; i < 100; i++) {
            dataList.add(new User("张三" + i, 18, "zhangsan@example.com", "123456", "12345678901", "上海", "男", "1990-01-01", "123456789012345678", "正常", "2023-07-01 10:00:00", "2023-07-01 10:00:00", "0", "无", "张三", "张三", "张三", "无", "无"));
            dataList.add(new User("王五" + i, 20, "wangwu@example.com", "123456", "12345678901", "北京", "女", "1990-01-01", "123456789012345678", "正常", "2023-07-01 10:00:00", "2023-07-01 10:00:00", "0", "无", "王五", "王五", "王五", "无", "无"));
            dataList.add(new User("李四" + i, 19, "lisi@example.com", "123456", "12345678901", "广州", "男", "1990-01-01", "123456789012345678", "正常", "2023-07-01 10:00:00", "2023-07-01 10:00:00", "0", "无", "李四", "李四", "李四", "无", "无"));

        }

        // 2. 定义输出文件路径 (输出到项目根目录)
        String filePath = "D:\\aa\\user_data.xlsx";

        // 3. 使用 POI 写入 Excel
        try (XSSFWorkbook workbook = new XSSFWorkbook()) {
            // 创建工作表
            XSSFSheet sheet = workbook.createSheet("用户信息");
            XSSFSheet sheet2 = workbook.createSheet("东方不败");

            // 创建表头
            Row headerRow = sheet.createRow(0);
            Row headerRow2 = sheet2.createRow(0);
            headerRow.createCell(0).setCellValue("姓名");
            headerRow.createCell(1).setCellValue("年龄");
            headerRow.createCell(2).setCellValue("邮箱");
            headerRow.createCell(3).setCellValue("密码");
            headerRow.createCell(4).setCellValue("手机号");
            headerRow.createCell(5).setCellValue("地址");
            headerRow.createCell(6).setCellValue("性别");
            headerRow.createCell(7).setCellValue("生日");
            headerRow.createCell(8).setCellValue("身份证号");
            headerRow.createCell(9).setCellValue("状态");
            headerRow.createCell(10).setCellValue("创建时间");
            headerRow.createCell(11).setCellValue("更新时间");
            headerRow.createCell(12).setCellValue("删除标识");
            headerRow.createCell(13).setCellValue("备注");
            headerRow.createCell(14).setCellValue("创建人");
            headerRow.createCell(15).setCellValue("更新人");
            headerRow.createCell(16).setCellValue("删除人");
            headerRow.createCell(17).setCellValue("删除原因");
            headerRow.createCell(18).setCellValue("删除时间");


            headerRow2.createCell(0).setCellValue("姓名");
            headerRow2.createCell(1).setCellValue("年龄");
            headerRow2.createCell(2).setCellValue("邮箱");
            headerRow2.createCell(3).setCellValue("密码");
            headerRow2.createCell(4).setCellValue("手机号");
            headerRow2.createCell(5).setCellValue("地址");
            headerRow2.createCell(6).setCellValue("性别");
            headerRow2.createCell(7).setCellValue("生日");
            headerRow2.createCell(8).setCellValue("身份证号");
            headerRow2.createCell(9).setCellValue("状态");
            headerRow2.createCell(10).setCellValue("创建时间");
            headerRow2.createCell(11).setCellValue("更新时间");
            headerRow2.createCell(12).setCellValue("删除标识");
            headerRow2.createCell(13).setCellValue("备注");
            headerRow2.createCell(14).setCellValue("创建人");
            headerRow2.createCell(15).setCellValue("更新人");
            headerRow2.createCell(16).setCellValue("删除人");
            headerRow2.createCell(17).setCellValue("删除原因");
            headerRow2.createCell(18).setCellValue("删除时间");


            // 填充数据
            int rowNum = 1;
            for (User user : dataList) {
                Row row = sheet.createRow(rowNum++);
                row.createCell(0).setCellValue(user.getName());
                row.createCell(1).setCellValue(user.getAge());
                row.createCell(2).setCellValue(user.getEmail());
                row.createCell(3).setCellValue(user.getPassword());
                row.createCell(4).setCellValue(user.getPhone());
                row.createCell(5).setCellValue(user.getAddress());
                row.createCell(6).setCellValue(user.getSex());
                row.createCell(7).setCellValue(user.getBirth());
                row.createCell(8).setCellValue(user.getIdCard());
                row.createCell(9).setCellValue(user.getStatus());
                row.createCell(10).setCellValue(user.getCreateTime());
                row.createCell(11).setCellValue(user.getUpdateTime());
                row.createCell(12).setCellValue(user.getDeleteFlag());
                row.createCell(13).setCellValue(user.getRemark());
                row.createCell(14).setCellValue(user.getCreateUser());
                row.createCell(15).setCellValue(user.getUpdateUser());
                row.createCell(16).setCellValue(user.getDeleteUser());
                row.createCell(17).setCellValue(user.getDeleteReason());
                row.createCell(18).setCellValue(user.getDeleteTime());

                Row row2 = sheet2.createRow(rowNum++);
                row2.createCell(0).setCellValue(user.getName());
                row2.createCell(1).setCellValue(user.getAge());
                row2.createCell(2).setCellValue(user.getEmail());
                row2.createCell(3).setCellValue(user.getPassword());
                row2.createCell(4).setCellValue(user.getPhone());
                row2.createCell(5).setCellValue(user.getAddress());
                row2.createCell(6).setCellValue(user.getSex());
                row2.createCell(7).setCellValue(user.getBirth());
                row2.createCell(8).setCellValue(user.getIdCard());
                row2.createCell(9).setCellValue(user.getStatus());
                row2.createCell(10).setCellValue(user.getCreateTime());
                row2.createCell(11).setCellValue(user.getUpdateTime());
                row2.createCell(12).setCellValue(user.getDeleteFlag());
                row2.createCell(13).setCellValue(user.getRemark());
                row2.createCell(14).setCellValue(user.getCreateUser());
                row2.createCell(15).setCellValue(user.getUpdateUser());
                row2.createCell(16).setCellValue(user.getDeleteUser());
                row2.createCell(17).setCellValue(user.getDeleteReason());
                row2.createCell(18).setCellValue(user.getDeleteTime());
            }

            // 自动调整列宽(可选)
//            for (int i = 0; i < 20; i++) {
//                sheet.autoSizeColumn(i);
//            }
            String format = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
            String waterMarkText = "张三|测试部门本次吃饭好成绩|wangwu@example.com|" + format;

//            ExcelWatermarkUtil.setWaterMark(workbook, sheet, waterMarkText, null);
            ExcelWatermarkUtilV2.setWaterMark(workbook, waterMarkText, null);

//            WaterMark waterMark = new WaterMark();
//            waterMark.setPhone("1231343131");
//            waterMark.setName("张三");
//            Date date = new Date();
//            waterMark.setDate(date.toString());
//            PaintWaterMarkUtils.painWaterMark(workbook, sheet, waterMark);

            // 写入本地文件
            try (FileOutputStream outputStream = new FileOutputStream(filePath)) {
                workbook.write(outputStream);
            }

            System.out.println("Excel文件生成成功,路径:" + filePath);

        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        new ExcelExportDemo().m1();
    }

    // 简单的内部实体类,方便模拟数据
    @Data
    static class User {
        private String name;
        private int age;
        private String email;
        private String password;
        private String phone;
        private String address;
        private String sex;
        private String birth;
        private String idCard;
        private String status;
        private String createTime;
        private String updateTime;
        private String deleteFlag;
        private String remark;
        private String createUser;
        private String updateUser;
        private String deleteUser;
        private String deleteReason;
        private String deleteTime;

        public User(String name, int age, String email, String password, String phone, String address, String sex, String birth, String idCard, String status, String createTime, String updateTime, String deleteFlag, String remark, String createUser, String updateUser, String deleteUser, String deleteReason, String deleteTime) {
            this.name = name;
            this.age = age;
            this.email = email;
            this.password = password;
            this.phone = phone;
            this.address = address;
            this.sex = sex;
            this.birth = birth;
            this.idCard = idCard;
            this.status = status;
            this.createTime = createTime;
            this.updateTime = updateTime;
            this.deleteFlag = deleteFlag;
            this.remark = remark;
            this.createUser = createUser;
            this.updateUser = updateUser;
            this.deleteUser = deleteUser;
            this.deleteReason = deleteReason;
            this.deleteTime = deleteTime;
        }
    }
}




package com.heart;


import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFSheet;

import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.Color;
import java.awt.Font;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;

/**
 * Excel水印工具类(优化版V2)
 * 解决问题:
 * 1. 水印文字显示不全 - 增大图片高度,确保文字完整显示
 * 2. 多个水印互相遮挡 - 大幅增大间距,确保完全不重叠
 *
 * 关键设计原则:
 * - 间距必须远大于水印占用区域,因为倾斜的水印会延伸出锚点矩形
 * - 水印右下角会因倾斜而超出锚点区域,需要足够的间距缓冲
 */
public class ExcelWatermarkUtilV2 {

    // 水印配置常量
    private static final int WATERMARK_IMAGE_WIDTH = 400;   // 水印图片宽度(减小以降低密度)
    private static final int WATERMARK_IMAGE_HEIGHT = 120;   // 水印图片高度
    private static final int FONT_SIZE = 20;                // 字体大小
    private static final int ROTATION_ANGLE = -20;          // 旋转角度(减小倾斜度)

    // 水印布局配置 - 关键:间距必须远大于水印占用区域
    // 水印倾斜后右下角会延伸,所以间距需要足够大
    private static final int WATERMARK_COLS_SPAN = 6;       // 每个水印占用的列数
    private static final int WATERMARK_ROWS_SPAN = 12;       // 每个水印占用的行数
    private static final int COL_SPACING = 6;               // 列间距(是水印宽度的3倍)
    private static final int ROW_SPACING = 12;              // 行间距(是水印高度的3倍)

    /**
     * 给Excel Sheet添加平铺水印(支持自定义参数)
     *
     * @param workbook   工作簿
     * @param watermark  水印文字
     * @param password   保护密码(可为null)
     */
    public static void setWaterMark(Workbook workbook,  String watermark, String password) {
        try {
            // 生成水印图片
            BufferedImage image = createWatermarkImage(watermark);
            ByteArrayOutputStream os = new ByteArrayOutputStream();
            ImageIO.write(image, "png", os);


            // 为所有工作表设置背景图片
            for (int i = 0; i < workbook.getNumberOfSheets(); i++) {
                Sheet sheet=workbook.getSheetAt(i);
                int pictureIdx = sheet.getWorkbook().addPicture(os.toByteArray(), Workbook.PICTURE_TYPE_PNG);
                Drawing<?> drawing = sheet.createDrawingPatriarch();
                // 计算工作表范围
                int maxRow = Math.max(sheet.getLastRowNum() + 20, 50);
                int maxCol = getMaxColumn(sheet) + 10;

                // 平铺水印,使用简单网格布局(不交错,避免遮挡)
                // 间距已经设置得足够大,确保倾斜的水印不会重叠
                for (int col = 0; col < maxCol; col += COL_SPACING) {
                    for (int row = 0; row < maxRow; row += ROW_SPACING) {
                        // 创建锚点
                        ClientAnchor anchor = drawing.createAnchor(
                                0, 0, 0, 0,
                                col, row,
                                col + WATERMARK_COLS_SPAN, row + WATERMARK_ROWS_SPAN
                        );
                        anchor.setAnchorType(ClientAnchor.AnchorType.MOVE_AND_RESIZE);
                        drawing.createPicture(anchor, pictureIdx);
                    }
                }

                protectSheet(sheet, password);
            }



        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 保护工作表,但允许编辑未锁定的单元格
     */
    private static void protectSheet(Sheet sheet, String password) {
        sheet.protectSheet(password != null ? password : "");

        if (sheet instanceof XSSFSheet) {
            XSSFSheet xssfSheet = (XSSFSheet) sheet;

            // 禁止用户进行的操作
            xssfSheet.lockDeleteColumns(false);
            xssfSheet.lockDeleteRows(false);
            xssfSheet.lockFormatCells(false);
            xssfSheet.lockFormatColumns(false);
            xssfSheet.lockFormatRows(false);
            xssfSheet.lockInsertColumns(false);
            xssfSheet.lockInsertRows(false);

            // 锁定绘图对象(包括水印图片)
            xssfSheet.lockObjects(true);

            // 允许选择锁定和未锁定的单元格
            xssfSheet.lockSelectLockedCells(true);
            xssfSheet.lockSelectUnlockedCells(true);

            // 禁止排序和自动筛选
            xssfSheet.lockSort(false);
            xssfSheet.lockAutoFilter(false);
        }
    }

    /**
     * 生成水印图片(优化版V2)
     *
     * 优化点:
     * 1. 根据文字长度动态计算图片宽度
     * 2. 减小图片尺寸,配合更大的间距避免遮挡
     * 3. 减小旋转角度,降低水印延伸范围
     */
    private static BufferedImage createWatermarkImage(String text) {
        Font font = loadFontFromResource("fonts/SarasaGothicSC-Regular.ttf", Font.BOLD, FONT_SIZE);
        // 创建临时Graphics以计算文字尺寸
        BufferedImage tempImage = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB);
        Graphics2D tempG2d = tempImage.createGraphics();
        tempG2d.setFont(font);
        FontMetrics metrics = tempG2d.getFontMetrics();

        // 计算文字实际尺寸
        int textWidth = metrics.stringWidth(text);
        int textHeight = metrics.getHeight();
        tempG2d.dispose();

        // 根据旋转角度计算需要的额外空间
        double radians = Math.toRadians(Math.abs(ROTATION_ANGLE));
        int rotatedWidth = (int) (textWidth * Math.cos(radians) + textHeight * Math.sin(radians));
        int rotatedHeight = (int) (textWidth * Math.sin(radians) + textHeight * Math.cos(radians));

        // 设置图片尺寸,留有足够边距
        int width = Math.max(WATERMARK_IMAGE_WIDTH, rotatedWidth + 30);
        int height = Math.max(WATERMARK_IMAGE_HEIGHT, rotatedHeight + 15);

        // 创建带透明度的图片
        BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
        Graphics2D g2d = image.createGraphics();

        // 启用抗锯齿
        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
        g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);

        // 设置透明背景
        g2d.setComposite(AlphaComposite.Clear);
        g2d.fillRect(0, 0, width, height);
        g2d.setComposite(AlphaComposite.SrcOver);

        // 设置字体、颜色、透明度
        g2d.setColor(new Color(200, 200, 200, 120)); // 浅灰色,透明度约47%

//        g2d.setFont(new Font("微软雅黑", Font.BOLD, FONT_SIZE));
        g2d.setFont(font);

        // 设置旋转角度(以图片中心为旋转点)
        g2d.rotate(Math.toRadians(ROTATION_ANGLE), (double) width / 2, (double) height / 2);

        // 计算文字居中位置
        metrics = g2d.getFontMetrics();
        int x = (width - metrics.stringWidth(text)) / 2;
        int y = height / 2 + metrics.getAscent() / 2 - metrics.getDescent() / 2;

        // 绘制文字
        g2d.drawString(text, x, y);

        g2d.dispose();
        return image;
    }

    /**
     * 获取工作表的列数
     *
     * @param sheet 工作表
     * @return 最大列数
     */
    private static int getMaxColumn(Sheet sheet) {
        int maxColumns = 0;
        for (int i = 0; i <= sheet.getLastRowNum(); i++) {
            Row row = sheet.getRow(i);
            if (row != null) {
                maxColumns = Math.max(maxColumns, row.getLastCellNum());
            }
        }
        return Math.max(maxColumns, 6);
    }

    /**
     * 从类路径加载字体
     */
    private static Font loadFontFromResource(String fontPath, int style, float size) {
        try (InputStream is = ExcelWatermarkUtil.class.getClassLoader().getResourceAsStream(fontPath)) {
            if (is == null) {
                throw new RuntimeException("字体文件未找到: " + fontPath);
            }
            return Font.createFont(Font.TRUETYPE_FONT, is).deriveFont(style, size);
        } catch (Exception e) {
            throw new RuntimeException("加载字体失败", e);
        }
    }
}

效果二

单元格可被编辑,但是全选复制到新的文件,水印失效,保密性不强

复制代码
package com.heart;

import org.apache.poi.openxml4j.opc.PackagePart;
import org.apache.poi.openxml4j.opc.PackageRelationship;
import org.apache.poi.openxml4j.opc.TargetMode;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.openxmlformats.schemas.spreadsheetml.x2006.main.CTWorksheet;

import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.FileOutputStream;

/**
 * Excel 水印导出 单元格可被修改,但是复制到新的xlsx,无水印
 */
public class ExcelWatermarkExport {

    public static void main(String[] args) throws Exception {

        XSSFWorkbook workbook = new XSSFWorkbook();
        XSSFSheet sheet = workbook.createSheet("Sheet1");

        // 写入测试数据
        for (int i = 0; i < 20; i++) {
            Row row = sheet.createRow(i);
            for (int j = 0; j < 10; j++) {
                Cell cell = row.createCell(j);
                cell.setCellValue("测试数据");
            }
        }

        // 1. 生成水印图片
        byte[] imageBytes = createWatermarkImage("admin|2026-02-07|测试部门");

        // 2. 添加图片到 workbook
        int pictureIdx = workbook.addPicture(imageBytes, Workbook.PICTURE_TYPE_PNG);

        // 3. 获取图片对应的 PackagePart
        PackagePart sheetPart = sheet.getPackagePart();
        PackagePart imagePart = workbook.getAllPictures().get(pictureIdx).getPackagePart();

        // 4. 建立 relationship
        PackageRelationship relationship = sheetPart.addRelationship(
                imagePart.getPartName(),
                TargetMode.INTERNAL,
                "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image"
        );

        // 5. 写入底层 XML
        CTWorksheet ctWorksheet = sheet.getCTWorksheet();
        ctWorksheet.addNewPicture().setId(relationship.getId());

        // 6. 允许编辑
        CellStyle unlockedStyle = workbook.createCellStyle();
        unlockedStyle.setLocked(false);
        for (Row row : sheet) {
            for (Cell cell : row) {
                cell.setCellStyle(unlockedStyle);
            }
        }

        // 7. 启用保护
        sheet.protectSheet("123456");

        try (FileOutputStream fos = new FileOutputStream("D:\\aa\\watermark.xlsx")) {
            workbook.write(fos);
        }

        workbook.close();
    }

    private static byte[] createWatermarkImage(String text) throws Exception {

        int width = 1000;
        int height = 600;

        BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
        Graphics2D g2d = image.createGraphics();

        g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.15f));
        g2d.setColor(Color.GRAY);
//        g2d.setFont(new Font("Arial", Font.BOLD, 100));
        g2d.setFont(new Font("微软雅黑", Font.BOLD, 100));

        g2d.rotate(Math.toRadians(-30), width / 2, height / 2);

        FontMetrics fm = g2d.getFontMetrics();
        int x = (width - fm.stringWidth(text)) / 2;
        int y = height / 2;

        g2d.drawString(text, x, y);
        g2d.dispose();

        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ImageIO.write(image, "png", baos);
        return baos.toByteArray();
    }
}

本人比较懒,有什么问题可以直接评论,我会回答的

相关推荐
小马爱打代码2 小时前
SpringBoot + JVM 内存泄漏监控 + Heap Dump 自动采集:OOM 前自动预警并留存现场
jvm·spring boot·后端
Soofjan2 小时前
Go Map SwissTable Iter 迭代流程(源码笔记 7)
后端
Lyyaoo.2 小时前
What is Maven?
java·spring boot·maven
李慕婉学姐2 小时前
Springboot传统文化服饰交流平台k79z52ic(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。
数据库·spring boot·后端
23.2 小时前
【Java】NIO零拷贝:为何transferTo需要循环调用?
java·面试·nio
I_LPL2 小时前
day48 代码随想录算法训练营 图论专题1
java·算法·深度优先·图论·广度优先·求职面试
架构师沉默2 小时前
如果 Spring 没了,Java 会怎么样?
java·后端·架构
不会写DN2 小时前
Go 语言并发编程的 “工具箱”
开发语言·后端·golang
文心快码BaiduComate2 小时前
Comate 4.0的自我进化:后端“0帧起手”写前端、自己修自己!
前端·后端·架构