前置文章
Windows VMWare Centos环境下安装Docker并配置MySqlhttps://blog.csdn.net/u013224722/article/details/148928081 Windows VMWare Centos Docker部署Springboot应用
https://blog.csdn.net/u013224722/article/details/148958480
Windows VMWare Centos Docker部署Nginx并配置对Springboot应用的访问代理https://blog.csdn.net/u013224722/article/details/149007158
Windows VMWare Centos Docker部署Springboot + mybatis + MySql应用https://blog.csdn.net/u013224722/article/details/149041367
一、Springboot实现文件上传接口
修改FileRecordMapper相关文件,新增文件记录查询功能代码。(数据库表结构可参考前置文章)
java
# FileRecordMapper.java 新增
List<FileRecord> selectAll();
# FileRecordMapper.xml 新增
<select id="selectAll" resultMap="BaseResultMap">
select
<include refid="Base_Column_List" />
from files
</select>
新建FileController.java,创建文件上传、管理相关接口
java
package com.duelapi.controller;
import com.duelapi.service.IFileService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
@Controller
@RequestMapping("/file")
public class FileController {
private IFileService fileService;
@Autowired
public FileController(IFileService fileService) {
this.fileService = fileService;
}
@RequestMapping(value = "/uploadFile", method = RequestMethod.POST)
@ResponseBody
public Map<String, Object> uploadFile(
@RequestParam(value = "file") MultipartFile fileInfo,
@RequestParam(value = "memberId", required = false) Integer memberId) {
try {
return this.fileService.uploadFile(fileInfo, memberId);
} catch (IOException ex) {
Map<String, Object> resultMap = new HashMap<>();
resultMap.put("status", "-1");
resultMap.put("msg", "error");
return resultMap;
}
}
@RequestMapping(value = "/getFileList", method = RequestMethod.GET)
@ResponseBody
public Map<String, Object> getFileList() {
return this.fileService.getFileList();
}
@RequestMapping(value = "/deleteFile", method = RequestMethod.GET)
@ResponseBody
public Map<String, Object> deleteFile(@RequestParam(value = "id") Integer id) {
return this.fileService.deleteFile(id);
}
}
新建Interface IFileService.java文件
java
package com.duelapi.service;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.Map;
public interface IFileService {
Map<String, Object> uploadFile(MultipartFile fileInfo, Integer memberId) throws IOException;
Map<String, Object> getFileList();
Map<String, Object> deleteFile(Integer id);
}
新建FileService.java文件,处理文件存储。
java
package com.duelapi.serviceimpl;
import com.duelapi.mapper.FileRecordMapper;
import com.duelapi.model.FileRecord;
import com.duelapi.service.IFileService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Service
public class FileService implements IFileService {
private FileRecordMapper fileMapper;
@Autowired
public FileService(FileRecordMapper fileMapper)
{
this.fileMapper = fileMapper;
}
@Override
public Map<String, Object> uploadFile(MultipartFile fileInfo, Integer memberId) throws IOException {
Map<String, Object> resultMap = new HashMap<>();
try{
String fileName = fileInfo.getOriginalFilename().trim();
String fileType = fileName.substring(fileName.lastIndexOf(".")).toLowerCase();
String relativePath = "";
if (memberId != null)
relativePath = memberId + "/";
else
relativePath = "ungroup/";
String targetFileName = System.currentTimeMillis() + fileType;
String sCacheFile = "/uploads/Cache/" + targetFileName;
String sTargetFile = "/uploads/" + relativePath + targetFileName;
File cacheFile = new File(sCacheFile);
if (cacheFile.exists())
cacheFile.delete();
if (!cacheFile.getParentFile().exists())
cacheFile.getParentFile().mkdirs();
fileInfo.transferTo(cacheFile);
File targetFile = new File(sTargetFile);
if(targetFile.exists())
targetFile.delete();
if (!targetFile.getParentFile().exists())
targetFile.getParentFile().mkdirs();
cacheFile.renameTo(targetFile);
String sUrl = "http://192.168.23.134:38080"+ "/uploads/" + relativePath + targetFileName;
FileRecord fileRec = new FileRecord();
fileRec.setFileName(fileName);
fileRec.setFileType(fileType);
fileRec.setStatusId(1);
fileRec.setServerSavePath(sTargetFile);
fileRec.setUrl(sUrl);
fileRec.setUserId(memberId);
int nFlag = fileMapper.insertSelective(fileRec);
if(nFlag == 1){
resultMap.put("status", 1);
resultMap.put("msg", "success");
resultMap.put("url", sUrl);
resultMap.put("savePath", sTargetFile);
}
else {
resultMap.put("status", 0);
resultMap.put("msg", "Failed to save data to the database!");
}
return resultMap;
}
catch (Exception ex){
resultMap.put("status", -1);
resultMap.put("msg", ex.getMessage());
return resultMap;
}
}
@Override
public Map<String, Object> getFileList() {
List<FileRecord> arrRecords = this.fileMapper.selectAll();
List<Map<String, Object>> ltMaps = new ArrayList<>();
int nAmount = 0;
if (arrRecords != null && !arrRecords.isEmpty()) {
ltMaps = arrRecords.stream().map(vo -> vo.toMap()).collect(Collectors.toList());
nAmount = arrRecords.size();
}
Map<String, Object> resultMap = new HashMap<>();
resultMap.put("rows", ltMaps);
resultMap.put("total", nAmount);
return resultMap;
}
@Override
public Map<String, Object> deleteFile(Integer id) {
Map<String, Object> resultMap = new HashMap<>();
FileRecord fileRec = this.fileMapper.selectByPrimaryKey(id);
if(fileRec != null){
String sFile = fileRec.getServerSavePath();
File file = new File(sFile);
if (file.exists())
file.delete();
int nFlag = this.fileMapper.deleteByPrimaryKey(id);
if(nFlag == 1){
resultMap.put("status", 1);
resultMap.put("msg", "success");
}
else{
resultMap.put("status", 0);
resultMap.put("msg", "failed");
}
}
else{
resultMap.put("status", 0);
resultMap.put("msg", "file record missing!");
}
return resultMap;
}
}
二、H5实现测试板块
新建文件管理的测试html文件 - files.html

html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>测试</title>
<link href="./plugins/bootstrap/css/bootstrap.min.css" rel="stylesheet">
<link href="./plugins/bootstrap/css/bootstrap-table.min.css" rel="stylesheet">
</head>
<body>
<div style="max-width: 1360px; margin: 30px auto">
<div id="tabToolbar">
<a class="btn btn-info" href="index.html">Go To index Html</a>
</div>
<table id="tabMain" data-toolbar="#tabToolbar">
</table>
<div style="margin-top: 40px; background: #e7e7e7; padding: 30px">
<h4>添加文件</h4>
<input type="file" id="btnSelectFile" accept="*/*" style="display: none">
<a class="btn btn-primary" onclick="$('input[id=btnSelectFile]').click();">
选择上传
</a>
</div>
</div>
<script type="text/javascript" src="js/const.js"></script>
<script type="text/javascript" src="js/jquery.min.js?v2.1.4"></script>
<script src="./plugins/bootstrap/js/bootstrap.min.js"></script>
<script src="./plugins/bootstrap/js/bootstrap-table.min.js"></script>
<script>
$(document).ready(function () {
doUpdateTab();
bindSelectFileChange();
});
function doUpdateTab() {
$('#tabMain').bootstrapTable('destroy');
$('#tabMain').bootstrapTable({
method: 'get',
toolbar: '#tabToolbar', //工具按钮用哪个容器
striped: true, //是否显示行间隔色
cache: false, //是否使用缓存,默认为true,所以一般情况下需要设置一下这个属性(*)
pagination: true, //是否显示分页(*)
sortable: false, //是否启用排序
sortOrder: "desc", //排序方式
pageNumber: 1, //初始化加载第一页,默认第一页
pageSize: 50, //每页的记录行数(*)
pageList: [10, 25, 50, 100], //可供选择的每页的行数(*)
url: constUtils.Server + "file/getFileList",//这个接口需要处理bootstrap table传递的固定参数
queryParamsType: 'undefined', //默认值为 'limit' ,在默认情况下 传给服务端的参数为:offset,limit,sort
queryParams: function queryParams(queryParams) { //设置查询参数
return {};
},//前端调用服务时,会默认传递上边提到的参数,如果需要添加自定义参数,可以自定义一个函数返回请求参数
sidePagination: "server", //分页方式:client客户端分页,server服务端分页(*)
search: true, //是否显示表格搜索,此搜索是客户端搜索,不会进服务端,所以,个人感觉意义不大
strictSearch: false,
showColumns: true, //是否显示所有的列
showRefresh: true, //是否显示刷新按钮
minimumCountColumns: 2, //最少允许的列数
clickToSelect: true, //是否启用点击选中行
searchOnEnterKey: true,
columns: [
{
title: '序号',
align: 'center',
formatter: function (value, row, index) {
return index + 1;
}
},
{
field: 'fileName',
title: '文件名',
searchable: true,
align: 'center'
},
{
field: 'serverSavePath',
title: '服务端存储路径',
searchable: true,
align: 'center'
},
{
field: 'url',
title: '链接',
searchable: true,
align: 'center',
formatter: function (value, row, index) {
let fielType = row.fileType.toString().toLocaleLowerCase();
if(fielType == ".jpg" || fielType == ".png" || fielType == ".jpeg")
return '<img src="' + row.url + '" style="max-height: 80px">';
else return row.url;
}
},
{
field: 'userId',
title: 'memberId',
searchable: true,
align: 'center'
},
{
title: '操作',
align: 'center',
searchable: false,
formatter: function (value, row, index) {
return '<a class="btn" style="margin-left: 10px;" ' +
' onclick="deleteRecord(\'' + row.id + '\')">删除</a>';
}
}
],
onLoadSuccess: function (data) { //加载成功时执行
console.log(data)
},
onLoadError: function (err) {
console.log(err);
},
showToggle: false, //是否显示详细视图和列表视图的切换按钮
cardView: false, //是否显示详细视图
detailView: false, //是否显示父子表
});
}
function bindSelectFileChange() {
$('input[id=btnSelectFile]').change(function () {
let file = $('#btnSelectFile')[0].files[0];
if (file) {
let formData = new FormData();
formData.append("file", file);
formData.append("memberId", "1");
$.ajax({
type: 'post',
url: constUtils.Server + "file/uploadFile",
data: formData,
cache: false,
dataType: "json",
processData: false,
contentType: false,
success: function (res) {
console.log(res);
$('#tabMain').bootstrapTable('refresh');
},
error: function (err) {
console.log(err);
}
});
}
});
}
function deleteRecord(id) {
$.ajax({
method: "GET",
url: constUtils.Server + "file/deleteFile",
data: {
id: id
},
cache: false,
dataType: "json",
contentType: "application/json",
async: false, //同步
success: function (res) {
console.log(res);
$('#tabMain').bootstrapTable('refresh');
},
error: function (err) {
console.log(err);
}
});
}
</script>
</body>
</html>
三、Docker中的部署实现
1、打包Springboot应用
IDEA 修改Springboot pom.xml文件,将Springboot应用打包为 dapi-1.0.3.jar 文件。我的打包方式可参照前置文章。

2、修改Dockerfile文件
将jar包拷贝至 VMWare Centos 中,并修改Dockerfile文件:
html
FROM openjdk:24
# 后端工作目录
VOLUME /app
# 后端jar包名称
COPY dapi-1.0.3.jar /app/dapi.jar
# 后端项目的端口号
EXPOSE 8093
#启动时指令
ENTRYPOINT ["java", "-jar", "/app/dapi.jar"]
3、卸载之前的容器
VMWare Centos Terminal终端中卸载之前的Springboot应用,(前置文章中安装的容器)
html
sudo docker stop dapi
sudo docker rm dapi
sudo docker rmi dapi:0.0.2
4、新建存储路径
VMWare Centos 中创建文件夹用于存储上传的文件。
新建文件夹【uploads】,创建后完整路径为【/home/duel/workspace/nginx/html/dweb/uploads】

其中【uploads】文件夹所在路径,已经被我安装的Nginx通过指令挂载:
html
# Nginx容器安装时包含的映射指令
-v /home/duel/workspace/nginx/html:/usr/share/nginx/html
即系统路径【/home/duel/workspace/nginx/html】 挂载到了Nginx的 【/usr/share/nginx/html】
通过IP加端口号访问Docker Nginx时:
【http://Centos_IP:38080/】指向目录 【/usr/share/nginx/html/dweb】 ,即指向【/home/duel/workspace/nginx/html/dweb】
这样,新上传的文件存储在【uploads】文件夹里,就可以通过 【http://Centos_IP:38080/uploads/* 】的方式进行访问。
我的Nginx安装指令及配置如下,详细可参考前置文章中的详细搭建流程。
html
# 我安装Nginx的指令
sudo docker run --name nginx -p 80:80 -p 443:443 -p 38080:38080 -p 38081:38081
-v /home/duel/workspace/nginx/html:/usr/share/nginx/html
-v /home/duel/workspace/nginx/conf/nginx.conf:/etc/nginx/nginx.conf
-v /home/duel/workspace/nginx/conf.d:/etc/nginx/conf.d
-v /home/duel/workspace/nginx/logs:/var/log/nginx
-v /home/duel/workspace/nginx/ssl:/etc/nginx/ssl
-d --restart=always nginx:latest
XML
# 访问静态文件
server {
listen 38080;
server_name localhost;
location / {
root /usr/share/nginx/html/dweb;
index index.html index.htm;
}
location ~* \.(html|css|js|png|jpg|gif|ico|mp4|mkv|rmvb|flv|eot|svg|ttf|woff|woff2|pptx|rar|zip)$ {
root /usr/share/nginx/html/dweb;
autoindex on;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
5、安装新版Springboot应用、挂载存储路径
html
# 通过修改过的Dockerfile 加载新的Jar包
$ sudo docker build -t dapi:1.0.3 .
#启动容器、映射端口、 映射文件存储路径
$ sudo docker run --name dapi -p 8093:8093
-v /home/duel/workspace/nginx/html/dweb/uploads:/uploads
-d --restart=always dapi:1.0.3
Docker中安装Springboot容器时,将系统路径【/home/duel/workspace/nginx/html/dweb/uploads】挂载到了容器里的 【/uploads】目录。
Springboot中 文件存储到【/uploads】路径,即保存到了 【/home/duel/workspace/nginx/html/dweb/uploads】。
如下图所示,文件上传接口实现时,存储路径直接用挂载映射后的【/uploads】,返回相应Http链接即可。

四、测试
1、静态文件发布
将我的前后端分离的Html部分,拷贝到Centos中的【/home/duel/workspace/nginx/html/dweb】路径下,该路径为Nginx指定的系统路径,可通过 ip:38080访问。 Nginx容器安装以及文件夹的挂载可参考我的前置文章,里面有我的Nginx容器安装配置实践的完整描述。

2、宿主机测试
回到Windows系统,打开浏览器,访问发布到VMWare Centos中的静态html,测试相应接口。
通过ip和端口号,访问测试成功。

选择一张照片上传后,回显成功。 其中的 【/uploads】 路径,对应着容器挂载的系统路径【/home/duel/workspace/nginx/html/dweb/uploads】。

返回的图片地址链接也可正常访问到。

回到VMWare Centos,挂载的系统路径下,也可以看到上传的文件。

测试一下删除
实现删除接口时, 按 "挂载后的文件路径" 删除文件即可。即删除【/uploads/1/1751456548949.jpg】
html
FileRecord fileRec = this.fileMapper.selectByPrimaryKey(id);
String sFile = fileRec.getServerSavePath();
// sfile的值为 /uploads/1/1751456548949.jpg
File file = new File(sFile);
if (file.exists())
file.delete();
删除后,系统文件夹里面的照片已删除,测试成功。
五、小结
Docker +Springboot应用实现文件上传功能时,可创建【存储文件夹】挂载至Springboot应用容器中,通过【挂载后的路径】对文件进行添加、删除。 同时可创建Nginx容器,通过Nginx实现对【存储文件夹】的http访问服务。