🎯导读:本文探讨了在网站开发中图片存储的各种方法,包括本地文件系统存储、对象存储服务(如阿里云OSS)、数据库存储、分布式文件系统及内容分发网络(CDN)。文中详细对比了这些方法的优缺点,并针对不同规模的应用推荐了相应的存储策略。此外,还提供了一个基于SpringBoot框架实现服务器本地图片存储与回显的具体示例,涵盖了从图片上传到通过网关服务安全访问图片的全过程。
文章目录
- 前言
- 文件存储分类
- 本地文件系统存储
- 对象存储服务(OSS)
- 数据库存储
- 分布式文件系统
- 内容分发网络(CDN)
- 总结
- 服务器本地图片存储实现
- 图片上传实现
- Gateway 服务
- 将图片访问链接添加到白名单,跳过登录验证
- 过滤器
- Admin 服务
- 依赖
- Controller
- Service
- impl
- 错误码枚举类
- 工具类
- 图片回显
- 静态资源处理
- 功能测试
前言
在开发网站时,存储图片是常见的需求之一,尤其对于社交平台、电子商务网站等应用。例如社交平台要存储用户头像,要存储用户发布的朋友圈图片,聊天记录图片等等
文件存储分类
为了满足不同场景下的需求,图片存储有多种常见的方式,每种方式都有其适用场景和特点。以下是几种常用的图片存储方式及其优缺点分析
本地文件系统存储
描述:将图片直接保存在Web服务器的本地文件系统中,通常会有一个固定的目录用于存放这些图片文件。
优点:
- 简单易用:实现起来非常简单,适合小型项目快速搭建或者图片较少的项目。
- 成本低廉:初期成本较低,不需要额外支付存储费用。
缺点:
- 扩展性差:随着图片数量的增加,服务器存储空间会变得紧张,需要频繁地扩容。
- 备份困难:需要手动进行备份,且容易出现单点故障。
- 安全性低:服务器直接暴露在互联网上,存在安全隐患。
- 访问速度受限:图片访问速度受限于服务器带宽,如果有多人同时访问,部分用户需要等待。
对象存储服务(OSS)
描述:使用云服务商提供的对象存储服务,如阿里云OSS、AWS S3、Google Cloud Storage等,将图片存储在云端。
优点:
- 高可用性和持久性:提供多副本冗余,确保数据的高可用性和持久性。
- 易于扩展:可以根据实际需求动态调整存储空间,无需担心物理存储限制。
- 安全性高:提供多种安全机制,如访问控制、数据加密等。
- 全球分布:支持全球多区域部署,提高用户访问速度。
- 成本效益:按需付费,适合业务量波动较大的应用。
缺点:
- 成本随业务增长而增加:随着存储和带宽的增加,费用也会相应增加。
- 访问速度受网络影响:网络状况不佳时,图片加载速度可能变慢。
- 依赖第三方服务:需要考虑服务商的服务质量和稳定性。
- 需要严格控制图片安全:如果没有严格做好防控措施,图片一旦被有心之人频繁刷量,会造成巨大的费用
数据库存储
描述:将图片以二进制形式存储在数据库中,通常会使用BLOB(Binary Large Object)字段。
优点:
- 事务一致性:图片和相关数据可以保持事务一致性,适合需要强一致性的应用场景。
- 便于管理:图片和元数据可以集中管理,方便查询和操作。
缺点:
- 性能较差:读取和写入大文件时性能较差,可能会影响数据库的整体性能。
- 存储成本高:数据库存储成本较高,不适合大量图片的存储。
- 备份和恢复复杂:数据库备份和恢复过程较为复杂,且占用较多资源。
分布式文件系统
描述:使用分布式文件系统(如Hadoop HDFS、GlusterFS等)来存储图片,适合大规模分布式存储需求。
优点:
- 高可用性和扩展性:支持横向扩展,可以通过增加节点来提升存储能力和访问性能。
- 容错能力强:具有良好的容错能力,可以自动处理节点故障。
- 成本效益:相对于传统存储方式,成本更低,适合大规模数据存储。
缺点:
- 复杂度高:实现和维护相对复杂,需要专业的运维团队。
- 访问速度受网络影响:网络状况不佳时,访问速度可能受影响。
- 学习曲线陡峭:需要一定的学习成本和技术积累。
内容分发网络(CDN)
描述:结合对象存储服务和CDN,将图片存储在云存储中,并通过CDN加速分发,提高全球用户的访问速度。
优点:
- 全球加速:通过CDN缓存,提高全球用户的访问速度。
- 高可用性和持久性:结合对象存储服务,确保数据的高可用性和持久性。
- 成本效益:按需付费,适合业务量大的应用。
缺点:
- 成本较高:使用CDN会增加一定的成本。
- 配置复杂:需要配置CDN和对象存储服务,相对复杂。
- 也要严格做好防控措施,否则钱包非常危险
总结
选择合适的图片存储方式需要综合考虑项目的规模、预算、性能要求和未来的扩展性。对于小型项目或初创团队,可以先从本地文件系统存储开始,随着业务的增长逐步迁移到对象存储服务或分布式文件系统。对于大型应用或全球用户较多的场景,建议使用对象存储服务结合CDN,以确保高性能和高可用性。
服务器本地图片存储实现
对于个人开发的小网站,如果图片不多,网站访问量也不多,使用服务器本地存储的方式是比较适合的,本文主要讲解SpringBoot项目如何实现图片如何存储到服务器本地以及前端要怎样才能显示服务器中的图片资源
演示项目说明:该项目使用微服务架构开发,请求首先需要通过Gateway网关,然后转给对应的Admin服务,Admin服务中再处理图片的存储和回显
图片上传实现
Gateway 服务
将图片访问链接添加到白名单,跳过登录验证
spring:
profiles:
active: damWin
application:
name: vrs-gateway
cloud:
gateway:
routes:
- id: vrs-admin
uri: lb://vrs-admin/admin/**
predicates:
- Path=/admin/**
filters:
- name: TokenValidate
args:
whitePathList:
- /admin/user/v1/login
- /admin/user/v1/has-username
- /admin/user/v1/register
- /admin/pic/
过滤器
拦截请求,将网关服务的访问域名放置到请求头中,这样Admin服务就可以在请求头中拿到网关服务的访问域名了
package com.vrs.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import java.net.InetAddress;
import java.net.UnknownHostException;
/**
* @Author dam
* @create 2024/11/21 20:55
*/
@Component
public class CustomGlobalFilter {
@Value("${server.port}")
private int serverPort;
/**
* 将网关服务的域名端口放到请求头中,方便其他服务使用
* @return
*/
@Bean(name = "customGlobalFilter1")
public GlobalFilter customGlobalFilter() {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
String hostAndPort = getHostAndPort(request);
ServerHttpRequest modifiedRequest = request.mutate()
.header("X-Gateway-Host", hostAndPort)
.build();
ServerWebExchange modifiedExchange = exchange.mutate().request(modifiedRequest).build();
return chain.filter(modifiedExchange);
};
}
/**
* 获取网关服务的访问域名端口
* @param request
* @return
*/
private String getHostAndPort(ServerHttpRequest request) {
try {
// 获取请求的主机名和端口
String host = request.getURI().getHost();
if (host == null) {
// 如果请求中没有主机名,使用本地主机名
host = InetAddress.getLocalHost().getHostName();
}
// 获取请求的协议(HTTP或HTTPS)
String scheme = request.getURI().getScheme();
return scheme + "://" + host + ":" + serverPort;
} catch (UnknownHostException e) {
return "unknown";
}
}
}
Admin 服务
依赖
<!-- 图片压缩工具 -->
<dependency>
<groupId>net.coobird</groupId>
<artifactId>thumbnailator</artifactId>
<version>0.4.8</version>
</dependency>
Controller
注意:里面有一步是从请求头中获取网关的访问域名,后面再基于网关的访问域名来拼接图片访问路径,不能直接将Admin服务的域名暴露出去,不然用户可能直接绕过网关的登录验证来访问Admin服务
package com.vrs.controller;
import com.vrs.convention.result.Result;
import com.vrs.service.PictureService;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.util.Map;
/**
* @Author dam
* @create 2024/11/21 17:00
*/
@RequiredArgsConstructor
@RestController
@RequestMapping("/pic/")
public class PictureController {
private final PictureService pictureService;
/**
* 通用上传请求(单个)
*/
@PostMapping("/upload")
public Result<Map<String, Object>> uploadFile(MultipartFile file, HttpServletRequest request) throws Exception {
String gatewayHost = request.getHeader("X-Gateway-Host");
return pictureService.uploadFile(file, gatewayHost);
}
}
Service
package com.vrs.service;
import com.vrs.convention.result.Result;
import org.springframework.web.multipart.MultipartFile;
import java.util.Map;
/**
* @Author dam
* @create 2024/11/21 17:12
*/
public interface PictureService {
Result<Map<String, Object>> uploadFile(MultipartFile file, String request);
}
impl
package com.vrs.service.impl;
import com.vrs.common.utils.PictureUploadUtil;
import com.vrs.convention.exception.ClientException;
import com.vrs.convention.result.Result;
import com.vrs.convention.result.Results;
import com.vrs.service.PictureService;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.util.HashMap;
import java.util.Map;
import static com.vrs.common.enums.ErrorCodeEnum.PICTURE_UPLOAD_FAIL;
/**
* @Author dam
* @create 2024/11/21 17:12
*/
@Service
public class PictureServiceImpl implements PictureService {
@Override
public Result<Map<String, Object>> uploadFile(MultipartFile file, String gatewayDomain) {
// 上传并返回新文件名称
String fileName = PictureUploadUtil.upload(file);
String url = gatewayDomain + "/admin" + fileName;
Map<String, Object> resultMap = new HashMap<>();
resultMap.put("url", url);
resultMap.put("fileName", fileName);
resultMap.put("originalFileName", file.getOriginalFilename());
return Results.success(resultMap);
}
}
错误码枚举类
package com.vrs.common.enums;
import com.vrs.convention.errorcode.IErrorCode;
/**
* 用户错误码
*/
public enum ErrorCodeEnum implements IErrorCode {
PICTURE_UPLOAD_FAIL("B000301", "图片上传失败"),
PICTURE_NAME_EXCEED_LENGTH("B000302", "图片名超出长度"),
PICTURE_SUFFIX_ERROR("B000302", "图片名没有携带正常后缀名"),
PICTURE_TYPE_ERROR("B000302", "图片格式不对,仅限于 .png .jpg .jpeg .gif"),
;
private final String code;
private final String message;
ErrorCodeEnum(String code, String message) {
this.code = code;
this.message = message;
}
@Override
public String code() {
return code;
}
@Override
public String message() {
return message;
}
}
工具类
该工具类主要完成对图片的校验,然后将图片存储到服务器本地,最后返回图片的访问路径
package com.vrs.common.utils;
import com.vrs.convention.exception.ClientException;
import com.vrs.convention.exception.ServiceException;
import net.coobird.thumbnailator.Thumbnails;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.time.LocalDate;
import java.util.Objects;
import java.util.UUID;
import static com.vrs.common.enums.ErrorCodeEnum.*;
/**
* @Author dam
* @create 2024/11/21 17:18
*/
public class PictureUploadUtil {
/**
* 资源映射路径 前缀
*/
public static final String PIC_PREFIX = "/pic";
/**
* 图片存储的根路径
*/
public static final String UPLOAD_PATH = System.getProperty("user.dir") + File.separator + "upload";
/**
* 默认的文件名最大长度 100
*/
public static final int DEFAULT_FILE_NAME_LENGTH = 100;
public static final String upload(MultipartFile file) {
int fileNameLength = Objects.requireNonNull(file.getOriginalFilename()).length();
if (fileNameLength > DEFAULT_FILE_NAME_LENGTH) {
// --if-- 如果图片名称过程,抛异常
throw new ClientException(PICTURE_NAME_EXCEED_LENGTH);
}
String name = file.getOriginalFilename();
if (!name.contains(".")) {
// --if-- 如果图片没有正常后缀
throw new ClientException(PICTURE_SUFFIX_ERROR);
} else if (!(name.endsWith(".png") || name.endsWith(".jpg") || name.endsWith(".jpeg") || name.endsWith(".gif"))) {
// --if-- 校验文件是否为图片类型
throw new ClientException(PICTURE_TYPE_ERROR);
}
String[] split = name.split("\\.");
// 图片名称
String fileName = split[0];
// 图片后缀
String fileSuffix = split[1];
// 获取当前日期
LocalDate date = LocalDate.now();
// 获取年份
int year = date.getYear();
// 获取月份
int month = date.getMonthValue();
// 获取日期
int day = date.getDayOfMonth();
String dir = File.separator + year + File.separator + month + File.separator + day;
File dirFile = new File(UPLOAD_PATH + dir);
if (!dirFile.exists()) {
// 创建相应日期文件夹
dirFile.mkdirs();
}
// 生成一个唯一ID
String uuid = UUID.randomUUID().toString().replace("-", "");
// 相对路径
String relativePath = dir + File.separator + fileName + "-" + uuid + "." + fileSuffix;
// 生成图片要上传到的绝对路径
String absPath = UPLOAD_PATH + relativePath;
// 压缩存储
// try (InputStream is = file.getInputStream()) {
// // 设置目标文件
// File targetFile = new File(absPath);
//
// // 使用Thumbnails库调整图片分辨率
// Thumbnails.of(is)
// // 设置最大宽度和高度,保持原始比例
// .size(1920, 1080)
// .toFile(targetFile);
// } catch (Exception e) {
// e.printStackTrace();
// throw new ServiceException(e.getMessage());
// }
// 直接保存文件(不压缩)
try (InputStream is = file.getInputStream()) {
File targetFile = new File(absPath);
// 将输入流中的数据复制到目标文件中
java.nio.file.Files.copy(is, targetFile.toPath(), java.nio.file.StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
e.printStackTrace();
throw new ServiceException(e.getMessage());
}
return PIC_PREFIX + "/" + year + "/" + month + "/" + day + "/" + fileName + "-" + uuid + "." + fileSuffix;
}
}
图片回显
图片回显的实现比较简单,添加一个配置类即可
静态资源处理
这段代码的作用是:把网站上显示图片的URL路径(比如 /pic/**
)映射到服务器上的一个实际文件夹路径。这样设置后,用户通过浏览器访问这些URL时,就能直接看到存储在服务器指定文件夹里的图片
import com.vrs.common.utils.PictureUploadUtil;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* 通用配置
*
* @author dam
*/
@Configuration
public class ResourcesConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// 将图片请求路径映射到对应的本地图片路径
registry.addResourceHandler(PictureUploadUtil.PIC_PREFIX + "/**")
.addResourceLocations("file:" + PictureUploadUtil.UPLOAD_PATH + "/");
}
}
功能测试
使用Apifox软件发起一个Post请求,带上要上传的图片
通过链接访问图片,测试通过