上传文件以是系统中必不可少的功能,不论是社交方面的上传完善个人信息,以及分享个人动态,又或者到企业办公,文件传输等,文件上传早已经遍布我们的工作以及日常生活。
文件上传的必要前提
ok那闲杂话不多说,先来看看我们要使用上传功能必备的条件吧
- form 表单的 enctype 取值必须是:multipart/form-data
(默认值是:application/x-www-form-urlencoded)
enctype:是表单请求正文的类型- method 属性取值必须是 Post
- 提供一个文件选择域
先说说第一点,为什么取值需要使用multipart/form-data呢?首先表单提交的话我们知道发送出来的请求体都是如这种键值对结构的:username=zhangsan&&password=123
,都是拼起来的。而使用multipart(多部分的意思)会将表单分成多部分,这样就可以将上传文件相关,以及提交的一些信息分开。
然后是第二点为什么是使用post请求,get请求会把所有请求信息拼接到url上,但是url就是地址栏长度是有限的,文件很小也就没什么了,但是文件的数据较大的时候就不能满足条件了
最后第三个当然就是要有选中上传的选择域啦,这文件上传的入口。
当 form 表单的 enctype 取值不是默认值后,request.getParameter()将失效。
enctype=”application/x-www-form-urlencoded”时,form 表单的正文内容是:
key=value&key=value&key=value
当 form 表单的 enctype 取值为 Mutilpart/form-data 时,请求正文内容就变成:
每一部分都是 MIME 类型描述的正文
-----------------------------7de1a433602ac 分界符
Content-Disposition: form-data; name=“userName” 协议头
aaa 协议的正文
-----------------------------7de1a433602ac
Content-Disposition: form-data; name=“file”;
filename=“C:\Users\zhy\Desktop\fileupload_demofile\b.txt”
Content-Type: text/plain 协议的类型(MIME 类型)
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
-----------------------------7de1a433602ac–
那由于我们可能对请求体的各个部位理解起来,或者一些细节做的不够到位,虽然我们也可以自己解析,当然市面上也有很多关于这方面的工具包,那么我们就选择使用apach的Commons-fileupload协助我们解析,而我们重点则是放在流这一块。我们导入以下jar包:
搭建基本结构
首先搭建以下开发环境在此前的项目统一的springmvcdemo位置下,创建名为springmvc_day2_01_fileupload的wepapp模块module:
构建项目如上结构,导入所需依赖到pom中,编写好web.xml,springmvc.xml,以及几个空白jsp。
pom.xml:
<?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">
<parent>
<artifactId>springmvcdemo</artifactId>
<groupId>com.tho</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>springmvc_day2_01_fileupload</artifactId>
<packaging>war</packaging>
<name>springmvc_day2_01_fileupload Maven Webapp</name>
<!-- FIXME change it to the project's website -->
<url>http://www.example.com</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<spring.version>5.0.2.RELEASE</spring.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>jsp-api</artifactId>
<version>2.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.3.1</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.4</version>
</dependency>
</dependencies>
</project>
web.xml:
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >
<web-app>
<display-name>Archetype Created Web Application</display-name>
<!--springmvc核心配置器,前端控制器-->
<servlet>
<servlet-name>dispatcherServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!--配置Servlet的初始化参数,读取springmvc的配置文件,创建spring容器-->
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:springmvc.xml</param-value>
</init-param>
<!-- 配置servlet启动时加载对象 -->
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>dispatcherServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<!--配置过滤器,解决前端中文传输到后端乱码问题-->
<filter>
<filter-name>characterEncodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<!--设置过滤器初始化参数-->
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>characterEncodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!--使用Rest风格的URI,将页面普通的post请求转为delete或者put请求-->
<filter>
<filter-name>hiddenHttpMethodFilter</filter-name>
<filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>hiddenHttpMethodFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>
springmvc.xml:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <!-- 配置spring创建容器时要扫描的包 -->
<!-- 配置spring创建容器时要扫描的包 -->
<context:component-scan base-package="com.tho"/>
<!-- 配置视图解析器 -->
<bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/pages/"/>
<property name="suffix" value=".jsp"/>
</bean>
<mvc:resources location="/js/" mapping="/js/**"/>
<!--<!– 配置类型转换器工厂 –>-->
<!--<bean id="converterService" class="org.springframework.context.support.ConversionServiceFactoryBean">-->
<!--<!– 给工厂注入一个新的类型转换器 –>-->
<!--<property name="converters">-->
<!--<array>-->
<!--<!– 配置自定义类型转换器 –>-->
<!--<bean class="com.tho.utils.StringToDateConverter"/>-->
<!--</array>-->
<!--</property>-->
<!--</bean>-->
<!--配置spring开启注解mvc的支持,开启转换器组件支持-->
<mvc:annotation-driven />
</beans>
这样项目的基本结构就搭建完成了。
传统的上传方式
接下来开始编写案例:
创建UserController类:
package com.tho.controller;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import javax.servlet.http.HttpServletRequest;
import java.io.File;
import java.util.List;
@Controller
@RequestMapping("/user")
public class UserController {
/**
* 文件上传
*/
@RequestMapping("/fileupload1")
public String fileupload1(HttpServletRequest request) throws Exception {
System.out.println("文件上传。。。");
//使用fileupload组件完成文件上传
//上传位置
String path=request.getSession().getServletContext().getRealPath("/uploads/");
//判断,该路径是否存在
File file=new File(path);
if (!file.exists()){
//不存在就创建该文件夹
file.mkdirs();
}
//解析request对象,获取上传文件项
//先创建磁盘文件工厂,再把工厂放入Servlet文件上传对象中
DiskFileItemFactory factory = new DiskFileItemFactory();
ServletFileUpload upload = new ServletFileUpload(factory);
//解析request
List<FileItem> items = upload.parseRequest(request);
//遍历
for (FileItem item:items){
//进行判断,当前item对象是否是上传文件项
if (item.isFormField()){
//说明普通表单向
}else {
//说明上传文件项
//获取到上传文件的名称
String filename=item.getName();
//完成文件上传
item.write(new File(path,filename));
//删除临时文件方法
item.delete();
}
}
return "success";
}
}
编写index.jsp:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<h3>文件上传</h3>
<form action="user/fileupload1" method="post" enctype="multipart/form-data">
选择文件:<input type="file" name="upload"/>
<input type="submit" value="上传"/>
</form>
</body>
</html>
success.jsp:
<%@ page contentType="text/html;charset=UTF-8" language="java" isELIgnored="false" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<h3>文件上传成功</h3>
</body>
</html>
运行测试:
选择上传文件,然后提交
至此上传到本地服务器成功。
优化程序:现在程序上传的名字并不会改变,所以如果上传同名文件会将原来的覆盖。那么我们优化的方法就是使用uuid生成唯一的命名。
修改后Controller:
package com.tho.controller;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import javax.servlet.http.HttpServletRequest;
import java.io.File;
import java.util.List;
import java.util.UUID;
@Controller
@RequestMapping("/user")
public class UserController {
/**
* 文件上传
*/
@RequestMapping("/fileupload1")
public String fileupload1(HttpServletRequest request) throws Exception {
System.out.println("文件上传。。。");
//使用fileupload组件完成文件上传
//上传位置
String path=request.getSession().getServletContext().getRealPath("/uploads/");
//判断,该路径是否存在
File file=new File(path);
if (!file.exists()){
//不存在就创建该文件夹
file.mkdirs();
}
//解析request对象,获取上传文件项
//先创建磁盘文件工厂,再把工厂放入Servlet文件上传对象中
DiskFileItemFactory factory = new DiskFileItemFactory();
ServletFileUpload upload = new ServletFileUpload(factory);
//解析request
List<FileItem> items = upload.parseRequest(request);
//遍历
for (FileItem item:items){
//进行判断,当前item对象是否是上传文件项
if (item.isFormField()){
//说明普通表单向
}else {
//说明上传文件项
//获取到上传文件的名称
String filename=item.getName();
//把文件的名称设置唯一值,uuid
String uuid= UUID.randomUUID().toString().replace("-","");
//把uuid和原名进行拼接生成新的名字
filename=uuid+"_"+filename;
//完成文件上传
item.write(new File(path,filename));
//删除临时文件方法
item.delete();
}
}
return "success";
}
}
重新运行,选择同样的图片:
证明成功了,ok
SpringMVC上传方式
借用一下网上的图:
在index.jsp复制一个上传域:
<hr>
<h3>SpringMVC文件上传</h3>
<form action="user/fileupload2" method="post" enctype="multipart/form-data">
选择文件:<input type="file" name="upload"/>
<input type="submit" value="上传"/>
</form>
web.xml配置上以下代码:
<!-- 配置文件解析器对象,要求id名称必须是multipartResolver,10*1024*1024-->
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<property name="maxUploadSize" value="10485760"/>
</bean>
UserController加入带有springmvc文件解析的对象的方法:
/**
* SpringMVC文件上传
* MultipartFile upload ,这个参数必须和前端的选择域的name属性保持一致
* SpringMVC就会帮我们解析
*/
@RequestMapping("/fileupload2")
public String fileupload2(HttpServletRequest request, MultipartFile upload) throws Exception {
System.out.println("SpringMVC文件上传。。。");
//使用fileupload组件完成文件上传
//上传位置
String path=request.getSession().getServletContext().getRealPath("/uploads/");
//判断,该路径是否存在
File file=new File(path);
if (!file.exists()){
//不存在就创建该文件夹
file.mkdirs();
}
String filename = upload.getOriginalFilename();
String uuid=UUID.randomUUID().toString().replace("-","").toUpperCase();
filename=uuid+"_"+filename;
//上传文件
upload.transferTo(new File(file,filename));
return "success";
}
运行测试:
我们的这个方法里UUID去横杠后将其大写输出,所以证明成功了本案了
SpringMVC跨服务的上传方式
随着用户量和访问的持续增长,服务器开始剥离业务,所以实际生产中会有各种各样的服务器:
在实际开发中,我们会有很多处理不同功能的服务器。例如:
应用服务器:负责部署我们的应用
数据库服务器:运行我们的数据库
缓存和消息服务器:负责处理大并发访问的缓存和消息
文件服务器:负责存储用户上传文件的服务器
(注意:此处说的不是服务器集群)
新建一个tomcat模拟文件服务器
所以接下来就模拟跨服务,首先新建一个项目模拟作为文件存储的服务器,还是new 一个module:
什么都不用做,只是用来存储文件,所以在web-app根目录下创建一个uploads,先配置个不同的tomcat,我们新增一个9版本的以示区别。
idea配置一个tomcat9的服务器:
tomcat改名以及端口
启动文件服务器:
创建跨服务器SpringMVC上传方法
首先跨服务器上传我们需要使用Java提供的包:即现在需要4个Jar合计
<dependency>
<groupId>com.sun.jersey</groupId>
<artifactId>jersey-core</artifactId>
<version>1.18.1</version>
</dependency>
<dependency>
<groupId>com.sun.jersey</groupId>
<artifactId>jersey-client</artifactId>
<version>1.18.1</version>
</dependency>
给UserController加方法实现跨服务器
@RequestMapping("/fileupload3")
public String fileupload3( MultipartFile upload) throws Exception {
System.out.println("SpringMVC跨服务器文件上传。。。");
//定义一个上传文件服务器的路径
String path="http://localhost:9090/uploads/";
String filename = upload.getOriginalFilename();
String uuid=UUID.randomUUID().toString().replace("-","").toUpperCase();
filename=uuid+"_"+filename;
//创建库客户端的对象
Client client=Client.create();
//和图片服务器连接
WebResource webResource= client.resource(path + filename);//这里不用拼接斜杠的原因是上面路径已经带有了
//上传文件
webResource.put(upload.getBytes());
return "success";
}
给Index.jsp:
<hr>
<h3>SpringMVC跨服务器文件上传</h3>
<form action="user/fileupload3" method="post" enctype="multipart/form-data">
选择文件:<input type="file" name="upload"/>
<input type="submit" value="上传"/>
</form>
切换tomcat部署运行:
选择跨服务器上传:
500错误:Request processing failed; nested exception is com.sun.jersey.api.client.UniformInterfaceException: PUT xxx returned a response status of 403 Forbidden
原因是tomcat默认是只读的,而我们用这个tomcat作为独立的文件服务器的时候需要写入的,所以需要配置关闭只读。我们查看tomcat的conf目录下的web.xml文件可以知道。
<init-param>
<param-name>readonly</param-name>
<param-value>false</param-value>
</init-param>
保存两个重启tomcat服务
本人在实现最后一个案例的时候出现了几种错误,以下我来说明一下,出现了500响应404错误,原因细心的同学应该会发现,上面代码上传的路径使用的是绝对路径,而我们路径应带上war包路径才不会出现此情况:
500错误:Request processing failed; nested exception is com.sun.jersey.api.client.UniformInterfaceException: PUT xxx returned a response status of 404 not Found
修改重试
错误又来了响应409,就是由于路径案例路径没有uploads文件夹,手动创建一下
再试:悲剧有出现了
org.springframework.web.util.NestedServletException: Request processing failed; nested exception is com.sun.jersey.api.client.UniformInterfaceException: PUT http://localhost:9090/fileuploadserver_war_exploded/uploads/14e70c2e9a0e4efba446a058a82dad88_360截图20200912012409193.jpg returned a response status of 400 Bad Request
原因是啥呢,其实就是我们上传的文件名字带有中文导致的。
换个图片,选择没中文名的
至此总算排除万难实现了跨服务器传输文件,通过这个案例我们也得到几个启示,在实际开发中,处理文件上传需要注意几点:
1.url上传路径是否有误,怎么写更合理,更灵活,路径需不需要带war包名字等
2.服务器是否已经开启可写入
3.上传至文件服务器有没有对文件目录预处理,就是编写创建文件夹的代码,我们使用过OSS或是在其他文件存储服务,一般都会先创建一个空间,然后上传至该空间,其实这就是一种目录预处理
4.传输的文件的文件名最好也做预处理,由于用户传输的图片名字五花八门,中文也是很常见的,所以文件名的预处理也是不可获取的
接下来的博客会更新有关异常处理的内容,谢谢浏览以及关注!!
本系列代码托管在gitee,地址为:https://gitee.com/calmtho/springmvcdemo
最后发现起名的时候day2_01重复使用,将fileupload更正为springmvc_day2_02_fileupload