文件上传方案
JavaWeb后端服务开发过程中,有对文件上传服务的需求。
在之前的《Axios+SSM上传和获取图片》中,提供了图片上传和显示的解决方案,但是对于数据量更大的文件,虽然例如MySQL数据库提供了用于存储二进制文件的Blob字段类型,但是,在文件读写过程中,可能会造成数据表或者二进制文件损坏的情况。
因此,更加实用的方案是:将文件上传到Tomcat服务器所在主机或者远程主机的硬盘上存储,而将文件上传记录(即:文件链接/地址)以字符串形似存储到数据库中;等到用户通过浏览器页面,需要浏览文件时,再通过数据库中记录的文件链接信息,从本地或者远程主机的硬盘读取文件,将其响应给浏览器客户端展示/下载。
基于Ajax+HTML的文件上传方案
文件上传-二进制数据
在浏览器页面中实现文件选择功能,可以使用input type=file解决。
需要上传的数据中,不建议携带除了上传资源以外的数据。但通常可以是,普通数据(用户名/id等)+二进制文件数据(二进制文件数据通过二进制流的方式发送给服务器)。
通过Ajax发送二进制流数据
发送二进制流数据,仍然是通过FormData对象实现。即:
①将将要上传的数据存储到FormData对象中;
② 将processData属性的值设置为false,告诉浏览器发送对象请求数据
③ 将contentType属性的值设置为false,设置请求数据的类型为二进制类型。
④ 正常发送ajax请求即可
前端代码编写
为了方便演示,通过JSP编写网页代码,举例如下:
<%--
Created by IntelliJ IDEA.
User: 13241
Date: 2022/3/1
Time: 20:52
To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>主页</title>
</head>
<body>
文件上传demo
<form enctype="multipart/form-data">
<%--普通数据--%>
<p><label for="user">用户:</label><input id="user" type="text" name="username"/></p>
<%--二进制文件数据--%>
<p><label for="file">文件:</label><input id="file" type="file" name="file"/> <br>
<a id="uploadFile" href="javascript:void(0)" onclick="upload()">立即上传</a></p>
<p><input id="btnsubmit" type="submit" value="提交"/></p>
</form>
<script type="text/javascript" src="./js/jquery-3.6.0.min.js"></script>
<script type="text/javascript">
function upload(){
//获取要上传的文件
var file=document.getElementById("file").files[0];//二进制文件数据
var username=document.getElementById("user").value;//普通数据
if (file==undefined){
alert("请先选择文件.")
return false;
}
//将文件放入formData对象中
var formData=new FormData();
formData.append("file",file);
formData.append("username",username);
//ajax发送数据
$.ajax({
headers: {
//有需要的话-添加token
'token':"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2NDcyNzM0MTEsImVtYWlsIjoiMTMyNDE1NjkyMEBxcS5jb20ifQ.EQYaVAMMqumGLKid_5EPhdz2VBJQBTbGFSDfdBYPwlM",
},
url:"/stasys_v3/upload/file",
type:"POST",
data:formData,
processData:false,
contentType:false,
success:function (result) {
console.log(result)
}
})
}
</script>
</body>
</html>
文件上传-后端处理
前端通过Ajax,借助FormData对象上传二进制流文件数据,在后端的业务逻辑处理,需要借助commons-fileupload、commons-io开发包实现。
<!--文件上传依赖-->
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.4</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.8.0</version>
</dependency>
在编写代码时,需要在SpringMVC配置文件中,配置文件上传解析组件,具体配置如下:
<!--文件上传解析组件
id必须为multipartResolver
springmvc默认使用该id找该组件
-->
<bean
id="multipartResolver"
class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<!--单个文件的最大大小(以字节为单位)-1024*1024*512=512M-->
<property name="maxUploadSizePerFile" value="536870912"/>
</bean>
后端处理后,可以将文件存储的链接/地址包含在json对象中响应给浏览器前端。例如:
{state:true,msg:“服务器繁忙”,url:”上传成功的资源的请求地址”}
后端代码编写
package com.xwd.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletRequest;
import java.io.File;
import java.net.InetAddress;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
@Controller
@RequestMapping(value = "/upload")
public class UploadController {
//methods
@RequestMapping(value = "/file")
@ResponseBody
public Map<String,Object> uploadFile(@RequestParam(value = "file") MultipartFile file,
@RequestParam(value = "username") String username,
HttpServletRequest request){
System.out.println(file);
System.out.println("username="+username);
//获取protocol通信协议
String scheme = request.getScheme();// http
//获取服务名称
String serverName = request.getServerName();//localhost
//获取端口号
int serverPort = request.getServerPort();//8010
//contextPath
String contextPath = request.getContextPath();// /stasys_v3
String url=scheme+"://"+serverName+":"+serverPort+contextPath+"/upload/";
Map<String,Object> map=new HashMap<>();
// 控制文件大小
if(file.getSize()>1024*1024*512){
map.put("status",false);
map.put("message", "文件大小不能超过512M");
map.put("url",null);
return map;
}
try {
//保存文件到当前项目根目录下
String realPath = request.getServletContext().getRealPath("/upload");
File dir=new File(realPath);
//判断目录是否存在
if (!dir.exists()){
dir.mkdirs();//不存在-创建新目录
}
//获取文件名
String originalFilename = file.getOriginalFilename();
//使用UUID替换文件名——避免文件名冲突
String uuid= UUID.randomUUID().toString();
//获取文件拓展名
String extendsName = originalFilename.substring(originalFilename.lastIndexOf("."));
// 控制文件类型
if(!extendsName.equals(".zip")){
map.put("status",false);
map.put("message", "文件类型错误,必须为.zip压缩文件");
map.put("url",null);
return map;
}
//新的文件名-056af4c8-2a5a-4e6e-a108-45f689865264.zip
String newFileName=uuid.concat(extendsName);
//文件对应URL路径:http://localhost:8010/stasys_v3/upload056af4c8-2a5a-4e6e-a108-45f689865264.zip
String fileUrl = url.concat(newFileName);
//文件保存位置
File saveLoc = new File(dir,newFileName);
//保存文件
file.transferTo(saveLoc);
//填充返回值
map.put("status",true);
map.put("msg","文件上传成功!");
map.put("url","");
}catch (Exception e){
e.printStackTrace();
map.put("status",false);
map.put("msg","文件上传失败!");
map.put("url",fileUrl);
}
System.out.println(map.toString());
return map;
}
}
消息响应
文件服务器-分服务器文件上传
后端开发-服务器职责
常见的服务器类型,例如:
数据库服务器:运行数据库,提供基本属性数据的读写服务。
缓存和消息服务器:负责处理高并发访问的数据缓存和消息
文件服务器:负责存储用户上传的文件的服务器。
应用服务器:用于部署JavaWeb应用。
在实际开发中,会有很多职责不同的服务器,通过合理分工,可以提升项目的运行效率。
将Tomcat服务器作为文件服务器
制作并启动文件服务器
可单独解压一个Tomcat,作为文件服务器。
【0】创建文件保存文件夹:upload
【1】配置文件服务器Server端口号:8006
<!-- Note: A "Server" is not itself a "Container", so you may not
define subcomponents such as "Valves" at this level.
Documentation at /docs/config/server.html
-->
<Server port="8006" shutdown="SHUTDOWN">
<Listener className="org.apache.catalina.startup.VersionLoggerListener" />
【2】修改连接器端口号为:8090
<!-- A "Connector" represents an endpoint by which requests are received
and responses are returned. Documentation at :
Java HTTP Connector: /docs/config/http.html
Java AJP Connector: /docs/config/ajp.html
APR (HTTP/AJP) Connector: /docs/apr.html
Define a non-SSL/TLS HTTP/1.1 Connector on port 8080
-->
<Connector port="8090" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443" />
【3】设置远程服务器非只读,修改web.xml中servlet初始化参数readOnly为false.
<init-param>
<param-name>readonly</param-name>
<param-value>false</param-value>
</init-param>
【4】启动文件服务器
SSM项目:添加跨服务文件上传依赖
<!--跨服务文件上传依赖-->
<dependency>
<groupId>com.sun.jersey</groupId>
<artifactId>jersey-client</artifactId>
<version>1.19</version>
</dependency>
跨服务器文件上传:前端代码编写
前端代码无需改动。
跨服务器文件上传:后端代码编写
后端代码改动如下。
package com.xwd.controller;
import com.sun.jersey.api.client.Client;
import com.sun.jersey.api.client.WebResource;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletRequest;
import java.io.File;
import java.net.InetAddress;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* @ClassName UploadController
* @Description: com.xwd.controller
* @Auther: xiwd
* @Date: 2022/3/14 - 03 - 14 - 22:56
* @version: 1.0
*/
@Controller
@RequestMapping(value = "/upload")
public class UploadController {
//properties
private final static String FileServerDir="http://192.168.0.104:8090/upload/";
//setter
//getter
//constructors
//methods
@RequestMapping(value = "/file")
@ResponseBody
public Map<String,Object> uploadFile(@RequestParam(value = "file") MultipartFile file,
@RequestParam(value = "username") String username,
HttpServletRequest request){
System.out.println(file);
System.out.println("username="+username);
//获取protocol通信协议
String scheme = request.getScheme();// http
//获取服务名称
String serverName = request.getServerName();//localhost
//获取端口号
int serverPort = request.getServerPort();//8010
//contextPath
String contextPath = request.getContextPath();// /stasys_v3
String url=scheme+"://"+serverName+":"+serverPort+contextPath+"/upload/";
Map<String,Object> map=new HashMap<>();
// 控制文件大小
if(file.getSize()>1024*1024*512){
map.put("status",false);
map.put("message", "文件大小不能超过512M");
map.put("url",null);
return map;
}
try {
//保存文件到当前项目根目录下
String realPath = request.getServletContext().getRealPath("/upload");
File dir=new File(realPath);
//判断目录是否存在
if (!dir.exists()){
dir.mkdirs();//不存在-创建新目录
}
//获取文件名
String originalFilename = file.getOriginalFilename();
//使用UUID替换文件名——避免文件名冲突
String uuid= UUID.randomUUID().toString();
//获取文件拓展名
String extendsName = originalFilename.substring(originalFilename.lastIndexOf("."));
// 控制文件类型
if(!extendsName.equals(".zip")){
map.put("status",false);
map.put("message", "文件类型错误,必须为.zip压缩文件");
map.put("url",null);
return map;
}
//新的文件名-056af4c8-2a5a-4e6e-a108-45f689865264.zip
String newFileName=uuid.concat(extendsName);
//创建sun公司提供的jersey包中的com.sun.jersey.api.client.Client类的对象
Client client = Client.create();
//提供文件服务器存储路径-http://192.168.0.104:8090/+新文件名
WebResource webResource = client.resource(FileServerDir+newFileName);
//提供文件的二进制数据file.getBytes()——保存文件到另一个服务器
webResource.put(String.class, file.getBytes());
//填充返回值
map.put("status",true);
map.put("msg","文件上传成功!");
map.put("url",FileServerDir+newFileName);
}catch (Exception e){
e.printStackTrace();
map.put("status",false);
map.put("msg","文件上传失败!");
map.put("url",null);
}
System.out.println(map.toString());
return map;
}
}
补充:基于Axios+Element UI文件上传方案
后端代码
后端代码同改动之后的跨服务器文件上传代码一致。
Element UI组件使用与Axios数据请求
el-upload上传组件
使用Element UI的el-upload上传组件。
<el-form-item label="实证文件:">
<el-upload
class="upload-demo"
:file-list="form.filelist"
:show-file-list="true"
drag
action="http://www.baidu.com"
:before-upload="beforeUpload"
:on-change="addFile"
:on-success="uploadSuccess"
:multiple="false"
:auto-upload="false"
accept=".zip"
:limit="1"
>
<i class="el-icon-upload"></i>
<div class="el-upload__text">
将文件拖到此处,或<em>点击上传</em>
</div>
<div class="el-upload__tip" slot="tip">
只能上传单个zip压缩文件,重复上传会导致文件覆盖
</div>
</el-upload>
el-upload事件处理
Element UI的el-upload上传组件的事件处理函数,根据需要修改函数体代码即可。
//文件上传之前文件类型检验
beforeUpload(file) {
console.log(file)
},
//添加文件-添加文件、上传成功和上传失败时都会被调用
addFile(file, fileList) {
//记录文件名称
this.form.evidence = this.form.email + new Date().getTime() + file.name
//记录文件对象
this.form.file = file
console.log(this.form.file)
console.log(this.form.evidence)
},
//文件上传成功
uploadSuccess(response, file, fileList) {
console.log(response)
console.log(file)
console.log(fileList)
},
Axios发送文件上传请求
使用Axios发送文件上传请求。
axios({
headers: { 'Content-Type': 'multipart/form-data' },
url: 'http://localhost:8010/stasys_v3/upload/file',
method: 'POST',
data: formData,
})
.then((res) => {
console.log(res)
if (res.data && res.data.status) {
//文件已上传成功
this.$message({
message: '文件已上传!',
type: 'success',
})
} else {
this.$message.error('文件上传失败!')
}
})
.catch((error) => {
console.log(error)
this.$message.error('信息上报失败!')
})
响应结果如下:
msg: "文件上传成功!"
status: true
url: "http://192.168.0.104:8090/upload/ca71cdb0-9245-4091-a623-1b1be4fa5ed3.zip"