关于加载水印PDF、图片以及压缩包格式文件【把博客当工作记录】

写这篇文章的目的是让大家都可以学到东西,核心代码中列出了处理思维和调用方法,业务代码已经过滤掉了,希望大家不要做crud程序员!!要思考。。。该博客不懂时可联系下方。

1、流程图如下

2、策略描述

实现方式:

设计模式:父策略调动子策略

业务理念:在不影响原有业务的前提下增加优化

报错机制:当加载logo时报错,直接返回原有文件地址

业务分支:

1、pdf策略:

使用PDDocument处理pdf,主要流程如下

①生成下角PDF(自适应源文件大小)

②拉取源文件到本地

③转换PDF到JPG并合并文件(pdf直接合并会自动分页)

④将转换后的文件重新生成PDF

2、图片策略:

①生成下角PDF(自适应源文件大小)

②拉取源文件到本地

③转换pdf文件成图片并同源文件合并

3、压缩包策略:

根据压缩包的格式走不通的解压 压缩方式

①拉取压缩包到本地

②生成下角PDF

③解压压缩包并将生成的PDF放入

④压缩文件目录

剩余问题(优化性问题):

1、效率问题: 第一次下载文件时,因未处理过需要走策略模式,因处理步骤较多可能会很慢。

2、自适应问题:PDF自适应时会将清晰度降低,目前不走自适应

3、核心代码模块

一、图片格式处理

①生成左小角文件

java 复制代码
public void createJpgWithSize(String filePath, float width, float height, Contact contact) {
        try (PDDocument document = new PDDocument();
             FileOutputStream fio = new FileOutputStream(new File(filePath))) {
            // 创建具有指定大小的页面
            PDPage page = new PDPage(new PDRectangle(width, height));
            try (PDPageContentStream contentStream = new PDPageContentStream(document, page)) {
                // 设置字体和颜色
                File fontFile = new File("/data/config/easybii/simhei.ttf");
                PDType0Font font = PDType0Font.load(document, fontFile);
                contentStream.setFont(font, 22);
                contentStream.setNonStrokingColor(Color.black);

                String phone = " ";
                if (StringUtils.isNotBlank(contact.getTelephone())) {
                    phone += contact.getTelephone();
                }
                if (StringUtils.isNotBlank(contact.getTelephone1())) {
                    phone += "  " + contact.getTelephone();
                }
                // 写入文字
                contentStream.beginText();
                contentStream.newLineAtOffset(20, 15);
                contentStream.showText("报价联系人: " + contact.getName() + phone);
                contentStream.endText();
            }
            // 将页面添加到文档中
            document.addPage(page);
            // 保存文档
            document.save(fio);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

②将pdf文件转化为jpg

java 复制代码
  public void convertPDDocumentToImage(String pdfPath, String outputImagePath) {
        try (PDDocument document = PDDocument.load(new File(pdfPath))) {
            PDFRenderer renderer = new PDFRenderer(document);
            BufferedImage image = renderer.renderImageWithDPI(0, 300); // 0 表示第一页,300 是 DPI

            javax.imageio.ImageIO.write(image, "jpg", new File(outputImagePath));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

③压缩尺寸

java 复制代码
 /**
     * 压缩尺寸
     *
     * @param sourceImagePath
     * @param targetImagePath
     * @param targetWidth
     * @param targetHeight
     */
    public void resizeJpg(String sourceImagePath, String targetImagePath, float targetWidth, float targetHeight) {
        try {
            BufferedImage sourceImage = ImageIO.read(new File(sourceImagePath));

            float sourceWidth = sourceImage.getWidth();
            float sourceHeight = sourceImage.getHeight();

            double scaleX = (double) targetWidth / sourceWidth;
            double scaleY = (double) targetHeight / sourceHeight;
            double scale = Math.min(scaleX, scaleY);

            int newWidth = (int) (sourceWidth * scale);
            int newHeight = (int) (sourceHeight * scale);

            BufferedImage resizedImage = new BufferedImage(newWidth, newHeight, BufferedImage.TYPE_INT_RGB);

            Graphics2D graphics2D = resizedImage.createGraphics();
            graphics2D.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
            graphics2D.drawImage(sourceImage, 0, 0, newWidth, newHeight, null);
            graphics2D.dispose();

            ImageIO.write(resizedImage, "jpg", new FileOutputStream(new File(targetImagePath)));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

④合并俩个图片

java 复制代码
/**
     * 将俩张图片合并为一张图片
     *
     * @param image1Path
     * @param image2Path
     * @param mergedImagePath
     */
    public String mergeJpgImages(String image1Path, String image2Path, String mergedImagePath) {
        try {
            ImageInputStream input1 = ImageIO.createImageInputStream(new File(image1Path));
            ImageReader reader1 = ImageIO.getImageReaders(input1).next();
            reader1.setInput(input1);
            BufferedImage image1 = reader1.read(0);

            ImageInputStream input2 = ImageIO.createImageInputStream(new File(image2Path));
            ImageReader reader2 = ImageIO.getImageReaders(input2).next();
            reader2.setInput(input2);
            BufferedImage image2 = reader2.read(0);

            int width = Math.max(image1.getWidth(), image2.getWidth());
            int height = image1.getHeight() + image2.getHeight();

            BufferedImage mergedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);

            // 绘制第一张图片
            mergedImage.getGraphics().drawImage(image1, 0, 0, null);

            // 绘制第二张图片在第一张图片下方
            mergedImage.getGraphics().drawImage(image2, 0, image1.getHeight(), null);

            ImageIO.write(mergedImage, "jpg", new File(mergedImagePath));
        } catch (IOException e) {
            e.printStackTrace();
        }
        return mergedImagePath;
    }

⑤设置水印

java 复制代码
    public void addWatermark(String imagePath, String outputPath, String watermarkText) {
        try (FileOutputStream fio = new FileOutputStream(outputPath)) {
            BufferedImage image = ImageIO.read(new File(imagePath));
            Graphics2D g2d = image.createGraphics();
            // 设置字体
            Font font = new Font("Arial", Font.PLAIN, 50);
            g2d.setFont(font);
            // 设置颜色和透明度
            Color color = new Color(12, 12, 12, 45);
            g2d.setColor(color);
            int imageWidth = image.getWidth();
            int imageHeight = image.getHeight();
            // 计算每行和每列水印文字的数量
            int numRows = imageHeight / 50;
            int numCols = imageWidth / 100;
// 区分横版和竖版
            if (imageWidth > imageHeight) {
                // 横版
                // 右上角宽度 10%、高度 10%的位置
                int xRightTop = (int) (imageWidth * 0.8);
                int yRightTop = (int) (imageHeight * 0.2);
                AffineTransform transform = new AffineTransform();
                transform.rotate(Math.toRadians(-30), xRightTop, yRightTop);  // 应用倾斜变换
                g2d.setTransform(transform);
                g2d.drawString(watermarkText, xRightTop, yRightTop);

                // 左下角宽度 10%、高度 90%的位置
                int xLeftBottom = (int) (imageWidth * 0.1);
                int yLeftBottom = (int) (imageHeight * 0.9);
                AffineTransform transform2 = new AffineTransform();
                transform2.rotate(Math.toRadians(-30), xLeftBottom, yLeftBottom);  // 应用倾斜变换
                g2d.setTransform(transform2);
                g2d.drawString(watermarkText, xLeftBottom, yLeftBottom);

                // 中间
                int xMiddle = imageWidth / 2 - watermarkText.length() * 5;
                int yMiddle = imageHeight / 2;
                AffineTransform transform3 = new AffineTransform();
                transform3.rotate(Math.toRadians(-30), xMiddle, yMiddle);  // 应用倾斜变换
                g2d.setTransform(transform3);
                g2d.drawString(watermarkText, xMiddle, yMiddle);

            } else {
                // 竖版
                // 右上角宽度 10%、高度 10%的位置
                int xRightTop = (int) (imageWidth * 0.80);
                int yRightTop = (int) (imageHeight * 0.1);
                AffineTransform transform= new AffineTransform();
                transform.rotate(Math.toRadians(-30), xRightTop, yRightTop);  // 应用倾斜变换
                g2d.setTransform(transform);
                g2d.drawString(watermarkText, xRightTop, yRightTop);

                // 左下角宽度 90%、高度 90%的位置
                int xLeftBottom = (int) (imageWidth * 0.1);
                int yLeftBottom = (int) (imageHeight * 0.9);
                AffineTransform transform2 = new AffineTransform();
                transform2.rotate(Math.toRadians(-30), xLeftBottom, yLeftBottom);  // 应用倾斜变换
                g2d.setTransform(transform2);
                g2d.drawString(watermarkText, xLeftBottom, yLeftBottom);

                // 中间
                int xMiddle = imageWidth / 2 - watermarkText.length() * 5;
                int yMiddle = imageHeight / 2;
                AffineTransform transform3 = new AffineTransform();
                transform3.rotate(Math.toRadians(-30), xMiddle, yMiddle);  // 应用倾斜变换
                g2d.setTransform(transform3);
                g2d.drawString(watermarkText, xMiddle, yMiddle);
            }


            g2d.dispose();
            // 输出图片
            ImageIO.write(image, "jpg", fio);
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
二、PDF格式

①生成pdf文件同上

②PDF转图片同上

③转化原有PDF成图片并合并

java 复制代码
  /**
     * 将pdf文件转化成jpg
     *
     * @param pdf1Path
     * @param pdf2Path
     */
    public List<String> mergePDFImages(String pdf1Path, String jpgIndex,PDRectangle mediaBox) {
        try {
            checkAndCreateFolder("oldJpg");
            checkAndCreateFolder("newJpg");
            List<String> oldJpg = convertPDFToJPG(pdf1Path, "oldJpg");
            String path = "mergedImage" + System.currentTimeMillis();
            List<String> jpgLists = new ArrayList<>();
            int i = 0;
            for (String jpgNew : oldJpg) {
                mergeJpgImages(jpgNew, jpgIndex, path + i + ".jpg");
                //设置水印
                this.addWatermark(path + i + ".jpg", path + i + "2.jpg", "easybii");
                jpgLists.add(path + i + "2.jpg");
                i++;
            }
            fileWaterCommon.deleteSpecifiedFileInFolder("oldJpg", oldJpg);
            return jpgLists;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return new ArrayList<>();
    }
    public void checkAndCreateFolder(String folderPath) {
        File folder = new File(folderPath);
        if (!folder.exists()) {
            if (folder.mkdirs()) {
                log.info("文件夹创建成功!");
            } else {
                log.info("文件夹创建失败!");
            }
        } else {
            log.info("文件夹已存在!");
        }
    }

   /**
     * 将 PDF 文件的每一页转换为 JPG 图片
     *
     * @param pdfFilePath  PDF 文件的路径
     * @param dstImgFolder 图片保存的文件夹路径
     * @return 转换后的图片文件路径列表
     */
    public List<String> convertPDFToJPG(String pdfFilePath, String dstImgFolder) {

        List<String> imagePaths = new ArrayList<>();
        try (PDDocument document = PDDocument.load(new File(pdfFilePath))) {
            PDFRenderer renderer = new PDFRenderer(document);
            int pageCount = document.getNumberOfPages();
            for (int i = 0; i < pageCount; i++) {
                String imagePath = dstImgFolder + File.separator + System.currentTimeMillis() + (i + 1) + ".jpg";
                BufferedImage image = renderer.renderImageWithDPI(i, 300);
                ImageIO.write(image, "jpg", new FileOutputStream(imagePath));
                imagePaths.add(imagePath);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return imagePaths;
    }
/**
     * 将俩张图片合并为一张图片
     *
     * @param image1Path
     * @param image2Path
     * @param mergedImagePath
     */
    public String mergeJpgImages(String image1Path, String image2Path, String mergedImagePath) {
        try {
            ImageInputStream input1 = ImageIO.createImageInputStream(new File(image1Path));
            ImageReader reader1 = ImageIO.getImageReaders(input1).next();
            reader1.setInput(input1);
            BufferedImage image1 = reader1.read(0);

            ImageInputStream input2 = ImageIO.createImageInputStream(new File(image2Path));
            ImageReader reader2 = ImageIO.getImageReaders(input2).next();
            reader2.setInput(input2);
            BufferedImage image2 = reader2.read(0);

            int width = Math.max(image1.getWidth(), image2.getWidth());
            int height = image1.getHeight() + image2.getHeight();

            BufferedImage mergedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);

            // 绘制第一张图片
            mergedImage.getGraphics().drawImage(image1, 0, 0, null);

            // 绘制第二张图片在第一张图片下方
            mergedImage.getGraphics().drawImage(image2, 0, image1.getHeight(), null);

            ImageIO.write(mergedImage, "jpg", new File(mergedImagePath));
        } catch (IOException e) {
            e.printStackTrace();
        }
        return mergedImagePath;
    }


    public void addWatermark(String imagePath, String outputPath, String watermarkText) {
        try (FileOutputStream fio = new FileOutputStream(outputPath)) {
            BufferedImage image = ImageIO.read(new File(imagePath));
            Graphics2D g2d = image.createGraphics();
            // 设置字体
            Font font = new Font("Arial", Font.PLAIN, 50);
            g2d.setFont(font);
            // 设置颜色和透明度
            Color color = new Color(12, 12, 12, 45);
            g2d.setColor(color);
            int imageWidth = image.getWidth();
            int imageHeight = image.getHeight();
            // 区分横版和竖版
            if (imageWidth > imageHeight) {
                // 横版
                // 右上角宽度 10%、高度 10%的位置
                int xRightTop = (int) (imageWidth * 0.8);
                int yRightTop = (int) (imageHeight * 0.2);
                AffineTransform transform = new AffineTransform();
                transform.rotate(Math.toRadians(-30), xRightTop, yRightTop);  // 应用倾斜变换
                g2d.setTransform(transform);
                g2d.drawString(watermarkText, xRightTop, yRightTop);

                // 左下角宽度 10%、高度 90%的位置
                int xLeftBottom = (int) (imageWidth * 0.1);
                int yLeftBottom = (int) (imageHeight * 0.9);
                AffineTransform transform2 = new AffineTransform();
                transform2.rotate(Math.toRadians(-30), xLeftBottom, yLeftBottom);  // 应用倾斜变换
                g2d.setTransform(transform2);
                g2d.drawString(watermarkText, xLeftBottom, yLeftBottom);

                // 中间
                int xMiddle = imageWidth / 2 - watermarkText.length() * 5;
                int yMiddle = imageHeight / 2;
                AffineTransform transform3 = new AffineTransform();
                transform3.rotate(Math.toRadians(-30), xMiddle, yMiddle);  // 应用倾斜变换
                g2d.setTransform(transform3);
                g2d.drawString(watermarkText, xMiddle, yMiddle);

            } else {
                // 竖版
                // 右上角宽度 10%、高度 10%的位置
                int xRightTop = (int) (imageWidth * 0.80);
                int yRightTop = (int) (imageHeight * 0.1);
                AffineTransform transform= new AffineTransform();
                transform.rotate(Math.toRadians(-30), xRightTop, yRightTop);  // 应用倾斜变换
                g2d.setTransform(transform);
                g2d.drawString(watermarkText, xRightTop, yRightTop);

                // 左下角宽度 90%、高度 90%的位置
                int xLeftBottom = (int) (imageWidth * 0.1);
                int yLeftBottom = (int) (imageHeight * 0.9);
                AffineTransform transform2 = new AffineTransform();
                transform2.rotate(Math.toRadians(-30), xLeftBottom, yLeftBottom);  // 应用倾斜变换
                g2d.setTransform(transform2);
                g2d.drawString(watermarkText, xLeftBottom, yLeftBottom);

                // 中间
                int xMiddle = imageWidth / 2 - watermarkText.length() * 5;
                int yMiddle = imageHeight / 2;
                AffineTransform transform3 = new AffineTransform();
                transform3.rotate(Math.toRadians(-30), xMiddle, yMiddle);  // 应用倾斜变换
                g2d.setTransform(transform3);
                g2d.drawString(watermarkText, xMiddle, yMiddle);
            }

//
//            // 计算每行和每列水印文字的数量
//            int numRows = imageHeight / 50;
//            int numCols = imageWidth / 100;
//
//            for (int i = 0; i < numRows; i++) {
//                for (int j = 0; j < numCols; j++) {
//                    // 设置文字颜色和透明度
//                    // 计算文字位置
//                    int x = j * 500;
//                    int y = i * 400;
//                    AffineTransform transform = new AffineTransform();
//                    transform.rotate(Math.toRadians(-30), x, y);  // 应用倾斜变换
//                    g2d.setTransform(transform);
//                    // 绘制文字水印
//                    g2d.drawString(watermarkText, x, y);
//                }
//            }

            g2d.dispose();
            // 输出图片
            ImageIO.write(image, "jpg", fio);
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

④将处理好的jpg生成PDF

java 复制代码
    public void mergeJpgToPdf(List<String> jpgPaths, String pdfPath) {
        try (PDDocument document = new PDDocument()) {
            for (String jpgPath : jpgPaths) {
                try {
                    BufferedImage image = fileToBufferedImage(new File(jpgPath));
                    PDPage page = new PDPage(new org.apache.pdfbox.pdmodel.common.PDRectangle(image.getWidth(), image.getHeight()));
                    document.addPage(page);
                    PDPageContentStream contentStream = new PDPageContentStream(document, page);
                    contentStream.drawImage(LosslessFactory.createFromImage(document, image), 0, 0);
                    contentStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            document.save(new FileOutputStream(new File(pdfPath)));
            for (String jpgPath : jpgPaths) {
                deleteFile(jpgPath);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
三、压缩包格式

①处理zip格式

java 复制代码
/**
     * zip格式压缩包处理
     *
     * @param sourceZipPath
     * @param resultPdf
     * @param newFilePath
     * @param destDirPath
     * @param downPdf
     * @throws IOException
     * @throws URISyntaxException
     */
    public void unzipAndRezip(String sourceZipPath, String resultPdf, String newFilePath, String destDirPath, String downPdf) throws IOException, URISyntaxException {
        //下载安装包到本地
        URL urlPath = new URL(sourceZipPath);
        HttpURLConnection connection = (HttpURLConnection) urlPath.openConnection();
        InputStream inputStream = connection.getInputStream();
        OutputStream outputStream = new FileOutputStream(downPdf);
        byte[] buffer = new byte[1024];
        int length;
        while ((length = inputStream.read(buffer)) > 0) {
            outputStream.write(buffer, 0, length);
        }
        // 关闭InputStream
        inputStream.close();
        // 关闭OutputStream
        outputStream.close();

        File destDir = new File(destDirPath);
        if (!destDir.exists()) {
            destDir.mkdir();
        }
        this.unzip(downPdf, destDirPath);
//        this.addNewFileToDirectory(destDirPath, newFilePath);
        this.zipDirectory(destDirPath, resultPdf);
        // 清理临时文件夹
        deleteDirectory(destDir);
    }

 /**
     * 解压压缩包
     *
     * @param zipFilePath
     * @param destDirectory
     * @throws IOException
     */
    public void unzip(String zipFilePath, String destDirectory) throws IOException {
        ZipInputStream in = new ZipInputStream(new FileInputStream(zipFilePath), Charset.forName("GBK"));
        ZipEntry z;
        while ((z = in.getNextEntry()) != null) {
            if (z.isDirectory()) {
                String name = z.getName();
                name = name.substring(0, name.length() - 1);
                File f = new File(destDirectory + File.separator + name);
                if (!f.getParentFile().exists()) {
                    f.getParentFile().mkdirs();
                }
                if (!f.exists()) {
                    f.mkdir();
                }
                System.out.println("mkdir " + destDirectory + File.separator
                        + name);
            } else {
                File f = new File(destDirectory + File.separator
                        + z.getName());
                if (!f.getParentFile().exists()) {
                    f.getParentFile().mkdirs();
                }
                if (!f.exists()) {
                    f.createNewFile();
                }
                FileOutputStream out = new FileOutputStream(f);
                //此处注释部分第一次压缩后文件打不开,后来又可以
//                int b;
//                while ((b = in.read()) != -1) {
//                    out.write(b);
//                }
                //下面部分代码替换了上面注释部分的代码压缩正常
                byte[] bytes = new byte[1024];
                int b = -1;
                while ((b = in.read(bytes)) != -1) {
                    out.write(bytes, 0, b);
                }
                out.close();
            }
        }
        in.close();
    }

  /**
     * 生成压缩包
     *
     * @param sourceDirPath
     * @param zipFilePath
     * @throws IOException
     */
    public void zipDirectory(String sourceDirPath, String zipFilePath) throws IOException {
        try (ZipOutputStream zipOut = new ZipOutputStream(new FileOutputStream(zipFilePath))) {
            Path sourceDir = Paths.get(sourceDirPath);
            Files.walk(sourceDir)
                    .filter(path -> !Files.isDirectory(path))
                    .forEach(path -> {
                        ZipEntry zipEntry = new ZipEntry(sourceDir.relativize(path).toString());
                        try {
                            zipOut.putNextEntry(zipEntry);
                            Files.copy(path, zipOut);
                            zipOut.closeEntry();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    });
        }
    }

②下载rar

java 复制代码
 /**
     * 解压rar
     *
     * @param url         rar
     * @param extractPath 解压路径
     */
    public void unCompress(String url, String extractPath, String downPdf) throws IOException, URISyntaxException {
        URL urlPath = new URL(url);
        HttpURLConnection connection = (HttpURLConnection) urlPath.openConnection();
        InputStream inputStream = connection.getInputStream();
        OutputStream outputStream = new FileOutputStream(downPdf);
        byte[] buffer = new byte[1024];
        int length;
        while ((length = inputStream.read(buffer)) > 0) {
            outputStream.write(buffer, 0, length);
        }
        // 关闭InputStream
        inputStream.close();
        // 关闭OutputStream
        outputStream.close();
        try {
            RandomAccessFile randomAccessFile = new RandomAccessFile(downPdf, "r");
            IInArchive archive = SevenZip.openInArchive(null, new RandomAccessFileInStream(randomAccessFile));
            // 解压⽂件路径
            File extractDir = new File(extractPath);
            if (!extractDir.isDirectory()) {
                extractDir.mkdir();
            }
            int[] in = new int[archive.getNumberOfItems()];
            for (int i = 0; i < in.length; i++) {
                in[i] = i;
            }
            archive.extract(in, false, new ExtractCallback(archive, extractDir.getAbsolutePath()));
            archive.close();
            randomAccessFile.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }


    /**
     * 生成压缩包
     *
     * @param sourceDirPath
     * @param zipFilePath
     * @throws IOException
     */
    public void zipDirectory(String sourceDirPath, String zipFilePath) throws IOException {
        try (ZipOutputStream zipOut = new ZipOutputStream(new FileOutputStream(zipFilePath))) {
            Path sourceDir = Paths.get(sourceDirPath);
            Files.walk(sourceDir)
                    .filter(path -> !Files.isDirectory(path))
                    .forEach(path -> {
                        ZipEntry zipEntry = new ZipEntry(sourceDir.relativize(path).toString());
                        try {
                            zipOut.putNextEntry(zipEntry);
                            Files.copy(path, zipOut);
                            zipOut.closeEntry();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    });
        }
    }
相关推荐
干一行,爱一行2 分钟前
android camera data -> surface 显示
android
WaaTong11 分钟前
《重学Java设计模式》之 原型模式
java·设计模式·原型模式
m0_7430484411 分钟前
初识Java EE和Spring Boot
java·java-ee
AskHarries13 分钟前
Java字节码增强库ByteBuddy
java·后端
断墨先生18 分钟前
uniapp—android原生插件开发(3Android真机调试)
android·uni-app
小灰灰__33 分钟前
IDEA加载通义灵码插件及使用指南
java·ide·intellij-idea
夜雨翦春韭37 分钟前
Java中的动态代理
java·开发语言·aop·动态代理
程序媛小果1 小时前
基于java+SpringBoot+Vue的宠物咖啡馆平台设计与实现
java·vue.js·spring boot
追风林1 小时前
mac m1 docker本地部署canal 监听mysql的binglog日志
java·docker·mac
芒果披萨1 小时前
El表达式和JSTL
java·el