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
2
3
4
5
6
7
8
9
10
11
12
13
@SpringBootApplication
public class Application {
public static void main(String[] args) {
//SpringApplication.run(Application.class, args); 下面是引入局部变量的写法
//1. 引入IOC容器
ConfigurableApplicationContext run = SpringApplication.run(Application.class, args);
//2. 查看IOC容器内的组件
String[] Names = run.getBeanDefinitionNames();//获取所有组件定义的名字
for (String name : Names) { //打印组件的名字
System.out.println(name);
}
}
}

二. 注解和反射

1. 注解

1. 注解定义

​ java注解是在JDK5的时候引入的一种新特性。注解(也可以称为元数据)为在代码中添加信息提供了一种 形式化的方法,使得在代码中任一时刻可以非常方便的使用这些数据。

​ 注解类型定义了一种新的特殊接口类型,在接口关键期interface之前加@符号,即用@interface即可区分 注解与普通接口声明。目前大部分框架都是通过使用注解简化代码提高编码效率

​ 所有的注解本质上都是继承自 Annotation 接口。但是,手动定义一个接口继承 Annotation 接口无效的, 需要通过 @interface 声明注解,Annotation 接口本身也不定义注解类型,只是一个普通的接口。

2. 注解的属性

​ ①. 基本数据类型(int,float,boolean 等)

1
2
3
4
5
6
7
8
9
10
11
12
13
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface TestAnnotation {
//int基本数据类型
int type() default -1;

//boolean基本数据类型
boolean status() default false;

//Double基本数据类型
Double value();

}

​ ②. String类型

1
2
3
4
public @interface TestAnnotation {
//String类型
String Value() default "";
}

​ ③. Class 类型

1
2
3
4
public @interface TestAnnotation {
//Class类型
Class<?> Value() default String.class;
}

​ ④. enum枚举类型

1
2
3
4
5
6
7
enum Week {
Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday
}
public @interface TestAnnotation {
//enum枚举类型
Week Value() default Week.Sunday;
}

​ ⑤. 注解类型

1
2
3
4
5
6
7
@interface TestAnnotation01 {
boolean Value() default false;
}
public @interface TestAnnotation02 {
//enum枚举类型
TestAnnotation01 Value() default @TestAnnotation01(Value="true");
}

​ ⑥. 以上类型的数组(以枚举数组为例)

1
2
3
4
5
6
7
enum Week {
Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday
}
public @interface TestAnnotation {
//数组类型
Week[] Value();
}

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 c1 = user01.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请求方式:

    1. 获取参数: String fname = request.getParameter(“fname”);

    2. 将字符串打散成字节数组 byte[] bytes = fname.getBytes(“ISO-8859-1”);

    3. 将字节数组按照设定的编码重新组装成字符串 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方法下:

      1. String method = req.getMethod(); 获取请求的方式

      2. 各种if判断,根据请求方式不同,决定去调用不同的do方法

        1
        2
        3
        4
        5
        6
        7
        if (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")) {
      3. 在HttpServlet这个抽象类中,do方法都差不多:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      protected 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);
      }
      }
  • 小结

    1. 继承关系: HttpServlet -> GenericServlet -> Servlet
    2. Servlet中的核心方法: init() , service() , destroy()
    3. 服务方法: 当有请求过来时,service方法会自动响应(其实是tomcat容器调用的)
      在HttpServlet中我们会去分析请求的方式:到底是get、post、head还是delete等等
      然后再决定调用的是哪个do开头的方法
      那么在HttpServlet中这些do方法默认都是405的实现风格-要我们子类去实现对应的方法,否则默认会报405错误
    4. 因此,我们在新建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

1. Cookie的本质

​ 1)在浏览器端临时存储的数据

​ 2)它以键值对的形式存储数据

​ 3)键和值都是字符串类型

​ 4)Cookie的数据量很小

2.Cookie在浏览器和服务器之间的传递

​ 1)没有Cookie的状态

​ 在服务器端没有创建Cookie并返回的情况下,浏览器端不会保存Cookie信息。双方在请求和响 应的过程中也不会携带Cookie的数据。

​ 2)服务器创建Cookie对象并且返回

1
2
3
4
5
6
7
8
// 1.创建Cookie对象
Cookie cookie = new Cookie("cookie-message", "hello-cookie");

// 2.将Cookie对象添加到响应中
response.addCookie(cookie);

// 3.返回响应
request.getRequestDispatcher("hello01.html").forward(request,response);

​ 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
image-20220806144213363

​ 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
image-20220806144948283
​ 5)服务器读取Cookie的信息

1
2
3
4
5
6
7
8
9
// 1.通过request对象获取Cookie的数组
Cookie[] cookies = request.getCookies();

// 2.遍历数组
for (Cookie cookie : cookies) {
System.out.println("cookie.getName() = " + cookie.getName());
System.out.println("cookie.getValue() = " + cookie.getValue());
System.out.println();
}

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

    image-20220807210500757

  • 创建一个Session后, 服务器还会将HttpSession对象存放到服务器维护的Map集合中, 将Cookie的value(就是那个随机序列)作为Map集合的键, 将HttpSession对象作为Map集合的值,存储在服务器内部, 再把Cookie响应到浏览器

7. 服务器内部转发以及客户端重定向

1.服务器内部转发 :

​ request.getRequestDispatcher(“…”).forward(request,response);

  • 一次请求响应的过程,对于客户端而言,内部经过了多少次转发,客户端是不知道的

  • 地址栏没有变化

    04.服务器内部转发

2.客户端重定向

​ response.sendRedirect(“….”);

  • 两次请求响应的过程。客户端肯定知道请求URL有变化

  • 地址栏有变化

05.客户端重定向(1)

四.简单功能分析


1. 静态资源访问

  1. 静态资源

    可以理解为前端的固定页面,这里面包含HTML、CSS、JS、图片等等,不需要查数据库也不需要程序处理,直接就能够显示的页面

  2. 静态资源目录

    只要静态资源放在与java目录同级resource目录的特定文件夹下 /static ,/public,/resource,/META-INF/resource, 这个静态资源就能通过 当前项目根路径/+静态资源名直接访问

  3. 静态资源访问原理

    静态映射是/**, 当浏览器发送请求时, 会先去找Controller看能不能进行动态处理, 不能处理的所有请求会交给静态资源处理器, 如果静态资源也找不到则会报404错误, 因此如果静态资源名和Controller一个映射名相同时, 处理请求优先交给Controller

  4. 改变默认的静态资源访问前缀和路径

    静态资源访问默认是无前缀的, 因为之后会学习拦截器, 拦住浏览器发送部分请求(如登录拦截器防止跨过登录访问), 而静态映射是/**, 拦截器可能会将所有的静态资源也拦截掉, 因此我们通常会给静态资源路径加一个前缀防止被拦截

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    spring:
    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
    @Configuration(
    proxyBeanMethods = false
    )
    @ConditionalOnWebApplication(
    type = Type.SERVLET
    )
    @ConditionalOnClass({Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class})
    @ConditionalOnMissingBean({WebMvcConfigurationSupport.class})
    @AutoConfigureOrder(-2147483638)
    @AutoConfigureAfter({DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class, ValidationAutoConfiguration.class})
    public class WebMvcAutoConfiguration {}
  • 容器中配置的功能

    1
    2
    3
    4
    5
    6
    7
     //源码来源于 WebMvcAutoConfiguration.class
    @Configuration(
    proxyBeanMethods = false
    )
    @Import({WebMvcAutoConfiguration.EnableWebMvcConfiguration.class})
    @EnableConfigurationProperties({WebMvcProperties.class, WebProperties.class})
    @Order(0)
    • 在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
2
3
4
5
6
7
8
9
10
@RequestMapping(value = "/user",method = RequestMethod.GET)
public String getUser()
return "GET-张三";
}

@RequestMapping(value = "/user",method = RequestMethod.POST)
public String saveUser(){
return "POST-张三";
}

  • 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

  1. 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);
    }
  2. processRequest方法执行后, 先是一大串初始化过程, 然后再try-catch中执行doService方法

    1
    2
    3
    4
    5
    //源码来源于: FrameworkServlet.class的processRequest方法
    try {
    this.doService(request, response);
    //执行doService方法
    } catch (IOException | ServletException var16) {}
  3. FrameworkServlet类中的doService是一个抽象方法, 它会在子类DispatcherServlet中实现并且执行, 里面先进行初始化过程, 然后再try-catch中执行doDispatch方法, 把这个请求做转发

1
2
3
4
//源码来源于: DispatcherServlet.class的doService方法
try {
this.doDispatch(request, response);
} finally {}
  1. doDispatch()方法中含有核心功能, 是DispatcherServlet中最重要的方法, 每个请求最终都会调用doDispatcher方法, SpringMVC的功能分析要看这个方法

  2. 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);

  3. 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
2
3
4
@RequestMapping("/Test01/{id}/{userName}")
public String Test01(@PathVariable("id") String id,@PathVariable("userName") String userName){
return "success"+id+userName;
}

​ 此时就可以通过形参直接获取请求参数, 并且可以使用REST风格的请求路径通过/Test01/001/admin来访问handler

② HttpServletRequest request

​ 使用Servlet的API来获取请求参数, Servlet会将发送过来的请求封装成一个类, 这个类叫HttpServletRequest,

​ Controller本质也是个Servlet, 它是由DispatchServlet通过handlerMappings(处理器映射)来正确转发到对应

​ Controller的handler, 在Controller中也可以使用Servlet的API

1
2
3
4
5
6
@RequestMapping("/Test02")
public String Test02(HttpServletRequest request){
String Name = request.getParameter("name");
String Id = request.getParameter("id");
return Name+Id;
}
③ @RequestParam
  • 将请求参数和控制器方法的形参创建映射关系, 作用与形参上, 将请求参数和形参进行匹配
  • 注解的value和name属性是同义的, 都表示此形参对应的请求参数的名字
  • required属性表示这个参数是否必须传输到后端中, 默认设置为true, 此时此注解作用的形参如果在请求中, 前端没有传值过来, 那么后端就会报400错误, 设置为false则传入参数不是必须的, 就不会报错
  • defaultValue表示当没有传请求参数过来时, 后端参数对应形参的默认值, 只能在required属性为false的情况下使用
1
2
3
4
5
6
7
8
@RequestMapping("/Test03")
public String Test03(
//前端传过来userName在后端中取别名name使用,且如果请求不传会报错
@RequestParam(value = "userName",required = true) String name,
//前端传过来userId在后端中取别名id使用
@RequestParam(name = "userId",required = false,defaultValue="小明") Integer id){
return name+id.toString();
}

④通过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
    19
    public 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
    @RequestMapping("/TestPojo")
    public String TestPojo(User user){
    String name = user.getUsername();
    String password = user.getPassword();
    return name+password;
    }

    这时控制层形参与请求参数的属性建立了映射关系, 可以直接再形参使用user对象, 从来调取浏览器请求参数传过来的值

4. 普通参数和基本注解