Spring Boot 自动化脚本-多线程批量压缩图片

发布于:2024-12-07 ⋅ 阅读:(113) ⋅ 点赞:(0)

Spring Boot 自动化脚本-多线程批量压缩图片

  • 支持多线程
  • 支持多路径配置
  • 支持断点续压
  • 支持压缩后文件层级路径不变
  • 脚本一键启动,支持本地 main 调用或远程 POST 接口调用

背景:在进行数据迁移时,发现附件文件夹过于庞大,且大都为图片格式,一方面图片数量过多,再一方面,就是在文件上传时,未对图片进行压缩,导致磁盘占用过大。

解决方案:写一个脚本,对服务器图片进行压缩。
目标:压缩后不影响图片内容查看,且压缩后文件结构路径与原来一致。

安装

        <dependency>
            <groupId>net.coobird</groupId>
            <artifactId>thumbnailator</artifactId>
            <version>0.4.20</version>
        </dependency>

压缩

Thumbnails.of(inputFile)
          .scale(0.3) //scale是指定图片的大小,值在0到1之间,1就是原图大小
          .outputQuality(0.3) //图片的质量,值也是在0到1,越接近于1质量越好
          .toFile(outputFile);

处理逻辑

import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.ReUtil;
import cn.hutool.core.util.StrUtil;
import lombok.Data;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import net.coobird.thumbnailator.Thumbnails;

import java.io.File;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 解决图片附件目录过大问题,压缩图片处理
 * 支持多线程
 * 支持多路径配置
 * 支持断点续压
 * 支持压缩后文件层级路径不变
 *
 * @author jason
 */
@Slf4j
public class ImgReduceService {
    private static final ExecutorService EXECUTOR_SERVICE = Executors.newFixedThreadPool(20);

    public static void main(String[] args) {
        PathInfo pathInfo = new PathInfo();
        pathInfo.setInputBasePath("/data/attachment");
        pathInfo.setOutputBasePath("/data/output/attachment");

        PathInfo pathInfo1 = new PathInfo();
        pathInfo1.setInputBasePath("/data/attachment2");
        pathInfo1.setOutputBasePath("/data/output/attachment");

        ImgReduceService.start(CollectionUtil.newArrayList(pathInfo, pathInfo1));
    }

    @SneakyThrows
    public static void start(List<PathInfo> pathInfoList) {
        for (PathInfo pathInfo : pathInfoList) {
            String inputBasePath = pathInfo.getInputBasePath();
            String outputBasePath = pathInfo.getOutputBasePath();

            if (StrUtil.isBlank(inputBasePath)) {
                continue;
            }
            List<File> fileList = FileUtil.loopFiles(inputBasePath);
            log.info("文件数量:{}", fileList.size());

            for (File file : fileList) {
                String inputFile = FileUtil.getAbsolutePath(file);
                String inputPath = FileUtil.getAbsolutePath(FileUtil.getParent(file, 1));
                inputPath = StrUtil.replace(inputPath, "D:", "");
                inputPath = StrUtil.replace(inputPath, File.separator, "/");
                String outputPath = StrUtil.replace(inputPath, inputBasePath, "");
                outputPath = outputBasePath + outputPath;
                FileUtil.mkdir(outputPath);

                // 目标文件
                String outputFile = outputPath + "/" + file.getName();

                // 已存在的跳过
                if (FileUtil.exist(outputFile)) {
                    log.info("目标文件已存在:{}", outputFile);
                    continue;
                }

                String regex = ".*\\.(jpg|jpeg|png|gif|bmp)$";
                boolean isImage = ReUtil.isMatch(regex, file.getName());
                // 图片才处理
                if (!isImage) {
                    // 非图片,直接免压缩丢过去
                    FileUtil.copy(inputFile, outputPath, false);
                    continue;
                }

                // 压缩
                asyncReduce(inputFile, outputFile, outputPath);
            }
        }
    }

    /**
     * 压缩-多线程
     */
    @SneakyThrows
    private static void asyncReduce(String inputFile, String outputFile, String outputPath) {
        EXECUTOR_SERVICE.execute(() -> reduce(inputFile, outputFile, outputPath));
    }

    /**
     * 压缩-单线程
     */
    private static void reduce(String inputFile, String outputFile, String outputPath) {
        try {
            long startTime = System.currentTimeMillis();
            Thumbnails.of(inputFile)
                    .scale(0.3) //scale是指定图片的大小,值在0到1之间,1就是原图大小
                    .outputQuality(0.3) //图片的质量,值也是在0到1,越接近于1质量越好
                    .toFile(outputFile);
            log.info("源文件:{}", inputFile);
            log.info("目标文件:{}", outputFile);
            log.info("压缩耗时:{}ms", System.currentTimeMillis() - startTime);

//            long inputSize = FileUtil.size(FileUtil.file(inputFile));
//            long outputSize = FileUtil.size(FileUtil.file(outputFile));
//            log.info("源文件大小:{},压缩后大小:{}", DataSizeUtil.format(inputSize), DataSizeUtil.format(outputSize));
//            double f = (double) inputSize / outputSize;
//            log.info("压缩率:{}", NumberUtil.formatPercent(f, 2));
        } catch (Exception e) {
//            log.error("压缩异常", e);
            log.info("压缩异常:{},源文件路径:{}", e.getMessage(), inputFile);
            // 压缩失败,直接复制
            FileUtil.copy(inputFile, outputPath, false);
        }
    }

    /**
     * 配置信息
     */
    @Data
    public static class PathInfo {

        /**
         * 源文件根路径
         */
        private String inputBasePath;

        /**
         * 输入文件根路径
         */
        private String outputBasePath;

    }

}

java 简单部署

startup.sh 启动脚本

#!/bin/bash

nohup java -Xms2G -Xmx3G -jar job_api.jar > app.log 2>&1 &

shutdown.sh 停止脚本

#!/bin/bash

# 应用名称
APP_NAME=job_api

# 查找 Java 应用的进程ID
PID=$(ps -ef | grep $APP_NAME | grep -v grep | awk '{print $2}')

# 判断是否存在进程ID
if [ -z "$PID" ]; then
  echo "未找到名为 $APP_NAME 的进程"
else
  echo "正在终止名为 $APP_NAME 的进程,进程ID为:$PID"
  kill -9 $PID
fi

支持代码调用和接口调用

curl 'http://127.0.0.1:9092/job/index/reduce' \
--header 'Content-Type: application/json' \
--data '
[
  {
    "inputBasePath": "/home/env/attachment",
    "outputBasePath": "/home/env/output/attachment"
  },
  {
    "inputBasePath": "/home/env/attachment2",
    "outputBasePath": "/home/env/output/attachment"
  }
]
'

源码

https://gitee.com/zhaomingjian/workspace_jason_demo/tree/master/spring-boot-thumbnails


网站公告

今日签到

点亮在社区的每一天
去签到