springboot整合阿里云OSS实现多线程下文件上传(aop限制文件大小和类型)
内容涉及:
-
-
自定义注解及aop的使用:对上传文件格式(视频格式、图片格式)、不同类型文件进行大小限制(视频和图片各自自定义大小)
-
线程池使用:阿里云OSS多线程上传文件
-
业务需求
需求一:
-
前端传递单个或多个小文件(这里以图片为例)到后端;
-
后端对图片进行处理,并上传至阿里云oss;
-
上传完毕之后,返回图片链接给前端,如果格式支持,可以在线预览。
需求二:
-
前端上传大文件(这里以视频为例)到后端;
-
后端对视频进行分片处理,上传到oss;
-
上传完毕后,返回视频连接给前端。
过滤文件
主要使用了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;
}
可以根据实际需求,这里设置了注解的几个参数,分别是
max()/value()
,表示文件最大限制,默认为5,maxRequestSize()
,单次请求的总文件大小,即一次请求上传的多个文件的总大小,默认为50fileFormat()
,表示文件格式,默认是图片"images",如果上传视频的话需要改成"videos",文件类型可以根据需求自定义,修改getFileFormatLimit()
方法即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测试接口:可以看到已经成功了
参考
完整代码放在gitee了。【测试完整代码 在此】