查看原文
其他

聊聊文件上传的设计思路

小哈学Java 2024-04-16

来源:juejin.cn/post/7308621051524546597

👉 欢迎加入小哈的星球 ,你将获得: 专属的项目实战 / Java 学习路线 / 一对一提问 / 学习打卡 /  赠书福利


全栈前后端分离博客项目 2.0 版本完结啦, 演示链接:http://116.62.199.48/ ,新项目正在酝酿中。全程手摸手,后端 + 前端全栈开发,从 0 到 1 讲解每个功能点开发步骤,1v1 答疑,直到项目上线。目前已更新了239小节,累计38w+字,讲解图:1645张,还在持续爆肝中.. 后续还会上新更多项目,目标是将Java领域典型的项目都整一波,如秒杀系统, 在线商城, IM即时通讯,Spring Cloud Alibaba 等等,戳我加入学习,已有1100+小伙伴加入(早鸟价超低)


  • 一、引言
  • 二、介绍主角
    • 下面是一个简单的案例
  • 三、具体编写
    • 3.1 构思
    • 3.2 “赛前” 准备工作
    • 3.3 编写代码
  • 四、结束语

一、引言

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

二、介绍主角

  • @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")
@Data
public 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
@Data
public 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 文件访问路径
     * @return 文件名
     */
    private String extractFileNameFromUrl(String url) {
        // 举例: 如果 url 是 "/images/dir1/1.png",则提取出的文件名是 "1.png"
        return url.substring(url.lastIndexOf('/') + 1);
    }
}

到这里其实已经算是一个简单的实现了,但是还不够,我们需要通过FileStorageConfig的配置信息对文件进行校验,通过判断 FileStorageConfig.coverFileName 属性 决定是否使文件更改文件名。

对 FileStorageService 切面编程

  • 采用 AspectJ 进行切面编程,看代码太麻烦,我对每个方法进行口述一下
  • @Around 注解:用于定义环绕通知,指定切入点表达式为 execution(* com.yifei.service.FileStorageService.uploadFile*(..)),表示匹配 FileStorageService 接口中以 "uploadFile" 开头的方法。
  • aroundUploadFile() 方法:环绕通知方法,在目标方法执行前后进行增强操作。该方法的参数为 ProceedingJoinPoint 类型,用于获取目标方法的信息。
  • 在 aroundUploadFile() 方法中,首先获取目标方法的参数对象、参数名和参数类型。
  • 然后遍历参数对象,判断是否为文件对象、文件数组对象或允许上传的文件类型。
  • 根据配置文件参数类型,对文件对象或文件数组对象进行修改和校验。
  • modifyArguments() 方法:根据文件对象或文件数组对象的情况,对参数列表进行修改。
  • modifyFile() 方法:校验并修改文件对象。首先获取文件名和后缀进行校验,然后根据配置决定是否重命名文件。
  • isAllowedFile() 方法:校验文件名长度和文件类型是否允许上传。

这段代码通过切面编程的方式,在文件上传方法执行前后进行了增强操作,包括校验文件名、文件类型和文件重命名等。

/**
 * 增强 FileStorageService 上传文件方法 ( 校验 ,修改文件名 ... )
 *
 * @author yiFei
 */
@Aspect
@Component
public class FileStorageAspect {

    @Autowired
    private FileStorageConfig fileStorageConfig;

    @Around("execution(* com.yifei.service.FileStorageService.uploadFile*(..))")
    public Object aroundUploadFile(ProceedingJoinPoint joinPoint) throws Throwable {
        // 1.1 获取切面方法的参数对象
        Object[] args = joinPoint.getArgs();
        // 1.2 获取切面方法的参数对象的参数名
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        // 1.3 获取切面方法的参数对象的类型
        Class[] parameterTypes = signature.getParameterTypes();
        // 2. 遍历对象,进行增强 ( XXX: 提供思路,可通过 Java 8 的 Optional 类和 Stream API 进行简化和优化 )
        // 注 : filesIndex 标识 file 在 args 中的位置 ( file 和 files 在参数中只存在一个 )
        int fileArgsIndex = -1;
        MultipartFile file = null;
        MultipartFile[] files = null;
        String[] allowedExtension = null;

        for (int i = 0; i < args.length; i++) {
            // 可校验: "file".equals(parameterNames[i]) && parameterTypes[i].equals(MultipartFile.class)
            if (parameterTypes[i].equals(MultipartFile.class)) {
                // 2.1 如果为 file ( 重构 MultipartFile )
                file = (MultipartFile) args[i];
                // 2.1.1 标记 fileArgsIndex
                fileArgsIndex = i;
            } else if (parameterTypes[i].equals(MultipartFile[].class)) {
                // 2.2 如果为 files ( 遍历重构 MultipartFile )
                files = (MultipartFile[]) args[i];
                // 2.2.1 标记 fileArgsIndex
                fileArgsIndex = i;
            } else if (parameterTypes[i].equals(String[].class)) {
                // 2.3 如果为 allowedExtension ( 记录 allowedExtension 用于校验 )
                allowedExtension = (String[]) args[i];
            }
        }
        // 3. 判断是否修改文件 & 校验文件是否可以上传
        modifyArguments(file, files, args, fileArgsIndex, allowedExtension);
        // 4. 调用目标方法,并传入修改后的参数列表
        return joinPoint.proceed(args);
    }

    private void modifyArguments(MultipartFile file, MultipartFile[] files, Object[] args, int fileArgsIndex, String[] allowedExtension) {
        if (file != null || files != null) {
            if (file != null) {
                // 对 file 操作
                args[fileArgsIndex] = modifyFile(file, allowedExtension);
            } else {
                // 对 files 操作
                for (int i = 0; i < files.length; i++) {
                    files[i] = modifyFile(files[i], allowedExtension);
                }
                args[fileArgsIndex] = files;
            }
        }
    }

    /**
     * 校验以及修改文件
     *
     * @param file             上传的文件
     * @param allowedExtension 允许上传的文件类型
     * @return 修改后的文件
     */
    private MultipartFile modifyFile(MultipartFile file, String[] allowedExtension) {
        // 1. 获取文件名和后缀进行校验
        String originalFilename = Objects.requireNonNull(file.getOriginalFilename());
        String extension = FileUtils.getFileExtension(file);
        // 2. 校验文件
        isAllowedFile(allowedExtension, originalFilename, extension);
        // 3. 是否重命名文件
        if (fileStorageConfig.isCoverFileName()) {
            String uuid = UUID.fastUUID().toString().replace("-""");
            // 3.1 重命名文件
            String realName = originalFilename.substring(0, originalFilename.lastIndexOf(FileUtils.DOT));
            file = new CustomMultipartFile(file,  realName + "_" + uuid + FileUtils.DOT + extension);
        }
        return file;
    }

    /**
     * 校验是否允许上传
     *
     * @param allowedExtension 允许的类型
     * @param originalFilename 文件名
     * @param extension        文件类型
     */
    private void isAllowedFile(String[] allowedExtension, String originalFilename, String extension) {
        // 1. 校验文件名是否过长
        if (originalFilename.length() > fileStorageConfig.getFileNameLength()) {
            throw new ServiceException(ResultCode.FILE_NAME_TOO_LONG);
        }
        // 2. 校验是否允许上传
        String[] defaultAllowedExtension = fileStorageConfig.getAllowedExtension();
        if (allowedExtension != null && !FileUtils.isAllowedExtension(extension, allowedExtension)) {
            throw new ServiceException(ResultCode.FILE_TYPE_ERROR);
        } else if (defaultAllowedExtension != null && !FileUtils.isAllowedExtension(extension, defaultAllowedExtension)) {
            throw new ServiceException(ResultCode.FILE_TYPE_ERROR);
        }
    }
}

四、 结束语

这只是一个简单的上传、删除的实现,对于项目中包含大文件处理的需求,还是无法满足。像大文件传输、断点续传、文件校验等有待后续的工作。

👉 欢迎加入小哈的星球 ,你将获得: 专属的项目实战 / Java 学习路线 / 一对一提问 / 学习打卡 /  赠书福利


全栈前后端分离博客项目 2.0 版本完结啦, 演示链接:http://116.62.199.48/ ,新项目正在酝酿中。全程手摸手,后端 + 前端全栈开发,从 0 到 1 讲解每个功能点开发步骤,1v1 答疑,直到项目上线。目前已更新了239小节,累计38w+字,讲解图:1645张,还在持续爆肝中.. 后续还会上新更多项目,目标是将Java领域典型的项目都整一波,如秒杀系统, 在线商城, IM即时通讯,Spring Cloud Alibaba 等等,戳我加入学习,已有1100+小伙伴加入(早鸟价超低)



1. 我的私密学习小圈子~

2. 网易二面:CPU狂飙900%,该怎么处理?

3. Jackson 用起来!

4. SpringBoot + POI-TL 操作 Word,快速生成报表,短小精悍!

最近面试BAT,整理一份面试资料Java面试BATJ通关手册,覆盖了Java核心技术、JVM、Java并发、SSM、微服务、数据库、数据结构等等。

获取方式:点“在看”,关注公众号并回复 Java 领取,更多内容陆续奉上。

PS:因公众号平台更改了推送规则,如果不想错过内容,记得读完点一下在看,加个星标,这样每次新文章推送才会第一时间出现在你的订阅列表里。

“在看”支持小哈呀,谢谢啦

继续滑动看下一个
向上滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存