客户端上传例子:用户上传头像;
服务器直传在上节课中也讲到过,服务器将自动生成的图片,直接交给云服务器。
我们选择 qiniu云服务器,因为免费。
官网:
注册会比较麻烦,因此我们直接截图吧
创建云服务器 08:23
新建存储空间,利用域名来访问:
现在什么都没有
又新建一个存储空间,域名为:
在porm中导入qiniu所需的包
接下来在 application Properties中
做一些配置
AK用来标识用户身份,不是谁都可以往空间中传东西;
SK用来上传具体内容时为内容加密
需要将这两个在配置文件中设置。
# qiniu
# 用户身份密钥
qiniu.key.access=6RA-Uus95ZT_1znMrCMD8BpqfjT-K7OKmQTfKB48
# 文件内容加密的密钥
qiniu.key.secret=kPNnLFz2_tzztKUVpSLm0lYngtuHWyIq5LzTmLIL
# 分配的云空间名字以及域名
qiniu.bucket.header.name=community_header
quniu.bucket.header.url=http://pvghrij81.bkt.clouddn.com
# 分配第二个云空间的名字以及域名
qiniu.bucket.share.name=community_share
qiniu.bucket.share.url=http://pvghvvuzm.bkt.clouddn.com
接下来处理上传头像的逻辑:
上传头像的请求在UserController中:
@Value("${qiniu.key.access}")
private String accessKey;
@Value("${qiniu.key.secret}")
private String secretKey;
@Value("${qiniu.bucket.header.name}")
private String headerBucketName;
@Value("${quniu.bucket.header.url}")
private String headerBucketUrl;
@LoginRequired
@RequestMapping(path = "/setting", method = RequestMethod.GET)
public String getSettingPage(Model model) {
// 生成 上传文件名称
String fileName = CommunityUtil.generateUUID();//文件名称 生成 随机的(同名的话后者会覆盖前者,不能实现存留历史数据的功能)
// 设置响应信息
StringMap policy = new StringMap();
policy.put("returnBody", CommunityUtil.getJSONString(0));//响应体希望响应什么内容,七牛云客户端直传通常采用异步方式来传,因为异步返回JSON字符串,告诉我们有没有成功即可(成功的时候返回0),这样更方便,如果是同步的方式,响应个html,就乱套了
// 生成上传凭证(提供上传文件的名称),使得qiniu可以识别身份
Auth auth = Auth.create(accessKey, secretKey);//利用 与身份有关的accessKey,与内容有关的secretKey 生成的auth对象,以生成上传凭证uploadToken
String uploadToken = auth.uploadToken(headerBucketName, fileName, 3600, policy);//headerBucketName上传空间名字;fileName文件名;key过期时间1h。policy响应信息
model.addAttribute("uploadToken", uploadToken);
model.addAttribute("fileName", fileName);
return "/site/setting";
}
// 更新头像路径,当我们把数据提交给qiniu云以后,qiniu给我们返回一个消息0,代表成功,成功后要把user表里面的headurl做一个更新,更新为qinniu云的路径,如果不更的话,表里的路径没变,就白传了,网页还是显示的之前的头像,而不是传到qinniu云里面的
@RequestMapping(path = "/header/url", method = RequestMethod.POST)//因为要提交数据,更新数据,所以是POST
@ResponseBody
public String updateHeaderUrl(String fileName) {
if (StringUtils.isBlank(fileName)) {
return CommunityUtil.getJSONString(1, "文件名不能为空!");//1表示错误
}
//不为空 则需要拼URL
String url = headerBucketUrl + "/" + fileName;//头像有效的访问路径
userService.updateHeader(hostHolder.getUser().getId(), url);
return CommunityUtil.getJSONString(0);
}
重构好user controller了,因为废弃了原有的几个方法,对现有的方法进行改进
接下来处理对应的表单 setting.html
注释掉:
修改为:
<!--上传到七牛云-->
<form class="mt-5" id="uploadForm"> <!--异步上传不需要method="post" enctype="multipart/form-data" th:action="@{/user/upload}"-->
<div class="form-group row mt-4">
<label for="head-image" class="col-sm-2 col-form-label text-right">选择头像:</label>
<div class="col-sm-10">
<div class="custom-file">
<input type="hidden" name="token" th:value="${uploadToken}">
<input type="hidden" name="key" th:value="${fileName}">
<input type="file" class="custom-file-input" id="head-image" name="file" lang="es" required="">
<label class="custom-file-label" for="head-image" data-browse="文件">选择一张图片</label>
<div class="invalid-feedback">
该账号不存在!
</div>
</div>
</div>
</div>
<div class="form-group row mt-4">
<div class="col-sm-2"></div>
<div class="col-sm-10 text-center">
<button type="submit" class="btn btn-info text-white form-control">立即上传</button>
</div>
</div>
</form>
新建setting.js
$(function(){ <!--页面加载完 后调用此function-->
$("#uploadForm").submit(upload);<!--当点击提交按钮,触发表单的提交事件时,该事件由upload处理。Form id是uploadForm,当其触发提交事件时-->
});
function upload() {
$.ajax({
url: "http://upload-z1.qiniup.com",<!-- 表示请求要提交给谁-->
method: "post",<!-- 请求方式是post-->
processData: false,<!-- 不要把表单内容转为字符串,默认情况下,浏览器会把表单内容转为字符串 提交给服务器-->
contentType: false,<!-- contentType按理说应该是 html、json。即数据类型,false的意思是不让 jqury设置上传类型,浏览器自动设置。文件和别的数据不一样,是二进制的,当其和别的数据混编在一起时,边界如何确定呢;浏览器会给其加一个随机边界字符串,好去拆分 -->
data: new FormData($("#uploadForm")[0]),
success: function(data) {<!--成功的时候处理:-->
if(data && data.code == 0) {<!--data存在且data.code = 0-->
// 更新头像访问路径(异步)
$.post(
CONTEXT_PATH + "/user/header/url",
{"fileName":$("input[name='key']").val()},<!--要传数据的名字-->
function(data) {
data = $.parseJSON(data);//将data解析为JSON
if(data.code == 0) {//表示成功
window.location.reload();//刷新页面,看看头像是否改变
} else {
alert(data.msg);//失败则给出提示
}
}
);
} else {
alert("上传失败!");
}
}
});
return false; <!--false表示事件到此为止-->
}
测试:
访问首页、登录:
其头像是:
表里的数据:
现在的路径是本地路径,一回儿要改成 qiniu云的路径
接下来修改头像,上传新的头像。
头像为:
此时 url变为qiniu云的
接下来在 qiniu中查看
可以看到有一条数据
预览没有问题
于是上传头像便解决了。
这只是上传至云服务器的一种方案,通过客户端上传。
接下来演示通过服务器直传。
打开share controller
@Value("${qiniu.bucket.share.url}")//application properties中 qiniu.bucket.share.url
private String shareBucketUrl;
对share函数
文件名(随机,每次都要变,不然很容易重名)
异步生成长图
最终返回浏览器的路径 需要改动
一开始:
改为:
// 生成长图以后,如何访问呢---返回访问路径
Map<String, Object> map = new HashMap<>();
// map.put("shareUrl", domain + contextPath + "/share/image/" + fileName);//domain项目域名
map.put("shareUrl", shareBucketUrl + "/" + fileName);
打开EventConsumer
@Value("${qiniu.key.access}")
private String accessKey;
@Value("${qiniu.key.secret}")
private String secretKey;
@Value("${qiniu.bucket.share.name}")
private String shareBucketName;//上传空间名字
@Autowired
private ThreadPoolTaskScheduler taskScheduler;//执行定时任务的线程池,因为要考虑分布式部署的问题
在handleShareMessage中
String htmlUrl = (String) event.getData().get("htmlUrl");//event.getData()是map
String fileName = (String) event.getData().get("fileName");
String suffix = (String) event.getData().get("suffix");//后缀
//利用上述三个,把命令拼出来
String cmd = wkImageCommand + " --quality 75 "
+ htmlUrl + " " + wkImageStorage + "/" + fileName + suffix;
try {
Runtime.getRuntime().exec(cmd);//传入cmd,执行
logger.info("生成长图成功: " + cmd);
} catch (IOException e) {
logger.error("生成长图失败: " + e.getMessage());
}
//注意,logger.info("生成长图成功: " + cmd); 先执行,因为 Runtime.getRuntime().exec(cmd);//传入cmd,执行 因为图片大,所以慢,所以要等
// 启用定时器,监视该图片,每隔一段时间 检测是否生成图片,一旦生成了,则上传至七牛云.
UploadTask task = new UploadTask(fileName, suffix);//哪个服务器抢到这个消息,则只有该服务器会开启定时
Future future = taskScheduler.scheduleAtFixedRate(task, 500);//taskScheduler.scheduleAtFixedRate(task, 500)触发定时器的执行,定时器启动后有一个返回值 叫future,里面封装了用户的状态
task.setFuture(future);
class UploadTask implements Runnable {
// 文件名称
private String fileName;
// 文件后缀
private String suffix;
// 启动任务的返回值,用于停止定时器
private Future future;
// 开始时间,方便计算执行时间
private long startTime;
// 上传次数,保障任务停止
private int uploadTimes;
public UploadTask(String fileName, String suffix) {
this.fileName = fileName;
this.suffix = suffix;
this.startTime = System.currentTimeMillis();//创建 开始时间
}
public void setFuture(Future future) {
this.future = future;
}
@Override
public void run() {
// 生成图片失败
if (System.currentTimeMillis() - startTime > 30000) {//时间超过30s
logger.error("执行时间过长,终止任务:" + fileName);
future.cancel(true);//停止定时器
return;
}
// 上传图片至qiniu云失败
if (uploadTimes >= 3) {
logger.error("上传次数过多,终止任务:" + fileName);
future.cancel(true);//停止定时器
return;
}
String path = wkImageStorage + "/" + fileName + suffix;//本地存放文件完整 路径
File file = new File(path);//将path传入以实例化文件file
if (file.exists()) {//如果文件存在
logger.info(String.format("开始第%d次上传[%s].", ++uploadTimes, fileName));//%d 位置用++uploadTimes替换;%s用fileName替换
// 设置响应信息
StringMap policy = new StringMap();
policy.put("returnBody", CommunityUtil.getJSONString(0));
// 生成上传凭证
Auth auth = Auth.create(accessKey, secretKey);
String uploadToken = auth.uploadToken(shareBucketName, fileName, 3600, policy);//shareBucketName为空间名;fileName为文件名;3600为过期时间
// 指定上传机房
UploadManager manager = new UploadManager(new Configuration(Zone.zone1()));//zone1为华北机房
try {
// 开始上传图片
Response response = manager.put(
path, fileName, uploadToken, null, "image/" + suffix, false);
// 处理响应结果,得到json数据
JSONObject json = JSONObject.parseObject(response.bodyString());
if (json == null || json.get("code") == null || !json.get("code").toString().equals("0")) {
logger.info(String.format("第%d次上传失败[%s].", uploadTimes, fileName));
} else {
logger.info(String.format("第%d次上传成功[%s].", uploadTimes, fileName));
future.cancel(true);//任务结束
}
} catch (QiniuException e) {
logger.info(String.format("第%d次上传失败[%s].", uploadTimes, fileName));
}
} else {
logger.info("等待图片生成[" + fileName + "].");
}
}
}
测试:
图片越大,越容易失败,百度截图 图片小,第一次就成功了