文件上传【×】面向对象编程【√】

前言

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。在日常工作中,文件上传是一个很常见的功能。那后端如何优雅、完美地实现该功能呢?

本文将从上层异常类设计开始到上传工具类的代码实现,再到控制层暴露接口,最后使用 Postman 测试,行云流水,干货满满,MM 再也不用担心不会写文件上传了!!

封装

异常类

关系总览

文件异常类.png

BaseException

BaseException 基础异常类,其他异常类通过继承这个类描述异常所属模块、错误码、错误码对应的参数以及对应的错误消息,5 个重载的有参构造函数极大地提高了通用性和扩展性。

另外,重写 getMessage() 方法,使得 ExceptionHandlerExceptionResolver 异常解析时,能在控制台中清楚地看到异常类所属模块以及该类下对应的报错信息,利于后期维护和调试。

public class BaseException extends RuntimeException {
    private static final long serialVersionUID = 1L;

    // 所属模块
    private String module;

    // 错误码
    private String code;

    // 错误码对应的参数
    private Object[] args;

    // 错误消息
    private String defaultMessage;

    public BaseException(String module, String code, Object[] args, String defaultMessage) {
        this.module = module;
        this.code = code;
        this.args = args;
        this.defaultMessage = defaultMessage;
    }

    public BaseException(String module, String code, Object[] args) {
        this(module, code, args, null);
    }

    public BaseException(String module, String defaultMessage) {
        this(module, null, null, defaultMessage);
    }

    public BaseException(String code, Object[] args) {
        this(null, code, args, null);
    }

    public BaseException(String defaultMessage) {
        this(null, null, null, defaultMessage);
    }
    
    @Override
    public String getMessage() {
        String message = null;
        if (!StringUtils.isEmpty(code)) {
            message = code;
        }
        if (message == null) {
            message = defaultMessage;
        }
        return message;
    }
    // getter
}
复制代码

FileException

FileException 文件异常类,继承了 BaseException 基础类,仅有的一个有参构造函数使用了父类的构造器进行初始化,同时标明该异常类属于 file 模块,便于后期联调快速定位抛出异常位置。

public class FileException extends BaseException {
    private static final long serialVersionUID = 1L;

    public FileException(String code, Object[] args) {
        super("file", code, args, null);
    }

}
复制代码

FileNameLengthLimitExceededException

FileNameLengthLimitExceededException 文件名称超长限制异常类,继承了 FileException 文件异常类,属于文件异常类的子类,用来判断文件名称是否超过指定长度,有参构造函数使用父类的构造器初始化,传入错误 code 码来描述错误信息,同时将默认文件名称长度作为错误信息码对应的参数,以此来传达最大文件名称长度。

public class FileNameLengthLimitExceededException extends FileException {
    private static final long serialVersionUID = 1L;

    public FileNameLengthLimitExceededException(int defaultFileNameLength) {
        super("upload.filename.exceed.limit.length", new Object[]{defaultFileNameLength});
    }
}
复制代码

FileSizeLimitExceededException

FileSizeLimitExceededException 文件大小限制异常类,类似地不赘述!

public class FileSizeLimitExceededException extends FileException {
    private static final long serialVersionUID = 1L;

    public FileSizeLimitExceededException(long defaultMaxSize) {
        super("upload.exceed.max.size", new Object[]{defaultMaxSize});
    }
}
复制代码

InvalidExtensionException

InvalidExtensionException 文件扩展名校验异常类,继承 FileUploadException 文件上传异常类,通过自身构造函数传入的允许上传的文件扩展名数组、上传文件的扩展名和文件名,拼接错误提醒信息,以此来告知该扩展名在允许范围之外,同时给成员变量赋值。

此外,该类中写了 4 个静态内部类,统一继承了外部类,用来区分不同媒体类型扩展名检验异常,检验包括 IMAGE_EXTENSIONFLASH_EXTENSIONMEDIA_EXTENSIONVIDEO_EXTENSION 后面会涉及到。

public class InvalidExtensionException extends FileUploadException {
    private static final long serialVersionUID = 1L;

    // 允许上传的文件扩展名数组
    private String[] allowedExtension;
    // 上传文件的扩展名
    private String extension;
    // 上传文件名
    private String filename;

    public InvalidExtensionException(String[] allowedExtension, String extension, String filename) {
        super("filename : [" + filename + "], extension : [" + extension + "], allowed extension : [" + Arrays.toString(allowedExtension) + "]");
        this.allowedExtension = allowedExtension;
        this.extension = extension;
        this.filename = filename;
    }

    public static class InvalidImageExtensionException extends InvalidExtensionException {
        private static final long serialVersionUID = 1L;

        public InvalidImageExtensionException(String[] allowedExtension, String extension, String filename) {
            super(allowedExtension, extension, filename);
        }
    }

    public static class InvalidFlashExtensionException extends InvalidExtensionException {
        private static final long serialVersionUID = 1L;

        public InvalidFlashExtensionException(String[] allowedExtension, String extension, String filename) {
            super(allowedExtension, extension, filename);
        }
    }

    public static class InvalidMediaExtensionException extends InvalidExtensionException {
        private static final long serialVersionUID = 1L;

        public InvalidMediaExtensionException(String[] allowedExtension, String extension, String filename) {
            super(allowedExtension, extension, filename);
        }
    }

    public static class InvalidVideoExtensionException extends InvalidExtensionException {
        private static final long serialVersionUID = 1L;

        public InvalidVideoExtensionException(String[] allowedExtension, String extension, String filename) {
            super(allowedExtension, extension, filename);
        }
    }
    // getter
}
复制代码

工具类

文件上传工具类

FileUploadUtils,该类规定了默认最大文件大小、最大长度以及上传文件路径,最主要的方法就是文件上传,可指定基路径进行文件上传和以默认配置进行上传,上传前判断文件名是否超过默认最大文件名长度以及对上传文件大小和扩展名校验,验证不通过则抛出对应的异常信息,具体代码细节请看注释。

public class FileUploadUtils {
    // 默认最大文件大小 50M
    public static final long DEFAULT_MAX_SIZE = 50 * 1024 * 1024;

    // 默认的文件名最大长度 100
    public static final int DEFAULT_FILE_NAME_LENGTH = 100;

    // 默认上传的路径(从配置文件中拿)
    private static String defaultBaseDir = ProjectConfig.getProfile();

    // 设置默认上传基路径
    public static void setDefaultBaseDir(String defaultBaseDir) {
        FileUploadUtils.defaultBaseDir = defaultBaseDir;
    }

    public static String getDefaultBaseDir() {
        return defaultBaseDir;
    }

    /**
     * 可指定基路径进行文件上传
     *
     * @param baseDir 可指定的基路径
     * @param file   上传的文件
     * @return       路径文件名
     * @throws IOException 读写文件异常
     */
    public static final String upload(String baseDir, MultipartFile file)
            throws IOException, InvalidExtensionException {
        return upload(baseDir, file, MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION);
    }

    /**
     * 以默认配置进行文件上传
     *
     * @param file 上传的文件
     * @return 路径文件名
     * @throws IOException 读写文件异常
     * @throws InvalidExtensionException 文件扩展名校验异常
     */
    public static final String upload(MultipartFile file)
            throws IOException, InvalidExtensionException {
        return upload(getDefaultBaseDir(), file, MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION);
    }

    /**
     * 文件上传
     *
     * @param baseDir           相对应用的基目录
     * @param file             上传的文件
     * @param allowedExtension 允许上传文件类型
     * @return 路径文件名
     * @throws FileSizeLimitExceededException       如果超出最大大小
     * @throws FileNameLengthLimitExceededException 文件名太长
     * @throws IOException                          比如读写文件出错时
     * @throws InvalidExtensionException            文件扩展名校验异常
     */
    public static final String upload(String baseDir, MultipartFile file, String[] allowedExtension)
            throws FileSizeLimitExceededException, IOException, FileNameLengthLimitExceededException,
            InvalidExtensionException {

        // 获取上传文件名长度
        int fileNameLength = file.getOriginalFilename().length();

        // 判断文件名是否超过默认最大文件名长度
        if (fileNameLength > FileUploadUtils.DEFAULT_FILE_NAME_LENGTH) {
            throw new FileNameLengthLimitExceededException(FileUploadUtils.DEFAULT_FILE_NAME_LENGTH);
        }

        // 上传文件大小和扩展名校验,检验不通过则抛出异常
        assertAllowed(file, allowedExtension);

        // 重新对文件名编码,返回相对文件路径
        String fileName = extractFilename(file);

        // 拿相对文件路径去拼接配置文件中规定的基路径,获取绝对路径上的抽象文件
        File desc = getAbsoluteFile(baseDir, fileName);

        // 让抽象文件不再抽象,内存文件写入磁盘
        file.transferTo(desc);

        // 规定上传成功后返回路径文件名
        String pathFileName = getPathFileName(baseDir, fileName);
        return pathFileName;
    }

    /**
     * 重新对文件名编码,返回相对文件路径
     *
     * @param file 表单文件
     * @return 日期路径 + 编码后的文件名.对应扩展名
     */
    public static final String extractFilename(MultipartFile file) {
        String fileName = file.getOriginalFilename();
        String extension = getExtension(file);
        fileName = DateUtils.datePath() + "/" + IdUtils.fastUUID() + "." + extension;
        return fileName;
    }

    /**
     * 创建绝对路径上的文件目录
     *
     * @param uploadDir 上传文件基路径
     * @param fileName  文件名
     * @return 绝对路径上的抽象文件
     */
    public static final File getAbsoluteFile(String uploadDir, String fileName) {
        File desc = new File(uploadDir + File.separator + fileName);
        // 创建文件目录
        if (!desc.exists()) {
            if (!desc.getParentFile().exists()) {
                desc.getParentFile().mkdirs();
            }
        }
        return desc;
    }

    /**
     * 规定返回路径文件名
     * 
     * @param uploadDir 上传文件基路径
     * @param fileName 文件名
     * @return 资源前缀(/profile)/upload/日期路径 + 编码后的文件名.对应扩展名
     */
    public static final String getPathFileName(String uploadDir, String fileName) {
        int dirLastIndex = ProjectConfig.getProfile().length() + 1;
        String currentDir = StringUtils.substring(uploadDir, dirLastIndex);
        String pathFileName = Constants.RESOURCE_PREFIX + "/" + currentDir + "/" + fileName;
        return pathFileName;
    }

    /**
     * 文件大小以及扩展名校验
     *
     * @param file 上传的文件
     * @throws FileSizeLimitExceededException 如果超出最大大小
     * @throws InvalidExtensionException      检验文件合法扩展名
     */
    public static final void assertAllowed(MultipartFile file, String[] allowedExtension)
            throws FileSizeLimitExceededException, InvalidExtensionException {
        // 文件大小
        long size = file.getSize();
        if (DEFAULT_MAX_SIZE != -1 && size > DEFAULT_MAX_SIZE) {
            throw new FileSizeLimitExceededException(DEFAULT_MAX_SIZE / 1024 / 1024);
        }
        
        // 文件名
        String fileName = file.getOriginalFilename();
        // 文件扩展名
        String extension = getExtension(file);
        if (allowedExtension != null && !isAllowedExtension(extension, allowedExtension)) {
            if (allowedExtension == MimeTypeUtils.IMAGE_EXTENSION) {
                throw new InvalidExtensionException.InvalidImageExtensionException(allowedExtension, extension,
                        fileName);
            } else if (allowedExtension == MimeTypeUtils.FLASH_EXTENSION) {
                throw new InvalidExtensionException.InvalidFlashExtensionException(allowedExtension, extension,
                        fileName);
            } else if (allowedExtension == MimeTypeUtils.MEDIA_EXTENSION) {
                throw new InvalidExtensionException.InvalidMediaExtensionException(allowedExtension, extension,
                        fileName);
            } else if (allowedExtension == MimeTypeUtils.VIDEO_EXTENSION) {
                throw new InvalidExtensionException.InvalidVideoExtensionException(allowedExtension, extension,
                        fileName);
            } else {
                throw new InvalidExtensionException(allowedExtension, extension, fileName);
            }
        }

    }

    /**
     * 判断当前文件扩展名是否被允许
     *
     * @param extension         当前文件扩展名
     * @param allowedExtension  允许上传文件类型
     * @return 是否被允许
     */
    public static final boolean isAllowedExtension(String extension, String[] allowedExtension) {
        for (String str : allowedExtension) {
            if (str.equalsIgnoreCase(extension)) {
                return true;
            }
        }
        return false;
    }

    /**
     * 获取文件名的后缀
     *
     * @param file 表单文件
     * @return 后缀名
     */
    public static final String getExtension(MultipartFile file) {
        String extension = FilenameUtils.getExtension(file.getOriginalFilename());
        // 未成功获取到文件扩展名,则通过文件内容类型获取
        if (StringUtils.isEmpty(extension)) {
            extension = MimeTypeUtils.getExtension(file.getContentType());
        }
        return extension;
    }
}
复制代码

注意:获取配置文件设置信息,具体看我的这篇 ☞ 【避坑指南】配置读写分离 - 掘金 (juejin.cn)

上传文件路径

存放上传文件路径.png

编码文件名.png

返回路径文件名

即上传成功后,返回给前端路径文件名 => 资源前缀(/profile)/upload/日期路径 + 编码后的文件名.对应扩展名

媒体类型工具类

MimeTypeUtils 约定了一些图片格式常量,定义了一个通过文件内容类型获取对应的扩展名的静态方法。

例如: MimeTypeUtils.getExtension(file.getContentType())

public class MimeTypeUtils {
    
    public static final String IMAGE_JPG = "image/jpg";

    public static final String IMAGE_JPEG = "image/jpeg";

    public static final String IMAGE_BMP = "image/bmp";

    public static final String IMAGE_GIF = "image/gif";

    public static final String[] IMAGE_EXTENSION = {"bmp", "gif", "jpg", "jpeg", "png"};

    public static final String IMAGE_PNG = "image/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"};

    public static String getExtension(String prefix) {
        switch (prefix) {
            case IMAGE_PNG:
                return "png";
            case IMAGE_JPG:
                return "jpg";
            case IMAGE_JPEG:
                return "jpeg";
            case IMAGE_BMP:
                return "bmp";
            case IMAGE_GIF:
                return "gif";
            default:
                return "";
        }
    }
}
复制代码

此外,该类除了定义默认允许的扩展名外,还规定了常见图片、Flash、视频媒体扩展名,能够满足文件上传的不同业务需求。

控制层

代码

@RestController
@RequestMapping(value = "/file")
public class FileController {

    private static final Logger logger = LoggerFactory.getLogger(FileController.class);

    @PostMapping("/upload")
    public Result uploadFile(@RequestParam("file") MultipartFile file) throws IOException, InvalidExtensionException {
        if (!file.isEmpty()) {
            String path = FileUploadUtils.upload(ProjectConfig.getUploadPath(), file);
            Map<String, String> map = new HashMap<>(1);
            logger.info("返回路径文件名 {}", path);
            map.put("path", path);
            return ResultGenerator.genSuccessResult(map);
        }
        return ResultGenerator.genFailResult("上传失败,请选择文件");
    }
}
复制代码

说明

代码比较简单,直接调用文件上传工具类 FileUploadUtilsupload 方法传入两个参数,一个参数是文件上传基路径,另一个参数为要上传的文件,上传成功则返回给前端路径文件名,否则抛出的异常会被 GlobeExceptionHandler 全局异常处理。

全局异常处理

代码

无复杂逻辑,比较简单 ☟

@RestControllerAdvice
public class GlobeExceptionHandler {

    @ExceptionHandler(Exception.class)
    public Object handleException(Exception e, HttpServletRequest req) {
        Result result = new Result();
        result.setResultCode(HttpStatus.ERROR);

        // 区分是否为自定义异常
        if (e instanceof CustomException) {
            result.setMessage(e.getMessage());
        } else if (e instanceof FileNameLengthLimitExceededException) {
            result.setMessage("上传文件名过长");
        } else if (e instanceof FileSizeLimitExceededException) {
            result.setMessage("文件过大");
        } else if (e instanceof InvalidExtensionException) {
            result.setMessage("上传文件扩展名不允许");
        } else {
            result.setMessage("未知错误");
        }
        return result;
    }
}
复制代码

接口测试

使用 Postman 测试文件上传功能 ☟

正常情况:

文件上传接口测试.png

异常情况:

  1. 文件扩展名不允许

image.png

控制台:

Resolved [com.hualei.mybatis_plus_learning.exception.file.InvalidExtensionException: filename : [jfif], extension : [warm.jfif], allowed extension : [[bmp, gif, jpg, jpeg, png, doc, docx, xls, xlsx, ppt, pptx, html, htm, txt, rar, zip, gz, bz2, mp4, avi, rmvb, pdf]]]
复制代码
  1. 文件过大

image.png

控制台:

Resolved [com.hualei.mybatis_plus_learning.exception.file.FileSizeLimitExceededException: upload.exceed.maxSize]
复制代码
  1. 文件名过长

image.png

控制台:

Resolved [com.hualei.mybatis_plus_learning.exception.file.FileNameLengthLimitExceededException: upload.filename.exceed.length]
复制代码

结尾

撰文不易,欢迎大家点赞、评论,你的关注、点赞是我坚持的不懈动力,感谢大家能够看到这里!Peace & Love。

猜你喜欢

转载自juejin.im/post/7018460828219211784