文章目录
前言
在之前写过阿里OSS图片上传的案例和文章【OSS】服务端签名后直传实现阿里云存储上传文件,但是最近又发现了一个新的业务需求场景,简单流程如下:
- 前端传递单个或多个图片到后端;
- 后端对图片进行处理,并上传至图床;
- 上传完毕之后,返回图片链接给前端。
因此在这里写上这一篇文章进行补充,同时阿里云的OSS前置操作如注册和配置操作,和之前的文章:【OSS】服务端签名后直传实现阿里云存储上传文件之中的章节1到3.1
相同,因此这里不再过多重复了。
1、配置类编写
1.1、线程池配置
在刚开始思考这个功能的时候一个很容易的方案就浮现到眼前,即将获取到的文件集合按顺序逐一上传到OSS之中,但是当我们的文件过多,或者网络不良时,容易造成上传时间过长,极其不优雅。
因此,这里选用了线程池的方式对文件上传任务进行异步操作,提高处理速度。在编写过程中做过简单的几次比较,使用线程池和未使用之前的响应时间竟相差了五十倍以上,就离谱,果断选择线程池操作。线程池相关的文章可查看前段时间整理的文章【多线程】优雅使用线程池结合CompletableFuture实现异步编排
操作步骤如下:
- 首先在config包下添加线程池配置类ThreadPoolConfig:
@Configuration
public class ThreadPoolConfig {
@Bean
public ThreadPoolExecutor threadPoolExecutor(
@Value("${thread.pool.coreSize}") Integer coreSize,
@Value("${thread.pool.maxSize}") Integer maxSize,
@Value("${thread.pool.keepalive}") Integer keepalive,
@Value("${thread.pool.blockQueueSize}") Integer blockQueueSize
) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(
coreSize,
maxSize,
keepalive,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(blockQueueSize),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
executor.prestartCoreThread();
return executor;
}
}
- 配置文件中配置线程池的参数配置
thread:
pool:
# 核心线程数
coreSize: 10
# 最大线程数
maxSize: 50
# 非核心线程的存活时间
keepalive: 30
# 阻塞队列大小
blockQueueSize: 200
1.2、OSS配置
这里将阿里云OSS所需要的参数封装到了一个配置类中,并于配置文件所关联,具体步骤如下:
- 添加配置类AliyunOssConfig
@ConfigurationProperties(prefix = "aliyun.oss")
@Data
@Configuration
public class AliyunOssConfig {
private String endpoint;
private String bucket;
private String accessKey;
private String secretKey;
private String marketHost;
@Bean
public OSS ossClient() {
return new OSSClientBuilder().build(endpoint, accessKey, secretKey);
}
}
- 配置文件中设置配置参数
aliyun:
oss:
access-key: 你的AK
secret-key: 你的AS
endpoint: 你的地域节点
bucket: 你的Bucket名
market-host: "https://${aliyun.oss.bucket}.${aliyun.oss.endpoint}"
2、工具类编写
因为在这一块图片上传中涉及到的逻辑繁多,因此将其进行拆分,然后统一封装到工具类AliyunOssUtils之中,因此大家伙在开始之前记得在项目中新建一个工具包,将其添加进去。
@Slf4j
@Component
public class AliyunOssUtils {
}
2.1、图片判断
2.1.1、需求分析
因为这里使用的OSS承担的是图床功能,因此在上传到OSS之前,应先对文件进行判断,只有判断为图片之后才能够继续上传,否则返回错误信息。
这里图片判断使用的是ImageI类进行图片判断,这个是Java自带的一个图片处理的类,存在于javax包中。但是其具有局限性,目前发现支持的图片后缀有bmp/gif/jpg/jfif/jpeg/png
,虽然主流的图片后缀大部分都包含在内,但是并没有webp
格式,这是谷歌后续推出的图片格式,能够无损压缩图片大小至png的2/3。
在查询过资料后,发现只需要添加一个对webp格式操作的依赖进行拓展即可,整体步骤如下:
2.1.2、依赖导入
<!--webp格式工具-->
<dependency>
<groupId>org.sejda.imageio</groupId>
<artifactId>webp-imageio</artifactId>
<version>0.1.6</version>
</dependency>
2.1.3、逻辑实现
/**
* @param imageFile 需要判断的文件
* @return boolean
* @description 判断文件是否为图片
* @method isImage
* @author xbaozi
* @date 2022/9/13 13:25
**/
public 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();
}
}
2.2、MultipartFile类型转换
2.2.1、需求分析
因为在接收前端数据时使用的是Spring家的MultipartFile类型,但是ImageIO使用的是Java自带的File类型,因此需要编写工具方法将两种类型进行转换(强转不了的,我试过了,很安详)
2.2.2、逻辑实现
/**
* @return java.io.File
* @description MultipartFile转File
* @method multipartFileToFile
* @author xbaozi
* @date 2022/9/13 13:33
**/
public 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;
}
2.3、图片上传
2.3.1、需求分析
重头戏来了。在线程池配置的时候就提及到,顺序阻塞式上传图片相对于线程池来说犹如龟速,因此这一块将会使用线程池对图片上传任务进行操作。
2.3.2、前置操作
因为在图片上传中需要获取图片的后缀名,因此这里将获取后缀名的逻辑封装成了一个方法进行调用,获取出来的后缀名是待了小点的,如.png
。
/**
* @param filename 文件名
* @description 获取文件拓展名
* @method getExtensionName
* @author xbaozi
* @date 2022/9/21 21:21
**/
private 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;
}
2.3.3、逻辑实现
@Autowired
private OSS ossClient;
@Autowired
private AliyunOssConfig aliyunOssConfig;
@Autowired
private ThreadPoolExecutor executor;
/**
* @param multipartFile 需要上传的图片,可变参数
* @description 上传图片
* @method upload
* @author xbaozi
* @date 2022/9/13 13:21
**/
public List<String> upload(List<MultipartFile> multipartFile) {
log.info("开始上传照片……");
// 图片上传至阿里云OSS,设置返回路径集合
List<String> responseUrls = new ArrayList<>();
// 用户上传文件时指定的前缀,即存放在以时间命名的文件夹内
String dir = new SimpleDateFormat("yyyy-MM-dd").format(new Date());
CompletableFuture<PutObjectResult> future = null;
for (MultipartFile file : multipartFile) {
String originalFilename = file.getOriginalFilename();
// 设置上传到云存储的文件名,规则为"当前时间-UUID.源文件后缀名"
String cloudFileName = new StringBuilder()
.append(new SimpleDateFormat("HH:mm:ss").format(new Date()))
.append("-")
.append(IdUtil.simpleUUID())
//.append("-")
//.append(originalFilename)
.append(getExtensionName(originalFilename))
.toString();
// 设置上传到云存储的路径
String cloudPath = dir + "/" + cloudFileName;
// 线程池异步上传图片
future = CompletableFuture.supplyAsync(() -> {
try {
// 图片上传
return ossClient.putObject(aliyunOssConfig.getBucket(), cloudPath, file.getInputStream());
} catch (IOException e) {
throw new FileUploadException(
ResponseCodeEnum.FILE_UPLOAD_FAIL.getCode(),
ResponseCodeEnum.FILE_UPLOAD_FAIL.getMessage() + ":" + originalFilename);
}
}, executor).whenComplete((res, exception) -> {
// 判断是否出现异常
if (!ObjectUtils.isEmpty(exception)) {
throw new FileUploadException(
ResponseCodeEnum.NETWORK_FAILURE.getCode(),
ResponseCodeEnum.NETWORK_FAILURE.getMessage() + ":" + originalFilename);
}
// 判断是否正确返回,将图片链接添加到集合中
if (!ObjectUtils.isEmpty(res)) {
log.info("处理照片{}", aliyunOssConfig.getMarketHost() + "/" + cloudPath);
responseUrls.add(aliyunOssConfig.getMarketHost() + "/" + cloudPath);
}
});
}
// 等待线程执行完毕
if (future != null) {
future.join();
}
// 返回上传的图片链接集合
log.info("返回的路径为{}", responseUrls);
return responseUrls;
}
3、控制器编写
3.1、需求分析
在控制器中主要是对前端传递过来的文件集合进行处理判断,主要包含以下部分:
- 判断集合是否为空;
- 判断集合元素是否为空;
- 判断集合元素是否为图片;
- 上传文件过程中是否出现异常。
3.2、逻辑实现
@RestController
@RequestMapping("/api/oss")
@Api("阿里云oss云存储控制器")
@Slf4j
public class OssController {
@Autowired
private AliyunOssUtils ossUtils;
@PostMapping("/upload")
@ApiOperation("多图片上传")
public ResponseResult<List<String>> upload(@ApiParam("需要上传的图片") List<MultipartFile> multipartFile) {
// 判空
if (ObjectUtils.isEmpty(multipartFile)) {
throw new FileUploadException(ResponseCodeEnum.FILE_IS_EMPTY.getCode(), ResponseCodeEnum.FILE_IS_EMPTY.getMessage());
}
// 图片校验
for (MultipartFile file : multipartFile) {
// 图片为空
if (file.isEmpty()) {
return new ResponseResult<>(ResponseCodeEnum.FILE_IS_CORRUPTED.getCode(), ResponseCodeEnum.FILE_IS_CORRUPTED.getMessage());
}
// 判断是否为图片,防止恶意使用
File multipartFileToFile = ossUtils.multipartFileToFile(file);
if (!ossUtils.isImage(multipartFileToFile)) {
return new ResponseResult<>(ResponseCodeEnum.FORMAT_MISMATCH.getCode(), ResponseCodeEnum.FORMAT_MISMATCH.getMessage() + ",只支持bmp/gif/jpg/jfif/jpeg/png/webp格式");
}
}
// 文件上传,获取上传得到的图片地址返回
List<String> responseUrls = ossUtils.upload(multipartFile);
return new ResponseResult<>(ResponseCodeEnum.OK.getCode(), ResponseCodeEnum.OK.getMessage(), responseUrls);
}
}
4、结果演示
这里主要测试两种图片格式,分别是主流的png和特殊的webp图片后缀文件。
- png格式
- webp格式