SpringMVC的底层原理
在前面我们学习了SpringMVC的使用(67章博客开始),现在开始说明他的原理(实际上更多的细节只存在67章博客中,这篇博客只是讲一点深度,重复的东西尽量少说明点)
MVC 体系结构:
三层架构:
我们的开发架构一般都是基于两种形式,一种是 C/S 架构,也就是客户端/服务器,另一种是 B/S 架构 ,也就是浏览器服务器,在 JavaEE 开发中,几乎全都是基于 B/S 架构的开发,那么在 B/S 架构中,系 统标准的三层架构包括:表现层、业务层、持久层,三层架构在我们的实际开发中使用的⾮常多,所以我们的案例也都是基于三层架构设计的
三层架构中,每一层各司其职,接下来我们就说说每层都负责哪些方⾯
表现层 : 也就是我们常说的web 层,它负责接收客户端请求,向客户端响应结果,通常客户端使⽤http 协 议请求web 层,web 需要接收 http 请求,完成 http 响应, 表现层包括展示层和控制层:控制层负责接收请求,展示层负责结果的展示,表现层依赖业务层,接收到客户端请求一般会调用业务层进行业务处理,并将处理结果响应给客户端,表现层的设计一般都使用 MVC 模型(MVC 是表现层的设计模型,和其他层没有关系)
业务层 : 也就是我们常说的 service 层,它负责业务逻辑处理,和我们开发项目的需求息息相关,web 层依赖业 务层,但是业务层不依赖 web 层,业务层在业务处理时可能会依赖持久层,如果要对数据持久化需要保证事务一致性(也就是我们说的, 事务应该放到业务层来控制,这主要是保证持久层一个方法只干一件事情,一般都会这样,也是规范,这样比较好维护,否则持久层在一定程度也是业务层)
持久层 :也就是我们是常说的 dao 层,负责数据持久化,包括数据层即数据库和数据访问层,数据库是对数据进 行持久化的载体,数据访问层是业务层和持久层交互的接⼝,合起来就是持久层,业务层需要通过数据访问层将数据持久化 到数据库中,通俗的讲,持久层就是和数据库交互,对数据库表进行增删改查的
MVC设计模式:
MVC 全名是 Model View Controller,是模型(model),视图(view),控制器(controller)的缩写,是一 种用于设计创建 Web 应用程序表现层的模式,MVC 中每个部分各司其职:
Model(模型):模型包含业务模型和数据模型,数据模型用于封装数据,业务模型用于处理业 务,实际上就是处理业务逻辑,封装实体,虽然大多数是这样的说明,但是实际上M只是代表要返回的数据而已,只是这个数据由业务逻辑等等产生的,所以说成Service或者Dao层也行,说成返回的数据也行,但本质上是返回的数据而已,只是我们通常会将生成的数据过程,如业务逻辑也包括进去
View(视图): 通常指的就是我们的 jsp 或者 html,作用一般就是展示数据的,通常视图是依据 模型数据创建的
Controller(控制器): 是应用程序中处理用户交互的部分,作用一般就是处理程序逻辑的
即数据Model,视图View,数据与视图的交互地方Controller,简称为MVC
MVC提倡:每一层只编写自己的东⻄,不编写任何其他的代码,分层是为了解耦(降低联系),解耦是为了维护方便和分工协作
Spring MVC 是什么:
SpringMVC 全名叫 Spring Web MVC,是一种基于 Java 的实现 MVC 设计模型的请求驱动类型的轻量级Web 框架,属于SpringFrameWork 的后续产品
SpringMVC 已经成为 目前最主流的 MVC 框架 之一,并且 随着 Spring3.0 的发布,全⾯超越 Struts2, 成为最优秀的 MVC 框架
比如servlet、struts一般需要实现接⼝、而springmvc要让一个java类能够处理请求只需要添加注解就ok
它通过一套注解,让一个简单的 Java 类成为处理请求的控制器,⽽⽆须实现任何接⼝,同时它还⽀持RESTful 编程⻛格的请求
总之:Spring MVC和Struts2⼀样,都是 为了解决表现层问题 的web框架,它们都是基于 MVC 设计模 式的,⽽这些表现层框架的主要职责就是处理前端HTTP请求,只是SpringMVC(以后简称MVC了)更加的好而已(对于现在来说,并不保证以后的情况)
Spring MVC 本质可以认为是对servlet的封装,简化了我们serlvet的开发(具体原生的servlet的开发,可以到50博客学习,虽然servlet也是封装的,具体可以看27章博客的最后(这里说明了"最后",那么就不用看整个博客了))
如图:
也就是说只是用一个servlet完成的(用拦截确定类,而非servlet,这样也就完成了上面原生的处理了),但是这样与其他多个servlet相比,难道不会对该一个控制器造成负荷吗,这其实也是一个问题,假设有a,b两个方法,一个c类里面存放这a,b两个方法,和d,f这两个类分别都存放a,b这两个方法,其中100个线程同时调用一个c类里面的a,b方法和50个线程调用d里面的a,b方法和50个线程调用f里面的a,b方法,他们的资源利用是相同的吗,就是这样的问题,答:不是相同的,你这里可能会有疑惑,为什么不是相同的,大多数人可能会这样的认为,既然是调用,那么你都是拿取堆里面对象调用,只是其中一个被多个线程拿取的多,然而,拿取是同时进行的,自然速度一致,实际上这个速度在大多数情况或者实际情况可能正确,但是这里我并不这样的认为,解释如下:
大致流程如下:
即数据到控制器到视图,最终到响应,给我们显示,实际上控制器在一定程度上也可以和数据结合一起,只是为了解耦合所以我们通常也分开变成数据(业务了),所以如果非要精准的说的话,MVC只需要控制器(包括数据)以及视图就行,所以才会说SpringMVC是表现层的框架,而不包括数据之类的框架说明,当然,更加具体的在手写框架时,会明白的,现在可以大致了解
Spring Web MVC 工作流程:
需求:前端浏览器请求url:http://localhost:8080/xxx/demo/handle01(xxx是项目名称,8080是端口,这里可以自行改变,当然,请求路径你也可以改变,具体看你自己了),前端⻚⾯显示后台服务器的时间(具体看案例)
开发过程:
1:配置DispatcherServlet前端控制器
2:开发处理具体业务逻辑的Handler(@Controller、@RequestMapping)
3:xml配置⽂件配置controller扫描,配置springmvc三⼤件
4:将xml⽂件路径告诉springmvc(DispatcherServlet)
创建一个项目,如图:
对应的pom.xml(上面的文件该创建创建,当pom.xml刷新好后,那么webapp文件会发生改变):
< packaging> war</ packaging>
对应的web.xml:
<?xml version="1.0" encoding="UTF-8"?>
< web-app xmlns = " http://xmlns.jcp.org/xml/ns/javaee" xmlns: xsi= " http://www.w3.org/2001/XMLSchema-instance" xsi: schemaLocation= " http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version = " 4.0" > </ web-app>
在pom.xml中加上如下依赖:
< dependencies> < dependency> < groupId> org.springframework</ groupId> < artifactId> spring-webmvc</ artifactId> < version> 5.1.5.RELEASE</ version> </ dependency> </ dependencies>
然后再web.xml中加上如下:
< servlet> < servlet-name> dispatcherServlet</ servlet-name> < servlet-class> org.springframework.web.servlet.DispatcherServlet</ servlet-class> </ servlet> < servlet-mapping> < servlet-name> dispatcherServlet</ servlet-name> < url-pattern> /</ url-pattern> </ servlet-mapping>
一般tomcat的web.xml在其conf目录里面,如图:
我们继续说明:
< servlet>
< servlet-name> default</ servlet-name>
< servlet-class> org.apache.catalina.servlets.DefaultServlet</ servlet-class>
< init-param>
< param-name> debug</ param-name>
< param-value> 0</ param-value>
</ init-param>
< init-param>
< param-name> listings</ param-name>
< param-value> false</ param-value>
</ init-param>
< load-on-startup> 1</ load-on-startup>
</ servlet> < servlet-mapping> < servlet-name> default</ servlet-name> < url-pattern> /</ url-pattern> </ servlet-mapping>
< servlet> < servlet-name> jsp</ servlet-name> < servlet-class> org.apache.jasper.servlet.JspServlet</ servlet-class> < init-param> < param-name> fork</ param-name> < param-value> false</ param-value> </ init-param> < init-param> < param-name> xpoweredBy</ param-name> < param-value> false</ param-value> </ init-param> < load-on-startup> 3</ load-on-startup> </ servlet> < servlet-mapping> < servlet-name> jsp</ servlet-name> < url-pattern> *.jsp</ url-pattern> < url-pattern> *.jspx</ url-pattern> </ servlet-mapping>
在java资源文件夹下,创建com.controller包,然后创建DisController类:
package com. controller ; import org. springframework. stereotype. Controller ;
import org. springframework. ui. Model ;
import org. springframework. web. bind. annotation. RequestMapping ;
import org. springframework. web. servlet. ModelAndView ; import java. util. Date ;
@Controller
@RequestMapping ( "/demo" )
public class DisController { @RequestMapping ( "handle01" ) public ModelAndView handle01 ( ) { Date date = new Date ( ) ; ModelAndView modelAndView = new ModelAndView ( ) ; modelAndView. addObject ( "date" , date) ; modelAndView. setViewName ( "success" ) ; return modelAndView; }
}
public ModelAndView handle01 ( ModelAndView modelAndView) { Date date = new Date ( ) ; modelAndView. addObject ( "date" , date) ; modelAndView. setViewName ( "success" ) ; return modelAndView; }
在WEB-INF文件夹下,创建jsp目录,然后创建如下success.jsp文件:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html><head><title>Title</title></head><body><%-- ${ date },隔开也行(单纯不在单纯之间的空格是不算的) --%>跳转成功!服务器时间是:${date} </body>
</html>
在资源文件夹下加上springmvc.xml文件:
< 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/beanshttp://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/contexthttp://www.springframework.org/schema/context/spring-context.xsdhttp://www.springframework.org/schema/mvchttp://www.springframework.org/schema/mvc/spring-mvc.xsd" > < context: component-scan base-package = " com.controller" /> < bean id = " viewResolver" class = " org.springframework.web.servlet.view.InternalResourceViewResolver" > < property name = " prefix" value = " /WEB-INF/jsp/" > </ property> < property name = " suffix" value = " .jsp" > </ property> </ bean> < mvc: annotation-driven> </ mvc: annotation-driven> < mvc: default-servlet-handler/> < mvc: resources mapping = " /resources/**" location = " classpath:/" />
</ beans>
补充web.xml:
< servlet> < servlet-name> dispatcherServlet</ servlet-name> < servlet-class> org.springframework.web.servlet.DispatcherServlet</ servlet-class> < init-param> < param-name> contextConfigLocation</ param-name> < param-value> classpath:springmvc.xml</ param-value> </ init-param> </ servlet>
假设项目是springmvc这个名称,端口是8080,那么启动服务器,访问http://localhost:8080/springmvc/demo/handle01,若出现数据代表操作成功
Spring MVC 请求处理流程(前端控制器是唯一的一个servlet,所以自然都会经过他,自己看他的类就知道他继承或者实现谁了,会到HttpServlet,再到Servlet,其Servlet算是最终的,虽然还有其他的最终):
流程说明:
第一步:用户发送请求⾄前端控制器DispatcherServlet
第⼆步:DispatcherServlet收到请求调⽤HandlerMapping处理器映射器(一般是map保存的)
第三步:处理器映射器根据请求Url找到具体的Handler(后端控制器,可以根据xml配置、注解进行查找,因为查找,所以是映射),⽣成处理器对象及处理器拦截器(如果有则⽣成)一并返回DispatcherServlet,他负责创建
第四步:DispatcherServlet调⽤HandlerAdapter处理器适配器去调⽤Handler
第五步:处理器适配器执⾏Handler(controller的方法,生成对象了,这里相当于调用前面的handle01方法,他负责调用)
第六步:Handler执行完成给处理器适配器返回ModelAndView,即处理器适配器得到返回的ModelAndView,这也是为什么前面我们操作方法时,是可以直接操作他并返回的,而返回给的人就是处理器适配器,就算你不返回,那么处理器适配器或者在之前,即他们两个中间,可能会进行其他的处理,来设置ModelAndView,并给处理器适配器
第七步:处理器适配器向前端控制器返回 ModelAndView(因为适配或者返回数据,所以是适配),ModelAndView 是SpringMVC 框架的一个 底层对 象,包括 Model 和 View
第⼋步:前端控制器请求视图解析器去进行视图解析,根据逻辑视图名来解析真正的视图(加上前后的补充,即前面的配置视图解析器)
第九步:视图解析器向前端控制器返回View
第⼗步:前端控制器进行视图渲染,就是将模型数据(在 ModelAndView 对象中)填充到 request 域,改变了servlet,最终操作servlet来进行返回
第⼗一步:前端控制器向用户响应结果(jsp的)
即可以理解:请求找路径并返回(1,2,3),给路径让其判断路径并返回且获得对应对象(4,5,6,7),变成参数解析(如拼接) 进行转发(8,9),然后到jsp(10),最后渲染(11)
Spring MVC 九⼤组件:
请求参数绑定:
也就是SpringMVC如何接收请求参数,在原来的servlet中是这样接收的:
String ageStr = request. getParameter ( "age" ) ;
Integer age = Integer . parseInt ( ageStr) ;
然而SpringMVC框架对Servlet的封装,简化了servlet的很多操作,所以SpringMVC在接收整型参数的时候,直接在Handler(一般是对应Controller所操作的类里面的标注了@RequestMapping的方法)方法中声明形参即可,如:
@RequestMapping ( "xxx" )
public String handle ( Integer age) {
System . out. println ( age) ;
}
所以这里的参数绑定在一定程度上可以认为是取出参数值绑定到handler⽅法的形参上
在前面我们也可以这样(在DisController类中加上如下):
@RequestMapping ( "handle11" ) public String handle11 ( ModelAndView modelAndView) { Date date = new Date ( ) ; modelAndView. addObject ( "date" , date) ; return "success" ; } @RequestMapping ( "handle21" ) public String handle21 ( ) { Date date = new Date ( ) ; ModelAndView modelAndView = new ModelAndView ( ) ; modelAndView. addObject ( "date" , date) ; return "success" ; } @RequestMapping ( "handle31" ) public String handle31 ( ModelMap modelMap) { Date date = new Date ( ) ; modelMap. addAttribute ( "date" , date) ; return "success" ; } @RequestMapping ( "handle41" ) public String handle41 ( Model model) { Date date = new Date ( ) ; model. addAttribute ( "date" , date) ; return "success" ; } @RequestMapping ( "handle51" ) public String handle51 ( Map < String , Object > map) { Date date = new Date ( ) ; map. put ( "date" , date) ; return "success" ; }
默认⽀持 Servlet API 作为方法参数:
当需要使⽤HttpServletRequest、HttpServletResponse、HttpSession等原⽣servlet对象时,直 接在handler⽅法中形参声明使用即可
我们在前面的controller包下创建TestController类:
在这之前首先需要加上,如下依赖:
< dependency> < groupId> javax.servlet</ groupId> < artifactId> javax.servlet-api</ artifactId> < version> 3.1.0</ version> < scope> provided</ scope> </ dependency>
TestController类:
package com. controller ; import org. springframework. stereotype. Controller ;
import org. springframework. web. bind. annotation. RequestMapping ;
import org. springframework. web. servlet. ModelAndView ; import javax. servlet. http. HttpServletRequest ;
import javax. servlet. http. HttpServletResponse ;
import javax. servlet. http. HttpSession ;
import java. util. Date ; @Controller
@RequestMapping ( "/test" )
public class TestController { @RequestMapping ( "a1" ) public ModelAndView a1 ( HttpServletRequest request, HttpServletResponse response, HttpSession session) { String id = request. getParameter ( "id" ) ; System . out. println ( id) ; Date date = new Date ( ) ; ModelAndView modelAndView = new ModelAndView ( ) ; modelAndView. addObject ( "date" , date) ; modelAndView. setViewName ( "success" ) ; System . out. println ( request) ; System . out. println ( response) ; System . out. println ( session) ; return modelAndView; }
}
这里我们假设项目是springmvc的名称(或者说映射的),所以我们访问http://localhost:8081/springmvc/test/a1?id=2,查看显示和打印的结果
当然,还有很多种情况,这些基础我们到67章博客回顾即可,但是这里补充一个,就是布尔类型的,我们再TestController类中加上如下:
@RequestMapping ( "a2" ) public ModelAndView a2 ( boolean id) { System . out. println ( id) ; Date date = new Date ( ) ; ModelAndView modelAndView = new ModelAndView ( ) ; modelAndView. addObject ( "date" , date) ; modelAndView. setViewName ( "success" ) ; return modelAndView; }
访问http://localhost:8081/springmvc/test/a2?id=true,一般来说可以是true,false,1,0,由于对应"=“后面的作为一个字符串数,所以若是id=“true”,那么他这个字符串是"true”,注意,是这个:
String id = "true" ;
System . out. println ( id) ;
id = "\"true\"" ;
System . out. println ( id) ;
所以由于id="true"是不能变成boolean的,就如上面的第二个id不能直接变成boolean一样,但是第一个是可以的,这里需要注意
经过测试,boolean的值有true,false,1(代表true),0(代表false),当然,也可以是包装类,因为对于null,基本类型可能不行(会报错),这个时候我们大多使用包装类的,且他默认给出的绑定通常就是包装类的,所以前面的boolean就是拆箱得到的(就会考虑null的情况,而导致是否报错,虽然这个报错可能是捕获或者抛出来处理的,所以通常并不是空指针异常,这里可以自己测试),这里也了解即可(前端的id和id=基本都会使得id作为null,在前端,如input中的name没有id,或者id的value为空串,即=“”,一般也是null)
对 Restful ⻛格请求⽀持:
虽然大多数67章博客(或者从他开始),基本都说明了,但是有些是比较重要的,需要特别的再说明一次,比如这个Restful ⻛格请求,大多数我们需要这个来统一规划,而非按照某些自己定义的规划,这里来说明一下为什么会存在该风格,以及一定要使用该风格吗:
即Restful 是一种 web 软件架构⻛格,它不是标准也不是协议,它倡导的是一个资源定位及资源操作的风格,比如你走迷宫,有10条道路可以走到终点,那么Restful就是比较先到达终点的那条道路,即他只是一个好的方式而已,并非一定按照他,也就不谈标准或者协议了(协议一般也是一个标准,标准代表很多的意思,通常代表我们按照指定的规则或者双方按照指定的规则进行处理)
什么是 REST:
REST(英⽂:Representational State Transfer,简称 REST)描述了一个架构样式的⽹络系统, ⽐如web 应用程序,它⾸次出现在 2000 年 Roy Fielding 的博⼠论⽂中,他是 HTTP 规范的主要编写者之一,在目前主流的三种 Web 服务交互方案中,REST 相⽐于 SOAP(Simple Object Access protocol, 简单对象访问协议)以及 XML-RPC 更加简单明了,⽆论是对 URL 的处理还是对 Payload 的编码,REST 都倾向于用更加简单轻量的方法设计和实现,值得注意的是 REST 并没有一个明确的标准,⽽更像 是一种设计的⻛格,它本身并没有什么实用性,其核⼼价值在于如何设计出符合 REST ⻛格的⽹络接⼝,比如合理的请求,合理的状态码等等,甚至你可以加上返回一些提示信息,这些请求和返回的数据设计过程就是该风格需要处理的,而再这个基础之上我们使用GET,POST,PUT,DELETE和来处理请求,而REST风格基本就是上面的结合
Restful 的优点:
它结构清晰、符合标准、易于理解、扩展方便,所以正得到越来越多⽹站的采用
Restful 的特性:
资源(Resources):⽹络上的一个实体,或者说是⽹络上的一个具体信息,它可以是一段⽂本、一张图⽚、一⾸歌曲、一种服务,总之就是一个具体的存在,可以用一个 URI(统 一资源定位符)指向它,每种资源对应一个特定的 URI ,要获取这个资源,访问它的 URI 就可以,因此URI 即为每一个资源的独一⽆⼆的识别符
表现层(Representation):把资源具体呈现出来的形式,叫做它的表现层 (Representation),⽐ 如,⽂本可以用 txt 格式表现,也可以用 HTML 格式、XML 格式、JSON 格式表现,甚⾄可以采用⼆进 制格式
状态转化(State Transfer):每发出一个请求,就代表了客户端和服务器的一次交互过程,比如HTTP 协议,他是一个⽆状态协议,即所有的状态都保存在服务器端,因此,如果客户端想要操作服务器, 必须通过某种⼿段,让服务器端发⽣"状态转化"(State Transfer),⽽这种转化是建⽴在表现层 之上的,所以就是 “表现层状态转化” ,具体说, 就是 HTTP 协议⾥⾯,四个表示操作方式的动词:GET 、POST 、PUT 、DELETE,它们分别对应四种基本操作:GET 用来获取资源,POST 用来新建资源,PUT 用来更新资源,DELETE 用来删除资源,如get变成了获取资源,这是一种状态转化,一般我们常用的是get和post,分别对访问url进行不同的处理,即状态转化处理,使得他们各有其职位,即总体来说,首先我们在访问url之前,首先通过状态转化确定需要一些什么东西,需要干什么(特别的,如加上参数),然后进行表现层的访问,最后拿取资源
RESTful 的示例:
这里我们将RESTful简称为rest
没有rest的话,原有的url设计一般是:http://localhost:8080/user/queryUserById?id=3
上面不难看出,url中定义了动作(操作),因为queryUserById一看就知道是查询用户的id,所以参数具体锁定到操作的是谁
有了rest⻛格之后,那么设计应该是如下:
由于rest中,认为互联⽹中的所有东⻄都是资源,既然是资源就会有一个唯一的uri标识它,代表它,如:
http://localhost:8080/user/3 代表的是id为3的那个用户记录(资源),要注意由于侧重点代表的是资源,所以这个整体就是这样的处理,一般rest也默认这个3是查询id的(主要是数据库默认主键基本都是id作为名称,所以现在的规范基本都是如此)
可以看出的确贯彻url指向资源,这种情况由于默认的存在,我们一般就省略了queryUserById?id=3,这就是rest风格的处理方式
既然锁定资源之后如何操作它呢,常规操作就是增删改查
根据请求方式不同,代表要做不同的操作:
get:查询,获取资源
post:增加,新建资源
put:更新
delete:删除资源
在请求方式不同的情况之下,rest⻛格带来的直观体现就是传递参数方式的变化,参数可以在url中了(以url作为资源指向,并且自身作为参数,作为其风格,与传统的主要区别在于是url被作为参数的),而不用操作放在?后面如:queryUserById?id=3
示例:
/account/1 HTTP GET(前面的url就不给出了,虽然这个形式并非请求体的标准形式) :得到 id = 1 的 account
/account/1 HTTP DELETE:删除 id = 1 的 account
/account/1 HTTP PUT:更新 id = 1 的 account
请求头的标准形式(http的协议标准):一般是这样的:POST /account/1 HTTP/1.1
总结:
URL:资源定位符,通过URL地址去定位互联⽹中的资源(抽象的概念,⽐如图⽚、视频、app服务 等)
RESTful ⻛格 URL:互联⽹所有的事物都是资源,要求URL中只有表示资源的名称,没有动词
RESTful⻛格资源操作:使⽤HTTP请求中的method⽅法put、delete、post、get来操作资源,分别对 应添加、删除、修改、查询,不过一般使用时还是 post 和 get,put 和 delete⼏乎不使用(在前端中,提交表单时,一般也只会设置get和post,或者只能这样的设置,具体解决方式在后面会给出的,具体注意即可,一般由于action中写上不存在的,就如put,delete,他们是不能设置的,也就说明不存在,像不存在的一律都会默认为get,可以自己测试一下就知道了)
虽然前端表单不能设置,但并不意味着后端不能设置,因为这些名称除了get,其他的也只是名称上的不同而已,存放的数据操作基本一致
RESTful ⻛格资源表述:可以根据需求对URL定位的资源返回不同的表述(也就是返回数据类型,⽐如XML、JSON等数据格式)
Spring MVC ⽀持 RESTful ⻛格请求,具体讲的就是使用 @PathVariable 注解获取 RESTful ⻛格的请求URL中的路径变量
从上面你可能会感受到,post前面说明了是增加,为什么这里说明是更新呢,实际上post和put都是添加和更新的意思,所以他们在该rest风格下基本只是逻辑的不同,具体怎么使用看你自己,但是一般情况下,put是更新,post是添加
现在我们来操作一下示例:
先看一个图:
首先在webapp目录下加上test.jsp:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head><title>Title</title>
</head>
<body><%--查询指定id为15的数据--%>
<a href="demo/handle/15">rest_get测试</a><%--进行添加,添加不需要指定什么--%>
<form method="post" action="demo/handle"><input type="text" name="username"/><input type="submit" value="提交rest_post请求"/>
</form><%--更新,那么自然需要一个数据,也就是list(可以认为是姓名,如改成这个姓名,当然,后面还可以继续的补充),当然,有时候并不操作put,所以需要加上list才可,来防止后面请求的冲突,如后面的删除--%>
<form method="post" action="demo/handle/15/lisi"><input type="hidden" name="_method" value="put"/><input type="submit" value="提交rest_put请求"/>
</form>
<%--删除指定的id的数据--%>
<form method="post" action="demo/handle/15"><input type="hidden" name="_method" value="delete"/><input type="submit" value="提交rest_delete请求"/>
</form></body>
</html>
然后再controller包下添加DemoController类:
package com. controller ; import org. springframework. stereotype. Controller ;
import org. springframework. web. bind. annotation. PathVariable ;
import org. springframework. web. bind. annotation. RequestMapping ;
import org. springframework. web. bind. annotation. RequestMethod ;
import org. springframework. web. servlet. ModelAndView ; import java. util. Date ; @Controller
@RequestMapping ( "/demo" )
public class DemoController { @RequestMapping ( value = "/handle/{id}" , method = { RequestMethod . GET } ) public ModelAndView handleGet ( @PathVariable ( "id" ) Integer id) { Date date = new Date ( ) ; ModelAndView modelAndView = new ModelAndView ( ) ; modelAndView. addObject ( "date" , date) ; modelAndView. setViewName ( "success" ) ; return modelAndView; } @RequestMapping ( value = "/handle" , method = { RequestMethod . POST } ) public ModelAndView handlePost ( String username) { System . out. println ( username) ; Date date = new Date ( ) ; ModelAndView modelAndView = new ModelAndView ( ) ; modelAndView. addObject ( "date" , date) ; modelAndView. setViewName ( "success" ) ; return modelAndView; } @RequestMapping ( value = "/handle/{id}/{name}" , method = { RequestMethod . PUT } ) public ModelAndView handlePut ( @PathVariable ( "id" ) Integer id, @PathVariable ( "name" ) String username) { System . out. println ( id) ; System . out. println ( username) ; Date date = new Date ( ) ; ModelAndView modelAndView = new ModelAndView ( ) ; modelAndView. addObject ( "date" , date) ; modelAndView. setViewName ( "success" ) ; return modelAndView; } @RequestMapping ( value = "/handle/{id}" , method = { RequestMethod . DELETE } ) public ModelAndView handleDelete ( @PathVariable ( "id" ) Integer id) { System . out. println ( id) ; Date date = new Date ( ) ; ModelAndView modelAndView = new ModelAndView ( ) ; modelAndView. addObject ( "date" , date) ; modelAndView. setViewName ( "success" ) ; return modelAndView; } }
当然,为了保证编码的情况,我们需要在web.xml中加上这个(在67章博客说明了):
< 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>
继续测试一下(操作加上他和不加上他的情况,看看输入中文后的结果,当然,确保重新启动的是改变的代码,可以选择删除对应的编译后的处理),来看看结果即可
当然,若get乱码出现问题,那么说明其tomcat版本比较低,通常需要tomcat配置(具体可以百度,一般在tomcat的server.xml中进行修改),而出现这种情况一般代表tomcat都存在默认编码(编码是必然存在的,只是有些默认并不是utf-8而已,高版本中get一般都是utf-8,而低版本一般就不是,大多数都可能是iso8859(具体名称可能不是这样,这里只是简称))
我们回到之前的jsp中,可以看到,后面的更新(put)和删除(delete)都有一个隐藏域,并且name都是_method,他一般是解决表单不能操作put和delete的一种方式,因为我们可以通过他这个名称以及对应的值了判断进行某些处理,但是一般需要在到达方法之前进行处理,所以一般需要过滤器,而我们并不需要监听什么,所以过滤器即可,并且这个过滤器springmvc已经提供给我们了,所以我们使用即可
现在我们回到web.xml加上如下:
< 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>
然后我们回到之前的DemoController类中,将"//记得改成POST"中,我们已经改变的改变回去,然后操作,会发现,测试成功了,说明我们配置成功,并且解决了表单只能操作get,post,不能操作put和delete的问题,当然,如果put操作delete自然与put操作get是类似的错误,但是我们访问时,他只是打印了,并没有出现响应信息,还是报错的(首先是找到,没有找到的话,那么没有找到的报错先出现,自然不会出现这个报错了,报错在没有手动处理的情况下(try),可是不会操作后续的),这是为什么,因为只有当前的我们的请求是进行处理的,而转发,并不会进行处理,但是他是在内部进行的,所以错误信息也是不同的,为了验证转发不行,我们可以修改一下,修改如下:
@ResponseBody @RequestMapping ( value = "/handle/{id}" , method = { RequestMethod . DELETE } ) public String handleDelete ( @PathVariable ( "id" ) Integer id) { System . out. println ( id) ; Date date = new Date ( ) ; ModelAndView modelAndView = new ModelAndView ( ) ; modelAndView. addObject ( "date" , date) ; modelAndView. setViewName ( "success" ) ; System . out. println ( 1 ) ; return "11" ; }
这样就会操作直接的数据返回,若你访问后,出现了数据,就说明的确是转发不能操作了,即不会处理了
当然,后面的一些知识可能也在对应67章博客开始(到68章即可),就大致学习过,但是这里我们需要总结,以及完成一些常用工具类的处理,这个在以后开发中,也是有巨大作用的
Ajax Json交互:
交互:两个方向
1:前端到后台:前端ajax发送json格式字符串,后台直接接收为pojo(即类的)参数,使用注解@RequstBody
2:后台到前端:后台直接返回pojo对象,前端直接接收为json对象或者字符串,使用注解@ResponseBody
什么是 Json:
Json是一种与语⾔⽆关的数据交互格式,就是一种字符串,只是用特殊符号{}内表示对象、[]内表示数组,""内是属性或值,:表示后者是前者的值,比如:
{“name”: “Michael”}可以理解为是一个包含name为Michael的对象
[{“name”: “Michael”},{“name”: “Jerry”}]就表示包含两个对象的数组
@ResponseBody注解:
@responseBody注解的作用是将controller的方法返回的对象通过适当的转换器转换为指定的格式之后,写⼊到response对象的body区,通常用来返回JSON数据或者是XML数据,注意:在使用此注解之 后不会再⾛视图处理器,⽽是直接将数据写⼊到输⼊流中,他的效果等同于通过response对象输出指定 格式的数据(这个在68章博客中有提到过)
分析Spring MVC 使用 Json 交互:
我们重新创建一个项目,之前的不操作了,创建如下:
依赖如下:
< packaging> war</ packaging> < dependencies> < dependency> < groupId> org.springframework</ groupId> < artifactId> spring-webmvc</ artifactId> < version> 5.1.5.RELEASE</ version> </ dependency> < dependency> < groupId> com.fasterxml.jackson.core</ groupId> < artifactId> jackson-databind</ artifactId> < version> 2.9.8</ version> </ dependency> < dependency> < groupId> com.fasterxml.jackson.core</ groupId> < artifactId> jackson-core</ artifactId> < version> 2.9.8</ version> </ dependency> < dependency> < groupId> com.fasterxml.jackson.core</ groupId> < artifactId> jackson-annotations</ artifactId> < version> 2.9.0</ version> </ dependency> </ dependencies>
web.xml文件:
<?xml version="1.0" encoding="UTF-8"?>
< web-app xmlns = " http://xmlns.jcp.org/xml/ns/javaee" xmlns: xsi= " http://www.w3.org/2001/XMLSchema-instance" xsi: schemaLocation= " http://xmlns.jcp.org/xml/ns/javaeehttp://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version = " 4.0" > < servlet> < servlet-name> dispatcherServlet</ servlet-name> < servlet-class> org.springframework.web.servlet.DispatcherServlet</ servlet-class> < init-param> < param-name> contextConfigLocation</ param-name> < param-value> classpath:spring-mvc.xml</ param-value> </ init-param> < load-on-startup> 2</ load-on-startup> </ servlet> < servlet-mapping> < servlet-name> dispatcherServlet</ servlet-name> < url-pattern> /</ url-pattern> </ servlet-mapping>
</ web-app>
spring-mvc.xml:
< 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/beanshttp://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/mvchttp://www.springframework.org/schema/mvc/spring-mvc.xsdhttp://www.springframework.org/schema/contexthttp://www.springframework.org/schema/context/spring-context.xsd" > < context: component-scan base-package = " com.test" /> < mvc: annotation-driven> </ mvc: annotation-driven>
</ beans>
JsonController:
package com. test ; import org. springframework. stereotype. Controller ;
import org. springframework. web. bind. annotation. RequestMapping ;
import org. springframework. web. bind. annotation. ResponseBody ; @Controller
public class JsonController { @ResponseBody @RequestMapping ( "json" ) public String json ( ) { return "1" ; }
}
启动服务器,访问,若出现数据,代表操作成功
我们引入jq的js,在webapp目录下的WEB-INF中,创建js文件,然后将下面下载的js,放入进去:
链接:https://pan.baidu.com/s/1nRPfSHYOFx9pMHDMSsN0hg
提取码:alsk
然后我们在mvc的xml中(不是web.xml,是前面的spring-mvc.xml)加上如下:
< mvc: resources mapping = " /js/**" location = " /WEB-INF/js/" />
然后在com目录下,创建entity包,然后在里面创建User类(这里顺便将test目录修改成controller吧):
package com. entity ; public class User { String id; String username; public String getId ( ) { return id; } public void setId ( String id) { this . id = id; } public String getUsername ( ) { return username; } public void setUsername ( String username) { this . username = username; } @Override public String toString ( ) { return "User{" + "id='" + id + '\'' + ", username='" + username + '\'' + '}' ; }
}
在test(这里修改成了controller了),加上如下:
@ResponseBody @RequestMapping ( "ajax" ) public List < User > ajax ( @RequestBody List < User > list) { System . out. println ( list) ; return list; }
然后我们在webapp目录下创建如下的文件(index.jsp):
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head><title>Title</title>
</head>
<body><!--这个路径可以选择远程的路径,百度上查一查就行了
比如:<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
-->
<script src="js/jquery-3.4.1.min.js"></script>
<button id="btn">ajax提交</button>
<script>$("#btn").click(function(){let url = 'ajax';let data = '[{"id":1,"username":"张三"},{"id":2,"username":"李四"}]'$.ajax({type:'POST',//大小写可以忽略url:url,data:data,contentType:'application/json;charset=utf-8', //如这里success:function (data) {console.log(data);}})})
</script>
</body>
</html>
对应的路径我们不加上斜杠开头,因为一般代表是端口开始的,若测试后,对应前端打印的信息得到了,那么代表操作成功,注意:一般情况下,默认ajax只会传递寻常get,以及post的键值对,而不会将其数据变成字符串,这样会使得后端并不能进行解释(如对应的注解来解释),所以一般需要设置一些请求头(“如上面的//如这里”)
到这里我决定给出很多细节了,因为这些细节,大多数情况下,可能并没有博客会选择将所有情况进行处理,以及存在对应视频的说明,所以在进行类似变化时,大多数人是迷糊的,而只是记住要这样做,细节分别是两个方面:
ajax(或者说js,前端访问后端除了标签一般就是使用ajax来访问了,当然还有其他的,即有其他的类似ajax的底层源码处理,但是ajax我们使用的最多,所以这里说明ajax),以及文件的处理,其中文件的处理我会编写几个工具类来给你进行使用(当然,可能也只是一个方法,或者也不会进行提取编写,所以了解即可),并且文件也会通过ajax来进行处理的,这里先进行注意
由于有很多的细节,现在我决定将原生的servlet和现在的mvc框架一起来测试,来使得更加的理解这些,首先有多种情况,这里我决定来测试测试:
这里一般我们测试如下:
首先我们会使用原生ajax以及框架ajax的请求来处理,并且也会使用一些固定标签来处理某些文件的操作(包括ajax,即js来操作,当然,这里也会分成两个操作,即获取和提交,在后面会说明的)
上面是前端需要注意的操作,而后端需要注意的则是:他们的后台完成分别又由原生servlet和mvc来进行处理
所以通过上面的操作我们应该存在如下:
get请求(操作加上参数,这个处理完后,那么后面的参数处理无论是get还是post都能明白了,所以也基本只会测试一次),get操作的请求头,get的单文件,多文件,以及文件夹处理
post请求,post操作的请求头,post的单文件,多文件,以及文件夹处理
这10种情况分别由这四种组合分别处理,原生ajax以及原生servlet,原生ajax以及mvc,框架ajax以及原生servlet,框架ajax以及mvc
这加起来有40种操作方式,这里都会进行给出,其中会给出通过标签获取和提交,通过标签获取和js提交,通过js获取和标签提交,通过js获取和提交等等对文件的细节处理,由于这个获取提交只要我们操作一次就能明白了,当然,他们可能也会因为浏览器的原因(比如安全,或者说源码)使得不能进行处理,所以在后面说明时,就会进行在过程中只会处理一下,而不会多次处理,或者只是单纯的说明一下
首先是原生的ajax的请求和原生servlet后台的处理(学习并看后面的话,这里建议先学习学习50章博客):
这里我们将对应的10种情况都进行操作出来:
这里我们需要知道,ajax经常改变的有哪些,首先是请求方式,这里我们以get,post为主,因为其他的put和delete本质上也是操作post的参数类型存放的(虽然他们在后端都是一样的,包括get),还有ajax规定的url,以及data等等,当然,他们并不是非常重要,最重要并且没有系统学习的,是请求体和响应体的设置(这里就是大局的了,而不是对数据,这里我们将请求信息以请求体来进行说明,因为大多数参数就是在请求体的,具体自行辨别是整体还是局部),这里需要重要的考虑,这里也会顺便以后台的方式来说明get和post:
由于测试非常多,所以这里我们还是创建一个项目,他用来操作原生ajax以及原生servlet的,由于是原生的,那么我们就操作创建如下(当然,如果你只是创建一个maven,并且没有指定其他的组件,那么一般需要你引入相关的servlet依赖),这里我们就操作maven,那么首先引入依赖:
< packaging> war</ packaging>
< dependencies> < dependency> < groupId> javax.servlet</ groupId> < artifactId> javax.servlet-api</ artifactId> < version> 4.0.1</ version> </ dependency> </ dependencies>
创建的项目如下:
其中GetRequest内容如下(原生servlet,当然,这是相对原生的,因为如果还要底层的话,那么就不是servlet了(所以原生servlet就是这里的代码),那样的话,你可以参照27章博客最后的内容):
package com. test. controller ; import javax. servlet. * ;
import java. io. IOException ;
import java. io. PrintWriter ; public class GetRequest implements Servlet { @Override public void init ( ServletConfig servletConfig) throws ServletException { System . out. println ( "初始化" ) ; } @Override public ServletConfig getServletConfig ( ) { return null ; } @Override public void service ( ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException , IOException { System . out. println ( 1 ) ; servletResponse. setContentType ( "text/html;charset=UTF-8" ) ; PrintWriter writer = servletResponse. getWriter ( ) ; writer. write ( "<h1>" + 11 + "</h1>" ) ; } @Override public String getServletInfo ( ) { return null ; } @Override public void destroy ( ) { System . out. println ( "正在销毁中" ) ; }
}
web.xml如下:
<?xml version="1.0" encoding="UTF-8"?>
< web-app xmlns = " http://xmlns.jcp.org/xml/ns/javaee" xmlns: xsi= " http://www.w3.org/2001/XMLSchema-instance" xsi: schemaLocation= " http://xmlns.jcp.org/xml/ns/javaeehttp://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version = " 4.0" > < servlet> < servlet-name> GetRequest</ servlet-name> < servlet-class> com.test.controller.GetRequest</ servlet-class> </ servlet> < servlet-mapping> < servlet-name> GetRequest</ servlet-name> < url-pattern> /get</ url-pattern> </ servlet-mapping>
</ web-app>
index.jsp(谁说一定要分离的,我就使用jsp,因为只是测试而已,具体的代码不还是与html一样的):
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head><title>Title</title>
</head>
<body>
1
</body>
</html>
在启动后,在项目路径后面加上get,访问后,若出现对应的11(h1标签形式的),说明我们创建项目并测试成功
这下我们环境操作完毕,现在来进行原生ajax的处理:
这里我们修改index.jsp,或者加上如下代码:
<input type="button" οnclick="run1()" value="原生js实现Ajax的get请求"><br>
<script>function run1(){let x;//判断浏览器类型,并创建核心对象if(window.XMLHttpRequest){x =new XMLHttpRequest();}else{x = new ActiveXObject("Microsoft.XMLHTTP");}//建立连接get方式,资源路径,是否异步//GET大小写忽略x.open("GET","get",false); //关于请求路径是否加上/,可以选择参照67章博客中对/的说明(可以全局搜索"如action或者超链接(或者类似的)等等,不使用时,一般就是相",action与这里是一样的,或者说类似的),这里是不加上/的//提交请求,这里可以选择修改上面open的值,再来访问x.send();//等待响应,获取上面指定地址的响应结果x.onreadystatechange=function (){//判断提交状态和响应状态码是否都ok//请求已完成,且响应已就绪,200响应状态完成if(x.readyState == 4 && x.status == 200){console.log("第一个")//将响应的信息进行赋值let text =x.responseText;console.log(text)}}console.log("下一个")x.open("geT","get?username=tom",false);x.send();x.onreadystatechange=function (){if(x.readyState == 4 && x.status == 200){console.log("第二个")let text =x.responseText;console.log(text)}}}
</script>
启动,点击访问,出现如下(前端的,前端的出现了,那么后端的就不用看了,所以这里就不给出了,后续也是如此):
为什么"第二个"没有显示,这是因为当一个回调被设置后,就不能继续被设置了,并且在回调过程中,自然解除了同步(false),所以根据顺序操作,一般回调比较慢,所以是"下一个"先打印,这种细节是没有必要的,所以我们再次的修改,等待是send中进行的,这个要注意,所以我们修改如下:
function run1 ( ) { let x; if ( window. XMLHttpRequest) { x = new XMLHttpRequest ( ) ; } else { x = new ActiveXObject ( "Microsoft.XMLHTTP" ) ; } x. open ( "GET" , "get" , false ) ; x. send ( ) ; let text = x. responseText; console. log ( text) }
启动测试,查看打印信息(前端的),显示如下:
现在我们给他加上参数,分别需要测试如下几种情况(这个只需要测试一次就行了,前面也说明了这样的情况):
1 :x. open ( "GET" , "get?" , false ) ;
2 :x. open ( "GET" , "get??" , false ) ;
3 :x. open ( "GET" , "get?&" , false ) ;
4 :x. open ( "GET" , "get?&=" , false ) ;
5 :x. open ( "GET" , "get?=" , false ) ;
6 :x. open ( "GET" , "get? =1" , false ) ;
7 :x. open ( "GET" , "get?= " , false ) ;
8 :x. open ( "GET" , "get?=&" , false ) ;
9 :x. open ( "GET" , "get?name" , false ) ;
10 :x. open ( "GET" , "get?name=" , false ) ;
11 :x. open ( "GET" , "get?name= " , false ) ;
12 :x. open ( "GET" , "get?name&" , false ) ;
13 :x. open ( "GET" , "get?name&&" , false ) ;
14 :x. open ( "GET" , "get?name=1" , false ) ;
15 :x. open ( "GET" , "get?name=1?" , false ) ;
16 :x. open ( "GET" , "get?name=1&" , false ) ;
17 :x. open ( "GET" , "get?name=1&pass" , false ) ;
18 :x. open ( "GET" , "get?name=1&&pass" , false ) ;
19 :x. open ( "GET" , "get?nae=1&&pass" , false ) ;
20 :x. open ( "GET" , "get&" , false ) ;
21 :x. open ( "GET" , "get&name&" , false ) ;
22 :x. open ( "GET" , "get?name=1&name=2" , false ) ;
这22种情况,是在前端进行处理的,因为后端只是找参数得值,那么后端则进行补充:
@Override public void service ( ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException , IOException { String name = servletRequest. getParameter ( "name" ) ; String pass = servletRequest. getParameter ( "pass" ) ; String p = servletRequest. getParameter ( "?" ) ; String a = servletRequest. getParameter ( "&" ) ; String b = servletRequest. getParameter ( "" ) ; String c = servletRequest. getParameter ( " " ) ; System . out. println ( name + "," + pass + "," + p + "," + a + "," + b + "," + c) ; System . out. println ( 1 ) ; servletResponse. setContentType ( "text/html;charset=UTF-8" ) ; PrintWriter writer = servletResponse. getWriter ( ) ; writer. write ( "<h1>" + 11 + "</h1>" ) ; }
经过测试,上面的22种情况分别是:
总结:
我们也可以通过标签来处理,在学习前端表单时,应该会非常清除,实际上form的请求方式也只是将表单内容变成对应的请求参数的而已,所以这里我们测试一下即可(修改index.jsp):
<form action="get" method="get"><input type="text" name="name"><input type="text" name="pass"><input type="submit" value="提交">
</form>
<!--
输入:1,2:get?name=1&pass=12
输入:1,不输入:get?name=1&pass=
输入:不输入,不输入:get?name=&pass=
很明显去掉自然也就不存在了
都去掉:get?,即get?是基础
-->
我们继续操作这个get请求,这个时候我们可以加上一些请求头,比如:
x. setRequestHeader ( "Content-Type" , "application/json" ) ;
加上请求头自然需要在send发送请求之前加上的,然后执行访问,因为send是最终处理,这时发现结果是一样的,那么他有什么用呢,实际上get请求在操作过程中,并不会使用他,或者说,只会使用一些请求头(你就访问百度https://www.baidu.com/,查看网络中,对应的请求,看看是否有请求标头就知道了,get也是有的,因为一个完整的请求是基本必须存在其请求信息和响应信息),但是Content-Type是忽略的(实际上是设置的,之所以忽略是因为几乎用不上这些,或者给出的api中或多或少可能会判断一下(比如后面的multi-part的对应的错误),因为其url的保存基本只能由默认请求处理,所以这在一些版本中,可能并不会进行显示,所以说成是忽略也是可以的),为什么这里要说明这个,在后面你就会知道了
现在我们操作了get请求和get操作的请求头,那么现在来完成get的单文件处理,在这之前有个问题,get存在单文件的处理吗,答:并没有,或者很少,为什么说很少,这是因为get数据的保存是在url中的,url中加入的数据是有限的,所以如果是小文件的话(或者说某些符合字符的文件),get是可以完成的,现在我们来进行测试:
在真正测试get请求文件之前,首先我们要来确认get请求文件的流程思路是怎么来的,或者为什么只能将文件数据放在url中,现在来让你好好的理解get请求文件为什么要这样做,以及如果不这样做会出现什么:
在测试之前,我们必须要明白,get的作用就是在url中进行添加,而post则不是,他们是不同的操作方式,自然其对应的需求的请求也是不同的
在测试之前,我们还需要明白一件事情,前端要上传文件,一般就是上传二进制的代码,然后操作一些文件信息,无论你前端如何变化,本质也是如此,所以只需要掌握了拿取二进制信息发生的流程,那么无论什么情况下,文件上传的操作你就不会出现问题了,这里建议参考这个视频:https://v.douyin.com/iJ8YRXGw/,这个视频我感觉还是很好的,虽然没有从0到有的代码的说明
那么现在有一个问题,由于文件是从磁盘(文件系统)里面拿取的,我们并不能很好的手动的写上这些数据到url中,特别是图片等等,那么就需要一些操作来读取,比如通过标签,或者通过js手动的拿取等等(因为最终他们的基础代码是一致的),通过标签获取,一般是如下的操作(这里我们完全改变之前的index.jsp了,且记得放在body标签里面):
<input type="file" id="fileInput" />
<button οnclick="uploadFile()">上传</button><script>function uploadFile() {let fileInput = document.getElementById('fileInput');let file = fileInput.files[0]; // 获取文件选择框中的文件sendFileUsingGET(file); // 调用发送文件的函数}function sendFileUsingGET(file) {let xhr = new XMLHttpRequest();//原生中需要这个对象,或者说他也是相对原生的(因为你也会看XMLHttpRequest对象里面的源码的,这些源码相当于操作系统的接口一样,是提供的)//所以说,一般情况下,原生的意思代表是整个语言操作编写时自带的一些API或者固定语法意思,否则就应该说成是底层原理(考虑操作系统接口的处理,这不是单纯程序员所要理解和操作的东西了),原生也会称为底层源码let formData = new FormData();// 将文件添加到FormData,相当于表单操作的name和对应的值了formData.append('file', file);// 构建GET请求URL,将FormData中的数据作为查询参数let url = 'get?' + new URLSearchParams(formData).toString();console.log(new URLSearchParams(formData).toString()) //一般是这样的形式:file=%5Bobject+File%5D// 打开并发送GET请求xhr.open('GET', url, false);xhr.send();//后面的就不操作打印了,因为只是打印返回值而已,没有必要}
</script>
上面我们并没有通过标签提交,而是通过标签获取后,通过js提交,等下我们会说明其他三种情况,现在我们来看如下:
首先我们需要注意:基础代码即底层原理(上面有注释说明),基本代码即底层源码
在操作文件上传时,需要说明他为什么需要一些固定的操作:
在这里需要说明一下FormData对象,FormData是一个内置的 JavaScript API(所以可以将他看成原生),它用于创建关于文件的表单数据(前面说过,操作文件我们会使用标签的方式,其实标签的方式一般就是表单,而表单的基础代码就是这个的基础代码(html和css可以看成先变成js,然后变成基础代码,或者直接由其独有的与js的不同解释的器进行变成对应的基础代码),所以简单来说该对象可以认为是文件操作的底层源码(注意是文件,在后面会说明为什么),当然,这样的源码还有很多,但是他们的基础代码都是一样的),并将其用于 AJAX 请求中,一般情况下,我们使用这个对象即可,因为既然基础代码是一样的,那么其他的类你学会也没有什么帮助,只是一个换了名字的对象而已,它允许你构建以 multipart/form-data格式编码的数据(也就是表单对文件的处理),这种数据格式通常用于发送文件和其他类型的二进制数据,或者说,可以发送二进制的数据,所以简单来说他可以用来保存二进制并进行发送出去(到请求头),而不是只保存具体数据再保存,但是这样的对multipart/form-data的解释是无力的,为什么:
实际上任何数据都是二进制,只是再查看的时候会以当前查看的编码进行解析查看而已,所以这里需要注意一个地方,即为什么我们要用multipart/form-data来发送文件,解释如下:
因为在 HTTP 协议中,数据的传输有多种编码方式,而multipart/form-data是专门用于上传文件的一种编码类型
其中,HTTP 协议规定了多种数据传输的编码类型,常见的有application/x-www-form-urlencoded和multipart/form-data,这两种编码类型都是 POST 请求中用于向服务器传递数据的方式,而这里我们在尝试get,具体结果请看后面测试的结果
application/x-www-form-urlencoded是默认的表单数据编码类型(他是用来提交的编码,注意是表单的,大多数比较原始的需要加上他来操作,否则可能是什么都没有设置,更加的,一般默认的可能只是一个纯文本,也就是text/plain;charset=UTF-8,这才是底层的默认处理,一般来说post才会考虑默认加上该Content-Type(没有加,那么没有显示,只是post不会再浏览器显示而已,内部是处理的),而get没有,这是体现在浏览器的,了解即可),在这种编码类型下(基本上没有变化),表单数据会被转换成 URL 查询参数的形式(如果是post则会同样以对应的形式放在post对应的参数域中(这里的域对应与get来说,基本只是存在表面的说明(即没有更加深入说明),建议全局搜索这个"实际上存在一个空间,get和post的数据都是放在这个空间的",可以让你更加的理解post和get的存在方式),只是可能并不存在url那样的连贯,如&),例如get请求的key1=value1&key2=value2,这种编码方式适合传输普通的键值对数据,但对于文件数据,在不考虑其他判断的情况下,由于文件内容可能包含特殊字符(如&,=,特别的可能还存在&&&,那么在后端得到的数据就算结合,也可能得不到完整的文件,因为&&&自然会省略一个&),那么一个文件可能会变成多个键值对,并且由于特别的&&&的存在,所以文件不适合直接编码在 URL中,否则的话,可能导致读取的文件信息错误(不能进行没有完整的处理,因为可能也存在&&&),所以浏览器提交或者后端接收时,他们可能都会进行判断是什么类型以及对应的编码类型,来使得是否报错,最后不让你进行传递,一般在前端和后端基本都会进行判断的(底层源码中的处理,是底层源码的源码,因为对于底层原理应该是最底层的),除非前端是自行写的原生js或者后端也是自行写的原生servlet,这也是我们需要改变类型的一个原因,当然,就算是自己写的原生js或者原生servlet,前端和后端可能底层源码也判断了,但是这种情况我们看后面就知道了
multipart/form-data:这种编码类型用于传输二进制数据(如果是变量赋值的话,保存的也是编码后的二进制,这里在后面会说明的),包括文件数据,在这种编码类型下,表单数据会被分割成多个部分,每个部分都包含一个 Content-Disposition头和其他相关信息,以及对应的数据内容,其中,文件数据会以二进制的形式编码,而不会被转换成 URL 查询参数,相当于完整存放了,自然不会出现数据丢失的错误(get的&&&是会造成丢失的),这也导致get一般并不能操作他,也同样的由于get主要是操作url的,也导致了并不能很好的操作请求头,使得忽略一些东西,或者说,避免无意义的赋值(可能get和post都可以操作对应的域,只是分工导致并不会操作对方,或者只能操作某些东西),所以get可能会判断忽略请求头
对于后端代码修改(多余的注释删掉了):
package com. test. controller ; import javax. servlet. * ;
import javax. servlet. http. HttpServletRequest ;
import java. io. IOException ;
import java. io. PrintWriter ; public class GetRequest implements Servlet { @Override public void init ( ServletConfig servletConfig) throws ServletException { System . out. println ( "初始化" ) ; } @Override public ServletConfig getServletConfig ( ) { return null ; } @Override public void service ( ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException , IOException { System . out. println ( 1 ) ; System . out. println ( servletRequest) ; HttpServletRequest h = ( HttpServletRequest ) servletRequest; System . out. println ( h. getMethod ( ) ) ; servletResponse. setContentType ( "text/html;charset=UTF-8" ) ; PrintWriter writer = servletResponse. getWriter ( ) ; writer. write ( "<h1>" + 11 + "</h1>" ) ; } @Override public String getServletInfo ( ) { return null ; } @Override public void destroy ( ) { System . out. println ( "正在销毁中" ) ; }
}
现在我们启动,访问一下看看后端,可以发现打印的请求方式是get,这个时候我们后端并没有接收文件的信息,这需要特别的处理,即后端文件的接收,后端怎么接收文件的信息呢,在这之前,我们需要先修改index.jsp,因为在let url = ‘get?’ + new URLSearchParams(formData).toString();中,后面只是作为值,也就是说,像这样的形式file=%5Bobject+File%5D,只会得到file后面的字符串值,所以我们需要修改,修改如下:
function sendFileUsingGET ( file ) { let xhr = new XMLHttpRequest ( ) ; let formData = new FormData ( ) ; formData. append ( 'file' , file) ; xhr. open ( 'GET' , "get" , false ) ; xhr. send ( formData) ; }
我们继续修改service方法:
@Override public void service ( ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException , IOException { HttpServletRequest h = ( HttpServletRequest ) servletRequest; if ( h. getMethod ( ) . equals ( "GET" ) ) { System . out. println ( "GET" ) ; } if ( h. getMethod ( ) . equals ( "POST" ) ) { System . out. println ( "POST" ) ; } try { Part filePart = h. getPart ( "file" ) ; String fileName = filePart. getSubmittedFileName ( ) ; System . out. println ( "文件名:" + fileName) ; } catch ( Exception e) { e. printStackTrace ( ) ; System . out. println ( 2 ) ; } System . out. println ( 1 ) ; }
进行访问(记得点击上传文件,当然,你也可以不上传,甚至没有这个文件参数),可以发现报错了,并打印信息出现了两个GET,且没有返回值返回为什么,我们首先看错误:
这个错误的出现是这Part filePart = h.getPart(“file”);一行的原因,这个时候,如果你修改成了POST请求,那么他的错误也是这个地方,即这个错误与get和post无关,那么这个错误怎么解决,我们可以分析这个错误,没有提供multi-part配置,那么这个配置是什么:
Multi-Part 请求是一种在 HTTP 协议中用于传输二进制数据和文件数据的请求类型,在 Multi-Part 请求中,请求信息的数据被分割成多个部分(Part),每个部分都有自己的头部信息,以及对应的数据内容,这种格式允许将不同类型的数据(比如文本、二进制数据、文件等)同时包含在一个 HTTP 请求中,通常情况下,我们在前端上传文件或者提交包含文件的表单数据时,后端会接收到一个 Multi-Part 请求,Multi-Part 请求的内容类型(Content-Type)通常为 multipart/form-data,用于标识请求信息中的数据是 Multi-Part 格式,需要配置 Multi-Part 是因为在后端处理 Multi-Part 请求时,需要对请求信息进行解析,以提取其中的数据,并正确处理文件上传等操作,对于某些后端框架或服务或者服务器本身,它们可能默认不支持解析 Multi-Part 请求,因此需要进行相应的配置,告知后端如何处理 Multi-Part 数据,那么很明显,由于前面默认的编码格式是application/x-www-form-urlencoded(前面我们说明了这个操作文件的坏处,而引出了前后端会判断的处理,这种判断是建立在默认的情况下,所以也给出了我们可以通过其他方式来使得get进行处理文件),所以这里才会报错,也就是说,后端原生的也操作了判断,那么前端原生有没有判断,答:没有,但是由于前端必须设置multipart/form-data,他是一个请求头信息的,而get对他(Content-Type)是忽略的,也就造成了get并不能操作这个,而get不能操作这个,后端也判断报错,所以导致前端get不能操作文件的上传(即这条路被堵死了),但是这里我说了,我们需要进行操作get文件,所以我们应该这样的处理,修改index.jsp,在修改之前,我们应该要考虑一个问题,既然get操作不了对应的编码,且只能操作url,那么如果我们将对应的二进制文件放入到url中即可,并且通过编码来解决原来使得默认报错的问题就行了,最终通过这个编码提交,然后后端接收解码(这个时候是根据默认处理的编码来解决的,因为前面的编码只是对文件的一个保存而已,即两种,一种是上层,另外一种下层是交互后端的,这个几乎不会改变)进行接收文件就行,而不用操作总体的multipart/form-data的解码了,简单来说,无论是get还是post都是操作文件的解码和编码而已,只是其中一个是multipart/form-data另外一个是我们自定义的加上默认的(而get之所以需要定义还是因为对应编码的并不存在,根本原因是get是只能操作url,导致并不会操作对应的请求头,而使得他自身并不能进行其他操作,即需要我们手动的处理),其中这个url得到的二进制的数据自然是操作了编码的,为什么,实际上大多数语言中,其变量并不能直接的指向原始二进制,必然是通过一些编码而进行间接指向,这是因为二进制的处理过于底层,也只能在底层中进行计算,而不能在上层计算,所以说,你得到文件信息,也是通过对二进制进行编码来得到的,那么根据这个考虑,我们看如下修改的index.jsp(我们也自然不会引入一些js,都是使用自带的js的,即原生的):
<input type="file" id="fileInput"/>
<button οnclick="uploadFile()">上传</button><script>function uploadFile() {//前面没有说明这些,是因为他们是不会成功的,其实这个代表得到文件对应的信息,由于存在文件和多文件,所以下面才会存在数组的形式的let fileInput = document.getElementById('fileInput');console.log(fileInput)let file = fileInput.files[0]; // 获取文件选择框中的文件,由于只是单文件,所以只需要拿取下标为0的值即可,因为也只会是下标为0开始,因为只有一个console.log(file)//读取文件内容并进行 Base64 编码(注意是文件,也就是包括图片或者视频都可以),即将文件信息从二进制进行该编码处理,该编码对文件的操作使用的比较多,所以这里使用这个编码,也就是说,虽然我们不能使用get的multipart/form-data编码方式,但是我们可以直接来解决这个特殊字符的问题,如&,因为后端之所以默认判断报错,无非就是防止这种问题,即不能让你单纯的操作文件,但是他的判断一直存在,所以我们需要进行跳过,即按照其他方式的数据进行传递,也就只能将文件信息放在url中了,所以通过上面的说,也就是为什么get也只能将数据放在url的原因,根本原因还是get只能操作url,是否感觉上面的测试是多余的,但是也不要忘记了,前面也说了"以及如果不这样做会出现什么"这种情况,所以我也应该进行测试出来//一般使用该编码的他的好处可以降低数据大小的存在,那么变量就不会占用很多内存了,否则可能使用默认的变量给变量会造成比较大的内存占用(文件中的处理是操作系统的处理,而编码只是一种表现形式的指向而已)//创建了一个新的 FileReader 对象,用于读取文件内容,FileReader 是一个可以异步读取文件的 API(即你可以执行其他的任务,并且他是原生中js读取文件的操作,即也是原生的,我可没有引入一些js,要知道原生js的api可是有很多的,他们一般也是使用这些原生js组成的而已,比如vue)let reader = new FileReader();//设置了 FileReader 的 onload 事件处理函数,当文件读取完成时,该函数将会被触发reader.onload = function (e) {/*你可以将该e的数据复制下来(操作打印即可,即console.log(e);),然后记得两边没有分号",然后在url中粘贴,若出现你上传的图片,代表是真的获取了,他就是编码后的数据*/let fileData = e.target.result.split(',')[1]; // 获取Base64编码后的内容,因为这个时候他才算是真正的编码后面的数据,而前面的前缀只是一个标识而已,而非数据,所以通常并不影响后端的解码处理sendFileUsingGET(fileData); // 调用发送文件的函数};//开始读取指定的文件内容,并以 Base64 编码的形式返回结果,首先判断是否传递了文件if (file != undefined) {reader.readAsDataURL(file); //对应的异步出现一般体现于这里,并且他内部操作了编码,然后变成上面的e参数,同样的reader.onload 也是异步的处理,可以说reader基本都是异步的处理这是保证再处理多个文件时不会浪费很多时间,这也是没有办法的事情}}function sendFileUsingGET(fileData) {let xhr = new XMLHttpRequest();// 添加文件信息到 URL 中,这里之所以需要encodeURIComponent方法,是因为与默认的后端需要对应的编码一样,防止对应一些字符的处理,如&等等,由于get必然会在url中进行操作,那么如果不这样处理的话,可能会造成数据丢失let url = "get?file=" + encodeURIComponent(fileData);console.log(encodeURIComponent(fileData))// 打开并发送 GET 请求xhr.open('GET', url, false);xhr.send();}
</script>
前端代码编写完毕,可以发现,get和post一个体现在url,一个体现在域中,在这种情况下,get是存在局限的,而post则没有,前面的测试则多是体现在get的局限,也就是默认报错的原因,因为post可以解决,而get不能,由于get并不能操作对应的请求头(url导致,实际上是分工),所以导致get在某些操作情况下,需要进行手动处理,post可以设置来处理,而get不能,特别是文件的处理,即文件需要我们手动的处理,现在我们来从后端接收该文件信息,然后保存到本地,所以post是可以完成get的功能(这里特别的需要注意,在后面也会提到),且可以更好的完成其他的功能,但是也由于域的存在,导致可能会比get慢,这里需要考虑很多的问题了
至此get的测试我们说明完毕,即get的url自身的特性导致get的文件上传需要我们手动处理
简单来说,就是认为get有两条路,一个是与post一样的设置编码,另外一个是自身的url,很明显,设置编码的不行,那么自然只能操作自身url了,而post确存在这两种形式,只是post的url的形式在一个参数域中而已(虽然其两种都在该域中)
同样的由于分工不同,操作get只针对url,而post只针对域,从而造成方案的不同,并且很明显,大多数后端代码对文件的处理是操作multipart/form-data的,即是默认的判断处理,从而建议我们使用post操作文件,这也同样是在考虑url添加数据少的情况下(后面也会说明一下),也验证了分工不同的最终后续的影响,分工不同,导致默认不同(存在判断),导致方案不同,所以get在没有考虑对url的自定义编码的情况下,报错是正常的,因为你并不是按照正常的流程来处理,而使用了自身缺点,并且url还不能很好的分开数据或者需要更多操作来进行处理(如自定义的编码,使得还要编码一次),所以这也是建议get不操作文件的一个原因(特别如操作多文件,因为每个文件都需要进行编码处理来使得数据是正常的,甚至还有考虑大小的限制问题)
也就是说,若不考虑其他的因素,那么get和post其实是完全一样的,只是由于存放形式的不同导致有所限制,所以不考虑这些限制,get也可以完成所有post的操作,而这些限制的出现,只不过是人为规定,让我们可以方便的选择使用那一种,所以才会出现get和post,或者其他的请求形式
后端代码如下:
@Override public void service ( ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException , IOException { HttpServletRequest h = ( HttpServletRequest ) servletRequest; if ( h. getMethod ( ) . equals ( "GET" ) ) { System . out. println ( "GET" ) ; } if ( h. getMethod ( ) . equals ( "POST" ) ) { System . out. println ( "POST" ) ; } String file = servletRequest. getParameter ( "file" ) ; if ( file != null ) { byte [ ] decodedBytes = Base64 . getDecoder ( ) . decode ( file) ; FileOutputStream fileWriter = new FileOutputStream ( "F:/in.jpg" ) ; fileWriter. write ( decodedBytes) ; } else { } }
我们找一个文件,上传,然后你可能会出现如下的错误(也是不建议使用url或者说get操作文件的情况):
一般情况下,get的上限是与浏览器相关(在后端是与post一样的在同一个地方操作的(如前面的service方法),只是因为浏览器前的分工导致后端某些处理或者前端的处理发生一些变化),比如不同的浏览器对GET请求的URL长度有不同的限制,这些限制通常在2KB到8KB之间,例如,对于Internet Explorer,URL长度的限制可能较低,而对于现代的浏览器如Chrome、Firefox和Edge,通常会更大,所以现在我们随便创建一个txt文件,加上"1"这个数字,然后将后端对应的FileOutputStream fileWriter = new FileOutputStream(“F:/in.jpg”);的后缀jpg修改成txt,继续进行测试,这个时候可以发现,上传成功了,并且对应的F:/in.txt的数据与上传的一致,至此我们的get上传文件操作完毕
那么为什么浏览器要限制url的长度呢(后端出现报错是判断请求浏览器类型而进行处理的,前端还是发送过去的),实际上是提供传输速率的,这里了解即可,因为这是规定
现在我们改造后端和前端,将后端代码变成一个方法:
package com. test. controller ; import javax. servlet. * ;
import javax. servlet. http. HttpServletRequest ;
import java. io. * ;
import java. util. Base64 ; public class GetRequest implements Servlet { @Override public void init ( ServletConfig servletConfig) throws ServletException { System . out. println ( "初始化" ) ; } @Override public ServletConfig getServletConfig ( ) { return null ; } @Override public void service ( ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException , IOException { HttpServletRequest h = ( HttpServletRequest ) servletRequest; if ( h. getMethod ( ) . equals ( "GET" ) ) { System . out. println ( "GET" ) ; getFile ( h, servletRequest, servletResponse) ; } if ( h. getMethod ( ) . equals ( "POST" ) ) { System . out. println ( "POST" ) ; } } private static void getFile ( HttpServletRequest h, ServletRequest servletRequest, ServletResponse servletResponse) { servletResponse. setContentType ( "text/html;charset=UTF-8" ) ; try { PrintWriter writer = servletResponse. getWriter ( ) ; String file = servletRequest. getParameter ( "file" ) ; String filename = servletRequest. getParameter ( "filename" ) ; if ( file != null ) { String [ ] split = filename. split ( "\\." ) ; byte [ ] decodedBytes = Base64 . getDecoder ( ) . decode ( file) ; FileOutputStream fileWriter = new FileOutputStream ( "F:/in." + split[ 1 ] ) ; fileWriter. write ( decodedBytes) ; writer. write ( "<h1>" + "上传文件成功" + "</h1>" ) ; return ; } writer. write ( "<h1>" + "上传的文件为空" + "</h1>" ) ; return ; } catch ( Exception e) { e. printStackTrace ( ) ; } } @Override public String getServletInfo ( ) { return null ; } @Override public void destroy ( ) { System . out. println ( "正在销毁中" ) ; }
}
前端:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head><title>Title</title>
</head>
<body>
<input type="file" id="fileInput"/>
<button οnclick="uploadFile()">上传</button><script>function uploadFile() {let fileInput = document.getElementById('fileInput');let file = fileInput.files[0];let reader = new FileReader();reader.onload = function (e) {console.log(e);let fileData = e.target.result.split(',')[1];sendFileUsingGET(fileData, file.name);};if (file != undefined) {reader.readAsDataURL(file);}}function sendFileUsingGET(fileData, name) {let xhr = new XMLHttpRequest();let url = "file?file=" + encodeURIComponent(fileData) + "&filename=" + name;xhr.open('GET', url, false);xhr.send();console.log(xhr.responseText);}
</script>
</body>
</html>
上面我们操作了一下后缀,我们看看即可
一般情况下,你选择的文件可能与之前的文件有所联系,可能是日期,可能是唯一内容或者id,也就是说,如果你选择后,修改文件系统的对应文件,那么可能上传不了,一般存在reader.onload里面(即原来的信息,具体可以认为一个文件里面存在是否改变的信息,即会再次的进行处理,从getElementById获取,虽然他也可以获取文本的,但是内容可能还操作了对应指向的类型方法,所以了解即可),相当于操作了return;,即会使得当前方法直接停止不操作了(一般并不包括里面的异步处理,所以只是停止当前线程,异步是新开线程的),但也只是对该文件而言,如果是多个文件,那么没有改变的就不会操作return;,也就不会结束调用他的方法,return;可不是程序结束的意思,所以其他的还是会执行的
但是这里大多数人会存在疑惑,js是单线程的,为什么存在新开线程,这里就要说明一个问题,你认为页面渲染主要只由js来处理吗,实际上是浏览器来处理的,所以js只是一个重要的组件而已,而非全部,那么其他的线程可能并不是js来新开的,可以认为是浏览器,或者其他操作系统来新开的,就如浏览器存在js组件,自然会与他交互,浏览器新开一个线程与你交互有问题吗,没有问题,所以这也是js也是单线程,但是确存在异步根本原因,但是随着时间的发展,js可能也会诞生时间片的概念,使得js在单线程的情况下与java一样的进行切片处理多个线程的,这里了解即可
改造web.xml:
<?xml version="1.0" encoding="UTF-8"?>
< web-app xmlns = " http://xmlns.jcp.org/xml/ns/javaee" xmlns: xsi= " http://www.w3.org/2001/XMLSchema-instance" xsi: schemaLocation= " http://xmlns.jcp.org/xml/ns/javaeehttp://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version = " 4.0" > < servlet> < servlet-name> GetRequest</ servlet-name> < servlet-class> com.test.controller.GetRequest</ servlet-class> </ servlet> < servlet-mapping> < servlet-name> GetRequest</ servlet-name> < url-pattern> /file</ url-pattern> </ servlet-mapping>
</ web-app>
继续测试吧,上面我们操作了get的单文件的处理,并且是通过标签获取,js提交,现在我们来完成js获取,js提交,那么js可以完成获取文件吗,实际上js并不能获取我们系统的文件信息,这是防止浏览器访问本地文件的安全策略,特别的,如果对方网页中的js是删除你文件系统的所有文件,你怎么防止你,也就是说,只能通过上面的表单交互来进行文件的上传处理,也是浏览器唯一或者少的与文件交互的处理,并且也是需要用户与他进行处理的,但是我们可以选择不手动点击具体上传文件的按钮,操作如下:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head><title>Title</title>
</head>
<body>
<button id="openFileButton">打开文件选择器</button>
<script>let openFileButton = document.getElementById('openFileButton');openFileButton.addEventListener('click', () => {let fileInput = document.createElement('input');fileInput.type = 'file';fileInput.click();fileInput.addEventListener('change', (event) => {let file = event.target.files[0];let reader = new FileReader();reader.onload = function (e) {console.log(e);let fileData = e.target.result.split(',')[1];sendFileUsingGET(fileData, file.name);};if (file != undefined) {reader.readAsDataURL(file);}});});function sendFileUsingGET(fileData, name) {let xhr = new XMLHttpRequest();let url = "file?file=" + encodeURIComponent(fileData) + "&filename=" + name;xhr.open('GET', url, false);xhr.send();console.log(xhr.responseText);}
</script>
</body>
</html>
但是这里为什么还要加上标签,实际上浏览器并不允许单纯的没有与用户交互过的自动处理,所以如果你去掉了这里的用户点击的交互,那么后面的fileInput.click();不会进行
至此js获取和js提交也可以认为是完成了,那么js获取和标签提交呢,实际上更加的复杂,并且标签的获取和提交也是如此(但是这个标签获取和提交只是针对get来说是比较的复杂,而对post来说,甚至由于浏览器存在自带的处理,所以导致他可能是比标签获取,js提交还要好的,特别是多文件的处理,以后会说明的(后面既然会说明,所以这里就不进行提示再哪个地方了,按照顺序看下去即可,因为这样的话语还有很多的)),那么是为什么呢,这都是需要满足其标签以及js安全考虑的,所以如果需要进行改变,那么首先就是修改浏览器源码,否则是无能为力的,这里给出这些说明是为了让你知道标签获取,js提交是最好的处理方式,也是最安全的方式,即对应的三种我们说明完毕,这具体的实现那么等你解决浏览器源码后去了,关于js的获取,那么需要解决浏览器js可以获取文件系统的安全问题,而标签提交,则需要修改源码对get的处理,而不是默认按照名称(而不是内容)加在url中,所以说我们也只是在浏览器允许的情况下进行的处理,那么文件上传本质上也是这样的,所以你获取不了什么参数或者可以获取什么参数都是浏览器造成的,当然他通常也会有原因,但是并不完美而已,就如get没有一个好的方法变成post(虽然并没有什么必要)
至此,我们四种情况说明完毕,现在将前面的坑都填完了,接下来是正事,也就是get的多文件上传
get的多文件上传与单文件上传基本的类似的,但是还是有一点,前面的操作一般只能选择一个文件,也是浏览器的限制,前面我们说过了"我们也只是在浏览器允许的情况下进行的处理",所以我们需要进行特别的改变,我们继续修改index.jsp文件(上面的基本都是这个):
但是在修改之前,首先需要考虑一个事情,是前端访问一次后端进行多文件的处理,还是将多个文件分开处理呢,这里我就不选择一个了,而是都进行考虑,首先是多个文件统一处理,但是这里就需要考虑很多问题了,虽然比较复杂,但是确只需要一次的访问即可,但在一定程度上会占用url,如果是post,我们建议统一放入,post也的确是希望这样处理的,当然这是后面需要考虑的了,现在我们来完成get的多文件统一处理,但是由于前面我们使用new FileReader();时基本是异步的(前面说明了他是异步的原因,考虑文件传递速率的),所以我们需要一下前端的异步和同步的知识,怎么学习呢,一般情况下,我们学习这三个:async,await,Promise,他们有什么用:
接下来我来举个例子,并在这个例子中进行学习一下规定的api,你可以选择到vscode(这里可以选择看看44章博客)中进行处理:
function fetchUserData ( ) { console. log ( 9 ) return new Promise ( ( resolve, reject ) => { setTimeout ( ( ) => { console. log ( 1 ) resolve ( 5 ) ; } , 1000 ) ; } ) ; } async function getUserInfo ( ) { console. log ( 4 ) let userData = await fetchUserData ( ) ; console. log ( userData) } console. log ( 2 ) getUserInfo ( ) ; console. log ( 3 )
修改一下:
function fetchUserData ( ) { let currentTimeStamp = Date. now ( ) ; console. log ( 9 ) return new Promise ( ( resolve, reject ) => { console. log ( 1 ) resolve ( 5 ) ; } ) ; } async function getUserInfo ( ) { console. log ( 4 ) let userData = await fetchUserData ( ) ; console. log ( userData) } console. log ( 2 ) getUserInfo ( ) ; console. log ( 3 )
上面在很大程度上解释了三个关键字的说明,上面存在同步,异步,等待获取的处理,这三个在异步编程中是最为重要的形式,在java中,一般也需要如此,即同步,异步,以及在其中相应的等待处理,且包括数据的处理,当然等待处理可以认为是数据的处理,只是java更加的好,因为异步编程无非就是同步,异步,以及其中出现的数据处理,所以js和java的异步编程都是非常灵活的,虽然在java中外面一般会称为并发编程,也是归于他异步编程的优秀处理,他比js更加的灵活的,当然,这些规定的知识层面了解即可,深入java的就行,js的也可以顺序深入一下,他们归根揭底是不同的预言,所以请不要找共同点,相似的也最好不要,因为他们本来就不同,只是由于英文造成的某些关键字相同而已,如int,String等等
现在我们使用这些知识来解决前面的多文件统一处理,现在修改index.jsp:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head><title>Title</title>
</head>
<body>
<input type="file" id="fileInput" multiple/> <!--multiple可以选择多个文件,而也有webkitdirectory,可以选择文件夹,都写上,那么可以选择文件夹和文件(并且可以多选,因为multiple),他们都是一个补充,当然补充之间也存在覆盖关系,多文件包含单文件,即覆盖掉,文件夹也是如此,也就是说,加上了webkitdirectory,那么只能操作文件夹了-->
<button οnclick="uploadFile()">上传</button><script>async function uploadFile() {let fileInput = document.getElementById('fileInput');let fileData = [];let fileDatename = [];if (fileInput.files.length === 0) {return;}let readFilePromises = [];for (let y = 0; y < fileInput.files.length; y++) {if (fileInput.files[y] != undefined) {let reader = new FileReader();let readFilePromise = new Promise((resolve, reject) => {reader.onload = function (e) {console.log(e);fileData.push(e.target.result.split(',')[1]);fileDatename.push(fileInput.files[y].name);resolve(); // 标记异步操作完成};reader.readAsDataURL(fileInput.files[y]);});//保存Promise,准备一起处理readFilePromises.push(readFilePromise);}}// 等待所有异步操作完成,all方法必须先执行完毕才会考虑异步,所以后面的并不会进行处理,在前面我们也说明了哦,必须等方法执行完毕去了//而all就是处理所有的Promise的结果后,才会进行完毕,并且是按照添加顺序的,这样就使得我们可以得到正常数据的结果//且由于Promise的特性,必须是resolve()才会进行结束,且保证了顺序,所以无论你是否异步,最终的结果都会正确,因为只需要给最后的处理进行resolve()即可await Promise.all(readFilePromises);//完成后进行处理sendFileUsingGET(fileData, fileDatename);/*上面的操作是否看起来进行了同步呢,实际上从上往下看的确是,但是从内部看只是一个被等待造成的同步而已*/}function sendFileUsingGET(fileData, name) {console.log(fileData)console.log(name)//拿取了总共的数据现在我们来处理一下urllet xhr = new XMLHttpRequest();let url = "file?";for (let h = 0; h < fileData.length; h++) {if (h == fileData.length - 1) {url += "file=" + encodeURIComponent(fileData[h]) + "&filename=" + name[h];continue;}url += "file=" + encodeURIComponent(fileData[h]) + "&filename=" + name[h] + "&";}console.log(url)xhr.open('GET', url, false);xhr.send();console.log(xhr.responseText);}
</script>
</body>
</html>
后端代码如下:
package com. test. controller ; import javax. servlet. * ;
import javax. servlet. http. HttpServletRequest ;
import java. io. * ;
import java. util. Base64 ; public class GetRequest implements Servlet { @Override public void init ( ServletConfig servletConfig) throws ServletException { System . out. println ( "初始化" ) ; } @Override public ServletConfig getServletConfig ( ) { return null ; } @Override public void service ( ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException , IOException { HttpServletRequest h = ( HttpServletRequest ) servletRequest; if ( h. getMethod ( ) . equals ( "GET" ) ) { System . out. println ( "GET" ) ; getFile ( h, servletRequest, servletResponse) ; } if ( h. getMethod ( ) . equals ( "POST" ) ) { System . out. println ( "POST" ) ; } } private static void getFile ( HttpServletRequest h, ServletRequest servletRequest, ServletResponse servletResponse) { servletResponse. setContentType ( "text/html;charset=UTF-8" ) ; try { PrintWriter writer = servletResponse. getWriter ( ) ; String [ ] file = servletRequest. getParameterValues ( "file" ) ; String [ ] filename = servletRequest. getParameterValues ( "filename" ) ; if ( file != null ) { for ( int i = 0 ; i < file. length; i++ ) { String [ ] split = filename[ i] . split ( "\\." ) ; byte [ ] decodedBytes = Base64 . getDecoder ( ) . decode ( file[ i] ) ; FileOutputStream fileWriter = new FileOutputStream ( "F:/" + split[ 0 ] + "." + split[ 1 ] ) ; fileWriter. write ( decodedBytes) ; writer. write ( "<h1>" + "上传一个文件成功" + "</h1>" ) ; } return ; } writer. write ( "<h1>" + "上传的文件为空" + "</h1>" ) ; return ; } catch ( Exception e) { e. printStackTrace ( ) ; } } @Override public String getServletInfo ( ) { return null ; } @Override public void destroy ( ) { System . out. println ( "正在销毁中" ) ; }
}
测试一下吧
当然了,上面前端的三个关键字(姑且认为Promise也是吧)的灵活使用需要更多的练习,所以不用着急,先多看看思路
现在操作分开处理,当然,分开处理建议还是一样使用Promise,因为他是保证顺序的,当然,原来的异步由于执行先后的时间原因大多数基本都会存在好的顺序,但是还是存在风险,因为如果其中一个在某个时候变快(如某个耗费性能的进程在其操作过程中突然的关闭使得变快了),那么顺序就不一致了,所以建议使用Promise,现在我们修改前端和后端代码:
function sendFileUsingGET ( fileData, name) { console. log ( fileData) console. log ( name) let xhr = new XMLHttpRequest ( ) ; let url; for ( let h = 0 ; h < fileData. length; h++ ) { url = "file?file=" + encodeURIComponent ( fileData[ h] ) + "&filename=" + name[ h] ; console. log ( url) xhr. open ( 'GET' , url, false ) ; xhr. send ( ) ; console. log ( xhr. responseText) ; } }
当然,前端只需要改一下上面的即可,而后端可以不用动,因为数组的形式差不多是一样的操作即下标为0的处理,至此,我们的多文件操作完毕,现在我们来完成文件夹的处理:
文件夹的处理其实并不难,他与文件的处理只是多一个路径的处理而已,然而文件的操作也通常没有保存了路径(因为我们并不能通过浏览器访问文件系统,在前面有说明),所以文件夹的特别处理只是在后端根据自定义的路径进行创建文件夹的操作(大多数的操作都是如此),其他的与文件的处理完全一致,并且这里也会留下一个问题,等下进行给出,我们先改变一下前端(也就是index.jsp):
<input type="file" id="fileInput" multiple webkitdirectory/>
<!--虽然是补充,但是补充之间也有覆盖,所以如果选择了文件夹的处理,那么你就只能选择文件夹了-->
<button οnclick="uploadFile()">上传</button>
改变上面即可,因为文件夹处理,相当于自动多选了里面的所有文件,只是对比多文件的选择,我们只需要选择文件夹而已,所以我们直接的访问,看看对应后端的路径里面是否存在对应的文件吧(这里选择统一处理还是分开处理都行,建议分开处理,因为需要考虑get的长度,虽然现在影响不大,这里看你自己了)
至此,我们的文件夹处理也操作完毕,即原生js,原生servlet的get请求(带参数),get请求头处理,get单文件,多文件,文件夹的处理都操作完毕了,但是上面的文件夹处理的时候,说过了留下了一个问题,假设,如果我非要获取文件路径呢,我虽然不能读取或者修改你的文件内容(安全文件,我可以读取如代码的内容,找到破解方式,修改的话,自然也不能修改,这是最危险的地方),路径总能读取吧,经过我的思考,实际上路径的读取好像的确也不能,因为浏览器就是不能,你可能会有疑惑,为什么我们选择文件中弹出的框框有呢,要知道弹出这个框框的处理虽然是浏览器,但并不是浏览器自身打开的,而是浏览器调用文件系统打开的框框,所以他只是引用(可能会有参数改变对方的样式,这是文件系统的扩展内容),而非访问出来的
即get的相关处理都操作完毕,其实你现在只需要改变如下:
< input type = " file" id = " fileInput" multiple webkitdirectory />
即后面的multiple或者webkitdirectory,直接访问即可,这样可以完成,单文件,多文件,文件夹的处理了,这是通用的操作,这很重要哦,现在我们来完成原生js,和原生servlet的post请求,post操作的请求头,post的单文件,多文件,以及文件夹处理
现在我们将前面的处理中的get直接修改成post(你也可以就改变当前前端中的index.jsp的get请求即可,因为前面的都是一样的),看看结果是否相同,并且在后端中也进行相应的代码改变,当然,这里我给你测试完毕了,你就不要测试了,因为没有必要,经过大量的测试,发现,将get修改成post结果都可以处理,并且结果也一模一样,这也就证明了前面说过了"所以post是可以完成get的功能(这里特别的需要注意,在后面也会提到)",其中虽然有时候url是get形式的,但是当请求方式是post时,他会存在get形式的转换,也是post完成get请求的一个重要因素,这也给出我们在写某些函数时,可以反过来进行处理即,将给post的参数变成get形式
然而直接的说明并不好,因为并没有示例代码,所以我还是决定将测试结果写在这里:
首先是post的请求:
我们修改前端代码index.jsp:
<input type="button" οnclick="run1()" value="原生js实现Ajax的post请求"><br>
<script>function run1() {let x;if (window.XMLHttpRequest) {x = new XMLHttpRequest();} else {x = new ActiveXObject("Microsoft.XMLHTTP");}x.open("POST", "post?name=1&pass", false);x.send();let text = x.responseText;console.log(text)}
</script>
后端代码(创建PostRequest类):
package com. test. controller ; import javax. servlet. * ;
import java. io. IOException ;
import java. io. PrintWriter ; public class PostRequest implements Servlet { @Override public void init ( ServletConfig servletConfig) throws ServletException { System . out. println ( "初始化" ) ; } @Override public ServletConfig getServletConfig ( ) { return null ; } @Override public void service ( ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException , IOException { String name = servletRequest. getParameter ( "name" ) ; String pass = servletRequest. getParameter ( "pass" ) ; String p = servletRequest. getParameter ( "?" ) ; String a = servletRequest. getParameter ( "&" ) ; String b = servletRequest. getParameter ( "" ) ; String c = servletRequest. getParameter ( " " ) ; System . out. println ( name + "," + pass + "," + p + "," + a + "," + b + "," + c) ; System . out. println ( 1 ) ; servletResponse. setContentType ( "text/html;charset=UTF-8" ) ; PrintWriter writer = servletResponse. getWriter ( ) ; writer. write ( "<h1>" + 11 + "</h1>" ) ; } @Override public String getServletInfo ( ) { return null ; } @Override public void destroy ( ) { System . out. println ( "正在销毁中" ) ; }
}
web.xml加上如下:
< servlet> < servlet-name> PostRequest</ servlet-name> < servlet-class> com.test.controller.PostRequest</ servlet-class> </ servlet> < servlet-mapping> < servlet-name> PostRequest</ servlet-name> < url-pattern> /post</ url-pattern> </ servlet-mapping>
进行执行,看看结果,发现对应的结果与get是一样的,证明了get请求转换了post的方式,实际上post域中标准应该是这样写的:
我们继续修改index.jsp:
<input type="button" οnclick="run1()" value="原生js实现Ajax的post请求"><br>
<script>function run1() {let x;if (window.XMLHttpRequest) {x = new XMLHttpRequest();} else {x = new ActiveXObject("Microsoft.XMLHTTP");}x.open("POST", "post?ff=3", false);let body = {"name": 1,"pass": "",}console.log(body)console.log(JSON.stringify(body))x.send(JSON.stringify(body));let text = x.responseText;console.log(text)}
</script>
后端代码:
package com. test. controller ; import javax. servlet. * ;
import java. io. IOException ;
import java. io. PrintWriter ; public class PostRequest implements Servlet { @Override public void init ( ServletConfig servletConfig) throws ServletException { System . out. println ( "初始化" ) ; } @Override public ServletConfig getServletConfig ( ) { return null ; } @Override public void service ( ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException , IOException { String name = servletRequest. getParameter ( "name" ) ; String pass = servletRequest. getParameter ( "pass" ) ; String p = servletRequest. getParameter ( "ff" ) ; System . out. println ( name + "," + pass + "," + p) ; servletResponse. setContentType ( "text/html;charset=UTF-8" ) ; PrintWriter writer = servletResponse. getWriter ( ) ; writer. write ( "<h1>" + 11 + "</h1>" ) ; } @Override public String getServletInfo ( ) { return null ; } @Override public void destroy ( ) { System . out. println ( "正在销毁中" ) ; }
}
当我们去掉了JSON.stringify()会发现,也并没有打印,对应的都是null,为什么,实际上这里我需要注意一下:你认为getParameter方式的获取有没有条件,实际上是有的,在前面我们知道默认的处理是application/x-www-form-urlencoded,当对应的请求头中存在这个,那么对应的(getParameter)就可以进行处理(无论你是在url中还是域中都是如此,当然了,后端中该代码是会判断请求信息中是get还是post而进行选择url处理还是域处理的,这也是分工的判断,要不然,你写了就会进行分工呢,所以肯定还是操作了判断的),而get的处理一般都是操作这个的,这没有问题,而post对这个来说有点不同,情况如下的操作:
假设,你post是get的url转换的,那么默认会加上application/x-www-form-urlencoded,使得后端的getParameter可以接收(get本身就会加上),但是有些东西在请求头中可能并不会直接的进行显示,比如application/x-www-form-urlencoded在请求标头中可能并不会直接的显示(不显示他而已,具体情况,可能是其他纯文本的方式,而这个时候显示与不显示就需要看浏览器版本了),包括get和post,只是post如果没有进行get的转换,那么即没有显示,也没有进行设置,所以这个时候前端代码应该是如此的(两种都可以测试):
x. open ( "POST" , "post?ff=3" , false ) ; let body = { "name" : 1 , "pass" : "" , } console. log ( body) console. log ( JSON . stringify ( body) ) x. setRequestHeader ( "Content-Type" , "application/x-www-form-urlencoded" ) ; x. send ( JSON . stringify ( body) ) ; let text = x. responseText; console. log ( text)
x. open ( "POST" , "post?ff=3" , false ) ; let body = { "name" : 1 , "pass" : "" , } console. log ( body) console. log ( JSON . stringify ( body) ) x. setRequestHeader ( "Content-Type" , "application/x-www-form-urlencoded" ) ; x. send ( body) ; let text = x. responseText; console. log ( text)
然而上面的结果还是null,null,3,这是因为,你虽然设置了请求头,但是他的处理方式并不是处理某个对象或者一些字符串的操作,即需要是get的形式的,所以我们应该这样写:
x. open ( "POST" , "post?ff=3" , false ) ; let body = { "name" : 1 , "pass" : "" , } console. log ( body) console. log ( JSON . stringify ( body) ) x. setRequestHeader ( "Content-Type" , "application/x-www-form-urlencoded" ) ; x. send ( "name=1&pass=" ) ;
let text = x. responseText; console. log ( text)
打印了1,3,你可以继续修改:
x. open ( "POST" , "post" , false ) ; let body = { "ff" : "3" , "name" : 1 , "pass" : "" , } console. log ( body) console. log ( JSON . stringify ( body) ) x. setRequestHeader ( "Content-Type" , "application/x-www-form-urlencoded" ) ; x. send ( "name=1&pass=" ) ; let text = x. responseText; console. log ( text)
打印的信息是:1,null,可以发现ff是null,说明send最终的拼接是与get方式的url拼接的,或者他们最终都是操作一个域里面,所以ff直接没写时,那么他就没有
我们可以发现,实际上之所以浏览器默认对应的请求头是因为后端的操作,或者每个操作都是默认了处理请求头来完成数据的获取的,否则虽然你在网络上查看了他有参数,但是也并非获取,当然,这种操作也是由于原始处理造成的,原始处理在后面会说明的
从上面的测试来看,我们测试了post的请求,以及post的请求头的处理,其中Content-Type是请求头的处理
好了post的请求和请求头的处理我们初步完毕,在后面我们可能还会继续进行说明,所以先了解
现在我们来完成post的单文件处理,实际上单文件,多文件,和文件夹都可以使用前面的代码,这里我们可以给出:
前端:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head><title>Title</title>
</head>
<body>
<input type="file" id="fileInput" multiple/>
<button οnclick="uploadFile()">上传</button><script>async function uploadFile() {let fileInput = document.getElementById('fileInput');let fileData = [];let fileDatename = [];if (fileInput.files.length === 0) {return;}let readFilePromises = [];for (let y = 0; y < fileInput.files.length; y++) {if (fileInput.files[y] != undefined) {let reader = new FileReader();let readFilePromise = new Promise((resolve, reject) => {reader.onload = function (e) {console.log(e);fileData.push(e.target.result.split(',')[1]);fileDatename.push(fileInput.files[y].name);resolve();};reader.readAsDataURL(fileInput.files[y]);});readFilePromises.push(readFilePromise);}}await Promise.all(readFilePromises);sendFileUsingGET(fileData, fileDatename);}function sendFileUsingGET(fileData, name) {let xhr = new XMLHttpRequest();let url;for (let h = 0; h < fileData.length; h++) {url = "file?file=" + encodeURIComponent(fileData[h]) + "&filename=" + name[h];xhr.open('POST', url, false);xhr.send();}}
</script>
</body>
</html>
我们只是将GET变成了POST,即xhr.open(‘POST’, url, false);,修改web.xml:
< servlet> < servlet-name> PostRequest</ servlet-name> < servlet-class> com.test.controller.PostRequest</ servlet-class> </ servlet> < servlet-mapping> < servlet-name> PostRequest</ servlet-name> < url-pattern> /file</ url-pattern> </ servlet-mapping>
后端代码:
package com. test. controller ; import javax. servlet. * ;
import javax. servlet. http. HttpServletRequest ;
import java. io. FileOutputStream ;
import java. io. IOException ;
import java. io. PrintWriter ;
import java. util. Base64 ; public class PostRequest implements Servlet { @Override public void init ( ServletConfig servletConfig) throws ServletException { System . out. println ( "初始化" ) ; } @Override public ServletConfig getServletConfig ( ) { return null ; } @Override public void service ( ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException , IOException { HttpServletRequest h = ( HttpServletRequest ) servletRequest; if ( h. getMethod ( ) . equals ( "GET" ) ) { System . out. println ( "GET" ) ; } if ( h. getMethod ( ) . equals ( "POST" ) ) { System . out. println ( "POST" ) ; getFile ( h, servletRequest, servletResponse) ; } } private static void getFile ( HttpServletRequest h, ServletRequest servletRequest, ServletResponse servletResponse) { servletResponse. setContentType ( "text/html;charset=UTF-8" ) ; try { PrintWriter writer = servletResponse. getWriter ( ) ; String [ ] file = servletRequest. getParameterValues ( "file" ) ; String [ ] filename = servletRequest. getParameterValues ( "filename" ) ; if ( file != null ) { for ( int i = 0 ; i < file. length; i++ ) { String [ ] split = filename[ i] . split ( "\\." ) ; byte [ ] decodedBytes = Base64 . getDecoder ( ) . decode ( file[ i] ) ; FileOutputStream fileWriter = new FileOutputStream ( "F:/" + split[ 0 ] + "." + split[ 1 ] ) ; fileWriter. write ( decodedBytes) ; writer. write ( "<h1>" + "上传一个文件成功" + "</h1>" ) ; } return ; } writer. write ( "<h1>" + "上传的文件为空" + "</h1>" ) ; return ; } catch ( Exception e) { e. printStackTrace ( ) ; } } @Override public String getServletInfo ( ) { return null ; } @Override public void destroy ( ) { System . out. println ( "正在销毁中" ) ; }
}
选择测试者三个:
< input type = " file" id = " fileInput" />
< button onclick = " uploadFile ( ) " > 上传</ button> < input type = " file" id = " fileInput" multiple />
< button onclick = " uploadFile ( ) " > 上传</ button> < input type = " file" id = " fileInput" webkitdirectory />
< button onclick = " uploadFile ( ) " > 上传</ button>
经过测试,发现,都可以完成,即post完成了单文件,多文件,文件夹的处理,但是实际上前面的代码中,我们还不够优化,我们优化一下相应的后端代码:
if ( file != null ) { for ( int i = 0 ; i < file. length; i++ ) { String [ ] split = filename[ i] . split ( "\\." ) ; byte [ ] decodedBytes = Base64 . getDecoder ( ) . decode ( file[ i] ) ; FileOutputStream fileWriter = new FileOutputStream ( "F:/" + split[ 0 ] + "." + split[ 1 ] ) ; fileWriter. write ( decodedBytes) ; writer. write ( "<h1>" + "上传一个文件成功" + "</h1>" ) ; fileWriter. close ( ) ; } writer. close ( ) ; return ; }
post我们也操作完毕,但是还为之过早,我们知道,使用get的时候如果超过了url的限制,那么会报错,那如果post超过呢,他是不是不会报错了,所以我们先来测试一下:
首先修改前端代码:
function sendFileUsingGET ( fileData, name ) { let xhr = new XMLHttpRequest ( ) ; let url; for ( let h = 0 ; h < fileData. length; h++ ) { url = "file?file=" + encodeURIComponent ( fileData[ h] ) + "&filename=" + name[ h] ; xhr. open ( 'GET' , url, false ) ; xhr. send ( ) ; } }
相应的标签:
< input type = " file" id = " fileInput" />
< button onclick = " uploadFile ( ) " > 上传</ button>
重新启动服务器,然后上传一个图片文件,看看检查里面的网络信息有没有错误,发现还是出现如下:
java. lang. IllegalArgumentException: Request header is too large
一样的错误,现在我们将GET修改成POST,修改回来:xhr.open(‘POST’, url, false);
我们继续执行,会发现:
java. lang. IllegalArgumentException: Request header is too large
注意:在看下面时,不得不提到一点:在讨论时确实需要澄清一些术语,在HTTP协议中,请求头这个词通常具体指代请求中的请求头字段,而不包括请求行,请求行和请求头构成了HTTP请求的头部区域,而我们这里指的就是请求区域,只不过用请求头来表示,因为在学习时我们也基本认为请求行与请求头合称为请求头,但是也要注意,如果非要具体一点,请说成请求区域(或者请求头区域)
会发现是一样的错误,为什么,你认为url是属于请求信息中的哪个地方,实际上url属于请求头区域,也就是说,get出现这样的原因虽然我们说是url的限制,即后端的限制,但是实际上还有一个说法,就是请求头区域过大,因为url是请求头区域的,也就是说,url中添加的数据就算少于最大限制,可能也会出现这样的错误,因为请求头区域中并不是只包含他,所以大多数博客说明的url过大并不准确,真正的应该是请求头区域过大,只是url基本在请求行中而已
如:
一般情况下,如果粗略的说的话,我们会将请求体或者响应体称为他们的整体,所以存在两个意思,对数据来说,那么他们就是里面的体,对一种大局来说,就是一个整体,这个在前面说明时,就有类似的体会(是请求体和响应体的设置(这里就是大局的了,而不是对数据),可以全局搜索查看)
很明显虽然post是保存在域中,但是请求信息中也由于保证数据可见,则必然会写上对应的拼接的整个url,随着过程会进行get转换,但是这个值还是设置的,我们也会进行可见数据的处理,所以post若要解决这样的问题,我们应该要操作如下:
function sendFileUsingGET ( fileData, name ) { let xhr = new XMLHttpRequest ( ) ; let url; for ( let h = 0 ; h < fileData. length; h++ ) { url = "file" ; xhr. open ( 'POST' , url, false ) ; xhr. setRequestHeader ( "Content-Type" , "application/x-www-form-urlencoded" ) ; xhr. send ( "file=" + encodeURIComponent ( fileData[ h] ) + "&filename=" + name[ h] ) ; } }
我们执行测试后,可以发现,文件处理完毕,错误已经解决(这里也能说明,之前get也是可以操作图片的(因为我们只是修改参数存放位置,对应的值没有改变),只是一般由于url限制,所以操作不了),但是又有一个问题,我们发现生成的文件中,名称可能存在乱码(你可以测试一下中文)经过测试,在前端name[h]还是正确的,所以是后端的问题,当然,中文的处理可以看看50章博客中的内容:
private static void getFile ( HttpServletRequest h, ServletRequest servletRequest, ServletResponse servletResponse) { servletResponse. setContentType ( "text/html;charset=UTF-8" ) ; try { h. setCharacterEncoding ( "utf-8" ) ; PrintWriter writer = servletResponse. getWriter ( ) ; String [ ] file = servletRequest. getParameterValues ( "file" ) ; String [ ] filename = servletRequest. getParameterValues ( "filename" ) ; if ( file != null ) { for ( int i = 0 ; i < file. length; i++ ) { String [ ] split = filename[ i] . split ( "\\." ) ; byte [ ] decodedBytes = Base64 . getDecoder ( ) . decode ( file[ i] ) ; FileOutputStream fileWriter = new FileOutputStream ( "F:/" + split[ 0 ] + "." + split[ 1 ] ) ; fileWriter. write ( decodedBytes) ; writer. write ( "<h1>" + "上传一个文件成功" + "</h1>" ) ; fileWriter. close ( ) ; } writer. close ( ) ; return ; } writer. write ( "<h1>" + "上传的文件为空" + "</h1>" ) ; return ; } catch ( Exception e) { e. printStackTrace ( ) ; } }
当然,post可不比get,他的处理方式有非常多,也正是因为post是主要操作文件的,所以在前端存在多种方式来完成文件的处理(但是也正是因为这些方式的处理才会造成大多数人并不会原始的文件操作方式(如上面的前端传递文件信息的操作),在前面我们知道只需要将get修改成post一样可以完成文件上传,因为我并没有使用这些,自然也没有这些的限制,而是比较原始的处理,所以当你改变后,也只是h.getMethod()的值改变而已,其他的一模一样),当然,由于上面的是原生js的处理,所以这些方式你可以选择不用,但是也可以用一用,首先是标签的处理,与get不同的是,他可以进行直接的处理,所以我们修改jsp文件,操作如下:
< form action = " file" method = " post" enctype = " multipart/form-data" > < input type = " file" name = " file" > < input type = " text" name = " filename" > < input type = " submit" value = " 文件上传" >
</ form>
这个标签进行提交的处理,与前面的这个js是类似的(我们也操作过),甚至可以说是一样的:
<input type="file" id="fileInput"/>
<button οnclick="uploadFile()">上传</button><script>function uploadFile() {let fileInput = document.getElementById('fileInput');let file = fileInput.files[0];sendFileUsingGET(file);}function sendFileUsingGET(file) {let xhr = new XMLHttpRequest();let formData = new FormData();formData.append('file', file);xhr.open('POST', "get", false);xhr.send(formData);}
</script>
然而我们需要先通过formData来完成表单的操作,这个时候我们不操作文件,之前我们只是说明他的作用,并没有真正的使用他完成过某些操作,而是错误出现,因为之前操作的是get,所以我们来处理一下这个,由于在前面我们说了,formData主要是操作文件的,为什么,这是因为他自动携带了请求头信息:multipart/form-data,也就是说,他相当于表单加上了multipart/form-data(在请求头中可以看到),所以他才是一个操作了文件的一个api,但是也正是因为该请求头,所以单纯的,如前面的getParameterValues并不能进行处理(他getParameter的数组方式,与他getParameter是一样的需要对应的相同请求头,也自然是application/x-www-form-urlencoded),这个时候,我们只能使用原始的处理的,但是原始的处理我们并没有学习过,所以在进行测试之前,我们先学习一下这个原始处理,首先是get的处理,我们改变index.jsp:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head><title>Title</title>
</head>
<body>
<button οnclick="uploadFile()">上传</button><script>function uploadFile() {let xhr = new XMLHttpRequest();xhr.open('get', "get?name=1", false);xhr.send();}</script>
</body>
</html>
web.xml记得存在这个:
< servlet> < servlet-name> GetRequest</ servlet-name> < servlet-class> com.test.controller.GetRequest</ servlet-class> </ servlet> < servlet-mapping> < servlet-name> GetRequest</ servlet-name> < url-pattern> /get</ url-pattern> </ servlet-mapping>
后端的代码:
@Override public void service ( ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException , IOException { String name = servletRequest. getParameter ( "name" ) ; System . out. println ( name) ; servletResponse. setContentType ( "text/html;charset=UTF-8" ) ; PrintWriter writer = servletResponse. getWriter ( ) ; writer. write ( "<h1>" + 11 + "</h1>" ) ; }
至此,看看打印结果是否出现,出现后我们修改后端代码:
@Override public void service ( ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException , IOException { String name = servletRequest. getParameter ( "name" ) ; System . out. println ( name) ; BufferedReader br = servletRequest. getReader ( ) ; BufferedWriter bw = new BufferedWriter ( new FileWriter ( "F:/in.txt" ) ) ; String str = null ; while ( ( str = br. readLine ( ) ) != null ) { System . out. println ( str) ; bw. write ( str) ; bw. newLine ( ) ; } bw. close ( ) ; br. close ( ) ; servletResponse. setContentType ( "text/html;charset=UTF-8" ) ; PrintWriter writer = servletResponse. getWriter ( ) ; writer. write ( "<h1>" + 11 + "</h1>" ) ; }
我们可以发现str是null,也就是说,在url中设置,并不能获取对应请求体的数据,当我们修改post时,也是如此,那么可以说他只是获取对应请求信息中的multipart/form-data处理,所以我们修改前端:
function uploadFile ( ) { let xhr = new XMLHttpRequest ( ) ; let formData = new FormData ( ) ; formData. append ( 'name' , 1 ) ; xhr. open ( 'POST' , "get?name=1" , false ) ; xhr. send ( formData) ; }
查看网络,可以发现,有两个地方(他们是互不影响的):
注意上图中的显示是get和post共用的,因为也只是显示而已,并不代表存放的地方
第二个就是multipart/form-data的处理(在请求方式发生改变后,对应的send的参数可以接收这个对象,从而出现表单数据,这与对应的请求头application/x-www-form-urlencoded使得在send中加上对应url参数形式是一样的,但也只是针对后端数据的获取),也验证了formData操作了该请求头信息,你可以点击查看源代码看看内容,等下看看执行后生成文件的内容即可(当然,在某些情况下,他可能是隐藏的,可能的原因之一,是防止你赋值拿取信息,当然了,如果你能够破解浏览器,那么也行,但是如果可以破解了,你也大概率不会到这里了)
现在我们访问后端,可以发现str不为null了(对应的处理就是专门操作对应表单的信息的,所以不为null了,经过测试,之所以可以得到是因为servletRequest相关的获取(servletRequest.getInputStream()或者servletRequest.getReader())只能操作表单(并不绝对,看后面就知道了),所以当是表单时,那么就能操作,否则不会有,即不会有,那么基本是空数据了,自然得不到了),那么看看对应的生成的文件内容是什么,这个时候可以发现,与对应的(上面的查看源代码)内容一致,说明我们获取的就是对应传递的信息,要注意:对应的信息是分割的,我们可以通过分割的字符串来进行处理,当然,这是比较麻烦的(即原始操作),为了证明原始操作可以进行处理,所以我们来手动写一个,现在我们修改前端代码:
< % @ page contentType= "text/html;charset=UTF-8" language= "java" % >
< html>
< head> < title> Title < / title>
< / head>
< body>
< input type= "file" id= "fileInput" / >
< button onclick= "uploadFile()" > 上传< / button> < script> function uploadFile ( ) { let fileInput = document. getElementById ( 'fileInput') ; let file = fileInput. files[ 0 ] ; sendFileUsingGET ( file) ; } function sendFileUsingGET ( file) { let xhr = new XMLHttpRequest ( ) ; let formData = new FormData ( ) ; formData. append ( 'file' , file) ; formData. append ( 'name' , "22" ) ; xhr. open ( 'POST' , "get" , false ) ; xhr. send ( formData) ; }
< / script>
< / body>
< / html>
当然,通过前面的说明我们知道了原始操作,实际上原始操作最终还是服务器自身的api,而服务器的api也是根据网络编程来处理的,最终的最终都只是操作请求信息和响应信息,所以这个原始操作只是在利用比较原始的api来进行处理的(不是真正的底层,因为服务器也是编写的,要知道下层还有网络编程呢),所以存在后面的代码
我们编写后端代码就以这个为主(上面前面的检查元素里面的,下面是我上传图片后的其中一个结果):
后端代码是:
package com. test. controller ; import javax. servlet. * ;
import javax. servlet. http. HttpServletRequest ;
import java. io. * ;
import java. nio. charset. StandardCharsets ;
import java. util. * ; public class GetRequest implements Servlet { @Override public void init ( ServletConfig servletConfig) throws ServletException { System . out. println ( "初始化" ) ; } @Override public ServletConfig getServletConfig ( ) { return null ; } @Override public void service ( ServletRequest servletRequest, ServletResponse servletResponse) { try { HttpServletRequest h = ( HttpServletRequest ) servletRequest; String contentType = h. getHeader ( "Content-Type" ) ; String boundary = null ; if ( contentType != null && contentType. startsWith ( "multipart/form-data" ) ) { int boundaryIndex = contentType. indexOf ( "boundary=" ) ; if ( boundaryIndex != - 1 ) { boundary = contentType. substring ( boundaryIndex + "boundary=" . length ( ) ) ; } } if ( boundary != null ) { Map < Integer , Map < String , String > > mapMap = new HashMap < > ( ) ; Map < String , String > map = null ; ServletInputStream inputStream = servletRequest. getInputStream ( ) ; FileOutputStream fileWriter = null ; int hh = 0 ; int re = 0 ; byte [ ] fd = new byte [ 1024 ] ; int hkl = 0 ; int u = 0 ; int bh = 0 ; String name = "333.png" ; int ghk = 0 ; int count = 0 ; int jj = 4 ; int uh = 0 ; while ( ( re = inputStream. read ( ) ) != - 1 ) { if ( hkl >= fd. length) { byte [ ] ll = fd; fd = new byte [ fd. length * 2 ] ; for ( int g = 0 ; g < ll. length; g++ ) { fd[ g] = ll[ g] ; } } fd[ hkl] = ( byte ) re; hkl++ ; if ( ( char ) re == '\r' || ( char ) re == '\n' ) { ghk++ ; if ( count == 1 ) { if ( ghk >= 4 ) { if ( ghk % 2 == 0 ) { uh++ ; } fd = new byte [ 1024 ] ; hkl = 0 ; } } if ( count == 0 ) { if ( ghk == jj) { count = 1 ; fd = new byte [ 1024 ] ; hkl = 0 ; } } if ( ( char ) re == '\n' && ghk == 2 ) { if ( u == 0 ) { u++ ; byte [ ] ii = new byte [ hkl] ; for ( int yun = 0 ; yun < hkl; yun++ ) { ii[ yun] = fd[ yun] ; } byte [ ] iii = new byte [ ii. length - 2 ] ; for ( int b = 0 ; b < iii. length; b++ ) { iii[ b] = ii[ b] ; } String str = new String ( iii, "UTF-8" ) ; str = str. replace ( " " , "" ) ; byte [ ] iij = new byte [ ii. length + uh * 2 ] ; for ( int hkk = uh * 2 , kl = 0 ; hkk < iij. length; hkk++ ) { iij[ hkk] = ii[ kl] ; kl++ ; } int q = 0 ; int w = 1 ; for ( int gh = 0 ; gh < uh * 2 ; gh++ ) { if ( q == 0 && w == 1 ) { iij[ gh] = '\r' ; w = 0 ; q = 1 ; continue ; } if ( q == 1 && w == 0 ) { iij[ gh] = '\n' ; w = 1 ; q = 0 ; } } if ( str. equals ( "--" + boundary) ) { if ( uh > 0 ) { byte [ ] hj = new byte [ uh * 2 ] ; int ujg = 0 ; for ( int i = 0 ; i < hj. length; i++ ) { if ( ujg == 0 ) { hj[ i] = '\r' ; ujg = 1 ; continue ; } if ( ujg == 1 ) { hj[ i] = '\n' ; ujg = 0 ; } } uh = 0 ; fileWriter. write ( hj) ; } bh = 0 ; if ( fileWriter != null ) { count = 0 ; fileWriter. close ( ) ; dern ( name) ; } map = new HashMap < > ( ) ; mapMap. put ( hh, map) ; hh++ ; } if ( str. equals ( "--" + boundary + "--" ) ) { if ( uh > 0 ) { byte [ ] hj = new byte [ uh * 2 ] ; int ujg = 0 ; for ( int i = 0 ; i < hj. length; i++ ) { if ( ujg == 0 ) { hj[ i] = '\r' ; ujg = 1 ; continue ; } if ( ujg == 1 ) { hj[ i] = '\n' ; ujg = 0 ; } } uh = 0 ; fileWriter. write ( hj) ; fileWriter. close ( ) ; } break ; } uh = 0 ; if ( str. indexOf ( "Content-Disposition" ) >= 0 ) { int i = str. indexOf ( ";" ) ; String substring = str. substring ( i + 1 , str. length ( ) ) ; String s = "" ; char [ ] chars = substring. toCharArray ( ) ; for ( int o = 0 ; o < chars. length; o++ ) { if ( chars[ o] == ' ' ) { continue ; } s += chars[ o] ; } String [ ] split = s. split ( ";" ) ; for ( int k = 0 ; k < split. length; k++ ) { String [ ] split1 = split[ k] . split ( "=" ) ; String replace = split1[ 1 ] . replace ( "\"" , "" ) ; map. put ( split1[ 0 ] , replace) ; } } if ( str. indexOf ( "Content-Type" ) >= 0 ) { if ( str. indexOf ( "image/png" ) >= 0 || str. indexOf ( "text/plain" ) >= 0 ) { name = map. get ( "filename" ) ; if ( name != null ) { fileWriter = new FileOutputStream ( "F:/" + name) ; } bh = 1 ; } fd = new byte [ 1024 ] ; hkl = 0 ; continue ; } if ( bh == 1 ) { fileWriter. write ( iij) ; } fd = new byte [ 1024 ] ; hkl = 0 ; } } continue ; } ghk = 0 ; u = 0 ; } inputStream. close ( ) ; for ( int hhh = 0 ; hhh < mapMap. size ( ) ; hhh++ ) { Map < String , String > mapp = mapMap. get ( hhh) ; Set < Map. Entry < String , String > > entries = mapp. entrySet ( ) ; System . out. println ( hhh + ":" ) ; for ( Map. Entry e : entries) { System . out. println ( "{" + e. getKey ( ) + ":" + e. getValue ( ) + "}" ) ; } } servletResponse. setContentType ( "text/html;charset=UTF-8" ) ; PrintWriter writer = servletResponse. getWriter ( ) ; writer. write ( "<h1>" + 11 + "</h1>" ) ; System . out. println ( "操作完毕" ) ; } } catch ( Exception e) { e. printStackTrace ( ) ; } } @Override public String getServletInfo ( ) { return null ; } @Override public void destroy ( ) { System . out. println ( "正在销毁中" ) ; } private void dern ( String name) { try { FileInputStream fileInputStream = new FileInputStream ( "F:/" + name) ; int re = 0 ; byte [ ] fd = new byte [ 1024 ] ; int hkl = 0 ; while ( ( re = fileInputStream. read ( ) ) != - 1 ) { if ( hkl >= fd. length) { byte [ ] ll = fd; fd = new byte [ fd. length * 2 ] ; for ( int g = 0 ; g < ll. length; g++ ) { fd[ g] = ll[ g] ; } } fd[ hkl] = ( byte ) re; hkl++ ; } FileOutputStream fileWriter = new FileOutputStream ( "F:/" + name) ; byte [ ] fdd = new byte [ hkl - 2 ] ; for ( int u = 0 ; u < fdd. length; u++ ) { fdd[ u] = fd[ u] ; } fileWriter. write ( fdd) ; fileWriter. close ( ) ; fileInputStream. close ( ) ; } catch ( Exception e) { e. printStackTrace ( ) ; } }
}
这个代码是我临时编写,大概率还可以进行优化(单纯的利用比较原始api来完成最终结果),当然,没有一定的编程功底是很难编写出来,所以为了方便使用,servlet实际上也给出了对应的api来给我们来使用,就不需要自行编写了,后面会说明的
现在,我们上传执行,上传一个图片,看看图片信息是否生成,如果对应的在指定盘中出现了图片,那么说明操作完毕(上面默认在F盘下的,且名称是文件名称),当然,我们需要考虑一个点,对应文件的内容存在浏览器对应的区域时是如何处理的,特别是换行符怎么处理,我们看这个:
假设这个是文件信息:
1111111114444444
5555221
那么对应再前端的区域也是这个:
分割符
1111111114444444
5555221分割符
也就是说,绝对的一致,但是,由于最后的值(指的是空),需要换行到分割符中,所以上面的代码,由于考虑到了这样的情况,所以默认将后面的换行符都进行加上,所以我们最后的处理需要这样的存在:
dern ( name) ;
也就是说,实际上如果对应的文件的一行中存在换行,那么对应的区域也存在,若没有,那么也不存在(但是需要换行到分割符中,所以我们需要特别的处理最后两个字节),也就是说,我们确定原来的文件末尾是否存在换行符(他会影响上面的值,使得1后面多出几个换行,所以文件末尾是否存在换行符并不会影响数据的操作,只有到分割符的换行会影响整体性,但并不影响文件的显示),但是无论是否存在,在前面对应的地方(上面写的代码)都会加上换行符,并且最后去掉,并且,实际上不考虑去掉最后两个字节也没有问题,因为换行符并不会影响图片的显示,但是也只是末尾,头部可不要这样哦,而正是如此,如果非常的细节的话,实际上浏览器的文件上传在末尾的情况有些时候可能会出现一些问题(面临整体性的问题),然而上面的解释与实际情况是相同的,我们可以观察生成的文件,会发现,字节数是一样的(注意需要看属性,因为kb比较大,并不会细节的显示),这个一样通常在很多方面(如上面解决了//去掉最后两个字节,否则会导致生成的可能会多出几个字节,如换行符),实际上由于默认添加换行符,所以我们生成的文件一般会多出两个字节,也就是\r\n,你可以选择在生成的文件内容最后去掉最后一个的换行符,可以发现,他们的字节数量是一样的了
从这里我们也可以发现,关于IO流,无非就是字符和字节的保存和转换的变化,实际上无论api是如何操作,最终都是字符变成字节保存的
当然,上面换行符的出现,是因为浏览器在文件上传的过程中,多部分表单数据格式会引入一些额外的字符,包括换行符,甚至会对某些字符进行统一的处理但这些通常不会对上传的文件本身产生实质性的影响,而正是如此,所以一般情况下,内容是完全一致的,但是可能如换行符的存在会导致出现问题,虽然只是一个完整性的问题,也就是说,如果解决了这个问题,那么文件就完全的一样了,而不会有任何的不同,但是这里我为了完整性,所以才操作了最后去掉字节
至此,我们的后端代码操作完毕,当然,这也只是极小的部分代码,实际上情况会更加的复杂,正如代码注释中所说:其他的可能也要处理,但是我测试的数据一般是没有的,所以就不进行处理了
至此原始处理我可以说操作完毕
之前我们操作了post的多种方式(将get变成post的)的文件处理,但是他们都只是操作key-value的处理,也就是传统的处理,post有其他的处理,也就是multipart/form-data,而不是application/x-www-form-urlencoded,他的优势在于我们不需要操作继续的编码,如base64(上面是按照传统的multipart/form-data处理来的,并且使用比较原始的api)
实际上post可以完成get的处理,前面说过了,但是也正是他的域存在,所以post存在其他的处理也是正常的,也有属于操作他专门的方式,也就是之前默认的存在(可以操作请求头,需要请求头,需要multipart/form-data,解决Multi-Part错误,几乎没有需要的文件上限,即没有限制大小(get的url有限制))
上面我们了解即可,但是为了验证上面的原始处理比较正确,所以现在我们来操作多文件的处理,也是使用上面的代码,总需要都试一下:
修改前端代码:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head><title>Title</title>
</head>
<body>
<input type="file" id="fileInput" multiple/>
<button οnclick="uploadFile()">上传</button><script>function uploadFile() {let fileInput = document.getElementById('fileInput');let file = fileInput.files;sendFileUsingGET(file);}function sendFileUsingGET(file) {let xhr = new XMLHttpRequest();let formData = new FormData();for (let y = 0; y < file.length; y++) {formData.append('file', file[y]);}formData.append('name', "22");xhr.open('POST', "get", false);xhr.send(formData);}
</script>
</body>
</html>
<!--上面的情况,可以解决多个文件的处理,以后建议操作文件的时候,最好这样,因为他无论是选择多个还是一个都可以完成,而最好不用操作默认为0的处理,即let file = fileInput.files[0];,当然,前面我们只是测试测试而已,所以这样也没事,因为我们只是一个测试操作,而非正式的写代码-->
虽然对应的在检查中,比如说:
存在两个name,但是他并不会是覆盖的形式,因为对于域来说,他只负责存放数据,而数据的分别则是由分割符来完成,即或者说,实际上我们并没有具体参照过name,而是参照分割符,因为他们是一个整体(这个分割符到空白行之间的整个信息)
也就是说,实际上无论是多文件还是问文件夹都是如此(文件夹只是一个选择了一个文件里面的所有数据的多文件而已,还是属于多文件的范畴,实际上多文件可以,那么文件夹也可以,更甚至,文件可以,多文件也就可以),经过测试,文件夹也是可以的,但是域,即multipart/form-data与之前的处理有点不同,前面是最原始的操作而这个是经过中间处理的,我们可以发现,存在这个:
也就是说,带上了文件夹的名称了,因为他这个操作是封装的,要不然分割符怎么来的呢,或者说这也是浏览器给出的一个操作,所以在multipart/form-data的情况下,是存在对于文件夹名称的,实际上前面的操作中,即不是这样的方式下(当然,post可不比get,他的处理方式有非常多,也正是因为post是主要操作文件的,所以在前端存在多种方式来完成文件的处理),也就是前面第一次使用get的时候,拿取的这个:
let fileInput = document. getElementById ( 'fileInput' ) ; let file = fileInput. files; console. log ( file)
实际上里面的file中就存在对于的文件夹信息(前提是文件夹的处理,否则是对应的文件信息),也就是说,这些处理实际上还是浏览器自身的处理,而非multipart/form-data,只不过,在过程中,拿取了这个数据来操作了,之前我们操作get时,只是操作文件的内容,而并没有操作文件夹的处理,实际上我们还应该传递这个路径信息来进行处理的,经过测试,每个文件中存在webkitRelativePath参数(前端),如果是文件夹,那么他就不是"",那么前面操作文件时,需要判断一下这个的值是否为空即可,从而赋值这个值到后端,然后后端直接执行创建目录的方法来保证目录的创建,当然,如果没有目录,创建目录基本不会进行任何操作的,这也是一种优化,可以选择试一下,或者了解即可
当然,这里也可以给出一个优化,即创建文件的优化(考虑文件目录的创建),也可以给前面操作get时的处理的
代码如下:
if ( str. indexOf ( "Content-Type" ) >= 0 ) { if ( str. indexOf ( "image/png" ) >= 0 || str. indexOf ( "text/plain" ) >= 0 ) { name = map. get ( "filename" ) ; if ( name != null ) { String pa = "F:/" ; File file = new File ( pa + name) ; if ( file. exists ( ) ) { fileWriter = new FileOutputStream ( pa + name) ; } else { String name1 = file. getPath ( ) ; int i = name1. lastIndexOf ( "\\" ) ; String substring = name1. substring ( 0 , i) ; new File ( substring) . mkdirs ( ) ; fileWriter = new FileOutputStream ( pa + name) ; } } bh = 1 ; } fd = new byte [ 1024 ] ; hkl = 0 ; continue ; }
重启,测试文件夹的处理,发现操作成功了
至此,原始处理我们操作完毕,实际上上面在并发情况下,容易出现问题,这是因为可能同时操作对应的相同文件,或者操作相同文件,所以我们可以在很多文件上传的处理中,基本都会给文件名称进行处理,比如加上UUID等等的处理,来保证唯一,就是防止并发情况下的问题
当然,服务器一般存在写好的原始处理的api,而不用我们自己来写,比如我们可以这样处理:
修改后端代码(实际上将上面我写好的代码封装一下也可以使用的),前端代码操作单文件处理即可:
这里决定给出传统形式的处理,当然,给出的是get和post的判断,看如下:
HttpServletRequest h = ( HttpServletRequest ) servletRequest; if ( h. getMethod ( ) . equals ( "GET" ) ) { System . out. println ( "GET" ) ; } if ( h. getMethod ( ) . equals ( "POST" ) ) { System . out. println ( "POST" ) ; }
前面我们操作过这个,实际上由于某些东西是需要一些对应的请求的,所以在这之前我们通常需要判断,对应于框架来说,就是判断你的请求是get还是post了,实际上由于服务器原本的处理是service方法,那么在考虑传统的处理,比如50章博客中的dopost和doget实际上都只是在service里面进行的处理的,或者说也是根据这个判断来执行谁,所以实际上上面写的是最原始的,但是我们也知道,其实我们也利用了HttpServletRequest来操作方法,实际上只是因为用来接收的而已,还存在更加原始的,只是这里我们操作比较原始的,否则岂不是还要到网络编程里面去了
当然,有些依赖也存在这样的处理,相当于也封装了上面的原始操作(实际上上面的原始操作中,我最后是操作打印信息的,这个信息是否可以认为是一个依赖里面给与的方法的返回值呢)
现在我们来操作一些自带的api来处理文件:
我们创建re类,然后写上如下(复刻一下对应的50章博客的处理,实际上对应的配置或者代码可能还有更多的操作,当然,这里我们就不考虑复杂的兜底操作了,所以直接的简单处理便可):
package com. test. controller ; import javax. servlet. * ;
import javax. servlet. http. HttpServletRequest ;
import javax. servlet. http. HttpServletResponse ; public class re implements Servlet { @Override public void init ( ServletConfig servletConfig) throws ServletException { System . out. println ( "初始化" ) ; } @Override public ServletConfig getServletConfig ( ) { return null ; } @Override public void service ( ServletRequest servletRequest, ServletResponse servletResponse) { HttpServletRequest h = ( HttpServletRequest ) servletRequest; if ( h. getMethod ( ) . equals ( "GET" ) ) { System . out. println ( "GET" ) ; try { doGet ( ( HttpServletRequest ) servletRequest, ( HttpServletResponse ) servletResponse) ; } catch ( Exception e) { e. printStackTrace ( ) ; } } if ( h. getMethod ( ) . equals ( "POST" ) ) { System . out. println ( "POST" ) ; try { doPost ( ( HttpServletRequest ) servletRequest, ( HttpServletResponse ) servletResponse) ; } catch ( Exception e) { e. printStackTrace ( ) ; } } } protected void doPost ( HttpServletRequest servletRequest, HttpServletResponse servletResponse) { } protected void doGet ( HttpServletRequest servletRequest, HttpServletResponse servletResponse) { } @Override public String getServletInfo ( ) { return null ; } @Override public void destroy ( ) { System . out. println ( "正在销毁中" ) ; } }
然后创建一个reen类:
package com. test. controller ; import javax. servlet. ServletException ;
import javax. servlet. http. HttpServletRequest ;
import javax. servlet. http. HttpServletResponse ;
import java. io. IOException ; public class reen extends re { @Override protected void doGet ( HttpServletRequest req, HttpServletResponse resp) { System . out. println ( 1 ) ; } @Override protected void doPost ( HttpServletRequest req, HttpServletResponse resp) { System . out. println ( 2 ) ; }
}
在web.xml中进行编写:
< servlet> < servlet-name> reen</ servlet-name> < servlet-class> com.test.controller.reen</ servlet-class> </ servlet> < servlet-mapping> < servlet-name> reen</ servlet-name> < url-pattern> /reen</ url-pattern> </ servlet-mapping>
这样,就会定位到reen的方法(因为他也是对应接口的子类),然后执行reen的service方法,由于service是其父类的,那么使用其父类的,并且由于重写了对应的doPost和doGet,那么执行自己的版本,我们在前面进行访问:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head><title>Title</title>
</head>
<body>
<button οnclick="uploadFile()">访问</button><script>function uploadFile() {sendFileUsingGET();}function sendFileUsingGET(file) {let xhr = new XMLHttpRequest();xhr.open('POST', "reen", false);xhr.send();xhr.open('GET', "reen", false);xhr.send();}
</script>
</body>
</html>
点击访问,访问两次,若后端打印了对应的1,2,那么说明操作成功,我的打印信息是:
初始化
POST
2
GET
1
正好对应,即我们操作完毕,在这种情况下,我们开始使用服务器一般存在写好的原始处理的api来完成我们的文件处理了:
后端代码修改如下(我们创建一个FileUploadServlet类):
package com. test. controller ; import javax. servlet. annotation. MultipartConfig ;
import javax. servlet. annotation. WebServlet ;
import javax. servlet. http. HttpServlet ;
import javax. servlet. http. HttpServletRequest ;
import javax. servlet. http. HttpServletResponse ;
import javax. servlet. http. Part ;
import java. io. InputStream ;
import java. nio. file. Files ;
import java. nio. file. Path ;
import java. nio. file. Paths ;
import java. nio. file. StandardCopyOption ; @WebServlet ( "/upload" )
@MultipartConfig ( fileSizeThreshold = 1024 * 1024 * 2 , maxFileSize = 1024 * 1024 * 10 , maxRequestSize = 1024 * 1024 * 50
)
public class FileUploadServlet extends HttpServlet { protected void doPost ( HttpServletRequest request, HttpServletResponse response) { try { Part filePart = request. getPart ( "file" ) ; String fileName = filePart. getSubmittedFileName ( ) ; InputStream fileContent = filePart. getInputStream ( ) ; Path targetPath = Paths . get ( "F:/" + fileName) ; Files . copy ( fileContent, targetPath, StandardCopyOption . REPLACE_EXISTING ) ; response. getWriter ( ) . write ( "文件上传成功:" + fileName) ; } catch ( Exception e) { e. printStackTrace ( ) ; } }
}
前端代码:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head><title>Title</title>
</head>
<body>
<input type="file" id="fileInput"/>
<button οnclick="uploadFile()">上传</button><script>function uploadFile() {let fileInput = document.getElementById('fileInput');let file = fileInput.files;console.log(file)sendFileUsingGET(file);}function sendFileUsingGET(file) {let xhr = new XMLHttpRequest();let formData = new FormData();for (let y = 0; y < file.length; y++) {formData.append('file', file[y]);}formData.append('name', "22");xhr.open('POST', "upload", false);xhr.send(formData);}
</script>
</body>
</html>
开始文件处理(又何尝不是文件上传的处理呢),我们可以发现,文件的信息被保存了,并且内容一模一样,说明他也是解决了换行,或者空白符的处理,那多文件呢,那么需要这样的处理:
package com. test. controller ; import javax. servlet. annotation. MultipartConfig ;
import javax. servlet. annotation. WebServlet ;
import javax. servlet. http. HttpServlet ;
import javax. servlet. http. HttpServletRequest ;
import javax. servlet. http. HttpServletResponse ;
import javax. servlet. http. Part ;
import java. io. InputStream ;
import java. nio. file. Files ;
import java. nio. file. Path ;
import java. nio. file. Paths ;
import java. nio. file. StandardCopyOption ;
import java. util. Collection ; @WebServlet ( "/upload" )
@MultipartConfig ( fileSizeThreshold = 1024 * 1024 * 2 , maxFileSize = 1024 * 1024 * 10 , maxRequestSize = 1024 * 1024 * 50 )
public class FileUploadServlet extends HttpServlet { protected void doPost ( HttpServletRequest request, HttpServletResponse response) { try { Collection < Part > parts = request. getParts ( ) ; for ( Part part : request. getParts ( ) ) { if ( "file" . equals ( part. getName ( ) ) ) { String fileName = part. getSubmittedFileName ( ) ; InputStream fileContent = part. getInputStream ( ) ; Path targetPath = Paths . get ( "F:/" + fileName) ; Files . copy ( fileContent, targetPath, StandardCopyOption . REPLACE_EXISTING ) ; response. getWriter ( ) . write ( "文件上传成功:" + fileName) ; } } } catch ( Exception e) { e. printStackTrace ( ) ; } }
}
那么文件夹呢,实际上如果是文件夹,那么名称可能需要处理一下(如创建目录),当然,前面我们也处理过,这个我们只给出这个名称的获取方式,就不操作具体的目录创建了:
String fileName = part. getSubmittedFileName ( ) ;
实际上对于陌生的默认接收信息的类(如Part),需要知道他的具体的结构,最好调试一下看看的的信息,也可以更好的知道其方法的作用(如大多数信息,如变量,对于的方法名称可能与他对应的,或者查看对应的方法是否是操作或者得到他即可)
至此,我们利用服务器自带的编写好或者封装好的api(里面存在原始api(原始代办最底层的意思,即没有其他api操作获取了))完成了这些操作,至此,post请求还剩下最后一个处理,即标签处理,我们修改前端:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head><title>Title</title>
</head>
<body>
<form action="upload" method="post" enctype="multipart/form-data"><input type="file" name="file"><input type="file" name="file"><input type="file" name="file"><input type="text" name="filename"><input type="submit" value="文件上传">
</form>
</body>
</html>
检查中的截图如下:
也就是说,结果是一样的,当然,上面的顺序是根据标签顺序来的,并且如果一个文件存在多文件选择,那么按照多文件选择来(多文件选择时可能是按照某种编码来进行排序,比如ascii,这在某种情况下,无论你多文件如果选择,都是2.txt在qq截图前面,这里可以了解,因为并没有很重要),所以将上面标签处理说成formData也不为过,无论是多文件和单文件,还是文件夹的一起操作,都只是对应的参数值不同而已,其中文件夹多一个目录,结构都是一样的
至此,我们的post操作就已经完美完成
经过上面的处理,完美原生的js,原生的servlet完成了get和post的所有操作了
实际上对原生的处理完成后,其他的任何变化,我们都基本很容易的理解,那么看如下:
现在操作框架js,和原生servlet的操作,框架js,我们可以选择jq,当然,经过前面的原生的处理,我们在考虑操作框架时,或多或少觉得没有必要,因为我们很容易的理解完毕,所以我们选择的进行处理,那么我们操作jq的多文件的处理:
修改前端(这里的jq很容易的引入,因为不是mvc的相对全部拦截):
< input type= "file" id= "fileInput" / >
< button onclick= "uploadFile()" > 上传< / button>
< script src= "https://code.jquery.com/jquery-3.6.0.min.js" > < / script>
< script> function uploadFile ( ) { let fileInput = document. getElementById ( 'fileInput') ; let file = fileInput. files; console. log ( file) sendFileUsingGET ( file) ; } function sendFileUsingGET ( file) { let formData = new FormData ( ) ; for ( let y = 0 ; y < file. length; y++ ) { formData. append ( 'file' , file[ y] ) ; } formData. append ( 'name' , "22" ) ; $. ajax ( { url: 'upload' , type: 'POST' , data: formData, processData: false , contentType: false , success: function ( data) { console. log ( data) } , error: function ( error) { console. log ( data) } } ) ; }
< / script>
我们只是将ajax进行了改变而已,一般在前端中,ajax一般都是封装好的,而基本都不是原生的处理
后端代码还是之前的代码(使用了Part类的代码,也就是前面我们操作了集合的处理):
package com. test. controller ; import javax. servlet. annotation. MultipartConfig ;
import javax. servlet. annotation. WebServlet ;
import javax. servlet. http. HttpServlet ;
import javax. servlet. http. HttpServletRequest ;
import javax. servlet. http. HttpServletResponse ;
import javax. servlet. http. Part ;
import java. io. InputStream ;
import java. nio. file. Files ;
import java. nio. file. Path ;
import java. nio. file. Paths ;
import java. nio. file. StandardCopyOption ;
import java. util. Collection ; @WebServlet ( "/upload" )
@MultipartConfig ( fileSizeThreshold = 1024 * 1024 * 2 , maxFileSize = 1024 * 1024 * 10 , maxRequestSize = 1024 * 1024 * 50 )
public class FileUploadServlet extends HttpServlet { protected void doPost ( HttpServletRequest request, HttpServletResponse response) { try { Collection < Part > parts = request. getParts ( ) ; for ( Part part : request. getParts ( ) ) { if ( "file" . equals ( part. getName ( ) ) ) { String name = part. getName ( ) ; String fileName = part. getSubmittedFileName ( ) ; InputStream fileContent = part. getInputStream ( ) ; Path targetPath = Paths . get ( "F:/" + fileName) ; Files . copy ( fileContent, targetPath, StandardCopyOption . REPLACE_EXISTING ) ; response. getWriter ( ) . write ( "文件上传成功:" + fileName) ; } } } catch ( Exception e) { e. printStackTrace ( ) ; } }
}
我们执行后,可以发现操作成功,这里我们需要提一下:由于使用对应的api里面可能是处理默认编码的,而不是与我们自行编写的代码一样(前面字符处理中文的操作)操作指定的解决,但是他自然也会考虑这样的情况,所以存在如下:
后面拿取中文名称的文件时,是一个乱码的,实际上我们只需要这样就能解决:
request. setCharacterEncoding ( "utf-8" ) ;
Collection < Part > parts = request. getParts ( ) ;
现在我们继续测试,如果编码正确,那么操作成功
现在我们可以选择看看ajax的一些参数:
url: 'upload' ,
type: 'POST' ,
data: formData,
processData: false ,
contentType: false ,
前面三个很好理解,其中url和type我们就不做说明,但是data需要,因为他的类型,决定了是操作什么样的参数,并且也会决定请求头,在前面我们也知道这样的处理:
function sendFileUsingGET ( fileData, name) { let xhr = new XMLHttpRequest ( ) ; let url; for ( let h = 0 ; h < fileData. length; h++ ) { url = "file" ; xhr. open ( 'POST' , url, false ) ; xhr. setRequestHeader ( "Content-Type" , "application/x-www-form-urlencoded" ) ; xhr. send ( "file=" + encodeURIComponent ( fileData[ h] ) + "&filename=" + name[ h] ) ; } }
send也可以存在对应的值,当然如果要设置请求头,那么jq的ajax应该是如此:
type : 'GET' , headers : { 'Header-Name1' : 'Header-Value1' , 'Header-Name2' : 'Header-Value2' , } ,
在继续说明之前,我们需要一下对应的一些请求头对应的信息,我们看看这个前端:
let xhr = new XMLHttpRequest ( ) ; xhr. open ( 'POST' , "upload?k=4" , false ) ; xhr. setRequestHeader ( "Content-Type" , "application/json" ) ; xhr. send ( "nn=3" ) ;
得到的信息是这样的(字符串一般都是载荷,无论是否设置了上面的请求头):
他也是一个新的请求头处理(在后面会说明的,在mvc中存在对应的注解,如@ResponseBody进行处理,这个在68章博客有说明,这里就不操作了,实际上他只是操作一个特殊变化,这个在操作完这些原生和框架的说明后,单独的进行处理说明,因为他基本是最为重要的),当然,get是操作不了的(忽略,前面只能操作上面的"查询字符串的信息",因为他是键值对,这个get和post是同一个地方,虽然一个是域一个是url,但是最终的操作是一样的处理,这里只是显示,即这个整体域)
现在我们继续来说jq的ajax的参数,主要说明这两个地方:
processData : false ,
contentType : false ,
实际上这些参数可有可无,因为这都是js框架补充的而已,只需要前面三个正确基本就行了(一般需要加上请求头,除非有自动的处理,如formData),所以我们删除这两个,然后再来测试,发现不行(前端报错),加上了processData和contentType就行了,也就是说,jq为了严谨,必须需要设置contentType和processData这个,虽然存在自动的处理,即formData,这个时候你可以选择设置也可以不设置(设置相同的请求头,是操作覆盖的),所以建议加上这些即可,当然,在不同的版本中,可能并不需要他们,这都是需要注意的(chatgpt是一个很好的工具)
当然,上面的代码还有一个地方有问题,返回信息也需要进行处理编码,因为他可能也是处理默认的,所以需要加上这个:
response. setContentType ( "text/html;charset=UTF-8" ) ;
response. getWriter ( ) . write ( "文件上传成功:" + fileName) ;
再来测试,然后看返回信息(上面图片检查中的预览就可以看到),如果正确,那么我们操作完毕
至此我们操作框架js,原始servlet已经完毕,当然,对应的get请求我们就不测试了,具体流程与前面是一样的,底层都是原生的处理,只是可能存在一些判断,而这些判断也会随着版本而发生改变,所以我们并不需要特别的了解,如果以后出现了什么问题,或者你不知道他存在什么判断,你完全可以网上找资料,当然,chatgpt是很好的选择,他的意义很大程度上帮助我们程序员并不需要记住一些容易变化的框架或者代码,只需要知道原理即可,也让我们可以学习更多的知识,而不用反复的记住一些重复的代码了
现在我们来操作原生js和框架servlet,这个博客主要说明的是mvc,也是我们进行这些测试的原因,没有之前的测试,mvc的一些api是难以懂得其原理的,所以现在开始操作,前面的项目主要是操作原生servlet的,现在我们重写来一个项目:
对应的依赖:
mvc的依赖封装了很多的东西,无非就是一些配置处理,以及拦截的处理,拦截的处理实际上可以看成Spring中类似的拦截,无非就是拿取信息对比而已
< packaging> war</ packaging> < dependencies> < dependency> < groupId> org.springframework</ groupId> < artifactId> spring-webmvc</ artifactId> < version> 5.1.5.RELEASE</ version> </ dependency> < dependency> < groupId> javax.servlet</ groupId> < artifactId> javax.servlet-api</ artifactId> < version> 3.1.0</ version> </ dependency> < dependency> < groupId> com.fasterxml.jackson.core</ groupId> < artifactId> jackson-databind</ artifactId> < version> 2.9.8</ version> </ dependency> < dependency> < groupId> com.fasterxml.jackson.core</ groupId> < artifactId> jackson-core</ artifactId> < version> 2.9.8</ version> </ dependency> < dependency> < groupId> com.fasterxml.jackson.core</ groupId> < artifactId> jackson-annotations</ artifactId> < version> 2.9.0</ version> </ dependency> </ dependencies>
web.xml:
<?xml version="1.0" encoding="UTF-8"?>
< web-app xmlns = " http://xmlns.jcp.org/xml/ns/javaee" xmlns: xsi= " http://www.w3.org/2001/XMLSchema-instance" xsi: schemaLocation= " http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version = " 4.0" > < servlet> < servlet-name> dispatcherServlet</ servlet-name> < servlet-class> org.springframework.web.servlet.DispatcherServlet</ servlet-class> </ servlet> < servlet-mapping> < servlet-name> dispatcherServlet</ servlet-name> < url-pattern> /</ url-pattern> </ servlet-mapping> </ web-app>
对应的在java资源文件夹下,创建controller包,然后创建upload类:
package controller ; import org. springframework. stereotype. Controller ;
import org. springframework. web. bind. annotation. RequestMapping ; import javax. servlet. http. HttpServletRequest ;
import javax. servlet. http. HttpServletResponse ; @Controller
@RequestMapping ( "/test" )
public class upload { @RequestMapping ( "a" ) public void handle01 ( HttpServletRequest request, HttpServletResponse response) { try { response. setContentType ( "text/html;charset=UTF-8" ) ; response. getWriter ( ) . write ( "操作" ) ; } catch ( Exception e) { e. printStackTrace ( ) ; } }
}
然后在资源文件夹下创建springmvc.xml文件:
< beans xmlns = " http://www.springframework.org/schema/beans" xmlns: context= " http://www.springframework.org/schema/context" xmlns: xsi= " http://www.w3.org/2001/XMLSchema-instance" xsi: schemaLocation= " http://www.springframework.org/schema/beanshttp://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/contexthttp://www.springframework.org/schema/context/spring-context.xsd" > < context: component-scan base-package = " controller" /> </ beans>
修改web.xml:
<?xml version="1.0" encoding="UTF-8"?>
< web-app xmlns = " http://xmlns.jcp.org/xml/ns/javaee" xmlns: xsi= " http://www.w3.org/2001/XMLSchema-instance" xsi: schemaLocation= " http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version = " 4.0" > < servlet> < servlet-name> dispatcherServlet</ servlet-name> < servlet-class> org.springframework.web.servlet.DispatcherServlet</ servlet-class> < init-param> < param-name> contextConfigLocation</ param-name> < param-value> classpath:springmvc.xml</ param-value> </ init-param> </ servlet> < servlet-mapping> < servlet-name> dispatcherServlet</ servlet-name> < url-pattern> /</ url-pattern> </ servlet-mapping> </ web-app>
因为这里是起始,需要操作扫描的然后操作处理拦截
然后我们启动项目,直接访问http://localhost:8080/mvcservlet/test/a,如果出现了操作,说明操作完毕(当然,返回值是void的情况在67章博客也有说明,具体可以去看看)
接下来我们来进行修改了,这里我们有点不同的操作,即请求头多一种方式,在后面我们就知道了
首先他也有10种情况,大多数对前端框架的变化也只有一点点(都是通过原生js来改变的,而原生js我们已经完全的写出来了,所以就不多说,但是后端的,我们只是部分写出,因为有很多情况需要判断,而我们只判断了一点,在这种情况下,后端就应该多说明一下的),所以前面的jq我们基本知识粗略的说明,但是后端框架由于设计的东西比较多,所以这里需要着重说明,但是一眼看出来的就没有必要了,比如get,或者带参数,post或者也带参数,这里再方法里面处理时,与原生的servlet是一模一样的处理,而get对文件的操作其实也是如此,再不考虑请求头的情况下,处理完全一样,我们可以复刻一下:
前端代码(创建index.jsp):
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head><title>Title</title>
</head>
<body>
<input type="file" id="fileInput" multiple/>
<button οnclick="uploadFile()">上传</button><script>async function uploadFile() {let fileInput = document.getElementById('fileInput');let fileData = [];let fileDatename = [];if (fileInput.files.length === 0) {return;}let readFilePromises = [];for (let y = 0; y < fileInput.files.length; y++) {if (fileInput.files[y] != undefined) {let reader = new FileReader();let readFilePromise = new Promise((resolve, reject) => {reader.onload = function (e) {console.log(e);fileData.push(e.target.result.split(',')[1]);fileDatename.push(fileInput.files[y].name);resolve();};reader.readAsDataURL(fileInput.files[y]);});readFilePromises.push(readFilePromise);}}await Promise.all(readFilePromises);sendFileUsingGET(fileData, fileDatename);}function sendFileUsingGET(fileData, name) {console.log(fileData)console.log(name)let xhr = new XMLHttpRequest();let url = "test/a?";for (let h = 0; h < fileData.length; h++) {if (h == fileData.length - 1) {url += "file=" + encodeURIComponent(fileData[h]) + "&filename=" + name[h];continue;}url += "file=" + encodeURIComponent(fileData[h]) + "&filename=" + name[h] + "&";}console.log(url)xhr.open('GET', url, false);xhr.send();console.log(xhr.responseText);}
</script>
</body>
</html>
后端代码(前面我们操作过了):
package controller ; import org. springframework. stereotype. Controller ;
import org. springframework. web. bind. annotation. RequestMapping ; import javax. servlet. http. HttpServletRequest ;
import javax. servlet. http. HttpServletResponse ;
import java. io. FileOutputStream ;
import java. io. PrintWriter ;
import java. util. Base64 ; @Controller
@RequestMapping ( "/test" )
public class upload { @RequestMapping ( "a" ) public void handle01 ( HttpServletRequest request, HttpServletResponse response) { response. setContentType ( "text/html;charset=UTF-8" ) ; try { PrintWriter writer = response. getWriter ( ) ; String [ ] file = request. getParameterValues ( "file" ) ; String [ ] filename = request. getParameterValues ( "filename" ) ; if ( file != null ) { for ( int i = 0 ; i < file. length; i++ ) { String [ ] split = filename[ i] . split ( "\\." ) ; byte [ ] decodedBytes = Base64 . getDecoder ( ) . decode ( file[ i] ) ; FileOutputStream fileWriter = new FileOutputStream ( "F:/" + split[ 0 ] + "." + split[ 1 ] ) ; fileWriter. write ( decodedBytes) ; writer. write ( "<h1>" + "上传一个文件成功" + "</h1>" ) ; } return ; } writer. write ( "<h1>" + "上传的文件为空" + "</h1>" ) ; return ; } catch ( Exception e) { e. printStackTrace ( ) ; } }
}
执行一下,然后修改:
xhr. open ( 'GET' , url, false ) ;
到
xhr. open ( 'POST' , url, false ) ;
发现结果是一样的,在没有考虑其他操作时,基本上是不会出现什么判断错误,当然,这里还是建议使用小文件,具体原因看前面就知道了,那么既然mvc也一样的是操作了servlet,但是他也存在对应的设置,或者对前面的配置进行了某些处理,比如:
@MultipartConfig ( fileSizeThreshold = 1024 * 1024 * 2 , maxFileSize = 1024 * 1024 * 10 , maxRequestSize = 1024 * 1024 * 50 )
他可能是一个设置了默认的值,而不是根据servlet的默认,当然,我们可以通过配置文件进行处理,这些我们了解即可(一般情况下,mvc并不会默认的进行设置,通常需要我们手动的处理),所以我们主要学习的就是其api了,也就是说mvc实际上也只是多出了几个api,或者内部通过某些操作也进行了封装,像自动保存了Collection< Part> parts = request.getParts();的值一样,其他的基本上不用进行什么说明,这里我们前端使用标签(表单),mvc使用api来进行处理:
前端是:
<form action="test/a" method="post" enctype="multipart/form-data"><input type="file" name="file"><input type="text" name="filename"><input type="submit" value="提交">
</form>
后端在mvc是进行封装的,在前面我们以及了解了这个:
他就相当于操作了原生api的处理后的结果进行保存的,或者可以说成是像自动保存了Collection< Part> parts = request.getParts();的值一样,所以他自然也会存在一些api来给我们使用
后端代码:
package controller ; import org. springframework. stereotype. Controller ;
import org. springframework. ui. Model ;
import org. springframework. web. bind. annotation. PostMapping ;
import org. springframework. web. bind. annotation. RequestMapping ;
import org. springframework. web. bind. annotation. RequestParam ;
import org. springframework. web. multipart. MultipartFile ;
import org. springframework. web. servlet. ModelAndView ; import javax. servlet. http. HttpServletRequest ;
import javax. servlet. http. HttpServletResponse ;
import java. io. FileOutputStream ;
import java. io. PrintWriter ;
import java. util. Date ;
@Controller
@RequestMapping ( "/test" )
public class upload { @PostMapping ( "a" ) public void handleFormUpload ( @RequestParam ( "file" ) MultipartFile file, @RequestParam ( "filename" ) String filename, HttpServletRequest request, HttpServletResponse response) { if ( ! file. isEmpty ( ) ) { try { request. setCharacterEncoding ( "utf-8" ) ; System . out. println ( "文件名称: " + filename) ; String name = file. getOriginalFilename ( ) ; name = new String ( name. getBytes ( "iso-8859-1" ) , "utf-8" ) ; byte [ ] bytes = file. getBytes ( ) ; FileOutputStream fileOutputStream = new FileOutputStream ( "F:/" + name) ; fileOutputStream. write ( bytes) ; response. setContentType ( "text/html;charset=UTF-8" ) ; PrintWriter writer = response. getWriter ( ) ; writer. write ( "<h1>" + "上传一个文件成功" + "</h1>" ) ; } catch ( Exception e) { e. printStackTrace ( ) ; } } }
}
我们需要配置对应的multi-part配置,因为一般mvc并不会默认处理(没有这个,自然也会报前面的那个Multi-Part错误,对于框架来说,可能会捕获这个错误,然后报其他错误),所以我们操作如下(否则的话,可能操作不了相关文件的组件或者说赋值或者说某些处理导致报错等等):
在springmvc.xml中加上如下:
< bean id = " multipartResolver" class = " org.springframework.web.multipart.commons.CommonsMultipartResolver" > < property name = " maxUploadSize" value = " 5242880" > </ property> < property name = " maxInMemorySize" value = " 40960" > </ property> </ bean>
上面的属性在一定程度上对应与这个:
现在我们执行看看结果,然而还不行,这是mvc由于主要操作拦截,他只是提供了对应的关联处理,一般我们需要如下的依赖:
< dependency> < groupId> commons-fileupload</ groupId> < artifactId> commons-fileupload</ artifactId> < version> 1.3.3</ version>
</ dependency>
< dependency> < groupId> commons-io</ groupId> < artifactId> commons-io</ artifactId> < version> 2.6</ version>
</ dependency>
现在我们再来进行操作,可以发现文件上传成功(一般来说,图片的资源被占用时,打开后,通常会使得文件感觉变大,这是图片与文件系统的关系,了解即可),如果是多文件,那么我们应该如此:
首先是对应前端修改一下:
< input type = " file" name = " file" multiple >
后端也修改一下(因为对应的MultipartFile file只能得到第一个文件(在多文件时,也是按照第一个的,自己测试就知道了)):
package controller ; import org. springframework. stereotype. Controller ;
import org. springframework. ui. Model ;
import org. springframework. web. bind. annotation. PostMapping ;
import org. springframework. web. bind. annotation. RequestMapping ;
import org. springframework. web. bind. annotation. RequestParam ;
import org. springframework. web. multipart. MultipartFile ;
import org. springframework. web. servlet. ModelAndView ; import javax. servlet. http. HttpServletRequest ;
import javax. servlet. http. HttpServletResponse ;
import java. io. FileOutputStream ;
import java. io. PrintWriter ;
import java. util. Date ;
import java. util. List ;
@Controller
@RequestMapping ( "/test" )
public class upload { @PostMapping ( "a" ) public void handleFormUpload ( @RequestParam ( "file" ) List < MultipartFile > fileList, @RequestParam ( "filename" ) String filename, HttpServletRequest request, HttpServletResponse response) { if ( fileList. size ( ) > 0 ) { try { for ( MultipartFile file : fileList) { request. setCharacterEncoding ( "utf-8" ) ; System . out. println ( "文件名称: " + filename) ; String name = file. getOriginalFilename ( ) ; name = new String ( name. getBytes ( "iso-8859-1" ) , "utf-8" ) ; byte [ ] bytes = file. getBytes ( ) ; FileOutputStream fileOutputStream = new FileOutputStream ( "F:/" + name) ; fileOutputStream. write ( bytes) ; response. setContentType ( "text/html;charset=UTF-8" ) ; PrintWriter writer = response. getWriter ( ) ; writer. write ( "<h1>" + "上传一个文件成功" + "</h1>" ) ; } } catch ( Exception e) { e. printStackTrace ( ) ; } } }
}
也就是说,本来就已经都处理好变成List的,如果你的参数只是一个,那么给List的第一个,否则都给你
我们可以发现,使用框架的mvc是比较容易的,虽然前面我们的处理封装一下也行,但是有现成的为什么不用呢,至此,原生js和框架servlet我们操作完毕,现在我们来操作框架js和框架servlet,由于js无论是否框架,对代码封装影响不大,即在前面我们也说明了"变化也只有一点点",所以这里的我们也只给出一个测试结果吧:
只需要修改前端即可:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head><title>Title</title>
</head>
<body>
<input type="file" id="fileInput"/>
<button οnclick="uploadFile()">上传</button>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script>function uploadFile() {let fileInput = document.getElementById('fileInput');let file = fileInput.files;console.log(file)sendFileUsingGET(file);}function sendFileUsingGET(file) {let formData = new FormData();for (let y = 0; y < file.length; y++) {formData.append('file', file[y]);}formData.append('filename', "22");$.ajax({url: 'test/a', // 提交到的URLtype: 'POST',data: formData,processData: false, // 不处理数据contentType: false, // 不设置内容类型success: function (data) {// 成功回调函数console.log(data)},error: function (error) {console.log(data)}});}
</script>
</body>
</html>
上传一个文件试一下吧,至此,我们的框架js和框架servlet操作完毕
至此,我们的所有请求说明基本操作完毕,当然,前面也说过了,需要说明一下这个:
xhr. setRequestHeader ( "Content-Type" , "application/json" ) ;
现在我们来操作这个,如果说:
xhr. setRequestHeader ( "Content-Type" , "application/x-www-form-urlencoded" ) ;
或者
multipart/ form- data
都有原生处理来进行,比如默认的可以存在getParameter进行获取,而multipart/form-data可以通过读取io流来处理,那么application/json呢,我们可以来看看:
首先说明一下这个请求头,在前面我们基本只是了解一下,并没有使用他操作过,其实该请求与multipart/form-data基本类似的,
所以他一般是不会放在url中,我们来试一下,首先修改前端(index.jsp):
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head><title>Title</title>
</head>
<body>
<button οnclick="uploadFile()">访问</button><script>function uploadFile() {sendFileUsingGET();}function sendFileUsingGET() {let xhr = new XMLHttpRequest();xhr.open('POST', "te/u", false);//可以选择加上看看结果(一般是一样的)xhr.setRequestHeader("Content-Type", "application/json");xhr.send("22");}
</script>
</body>
</html>
创建一个类,代码如下:
package controller ; import org. springframework. stereotype. Controller ;
import org. springframework. ui. Model ;
import org. springframework. web. bind. annotation. PostMapping ;
import org. springframework. web. bind. annotation. RequestMapping ;
import org. springframework. web. bind. annotation. RequestParam ;
import org. springframework. web. multipart. MultipartFile ;
import org. springframework. web. servlet. ModelAndView ; import javax. servlet. http. HttpServletRequest ;
import javax. servlet. http. HttpServletResponse ;
import java. io. BufferedReader ;
import java. io. FileOutputStream ;
import java. io. PrintWriter ;
import java. util. Date ;
import java. util. List ;
@Controller
@RequestMapping ( "/te" )
public class json { @PostMapping ( "u" ) public void handleFormUpload ( HttpServletRequest request, HttpServletResponse response) { response. setContentType ( "application/json" ) ; StringBuilder requestBody = new StringBuilder ( ) ; try { BufferedReader reader = request. getReader ( ) ; String line; while ( ( line = reader. readLine ( ) ) != null ) { requestBody. append ( line) ; } String jsonPayload = requestBody. toString ( ) ; response. getWriter ( ) . write ( jsonPayload) ; } catch ( Exception e) { e. printStackTrace ( ) ; } }
}
我们发现响应得到了22这个数据,并且他也可以操作getReader,为什么,不是说前面说他只能操作表单吗,实际上除了默认的头,一般其他的信息都会在域中,但是前面也说明过了由于分工不同,所以导致也可以说他们都操作同一个域,所以这里也只是一个判断而已,判断他不是默认所以可以除了,或者说,默认的存在在另外一个敌方,所以这里我应该需要对get和post的存放需要进行一个统一说明处理:实际上存在一个空间,get和post的数据都是放在这个空间的,其中get需要在url中拿取,而post是直接存在(所以在这个处理上,说成是不同的域也没有问题,或者说一个是url一个是域也行),而这个空间存在两个地方,一个是key-value,一个是直接的数据存在,默认get只能存放在key-value区域,而post则都可以,前提是请求头的改变,很明显,这个json的相关请求头就是第二个直接的数据存在的区域,所以可以与表单一样的操作(通过这样的说明,也就能更好的理解"无论你是在url中还是域中都是如此",在前面的某个地方说明了这个,可以全局搜索即可)
那么我们对这个请求头进行说明:
一般来说,他这个请求头只是一个传递字符串信息的请求,而不是一些如二进制,或者key-value形式的数据,就单纯的是一个字符串(所以也决定了他是被大量使用的原因,或者是当前主流使用的原因,因为任何的数据基本上都可以使用字符串来进行获取或者操作,文件也是如此(get的url操作文件方式不就是一个例子吗))
所以当我们进行读取时,就是得到一个字符串,也就是前面的22,但是前面的代码中,我们设置了:
response. setContentType ( "application/json" ) ;
我们也见过这样的处理:
response. setContentType ( "text/html;charset=UTF-8" ) ;
那么他们有什么不同呢:
在这之前,我们在前端中加上如下:
xhr. send ( "22" ) ;
console. log ( xhr. responseText) ;
当然,由于html自身在响应体中的数据也基本都是字符串,所以response.setContentType(“application/json”);也意味着可以一样的操作对应的html信息(与对应设置的是一样的),只是他好像并不能设置连接设置编码,一般需要通用的编码设置,如response.setCharacterEncoding(“UTF-8”);,
那么这个编码设置和response.setContentType(“text/html;charset=UTF-8”);编码设置有什么区别,实际上只是先后处理顺序而已,先操作这个通用的,然后操作这个response.setContentType(“text/html;charset=UTF-8”);的
所以在以后,我们一般也会使用response.setContentType(“application/json”);来代替这个html的设置(当然,可能response.setContentType(“text/html;charset=UTF-8”);是存在其他作用的,这里我们选择忽略了解),所以现在可以确定:
我们基本使用json来处理,那么json到底进行了json的什么处理呢,还是说他其实什么都没有做呢,我们看如下的例子:
前提:实际上从上面可以知道,编码的设置只是请求到响应直接的处理,而jsp其实他自身也存在这样的编码设置,你可以看看jsp是否存在:<%@ page contentType=“text/html;charset=UTF-8” language=“java” %>,他相当于操作了response.setContentType(“text/html;charset=UTF-8”);,这里我们了解即可,其中如果单纯的什么都没有加,好像也就是什么都没有操作,就是把这个字符串给出去,就如response.getWriter().write(“22”);,当然,json进行了什么设置我们还不清楚,但是可以这样的认为:
单纯的给数(如字符串):设置json,html,以及什么都没有设置,结果一样
给出某些特定的数:设置json,html,以及什么都没有设置,结果不一样(当然,这里我们选择考虑html与什么都没有设置是一样的,不考虑编码),所以按照这样的考虑,那么json是进行操作的,其他的基本没有,但是真的是这样的吗,所以我们来测试测试看看他进行了什么操作:
考虑到json可能会操作改变的处理,所以我们操作一下数据,因为字符串他们基本不会改变,所以我们测试如下的几种,如:
map集合,list集合,类,作为返回值进行处理,而由于你设置了一些响应设置,那么前端可能会判断这个设置而进行某些处理,当然,后端可能也会进行某些处理,基本都是围绕着这个设置来处理的,所以我们应该有如下的后端代码来测试一下他应该有的作用,这里我们需要考虑到对应的流只会操作字符串,所以对应的集合一般是如此的:
首先我们操作map:
response. setContentType ( "application/json" ) ; try { response. getWriter ( ) . write ( "{\"message\": \"Hello, world!\"}" ) ; } catch ( Exception e) { e. printStackTrace ( ) ; }
设置list:
response. setContentType ( "application/json" ) ; try { response. getWriter ( ) . write ( "[1,2,3]" ) ; } catch ( Exception e) { e. printStackTrace ( ) ; }
设置类:
首先创建test类:
package controller ; public class test { String message; @Override public String toString ( ) { return "test{" + "message='" + message + '\'' + '}' ; }
}
response. setContentType ( "application/json" ) ; try { test test = new test ( ) ; test. message = "Hello, world!" ; response. getWriter ( ) . write ( test. toString ( ) ) ; } catch ( Exception e) { e. printStackTrace ( ) ; }
结果分别是:
{ "message" : "Hello, world!" }
[ 1 , 2 , 3 ]
test{ message= 'Hello , world! '}
所以我们可以得出结论,这个响应头无任何作用,因为由于响应信息的字符串是绝对的,所以这个响应头是没有作用的,如果说,html的头,可能服务器会自动的进行某些处理(甚至也只是操作了编码而已,同理,这个json作为请求头时也是如此,本质上这些都只是一个请求吧标识,服务器读取这些标识,而产生一些处理,如文件中的某些api需要对应的文件请求标识头),那么这个json则没有任何操作,所以我们应该明白一个问题:
响应头信息基本只是一个提示,具体作用由当前代码的判断(判断响应头),或者服务器自身,又或者前端的某些处理来进行的(比如请求网络中,载荷的展示效果),而非自身的某些处理,实际上任何头都是如此(包括请求头),就如前面的请求头的默认格式(和文件请求头那里的说明),也就是说json的响应头没有任何处理,那么总结如下:
单纯的给数或者给其他的(如字符串):设置json,html,以及什么都没有设置,结果基本一样(不考虑其他的处理,如编码)
那么这个设置也就是一个提示,但是也由于是提示,所以一般的后端或者前端可能会判断然后操作,大多数后端或者前端框架基本都是如此,所以这里我们了解即可,因为他是根据当前情况来分析的(简单来说,前端和后端都会处理请求头和响应头的对应标识,而决定前端和后端的请求发送(前端),请求接收(后端),响应发送(后端),响应接收(前端)等等的一致问题),比如看对应的框架(包括前端和后端)是如何的处理,比如对应的注解@ResponseBody ,当然,知道原理并非意味着对框架熟悉(也就是说,会报错,或者会犯错),因为是需要遵循框架来的,但是原理有利于自行编写框架,和理解框架,一般mvc中@ResponseBody只能有一个,且他是操作json,而由于操作json,导致寻常的通过key-value来获取的处理是操作不了的,因为他只是传递字符串,也只能根据字符串来进行接收(也就是请求载荷,对浏览器来说,他们是不同的处理的)
到这里,我们基本说明完毕,简单来说,请求头和响应头的信息都只是一个信息,然后这些信息会使得浏览器以及服务器进行某些判断处理,也会由框架也进行某些补充判断处理,最终这些信息决定了返回和接收的信息处理方式(如文件通常需要读取其请求体信息)
所以前面所说明的总结论和所有的测试的结果最后得到如此:由各种请求信息和响应信息,组成的一系列的操作方式,由这些操作方式来操作具体的数据(当然,如果需要修改这些方式,自然需要对协议方面进行修改,这就需要看更加原始的操作了,在27章博客最后就有一个小例子)
你可能会有疑惑,直接说出这句话就可以了,为什么需要前面的测试或者铺垫,其实在以后,你就算知道原理,但是原理只是让你知道为什么,而实际操作中的细节是体现不出来的,而上面基本上几乎将所有细节进行了说明,那么以后出现问题,可以选择参照这里,所以说原理需要和实际情况来结合,你知道原理了,但是你的原理只是知道他是这样的,但是中间的其他限制,如需要这样写,需要写在这里你也并不明白,或者说,你只是一个大致的原理(因为你完全没有全部揉碎,只是一个总结的原理而已,而这个原理虽然到达高本质,但是对实现的细节的其他低本质并不清楚,而实践是清楚低本质的操作,所以原理需要结合实践)
至此,我们的请求才算完美的说明完毕,那么就以现在的知识,看后面的内容吧
这个时候,我们回到之前的操作(也就是引出我们说明请求的哪个):
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head><title>Title</title>
</head>
<body>
<script src="js/jquery-3.4.1.min.js"></script><!--<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>-->
<button id="btn">ajax提交</button>
<script>$("#btn").click(function(){let url = 'ajax';let data = '[{"id":1,"username":"张三"},{"id":2,"username":"李四"}]'$.ajax({type:'POST',//大小写可以忽略url:url,data:data,contentType:'application/json;charset=utf-8', //如这里success:function (data) {console.log(data);}})})
</script>
</body>
</html>
在前面我们提到了,需要如下:
processData: false ,
contentType: false ,
为什么processData没有呢,实际上contentType:'application/json;charset=utf-8’中,或者jq的该框架处理了这个请求头,所以默认的处理了processData,即按照这个编码来进行了改变数据,我们可以复现一下:
在前面我们只是说明他会自动的处理,但是原因是什么,我们并没有操作过
按照前面的代码,我们修改前端:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
< html>
< head> < title> Title</ title>
</ head>
< body>
< input type = " file" id = " fileInput" />
< button onclick = " uploadFile ( ) " > 上传</ button>
< script src = " https://code.jquery.com/jquery-3.6.0.min.js" > </ script>
< script> function uploadFile ( ) { let fileInput = document. getElementById ( 'fileInput' ) ; let file = fileInput. files; console. log ( file) sendFileUsingGET ( file) ; } function sendFileUsingGET ( file ) { let formData = new FormData ( ) ; for ( let y = 0 ; y < file. length; y++ ) { formData. append ( 'file' , file[ y] ) ; } formData. append ( 'name' , "22" ) ; $. ajax ( { url : 'upload' , type : 'POST' , data : formData, processData : false , contentType : false , success : function ( data ) { console. log ( data) } , error : function ( error ) { console. log ( data) } } ) ; }
</ script>
</ body>
</ html>
后端如下:
package controller ; import javax. servlet. annotation. MultipartConfig ;
import javax. servlet. annotation. WebServlet ;
import javax. servlet. http. HttpServlet ;
import javax. servlet. http. HttpServletRequest ;
import javax. servlet. http. HttpServletResponse ;
import javax. servlet. http. Part ;
import java. io. InputStream ;
import java. nio. file. Files ;
import java. nio. file. Path ;
import java. nio. file. Paths ;
import java. nio. file. StandardCopyOption ;
import java. util. Collection ; @WebServlet ( "/upload" )
@MultipartConfig ( fileSizeThreshold = 1024 * 1024 * 2 , maxFileSize = 1024 * 1024 * 10 , maxRequestSize = 1024 * 1024 * 50 )
public class FileUploadServlet extends HttpServlet { protected void doPost ( HttpServletRequest request, HttpServletResponse response) { try { System . out. println ( 1 ) ; request. setCharacterEncoding ( "utf-8" ) ; Collection < Part > parts = request. getParts ( ) ; for ( Part part : request. getParts ( ) ) { if ( "file" . equals ( part. getName ( ) ) ) { String name = part. getName ( ) ; String fileName = part. getSubmittedFileName ( ) ; InputStream fileContent = part. getInputStream ( ) ; Path targetPath = Paths . get ( "F:/" + fileName) ; Files . copy ( fileContent, targetPath, StandardCopyOption . REPLACE_EXISTING ) ; response. setContentType ( "text/html;charset=UTF-8" ) ; response. getWriter ( ) . write ( "文件上传成功:" + fileName) ; } } } catch ( Exception e) { e. printStackTrace ( ) ; } }
}
在mvc中处理原始的时候,一般情况下会按照原始的优先处理,比如你可以选择再次的创建一个类:
package controller ; import org. springframework. stereotype. Controller ;
import org. springframework. web. bind. annotation. PostMapping ;
import org. springframework. web. bind. annotation. RequestMapping ; import javax. servlet. http. HttpServletRequest ;
import javax. servlet. http. HttpServletResponse ;
@Controller
@RequestMapping ( "/upload" )
public class up { @PostMapping ( "u" ) public void handleFormUpload ( HttpServletRequest request, HttpServletResponse response) { try { System . out. println ( 2 ) ; response. getWriter ( ) . write ( "Hello, world!" ) ; } catch ( Exception e) { e. printStackTrace ( ) ; } }
}
这个时候,打印了1,而不会打印2,所以原始的优先处理(这个优先只是一次,也就是说,不会执行mvc的了)
但是实际上只是由于@WebServlet的配置大于xml的优先等级,这是servlet的作用(他规定的),否则如果都是配置文件,一般需要看先后顺序了,一般写在前面的是优先的,这里具体需要看版本说明,因为随着版本的改变,可能后写的优先,但一般是写在前面的优先(所以在一个博客说明了谁优先时,建议自己测试一遍)
现在我们去掉一个代码,我们看前端:
$. ajax ( { url : 'upload' , type : 'POST' , data : formData,
contentType : false , success : function ( data ) { console. log ( data) } , error : function ( error ) { console. log ( data) } } ) ;
然后我们继续访问,出现了之前的问题,即jq为了严谨,必须需要设置contentType和processData这个,对应的错误(这个报错可以看,这是jq的提示报错,会随着时间而发生改变的,所以最好自己看,这里就不给出了)
很明显,前面我们说明了"存在自动的处理",说明的就是他们各自的自动处理,如果都是false,那么内容的类型和数据的处理等等会根据值来自动处理(如果不写,那么直接报错,一般必须写的,而基本没有什么默认的处理,认为是防止文件的错误操作),当数据的处理不加时,并且类型是自动时,那么说明数据的处理不会进行,所以报错,但是由于类型通常是数据的处理的前提,所以jq存在若你指定了类型,那么会按照对应类型来自动处理数据(这里有待疑问,后面会揭晓),而不是只是判断类型,所以存在这样的操作了:
contentType : 'application/json;charset=utf-8' ,
修改后,我们继续执行,发现报错,因为对应类型与自动处理的数据类型不对,要知道,自动处理时,是需要根据内容的,而你的类型与内容不同,自然会报错,所以在某种程度上contentType只是一个定义了数据处理的方式的自动处理而已(因为jq为了防止特别的情况,还会判断你设置的类型是否一致的,实际上数据也会判断,我们看后面的总结即可),所以我们需要这样的修改:
contentType: 'multipart/ form- data',
现在我们执行,发现还是执行错误,出现了没有分割符的操作,说明jq的操作在某种程度上,使用了某种原始api设置了分割符,所以我们需要这样:
contentType : "multipart/form-data;boundary=----WebKitFormBoundary85LOXlPgPrx4eO" ,
这个分割符一般情况下,还是建议是很多的处理,或者使用某些原始api生成,这里我们使用以前操作过的分割符来操作的(可以测试一下自定义的,虽然可能会出现问题(如与内容出现一样的)),默认情况下,大多数分割符与multipart/form-data;是空格隔开,但是并不影响对应的获取信息,所以并不需要注意
执行还是报错,加上processData: false,即可,在这里也就说明了processData在没有设置时,其实只是按照字符串来处理的(前提是设置了类型,这里解释contentType:‘application/json;charset=utf-8’,可以单独操作的原因),设置了false,才会按照contentType指定的处理来操作,这里也就又解释了"这里有待疑问,后面会揭晓",即默认不写时是字符串,否则按照指定的处理,如果没有指定,那么都自动处理
总结:
processData和contentType都不加,报错(在jq的不同版本下,有些版本不加可能默认为false(具体可以百度,可能现在不会了),无论是否单独不加都是如此,那么这里就需要考虑为false的情况了,我们注意即可)
processData不加,contentType加false,contentType没有指定,所以processData没有操作,那么必须指定数据处理
processData不加,contentType加指定类型,contentType有指定,所以processData默认操作字符串,这个时候,判断指定类型是否与内容一致,否则报错,若一致,那么判断类型对应的数据处理是否也与字符串的处理一致,否则也报错
processData加false,contentType不加,报错,必须指定类型
processData加false,contentType加false,类型和数据都按照自动来处理,即按照内容来处理
processData加false,contentType加指定类型,判断内容的类型与指定类似是否一致,否则报错,如果一致,那么由于processData加false,所以数据处理与指定类型的数据处理一致
而大多数我们基本在ajax中使用的都是contentType:‘application/json;charset=utf-8’,,所以jq大多数只会操作contentType就行了,其他的一般不会处理,因为存在自动的或者默认的处理
所以可以知道其实无论你是否设置jq都会进行判断,所以有时候写了比不写还是不好的处理,只是不写的话,我们并不能知道他是怎么处理的,不利于维护,并且他也存在判断,所以你可以放心的写,而不会出现数据的问题(如防止文件的错误操作),所以jq的ajax考虑的比较多,但是非常安全,且也存在提示或者说维护的操作
到此,我们也可以知道,前端框架也会存在各种的判断,在以后学习新的处理时,一般来说,考虑到请求头,即类型处理,以及处理的数据即可,因为其他的参数一般都会很直观的给出的
所以我们继续回到这里:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head><title>Title</title>
</head>
<body>
<script src="js/jquery-3.4.1.min.js"></script><!--<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>-->
<button id="btn">ajax提交</button>
<script>$("#btn").click(function(){let url = 'ajax';let data = '[{"id":1,"username":"张三"},{"id":2,"username":"李四"}]'$.ajax({type:'POST',//大小写可以忽略url:url,data:data,contentType:'application/json;charset=utf-8', //如这里success:function (data) {console.log(data);}})})
</script>
</body>
</html>
然后后端代码如下(User类):
String id; String username; public String getId ( ) { return id; } public void setId ( String id) { this . id = id; } public String getUsername ( ) { return username; } public void setUsername ( String username) { this . username = username; } @Override public String toString ( ) { return "User{" + "id='" + id + '\'' + ", username='" + username + '\'' + '}' ; }
package controller ; import org. springframework. web. bind. annotation. RequestBody ;
import org. springframework. web. bind. annotation. RequestMapping ;
import org. springframework. web. bind. annotation. ResponseBody ; import java. util. List ; @Controller
public class ajax { @ResponseBody @RequestMapping ( "ajax" ) public List < User > ajax ( @RequestBody List < User > list) { System . out. println ( list) ; return list; }
}
执行,访问一下前端的ajax提交,若出现了对应的数据,然后看看后端的数据,是否是对的,如果是,那么我们操作成功
这个时候我们需要说明一下@RequestBody和@ResponseBody了,根据前面的说明,很明显@RequestBody是框架或者说依赖的处理,他判断了对应的请求头来进行一下数据的处理,使得可以被list接收,而@ResponseBody同样如此,他也操作了对应的请求处理,只是他默认处理响应头,我们可以在检查元素中的响应标头中看到这个:
Content - Type : application/ json; charset= UTF - 8
也就是说,他也操作了对应的设置,然后也根据框架或者依赖进行初级的反向的处理,要知道,在前面我们说过了"实际上任何头都是如此(包括请求头)",所以他们的设置只是一个值,具体操作都是有代码来处理的,而这个代码一般就是框架或者依赖来完成的,所以在mvc中存在这样的数据转换(也只是根据请求信息来完成的,也就是处理了原生字符串,这里在67章博客可以知道,所以我们了解即可),而在原生中,一般并没有,就如我们前面操作时,对应的值只能是字符串,在中间你可以选择处理,就是这样的情况
为了给出mvc的转换,所以这里给出全部可能(当然,由于只是对参数的变化,所以与文件并不同,即并不会操作问题),由于只有框架会处理,所以这里考虑以下的情况:
原生js和mvc,框架和mvc,当然,我们也会加上标签和mvc的相关处理的
比如,使用标签或者js来说明全部的转换:
首先我们需要创建一个项目:
给出对应的依赖:
< packaging> war</ packaging> < dependencies> < dependency> < groupId> org.springframework</ groupId> < artifactId> spring-webmvc</ artifactId> < version> 5.1.5.RELEASE</ version> </ dependency> < dependency> < groupId> javax.servlet</ groupId> < artifactId> javax.servlet-api</ artifactId> < version> 3.1.0</ version> </ dependency> < dependency> < groupId> com.fasterxml.jackson.core</ groupId> < artifactId> jackson-databind</ artifactId> < version> 2.9.8</ version> </ dependency> < dependency> < groupId> com.fasterxml.jackson.core</ groupId> < artifactId> jackson-core</ artifactId> < version> 2.9.8</ version> </ dependency> < dependency> < groupId> com.fasterxml.jackson.core</ groupId> < artifactId> jackson-annotations</ artifactId> < version> 2.9.0</ version> </ dependency> </ dependencies>
目录如下:
对应的web.xml是如下的:
<?xml version="1.0" encoding="UTF-8"?>
< web-app xmlns = " http://xmlns.jcp.org/xml/ns/javaee" xmlns: xsi= " http://www.w3.org/2001/XMLSchema-instance" xsi: schemaLocation= " http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version = " 4.0" > < servlet> < servlet-name> dispatcherServlet</ servlet-name> < servlet-class> org.springframework.web.servlet.DispatcherServlet</ servlet-class> </ servlet> < servlet-mapping> < servlet-name> dispatcherServlet</ servlet-name> < url-pattern> /</ url-pattern> </ servlet-mapping> </ web-app>
创建controller包,然后创建test类:
package controller ; import org. springframework. stereotype. Controller ;
import org. springframework. web. bind. annotation. RequestMapping ;
@Controller
@RequestMapping ( "/test" )
public class test { @RequestMapping ( "a" ) public void a ( ) { System . out. println ( 1 ) ; }
}
在资源文件中,加上springmvc.xml:
< beans xmlns = " http://www.springframework.org/schema/beans" xmlns: context= " http://www.springframework.org/schema/context" xmlns: xsi= " http://www.w3.org/2001/XMLSchema-instance" xsi: schemaLocation= " http://www.springframework.org/schema/beanshttp://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/contexthttp://www.springframework.org/schema/context/spring-context.xsd" > < context: component-scan base-package = " controller" /> </ beans>
在补充web.xml:
< servlet> < servlet-name> dispatcherServlet</ servlet-name> < servlet-class> org.springframework.web.servlet.DispatcherServlet</ servlet-class> < init-param> < param-name> contextConfigLocation</ param-name> < param-value> classpath:springmvc.xml</ param-value> </ init-param> </ servlet>
先执行访问这个路径,看看是否打印,若打印了,那么我们操作如下:
首先创建index.jsp:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head><title>Title</title>
</head>
<body><form action="test/a" method="post"><input type="text" name="text1"><input type="text" name="text2"><input type="text" name="text3"><input type="submit" value="提交">
</form></body>
</html>
现在,我们修改这样的:
package controller ; import org. springframework. stereotype. Controller ;
import org. springframework. web. bind. annotation. RequestMapping ;
@Controller
@RequestMapping ( "/test" )
public class test { @RequestMapping ( "a" ) public void a ( String text1) { System . out. println ( text1) ; System . out. println ( 1 ) ; }
}
使用post和get来访问(后面都这样处理),如果出现对应的数据,代表操作完成,如果是这样呢:
package controller ; import org. springframework. stereotype. Controller ;
import org. springframework. web. bind. annotation. RequestMapping ; import java. util. Arrays ; @Controller
@RequestMapping ( "/test" )
public class test { @RequestMapping ( "a" ) public void a ( String [ ] text1) { System . out. println ( Arrays . toString ( text1) ) ; System . out. println ( 1 ) ; }
}
前端则需要这样:
<form action="test/a" method="get"><input type="text" name="text1"><input type="text" name="text1"><input type="text" name="text1"><input type="submit" value="提交">
</form>
或者:
<form action="test/a" method="get"><input type="text" name="text1"><input type="text" name="text2"><input type="text" name="text3"><input type="submit" value="提交">
</form>
分别得到1,2,3和1(get和post都是如此)
如果后端是这样呢(创建一个类):
package controller ; public class User { private String id; private String name; private String pass; @Override public String toString ( ) { return "User{" + "id='" + id + '\'' + ", name='" + name + '\'' + ", pass='" + pass + '\'' + '}' ; } public String getId ( ) { return id; } public void setId ( String id) { this . id = id; } public String getName ( ) { return name; } public void setName ( String name) { this . name = name; } public String getPass ( ) { return pass; } public void setPass ( String pass) { this . pass = pass; }
}
然后对应的controller修改如下:
package controller ; import org. springframework. stereotype. Controller ;
import org. springframework. web. bind. annotation. RequestMapping ;
@Controller
@RequestMapping ( "/test" )
public class test { @RequestMapping ( "a" ) public void a ( User text1) { System . out. println ( text1) ; System . out. println ( 1 ) ; }
}
那么前端需要这样(get和post都是如此):
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head><title>Title</title>
</head>
<body><form action="test/a" method="post"><!--name(属性,而不是属性值,name="id"中name是属性,说明的就是这个)中的大小写一般是忽略的--><input type="text" name="id"><input type="text" name="name"><input type="text" name="pass"><input type="submit" value="提交">
</form></body>
</html>
如果是类数组呢,也就是这样的:
package controller ; import org. springframework. stereotype. Controller ;
import org. springframework. web. bind. annotation. RequestMapping ;
@Controller
@RequestMapping ( "/test" )
public class test { @RequestMapping ( "a" ) public void a ( User [ ] text1) { System . out. println ( text1) ; System . out. println ( 1 ) ; }
}
实际上mvc并不支持将对象数组在标签中进行处理,这是为什么,假设有数组(Object[]),你怎么处理呢,所以大多数对象数组,一般并不会直接的自动处理,即需要手动的处理,当然了,基础类型(可以是Integer,String,等等)的数组是可以的,所以一般来说,如果没有手动处理的话,那么自动的处理就会报错,即这种情况我们忽略(当然,这种情况是可以判断解决的,只是mvc没有进行处理,因为手动处理是最好的方式,特别的因为出现这种情况的代码一般是复杂的,即一般会手动,那么mvc一般也不会这样的判断处理的)
如果是这样的呢:
package controller ; import org. springframework. stereotype. Controller ;
import org. springframework. web. bind. annotation. RequestMapping ; import java. util. Arrays ;
import java. util. List ; @Controller
@RequestMapping ( "/test" )
public class test { @RequestMapping ( "a" ) public void a ( List < String > text1) { System . out. println ( text1. toArray ( ) ) ; System . out. println ( Arrays . toString ( text1. toArray ( ) ) ) ; System . out. println ( 1 ) ; }
}
实际上mvc也不支持List在标签中的处理(因为同样的List中也会存在Object的情况)
如果后端是如下呢:
package controller ; import org. springframework. stereotype. Controller ;
import org. springframework. web. bind. annotation. RequestMapping ; import java. util. Arrays ;
import java. util. List ;
import java. util. Map ; @Controller
@RequestMapping ( "/test" )
public class test { @RequestMapping ( "a" ) public void a ( Map < String , String > text1) { System . out. println ( text1) ; System . out. println ( 1 ) ; }
}
同理,由于里面也会存在Object,所以也不支持,但是如果是这样呢:
创建一个类:
package controller ; import java. util. List ;
import java. util. Map ; public class QueryVo { String id; User user; List < User > userList; Map < String , User > userMap; @Override public String toString ( ) { return "QueryVo{" + "id='" + id + '\'' + ", user=" + user + ", userList=" + userList + ", userMap=" + userMap + '}' ; } public String getId ( ) { return id; } public void setId ( String id) { this . id = id; } public User getUser ( ) { return user; } public void setUser ( User user) { this . user = user; } public List < User > getUserList ( ) { return userList; } public void setUserList ( List < User > userList) { this . userList = userList; } public Map < String , User > getUserMap ( ) { return userMap; } public void setUserMap ( Map < String , User > userMap) { this . userMap = userMap; }
}
前端需要这样写:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head><title>Title</title>
</head>
<body><form action="test/a" method="post">搜索关键字:<input type="text" name="keyword"> <br>user对象:<input type="text" name="user.id" placeholder="编号"><input type="text" name="user.username" placeholder="姓名"><br>list集合<br>第一个元素:<input type="text" name="userList[0].id" placeholder="编号"><!--可以写0或者不写,当然有些版本可能必须要写0了--><input type="text" name="userList[0].username" placeholder="姓名"><br>第二个元素:<input type="text" name="userList[1].id" placeholder="编号"><input type="text" name="userList[1].username" placeholder="姓名"><br>map集合<br>第一个元素:<input type="text" name="userMap['u1'].id" placeholder="编号"><input type="text" name="userMap['u1'].username" placeholder="姓名"><br>第二个元素:<input type="text" name="userMap['u1'].id" placeholder="编号"><input type="text" name="userMap['u1'].username" placeholder="姓名"><br><input type="submit" value="复杂类型">
</form></body>
</html>
当你写上后,把数据都加上,只有匹配的才会进行处理(这里其实在67章博客就有说明的)
当然,如果你将对应的集合的User修改成了String,那么就需要这样:
list集合<br>第一个元素:<input type="text" name="userList[0]" placeholder="编号"><!--可以写0或者不写,当然有些版本可能必须要写0了--><input type="text" name="userList[0]" placeholder="姓名"><br>第二个元素:<input type="text" name="userList[1]" placeholder="编号"><input type="text" name="userList[1]" placeholder="姓名"><br>map集合<br>第一个元素:<input type="text" name="userMap['u1']" placeholder="编号"><input type="text" name="userMap['u1']" placeholder="姓名"><br>第二个元素:<input type="text" name="userMap['u1']" placeholder="编号"><input type="text" name="userMap['u1']" placeholder="姓名"><br>
他们是会处理合并的,在67章博客有具体说明的哦,并且前面的关于这样的操作其实get和post都是一样的处理,为什么这里支持了,这是因为有了选择,之前的直接的list和map我们是难以确定的,就如list和数组有相似的处理,比较难以分辨,而map与集合也有相似的,所以导致只能在其他的类里面才能进行处理了,实际上这些基本在请求字符串(前面或多或少可以知道)中操作的,所以关于js的处理就不说明了,只操作标签的处理了,然而在某种情况前端如果直接传递对象的话,那么后端是怎么处理的,实际上是前端怎么处理的,在前面我们知道是操作了类型最终处理的,一般情况下,对象会考虑变成键值对,使得是我们正常的情况,其他的一般不会处理,也就是说,至此我们应该操作完毕了,而对字符串的处理我们实际上只需要看68章博客即可(而70章博客中,有具体的变化情况的说明),这里唯一需要的是知道直接的List和Map和对象数组不能处理(当然,也不一定,在某种程度上,可能是需要一些操作,或者需要其他版本的),所以一般情况下,我们基本只会使用字符串来进行数据的交互的,而不是操作自动的处理的(因为他们的变化,最终还是字符串的,倒不如直接回到原来的地方呢,没有必要多次一举,所以我们操作json就行了)
由于博客字数限制,其他内容,请到下一篇博客(111章博客)去看