csdn推荐
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方法,他们的资源利用是相同的吗,就是这样的问题,答:不是相同的,你这里可能会有疑惑,为什么不是相同的,大多数人可能会这样的认为,既然是调用,那么你都是拿取堆里面对象调用,只是其中一个被多个线程拿取的多,然而,拿取是同时进行的,自然速度一致,实际上这个速度在大多数情况或者实际情况可能正确,但是这里我并不这样的认为,解释如下:
/*
由于数据都是存在硬件层面的,所以这里以硬件层面来进行说明:
在硬件层面,多个线程同时读取同一个数据时,会存在电路访问冲突的问题(既然都是拿取他,总不能一条线走你们两个电吧,这就是电路访问冲突),这时当多个线程同时访问同一个数据时,可能会出现竞争条件,导致电路冲突和数据不一致的问题,然而,现代计算机在处理多线程并发读取时会采用各种优化措施来尽量提高并发性和效率,以下是一些硬件层面上的优化技术,使得多个线程可以在某种程度上同时读取同一个数据:
处理器缓存:现代处理器通常具有多级缓存,包括L1、L2、L3等级别的缓存,当多个线程同时访问同一个数据时,处理器会尽量从缓存中读取数据,而不是直接从主存中获取,这样可以避免不必要的主存访问冲突,提高读取速度
缓存一致性协议:在多核处理器中,各个核心的缓存之间需要保持一致性,以确保读取到的数据是最新的,常用的缓存一致性协议(如MESI、MOESI等)可以有效地解决缓存一致性问题,使得多个核心可以同时读取同一个数据
数据预取:处理器会根据访存模式和预测算法预取数据到缓存中,以减少对主存的访问延迟,这可以提前将数据加载到缓存中,以备后续线程的读取操作,从而减少冲突和等待时间
总结来说,尽管在硬件层面上存在电路访问冲突的问题,但现代计算机通过缓存、缓存一致性协议和数据预取等技术来优化并发读取,以实现尽可能的同时读取性能,这些技术可以减少冲突,提高并发性,使得多个线程可以近似同时读取同一个数据
所以可以说,读取根本来说并不是同时读取,只是读取的速度够快,所以看起来是同时读取的,并且也足够快,且不会造成数据的问题,所以我们通常只会考虑并发情况下的写操作,而非读操作(不会改变数据不考虑,因为读的操作并不会改变数据,所以无论什么时候读,都没有影响,也就基本不会考虑多线程的问题,除非必须要考虑读的一致性,比如需要读取的数据进行写或者其他操作,而非只进行读),同样的在硬件层面出现的问题,在软件层面必然也会出现(这是底层决定上层,就算没有,也只是降低这样的影响,而非完全消除)
所以综上所述,当多个线程访问同一个类里面的方法和分开访问时,访问同一个类的两个方法需要的时间更加的多,而不是同样的时间,但是这些速度在考虑MVC的方便性上是微不足道的,这也是为什么如果你自己写servlet和使用MVC时MVC虽然可能会慢点,但是还是使用MVC的主要原因,因为他提升的速度还不足以让我放弃这个方便性,方便性有时也会称为维护性,并非代码越少越好维护,是需要考虑非常多的情况的,MVC的内部代码可比你写的要多很多,但我们还是使用MVC的
这里也就提到了,维护性和性能他们之间有个临界点使得我们要考虑谁了,而MVC是考虑维护性,这也是为什么大多数我们会写循环,而非直接的都进行打印,这就是考虑方便性,在性能问题上循环虽然需要更多的时间(循环自身需要操作,自然绝对的比单纯打印慢,循环+打印!=打印),但是方便许多
上面的读取操作需要一致性(与实际情况一致)的例子:
考虑到读取必然会重新读取,所以读取的一致性通常认为是互相不一致的,假如,有三个人一起聊天
如A,B,C一起聊天,其中有两个消息,A和B看到了,他们两个刷新界面,后台开始重新读取,这个时候C发送消息,然后A和B刷新完毕,突然发现,A有三个消息,而B只有两个,这就是互相不一致问题,所以读取在某些情况下,是需要考虑一致性的,也就是互相一致,最终是一致性问题
*/
大致流程如下:
即数据到控制器到视图,最终到响应,给我们显示,实际上控制器在一定程度上也可以和数据结合一起,只是为了解耦合所以我们通常也分开变成数据(业务了),所以如果非要精准的说的话,MVC只需要控制器(包括数据)以及视图就行,所以才会说SpringMVC是表现层的框架,而不包括数据之类的框架说明,当然,更加具体的在手写框架时,会明白的,现在可以大致了解 Spring Web MVC 工作流程: 需求:前端浏览器请求url::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:
<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;
}
}
//也可以这样
//放入参数,默认给你创建,其中里面的Model也行(虽然参数为Model时,只会是数据,而非视图)
public ModelAndView handle01(ModelAndView modelAndView) {
Date date = new Date(); //服务器时间
//返回服务器时间到前端页面
modelAndView.addObject("date", date);
modelAndView.setViewName("success");
return modelAndView; //手动的返回数据和视图给前端控制器来处理
}
在WEB-INF文件夹下,创建jsp目录,然后创建如下success.jsp文件:
Title
跳转成功!服务器时间是:${date}
在资源文件夹下加上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/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/mvc
http://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,那么启动服务器,访问: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 九⼤组件:
/*
1:HandlerMapping(处理器映射器):
HandlerMapping 是用来查找 Handler 的,也就是处理器,具体的表现形式可以是类(类的形式可以百度,这里就不说明了,虽然最终都是方法的执行,只是类的形式可能需要多个类,或者一个类,具体情况看当时的操作方式,只是现在这种一般不会使用了,所以了解即可,一般需要实现implements org.springframework.web.servlet.mvc.Controller接口,他里面的那个方法一般就是操作的方法,即类似于Handler),也可以是方法,⽐如,标注了@RequestMapping的每个方法都可以看成是一个Handler,Handler负责具体实际的请求处理,在请求到达后,HandlerMapping 的作用便是找到请求相应的处理器Handler 和 Interceptor(或者拦截的东西,虽然是对应的),他主要的作用是找到具体的类,并创建他的对象
2:HandlerAdapter(处理器适配器):
HandlerAdapter 是一个适配器,因为 Spring MVC 中 Handler 可以是任意形式的,只要能处理请求即可,但是把请求交给 Servlet 的时候,由于 Servlet 的方法结构都是doService(HttpServletRequest req,HttpServletResponse resp)形式的(这里的doService应该是doxxx),要让固定的 Servlet 处理方法调用 Handler 来进行处理,便是 HandlerAdapter 的职责,他主要的作用是利用映射器创建的对象来进行调用,大多数方法里面的参数设置的值,就是他来完成的
3:HandlerExceptionResolver:
HandlerExceptionResolver 用于处理 Handler 产⽣的异常情况,它的作用是根据异常设置ModelAndView,之后交给渲染方法进行渲染,渲染方法会将 ModelAndView 渲染成⻚⾯(这也是为什么我们可以得到异常信息,而非空的,我们在浏览器上看到的异常信息都是处理好的,否则没有任何数据出现(主要看响应体),当然,除了一些框架或者软件自定义的处理,浏览器或者协议之间也存在默认的处理,比如在服务器发生报错时,可能在浏览器中会出现"无法访问此网站"这个信息(默认的))
4:ViewResolver:
ViewResolver即视图解析器,用于将String类型的视图名和Locale解析为View类型的视图,只有一个resolveViewName()方法(两个参数:视图名和Locale),从方法的定义可以看出,Controller层返回的String类型视图名viewName 最终会在这⾥被解析成为View,View是用来渲染⻚⾯的,也就是说,它会将程序返回的参数和数据填⼊模板中,⽣成html⽂件(如jsp的),ViewResolver 在这个过程主要完成两件事情:
ViewResolver 找到渲染所用的模板(第一件⼤事)和所用的技术(第⼆件⼤事,其实也就是找到视图的类型,如JSP)并填⼊参数,默认情况下,Spring MVC会⾃动为我们配置一个InternalResourceViewResolver,是针对 JSP 类型视图的
5:RequestToViewNameTranslator:
RequestToViewNameTranslator 组件的作用是从请求中获取 ViewName(ViewName就是值,View是拼接后的视图),因为 ViewResolver 根据ViewName 查找 View,但有的 Handler 处理完成之后,没有设置 View,或者说也没有设置 ViewName,便要通过这个组件从请求中查找ViewName,默认情况下,当我们没有给出视图名时,会将请求参数(一般是整个请求路径,即@RequestMapping对应的,这个参数通常并不是方法的参数,方法参数和请求参数是不同的,并且这里也通常不代表给出的请求参数,所以在某种情况下,我们建议使用请求路径来代表这里,即会将请求路径作为拼接参数)作为拼接对象,这个组件一般我们并没有使用,通常使用在手动封装返回响应体时的处理,具体可以百度,这里我经过测试:首先将视图解析器注释,并且视图名也注释,请求http://localhost:8080/springmvc/demo/handle01得到源服务器未能找到目标资源的表示或者是不愿公开一个已经存在的资源表示(这是因为当没有视图操作时,自然什么都不会出现,自然会触发没找到(浏览器与http中的处理中,当没有数据返回时就会这样,更何况这里SpringMVC进行处理了,而不操作浏览器默认,使得浏览器得到规定SpringMVC默认的错误页面),这个时候你可以选择手动操作,而不是进行拼接,当然,这里考虑的是拼接的),当视图解析器不注释,继续访问这个,得到[/WEB-INF/jsp/demo/handle01.jsp]未找到,也就是说,他默认将请求的URL,即demo/handle01作为拼接的视图名(正好是找到该方法的具体URL,而不是最后的handle01),正好对应于"当我们没有给出视图名时,会将请求参数作为拼接对象"
但是也存在这样的操作:
如果是这样的
@Controller
@RequestMapping("/test")
public class upload {
@RequestMapping("handle11")
public String handle11(ModelAndView modelAndView) {
System.out.println(1);
Date date = new Date();
modelAndView.addObject("date", date);
return "success.jsp";
}
}
那么我们可以发现,他有视图名称,但是对应的结果却是在项目里面的:xxx/test/success.jsp,xx代表项目名称
当我们注释掉前面的test,访问对应的url,会得到在xxx/success.jsp,所以实际上视图本身也会存在路径的问题,在这里我们可以知道,视图与类上的路径是有一定联系的,而没有视图时,则与全部路径有联系,这里再67章博客我们并没有进行说明,所以需要注意
6:LocaleResolver:
ViewResolver 组件的 resolveViewName 方法需要两个参数,一个是视图名,一个是 Locale(中文意思一般是:区域),LocaleResolver 用于从请求中解析出 Locale,⽐如中国 Locale 是 zh-CN,用来表示一个区域,这个组件也是 i18n 的基础,一般操作默认,所以前面的方法中,我们并没有进行处理
7:ThemeResolver:
ThemeResolver 组件是用来解析主题的,主题是样式、图⽚及它们所形成的显示效果的集合,Spring MVC 中一套主题对应一个 properties⽂件,⾥⾯存放着与当前主题相关的所有资源,如图⽚、CSS样式等,创建主题⾮常简单,只需准备好资源,然后新建一个"主题名.properties"并将资源设置进去,放在classpath下,之后便可以在⻚⾯中使用了,SpringMVC中与主题相关的类有ThemeResolver、ThemeSource和Theme,ThemeResolver负责从请求中解析出主题名,ThemeSource根据主题名找到具体的主题,其抽象也就是Theme,可以通过Theme来获取主题和具体的资源,这里可以选择百度查看,所以了解即可
8:MultipartResolver:
MultipartResolver 用于上传请求,通过将普通的请求包装成 MultipartHttpServletRequest 来实现,MultipartHttpServletRequest 可以通过 getFile() 方法 直接获得⽂件,如果上传多个⽂件,还可以调用 getFileMap()方法得到Map这样的结构,MultipartResolver 的作用就是封装普通的请求,使其拥有⽂件上传的功能
9:FlashMapManager:
FlashMap 用于重定向时的参数传递,⽐如在处理用户订单时候,为了避免重复提交,可以处理完post请求之后重定向到一个get请求,这个get请求可以用来显示订单详情之类的信息,这样做虽然可以规避用户重新提交订单的问题,但是在这个⻚⾯上要显示订单的信息,这些数据从哪⾥来获得呢,因为重定向时没有传递参数这一功能的,如果不想把参数写进URL(不推荐写入URL),那么就可以通过FlashMap来传递,只需要在重定向之前将要传递的数据写⼊请求(可以通过ServletRequestAttributes.getRequest()方法获得)的属性OUTPUT_FLASH_MAP_ATTRIBUTE中,这样在重定向之后的Handler中Spring就会⾃动将其设置到Model中,在显示订单信息的⻚⾯上就可以直接从Model中获取数据,FlashMapManager 就是用来管理 FalshMap 的
总结:
1:HandlerMapping(处理器映射器):创建对象
2:HandlerAdapter(处理器适配器):使用创建的对象的方法,也操作好了参数
3:HandlerExceptionResolver:处理异常的
4:ViewResolver:操作视图的解析,即视图解析器,需要两个参数:视图名和Locale
5:RequestToViewNameTranslator:从请求中拿取视图名
6:LocaleResolver:给视图需要的Locale,一般存在默认,所以通常不处理
7:ThemeResolver:主题,了解即可
8:MultipartResolver:文件上传的处理
9:FlashMapManager:给重定向时的(参数)数据
这些组件或多或少可以放入到参数列表中,但是通常是不行的,具体可以自己测试,SpringMVC会自动进行处理给出对象的,但是有些组件是可以的,就如前面的public ModelAndView handle01(ModelAndView modelAndView) {中,可以写上ModelAndView一样,同样的,上面的组件可能也会存在一些操作他们的参数写上,比如文件上传的MultipartHttpServletRequest可以作为参数来操作MultipartResolver
当然还存在一个组件:DispatcherServlet(前端控制器),然而他是主要的中转,所以一般是基础操作,而非将他放入组件的行列,当然,你也可以放入,因为他的确也操作了组件功能,所以这里也可以称为十大组件
10:DispatcherServlet(前端控制器):
用户请求到达前端控制器,它就相当于 MVC 模式中的 C(他代表C的根本处理,所以说成C也不为过),DispatcherServlet 是整个流程控制的中心,由它调用其它组件处理用户的请求,DispatcherServlet 的存在降低了组件之间的耦合性,因为只需要与他建立关系即可,而不用都进行建立关系,这样就不用将所有的组件串起来了
实际上也可以简便的说明,即存在三大组件:
处理器映射器:HandlerMapping
处理器适配器:HandlerAdapter
视图解析器:ViewResolver
若是四大组件:
那么在上述三大组件的基础上加上:
前端控制器:DispatcherServlet
对于文件上传,大多数情况下,原生(算是吧)我们利用DiskFileItemFactory即可(import org.apache.commons.fileupload.disk.DiskFileItemFactory;),依赖一般是:
commons-fileupload
commons-fileupload
1.2.1
而框架,如MVC的,我们利用MultipartResolver即可(当然,里面也是利用了依赖,再spring-web里面)
那么真正的原生是怎么处理的,实际上这是考虑服务器中对http请求的数据拿取了,这里了解即可,若需要知道,可以选择去造一下轮子(网上找资源吧),学习指点:利用网络编程来完成文件的操作,自然也就完成图片的操作了
*/
请求参数绑定: 也就是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);
}
//那么他是怎么变成的呢,这里再67章博客有细节的说明看看即可,然而细节我们并不需要太死磕的,因为技术会更新,那么细节可能也会发生改变,并且这也不需要记住,大多数自动变化会合理的,所以看看即可
所以这里的参数绑定在一定程度上可以认为是取出参数值绑定到handler⽅法的形参上 在前面我们也可以这样(在DisController类中加上如下):
@RequestMapping("handle11")
public String handle11(ModelAndView modelAndView) {
Date date = new Date();
modelAndView.addObject("date", date);
return "success"; //不会设置,因为他是modelAndView(他一般是给你创建的,而不是拿取固有的)
}
@RequestMapping("handle21")
public String handle21() {
Date date = new Date();
ModelAndView modelAndView = new ModelAndView();
modelAndView.addObject("date", date);
return "success"; //没有数据date,因为是我们创建的
}
//下面这三个才会可以
@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";
}
/*
执行访问,看看结果,从结果可以看到,若没有返回ModelAndView的话,那么使用他是不能得到数据的,只能使用他里面的成员变量ModelMap来处理,即我们只会看返回值(返回整体就是他,返回部分,需要特别的给出部分,而不是整体对象,即需要这些Model,ModelMap,Map等等,即他们就是部分),即一般情况下,我们一个线程可能只能操作一个ModelAndView(完整的),且当操作完,即返回后,那么自然清除,如果你直接的返回,说明你操作了他里面的视图,那么这里只能是ModelMap(需要完整的才行,且需要是同一个,且根据返回值来决定的),这可能是内部的操作方式(有可能是,他一般是给你创建的,而不是拿取固有的,其他三个是拿取固有的),所以了解即可
当然也存在Model和map来替换ModelMap操作,且Model或者ModelMap中,他们保存的数据最终通常都是保存在request域里面的
所以简单来说,Model,ModelMap,最终都是操作map的(所以可以直接使用map作为参数),并且他们map里面的值最终会保存到request中,你可以打印他们,因为他们的对象我框架给的,你可以选择看一看,经过测试打印后,他们的对象是一样的,所以他们基本都是同一个对象,只是操作方式不同而已,最终都是相同的操作,即操作map的,并且最终给到request域中,至此我们说明完毕
在对应的return前面加上如下即可:
System.out.println(modelMap);
System.out.println(modelMap.getClass());
System.out.println(model);
System.out.println(model.getClass());
System.out.println(map);
System.out.println(map.getClass());
对应我的打印结果是:
{date=Thu Jul 06 17:07:24 CST 2023}
class org.springframework.validation.support.BindingAwareModelMap
{date=Thu Jul 06 17:07:26 CST 2023}
class org.springframework.validation.support.BindingAwareModelMap
{date=Thu Jul 06 17:07:45 CST 2023}
class org.springframework.validation.support.BindingAwareModelMap
即对象的确是一个对象
并且你查找这个对象,在里面看看,可以发现,他是其他的子类,其中ModelMap和Model同级,看如下:
public class BindingAwareModelMap extends ExtendedModelMap {
public class ExtendedModelMap extends ModelMap implements Model {
public interface Model {
public class ModelMap extends LinkedHashMap {
public class LinkedHashMap
extends HashMap
implements Map
{
所以前面的:
modelMap.addAttribute("date", date);
model.addAttribute("date", date);
map.put("date", date);
最终都是操作BindingAwareModelMap对象里面的对应方法,且最终的结果都是操作map,并且最终给到request域(一般称为请求域,因为一般是保存请求的信息的)中,使得可以在前面jsp中使用${date}获得(具体为什么可以获得,可以去52章博客查看)
*/
默认⽀持 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的名称(或者说映射的),所以我们访问: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;
}
访问: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就是String id = "true";
而id="true"就是id = ""true"";
这里要注意
*/
所以由于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 的应用程序接口(API),它强调使用统一的、无状态的、可扩展的和可缓存的方式进行通信
Restful 风格的请求具有以下优点:
1:可读性强:Restful 风格的 API 设计具有清晰的结构,使用直观的 HTTP 方法(GET、POST、PUT、DELETE)和资源路径,易于理解和使用,一般是如下的情况:
GET:读取(Read)
POST:新建(Create)
PUT:更新(Update)
DELETE:删除(Delete))
2:可扩展性好:Restful 风格的请求允许通过添加新的资源路径来扩展 API,使得系统可以更容易地支持新的功能和端点
3:可移植性强:Restful 风格的请求使用标准的 HTTP 方法和状态码,使得 API 可以在不同的平台和技术之间进行移植和互操作
4:缓存支持:Restful 风格的请求利用 HTTP 协议的缓存机制,可以有效地利用缓存来提高性能和减少服务器负载
然而,使用 Restful 风格的请求并不是强制性的,在某些情况下,非 Restful 风格的请求也可以满足特定的需求,例如,当需要传输大量数据、进行复杂的操作或需要遵循特定的业务规则时,可能需要使用非 Restful 风格的自定义请求(具体可以百度,因为并不是存在上面的四种情况,只是存在共性而已,比如要读取非常多的情况,并且参数非常多,这个时候利用get是否好呢,很明显,我们可能是使用post的)
总而言之,使用 Restful 风格的请求通常是一个良好的设计选择,尤其适用于构建基于 Web 的 API,但在实际开发中,根据项目需求和特定情况,选择合适的请求风格是很重要的,因为该风格是参照URL的,而URL一般也存在限制,对于需要指定参数,一般都不会使用这个(风格并非标准,可以选择可以不选,所以自然可以共存多种风格,只是不建议在大项目中共存而已,因为在大项目中一般需要统一一种方式,并且可以完成需求的方式,所以越原始但是比较不原始的处理方法有时是比较好的,之所以我完全使用原生是因为太麻烦,即得到比付出的要少的),所以大多数情况下,该风格也只是适用于平常项目,而非复杂的项目(实际上越没有花里胡哨(该风格可以认为是花里胡哨)的,针对大项目来说是越友好的,因为能够更好的处理细节)
*/
即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设计一般是::8080/user/queryUserById?id=3 上面不难看出,url中定义了动作(操作),因为queryUserById一看就知道是查询用户的id,所以参数具体锁定到操作的是谁 有了rest⻛格之后,那么设计应该是如下: 由于rest中,认为互联⽹中的所有东⻄都是资源,既然是资源就会有一个唯一的uri标识它,代表它,如: :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:
Title
rest_get测试
然后再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 {
//对应与:查询指定id为15的数据
@RequestMapping(value = "/handle/{id}", method = {RequestMethod.GET}) //后面的请求方式也会当成拦截成功的条件的,否则相当于不存在,但是若对应的路径存在,只是请求方式不同,那么会报错,而非不存在,这里需要注意的,即总体流程中,首先整体考虑(路径加上请求都正确),然后局部考虑(路径正确,请求不正确),最后路径考虑(路径不正确,这个时候无论你请求是否正确都没有用的,因为路径首先是需要满足的),那么判断流程是:首先查看路径是否存在,若存在,那么查看请求是否正确,其中若路径没有,那么没有找到,若路径找到了,但是请求不正确,那么报错(默认如果不写请求的话,一般都是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;
}
//对应与:更新,那么自然需要一个数据,也就是list(可以认为是姓名,如改成这个姓名,当然,后面还可以继续的补充),当然,有时候并不操作put,所以需要加上list才可,来防止后面请求的冲突,如后面的删除
@RequestMapping(value = "/handle/{id}/{name}", method = {RequestMethod.PUT}) //记得改成POST
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;
}
//对应与:删除指定的id的数据
@RequestMapping(value = "/handle/{id}", method = {RequestMethod.DELETE}) //记得改成POST
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),可是不会操作后续的),这是为什么,因为只有当前的我们的请求是进行处理的,而转发,并不会进行处理,但是他是在内部进行的,所以错误信息也是不同的,为了验证转发不行,我们可以修改一下,修改如下:
//对应与:删除指定的id的数据
@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文件:
<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: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/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<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,放入进去: 提取码: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):
Title
$("#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);
}
})
})
对应的路径我们不加上斜杠开头,因为一般代表是端口开始的,若测试后,对应前端打印的信息得到了,那么代表操作成功,注意:一般情况下,默认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 {
//注意:他们只是一个补充而言,真正的初始化其实已经操作的,所以你只需要知道他们只是一个执行顺序而言,只是由于名称的缘故,所以我们一般会将具体需求进行分开处理
//大多数框架的什么初始化都是这样的说明,是一样的意思
//void init(ServletConfig config),由servlet容器调用,以向servlet指示servlet正在被放入服务中
@Override
public void init(ServletConfig servletConfig) throws ServletException {
System.out.println("初始化");
}
//ServletConfig getServletConfig(),返回ServletConfig对象,该对象包含此servlet的初始化和启动参数
@Override
public ServletConfig getServletConfig() {
return null;
}
//void service(ServletRequest req,ServletResponse res),由servlet容器调用,以允许servlet响应请求,主要操作get和post的
//这里才是post和get真正的调用者,其实大多数我们在servlet中,我们一般会继承过一个类HttpServlet,他里面的doGet和doPost最终是这个执行的,你可以看看他里面的方法就知道了(后面会进行模拟的)
//这里我会进行模拟的,根据这里的说明,可以认为mvc也是如此,只是在下层继续封装了(后面手写时会知道的),最终还是会到这里的
@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(""
+ 11 + "");
}
//String getServletInfo(),返回有关servlet的信息,如作者、版本和版权
@Override
public String getServletInfo() {
return null;
}
//void destroy(),由servlet容器调用,以向servlet指示该servlet正在退出服务
@Override
public void destroy() {
System.out.println("正在销毁中");
}
}
web.xml如下:
<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>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一样的):
Title
1
在启动后,在项目路径后面加上get,访问后,若出现对应的11(h1标签形式的),说明我们创建项目并测试成功 这下我们环境操作完毕,现在来进行原生ajax的处理: 这里我们修改index.jsp,或者加上如下代码:
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)
}
}
}
启动,点击访问,出现如下(前端的,前端的出现了,那么后端的就不用看了,所以这里就不给出了,后续也是如此):
/*
下一个
第一个
11
*/
为什么"第二个"没有显示,这是因为当一个回调被设置后,就不能继续被设置了,并且在回调过程中,自然解除了同步(false),所以根据顺序操作,一般回调比较慢,所以是"下一个"先打印,这种细节是没有必要的,所以我们再次的修改,等待是send中进行的,这个要注意,所以我们修改如下:
function run1(){
let x;
//判断浏览器类型,并创建核心对象
if(window.XMLHttpRequest){
x =new XMLHttpRequest();
}else{
x = new ActiveXObject("Microsoft.XMLHTTP");
}
//建立连接.get方式,资源路径,是否异步
//GET大小写忽略
x.open("GET","get",false);
//提交请求,这里可以选择修改上面open的值,再来访问
x.send();
let text =x.responseText;
console.log(text)
}
启动测试,查看打印信息(前端的),显示如下:
/*
11
*/
现在我们给他加上参数,分别需要测试如下几种情况(这个只需要测试一次就行了,前面也说明了这样的情况):
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(""
+ 11 + "");
}
经过测试,上面的22种情况分别是:
/*
1:null,null,null,null,null,null
2:null,null,,null,null,null
3:null,null,null,null,null,null
4:null,null,null,null,null,null
5:null,null,null,null,null,null
6:null,null,null,null,null,1
7:null,null,null,null,null,null
8:null,null,null,null,null,null
9:,null,null,null,null,null
10:,null,null,null,null,null
11:,null,null,null,null,null
12:,null,null,null,null,null
13:,null,null,null,null,null
14:1,null,null,null,null,null
15:1?,null,null,null,null,null
16:1,null,null,null,null,null
17:1,,null,null,null,null
18:1,,null,null,null,null
19:null,,null,null,null,null
20:访问失败么有找到资源(前端访问后端报错了,即没有找到后端,后端将错误信息返回给前端打印出来了)
21:与20一样的错误
22:1,null,null,null,null,null,这里很明显,只是拿取第一个,即写在前面的一个
*/
总结:
/*
1:x.open("GET","get?",false);,null,null,null,null,null,null
2:x.open("GET","get??",false);,null,null,,null,null,null
3:x.open("GET","get?&",false);,null,null,null,null,null,null
4:x.open("GET","get?&=",false);,null,null,null,null,null,null
5:x.open("GET","get?=",false);,null,null,null,null,null,null
6:x.open("GET","get? =1",false);,null,null,null,null,null,1
7:x.open("GET","get?= ",false);,null,null,null,null,null,null
8:x.open("GET","get?=&",false);,null,null,null,null,null,null
9:x.open("GET","get?name",false);,,null,null,null,null,null
10:x.open("GET","get?name=",false);,,null,null,null,null,null
11:x.open("GET","get?name= ",false);,,null,null,null,null,null
12:x.open("GET","get?name&",false);,,null,null,null,null,null
13:x.open("GET","get?name&&",false);,,null,null,null,null,null
14:x.open("GET","get?name=1",false);,1,null,null,null,null,null
15:x.open("GET","get?name=1?",false);,1?,null,null,null,null,null
16:x.open("GET","get?name=1&",false);,1,null,null,null,null,null
17:x.open("GET","get?name=1&pass",false);,1,,null,null,null,null
18:x.open("GET","get?name=1&&pass",false);,1,,null,null,null,null
19:x.open("GET","get?nae=1&&pass",false);,null,,null,null,null,null
20:x.open("GET","get&",false);,报错
21:x.open("GET","get&name&",false);,报错
22:x.open("GET","get?name=1&name=2",false);,1,null,null,null,null,null,这里很明显,只是拿取第一个,即写在前面的一个
结论:如果一个请求没有找到?,那么他整体就是一个请求,即&当成值来使用,当找到第一个?时,这个时候后面的?作为正常值来使用,而&作为连接,不参与值来使用了,在考虑这种情况时,值=和值,代表这个值的value为"",比如10,11的结果,而=和 =中后端接收的""是接收不到的,只有" "可以,所以6存在值,即""和&在后端是接收不到了,单纯的接收不到,转义也是没有用的,在url中&也就是&,所以没有用的,当然了,在不考虑转义的替换的情况下,都是将对应的值直接放入,所以\&就是放入&,那么这个时候,后?后面又有,会导致语法错误(?后面是参数处理了,其中不能存在,是不会允许的(考虑到转义所以才会这样的),但是/可以,/会作为值,你可以测试get?/a或者get?a就知道了)
当然,上面是浏览器自身的规则处理,以及报错的处理,与后端关系不大,最终还是需要看浏览器给后端的值对于后端来说,只会存在""和其他的值处理,还是null,对于url中就是是否存在参数,如name,name=,不存在name,以及name=1等等,当然,name=1& =1中," "是为1的,因为前端的""才是代表没有参数,而" "代表" ",这里了解即可
简单来说,如果存在name=,那么在某个地方(这里针对post来说就是):
name:"",这样的,get也是如此,只是他是解析url到后端,而post则是直接的设置这里,但是他们的结果都是一样的,比如表单中写name="",那么相当于?=1,中=1前面的不写,他们最终都是一样的,所以get和post其实是一样的,只是这个参数和值的存放不同,导致get和post分成了一些操作,这里再后面会说明的
22:22:x.open("GET","get?name=1&name=2",false);,1,null,null,null,null,null 这里很明显,只是拿取第一个,即写在前面的一个
当然,22这里我应该还需要测试,拿取两个,在后端的String name = servletRequest.getParameter("name");中后面可以继续加上:
String[] names = servletRequest.getParameterValues("name");
for (int i = 0; i < names.length; i++) {
if (i == names.length - 1) {
System.out.println(names[i]);
continue;
}
System.out.print(names[i] + ",");
}
最终打印出:1,2
*/
我们也可以通过标签来处理,在学习前端表单时,应该会非常清除,实际上form的请求方式也只是将表单内容变成对应的请求参数的而已,所以这里我们测试一下即可(修改index.jsp):
我们继续操作这个get请求,这个时候我们可以加上一些请求头,比如:
x.setRequestHeader("Content-Type", "application/json"); // 设置JSON请求头
加上请求头自然需要在send发送请求之前加上的,然后执行访问,因为send是最终处理,这时发现结果是一样的,那么他有什么用呢,实际上get请求在操作过程中,并不会使用他,或者说,只会使用一些请求头(你就访问百度,查看网络中,对应的请求,看看是否有请求标头就知道了,get也是有的,因为一个完整的请求是基本必须存在其请求信息和响应信息),但是Content-Type是忽略的(实际上是设置的,之所以忽略是因为几乎用不上这些,或者给出的api中或多或少可能会判断一下(比如后面的multi-part的对应的错误),因为其url的保存基本只能由默认请求处理,所以这在一些版本中,可能并不会进行显示,所以说成是忽略也是可以的),为什么这里要说明这个,在后面你就会知道了 现在我们操作了get请求和get操作的请求头,那么现在来完成get的单文件处理,在这之前有个问题,get存在单文件的处理吗,答:并没有,或者很少,为什么说很少,这是因为get数据的保存是在url中的,url中加入的数据是有限的,所以如果是小文件的话(或者说某些符合字符的文件),get是可以完成的,现在我们来进行测试: 在真正测试get请求文件之前,首先我们要来确认get请求文件的流程思路是怎么来的,或者为什么只能将文件数据放在url中,现在来让你好好的理解get请求文件为什么要这样做,以及如果不这样做会出现什么: 在测试之前,我们必须要明白,get的作用就是在url中进行添加,而post则不是,他们是不同的操作方式,自然其对应的需求的请求也是不同的 在测试之前,我们还需要明白一件事情,前端要上传文件,一般就是上传二进制的代码,然后操作一些文件信息,无论你前端如何变化,本质也是如此,所以只需要掌握了拿取二进制信息发生的流程,那么无论什么情况下,文件上传的操作你就不会出现问题了,这里建议参考这个视频:,这个视频我感觉还是很好的,虽然没有从0到有的代码的说明 那么现在有一个问题,由于文件是从磁盘(文件系统)里面拿取的,我们并不能很好的手动的写上这些数据到url中,特别是图片等等,那么就需要一些操作来读取,比如通过标签,或者通过js手动的拿取等等(因为最终他们的基础代码是一致的),通过标签获取,一般是如下的操作(这里我们完全改变之前的index.jsp了,且记得放在body标签里面):
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();
//后面的就不操作打印了,因为只是打印返回值而已,没有必要
}
上面我们并没有通过标签提交,而是通过标签获取后,通过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); //org.apache.catalina.connector.RequestFacade@5b7236d6
//可以强转并且可以赋值,必然存在上下级关系,而非隔代的关系,并且打印出现的是org.apache.catalina.connector.RequestFacade@5b7236d6
//但是在当前idea中并不能直接的查看,为什么,因为idea中或者说maven中只是管理他导入的依赖操作,而自身的或者服务器等等并不会给出,所以需要自行去找
//我们可以到服务器目录中,如我的就是C:apache-tomcat-8.5.55lib,找到catalina.jar,然后解压,根据目录找到如下:
//比如我的就是C:ceshicatalinaorgapachecatalinaconnector,然后找到RequestFacade.class
//那么如何查看呢,实际上idea就有这样的方式,我们复制这个class,粘贴到当前项目的target(需要编译)目录中,点击查看即可(当然,由于他的反编译并不能保证100%正确,所以有些与源码可能是不同的,但是大致相似)
//这个时候我们并不能在idea中进行删除他(不能局部删除,所以也就是说,可以删除target目录,包括class他所在的目录,原本编译的进行删除时,与你复制粘贴的是不同的,一般可能是因为你使用他了,当然,如果你没有使用过,那么他与你复制粘贴的出现的不能删除的错误是一样的,或者类似的),这是idea的判断规定,所以需要自行到文件系统(比如在c盘,你手动到c盘中删除,而非在idea中,这些盘简称为(操作系统的)文件系统的)中进行删除或者删除目录
//这个时候你点击进去可以看到public class RequestFacade implements HttpServletRequest {,即他的确是实现的,所以也证明了可以进行赋值
//当然,由于服务器或者说tomcat的类很多,所以这里我们并不能将里面进行学习完毕,但是idea关联了tomcat,那么中间操作的类加载的过程肯定是加载的,所以你不用想他是怎么处理并且操作的,任何的类都是加载才会操作的,包括jdk的自带的类,只是我们不知道而已,这里可以选择到104章博客进行学习
HttpServletRequest h = (HttpServletRequest) servletRequest;
System.out.println(h.getMethod()); //可以查看请求方式
servletResponse.setContentType("text/html;charset=UTF-8");
PrintWriter writer = servletResponse.getWriter();
writer.write(""
+ 11 + "");
}
@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); /*当然,在get请求中,给加上请求参数是无意义的,也就是说,相当于send(),get请求基本只会加载url后面,如果存在某些框架是给出对应的一个对象的,那么不要怀疑,底层也是进行解析放在url中的
由于前面对get的请求操作我们已经给出了22种,那么就不考虑在url后面添加信息了,所以对应的需求中,我们才会有层级,即get请求(操作加上参数,这个处理完后,那么后面的参数处理无论是get还是post都能明白了,所以也基本只会测试一次),get操作的请求头,get的单文件,多文件,以及文件夹处理
这里已经操作参数了,现在是get的单文件
*/
}
我们继续修改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 {
//你看到上面的处理,应该就会知道,哦,原来get和post并没有区别(上面的判断就能判断保证执行谁了),在前面也说过了,get和post其实是一样的,只是这个参数和值的存放不同,导致get和post分成了一些操作(如文件的处理等等)
//同样的,如果我们没有上面的判断的话,那么他们的操作就是一样的,自然会导致出现问题,这里我们就不进行操作,就只是打印,到底看看,什么情况下,必须需要进行分开
Part filePart = h.getPart("file"); // 获取名为 "file" 的文件Part对象
String fileName = filePart.getSubmittedFileName(); // 获取上传的文件名
System.out.println("文件名:" + fileName);
} catch (Exception e) {
e.printStackTrace();
System.out.println(2);
}
System.out.println(1);
}
进行访问(记得点击上传文件,当然,你也可以不上传,甚至没有这个文件参数),可以发现报错了,并打印信息出现了两个GET,且没有返回值返回为什么,我们首先看错误:
/*
打印2,1后(GET也会的),错误出现(因为他打印是比较慢的,但是由于打印自身也不是异步,那么只能说明是在打印准备显示或者赋值时是异步的,也就是说的大多了比较慢,即打印也需要时间)
错误:由于没有提供multi-part配置,无法处理parts
*/
这个错误的出现是这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的,即原生的):
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();
}
前端代码编写完毕,可以发现,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) {
// 解码 Base64 数据,Base64也是java中原生的代码,因为我并没有引入任何相关的依赖
//Base64.getDecoder()得到一个对象static final Decoder RFC4648 = new Decoder(false, false);
//即Decoder对象,他是 Java 提供的用于解码 Base64 编码数据的类
//而其方法decode(String base64)则是将 Base64 编码的字符串解码为字节数组
byte[] decodedBytes = Base64.getDecoder().decode(file); //一般情况下,我们是需要解码encodeURIComponent的数据的,但是getParameter中,内部通常解决了这样的情况,所以我们并不需要手动的解码
// 根据实际需求,这里可以对解码后的数据进行进一步处理
// 例如保存文件到服务器等操作
//这里我们试着将文件保存到本地,如果可以自然也能操作保存到服务器了
FileOutputStream fileWriter = new FileOutputStream("F:/in.jpg");
fileWriter.write(decodedBytes);
} else {
}
}
我们找一个文件,上传,然后你可能会出现如下的错误(也是不建议使用url或者说get操作文件的情况):
/*
java.lang.IllegalArgumentException: Request header is too large
翻译:java.lang.IollegalArgumentException:请求标头太大
他一般代表:通常发生在HTTP请求头过大时,服务器无法处理该请求,这种情况可能发生在GET请求中,特别是在您尝试在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(""
+ "上传文件成功" + "");
return; //提前结束吧,执行完结束和现在结束差不多,这是独有的void的return结束方式,不能加上其他信息哦(可以加空格)
}
writer.write(""
+ "上传的文件为空" + "");
return;
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public String getServletInfo() {
return null;
}
@Override
public void destroy() {
System.out.println("正在销毁中");
}
}
前端:
Title
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);
}
上面我们操作了一下后缀,我们看看即可 一般情况下,你选择的文件可能与之前的文件有所联系,可能是日期,可能是唯一内容或者id,也就是说,如果你选择后,修改文件系统的对应文件,那么可能上传不了,一般存在reader.onload里面(即原来的信息,具体可以认为一个文件里面存在是否改变的信息,即会再次的进行处理,从getElementById获取,虽然他也可以获取文本的,但是内容可能还操作了对应指向的类型方法,所以了解即可),相当于操作了return;,即会使得当前方法直接停止不操作了(一般并不包括里面的异步处理,所以只是停止当前线程,异步是新开线程的),但也只是对该文件而言,如果是多个文件,那么没有改变的就不会操作return;,也就不会结束调用他的方法,return;可不是程序结束的意思,所以其他的还是会执行的 但是这里大多数人会存在疑惑,js是单线程的,为什么存在新开线程,这里就要说明一个问题,你认为页面渲染主要只由js来处理吗,实际上是浏览器来处理的,所以js只是一个重要的组件而已,而非全部,那么其他的线程可能并不是js来新开的,可以认为是浏览器,或者其他操作系统来新开的,就如浏览器存在js组件,自然会与他交互,浏览器新开一个线程与你交互有问题吗,没有问题,所以这也是js也是单线程,但是确存在异步根本原因,但是随着时间的发展,js可能也会诞生时间片的概念,使得js在单线程的情况下与java一样的进行切片处理多个线程的,这里了解即可 改造web.xml:
<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>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是删除你文件系统的所有文件,你怎么防止你,也就是说,只能通过上面的表单交互来进行文件的上传处理,也是浏览器唯一或者少的与文件交互的处理,并且也是需要用户与他进行处理的,但是我们可以选择不手动点击具体上传文件的按钮,操作如下:
Title
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);
}
但是这里为什么还要加上标签,实际上浏览器并不允许单纯的没有与用户交互过的自动处理,所以如果你去掉了这里的用户点击的交互,那么后面的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,他们有什么用:
/*
async 和 await 是 JavaScript 中用于处理异步操作的关键字,它们使异步编程更加清晰和易于管理,在传统的异步编程中,只使用回调函数和 Promise 用于处理异步操作时,往往会导致嵌套深度增加、回调地狱等问题,而取代回调函数的async 和 await 通过使用更直观的语法,使异步代码看起来更像是同步代码,从而提高了代码的可读性和维护性
async:async 关键字用于标记一个函数是异步的(特别注意,是函数哦),这意味着函数内部可以包含 await 关键字,并且在该函数内部使用的任何 await 表达式都会暂停函数的执行,等待 Promise 解决,并在解决后恢复执行
await:await 关键字只能在使用了 async 关键字标记的函数内部使用,它用于等待一个 Promise 解决,并返回 Promise 的解决值,在 await 表达式后面的代码会在 Promise 解决后继续执行,使用 await 可以让异步代码看起来像同步代码,避免了回调函数的嵌套
但是要注意:async并不是整体函数进行异步,他只是标记,而不是使得是异步的,标记可不是改变哦,但是也正是标记,所以才会与await,以及Promise进行处理,而这个标记存在的异步意义是:
使用了 async 关键字,只有在函数内部使用了 await 或者返回了一个 Promise 的情况下,async 才会影响函数的执行方式,如果函数内部没有使用 await,也没有返回一个 Promise,那么函数的行为仍然是同步的
所以简单来说await之前同步,只会异步,中间可以选择操作一下Promise,所以上面才会说是标记哦,并且如果返回的Promise是没有被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)
/*
流程:我们只是定义给fetchUserData函数,但是可没有执行他,首先给getUserInfo加上异步,那么当后面的2打印后,执行他
注意:这个时候还是同步的,因为只有在函数内部使用了 await 或者返回了一个 Promise 的情况下,async 才会影响函数的执行方式
而由于Promise的返回是有await处理的,所以也就真正使得异步的是await,即有await那么前面同步后面异步,否则都是同步
这个时候还是同步的,那么打印4,然后经过await fetchUserData();,现在由于使用了await,那么是异步了,即直接使得改方法变成异步,但是使用了await的证明是对应的方法执行完毕,也就是说fetchUserData方法执行完才算使用,所以这个时候会打印9,并且由于await的使用会导致获取Promise的值,所以里面的打印会慢一点
而方法是异步,自然使得外面的3进行打印,因为他只是给函数(方法)的,内部还是一个单独的线程处理,所以局部还是同步的,但是由于是等待1秒执行,并且是需要接收resolve的值
所以1秒后,打印1,5
所以打印结果是:
2
4
9
3
1
5
执行一下看看结果是否正确吧
*/
修改一下:
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)
/*
打印的是:
2
4
9
1
3
5
为什么1也打印了,我前面说明了由于await的使用会导致获取Promise的值,而获取的值的主要时间是resolve,所以在同时进行异步处理时,异步信息给方法和异步信息给当前是慢一点的,所以1会先打印,但是由于resolve的时间较多,所以他是后打印的
*/
上面在很大程度上解释了三个关键字的说明,上面存在同步,异步,等待获取的处理,这三个在异步编程中是最为重要的形式,在java中,一般也需要如此,即同步,异步,以及在其中相应的等待处理,且包括数据的处理,当然等待处理可以认为是数据的处理,只是java更加的好,因为异步编程无非就是同步,异步,以及其中出现的数据处理,所以js和java的异步编程都是非常灵活的,虽然在java中外面一般会称为并发编程,也是归于他异步编程的优秀处理,他比js更加的灵活的,当然,这些规定的知识层面了解即可,深入java的就行,js的也可以顺序深入一下,他们归根揭底是不同的预言,所以请不要找共同点,相似的也最好不要,因为他们本来就不同,只是由于英文造成的某些关键字相同而已,如int,String等等 现在我们使用这些知识来解决前面的多文件统一处理,现在修改index.jsp:
Title
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 {
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)
//拿取了总共的数据现在我们来处理一下url
let 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);
}
后端代码如下:
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(""
+ "上传一个文件成功" + "");
}
return;
}
writer.write(""
+ "上传的文件为空" + "");
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)
//拿取了总共的数据现在我们来处理一下url
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):
改变上面即可,因为文件夹处理,相当于自动多选了里面的所有文件,只是对比多文件的选择,我们只需要选择文件夹而已,所以我们直接的访问,看看对应后端的路径里面是否存在对应的文件吧(这里选择统一处理还是分开处理都行,建议分开处理,因为需要考虑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:
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)
}
后端代码(创建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(""
+ 11 + "");
}
@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:
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)
}
后端代码:
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); //null,null,3
servletResponse.setContentType("text/html;charset=UTF-8");
PrintWriter writer = servletResponse.getWriter();
writer.write(""
+ 11 + "");
}
@Override
public String getServletInfo() {
return null;
}
@Override
public void destroy() {
System.out.println("正在销毁中");
}
}
//上面打印了3,也就是说get的(url)确会进行转换,但是其他两个是null,说明JSON.stringify(body)并不行,所以去掉JSON.stringify()函数的处理看看
当我们去掉了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=");
//这个时候你可以选择去掉x.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");,可以发现,没有获取,即对应的值为null,也可以更加的验证getParameter是靠对应请求头获取
let text = x.responseText;
console.log(text)
打印了1,3,你可以继续修改:
x.open("POST", "post", false); //可以写成post?,也可以不写
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的单文件处理,实际上单文件,多文件,和文件夹都可以使用前面的代码,这里我们可以给出: 前端:
Title
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 {
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();
}
}
我们只是将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(""
+ "上传一个文件成功" + "");
}
return;
}
writer.write(""
+ "上传的文件为空" + "");
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(""
+ "上传一个文件成功" + "");
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();
}
}
文章来源:https://blog.csdn.net/qq_59609098/article/details/139559991
微信扫描下方的二维码阅读本文
暂无评论内容