使用Itext9生成PDF水印,兼容不同生成引擎的坐标系(如: Skia、OpenPDF)

使用Itext9生成PDF水印,兼容不同生成引擎的坐标系(如: Skia、OpenPDF)

问题背景

不同的PDF生成引擎可能使用不同的坐标系约定:

  • 标准PDF坐标系:原点在页面左下角,Y轴向上
  • Skia引擎坐标系:可能存在Y轴翻转或其他矩阵变换
  • OpenPDF引擎:在某些情况下可能产生镜像或其他异常矩阵变换
  • 其他PDF库:各种第三方PDF库可能产生不同的矩阵配置

这种差异会导致在添加水印时出现文字镜像、位置错误或显示异常等问题。需要进行矩阵兼容性检测和修正。

核心检测逻辑

1. 页面翻转检测

java 复制代码
private static boolean pageFlip(PdfPage page)

该函数通过分析PDF页面的内容流,检测是否存在需要镜像修正的变换矩阵。

检测流程:

  1. 获取页面内容流
  2. 使用PdfCanvasProcessor处理内容
  3. 通过EnhancedMirrorListener监听各类渲染事件
  4. 分析变换矩阵判断是否需要修正

2. 变换矩阵分析

java 复制代码
private static boolean needsMirrorCorrection(Matrix m)

该方法是最核心的矩阵兼容性检测逻辑,通过分析PDF变换矩阵的数学特性,判断是否需要进行镜像修正。它能兼容多种PDF引擎(包括Skia、OpenPDF等)产生的不同矩阵配置。

变换矩阵结构

PDF使用3×3齐次变换矩阵,其中前6个元素(a, b, c, d, e, f)分别对应:

复制代码
[ a  b  0 ]   [ a  b  0 ]
[ c  d  0 ] = [ c  d  0 ]
[ e  f  1 ]   [ e  f  1 ]
  • a (0,0):X轴缩放因子
  • b (0,1):Y轴倾斜因子
  • c (1,0):X轴倾斜因子
  • d (1,1):Y轴缩放因子
  • e (2,0):X轴平移量
  • f (2,1):Y轴平移量
精度控制
java 复制代码
private static final float EPS = 1e-3f;

使用EPS阈值处理浮点数精度问题,避免因数值误差导致的误判。

详细检测逻辑

情况1:标准Y轴翻转检测

java 复制代码
boolean skiaFlip = Math.abs(b) < EPS && Math.abs(c) < EPS &&
        d < -EPS && Math.abs(a + d) < Math.max(Math.abs(a), Math.abs(d)) * 0.1f;

数学原理:

  • Math.abs(b) < EPS && Math.abs(c) < EPS:确认无倾斜变换,仅包含缩放和平移
  • d < -EPS:Y轴缩放为负值,表示Y轴翻转
  • Math.abs(a + d) < Math.max(Math.abs(a), Math.abs(d)) * 0.1f:X轴缩放与Y轴缩放绝对值近似相等

适用场景:

  • Skia引擎的Y轴翻转
  • OpenPDF某些版本的垂直镜像
  • 其他具有Y轴翻转特性的PDF库

矩阵示例:

复制代码
[ 1.0   0.0   0 ]
[ 0.0  -1.0   0 ]
[ 0.0   0.0   1 ]

情况2:水平镜像翻转检测

java 复制代码
boolean horizontalFlip = Math.abs(b) < EPS && Math.abs(c) < EPS &&
        a < -EPS && Math.abs(a - d) < Math.max(Math.abs(a), Math.abs(d)) * 0.1f;

数学原理:

  • 无倾斜变换约束同情况1
  • a < -EPS:X轴缩放为负值,表示X轴翻转
  • Math.abs(a - d) < Math.max(Math.abs(a), Math.abs(d)) * 0.1f:X轴与Y轴缩放因子绝对值近似相等

适用场景:

  • OpenPDF的水平镜像问题
  • 其他产生水平翻转的PDF引擎
  • 某些坐标系转换导致的水平镜像

矩阵示例:

复制代码
[ -1.0   0.0   0 ]
[  0.0   1.0   0 ]
[  0.0   0.0   1 ]

情况3:问题矩阵通用检测

java 复制代码
boolean problematicMatrix = Math.abs(a + d) < EPS ||
        (a < -EPS && d > EPS) ||
        (Math.abs(a - d) > 2.0f && Math.abs(b) < EPS && Math.abs(c) < EPS);

子情况3.1:奇异矩阵检测

java 复制代码
Math.abs(a + d) < EPS

数学原理: a + d ≈ 0 意味着矩阵行列式可能接近零,属于奇异矩阵,会导致渲染异常。

子情况3.2:不对称缩放检测

java 复制代码
(a < -EPS && d > EPS)

数学原理: X轴负缩放但Y轴正缩放,这种不对称配置通常会导致文字显示异常。

子情况3.3:过度缩放检测

java 复制代码
(Math.abs(a - d) > 2.0f && Math.abs(b) < EPS && Math.abs(c) < EPS)

数学原理: 在无倾斜变换的情况下,X轴和Y轴缩放差异过大(超过2倍),可能导致渲染变形。

适用场景:

  • 各种PDF引擎的异常矩阵输出
  • PDF文档损坏或格式错误
  • 复杂变换操作导致的矩阵异常
  • 第三方PDF库的兼容性问题
容错机制

该方法通过多层次的检测逻辑实现高容错性:

  1. 渐进式检测:从特定类型到通用问题,逐步扩大检测范围
  2. 相对比较:使用相对比例而非绝对值,适应不同尺寸的页面
  3. 阈值控制 :通过EPS和比例系数控制误判率
  4. 逻辑或运算:任一条件满足即触发修正,确保异常不遗漏
兼容性覆盖

该检测方法能够识别和修正以下情况:

PDF引擎/场景 检测情况 修正策略
Skia引擎Y轴翻转 情况1 坐标系修正
OpenPDF水平镜像 情况2 镜像翻转修正
矩阵奇异或异常 情况3.1 通用修正处理
不对称缩放 情况3.2 通用修正处理
过度缩放变形 情况3.3 通用修正处理
其他未知矩阵 任意情况 通用修正处理
性能考虑
  • 计算复杂度:O(1) 常数时间复杂度
  • 内存使用:仅使用几个局部变量
  • 适用性:适用于批量处理大量PDF文件
  • 准确性:通过多重条件确保高准确率

3. 事件监听机制

EnhancedMirrorListener类监听三类渲染事件:

事件类型 数据源 检测矩阵 说明
RENDER_PATH PathRenderInfo getCtm() 路径绘制的当前变换矩阵
RENDER_IMAGE ImageRenderInfo getImageCtm() 图像绘制的变换矩阵
RENDER_TEXT TextRenderInfo getTextMatrix() 文字绘制的变换矩阵

兼容处理策略

1. Skia格式处理

java 复制代码
public static void addWatermark2SKiaFormat(PdfDocument pdfDoc, Paragraph watermark, float xSpacing, float ySpacing)

处理特点:

  • 应用坐标系修正变换
  • 使用(100, 100)的平移补偿
  • 调整水印起始位置确保完整覆盖
  • 保持原始水印属性(字体、大小、颜色、透明度)

2. 坐标系修正变换

java 复制代码
private static AffineTransform createCoordinateTransform() {
    AffineTransform at = new AffineTransform();
    at.translate(100, 100);
    return at;
}

设计考虑:

  • 仅进行平移变换,不影响缩放
  • 补偿页面坐标系偏移
  • 避免对水印文字大小的二次缩放

3. 水印位置计算

X轴起始位置:

java 复制代码
private static float calculateWatermarkXStart(float xSpacing) {
    return -xSpacing - 100; // 补偿平移影响
}

Y轴起始位置:

java 复制代码
private static float calculateWatermarkYStart(float ySpacing, float pageHeight) {
    return pageHeight + ySpacing - 100; // 补偿平移影响
}

4. 普通格式处理

对于needsMirrorCorrection方法返回false的标准PDF,使用简化处理逻辑:

  • 直接在原始坐标系中绘制水印
  • 使用固定的起始位置偏移
  • 保持45度旋转角度
  • 无需应用坐标变换修正

关键参数

参数名 说明
EPS 1e-3f 浮点比较阈值,防止数值误差
xSpacing 125f 水印横向间距
ySpacing 150f 水印纵向间距
平移量 (100, 100) 坐标系修正补偿量
比例系数 0.1f 用于检测近似相等的缩放因子
缩放差异阈值 2.0f 检测过度缩放的差异阈值

needsMirrorCorrection方法核心流程

java 复制代码
private static boolean needsMirrorCorrection(Matrix m) {
    if (m == null) return false;

    // 提取矩阵关键元素
    float a = m.get(0);   // X轴缩放
    float b = m.get(1);   // Y轴倾斜
    float c = m.get(3);   // X轴倾斜
    float d = m.get(4);   // Y轴缩放

    // 三种检测情况的逻辑或运算
    boolean skiaFlip = /* 情况1逻辑 */;
    boolean horizontalFlip = /* 情况2逻辑 */;
    boolean problematicMatrix = /* 情况3逻辑 */;

    return skiaFlip || horizontalFlip || problematicMatrix;
}

使用流程

  1. 检测阶段 :调用pageFlip()检测首页是否需要修正
  2. 矩阵分析pageFlip()内部调用needsMirrorCorrection()对每个检测到的矩阵进行分析
  3. 分支处理
    • 需要修正:调用addWatermark2SKiaFormat()
    • 无需修正:调用addWatermark2NormalFormat()
  4. 统一输出:返回添加水印后的PDF流

矩阵检测的实际应用

检测触发条件

当PDF页面中包含以下任一情况时,needsMirrorCorrection将返回true

  1. Skia引擎生成的PDF:常见的Y轴翻转矩阵
  2. OpenPDF特定版本:可能产生水平镜像
  3. 复杂变换操作:多层嵌套的矩阵变换
  4. PDF文档异常:损坏或不规范的矩阵定义

修正策略选择

根据needsMirrorCorrection的返回值选择不同的水印添加策略:

  • 返回true :使用addWatermark2SKiaFormat()进行坐标系修正
  • 返回false :使用addWatermark2NormalFormat()标准处理

调试支持

工具类提供了详细的调试输出,特别针对needsMirrorCorrection方法:

java 复制代码
// 调试:打印变换矩阵信息
private static void logTransformMatrix(AffineTransform at, String description)

// 记录检测结果
if (listener.needsCorrection) {
    System.out.printf("检测到需要镜像修正的页面,扫描到 %d 个矩阵%n", listener.matrixCount);
}

生产环境可通过注释相关调试代码关闭输出。

注意事项

  1. 性能考虑:仅检测首页矩阵作为判断依据,避免全文档扫描的性能开销
  2. 容错机制 :使用EPS阈值和相对比例处理浮点数精度问题
  3. 向后兼容:确保对标准PDF的正常处理不受影响
  4. 内存管理 :及时关闭PdfDocument和相关流对象
  5. 矩阵范围:该方法专注于检测缩放和倾斜相关的变换,不涉及平移量的分析

完整代码

java 复制代码
import com.itextpdf.kernel.font.PdfFont;
import com.itextpdf.kernel.font.PdfFontFactory;
import com.itextpdf.kernel.geom.AffineTransform;
import com.itextpdf.kernel.geom.Matrix;
import com.itextpdf.kernel.pdf.*;
import com.itextpdf.kernel.pdf.canvas.PdfCanvas;
import com.itextpdf.kernel.colors.DeviceRgb;
import com.itextpdf.kernel.pdf.canvas.parser.EventType;
import com.itextpdf.kernel.pdf.canvas.parser.PdfCanvasProcessor;
import com.itextpdf.kernel.pdf.canvas.parser.data.IEventData;
import com.itextpdf.kernel.pdf.canvas.parser.data.ImageRenderInfo;
import com.itextpdf.kernel.pdf.canvas.parser.data.PathRenderInfo;
import com.itextpdf.kernel.pdf.canvas.parser.data.TextRenderInfo;
import com.itextpdf.kernel.pdf.canvas.parser.listener.IEventListener;
import com.itextpdf.layout.Canvas;
import com.itextpdf.layout.element.Paragraph;
import com.itextpdf.layout.properties.TextAlignment;
import com.itextpdf.kernel.geom.Rectangle;
import com.itextpdf.layout.properties.VerticalAlignment;

import java.io.*;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

/**
 * @ClassName: FileWatermarkUtils
 * @Description: 文件水印工具类
 * @Author: Maxon Phong
 * @Date: 2025/10/10 9:50
 **/
public class FileWatermarkUtils {


    /**
     * PDF加水印
     *
     * @param file          文件
     * @param watermarkText 水印文本
     * @return 添加水印后的PDF文件
     */
    public static InputStream addWatermark2PDF(File file, String watermarkText) {
        try {
            return addWatermark2PDF(new FileInputStream(file), watermarkText);
        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * PDF加水印
     *
     * @param inputStream   文件流
     * @param watermarkText 水印文本
     * @return 添加水印后的PDF文件流
     */
    public static InputStream addWatermark2PDF(InputStream inputStream, String watermarkText) {
        if (null == inputStream) {
            throw new RuntimeException("输入文件不能为空");
        }
        if (null == watermarkText) {
            return inputStream;
//            watermarkText = StringUtils.EMPTY;
        }
        try {
            // ------------------- 1. 基础文档准备 -------------------
            ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
            PdfReader reader = new PdfReader(inputStream);
            PdfWriter writer = new PdfWriter(outputStream);
            PdfDocument pdfDoc = new PdfDocument(reader, writer);

            // 中文字体(STSong-Light)支持中文水印
            PdfFont font = PdfFontFactory.createFont("STSong-Light", "UniGB-UCS2-H");

            // ------------------- 2. 水印样式(Paragraph) -------------------
            Paragraph paragraph = new Paragraph(watermarkText)
                    .setFont(font)
                    .setFontSize(15)                     // 这里写你想要的字号
                    .setFontColor(new DeviceRgb(200, 200, 200))
                    .setOpacity(0.5f);                    // 透明度

            // 水印平铺的间距(可自行调节)
            final float xSpacing = 125f;
            final float ySpacing = 150f;

            // 判断文件格式 - 使用增强的镜像检测
            // 首页判断
            if (pageFlip(pdfDoc.getPage(1))) {
                addWatermark2SKiaFormat(pdfDoc, paragraph, xSpacing, ySpacing);
            } else {
                addWatermark2NormalFormat(pdfDoc, paragraph, xSpacing, ySpacing);
            }


            pdfDoc.close();
            // 返回添加水印后的PDF流
            return new java.io.ByteArrayInputStream(outputStream.toByteArray());
        } catch (Exception e) {
            throw new RuntimeException("添加水印失败: " + e.getMessage(), e);
        }
    }

    /**
     * PDF加水印 -- Skia 格式(增强版,自动修正镜像问题)
     *
     * @param pdfDoc    pdf文档
     * @param watermark 水印
     * @param xSpacing  水印横向间距
     * @param ySpacing  水印纵向间距
     */
    public static void addWatermark2SKiaFormat(PdfDocument pdfDoc, Paragraph watermark, float xSpacing, float ySpacing) {
        //  ------------------- 3. 遍历每页并绘制 -------------------
        int numberOfPages = pdfDoc.getNumberOfPages();
        for (int i = 1; i <= numberOfPages; i++) {
            PdfPage page = pdfDoc.getPage(i);
            Rectangle pageSize = page.getPageSize();

            // ① 在页面最上层(相当于 getOverContent)创建 PdfCanvas
            PdfCanvas pdfCanvas = new PdfCanvas(page.newContentStreamAfter(),
                    page.getResources(), pdfDoc);

            // ② 保存当前图形状态,后面恢复,防止影响其它页面
            pdfCanvas.saveState();


            // ④ 智能坐标系修正 - 分两步处理,避免影响字体大小
            // 步骤1:只对坐标变换进行修正,不对缩放操作
            AffineTransform coordinateTransform = createCoordinateTransform();

            // 调试:打印变换矩阵信息
//            logTransformMatrix(coordinateTransform, "Skia坐标系修正");

            pdfCanvas.concatMatrix(coordinateTransform);

            // ⑤ 使用高层 Canvas 绘制文字
            Canvas waterMarkCanvas = new Canvas(pdfCanvas, pageSize);

            // 计算起始坐标,确保覆盖整个页面
            float xStart = calculateWatermarkXStart(xSpacing);
            float yStart = calculateWatermarkYStart(ySpacing, pageSize.getHeight());

            // 绘制水印网格 - 确保覆盖整个页面(考虑100像素的平移)
            // Y轴循环范围:从页面顶部+100 到 页面底部+100
            float yEnd = -100; // 考虑平移的结束位置
            for (float y = yStart; y > yEnd; y -= ySpacing) {
                // X轴循环范围:从-100-间距 到 页面宽度+100+间距
                float xEnd = pageSize.getWidth() + 100 + xSpacing;
                for (float x = xStart; x < xEnd; x += xSpacing) {
                    waterMarkCanvas.showTextAligned(watermark,
                            x, y, i,
                            TextAlignment.CENTER,
                            VerticalAlignment.MIDDLE,
                            (float) Math.toRadians(45));   // 45° 旋转
                }
            }

            waterMarkCanvas.close();   // 结束 Canvas 使用
            pdfCanvas.restoreState(); // 恢复图形状态,防止影响后续页面
        }
    }

    /**
     * 创建坐标系修正变换矩阵(只处理坐标,不影响缩放)
     */
    private static AffineTransform createCoordinateTransform() {
        AffineTransform at = new AffineTransform();
        at.translate(100, 100);
        return at;
    }


    /**
     * 计算水印X轴起始位置
     */
    private static float calculateWatermarkXStart(float xSpacing) {
        // 从页面左侧开始,确保完整覆盖,考虑(100,100)平移
        return -xSpacing - 100; // 补偿平移影响
    }

    /**
     * 计算水印Y轴起始位置
     */
    private static float calculateWatermarkYStart(float ySpacing, float pageHeight) {
        // 考虑我们有(100, 100)的平移,调整起始位置
        // 从页面顶部开始,减去平移量
        return pageHeight + ySpacing - 100; // 补偿平移影响
    }

    /**
     * 记录水印调试信息
     */
    private static void logWatermarkInfo(int pageNum, Rectangle pageSize, float userUnit, float xStart, float yStart) {
        // 生产环境可以注释掉这些调试信息
        System.out.printf(
                "页面 %d 水印信息: 尺寸=%.1fx%.1f, UserUnit=%.2f, 起始=(%.1f,%.1f)%n",
                pageNum, pageSize.getWidth(), pageSize.getHeight(), userUnit, xStart, yStart
        );
    }

    /**
     * 打印变换矩阵信息(调试用)
     */
    private static void logTransformMatrix(AffineTransform at, String description) {
        System.out.printf(
                "%s - 变换矩阵: [%f, %f, %f, %f, %f, %f]%n",
                description,
                at.getScaleX(), at.getShearY(), at.getShearX(), at.getScaleY(),
                at.getTranslateX(), at.getTranslateY()
        );
    }


    /**
     * PDF加水印 -- 普通格式
     *
     * @param pdfDoc    pdf文档
     * @param watermark 水印文本
     * @param xSpacing  水印横向间距
     * @param ySpacing  水印纵向间距
     */
    public static void addWatermark2NormalFormat(PdfDocument pdfDoc, Paragraph watermark, float xSpacing, float ySpacing) {
        // 遍历每一页添加水印
        int numberOfPages = pdfDoc.getNumberOfPages();
        for (int i = 1; i <= numberOfPages; i++) {
            PdfPage page = pdfDoc.getPage(i);
            Rectangle pageSize = page.getPageSize();

            PdfCanvas pdfCanvas = new PdfCanvas(page.newContentStreamAfter(), page.getResources(), pdfDoc);

            // 计算水印的位置,实现平铺效果
            float xStart = -75;   // 起始X位置
            float yStart = pageSize.getHeight() - 130; // 起始Y位置

            // 在整个页面上循环添加水印
            Canvas waterMarkCanvas = new Canvas(pdfCanvas, pageSize);
            for (float y = yStart; y > 0; y -= ySpacing) {
                for (float x = xStart; x < pageSize.getWidth() + 300; x += xSpacing) {
                    waterMarkCanvas.showTextAligned(watermark, x, y, i,
                            TextAlignment.CENTER,
                            VerticalAlignment.MIDDLE,
                            (float) Math.toRadians(45)
                    );
                }
            }

            waterMarkCanvas.close();
        }
    }

    /**
     * 判定阈值,防止浮点误差
     */
    private static final float EPS = 1e-3f;  // 放宽阈值,提高容错性

    /**
     * 判断是否需要镜像修正(包括Skia翻转和其它坐标系问题)
     */
    private static boolean needsMirrorCorrection(Matrix m) {
        if (m == null) return false;

        // 3×3 矩阵的前 6 项对应 a、b、c、d、e、f(行主序)
        float a = m.get(0);   // 第 1 行第 1 列 (X缩放)
        float b = m.get(1);   // 第 1 行第 2 列 (Y倾斜)
        float c = m.get(3);   // 第 2 行第 1 列 (X倾斜)
        float d = m.get(4);   // 第 2 行第 2 列 (Y缩放)

        // 情况1:标准Skia Y轴翻转(a > 0, d < 0, 且 |a| ≈ |d|)
        boolean skiaFlip = Math.abs(b) < EPS && Math.abs(c) < EPS &&
                d < -EPS && Math.abs(a + d) < Math.max(Math.abs(a), Math.abs(d)) * 0.1f;

        // 情况2:水平镜像翻转(a < 0, d > 0, 且 |a| ≈ |d|)
        boolean horizontalFlip = Math.abs(b) < EPS && Math.abs(c) < EPS &&
                a < -EPS && Math.abs(a - d) < Math.max(Math.abs(a), Math.abs(d)) * 0.1f;

        // 情况3:检测其它可能导致文字镜像的矩阵问题
        boolean problematicMatrix = Math.abs(a + d) < EPS ||
                (a < -EPS && d > EPS) ||
                (Math.abs(a - d) > 2.0f && Math.abs(b) < EPS && Math.abs(c) < EPS);

        return skiaFlip || horizontalFlip || problematicMatrix;
    }


    /**
     * 解析单页内容,返回是否检测到需要镜像修正的格式(增强版)
     */
    private static boolean pageFlip(PdfPage page) {
        // 读取页面全部内容流(iText 会自动合并多个流)
        byte[] contentBytes = page.getContentBytes();
        if (contentBytes == null) {
            return false; // 空页
        }

        // 增强的镜像检测 Listener
        class EnhancedMirrorListener implements IEventListener {
            boolean needsCorrection = false;
            int matrixCount = 0;

            @Override
            public void eventOccurred(IEventData data, EventType type) {
                // 检测所有可能包含变换矩阵的事件
                if (type == EventType.RENDER_PATH) {
                    PathRenderInfo pri = (PathRenderInfo) data;
                    if (needsMirrorCorrection(pri.getCtm())) {
                        needsCorrection = true;
                    }
                    matrixCount++;
                } else if (type == EventType.RENDER_IMAGE) {
                    ImageRenderInfo iri = (ImageRenderInfo) data;
                    if (needsMirrorCorrection(iri.getImageCtm())) {
                        needsCorrection = true;
                    }
                    matrixCount++;
                } else if (type == EventType.RENDER_TEXT) {
                    TextRenderInfo tri = (TextRenderInfo) data;
                    if (needsMirrorCorrection(tri.getTextMatrix())) {
                        needsCorrection = true;
                    }
                    matrixCount++;
                }
            }

            @Override
            public Set<EventType> getSupportedEvents() {
                return new HashSet<>(Arrays.asList(
                        EventType.RENDER_PATH,
                        EventType.RENDER_IMAGE,
                        EventType.RENDER_TEXT));
            }
        }

        EnhancedMirrorListener listener = new EnhancedMirrorListener();
        PdfCanvasProcessor processor = new PdfCanvasProcessor(listener);
        processor.processContent(contentBytes, page.getResources());

        // 记录检测结果(调试用)
        if (listener.needsCorrection) {
            System.out.printf("检测到需要镜像修正的页面,扫描到 %d 个矩阵%n", listener.matrixCount);
        }

        return listener.needsCorrection;
    }
   
}
相关推荐
我命由我123451 小时前
Java NIO 编程 - NIO Echo Server、NIO Client(NIO 异步客户端、NIO Selector 异步客户端)
java·开发语言·网络·java-ee·intellij-idea·intellij idea·nio
拓端研究室3 小时前
专题:2025AI产业全景洞察报告:企业应用、技术突破与市场机遇|附920+份报告PDF、数据、可视化模板汇总下载
大数据·人工智能·pdf
南风微微吹3 小时前
2026年新大纲普通话考试真题题库50套PDF电子版
pdf·普通话
断剑zou天涯4 小时前
【算法笔记】窗口内最大值或最小值的更新结构
java·笔记·算法
m***66735 小时前
SQL 实战—递归 SQL:层级结构查询与处理树形数据
java·数据库·sql
鲸沉梦落6 小时前
Java中的Stream
java
yihuiComeOn7 小时前
[源码系列:手写Spring] AOP第二节:JDK动态代理 - 当AOP遇见动态代理的浪漫邂逅
java·后端·spring
Porunarufu8 小时前
Java·关于List
java·开发语言
靠沿8 小时前
Java数据结构初阶——Collection、List的介绍与ArrayList
java·数据结构·list