Windows VMWare Centos Docker部署Springboot 应用实现文件上传返回文件http链接

发布于:2025-07-07 ⋅ 阅读:(19) ⋅ 点赞:(0)

前置文章

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;

    @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文件

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;

    @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

<!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文件:

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.jpg
  File file = new File(sFile);
  if (file.exists())
       file.delete();

 删除后,系统文件夹里面的照片已删除,测试成功。

五、小结 

Docker +Springboot应用实现文件上传功能时,可创建【存储文件夹】挂载至Springboot应用容器中,通过【挂载后的路径】对文件进行添加、删除。 同时可创建Nginx容器,通过Nginx实现对【存储文件夹】的http访问服务。

六、源码参考

我的实践源码:https://gitee.com/duelcode/duel-api


网站公告

今日签到

点亮在社区的每一天
去签到