springboot整合阿里云OSS实现多线程下文件上传(aop限制文件大小和类型)

发布时间 2023-04-12 20:22:18作者: destiny-2015

内容涉及:

  1. springboot整合阿里云oss

  2. 自定义注解及aop的使用:对上传文件格式(视频格式、图片格式)、不同类型文件进行大小限制(视频和图片各自自定义大小)

  3. 线程池使用:阿里云OSS多线程上传文件

  4. 阿里云OSS分片上传大文件

 

业务需求

需求一:

  1. 前端传递单个或多个小文件(这里以图片为例)到后端;

  2. 后端对图片进行处理,并上传至阿里云oss;

  3. 上传完毕之后,返回图片链接给前端,如果格式支持,可以在线预览。

 

需求二:

  1. 前端上传大文件(这里以视频为例)到后端;

  2. 后端对视频进行分片处理,上传到oss;

  3. 上传完毕后,返回视频连接给前端。

 

过滤文件

主要使用了aop切面+自定义注解来切入请求中,过滤文件的类型,将不符合要求的文件剔除;

这里主要实现注解的方式来过滤文件类型和自定义文件大小

 

添加自定义注解类

package org.aliyunoss.aop;

import org.aliyunoss.utils.FileLimitUnit;
import org.springframework.core.annotation.AliasFor;

import java.lang.annotation.*;
/**
 * @Description :FileLimit 注解,内置参数value,max,以及文件单位
 */
@Documented
@Target(ElementType.METHOD) // 作用与方法上
@Retention(RetentionPolicy.RUNTIME) // RUNTIME: 在运行时有效(即运行时保留)
public @interface FileLimit {
    @AliasFor("max") // @AliasFor 表示其可与max互换别名:当注解指定value时,为max赋值
    int value() default 5;
    // 定义单个文件最大限制
    @AliasFor("value") // @AliasFor 表示其可与value互换别名:当注解指定max是,为value赋值
    int max() default 5;
    // 文件单位,默认定义为MB

    //定义单次上传文件的总大小,默认50MB
    int maxRequestSize() default 50;

    //上传文件格式,默认是图片
    String fileFormat() default "images";
    FileLimitUnit unit() default FileLimitUnit.MB;
}

可以根据实际需求,这里设置了注解的几个参数,分别是

  1. max()/value(),表示文件最大限制,默认为5,
  2. maxRequestSize(),单次请求的总文件大小,即一次请求上传的多个文件的总大小,默认为50
  3. fileFormat(),表示文件格式,默认是图片"images",如果上传视频的话需要改成"videos",文件类型可以根据需求自定义,修改getFileFormatLimit()方法即
  4. FileLimitUnit类型的unit()参数表示文件大小的类型,默认是MB,可以设置成KB或GB

 

 

添加aop切面类

@Before:前置通知, 在目标方法(切入点)执行之前执行。

因为我们需要在请求过来时就对文件过滤,所以这里使用前置通知@Before

springboot中使用aop功能需要引入aop的依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
    <version>2.5.0</version>
</dependency>

 

添加aop切面类

package org.aliyunoss.aop;

import cn.hutool.core.io.FileTypeUtil;
import org.aliyunoss.utils.MyFileUtils;
import org.aliyunoss.vo.ErrorCode;
import org.aliyunoss.vo.MyAppException;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.processing.FilerException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Aspect
@Component
public class FileLimitAop {
    // 定义默认的单个文件最大限制 5MB 。5Mb = 5 * 1024 * 1024 byte
    private static final long MAX_FILE_SIZE = 5 * 1024 * 1024;
    private static final long MAX_REQUEST_SIZE = 50 * 1024 * 1024;

    // 注意,这里要指定注解的全限定类名。不然无法进入AOP拦截自定义注解FileLimit
    @Pointcut("@annotation(org.aliyunoss.aop.FileLimit)")
    public void pointcut() {
    }

    /**
     * 方法体执行之前执行
     */
    @Before("pointcut()")
    public void beforeLog(JoinPoint joinPoint) throws FilerException {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        FileLimit annotation = AnnotationUtils.getAnnotation(signature.getMethod(), FileLimit.class);
        if (null == annotation) {
            return;
        }
        // 执行文件检查
        fileSizeLimit(joinPoint, annotation);
    }

    // 判定文件大小是否合格,如果不合格,直接跑出自定义异常FileLimitException。进而阻塞方法正常进行。
    private void fileSizeLimit(JoinPoint joinPoint, FileLimit annotation) throws FilerException {
        // 获取AOP签名
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        // 获取注解的指定最大文件大小
        Map annotationMaxFileSize = getAnnotationMaxFileSize(annotation);
        long maxFileSize = (long) annotationMaxFileSize.get("maxFileSize");
        long maxRequestSize = (long) annotationMaxFileSize.get("maxRequestSize");
        // 通过AOP签名 获取接口参数,调用方法获取文件
        //=
        List<MultipartFile> multipartFileList= new ArrayList<>();
        Object[] args = joinPoint.getArgs();

        for (Object arg : args) {
            multipartFileList = (List<MultipartFile>) arg;
        }
        //保存总文件大小
        long maxSize = 0;
        for (MultipartFile multipartFile : multipartFileList) {
            if (null != multipartFile) {
                long size = multipartFile.getSize();
                if (0 == size) {
                    //自定义异常
                    throw new MyAppException(ErrorCode.FILE_DATA_EXCEPTION.getCode(), "文件数据(大小为0)异常");
                }
                maxSize += size;
                if (multipartFile.getSize() > maxFileSize) {
                    String msg = "文件大小不得超过 " + annotation.max() + annotation.unit().toString();
                    //System.out.println(msg);
                    throw new MyAppException(ErrorCode.FILE_DATA_EXCEPTION.getCode(), msg);
                }
            } else {
                throw new MyAppException(ErrorCode.FILE_DATA_EXCEPTION.getCode(), "文件为null");
            }
        }

        if (maxSize > maxRequestSize) {
            throw new MyAppException(ErrorCode.FILE_DATA_EXCEPTION.getCode(), "单次上传总文件大小不能超过" + annotation.maxRequestSize() + annotation.unit());
        }

        List fileFormatLimit = getFileFormatLimit(annotation);
        //判断文件格式 根据文件头信息判断
        for (MultipartFile multipartFile : multipartFileList) {
            String type = FileTypeUtil.getType(MyFileUtils.multipartFileToFile(multipartFile));
            //判断类型是String类型,因此可以直接用contains方法
            boolean contains = fileFormatLimit.contains(type);
            if (!contains){
                throw  new MyAppException(ErrorCode.FILE_DATA_EXCEPTION.getCode(), "文件格式不正确");
            }
        }
    }

    // 获取使用注解指定最大文件大小。如果没有指定文件大小,就用默认值
    public Map getAnnotationMaxFileSize(FileLimit fileLimit) {
        Map map = new HashMap();

        if (null == fileLimit) {
            map.put("maxFileSize", MAX_FILE_SIZE);
            map.put("maxRequestSize", MAX_REQUEST_SIZE);
            return map;
        }
        switch (fileLimit.unit()) {
            case MB:
                map.put("maxFileSize", (long) fileLimit.max() << 20);
                map.put("maxRequestSize", (long) fileLimit.maxRequestSize() << 20);
                return map;
            case KB:
                map.put("maxFileSize", (long) fileLimit.max() << 10);
                map.put("maxRequestSize", (long) fileLimit.maxRequestSize() * 1024);
                return map;
            default:
                map.put("maxFileSize", MAX_FILE_SIZE);
                map.put("maxRequestSize", MAX_REQUEST_SIZE);
                return map;
        }
    }

    //    获取上传文件类型限制
    public List getFileFormatLimit(FileLimit fileLimit) {
        List fileFormatLimitList = new ArrayList();
        if (null == fileLimit) {
            fileFormatLimitList.add("bmp");
            fileFormatLimitList.add("gif");
            fileFormatLimitList.add("ico");
            fileFormatLimitList.add("jfif");
            fileFormatLimitList.add("jpeg");
            fileFormatLimitList.add("jpg");
            fileFormatLimitList.add("png");
            fileFormatLimitList.add("tif");
            fileFormatLimitList.add("tiff");
            fileFormatLimitList.add("webp");
            fileFormatLimitList.add("wbmp");
            ////bmp/gif/jpg/jfif/jpeg/png/webp/wbmp/ico/tif/tiff
            //JSONObject jso1 = new JSONObject();
            //jso1.put("type", "BMP");
            //jso1.put("contentType", "image/bmp");
            //JSONObject jso2 = new JSONObject();
            //jso2.put("type", "gif");
            //jso2.put("contentType", "image/gif");
            //JSONObject jso3 = new JSONObject();
            //jso3.put("type", "jfif");
            //jso3.put("contentType", "image/jpeg");
            //JSONObject jso4 = new JSONObject();
            //jso4.put("type", "jpeg");
            //jso4.put("contentType", "image/jpg");
            //JSONObject jso5 = new JSONObject();
            //jso5.put("type", "jpg");
            //jso5.put("contentType", "image/jpg");
            //JSONObject jso6 = new JSONObject();
            //jso6.put("type", "webp");
            //jso6.put("contentType", "image/webp");
            //JSONObject jso7 = new JSONObject();
            //jso7.put("type", "png");
            //jso7.put("contentType", "image/png");
            //JSONObject jso8 = new JSONObject();
            //jso8.put("type", "tif");
            //jso8.put("contentType", "image/tiff");
            //JSONObject jso9 = new JSONObject();
            //jso9.put("type", "tiff");
            //jso9.put("contentType", "image/tiff");
            //JSONObject jso10 = new JSONObject();
            //jso10.put("type", "ico");
            //jso10.put("contentType", "image/x-icon");
            //JSONObject jso11 = new JSONObject();
            //jso11.put("type", "wbmp");
            //jso11.put("contentType", "image/vnd.wap.wbmp");
            //
            //fileFormatLimitList.add(jso1);
            //fileFormatLimitList.add(jso2);
            //fileFormatLimitList.add(jso3);
            //fileFormatLimitList.add(jso4);
            //fileFormatLimitList.add(jso5);
            //fileFormatLimitList.add(jso6);
            //fileFormatLimitList.add(jso7);
            //fileFormatLimitList.add(jso8);
            //fileFormatLimitList.add(jso9);
            //fileFormatLimitList.add(jso10);
            //fileFormatLimitList.add(jso11);
            return fileFormatLimitList;
        }
        switch (fileLimit.fileFormat()) {
            case "images":
                fileFormatLimitList.add("bmp");
                fileFormatLimitList.add("gif");
                fileFormatLimitList.add("ico");
                fileFormatLimitList.add("jfif");
                fileFormatLimitList.add("jpeg");
                fileFormatLimitList.add("jpg");
                fileFormatLimitList.add("png");
                fileFormatLimitList.add("tif");
                fileFormatLimitList.add("tiff");
                fileFormatLimitList.add("webp");
                fileFormatLimitList.add("wbmp");
                return fileFormatLimitList;
            case "videos":
                fileFormatLimitList.add("avi");
                fileFormatLimitList.add("flv");
                fileFormatLimitList.add("mp4");
                fileFormatLimitList.add("mpeg");//
                fileFormatLimitList.add("wmv");
                fileFormatLimitList.add("wma");//
                fileFormatLimitList.add("w4a");//
                fileFormatLimitList.add("wov");//
                fileFormatLimitList.add("3GP");//
                fileFormatLimitList.add("webm");//
                fileFormatLimitList.add("vob");//
                fileFormatLimitList.add("mkv");//
                return fileFormatLimitList;
        }
        return fileFormatLimitList;
    }
            //case "AVI": contentType = "video/avi";break;
            //case "FLV": contentType = "video/x-flv";break;
            //case "MP4": contentType = "video/mpeg4";break;
            //case "MPEG": contentType = "video/mpg";break;
            //case "WMV": contentType = "video/x-ms-wmv";break;
            //case "WMA": contentType = "video/wma";break;
            //case "W4A": contentType = "video/mp4";break;
            //case "W4V": contentType = "video/mp4";break;
            //case "WOV": contentType = "video/quicktime";break;
            //case "3GP": contentType = "video/3gpp";break;
            //case "WEBM": contentType = "video/webm";break;
            //case "VOB": contentType = "video/vob";break;
            //case "MKV": contentType = "video/x-matroska";break;
}

 

工具类

一个枚举类,用来定义限制文件大小的单位的

package org.aliyunoss.utils;

public enum FileLimitUnit {
    KB, MB, GB
}

 

自定义了一个工具类,封装了判断文件类型、MultipartFile转File、以及根据文件类型来返回contentType等方法;本来想把hutool工具类里面的方法抽出来的,太麻烦了。。。后面还是直接用了hutool工具类来判断文件类型。

package org.aliyunoss.utils;

import org.apache.commons.lang3.StringUtils;
import org.springframework.web.multipart.MultipartFile;

import javax.imageio.ImageIO;
import java.awt.*;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

/**
 * @Author YK Fei
 * @Date 2023/1/4 10:15
 * @MethodName
 * @Param
 * @Return
 * 实现文件名长度和名称的限制
 */
public class MyFileUtils {
    /**
     * @param imageFile 需要判断的文件
     * @return boolean
     * @description 判断文件是否为图片
     * @method isImage
     **/
    public static boolean isImage(File imageFile) {
        if (!imageFile.exists()) {
            return false;
        }
        Image img = null;
        try {
            img = ImageIO.read(imageFile);
            return img != null && img.getWidth(null) > 0 && img.getHeight(null) > 0;
        } catch (Exception e) {
            return false;
        } finally {
            // 最终重置为空
            img = null;
            imageFile.delete();
        }
    }
    /**
     * @Author YK Fei
     * @Date 2023/1/5 15:52
     * @MethodName isVideo
     * @Param [multipartFile]
     * @Return boolean
     * @Description 判断文件是否为视频格式(.mp4 .avi .wmv .mpg .mpeg .mpv .rm .ram .swf .flv .mov .qt .navi)
     */
    public static boolean isVideo(MultipartFile multipartFile){

        List<String> formatList = new ArrayList<>();

        formatList.add("avi");
        formatList.add("flv");
        formatList.add("mov");
        formatList.add("mp4");
        formatList.add("mpg");
        formatList.add("mpeg");
        formatList.add("mpv");
        formatList.add("navi");
        formatList.add("qt");
        formatList.add("rm");
        formatList.add("ram");
        formatList.add("ram");
        formatList.add("ram");
        formatList.add("swf");
        formatList.add("wmv");



        String originalFilename = multipartFile.getOriginalFilename();
        String format = StringUtils.substringAfterLast(originalFilename,".");
        for (int i = 0; i < formatList.size(); i++) {
            if (format.equalsIgnoreCase(formatList.get(i))){
                return true;
            }
        }

        return true;
    }


    /**
     * @return java.io.File
     * @description MultipartFile转File
     * @method multipartFileToFile
     **/
    public  static File multipartFileToFile(MultipartFile multipartFile) {
        if (multipartFile.isEmpty()) {
            return null;
        }
        InputStream inputStream = null;
        try {
            inputStream = multipartFile.getInputStream();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        File file = new File(Objects.requireNonNull(multipartFile.getOriginalFilename()));
        try {
            OutputStream os = Files.newOutputStream(file.toPath());
            int bytesRead;
            byte[] buffer = new byte[8192];
            while ((bytesRead = inputStream.read(buffer, 0, 8192)) != -1) {
                os.write(buffer, 0, bytesRead);
            }
            os.close();
            inputStream.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return file;
    }

    /**
     * @param filename 文件名
     * @description 获取文件拓展名
     * @method getExtensionName
     **/
    public static String getExtensionName(String filename) {
        if ((filename != null) && (filename.length() > 0)) {
            int dot = filename.lastIndexOf('.');
            if ((dot > -1) && (dot < (filename.length() - 1))) {
                return filename.substring(dot);
            }
        }
        return filename;
    }


    /**
     * Description: 判断OSS服务文件上传时文件的contentType
     * @param filenameExtension 文件后缀
     * @return String
     */
    public static String getContentType(String filenameExtension) {
        String contentType = "";
        switch(filenameExtension.toUpperCase()) {
            //image contentType
            case "BMP": contentType = "image/bmp";break;
            case "GIF": contentType = "image/gif";break;
            case "JPEG":
            case "JPG":contentType = "image/jpg";break;
            case "ICO": contentType="image/x-icon";break;
            case "TIF":
            case "TIFF": contentType="image/tiff";break;
            case "PNG": contentType = "image/png";break;
            case "WBMP": contentType = "image/vnd.wap.wbmp";break;
            case "WEBP": contentType = "image/webp";break;
            case "JFIF": contentType = "image/jpeg";break;
            //video contentType
            case "AVI": contentType = "video/avi";break;
            case "FLV": contentType = "video/x-flv";break;
            case "MP4": contentType = "video/mpeg4";break;
            case "MPEG": contentType = "video/mpg";break;
            case "WMV": contentType = "video/x-ms-wmv";break;
            case "WMA": contentType = "video/wma";break;
            case "W4A": contentType = "video/mp4";break;
            case "W4V": contentType = "video/mp4";break;
            case "WOV": contentType = "video/quicktime";break;
            case "3GP": contentType = "video/3gpp";break;
            case "WEBM": contentType = "video/webm";break;
            case "VOB": contentType = "video/vob";break;
            case "MKV": contentType = "video/x-matroska";break;

            case "HTML": contentType = "text/html";break;
            case "TXT": contentType = "text/plain";break;
            case "VSD": contentType = "application/vnd.visio";break;
            case "PPTX":
            case "PPT": contentType = "application/vnd.ms-powerpoint";break;
            case "DOCX": contentType = "application/vnd.openxmlformats-officedocument.wordprocessingml.document";break;
            case "DOC": contentType = "application/msword";break;
            case "XLSX": contentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";break;
            case "XLS": contentType = "application/vnd.ms-excel";break;
            case "XML": contentType = "text/xml";break;
            case "PDF": contentType = "application/pdf";break;


            default:contentType="file";
        }
        return contentType;
    }

}

 

hutool工具类中判断文件类型的是 FileTypeUtil工具类,通过调用FileTypeUtil.getType即可判断。原理:通过读取文件流中前几位byte值来判断文件类型,如果判断不出来的话那么就会根据文件后缀来判断。

 

 

FileTypeUtil局限性:对于文本、zip判断不准确,对于视频、图片类型判断准确

 

通过查看源码中的FILE_TYPE_MAP,可以看到,FileTypeUtile可以识别出以下文件类型:

		FILE_TYPE_MAP.put("ffd8ff", "jpg"); // JPEG (jpg)
		FILE_TYPE_MAP.put("52494646", "webp");
		FILE_TYPE_MAP.put("89504e47", "png"); // PNG (png)
		FILE_TYPE_MAP.put("4749463837", "gif"); // GIF (gif)
		FILE_TYPE_MAP.put("4749463839", "gif"); // GIF (gif)
		FILE_TYPE_MAP.put("49492a00227105008037", "tif"); // TIFF (tif)
		// https://github.com/sindresorhus/file-type/blob/main/core.js#L90
		FILE_TYPE_MAP.put("424d", "bmp"); // 位图(bmp)
		FILE_TYPE_MAP.put("41433130313500000000", "dwg"); // CAD (dwg)
		FILE_TYPE_MAP.put("7b5c727466315c616e73", "rtf"); // Rich Text Format (rtf)
		FILE_TYPE_MAP.put("38425053000100000000", "psd"); // Photoshop (psd)
		FILE_TYPE_MAP.put("46726f6d3a203d3f6762", "eml"); // Email [Outlook Express 6] (eml)
		FILE_TYPE_MAP.put("5374616E64617264204A", "mdb"); // MS Access (mdb)
		FILE_TYPE_MAP.put("252150532D41646F6265", "ps");
		FILE_TYPE_MAP.put("255044462d312e", "pdf"); // Adobe Acrobat (pdf)
		FILE_TYPE_MAP.put("2e524d46000000120001", "rmvb"); // rmvb/rm相同
		FILE_TYPE_MAP.put("464c5601050000000900", "flv"); // flv与f4v相同
		FILE_TYPE_MAP.put("0000001C66747970", "mp4");
		FILE_TYPE_MAP.put("00000020667479706", "mp4");
		FILE_TYPE_MAP.put("00000018667479706D70", "mp4");
		FILE_TYPE_MAP.put("49443303000000002176", "mp3");
		FILE_TYPE_MAP.put("000001ba210001000180", "mpg"); //
		FILE_TYPE_MAP.put("3026b2758e66cf11a6d9", "wmv"); // wmv与asf相同
		FILE_TYPE_MAP.put("52494646e27807005741", "wav"); // Wave (wav)
		FILE_TYPE_MAP.put("52494646d07d60074156", "avi");
		FILE_TYPE_MAP.put("4d546864000000060001", "mid"); // MIDI (mid)
		FILE_TYPE_MAP.put("526172211a0700cf9073", "rar"); // WinRAR
		FILE_TYPE_MAP.put("235468697320636f6e66", "ini");
		FILE_TYPE_MAP.put("504B03040a0000000000", "jar");
		FILE_TYPE_MAP.put("504B0304140008000800", "jar");
		// MS Excel 注意:word、msi 和 excel的文件头一样
		FILE_TYPE_MAP.put("d0cf11e0a1b11ae10", "xls");
		FILE_TYPE_MAP.put("504B0304", "zip");
		FILE_TYPE_MAP.put("4d5a9000030000000400", "exe"); // 可执行文件
		FILE_TYPE_MAP.put("3c25402070616765206c", "jsp"); // jsp文件
		FILE_TYPE_MAP.put("4d616e69666573742d56", "mf"); // MF文件
		FILE_TYPE_MAP.put("7061636b616765207765", "java"); // java文件
		FILE_TYPE_MAP.put("406563686f206f66660d", "bat"); // bat文件
		FILE_TYPE_MAP.put("1f8b0800000000000000", "gz"); // gz文件
		FILE_TYPE_MAP.put("cafebabe0000002e0041", "class"); // class文件
		FILE_TYPE_MAP.put("49545346030000006000", "chm"); // chm文件
		FILE_TYPE_MAP.put("04000000010000001300", "mxp"); // mxp文件
		FILE_TYPE_MAP.put("6431303a637265617465", "torrent");
		FILE_TYPE_MAP.put("6D6F6F76", "mov"); // Quicktime (mov)
		FILE_TYPE_MAP.put("FF575043", "wpd"); // WordPerfect (wpd)
		FILE_TYPE_MAP.put("CFAD12FEC5FD746F", "dbx"); // Outlook Express (dbx)
		FILE_TYPE_MAP.put("2142444E", "pst"); // Outlook (pst)
		FILE_TYPE_MAP.put("AC9EBD8F", "qdf"); // Quicken (qdf)
		FILE_TYPE_MAP.put("E3828596", "pwl"); // Windows Password (pwl)
		FILE_TYPE_MAP.put("2E7261FD", "ram"); // Real Audio (ram)
		// https://stackoverflow.com/questions/45321665/magic-number-for-google-image-format

 

注意,该工具类对 xlsx、docx等Office2007的格式,全部识别为zip,因为新版采用了OpenXML格式,这些格式本质上是XML文件打包成zip

 

解决方案

需求一:上传多个小文件

通常的解决方案是前端传入multipartFiles集合到后端接口,通过自定义注解+aop切面来对文件进行过滤,筛选出不符合要求的文件(如文件过大,文件类型不匹配),然后通过单线程循环调用阿里云OSS提供的putObject方法,将获取到的文件集合按照顺序逐一上传到OSS之中

缺点:网络不良时,容易造成文件丢失,需要重新上传,进而提高等待时间

 

 

 

上传文件核心代码:

    @Override
    public List<String> uploadImages(List<MultipartFile> multipartFile) {
        this.ossClient = AliyunOssConfig.createOss(aliyunOssConfig);
        //保存上传后返回的云端文件URLs
        List<String> responseUrls = new ArrayList<>();
        //设置url过期时间 上传时间后五年后失效
        Date expiration = new Date(System.currentTimeMillis() + 5 * 365 * 24 * 3600 * 1000);
        String dir = new SimpleDateFormat("yyyy-MM-dd").format(new Date());

        try {
            for (MultipartFile file : multipartFile) {
                String originalFilename = file.getOriginalFilename();
                String cloudFileName = new StringBuilder()
                        .append(UUID.randomUUID().toString())
                        .append(MyFileUtils.getExtensionName(originalFilename))
                        .toString();
                //阿里云OSS bucket下存储位置
                String cloudPath = dir + "/" + cloudFileName;
                //设置ContentType,使得返回的url可以在网页中预览(仅有少部分格式支持在线预览)  默认不设置或不支持在线预览的,返回的url是下载附件,而不是预览(可以从前端传个参数来判断是在线预览还是下载)

                ObjectMetadata objectMetadata = new ObjectMetadata();
                //判断文件类型(获取扩展名方式)
                //objectMetadata.setContentType(MyFileUtils.getContentType(StringUtils.substringAfterLast(originalFilename, ".")));
                //判断文件类型,通过hutool工具类,本质是根据件流头部16进制字符串进行判断
                objectMetadata.setContentType(MyFileUtils.getContentType(FileTypeUtil.getType(MyFileUtils.multipartFileToFile(file))));

                // 设置URL过期时间为1小时。
                InputStream multipartFileInputStream = file.getInputStream();
                //PutObjectRequest putObjectRequest = new PutObjectRequest(aliyunOssConfig.getBucket(), cloudPath, multipartFileInputStream);

                ossClient.putObject(aliyunOssConfig.getBucket(), cloudPath, multipartFileInputStream, objectMetadata);
                //ossClient.generatePresignedUrl()
                String url = ossClient.generatePresignedUrl(aliyunOssConfig.getBucket(), cloudPath, expiration).toString();
                //去掉url尾部的Expires信息、OSSAccessKeyId信息以及Signature信息
                url = url.substring(0, url.indexOf("?"));
                responseUrls.add(url);
                
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 关闭流
            ossClient.shutdown();
        }
        return responseUrls;
    }

 

这里可以使用线程池来优化上传:

    public List<String> uploadImages(List<MultipartFile> multipartFile) {
        //保存上传后返回的云端文件URLs
        List<String> responseUrls = Collections.synchronizedList(new ArrayList<>());
        //设置url过期时间 上传时间后五年后失效
        Date expiration = new Date(System.currentTimeMillis() + 5 * 365 * 24 * 3600 * 1000);
        // 用户上传文件时指定的前缀,即存放在以时间命名的文件夹内
        String dir = new SimpleDateFormat("yyyy-MM-dd").format(new Date());
        //定义List类型的submit,用来接收上传文件的url
        Future<List> submit = null;

        int coreThreads = Runtime.getRuntime().availableProcessors();
        logger.info("当前计算机核心线程数:" + coreThreads);
        //创建线程池 核心线程数:当前当前计算机核心线程数  同时容纳最大线程:5*当前当前计算机核心线程数  非核心空闲线程存活时间:30 存活时间单位:毫秒(1/1000s) 任务队列:当前当前计算机核心线程数*10 拒绝策略:默认
        //ThreadPoolExecutor threadPoolExecutor = new org.apache.tomcat.util.threads.ThreadPoolExecutor
        //        (coreThreads, coreThreads * 5, 30L, TimeUnit.MILLISECONDS, new LinkedBlockingDeque<>(coreThreads * 10));

            for (MultipartFile file : multipartFile) {

                // 多线程上传图片 使用submit方法 为了返回上传文件的url
                submit= executor.submit((Callable<List>)()->{
                    ossClient = AliyunOssConfig.createOss(aliyunOssConfig);
                    //获得原始文件名称
                    String originalFilename = file.getOriginalFilename();
                    // 设置上传到云存储的文件名,规则为"当前时间-UUID.源文件后缀名"
                    String cloudFileName = new StringBuilder()
                            .append(UUID.randomUUID().toString())
                            .append(MyFileUtils.getExtensionName(originalFilename))
                            .toString();
                    //阿里云OSS bucket下存储位置
                    String cloudPath = dir + "/" + cloudFileName;
                    //设置ContentType,使得返回的url可以在网页中预览(仅有少部分格式支持在线预览)  默认不设置或不支持在线预览的,返回的url是下载附件,而不是预览(可以从前端传个参数来判断是在线预览还是下载)
                    ObjectMetadata objectMetadata = new ObjectMetadata();
                    //判断文件类型(获取扩展名方式)
                    //objectMetadata.setContentType(MyFileUtils.getContentType(StringUtils.substringAfterLast(originalFilename, ".")));
                    //判断文件类型,通过hutool工具类,本质是根据件流头部16进制字符串进行判断
                    objectMetadata.setContentType(MyFileUtils.getContentType(FileTypeUtil.getType(MyFileUtils.multipartFileToFile(file))));

                    InputStream multipartFileInputStream = file.getInputStream();
                    try {
                        ossClient.putObject(aliyunOssConfig.getBucket(), cloudPath, multipartFileInputStream, objectMetadata);
                    } catch (Exception e){
                        e.printStackTrace();
                    }
                    String url = ossClient.generatePresignedUrl(aliyunOssConfig.getBucket(), cloudPath, expiration).toString();
                    //去掉url尾部的Expires信息、OSSAccessKeyId信息以及Signature信息
                    url = url.substring(0, url.indexOf("?"));
                    responseUrls.add(url);
                    return responseUrls;
                });
            }
        //关闭oss资源
        //ossClient.shutdown();
        //executor.shutdown();
        try {
            //将线程池返回的结果返回
            return submit.get();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

 

上传大文件,分片上传

 

    @Override
    public Result fileUploadZone(MultipartFile file) {
        this.ossClient = AliyunOssConfig.createOss(aliyunOssConfig);
        try {
            /*
             耗时记录输出
            */
            Date date = new Date();
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            String beginTime = sdf.format(date);
            long l1 = System.currentTimeMillis();
            long test = l1;
            //String beginTime = date.getHours() + ":" + date.getMinutes() + ":" + date.getSeconds();

            logger.info("fileUploadZone-开始上传时间:" + beginTime + "时间戳:" + l1);

            //设置url过期时间 上传时间后一周内失效
            Date expiration = new Date(System.currentTimeMillis() + 7 * 24 * 3600 * 1000);
            //获取文件的原始名字
            String originalfileName = file.getOriginalFilename();
            //文件后缀
            String suffix = originalfileName.substring(originalfileName.lastIndexOf(".") + 1);
            //重新命名文件,文件夹要是改动,app记录删除的地方一并改动
            String pack = "file/";
            String fileName = "file_" + System.currentTimeMillis() + "." + suffix;
            String cloudPath = pack + fileName;

            // 创建InitiateMultipartUploadRequest对象。
            InitiateMultipartUploadRequest request = new InitiateMultipartUploadRequest(aliyunOssConfig.getBucket(), cloudPath);
            // 如果需要在初始化分片时设置文件存储类型,请参考以下示例代码。
            ObjectMetadata objectMetadata = new ObjectMetadata();
            objectMetadata.setContentType(MyFileUtils.getContentType(FileTypeUtil.getType(MyFileUtils.multipartFileToFile(file))));
            // ObjectMetadata metadata = new ObjectMetadata();
            //objectMetadata.setHeader(OSSHeaders.OSS_STORAGE_CLASS, StorageClass.Standard.toString());
            //String files = URLEncoder.encode(cloudPath, "UTF-8");
            //objectMetadata.setHeader("Content-Disposition", "filename*=utf-8''" + files);
            request.setObjectMetadata(objectMetadata);
            // 初始化分片。
            InitiateMultipartUploadResult upresult = ossClient.initiateMultipartUpload(request);
            // 返回uploadId,它是分片上传事件的唯一标识,可以根据这个uploadId发起相关的操作,如取消分片上传、查询分片上传等。
            String uploadId = upresult.getUploadId();
            // partETags是PartETag的集合。PartETag由分片的ETag和分片号组成。
            List<PartETag> partETags = new ArrayList<PartETag>();
            // 计算文件有多少个分片。
            // 2MB
            final long partSize = 2 * 1024 * 1024L;
            long fileLength = file.getSize();
            int partCount = (int) (fileLength / partSize);
            if (fileLength % partSize != 0) {
                partCount++;
            }
            // 遍历分片上传。
            for (int i = 0; i < partCount; i++) {
                long startPos = i * partSize;
                long curPartSize = (i + 1 == partCount) ? (fileLength - startPos) : partSize;
                // 跳过已经上传的分片。
                InputStream instream = file.getInputStream();
                instream.skip(startPos);
                UploadPartRequest uploadPartRequest = new UploadPartRequest();
                uploadPartRequest.setBucketName(aliyunOssConfig.getBucket());
                uploadPartRequest.setKey(cloudPath);
                uploadPartRequest.setUploadId(uploadId);
                uploadPartRequest.setInputStream(instream);
                // 设置分片大小。除了最后一个分片没有大小限制,其他的分片最小为100 KB。
                uploadPartRequest.setPartSize(curPartSize);
                // 设置分片号。每一个上传的分片都有一个分片号,取值范围是1~10000,如果超出这个范围,OSS将返回InvalidArgument的错误码。
                uploadPartRequest.setPartNumber(i + 1);
                // 每个分片不需要按顺序上传,甚至可以在不同客户端上传,OSS会按照分片号排序组成完整的文件。
                UploadPartResult uploadPartResult = ossClient.uploadPart(uploadPartRequest);
                // 每次上传分片之后,OSS的返回结果包含PartETag。PartETag将被保存在partETags中。
                partETags.add(uploadPartResult.getPartETag());

                /*
                耗时记录输出
                 */
                Date dateBlock = new Date();
                SimpleDateFormat sdfBlock = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
                String blockBeginTime = sdf.format(date);
                long block = System.currentTimeMillis();
                logger.info("第" + i + "块block上传时间:" + blockBeginTime + "当前时间戳:" + block + ",耗时:" + (block - test));
                test = block;
            }
            /**
             * 创建CompleteMultipartUploadRequest对象。
             * 在执行完成分片上传操作时,需要提供所有有效的partETags。OSS收到提交的partETags后,会逐一验证每个分片的有效性。
             * 当所有的数据分片验证通过后,OSS将把这些分片组合成一个完整的文件。
             */

            //设置ContentType,使得返回的url可以在网页中预览

            CompleteMultipartUploadRequest uploadRequest = new CompleteMultipartUploadRequest(aliyunOssConfig.getBucket(), cloudPath, uploadId, partETags);
            // 在完成文件上传的同时设置文件访问权限。
            uploadRequest.setObjectACL(CannedAccessControlList.PublicRead);
            // 完成上传。
            ossClient.completeMultipartUpload(uploadRequest);
            String url = ossClient.generatePresignedUrl(aliyunOssConfig.getBucket(), cloudPath, expiration).toString();
            //去掉url尾部的Expires信息、OSSAccessKeyId信息以及Signature信息
            url = url.substring(0, url.indexOf("?"));
            // 关闭OSSClient。
            ossClient.shutdown();

            Date date2 = new Date();
            SimpleDateFormat sdf2 = new SimpleDateFormat("yyyy-MM-ddHH:mm:ss");
            String endTime = sdf2.format(date2);
            long l2 = System.currentTimeMillis();
            logger.info("fileUploadZone-结束上传时间:" + endTime + " 总耗时:" + (l2 - l1) + "ms");
            Map<String, Object> map = new HashMap<>();
            map.put("url", url);
            map.put("name", fileName);
            return Result.success(map);

        } catch (Exception e) {
            e.printStackTrace();
            ossClient.shutdown();
            //logger.error(e.getMessage());
            return Result.fail(111111, "操作失败!");
        }
    }

 

 

最后附上Controller层代码,注解生效。这里指贴了一个controller,测试用例写了好几种,其实本质都差不多

@PostMapping("/uploadByThreads")
    @FileLimit(max = 30,maxRequestSize = 1000,fileFormat = "images",unit = FileLimitUnit.MB)
    public Result uploadImages(@RequestParam("images") List<MultipartFile> multipartFile) {

        // 文件上传,获取上传得到的图片地址返回
        List<String> responseUrls = ossService.uploadImages(multipartFile);
        return Result.success(responseUrls);
    }

 

ApiPost测试接口:可以看到已经成功了

 

参考

[1]【OSS】SpringBoot搭配线程池整合阿里云OSS实现图片异步上传--陈宝子

[2]springboot整合阿里云oss上传文件(图片或视频)--热河不是河

[3]阿里云oss分片上传,大文件上传--潜水的章鱼

[4]Multipart自定义资源限制文件大小限制设计-aop切面切入Multipart的文件大小拦截

 

 完整代码放在gitee了。【测试完整代码 在此】