Windows VMWare Centos Docker部署Springboot 应用实现文件上传返回文件http链接
前置文章
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相关文件,新增文件记录查询功能代码。(数据库表结构可参考前置文章)
# FileRecordMapper.java 新增List<FileRecord> selectAll();# FileRecordMapper.xml 新增<select id="selectAll" resultMap="BaseResultMap">select<include refid="Base_Column_List" />from files</select>
新建FileController.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;@Autowiredpublic FileController(IFileService fileService) {this.fileService = fileService;}@RequestMapping(value = "/uploadFile", method = RequestMethod.POST)@ResponseBodypublic 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)@ResponseBodypublic Map<String, Object> getFileList() {return this.fileService.getFileList();}@RequestMapping(value = "/deleteFile", method = RequestMethod.GET)@ResponseBodypublic Map<String, Object> deleteFile(@RequestParam(value = "id") Integer id) {return this.fileService.deleteFile(id);}
}
新建Interface IFileService.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文件,处理文件存储。
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;@Autowiredpublic FileService(FileRecordMapper fileMapper){this.fileMapper = fileMapper;}@Overridepublic 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 + "/";elserelativePath = "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;}}@Overridepublic 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;}@Overridepublic 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
<!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,sortqueryParams: 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文件:
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应用,(前置文章中安装的容器)
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通过指令挂载:
# 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安装指令及配置如下,详细可参考前置文章中的详细搭建流程。
# 我安装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
# 访问静态文件
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应用、挂载存储路径
# 通过修改过的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】
FileRecord fileRec = this.fileMapper.selectByPrimaryKey(id);String sFile = fileRec.getServerSavePath();// sfile的值为 /uploads/1/1751456548949.jpgFile file = new File(sFile);if (file.exists())file.delete();
删除后,系统文件夹里面的照片已删除,测试成功。
五、小结
Docker +Springboot应用实现文件上传功能时,可创建【存储文件夹】挂载至Springboot应用容器中,通过【挂载后的路径】对文件进行添加、删除。 同时可创建Nginx容器,通过Nginx实现对【存储文件夹】的http访问服务。
六、源码参考
我的实践源码:https://gitee.com/duelcode/duel-api