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();
    }
}

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

相关推荐
Agent手记11 分钟前
终端消费数据自动采集与分析智能体的搭建思路:2026全链路技术架构与实战解析
java·开发语言·人工智能·ai·架构
这是程序猿22 分钟前
mysql的安装教程
java·人工智能·windows·mysql
小Y._23 分钟前
Spring Boot 4.0 发布:Jackson 3 强制迁移、虚拟线程原生支持、弹性能力一文搞定
java
SunnyDays101134 分钟前
Java 合并 Excel 文件的几种实用方法
java·合并 excel
t***54441 分钟前
如何确认 Clang 是否在 Dev-C++ 中成功应用
java·开发语言·c++
Victor35643 分钟前
MongoDB(101)如何处理MongoDB中的慢查询?
后端
weiwen140844 分钟前
快递100 API 工具类封装实践:签名、请求与缓存防锁单
spring boot·spring·缓存
m0_7520356344 分钟前
idea的debug configurations里面的shorten command line作用
java·ide·intellij-idea
一顿操作猛如虎,啥也不是!1 小时前
VISUAL STUDIO和IDEA-c#和java调试快捷键
java
Victor3561 小时前
MongoDB(102)如何处理MongoDB中的数据冲突?
后端