SpringBoot 文件上传下载工具样例
最近工作遇到这样的情景:一大堆 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/
欢迎入群一起讨论