专栏名称: ImportNew
伯乐在线旗下账号,专注Java技术分享,包括Java基础技术、进阶技能、架构设计和Java技术领域动态等。
目录
相关文章推荐
厦门日报  ·  备受瞩目!福建这场县域经济盛会即将召开 ·  14 小时前  
厦门日报  ·  蛇年上班首日,百亿级新能源项目正式开工! ·  22 小时前  
厦门日报  ·  谷歌,被立案调查! ·  昨天  
51好读  ›  专栏  ›  ImportNew

聊聊文件上传的设计思路

ImportNew  · 公众号  ·  · 2024-02-27 07:36

正文

(给 ImportNew加星标,提高Java技能)


一、引言


为了满足不同环境和需求的变化,我们需要让自己写的代码涉及面更加广泛,为了支持不同平台的对象,我决定设计一个支持各种平台的文件上传,删除功能,相信一点能满足你的需求。

二、介绍主角


  • @ConditionalOnProperty:根据指定的属性值条件,决定是否创建该组件的实例。这使得组件的创建可以根据配置文件中的属性进行动态控制。
  • @ConfigurationProperties:将配置文件中的属性值绑定到该类的字段上,实现属性的自动注入。这样可以方便地从配置文件中读取和使用属性值。


下面是一个简单的案例


首先,定义一个文件上传接口 FileUploadService,其中包含文件上传的方法:

public interface FileUploadService {    void uploadFile(MultipartFile file);}

然后,创建不同平台的文件上传实现类,例如 LocalFileUploadService 和 S3FileUploadService:

@Component@ConditionalOnProperty(value = "file.upload.platform", havingValue = "local")public class LocalFileUploadService implements FileUploadService {    @Override    public void uploadFile(MultipartFile file) {        // 在本地平台上实现文件上传逻辑        System.out.println("Uploading file to local platform...");    }}
@Component@ConditionalOnProperty(value = "file.upload.platform", havingValue = "s3")public class S3FileUploadService implements FileUploadService { @Override public void uploadFile(MultipartFile file) { // 在S3平台上实现文件上传逻辑 System.out.println("Uploading file to S3 platform..."); }}

通过在配置文件(例如 application.properties)中设置 file.upload.platform 属性的值,可以选择性地使用不同平台的文件上传实现类:

file.upload.platform=local


file.upload.platform=s3

根据配置的不同,将使用相应的文件上传实现类。

这样,你就可以根据需要选择性地实现不同平台的文件上传接口,并通过配置文件来控制使用哪个实现类。

三、具体编写


给大家一个供参考的文件夹路径:


3.1 构思


我们需要简化的一些方面:

  1. 文件需要校验文件可用性
  2. 上传的文件是否需要重命名
  3. 接口的设计是否应该更广,比如支持上传到指定目录
  4. 使用者如何选择自己项目对应的平台等等

对于以上需求,我们先分析后,再进行实现。

我们需要对上传文件进行自定义校验,比如文件名,文件大小、文件后缀判断、对文件重命名等等,这些都是每个接口可能需要实现的内容,我们不可能让每个接口都去实现,这样就会造成以下情况:

// 伪代码展示冗余操作
// 本地public class LocalFileStorage{ public String uploadFile(String dir, MultipartFile file, String[] allowedExtension) { // 1. 校验文件大小 // 2. 判断文件类型 // 3. 重命名文件防止覆盖 }
}
// 阿里云public class AliyunFileStorage{
public String uploadFile(String dir, MultipartFile file, String[] allowedExtension) { // 1. 校验文件大小 // 2. 判断文件类型 // 3. 重命名文件防止覆盖 } }
// 腾讯云public class TencentFileStorage{
public String uploadFile(String dir, MultipartFile file, String[] allowedExtension) { // 1. 校验文件大小 // 2. 判断文件类型 // 3. 重命名文件防止覆盖 } }
// 其他的实现 ...

这让我想到了,AOP(Aspect Oriented Programming),我们可以对接口进行切面,在切面中实现这些不就好了?这样一下就解决了第 1 点和第 2 点。

第 3 点:构思的解决方案主要是在接口上进行扩展,很好解决。

第 4 点在文章开始已经说明,采用 @ConditionalOnProperty(..) 即可.

下面让我们来具体实现一下吧。

3.2 “赛前” 准备工作


项目中使用的 hutool 工具类,可自行导入。


媒体类型定义


MimeTypeConstant.java

/** * 媒体类型常量 * * @author yiFei */public class MimeTypeConstant {
public static final String[] IMAGE_EXTENSION = {"bmp", "gif", "jpg", "jpeg", "png"};
public static final String[] FLASH_EXTENSION = {"swf", "flv"};
public static final String[] MEDIA_EXTENSION = {"swf", "flv", "mp3", "wav", "wma", "wmv", "mid", "avi", "mpg", "asf", "rm", "rmvb"};
public static final String[] VIDEO_EXTENSION = {"mp4", "avi", "rmvb"};
public static final String[] DEFAULT_ALLOWED_EXTENSION = { // 图片 "bmp", "gif", "jpg", "jpeg", "png", // word excel powerpoint "doc", "docx", "xls", "xlsx", "ppt", "pptx", "html", "htm", "txt", // 压缩文件 "rar", "zip", "gz", "bz2", // 视频格式 "mp4", "avi", "rmvb", // pdf "pdf"};}


配置类定义


用于读取配置类中一些基础配置方便后续根据用户配置进行校验。

FileStorageConfig.java

/** * 文件上传配置类 * 如果需要限制单个文件大小和最大文件大小: * 请通过 spring.servlet.multipart.max-file-size / max-request-size 设置 * * @author yiFei */@Component@ConfigurationProperties(prefix = "file.storage")@Datapublic class FileStorageConfig {    /**     * 上传服务器类型: 本地上传(local) / Minio(minio) / 七牛云(qiniu) / 阿里云(aliyun) / 腾讯云(tencent)     */    private String type = "local";    /**     * 默认支持文件上传类型:     * 可在调用上传方法时,覆盖该属性     */    private String[] allowedExtension = IMAGE_EXTENSION;    /**     * 上传文件名最大值     */    private int fileNameLength = 100;    /**     * 是否覆盖文件名     */    private boolean coverFileName = true;
// /**// * 单个文件最大值// */// private String maxFileSize = "";// /**// * 多个文件最大值// */// private String maxRequestSize = "";}


FileUtils 工具类编写


public class FileUtils {
public static final String DOT = ".";
public static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy" + File.separator + "MM" + File.separator + "dd" + File.separator);
/** * 判断MIME类型是否是允许的MIME类型 * * @param extension 文件类型 * @param allowedExtension 允许的文件类型 * @return 是否允许 */ public static boolean isAllowedExtension(String extension, String[] allowedExtension) { for (String str : allowedExtension) { if (str.equalsIgnoreCase(extension)) { return true; } } return false; }
/** * 获取文件名的后缀 * 举例: *.jpg ==== > jpg * * @param file 文件 * @return 后缀名 */ public static String getFileExtension(MultipartFile file) { // 1. 获取文件名 String originalFilename = file.getOriginalFilename(); if (originalFilename == null) { throw new RuntimeException("FileUtils: originalFilename is null"); } String fileExtension; int lastDotIndex = originalFilename.lastIndexOf('.'); if (lastDotIndex > 0) { // 1.1 如果是 *.jpg ==== > jpg fileExtension = originalFilename.substring(lastDotIndex + 1); } else { // 1.2 如果用户未传入后缀,根据上传类型判断后缀 MimeType mimeType = MimeTypeUtils.parseMimeType(Objects.requireNonNull(file.getContentType())); fileExtension = mimeType.getSubtype(); } return fileExtension; }
/** * 矫正用户传入的路径 ( 会自动拼接后缀 File.separator , 会矫正 // 或者 /// 等 ) * * @param paths 路径集合 * @return 矫正后的路径 */ public static String correctAndJoinPaths(String... paths) { StringBuilder result = new StringBuilder(); for (String path : paths) { if (path != null && !path.isEmpty()) { if (!result.isEmpty() && result.charAt(result.length() - 1) != File.separatorChar && !path.startsWith(File.separator)) { result.append(File.separator); } result.append(path.replaceAll("[" + File.separator + "/\\]+$", "")); } } if (!result.isEmpty() && result.charAt(result.length() - 1) != File.separatorChar) { result.append(File.separator); } return result.toString(); }
/** * 返回生成的日期路径 * "yyyy" + File.separator + "MM" + File.separator + "dd" + File.separator * * @return 日期路径 */ public static String datePath() { return LocalDate.now().format(FORMATTER); }
}

3.3 编写代码



定义 FileStorageService 接口


  • 这个接口提供了一组方法来处理文件上传和删除的操作,并提供了一些默认实现来简化使用。你可以根据自己的需求实现该接口,并在实现类中提供具体的上传和删除逻辑。
  • 虽然看着接口内容很多,但是实现者只需要实现 uploadFile() , deleteFile() 即可。

/** * 文件上传接口 * * @author yiFei */public interface FileStorageService {    /**     * 上传单个文件     *     * @param dir              文件存放路径 ( 用于调用者动态分类 ) ( 编写时请注意 dir 可能为空 )     * @param file             文件     * @param allowedExtension 允许上传的文件类型     * @return 文件上传后的访问路径     */    String uploadFile(String dir, MultipartFile file, String[] allowedExtension);
/** * 上传多个文件 * * @param dir 保存路径 * @param files 文件 * @param allowedExtension 允许上传的文件类型 * @return 文件上传后的访问路径数组 */ default String[] uploadFiles(String dir, MultipartFile[] files, String[] allowedExtension) { return Arrays.stream(files).map(file -> this.uploadFile(dir, file, allowedExtension)).toArray(String[]::new); }
/** * 删除单个文件 * * @param dir 保存路径 * @param url 文件访问路径 * @return 是否删除成功,注: 不报错则返回 true */ boolean deleteFile(String dir, String url);
/** * 删除多个文件 * * @param dir 保存路径 * @param urls 文件访问路径集合 * @return 是否删除成功,注: 不报错则返回 true */ default boolean deleteFiles(String dir, String[] urls) { return Arrays.stream(urls).allMatch(url -> this.deleteFile(dir, url)); }
/** * 删除单个文件 * * @param url 文件访问路径 * @return 是否删除成功,注: 不报错则返回 true */ default boolean deleteFile(String url) { return deleteFile("", url); }
/** * 删除多个文件 * * @param urls 文件访问路径集合 * @return 是否删除成功,注: 不报错则返回 true */ default boolean deleteFiles(String[] urls) { return Arrays.stream(urls).allMatch(this::deleteFile); }
/** * 上传单个文件( 使用 FileStorageConfig 中允许的文件类型) * * @param dir 文件存放路径 * @param file 文件 * @return 文件上传后的访问路径 */ default String uploadFile(String dir, MultipartFile file) { return uploadFile(dir, file, null); }
/** * 上传单个文件( 使用 FileStorageConfig 中允许的文件类型、直接存储在 baseDir 文件夹下) * * @param file 文件 * @param allowedExtension 允许上传的文件类型 * @return 文件上传后的访问路径 */ default String uploadFile(MultipartFile file, String[] allowedExtension) { return uploadFile("", file, allowedExtension); }
/** * 上传单个文件( 使用 FileStorageConfig 中允许的文件类型、直接存储在 baseDir 文件夹下) * * @param file 文件 * @return 文件上传后的访问路径 */ default String uploadFile(MultipartFile file) { return uploadFiles("", new MultipartFile[]{file}, null)[0]; }
/** * 上传多个文件( 使用 FileStorageConfig 的 allowedExtension) * * @param dir 文件存放路径 * @param files 文件集合 * @return 文件上传后的访问路径 */ default String[] uploadFiles(String dir, MultipartFile[] files) { return Arrays.stream(files).map(file -> this.uploadFile(dir, file)).toArray(String[]::new); }
/** * 上传多个文件( 使用 FileStorageConfig 的 allowedExtension) * * @param files 文件集合 * @param allowedExtension 允许上传的文件类型 * @return 文件上传后的访问路径 */ default String[] uploadFiles(MultipartFile[] files, String[] allowedExtension) { return Arrays.stream(files).map(file -> this.uploadFile(file, allowedExtension)).toArray(String[]::new); }
/** * 上传多个文件( 使用 FileStorageConfig 的 allowedExtension) * * @param files 文件集合 * @return 文件上传后的访问路径 */ default String[] uploadFiles(MultipartFile[] files) { return Arrays.stream(files).map(this::uploadFile).toArray(String[]::new); }
}

本地上传进行实现


接口对应实现,这里只给出本地文件上传的实现方法,其他方法类似。

/** * 本地文件上传 ( 默认 ) * 注: 设置matchIfMissing = true会使havingValue失效。这里只是为表明此类加载的是 local * * @author yiFei */@Component@ConditionalOnProperty(value = "file.storage.type", havingValue = "local", matchIfMissing = true)@ConfigurationProperties(prefix = "file.storage.local")@RequiredArgsConstructor@Datapublic class LocalFileStorageImpl implements FileStorageService {
private static final Logger log = LoggerFactory.getLogger(LocalFileStorageImpl.class); /** * 上传路径 */ private String uploadPath; /** * 访问的路径名 ( 请根据项目情况控制是否放行文件,比如允许不登录即可访问 /images ) */ private String accessUrl = "/images";

/** * 配置文件访问路径和实际文件存储路径的映射关系,使得通过指定的访问路径可以访问到对应的文件系统中的资源 * * @return WebMvcConfigurer */ @Bean public WebMvcConfigurer resourceHandlerConfigurer() { return new WebMvcConfigurer() { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { /* 映射: /images/** ---------> file:uploadPath 举例: http://ip:host/images/1.png ---------> file:\www\images\1.png */ String fileUploadPath = FileUtils.correctAndJoinPaths(uploadPath); registry.addResourceHandler(accessUrl + "/**").addResourceLocations("file:" + fileUploadPath); } }; }
/** * 上传单个文件 * * @param dir 文件存放路径 ( 用于调用者动态分类 ) ( 编写时请注意 dir 可能为空 ) * @param file 文件 * @param allowedExtension 允许上传的文件类型 * @return 文件上传后的访问路径 */ @Override public String uploadFile(String dir, MultipartFile file, String[] allowedExtension) { // 获取上传文件的绝对路径 String absolutePath = FileUtils.correctAndJoinPaths(uploadPath, dir, FileUtils.datePath()); try { // 1.1 获取文件对象上传 File absoluteFile = getAbsoluteFile(absolutePath, file.getOriginalFilename()); // 1.2 上传文件 file.transferTo(absoluteFile); } catch (IOException e) { log.error("上传文件失败, 常见错误: 未开启路径权限: {}", absolutePath); throw new ServiceException(ResultCode.FILE_UPLOAD_ERROR); } // 2. 返回给前端一个访问链接 return getRequestFileName(file, accessUrl + "/" + dir + "/" + FileUtils.datePath()); }
/** * 删除单个文件 * * @param dir 保存路径 * @param url 文件访问路径 * @return 是否删除成功,注: 不报错则返回 true */ @Override public boolean deleteFile(String dir, String url) { // 1. 从 url 中获取文件名 String fileName = extractFileNameFromUrl(url); // 2. 获取文件所在路径 String absolutePath = FileUtils.correctAndJoinPaths(uploadPath, dir, fileName); // 3. 构建文件对象 File file = new File(absolutePath); // 4. 删除文件 if (file.exists()) { // 4.1 文件存在,进行删除 if (!file.delete()) { log.error("File deletion failed: {}", absolutePath); return false; } } else { // 4.2 文件不存在,返回 false log.warn("File does not exist for deletion: {}", absolutePath); return false; } return true; }
/** * 获取该文件的访问路径 * * @param file 文件 * @param accessUrl 访问路径 * @return url */ private String getRequestFileName(MultipartFile file, String accessUrl) { HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest(); String baseUrl = request.getRequestURL().toString().replace(request.getRequestURI(), request.getContextPath()); String originalFilename = file.getOriginalFilename(); // 注: 拼接访问路径为 "/" String requestFileName = baseUrl + accessUrl + originalFilename; return requestFileName.replace("//", "/").replace(File.separator, "/"); }
/** * @param uploadDir 上传文件路径 * @param fileName 文件名 * @return File */ private File getAbsoluteFile(String uploadDir, String fileName) { File desc = new File(uploadDir + fileName); // 创建上传文件需要的文件夹 if (!desc.exists()) { if (!desc.getParentFile().exists()) { desc.getParentFile().mkdirs(); } } return desc; }
/** * 从文件访问路径中提取文件名 * * @param url 文件访问路径





请到「今天看啥」查看全文