【源代码解读】图片上传的组件Apache fileupload如何使用

前端代码

图片上传的组件

在若依框架中,已经事先集成了文件上传,图片上传等功能。

该组件使用非常简单。

以一个图片上传的功能举例,如下:

在这里插入图片描述
前端代码如下:

	  <el-form-item label="背景图">
          <imageUpload v-model="form.backImg"/>
      </el-form-item>

区区的三行代码就实现了图片上传。

backImg对应着实体类中的背景图属性,映射到数据库,实际上存放的是图片上传的路径。

在这里插入图片描述

如何启用

为什么三行代码就能实现图片上传的功能呢?

其实,图片上传是公共组件,它是在main.js中进行全局挂载的。

main.js

// 文件上传组件
import FileUpload from "@/components/FileUpload"
// 图片上传组件
import ImageUpload from "@/components/ImageUpload"

可以找一下这个组件,在ruoyi-ui\src\components\ImageUpload\index.vue页面中进行初始化。

页面展示

<!-- 上传提示 -->
    <div class="el-upload__tip" slot="tip" v-if="showTip">
      请上传
      <template v-if="fileSize"> 大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b> </template>
      <template v-if="fileType"> 格式为 <b style="color: #f56c6c">{{ fileType.join("/") }}</b> </template>
      的文件
    </div>
    
// 是否显示提示
    isShowTip: {
      type: Boolean,
      default: true
    }

提示效果如下,如果上面的js代码改为false,则不显示提示。
在这里插入图片描述
图片数量限制

// 图片数量限制
    limit: {
      type: Number,
      default: 1,
    },

当前只允许上传一张图片,如果改为>1的数值,则允许上传多张。

 <el-upload
      :action="uploadImgUrl"
      list-type="picture-card"
      :on-success="handleUploadSuccess"
      :before-upload="handleBeforeUpload"
      :limit="limit"
      :on-error="handleUploadError"
      :on-exceed="handleExceed"
      name="file"
      :on-remove="handleRemove"
      :show-file-list="true"
      :headers="headers"
      :file-list="fileList"
      :on-preview="handlePictureCardPreview"
      :class="{hide: this.fileList.length >= this.limit}"
    >
      <i class="el-icon-plus"></i>
    </el-upload>
    
data() {
    return {
      dialogImageUrl: "",
      dialogVisible: false,
      hideUpload: false,
      uploadImgUrl: process.env.VUE_APP_BASE_API + "/file/upload", // 上传的图片服务器地址
      headers: {
        Authorization: "Bearer " + getToken(),
      },
      fileList: []
    };
  },

uploadImgUrl:在.env.development文件中可以找到VUE_APP_BASE_API的定义值。

该值无关紧要,主要是/file/upload

可以看下网关中,给/file/upload这个映射路径配置的解析模块是什么?

# 文件服务
        - id: ruoyi-file
          uri: lb://ruoyi-file
          predicates:
            - Path=/file/**
          filters:
            - StripPrefix=1

可以看到,它是ruoyi-file模块解析的。

可以去后台看下,对应的代码。

后端代码

在这里插入图片描述

SysFileController

Java代码如下:

@RestController
public class SysFileController
{
    private static final Logger log = LoggerFactory.getLogger(SysFileController.class);

    @Autowired
    private ISysFileService sysFileService;

    /**
     * 文件上传请求
     */
    @PostMapping("upload")
    public R<SysFile> upload(MultipartFile file)
    {
        try
        {
            // 上传并返回访问地址
            String url = sysFileService.uploadFile(file);
            SysFile sysFile = new SysFile();
            sysFile.setName(FileUtils.getName(url));
            sysFile.setUrl(url);
            return R.ok(sysFile);
        }
        catch (Exception e)
        {
            log.error("上传文件失败", e);
            return R.fail(e.getMessage());
        }
    }
}

LocalSysFileServiceImpl

ISysFileService 有三个接口,起作用的是LocalSysFileServiceImpl

在这里插入图片描述

为什么是LocalSysFileServiceImpl,因为在它的头部用@Primary注解修饰

@Primary :自动装配时出现多个Bean时,被注解为@Primary的Bean将作为首选者,否则报错
@Autowired: 默认按类型装配,如果我们想使用按名称装配,可以结合@Qualifier注解一起使用
@Autowired @Qualifier(“personDaoBean”) 存在多个实例配合使用

Nacos配置

本机地址,资源映射路径,根路径,是在nacos中配置的,配置内容如下:

# 本地文件上传    
file:
    domain: http://127.0.0.1:9404
    path: D:/ruoyi/uploadPath
    prefix: /statics

# FastDFS配置
fdfs:
  domain: http://8.129.231.12
  soTimeout: 3000
  connectTimeout: 2000
  trackerList: 8.129.231.12:22122

# Minio配置
minio:
  url: http://8.129.231.12:9000
  accessKey: minioadmin
  secretKey: minioadmin
  bucketName: test

FileUploadUtils

LocalSysFileServiceImpl最后是通过FileUploadUtils完成图片上传的操作。

	/**
     * 资源映射路径 前缀 :/statics
     */
    @Value("${file.prefix}")
    public String localFilePrefix;

    /**
     * 域名或本机访问地址:http://127.0.0.1:9404
     */
    @Value("${file.domain}")
    public String domain;
    
    /**
     * 上传文件存储在本地的根路径:D:/ruoyi/uploadPath
     */
    @Value("${file.path}")
    private String localFilePath;

    /**
     * 本地文件上传接口
     */
    @Override
    public String uploadFile(MultipartFile file) throws Exception{
        // localFilePath = D:/ruoyi/uploadPath
        String name = FileUploadUtils.upload(localFilePath, file);
        String url = domain + localFilePrefix + name;
        return url;
    }

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;

    /**
     * 根据文件路径上传
     *
     * @param baseDir 相对应用的基目录
     * @param file 上传的文件
     * @return 文件名称
     * @throws IOException
     */
    public static final String upload(String baseDir, MultipartFile file) throws IOException{
        try{
            return upload(baseDir, file, MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION);
        }
        catch (Exception e){
            throw new IOException(e.getMessage(), e);
        }
    }
    /**
     * 文件上传
     * @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();
        // 如果文件名长度>100,则抛出异常
        if (fileNamelength > FileUploadUtils.DEFAULT_FILE_NAME_LENGTH){
            throw new FileNameLengthLimitExceededException(FileUploadUtils.DEFAULT_FILE_NAME_LENGTH);
        }
		// 文件校验:大小检验和文件类型校验
        assertAllowed(file, allowedExtension);
        // 设置文件上传后,新的文件名称
        // 举例: 2021\11\18\cea56bf9-0f6f-4bba-b747-d46c89ed9a36.png
        String fileName = extractFilename(file);
		// baseDir: D:\ruoyi\uploadPath
		// fileName: 2021\11\18\cea56bf9-0f6f-4bba-b747-d46c89ed9a36.png
		// 拼接后为 D:\ruoyi\uploadPath\2021\11\18\cea56bf9-0f6f-4bba-b747-d46c89ed9a36.png
		// 它会判断D:\ruoyi\uploadPath\2021\11\18这个目录是否存在,如果不存在,则创建
		// 最后会根据系统,返回一个绝对路径的文件名
        File desc = getAbsoluteFile(baseDir, fileName);
        // 将上传的文件,写入绝对路径的文件中
        file.transferTo(desc);
        // 上传成功后,返回fileName为:\2021\11\18\cea56bf9-0f6f-4bba-b747-d46c89ed9a36.png
        // 其实是在fileName前面加了个斜杠/
        String pathFileName = getPathFileName(fileName);
        return pathFileName;
    }
    /**
     * 编码文件名
     */
    public static final String extractFilename(MultipartFile file){
        String fileName = file.getOriginalFilename();
        String extension = getExtension(file);
        // DateUtils.datePath() 按照yyyy/MM/dd进行格式化,对应文件分隔符
        fileName = DateUtils.datePath() + "/" + IdUtils.fastUUID() + "." + extension;
        return fileName;
    }

    private static final File getAbsoluteFile(String uploadDir, String fileName) throws IOException{
        File desc = new File(uploadDir + File.separator + fileName);

        if (!desc.exists()){
            if (!desc.getParentFile().exists()){
                desc.getParentFile().mkdirs();
            }
        }
        return desc.isAbsolute() ? desc : desc.getAbsoluteFile();
    }
    
    private static final String getPathFileName(String fileName) throws IOException
    {
        String pathFileName = "/" + 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();
        // 文件最大50M
        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);
        // allowedExtension在MimeTypeUtils类中指定,定义了图片,文档,视频等多种格式
        // !isAllowedExtension(extension, allowedExtension)如果不允许该格式,则为true,抛出异常
        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{
            // 根据allowedExtension的定义,这是最终的处理
                throw new InvalidExtensionException(allowedExtension, extension, fileName);
            }
        }
    }

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

    /**
     * 获取文件名的后缀
     */
    public static final String getExtension(MultipartFile file){
        String extension = FilenameUtils.getExtension(file.getOriginalFilename());
        if (StringUtils.isEmpty(extension))
        {
            extension = MimeTypeUtils.getExtension(file.getContentType());
        }
        return extension;
    }
}

return upload(baseDir, file, MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION);

格式校验

MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION 定义如下:
在这里插入图片描述
extractFilename(MultipartFile file)的格式如下:
在这里插入图片描述
file.transferTo(desc)方法最终是由 CommonsMultipartFile实现的。

CommonsMultipartFile

源代码如下:

@Override
	public void transferTo(File dest) throws IOException, IllegalStateException {
		if (!isAvailable()) {
			throw new IllegalStateException("File has already been moved - cannot be transferred again");
		}

		if (dest.exists() && !dest.delete()) {
			throw new IOException(
					"Destination file [" + dest.getAbsolutePath() + "] already exists and could not be deleted");
		}

		try {
			this.fileItem.write(dest);
			LogFormatUtils.traceDebug(logger, traceOn -> {
				String action = "transferred";
				if (!this.fileItem.isInMemory()) {
					action = (isAvailable() ? "copied" : "moved");
				}
				return "Part '" + getName() + "',  filename '" + getOriginalFilename() + "'" +
						(traceOn ? ", stored " + getStorageDescription() : "") +
						": " + action + " to [" + dest.getAbsolutePath() + "]";
			});
		}
		catch (FileUploadException ex) {
			throw new IllegalStateException(ex.getMessage(), ex);
		}
		catch (IllegalStateException | IOException ex) {
			// Pass through IllegalStateException when coming from FileItem directly,
			// or propagate an exception from I/O operations within FileItem.write
			throw ex;
		}
		catch (Exception ex) {
			throw new IOException("File transfer failed", ex);
		}
	}

这段代码里最核心的方法是this.fileItem.write(dest);

这个方法实际上是org.apache.commons.fileupload.disk.DiskFileItem实现的。

DiskFileItem.java

@Override
    public void write(File file) throws Exception {
        if (isInMemory()) {
            FileOutputStream fout = null;
            try {
                fout = new FileOutputStream(file);
                fout.write(get());
                fout.close();
            } finally {
                IOUtils.closeQuietly(fout);
            }
        } else {
            File outputFile = getStoreLocation();
            if (outputFile != null) {
                // Save the length of the file
                size = outputFile.length();
                /*
                 * The uploaded file is being stored on disk
                 * in a temporary location so move it to the
                 * desired file.
                 */
                FileUtils.moveFile(outputFile, file);
            } else {
                /*
                 * For whatever reason we cannot write the
                 * file to disk.
                 */
                throw new FileUploadException(
                    "Cannot write uploaded file to disk!");
            }
        }
    }

FileUtils.moveFile(outputFile, file);最终是在org.apache.commons.io.FileUtils中实现的。

FileUtils中进行了一系列校验,最终交给java.nio.file.Files处理。

public static Path copy(Path source, Path target, CopyOption... options)
        throws IOException
    {
        FileSystemProvider provider = provider(source);
        if (provider(target) == provider) {
            // same provider
            provider.copy(source, target, options);
        } else {
            // different providers
            CopyMoveHelper.copyToForeignTarget(source, target, options);
        }
        return target;
    }

最后跑到jdk中处理逻辑去了。

posted @ 2021-11-24 22:22  layman~  阅读(349)  评论(0编辑  收藏  举报