在项目中,通常会涉及到图片信息的上传下载,常见的有用户头像、背景图、商品展示图、图片评论等。相比于常见的姓名、电话、商品描述等文字信息,图片类型的数据占用的空间会大很多,通常一张头像的大小基本都在几百K以上,因此,当项目中的图片信息量增多时,该如何解决图片的存储问题?
在做小型项目demo时,数据量小,通常我们可以直接将项目涉及到的图片信息直接存储在本地服务器,简单方便,但当图片数据量很大时,就会占用越来越多的本地内存,解决方案对本地服务器(就是跑项目的电脑)进行内存增加,成本相对较高。
另一种解决方案是,直接将图片存储在云存储服务中,如阿里云OSS、腾讯云COS等,这样不仅减少了本地内存的占用,还可以更加灵活地管理图片,并且可以实现跨多个服务器的负载均衡。
1. 可用性:本地服务器受本地网络和硬件故障的影响,可能会出现图片不可用的情况;而云服务器通常具有高可用性和容错能力,可以提供更好的可用性。
2. 存储空间:本地服务器的存储空间可能受到限制,需要手动扩展;而云服务器可以根据需要提供更多的存储空间。
3. 数据安全性:本地服务器的数据安全性通常依赖于服务器的安全性和访问控制;而云服务器通常提供多种安全措施,如数据备份、加密和访问控制等,同时有专业人员维护,可以提供更高的数据安全性。
4. 成本:本地服务器需要购买硬件和维护软件,成本较高;而云服务器通常是按需付费的,可以根据实际使用情况灵活调整成本。
下面是结合业务场景,展示两种方式在项目中的代码实现:
业务场景:在员工信息表展示页面中添加员工头像列,实现员工头像的上传、展示、下载。
如图:
页面上传 这里前端使用的是 elementUI 的 upload上传组件 实现
组件详情参考地址:组件 | Elementhttps://element.eleme.cn/#/zh-CN/component/upload
原理 比较简单,就是后端获取到上传文件之后,将文件存储到自己定义的文件路径,之后为该文件(这里指的就是头像图片)创建一个唯一的文件名,防止文件名重复出现覆盖情况(比如为不同的人上传了相同一张的头像图片),并用于获取对应图片信息回显。
为了方便理解前后端交互的实现,以及这张头像在前后端的流转过程,这里将按上传头像的步骤一步步展示前后端实现代码。
1、在页面点击头像上传按钮框,选取图片上传
上传之后来看下前端上传功能都做了些什么操作。
由于前端页面代码较长,这里只摘取代码相关部分。
内嵌了upload组件用于上传头像,以及一个标签用于上传之后展示头像,这里组件部分作了简单说明,具体使用详情参考element官网案例及属性介绍。
头像下载
简单一下方法之间调用关系:
getDataList 方法是获取页面表单数据用的,查询数据库表单的所有数据进行展示,在添加头像之后,头像字段avatar就不为空了,这里在获取数据之后的回调函数中会遍历avatar,调用头像下载方法,获取带头像的人员对应的头像,之后再展示到页面。
beforeAvatarUpload 是在选择图片完成,上传之前会执行的一个校验函数,这里主要是判断选取的头像是否符合自定义的要求,比如这里限制只能是jpg格式,且不大于2M。
handleAvatarSuccess 是上传完图片之后执行的方法,进行上传成功之后的一些操作,比如调用getDataList方法刷新页面数据等。
//上传成功后执行的钩子
handleAvatarSuccess(res, file) {
this.imageUrl = URL.createObjectURL(file.raw);
this.$message({
message: "上传成功",
type: "success",
duration: 1500,
onClose: () => { //关闭时的回调函数,
this.getDataList() //调用方法重新查询数据更新页面
}
})
},
//上传之前的钩子 校验图片合理性
beforeAvatarUpload(file) {
//此处限制了只能上传jpg格式文件 且文件不超过2M
const isJPG = file.type === 'image/jpeg';
const isLt2M = file.size / 1024 / 1024 < 2;
if (!(isJPG)) {
this.$message.error('上传头像图片只能是 JPG 格式!');
}
if (!isLt2M) {
this.$message.error('上传头像图片大小不能超过 2MB!');
}
return isJPG && isLt2M;
},
// 上传到本地 头像回显方法
imgReturn(row){
this.imageUrl = this.$http({
url: this.$http.adornUrl('/emp/user/download'),
method: 'get',
params: this.$http.adornParams({
'id': row.id
}),
responseType: 'blob' // 设置响应类型为blob
}).then(response => {
if (response.status === 200) {
// 创建一个临时URL
this.imageUrl = URL.createObjectURL(response.data)
row.avatar = this.imageUrl
}
})
},
//头像下载函数
downLoad(url) {
let link = document.createElement('a');
link.href = url;
link.download = '头像'; // 下载的文件名
link.click();
},
// 获取数据列表
getDataList() {
// 设置显示数据列表正在加载
this.dataListLoading = true
// 发送get请求获取数据集合
this.$http({
url: this.$http.adornUrl('/emp/user/list'),
method: 'get',
//设置请求参数:当前页码、每页条目、用户名
params: this.$http.adornParams({
'page': this.pageIndex,
'limit': this.pageSize,
'name': this.dataForm.name
})
}).then(({data}) => { // 回调函数处理返回数据
// 如果返回数据不为null且存在code属性为0,
//则将返回结果中的page.list赋值给this.dataList,将page.totalCount赋值给 this.totalPage
if (data && data.code === 0) {
this.dataList = data.page.list
this.totalPage = data.page.totalCount
//遍历数据集合 有头像的获取头像回显(本地存储)
for(let i = 0; i < this.dataList.length; i++){
if (this.dataList[i].avatar !== null && this.dataList[i].avatar !== ""){
console.log("for=>"+this.dataList[i].avatar)
this.imgReturn(this.dataList[i])
}
}
} else {
// 如果返回数据为null或code属性不为0,则设置this.dataList=[]和this.totalPage=0
this.dataList = []
this.totalPage = 0
}
//数据解析完成 将数据加载关闭
this.dataListLoading = false
})
}
前端提交数据之后,就会更根据访问路径将数据传递到后端对应的控制器,进行相应的数据处理,数据持久化等操作。
具体过程参考代码注释。
@RestController
@RequestMapping("emp/user")
public class EmpUserController {
@Autowired
private EmpUserService empUserService;
//通过@Value注解 直接给属性注入值
@Value("D:\\DellYangzhou\\upload\\img")
private String uploadPath;
//这里暂时用不到,是使用云存储时的自定义工具类
@Autowired
private AliOSSUtils aliOSSUtils;
/**
* 本地头像下载 回显
*/
@GetMapping("/download")
public void download(Long id, HttpServletResponse response) throws IOException {
EmpUserEntity emp = empUserService.getById(id);
if (emp.getAvatar() != null) {
// 根据头像存储地址创建文件输入流,读取头像
String filePath = uploadPath + File.separator + emp.getAvatar();
System.out.println("文件位置:" + filePath);
FileInputStream fileInputStream = new FileInputStream(filePath);
// 创建Servlet输出流,用于将文件内容写入响应
ServletOutputStream outputStream = response.getOutputStream();
// 设置响应的内容类型为image/jpeg,即图片类型为JPEG格式
response.setContentType("image/jpeg");
int len;
byte[] bytes = new byte[1024];
// 循环读取文件内容,并将内容写入响应输出流
while ((len = fileInputStream.read(bytes)) != -1) {
outputStream.write(bytes, 0, len);
outputStream.flush();
}
// 关闭文件输入流和输出流
fileInputStream.close();
outputStream.close();
}
}
/**
* 头像上传
* 方法一:图片上传到本地存储
*/
@PostMapping("/upload")
public R upload(MultipartFile avatar, Long id) throws IOException {
//获取 原始文件名
String originalFileName = avatar.getOriginalFilename();
System.out.println("原始文件名:" + originalFileName);
//断言 判断文件名是否有值 没有则抛出异常中断程序执行
assert originalFileName != null;
//使用UUID通用唯一识别码 + 后缀名的形式
//设置唯一文件路径 防止文件名重复 出现覆盖的情况
String fileName = UUID.randomUUID().toString() + originalFileName.substring(originalFileName.lastIndexOf("."));
//打印查看
System.out.println("唯一文件名:" + fileName);
// 指定文件保存的路径
String filePath = uploadPath + File.separator + fileName;
//文件名保存到对应数据的头像图片字段
EmpUserEntity emp = new EmpUserEntity();
//将文件名保存到数据库表的头像字段
emp.setAvatar(fileName);
emp.setId(id);
boolean uploadPhoto = empUserService.updateById(emp);
//图片路径保存到数据库表成功之后执行 将图片放入对应路径
if (uploadPhoto) {
//根据上传路径创建文件夹File对象
File saveAddress = new File(uploadPath);
if (!saveAddress.exists()) {
saveAddress.mkdirs();// 如果文件夹不存在 创建保存文件对应的文件夹
}
// 将上传的文件保存到指定路径
avatar.transferTo(new File(filePath));
return R.ok();
}
return R.error("上传失败,请确认参数信息是否正确");
}
}
在选取图片上传之后,浏览器发起请求,将图片文件以及行数据id传递到后端控制器。
后端控制器获取到上传文件数据,并为该文件生成唯一文件名,持久化存储到数据库,并将源文件图片存放到自定义本地目录,返回处理结果。
上传处理方法中,File.separator是Java IO包中File类的一个静态常量,它表示操作系统的文件分隔符。在Windows系统中,文件分隔符是"\",而在Unix和Linux系统中,文件分隔符是"/"。通过使用File.separator,我们可以根据当前操作系统的不同,动态地生成正确的文件路径。
这样可以保证我们的代码在不同的操作系统上都能够正常工作,提高了代码的可移植性。在上述代码中,我们使用uploadPath作为根路径,然后根据fileName拼接文件名,最终得到指定文件的完整路径。
上传操作完成之后,图片源文件写入了本地自定义目录,同时将生成的唯一文件名存储到了数据库中的对应人员当中。
前端刷新数据时,再次获取到的人员信息就会携带头像文件名,再经由头像下载回显方法拼接上本地磁盘路径,找到对应源文件读取并通过response写入前端,从而完成图片的显示操作。
头像下载 操作和回显调用的方法一样,只是再将通过response写到前端的数据通过下载按钮保存到其他位置而已,相当于复制另存为。
OK,到这里头像上传保存到本地的实现方式就已经结束了,整个流程大概就是这么个样子。
将上传文件保存到本地服务器的方式在实际生产环境中基本不会使用,通常只是在学习阶段用于练习,在实际开发中基本上用的都是云存储的方式。
将数据交由大厂维护的云服务器管理,减少本地服务器的负载,降低成本,同时大厂维护的云服务器通常都有异地备份等容灾功能,在数据安全性方面来说也更加可靠。
这里还是基于一样的业务场景,实现头像的上传下载,云服务器采用当前市面上使用广泛的阿里云OSS对象存储,将上传的文件保存到阿里云服务器。
了解阿里云OSS对象存储的使用,阿里云官网:阿里云-计算,为了无法计算的价值 (aliyun.com)https://www.aliyun.com/实现上传下载功能需要了解的东西,包括阿里云账号的注册、对象存储OSS服务的开通、Bucket的创建以及AccessKey访问凭证的生成和使用等,这个工作看一遍就会,这里就不再赘述了,随便找一篇博客跟着学学就行。
关于收费问题,不用太担心,我们个人日常练习的项目也用不了多大的空间,访问量也不大,基本上产生不了什么费用,个人小型项目练习的话,按0.12/G的标准存储计算,完全可以忽略不计,所以大可放心使用。
具体价格计算参考:价格计算器 (aliyun.com)https://www.aliyun.com/price/product?spm=a311a.7996332.0.0.64c4530ckx4Ajz#/oss/detail/ossbag
了解了阿里云OSS对象存储的使用之后,下边看代码实现过程。
前端代码方面和保存到本地没太大变化,相比与方式一,只是上传之后图片保存的位置发生了变化而已。
代码无变化,跟方式一的一致
这部分变化也不大,只是在这里俺偷了个懒,直接将上传到阿里云OSS对应的文件路径url作为头像属性值,直接存在了数据库表数据的头像字段,所以不需要通过二次请求获取图片源文件,后端代码有体现。
//上传成功后执行的钩子
handleAvatarSuccess(res, file) {
this.imageUrl = URL.createObjectURL(file.raw);
this.$message({
message: "上传成功",
type: "success",
duration: 1500,
onClose: () => { //关闭时的回调函数,
this.getDataList() //调用方法重新查询数据更新页面
}
})
},
//上传之前的钩子 校验图片合理性
beforeAvatarUpload(file) {
//此处限制了只能上传jpg格式文件 且文件不超过2M
const isJPG = file.type === 'image/jpeg';
const isLt2M = file.size / 1024 / 1024 < 2;
if (!(isJPG)) {
this.$message.error('上传头像图片只能是 JPG 格式!');
}
if (!isLt2M) {
this.$message.error('上传头像图片大小不能超过 2MB!');
}
return isJPG && isLt2M;
},
//头像下载函数
downLoad(url) {
let link = document.createElement('a');
link.href = url;
link.download = '头像'; // 下载的文件名
link.click();
},
// 获取数据列表
getDataList() {
// 设置显示数据列表正在加载
this.dataListLoading = true
// 发送get请求获取数据集合
this.$http({
url: this.$http.adornUrl('/emp/user/list'),
method: 'get',
//设置请求参数:当前页码、每页条目、用户名
params: this.$http.adornParams({
'page': this.pageIndex,
'limit': this.pageSize,
'name': this.dataForm.name
})
}).then(({data}) => { // 回调函数处理返回数据
// 如果返回数据不为null且存在code属性为0,
//则将返回结果中的page.list赋值给this.dataList,将page.totalCount赋值给 this.totalPage
if (data && data.code === 0) {
this.dataList = data.page.list
this.totalPage = data.page.totalCount
} else {
// 如果返回数据为null或code属性不为0,则设置this.dataList=[]和this.totalPage=0
this.dataList = []
this.totalPage = 0
}
//数据解析完成 将数据加载关闭
this.dataListLoading = false
})
}
(1)控制器类
@RestController
@RequestMapping("emp/user")
public class EmpUserController {
@Autowired
private EmpUserService empUserService;
//注解注入工具类
@Autowired
private AliOSSUtils aliOSSUtils;
/**
* 头像上传
* 方法二:图片上传到阿里云OSS保存
*/
@PostMapping("/upload")
public R upload(MultipartFile avatar, Long id) throws Exception {
//调用工具类方法将文件存入阿里云OSS
String url = aliOSSUtils.upload(avatar);
//将返回的url保存到数据库对应人员记录中
EmpUserEntity emp = new EmpUserEntity();
emp.setAvatar(url);
emp.setId(id);
//将url保存到数据库
if (empUserService.updateUser(emp)) {
System.out.println("id=" + id + ",图片上传完毕,url已存入数据库");
}
//返回对应文件的url
return R.ok().put("url", url);
}
}
(2)工具类
这里的工具类,是按照阿里云OSS对象存储的上传快速入门案例代码改装而来,用于将文件上传到配置的阿里云对象存储空间,CV之后本地化即可。
详情可以参考官网地址:如何使用OSSJavaSDK完成常见操作_对象存储 OSS-阿里云帮助中心 (aliyun.com)https://help.aliyun.com/zh/oss/developer-reference/getting-started?spm=a2c4g.11186623.0.0.56473ec2H5QqWG
代码中的两个自动注入属性,是在配置文件中自行定义好的,访问凭证则存在了环境变量中,这四个变量是上传到阿里云OSS不可缺少的。
/**
* 阿里云OSS文件上传工具类
*/
@Component
public class AliOSSUtils {
@Value("${aliOSS-utils.endpoint}")
private String endpoint; //服务端点
@Value("${aliOSS-utils.bucketName}")
private String bucketName; //bucket名称
// @Value("${aliOSS-utils.packageName01}")
// private String packageName01; //对象存储二级包路径
public String upload(MultipartFile file) throws Exception {
// 从环境变量中获取访问凭证。运行本代码示例之前,请确保已设置环境变量OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET。
EnvironmentVariableCredentialsProvider credentialsProvider = CredentialsProviderFactory.newEnvironmentVariableCredentialsProvider();
//获取上传文件的输入流
InputStream inputStream = file.getInputStream();
//使用UUID通用唯一识别码 + 后缀名的形式 防止文件名重复导致覆盖
String originalFileName = file.getOriginalFilename();
//增加断言 null值抛出异常
assert originalFileName != null;
String fileName = UUID.randomUUID().toString() + originalFileName.substring(originalFileName.lastIndexOf("."));
// 创建OSSClient实例。
OSS ossClient = new OSSClientBuilder().build(endpoint, credentialsProvider);
//创建对象名称(对象存储的位置)
String objectName = fileName;
//存储文件访问路径
String url = null;
//上传文件到OSS
try {
// 创建PutObjectRequest对象。
PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, objectName, inputStream);
// 如果需要上传时设置存储类型和访问权限,请参考以下示例代码。
// ObjectMetadata metadata = new ObjectMetadata();
// metadata.setHeader(OSSHeaders.OSS_STORAGE_CLASS, StorageClass.Standard.toString());
// metadata.setObjectAcl(CannedAccessControlList.Private);
// putObjectRequest.setMetadata(metadata);
// 上传文件。
PutObjectResult result = ossClient.putObject(putObjectRequest);
//拼接文件访问路径并返回 在endpoint名称中加入bucket名称 最后拼接上文件名
url = endpoint.split("//")[0] + "//" + bucketName + "." + endpoint.split("//")[1] + "/" + fileName;
} catch (OSSException oe) {
System.out.println("Caught an OSSException, which means your request made it to OSS, "
+ "but was rejected with an error response for some reason.");
System.out.println("Error Message:" + oe.getErrorMessage());
System.out.println("Error Code:" + oe.getErrorCode());
System.out.println("Request ID:" + oe.getRequestId());
System.out.println("Host ID:" + oe.getHostId());
} catch (ClientException ce) {
System.out.println("Caught an ClientException, which means the client encountered "
+ "a serious internal problem while trying to communicate with OSS, "
+ "such as not being able to access the network.");
System.out.println("Error Message:" + ce.getMessage());
} finally {
inputStream.close();
if (ossClient != null) {
ossClient.shutdown();
}
}
return url;
}
}
还是拿强哥测试一下
在上传之前看一下当前云服务器中的文件,当前包含一个文件夹一共是9条记录。
接下来给强哥上传头像
后端控制台打印
数据库查看
云服务器Bucket查看,成功上传并保存到云服务器中。
下载功能测试
对了,存储到云服务器的文件是可以通过文件url直接进行下载的。
完结撒花!