【源代码解读】图片上传的组件Apache fileupload如何使用
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中处理逻辑去了。