SpringBoot系列(三十)上传文件

一、创建Package并命名为module

二、在module下创建package并命名为file

三、在创建File类

在module/file创建类File

package com.fcors.fcors.module.file;

import lombok.*;

/**
 * @author pedro@TaleLin
 */
@Setter
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class File {

    /**
     * 真实url
     */
    private String url;

    /**
     * 前端上传的key
     */
    private String key;

    /**
     * 若 local,表示文件路径
     */
    private String path;

    /**
     * LOCAL REMOTE
     */
    private String type;

    /**
     * 文件名称
     */
    private String name;

    /**
     * 扩展名,例:.jpg
     */
    private String extension;

    /**
     * 文件大小
     */
    private Integer size;

    /**
     * md5值,防止上传重复文件
     */
    private String md5;
}

四、创建接口Uploader

在module/file创建接口Uploader

package com.fcors.fcors.module.file;

import org.springframework.util.MultiValueMap;
import org.springframework.web.multipart.MultipartFile;

import java.util.List;

/**
 * 文件上传服务接口
 *
 * @author pedro@TaleLin
 */
public interface Uploader {

    /**
     * 上传文件
     *
     * @param fileMap 文件map
     * @return 文件数据
     */
    List<File> upload(MultiValueMap<String, MultipartFile> fileMap);

    /**
     * 上传文件
     *
     * @param fileMap    文件map
     * @param uploadHandler 预处理器
     * @return 文件数据
     */
    List<File> upload(MultiValueMap<String, MultipartFile> fileMap, UploadHandler uploadHandler);
}

五、创建接口UploadHandler

在module/file下创建接口UploadHandler

package com.fcors.fcors.module.file;


import org.springframework.util.MultiValueMap;
import org.springframework.web.multipart.MultipartFile;

import java.util.List;

/**
 * 文件前预处理器
 *
 * @author pedro@TaleLin
 */
public interface UploadHandler {

    /**
     * 在文件写入本地或者上传到云之前调用此方法
     *
     * @return 是否上传,若返回false,则不上传
     */
    boolean preHandle(File file);

    /**
     * 在文件写入本地或者上传到云之后调用此方法
     */
    void afterHandle(File file);
}

六、创建FileUtil

在module/file下创建类FileUtil

package com.fcors.fcors.module.file;


import org.springframework.util.DigestUtils;
import org.springframework.util.unit.DataSize;

import java.io.File;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Path;

/**
 * @author pedro@TaleLin
 * @author colorful@TaleLin
 */
public class FileUtil {

    public static FileSystem getDefaultFileSystem() {
        return FileSystems.getDefault();
    }

    public static boolean isAbsolute(String str) {
        Path path = getDefaultFileSystem().getPath(str);
        return path.isAbsolute();
    }

    @SuppressWarnings("ResultOfMethodCallIgnored")
    public static void initStoreDir(String dir) {
        String absDir;
        if (isAbsolute(dir)) {
            absDir = dir;
        } else {
            String cmd = getCmd();
            Path path = getDefaultFileSystem().getPath(cmd, dir);
            absDir = path.toAbsolutePath().toString();
        }
        java.io.File file = new File(absDir);
        if (!file.exists()) {
            file.mkdirs();
        }
    }

    public static String getCmd() {
        return System.getProperty("user.dir");
    }

    public static String getFileAbsolutePath(String dir, String filename) {
        if (isAbsolute(dir)) {
            return getDefaultFileSystem()
                    .getPath(dir, filename)
                    .toAbsolutePath().toString();
        } else {
            return getDefaultFileSystem()
                    .getPath(getCmd(), dir, filename)
                    .toAbsolutePath().toString();
        }
    }

    public static String getFileExt(String filename) {
        int index = filename.lastIndexOf('.');
        return filename.substring(index);
    }

    public static String getFileMD5(byte[] bytes) {
        return DigestUtils.md5DigestAsHex(bytes);
    }

    public static Long parseSize(String size) {
        DataSize singleLimitData = DataSize.parse(size);
        return singleLimitData.toBytes();
    }
}

7、创建FileConstant

在module/file下创建类 FileConstant

package com.fcors.fcors.module.file;


/**
 * 文件相关常量值
 *
 * @author pedro@TaleLin
 */
public class FileConstant {

    /**
     * 本地文件
     */
    public static final String LOCAL = "LOCAL";

    /**
     * 远程文件,例如OSS
     */
    public static final String REMOTE = "REMOTE";
}

8、创建AbstractUploader

在module/file下创建类 AbstractUploader

package io.github.talelin.latticy.module.file;

import io.github.talelin.autoconfigure.exception.*;
import org.springframework.util.MultiValueMap;
import org.springframework.web.multipart.MultipartFile;

import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

/**
 * 文件上传类的基类
 * 模版模式
 *
 * @author pedro@TaleLin
 * @author Juzi@TaleLin
 * @author colorful@TaleLin
 */
public abstract class AbstractUploader implements Uploader {

    private UploadHandler uploadHandler;

    @Override
    public List<File> upload(MultiValueMap<String, MultipartFile> fileMap) {
        checkFileMap(fileMap);
        // 得到单个文件的大小限制
        // 本地存储需先初始化存储文件夹
        return handleMultipartFiles(fileMap);
    }

    @Override
    public List<File> upload(MultiValueMap<String, MultipartFile> fileMap, UploadHandler uploadHandler) {
        this.uploadHandler = uploadHandler;
        return this.upload(fileMap);
    }

    protected List<File> handleMultipartFiles(MultiValueMap<String, MultipartFile> fileMap) {
        long singleFileLimit = getSingleFileLimit();
        List<File> res = new ArrayList<>();
        fileMap.keySet().forEach(key -> fileMap.get(key).forEach(file -> {
            if (!file.isEmpty()) {
                handleOneFile0(res, singleFileLimit, file);
            }
        }));
        return res;
    }

    private void handleOneFile0(List<File> res, long singleFileLimit, MultipartFile file) {
        byte[] bytes = getFileBytes(file);
        String[] include = getFileProperties().getInclude();
        String[] exclude = getFileProperties().getExclude();
        String ext = checkOneFile(include, exclude, singleFileLimit, file.getOriginalFilename(), bytes.length);
        String newFilename = getNewFilename(ext);
        String storePath = getStorePath(newFilename);
        // 生成文件的md5值
        String md5 = FileUtil.getFileMD5(bytes);
        File fileData = File.builder().
                name(newFilename).
                md5(md5).
                key(file.getName()).
                path(storePath).
                size(bytes.length).
                type(getFileType()).
                extension(ext).
                build();
        // 如果预处理器不为空,且处理结果为false,直接返回, 否则处理
        if (uploadHandler != null && !uploadHandler.preHandle(fileData)) {
            return;
        }
        boolean ok = handleOneFile(bytes, newFilename);
        if (ok) {
            res.add(fileData);
            // 上传到本地或云上成功之后,调用afterHandle
            if (uploadHandler != null) {
                uploadHandler.afterHandle(fileData);
            }
        }
    }

    private long getSingleFileLimit() {
        String singleLimit = getFileProperties().getSingleLimit();
        return FileUtil.parseSize(singleLimit);
    }

    /**
     * 得到文件配置
     *
     * @return 文件配置
     */
    protected abstract FileProperties getFileProperties();

    /**
     * 处理一个文件
     */
    protected abstract boolean handleOneFile(byte[] bytes, String newFilename);

    /**
     * 返回文件路径
     *
     * @param newFilename 文件名
     * @return 文件路径
     */
    protected abstract String getStorePath(String newFilename);

    /**
     * 返回文件存储位置类型
     *
     * @return LOCAL | REMOTE
     */
    protected abstract String getFileType();

    /**
     * 获得新文件的名称
     *
     * @param ext 文件后缀
     * @return 新名称
     */
    protected String getNewFilename(String ext) {
        String uuid = UUID.randomUUID().toString().replace("-", "");
        return uuid + ext;
    }

    /**
     * 检查文件
     */
    protected void checkFileMap(MultiValueMap<String, MultipartFile> fileMap) {
        if (fileMap.isEmpty()) {
            throw new NotFoundException(10026);
        }
        int nums = getFileProperties().getNums();
        if (fileMap.size() > nums) {
            throw new FileTooManyException(10121);
        }
    }

    /**
     * 获得文件的字节
     *
     * @param file 文件
     * @return 字节
     */
    protected byte[] getFileBytes(MultipartFile file) {
        byte[] bytes;
        try {
            bytes = file.getBytes();
        } catch (Exception e) {
            throw new FailedException(10190, "read file date failed");
        }
        return bytes;
    }

    /**
     * 单个文件检查
     *
     * @param singleFileLimit 单个文件大小限制
     * @param originName      文件原始名称
     * @param length          文件大小
     * @return 文件的扩展名,例如: .jpg
     */
    protected String checkOneFile(String[] include, String[] exclude, long singleFileLimit, String originName, int length) {
        // 写到了本地
        String ext = FileUtil.getFileExt(originName);
        // 检测扩展
        if (!this.checkExt(include, exclude, ext)) {
            throw new FileExtensionException(ext + "文件类型不支持");
        }
        // 检测单个大小
        if (length > singleFileLimit) {
            throw new FileTooLargeException(originName + "文件不能超过" + singleFileLimit);
        }
        return ext;
    }

    /**
     * 检查文件后缀
     *
     * @param ext 后缀名
     * @return 是否通过
     */
    protected boolean checkExt(String[] include, String[] exclude, String ext) {
        int inLen = include == null ? 0 : include.length;
        int exLen = exclude == null ? 0 : exclude.length;
        // 如果两者都有取 include,有一者则用一者
        if (inLen > 0 && exLen > 0) {
            return this.findInInclude(include, ext);
        } else if (inLen > 0) {
            // 有include,无exclude
            return this.findInInclude(include, ext);
        } else if (exLen > 0) {
            // 有exclude,无include
            return this.findInExclude(exclude, ext);
        } else {
            // 二者都没有
            return true;
        }
    }

    protected boolean findInInclude(String[] include, String ext) {
        for (String s : include) {
            if (s.equals(ext)) {
                return true;
            }
        }
        return false;
    }

    protected boolean findInExclude(String[] exclude, String ext) {
        for (String s : exclude) {
            if (s.equals(ext)) {
                return true;
            }
        }
        return false;
    }
}

9、创建FileProperties

package com.fcors.fcors.module.file;

import com.fcors.fcors.common.factory.YamlPropertySourceFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Component;

/**
 * @author pedro@TaleLin
 */
@Component
@ConfigurationProperties("lin.file")
@PropertySource(
        value = "classpath:config/config.yml",

        encoding = "UTF-8", factory = YamlPropertySourceFactory.class)
public class FileProperties {

    private static final String[] DEFAULT_EMPTY_ARRAY = new String[0];

    private String storeDir = "/assets";

    private String singleLimit = "2MB";

    private Integer nums = 10;

    private String domain;

    private String[] exclude = DEFAULT_EMPTY_ARRAY;

    private String[] include = DEFAULT_EMPTY_ARRAY;

    /**
     * 文件存储路径
     */
    private String servePath = "assets/**";

    public String getServePath() {
        return servePath;
    }

    public void setServePath(String servePath) {
        this.servePath = servePath;
    }

    public String getStoreDir() {
        return storeDir;
    }

    public void setStoreDir(String storeDir) {
        this.storeDir = storeDir;
    }

    public String getSingleLimit() {
        return singleLimit;
    }

    public void setSingleLimit(String singleLimit) {
        this.singleLimit = singleLimit;
    }

    public Integer getNums() {
        return nums;
    }

    public void setNums(Integer nums) {
        this.nums = nums;
    }

    public String[] getExclude() {
        return exclude;
    }

    public void setExclude(String[] exclude) {
        this.exclude = exclude;
    }

    public String[] getInclude() {
        return include;
    }

    public void setInclude(String[] include) {
        this.include = include;
    }

    public String getDomain() {
        return domain;
    }

    public void setDomain(String domain) {
        this.domain = domain;
    }
}

10、在common/factory创建YamlPropertySourceFactory

package com.fcors.fcors.common.factory;

import org.springframework.boot.env.YamlPropertySourceLoader;
import org.springframework.core.env.PropertySource;
import org.springframework.core.io.support.EncodedResource;
import org.springframework.core.io.support.PropertySourceFactory;

import java.io.IOException;
import java.util.List;

public class YamlPropertySourceFactory implements PropertySourceFactory {
    @Override
    public PropertySource<?> createPropertySource(String name, EncodedResource resource) throws IOException {
        List<PropertySource<?>> sources = new YamlPropertySourceLoader().load(resource.getResource().getFilename(), resource.getResource());
        return sources.get(0);
    }
}

11、创建LocalUploader

在extension/file中创建类

package com.fcors.fcors.extension.file;


import com.fcors.fcors.module.file.AbstractUploader;
import com.fcors.fcors.module.file.FileConstant;
import com.fcors.fcors.module.file.FileProperties;
import com.fcors.fcors.module.file.FileUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;

import javax.annotation.PostConstruct;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * 文件上传服务默认实现,上传到本地
 *
 * @author pedro@TaleLin
 */
@Slf4j
public class LocalUploader extends AbstractUploader {

    @Autowired
    private FileProperties fileProperties;


    @PostConstruct
    public void initStoreDir() {
        // 本地存储需先初始化存储文件夹
        FileUtil.initStoreDir(this.fileProperties.getStoreDir());
    }

    @Override
    protected boolean handleOneFile(byte[] bytes, String newFilename) {
        String absolutePath =
                FileUtil.getFileAbsolutePath(fileProperties.getStoreDir(), getStorePath(newFilename));
        try {
            BufferedOutputStream stream =
                    new BufferedOutputStream(new FileOutputStream(new java.io.File(absolutePath)));
            stream.write(bytes);
            stream.close();
        } catch (Exception e) {
            log.error("write file to local err:", e);
            // throw new FailedException(10190);
            return false;
        }
        return true;
    }

    @Override
    protected FileProperties getFileProperties() {
        return fileProperties;
    }

    @SuppressWarnings("ResultOfMethodCallIgnored")
    @Override
    protected String getStorePath(String newFilename) {
        Date now = new Date();
        String format = new SimpleDateFormat("yyyy/MM/dd").format(now);
        Path path = Paths.get(fileProperties.getStoreDir(), format).toAbsolutePath();
        java.io.File file = new File(path.toString());
        if (!file.exists()) {
            file.mkdirs();
        }
        return Paths.get(format, newFilename).toString();
    }

    @Override
    protected String getFileType() {
        return FileConstant.LOCAL;
    }
}

12、QiniuUploader

在extension/file中创建类

package com.fcors.fcors.extension.file;

import com.fcors.fcors.module.file.FileConstant;
import com.qiniu.common.QiniuException;
import com.qiniu.http.Response;
import com.qiniu.storage.Configuration;
import com.qiniu.storage.Region;
import com.qiniu.storage.UploadManager;
import com.fcors.fcors.module.file.AbstractUploader;
import com.fcors.fcors.module.file.FileProperties;
import com.qiniu.util.Auth;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;

import java.io.ByteArrayInputStream;

@Slf4j
public class QiniuUploader extends AbstractUploader {

    @Autowired
    private FileProperties fileProperties;

    @Value("${lin.file.qiniuyun.access-key}")
    private String accessKey;

    @Value("${lin.file.qiniuyun.secret-key}")
    private String secretKey;

    @Value("${lin.file.qiniuyun.bucket}")
    private String bucket;

    private UploadManager uploadManager;

    private String upToken;

    public void initUploadManager() {
        Configuration cfg = new Configuration(Region.region2());
        uploadManager = new UploadManager(cfg);
        Auth auth = Auth.create(accessKey, secretKey);
        upToken = auth.uploadToken(bucket);
    }

    @Override
    protected FileProperties getFileProperties() {
        return fileProperties;
    }

    @Override
    protected String getStorePath(String newFilename) {
        return fileProperties.getDomain() + newFilename;
    }

    @Override
    protected String getFileType() {
        return FileConstant.REMOTE;
    }

    /**
     * 处理一个文件数据
     *
     * @param bytes       文件数据,比特流
     * @param newFilename 新文件名称
     * @return 处理是否成功,如果出现异常则返回 false,避免把失败的写入数据库
     */
    @Override
    protected boolean handleOneFile(byte[] bytes, String newFilename) {
        initUploadManager();
        ByteArrayInputStream byteInputStream = new ByteArrayInputStream(bytes);
        try {
            Response response = uploadManager.put(byteInputStream, newFilename, upToken, null, null);
            log.info(response.toString());
            return response.isOK();
        } catch (QiniuException ex) {
            Response r = ex.response;
            log.error("qiniuyun upload file error: {}", r.error);
            return false;
        }
    }
}

13、UploaderConfiguration

在extension/file中创建类 UploaderConfiguration

package com.fcors.fcors.extension.file;

import com.fcors.fcors.module.file.Uploader;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;

/**
 * 文件上传配置类
 *
 * @author Juzi@TaleLin
 * @author colorful@TaleLin
 */
@Configuration(proxyBeanMethods = false)
public class UploaderConfiguration {
    /**
     * @return 本地文件上传实现类
     */
    @Bean
    @Order
    @ConditionalOnMissingBean
    public Uploader uploader(){
        return new LocalUploader();
    }
}

14、创建config.yml

在resource/config创建config.yml

#文件上传配置

lin:
  file:
    # 文件服务域名
    domain: http://localhost:8082/
    # 排除文件类型
    exclude:
    # 包括文件类型
    include:
      - .jpg
      - .png
      - .jpeg
    # 文件最大数量
    nums: 10
    # 服务器文件路径
    serve-path: assets/**
    # 单个文件最大体积
    single-limit: 2MB
    # 本地文件保存位置
    store-dir: assets/
spring:
  servlet:
    multipart:
      # 总体文件最大体积(只能从max-file-size设置总体文件的大小)
      max-file-size: 20MB

15、创建FileBO

package com.fcors.fcors.bo;


import lombok.Data;

/**
 * @author pedro@TaleLin
 * @author Juzi@TaleLin
 */
@Data
public class FileBO {

    /**
     * 文件 id
     */
    private Integer id;

    /**
     * 文件 key,上传时指定的
     */
    private String key;

    /**
     * 文件路径
     */
    private String path;

    /**
     * 文件 URL
     */
    private String url;
}

16、在controller创建FileController


/**
 * @author pedro@TaleLin
 * @author Juzi@TaleLin
 */
@RestController
@RequestMapping("/cms/file")
public class FileController {

    @Autowired
    private FileService fileService;

    /**
     * 文件上传
     *
     * @param multipartHttpServletRequest 携带文件的 request
     * @return 文件信息
     */
    @PostMapping
    @LoginRequired
    public List<FileBO> upload(MultipartHttpServletRequest multipartHttpServletRequest) {
        MultiValueMap<String, MultipartFile> fileMap =
                multipartHttpServletRequest.getMultiFileMap();
        return fileService.upload(fileMap);
    }
}

17创建FileDO

18创建BaseModel

@Data
public class BaseModel {
    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;

    @JsonIgnore
    private Date createTime;

    @JsonIgnore
    private Date updateTime;

    @TableLogic
    @JsonIgnore
    private Date deleteTime;
}

19在model下创建FileDO

@Data
@TableName("lin_file")
@EqualsAndHashCode(callSuper = true)
public class FileDO extends BaseModel implements Serializable {

    private static final long serialVersionUID = -3203293656352763490L;

    private String path;

    /**
     * LOCAL REMOTE
     */
    private String type;

    private String name;

    private String extension;

    private Integer size;

    /**
     * md5值,防止上传重复文件
     */
    private String md5;
}

20创建FileMapper

package com.fcors.fcors.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.fcors.fcors.model.FileDO;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;

@Repository
public interface FileMapper extends BaseMapper<FileDO> {

    FileDO selectByMd5(@Param("md5") String md5);

    int selectCountByMd5(@Param("md5") String md5);
}

21创建Service

21.1接口FileService

package com.fcors.fcors.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.fcors.fcors.bo.FileBO;
import com.fcors.fcors.model.FileDO;
import org.springframework.util.MultiValueMap;
import org.springframework.web.multipart.MultipartFile;

import java.util.List;

/**
 * @author pedro@TaleLin
 */
public interface FileService extends IService<FileDO> {

    /**
     * 上传文件
     *
     * @param fileMap 文件map
     * @return 文件数据
     */
    List<FileBO> upload(MultiValueMap<String, MultipartFile> fileMap);

    /**
     * 通过md5检查文件是否存在
     *
     * @param md5 md5
     * @return true 表示已存在
     */
    boolean checkFileExistByMd5(String md5);
}

21.2实现类FileServiceImpl

package com.fcors.fcors.service;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.fcors.fcors.bo.FileBO;
import com.fcors.fcors.mapper.FileMapper;
import com.fcors.fcors.model.FileDO;
import com.fcors.fcors.module.file.*;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.MultiValueMap;
import org.springframework.web.multipart.MultipartFile;

import java.util.ArrayList;
import java.util.List;

@Service
public class FileServiceImpl extends ServiceImpl<FileMapper, FileDO> implements FileService {

    @Autowired
    private Uploader uploader;

    /**
     * 文件上传配置信息
     */
    @Autowired
    private FileProperties fileProperties;

    /**
     * 为什么不做批量插入
     * 1. 文件上传的数量一般不多,3个左右
     * 2. 批量插入不能得到数据的id字段,不利于直接返回数据
     * 3. 批量插入也仅仅只是一条sql语句的事情,如果真的需要,可以自行尝试一下
     */
    @Override
    public List<FileBO> upload(MultiValueMap<String, MultipartFile> fileMap) {
        List<FileBO> res = new ArrayList<>();

        uploader.upload(fileMap, new UploadHandler() {
            @Override
            public boolean preHandle(File file) {
                FileDO found = baseMapper.selectByMd5(file.getMd5());
                // 数据库中不存在,存储操作放在上传到本地或云上之后,
                // 修复issue131:https://github.com/TaleLin/lin-cms-spring-boot/issues/131
                if (found == null) {
                    return true;
                }
                // 已存在,则直接转化返回
                res.add(transformDoToBo(found, file.getKey()));
                return false;
            }

            @Override
            public void afterHandle(File file) {
                // 保存到数据库, 修复issue131:https://github.com/TaleLin/lin-cms-spring-boot/issues/131
                FileDO fileDO = new FileDO();
                BeanUtils.copyProperties(file, fileDO);
                getBaseMapper().insert(fileDO);
                res.add(transformDoToBo(fileDO, file.getKey()));
            }
        });
        return res;
    }

    @Override
    public boolean checkFileExistByMd5(String md5) {
        return this.getBaseMapper().selectCountByMd5(md5) > 0;
    }

    private FileBO transformDoToBo(FileDO file, String key) {
        FileBO bo = new FileBO();
        BeanUtils.copyProperties(file, bo);
        if (file.getType().equals(FileConstant.LOCAL)) {
            String s = fileProperties.getServePath().split("/")[0];

            // replaceAll 是将 windows 平台下的 \ 替换为 /
            if(System.getProperties().getProperty("os.name").toUpperCase().contains("WINDOWS")){
                bo.setUrl(fileProperties.getDomain() + s + "/" + file.getPath().replaceAll("\\\\","/"));
            }else {
                bo.setUrl(fileProperties.getDomain() + s + "/" + file.getPath());
            }
        } else {
            bo.setUrl(file.getPath());
        }
        bo.setKey(key);
        return bo;
    }
}
JAVA、基础技术、技术与框架SpringBoot系列(三十)上传文件插图