SpringBoot2-基础

SpringBoot2 核心技术与响应式编程

学习要求

  • 熟悉Spring基础
  • 熟悉Maven使用

环境要求

学习资料

一、SpringBoot 是什么

1、为什么要用 SpringBoot

Spring Boot makes it easy to create stand-alone, production-grade Spring based Applications that you can "just run".

能快速创建出生产级别的 Spring 应用。

2、SpringBoot 优点

  • Create stand-alone Spring applications

    • 创建独立 Spring 应用
  • Embed Tomcat, Jetty or Undertow directly (no need to deploy WAR files)

    • 内嵌 Web 服务器(默认使用 Tomcat)
  • Provide opinionated 'starter' dependencies to simplify your build configuration

    • 自动starter依赖,简化构建配置
  • Automatically configure Spring and 3rd party libraries whenever possible

    • 自动配置 Spring 以及第三方功能
  • Provide production-ready features such as metrics, health checks, and externalized configuration

    • 提供生产级别的监控、健康检查及外部化配置
  • Absolutely no code generation and no requirement for XML configuration

    • 无代码生成、无需编写 XML

SpringBoot 是整合 Spring 技术栈的一站式框架。

SpringBoot 是简化 Spring 技术栈的快速开发脚手架。

3、SpringBoot 缺点

  • 迭代快,需要时刻关注版本变化。
  • 封装太深,内部原理复杂,不容易精通。

4、微服务

James Lewis and Martin Fowler (2014) 提出微服务完整概念。

In short, the microservice architectural style is an approach to developing a single application as a suite of small services, each running in its own process and communicating with lightweight mechanisms, often an HTTP resource API. These services are built around business capabilities and independently deployable by fully automated deployment machinery. There is a bare minimum of centralized management of these services, which may be written in different programming languages and use different data storage technologies.-- James Lewis and Martin Fowler (2014)

  • 微服务是一种架构风格
  • 一个应用拆分为一组小型服务
  • 每个服务运行在自己的进程内,也就是可独立部署和升级
  • 服务之间使用轻量级 HTTP 交互
  • 服务围绕业务功能拆分
  • 可以由全自动部署机制独立部署
  • 去中心化服务自治。服务可以使用不同的语言不同的存储技术

5、分布式

分布式的困难

  • 配置管理
  • 服务发现
  • 远程调用
  • 负载均衡
  • 服务容错
  • 服务监控
  • 链路追踪
  • 日志管理
  • 任务调度
  • ......

分布式的解决

  • SpringBoot + SpringCloud

6、云原生(Cloud Native)

原生应用如何上云。

上云的困难

  • 服务自愈
  • 弹性伸缩
  • 服务隔离
  • 自动化部署
  • 灰度发布
  • 流量治理
  • ......

上云的解决

  • Docker 容器化技术
  • Kubernetes 容器编排,简称 k8s
  • DevOps,企业 CI/CD,构建企业云平台
  • 拥抱新一代架构 Service MeshServerless

7、如何学习SpringBoot

7.1、官网文档架构



查看版本新特性;
https://github.com/spring-projects/spring-boot/wiki#release-notes

二、基础入门

1、环境准备

参照官方帮助文档配置:

  • Java 8 及以上。

  • Maven 3.3+:项目管理工具,可以对 Java 项目进行构建、依赖管理。

    • pom.xml 按照官方帮助文档设定。
  • Gradle 6.x:基于 Apache Ant 和 Apache Maven 概念的项目自动化构建开源工具。它使用一种基于 Groovy 的特定领域语言(DSL)来声明项目设置,目前也增加了基于 Kotlin 语言的 kotlin-based DSL,抛弃了基于 XML 的各种繁琐配置。

  • SpringBoot CLI:命令行工具,用于使用 Spring 进行快速原型搭建。它允许你运行 Groovy 脚本,这意味着你可以使用类 Java 的语法,并且没有那么多的模板代码。

  • IntellIJ IDEA: 微云上下载。

1.1 Maven 设置

给 Maven 的 settings.xml 中添加默认 JDK 版本以及默认 UTF-8 编码。

<profiles>
    <profile>
      <id>JDK1.8</id>
      <activation>
        <activeByDefault>true</activeByDefault>
        <jdk>1.8</jdk>
      </activation>
      <properties>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <maven.compiler.compilerVersion>1.8</maven.compiler.compilerVersion>
        <!--编译编码-->
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
      </properties>
    </profile>
</profiles>

1.2 IDEA 设置

将 Maven 整合进来,IDEA 中 设置新项目设置 中的 Maven 都要设定。

2、HelloWorld

功能:浏览器发送/hello请求,服务器接受请求并处理,响应 Hello World 字符串。

2.1 创建一个 Maven 工程

新建一个名为 spring-boot-01-helloworld 的 Maven 工程。

2.2 导入 SpringBoot 相关的依赖

pom.xml 中加入 SpringBoot 依赖。

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.2.6.RELEASE</version>
    <!-- 一般依赖最新版本 -->
</parent>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <!-- Web 应用开发场景 -->
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>

2.3 编写主程序 MainApplication

src\main\java 下新建一个 com.xxx.HelloWorldMainApplication类。

/**
 *  SpringBootApplication 标注一个主程序类,说明这是一个SpringBoot应用
 */
@SpringBootApplication
public class HelloWorldMainApplication {
    public static void main(String[] args) {
        // 启动Spring应用
        SpringApplication.run(HelloWorldMainApplication.class,args);
    }
}

2.4 编写 Controller、Service

src\main\java 下新建一个 controller.HelloController 类。

@Controller // 作为控制器组件注册到容器中
public class HelloController {
    @ResponseBody // 返回结果(json或xml)直接写入 HTTP response body 中,一般在Ajax异步获取数据时使用
    @RequestMapping("/hello") // 映射/hello请求,即该方法处理/hello请求
    public String hello(){
        return "Hello World!";
    }
}

2.5 运行主程序测试

Ctrl+F5 运行 HelloWorldMainApplicationmain方法。

2.6、简化配置

application.properties

server.port=8888

2.7 简化部署

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <!-- Maven 插件,可以将应用打包成一个可执行的 jar 包 -->
            <artifactId>spring-boot-maven-plugin</artifactId>
            <configuration>
                <!-- 打包时跳过测试 -->
                <skipTests>true</skipTests>
            </configuration>
        </plugin>
    </plugins>
</build>

将这个应用打成 jar 包,直接使用java -jar的命令进行执行。

3、Hello World探究

3.1 POM文件

3.1.1 依赖管理

项目下的 pom.xml

依赖管理    
<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.4.RELEASE</version>
</parent>

spring-boot-starter-parent他的父项目
 <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-dependencies</artifactId>
    <version>2.3.4.RELEASE</version>
  </parent>

几乎声明了所有开发中常用的依赖的版本号,自动版本仲裁机制

以后我们导入默认依赖是不需要写版本号的,自动版本仲裁(没有在 dependencies 里面管理的依赖需要声明版本号)。

1、引入依赖默认都可以不写版本
2、引入非版本仲裁的jar,要写版本号。

可以修改默认版本号

1、查看spring-boot-dependencies里面规定当前依赖的版本 用的 key。
2、在当前项目里面重写配置
    <properties>
        <mysql.version>5.1.43</mysql.version>
    </properties>

3.1.2 启动器

<dependency>
    <groupId>org.springframework.boot</groupId>
    <!-- Web 应用开发场景 -->
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

spring-boot-starter-*:SpringBoot 官方场景启动器,会导入相关开发场景所需的所有依赖。

*-spring-boot-starter:自定义的场景启动器。

SpringBoot 将所有的功能场景都抽取出来,做成一个个的 starter(启动器),只需要在项目里面引入这些 starter,相关场景的所有依赖都会导入进来。要用什么功能就导入什么场景的启动器,引入之后记得点击编辑区右上角的 m 图标,来下载这些依赖。

SpringBoot 支持的所有场景

https://docs.spring.io/spring-boot/docs/current/reference/html/using-spring-boot.html#using-boot-starter

所有场景启动器最底层的依赖

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter</artifactId>
  <version>2.3.4.RELEASE</version>
  <scope>compile</scope>
</dependency>

3.2 主程序类

/**
 *  SpringBootApplication 标注一个主程序类,说明这是一个SpringBoot应用
 */
@SpringBootApplication
public class HelloWorldMainApplication {
    public static void main(String[] args) {
        // 启动Spring应用
        SpringApplication.run(HelloWorldMainApplication.class, args);
    }
}

@SpringBootApplication:标注在某个类上说明这个类是 SpringBoot 的主配置类,SpringBoot 就应该运行这个类的 main() 方法来启动 SpringBoot 应用。

4、快速创建 SpringBoot 项目

4.1 IDEA:使用 Spring Initializer 快速创建项目

项目类型选择 Spring Initializr,修改完(公司名)、工件(项目名)和Java版本后,选择我们需要的模块(如 Spring Web),向导会联网创建 SpringBoot 项目并下载到本地。

  • 主程序已经生成好了,只需编写自己的业务代码。
  • resources文件夹中目录结构
    • static:静态资源, js、css、images。
    • templates:模板页面(SpringBoot 默认默认不支持 jsp 页面),可以使用模板引擎(FreeMarkerThymeleaf)。
    • application.properties:配置文件,可以修改一些默认设置。

三、容器功能

组件添加

原先原生spring的方式:

先写两个类

User

package com.example.bot.bean;

public class User {
    private String name;
    private Integer age;

    public User() {

    }

    public User(String name, Integer age) {
        this.name = name;
        this.age = age;
    }

    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public Integer getAge() { return age; }
    public void setAge(Integer age) { this.age = age; }


    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

Pet

package com.example.bot.bean;

public class Pet {
    private String name;
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }

    public Pet() {
    }

    public Pet(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Pet{" +
                "name='" + name + '\'' +
                '}';
    }
}

然后bean.xml注册

@Configuration

Myconfig

package com.example.bot.config;

import com.example.bot.bean.Pet;
import com.example.bot.bean.User;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * 1.配置类里面使用@Bean标注在方法上给容器注册组件,默认也是单实例得
 * 2.配置类本身也是组件
 * 3.proxyBeanMethods:代理 bean 的方法
 */
@Configuration(proxyBeanMethods = true)  //告诉SpringBoot 这是一个配置类 == bean.xml 配置文件
public class Myconfig {
    /**
     * 外部无论对配置类中的这个组件注册方法调用多少次获取的都是之前注册容器的单实例对象
     * @return
     */
    @Bean //给容器中添加组件。以方法名作为组件的id。返回类型就是组件类型。返回的值,就似乎组件在容器中的实例
    public User user01(){
        return new User("zhangsan", 18);
    }

    @Bean("tom")  //加了参数的话,组件的id 就是 tom ,而不是tomcatPet
    public Pet tomcatPet(){
        return new Pet("tomcat");
    }
}

MainApplication

package com.example.bot;

import com.example.bot.bean.Pet;
import com.example.bot.bean.User;
import com.example.bot.config.Myconfig;
import jdk.internal.org.objectweb.asm.tree.analysis.SourceInterpreter;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Configuration;

@SpringBootApplication
public class MainApplication {
    public static void main(String[] args) {
        //1. 返回我们IOC 容器
        ConfigurableApplicationContext run = SpringApplication.run(MainApplication.class, args);

        //2. 查看容器里面的组件
        String[] names = run.getBeanDefinitionNames();
        for(String name : names) {
            System.out.println(name);
        }
        //3.从容器中获取组件
        Pet tom01 = run.getBean("tom", Pet.class);
        Pet tom02 = run.getBean("tom", Pet.class);
        System.out.println("组件:" + (tom01 == tom02));   //组件:true   表示默认单例的

        Myconfig bean = run.getBean(Myconfig.class);
        //说明配置类Myconfig本身也是组件
        System.out.println(bean);    //com.example.bot.config.Myconfig$$EnhancerBySpringCGLIB$$e92f60bb@3003697

        //如果 @Configuration(proxyBeanMethods = true) 代理对象调用方法。SpringBoot总会检查这个组件是否在容器中有
        //保持组件单实例
        User user = bean.user01();
        User user1 = bean.user01();
        System.out.println(user == user1);
    }
}

@Import

@Import({User.class, DBHelper.class})   //给容器中自动创建出这两个类型的组件、默认组件的名字就是全类名
@Configuration(proxyBeanMethods = true)  //告诉SpringBoot 这是一个配置类 == bean.xml 配置文件
public class Myconfig {
    /**
     * 外部无论对配置类中的这个组件注册方法调用多少次获取的都是之前注册容器的单实例对象
     * @return
     */
    @Bean //给容器中添加组件。以方法名作为组件的id。返回类型就是组件类型。返回的值,就似乎组件在容器中的实例
    public User user01(){
        return new User("zhangsan", 18);
    }

    @Bean("tom")  //加了参数的话,组件的id 就是 tom ,而不是tomcatPet
    public Pet tomcatPet(){
        return new Pet("tomcat");
    }
}

开发小技巧

Lombok

简化JavaBean开发

<groupId>org.projectlombok</groupId>
	<artifactId>lombok</artifactId>
</dependency>


idea中搜索安装lombok插件(新版本idea都自带了)
===============================简化JavaBean开发===================================
@NoArgsConstructor   //无参构造器
//@AllArgsConstructor   //全参构造器
@Data       //一个Data
@ToString
@EqualsAndHashCode
public class User {

    private String name;
    private Integer age;

    private Pet pet;

    public User(String name,Integer age){
        this.name = name;
        this.age = age;
    }


}



================================简化日志开发===================================
@Slf4j
@RestController
public class HelloController {
    @RequestMapping("/hello")
    public String handle01(@RequestParam("name") String name){
        
        log.info("请求进来了....");
        
        return "Hello, Spring Boot 2!"+"你好:"+name;
    }
}

dev-tools

热更新,项目启动后又改了东西,快速重加载

dev tools是快速重启,底层有两个类加载器,一个负责加载第三方库,一个负责加载本地java类。第三方库源码是不会变的,也就是重新启动时,只需要加载本地java类就可以。节省了重新启动的时间

<dependency>
	<groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
    <optional>true</optional>
</dependency>

项目或者页面修改以后:Ctrl+F9;

Spring Initializer

项目类型选择 Spring Initializr,修改完(公司名)、工件(项目名)和Java版本后,选择我们需要的模块(如 Spring Web),向导会联网创建 SpringBoot 项目并下载到本地。

  • 主程序已经生成好了,只需编写自己的业务代码。
  • resources文件夹中目录结构
    • static:静态资源, js、css、images。
    • templates:模板页面(SpringBoot 默认默认不支持 jsp 页面),可以使用模板引擎(FreeMarkerThymeleaf)。
    • application.properties:配置文件,可以修改一些默认设置。

配置文件

SpringBoot 使用一个全局的配置文件,配置文件名是固定的。

  • application.properties
  • application.ymlapplication.yaml(建议使用)

配置文件的作用:修改 SpringBoot 自动配置的默认值。

YAML(YAML Ain't Markup Language)

YAML  A Markup Language:是一种**标记语言**

YAML  isn't Markup Language:不是一种标记语言

Yet another  Markup Language:仍是一种标记语言

优点:没有额外的定界符,更轻量,更易读

YAML语法

2.1 基本语法

  • key: value -> value 前面一定要有空格
  • 大小写敏感
  • 使用缩进表示层级关系
  • 缩进不允许使用tab,只允许空格
  • 缩进的空格数不重要,只要相同层级的元素左对齐即可
  • # 表示注释
  • 字符串无需加引号,如果要加,''与""表示字符串内容 会被 转义/不转义
server:
    port: 8081
    path: /hello

2.2 数据类型

字面量:单个的、不可再分的值。date、boolean、string、number、null

k: v

字符串默认不用加上单引号或者双引号。

"":双引号。**不会转义**字符串里面的特殊字符,特殊字符会作为本身想表示的意思。
`name:  "zhangsan \n lisi"`	输出:**zhangsan** **换行**  **lisi**

'':单引号。**会转义**特殊字符,特殊字符最终只是一个普通的字符串数据。
`name:  'zhangsan \n lisi'`	输出:**zhangsan** **\n**  **lisi**

对象:键值对的集合。map、hash、set、object

行内写法:  k: {k1:v1,k2:v2,k3:v3}
#或
k: 
	k1: v1
  k2: v2
  k3: v3

数组:一组按次序排列的值。array、list、queue

行内写法:  k: [v1,v2,v3]
#或者
k:
 - v1
 - v2
 - v3

示例

JavaBean:

@Data
public class Person {
	
	private String userName;
	private Boolean boss;
	private Date birth;
	private Integer age;
	private Pet pet;
	private String[] interests;
	private List<String> animal;
	private Map<String, Object> score;
	private Set<Double> salarys;
	private Map<String, List<Pet>> allPets;
}

@Data
public class Pet {
	private String name;
	private Double weight;
}

配置文件 application.yml

# yaml表示以上对象
person:
  userName: zhangsan
  boss: false
  birth: 2019/12/12 20:12:33
  age: 18
  pet: 
    name: tomcat
    weight: 23.4
  interests: [篮球,游泳]
  animal: 
    - jerry
    - mario
  score:
    english: 
      first: 30
      second: 40
      third: 50
    math: [131,140,148]
    chinese: {first: 128,second: 136}
  salarys: [3999,4999.98,5999.99]
  allPets:
    sick:
      - {name: tom}
      - {name: jerry,weight: 47}
    health: [{name: mario,weight: 47}]

配置提示

自定义的类和配置文件绑定一般没有提示,我们可以导入配置文件处理器

<!-- 导入配置文件处理器,配置文件进行绑定就会有提示 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>


 <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.springframework.boot</groupId>
                            <artifactId>spring-boot-configuration-processor</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

配置文件的坑

中文乱码

application.properties 配置中文值的时候,读取出来的属性值会出现乱码问题。但是 application.yml 不会出现乱码问题。原因是,Spring Boot 是以 ISO-8859-1 的编码方式读取 application.properties 配置文件。

加载顺序

Java 的 Properties 加载属性文件后是无法保证输出的顺序与文件中一致的,因为 Properties 是继承自 Hashtable 的, key/value 都是直接存在 Hashtable 中的,而 Hashtable 是不保证进出顺序的。

所以如果需要属性保持有序,请使用 application.yml

用户名

如果定义一个键值对 user.name=xxx , 这里会读取不到对应写的属性值。为什么呢?SpringBoot 的默认 StandardEnvironment 首先将会加载 systemEnvironment 作为首个 PropertySource。而 source 即为 System.getProperties(),按照读取顺序,返回 systemEnvironment 的值即 System.getProperty("user.name"),Mac 电脑会读取自己的登录账号。

@Value@ConfigurationProperties比较

项目 @ConfigurationProperties @Value
功能 批量注入配置文件中的属性 分别指定
松散绑定(松散语法) 支持 不支持
SpEL 不支持 支持
JSR-303数据校验 支持 不支持
复杂类型封装 支持 不支持

无论配置文件是 yml 还是 properties 他们都能获取到值。

如果说,我们只是在某个业务逻辑中需要获取一下配置文件中的某项值,使用 @Value

如果说,我们专门编写了一个 JavaBean 来和配置文件进行映射,我们就直接使用@ConfigurationProperties

web开发

请求响应参数处理

请求映射

REST 使用

  • @xxxMapping@GetMapping@PostMapping@PutMapping@DeleteMapping@PatchMapping是对 Put 的补充,区别是 Patch 是部分更新,Put 是全部更新,这些注解都是 Spring4.3 引入的);

  • REST 风格支持(使用 HTTP 请求方式动词来表示对资源的操作)

操作 以前 现在(REST 风格)
获取用户 /getUser 的 GET 请求 /user/id 的 GET 请求
保存用户 /saveUser 的 POST 请求 /user/id 的 POST 请求
修改用户 /editUser 的 POST 请求 /user/id 的 PUT 请求
删除用户 /deleteUser 的 POST 请求 /user/id 的 DELETE 请求
  • 核心过滤器:浏览器 form 表单只支持 GET 与 POST 请求,而 DELETE、PUT 等 method 并不支持,Spring 3.0 添加了一个过滤器 HiddenHttpMethodFilter,可以将这些请求转换为标准的 HTTP 方法,使得支持 GET、POST、PUT 与 DELETE 请求。

用法: SpringBoot 中手动开启 REST 支持,表单 method=post,隐藏域 _method=put

REST 原理

原理(表单提交要使用 REST 的时候)

  • 表单提交会带上 _method=使用的方法

  • 请求过来被 HiddenHttpMethodFilter 拦截。

    • 请求是否正常,并且是 POST

      • 获取到 _method 的值,统一转换成大写。

      • 兼容以下请求:PUTDELETEPATCH

      • 原生 request(post),包装模式 requesWrapper 重写了 getMethod 方法,返回的是传入的值。

      • 过滤器链放行的时候用 wrapper,以后的方法调用 getMethod 是调用 requesWrapper 的。

使用客户端工具发送 REST 请求

  • 如 PostMan 直接发送 Put、Delete 等方式请求,无需 Filter。
spring:
  mvc:
    hiddenmethod:
      filter:
        enabled: true   #开启页面表单的 REST 功能

扩展:如何把 _method 这个名字换成我们自己喜欢的。

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

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

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

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

@Configuration(proxyBeanMethods = false)
public class WebConfig {
// 自定义 HiddenHttpMethodFilter,将隐藏域方法名换成自己喜欢的 _m
	@Bean
	public HiddenHttpMethodFilter hiddenHttpMethodFilter(){
    	HiddenHttpMethodFilter methodFilter = new HiddenHttpMethodFilter();
    	methodFilter.setMethodParam("_m");
    	return methodFilter;
	}
}

请求映射原理

SpringMVC 功能分析都从 org.springframework.web.servlet.DispatcherServlet.doDispatch() 方法开始。

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
    HttpServletRequest processedRequest = request;
    HandlerExecutionChain mappedHandler = null;
    boolean multipartRequestParsed = false;
    WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

    try {
        try {
            ModelAndView mv = null;
            Object dispatchException = null;

            try {
                processedRequest = this.checkMultipart(request);
                multipartRequestParsed = processedRequest != request;
                // 找到当前请求使用哪个 Handler(Controller 的方法)处理
                mappedHandler = this.getHandler(processedRequest);
            
@Nullable
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
    if (this.handlerMappings != null) {
        Iterator var2 = this.handlerMappings.iterator();
        while(var2.hasNext()) {
            HandlerMapping mapping = (HandlerMapping)var2.next();
            HandlerExecutionChain handler = mapping.getHandler(request);
            if (handler != null) {
                return handler;
            }
        }
    }
    return null;
}

RequestMappingHandlerMapping:保存了所有 @RequestMapping(请求路径) 和 HandlergController 的方法) 的映射规则。

所有的请求映射都在 HandlerMapping 中。

  • SpringBoot 自动配置欢迎页的 WelcomePageHandlerMapping,访问 / 能访问到index.html

  • SpringBoot 自动配置了默认的 RequestMappingHandlerMapping

  • 请求进来,挨个尝试所有的 HandlerMapping 看是否有请求信息;

  • 如果有,就找到这个请求对应的 Handler

  • 如果没有,就遍历下一个 HandlerMapping

如果我们需要一些自定义的映射处理,可以通过给容器中放 HandlerMapping 来自定义 HandlerMapping

有的时候,比如说同一组 API 有不同的版本如 v1,v2,我们可以在 Controller 中写两组mapping(比如 v1/userv2/user)。但同时我们也可以放在两个包下,都是 /user,这个时候我们就可以自定义 HandlerMapping,把 v1/user 映射到一个包下的 /user,把 v2/user映射到另外一个包下的 /user

普通参数与基本注解

3.2.1 注解

  • @PathVariable:请求路径中的值
  • @RequestParam:请求参数中的值
  • @RequestHeader:请求 Header 中的值
  • @RequestBody:请求 Body 中的值
  • @ModelAttribute:绑定请求参数到实体对象
  • @CookieValue:获取浏览器中的 Cookie 值
  • @MatrixVariable:以分号分隔的矩阵变量键值对,形如 ;name1=value1;name2=value2,value3
@RestController
public class ParameterTestController {


    //  car/2/owner/zhangsan
    @GetMapping("/car/{id}/owner/{username}")
    public Map<String,Object> getCar(@PathVariable("id") Integer id,
                                     @PathVariable("username") String name,
                                     @PathVariable Map<String,String> pv,
                                     @RequestHeader("User-Agent") String userAgent,
                                     @RequestHeader Map<String,String> header,
                                     @RequestParam("age") Integer age,
                                     @RequestParam("inters") List<String> inters,
                                     @RequestParam Map<String,String> params,
                                     @CookieValue("_ga") String _ga,
                                     @CookieValue("_ga") Cookie cookie){


        Map<String,Object> map = new HashMap<>();

//        map.put("id",id);
//        map.put("name",name);
//        map.put("pv",pv);
//        map.put("userAgent",userAgent);
//        map.put("headers",header);
        map.put("age",age);
        map.put("inters",inters);
        map.put("params",params);
        map.put("_ga",_ga);
        System.out.println(cookie.getName()+"===>"+cookie.getValue());
        return map;
    }


    @PostMapping("/save")
    public Map postMethod(@RequestBody String content){
        Map<String,Object> map = new HashMap<>();
        map.put("content",content);
        return map;
    }

请求处理-@RequestAttribute

@Controller
public class RequestController {

    @GetMapping("/goto")
    public String goToPage(HttpServletRequest request){

        request.setAttribute("msg","成功了...");
        request.setAttribute("code",200);
        return "forward:/success";  //转发到  /success请求
    }

    @GetMapping("/params")
    public String testParam(Map<String,Object> map,
                            Model model,
                            HttpServletRequest request,
                            HttpServletResponse response){
        map.put("hello","world666");
        model.addAttribute("world","hello666");
        request.setAttribute("message","HelloWorld");

        Cookie cookie = new Cookie("c1","v1");
        response.addCookie(cookie);
        return "forward:/success";
    }

    ///<-----------------主角@RequestAttribute在这个方法
    @ResponseBody
    @GetMapping("/success")
    public Map success(@RequestAttribute(value = "msg",required = false) String msg,
                       @RequestAttribute(value = "code",required = false)Integer code,
                       HttpServletRequest request){
        
        //也可以直接从request里面 通过 getAttribute 获取
        Object msg1 = request.getAttribute("msg");

        Map<String,Object> map = new HashMap<>();
        Object hello = request.getAttribute("hello");
        Object world = request.getAttribute("world");
        Object message = request.getAttribute("message");

        map.put("reqMethod_msg",msg1);
        map.put("annotation_msg",msg);
        map.put("hello",hello);
        map.put("world",world);
        map.put("message",message);

        return map;
    }
}

拦截器

HandlerInterceptor 接口

/**
 * 登录检查
 * 1、配置好拦截器要拦截哪些请求
 * 2、把这些配置放在容器中
 */
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {

    /**
     * 目标方法执行之前
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        String requestURI = request.getRequestURI();
        log.info("preHandle拦截的请求路径是{}",requestURI);

        //登录检查逻辑
        HttpSession session = request.getSession();

        Object loginUser = session.getAttribute("loginUser");

        if(loginUser != null){
            //放行
            return true;
        }

        //拦截住。未登录。跳转到登录页
        request.setAttribute("msg","请先登录");
//        re.sendRedirect("/");
        request.getRequestDispatcher("/").forward(request,response);
        return false;
    }

    /**
     * 目标方法执行完成以后
     * @param request
     * @param response
     * @param handler
     * @param modelAndView
     * @throws Exception
     */
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        log.info("postHandle执行{}",modelAndView);
    }

    /**
     * 页面渲染以后
     * @param request
     * @param response
     * @param handler
     * @param ex
     * @throws Exception
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        log.info("afterCompletion执行异常{}",ex);
    }
}

配置拦截器

/**
 * 1、编写一个拦截器实现HandlerInterceptor接口
 * 2、拦截器注册到容器中(实现WebMvcConfigurer的addInterceptors)
 * 3、指定拦截规则【如果是拦截所有,静态资源也会被拦截】
 */
@Configuration
public class AdminWebConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor())
                .addPathPatterns("/**")  //所有请求都被拦截包括静态资源
                .excludePathPatterns("/","/login","/css/**","/fonts/**","/images/**","/js/**"); //放行的请求
    }
}

拦截器原理

1、根据当前请求,找到HandlerExecutionChain【可以处理请求的handler以及handler的所有 拦截器】

2、先来顺序执行 所有拦截器的 preHandle方法

  • 1、如果当前拦截器prehandler返回为true。则执行下一个拦截器的preHandle
  • 2、如果当前拦截器返回为false。直接 倒序执行所有已经执行了的拦截器的 afterCompletion;

3、如果任何一个拦截器返回false。直接跳出不执行目标方法

4、所有拦截器都返回True。执行目标方法

5、倒序执行所有拦截器的postHandle方法。

6、前面的步骤有任何异常都会直接倒序触发 afterCompletion

7、页面成功渲染完成以后,也会倒序触发 afterCompletion

数据访问

整合MyBatis操作

https://github.com/mybatis

引入 MyBatis 的官方 starter

<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.4</version>
</dependency>

配置模式

  • 配置 MyBatis 的配置文件,SqlMapConfig.xml(名称不固定);

  • 通过配置文件,加载 MyBatis 运行环境,创建 SqlSessionFactory 会话工厂,SqlSessionFactory 使用单例方式;

  • 通过 SqlSessionFactory 创建 SqlSessionSqlSession 是一个面向用户的接口(提供操作数据库方法),实现对象是线程不安全的,建议 sqlSession 应用在方法体内;

  • 调用 sqlSession 的方法去操作数据,如果需要提交事务,需要执行 sqlSessioncommit() 方法;

  • 释放资源,关闭 sqlSession

// 表示这是一个 Spring 配置类
@Configuration 
// SqlSessionFactory 和 SqlSessionFactoryBean 存在时才生效
@ConditionalOnClass({SqlSessionFactory.class, SqlSessionFactoryBean.class})
// DataSource 的 Canidate 注册到 Spring 容器中时才生效
@ConditionalOnSingleCandidate(DataSource.class)
// 使 MybatisProperties 注解类生效
@EnableConfigurationProperties({MybatisProperties.class})
// 在 DataSourceAutoConfiguration 和 MybatisLanguageDriverAutoConfiguration 自动配置之后执行
@AutoConfigureAfter({DataSourceAutoConfiguration.class, MybatisLanguageDriverAutoConfiguration.class})
public class MybatisAutoConfiguration implements InitializingBean {

@ConfigurationProperties(prefix = "mybatis")
public class MybatisProperties
  • @Import(AutoConfiguredMapperScannerRegistrar.class)

  • Mapper: 我们只要写操作 MyBatis 的接口,@Mapper 就会被自动扫描进来。

可以修改配置文件中 mybatis 开始的所有;

mybatis:
  config-location: classpath:mybatis/mybatis-config.xml  #全局配置文件位置
  mapper-locations: classpath:mybatis/mapper/*.xml  #sql映射文件位置

Mapper 接口绑定 xml

Mapper接口--->绑定Xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.atguigu.admin.mapper.AccountMapper">
<!--    public Account getAcct(Long id); -->
    <select id="getAcct" resultType="com.atguigu.admin.bean.Account">
        select * from  account_tbl where  id=#{id}
    </select>
</mapper>

也可以直接在 application.properties里面改,此时要把config-location注释掉

# 配置mybatis规则
mybatis:
#  config-location: classpath:mybatis/mybatis-config.xml
  mapper-locations: classpath:mybatis/mapper/*.xml
  configuration:
    map-underscore-to-camel-case: true
    
 可以不写全局;配置文件,所有全局配置文件的配置都放在configuration配置项中即可

步骤:

  1. 导入 MyBatis 官方 starter
  2. 编写 Mapper 接口,标准 @Mapper 注解;
  3. 编写 sql 映射文件并绑定 Mapper 接口;
  4. 在 **application.yaml **中指定 Mapper 配置文件的位置,以及指定全局配置文件的信息 (建议配置在 mybatis.configuration)。

注解模式

这时候可以不写 mapper.xml 文件

@Mapper
public interface CityMapper {
    @Select("select * from city where id=#{id}")
    public City getById(Long id);

    public void insert(City city);
}

混合模式

@Mapper
public interface CityMapper {
    @Select("select * from city where id=#{id}")
    public City getById(Long id);

    public void insert(City city);
}

最佳实践

  1. 引入 mybatis-starter
  2. 配置 application.yaml 中,指定 mapper-location 位置即可;
  3. 编写 Mapper 接口并标注 @Mapper 注解;
  4. 简单方法直接注解方式;
  5. 复杂方法编写 mapper.xml 进行绑定映射;
  6. @MapperScan("com.atguigu.admin.mapper") 标注在主应用类上,其他的接口就可以不用标注 @Mapper 注解。
posted @ 2022-02-26 15:30  dongye95  阅读(121)  评论(0编辑  收藏  举报