springboot-图片上传

gs_huang

1 概述

新做的博客系统需要在 markdown 文本中插入图片, 之前完成过上传图片的相关配置,但未做总结,借着这个机会,对于 springboot 上传图片接口的相关配置和操作,做一个系统性阐述。以作为未来相关业务的参考。

本文主要阐述后端相关配置,少量前端(vue3)内容仅是为了作为测试。

2 配置文件

配置相关信息仅需两步:

  1. yaml 文件中配置 相关路径静态资源
  2. 在配置类中配置静态资源处理器。

2.1 yaml 文件配置

为保证上传路径的 可配置性,这里的上传路径相关字符串全部配置在 application.yaml 文件中,然后再使用 @Value() 注解注入即可。

注:无关配置已省略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
upload:
upload-path: ./upload
image:
user: ${upload.upload-path}/image/avatar # 用户图片(包含头像和封面)路径
common: ${upload.upload-path}/image/common # 通用图片(主要是文章公告等图片)路径

spring:
# 配置静态资源路径
web:
resources:
static-locations: classpath:/static/, file:${upload.upload-path}

# 限制上传文件最大值
servlet:
multipart:
max-file-size: 100MB
max-request-size: 100MB

2.2 配置类

这里主要配置静态资源处理器,可以理解为请求 url 到文件路径的映射。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* @author gs_huang
* @date 2024/4/9 11:19
*/
@Configuration
public class ResourceConfig implements WebMvcConfigurer {

@Value("${upload.upload-path}/")
private String uploadPath;

@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/upload/**").addResourceLocations("file:" + uploadPath);
WebMvcConfigurer.super.addResourceHandlers(registry);
}
}

3 保存图片

配置好上传路径和静态资源处理器后,就需要向外提供保存图片的接口了。

这里需要在 service 层和 controller 层提供相应的保存图片以及把相关数据保存至数据库的方法。

3.1 service 层

1、service 接口:

1
2
3
4
5
6
7
8
9
10
public interface PictureService extends IService<Picture> {

/**
* @param url 请求 url
* @param file 图片文件
* @return 上传反馈
*/
Picture uploadImage(String url, MultipartFile file);

}

注意我这里继承 IService<Picture> 是因为我使用了 Mybatis-Plus ,不影响本文阐述功能。

2、serviceImpl 实现类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
@Service
public class PictureServiceImpl extends ServiceImpl<PictureMapper, Picture>
implements PictureService{

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

@Value("${upload.image.common}/")
private String commonPath;

/**
* 日期路径格式
*/
private final String datePathFormat = "yyyy/MM/dd/";

/**
* 日期格式
*/
private final SimpleDateFormat sdf = new SimpleDateFormat(datePathFormat);

@Override
public Picture uploadImage(String url, MultipartFile file) {
//1.获取日期字符串
String formatDate = sdf.format(new Date());

//2.获取新文件名
String newFileName = getNewFileName(file.getOriginalFilename());

//3.保存图片

//3.1 判断文件夹是否存在,不存在则创建
String imageDirPath = commonPath + formatDate;
File imageDir = new File(imageDirPath);
if (!imageDir.exists()){
imageDir.mkdirs();
}

//3.2 拼接文件完整路径
String imageFilePath = imageDirPath + "/" + newFileName;

//3.3 保存
try {
file.transferTo(new File(imageDir.getAbsoluteFile(), newFileName));

} catch (Exception e){
logger.error("文件 {} 保存失败", imageFilePath, e);
return null;
}

//4.拼接请求路径

//4.1 正则匹配请求前缀
Pattern pattern = Pattern.compile("(.*)/admin/picture.*");
Matcher matcher = pattern.matcher(url);

String urlPrefix = "http://localhost:18080";

while(matcher.find()){
urlPrefix = matcher.group(1);
}
String urlPath = urlPrefix + "/upload/image/common/" + formatDate + newFileName;

//5.构造存储数据
Picture picture = new Picture();

Date nowDate = new Date();
picture.setCreated(nowDate);
picture.setEdited(nowDate);
picture.setPath(urlPath);
picture.setStatus(1);

String pictureId = IdWorker.getIdStr(picture);
picture.setId(pictureId);

//6.图片数据存入数据库
save(picture);

//7.返回图片信息
return picture;
}

/**
* 根据旧文件名生成新文件名,使用 uuid 生成
* @param oldName 旧文件名
* @return 新文件名
*/
public String getNewFileName(String oldName){

//1.旧名称判空(这里使用了 hutools)
if (StrUtil.isEmpty(oldName)){
return null;
}

//2.正则匹配获取文件后缀
Pattern sufixPattern = Pattern.compile(".*(\\..*)");
Matcher matcher = sufixPattern.matcher(oldName);

//3.UUID 生成文件新名称(这里使用了 hutools)
String newFileName = UUID.randomUUID().toString();
if (matcher.find()){
newFileName += matcher.group(1);
}

//4.返回新名称
return newFileName;
}
}

3.2 controller 层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* @author gs_huang
* @date 2024/4/9 9:52
*/
@RestController
@RequestMapping("/admin/picture")
public class PictureController {

@Autowired
private PictureService pictureService;

@PostMapping("/uploadImage")
public Result uploadImage(HttpServletRequest request, @RequestParam("file") MultipartFile file){

if (file == null) {
return Result.formatError("文件错误");
}
Picture picture = pictureService.uploadImage(request.getRequestURL().toString(), file);

return picture != null ? Result.success(picture) : Result.error("上传图片失败");
}
}

4 前端调用

这里我开发的是 vue3 整合 v-md-editor 后,markdown 文本上传图片的功能,所以测试也是使用的其提供的回调方法。

这里有个坑,即 v-md-editor 提供的上传图片回调方法必须使用 formdata 格式上传,否则后端会报错!

4.1 api 接口

以下我自己封装的 axios 实例 http.js,如果你有自己的实例,可以忽略以下代码.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// axios 基础封装
import axios from 'axios'
import { authStore } from '@/stores/auth'
import router from '@/router'

const httpInstance = axios.create({
baseURL: 'http://localhost:18081',
timeout: 5000
})

// 拦截器

// axios 请求拦截器
httpInstance.interceptors.request.use(config => {
config.headers['token'] = authStore().token
return config
}, e => Promise.reject(e))

// axios 响应拦截器
httpInstance.interceptors.response.use(res => {
if ("token" in res.headers) {
authStore().setToken(res.headers["token"])
}

if (res.data.code === 401) {
router.push('/login')
authStore().removeToken();
authStore().removeUserAuth();
}

return res.data
}, e => {
return Promise.reject(e)
})

// 暴露出请求实例
export default httpInstance

这里会用到上述 httpInstance 实例。
picture.js:

1
2
3
4
5
6
7
8
9
import httpInstance from '@/utils/http'

export function uploadImageAPI(file) {
return httpInstance({
url: `/admin/picture/uploadImage`,
method: 'post',
data: file
})
}

4.2 调用接口

这里会忽略掉无关代码。

AddNotice.vue:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<script setup>
import { ElMessage } from "element-plus";
import { uploadImageAPI } from "@/apis/admin/picture";

const uploadImage = async (insertImage, file) => {
const formData = new FormData();
formData.append("file", file);

const res = await uploadImageAPI(formData);
if (res.code === 200) {
insertImage({
url: res.data.path,
desc: res.data.id,
});

ElMessage({
type: "success",
message: "图片上传成功",
plain: true,
});
} else {
ElMessage({
type: "error",
message: res.msg,
plain: true,
});
}
};

// 上传图片
const handleUploadImage = (event, insertImage, files) => {
console.log(files[0]);

uploadImage(insertImage, files[0]);
};
</script>

<template>
<div class="add-notice">
<v-md-editor
v-model="addNotice.content"
height="calc(100% - 150px)"
:include-level="[1, 2, 3]"
@save="saveLocal"
@blur="saveLocal"
:disabled-menus="[]"
@upload-image="handleUploadImage"
></v-md-editor>
</div>
</template>

5 效果展示

springboot-image-upload-v-md-editor-show

6 踩坑记录

对于 v-md-editor 中图片上传的数据格式,必须要使用 formdata 来进行封装才行,而不是一味的修改请求 headers 中的 Content-Type

封装 formdata 关键代码:

1
2
const formData = new FormData();
formData.append("file", file);

封装完成后即可将 formdata 作为 file 对象传输给后端。

但是对于 elementUI 中的文件上传,则可以使用原格式,不需要封装 formdata,即修改 headers 即可。

修改 headers 示例如下:

1
2
3
4
5
6
7
8
9
10
export function uploadImage(file) {
return request({
headers: {
'Content-Type': 'multipart/form-data',
},
url: `/admin/picture/uploadImage`,
method: 'post',
data: file
})
}

7 写在最后

v-md-editor 官网:https://code-farmer-i.github.io/vue-markdown-editor/zh/

关于 v-md-editor 图片上传,参考:https://code-farmer-i.github.io/vue-markdown-editor/zh/senior/upload-image.html

最后感慨一句,vue3 整合 v-md-editor 成功后,是真的帅啊!

  • 标题: springboot-图片上传
  • 作者: gs_huang
  • 创建于: 2024-04-09 11:02:11
  • 更新于: 2024-04-09 15:39:19
  • 链接: https://blog.itsheng.cn/2024/04/09/springboot-图片上传/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
 评论