Java:110-SpringMVC的底层原理(上篇)

作者 : admin 本文共125844个字,预计阅读时间需要315分钟 发布时间: 2024-06-10 共1人阅读

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 的后续产品

Java:110-SpringMVC的底层原理(上篇)插图

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章博客的最后(这里说明了”最后”,那么就不用看整个博客了))
如图:

Java:110-SpringMVC的底层原理(上篇)插图(1)

也就是说只是用一个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是考虑维护性,这也是为什么大多数我们会写循环,而非直接的都进行打印,这就是考虑方便性,在性能问题上循环虽然需要更多的时间(循环自身需要操作,自然绝对的比单纯打印慢,循环+打印!=打印),但是方便许多
*/
大致流程如下:

Java:110-SpringMVC的底层原理(上篇)插图(2)

即数据到控制器到视图,最终到响应,给我们显示,实际上控制器在一定程度上也可以和数据结合一起,只是为了解耦合所以我们通常也分开变成数据(业务了),所以如果非要精准的说的话,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)
创建一个项目,如图:

Java:110-SpringMVC的底层原理(上篇)插图(3)

对应的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>
<!--可以继续写一个来进行拦截补充,看先后顺序,如果前面一个没有拦截,看后面一个,以此类推,直到都没有拦截,当然,他们的拦截不能是一样的,所以没有存在上面是/下面也是/的情况,就算是/a都可以,但不能继续是/了-->







一般tomcat的web.xml在其conf目录里面,如图:

Java:110-SpringMVC的底层原理(上篇)插图(4)

我们继续说明:

<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:/"/> <!--该方式与上面的一起时,该方式优先的处理,并不是覆盖,优先使用方式一,除非我没有拦截到,当然,如果是什么拦截链,那么可能会继续处理,但是到那个时候会特别的说明,否则我们一般默认为覆盖(在前端css的覆盖,差不多也是这样),很明显,这里并不是覆盖,所以是一个链,那么当配置无法找到对应的静态资源,或者请求不匹配该路径时,那么配置就会起作用,这里也了解即可-->
<!--这也就可以访问资源文件夹里面的了,首先mapping匹配你的路径,由于存在开头的/,那么去项目/路径,那么若你访问http://localhost:8081/springmvc/resources/s.html,由于是开头的/,即是resources/s.html,那么就是匹配了resources/**,即放行,放行后,就可以去对应的文件中找了,由于指定的文件为classpath:/,代表项目根目录,所以再该根目录里查找resources/s.html,正好是资源文件里面的s.html,那么将他的结果给你看,这就是全部的流程
当然,大多数mapping默认是在项目/下的,所以加不加好像并无所谓,所以这里删除开头/,照样可能找resources/s.html,而不是s.html,所以注意即可,一般的上面的说明只是建立在起始条件下的:
大多数/的省略只是绝对和相对路径的说明,所以我们最好使用/,在前面的情况http://localhost:8081/springmvc/resources/s.html如果出现省略还是一样的,大多数是因为请求就是resources/s.html,而非s.html,所以在起始条件下,上面的说明都是正确的,无论是否省略都是如此,但是不是在起始条件下就不是了,所以需要特别的注意,所以这里可以解释如下(比如如下):
中的mapping属性省略第一个斜杠/:如果省略了第一个斜杠,路径模式将被解释为相对路径,在这种情况下,js/**将被解释为相对于当前请求上下文路径的路径,例如,如果当前请求上下文路径为/myapp,那么js/**将解析为/myapp/js/**
中的location属性省略第一个斜杠/:如果省略了第一个斜杠,路径将被解释为相对于当前请求上下文的相对路径,在这种情况下,js/WEB-INF/将被解释为相对于当前请求上下文路径的路径。同样,如果当前请求上下文路径为/myapp,那么js/WEB-INF/将解析为/myapp/js/WEB-INF/
省略第一个斜杠可能导致资源路径解析错误,因为最终的路径可能不是预期的路径,主要他是可变的,因此,为了确保路径的正确解析和映射,通常建议在标签的mapping属性和location属性中开头都明确地使用斜杠/,那么结尾的/呢,实际上结尾的可以加可以不加
实际上大多数配置中,路径只会分为相对和绝对的,特别的,开头加上斜杠大多数都会是绝对路径,且相对于项目来说,虽然有些加多了可能到起始浏览器(如重定向),所以需要这样说,对于项目里面的路径,大多数框架都只会分为相对和绝对(项目路径的绝对),这里了解即可
-->
</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算是最终的,虽然还有其他的最终):

Java:110-SpringMVC的底层原理(上篇)插图(5)

流程说明:
第一步:用户发送请求⾄前端控制器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的名称(或者说映射的),所以我们访问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就是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设计一般是: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是添加
现在我们来操作一下示例:
先看一个图:

Java:110-SpringMVC的底层原理(上篇)插图(6)

首先在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 交互:
我们重新创建一个项目,之前的不操作了,创建如下:

Java:110-SpringMVC的底层原理(上篇)插图(7)

依赖如下:
<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,放入进去:
链接:http://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):

Title

对应的路径我们不加上斜杠开头,因为一般代表是端口开始的,若测试后,对应前端打印的信息得到了,那么代表操作成功,注意:一般情况下,默认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>
创建的项目如下:

Java:110-SpringMVC的底层原理(上篇)插图(8)

其中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,或者加上如下代码:

启动,点击访问,出现如下(前端的,前端的出现了,那么后端的就不用看了,所以这里就不给出了,后续也是如此):
/*
下一个
第一个

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请求在操作过程中,并不会使用他,或者说,只会使用一些请求头(你就访问百度http://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则不是,他们是不同的操作方式,自然其对应的需求的请求也是不同的
在测试之前,我们还需要明白一件事情,前端要上传文件,一般就是上传二进制的代码,然后操作一些文件信息,无论你前端如何变化,本质也是如此,所以只需要掌握了拿取二进制信息发生的流程,那么无论什么情况下,文件上传的操作你就不会出现问题了,这里建议参考这个视频:http://v.douyin.com/iJ8YRXGw/,这个视频我感觉还是很好的,虽然没有从0到有的代码的说明
那么现在有一个问题,由于文件是从磁盘(文件系统)里面拿取的,我们并不能很好的手动的写上这些数据到url中,特别是图片等等,那么就需要一些操作来读取,比如通过标签,或者通过js手动的拿取等等(因为最终他们的基础代码是一致的),通过标签获取,一般是如下的操作(这里我们完全改变之前的index.jsp了,且记得放在body标签里面):


上面我们并没有通过标签提交,而是通过标签获取后,通过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.55\lib,找到catalina.jar,然后解压,根据目录找到如下:
//比如我的就是C:\ceshi\catalina\org\apache\catalina\connector,然后找到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的,即原生的):


前端代码编写完毕,可以发现,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

上面我们操作了一下后缀,我们看看即可
一般情况下,你选择的文件可能与之前的文件有所联系,可能是日期,可能是唯一内容或者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

但是这里为什么还要加上标签,实际上浏览器并不允许单纯的没有与用户交互过的自动处理,所以如果你去掉了这里的用户点击的交互,那么后面的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

后端代码如下:
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:

后端代码(创建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:

后端代码:
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

我们只是将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();
}
}
相应的标签:
<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 /task01_demo01/demo1.html HTTP/1.1 	请求行,包括请求类型,请求路径,协议版本
Host: localhost:8088               请求头  主机:主机地址,请求的服务器的地址
Content-Length: 21                  最下面的请求体的长度
Cache-Control: max-age=0			浏览器相关信息,在服务器上修改代码可以进行实施更新
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64)    浏览器相关信息
//空白行
name=scott&pwd=123456                请求体,即请求数据,不是请求过去的内容,这是添加的数据
*/
/*
响应行用来说明HTTP协议版本号和状态码以及状态消息,格式如下:
协议的版本(1.0 1.1) 状态码 (200 成功 404 路径错误 500 服务错误) 状态信息
响应头用来说明客户端要使用的一些附加信息,格式(key:value)
空白行就是响应头部的空行,即使后面的请求数据为空则必须有空行
响应体用来服务器返回给客户端的文本信息
举例如下:
HTTP/1.1 200 OK    				响应行   协议版本 状态码 很成功
Content-Type: text/html 			响应头 这里是我要给你的的内容类型,文本的html类型
Content-Length: 588                  我要给你的内容长度
Date: Thu, 08 Sep 2021 12:59:54 GMT   	日期
//空白行
示例1   	响应体 服务器给你的信息(内容)

这是一个HTML页面

这里省略了很多,只给你html标签里的东西 */
一般情况下,如果粗略的说的话,我们会将请求体或者响应体称为他们的整体,所以存在两个意思,对数据来说,那么他们就是里面的体,对一种大局来说,就是一个整体,这个在前面说明时,就有类似的体会(是请求体和响应体的设置(这里就是大局的了,而不是对数据),可以全局搜索查看)
很明显虽然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("

" + "上传一个文件成功" + "

"
); fileWriter.close(); } writer.close(); return; } writer.write("

" + "上传的文件为空" + "

"
); 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是类似的(我们也操作过),甚至可以说是一样的:


然而我们需要先通过formData来完成表单的操作,这个时候我们不操作文件,之前我们只是说明他的作用,并没有真正的使用他完成过某些操作,而是错误出现,因为之前操作的是get,所以我们来处理一下这个,由于在前面我们说了,formData主要是操作文件的,为什么,这是因为他自动携带了请求头信息:multipart/form-data,也就是说,他相当于表单加上了multipart/form-data(在请求头中可以看到),所以他才是一个操作了文件的一个api,但是也正是因为该请求头,所以单纯的,如前面的getParameterValues并不能进行处理(他getParameter的数组方式,与他getParameter是一样的需要对应的相同请求头,也自然是application/x-www-form-urlencoded),这个时候,我们只能使用原始的处理的,但是原始的处理我们并没有学习过,所以在进行测试之前,我们先学习一下这个原始处理,首先是get的处理,我们改变index.jsp:

Title

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("

" + 11 + "

"
); }
至此,看看打印结果是否出现,出现后我们修改后端代码:
@Override
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
String name = servletRequest.getParameter("name");
System.out.println(name);
//这样也可以:BufferedInputStream fileInputStream = new BufferedInputStream(servletRequest.getInputStream());,他一般是用来指定获取的,当然,他是获取字节流的,而下面是字符流的,对文件来操作时,建议操作字节流(比如写入,当然,这里获取由于需要判断,所以需要字符),这里就这样操作了
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("

" + 11 + "

"
); }
我们可以发现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); //虽然请求的url是get名称,但是也只是一个名称而已,所以并不需要完美(在某些方面他的名称只是一个获取而已,而非代表是get请求,所以注意即可)
xhr.send(formData);
}
查看网络,可以发现,有两个地方(他们是互不影响的):

Java:110-SpringMVC的底层原理(上篇)插图(9)

注意上图中的显示是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来进行处理的(不是真正的底层,因为服务器也是编写的,要知道下层还有网络编程呢),所以存在后面的代码
我们编写后端代码就以这个为主(上面前面的检查元素里面的,下面是我上传图片后的其中一个结果):

Java:110-SpringMVC的底层原理(上篇)插图(10)

后端代码是:
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;
//boolean startsWith(String prefix),判断字符串是否以参数字符串开头
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;
//我们需要这样操作,因为我们并不能使用除了对应二进制的(如ascii)的编码操作图片(二进制)的数据,因为由于并不识别,找相似的原因,他们并不会对应,所以可能会造成文件变大或者变小,所以我们需要进行另外的方式
//并且,也不能操作多次,因为对应的流是同一个,容易出现下标的问题,所以我们需要这样的处理,即一起处理
//但是与之前的servletRequest.getReader()还是有区别,之前的servletRequest.getReader()是直接拿取对应文件的信息的
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++;
//这里代入了空白行,所以这里可能会判断多次,而ghk就是防止加入了空白行的信息的,并且如果以后也存在
//那么由于这个的原因,会导致必然存在一个正确的数才会进行处理
if ((char) re == '\r' || (char) re == '
') {
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 == '
' && 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];
}
//得到对应的一行数据
//对字符处理,即将读取的字节按照UTF-8变成字符,由于大多数表单处理的都是UTF-8,所以根据这样的,可以不会出现编码问题(如文件名称)
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] = '
';
w = 1;
q = 0;
}
}
//判断第一行的内容是否是这个,由于在每个数据开始都会存在--的添加,所以这里是"--"+boundary
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] = '
';
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] = '
';
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);
}
}
//这个是文件的类型,如果是需要传递pdf的话,那么这里通常需要判断是否是application/pdf,具体可以在网络的请求的载荷(也可以说是请求信息或者参数信息)中看到
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("

" + 11 + "

"
); 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盘下的,且名称是文件名称),当然,我们需要考虑一个点,对应文件的内容存在浏览器对应的区域时是如何处理的,特别是换行符怎么处理,我们看这个:
假设这个是文件信息:
111111111
4444444
5555
22
1
那么对应再前端的区域也是这个:
分割符
111111111
4444444
5555
22
1
分割符
也就是说,绝对的一致,但是,由于最后的值(指的是空),需要换行到分割符中,所以上面的代码,由于考虑到了这样的情况,所以默认将后面的换行符都进行加上,所以我们最后的处理需要这样的存在:
//去掉最后两个字节
dern(name);
也就是说,实际上如果对应的文件的一行中存在换行,那么对应的区域也存在,若没有,那么也不存在(但是需要换行到分割符中,所以我们需要特别的处理最后两个字节),也就是说,我们确定原来的文件末尾是否存在换行符(他会影响上面的值,使得1后面多出几个换行,所以文件末尾是否存在换行符并不会影响数据的操作,只有到分割符的换行会影响整体性,但并不影响文件的显示),但是无论是否存在,在前面对应的地方(上面写的代码)都会加上换行符,并且最后去掉,并且,实际上不考虑去掉最后两个字节也没有问题,因为换行符并不会影响图片的显示,但是也只是末尾,头部可不要这样哦,而正是如此,如果非常的细节的话,实际上浏览器的文件上传在末尾的情况有些时候可能会出现一些问题(面临整体性的问题),然而上面的解释与实际情况是相同的,我们可以观察生成的文件,会发现,字节数是一样的(注意需要看属性,因为kb比较大,并不会细节的显示),这个一样通常在很多方面(如上面解决了//去掉最后两个字节,否则会导致生成的可能会多出几个字节,如换行符),实际上由于默认添加换行符,所以我们生成的文件一般会多出两个字节,也就是\r
,你可以选择在生成的文件内容最后去掉最后一个的换行符,可以发现,他们的字节数量是一样的了
从这里我们也可以发现,关于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有限制))
上面我们了解即可,但是为了验证上面的原始处理比较正确,所以现在我们来操作多文件的处理,也是使用上面的代码,总需要都试一下:
修改前端代码:

Title

虽然对应的在检查中,比如说:

Java:110-SpringMVC的底层原理(上篇)插图(11)

存在两个name,但是他并不会是覆盖的形式,因为对于域来说,他只负责存放数据,而数据的分别则是由分割符来完成,即或者说,实际上我们并没有具体参照过name,而是参照分割符,因为他们是一个整体(这个分割符到空白行之间的整个信息)
也就是说,实际上无论是多文件还是问文件夹都是如此(文件夹只是一个选择了一个文件里面的所有数据的多文件而已,还是属于多文件的范畴,实际上多文件可以,那么文件夹也可以,更甚至,文件可以,多文件也就可以),经过测试,文件夹也是可以的,但是域,即multipart/form-data与之前的处理有点不同,前面是最原始的操作而这个是经过中间处理的,我们可以发现,存在这个:

Java:110-SpringMVC的底层原理(上篇)插图(12)

也就是说,带上了文件夹的名称了,因为他这个操作是封装的,要不然分割符怎么来的呢,或者说这也是浏览器给出的一个操作,所以在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 {
//默认是\开始的,在windows中一般是\,linux中一般是/
//当然这也只是文件系统的处理或者显示,在其他操作或者显示的处理上,大多数都是/
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("正在销毁中");
}
}
/*
我们可以查询,对应的类HttpServlet是否与这个re一样的结构,即service,doPost,doGet,所以说,如果我们继承了re可以操作,那么自然,继承了HttpServlet一样可以操作(与对应的50章博客类似了,即复现)
*/
然后创建一个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,那么执行自己的版本,我们在前面进行访问:

Title

点击访问,访问两次,若后端打印了对应的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( //配置这个,除了对应的请求头需要multipart/form-data,一级post请求外,在前面我们也提到过,对于某些后端框架或服务或者服务器本身,它们可能默认不支持解析 Multi-Part 请求
//而这个就是进行配置,否则还是会报对应的错误
fileSizeThreshold = 1024 * 1024 * 2,  // 2MB,指定了一个文件大小的阈值,以字节为单位,如果上传的文件大小小于这个阈值,文件将存储在内存中,如果文件大小超过这个阈值,文件将存储在磁盘上,这里理解成io流的刷新机制即可,默认为0,即读取一个操作一个到磁盘(文件)里面
maxFileSize = 1024 * 1024 * 10,       // 10MB,用于限制单个上传文件的最大大小,以字节为单位,在这个示例中,最大文件大小被设置为 10MB,这意味着用户无法上传超过 10MB 大小的文件,实际上这个,我们也可以在前面操作的代码中来计算,从而完成(比如判断总字节,如果超过,删除对应文件便可),默认是无限大
maxRequestSize = 1024 * 1024 * 50     // 50MB,配置用于限制整个请求的最大大小,包括所有上传文件和其他请求参数,在这个示例中,最大请求大小被设置为 50MB,这意味着整个请求不能超过 50MB,类似于get中的请求体大小(虽然那里说明的是请求头区域),get有限制,我们post拥有他的能力,自然也可以进行设置,默认是无限的
)
public class FileUploadServlet extends HttpServlet { //前面我们复现了一个简易版的,所以我们直接使用他即可
//这样就不用操作强转了,使用上面的继承,判断post,自然到这里来
protected void doPost(HttpServletRequest request, HttpServletResponse response) {
try {
// 获取文件Part,这里相当于之前我们代码的处理,只是他里面封装了关键的信息,又或者说,将关键信息放在临时文件里面,最后拿取进行处理,所以存在filePart.getInputStream();
Part filePart = request.getPart("file");
// 获取上传的文件名
String fileName = filePart.getSubmittedFileName();
// 获取文件输入流
InputStream fileContent = filePart.getInputStream();
// 将文件保存到服务器上的目标位置
Path targetPath = Paths.get("F:/" + fileName);
//StandardCopyOption.REPLACE_EXISTING 选项用于在目标文件已存在时覆盖它
//这意味着如果目标文件已经存在,它将被新上传的文件内容替换
//将左边给右边,大多数移动数据的,比如文件,类,集合,等等,基本都是左边移动右边,但是还是需要看实际操作来判断,要不然为什么是大多数呢
//所以建议在操作对应的代码时,请给出注释
Files.copy(fileContent, targetPath, StandardCopyOption.REPLACE_EXISTING);
// 可以在这里对文件进行进一步处理或响应客户端
response.getWriter().write("文件上传成功:" + fileName);
} catch (Exception e) {
e.printStackTrace();
}
}
}
前端代码:

Title

开始文件处理(又何尝不是文件上传的处理呢),我们可以发现,文件的信息被保存了,并且内容一模一样,说明他也是解决了换行,或者空白符的处理,那多文件呢,那么需要这样的处理:
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()) {
//与我们的代码一样,存在多个相同的name,因为对于域来说,他只负责存放数据,而数据的分别则是由分割符来完成
//或者说(这里前面说明过了),实际上我们并没有参照name,而是参照分割符,因为他们是一个整体(即part,用part存放整体信息)
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对文件来说单纯的就是文件名称,如果是文件夹,那么这个值会加上目录的,但是确好像并没有具体的文件名称,这是因为在域中也没有之间的文件名称信息(对文件夹来说),所以传递时需要考虑目录的创建,当然,如果前端真的考虑的话,实际上可以传递具体文件名的,因为在前端是可以得到这个名称(虽然他可能也是拿取对应参数来得到的,如查找字符串或者根据/或者\来区分的)
实际上对于陌生的默认接收信息的类(如Part),需要知道他的具体的结构,最好调试一下看看的的信息,也可以更好的知道其方法的作用(如大多数信息,如变量,对于的方法名称可能与他对应的,或者查看对应的方法是否是操作或者得到他即可)
至此,我们利用服务器自带的编写好或者封装好的api(里面存在原始api(原始代办最底层的意思,即没有其他api操作获取了))完成了这些操作,至此,post请求还剩下最后一个处理,即标签处理,我们修改前端:

Title
检查中的截图如下:

Java:110-SpringMVC的底层原理(上篇)插图(13)

也就是说,结果是一样的,当然,上面的顺序是根据标签顺序来的,并且如果一个文件存在多文件选择,那么按照多文件选择来(多文件选择时可能是按照某种编码来进行排序,比如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="http://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', // 提交到的URL
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(); //只是一个类型,比如是Content-Disposition: form-data; name="file"; filename="QQ截图20230816093813.png"中的name,而下面的则是filename
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"); //加上这个,一般情况下,Part的编码是操作这个的,所以设置这个即可,而前面我们由于基本是最原始的处理,所以只能指定了
Collection<Part> parts = request.getParts();
现在我们继续测试,如果编码正确,那么操作成功
现在我们可以选择看看ajax的一些参数:
url: 'upload', // 提交到的URL
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', // HTTP请求方法
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");
得到的信息是这样的(字符串一般都是载荷,无论是否设置了上面的请求头):

Java:110-SpringMVC的底层原理(上篇)插图(14)

他也是一个新的请求头处理(在后面会说明的,在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"); /*之前的我们基本都操作了,当然,一般如果是直接到jsp,通常由于jsp存在对应编码,所以我们可能并不需要设置,这里可以选择在50章博客里或者其后面了解*/
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:

<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/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">
<context:component-scan base-package="controller"/>
</beans>
修改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: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):

Title

后端代码(前面我们操作过了):
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("

" + "上传一个文件成功" + "

"
); } return; } writer.write("

" + "上传的文件为空" + "

"
); 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)
//这里补充一下,一般情况下,他们的值若为-1,那么则是默认的,同理,相关mvc处理时,若值也是-1,那么也是默认的,如果存在不支持-1的设置,那么一般可能是版本问题,同理mvc也是如此,这里需要注意哦
他可能是一个设置了默认的值,而不是根据servlet的默认,当然,我们可以通过配置文件进行处理,这些我们了解即可(一般情况下,mvc并不会默认的进行设置,通常需要我们手动的处理),所以我们主要学习的就是其api了,也就是说mvc实际上也只是多出了几个api,或者内部通过某些操作也进行了封装,像自动保存了Collection parts = request.getParts();的值一样,其他的基本上不用进行什么说明,这里我们前端使用标签(表单),mvc使用api来进行处理:
前端是:

后端在mvc是进行封装的,在前面我们以及了解了这个:
/*
8:MultipartResolver:
MultipartResolver 用于上传请求,通过将普通的请求包装成 MultipartHttpServletRequest 来实现,MultipartHttpServletRequest 可以通过 getFile() 方法 直接获得⽂件,如果上传多个⽂件,还可以调用 getFileMap()方法得到Map这样的结构,MultipartResolver 的作用就是封装普通的请求,使其拥有⽂件上传的功能
*/
他就相当于操作了原生api的处理后的结果进行保存的,或者可以说成是像自动保存了Collection 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 {
//没什么用,因为他只是在得到之前进行的处理,而getOriginalFilename的操作是在操作之前就已经处理了,因为上面的优先注入,所以这个并没有什么对他的作用
request.setCharacterEncoding("utf-8");
//用一个文件试一下
System.out.println("文件名称: " + filename);
String name = file.getOriginalFilename(); //一般是获取文件的原名,与前面的String fileName = part.getSubmittedFileName();基本一样的结果,当然他们都是封装了对应的原始处理操作的信息,所以可以直接的拿取
//我们手动的操作一下,先用对应的编码处理回来,然后用原来传递过来的编码进行处理
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("

" + "上传一个文件成功" + "

"
); } 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>
上面的属性在一定程度上对应与这个:
/*
fileSizeThreshold = 1024 * 1024 * 2,  // 2MB,指定了一个文件大小的阈值,以字节为单位,如果上传的文件大小小于这个阈值,文件将存储在内存中,如果文件大小超过这个阈值,文件将存储在磁盘上,这里理解成io流的刷新机制即可,默认为0,即读取一个操作一个到磁盘(文件)里面
maxFileSize = 1024 * 1024 * 10,       // 10MB,用于限制单个上传文件的最大大小,以字节为单位,在这个示例中,最大文件大小被设置为 10MB,这意味着用户无法上传超过 10MB 大小的文件,实际上这个,我们也可以在前面操作的代码中来计算,从而完成(比如判断总字节,如果超过,删除对应文件便可),默认是无限大
maxRequestSize = 1024 * 1024 * 50     // 50MB,配置用于限制整个请求的最大大小,包括所有上传文件和其他请求参数,在这个示例中,最大请求大小被设置为 50MB,这意味着整个请求不能超过 50MB,类似于get中的请求区域大小,get有限制,我们post拥有他的能力,自然也可以进行设置,默认是无限的
上面的三个配置中其中:
maxUploadSize = maxFileSize
maxInMemorySize = fileSizeThreshold
他们后面的数都是代表字节
即:
等价于
maxFileSize = 1024 * 1024 * 5
因为1024 * 1024 * 5 = 5242880
等价于
fileSizeThreshold = 1024 * 40
因为1024 * 40 = 40960
当然,前面的
@MultipartConfig(
fileSizeThreshold = 1024 * 1024 * 2,
maxFileSize = 1024 * 1024 * 10,
maxRequestSize = 1024 * 1024 * 50)
并不只是这三个属性,并且mvc的文件上传解析器也同样如此,因为他们基本是完全对应的,就算不对应,也必然存在MultipartConfig中有的,因为mvc只是以他为基准的,只能小于等于他,而不能大于他的配置,如果配置数量多,那么不用思考,一定是有几个配置是需要一起影响或者关联MultipartConfig中的
*/
现在我们执行看看结果,然而还不行,这是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("

" + "上传一个文件成功" + "

"
); } } catch (Exception e) { e.printStackTrace(); } } } }
也就是说,本来就已经都处理好变成List的,如果你的参数只是一个,那么给List的第一个,否则都给你
我们可以发现,使用框架的mvc是比较容易的,虽然前面我们的处理封装一下也行,但是有现成的为什么不用呢,至此,原生js和框架servlet我们操作完毕,现在我们来操作框架js和框架servlet,由于js无论是否框架,对代码封装影响不大,即在前面我们也说明了”变化也只有一点点”,所以这里的我们也只给出一个测试结果吧:
只需要修改前端即可:

Title

上传一个文件试一下吧,至此,我们的框架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):

Title

创建一个类,代码如下:
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) {
// 设置响应的内容类型为 JSON
response.setContentType("application/json");
// 从请求中读取 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); //这里多加一行代码(基础api,基本是最底层的了,还要底层的是没有必要继续学习的,除非学习浏览器的源码或者感兴趣,一般我们说到的原始基本就是直接的最底层,而不是直接到二进制的处理)
/*
response.setContentType("application/json");
和
response.setContentType("text/html;charset=UTF-8"); 
他们是两种不同的HTTP响应内容类型设置,它们用于告诉浏览器或客户端接收到的数据的格式和编码方式(响应体信息,请求体给服务器,响应体给客户端,注意:这里对请求体和响应体的说明是请求信息和响应信息,一般我们并不会直接的指向说明请求信息里面的请求体和响应信息里面的响应体,这里注意即可)
response.setContentType("application/json");:
这行代码设置了响应的内容类型为JSON(JavaScript Object Notation),这意味着服务器将返回JSON格式的数据,JSON是一种轻量级的数据交换格式,常用于前端与后端之间的数据交互,浏览器或客户端通常会解析JSON数据以在页面上渲染或使用这些数据,那么假设你传递的数据是22,那么通过这个设置后,这个22在响应信息的请求体之前会进行一些处理,当然了,单纯的22与response.setContentType("text/html;charset=UTF-8"); 基本没有关系,所以你将前面修改成response.setContentType("text/html;charset=UTF-8"); 结果也是一样的
response.setContentType("text/html;charset=UTF-8");:
这行代码设置了响应的内容类型为HTML,并指定了字符编码为UTF-8,这意味着服务器将返回HTML格式的数据,并且该HTML数据使用UTF-8字符编码进行编码,HTML是用于构建网页的标记语言,而UTF-8是一种广泛使用的字符编码,支持多种语言的字符集
总结:
application/json 适用于返回JSON数据,通常用于API响应
text/html;charset=UTF-8 适用于返回HTML数据,通常用于构建网页,字符编码的设置确保了文本能够正确地显示特殊字符和多语言字符集
响应的内容类型设置应根据服务器返回的数据类型来选择,如果您的服务器端点返回JSON数据,那么您应该使用 application/json,如果返回HTML页面,那么您应该使用 text/html;charset=UTF-8 或类似的内容类型,这有助于浏览器或客户端正确解释和渲染响应数据
*/
当然,由于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是否存在:,他相当于操作了response.setContentType(“text/html;charset=UTF-8”);,这里我们了解即可,其中如果单纯的什么都没有加,好像也就是什么都没有操作,就是把这个字符串给出去,就如response.getWriter().write(“22”);,当然,json进行了什么设置我们还不清楚,但是可以这样的认为:
单纯的给数(如字符串):设置json,html,以及什么都没有设置,结果一样
给出某些特定的数:设置json,html,以及什么都没有设置,结果不一样(当然,这里我们选择考虑html与什么都没有设置是一样的,不考虑编码),所以按照这样的考虑,那么json是进行操作的,其他的基本没有,但是真的是这样的吗,所以我们来测试测试看看他进行了什么操作:
考虑到json可能会操作改变的处理,所以我们操作一下数据,因为字符串他们基本不会改变,所以我们测试如下的几种,如:
map集合,list集合,类,作为返回值进行处理,而由于你设置了一些响应设置,那么前端可能会判断这个设置而进行某些处理,当然,后端可能也会进行某些处理,基本都是围绕着这个设置来处理的,所以我们应该有如下的后端代码来测试一下他应该有的作用,这里我们需要考虑到对应的流只会操作字符串,所以对应的集合一般是如此的:
/*
map集合:
"{\"message\": \"Hello, world!\"}"
list集合:
"[1,2,3]"
类:
"{\"message\": \"Hello, world!\"}"
*/
首先我们操作map:
  // 设置响应的内容类型为 JSON
response.setContentType("application/json");
try {
response.getWriter().write("{\"message\": \"Hello, world!\"}");
} catch (Exception e) {
e.printStackTrace();
}
设置list:
     // 设置响应的内容类型为 JSON
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 + '\'' +
'}';
}
}
   // 设置响应的内容类型为 JSON
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章博客最后就有一个小例子)
你可能会有疑惑,直接说出这句话就可以了,为什么需要前面的测试或者铺垫,其实在以后,你就算知道原理,但是原理只是让你知道为什么,而实际操作中的细节是体现不出来的,而上面基本上几乎将所有细节进行了说明,那么以后出现问题,可以选择参照这里,所以说原理需要和实际情况来结合,你知道原理了,但是你的原理只是知道他是这样的,但是中间的其他限制,如需要这样写,需要写在这里你也并不明白,或者说,你只是一个大致的原理(因为你完全没有全部揉碎,只是一个总结的原理而已,而这个原理虽然到达高本质,但是对实现的细节的其他低本质并不清楚,而实践是清楚低本质的操作,所以原理需要结合实践)
至此,我们的请求才算完美的说明完毕,那么就以现在的知识,看后面的内容吧
这个时候,我们回到之前的操作(也就是引出我们说明请求的哪个):

Title

在前面我们提到了,需要如下:
processData: false, // 不处理数据,不进行中途改变数据
contentType: false, // 不设置内容类型,自动设置请求头
为什么processData没有呢,实际上contentType:’application/json;charset=utf-8’中,或者jq的该框架处理了这个请求头,所以默认的处理了processData,即按照这个编码来进行了改变数据,我们可以复现一下:
在前面我们只是说明他会自动的处理,但是原因是什么,我们并没有操作过
按照前面的代码,我们修改前端:

<html>
<head>
<title>Title</title>
</head>
<body>
<input type="file" id="fileInput"/>
<button onclick="uploadFile()">上传</button>
<script src="http://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', // 提交到的URL
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', // 提交到的URL
type: 'POST',
data: formData,
//这里去掉了processData: false
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考虑的比较多,但是非常安全,且也存在提示或者说维护的操作
到此,我们也可以知道,前端框架也会存在各种的判断,在以后学习新的处理时,一般来说,考虑到请求头,即类型处理,以及处理的数据即可,因为其他的参数一般都会很直观的给出的
所以我们继续回到这里:

Title

然后后端代码如下(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>
目录如下:

Java:110-SpringMVC的底层原理(上篇)插图(15)

对应的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>
</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/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">
<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:

Title
现在,我们修改这样的:
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);
}
}
前端则需要这样:

或者:

分别得到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都是如此):

Title
如果是类数组呢,也就是这样的:
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;
}
}
前端需要这样写:

Title
搜索关键字:

user对象:
list集合
第一个元素:
第二个元素:
map集合
第一个元素:
第二个元素:
当你写上后,把数据都加上,只有匹配的才会进行处理(这里其实在67章博客就有说明的)
当然,如果你将对应的集合的User修改成了String,那么就需要这样:
 list集合
第一个元素:
第二个元素:
map集合
第一个元素:
第二个元素:
他们是会处理合并的,在67章博客有具体说明的哦,并且前面的关于这样的操作其实get和post都是一样的处理,为什么这里支持了,这是因为有了选择,之前的直接的list和map我们是难以确定的,就如list和数组有相似的处理,比较难以分辨,而map与集合也有相似的,所以导致只能在其他的类里面才能进行处理了,实际上这些基本在请求字符串(前面或多或少可以知道)中操作的,所以关于js的处理就不说明了,只操作标签的处理了,然而在某种情况前端如果直接传递对象的话,那么后端是怎么处理的,实际上是前端怎么处理的,在前面我们知道是操作了类型最终处理的,一般情况下,对象会考虑变成键值对,使得是我们正常的情况,其他的一般不会处理,也就是说,至此我们应该操作完毕了,而对字符串的处理我们实际上只需要看68章博客即可(而70章博客中,有具体的变化情况的说明),这里唯一需要的是知道直接的List和Map和对象数组不能处理(当然,也不一定,在某种程度上,可能是需要一些操作,或者需要其他版本的),所以一般情况下,我们基本只会使用字符串来进行数据的交互的,而不是操作自动的处理的(因为他们的变化,最终还是字符串的,倒不如直接回到原来的地方呢,没有必要多次一举,所以我们操作json就行了)
由于博客字数限制,其他内容,请到下一篇博客(111章博客)去看
本站无任何商业行为
个人在线分享 » Java:110-SpringMVC的底层原理(上篇)
E-->