本文主要对SpringBoot2.x集成七牛云对象存储Kodo进行简单总结,其中SpringBoot使用的2.4.5
版本。
一、七牛云对象存储Kodo简介
七牛云对象存储Kodo是七牛云提供的高可靠、强安全、低成本、可扩展的存储服务。您可通过控制台、API、SDK等方式简单快速地接入七牛存储服务,实现海量数据的存储和管理。通过Kodo可以进行文件的上传、下载和管理。
七牛云对象存储Kodo官方文档:https://developer.qiniu.com/kodo。
二、准备工作
1.注册七牛云并认证
首先需要在七牛云官网注册七牛云账号,然后对账号进行认证,之后才可以在对象存储中新建存储空间。
2.新建空间
在对象存储的空间管理中新建空间:
这里新建了两个用于测试的空间:
在空间概览中可以获取一个CDN测试域名,这里使用测试域名来测试:
测试域名只能用于测试,生成后30天后会被回收。如果是生产环境,需要配置域名,可以在域名管理中绑定备案过的域名:
3.七牛云密钥
在个人中心的密钥管理下获取七牛云密钥:
三、集成七牛云对象存储kodo
通过Maven新建一个名为springboot-qiniu-kodo
的项目。
1.引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.qiniu</groupId>
<artifactId>qiniu-java-sdk</artifactId>
<version>7.7.0</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.5</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>
<!-- lombok插件 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.8</version>
</dependency>
2.编写配置类
package com.rtxtitanv.config;
import com.qiniu.cdn.CdnManager;
import com.qiniu.storage.BucketManager;
import com.qiniu.storage.Region;
import com.qiniu.storage.UploadManager;
import com.qiniu.storage.persistent.FileRecorder;
import com.qiniu.util.Auth;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.web.servlet.MultipartConfigFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.unit.DataSize;
import javax.servlet.MultipartConfigElement;
import java.io.IOException;
import java.nio.file.Paths;
/**
* @author rtxtitanv
* @version 1.0.0
* @name com.rtxtitanv.config.QiniuKodoConfig
* @description 七牛云对象存储配置类
* @date 2021/6/20 12:39
*/
@ConfigurationProperties(prefix = "qiniu.kodo")
@Configuration
@Data
public class QiniuKodoConfig {
private String accessKey;
private String secretKey;
private String bucket;
private String region;
private String domain;
/**
* 带有指定Region对象的配置实例
*
* @return com.qiniu.storage.Configuration
*/
@Bean
public com.qiniu.storage.Configuration config() {
if ("huadong".equals(region)) {
return new com.qiniu.storage.Configuration(Region.huadong());
}
if ("huabei".equals(region)) {
return new com.qiniu.storage.Configuration(Region.huabei());
}
if ("huanan".equals(region)) {
return new com.qiniu.storage.Configuration(Region.huanan());
}
if ("beimei".equals(region)) {
return new com.qiniu.storage.Configuration(Region.beimei());
}
if ("xinjiapo".equals(region)) {
return new com.qiniu.storage.Configuration(Region.xinjiapo());
}
return new com.qiniu.storage.Configuration();
}
/**
* 七牛云上传管理器实例
*
* @return com.qiniu.storage.UploadManager
*/
@Bean
public UploadManager uploadManager() {
return new UploadManager(config());
}
/**
* 断点续传的七牛云上传管理器实例
*
* @return com.qiniu.storage.UploadManager
* @throws IOException IOException
*/
@Bean
public UploadManager resumableUploadManager() throws IOException {
com.qiniu.storage.Configuration config = config();
// 指定分片上传版本
config.resumableUploadAPIVersion = com.qiniu.storage.Configuration.ResumableUploadAPIVersion.V2;
// 设置分片上传并发,1:采用同步上传;大于1:采用并发上传
config.resumableUploadMaxConcurrentTaskCount = 2;
String localTempDir = Paths.get(System.getenv("java.io.tmpdir"), bucket).toString();
// 设置断点续传文件进度保存目录
FileRecorder fileRecorder = new FileRecorder(localTempDir);
return new UploadManager(config, fileRecorder);
}
/**
* 认证信息实例
*
* @return com.qiniu.util.Auth
*/
@Bean
public Auth auth() {
return Auth.create(accessKey, secretKey);
}
/**
* 空间资源管理器实例
*
* @return com.qiniu.storage.BucketManager
*/
@Bean
public BucketManager bucketManager() {
return new BucketManager(auth(), config());
}
/**
* CDN管理器实例
*
* @return com.qiniu.cdn.CdnManager
*/
@Bean
public CdnManager cdnManager() {
return new CdnManager(auth());
}
/**
* 文件上传配置
*
* @return javax.servlet.MultipartConfigElement
*/
@Bean
public MultipartConfigElement multipartConfigElement() {
MultipartConfigFactory multipartConfigFactory = new MultipartConfigFactory();
// 设置multipart/form-data请求允许的最大数据大小,这里设置为1Gb
multipartConfigFactory.setMaxRequestSize(DataSize.ofGigabytes(1));
// 设置上传文件允许的最大大小,这里设置为1Gb
multipartConfigFactory.setMaxFileSize(DataSize.ofGigabytes(1));
return multipartConfigFactory.createMultipartConfig();
}
}
3.编写配置文件
# 自定义七牛云存储配置
qiniu:
kodo:
# 配置accessKey
accessKey: tE5fkJWqWZ0wZFZLSNfkgjJCQ-5_IR-cm2LuNIiu
# 配置secretKey
secretKey: NCvKu-Xs7TvggV8_n7svfiPDRuB4yI622i7R_Y6w
# 配置空间名称
bucket: public-test-space
# 配置机房
region: huanan
# 配置域名
domain: qv1q6zvik.hn-bkt.clouddn.com
4.编写工具类
package com.rtxtitanv.util;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
/**
* @author rtxtitanv
* @version 1.0.0
* @name com.rtxtitanv.util.SpringUtil
* @description 获取Bean的工具类
* @date 2021/6/21 11:24
*/
@Component
public class SpringUtil implements ApplicationContextAware {
private static ApplicationContext applicationContext;
/**
* 设置ApplicationContext
*
* @param applicationContext 上下文对象
* @throws BeansException BeansException
*/
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
SpringUtil.applicationContext = applicationContext;
}
/**
* 获取ApplicationContext实例
*
* @return ApplicationContext实例
*/
public static ApplicationContext getApplicationContext() {
return applicationContext;
}
/**
* 通过名称获取Bean
*
* @param name 名称
* @return Bean
*/
public static Object getBean(String name) {
return getApplicationContext().getBean(name);
}
/**
* 通过class获取Bean
*
* @param requiredType class对象
* @param <T> class所代表的类型
* @return Bean
*/
public static <T> T getBean(Class<T> requiredType) {
return getApplicationContext().getBean(requiredType);
}
/**
* 通过name和class获取Bean
*
* @param name name
* @param requiredType class
* @param <T> class所代表的类型
* @return Bean
*/
public static <T> T getBean(String name, Class<T> requiredType) {
return getApplicationContext().getBean(name, requiredType);
}
}
package com.rtxtitanv.util;
import com.alibaba.fastjson.JSON;
import com.google.gson.Gson;
import com.qiniu.cdn.CdnManager;
import com.qiniu.cdn.CdnResult;
import com.qiniu.common.QiniuException;
import com.qiniu.http.Response;
import com.qiniu.storage.BucketManager;
import com.qiniu.storage.UploadManager;
import com.qiniu.storage.model.DefaultPutRet;
import com.qiniu.util.Auth;
import com.qiniu.util.StringMap;
import com.rtxtitanv.config.QiniuKodoConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
/**
* @author rtxtitanv
* @version 1.0.0
* @name com.rtxtitanv.util.QiniuKodoUtils
* @description 七牛云对象存储工具类
* @date 2021/6/20 12:35
*/
public class QiniuKodoUtils {
private static final Logger LOGGER = LoggerFactory.getLogger(QiniuKodoUtils.class);
private static final String BUCKET;
private static final String DOMAIN;
private static final UploadManager UPLOAD_MANAGER;
private static final UploadManager RESUMABLE_UPLOAD_MANAGER;
private static final BucketManager BUCKET_MANAGER;
private static final Auth AUTH;
private static final CdnManager CDN_MANAGER;
static {
QiniuKodoConfig qiniuKodoConfig = SpringUtil.getBean(QiniuKodoConfig.class);
BUCKET = qiniuKodoConfig.getBucket();
DOMAIN = qiniuKodoConfig.getDomain();
UPLOAD_MANAGER = SpringUtil.getBean("uploadManager", UploadManager.class);
RESUMABLE_UPLOAD_MANAGER = SpringUtil.getBean("resumableUploadManager", UploadManager.class);
BUCKET_MANAGER = SpringUtil.getBean(BucketManager.class);
AUTH = SpringUtil.getBean(Auth.class);
CDN_MANAGER = SpringUtil.getBean(CdnManager.class);
}
/**
* 简单上传的凭证
*
* @return 上传凭证
*/
public static String getUpToken() {
return AUTH.uploadToken(BUCKET);
}
/**
* 覆盖上传的凭证
*
* @param key 要想进行覆盖的文件名称,必须与上传文件名一致
* @return 上传凭证
*/
public static String getUpToken(String key) {
return AUTH.uploadToken(BUCKET, key);
}
/**
* 自定义上传回复的凭证
*
* @return 上传凭证
*/
public static String getCustomUpToken() {
StringMap putPolicy = new StringMap();
// 通过设置returnBody参数来实现返回的JSON格式的内容
putPolicy.put("returnBody",
"{\"key\":\"$(key)\",\"hash\":\"$(etag)\",\"bucket\":\"$(bucket)\",\"fsize\":$(fsize)}");
long expireSeconds = 3600;
return AUTH.uploadToken(BUCKET, null, expireSeconds, putPolicy);
}
/**
* 带回调业务服务器的凭证
*
* @return 上传凭证
*/
public static String getUpTokenWithCallback() {
StringMap putPolicy = new StringMap();
// 回调地址,该地址需要允许公网访问
putPolicy.put("callbackUrl", "http://83527e8a63ff.ngrok.io/qiniu/upload/callback");
putPolicy.put("callbackBody",
"{\"key\":\"$(key)\",\"hash\":\"$(etag)\",\"bucket\":\"$(bucket)\",\"fsize\":$(fsize)}");
putPolicy.put("callbackBodyType", "application/json");
long expireSeconds = 3600;
return AUTH.uploadToken(BUCKET, null, expireSeconds, putPolicy);
}
/**
* 本地文件上传
*
* @param localFilePath 本地路径
* @param fileName 文件名
* @param override 是否覆盖上传凭证
* @return 文件链接
*/
public static String uploadFile(String localFilePath, String fileName, boolean override) {
String upToken;
if (override) {
// 覆盖上传凭证
upToken = getUpToken(fileName);
} else {
upToken = getUpToken();
}
try {
Response response = UPLOAD_MANAGER.put(localFilePath, fileName, upToken);
String fileUrl = fileUrl(response, fileName);
String[] urls = {fileUrl};
refreshFiles(urls);
return fileUrl;
} catch (QiniuException ex) {
qiniuException(ex);
throw new RuntimeException(ex.getMessage());
}
}
/**
* 字节数组上传
*
* @param bytes 字节数组
* @param fileName 文件名
* @return 文件链接
*/
public static String uploadFile(byte[] bytes, String fileName) {
try {
Response response = UPLOAD_MANAGER.put(bytes, fileName, getUpToken());
return fileUrl(response, fileName);
} catch (QiniuException ex) {
qiniuException(ex);
throw new RuntimeException(ex.getMessage());
}
}
/**
* 数据流上传
*
* @param inputStream 输入流
* @param fileName 文件名
* @return 文件链接
*/
public static String uploadFile(InputStream inputStream, String fileName) {
try {
Response response = UPLOAD_MANAGER.put(inputStream, fileName, getUpToken(), null, null);
return fileUrl(response, fileName);
} catch (QiniuException ex) {
qiniuException(ex);
throw new RuntimeException(ex.getMessage());
}
}
/**
* 断点续传
*
* @param inputStream 输入流
* @param fileName 文件名
* @return 文件链接
*/
public static String resumableUploadFile(InputStream inputStream, String fileName) {
try {
Response response = RESUMABLE_UPLOAD_MANAGER.put(inputStream, fileName, getUpToken(), null, null);
return fileUrl(response, fileName);
} catch (QiniuException ex) {
qiniuException(ex);
throw new RuntimeException(ex.getMessage());
}
}
/**
* 手动拼接方式获取公开空间文件链接
*
* @param fileName 文件名
* @return 文件链接
* @throws UnsupportedEncodingException
*/
public static String getFileUrl(String fileName) throws UnsupportedEncodingException {
// 对文件名进行urlencode以兼容不同的字符
String encoderFileName = URLEncoder.encode(fileName, "UTF-8").replace("+", "20%");
// 拼接文件链接
String finalUrl = String.format("%s/%s", "http://" + DOMAIN, encoderFileName);
LOGGER.info(finalUrl);
return finalUrl;
}
/**
* 手动拼接方式获取私有空间文件链接
*
* @param fileName 文件名
* @return 文件链接
* @throws UnsupportedEncodingException
*/
public static String getPrivateFileUrl(String fileName) throws UnsupportedEncodingException {
// 构建对应的公开空间访问链接
String encoderFileName = URLEncoder.encode(fileName, "UTF-8").replace("+", "20%");
String publicUrl = String.format("%s/%s", "http://" + DOMAIN, encoderFileName);
// 1小时,可以自定义链接过期时间
long expireInSeconds = 3600;
// 对该链接进行私有授权签名
String finalUrl = AUTH.privateDownloadUrl(publicUrl, expireInSeconds);
LOGGER.info(finalUrl);
return finalUrl;
}
/**
* 删除空间中的文件
*
* @param fileName 文件名
* @return
*/
public static String delete(String fileName) {
try {
Response response = BUCKET_MANAGER.delete(BUCKET, fileName);
return response.statusCode == 200 ? "删除成功" : "删除失败";
} catch (QiniuException ex) {
LOGGER.error(ex.code() + "");
LOGGER.error(ex.response.toString());
throw new RuntimeException(ex.getMessage());
}
}
/**
* 文件刷新
*
* @param urls 待刷新的文件链接数组
*/
public static void refreshFiles(String[] urls) {
try {
CdnResult.RefreshResult refreshResult = CDN_MANAGER.refreshUrls(urls);
LOGGER.info(refreshResult.code + "");
} catch (QiniuException e) {
LOGGER.error(e.response.toString());
}
}
/**
* 返回文件链接
*
* @param response com.qiniu.http.Response
* @param fileName 文件名
* @return 文件链接
* @throws QiniuException QiniuException
*/
private static String fileUrl(Response response, String fileName) throws QiniuException {
// 解析上传成功的结果
DefaultPutRet putRet = new Gson().fromJson(response.bodyString(), DefaultPutRet.class);
LOGGER.info(putRet.key);
LOGGER.info(putRet.hash);
LOGGER.info("上传文件成功 {}", JSON.toJSONString(putRet));
return "http://" + DOMAIN + "/" + fileName;
}
/**
* QiniuException异常处理
*
* @param ex QiniuException
*/
private static void qiniuException(QiniuException ex) {
Response response = ex.response;
LOGGER.error(response.toString());
LOGGER.error("上传文件失败 {}", ex);
try {
LOGGER.error(response.bodyString());
} catch (QiniuException e) {
e.printStackTrace();
}
}
}
5.Controller层
package com.rtxtitanv.controller;
import com.rtxtitanv.service.QiniuKodoService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
/**
* @author rtxtitanv
* @version 1.0.0
* @name com.rtxtitanv.controller.QiniuKodoController
* @description QiniuKodoController
* @date 2021/6/20 12:36
*/
@RestController
public class QiniuKodoController {
@Resource
private QiniuKodoService qiniuKodoService;
@PostMapping("/upload/path")
public String uploadPath(@RequestParam(value = "localFilePath") String localFilePath,
@RequestParam(value = "fileName") String filename, @RequestParam(value = "override") boolean override) {
return qiniuKodoService.uploadPath(localFilePath, filename, override);
}
@PostMapping("/upload/bytes")
public String uploadBytes(@RequestPart(value = "file") MultipartFile file) {
return qiniuKodoService.uploadBytes(file);
}
@PostMapping("/upload/stream")
public String uploadStream(@RequestPart(value = "file") MultipartFile file) {
return qiniuKodoService.uploadStream(file);
}
@PostMapping("/upload/resumable")
public String resumableUpload(@RequestPart(value = "file") MultipartFile file) {
return qiniuKodoService.resumableUpload(file);
}
@GetMapping("/download/{fileName}")
public void download(@PathVariable(value = "fileName") String fileName, HttpServletResponse response) {
try {
String fileUrl = qiniuKodoService.download(fileName);
response.sendRedirect(fileUrl);
} catch (IOException e) {
e.printStackTrace();
}
}
@GetMapping("/download/private/{fileName}")
public void privateDownload(@PathVariable(value = "fileName") String fileName, HttpServletResponse response) {
try {
String fileUrl = qiniuKodoService.privateDownload(fileName);
response.sendRedirect(fileUrl);
} catch (IOException e) {
e.printStackTrace();
}
}
@DeleteMapping("/delete/{fileName}")
public String delete(@PathVariable(value = "fileName") String fileName) {
return qiniuKodoService.delete(fileName);
}
@GetMapping("/token")
public String getUpToken() {
return qiniuKodoService.getUpToken();
}
@GetMapping("/token/custom")
public String getCustomUpToken() {
return qiniuKodoService.getCustomUpToken();
}
@GetMapping("/token/callback")
public String getUpTokenWithCallback() {
return qiniuKodoService.getUpTokenWithCallback();
}
@PostMapping("/qiniu/upload/callback")
public void qiNiuCallback(HttpServletRequest request) {
try {
String line = "";
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(request.getInputStream()));
StringBuilder stringBuilder = new StringBuilder();
while ((line = bufferedReader.readLine()) != null) {
stringBuilder.append(line);
}
Logger logger = LoggerFactory.getLogger(QiniuKodoController.class);
logger.info("callbackBody:" + stringBuilder);
} catch (IOException e) {
e.printStackTrace();
}
}
}
6.Service层
package com.rtxtitanv.service;
import org.springframework.web.multipart.MultipartFile;
import java.io.UnsupportedEncodingException;
/**
* @author rtxtitanv
* @version 1.0.0
* @name com.rtxtitanv.service.QiniuKodoService
* @description QiniuKodoService
* @date 2021/6/20 12:37
*/
public interface QiniuKodoService {
/**
* 本地文件上传
*
* @param localFilePath 本地文件路径
* @param fileName 文件名
* @param override 是否覆盖上传凭证
* @return 文件链接
*/
String uploadPath(String localFilePath, String fileName, boolean override);
/**
* 字节数组上传
*
* @param file MultipartFile对象
* @return 文件链接
*/
String uploadBytes(MultipartFile file);
/**
* 数据流上传
*
* @param file MultipartFile对象
* @return 文件链接
*/
String uploadStream(MultipartFile file);
/**
* 断点续传
*
* @param file MultipartFile对象
* @return 文件链接
*/
String resumableUpload(MultipartFile file);
/**
* 公开空间文件下载
*
* @param fileName 文件名
* @return 文件链接
* @throws UnsupportedEncodingException
*/
String download(String fileName) throws UnsupportedEncodingException;
/**
* 私有空间文件下载
*
* @param fileName 文件名
* @return 文件链接
* @throws UnsupportedEncodingException
*/
String privateDownload(String fileName) throws UnsupportedEncodingException;
/**
* 删除空间中的文件
*
* @param fileName 文件名
* @return
*/
String delete(String fileName);
/**
* 获取简单上传的配置
*
* @return 上传凭证
*/
String getUpToken();
/**
* 获取自定义上传回复的凭证
*
* @return 上传凭证
*/
String getCustomUpToken();
/**
* 获取带回调业务服务器的凭证
*
* @return 上传凭证
*/
String getUpTokenWithCallback();
}
package com.rtxtitanv.service.impl;
import com.qiniu.util.StringUtils;
import com.rtxtitanv.service.QiniuKodoService;
import com.rtxtitanv.util.QiniuKodoUtils;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.UUID;
/**
* @author rtxtitanv
* @version 1.0.0
* @name com.rtxtitanv.service.impl.QiniukodoServiceImpl
* @description QiniukodoService实现类
* @date 2021/6/20 12:37
*/
@Service
public class QiniukodoServiceImpl implements QiniuKodoService {
@Override
public String uploadPath(String localFilePath, String fileName, boolean override) {
if (StringUtils.isNullOrEmpty(localFilePath)) {
throw new RuntimeException("文件路径为空");
}
return QiniuKodoUtils.uploadFile(localFilePath, fileName, override);
}
@Override
public String uploadBytes(MultipartFile file) {
String originalFilename = file.getOriginalFilename();
String fileName = UUID.randomUUID() + "-" + originalFilename;
if (file.isEmpty()) {
throw new RuntimeException("文件为空");
}
try {
byte[] bytes = file.getBytes();
return QiniuKodoUtils.uploadFile(bytes, fileName);
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException(e.getMessage());
}
}
@Override
public String uploadStream(MultipartFile file) {
String originalFilename = file.getOriginalFilename();
String fileName = UUID.randomUUID() + "-" + originalFilename;
if (file.isEmpty()) {
throw new RuntimeException("文件为空");
}
try {
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(file.getBytes());
return QiniuKodoUtils.uploadFile(byteArrayInputStream, fileName);
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException(e.getMessage());
}
}
@Override
public String resumableUpload(MultipartFile file) {
String originalFilename = file.getOriginalFilename();
String fileName = UUID.randomUUID() + "-" + originalFilename;
if (file.isEmpty()) {
throw new RuntimeException("文件为空");
}
try {
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(file.getBytes());
return QiniuKodoUtils.resumableUploadFile(byteArrayInputStream, fileName);
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException(e.getMessage());
}
}
@Override
public String download(String fileName) throws UnsupportedEncodingException {
if (StringUtils.isNullOrEmpty(fileName)) {
throw new RuntimeException("文件名为空");
}
return QiniuKodoUtils.getFileUrl(fileName);
}
@Override
public String privateDownload(String fileName) throws UnsupportedEncodingException {
if (StringUtils.isNullOrEmpty(fileName)) {
throw new RuntimeException("文件名为空");
}
return QiniuKodoUtils.getPrivateFileUrl(fileName);
}
@Override
public String delete(String fileName) {
if (StringUtils.isNullOrEmpty(fileName)) {
throw new RuntimeException("文件名为空");
}
return QiniuKodoUtils.delete(fileName);
}
@Override
public String getUpToken() {
return QiniuKodoUtils.getUpToken();
}
@Override
public String getCustomUpToken() {
return QiniuKodoUtils.getCustomUpToken();
}
@Override
public String getUpTokenWithCallback() {
return QiniuKodoUtils.getUpTokenWithCallback();
}
}
四、七牛云对象存储测试
运行项目,使用Postman进行七牛云对象存储相关测试。
1.服务端直传测试
文件上传分为客户端上传(主要是指网页端和移动端等面向终端用户的场景)和服务端上传两种场景。服务端直传是指客户利用七牛服务端SDK从服务端直接上传文件到七牛云,交互的双方一般都在机房里面,所以服务端可以自己生成上传凭证,然后利用SDK中的上传逻辑进行上传,最后从七牛云获取上传的结果,这个过程中由于双方都是业务服务器,所以很少利用到上传回调的功能,而是直接自定义returnBody
来获取自定义的回复内容。
(1)本地文件上传
直接指定文件的完整路径即可完成本地文件上传。发送如下POST请求上传文件,请求地址为http://localhost:8080/upload/path
,不覆盖上传的凭证:
然后查看空间管理中的文件管理,文件成功上传:
上传的时候如果没有指定fileName,则以文件内容的hash值作为文件名。
然后访问返回的文件链接查看文件:
不覆盖上传的凭证,再次发送如下POST请求上传另一个文件,但文件名与之前的一致会报错:
查看控制台打印的错误日志:
如果想要对文件进行覆盖,需要覆盖上传的凭证,并且想要覆盖的文件名必须与上传的文件名一致。发送如下POST请求上传另一个文件,覆盖上传的凭证:
在存储空间中查看文件,这里可以看到文件大小已经变化了:
然后访问返回的文件链接查看文件,发现文件已经被覆盖:
这里注意在上传成功之后需要刷新文件。因为访问的时候会访问到CDN缓存,如果不刷新文件,查看到的还是原来的文件。也可以在CDN中的刷新预取中刷新文件:
(2)字节数组上传
可以支持将内存中的字节数组上传到空间中。发送如下POST请求上传文件,请求地址为http://localhost:8080/upload/bytes
:
查看存储空间中的文件,发现成功上传:
然后访问返回的文件链接查看文件:
(3)数据流上传
通过InputStream
进行上传,适用于所有的InputStream
子类。发送如下POST请求上传文件,请求地址为http://localhost:8080/upload/stream
:
查看存储空间中的文件,发现成功上传:
然后访问返回的文件链接查看文件:
(4)断点续传
断点续传是在分片上传的基础上实现。SDK内置表单上传和分片上传两种上传方式。表单上传适合小文件上传,分片上传适合大文件上传。七牛文件上传管理器UploadManager
上传文件时,会自动根据定义的Configuration.putThreshold
来判断是采用表单上传还是分片上传的方法,超过了定义的Configuration.putThreshold
就会采用 分片上传的方法,可以在构造该类对象的时候,通过Configuration
类来自定义这个值。分片上传分为V1和V2版,默认为V1版。
发送如下POST请求上传文件,请求地址为http://localhost:8080/upload/resumable
,上传过程中断开网络后再连接:
查看存储空间中的文件,发现成功上传:
2.文件下载测试
文件下载分为公开空间的文件下载和私有空间的文件下载。文件的链接地址可以通过手动拼接方式和sdk自动生成方式获取,这里使用手动拼接方式获取文件的链接地址。对于公开空间,其访问的链接主要是将空间绑定的域名拼接上空间里面的文件名即可访问,对于私有空间,首先需要按照公开空间的文件访问方式构建对应的公开空间访问链接,然后再对这个链接进行私有授权签名。
(1)公开空间下载
访问http://localhost:8080/download/27c53ab8-5287-4151-8b80-47635b55df24-87091240.png
,查看到了文件,可见最终成功重定向到了文件链接地址:
查看控制台打印的文件链接:
(2)私有空间下载
将配置文件中的空间名和域名修改为私有空间的空间名和域名后重启项目,然后向私有空间上传一个文件:
按照公开空间的方式无法下载私有空间的文件:
访问http://localhost:8080/download/private/b0eda5b4-a070-48f3-9cc7-9e969fa6e20d-72780175.png
,查看到了文件,可见最终成功重定向到了文件链接地址:
查看控制台打印的文件链接:
3.删除文件测试
发送DELETE请求http://localhost:8080/delete/b0eda5b4-a070-48f3-9cc7-9e969fa6e20d-72780175.png
删除指定文件:
查看存储空间中的文件,发现成功删除:
4.客户端上传测试
客户端(移动端或者Web端)上传文件的时候,需要从客户自己的业务服务器获取上传凭证,而这些上传凭证是通过服务端的SDK来生成的,然后通过客户自己的业务API分发给客户端使用。根据上传的业务需求不同,七牛云Java SDK支持丰富的上传凭证生成方式。
(1)简单上传凭证
最简单的上传凭证只需要AccessKey
,SecretKey
和Bucket
就可以。访问http://localhost:8080/token获取简单上传的凭证:
复制上传凭证然后然后上传如下文件到华南区域的加速上传地址http://upload-z2.qiniup.com:
表单没有指定key,可以使用上传策略的
saveKey
字段所指定魔法变量生成Key,如果没有模板,则使用文件内容Hash值作为Key。
查看存储空间中的文件,发现成功上传:
将返回的key拼接到http://localhost:8080/download/private/
访问即可重定向到文件链接地址查看文件:
客户端上传的地址在七牛云对象存储的存储区域中查看,不同的存储区域的域名不同。七牛云对象存储的存储区域:https://developer.qiniu.com/kodo/1671/region-endpoint-fq。
(2)自定义上传回复凭证
默认情况下,文件上传到七牛之后,在没有设置returnBody
或者callback
相关的参数情况下,七牛返回给上传端的回复格式为hash
和key
。之前的简单上传凭证就是默认的回复格式。通过设置returnBody
参数可以实现自定义返回的JSON格式的内容。
访问http://localhost:8080/token/custom获取自定义上传回复的凭证:
复制上传凭证然后然后上传如下文件到华南区域的加速上传地址http://upload-z2.qiniup.com,返回的是自定义的JSON格式:
查看存储空间中的文件,发现成功上传:
将返回的key拼接到http://localhost:8080/download/private/
访问即可重定向到文件链接地址查看文件:
(3)带回调业务服务器的凭证
回调通知是指客户端在上传时指定服务端在处理完上传请求后,应该通知某个特定服务器,在该服务器确认接收了该回调后才将所有结果返回给客户端。
开发者可以要求七牛云存储在某文件上传完成后向特定的URL发起一个回调请求。七牛云存储会将该回调的响应内容作为文件上传响应的一部分一并返回给客户端。回调的流程如下:
要启用回调功能,业务服务器在签发上传凭证时需要设置上传策略(PutPolicy)中的callbackUrl
和callbackBody
字段,callbackUrl
需要允许公网访问。在上传策略里面设置了上传回调相关参数的时候,七牛在文件上传到服务器之后,会主动地向callbackUrl
发送POST请求的回调,回调的内容为callbackBody
模版所定义的内容。业务服务器在收到来自七牛的回调请求的时候,可以根据请求头部的Authorization
字段来进行验证,查看该请求是否是来自七牛的未经篡改的请求。
自定义上传回复的凭证适用于上传端(无论是客户端还是服务端)和七牛服务器之间进行直接交互的情况下。在客户端上传的场景之下,有时候客户端需要在文件上传到七牛之后,从业务服务器获取相关的信息,这个时候就要用到七牛的上传回调及相关回调参数的设置。
访问http://localhost:8080/token/callback获取带回调业务服务器的凭证:
复制上传凭证然后然后上传如下文件到华南区域的加速上传地址http://upload-z2.qiniup.com:
查看存储空间中的文件,发现成功上传:
查看控制台打印的日志,可以说明在文件上传成功之后七牛云存储主动向callbackUrl
发送了POST请求的回调,回调的内容为callbackBody
模版所定义的内容:
将key拼接到http://localhost:8080/download/private/
访问即可重定向到文件链接地址查看文件:
由于callbackUrl需要公网能访问的地址,要通过回调访问到本地运行的项目,这里需要用到内网穿透工具,可以使用Ngrok来进行内网穿透,在公网访问本地的项目。在Ngrok官网https://ngrok.com/注册账号并下载。然后找到Authtoken:
然后运行gnrok.exe,执行以下命令:
# Your Authtoken为你的密钥
ngrok authotoken Your Authtoken
ngrok http 8080
执行之后,就可以使用http://83527e8a63ff.ngrok.io在公网访问http://localhost:8080:
代码示例