Java 图片上传与 WebP 转换实现

本代码主要处理图片上传到本地 image 文件夹,限制文件类型和大小,并自动将非 WebP 格式的图片转换为 WebP 格式保存。

1. 项目依赖配置

首先需要在 pom.xml 中添加必要的依赖:

<dependencies>
    <!-- WebP图像处理 -->
    <dependency>
        <groupId>org.sejda.imageio</groupId>
        <artifactId>webp-imageio</artifactId>
        <version>0.1.6</version>
    </dependency>
    
    <!-- Spring Web (如果使用Spring Boot) -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    
    <!-- Thumbnailator (可选,用于缩略图生成) -->
    <dependency>
        <groupId>net.coobird</groupId>
        <artifactId>thumbnailator</artifactId>
        <version>0.4.18</version>
    </dependency>
</dependencies>

2. Spring Boot配置文件

在 application.properties 中设置文件上传大小限制:

# 设置最大文件上传大小为20MB
spring.servlet.multipart.max-file-size=20MB
spring.servlet.multipart.max-request-size=20MB

3. 图片上传与转换服务类

import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import javax.imageio.ImageIO;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.stream.FileImageOutputStream;
import java.awt.image.BufferedImage;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Iterator;
import java.util.Locale;

@Service
public class ImageUploadService {
    
    // 允许上传的图片类型
    private static final String[] ALLOWED_EXTENSIONS = {"jpg", "jpeg", "png", "webp"};
    // 最大文件大小20MB
    private static final long MAX_FILE_SIZE = 20 * 1024 * 1024;
    // 上传目录
    private static final String UPLOAD_DIR = "image/";

    /**
     * 上传图片并转换为WebP格式
     * @param file 上传的图片文件
     * @return 保存后的文件路径
     * @throws IOException 文件操作异常
     * @throws IllegalArgumentException 文件格式或大小不符合要求
     */
    public String uploadAndConvertToWebp(MultipartFile file) throws IOException, IllegalArgumentException {
        // 检查文件是否为空
        if (file.isEmpty()) {
            throw new IllegalArgumentException("上传的文件为空");
        }

        // 检查文件大小
        if (file.getSize() > MAX_FILE_SIZE) {
            throw new IllegalArgumentException("文件大小超过20MB限制");
        }

        // 获取文件扩展名并验证
        String originalFilename = file.getOriginalFilename();
        String extension = getFileExtension(originalFilename).toLowerCase();
        if (!isAllowedExtension(extension)) {
            throw new IllegalArgumentException("只允许上传jpg, jpeg, png, webp格式的图片");
        }

        // 验证文件内容是否确实是图片
        if (!isValidImage(file)) {
            throw new IllegalArgumentException("文件内容不是有效的图片");
        }

        // 创建上传目录(如果不存在)
        Path uploadPath = Paths.get(UPLOAD_DIR);
        if (!Files.exists(uploadPath)) {
            Files.createDirectories(uploadPath);
        }

        // 读取原始图片
        BufferedImage image = ImageIO.read(file.getInputStream());
        if (image == null) {
            throw new IllegalArgumentException("无法读取图片内容");
        }

        // 生成新的文件名(使用原始文件名+webp扩展名)
        String fileNameWithoutExt = originalFilename.substring(0, originalFilename.lastIndexOf('.'));
        String webpFileName = fileNameWithoutExt + ".webp";
        Path webpFilePath = uploadPath.resolve(webpFileName);

        // 如果原始格式已经是webp,直接保存
        if ("webp".equals(extension)) {
            Files.copy(file.getInputStream(), webpFilePath);
        } else {
            // 转换为webp格式
            convertToWebp(image, webpFilePath.toFile());
        }

        return webpFilePath.toString();
    }

    /**
     * 将图片转换为WebP格式
     * @param image 原始图片
     * @param outputFile 输出文件
     * @throws IOException 文件操作异常
     */
    private void convertToWebp(BufferedImage image, File outputFile) throws IOException {
        // 获取WebP ImageWriter
        Iterator<ImageWriter> writers = ImageIO.getImageWritersByMIMEType("image/webp");
        if (!writers.hasNext()) {
            throw new IllegalStateException("没有找到WebP格式的ImageWriter");
        }
        ImageWriter writer = writers.next();

        // 配置编码参数
        WebPWriteParam writeParam = new WebPWriteParam(Locale.getDefault());
        writeParam.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
        writeParam.setCompressionType(writeParam.getCompressionTypes()[WebPWriteParam.LOSSY_COMPRESSION]);
        writeParam.setCompressionQuality(0.8f); // 设置质量为80%

        try (FileImageOutputStream output = new FileImageOutputStream(outputFile)) {
            writer.setOutput(output);
            writer.write(null, new IIOImage(image, null, null), writeParam);
        } finally {
            writer.dispose();
        }
    }

    /**
     * 获取文件扩展名
     * @param filename 文件名
     * @return 扩展名
     */
    private String getFileExtension(String filename) {
        int dotIndex = filename.lastIndexOf('.');
        if (dotIndex > 0 && dotIndex < filename.length() - 1) {
            return filename.substring(dotIndex + 1);
        }
        return "";
    }

    /**
     * 检查扩展名是否允许
     * @param extension 文件扩展名
     * @return 是否允许
     */
    private boolean isAllowedExtension(String extension) {
        for (String allowed : ALLOWED_EXTENSIONS) {
            if (allowed.equalsIgnoreCase(extension)) {
                return true;
            }
        }
        return false;
    }

    /**
     * 验证文件内容是否是有效的图片
     * @param file 上传的文件
     * @return 是否是有效图片
     * @throws IOException 文件操作异常
     */
    private boolean isValidImage(MultipartFile file) throws IOException {
        try (InputStream input = file.getInputStream()) {
            // 读取文件头
            byte[] header = new byte[8];
            int read = input.read(header);
            if (read < 4) {
                return false;
            }

            // 检查文件头是否符合图片格式
            String realType = getRealFileType(header);
            String extension = getFileExtension(file.getOriginalFilename()).toLowerCase();
            
            return realType.equalsIgnoreCase(extension);
        }
    }

    /**
     * 通过魔数验证文件真实类型
     * @param fileBytes 文件头字节
     * @return 真实文件类型
     */
    private String getRealFileType(byte[] fileBytes) {
        if (fileBytes == null || fileBytes.length < 4) {
            return "unknown";
        }

        // 读取前4个字节并转换为十六进制
        StringBuilder hexBuilder = new StringBuilder();
        for (int i = 0; i < Math.min(4, fileBytes.length); i++) {
            hexBuilder.append(String.format("%02X", fileBytes[i]));
        }
        String fileHeader = hexBuilder.toString();

        // 检查匹配的魔数
        if (fileHeader.startsWith("FFD8FF")) {
            return "jpg";
        } else if (fileHeader.startsWith("89504E47")) {
            return "png";
        } else if (fileHeader.startsWith("47494638")) {
            return "gif";
        } else if (fileHeader.startsWith("424D")) {
            return "bmp";
        } else if (fileHeader.startsWith("52494646")) { // WEBP: RIFF
            return "webp";
        }

        return "unknown";
    }
}

4. Spring Boot控制器

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

@RestController
@RequestMapping("/api/images")
public class ImageUploadController {

    @Autowired
    private ImageUploadService imageUploadService;

    @PostMapping("/upload")
    public ResponseEntity<?> uploadImage(@RequestParam("file") MultipartFile file) {
        try {
            String filePath = imageUploadService.uploadAndConvertToWebp(file);
            return ResponseEntity.ok().body("图片上传成功: " + filePath);
        } catch (IllegalArgumentException e) {
            return ResponseEntity.badRequest().body(e.getMessage());
        } catch (IOException e) {
            return ResponseEntity.internalServerError().body("文件处理错误: " + e.getMessage());
        }
    }
}

5. 功能说明

这个实现提供了以下功能:

  1. 文件类型验证:通过文件扩展名和文件头魔数双重验证,确保上传的是 jpg、jpeg、png 或 webp 格式的图片

  2. 文件大小限制:限制上传文件最大为 20MB

  3. 自动转换 WebP:如果上传的不是 webp 格式图片,会自动转换为 webp 格式保存

  4. 安全性检查:验证文件内容确实是图片,防止伪造文件扩展名的攻击

  5. 目录自动创建:如果目标目录不存在会自动创建

6. 使用说明

  1. 将代码集成到 Spring Boot 项目中

  2. 确保image/目录有写入权限

  3. 通过 POST 请求/api/images/upload上传图片

  4. 上传成功后,图片会以 webp 格式保存在image/目录下

7. 扩展建议

  1. 缩略图生成:可以结合 Thumbnailator 库生成缩略图

  2. 异步处理:对于大文件可以考虑使用异步处理

  3. 分布式存储:如果需要,可以将图片保存到云存储服务

  4. 更详细的错误处理:可以根据业务需求添加更详细的错误分类和处理


Java 图片上传与 WebP 转换实现
https://uniomo.com/archives/java-tu-pian-shang-chuan-yu-webp-zhuan-huan-shi-xian
作者
雨落秋垣
发布于
2025年10月25日
许可协议