ps:时隔两年了没有更新博客了,今天出一篇关于又拍云Java客户端封装的工具类;都自己写的,里面的部分缺失的部分可以根据代码上下文猜出来的,就不发了。
pom.xml:
<dependency>
<groupId>com.upyun</groupId>
<artifactId>java-sdk</artifactId>
<version>4.2.3</version>
</dependency>
又拍云工具类: UpyOssUtil
package com.xx.util;
import cn.hutool.core.date.StopWatch;
import cn.hutool.core.lang.Snowflake;
import cn.hutool.core.util.ReflectUtil;
import com.google.common.base.Splitter;
import com.upyun.ParallelUploader;
import com.upyun.RestManager;
import com.upyun.UpYunUtils;
import lombok.extern.slf4j.Slf4j;
import okhttp3.Response;
import java.io.File;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiFunction;
import java.util.stream.Collectors;
/**
* 又拍云 对象存储工具类
* <p>
* 注:接口返回的如http://xxxx.test.upcdn.net/test/20211215135525.png
* 如果调用api的时候,添加了content-secret(文件秘钥仅支持大小写的 A-Z 以及数字 0-9 不支持符号),那么就代表这个地址的文件是私有的,
* 直接用http://xxxx.test.upcdn.net/test/20211215135525.png是只会得到404,
* 需要用如:http://xxxx.test.upcdn.net/test/20211215135525.png!abc(!:间隔符,abc:content-secret的值)
*
* @author lucifer
*/
@Slf4j
public class UpyOssUtil {
/**
* 构造对象缓存
*/
private static final SimpleCache<Class<?>, Object> INSTANCE_CACHE = new SimpleCache<>();
/**
* 上传
*
* @param upYunConf 又拍云必填配置
* @param params 又拍云的可选参数(参考:{@link com.upyun.Params})
* @param biFunction function
* @return
*/
private static <T> List<String> upload(UpYunConf upYunConf, Map<String, String> params, Class<T> clazz, BiFunction<T, Map<String, String>, List<String>> biFunction) {
//校验参数
check(StrUtils.isNotBlank(upYunConf.getNameSpace()) || StrUtils.isNotBlank(upYunConf.getUserName()) || StrUtils.isNotBlank(upYunConf.getPassword()) || StrUtils.isNotBlank(upYunConf.getAddress())
, "upYunConf中必填属性可能存在空值");
//获取对象实例(先从缓存中获取)
T instance = getInstance(clazz, upYunConf.getNameSpace(), upYunConf.getUserName(), upYunConf.getPassword());
List<String> urlList = biFunction.apply(instance, params);
if (CollUtil.isNotEmpty(urlList)) {
return urlList.stream().map(url -> {
if (isRewriteUrl(url)) {
//得到的URL如:https://v0.api.upyun.com/mall-cert/test/20211215135525.png
//需要进行改写:又拍云控制台提供的域名/目录/文件名.文件后缀名 如:http://mall-cert.test.upcdn.net/test/20211215135525.png
Object bucketName = ReflectUtil.getFieldValue(instance, "bucketName");
return removeFutileSlash(StrUtils.join(getScheme(upYunConf.getAddress()), StrUtils.substring(url, bucketName.toString())));
}
return url;
}).collect(Collectors.toList());
}
return urlList;
}
/**
* 上传
*
* @param upYunConf 又拍云配置
* @param uploadDirPath 上传至又拍云的目录路径 (/test /test/a/)
* @param data 文件字节数组
* @param fileName 文件名,包含文件名后缀(.jpg .txt .png) 注:文件名重复,会出现覆盖,如果该字段不传,那么fileSuffix就必填
* @param fileSuffix 文件名后缀(.jpg .txt .png),如果文件名fileName参数传值了,这个字段可以不传了,优先级fileName高
* @param params 又拍云的可选参数(参考:{@link com.upyun.Params})
* @param checkMD5 是否校验md5
* // 设置待上传文件的 Content-MD5 值
* // 如果又拍云服务端收到的文件MD5值与用户设置的不一致,将会报 406 NotAcceptable 错误
* @return
*/
public static String upload(UpYunConf upYunConf, String uploadDirPath, byte[] data, String fileName, String fileSuffix, Map<String, String> params, boolean checkMD5) {
return upload(upYunConf, params, RestManager.class, (restManager, map) -> {
String url = "";
try {
//校验
check(isDirectory(uploadDirPath), "上传至又拍云的目录路径不合法,请校验");
check(StrUtils.isNotBlank(fileName) || StrUtils.isNotBlank(fileSuffix), "文件名fileName或者文件名后缀fileSuffix,两个字段必填其中一个");
String fileNameTemp = fileName;
if (StrUtils.isBlank(fileNameTemp)) {
fileNameTemp = StrUtils.join(new Snowflake().nextIdStr(), fileSuffix);
}
String uploadPath = uploadDirPath.endsWith("/") ? StrUtils.join(uploadDirPath, fileNameTemp) : StrUtils.join(uploadDirPath, "/", fileNameTemp);
//是否校验md5
if (checkMD5) {
params.put(RestManager.PARAMS.CONTENT_MD5.getValue(), UpYunUtils.md5(data));
}
Response response = restManager.writeFile(uploadPath, data, map);
if (response.isSuccessful()) {
url = response.request().url().toString();
}
log.debug("文件上传 message:{}", response.message());
} catch (Exception e) {
log.error("文件上传失败:error:{}", e);
}
return Collections.singletonList(url);
}).get(0);
}
/**
* 上传
*
* @param bucketName 桶
* @param userName 用户名
* @param password 密码,需要MD5加密
* @param hosts 如:www.baidu.com
* @param uploadDirPath uploadDirPath 上传至又拍云的目录路径 (/test /test/a/)
* @param data 字节数组
* @param fileSuffix 文件后缀名
* @param params 又拍云的可选参数(参考:{@link com.upyun.Params})
* @return
*/
public static String upload(String bucketName, String userName, String password, String hosts, String uploadDirPath, byte[] data, String fileSuffix, Map<String, String> params) {
return upload(UpYunConf.buildUpYunConf(bucketName, userName, password, hosts), uploadDirPath, data, null, fileSuffix, params, true);
}
/**
* 上传
*
* @param bucketName 桶
* @param userName 用户名
* @param password 密码,需要MD5加密
* @param hosts 如:www.baidu.com
* @param uploadDirPath 上传至又拍云的目录路径 (/test /test/a/)
* @param data 字节数组
* @param fileSuffix 文件后缀名
* @return
*/
public static String upload(String bucketName, String userName, String password, String hosts, String uploadDirPath, byte[] data, String fileSuffix) {
return upload(UpYunConf.buildUpYunConf(bucketName, userName, password, hosts), uploadDirPath, data, null, fileSuffix, new HashMap<>(), true);
}
/**
* 上传,可以自己设置是否校验md5,如果文件过大,建议设置为false
*
* @param upYunConf 又拍云配置
* @param uploadDirPath 上传至又拍云的目录路径 (/test /test/a/)
* @param dataList 文件字节数组
* @param fileSuffix 文件名后缀(.jpg .txt .png)
* @param checkMD5 是否校验md5
* // 设置待上传文件的 Content-MD5 值
* // 如果又拍云服务端收到的文件MD5值与用户设置的不一致,将会报 406 NotAcceptable 错误
* @return
*/
public static List<String> upload(UpYunConf upYunConf, String uploadDirPath, List<byte[]> dataList, String fileSuffix, boolean checkMD5) {
return upload(upYunConf, uploadDirPath, dataList, fileSuffix, new HashMap<>(), checkMD5);
}
/**
* 上传 (默认校验md5,如果文件过大,不建议使用该方法)
*
* @param upYunConf 又拍云配置
* @param uploadDirPath 上传至又拍云的目录路径 (/test /test/a/)
* @param dataList 文件字节数组
* @param fileSuffix 文件名后缀(.jpg .txt .png)
* @return
*/
public static List<String> upload(UpYunConf upYunConf, String uploadDirPath, List<byte[]> dataList, String fileSuffix) {
return upload(upYunConf, uploadDirPath, dataList, fileSuffix, new HashMap<>(), true);
}
/**
* 上传
*
* @param upYunConf 又拍云配置
* @param uploadDirPath 上传至又拍云的目录路径 (/test /test/a/)
* @param dataList 文件字节数组
* @param fileSuffix 文件名后缀(.jpg .txt .png)
* @param params 又拍云的可选参数(参考:{@link com.upyun.Params})
* @param checkMD5 是否校验md5
* // 设置待上传文件的 Content-MD5 值
* // 如果又拍云服务端收到的文件MD5值与用户设置的不一致,将会报 406 NotAcceptable 错误
* @return
*/
public static List<String> upload(UpYunConf upYunConf, String uploadDirPath, List<byte[]> dataList, String fileSuffix, Map<String, String> params, boolean checkMD5) {
List<String> resultList = new ArrayList<>();
try {
List<Callable<String>> tasks = new ArrayList<>();
for (byte[] bytes : dataList) {
Callable<String> callable = () -> upload(upYunConf, uploadDirPath, bytes, null, fileSuffix, params, checkMD5);
tasks.add(callable);
}
//提交任务
resultList.addAll(submit(tasks));
} catch (Exception ex) {
log.error("文件上传失败:error:{}", ex);
}
return resultList;
}
/**
* 上传 (默认校验md5,如果文件过大,不建议使用该方法,选择可以将md5校验设置为false的方法)
*
* @param bucketName 桶
* @param userName 用户名
* @param password 密码,需要MD5加密
* @param hosts 如:www.baidu.com
* @param uploadPath 上传文件路径(如果上传文件路径:/test,文件名后缀:.jpg,那么就会后台算法生成文件名,得到的路径如:/test/123.jpg;如果上传文件的路径是/test/123.jpg,那么参数fileSuffix可以为空)
* @param dataList 文件字节数组
* @param fileSuffix 文件名后缀(.jpg .txt .png)
* @return
*/
public static List<String> upload(String bucketName, String userName, String password, String hosts, String uploadPath, List<byte[]> dataList, String fileSuffix) {
return upload(UpYunConf.buildUpYunConf(bucketName, userName, password, hosts), uploadPath, dataList, fileSuffix, new HashMap<>(), true);
}
/**
* 上传 (默认校验md5,如果文件过大,不建议使用该方法,选择可以将md5校验设置为false的方法)
*
* @param bucketName 桶
* @param userName 用户名
* @param password 密码,需要MD5加密
* @param hosts 如:www.baidu.com
* @param uploadPath 上传文件路径(如果上传文件路径:/test,文件名后缀:.jpg,那么就会后台算法生成文件名,得到的路径如:/test/123.jpg;如果上传文件的路径是/test/123.jpg,那么参数fileSuffix可以为空)
* @param dataList 文件字节数组
* @param fileSuffix 文件名后缀(.jpg .txt .png)
* @param checkMD5 是否校验md5
* // 设置待上传文件的 Content-MD5 值
* // 如果又拍云服务端收到的文件MD5值与用户设置的不一致,将会报 406 NotAcceptable 错误
* @return
*/
public static List<String> upload(String bucketName, String userName, String password, String hosts, String uploadPath, List<byte[]> dataList, String fileSuffix, boolean checkMD5) {
return upload(UpYunConf.buildUpYunConf(bucketName, userName, password, hosts), uploadPath, dataList, fileSuffix, new HashMap<>(), checkMD5);
}
/**
* 上传 (默认校验md5,如果文件过大,不建议使用该方法,选择可以将md5校验设置为false的方法)
*
* @param bucketName 桶
* @param userName 用户名
* @param password 密码,需要MD5加密
* @param hosts 如:www.baidu.com
* @param uploadDirPath 上传至又拍云的目录路径 (/test /test/a/)
* @param files 多文件
* @param params 又拍云的可选参数(参考:{@link com.upyun.Params})
* @return
*/
public static List<String> parallelUpload(String bucketName, String userName, String password, String hosts, String uploadDirPath, List<File> files, Map<String, String> params) {
return parallelUpload(UpYunConf.buildUpYunConf(bucketName, userName, password, hosts), uploadDirPath, files, params);
}
/**
* 上传
*
* @param bucketName 桶
* @param userName 用户名
* @param password 密码,需要MD5加密
* @param hosts 如:www.baidu.com
* @param uploadDirPath 上传至又拍云的目录路径 (/test /test/a/)
* @param files 多文件
* @return
*/
public static List<String> parallelUpload(String bucketName, String userName, String password, String hosts, String uploadDirPath, List<File> files) {
return parallelUpload(UpYunConf.buildUpYunConf(bucketName, userName, password, hosts), uploadDirPath, files, new HashMap<>());
}
/**
* 上传
*
* @param upYunConf 又拍云配置
* @param uploadDirPath 上传至又拍云的目录路径 (/test /test/a/)
* @param files 多文件
* @return
*/
public static List<String> parallelUpload(UpYunConf upYunConf, String uploadDirPath, List<File> files) {
return parallelUpload(upYunConf, uploadDirPath, files, new HashMap<>());
}
/**
* 上传
*
* @param upYunConf 又拍云配置
* @param uploadDirPath 上传至又拍云的目录路径 (/test /test/a/)
* @param files 多文件
* @param params 又拍云的可选参数(参考:{@link com.upyun.Params})
* @return
*/
public static List<String> parallelUpload(UpYunConf upYunConf, String uploadDirPath, List<File> files, Map<String, String> params) {
return parallelUpload(upYunConf, uploadDirPath, files, params, true, true);
}
/**
* 上传
*
* @param upYunConf 又拍云配置
* @param uploadDirPath 上传至又拍云的目录路径 (/test /test/a/)
* @param files 多文件
* @param checkMD5 是否校验md5,如果文件过大,建议设置为false
* // 设置待上传文件的 Content-MD5 值
* // 如果又拍云服务端收到的文件MD5值与用户设置的不一致,将会报 406 NotAcceptable 错误
* @param isUseGenerateFileName 是否使用算法生成文件名,TRUE 使用方法中的算法生成文件名;false 则使用文件本身的文件名
* @return
*/
public static List<String> parallelUpload(UpYunConf upYunConf, String uploadDirPath, List<File> files, boolean checkMD5, boolean isUseGenerateFileName) {
return parallelUpload(upYunConf, uploadDirPath, files, new HashMap<>(), checkMD5, isUseGenerateFileName);
}
/**
* 上传
*
* @param upYunConf 又拍云配置
* @param uploadDirPath 上传至又拍云的目录路径 (/test /test/a/)
* @param files 多文件
* @param isUseGenerateFileName 是否使用算法生成文件名,TRUE 使用方法中的算法生成文件名;false 则使用文件本身的文件名
* @return
*/
public static List<String> parallelUpload(UpYunConf upYunConf, String uploadDirPath, List<File> files, boolean isUseGenerateFileName) {
return parallelUpload(upYunConf, uploadDirPath, files, new HashMap<>(), true, isUseGenerateFileName);
}
/**
* 上传
*
* @param upYunConf 又拍云配置
* @param uploadDirPath 上传至又拍云的目录路径 (/test /test/a/)
* @param files 多文件
* @param params 又拍云的可选参数(参考:{@link com.upyun.Params})
* @param checkMD5 是否校验md5,如果文件过大,建议设置为false
* // 设置待上传文件的 Content-MD5 值
* // 如果又拍云服务端收到的文件MD5值与用户设置的不一致,将会报 406 NotAcceptable 错误
* @param isUseGenerateFileName 是否使用算法生成文件名,TRUE 使用方法中的算法生成文件名;false 则使用文件本身的文件名
* @return
*/
public static List<String> parallelUpload(UpYunConf upYunConf, String uploadDirPath, List<File> files, Map<String, String> params, boolean checkMD5, boolean isUseGenerateFileName) {
return upload(upYunConf, params, ParallelUploader.class, (parallelUploader, map) -> {
List<String> resultList = new ArrayList<>();
try {
check(isDirectory(uploadDirPath), "上传至又拍云的目录路径不合法,请校验");
//设置上传进度监听
parallelUploader.setOnProgressListener((index, total) -> log.debug("文件上传中=====:index::{},total::{}", index, index * 100 / total + "%"));
//设置 MD5 校验
parallelUploader.setCheckMD5(checkMD5);
//利用线程池,多文件进行上传
List<Callable<String>> tasks = new ArrayList<>();
for (File file : files) {
Callable<String> callable = () -> {
String fileName = file.getName();
//如果使用方法中算法生成的文件名(注:文件名重复,会出现覆盖)
if (isUseGenerateFileName) {
fileName = StrUtils.join(new Snowflake().nextIdStr(), getFileSuffix(file));
}
String uploadPath = uploadDirPath.endsWith("/") ? StrUtils.join(uploadDirPath, fileName) : StrUtils.join(uploadDirPath, "/", fileName);
boolean upload = parallelUploader.upload(file.getPath(), uploadPath, params);
if (upload) {
return removeFutileSlash(StrUtils.join(getScheme(upYunConf.getAddress()), uploadPath));
}
return "";
};
tasks.add(callable);
}
//提交任务
resultList.addAll(submit(tasks));
} catch (Exception e) {
log.error("文件上传失败:error:{}", e);
}
return resultList;
});
}
/**
* 下载(response.body() 包含文件流信息)
*
* @param upYunConf 又拍云配置
* @param filePath 文件路径 /test/1484464385422077952.jpg
* @return
*/
public static byte[] read(UpYunConf upYunConf, String filePath) {
RestManager restManager = new RestManager(upYunConf.getNameSpace(), upYunConf.getUserName(), upYunConf.getPassword());
try {
Response result = restManager.readFile(filePath);
if (result.isSuccessful()) {
return result.body().bytes();
}
} catch (Exception ex) {
log.error("文件读取失败:{}", ex);
}
return null;
}
/**
* 下载文件,返回未加如data:image/png;base64前缀的字符串(使用Base64编码方案将指定的字节数组编码为字符串)
*
* @param upYunConf 又拍云配置
* @param filePath 又拍云文件路径 /test/1484464385422077952.jpg
* @return
*/
public static String readImage(UpYunConf upYunConf, String filePath) {
byte[] read = read(upYunConf, filePath);
return Base64.getEncoder().encodeToString(read);
}
/**
* 下载文件,返回未加如data:image/png;base64前缀的字符串(使用Base64编码方案将指定的字节数组编码为字符串)
*
* @param spec 要解析为 URL的字符
* @return
*/
public static String readImage(String spec) {
return Base64.getEncoder().encodeToString(IoUtil.readBytes(read(spec)));
}
/**
* 下载文件,使用Base64编码方案将指定的字节数组编码为字符串,并在前面加上类似前缀data:image/png;base64,
*
* @param upYunConf
* @param filePath 文件后缀名 如: jpg,不带.符号
* @return
*/
public static String base64StrToImage(UpYunConf upYunConf, String filePath) {
return StrUtils.join("data:image/", getFileSuffix(filePath), ";base64,", readImage(upYunConf, filePath));
}
/**
* 下载文件,使用Base64编码方案将指定的字节数组编码为字符串,并在前面加上类似前缀data:image/png;base64,
*
* @param spec 要解析为 URL的字符串
* @param fileSuffix 文件后缀名 如: jpg,不带.符号
* @return
*/
public static String base64StrToImage(String spec, String fileSuffix) {
//从要解析为 URL的字符串中获取文件后缀名,由于该文件设置了content-secret,所以获取文件后缀名,就会有pem!abc123,需要去掉content-secret(!abc123)
return StrUtils.join("data:image/", StrUtils.isNotBlank(fileSuffix) ? fileSuffix : getFileSuffix(spec), ";base64,", readImage(spec));
}
/**
* 下载文件,使用Base64编码方案将指定的字节数组编码为字符串,并在前面加上类似前缀data:image/png;base64,
*
* @param spec 要解析为 URL的字符串
* @return
*/
public static String base64StrToImage(String spec) {
return base64StrToImage(spec, null);
}
/**
* 下载文件
*
* @param spec 要解析为 URL的字符串
* @return
*/
public static InputStream read(String spec) {
return FileUtil.getFileInputStream(spec);
}
/**
* 获取文件后缀名(带.)
*
* @param file 文件
* @return
*/
private static String getFileSuffix(File file) {
return "." + FileUtil.getSuffix(file);
}
/**
* 获取文件后缀名(不带.)
*
* @param filePath 文件路径
* @return
*/
private static String getFileSuffix(String filePath) {
String fileSuffix = FileUtil.getSuffix(filePath);
//标识符可为半角字符:“!”,“-”,“_” 三种,可以在管理中进行更改。(默认是"!")
if (fileSuffix.contains("!")) {
fileSuffix = StrUtils.sub(fileSuffix, 0, "!");
} else if (fileSuffix.contains("-")) {
fileSuffix = StrUtils.sub(fileSuffix, 0, "-");
} else if (fileSuffix.contains("_")) {
fileSuffix = StrUtils.sub(fileSuffix, 0, "_");
}
return fileSuffix;
}
/**
* 是否是又拍云的路径
* 如:
* /test/ true
* /test true
* /test/a.txt false
* /test/a.jpg false
*
* @return
*/
private static boolean isDirectory(String path) {
//这里简单判断下,路径包含1个及以上的/,并且不带.
return StrUtils.countShow(path, "/") >= 1 & !path.contains(".");
}
/**
* 判断是否为true,为false则抛异常
*
* @param flag
* @param msg
* @param <T>
*/
public static <T> void check(boolean flag, String msg) {
if (!flag) {
throw new IllegalArgumentException(msg);
}
}
/**
* 是否需要重写URL
*
* @param url
* @return
*/
private static boolean isRewriteUrl(String url) {
List<String> apiDomainList = Arrays.asList(RestManager.ED_AUTO, RestManager.ED_CNC, RestManager.ED_CTT, RestManager.ED_TELECOM);
for (String apiDomain : apiDomainList) {
if (url.contains(apiDomain)) {
return true;
}
}
return false;
}
/**
* 将URL中无用的//替换成为/
* 如:https://xx//test/20211215135525.png 替换成 https:/xx/test/20211215135525.png
*
* @param text
* @return
*/
private static String removeFutileSlash(String text) {
//如果url中出现了一次以上的//,则拼接有问题
if (StrUtils.countShow(text, "//") > 1) {
//则将第二次及其后面的//替换成/
//以双斜杠拆分
List<String> strList = Splitter.on("//").splitToList(text);
//再将如http://或者https://与后面的字符串用/拼接
text = StrUtils.join(strList.stream().findFirst().get(), "//", strList.stream().skip(1).collect(Collectors.joining("/")));
}
return text;
}
/**
* "http" or "https".
*
* @return
*/
private static String getScheme(String hosts) {
return hosts.startsWith("http") || hosts.startsWith("https") ? hosts : "http://" + hosts;
}
/**
* 利用反射获取构造方法,并且实例化对象
* 这里返回的实例对象(可能会是RestManager、SerialUploader、ParallelUploader等其中一个)
*
* @param clazz 类
* @param args 参数
* @param <T>
* @return
*/
private static <T> T getInstance(Class<T> clazz, Object... args) {
//先从缓存中获取
T t = (T) INSTANCE_CACHE.get(clazz);
try {
if (Objects.isNull(t)) {
Constructor<T> constructor = ReflectUtil.getConstructor(clazz, String.class, String.class, String.class);
T newInstance = constructor.newInstance(args);
INSTANCE_CACHE.put(clazz, newInstance);
return newInstance;
}
} catch (Exception e) {
log.error("获取构造方法失败,error:{}", e);
}
return t;
}
/**
* 线程池 提交任务
*
* @return
*/
public static List<String> submit(List<Callable<String>> tasks) throws Exception {
List<String> result = new ArrayList<>();
StopWatch stopWatch = new StopWatch("耗时统计:");
AtomicInteger taskNum = new AtomicInteger();
int availableProcessors = Runtime.getRuntime().availableProcessors();
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(availableProcessors,
availableProcessors + 1,
60,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(50),
new ThreadPoolExecutor.CallerRunsPolicy());
try {
for (Callable<String> task : tasks) {
stopWatch.start(StrUtils.join("task:", taskNum.addAndGet(1)));
result.add(threadPoolExecutor.submit(task).get());
stopWatch.stop();
}
return result;
} finally {
//打印耗时结果
log.debug(stopWatch.prettyPrint(TimeUnit.MILLISECONDS));
threadPoolExecutor.shutdown();
}
}
}