SpringBoot-Web开发
SpringBoot - web开发 作者:Crisp
[TOC]
一. IOC容器概念
1. Inversion of Control(IOC)控制反转思想:
对象的创建控制权由程序转移到外部,由自己主动new产生对象改成外部提供对象, 将对象的创建控制权转移到外部
这种思想称为控制反转, 让程序耦合度降低, 改动代码时不会牵一发而动全身
目标: 充分解耦
最终效果: 使用对象时不仅可以直接从IOC容器中获取, 并且获取到的bean已经绑定了所有的依赖关系
2. IOC容器:
是可以为程序在外部创建,提供对象的容器, 管理对象创建和初始化的过程,
sevice层, dao层等对象均可放入IOC容器中,
被创建或者被管理的对象在IOC容器中统称为Bean
3. DI(Dependency Injection)依赖注入:
- 在业务中, 会遇到dao层对象依赖于service层对象这种情况, 而IOC容器也可以解决对象依赖问题
- 在容器中建立bean与bean之间的依赖关系的整个过程, 称为依赖注入
- springMVC中通过xml配置propert标签的ref属性完成依赖注入,springboot中通过@Configuration注解的proxyBeanMethods属性完成依赖注入
4. SpringBoot中创建bean:
- 不同于SpringMVC在xml中写bean的配置, springboot创建bean依赖注解,两者起到的效果相同
- 使用@Configuration类注解与@bean方法注解创建配置类, 运行时就能向IOC容器中注册bean
5. 遍历查看IOC容器中的组件:
SpringBoot自动引入了SpringMVC的一些常用组件,比如dispatcherServlet, characterEncodingFilter字符编码拦截器(防止字符乱码)等组件,详细可在application内通过遍历IOC容器中的所有组件名能找到(启动application的那个代码就会返回IOC容器)
1 |
|
二. 注解和反射
1. 注解
1. 注解定义
java注解是在JDK5的时候引入的一种新特性。注解(也可以称为元数据)为在代码中添加信息提供了一种 形式化的方法,使得在代码中任一时刻可以非常方便的使用这些数据。
注解类型定义了一种新的特殊接口类型,在接口关键期interface之前加@符号,即用@interface即可区分 注解与普通接口声明。目前大部分框架都是通过使用注解简化代码提高编码效率
所有的注解本质上都是继承自 Annotation 接口。但是,手动定义一个接口继承 Annotation 接口无效的, 需要通过 @interface 声明注解,Annotation 接口本身也不定义注解类型,只是一个普通的接口。
2. 注解的属性
①. 基本数据类型(int,float,boolean 等)
1 |
|
②. String类型
1 | public TestAnnotation { |
③. Class 类型
1 | public TestAnnotation { |
④. enum枚举类型
1 | enum Week { |
⑤. 注解类型
1 | TestAnnotation01 { |
⑥. 以上类型的数组(以枚举数组为例)
1 | enum Week { |
3. 元注解
是可以标注在注解上的注解, 详细请看SpringBoot面向注解编程文档的JAVA基础注解篇
2. 反射
1. 反射(Java Reflection)定义
Reflection(反射)是Java被视为动态语言的关键, 反射机制允许程序在执行期间借助于Reflection API取得任何类的内部信息, 并且能直接操作任意对象的内部属性及方法, 反射的优点是实现动态创建对象和编译, 体现出很大的灵活性, 缺点是对性能又影响, 使用反射基本上是一种解释操作, 我们可以告诉JVM, 我们希望做什么并且它满足我们的要求, 这类操作总是慢于直接执行(直接new出对象实例)相同的操作
Class c = Class.forName(“java.lang.String”) //反射获取Class对象, forName参数是全类名(包名+类名)
加载完类后, 在堆内存的方法区中就产生了一个Class类型的对象(一个类只有一个Class对象), ,类的整个结构都会被封装在Class对象中, 这个对象就包含了完整的类的结构信息。我们可以通过这个Class类型的对象看到类的结构。
Class本身也是一个类, 但Class对象只能由系统创建对象, 一个Class对象对应的是加载到JVM中的一个.class文件, 每个类的实例都会记得自己是由哪个Class实例所生成, Class类是Reflection的根源 这个Class对象就像一面镜子, 透过这个给镜子看到类的结构, 所以, 我们形象的称之为: 反射操作任意对象的内部属性及方法
正常方式:
import引入需要的"包类"名称
->通过new实例化
->取得实例化对象
反射方式:
实例化对象
->getClass()方法
->得到完整的"包类"名称
2. JAVA反射机制提供的功能
在运行是判断任意一个对象所属的类
在运行时构造任意一个类的对象
在运行是判断任意一个类所具有的成员变量和方法
在运行是获取泛型信息
在运行时调用任意一个对象的成员变量和方法
在运行时处理注解
生成动态代理
3. 反射相关的主要API
java.lang.Class :代表一个类
java.lang.reflect.Method :代表类的方法
java.lang.reflect.Field :代表类的成员变量
java.lang.reflect.Constructor :代表类的构造器
4. Class类常用的方法
方法 | 代码示例 | 功能 |
---|---|---|
Object.getClass() | User user01 = new User(“张三”,0001,18); Class<?> c4 = user01.getClass(); |
返回值为Class对象, 源自于Object类中获取class对象的方法, 任何类都继承于Object, 因此每个实例对象都可以调用getClass方法来获得Class对象 |
class.forName(“String name”) | Class<?> c1 = Class.forName(“reflectionTest.User”); | 返回值为Class对象, Class类获取class对象的方法, name为全类名 |
Object.class | User user01 = new User(); Class |
返回值为Class对象, 直接使用Object类自带的class属性, 可以直接返回一个当前类的Class对象, 返回的也不再是泛型而是确定的类型 |
Object.TYPE | Class c1 = Integer.TYPE; System.out.println(c1); 打印出Int |
返回值为Class对象, 基本内置包装类都有一个Type属性, 调用它会直接返回内置类的Class对象 |
class.newInstance() | Object user02 = c1.newInstance(); | 返回值为Object对象, 调用缺省构造函数, 返回Class对象对应的一个实例, 这个实例等价于User user01 = new User();产生的实例 |
class.getName() | String name = c1.getName(); System.out.println(name); 打印出reflectionTest.User |
返回值为String类型, 返回此Class对象所表示的实体(类, 接口, 数组类或void)的名称(大概是全类名) |
class.getSurperClass() | Class<?> c1Superclass = c1.getSuperclass(); System.out.println(c1Superclass); 打印出class java.lang.Object |
返回值为Class对象类型, 返回当前Class对象的父类的Class对象 |
class.gettinterfaces() | Class<?>[] interfaces = c1.getInterfaces(); | 返回值为Class对象数组, 获取当前Class对象的接口 |
class.getClassLoader() | ClassLoader classLoader = c1.getClassLoader(); | 返回值为该类的类加载器(ClassLoader) |
class.getConstructors() | Constructor<?> constructor = c1.getConstructor(); | 返回值为Constructors对象数组 |
class1.getMethod(“String name”); | Method method = c1.getMethod(“getAge”); | 返回值为Method对象类型, 返回Class类的一个方法, 参数是方法名, getDeclaredMethod是返回所有方法, 其他get方法加上Declared都是返回所有 |
三. Servlet
1. Servlet的编码设置
tomcat8之前,设置编码:
get请求方式:
获取参数: String fname = request.getParameter(“fname”);
将字符串打散成字节数组 byte[] bytes = fname.getBytes(“ISO-8859-1”);
将字节数组按照设定的编码重新组装成字符串 fname = new String(bytes,”UTF-8”);
post请求方式:
request.setCharacterEncoding(“UTF-8”);
需要注意的是,设置编码(post)这一句代码必须在所有的获取参数动作之前tomcat8开始, 设置编码, 只需要针对post方式, Get方式自动设置编码(基于tomcat8)
2. Servlet继承关系
继承关系
javax.servlet.Servlet接口
javax.servlet.GenericServlet抽象类
javax.servlet.http.HttpServlet抽象子类相关方法
javax.servlet.Servlet接口:
包含三个抽象方法void init(config) - 初始化方法
void service(request,response) - 服务方法
void destory() - 销毁方法javax.servlet.GenericServlet抽象类:
实现了destory和init方法, service方法依然是抽象的
javax.servlet.http.HttpServlet 抽象子类:
void service(request,response) -service方法不再是抽象的
在javaweb的servlet中, 浏览器发过来的请求和相应给浏览器的请求被包装成两个对象,
分别是HttpServletRequest请求对象, HttpServletResponse响应对象, 在Servlet中可使用这两个对象
在service方法下:
String method = req.getMethod(); 获取请求的方式
各种if判断,根据请求方式不同,决定去调用不同的do方法
1
2
3
4
5
6
7if (method.equals("GET")) {
this.doGet(req,resp);
} else if (method.equals("HEAD")) {
this.doHead(req, resp);
} else if (method.equals("POST")) {
this.doPost(req, resp);
} else if (method.equals("PUT")) {在HttpServlet这个抽象类中,do方法都差不多:
1
2
3
4
5
6
7
8
9protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String protocol = req.getProtocol();
String msg = lStrings.getString("http.method_get_not_supported");
if (protocol.endsWith("1.1")) {
resp.sendError(405, msg);
} else {
resp.sendError(400, msg);
}
}
小结
- 继承关系: HttpServlet -> GenericServlet -> Servlet
- Servlet中的核心方法: init() , service() , destroy()
- 服务方法: 当有请求过来时,service方法会自动响应(其实是tomcat容器调用的)
在HttpServlet中我们会去分析请求的方式:到底是get、post、head还是delete等等
然后再决定调用的是哪个do开头的方法
那么在HttpServlet中这些do方法默认都是405的实现风格-要我们子类去实现对应的方法,否则默认会报405错误 - 因此,我们在新建Servlet时,我们才会去考虑请求方法,从而决定重写哪个do方法
3. HttpServletRequest类
①HttpServletRequest类的作用
HttpServletRequest对象代表客户端的请求,当客户端通过HTTP协议访问服务器时,HTTP请求头中的所有信 息都封装在这个对象中,通过这个对象提供的方法,可以获得客户端请求的所有信息, 这个类在Servlet的源码和 应用中都大量使用
②获取客户端信息的API
方法 | 功能 |
---|---|
getRequestURL() | 返回客户端发出请求时的完整URL |
getRequestURI() | 返回请求行中的参数部分 |
getQueryString () | 方法返回请求行中的参数部分(参数名+值) |
getRemoteHost() | 返回发出请求的客户端的完整主机名 |
getRemoteAddr() | 返回发出请求的客户端的IP地址 |
getPathInfo() | 返回请求URL中的额外路径信息。额外路径信息是请求URL中的位于Servlet的路径之后和查询参数之前的内容,它以”/“开头 |
getRemotePort() | 返回客户端所使用的网络端口号 |
getLocalAddr() | 返回WEB服务器的IP地址 |
getLocalName() | 返回WEB服务器的主机名 |
③获得客户端的请求头
方法 | 功能 |
---|---|
getParameter(String name) | 根据name获取请求参数(常用) |
getParameterValues(String name) | 根据name获取请求参数列表(当出现复选框,一个属性对应多个值时使用) |
getParameterMap() | 返回的是一个Map类型的值,该返回值记录着前端(如jsp页面)所提交请求中的请求参数和请求参数值的映射关系。(编写框架时常用) |
④请求转发(服务器内部转发)
指一个web资源收到客户端请求后,通知服务器去调用另外一个web资源进行处理。
ServletContext的getRequestDispatcher(String path)方法
RequestDispatcher reqDispatcher =this.getServletsContext().getRequestDispatcher(“/Handler01”); reqDispatcher.forward(request, response);
该方法返回一个RequestDispatcher对象,调用这个对象的forward方法可以实现请求转发
HttpServletRequest对象提供的getRequestDispatcher(String path)方法
request.getRequestDispatcher(“/Handler01”).forward(request, response);
该方法返回一个RequestDispatcher对象,调用这个对象的forward方法可以实现请求转发
request对象同时也是一个域对象(Map容器),开发人员通过request对象在实现转发时,把数据通过request对象带给其它web资源处理。
String data=”hello”;
//将数据存放到request对象中,此时把request对象当作一个Map容器来使用
request.setAttribute(“data”, data);
将请求转发(forward)到Handler01进行处理
request.getRequestDispatcher(“/Handler01”).forward(request, response);
⑤作为域对象(Map容器)使用
方法 | 功能 | 代码示例 |
---|---|---|
setAttribute(String name,Object o) | 将数据作为request对象的一个属性存放到request对象中 | request.setAttribute(“data”, data); |
getAttribute(String name) | 获取request对象的name属性的属性值 | request.getAttribute(“data”) |
removeAttribute(String name) | 移除request对象的name属性 | request.removeAttribute(“data”) |
getAttributeNames() | 获取request对象的所有属性名 | Enumeration attrNames = request.getAttributeNames(); |
getSession() | 获取Session作用域 | HttpSession session = request.getSession(); |
4 .Http协议
① Http 称之为 超文本传输协议
② Http是无状态的
③ Http请求响应包含两个部分:请求和响应
- 请求:
请求包含三个部分: 1.请求行 ; 2.请求消息头 ; 3.请求主体- 1)请求行包含是三个信息: 1. 请求的方式 ; 2.请求的URL ; 3.请求的协议(一般都是HTTP1.1)
- 2)请求消息头中包含了很多客户端需要告诉服务器的信息,比如:我的浏览器型号、版本、我能接收的内容的类型、我给你发的内容的类型、内容的长度等等
- 3)请求体,三种情况
get方式,没有请求体,但是有一个queryString
post方式,有请求体,form data
json格式,有请求体,request payload
- 响应:
响应也包含三本: 1. 响应行 ; 2.响应头 ; 3.响应体- 1)响应行包含三个信息:1.协议 2.响应状态码(200) 3.响应状态(ok)
- 2)响应头:包含了服务器的信息;服务器发送给浏览器的信息(内容的媒体类型、编码、内容长度等)
- 3)响应体:响应的实际内容(比如请求add.html页面时,响应的内容就是<form….)
5. 会话
1. Http是无状态的
- HTTP 无状态 :服务器无法判断这两次请求是同一个客户端发过来的,还是不同的客户端发过来的
- 无状态带来的现实问题:第一次请求是添加商品到购物车,第二次请求是结账;如果这两次请求服务器无法区分是同一个用户的,那么就会导致混乱
- 通过会话跟踪技术来解决无状态的问题。
2.会话跟踪技术(以session为例)
- 客户端第一次发请求给服务器,服务器获取session,获取不到,则创建新的,然后响应给客户端
- 下次客户端给服务器发请求时,会把sessionID带给服务器,那么服务器就能获取到了,那么服务器就判断这一次请求和上次某次请求是同一个客户端,从而能够区分开客户端
- 常用的API:
| 方法 | 功能 |
| :------------------------------: | :----------------------------------------: |
| request.getSession() | 获取当前的会话,没有则创建一个新的会话 |
| request.getSession(true) | 效果和不带参数相同 |
| request.getSession(false | 获取当前会话,没有则返回null,不会创建新的 |
| session.getId() | 获取sessionID |
| session.isNew() | 判断当前session是否是新的 |
| session.getMaxInactiveInterval() | session的非激活间隔时长,默认1800秒 |
| session.invalidate() | 强制性让会话立即失效 |
| ... | ... |
3. session保存作用域设定
- session保存作用域是和具体的某一个session对应的
- 常用的API:
| 方法 | 功能 |
| :—————————-: | :———————–: |
| void session.setAttribute(k,v) | 往作用域中添加键值对 |
| Object session.getAttribute(k) | 根据键从作用域中取值 |
| void removeAttribute(k) | 移除作用域中键为k的键值对 |
4.四个保存作用域
原始情况下,保存作用域我们可以认为有四个: page(页面级别,现在几乎不用) , request(一次请求响应范 围) , session(一次会话范围) , application(整个应用程序范围)
1) request:一次请求响应范围
2) session:一次会话范围有效
3) application: 一次应用程序范围有效
以上page, request, application都能使用和session相同功能不同作用域的API
6. Cookie
1. Cookie的本质
1)在浏览器端临时存储的数据
2)它以键值对的形式存储数据
3)键和值都是字符串类型
4)Cookie的数据量很小
2.Cookie在浏览器和服务器之间的传递
1)没有Cookie的状态
在服务器端没有创建Cookie并返回的情况下,浏览器端不会保存Cookie信息。双方在请求和响 应的过程中也不会携带Cookie的数据。
2)服务器创建Cookie对象并且返回
1 | // 1.创建Cookie对象 |
3)服务器返回Cookie的响应消息头
响应标头
Content-Type: text/html ; charset=UTF-8
Date: Tue,16 Mar 2021 06:13:52 GMT
Server: Apache-Coyote/1.1
Set-Cookie: cookie - mes sage=hello- cookie
Transfer- Encoding: chunked
4)浏览器拿到Cookie后, 以后的每一个请求都会携带Cookie信息
请求标头
Accept: text/html , application/xhtm1
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN, zh;q=0.9, en;
Connection: keep-alive
Cookie: cookie -message-hello-cookie;
Host: localhost: 8080
5)服务器读取Cookie的信息
1 | // 1.通过request对象获取Cookie的数组 |
3.Cookie的时效性
会话级Cookie
- 服务器端并没有明确指定Cookie的存在时间
- 在浏览器端,Cookie数据存在于内存中
- 只要浏览器还开着,Cookie数据就一直都在
- 浏览器关闭,内存中的Cookie数据就会被释放
持久化Cookie
- 服务器端明确设置了Cookie的存在时间
- 在浏览器端,Cookie数据会被保存到硬盘上
- Cookie在硬盘上存在的时间根据服务器端限定的时间来管控,不受浏览器关闭的影响
- 持久化Cookie到达了预设的时间会被释放
设置时效, 并区分会话级Cookie和持久化Cookie和设置Cookie作用域名和url的代码
1
2
3
4
5
6
7
8
9
10
11// ※给Cookie设置过期时间
// 正数:Cookie的过期时间,以秒为单位
// 负数:表示这个Cookie是会话级的Cookie,浏览器关闭时释放
// 0:通知浏览器立即删除这个Cookie
cookie.setMaxAge(20);
//设置Cookie作用的domin
cookie.setDomain(pattern);
//设置Coolie作用的uri
cookie.setPath(uri);服务器端返回Cookie附带过期时间的响应消息头中Set-Cookie会显示Expires=Tue+过期时间+时区
服务器通知浏览器删除Cookie时的响应消息头中Set-Cookie会显示Expires=Thu+初始时间戳
4.Cookie和会话的关系
Cookie是客户端的技术
Session是服务器端的技术
Session依赖于Cookie
每当创建Session时, 客户端就会创建一个key为JSESSIONID, value为随机序列的Cookie
当服务端创建一个Session时(request.getSession()), 响应报文会携带一个Cookie
Response Headers
Set-Cookie: JSESSIONID=一串随机序列; Path=/springmvc; HttpOnly
创建一个Session后, 服务器还会将HttpSession对象存放到服务器维护的Map集合中, 将Cookie的value(就是那个随机序列)作为Map集合的键, 将HttpSession对象作为Map集合的值,存储在服务器内部, 再把Cookie响应到浏览器
7. 服务器内部转发以及客户端重定向
1.服务器内部转发 :
request.getRequestDispatcher(“…”).forward(request,response);
一次请求响应的过程,对于客户端而言,内部经过了多少次转发,客户端是不知道的
地址栏没有变化
2.客户端重定向
response.sendRedirect(“….”);
两次请求响应的过程。客户端肯定知道请求URL有变化
地址栏有变化
四.简单功能分析
1. 静态资源访问
静态资源
可以理解为前端的固定页面,这里面包含HTML、CSS、JS、图片等等,不需要查数据库也不需要程序处理,直接就能够显示的页面
静态资源目录
只要静态资源放在与java目录同级resource目录的特定文件夹下
/static
,/public
,/resource
,/META-INF/resource
, 这个静态资源就能通过 当前项目根路径/+静态资源名直接访问静态资源访问原理
静态映射是/**, 当浏览器发送请求时, 会先去找Controller看能不能进行动态处理, 不能处理的所有请求会交给静态资源处理器, 如果静态资源也找不到则会报404错误, 因此如果静态资源名和Controller一个映射名相同时, 处理请求优先交给Controller
改变默认的静态资源访问前缀和路径
静态资源访问默认是无前缀的, 因为之后会学习拦截器, 拦住浏览器发送部分请求(如登录拦截器防止跨过登录访问), 而静态映射是/**, 拦截器可能会将所有的静态资源也拦截掉, 因此我们通常会给静态资源路径加一个前缀防止被拦截
1
2
3
4
5
6
7
8
9
10spring:
mvc:
static-path-pattern: /res/**
#这样以后访问静态资源就要加上/res前缀
spring:
web:
resources:
static-locations: [classpath:/name1/]
#这样就能改变静态资源路径(2.5.6以前的版本没有web:), classpath参数是个string数组,
#可指定多个resource目录下的文件夹
2. 静态资源配置原理
SpringBoot启动默认加载 xxxAutoConfiguration类 (自动配置类)
SpringMVC功能的自动配置类 WebMvcAutoConfiguration 会生效 所在包org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration
1
2
3
4
5
6
7
8
9
10
11
12//源码来源于 WebMvcAutoConfiguration.class
public class WebMvcAutoConfiguration {}容器中配置的功能
1
2
3
4
5
6
7//源码来源于 WebMvcAutoConfiguration.class
在WebMvcAutoConfigurationAdapter类中发现配置文件和相关类(WebMvcProperties, WebProperties)进行了绑定
ctrl点进类中可以发现, WebMvcProperties与前缀spring.mvc进行绑定, WebProperties与前缀spring.web进行绑定
WebMvcAutoConfigurationAdapter类只有一个有参构造器, 有参构造器的所有值都会在容器中确定
资源处理的默认规则
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21//源码来源于 WebMvcAutoConfiguration.class的addResourceHandlers方法
public void addResourceHandlers(ResourceHandlerRegistry registry) {
if (!this.resourceProperties.isAddMappings()) {
logger.debug("Default resource handling disabled");
/*静态资源被禁用了*/
} else {
this.addResourceHandler(registry, "/webjars/**", "classpath:/META-INF/resources/webjars/");
/*规定外部依赖的jar包中静态资源路径规则
设置为/META-INF/resources/webjars/路径下
这样所有外部jar包静态资源路径下的静态资源都能被直接访问*/
this.addResourceHandler(registry, this.mvcProperties.getStaticPathPattern(), (registration) -> {
registration.addResourceLocations(this.resourceProperties.getStaticLocations());
/*getStaticPathPattern()默认静态资源目录配置的位置*/
if (this.servletContext != null) {
ServletContextResource resource = new ServletContextResource(this.servletContext, "/");
registration.addResourceLocations(new Resource[]{resource});
}
});
}
}- addResourceHandlers方法首先会执行isAddMappings()来请求add-mapping, 这个属性与配置文件的add-mapping进行绑定
- 如果配置文件中的add-mapping设置为false, !this.resourceProperties.isAddMappings()返回值为true, 下面的所有代码都不执行, SpringBoot所有的静态资源将全部无法访问
- 如果配置文件中的add-mapping设置为true, 则会在下面的代码注册静态资源默认访问规则
- 默认静态资源路径位置存在于getStaticPathPattern()方法, 里面指定了所有静态资源所在路径
- 欢迎页的处理在WelcomePageHandlerMapping方法中, (
注: Handler[处理映射器]
捕捉到需要处理特定请求后, 返回处理这个请求的方法)
四. 请求参数处理
1. REST( 使用HTTP请求动词来表示对资源的操作)原理
1 |
|
RequestMethod枚举类参数: GET(获取), HEAD, POST(保存), PUT(修改), PATCH, DELETE(删除), OPTIONS, TRACE;
如果用来请求的前端语言不支持GET, POST以外的请求, 就要手动开启HiddenHttpMethodFilter, 并且表单method设置为post, 隐藏域_method=xx就能处理请求
1
2
3
4
5
6
7
8
9<form action="/user" method="get">
<input value="REST-GET 提交" type="submit">
</form>
<form action="/user" method="post">
<input name="_method" type="hidden" value="DELETE">
<input value="REST-DELETE 提交" type="submit">
</form>
- 详细在WebMvcAutoConfiguration的OrderedHiddenHttpMethodFilter方法中
- 手动开启HiddenHttpMethodFilter方法: 配置文件中spring: mvc: hiddenmethod: filter: enable: true开启页面表单的REST功能
REST原理(表单提交要使用REST时)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22//源码来源于 HiddenHttpMethodFilter.class
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
HttpServletRequest requestToUse = request;
if ("POST".equals(request.getMethod()) && request.getAttribute("javax.servlet.error.exception") == null) {
//判断: 如果请求为POST并且请求正常
String paramValue = request.getParameter(this.methodParam);
//获取请求参数
if (StringUtils.hasLength(paramValue)) {
//判断: 如果请求参数不为空
String method = paramValue.toUpperCase(Locale.ENGLISH);
//将请求参数转换为大学
if (ALLOWED_METHODS.contains(method)) {
//判断: 查看传进来的请求参数 是否为允许的请求
//ALLOWED_METHODS: 兼容PUT DELETE PATCH
requestToUse = new HiddenHttpMethodFilter.HttpMethodRequestWrapper(request, method);
//通过HttpMethodRequestWrapper对原生request进行包装
}
}
}
filterChain.doFilter((ServletRequest)requestToUse, response);
}- 表单提交时会带上
_method=PUT
参数, - 映射请求处理时先被HiddenHttpMethodFilter过滤器拦截, 并在doFilterInternal方法中先检查是不是post请求, 请求是否正常
- 之后执行request.getParameter()方法获取请求参数, 转换为大写后查看请求是否合法
- 通过HttpMethodRequestWrapper对原生request进行包装, 包装模式requestWrapper重写了getMethod方法, 过滤器链放行的时候用wrapper, 以后的方法调用getmethod是调用requesWarpper里面的
- 表单提交时会带上
REST使用客户端工具, 如PostMan直接发送Put, delete等方式请求, 无需Filter(在HTTP层,请求就已经不是post了), 此时spring: mvc: hiddenmethod: filter: enable: 就没有必要开启了
2. 请求映射原理
所有外部发来的请求都会传入DispatcherSelvlet, 这是所有请求的开始.
DispatcherSelvlet继承于HttpSelvlet类, Ctrl+F12打开查看HttpSelvlet类结构可以看到doGet和doPost等方法
在DispatcherSelvlet按Ctrl+H可以查看继承树, 继承关系为:
Object <- GenericServlet <- HttpServlet <- FrameworkServlet <- DispatcherServlet
FrameworkServlet重写的doGet等方法执行时, 会调用FrameworkServlet类中的processRequest方法
1
2
3
4//源码来源于: FrameworkServlet.class
protected final void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.processRequest(request, response);
}processRequest方法执行后, 先是一大串初始化过程, 然后再try-catch中执行doService方法
1
2
3
4
5//源码来源于: FrameworkServlet.class的processRequest方法
try {
this.doService(request, response);
//执行doService方法
} catch (IOException | ServletException var16) {}FrameworkServlet类中的doService是一个抽象方法, 它会在子类DispatcherServlet中实现并且执行, 里面先进行初始化过程, 然后再try-catch中执行doDispatch方法, 把这个请求做转发
1 | //源码来源于: DispatcherServlet.class的doService方法 |
doDispatch()方法中含有核心功能, 是DispatcherServlet中最重要的方法, 每个请求最终都会调用doDispatcher方法, SpringMVC的功能分析要看这个方法
doDispatcher会加载很多关键配置, 调用getHandler方法查看当前请求该由哪个handler(Controller处理器的方法)处理
1
2
3
4
5
6
7
8
9
10
11//源码来源于: DispatcherServlet.class的doDispatcher方法
//源码只截取了部分
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
try {
try {
try {
processedRequest = this.checkMultipart(request);
multipartRequestParsed = processedRequest != request;
//getHandler: 查看当前请求该由哪个handler(Controller处理器的方法)处理
mappedHandler = this.getHandler(processedRequest);
getHandler()方法会使用handlerMappings(处理器映射, 保存众多/xxx请求应该由xxxHandler处理的映射, 所有的请求映射都保存再handlerMapping中), 默认有五个handlerMapping, 包含
RequestMappingHandlerMapping 保存了所有@RequestMapping注解和handler的映射规则,这是SpringBoot配置的默认handlerMapping
WelcomepageHandlerMapping 欢迎页访问规则, 处理所有/index请求
BeanNameUrlHandlerMapping
RouterFunctionHandlerMapping
SimpleUrlHandlerMapping
注①: springboot运行时, 就会将各个映射和处理器的特定方法联系起来, 并将这些规则保存至HandlerMapping的注册中心(MappingRegistry)中, 这样有请求访问映射时, 就会迅速得知要把这个请求交给哪个方法处理
注②: 所有handlerMapping在源码中都是加了@Bean的组件, 当我们需要一些自定义映射规则时, 也可以自己给容器中放handlerMapping, 实现自定义handlerMapping
注③: 所有的请求映射都保存再handlermapping中,例如:springboot自动配置了欢迎页的handlerMapping,访问/直接能访问到index.html
注④: 请求进来, 挨个尝试所有的handlermapping看是否有请求信息,如果有就找到对应的handler(处理器中的方法), 如果没有就找下一个handlermapping
getHandler方法里有一个循环, 遍历handlerMappings挨个寻找哪个HandlerMapping 能处理这个映射, 找到handlerMapping后就通过请求参数找到对应的handler, 最终将请求交给handler处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15//源码来源于: DispatcherServlet.class的getHandler方法
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
if (this.handlerMappings != null) {
Iterator var2 = this.handlerMappings.iterator();
//this.handlerMappings.iterator();获取指针, 通过hasNext遍历系统中的 HandlerMapping查看谁能处理请求
while(var2.hasNext()) {
HandlerMapping mapping = (HandlerMapping)var2.next();
HandlerExecutionChain handler = mapping.getHandler(request);
if (handler != null) {
return handler;
}
}
}
return null;
}
3. 获取请求参数
① @PathVariable注解
将@PathVariable注解作用于形参上, 可以把URL 中占位符参数绑定到控制器处理方法的形参中
1 |
|
此时就可以通过形参直接获取请求参数, 并且可以使用REST风格的请求路径通过/Test01/001/admin来访问handler
② HttpServletRequest request
使用Servlet的API来获取请求参数, Servlet会将发送过来的请求封装成一个类, 这个类叫HttpServletRequest,
Controller本质也是个Servlet, 它是由DispatchServlet通过handlerMappings(处理器映射)来正确转发到对应
Controller的handler, 在Controller中也可以使用Servlet的API
1 |
|
③ @RequestParam
- 将请求参数和控制器方法的形参创建映射关系, 作用与形参上, 将请求参数和形参进行匹配
- 注解的value和name属性是同义的, 都表示此形参对应的请求参数的名字
- required属性表示这个参数是否必须传输到后端中, 默认设置为true, 此时此注解作用的形参如果在请求中, 前端没有传值过来, 那么后端就会报400错误, 设置为false则传入参数不是必须的, 就不会报错
- defaultValue表示当没有传请求参数过来时, 后端参数对应形参的默认值, 只能在required属性为false的情况下使用
1 |
|
④通过POJO获取请求参数
当浏览器传输的请求参数和一个POJO实体类中的参数名属性名一致时, 就可以再控制器形参直接放置实体类POJO, SpringMVC会自动为此实体类的属性和请求参数中同名参数建立映射关系, 从而可以直接调用此POJO来获取参数
前端代码示例
1
2
3
4
5
6
7
8
9
10<form th:acti on="@{/testPojo}" method="post ">
用户名: <input type="text" name="username"> <br>
密码: <input type="password" name="password"> <br>
性别: <input type="radio" name="sex" value="男">男
<input type="radio" name="sex" value="女">女<br>
年龄: <input type="text" name="age"> <br>
邮箱: <input type="text" name=" email"> <br>
<input type=" submit">
</form>POJO类(省略Get Set方法)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19public class User {
private String username;
private String password;
private String sex;
private Integer age;
private String email;
public User(){
}
public User(String username, String password, String sex, Integer age, String email) {
this.username = username;
this.password = password;
this.sex = sex;
this.age = age;
this.email = email;
}
}控制层
1
2
3
4
5
6
public String TestPojo(User user){
String name = user.getUsername();
String password = user.getPassword();
return name+password;
}这时控制层形参与请求参数的属性建立了映射关系, 可以直接再形参使用user对象, 从来调取浏览器请求参数传过来的值
4. 普通参数和基本注解