SpringBoot 文件上传下载工具样例

发布时间 2023-11-21 14:12:28作者: Xproer-松鼠

最近工作遇到这样的情景:一大堆 linux 内网服务器,上面部署了 mysql,nacos,xxl job 等中间件,当然也给了一个很干净的 windows 内网服务器,什么软件都没有安装。比较欣慰的是:可以通过浏览器访问 nacos、xxl job 的管理页面。不幸的是:没有安装 mysql 客户端和 xshell 等工具。我可以通过中间机连接到 linux 服务器去上传和下载文件,但是无法往连接的 windows 服务器去上传和下载文件。

遇到这种情况,比较好的办法就是:在其中一台 linux 服务器上部署一个 SpringBoot 程序,提供简单的文件上传和下载功能。比如采用 Get 请求提供文件下载接口,这样 windows 服务器就通过浏览器地址栏访问 SpringBoot 的下载接口,从 linux 服务器上下载到所需要的组件进行安装,比如下载 xshell 和 xftp 进行安装,后续就可以使用 xshell 或 xftp 操作各个 linux 服务器上传和下载文件就方便多了。

由此可见,提前准备好一个 SpringBoot 开发的文件上传下载程序,是一件比较重要的事情,没准儿哪天就能够解决燃眉之急。本篇博客就简单的罗列一下上传和下载的核心代码,在博客的最后会提供源代码,关键紧急时刻大家可以随时下载直接使用。


一、搭建工程

搭建一个 SpringBoot 工程 fileupdown 结构如下:

 

从上图可见,结构非常简单,具体如下:

  • Result 是用来组织返回结果的实体对象
  • UpdownController 提供上传文件、下载文件、展示已上传的文件列表、删除文件、清空文件的接口
  • index.html 提供了一个简单的可视化界面,没有使用任何的 js ,主要使用的是 Form 表单实现功能

下面看一下 pom 文件的内容:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.4.5</version>
    </parent>

    <groupId>com.jobs</groupId>
    <artifactId>fileupdown</artifactId>
    <version>1.0</version>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <scope>compile</scope>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.26</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>2.4.5</version>
            </plugin>
        </plugins>
    </build>
</project>

使用的 SpringBoot 的版本是 2.4.5 ,引用 lombok 的依赖,目的是为了记录简单的日志。


二、代码细节

Result 的实体类代码如下:

package com.jobs.common;

import lombok.Data;
import java.io.Serializable;

@Data
public class Result<T> implements Serializable {

    private Integer code; //状态码:1 成功,其它数字为失败

    private String msg; //状态信息

    private T data; //要返回的数据

    public static <T> Result<T> success(T object) {
        Result<T> r = new Result<T>();
        r.code = 1;
        r.msg = "成功";
        r.data = object;
        return r;
    }

    public static <T> Result<T> fail(Integer code, String msg) {
        Result r = new Result();
        r.code = code;
        r.msg = msg;
        return r;
    }
}

UpdownController 提供的接口代码如下:(除了上传文件的接口必须得使用 Post 请求外,其它接口都采用 Get 请求,方便在浏览器地址栏上直接输入 url 进行操作,虽然本博客的 Demo 中提供了简单的 index.html 可视化界面)

package com.jobs.controller;

import com.jobs.common.Result;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

@RestController
@RequestMapping("/updown")
public class UpdownController {

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

    //获取已经上传的文件列表,按照上传的先后顺序排列
    @GetMapping("/list")
    public Result<List<String>> getFileList() {
        File dir = new File(uploadPath);
        if (!dir.exists()) {
            dir.mkdirs();
        }

        //获取文件夹下的文件列表
        File[] flist = dir.listFiles();
        List<String> sss = Arrays.stream(flist).filter(s -> s.isFile())
                .sorted((s1, s2) -> {
                    //按照文件修改时间倒序排列,后上传的文件,排在前面
                    return (int) (s2.lastModified() - s1.lastModified());
                })
                .map(s -> s.getName()).collect(Collectors.toList());
        return Result.success(sss);
    }

    //上传文件
    @PostMapping("/upload")
    public Result<String> upload(MultipartFile file) {
        /**
         //如果你想要上传的文件,使用一个随机的文件名称时,可以使用这些代码
         //原始文件名
         String originalName = file.getOriginalFilename();
         //获取后缀名
         String extName = "";
         int index = originalName.lastIndexOf(".");
         if (index > -1) {
         extName = originalName.substring(index);
         }

         String newName = UUID.randomUUID().toString() + extName;
         File dir = new File(uploadPath);
         if (!dir.exists()) {
         dir.mkdirs();
         }

         try {
         file.transferTo(new File(uploadPath + newName));
         } catch (IOException e) {
         e.printStackTrace();
         }

         return Result.success(newName);*/

        //这里保留上传后的文件名跟原始文件名一致,服务器上如果文件已存在,则直接覆盖
        //原始文件名
        String originalName = file.getOriginalFilename();
        try {
            File dir = new File(uploadPath);
            if (!dir.exists()) {
                dir.mkdirs();
            }
            file.transferTo(new File(uploadPath + originalName));
        } catch (Exception ex) {
            return Result.fail(0, ex.getMessage());
        }

        return Result.success("上传成功");
    }

    //下载文件
    //之所以使用 get 请求,主要是方便在浏览器地址栏上输入文件名,然后进行下载
    @GetMapping("/download")
    public void download(String name, HttpServletResponse response) throws UnsupportedEncodingException {

        //下载文件的响应类型,这里统一设置成了文件流
        //你可以根据自己所提供下载的文件类型,使用不同的响应 mime 类型
        response.setContentType("application/octet-stream;charset=utf-8");
        //设置下载弹出框中默认显示的文件名称,如果指定中文名称的话,需要转成 iso8859-1 编码,解决乱码问题
        String fileName = new String(name.getBytes(), "iso8859-1");
        response.addHeader("Content-Disposition", "attachment;filename=" + fileName);

        try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(uploadPath + fileName))) {
            ServletOutputStream outputStream = response.getOutputStream();
            byte[] bArr = new byte[1024];
            int len;
            while ((len = bis.read(bArr)) != -1) {
                outputStream.write(bArr, 0, len);
            }
        } catch (Exception ex) {
            return;
        }
    }

    //删除具体一个文件
    //之所以使用 get 请求,主要是方便在浏览器地址栏上输入文件名,然后进行删除
    @GetMapping("/delete")
    public Result<String> delete(String name) {
        File file = new File(uploadPath + name);
        if (!file.exists()) {
            return Result.fail(0, name + ",已经不存在,无法删除");
        }

        try {
            file.delete();
            return Result.success(name + ",删除成功");
        } catch (Exception ex) {
            return Result.fail(0, ex.getMessage());
        }
    }

    //清空所有已上传的文件
    //之所以使用 get 请求,主要是方便在浏览器地址栏上输入文件名,然后进行清空
    @GetMapping("/clear")
    public Result<String> clear() {
        File dir = new File(uploadPath);
        if (!dir.exists()) {
            dir.mkdirs();
        }

        //获取文件夹下的文件列表
        File[] flist = dir.listFiles();
        for (File f : flist) {
            if (f.isFile()) {
                //删除文件
                f.delete();
            } else {
                //删除文件夹
                deleteDirectory(f);
            }
        }

        return Result.success("清空删除成功");
    }

    /**
     * 递归删除一个文件夹,以及文件夹下的所有内容
     */
    private void deleteDirectory(File dir) {
        if (dir.exists()) {
            if (dir.isDirectory()) {
                File[] flist = dir.listFiles();
                for (File f : flist) {
                    if (f.isDirectory()) {
                        deleteDirectory(f);
                    }
                    f.delete();
                }
            }
            dir.delete();
        }
    }
}

index.html 页面的源代码如下所示:(对于 SpringBoot 来说,如果 resources/static 下面有 index.html 页面的话,直接在地址栏上输入域名或者 ip 和端口,就可以访问这个 index.html 页面)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>上传文件测试</title>
</head>
<body>
<fieldset>
    <legend>文件上传</legend>
    <form id="Form1" action="/updown/upload" method="post" enctype="multipart/form-data">
        <table>
            <tr>
                <td>请选择文件:</td>
                <td>
                    <input type="file" name="file">
                    <button type="submit">上传文件</button>
                </td>
            </tr>
        </table>
    </form>
</fieldset>
<br/>
<fieldset>
    <legend>查看服务器上的文件名,当完成文件上传或删除操作后,请手动刷新打开的新页面</legend>
    <a href="/updown/list" target="_blank">点击查看可以【下载】或【删除】的文件名</a>
</fieldset>
<br/>
<fieldset>
    <legend>文件下载,支持中文文件名称</legend>
    <form id="Form2" action="/updown/download" method="get">
        <table>
            <tr>
                <td>请输入要下载的文件名:</td>
                <td>
                    <input type="text" name="name">
                    <button type="submit">下载文件</button>
                </td>
            </tr>
        </table>
    </form>
</fieldset>
<br/>
<fieldset>
    <legend>删除文件,支持中文文件名称</legend>
    <form id="Form3" action="/updown/delete" method="get">
        <table>
            <tr>
                <td>请输入要删除的文件名:</td>
                <td>
                    <input type="text" name="name">
                    <button type="submit">删除文件</button>
                </td>
            </tr>
        </table>
    </form>
</fieldset>
<br/>
<fieldset>
    <legend>清空文件(删除的是服务器上的固定文件夹下的文件,可以放心清空)</legend>
    <form id="Form4" action="/updown/clear" method="get">
        <table>
            <tr>
                <td>清空服务器上的文件:</td>
                <td>
                    <button type="submit">清空文件</button>
                </td>
            </tr>
        </table>
    </form>
</fieldset>
</body>
</html>

最后再列出 application.yml 配置文件的内容:

server:
  port: 9000
spring:
  application:
    name: fileupdown
  servlet:
    multipart:
      # 单个文件的上传大小限制
      max-file-size: 100MB
      # 如果同时上传多个文件时,总大小限制
      max-request-size: 100MB
# 服务器上保存上传文件的文件夹全路径(最后以分隔符结尾)
# windows服务器全路径示例:d:/tmp/
# linux服务器全路径示例:/tmp/
upload:
  path: D:/temp/

在配置文件中,需要重点关注的就是上传文件的大小限制,以及上传的目录。需要注意的是:UpdownController 中所提供的接口,都是基于 upload.path 所配置的目录进行操作的,因此可以放心的使用接口,不会对服务器的其它文件造成影响。

完成以上操作后,启动 SpringBoot 程序,输入 http://localhost:9000 就可以直接访问到 index.html 页面:

 


三、部署使用

我们把工程代码打包之后,会得到一个 fileupdown-1.0.jar 文件,由于 application.yml 也被打包到了 jar 包中,因此内部的 application.yml 肯定是无法修改了,因此我们需要从代码中复制出来一个 application.yml 的文件,启动 SpringBoot 程序时,使用外部的 application.yml 配置文件。

假设我把 jar 包和外部的 application.yml 配置文件都放在了 /root/fileupdown 目录下,我的 jdk 安装在了 /usr/java/jdk1.8.0_151 目录下,此时启动命令如下所示:(反斜杠( \ )表示命令换行,主要是因为命令太长,因此需要换行编写)

# 启动命令最好使用绝对路径编写
nohup /usr/java/jdk1.8.0_151/bin/java \
   -jar /root/fileupdown/fileupdown-1.0.jar \
   --spring.config.location=/root/fileupdown/application.yml \
   > /root/fileupdown/fileupdown.log 2>&1 &

想要停止 SpringBoot 服务,只需要通过如下两个命令即可实现:

# 查询出 fileupdown-1.0.jar 运行的进程信息,最主要是获取到进程号
ps -ef | grep fileupdown

# 通过进程号,杀死进程
kill -9 进程号

当然也可以把 SpringBoot 部署成 linux 服务,这样后续启动和停止服务就方便多了。

进入 linux 的 /etc/systemd/system 目录,新建一个 fileupdown.service 文件,填写内容如下:

[Unit]
Description=fileupdown 
After=syslog.target network.target 
 
[Service]
Type=simple

#注意:ExecStart 后面的命令脚本都是写在一行中,没有手动进行换行,只是横向空间不够,自动换行了
ExecStart=/usr/java/jdk1.8.0_151/bin/java -jar /root/fileupdown/fileupdown-1.0.jar --spring.config.location=/root/fileupdown/application.yml > /root/fileupdown/fileupdown.log 2>&1 &
ExecStop=/bin/kill -15 $MAINPID 
 
User=root
Group=root 
 
[Install]
WantedBy=multi-user.target 

然后执行以下命令即可:(服务名称就是上面创建的文件名称:fileupdown.service ,可以简写为 fileupdown)

# 重新加载 daemon 服务
systemctl daemon-reload

# 启动 fileupdown 服务
systemctl start fileupdown

# 将 fileupdown 服务设置为开机启动
systemctl enable fileupdown

# 查看 fileupdown 是否已经设置为开机启动
# 如果能够查询到内容,则表明 fileupdown 已经添加为开机启动
systemctl list-unit-files | grep fileupdown

### 对于已经安装成的 linux 服务的 fileupdown 服务,还可以使用如下命令进行操作服务
systemctl stop fileupdown
systemctl restart fileupdown
systemctl disable fileupdown

如果没有关闭 linux 防火墙,还需要将 SpringBoot 的启动端口添加到防火墙开放端口列表中

# 本 Demo 配置的 fileupdown 的启动端口配置的是 9000
# 防火墙放开 9000 端口
firewall-cmd --zone=public --add-port=9000/tcp --permanent

# 重新加载防火墙
firewall-cmd --reload

# 查看防火墙对外开放的端口
firewall-cmd --list-ports

 

参考文章:http://blog.ncmem.com/wordpress/2023/11/21/springboot-%e6%96%87%e4%bb%b6%e4%b8%8a%e4%bc%a0%e4%b8%8b%e8%bd%bd%e5%b7%a5%e5%85%b7%e6%a0%b7%e4%be%8b/

欢迎入群一起讨论