SpringBoot2 核心知识点

1. SpringBoot 简介

1.1 Spring能做什么

SpringBoot2 核心知识点_第1张图片

1.2 Spring 的生态

官网:https://spring.io/projects/spring-boot

覆盖了:web 开发、数据访问、安全控制、分布式、消息服务、移动开发、批处理…

1.3 Spring 5 重大升级

1.3.1 响应式编程

SpringBoot2 核心知识点_第2张图片

1.3.2 内部源码设计

基于 java 的版本最低为 jdk1.8

基于 Java8 的一些新特性,如:接口默认实现,重新设计源码架构

1.4 为什么用 SpringBoot

SpringBoot makes it easy to create stand-alone,production-grade

Spring based Applications that you can “just run”

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

1.4.1 SpringBoot 有点
  • Create stand-alone Spring applications
    • 创建独立 Spring 应用
  • Embed Tomcat,Jetty or Undertow directly (no need to deploy WAR files)
    • 内嵌web 服务器
  • Provide opinionated ‘starter’ dependencies to simplify your build configuration
    • 自动 starer 依赖,简化构建配置
  • Automatically configure Spring and 3rd part 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 技术栈的快速开发脚手架

1.4.2 SpringBoot 缺点

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

1.5 时代背景

1.5.1 微服务

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

https://martinfowler.com/microservices/

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

SpringBoot2 核心知识点_第3张图片

分布式的困难:

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

分布式的解决

  • SpringBoot + SpringCloud

SpringBoot2 核心知识点_第4张图片

1.6 云原生

原生应用如何上云,Cloud Native

上云的困难

  • 服务自愈

    加入 a服务在 5 台服务器上都有,3 台b 服务器,3 台 c 服务器,然后全部部署上去,突然后一天 c的一台服务器宕机了,然后c 服务能不能自愈(在别的服务器又拉起一个 c 服务)?

  • 弹性伸缩

    突然流量高峰期,a 要调用 b,b 要调用 c,c 部署的少了不够用,我们希望在 c 不够用的时候在自动的扩充 3 服务器,流量高峰期过去后将他们再下架

  • 服务隔离

    假设 c 再 1 号服务器部署,然后再 1 号服务器同时部署的可能有 d,e,f,应该希望当同一台服务器上的服务某一个出现故障后不会影响别的服务的正常运行

  • 自动化部署

    整个微服务全部部署不可能手工去部署

  • 灰度发布

    某一个服务版本有更新,如果直接将之前的版本替换成新的版本,有可能会出现故障,如果新版本不稳定,那么整个系统就会坏掉,可以先将 多个服务器中的旧版本替换为新的,验证是否能正常运行,经过长时间的验证没有出现问题则全部替换为新版本

  • 流量治理

    b 服务器性能不高,所以可以通过流量治理手段服务器只能接受少量的流量

1.7 SpringBoot 官网

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

SpringBoot2 核心知识点_第5张图片

  • 查看版本新特性

SpringBoot2 核心知识点_第6张图片

2. 第一个 SpringBoot应用

开发环境环境版本要求:

  1. jdk 1.8
  2. maven 3.3+

搭建步骤

  1. 使用ides 创建一个空的 maven 项目
  2. 导入依赖

<parent>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-parentartifactId>
    <version>2.5.2version>
parent>


<dependencies>
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-webartifactId>
    dependency>
dependencies>


<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-maven-pluginartifactId>
        plugin>
    plugins>
build>

  1. 创建主程序(启动类)
/**
 * 主程序类
 * @SpringBootApplication 表示这是一个 SpringBoot 应用
 */
@SpringBootApplication
public class MainApplication {
    public static void main(String[] args) {
        SpringApplication.run(MainApplication.class);
    }
}
  1. controller 业务代码
@RestController
public class HelloController {

    @RequestMapping("/hello")
    public String hello() {
        return "Hello SpringBoot2";
    }

}
  1. 运行测试

    可以看到端口号默认 8080,可以创建配置文件,再配置文件中进行修改:server.port=8088

SpringBoot2 核心知识点_第7张图片

SpringBoot2 核心知识点_第8张图片

3. 自动装配原理

3.1 SpringBoot 特点

  • 父项目做依赖
<parent>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-parentartifactId>
    <version>2.5.2version>
parent>
几乎声明了所有开发中常用依赖的版本号,自动版本仲裁机制

SpringBoot2 核心知识点_第9张图片

SpringBoot2 核心知识点_第10张图片

  • 开发导入 starter 场景启动器

在 pom.xml 中见到很多的 spring-boot-starter-*,只要引入 starter,这个场景的所有常规需要的依赖都会自动引入

类似于 :*-spring-boot-starter,这种是第三方为我们提供的简化的依赖

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

<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starterartifactId>
    <version>2.5.2version>
    <scope>compilescope>
dependency>
  • 无需关注需要引入依赖的版本号,springboot 会自动进行版本的仲裁
  1. 引入依赖默认都可以不写版本号

  2. 引入非版本仲裁的jar 要加入版本号

  3. 可以修改默认的版本号

  4. 可以修改默认的版本号

    首先查看默认配置的版本号使用的方式,然后使用key 进行修改

<properties>
	<mysql.version>5.1.4mysql.version>
properties>

3.2 自动配置

  • 自动配置好了 Tomcat

    我们只需要在配置文件中设置tomcat 的属性就可以了

SpringBoot2 核心知识点_第11张图片

  • 自动配置了 SpringMVC 的各个组件

SpringBoot2 核心知识点_第12张图片

  • 自动配置好 Web 常见功能,如:字符编码问题

    在之前 SpringMVC 开发中,需要在 web.xml 中配置 characterEncodingFilter,在 SpirngBoot 中只需要引入 web 的启动器就可以了。

ConfigurableApplicationContext run = SpringApplication.run(MainApplication.class);
String[] names = run.getBeanDefinitionNames();
for (String name : names) {
    System.out.println(name);
}

SpringBoot2 核心知识点_第13张图片SpringBoot2 核心知识点_第14张图片

  • 自动配置好包结构

    在 SpringMvc中需要在 xml 文件中设置 componentScan,SpringBoot中无需配置

    • 主程序所在包及其下面的所有子包里面的组件都会被默认扫描(约定的规则)

    • 无需以前的包扫描配置

    • 要想改变扫描的路径

      @SpringBootApplication(scanBasePackages = "xx.xx.xx")

      或者使用注解 @ComponentScan 指定扫描路径

SpringBoot2 核心知识点_第15张图片

  • 各种配置拥有默认值

    • 例如:tomcat 有默认端口

    SpringBoot2 核心知识点_第16张图片

    • 例如文件上传:MultipartProperties

    SpringBoot2 核心知识点_第17张图片

    • 默认配置最终都是映射到某个类上,如:MultipartProperties 映射到 multipartProperties 类上

    SpringBoot2 核心知识点_第18张图片

    • 配置文件最终会绑定每个类上,这个类会在容器中创建对象
  • 按需加载所有自动配置项 Conditional

    • 非常多的 starter,pom.xml 中引入那个启动那个
    • 引入了那些场景这个场景的自动配置才会开启
    • SpringBoot 所有的自动配置功能都在 spring-boot-autoconfigure包里面。
<dependency>
  <groupId>org.springframework.bootgroupId>
  <artifactId>spring-boot-autoconfigureartifactId>
  <version>2.5.2version>
  <scope>compilescope>
dependency>

3.3 容器功能

3.3.1 组件添加
  1. @Configuration

    Full 模式与 Lite 模式

    配置类组件之间无依赖关系用 Lite 模式加速容器启动过程,配置类组件之间有依赖关系,方法会调用得到之前单实例组件,用 Full 模式

假设有一个 User 类,想要注册组件

  • 在 SpringMVC 情况下需要先创建一个 beans.xml 文件,然后使用 bean标签进行配置
<bean id="user1" class="com.example.pojo.User">
    <property name="name" value="zhangsan">property>
bean>
  • 在 SpringBoot 情况下只需要创建一个配置类加上 @Configuration
/**
 * 1. 配置类里面使用 @Bean 标注在方法上给容器注册组件,默认也是单实例的
 * 2. 配置类加上 @Configuration 也是一个组件
 * 3. proxyBeanMethods:代理 bean 的方法,如果为 true 外部无论对配置类中的这个组件注册方法调用多少次
 *      获取的都是之前注册容器中的单实例对象。如果为 true 都会去容器中找组件
 *      Full(proxyBeanMethods = true)
 *      Lite(proxyBeanMethods = false) 为 false 组件在容器中不会保存代理对象,每一次调用都会产生一个新的对象
 *    解决组件依赖的场景
 *    组件依赖必须使用 Full 模式(默认),其他默认是否 Lite 模式
 * 4. 如果是 false ,SpringBoot 不会检查容器中方法返回的东西是否存在,提高运行的效率
 *    如果是 true,则每次执行都会检查
 * 5. 如果只是向容器中配置组件,别人也不依赖这个组件则设置成 false
 *    如果组件在下面别人还要用就设置为true,保证容器中的组件就是要依赖的组件
 */
@Configuration(proxyBeanMethods = true)  // 告诉 SpringBoot 这是一个配置类 == SpringMvc 中的 beans.xml
public class MyConfig {
    @Bean   // 给容器中添加组件,id 为方法名,返回类型为组件类型,返回值就是组件在容器中的实例
    public User user1() {
        return new User("zhangsan");
    }
}
  1. @Bean、@Component、@Controller、@Service、@Repository

    与 SpringMvc 中使用方法一样

  2. @ComponentScan、@Import

    • @ComponentScan 就是配置包扫描的

    • @Import 给容器中导入一个组件

      在容器中组件上面使用

    在我的配置文件中导入 User 和 DispatcherServlet两个组件

@Import({User.class, DispatcherServlet.class})
@Configuration
public class MyConfig {
}

​ 然后在启动类中得到 bean 然后输出查看

@SpringBootApplication
public class MainApplication {
    public static void main(String[] args) {
        ConfigurableApplicationContext run = SpringApplication.run(MainApplication.class);
		// 因为可能有多个,所以返回值结果为一个数组类型
        String[] beanNamesForType = run.getBeanNamesForType(User.class);
        for (String s : beanNamesForType) {
            System.out.println(s);
        }
        String[] beanNamesForType1 = run.getBeanNamesForType(DispatcherServlet.class);
        for (String s : beanNamesForType1) {
            System.out.println(s);
        }
    }
}

SpringBoot2 核心知识点_第19张图片

  • 使用 ImportSelector 接口导入组件
  1. 创建一个 MyImportSelector 类实现 ImportSelector 接口,并且实现selectImports 方法
  2. 然后返回一个数组,包括要注册的类的全类名
// 自定义逻辑返回需要导入的组件
// 由于实现了 ImportSelector,所以把注册的方法的全类名返回
public class MyImportSelector implements ImportSelector {
    // 返回值,就是要导入到容器中的组件全类名
    // AnnotationMetadata:当前标注 @Import 注解的类的所有注解信息
    @Override
    public String[] selectImports(AnnotationMetadata annotationMetadata) {
        // 方法不要返回 null 值,可以返回一个 空空数组
        return new String[]{"com.example.pojo.Test1","com.example.pojo.Test2"};
    }
}
  1. 使用 Import 注解注册 MyImportSelector
//2. ImportSlector:返回需要导入的组件的全类名数组
@Import({User.class, DispatcherServlet.class,MyImportSelector.class})
  1. 在主启动类中输出查看

SpringBoot2 核心知识点_第20张图片

  • 使用 ImportBeanDefinitionRegistrar 接口注册bean
  1. 创建一个 MyImportBeanDefinitionRegistrar 类实现 ImportBeanDefinitionRegistrar 接口,并且实现registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry)方法
public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {

    /**
     * @param importingClassMetadata:当前类的注释信息
     * @param registry:BeanDefinition 的注册类
     *                 把所有需要添加到容器中的 Bean,
     *                调用BeanDefinitionRegistry.registerBeanDefinition手工注册进来
     */
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        // 指定 bean 名
        boolean test1 = registry.containsBeanDefinition("com.example.pojo.Test1");
        boolean test2 = registry.containsBeanDefinition("com.example.pojo.Test2");
        if (test1 && test2) {
            // 指定 Bean 定义信息(Bean 的类型,bean 的作用域都可以在这里指定)
            RootBeanDefinition rootBeanDefinition = new RootBeanDefinition(Test1.class);
            // 注册一个 bean,指定 bean 名为 test
            registry.registerBeanDefinition("test",rootBeanDefinition );
        }
    }
}
  1. 使用 Import 注解注册 MyImportBeanDefinitionRegistrar
  2. 在主启动类中输出查看注册成功

SpringBoot2 核心知识点_第21张图片

  1. Conditional

条件装配:满足 Conditional 指定条件,则进行组件的注入

SpringBoot2 核心知识点_第22张图片

  • ConditionalOnBean:当容器中有某些组件的时候做一些事情
  • ConditionalOnMissingBean:当容器中没有某些组件的时候做一些事情
  • ConditionalOnClass:当容器中有某些类的时候做一些事情
  • ConditionalOnMissingBean:当容器中没有某些类的时候做一些事情
  • ConditionalOnJava:当 java 版本是某一个版本的时候做一些事情
  • ConditionalOnResource:当根目录下有某些资源的时候做一些事情
  • ConditionalOnWebApplication:是web应用的时候做一些事情
  • ConditionalOnSingleCandidate:当容器组件只有一个实例或者有多个实例但是只有一个主实例的时候才生效
  • ConditionalOnProperty:当配置文件中配置了某个属性的时候生效
@Configuration
public class MyConfigConditional {
    @ConditionalOnBean(name = "pet")
    // @ConditionalOnBean(name = "pet") 如果 pet 组件注册到容器中,则 user 组件也会被注册到容器中
    // 如果 pet 没有 @Bean 注册,则 user 组件也不会注册到容器中
    // @ConditionalOnBean 是容器如果有某一个组件,就会将加本注解的组件注册到容器,条件不成立不会将组件注册到容器
    @Bean
    public User user() {
        return new User("张三");
    }
    //@Bean     
    public Pet pet() {
        return new Pet();
    }
}
3.3.2 原生配置文件引入
  • @ImportResource 导入 Spring 的配置文件,让配置文件进行生效

在 SpringMvc 模式下有一个配置文件,然后再配置文件中有很多的 bean 标签注册了很多的组件

然后再 SpringBoot 中想使用这些组件不用一个一个进行修改,只需要再要使用的类上使用注解

@ImportResource("classpath:beans.xml")

SpringBoot2 核心知识点_第23张图片

3.3.3 配置绑定

我们习惯于把经常爱变化的东西配置到配置文件中,比如数据库的已连接地址账号密码等,之前的操作中我们需要加载配置文件,然后得到每一个 key value 的值,然后把这些 k v 值一一对应封装到 JavaBean 中。在 SpringBoot 中这个过程会变得非常简单,这个过程就叫做配置绑定。

方式一:@ConfigurationProperties

  1. 创建一个 Car 类,包括 brand 和 price 属性
  2. 在配置文件中设置属性
car.brand=BMcar.price=1999
  1. 在 Car 类上面进行绑定、

    注意:只有在容器中的组件,才能使用这个配置绑定的功能
    @Component

/**
 * 只有在容器中的组件,才能使用这个配置绑定的功能
 */
@Component
@ConfigurationProperties(prefix = "car")
public class Car {

    private String brand;
    private Integer price;
	// 省略 getter / setter 方法
}
  1. 在 Controller 中使用 @Autowired 注入,然后返回
@Autowired
private Car car;
@RequestMapping("/car")
public Car car() {
    return car;
}
  1. 通过浏览器访问端口测试,输出结果

SpringBoot2 核心知识点_第24张图片

方式二:@EnabledConfigurationProperties + @ConfigurationProperties

因为我们有时候可能需要使用第三方的组件,而这些组件我们是不能使用 @Component 注册到容器中的,所以可以使用这种方式

  1. 在我们自动的配置文件上使用 @EnabledConfigurationProperties 注解
@Configuration
@EnableConfigurationProperties(Car.class)
// 1. 开启 Car 配置绑定功能
// 2. 把这个 Car 这个组件自动注册到容器中
public class MyConfig {
}
  1. 在 Car 类上使用 @ConfigurationProperties(prefix = “car”)
  2. 测试运行

3.4 自动配置原理

3.4.1 启动类引导加载自动配置类

@SpringBootApplication 点进去是一个合成注解的类

@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
		@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
  1. @SpringBootConfiguration

    点进去看到一个有 @Configuration 注解的类,代表当前就是一个配置类,也就是 main 程序也就是 Spring 中的一个核心配置类

@Configuration
@Indexed
public @interface SpringBootConfiguration {
    @AliasFor(
        annotation = Configuration.class
    )
    boolean proxyBeanMethods() default true;
}
  1. @ComponentScan

    表示指定要扫描那些包

  2. @EnableAutoConfiguration

    点击后可以看到也是一个合成注解

@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {}
  • @AutoConfigurationPackage

    自动配置包,指定了默认的包结构的规则

    这就解释了为啥 MainApplication 所在的包的注解才能生效

// 给容器中导入一个组件,这里的 Register 是给容器中批量注册组件
// 将指定的一个包下的所有组件导入进来
@Import(AutoConfigurationPackages.Registrar.class)
public @interface AutoConfigurationPackage {

SpringBoot2 核心知识点_第25张图片

  • @Import(AutoConfigurationImportSelector.class)
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
	if (!isEnabled(annotationMetadata)) {
		return NO_IMPORTS;
	}
	AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata);
	return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}

利用 getAutoConfigurationEntry(annotationMetadata); 方法给容器中批量导入一些组件,获取所有配置的集合

这个方法会先将得到的所有组件去掉重复的,移除一些没有用到的等等操作,然后返回。

SpringBoot2 核心知识点_第26张图片

getCandidateConfigurations(),利用 Spring 的工厂加载一些东西

protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
	List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
			getBeanClassLoader());
	Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you "
			+ "are using a custom packaging, make sure that file is correct.");
	return configurations;
}
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
	ClassLoader classLoaderToUse = classLoader;
	if (classLoaderToUse == null) {
		classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
	}
	String factoryTypeName = factoryType.getName();
	return loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList());
}
private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
    		Map<String, List<String>> result = cache.get(classLoader);
		if (result != null) {
			return result;
		}

		result = new HashMap<>();
		try {
			Enumeration<URL> urls = classLoader.getResources(FACTORIES_RESOURCE_LOCATION);
			while (urls.hasMoreElements()) {
				URL url = urls.nextElement();
   
classLoader.getResources(FACTORIES_RESOURCE_LOCATION); 这个会加载资源文件,文件位置点进去就可以看到
    	public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
// 这个方法最终会加载得到所有的组件

META-INF/spring.factories 位置来加载一个文件,默认扫描我们当前系统里面所有 META-INF/spring.factories 位置的文件

SpringBoot2 核心知识点_第27张图片

在这个文件里面就是所有自动配置的东西,通过这个机制进行自动配置,其实就是在配置文件中写死的

意思就是这个文件中写死了 spring-boot 一启动就要给容器中加载的所有配置类

3.4.2 按需开启自动配置项

虽然自动配置项在启动的时候会默认全部加载,但是最终会按照条件装配规则按需装配的。

SpringBoot2 核心知识点_第28张图片

3.4.3 修改默认配置

以 DispatcherServletAutoConfiguration 为例

首先查看类上面的 Conditioinal 注解生效后,然后查看方法上面的 Conditional 注解生效,然后就会注册这个 bean,为什么使用 dispatcherServlet 不会进行一些别的操作,因为在这里Spring 已经为我们创建好了对象,并且做了一系列的配置然后返回 dispatcherServlet

SpringBoot2 核心知识点_第29张图片

SpringBoot2 核心知识点_第30张图片

@EnableConfigurationProperties(WebMvcProperties.class)

​ 这里就是对 application.properties 文件进行一个绑定

这个就是我们要导入的配置文件,他会从我们的 application.properties 文件中找到相应的自己的配置,然后进行配置的设置

SpringBoot2 核心知识点_第31张图片

@ConfigurationProperties(prefix = “spring.mvc”)

通过 spirng.mvc 这个前缀就可以修改默认的配置信息

SpringBoot2 核心知识点_第32张图片

  • multipartResolver
@Bean
@ConditionalOnBean(MultipartResolver.class)	// 判断容器中有这个类的组件,条件判断是否生效
@ConditionalOnMissingBean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME)// 如果容器中没有 multipartResolver 这个组件生效,如果有的话就是用户自己定义了自己的组件,然后不生效
public MultipartResolver multipartResolver(MultipartResolver resolver) {
    // @Bean 标注的方法传入到了对象参数,这个参数的值就会从容器中找
    // multipartResolver ,防止有一些用户配置的文件上传解析器不符合规范
	// Detect if the user has created a MultipartResolver but named it incorrectly
	return resolver;
}

大概就是这个意思,可能不太准确:这个方法并没有进行什么设置,只是怕有的人使用的时候把文件上传的名字写错了,然后这里 MultipartResolver resolver 接受到传入的值,然后进行一个相当于重命名的操作进行 return,multipartResolver 这个名字就是注册好的 bean

public static final String MULTIPART_RESOLVER_BEAN_NAME = “multipartResolver”;

SpringBoot 默认会在底层配置好所有的组件,但是说如果用户使用 @Bean 等一些注解配置了自己的组件,则以用户配置的组件优先 @ConditionalOnMissingBean

  • SpringBoot 先加载所有的自动配置类 xxxxAutoConfiguration

  • 每个自动配置类按照条件进行生效,默认都会绑定配置文件指定的值

    从 xxxxProperties 里面拿到,xxxxProperties 和配置文件进行了绑定

  • 生效的配置类就会给容器中装配很多组件

  • 只要容器中有这些组件,相当于这些功能就已经是实现了

  • 自定义配置

    1. 直接 @Bean 替换底层的组件
    2. 用户去看这个组件是获取的配置文件什么值就去修改

    xxxxAutoConfiguration —> 组件 —> xxxxProperties 里面拿值 ----> application.properties

3.4.4 使用
  • 引入场景的依赖

  • 查看自动配置了那些(选择查看)

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

    • 引入场景对应的自动配置一般都生效了
    • 配置文件中 debug = true 开启自动配置报告。
      • Negative 不生效的
      • Positive 生效的

SpringBoot2 核心知识点_第33张图片

  • 是否需要修改
    • 修改配置项目

      • https://docs.spring.io/spring-boot/docs/current/reference/html/application-properties.html#application-properties
      • 自己分析 xxxxProperties 绑定了配置文件的那些前缀
    • 自定义加入或者替换的组件

      • @Bean 等注解实现
      • 自定义器:xxxxCustomizer

3.5 开发技巧

3.5.1 Lombok

在我们床架实体类后我们需要手动生成它的有/无 参构造方法、get/set 方法,toString 方法等,使用 Lombok 后只需要使用简单的注解可以实现以上的内容

  1. 在 Idea 中 setting – plugins 中安装 Lombok 插件
  2. 导入 Lombok 的启动器

在 Spring Boot 父依赖中找到,然后在 pom.xml 文件中引入

SpringBoot2 核心知识点_第34张图片

<dependency>
  <groupId>org.projectlombok</groupId>
  <artifactId>lombok</artifactId>
</dependency>
  1. 在实体类加上注解
@Data   // 生成 get/set 方法
@ToString   // 生成 toString 方法
@AllArgsConstructor     // 生成全参构造器
@NoArgsConstructor      // 生成无参构造器
public class User {

    private String name;

}

SpringBoot2 核心知识点_第35张图片

  • 在类上面使用 @Slf4j 注解,使用日志功能
@Slf4j
@RestController
public class HelloController {

    @RequestMapping("/hello")
    public String hello() {
        log.info("请求进到这里来了。。。。。");
        return "Hello SpringBoot2";
    }

}

启动浏览器访问这个接口后,控制台会输出我们给定的日志信息

SpringBoot2 核心知识点_第36张图片

3.5.2 Spring Initailizr

快速的创建好 Spring Boot 应用

SpringBoot2 核心知识点_第37张图片

SpringBoot2 核心知识点_第38张图片

SpringBoot2 核心知识点_第39张图片

选择好场景之后,我们点击 Next ,然后 idea 就会联网把我们的项目下载好

3.5.3 dev-tools

热更新,我们在做项目的时候可能会进行修改,然后不想每次启动后去点击启动按钮,可以使用 dev-tools 热更新,修改完代码后使用 Ctrl + F9(对项目重新编译一下,然后重新加载) 就可以实时更新了

SpringBoot2 核心知识点_第40张图片

  1. 导入依赖
<dependencies>
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-devtoolsartifactId>
        <optional>trueoptional>
    dependency>
dependencies>
  1. 项目修改后使用快捷键 Ctrl + F9 就可以重新加载项目

4. 配置文件

4.1 文件类型

4.1.1 properties

在之前使用过的 properties 对属性继续配置,配置名=值

4.1.2 yaml
  1. 简介

YMAL 是 “YAML Aint’s Markup Language” (YAML 不是一种标志语言)的递归缩 写。在开发的这种语言时,YMAL 的意思其实是:“Yet Another Markup Language”(仍是一种标记语言)。

  • 非常适合用来做数据为中心的配置文件
  1. 基本语法
  • key: value — 注意:key 和 value 中间必须有一个空格,类似于 json 字符串的格式
  • 大小写敏感
  • 使用缩进表示层级关系
  • 缩进不允许使用 tab,只允许空格
  • 缩进的空格数不重要,只要相同层级的元素左对齐即可
  • ‘#’ 表示注解
  • ‘’ 与 “” 表示字符串内容,会被 转义/不转义
  1. 数据类型
  • 字面量:单个的、不可再分的值。date、boolean、string、number、null
key: value
  • 对象:键值对的集合。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
     
  1. 案例
  • Person.java
@ConfigurationProperties(prefix = "person")
@Component
@Data
@ToString
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;
}
  • Pet.java
@Component
@ToString
@Data
public class Pet {

    private String name;
    private Integer age;

}
  • properties.yml
person:
  userName: 张三
  boss: true
  birth: 2020/09/23
  age: 20
  pet:
    name: 小黄
    age: 10
  interests:
    - 篮球
    - 足球
  animal:
    - 小猫
    - 小狗
  score:
    语文:
      first: 33
      second: 44
      third: 55
    数学: [30,50,89]
  salarys:
    - 20000
    - 1000
  allPets:
    sick:
      - name: 小猫
        age: 10
    health:
      - name: 小狗
        age: 30
  • 测试运行结果

SpringBoot2 核心知识点_第41张图片

4.2 配置处理器

在编写 yaml 文件的时候,输入的时候是没有提示信息的,在项目中加入配置处理器

官方地址:https://docs.spring.io/spring-boot/docs/current/reference/html/configuration-metadata.html#configuration-metadata.format

  1. 在pom.xml 中导入依赖

<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-configuration-processorartifactId>
    <optional>trueoptional>
dependency>
  1. 重启项目

  2. 然后在 yaml 中输入就会有提示信息

SpringBoot2 核心知识点_第42张图片

  1. 在打包插件中加入一个移除打包配置处理器的插件,因为这个只是开发的时候使用,打包是用不着的
<build>
        <plugins>
            <plugin>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-maven-pluginartifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.springframework.bootgroupId>
                            <artifactId>spring-boot-configuration-processorartifactId>
                        exclude>
                    excludes>
                configuration>
            plugin>
        plugins>
build>

5. Web 开发

一个源码分析的链接:https://www.cnblogs.com/seazean/p/15109440.html

官网内容:https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.developing-web-applications

5.1 SpringMVC 自动配置概览

Spring Boot provides auto-configuration for Spring MVC that works well with most applications.(大多数场景我们都无需自定义配置)

The auto-configuration adds the following features on top of Spring’s defaults:

  • Inclusion of ContentNegotiatingViewResolver and BeanNameViewResolver beans.
    • 内容协商试图解析器和 BeanName 试图解析器
  • Support for serving static resources, including support for WebJars (covered later in this document).
    • 静态资源(包括 webjars)
  • Automatic registration of Converter, GenericConverter, and Formatter beans.
    • 自动注册:Converter, GenericConverter, and Formatter
  • Support for HttpMessageConverters (covered later in this document).
    • 支持HttpMessageConverters
  • Automatic registration of MessageCodesResolver (covered later in this document).
    • 自动注册 MessageCodesResolver,国际化用
  • Static index.html support.
    • 静态 index.html 页支持
  • Automatic use of a ConfigurableWebBindingInitializer bean (covered later in this document).
    • 自动使用ConfigurableWebBindingInitializer,DataBinder 负责将请求数据绑定到 JavaBean 上

If you want to keep those Spring Boot MVC customizations and make more MVC customizations (interceptors, formatters, view controllers, and other features), you can add your own @Configuration class of type WebMvcConfigurer but without @EnableWebMvc.

不用 @EnableWebMvc 注解,使用 @Configuration + WebMvcConfigurer 自定义规则

If you want to provide custom instances of RequestMappingHandlerMapping, RequestMappingHandlerAdapter, or ExceptionHandlerExceptionResolver, and still keep the Spring Boot MVC customizations, you can declare a bean of type WebMvcRegistrations and use it to provide custom instances of those components.

声明 WebMvcRegistrations 改变默认底层组件

If you want to take complete control of Spring MVC, you can add your own @Configuration annotated with @EnableWebMvc, or alternatively add your own @Configuration-annotated DelegatingWebMvcConfiguration as described in the Javadoc of @EnableWebMvc.

使用 @EnableWebMvc + @Configuration + DelegatingWebMvcConfiguration 全面接管 SpringMVC

5.2 简单功能分析

5.2.1 静态资源访问

官网地址:https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.developing-web-applications.spring-mvc.static-content

  • By default, Spring Boot serves static content from a directory called /static (or /public or /resources or /META-INF/resources) in the classpath or from the root of the ServletContext.

    • Spring Boot 为我们提供静态资源的目录,也就是说当我们将所有的静态资源(图片,js,css文件等)放到 /static (or /public or /resources or /META-INF/resources) 这些文件夹下,然后访问我们当前项目的根路径就可以访问到静态资源了,因为它使用了 ResourceHttpRequestHandler 处理了
  • 只要我们将静态资源放在类路径下的:/static /public /resources /META-INF/resources 文件下,我们就可以通过当前项目的根路径/ + 静态资源名来进行访问。

测试访问根目录下的资源

  1. 首先在根目录下创建相对应的 4 个文件夹,并且在每一个文件夹下放入一个图片

SpringBoot2 核心知识点_第43张图片

  1. 然后使用浏览器通过 localhost:8080/资源名字.扩展名 就可以访问到

SpringBoot2 核心知识点_第44张图片

  1. 在每一个文件下的文件修改为同一个名称,然后通过浏览器访问测试优先级
    1. /META-INF/resources
    2. /resources
    3. static
    4. public

假设我们 controller 中的动态请求路径与根目录下静态资源的名字相同

在这种情况下,当请求进来的时候,会先去 Controller 中看能不能进行处理,不能处理的请求又都交给资源静态处理器,因为静态资源映射的是 /**,所以会在根目录下找相应的静态资源,如果静态资源也找不到就会响应 404 页面。

SpringBoot2 核心知识点_第45张图片

静态资源访问前缀

  • By default, resources are mapped on /**, but you can tune that with the spring.mvc.static-path-pattern property. For instance, relocating all resources to /resources/** can be achieved as follows:

    默认情况下,映射的路径是 /**,也就是说访问我们的静态资源只需要写静态资源名就会自动的找到静态资源。如果想要改变这个静态资源访问的路径,可以通过修改 spring.mvc.static-path-pattern 实现,也就是说给请求加一个前缀,以防止项目中使用拦截器静态资源会被拦截

spring:
  mvc:
    static-path-pattern: /res/**
# 表示 /res 下面的都是静态资源请求,在访问静态资源的时候就访问这个地址加静态资源名

SpringBoot2 核心知识点_第46张图片

改变默认的静态资源路径

spring:
  web:
    resources:
      static-locations: classpath:/myresources

指定了 static-locations 后,所有请求的静态资源文件都会去指定的文件加下找,别的位置找不到的。

SpringBoot2 核心知识点_第47张图片

SpringBoot2 核心知识点_第48张图片

支持 webjars

官网:https://www.webjars.org/

webjars 就是将 jquery、js、css 等静态文件打包成 jar 包

以 jquery 为例测试

  1. 在官网找到对应 jar 包
<dependency>
    <groupId>org.webjarsgroupId>
    <artifactId>jqueryartifactId>
    <version>3.5.1version>
dependency>
  1. 找打 jquery 的 webjars 目录

SpringBoot2 核心知识点_第49张图片

  1. 浏览器访问路径:localhost:8080/webjars/jquery/3.5.1/jquery.js 就可以看到这个js 文件的内容了

SpringBoot2 核心知识点_第50张图片

5.2.2 欢迎页

Spring Boot supports both static and templated welcome pages. It first looks for an index.html file in the configured static content locations. If one is not found, it then looks for an index template. If either is found, it is automatically used as the welcome page of the application.

Spring Boot 既支持静态欢迎页面,也支持模板欢迎页面。如果我们是前者,我们将 index.html 静态资源文件放到静态资源路径下,就会被当成欢迎页面,也就是访问项目根路径默认展示的页面。或者静态资源路径下没有存在这个页面,也会给我们找 index 这个模板(有一个 Controller 处理 index 请求,最终跳回页面,这个 index 模板最终也会作为我们的欢迎页)

静态资源路径下 index.html 页面

这里我们可以在 yml 文件中配置自己的静态资源路径,然后将我们的 index.html 页面放到自己定义的静态资源文件下,但是不可以配置静态资源访问路径,否则导致 index.html 不能被默认访问;也可以放到默认生成的 static 静态资源目录下,然后通过 localhost:8080 就可以访问到我们的欢迎页

SpringBoot2 核心知识点_第51张图片

SpringBoot2 核心知识点_第52张图片

SpringBoot2 核心知识点_第53张图片

Controller 根据请求处理 /index 跳转到 欢迎页

5.2.3 自定义 Favicon

每一个网站都有一个自己的图标,例如 Spring 官网的:

image-20210923110600679

这个配置好像在 2.3.x 版本后就没有了

如果要使用,只需要把图标改名为 favicon.ico 放到静态资源目录下就可以了,就会被自动配置为应用的图标

SpringBoot2 核心知识点_第54张图片

SpringBoot2 核心知识点_第55张图片

5.2.4 静态资源配置原理
  • SpringBoot 启动后会默认加载很多的 xxxxAutoConfiguration 类即自动配置类
  • 如果想要查看 SpringMVC 功能的自动配置类,大多数都集中在:WebMvcAutoConfiguration 这个类中
查看 WebMvcAutoConfiguration 是否生效
    
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,
		ValidationAutoConfiguration.class })
public class WebMvcAutoConfiguration {}
  • 查看 WebMvcAutoConfiguration 给容器中配置了什么东西

    • OrderedHiddenHttpMethodFilter SpringMvc 为了兼容 RestFul 风格的

    • OrderedFormContentFilter 表单内容的过滤器

    • WebMvcAutoConfigurationAdapter

@Configuration(proxyBeanMethods = false)
@Import(EnableWebMvcConfiguration.class)
// 配置文件的相关属性和 xxx 进行了绑定
@EnableConfigurationProperties({ WebMvcProperties.class,
		org.springframework.boot.autoconfigure.web.ResourceProperties.class, WebProperties.class })
@Order(0)
public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer, ServletContextAware {
  1. WebMvcProperties

    与前缀 spring.mvc 的配置文件进行绑定

@ConfigurationProperties(prefix = "spring.mvc")
public class WebMvcProperties {
  1. ResourceProperties

    与前缀 spring.resources 的配置文件进行绑定

@ConfigurationProperties(prefix = "spring.resources", ignoreUnknownFields = false)
public class ResourceProperties extends Resources {

注意: 一个配置类只有一个有参构造器,特性:有参构造器中所有参数的值都会从容器中确定

// ResourceProperties resourceProperties 获取所有和 `spring.resources` 绑定的所有值的对象
// WebMvcProperties mvcProperties  获取所有和 `spring.mvc` 绑定的所有值的对象
// ListableBeanFactory beanFactory  相当于是找的 IOC,容器工厂 bean 工厂,找 Spring 的容器
// HttpMessageConverters  找到所有的 HttpMessageConverters
// ResourceHandlerRegistrationCustomizer 找到资源处理器的自定义器
// DispatcherServletPath 处理的路径
// ServletRegistrationBean 给应用注册原生的 servlet,filter 等
public WebMvcAutoConfigurationAdapter(
		org.springframework.boot.autoconfigure.web.ResourceProperties resourceProperties,
		WebProperties webProperties, WebMvcProperties mvcProperties, ListableBeanFactory beanFactory,
		ObjectProvider<HttpMessageConverters> messageConvertersProvider,
		ObjectProvider<ResourceHandlerRegistrationCustomizer> resourceHandlerRegistrationCustomizerProvider,
		ObjectProvider<DispatcherServletPath> dispatcherServletPath,
		ObjectProvider<ServletRegistrationBean<?>> servletRegistrations) {
	this.resourceProperties = resourceProperties.hasBeenCustomized() ? resourceProperties
			: webProperties.getResources();
	this.mvcProperties = mvcProperties;
	this.beanFactory = beanFactory;
	this.messageConvertersProvider = messageConvertersProvider;
	this.resourceHandlerRegistrationCustomizer = resourceHandlerRegistrationCustomizerProvider.getIfAvailable();
	this.dispatcherServletPath = dispatcherServletPath;
	this.servletRegistrations = servletRegistrations;
	this.mvcProperties.checkConfiguration();
}

资源处理的默认规则

@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
	if (!this.resourceProperties.isAddMappings()) {
		logger.debug("Default resource handling disabled");
		return;
	}
    // webjars 规则
	addResourceHandler(registry, "/webjars/**", "classpath:/META-INF/resources/webjars/");
    // 静态资源路径的配置规则
	addResourceHandler(registry, this.mvcProperties.getStaticPathPattern(), (registration) -> {
		registration.addResourceLocations(this.resourceProperties.getStaticLocations());
		if (this.servletContext != null) {
			ServletContextResource resource = new ServletContextResource(this.servletContext, SERVLET_LOCATION);
			registration.addResourceLocations(resource);
		}
	});
}
  • if (!this.resourceProperties.isAddMappings()) { isAddMapping 点进去,可以得到信息:private boolean addMappings = true;,这里默认是true ,我们可以在 yaml 文件中设置为false ,如果为 false 则表示过滤掉所有静态资源的请求

    因为这个方法的下面都是静态资源的配置的信息,所以如果设置为 false 则直接return,不会向下执行,所以可以理解为过滤掉所有静态资源

spring:
  resources:
    add-mappings: false		#禁用掉所有静态资源
  • 注册第一种访问规则 addResourceHandler(registry, "/webjars/**", "classpath:/META-INF/resources/webjars/");,这就是为什么访问 webjars 文件夹下的静态资源可以直接访问

  • addResourceHandler(registry, this.mvcProperties.getStaticPathPattern(), (registration)

    静态资源路径的配置规则

    this.mvcProperties.getStaticPathPattern() 在 WebMvcProperties 中,这个文件与 prefix = “spring.mvc” 绑定的

    然后在 this.resourceProperties.getStaticLocations() 找静态资源的路径 private static final String[] CLASSPATH_RESOURCE_LOCATIONS = { "classpath:/META-INF/resources/","classpath:/resources/", "classpath:/static/","classpath:/public/" };

欢迎页的处理规则

HandlerMapping 处理器映射,里面保存了每一个 Handler 能处理那些请求,请求一过来 HandlerMapping 就会看,那个请求交给谁处理,找到以后用反射调用可以处理的方法

@Bean
public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext applicationContext,
		FormattingConversionService mvcConversionService, ResourceUrlProvider mvcResourceUrlProvider) {
	WelcomePageHandlerMapping welcomePageHandlerMapping = new WelcomePageHandlerMapping(
			new TemplateAvailabilityProviders(applicationContext), applicationContext, getWelcomePage(),
			this.mvcProperties.getStaticPathPattern());
	welcomePageHandlerMapping.setInterceptors(getInterceptors(mvcConversionService, mvcResourceUrlProvider));
	welcomePageHandlerMapping.setCorsConfigurations(getCorsConfigurations());
	return welcomePageHandlerMapping;
}


WelcomePageHandlerMapping(TemplateAvailabilityProviders templateAvailabilityProviders,
		ApplicationContext applicationContext, Resource welcomePage, String staticPathPattern) {
    // 这里可以得到信息,要使用 欢迎页 就必须配置路径为 /**
	if (welcomePage != null && "/**".equals(staticPathPattern)) {
		logger.info("Adding welcome page: " + welcomePage);
		setRootViewName("forward:index.html");
	}
    // 否则调用 Controller 看能不能处理这个请求
	else if (welcomeTemplateExists(templateAvailabilityProviders, applicationContext)) {
		logger.info("Adding welcome page template: index");
		setRootViewName("index");
	}
}

5.3 请求参数处理

5.3.1 Rest使用原理
  • @xxxxMapping

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

    • 之前:/getUser 获取用户 /delUser 删除用户 /updUser 修改用户 /addUser 添加用户
    • 现在:/user GET-获取用户 DELETE-删除用户 PUT-修改用户 POST-添加用户

    之前在 MVC 中使用 Rest 风格的时候,我们需要在配置一个 HiddenHttpMethodFilter

假设没有配置 HiddenHttpMethodFilter

因为在 html 表单提交之后 get 和 post 两种方式

  1. html,表示 4 中提交方式
<form action="/user" method="get">
    <input type="submit" value="GET-提交">
form>

<form action="/user" method="post">
    <input type="submit" value="POST-提交">
form>

<form action="/user" method="delete">
    <input type="submit" value="DELETE-提交">
form>

<form action="/user" method="put">
    <input type="submit" value="PUT-提交">
form>
  1. controller,4 中接受请求的方式
@RequestMapping(value = "/user",method = RequestMethod.GET)
public String getUser() {
    return "GET-用户";
}
@RequestMapping(value = "/user",method = RequestMethod.POST)
public String postUser() {
    return "POST-用户";
}
@RequestMapping(value = "/user",method = RequestMethod.DELETE)
public String deleteUser() {
    return "DELETE-用户";
}
@RequestMapping(value = "/user",method = RequestMethod.PUT)
public String putUser() {
    return "PUT-用户";
}
  1. 没有配置 HiddenHttpMethodFilter,这种情况下 get 访问会请求 get 的controller,post 访问会请求 post 的controlller,但是如果如果使用 delete、put 则都会走 get 请求,因为 html 中表达的提交方式只有两种,如果请求方式不是这两种就默认以 get 方式请求处理

解决方式:

在 SpringBoot 中已经配置好了 HiddenHttpMethodFilter

@Bean
@ConditionalOnMissingBean(HiddenHttpMethodFilter.class)
@ConditionalOnProperty(prefix = "spring.mvc.hiddenmethod.filter", name = "enabled")
public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {
	return new OrderedHiddenHttpMethodFilter();
}

点进去我们会发现

public class HiddenHttpMethodFilter extends OncePerRequestFilter {

	private static final List<String> ALLOWED_METHODS =
			Collections.unmodifiableList(Arrays.asList(HttpMethod.PUT.name(),
					HttpMethod.DELETE.name(), HttpMethod.PATCH.name()));

	/** Default method parameter: {@code _method}. */
    
    // 这里表示我们只需要带一个隐藏的 _method 项就可以使用 Rest 风格
	public static final String DEFAULT_METHOD_PARAM = "_method";
    
注意:在html 总 form 请求的时候 method 必须是 post 方式,因为在 doFilterInternal 中只有 POST 请求才能生效  "POST".equals(request.getMethod()
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
		throws ServletException, IOException {
	HttpServletRequest requestToUse = request;
	if ("POST".equals(request.getMethod()) && request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) == null) {
        // 如果是 post 请求,就会拿到 methodParam = DEFAULT_METHOD_PARAM = "_method"
		String paramValue = request.getParameter(this.methodParam);
		if (StringUtils.hasLength(paramValue)) {
			String method = paramValue.toUpperCase(Locale.ENGLISH);
			if (ALLOWED_METHODS.contains(method)) {
				requestToUse = new HttpMethodRequestWrapper(request, method);
			}
		}
	}

根据上面得到的信息,在 html 中加上隐藏 并且设置 _method,发现还是不行

<form action="/user" method="get">
    <input type="submit" value="GET-提交">
form>

<form action="/user" method="post">
    <input type="submit" value="POST-提交">
form>

<form action="/user" method="post">
    <input name="_method" type="hidden" value="DELETE"/>
    <input type="submit" value="DELETE-提交">
form>

<form action="/user" method="post">
    <input name="_method" type="hidden" value="PUT"/>
    <input type="submit" value="PUT-提交">
form>

然后我们看到 @ConditionalOnProperty(prefix = “spring.mvc.hiddenmethod.filter”, name = “enabled”),可能默认值是 false

在 yaml 文件中将这个属性的值设置为 true 就可以正常使用 Rest 风格进行访问了

spring:
  mvc:
    hiddenmethod:
      filter:
        enabled: true

Rest 原理 — 基于表单提交使用 Rest

首先,提交表单的时候会带上 _method 参数,以及真正提交方式的参数

在 Spring Boot 有过滤器,所以当请求过来的时候会被 HiddenMethodFilter 拦截

然后处理请求

protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
		throws ServletException, IOException {
	HttpServletRequest requestToUse = request;	// 原生的请求 
    // "POST".equals(request.getMethod()) 然后判断原生的请求方式是不是 POST,这就是为什么使用 delete 和 put 的时候要求请求方式是 POST 才能使用 Rest 风格
    // request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE)  判断我们当前的请求中有没有错误
	if ("POST".equals(request.getMethod()) && request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) == null) {
        // request.getParameter 在原生的请求中能获取到请求的参数,即获取到 _method 参数的值
        // methodParam = "_method"
		String paramValue = request.getParameter(this.methodParam);
		if (StringUtils.hasLength(paramValue)) {
            // 然后将 _method 参数的转换成大写,也就是说表单提交的参数 delete 大小写无所谓
			String method = paramValue.toUpperCase(Locale.ENGLISH);
            // 判断它们允许(除了 get和 post 外 兼容	PUT、DELETE、PATCH)的请求方式中包不包含提交的请求
			if (ALLOWED_METHODS.contains(method)) {
                // 原生 request(post) 包装模式 requestWrapper 重写了 getMethod 方法,返回的是传入的值
				requestToUse = new HttpMethodRequestWrapper(request, method);
			}
		}
	}
    // 过滤器链放行的时候用 wrapper,以后的方法调用 getMethod 是调用 requestWrapper 的
	filterChain.doFilter(requestToUse, response);
}

SpringBoot2 核心知识点_第56张图片

上面 Controller 请求方式的切换

  • @GetMapping("/user")
  • @PostMapping("/user")
  • @DeleteMapping("/user")
  • @PutMapping("/user")

扩展:把 _method 自定义

自定义一个 WebConfig 类,注册自己的 HiddenMethodFilter

@Bean
public HiddenHttpMethodFilter hiddenHttpMethodFilter() {
    HiddenHttpMethodFilter hiddenHttpMethodFilter = new HiddenHttpMethodFilter();
    hiddenHttpMethodFilter.setMethodParam("_m");
    return hiddenHttpMethodFilter;
}

SpringBoot2 核心知识点_第57张图片

5.3.2 请求映射

在 Spring Boot 中,所有的请求都会到 DispatherServlet 中,其实 SpringBoot 底层使用的还是 SpringMVC

DispatcherServelt 继承 FrameworkServelt 继承 HttpServelt

当请求开始的时候 HttpServlet 的doGet 最终都会调用到 FrameServlet 中的 processRequest,在 processRequest 中又去调用 doService,在最终的 DispatcherServlet 中对 doService 进行了实现

  • FrameworkServlet.java,在 HttpServletBean 中没有找到 doGet 请求,然后再它的子类FrameworkServlet 中找
@Override
protected final void doGet(HttpServletRequest request, HttpServletResponse response)
		throws ServletException, IOException {
    // HttpServlet 的doGet 最终都会调用到 FrameServlet 中的 processRequest
	processRequest(request, response);
    // 点进去这个方法发现又调用了本类的 doService 方法
}
protected final void processRequest(HttpServletRequest request, HttpServletResponse response)
		throws ServletException, IOException {
	long startTime = System.currentTimeMillis();
	Throwable failureCause = null;
	LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext();
	LocaleContext localeContext = buildLocaleContext(request);
	RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();
	ServletRequestAttributes requestAttributes = buildRequestAttributes(request, response, previousAttributes);
	WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
	asyncManager.registerCallableInterceptor(FrameworkServlet.class.getName(), new RequestBindingInterceptor());
	initContextHolders(request, localeContext, requestAttributes);
	try {
        // 这是核心部分
		doService(request, response);
	}
	catch (ServletException | IOException ex) {
		failureCause = ex;
		throw ex;
	}
	catch (Throwable ex) {
		failureCause = ex;
		throw new NestedServletException("Request processing failed", ex);
	}
	finally {
		resetContextHolders(request, previousLocaleContext, previousAttributes);
		if (requestAttributes != null) {
			requestAttributes.requestCompleted();
		}
		logResult(request, response, failureCause, asyncManager);
		publishRequestHandledEvent(request, response, startTime, failureCause);
	}
}

进去 doService 发现是一个抽象方法,然后去 DispatcherServlet 中找到对应的方法

protected abstract void doService(HttpServletRequest request, HttpServletResponse response)
			throws Exception;
  • DispatcherServlet 最终对 doService 做了实现,然后发现 doService 中又调用了 doDispatch 方法,而这个 doDispatch 方法就是请求映射的核心内容,每个请求都会调用 doDispatch 方法

  • doDispatch 所有 SpringMVC 功能分析都从这个方法开始

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
	HttpServletRequest processedRequest = request;
	HandlerExecutionChain mappedHandler = null;
	boolean multipartRequestParsed = false;
	WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
	try {
		ModelAndView mv = null;
		Exception dispatchException = null;
		try {
			processedRequest = checkMultipart(request);
			multipartRequestParsed = (processedRequest != request);
			// 找到那个 Handler 来处理我们的请求
			mappedHandler = getHandler(processedRequest);
  • processedRequest 中会保存有我们的请求地址信息等

SpringBoot2 核心知识点_第58张图片

进入 getHandler 中我们会看到 HadnlerMapping(处理器映射) 有 5 个,也就是说我们的 这里面保存到了我们的映射规则, /*** 由谁处理等

SpringBoot2 核心知识点_第59张图片

  • WelcomePageHandlerMapping 里面保存了所有 index 请求

SpringBoot2 核心知识点_第60张图片

  • RequestMappingHandlerMapping ,这个就是所有的 @RequestMapping 注解的处理器映射,这个里面保存了所有的 @RequestMappnig 和 Handler 的映射规则,就是当我们的应用启动的时候 SpirngMVC 自动扫描我们的所有的 Controller ,并解析注解,把注解信息保存到 HandlerMapping 中

这里有 5 个 HandlerMapping ,启动后会遍历这 5 个查找看谁能处理这个请求

  • 查看 RequestMappingHandlerMapping 中的信息,发现我们自己写的所有的路径在这里都进行了映射

SpringBoot2 核心知识点_第61张图片

所有的请求映射都保存在 HandlerMapping 中

SpringBoot 中自动除了配置欢迎页的 HandlerMapping。访问 / 能访问到 index.html

请求进来的时候,会逐个查看所有的 HandlerMapping 是否有请求的信息

​ 如果有就找到这个请求对应的 handler

​ 如果没有就找下一个 HandlerMapping

上面这些 HandlerMapping 都可以在 WebMvcConfiguration 类中找到

HandlerMapping 其实就是保存那个请求由谁进行处理

5.3.3 普通参数与基本注解

第一部分:注解

  • @PathVariable 路径变量
@GetMapping("/user/{name}")
public Map testParam(@PathVariable String name) {
    HashMap<Object, Object> map = new HashMap<>();
    map.put("name",name);
    return map;
}

SpringBoot2 核心知识点_第62张图片

  • @RequestHeader 获取请求头
@GetMapping("/user/{name}")
public Map testParam(@RequestHeader Map<String,String> head) {
    HashMap<Object, Object> map = new HashMap<>();
    map.put("head",head);
    return map;
}

SpringBoot2 核心知识点_第63张图片

  • @RequestParam
@GetMapping("/user1")
public Map testParam(@RequestParam("name") String name) {
    HashMap<Object, Object> map = new HashMap<>();
    map.put("name",name);
    return map;
}

SpringBoot2 核心知识点_第64张图片

  • @CookieValue
@GetMapping("/user1")
public Map testParam(@CookieValue("_ga") String _ga) {
    HashMap<Object, Object> map = new HashMap<>();
    map.put("_ga",_ga);
    return map;
}
  • @RequestBody
<form action="/save" method="post">
    <input name="name" />
    <input name="age" />
    <input type="submit" value="提交" />
form>
@PostMapping("/save")
public Map save(@RequestBody String content) {
    HashMap<String, Object> map = new HashMap<>();
    map.put("content",content);
    return map;
}

SpringBoot2 核心知识点_第65张图片

  • RequestAttribute 获取到 request 域属性

模拟页面的跳转

@Controller
public class RequestAttributeController {
    @GetMapping("/goto")
    public String goTo(HttpServletRequest request) {
        request.setAttribute("name","张三");
        request.setAttribute("age",20);
        return  "forward:/success";
    }
    @ResponseBody
    @GetMapping("/success")
    public Map successTest(@RequestAttribute("name") String name,
                           @RequestAttribute("age") Integer age,
                           HttpServletRequest request) {
        String nameRequest = (String) request.getAttribute("name");
        HashMap<String, Object> map = new HashMap<>();
        map.put("nameRequest",nameRequest);
        map.put("nameAnnotation", name);
        return map;
    }
}

测试运行

SpringBoot2 核心知识点_第66张图片

  • MatrixVariable 矩阵变量

矩阵变量需要在 SpringBoot 中手动开启,还应当绑定在路径变量中,若是有多个矩阵变量,应当使用英文符号;进行分割,若是一个矩阵变量有多个值,应当使用英文符号进行分割,或者命名多个重复的 key即可。

启动矩阵变量

在 WebMvcConfiguration 中找到方法 public void configurePathMatch(PathMatchConfigurer configurer) {,进入url 路径帮助器 UrlPathHelper urlPathHelper = new UrlPathHelper();,然后可以看到private boolean removeSemicolonContent = true; 这里有一个属性是移除分号的,默认的true


	/**
	 * 分号要是移除就会把 url 中分号后面的内容全部都去掉,即忽略了参数
	 * Set if ";" (semicolon) content should be stripped from the request URI.
	 * 

Default is "true". */ public void setRemoveSemicolonContent(boolean removeSemicolonContent) { checkReadOnly(); this.removeSemicolonContent = removeSemicolonContent; }

对于路径的处理都是 UrlPathHelper 进行解析的,

removeSemicolonContent --移除分号内容

解决方法:

自定义我们自己的 UrlPathHelper

@Controller
// 实现接口,重写方法,设置为 false
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void configurePathMatch(PathMatchConfigurer configurer) {
        UrlPathHelper urlPathHelper = new UrlPathHelper();
        urlPathHelper.setRemoveSemicolonContent(false);
        configurer.setUrlPathHelper(urlPathHelper);
    }
}

在之前的访问路径中我们使用 /user/{id}?xxx=xxx&xxx=xxx,只用 RequestParam 获得参数

矩阵变量:

  1. /cars/sell;low=34;brand=byd,audi,yd
  2. /cars/sell;low=34;brand=byd;brand=audi;brand=yd
// 1. /cars/sell;low=34;brand=byd,audi,yd 访问路径方式
		/cars/sell;low=34;brand=byd;brand=audi;brand=yd
// 2. SpringBoot 中默认是禁用了矩阵变量功能,需要手动开启矩阵变量的url 的路径变量才能被解析
// 3. 矩阵变量必须有 url 路径变量才能被解析,如果直接写路径会找到 404
@GetMapping("/cars/{path}")
public Map carsSell(@MatrixVariable("low") Integer low,
                    @MatrixVariable("brand") List<String> brand) {
    HashMap<String, Object> map = new HashMap<>();
    map.put("low",low);
    map.put("brand",brand);
    return map;
}

SpringBoot2 核心知识点_第67张图片

  1. /boss/1;age=20/2;age=10
@GetMapping("/boss/{bossId}/{empId}")
public Map boss(@MatrixVariable(value = "age",pathVar = "bossId") Integer bossAge,
                @MatrixVariable(value = "age",pathVar = "empId") Integer empAge) {
    HashMap<Object, Object> map = new HashMap<>();
    map.put("boossAge",bossAge);
    map.put("empAge",empAge);
    return map;
}

SpringBoot2 核心知识点_第68张图片

5.3.4 各种类型参数解析原理分析

进入 DispatcherServlet 类中,找到 doDispatch 方法,然后

首先在 HandlerMapping 中找到处理请求的 Handler,为当前 Handler 找一个适配器 HandlerAdapter

// Determine handler adapter for the current request.
// 决定一个 Handler 的适配器为当前请求
// 在此之前我们已经找到那个方法能够处理这个请求了
// SpringMVC 需要在底层通过反射调用controller 中的方法,以及一大堆的参数,SpringBoot 就把这些封装到了 HandlerAdapter 中,相当于这就是一个大的反射工具。
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

然后找到了 4 个 HandlerAdapter(处理器适配器,可以完成不同的功能)

SpringBoot2 核心知识点_第69张图片

  • RequestMappingHandlerAdapter

支持方法上标注 @RequestMapping 这些注解的适配器

  • HandlerFunctionAdapter

支持函数式编程的适配器

进入 supports 方法,这里会把 handler 封装一个 HandlerMethod

@Override
public final boolean supports(Object handler) {
	return (handler instanceof HandlerMethod && supportsInternal((HandlerMethod) handler));
}

SpringBoot2 核心知识点_第70张图片

返回的是一个 RequestMappingHandlerAdapter

SpringBoot2 核心知识点_第71张图片

至此,找到的请求的适配器

DiapatcherServlet 的 doDispatche 方法中

  • 执行目标方法
// Actually invoke the handler.
// 真正的执行 handler
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

进去 ha.handle,然后进入 handlerInternal

SpringBoot2 核心知识点_第72张图片

对于目标方法的真正执行都在 RequestMappingHandlerAdapter 类的 handleInternal 方法中

SpringBoot2 核心知识点_第73张图片

向下走到 invokeHandlerMethod 方法

// No synchronization on session demanded at all...
// 执行目标方法
mav = invokeHandlerMethod(request, response, handlerMethod);
  • argumentResolvers 参数解析

进到 invokeHandlerMethod 方法可以看到 27 个参数解析器 argumentResolvers

执行目标方法的核心关键会设置参数解析器,将来目标方法的每一个参数值是什么是由这个参数解析器确定的,确定将要执行的目标方法的每一个参数值是什么

SpringMVC 目标方法能写多少种参数类型,取决于参数解析器

这个参数解析器其实就是一个接口:HandlerMethodArgumentResolver,

SpringBoot2 核心知识点_第74张图片

这个接口中,接口第一个方法 supportsParameter 判断接口是否支持这个方法,即当前解析器支持解析那种参数。

如果支持就调用 resolveArgument 解析方法进行解析

SpringBoot2 核心知识点_第75张图片

  • returnValueHandlers 返回值处理器

从这里我们可以看到目标方法可以写多少种类型的返回值

SpringBoot2 核心知识点_第76张图片

SpringMVC 会提前把参数解析器和返回值处理器都放到一个目标方法包装的 ServletInvocableHandlerMethod 这个可执行的方法中

SpringBoot2 核心知识点_第77张图片

向下找到方法 invocableMethod.invokeAndHandle(webRequest, mavContainer); 执行并处理方法来执行目标方法,invocableMethod 里面封装了各种处理器

public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer,
		Object... providedArgs) throws Exception {
    // 这个方法执行的时候进入目标方法,然后再向下执行,真正执行目标方法
	Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
	setResponseStatus(webRequest);
	if (returnValue == null) {
		if (isRequestNotModified(webRequest) || getResponseStatus() != null || mavContainer.isRequestHandled()) {
			disableContentCachingIfNecessary(webRequest);
			mavContainer.setRequestHandled(true);
			return;
		}
	}
	else if (StringUtils.hasText(getResponseStatusReason())) {
		mavContainer.setRequestHandled(true);
		return;
	}

进入 invokeForRequest(webRequest, mavContainer, providedArgs); 方法中

@Nullable
public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
		Object... providedArgs) throws Exception {
    // 获取方法的所有参数的值 确定方法参数值
	Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
	if (logger.isTraceEnabled()) {
		logger.trace("Arguments: " + Arrays.toString(args));
	}
	return doInvoke(args);
}

SpringBoot2 核心知识点_第78张图片

// 真正的如何确定目标方法的每一个值
protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
		Object... providedArgs) throws Exception {
    // 获取所有参数的参数声明
	MethodParameter[] parameters = getMethodParameters();
	if (ObjectUtils.isEmpty(parameters)) {
		return EMPTY_ARGS;
	}
    // 所有目标方法确定好的值
	Object[] args = new Object[parameters.length];
    // 遍历所有参数
	for (int i = 0; i < parameters.length; i++) {
		MethodParameter parameter = parameters[i];
		parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
        // 给 args 赋值
		args[i] = findProvidedArgument(parameter, providedArgs);
		if (args[i] != null) {
			continue;
		}
        // 首先判断当前解析器是否支持这中参数类型
		if (!this.resolvers.supportsParameter(parameter)) {
			throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver"));
		}
		try {
            // 
			args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
		}
		catch (Exception ex) {
			// Leave stack trace for later, exception may actually be resolved and handled...
			if (logger.isDebugEnabled()) {
				String exMsg = ex.getMessage();
				if (exMsg != null && !exMsg.contains(parameter.getExecutable().toGenericString())) {
					logger.debug(formatArgumentError(parameter, exMsg));
				}
			}
			throw ex;
		}
	}
    // 所有目标方法确定好的值
	return args;
}

SpringBoot2 核心知识点_第79张图片

确定目标方法的参数值

首先遍历判断所有参数解析器那个支持解析这个参数

@Nullable
private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
	HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter);
	if (result == null) {
		for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) {
			if (resolver.supportsParameter(parameter)) {
				result = resolver;
				this.argumentResolverCache.put(parameter, result);
				break;
			}
		}
	}
	return result;
}

解析参数的值

@Override
@Nullable
public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
		NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
	NamedValueInfo namedValueInfo = getNamedValueInfo(parameter);
	MethodParameter nestedParameter = parameter.nestedIfOptional();
        // 得到参数变量
	Object resolvedName = resolveEmbeddedValuesAndExpressions(namedValueInfo.name);
	if (resolvedName == null) {
		throw new IllegalArgumentException(
				"Specified name must not resolve to null: [" + namedValueInfo.name + "]");
	}
	// 确定值
	Object arg = resolveName(resolvedName.toString(), nestedParameter, webRequest);
	if (arg == null) {
		if (namedValueInfo.defaultValue != null) {
			arg = resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue);
		}
		else if (namedValueInfo.required && !nestedParameter.isOptional()) {
			handleMissingValue(namedValueInfo.name, nestedParameter, webRequest);
		}
		arg = handleNullValue(namedValueInfo.name, arg, nestedParameter.getNestedParameterType());
	}
	else if ("".equals(arg) && namedValueInfo.defaultValue != null) {

所有支持的注解的类型 HandlerMethodArgumentResolver

SpringBoot2 核心知识点_第80张图片

5.3.5 Servlet API

WebRequest、ServletRequest、MultipartRequest、HttpSession、java.servlet.http.PushBuilder、Principal、InputStream、Reader、HttpMethod、Locale、TimeZone、Zoneld

ServletRequestMethodArgumentResolver 以上的部分参数

@Override
	public boolean supportsParameter(MethodParameter parameter) {
		Class<?> paramType = parameter.getParameterType();
		return (WebRequest.class.isAssignableFrom(paramType) ||
				ServletRequest.class.isAssignableFrom(paramType) ||
				MultipartRequest.class.isAssignableFrom(paramType) ||
				HttpSession.class.isAssignableFrom(paramType) ||
				(pushBuilder != null && pushBuilder.isAssignableFrom(paramType)) ||
				Principal.class.isAssignableFrom(paramType) ||
				InputStream.class.isAssignableFrom(paramType) ||
				Reader.class.isAssignableFrom(paramType) ||
				HttpMethod.class == paramType ||
				Locale.class == paramType ||
				TimeZone.class == paramType ||
				ZoneId.class == paramType);
	}
5.3.6 复杂参数

Map、Model(map、model里面的数据会被放在request的请求域 request.setAttribute)、Erros/BindingResult、RedirectAttributes(重定向携带数据)、ServletResponse(response 响应)、SessionStatus、UriComponentsBuilder、ServletUriComponentsBuilder

Map<String,Object> map, Model model, HttpServletRequest request
// 都是可以给 request 域中放数据,以后方便 request.Attribute 获取

原理

Map、Model 类型的参数,会返回 mavContainer.getModel(); --> BindinigAwareModelMap 是 Model 也是 Map

无论是 Map 还是 Model 类型最终都会调用这个方法 mavContainer.getModel(); 获取到值的

public class ModelAndViewContainer {

	private boolean ignoreDefaultModelOnRedirect = false;

	@Nullable
	private Object view;

	private final ModelMap defaultModel = new BindingAwareModelMap();

SpringBoot2 核心知识点_第81张图片

SpringBoot2 核心知识点_第82张图片

SpringBoot2 核心知识点_第83张图片

解析完参数后会进行转发,

SpringBoot2 核心知识点_第84张图片

InvocableHandlerMethod.java 类中执行 this.returnValueHandlers.hanleReturnValue 进行返回值的处理

解析参数的值后,将所有的数据都放在 ModelAndViewContainer中,包含要去的页面地址 View,还包括 Model 数据。

5.4 视图解析与模板引擎

视图解析就是 SpringBoot 在处理完请求之后来跳转到某个页面的这个过程。

视图解析:因为 SpringBoot 默认打包方式是一个jar包即压缩包,jsp 不支持打包成压缩包,所以 SpringBoot 默认不支持 jsp,需要引入第三方模板引擎技术实现页面的渲染

5.4.1 试图解析

经常使用的方式就是处理完请求之后进行转发或者重定向到一个指定的视图页面

SpringBoot2 核心知识点_第85张图片

视图解析的原理过程

  1. 目标方法处理的过程中,所有数据都会被放在 ModelAndViewContainer 里面。包括数据和视图地址

  2. 方法的参数是一个自定义类型对象(从请求参数中确定的),把它重新放在 ModelAndViewContainer

  3. 任何目标方法执行完成以后都会返回 ModelAndView(数据和视图地址)

  4. processDispatchResult 处理派发结果(页面该如何响应)

    1. render(mv、request、response); 进行页面渲染逻辑

      根据方法的 String 返回值得到 View 对象[定义了页面的渲染逻辑]

      所有的视图解析器尝试是否能根据当前返回值得到 View 对象

      得到了 redirect:/main.html --> Thymeleaf new RedirectView()

      ContentNegotiationViewResolver 里面包含了下面所有的视图解析器,内部还是利用下面所有视图解析器得到视图对象

      view.render(mv.getModelInternal(),request,response); 视图对象调用自定义的 render 进行页面渲染工作

      • RedirectView 如何渲染【重定向到一个页面】
      • 获取目标 url 地址
      • response.sendRedirect(encodedURL)

视图解析

  • 返回值以 forward: 开始:new InternalResourceView(forwardUrl); --> 转发 request.getRequestDispatcher(path).forward(request,response);
  • 返回值以 redirect:开始:new RedirectView() --> render 就是重定向
  • 返回值是普通字符串:new ThymeleafView() --> 自定义视图解析器、自定义视图
5.4.2 thymeleaf 模板引擎

官网:thymeleaf.org

Thymeleaf is a modern server-side Java template engine for both web and standalone environments, capable of

processing HTML, XML, JavaScript, CSS and even plain text.

thymeleaf 是一个现代化的服务端的 Java 模板引擎。

5.4.3 thymeleaf 基础使用语法
  1. 表达式
表达式名字 语法 用途
变量取值 ${} 获取请求域、session域、对象等值
选择变量 *{} 获取上下文对象值
消息 #{} 获取国际化等值
链接 @{} 生成链接
片段表达式 ~{} jsp:include 作用,引入公共页面片段
  1. 字面量

    • 文本值:‘text’

    • 数字:0,44,3.3

    • 布尔值:true,false

    • 空值:null

    • 变量:value,key

  2. 文本操作

    字符串拼接:+

    变量替换:|My name is $(name)|

  3. 数字运算

    +,-,*,/,%

  4. 布尔运算

    and,or,!,not

  5. 比较运算

    < > >= <= (gt,lt,ge,le)

    等式:== != (eq,ne)

  6. 条件运算

    if-then:(if)?(then)

    if-then-else:(if)?(then):(else)

    Default:(value)?:(defaultvalue)

  7. 设置属性值 th:attr

  • 设置单个值
<form action="subscribe.html" th:attr="action=@{/subscribe}">
  <fieldset>
    <input type="text" name="email" />
    <input type="submit" value="Subscribe!" th:attr="value=#{subscribe.submit}"/>
  fieldset>
form>
  • 设置多个值
<img src="../../images/gtvglogo.png"  th:attr="src=@{/images/gtvglogo.png},title=#{logo},alt=#{logo}" />
  1. 迭代
<tr th:each="prod : ${prods}">
        <td th:text="${prod.name}">Onionstd>
        <td th:text="${prod.price}">2.41td>
        <td th:text="${prod.inStock}? #{true} : #{false}">yestd>
tr>
<tr th:each="prod,iterStat : ${prods}" th:class="${iterStat.odd}? 'odd'">
  <td th:text="${prod.name}">Onionstd>
  <td th:text="${prod.price}">2.41td>
  <td th:text="${prod.inStock}? #{true} : #{false}">yestd>
tr>
  1. 条件运算
<a href="comments.html"
th:href="@{/product/comments(prodId=${prod.id})}"
th:if="${not #lists.isEmpty(prod.comments)}">viewa>
<div th:switch="${user.role}">
  <p th:case="'admin'">User is an administratorp>
  <p th:case="#{roles.manager}">User is a managerp>
  <p th:case="*">User is some other thingp>
div>
  1. 优先级

SpringBoot2 核心知识点_第86张图片

5.4.4 SpringBoot 中使用 thymelef 模板引擎
  1. 创建项目
  2. 引入 xml 启动器
<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-thymeleafartifactId>
dependency>
  1. 引入 启动器后 SpringBoot 会为我们自动配置好 thymeleaf
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(ThymeleafProperties.class)
@ConditionalOnClass({ TemplateMode.class, SpringTemplateEngine.class })
@AutoConfigureAfter({ WebMvcAutoConfiguration.class, WebFluxAutoConfiguration.class })
public class ThymeleafAutoConfiguration {

自动配置好的东西,在 ThymeleafAutoConfiguration 类中可以看到

  • SpringBoot 配置好了 private final ThymeleafProperties properties;
  • 配置好了模板引擎:SpringTemplateEngine engine = new SpringTemplateEngine();
  • 配置好了视图解析器:ThymeleafViewResolver

使用的时候只需要开发页面就可以了。

ThymeleafProperties 类中可以看到已经配置好的前缀和后缀

@ConfigurationProperties(prefix = "spring.thymeleaf")
public class ThymeleafProperties {

	private static final Charset DEFAULT_ENCODING = StandardCharsets.UTF_8;
	// 前缀,templates 这个文件夹在创建项目的时候文件夹已经创建好了
	public static final String DEFAULT_PREFIX = "classpath:/templates/";
	// 后缀,默认都是 xxxx.html 页面
	public static final String DEFAULT_SUFFIX = ".html";
  1. 编写 Controller 实现页面的跳转
@Controller
public class ViewController {
    @GetMapping("/thymeleaf")
    public String toPage(Model model) {
        // model 中的数据会被放到请求域中,request.setAttribute("xxx","xxx");
        model.addAttribute("message","Hello,World");
        model.addAttribute("url","http://www.baidu.com");
        return "success";
    }
}
  1. html 页面

<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Titletitle>
head>
<body>
<h1 th:text="${message}">Thymeleaf 你好h1>


<a href="www.baidu.com" th:href="${url}">百度a>
body>
html>

SpringBoot2 核心知识点_第87张图片

5.5 拦截器

public interface HandlerInterceptor {
	default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
			throws Exception {
		return true;
	}
	default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
			@Nullable ModelAndView modelAndView) throws Exception {
	}
	default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
			@Nullable Exception ex) throws Exception {
	}
}

使用步骤

  1. 编写一个拦截器实现 HandlerInterceptor 接口,拦截器中写上拦截规则
/**
 * 登录检查
 * 1. 配置好拦截器要拦截那些请求
 * 2. 把这些配置放在容器中
 */
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 {
        // 登录检查
        HttpSession session = request.getSession();
        Object loginUser = session.getAttribute("loginUser");
        if (loginUser != null) {
            // 放行
            return true;
        }
        // 拦截,拦截住的都是为登录的,跳转都登陆页
        request.setAttribute("msg","请登录后请求");
        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 {

    }

    /**
     * 页面渲染以后
     * @param request
     * @param response
     * @param handler
     * @param ex
     * @throws Exception
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

    }
}
  1. 拦截器注册到容器中(实现 WebMvcConfigurer 的 addInterceptors),指定拦截规则【如果是拦截所有,静态资源也会被拦截】
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

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

5.6 文件上传

  • html
文件上传表单
<form th:action="@{/upload}" method="post" enctype="multipart/form-data">
	<label>单个文件label>
	<input type="file" name="headImage">
	<label>多个文件label>
	<input type="file" name="photos" multiple>
	<input type="submit" value="提交">
form>
  • Controller
/**
 * 测试文件上传
 */
@Slf4j
@Controller
public class FormController {

    @GetMapping("/form")
    public String toFormPage() {
        return "form/forms-upload";
    }

    /**
     * MultipartFile 自动封装上传过来的文件
     *
     * @param name
     * @param age
     * @param headImage
     * @param photos
     * @return
     */
    @PostMapping("/upload")
    public String upload(@RequestParam("name") String name,
                         @RequestParam("age") Integer age,
                         @RequestPart("headImage") MultipartFile headImage,
                         @RequestPart("photos") MultipartFile[] photos) throws IOException {

        log.info("name={},age={},headImage={},photoSize={}", name, age, headImage.getSize(), photos.length);

        if (!headImage.isEmpty()) {
            // 保存到文件服务器:OSS 服务器
            String filename = headImage.getOriginalFilename();
            headImage.transferTo(new File("D:\\" + filename));
        }

        if (photos.length > 0) {
            for (MultipartFile photo : photos) {
                if (!photo.isEmpty()) {
                    String filename1 = photo.getOriginalFilename();
                    photo.transferTo(new File("D:\\" + filename1));
                }
            }
        }


        return "main";

    }
}

  • 修改文件上传大小
spring.servlet.multipart.max-file-size=10MB
spring.servlet.multipart.max-request-size=100MB

文件上传原理

文件上传的所有配置都被封装到了 MultipartAutoConfiguration 类里面了

文件上传所有的配置被封装到了 MultipartProperties.class

自动配置好了文件上传解析器 StandardServletMultipartResolver

@Bean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME) // MULTIPART_RESOLVER_BEAN_NAME = multipartResolver
@ConditionalOnMissingBean(MultipartResolver.class) // 如果类中没有自定义配置的时候生效
// 文件上传解析器,只能上传标准的以 Servlet 方式上传的文件
public StandardServletMultipartResolver multipartResolver() {
	StandardServletMultipartResolver multipartResolver = new StandardServletMultipartResolver();
	multipartResolver.setResolveLazily(this.multipartProperties.isResolveLazily());
	return multipartResolver;
}

首先找到 DispatcherServletdoDispatch 方法中

boolean multipartRequestParsed = false; 记录一下文件上传是否已经被解析了

processedRequest = checkMultipart(request); 判断当前请求是不是一个文件上传请求,如果是把这个 request 包装,包装成一个 processedRequest。进去之后可以看到详情

protected HttpServletRequest checkMultipart(HttpServletRequest request) throws MultipartException {
    // this.multipartResolver.isMultipart(request) 判断当前是不是文件上传请求,全系统只有一个
	if (this.multipartResolver != null && this.multipartResolver.isMultipart(request)) {
		if (WebUtils.getNativeRequest(request, MultipartHttpServletRequest.class) != null) {
			if (DispatcherType.REQUEST.equals(request.getDispatcherType())) {
				logger.trace("Request already resolved to MultipartHttpServletRequest, e.g. by MultipartFilter");
			}
		}
		else if (hasMultipartException(request)) {
			logger.debug("Multipart resolution previously failed for current request - " +
					"skipping re-resolution for undisturbed error rendering");
		}
		else {
			try {
				return this.multipartResolver.resolveMultipart(request);
			}
			catch (MultipartException ex) {
				if (request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) != null) {
					logger.debug("Multipart resolution failed for error dispatch", ex);
					// Keep processing error dispatch with regular request handle below
				}
				else {
					throw ex;
				}
			}
		}
	}
	// If not returned before: return original request.
	return request;
}

进去 isMultipart 方法

@Override
public boolean isMultipart(HttpServletRequest request) {
   return StringUtils.startsWithIgnoreCase(request.getContentType(),
                                           // 判断上传是否是 multipart/
         (this.strictServletCompliance ? MediaType.MULTIPART_FORM_DATA_VALUE : "multipart/"));
}

因为上面的 multipart/ 判断,所以在上传文件的表单中必须写

5.7 异常处理

5.7.1 错误处理
  1. 默认规则

默认情况下,Spring Boot 会提供 /error 处理所有错误的映射

对于机器客户端,它将生成 JSON 响应,其中包含错误,HTTP 状态和异常消息的详细信息,对于浏览器客户端,响应一个 whitelabel 错误视图,以 HTML 格式呈现相同的数据。

  • 浏览器端

SpringBoot2 核心知识点_第88张图片

要对其进行自定义,添加 View 解析为 error

要完全替换默认行为,可以实现 ErrorController 并注册该类型的 Bean 定义,或添加 ErrorAttributes 类型的组件以使用现有机制来替换其内容。

如果我们想要自定义错误页面,在 public 文件夹下或者 templates 文件夹下创建 error文件夹,在文件夹创建错误页面(4xx.html,5xx.html),这里的错误文件会被自动解析

SpringBoot2 核心知识点_第89张图片

SpringBoot2 核心知识点_第90张图片

  1. 定制错误处理逻辑
  • 自定义错误页面

    • error/404.html error/5xx.html 有精确的错误状态页面就精确匹配,没有就找到 4xx.html,如果都没有就触发白页
    • ControllerAdvice + @ExceptionHandler 处理全局异常,底层是 ExceptionHandlerExceptionResolver 提供的处理支持
/**
 * 处理整个 web controller 的异常
 */
@Slf4j
@ControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler({ArithmeticException.class,NullPointerException.class})// 处理异常
    public String mathException(Exception e) {
        log.info("异常{}",e);
        return "login";// 处理异常后跳转的视图地址
    }
}
  • ResponseStatus + 自定义异常

底层是 ResponseStatusExceptionResolver,把 responseStatus 注解的信息底层调用 response.sendError(statusCode,resolverReason); tomcat 发送的 /error

/**
 * 自定义异常类,当 throw 抛出此异常的时候给出状态信息,异常信息
 */
@ResponseStatus(value = HttpStatus.FORBIDDEN, reason = "用户数量太多")
public class UserTooManyException extends RuntimeException{
    public UserTooManyException() {

    }
    public UserTooManyException(String message) {
        super(message);
    }
}
// Controller 中 模拟异常
@GetMapping("/form")
public String toFormPage() {
    if (3 > 1) {
        // 抛出异常
        throw new UserTooManyException();
    }
    return "form/forms-upload";
}

抛出异常的时候会跳转到 404 页面给出提示信息 message

SpringBoot2 核心知识点_第91张图片

  • Spring 底层的异常,如 参数类型转换异常

DefaultHandlerExceptionResolver 处理框架底层的异常

response.sendError(HttpServletResponse.SC_BAD_REQUEST,ex.getMessage());

  • 自定义实现 HandlerExceptionResolver 处理异常,可以作为默认的全局异常处理规则
@Order(value = Ordered.HIGHEST_PRECEDENCE) // 优先级,数字越小优先级越高
@Component
public class CustomerHandlerExceptionResolver implements HandlerExceptionResolver {
  @Override
  public ModelAndView resolveException(HttpServletRequest request,
                                       HttpServletResponse response,
                                       Object handler,
                                       Exception ex) {

      try {
          response.sendError(500,"我的错误信息");
      } catch (IOException e) {
          e.printStackTrace();
      }

      return new ModelAndView();
  }
}

SpringBoot2 核心知识点_第92张图片

  • ErrorViewResolver 实现自定义处理异常

    response.sendError error 请求就会转发给 Controller

    你的异常没有任何人能处理。tomcat 底层 response.sendError error 请求就会转给 Controller

    basicErrorController 要去的页面地址是 ErrorViewResolver

  1. 异常处理自动配置

ErrorMvcAutoConfiguration 自动配置了异常处理规则

@AutoConfigureBefore(WebMvcAutoConfiguration.class)
// 绑定了一些配置文件
@EnableConfigurationProperties({ ServerProperties.class, WebMvcProperties.class })
public class ErrorMvcAutoConfiguration {
@Bean
// 当容器中没有这个组件的时候生效,容器中放入的组件 ,类型 DefaultErrorAttributes  id 为 errorAttributes
@ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT)
public DefaultErrorAttributes errorAttributes() {
	return new DefaultErrorAttributes();
}
@Bean
// 容器中放入的组件,类型:BasicErrorController  id:basicErrorController
@ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)
public BasicErrorController basicErrorController(ErrorAttributes errorAttributes,
		ObjectProvider<ErrorViewResolver> errorViewResolvers) {
	return new BasicErrorController(errorAttributes, this.serverProperties.getError(),
			errorViewResolvers.orderedStream().collect(Collectors.toList()));
}
  • BasicErrorController进去之后发现,响应 JSON 和 白页适配响应
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")	// 处理默认 /error 路径的请求 
public class BasicErrorController extends AbstractErrorController {
    
    	@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
	public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
		HttpStatus status = getStatus(request);
		Map<String, Object> model = Collections
				.unmodifiableMap(getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
		response.setStatus(status.value());
		ModelAndView modelAndView = resolveErrorView(request, response, status, model);
		return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);// 页面响应
	}
    
    @RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
	HttpStatus status = getStatus(request);
	if (status == HttpStatus.NO_CONTENT) {
		return new ResponseEntity<>(status);
	}
	Map<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
	return new ResponseEntity<>(body, status);
}

    要么响应页面,要么把 ResponseEntity 中的数据响应出去,相当于一个 json
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(prefix = "server.error.whitelabel", name = "enabled", matchIfMissing = true)
@Conditional(ErrorTemplateMissingCondition.class)
protected static class WhitelabelErrorViewConfiguration {
	private final StaticView defaultErrorView = new StaticView();
    // 容器中还会有一个 view 组件,这个组件的id 叫做 error
	@Bean(name = "error")
	@ConditionalOnMissingBean(name = "error")
	public View defaultErrorView() {
		return this.defaultErrorView;
	}
	// If the user adds @EnableWebMvc then the bean name view resolver from
	// WebMvcAutoConfiguration disappears, so add it back in to avoid disappointment.
	@Bean
	@ConditionalOnMissingBean
    // 为了解析 view 视图,配置了一个 BeanNameViewResolver 的视图解析器
    // 按照返回的视图名(error)作为组件的 id 去容器中找 View 对象
    // 只要请求发到 /error 路径,就会找 error 视图,error 视图又是 View 中的一个组件,利用视图解析器找到 error 视图,最终 View 渲染的是什么样,页面就是什么样
	public BeanNameViewResolver beanNameViewResolver() {
		BeanNameViewResolver resolver = new BeanNameViewResolver();
		resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 10);
		return resolver;
	}
}

如果想要返回页面,就会找 error 视图,默认是一个百页

// 写出去的是 JSON
@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
// 错误视图、错误页
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
  • DefaultErrorViewResolverConfiguration 错误视图解析器组件

    进去 DefaultErrorViewResolver

public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered {

	private static final Map<Series, String> SERIES_VIEWS;

	static {
		Map<Series, String> views = new EnumMap<>(Series.class);
        // 如果是客户端错误就是 4xx
		views.put(Series.CLIENT_ERROR, "4xx");
        // 如果是服务端错误就是 5xx
		views.put(Series.SERVER_ERROR, "5xx");
		SERIES_VIEWS = Collections.unmodifiableMap(views);
	}
    
    @Override
    // 解析得到视图对象
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
	ModelAndView modelAndView = resolve(String.valueOf(status.value()), model);
	if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
		modelAndView = resolve(SERIES_VIEWS.get(status.series()), model);
	}
	return modelAndView;
}
    
    // 如果发生错误,会以 HTTP 的状态码作为试图页面地址
    // viewName 得到的其实是一个状态码,如果是 404错误就会找 error/404.html 的页面
    private ModelAndView resolve(String viewName, Map<String, Object> model) {
	String errorViewName = "error/" + viewName;
	TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName,
			this.applicationContext);
	if (provider != null) {
		return new ModelAndView(errorViewName, model);
	}
	return resolveResource(errorViewName, model);
}
  • DefaultErrorAttributes 定义了最终错误里面可以包含的那些内容
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
    // 保存错误的默认属性  status  trace  exception ......
    this.storeErrorAttributes(request, ex);
    return null;
}
  1. 异常处理流程
  • 执行目标方法,目标方法运行期间有任何异常都会被 catch,并且用 dispatchException 进行封装,标志当前请求结束

DispatcherServlet 的 doDispatcher 方法中可以看到

	mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
	if (asyncManager.isConcurrentHandlingStarted()) {
		return;
	}
	applyDefaultViewName(processedRequest, mv);
	mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
	dispatchException = ex;
}
catch (Throwable err) {
	// As of 4.3, we're processing Errors thrown from handler methods as well,
	// making them available for @ExceptionHandler methods and other scenarios.
	dispatchException = new NestedServletException("Handler dispatch failed", err);
}
  • 当异常被捕获之后进入视图解析流程(页面渲染流程)

    mappedHandler:那个 Controller 处理器

    mv:只有目标方法正确执行了才有值

    dispatchException:目标方法中存在的异常

	processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception ex) {
	triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch (Throwable err) {
	triggerAfterCompletion(processedRequest, response, mappedHandler,
			new NestedServletException("Handler processing failed", err));
}
finally {
	if (asyncManager.isConcurrentHandlingStarted()) {
		// Instead of postHandle and afterCompletion
		if (mappedHandler != null) {
			mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
		}
	}
	else {
		// Clean up any resources used by a multipart request.
		if (multipartRequestParsed) {
			cleanupMultipart(processedRequest);
		}
	}
}



private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
			@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
			@Nullable Exception exception) throws Exception {
		
		boolean errorView = false;
		// execption 保存的异常,如果异常不为空则执行下面代码
		if (exception != null) {
            // 判断异常是不是定义信息异常
			if (exception instanceof ModelAndViewDefiningException) {
				logger.debug("ModelAndViewDefiningException encountered", exception);
				mv = ((ModelAndViewDefiningException) exception).getModelAndView();
			}
			else {
				Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
                // 处理 handler 的异常,处理结果保存为一个 mv(ModelAndView)
				mv = processHandlerException(request, response, handler, exception);
				errorView = (mv != null);
			}
		}
  • mv = processHandlerException 处理 handler 发生的异常,处理完成返回 mv(ModelAndView)

    进去方法

@Nullable
protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response,
		@Nullable Object handler, Exception ex) throws Exception {
	// Success and error responses may use different content types
	request.removeAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);
	// Check registered HandlerExceptionResolvers...
	ModelAndView exMv = null;	// 首先定义一个 ModelAndView
	if (this.handlerExceptionResolvers != null) {
        // 遍历所有的 HandlerExceptionResolver,查看谁能处理当前异常,处理器异常解析器
		for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers) {
			exMv = resolver.resolveException(request, response, handler, ex);
			if (exMv != null) {
				break;
			}
		}
	}
	if (exMv != null) {
		if (exMv.isEmpty()) {
			request.setAttribute(EXCEPTION_ATTRIBUTE, ex);
			return null;
		}
		// We might still need view name translation for a plain error model...
		if (!exMv.hasView()) {
			String defaultViewName = getDefaultViewName(request);
			if (defaultViewName != null) {
				exMv.setViewName(defaultViewName);
			}
		}
		if (logger.isTraceEnabled()) {
			logger.trace("Using resolved error view: " + exMv, ex);
		}
		else if (logger.isDebugEnabled()) {
			logger.debug("Using resolved error view: " + exMv);
		}
		WebUtils.exposeErrorRequestAttributes(request, ex, getServletName());
		return exMv;
	}
	throw ex;
}
  • 系统中默认的异常解析器

在上面的异常的自动配置的时候就放了一个 DefaultErrorAttributes组件,其实就是一个 Handler 的异常处理器,专门处理异常

SpringBoot2 核心知识点_第93张图片

  • DefaultErrorAttributes

调用接口方法处理异常

public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
    // 保存 errorAttributes 错误的属性信息
    this.storeErrorAttributes(request, ex);
    // 返回 null
    return null;
}

private void storeErrorAttributes(HttpServletRequest request, Exception ex) {
    // 给 request 域中 ERROR_INTERNAL_ATTRIBUTE 属性
    request.setAttribute(ERROR_INTERNAL_ATTRIBUTE, ex);
}

默认没有任何人能够处理异常,则异常会被抛出,如果没有任何能处理,则底层会发送 /error 请求

发送 /error 请求后会被底层的 BasicErrorController 进行处理

// 解析错误视图,包括错误的状态请求数据等信息
ModelAndView modelAndView = resolveErrorView(request, response, status, model);


protected ModelAndView resolveErrorView(HttpServletRequest request, HttpServletResponse response, HttpStatus status,
		Map<String, Object> model) {
    // 遍历所有的 ErrorViewResolver 查看谁能解析,如果能解析则封装 ModelAndView
	for (ErrorViewResolver resolver : this.errorViewResolvers) {
		ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);
		if (modelAndView != null) {
			return modelAndView;
		}
	}
	return null;
}

默认只有一个 DefaultErrorViewResolver ,就是之前在ErrorMvcAutoConfiguration 中放入到组件

DefaultErrorViewResolver 作用就是把响应状态码作为错误页的地址拼接成 error/5xx.html,最终把模板引擎响应这个页面

‘//error/404.’
‘//error/404.html’
‘//error/4xx.’
‘//error/4xx.html’

5.8 Web 原生组件注入(Servlet、Filter、Listener)

如何在使用 Spring Boot 的过程中注入 web 的原生组件(Servlet、Filter、Listener)

官网:https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.developing-web-applications.embedded-container.servlets-filters-listeners

在之前 SpringMVC 要使用这些组件,需要把这些组件写好之后配置在 web.xml 文件中

5.8.1 使用 Servlet API

When using an embedded container, automatic registration of classes annotated with @WebServlet, @WebFilter, and @WebListener can be enabled by using @ServletComponentScan.

编写一些 servelt ,然后在主启动类上使用注解 @ServletComponentScan

// 指定原生 Servlet 组件都放在哪里
@ServletComponentScan(basePackages = "com.thymeleaf")
@SpringBootApplication
public class ThymeleafApplication {

    public static void main(String[] args) {
        SpringApplication.run(ThymeleafApplication.class, args);
    }

}

Servlet

// 直接响应,没有经过 Spring 的拦截器
@WebServlet(urlPatterns = "/myservlet")
public class MyServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.getWriter().write("resources Servlet");
    }
}

SpringBoot2 核心知识点_第94张图片

  • 为什么自己写的 MyServlet 映射的路径直接相应,而不会经过 Spring 的拦截器?

    从整个系统来看,一共有两个 Serlvet,

    一个是自定义的 MyServlet,它要处理的路径是 /myservlet 路径

    另一个是 DispatcherServlet,它处理的路径是 / 路径

    扩展:DispatcherServlet 如何注册的

    • 容器中自动配置了 DispatcherServlet,属性绑定到 WebMvcProperties 中,对应的配置项部分是 spring.mvc
    • 然后通过 ServletRegistrationBean 把 DispatcherServlet 配置进来
    • 多个请求路径的话会采用精确优先原则
    DispatcherServletRegistrationBean registration = new DispatcherServletRegistrationBean(dispatcherServlet,
        webMvcProperties.getServlet().getPath()); 这里的 getPath() 进去之后找到的是就是 / 路径
      private String path = "/";
    

SpringBoot2 核心知识点_第95张图片

Filter

@Slf4j
@WebFilter(urlPatterns = {"/myservlet"})// 要拦截的 url 地址
public class MyFilter extends HttpFilter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        log.info("MyFilter初始化完成");
    }


    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        log.info("MyFilter工作");
        chain.doFilter(request,response);
    }

    @Override
    public void destroy() {
        log.info("MyFilter销毁");
    }
}

SpringBoot2 核心知识点_第96张图片

Listener

@Slf4j
@WebListener
public class MyServletContextListener implements ServletContextListener {

    @Override
    public void contextInitialized(ServletContextEvent sce) {
        log.info("MyServletContextListener监听到初始化项目完成");
    }

    @Override
    public void contextDestroyed(ServletContextEvent sce) {
        log.info("MyServletContextListener监听到项目摧毁");
    }
}

SpringBoot2 核心知识点_第97张图片

5.8.2 使用 RegistrationBean

If convention-based mapping is not flexible enough, you can use the ServletRegistrationBean, FilterRegistrationBean, and ServletListenerRegistrationBean classes for complete control.

@Configuration(proxyBeanMethods = true) // 保证依赖的组件始终是单实例的
public class MyRegistConfig {

    @Bean
    public ServletRegistrationBean myServlet() {
        MyServlet myServlet = new MyServlet();
        // 传入参数,1.自定刚才创建好的 MyServlet 类,2.访问的路径
        return new ServletRegistrationBean(myServlet, "/myservlet", "/myservlet1");
    }

    @Bean
    public FilterRegistrationBean myFilter() {
        MyFilter myFilter = new MyFilter();
        // 第一个参数是自定义的 MyFilter类,第二个参数是组件中的 myServlet,表示拦截的是 myServlet组件的访问路径
        // return new FilterRegistrationBean(myFilter,myServlet());
        // 拦截指定的路径
        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(myFilter);
        filterRegistrationBean.setUrlPatterns(Arrays.asList("/myservlet", "/css/*"));
        return filterRegistrationBean;

    }

    @Bean
    public ServletListenerRegistrationBean myServletListenerRegistration() {
        MyServletContextListener myServletContextListener = new MyServletContextListener();
        return new ServletListenerRegistrationBean(myServletContextListener);
    }

}

5.9 嵌入式 Servlet 容器

5.9.1 切换嵌入式 Servlet 容器

Under the hood, Spring Boot uses a different type of ApplicationContext for embedded servlet container support. The ServletWebServerApplicationContext is a special type of WebApplicationContext that bootstraps itself by searching for a single ServletWebServerFactory bean. Usually a TomcatServletWebServerFactory, JettyServletWebServerFactory, or UndertowServletWebServerFactory has been auto-configured.

Spring Boot 启动期间用了一个特殊的 IOC 容器(ServletWebServerApplicationContext),如果 Spring Boot 发现当前是一个 web 容器的话,IOC 容器就会变成ServletWebServerApplicationContext,这个容器在项目启动的时候会搜索 ServletWebServerFactory(Servlet 的web 服务器工厂),

  • 当Spring Boot 应用启动发现当前是 Web 应用,web 场景包-导入 tomcat

  • web 应用会创建一个 web 版的 IOC 容器(ServletWebServerApplicationContext)

  • Spring Boot 底层有很多的 Web 服务器工厂 TomcatServletWebServerFactory, JettyServletWebServerFactory, or UndertowServletWebServerFactory

  • 底层会有一个自动配置类 ServletWebServerFactoryAutoConfiguration

  • ServletWebServerFactoryAutoConfiguration 导入了 ServletWebServerFactoryConfiguration(工厂的配置类)

  • ServletWebServerFactoryConfiguration 根据动态判断系统中到底导入了那个 Web 服务器的包,(默认 web-starter 导入 tomcat 的包),进去会看到给容器中放了 TomcatServletWebServerFactory, JettyServletWebServerFactory, or UndertowServletWebServerFactory

  • TomcatServletWebServerFactory 最终创建出来 Tomcat 服务器,并启动

    TomcatWebServer 的构造器有 初始化方法

public TomcatWebServer(Tomcat tomcat, boolean autoStart, Shutdown shutdown) {
	Assert.notNull(tomcat, "Tomcat Server must not be null");
	this.tomcat = tomcat;
	this.autoStart = autoStart;
	this.gracefulShutdown = (shutdown == Shutdown.GRACEFUL) ? new GracefulShutdown(tomcat) : null;
	initialize();
}

initialize() 方法中调用启动服务器
    this.tomcat.start();
  • 内嵌服务器,就是手动把启动服务器的代码调用(tomcat 的 jar 包存在)

默认支持的 WebServer

  • TomcatJettyUnderTow

  • ServletWebServerApplicationContext 容器启动寻找 ServletWebServerFactory 并引导创建服务器

  • 切换服务器

image-20210930095802099

<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-webartifactId>
    <exclusions>
        <exclusion>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-tomcatartifactId>
        exclusion>
    exclusions>
dependency>
<dependency>
     
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-undertowartifactId>
dependency>

SpringBoot2 核心知识点_第98张图片

5.9.2 定制 Servlet 容器
@EnableConfigurationProperties(ServerProperties.class)
public class ServletWebServerFactoryAutoConfiguration {
    
  
    
@ConfigurationProperties(prefix = "server", ignoreUnknownFields = true)
public class ServerProperties {
  • 实现 WebServletFactoryCustomizer< ConfigurableServletWebServerFactory >
    • 把配置文件的值和 ServletWebServerFactory 进行绑定
    • xxxxCustomizer:定制器,可以改变 xxxx 的默认规则
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory;
import org.springframework.stereotype.Component;

@Component
public class MyWebServerFactoryCustomizer implements WebServerFactoryCustomizer<ConfigurableServletWebServerFactory> {

    @Override
    public void customize(ConfigurableServletWebServerFactory server) {
        server.setPort(9000);
    }

}
  • 修改配置文件 server.xxx

SpringBoot2 核心知识点_第99张图片

  • 直接自定义 ConfigurableServletWebServerFactory
@Bean
public ConfigurableServletWebServerFactory getConfigurableServletWebServerFactory () throws UnknownHostException {
    TomcatServletWebServerFactory tomcatServletWebServerFactory = new TomcatServletWebServerFactory();
    tomcatServletWebServerFactory.setPort(8088);
    InetAddress address = InetAddress.getByName("127.0.0.1");
    tomcatServletWebServerFactory.setAddress(address);
    return tomcatServletWebServerFactory;
}

5.10 定制化原理

5.10.1 定制化的常见方式
  1. 修改配置文件
  2. xxxxCustomizer;
  3. 编写自定义的配置类 xxxConfiguation; + @Bean 替换、增加容器中默认组件;视图解析器
  4. Web 应用编写一个配置类实现 WebMvcConfigurer 即可定制化 Web 功能; + @Bean 给容器中再扩展一些组件
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
  1. @EnableWebMvc + WebMvcConfigurer ---- @Bean 可以全面接管 SpringMVC,所有规则全部自己重新配置;实现定制和扩展功能

    原理:

    1. WebMvcAutoConfiguration 默认的 SpringMVC的自动配置功能类。静态资源、欢迎页…

    2. 一旦使用 @EnableWebMvc,会 @Import(DelegatingWebMvcConfiguration.class)

    3. DelegatingWebMvcConfigurer 的作用,只保证 SpringMVC 最基本的使用

      • 把所有系统中的 WebMvcCofigurer 拿过来,所有功能的定制都是这些 WebMvcConfigurer 合起来一起生效
      • 自动配置了一些非常底层的组件。RequestMappingHandlerMapping、这些组件依赖的组件都是从容器中获取
      • public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport
    4. WebMvcAutoConfiguration 里面的配置要能生效必须

      @ConditionalOnMissingBean (WebMvcConfigurationSupport.class)

    5. @EnableWebMvc 导致了 WebMvcAutoConfiguration 没有生效

5.10.2 原理分析

引入场景 starter – xxxxAutoConfiguration – 导入xxx组件 – 绑定xxxProperties — 绑定配置文件项

在我们使用过程中,第一步引入场景starter,然后绑定配置文件就可以使用了,中间的部分 Spring Boot 帮助我们处理了。

6. 数据访问

6.1 SQL

6.1.1 数据源的自动配置
  1. 导入 JDBC 的场景
<dependency>    
	<groupId>org.springframework.bootgroupId>    
	<artifactId>spring-boot-starter-jdbcartifactId>
dependency>

SpringBoot2 核心知识点_第100张图片

从上面导入的内容我们可以看到,少了一个重要的内容,就是数据的驱动

因为它也不知道我们要使用什么数据库(MySQL,SQLServer,还是 Orcalc)

引入 mysql 驱动依赖,不需要写 version,因为 Spring Boot 已经对驱动的版本进行了仲裁


<dependency>
    <groupId>mysqlgroupId>
    <artifactId>mysql-connector-javaartifactId>
dependency>

官方(默认)版本:8.0.26 ,需要注意我们自己的数据库版本要和默认的版本保持对应

方法1:依赖的时候引入具体的版本(maven 的就近依赖原则)

<dependency>
    <groupId>mysqlgroupId>
    <artifactId>mysql-connector-javaartifactId>
    <version>5.1.49version>
dependency>

方法2:官方里面定义版本在 properties

<properties>
    <mysql.version>5.1.49mysql.version>
properties>
6.1.2 分析自动配置
  1. 自动配置的类

    DataSourceAutoConfiguration 数据源的自动配置的类

    • 要想数据源的消息,只需要在配置文件中修改 spring.datasource 为前缀的东西

    • 数据库连接池的配置,是自己容器中没有 DataSource 才自动配置的

    • 底层配置好的连接池是:HikariDataSource

    @Configuration(proxyBeanMethods = false)
     @Conditional(PooledDataSourceCondition.class)
     @ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
     @Import({ DataSourceConfiguration.Hikari.class, DataSourceConfiguration.Tomcat.class,
     	DataSourceConfiguration.Dbcp2.class, DataSourceConfiguration.OracleUcp.class,
     	DataSourceConfiguration.Generic.class, DataSourceJmxConfiguration.class })
     protected static class PooledDataSourceConfiguration {
    

    DataSourceTransactionManagerAutoConfiguration 事务管理器的自动配置

    JdbcTemplateConfiguration JdbcTemplate 的自动配置,可以操作数据库

    • @ConfigurationProperties(prefix = "spring.jdbc") 可以修改这个配置项来修改 JdbcTemplates

    等等…

SpringBoot2 核心知识点_第101张图片

修改配置项

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/studentgrade?userUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
    username: root
    password: root
    driver-class-name: com.mysql.jdbc.Driver
  • 测试连接的数据库
// 使用 Spirng Boot 给我们注册好的 JdbcTemplate m
@Autowired
JdbcTemplate jdbcTemplate;
@Test
void contextLoads() {
    Long count = jdbcTemplate.queryForObject("select count(*) from student", Long.class);
    System.out.println(count);
}
6.1.3 Druid 数据源

平常的开发中 Druid 数据源也是非常受欢迎的,由于它有对数据源的整套的解决方案,数据源的全访问监控(防止 SQL 的注入等…)

druid 官方 github 地址 https://github.com/alibaba/druid

整合第三方技术的两种方式:

  • 自定义
  • 找 starter
6.1.4 自定义方式
  1. 引入依赖
<dependency>
    <groupId>com.alibabagroupId>
    <artifactId>druidartifactId>
    <version>1.1.17version>
dependency>
  1. 配置数据源

配置文件中可以配置的属性信息

spring:
  datasource:
    username: root
    password: root
    url: jdbc:mysql://localhost:3306/studentgrade?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
    driver-class-name: com.mysql.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource

    #SpringBoot默认是不注入这些的,需要自己绑定
    #druid数据源专有配置
    initialSize: 5
    minIdle: 5
    maxActive: 20
    maxWait: 60000
    timeBetweenEvictionRunsMillis: 60000
    minEvictableIdleTimeMillis: 300000
    validationQuery: SELECT 1 FROM DUAL
    testWhileIdle: true
    testOnBorrow: false
    testOnReturn: false
    poolPreparedStatements: true

    #配置监控统计拦截的filters,stat:监控统计、log4j:日志记录、wall:防御sql注入
    #如果允许报错,java.lang.ClassNotFoundException: org.apache.Log4j.Properity
    #则导入log4j 依赖就行
    filters: stat,wall,log4j
    maxPoolPreparedStatementPerConnectionSize: 20
    useGlobalDataSourceStat: true
    connectionoProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
@Configuration
public class MyDataSourceConfig {

    // 默认的自动配置是判断容器中没有才会配置@ConditionalOnMissingBean(DataSource.class)
    @Bean
    @ConfigurationProperties("spring.datasource")// 绑定 application.yml中配置数据源的信息
    public DataSource dataSource() throws SQLException {

        DruidDataSource druidDataSource = new DruidDataSource();
        // 开启内置监控功能
        // 这里的 set 都可以写在 yml 配置文件中
        druidDataSource.setFilters("stat");
        return druidDataSource;

    }
}
  1. 测试运行就可以正常的使用 Druid 的连接池了

Druid 的内置监控功能

在自定义的数据源配置类MyDataSourceConfig 中注册监控的组件

    /**
     * 配置 druid 的监控页功能,这里配置好之后需要在 DataSource 组件中开启内置监控功能,上面代码中有(druidDataSource.setFilters("stat");)
     * @return
     */
    @Bean
    public ServletRegistrationBean statViewServlet() {

        StatViewServlet statViewServlet = new StatViewServlet();

        ServletRegistrationBean<StatViewServlet> registrationBean = new ServletRegistrationBean<StatViewServlet>(statViewServlet,"/druid/*");
        // 设置查看的时候的用户名和密码
        registrationBean.addInitParameter("loginUsername","admin");
registrationBean.addInitParameter("loginPassword","111111");
        return registrationBean;
    }

然后启动程序后通过浏览器访问 localhost:8080/druid 就可以跳转到监控页面了

SpringBoot2 核心知识点_第102张图片

SpringBoot2 核心知识点_第103张图片

登录之后在 Session 监控中可以看到信息

SpringBoot2 核心知识点_第104张图片

开启 Web 应用

/**
 * WebStatFilter 用于采集 web-jdbc 关联监控的数据
 * @return
 */
@Bean
public FilterRegistrationBean WebStatFilter() {
    WebStatFilter webStatFilter = new WebStatFilter();
    FilterRegistrationBean<WebStatFilter> registrationBean = new FilterRegistrationBean<WebStatFilter>(webStatFilter);
    registrationBean.setUrlPatterns(Arrays.asList("/*"));
    // 排除掉一些静态
    registrationBean.addInitParameter("exclusions","*.js,*.jpg,*.gif,*.css,*.png,*.ico,/druid/*");
    return registrationBean;
}

SpringBoot2 核心知识点_第105张图片

SQL防火墙

// 开启防火墙只需要在 DataSource 中配置就可以了druidDataSource.setFilters("stat,wall");

SpringBoot2 核心知识点_第106张图片

6.1.5 官方 starter 方式

使用官方的场景启动器,上面的那些配置就不需要了

  1. 引入启动器
<dependency>
   <groupId>com.alibabagroupId>
   <artifactId>druid-spring-boot-starterartifactId>
   <version>1.1.17version>
dependency>
  1. 自动配置
@Configuration
@ConditionalOnClass(DruidDataSource.class)
// 如果 Spring 官方的数据源在前,则下面的 DataSource 就会不生效了
@AutoConfigureBefore(DataSourceAutoConfiguration.class)
// 绑定的配置文件  在 `spring.datasource.druid` 下配置
@EnableConfigurationProperties({DruidStatProperties.class, DataSourceProperties.class})
@Import({
    DruidSpringAopConfiguration.class,// 监控 Spring Bean,在`spring.datasource.druid.aop-patterns`下配置
    DruidStatViewServletConfiguration.class,// 开启监控页的功能,在`spring.datasource.druid.stat-view-servlet.enabled`下配置,默认是开启的
    DruidWebStatFilterConfiguration.class,// web 监控配置,默认开启的,在`spring.datasource.druid.web-stat-filter`下开启
    DruidFilterConfiguration.class// 所有 Druid 自己 filter 的配置,这个会给容器中放入很多的组件,想要开启什么功能,这个里面都有配置的
        })
public class DruidDataSourceAutoConfigure {

    private static final Logger LOGGER = LoggerFactory.getLogger(DruidDataSourceAutoConfigure.class);

    @Bean(initMethod = "init")
    @ConditionalOnMissingBean
    public DataSource dataSource() {
        LOGGER.info("Init DruidDataSource");
        return new DruidDataSourceWrapper();
    }
}

  • DruidFilterConfiguration.class 类
private static final String FILTER_STAT_PREFIX = "spring.datasource.druid.filter.stat";
private static final String FILTER_CONFIG_PREFIX = "spring.datasource.druid.filter.config";
private static final String FILTER_ENCODING_PREFIX = "spring.datasource.druid.filter.encoding";
private static final String FILTER_SLF4J_PREFIX = "spring.datasource.druid.filter.slf4j";
private static final String FILTER_LOG4J_PREFIX = "spring.datasource.druid.filter.log4j";
private static final String FILTER_LOG4J2_PREFIX = "spring.datasource.druid.filter.log4j2";
private static final String FILTER_COMMONS_LOG_PREFIX = "spring.datasource.druid.filter.commons-log";
private static final String FILTER_WALL_PREFIX = "spring.datasource.druid.filter.wall";
private static final String FILTER_WALL_CONFIG_PREFIX = FILTER_WALL_PREFIX + ".config";

yml 配置文件,只是部分,详细查看druid 的官方文档,github地址上面有

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/studentgrade?userUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
    username: root
    password: root
    driver-class-name: com.mysql.jdbc.Driver
    druid:
      # 监控 Spring 这个包下的所有组件
      aop-patterns: com.thymeleaf.*
      filters: stat,wall,slf4j	#底层开启功能,stat(sql监控),wall(防火墙),slf4j(日志记录)
      stat-view-servlet:	# 配置监控页功能
        enabled: true
        login-username: admin
        login-password: 111111
        # 禁用掉重置
        reset-enable: false
      web-stat-filter:
        # 开启监控web 应用
        enabled: true
        url-pattern: /*
        exclusions: '*.js,*.jpg,*.gif,*.css,*.png,*.ico,/druid/*'
      filter:
        stat:	# 对上面 filters 里面的 stat 的详细配置
          # 慢查询时间
          slow-sql-millis: 1000
          log-slow-sql: true
          enabled: true
        wall:
          enabled: true
6.1.6 Mybatis

Mybatis 是第三方,所以 starter 是 mybatis-spring-boot-starter

github地址:https://github.com/mybatis

starter

<dependency>
    <groupId>org.mybatis.spring.bootgroupId>
    <artifactId>mybatis-spring-boot-starterartifactId>
    <version>2.2.0version>
dependency>

SpringBoot2 核心知识点_第107张图片

6.1.6.1 配置模式

之前使用 mybatis 的时候,需要有一个全局配置文件,创建一个 SqlSessionFactory,然后通过SqlSession 找到 Mapper 接口来操作数据库,所有的东西都需要手动进行编写。

  • MybatisAutoConfiguration 类
// 当引入了 mybatis 的 jar 包就不会生效了
@ConditionalOnClass({ SqlSessionFactory.class, SqlSessionFactoryBean.class })
// 当整个容器中只有一个候选的数据源生效
@ConditionalOnSingleCandidate(DataSource.class)
// 绑定配置文件
@EnableConfigurationProperties(MybatisProperties.class)
@AutoConfigureAfter({ DataSourceAutoConfiguration.class, MybatisLanguageDriverAutoConfiguration.class })
public class MybatisAutoConfiguration implements InitializingBean {}
    

@ConfigurationProperties(prefix = "mybatis")
public class MybatisProperties {
    public static final String MYBATIS_PREFIX = "mybatis";

可以在配置文件中修改 mybatis 开始的所有项来对mybatis 进行配置。

// 自动配置好了 SqlSessionFactory
@Bean
@ConditionalOnMissingBean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
  SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
  factory.setDataSource(dataSource);
  factory.setVfs(SpringBootVFS.class);
  if (StringUtils.hasText(this.properties.getConfigLocation())) {
    factory.setConfigLocation(this.resourceLoader.getResource(this.properties.getConfigLocation()));
      
      
// SqlSessionTemplate 里面组合了 SqlSession
@Bean
@ConditionalOnMissingBean
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
  ExecutorType executorType = this.properties.getExecutorType();
  if (executorType != null) {
    return new SqlSessionTemplate(sqlSessionFactory, executorType);
  } else {
    return new SqlSessionTemplate(sqlSessionFactory);
  }
    
    
    
// AutoConfiguredMapperScannerRegistrar 扫描配置文件都在那个位置,接口位置
  @Import(AutoConfiguredMapperScannerRegistrar.class)
  @ConditionalOnMissingBean({ MapperFactoryBean.class, MapperScannerConfigurer.class })
  public static class MapperScannerRegistrarNotFoundConfiguration implements InitializingBean {
public static class AutoConfiguredMapperScannerRegistrar implements BeanFactoryAware, ImportBeanDefinitionRegistrar 
  private BeanFactory beanFactory;
  @Override
// AnnotationMetadata 拿到注解
  public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
    if (!AutoConfigurationPackages.has(this.beanFactory)) {
      logger.debug("Could not determine auto-configuration package, automatic mapper scanning disabled.");
      return;
    }
      // 找到所有标注了 @Mapper 注解的接口,只要我们写的操作Mybatis 的接口标注了 @Mapper 注解就会被自动扫描进来
    logger.debug("Searching for mappers annotated with @Mapper");
    List<String> packages = AutoConfigurationPackages.get(this.beanFactory);
    if (logger.isDebugEnabled()) {
      packages.forEach(pkg -> logger.debug("Using auto-configuration base package '{}'", pkg));
    }
    BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class);

使用

  1. 在根路径下创建 mybatis 文件夹用于保存 mybatis 的配置文件,位置可以随意

SpringBoot2 核心知识点_第108张图片




DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>

    <settings>

        <setting name="mapUnderscoreToCamelCase	" value="true"/>
    settings>
configuration>



DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>

    <settings>

        <setting name="mapUnderscoreToCamelCase	" value="true"/>
    settings>
configuration>
  1. 在 yml 文件中指定创建的 mybatis 配置文件的位置
mybatis:
  config-location: classpath:mybatis/mybatis-config.xml
  mapper-locations: classpath:mybatis/mapper/*.xml
  1. 运行启动测试就可以了

    这里省略了 实体类、服务类、控制层、方法接口(名字必须与 xxxxMapper.xml名字一样)的代码,这些写法与之前使用 Mybatis 方法一样。

mybatis:
  # config-location: classpath:mybatis/mybatis-config.xml
  mapper-locations: classpath:mybatis/mapper/*.xml
  configuration:	# 指定 mybatis 全局配置文件中的相关配置项,注意两个不能同时使用,要么使用 yml,要么创建xml文件yml指定位置
    map-underscore-to-camel-case: true	# 也可以在 yml 配置文件中设置属性

总结:

  1. 导入 mybatis 官方starter
  2. 编写 mapper 接口
  3. 编写 sql 映射文件并绑定 mapper 接口
  4. 在 application.yml 中指定 Mapper 配置文件的位置,以及指定全局配置文件的位置(建议不适用全局文件,直接使用yml 中的 mybatis 标签下写配置信息)
6.1.6.2 注解模式
  • 可以在创建 Spring Boot 项目的时候指定 Mybatis 框架

SpringBoot2 核心知识点_第109张图片

使用注解方式与之前使用 MyBatis 一样,不需要写 mapper 的映射文件,只需要在接口上使用注解即可

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

混合方式就是可以使用注解也可以使用接口映射文件来进行数据库的存储访问,简单的 SQL 语句可以使用注解方式操作;如果 SQL 语句比较麻烦,就可以使用接口映射文件xml 的方式进行操作。

有的的复杂的语句也可以使用 @Options 注解来完成

@Insert("insert into city (name,state,country) values(#{name},#{state},#{country})")
@Options(useGeneratedKeys = true,keyProperty = "id")	// 设置自增的主键
Integer insertCity(City city);
6.1.7 MyBatis-Plus
6.1.7.1 简介

MyBatis-Plus(简称 MP)是一个 MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,为了简化开发、提高效率而生

官方:https://mp.baomidou.com/

6.1.7.2 简单的查询操作
  1. 引入依赖

    引入 Mybatis-Plus 的依赖后,之前的 jdbc 和 mybatis 的依赖都可以去掉,这个全部进行了封装

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.3.1.tmp</version>
</dependency>

SpringBoot2 核心知识点_第110张图片

自动配置了什么

@Configuration
@ConditionalOnClass({SqlSessionFactory.class, SqlSessionFactoryBean.class})
@ConditionalOnSingleCandidate(DataSource.class)	// 底层用的是我们的默认的数据源
@EnableConfigurationProperties({MybatisPlusProperties.class})
@AutoConfigureAfter({DataSourceAutoConfiguration.class, MybatisPlusLanguageDriverAutoConfiguration.class})
public class MybatisPlusAutoConfiguration implements InitializingBean {
    
     
@ConfigurationProperties(
    prefix = "mybatis-plus" // 配置项绑定,这块就是对 MyBatis-Plus 的绑定
)
public class MybatisPlusProperties { // 配置类
    private String[] mapperLocations = new String[]{"classpath*:/mapper/**/*.xml"};  // mapperLocations 自动配置好了,有默认值   classpath*:/mapper/**/*.xml 任意包的类路径下的所有 mapper 文件夹下任意路径下的所有 xml都是 SQL 映射文件
    

// SqlSessionFactory 核心组件配置好了
@Bean
@ConditionalOnMissingBean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
    
@Mapper 标注的接口也会被自动扫描
// 容器中也自动配置好了 SqlSessionTemplate
@Bean
@ConditionalOnMissingBean
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
  • 建议在主启动类加上扫描包注解 @MapperScan("com/thymeleaf/mapper") ,这样接口上不用再一个一个加 @Mapper 注解
  1. 编写数据库表对应的实体类,编写 UserMapper 接口
/**
 *  只需要我们的 mapper 继承 BaseMapper 就可以进行 CRUD
 */
public interface UserMapper extends BaseMapper<User> {
}

继承的 BaseMapper 类中封装大量的操作方法

SpringBoot2 核心知识点_第111张图片

  1. 在测试类中直接调用方法就可以了
@Test
void testUserMapper() {
    User user = userMapper.selectById(1);
    System.out.println(user);
}
6.1.7.3 分页查找

以 User 类为例

  1. 首先定义一个 UserMapper 接口
/**
 *  只需要我们的 mapper 继承 BaseMapper 就可以进行 CRUD
 */
public interface UserMapper extends BaseMapper<User> {
}
  1. 写 UserService 接口
/**
 * extends IService  继承 MyBatis-Plus 中的接口,IService 是所有 Service 的接口
 */
public interface UserService extends IService<User> {

}

ef
3. 写 UserServiceImpl (UserService 的实现类),这两个有一定的联系,为了简化,接口继承了 IService 接口,实现类继承了 ServiceImpl 类

/**
 * ServiceImpl
 *     UserMapper 表示的是要操作那个 Mapper
 *     User 返回数据的类型
 */
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper,User> implements com.thymeleaf.service.UserService  {

}
  1. Controller 调用接口进行数据的查询
@Autowired
UserService userService;
@GetMapping("/table-datatable")
public String dataTable(@RequestParam(value = "page", defaultValue = "1") Integer page, Model model) {
      List<User> users = userService.list();
      model.addAttribute("usersAll",users);
    // 分页查询数据
    Page<User> userPage = new Page<>(page, 2);
    // 分页查询结果
    Page<User> page1 = userService.page(userPage);
    // 获得当前页的数据
    model.addAttribute("page", page1);
    return "table/tables-datatable";
}
  1. 页面拿到分页信息内容进行展示
<div class="card-body">
	<div class="table-responsive">
	<table id="example1" class="table table-bordered table-hover display">
		<thead>
			<tr>
				<th>#th>
				<th>idth>
				<th>nameth>
				<th>ageth>
				<th>emailth>
				<th>操作th>
			tr>
		thead>										
		<tbody>
			<tr th:each="user,stat : ${page.records}">
				<td th:text="${stat.count}">td>
				<td th:text="${user.id}">Tiger Nixontd>
				<td th:text="${user.name}">System Architecttd>
				<td th:text="${user.age}">Edinburghtd>
				<td th:text="${user.email}">61td>
				<td><a th:href="@{/deleteUser/{id}(id=${user.id},page=${page.current})}">删除a>td>
			tr>
		tbody>
	table>
	div>
	<div class="row-fluid">
		<div class="span6">
			<div class="dataTables_info" id="dynamic-table_info">
				当前第 [[${page.current}]] 页  总计 [[${page.pages}]] 页  共 [[${page.total}]] 条记录
			div>
		div>
		<div class="span6">
			<div class="dataTables_paginate paging_bootstrap pagination">
				<ul>
					<li class="prev disabled"><a href="#">⬅ Previousa>li>
					<li th:class="${num == page.current?'active':''}" th:each="num : ${#numbers.sequence(1,page.pages)}">
						<a th:href="@{/table-datatable(page=${num})}">[[${num}]]a>li>
					<li class="next"><a href="#">Next ➡a>li>
				ul>
			div>
		div>
	div>
div>																												
  1. 运行展示

SpringBoot2 核心知识点_第112张图片

6.1.7.4 删除操作

其他定义的接口与上面一样

  1. Controller
/**
*	RedirectAttributes 带数据进行重定向
**/
@GetMapping("/deleteUser/{id}")
public String deleteUser(@PathVariable("id") Integer id,
                         @RequestParam(value = "page",defaultValue = "1") Integer page,
                         RedirectAttributes redirectAttributes) {
    userService.removeById(id);
    redirectAttributes.addAttribute("page",page);
    return "redirect:/table-datatable";
}
  1. HTML 页面的内容

<td><a th:href="@{/deleteUser/{id}(id=${user.id},page=${page.current})}">删除a>td>

6.2 NoSQL(Redis)

Redis 是一个开源(BSD许可)的,内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件。它支持多种类型的数据结构,如字符串(strings),散列(hashes),列表(lists),集合(sets),有序集合(sorted sets) 与范围查询,bitmaps,hyperloglogs 和 地理空间(geospatial) 索引半径查询。Redis 内置了复制(replication),LUA 脚本(Lua scripting),事务(transactions) 和不同级别的磁盘持久化(persistence),并通过 Redis哨兵(Sentinel) 和分区(Cluster) 提供高可用性(high availablity)。

6.2.1 Redis 自动配置

使用的第一步引入依赖

<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-data-redisartifactId>
dependency> 

SpringBoot2 核心知识点_第113张图片

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class)// 绑定配置文件
// 导入了 Lettuce 的客户端的连接配置,同时支持两个客户端操作 Redis
@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
public class RedisAutoConfiguration {// 自动配置类
    
    
// 属性类  spring.redis 下面的所有配置是对 Redis 的配置
@ConfigurationProperties(prefix = "spring.redis")
public class RedisProperties {
    
// 自动注入了 RedisTemplate,是以 K v 键值对的方式进行存储的,k 是Object ,v 是Object 
@Bean
@ConditionalOnMissingBean(name = "redisTemplate")
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
   
// 还自动注入了一个 StringRedisTemplate ,这个认为 key 和 value 都是 String 的
@Bean
@ConditionalOnMissingBean
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
    
在底层使用 RedisTemplateStringRedisTemplate 就可以操作 Redis l
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RedisClient.class)
// 当spring.redis.client-type 客户端类型是 lettuce 的时候
@ConditionalOnProperty(name = "spring.redis.client-type", havingValue = "lettuce", matchIfMissing = true)
class LettuceConnectionConfiguration extends RedisConnectionConfiguration {
    // 给容器中放一个客户端的资源
@Bean(destroyMethod = "shutdown")
@ConditionalOnMissingBean(ClientResources.class)
DefaultClientResources lettuceClientResources() {
    // 客户端连接工厂,之后客户端获取的连接都是从这里获取的
@Bean
@ConditionalOnMissingBean(RedisConnectionFactory.class)
LettuceConnectionFactory redisConnectionFactory(
6.2.2 Lettuce客户端操作

默认使用的就是 Lettuce 客户端

  1. 启动 Redis 的服务

    http://lss-coding.top/2021/09/19/%E6%95%B0%E6%8D%AE%E5%BA%93/Redis/Redis%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86%E7%82%B9%E6%80%BB%E7%BB%93/

  2. 在配置文件中配置 Redis 的访问地址和端口号

spring:
	redis:
    	host: 192.168.43.219
    	port: 6379
  1. 在测试类中输入 StringRedisTemplate 进行操作
@Autowired
StringRedisTemplate stringRedisTemplate;

@Test
void testRedis() {
    ValueOperations<String, String> stringStringValueOperations = stringRedisTemplate.opsForValue();
    stringStringValueOperations.set("k2","v2");
    String k1 = stringStringValueOperations.get("k2");
    System.out.println(k1);
}
6.2.3 jedis 客户端操作
  1. 导入 jedis 的依赖,Spring Boot 底层会对版本进行仲裁

<dependency>
    <groupId>redis.clientsgroupId>
    <artifactId>jedisartifactId>
dependency>
  1. 在 yml 配置文件中进行配置即可
spring:
	redis:
    	host: 192.168.43.219
    	port: 6379
    	client-type: jedis
        jedis:
      		pool:	# 对连接池的配置
        		max-active: 10

SpringBoot2 核心知识点_第114张图片

6.2.4 访问统计案例

对每一个访问的 url 进行访问次数的统计,统计结果放到 Redis 中,存储的 key 的路径的名称,value 每一次访问 + 1

  1. 定义一个拦截器,用于对访问的路径 + 1
@Controller
public class RedisUrlCountInterceptor implements HandlerInterceptor {
    @Autowired
    StringRedisTemplate redisTemplate;
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String requestURI = request.getRequestURI();
        // 每次访问当前计数 rui+1
        redisTemplate.opsForValue().increment(requestURI);
        return true;
    }
}
  1. 在自定义的 WebMvcConfiguration 中注册
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    /**
     * Filter 、Interceptor  区别
     * Filter 是 Servlet 定义的原生组件,好处就是脱离的 Spring 也可以使用
     * Interceptor 是 Spring 定义的接口,可以使用 Spring 的自动装配的功能
     */
    @Autowired
    RedisUrlCountInterceptor redisUrlCountInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {

        registry.addInterceptor(redisUrlCountInterceptor).addPathPatterns("/**")
                .excludePathPatterns("/","/login","/css/**","/data/**","/font-awesome/**","images/**","/js/**","/lib/**","/plugins/**");

    }
}

Filter 、Interceptor 区别

Filter 是 Servlet 定义的原生组件,好处就是脱离的 Spring 也可以使用
Interceptor 是 Spring 定义的接口,可以使用 Spring 的自动装配的功能

7. 单元测试

7.1 JUnit5 的变化

Spring Boot 2.2.0 版本开始引入 JUnit 5 作为单元测试默认库

作为最新版本的 JUnit 框架,JUnit5 与之前版本的 JUnit 框架有很大的不同。由三个不同子项目的几个不同模块组成。

JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage

JUnit Platform:JUnit Platform 是在 JVM 上启动测试框架的基础,不仅支持 JUnit 自制的测试引擎,其他测试引擎也都可以接入

JUnit Jupiter:提供了 JUnit 5 的新的编程模型,是 JUnit 5 新特性的核心。内部包含了一个测试引擎,用于在 JUnit Platform 上运行

JUnit Vintage:由于 JUnit 已经发展很多年,为了照顾老的项目,JUnit Vintage 提供了兼容 JUnit4.x,JUnit3.x 的测试引擎

SpringBoot2 核心知识点_第115张图片

注意:Spring Boot 2.4 以上的版本移除了默认对 Vintage 的依赖。如果需要兼容 JUnit 4 需要自行引入

  • JUnit 5’s Vintage Engine Removed from spring-boot-starter-test

    https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-2.4-Release-Notes

    2.4 版本移除了 JUnit 4 的兼容依赖 Vintage,如果想要继续兼容 JUnit4 的话需要自定引入依赖

<dependency>
    <groupId>org.junit.vintagegroupId>
    <artifactId>junit-vintage-engineartifactId>
    <scope>testscope>
    <exclusions>
        <exclusion>
            <groupId>org.hamcrestgroupId>
            <artifactId>hamcrest-coreartifactId>
        exclusion>
    exclusions>
dependency>

SpringBoot2 核心知识点_第116张图片

  • 现在的使用方式

使用非常方便,只需要写一个测试类,在测试类上使用 @Test 注解就可以了

@SpringBootTest
class ThymeleafApplicationTests {
	// 当我们创建一个 Spring Boot 项目后会自动给我们生成一个带有 @Test 的测试方法
    @Test
    void contextLoads() {

    }
}

如果想要做单元测试只需要引入测试的启动器

<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-testartifactId>
    <scope>testscope>
dependency>
  • 以前使用

需要使用 @Spring BootTest + @RunWith(SpringTest.class) 注解实现

Spring Boot 整合 JUnit 后使用非常的方便

  • 编写测试方法,标注 @Test 注解(注意是 JUnit 5 版本的注解)
  • JUnit 类具有 Spring 的功能,@Autowired、@Transactional(标注事务的注解,事务完成后会自动回滚) 注解都可以使用

7.2 JUnit 5 常用注解

官网列举了很多的注解:https://junit.org/junit5/docs/current/user-guide/#writing-tests-annotations

  1. @Test:表示方法是测试方法。但是与 JUnit 4 的@Test 不同,它的职责非常单一不能声明任何属性,扩展的测试将会由 Jupiter 提供额外测试

  2. @ParameterizedTest:表示方法是参数化测试

  3. @RepeatedTest:表示方法可重复执行

@RepeatedTest(5)
@Test
void testRepeated() {
    System.out.println("测试Repeated");
}

SpringBoot2 核心知识点_第117张图片

  1. @ DisplayName:为测试类或者测试方法设置展示名称
@DisplayName("测试JUnit5功能测试类")
public class JUnit5Test {

    @DisplayName("测试DisplayName注解")
    @Test
    void testDisplayName() {
        System.out.println(1);
    }

}

SpringBoot2 核心知识点_第118张图片

  1. @BeforeEach:表示在单个测试之前执行
@BeforeEach
void testBeforeEach() {
    System.out.println("测试就要开始了");
}

SpringBoot2 核心知识点_第119张图片

  1. @AfterEach:表示在单个测试之后执行
@AfterEach
void testAfterEach() {
    System.out.println("测试结束了");
}

SpringBoot2 核心知识点_第120张图片

  1. @BeforeAll:表示所有单元测试之前执行

写两个 @Test 方法,点类上的运行按钮执行两个测试方法

@DisplayName("测试DisplayName注解")
@Test
void testDisplayName() {
    System.out.println(1);
}
@Test
void test() {
    System.out.println(2);
}
@BeforeAll
static void testBeforeAll() {	// 需要定义为 static 方法,因为启动的时候就会调用这个方法
    System.out.println("所有测试就要开始了");
}

SpringBoot2 核心知识点_第121张图片

  1. @AfterAll:表示在所有单元测试之后执行

与 @BeforeAll 一样:定义两个测试类,定义为static 方法

@AfterAll
static void testAfterAll() {
    System.out.println("所有测试都结束了");
}

SpringBoot2 核心知识点_第122张图片

  1. @Tag:表示单元测试类型,类似于 JUnit 4 中的 @Categories

  2. @Disabled:表示测试类或者测试方法不执行,类似于 JUnit4 中的 @Ignore

@Disabled
@DisplayName("测试方法 2")
@Test
void test() {
    System.out.println(2);
}

SpringBoot2 核心知识点_第123张图片

  1. @Timeout:表示测试方法运行结果超过了指定的时间将会返回错误
/**
 * 方法的超时时间,如果超时抛出异常 TimeoutException
 * @throws InterruptedException
 */
@Test
@Timeout(value = 5,unit = TimeUnit.MILLISECONDS)
void testTimeout() throws InterruptedException {
    Thread.sleep(600);
}

SpringBoot2 核心知识点_第124张图片

  1. @ExtendWith:为测试类或测试方法提供扩展类引用,类似于 JUnit4 的 @RunWith 注解

在我们自定义的的测试类中没办法使用容器中组件

@Autowired
RedisTemplate redisTemplate;
@DisplayName("测试DisplayName注解")
@Test
void testDisplayName() {
    System.out.println(redisTemplate);
    System.out.println(1);
}

SpringBoot2 核心知识点_第125张图片

如果想要使用 容器中的组件,需要跟 Spring Boot 创建的时候自动创建的测试类一样,在类上加 @SpringTest 注解

@SpringBootTest
@DisplayName("测试JUnit5功能测试类")
public class JUnit5Test {
@BootstrapWith(SpringBootTestContextBootstrapper.class)
@ExtendWith({SpringExtension.class})	// Spring Boot 底层注解就有@ExtendWith注解,意思就是下面的测试都是使用 Spring的整个测试驱动进行测试
public @interface SpringBootTest {

7.3 断言(assertions)

断定某一件事情一定会发生,如果没发生就会认为它出了别的情况的错误。

断言是测试方法中的核心部分,用来对测试需要满足的条件进行验证。这些断言方法都是 org.junit.jupiter.api.Assertions 的静态方法。JUnit 5 内置的断言可以分为下面的几种类别。

检查业务逻辑返回的数据是否合理所有的测试运行结束以后,会有一个详细的测试报告(报告里面就会有那些方法成功,那些方法失败,失败的原因等等)

注意:如果有两个断言,第一个执行失败了第二个则不会执行

用来对单个值进行简单的验证

方法 说明
assertEquals 判断两个对象或两个原始类型是否相等
assertNotEquals 判断两个对象或两个原始类型是否不相等
assertSame 判断两个对象引用是否指向同一个对象
assertNotSame 判断两个对象引用是否指向不同的对象
assertTrue 判断给定的布尔值是否为 true
assertFalse 判断给定的布尔值是否为 false
assertNull 判断给定的对象引用是否为 null
assertNotNull 判断给定的对象引用是否不为 null
  1. 简单断言
  • assertEquals
@DisplayName("测试简单断言")
@Test
void testSimpleAssertions() {
    int add = add(2, 3);
    // 判断相等,如果不相等给出错误信息 AssertionFailedError
    assertEquals(6,add,"算数逻辑错误");
}
public int add(int num1, int num2) {
    return num1 + num2;
}

SpringBoot2 核心知识点_第126张图片

  • assertSame
@DisplayName("测试简单断言")
@Test
void testSimpleAssertions() {
      int add = add(2, 3);
    // 判断相等,如果不相等给出错误信息 AssertionFailedError
      assertEquals(6,add,"算数逻辑错误");
    Object ob1 = new Object();
    Object ob2 = new Object();
    assertSame(ob1,ob2);
}

SpringBoot2 核心知识点_第127张图片

  1. 数组断言
  • assertArrayEquals
@DisplayName("测试简单断言")
@Test
void testSimpleAssertions() {
      assertSame(ob1,ob2,"两个对象不一样");
  	  assertArrayEquals(new int[]{2,2},new int[]{1,2},"数组内容不相等");
}

image-20211003150904672

  1. 组合断言

assertAll 方法接受多个 org.junit.jupter.api.Executable 函数式接口的实例作为要验证的断言,可以通过 lambda 表达式很容易的提供这些断言。

判断的时候当所有的断言执行成功才成功,否则失败

@DisplayName("组合断言")
@Test
void all() {
    /**
     * 断言所有成功才会往下走
     */
    assertAll("test", () -> assertTrue(true && true,"结果不为true"),
            () -> assertEquals(1, 2,"值不相等"));
}

SpringBoot2 核心知识点_第128张图片

  1. 异常断言

在 JUnit 4 的时候,想要测试方法的异常情况时,需要用 @Rule 注解的 ExpectedException 变量还是比较麻烦的。而 JUnit 5 提供了一种新的断言方式 Assertions.assertThrows(),配合函数式编程就可以进行使用

@DisplayName("异常断言")
@Test
void testException() {
    // 断定业务逻辑一定会出现异常
    assertThrows(ArithmeticException.class, () -> {
        int i = 10 / 2;
    }, "为什么正常执行了,不应该有异常吗?");
}

image-20211003152244799

  1. 超时断言

Assertions.assertTimeout();为测试方法设置了超时时间

@DisplayName("超时断言")
@Test
void testAssertTimeout() {
    // 如果测试方法超过 1 s 就会出现异常
    Assertions.assertTimeout(Duration.ofMillis(1000),()->Thread.sleep(500));
}

SpringBoot2 核心知识点_第129张图片

  1. 快速失效

通过 fail 方法直接使得测试失败

@DisplayName("快速失败")
@Test
void testFail() {
    fail("测试失败");
}

SpringBoot2 核心知识点_第130张图片

7.4 前置条件(assumptions假设)

JUnit5 中的前置条件类似于断言,不同之处在于不满足的断言会使得测试方法失败,而不满足的前置条件只会使得测试方法终止执行。前置条件可以看成是测试方法执行的前提,当该前提不满足时,就没有继续执行的必要。

assumeTrue 和 assumeFalse 确保给定的条件为 true 或 false,不满足条件会使得测试执行终止。assumingThat 的参数是表示条件的布尔值和对应的 Executable 接口的实现对象。只有条件满足时,Executable 对象才会被执行;当条件不满足,测试执行并不会终止。

/**
 * 测试前置条件
 */
@DisplayName("测试前置条件")
@Test
void testAssumptions() {
    Assumptions.assumeTrue(true, "结果不是true");
    System.out.println("执行成功");
}

SpringBoot2 核心知识点_第131张图片

SpringBoot2 核心知识点_第132张图片

7.5 嵌套测试

JUnit 5 可以通过 Java 中的内部类和 @Nested 注解实现嵌套测试,从而可以更好的把相关的测试方法组织在一起。在内部类中可以使用 @BeforeEach 和 @AfterEach 注解,而且嵌套的层次没有限制。

  • 内层的 Test 可以驱动外层的 Test
  • 外层的 Test 不能驱动内层的 Test
@DisplayName("嵌套测试")
public class TestingAStackDemo {

    Stack<Object> stack;

    @Test
    @DisplayName("is instantiated with new Stack()")
    void isInstantiatedWithNew() {
        new Stack<>();
        // 嵌套测试下,外层的 test 不能驱动内层的 BeforeEach  BeforeAll (After) 之类的方法提前或者之后运行
        assertNull(stack);
    }

    @Nested // 表示嵌套测试
    @DisplayName("when new")
    class WhenNew {

        @BeforeEach
        void createNewStack() {
            stack = new Stack<>();
        }

        @Test
        @DisplayName("is empty")
        void isEmpty() {
            assertTrue(stack.isEmpty());
        }

        @Test
        @DisplayName("throws EmptyStackException when popped")
        void throwsExceptionWhenPopped() {
            assertThrows(EmptyStackException.class, stack::pop);
        }

        @Test
        @DisplayName("throws EmptyStackException when peeked")
        void throwsExceptionWhenPeeked() {
            assertThrows(EmptyStackException.class, stack::peek);
        }

        /**
         * 内存的 test 可以驱动外层,外层的不能驱动内层的
         */
        @Nested // 表示嵌套测试
        @DisplayName("after pushing an element")
        class AfterPushing {

            String anElement = "an element";

            @BeforeEach
            void pushAnElement() {
                    stack.push(anElement);
            }

            @Test
            @DisplayName("it is no longer empty")
            void isNotEmpty() {
                assertFalse(stack.isEmpty());
            }

            @Test
            @DisplayName("returns the element when popped and is empty")
            void returnElementWhenPopped() {
                assertEquals(anElement, stack.pop());
                assertTrue(stack.isEmpty());
            }

            @Test
            @DisplayName("returns the element when peeked but remains not empty")
            void returnElementWhenPeeked() {
                assertEquals(anElement, stack.peek());
                assertFalse(stack.isEmpty());
            }
        }
    }

}

7.6 参数化测试

参数化测试是 JUnit 5 很重要的一个新特性,它使得用不同的参数多次运行测试成为了可能,也为我们的单元测试带来许多便利。

利用 @ValueSource 等注解,指定入参,我们将可以使用不同的参数进行多次单元测试,而不需要没新增一个参数就新增一个单元测试,省去了很多冗余代码。

  1. @ValueSource:为参数测试指定入参来源,支持八大基础类以及 String 类型,Class 类型
@DisplayName("参数化测试")
@ParameterizedTest  // 标注为参数化测试
@ValueSource(ints = {1,2,3,4,5})
void testParameterized(int i) {
    System.out.println(i);
}

SpringBoot2 核心知识点_第133张图片

  1. @NullSource:表示为参数化测试提供一个 null 的入参

  2. @EnumSource:表示为参数化测试提供一个枚举入参

  3. @CsvFileSource:表示读取指定 CSV文件内容作为参数化测试入参

  4. @MethodSource:表示读取方法的返回值作为参数化测试入参(注意方法返回值需要是一个流)

@DisplayName("参数化测试")
@ParameterizedTest  // 标注为参数化测试
@MethodSource("stringProvider")
void testParameterized2(String i) {
    System.out.println(i);
}

// 方法返回 Stream
static Stream<String> stringProvider() {
    return Stream.of("apple","banana");
}

image-20211003162747336

如果参数化测试仅仅只能做到指定普通的入参还不是最厉害的,最强大之处的地方在于它可以支持外部的各类入参。如:CSV,YML,JSON 文件甚至方法的返回值也可以作为入参。只需要去实现 ArgumentsProvider 接口,任何外部文件都可以作为它的入参。

8. 指标监控

8.1 Spring Boot Actuator

8.1.1 简介

在以后每一个微服务在云上部署以后,我们都需要对其进行监控、追踪、审计、控制等。Spring Boot 就抽取了 Actuator 场景,使得我们每个微服务快速引用即可获得生产级别的应用监控、审计等功能。

<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-actuatorartifactId>
dependency>

SpringBoot2 核心知识点_第134张图片

8.1.2 1.x 与 2.x 不同
  1. 1.x
    • 支持 SpringMVC
    • 基于继承方式进行扩展
    • 层级 Metrics 配置
    • 自定义 Metrics 收集
    • 默认较少的安全策略
  2. 2.x
    • 支持 SpringMVC、JAX-RS 以及 WebFlus
    • 注解驱动进行扩展
    • 层级 & 名称空间 Metrics
    • 底层使用 MicroMeter,强大、便捷
    • 默认丰富的安全策略
8.1.3 使用

官网:https://docs.spring.io/spring-boot/docs/current/reference/html/actuator.html#actuator

  1. 引入场景启动器,上面有
  2. 启动项目访问路径:http://localhost:8080/actuator/**

SpringBoot2 核心知识点_第135张图片

SpringBoot2 核心知识点_第136张图片

因为代码中 Redis 连接是失败的,所以访问 health 端口显示状态是 DOWN 掉的

访问路径 locahost:8080/actuator/health 中 actuator 后面的称为 Endpoints

官网上的 Endpoints 都是可以监控的指标,可以看到有很多的监控端点,但是这些端点默认不是全部开启的,除了 shutdown 这个端点外

SpringBoot2 核心知识点_第137张图片

  1. 配置文件中配置所有 Endpoints 生效
# management  是所有 actuator 的配置
management:
  endpoints:
    enabled-by-default: true  # 默认开启所有的监控端点
    web:
      exposure:
        include: '*'  # 以 web 方式暴露所有端点
  • 例如访问:localhost:8080/actuator/conditions,得到所有配置生效的组件,/beans 可以得到所有 组件

SpringBoot2 核心知识点_第138张图片

SpringBoot2 核心知识点_第139张图片

8.2 Actuator Endpoint

8.2.1 常用端点

最常用的 Endpoint

  • Health:健康状况,当前应用是否健康
  • Metrics:运行时指标
  • Loggers:日志记录,方便追踪错误

官网全部有罗列

https://docs.spring.io/spring-boot/docs/current/reference/html/actuator.html#actuator.endpoints

SpringBoot2 核心知识点_第140张图片

SpringBoot2 核心知识点_第141张图片

SpringBoot2 核心知识点_第142张图片

8.2.2 Health Endpoint

健康检查端点,一般用于云平台,平台会定时检查应用的健康状况,我们就需要 Health Endpoint 可以为平台返回当前应用的一系列组件健康状况的集合。

中的几点:

  • health endpoint 返回的结果,应该是一系列健康检查后的一个汇总报告
  • 很多的健康检查默认已经自动配置好了,比如:数据库、redis 等
  • 可以很容易的添加自定义的健康检查机制

判断健康与否,需要取决于所有组件都是健康的才算健康,否则就是不健康。不健康就会提示 DOWN,健康提示 UP

management:
  endpoints:
    enabled-by-default: true  # 默认开启所有的监控端点
    web:
      exposure:
        include: '*'  # 以 web 方式暴露所有端点

  endpoint:
    health:
      show-details: always

image-20211003192540716

8.2.3 Metrics Endpoint

提供详细的、层级的、空间指标信息,这些信息可以被 pull(主动推送)或者 push(被动获取)方式得到:

  • 通过 Metrics 对接多种监控系统
  • 简化核心 Metrics 开发
  • 添加自定义 Metrics 或者扩展已有 Metrics

image-20211003192647025

8.2.4 暴露 Endpoints

支持的暴露方式

  1. JMX:默认暴露所有的 Endpoint

SpringBoot2 核心知识点_第143张图片

SpringBoot2 核心知识点_第144张图片

SpringBoot2 核心知识点_第145张图片

  1. HTTP:默认只暴漏 health 和 info 的 Endpoints

如果在使用过程中开启所有指标的访问是非常危险的,所以有时候可以自定义开启某一个需要的指标

management:
  endpoints:
    enabled-by-default: false # 关闭所有监控端点 为 true 表示开启所有监控端点
    web:
      exposure:
        include: '*'  # 以 web 方式暴露所有端点
# 将某一个指标的 enabled 设置为 true
  endpoint:
    health:
      show-details: always
      enabled: true
    info:
      enabled: true
    beans:
      enabled: true
    metrics:
      enabled: true

8.3 定制 EndPoint

8.3.1 定制 Health 信息
@Component
public class MyHealthIndicator  extends AbstractHealthIndicator {

    // 真实的检查方法
    @Override
    protected void doHealthCheck(Health.Builder builder) throws Exception {
        // 加入判断 MySQL 连接,在这里获取连接信息然后进行判断

        // 保存在一些判断过程中信息
        Map<String,Object> map = new HashMap<>();
        if (1 == 1) {
            // 健康
//            builder.up();
            builder.status(Status.UP);
            map.put("msg","判断了10次");
        }else{
            // 不健康
//            builder.down();
            builder.status(Status.DOWN);
            map.put("msg","判断了 0次");
        }

        builder.withDetail("code",100)  // 返回的状态信息
                .withDetails(map);  // 返回携带的信息
    }
}

image-20211003194400992

8.3.2 定制 Info 信息
  1. 编写配置文件
info:
  appName: thymeleaf
  version: 1.0
  mavenProjectVersion: @project.version@   # 得到 pom 文件中maven 的版本信息

SpringBoot2 核心知识点_第146张图片

  1. InfoContributor
@Component
public class AppInfo implements InfoContributor{
    @Override
    public void contribute(Info.Builder builder) {
        builder.withDetail("msg","你好")
                .withDetail("hello","world");
    }
}

SpringBoot2 核心知识点_第147张图片

8.3.3 定制 Metrics
  1. Spring Boot 支持自动配置的 Metrics
  • JVM metrics,report utilization of:
    • Various memory and buffer pools
    • Statistics related to garbage collection
    • Threads utilization
    • Number of classes loaded/unloaded
  • CPU metrics
  • File descriptor metrics
  • Kafka consumer and producer metrics
  • Log4j2 metrics:record the number of events logged to Log4j2 at each level
  • Logback metrics: record the number of events logged to Logback at each level
  • Uptime metrics: report a gauge for uptime and a fixed gauge representing the application’s absolute start time
  • Tomcat metrics (server.tomcat.mbeanregistry.enabled must be set to true for all Tomcat metrics to be registered)
  • Spring Integration metrics
  1. 增强Metrics

想要判断某一个方法被访问了多少次

  1. 在接口实现类中定义指标 Metrics
Counter counter;	// 数量
public UserServiceImpl(MeterRegistry registry) {
    counter = registry.counter("UserService.list.count");//UserService.list.count 自定义的指标的名称
}
@Override
public void TestMeterRegistry() {
    counter.increment();	// 调用这个方法一次加 1
}
  1. 测试访问
  2. localhost:8080/actuator/metrics

SpringBoot2 核心知识点_第148张图片

SpringBoot2 核心知识点_第149张图片

8.4 定制 Endpoint
  1. 创建一个自定义 Endpoint 类,注册到容器中
@Component
@Endpoint(id = "myservice")// 端点名
public class MyServiceEndpoint {

    // 端点的读操作
    @ReadOperation
    public Map getDockerInfo() {
        return Collections.singletonMap("dockerInfo","docker is running......");
    }

    @WriteOperation
    public void stopDocker() {
        System.out.println("Docker stopped......");
    }
}
  1. 启动运行访问

可以看到我们自定义的端点

SpringBoot2 核心知识点_第150张图片

拿到端点的值

SpringBoot2 核心知识点_第151张图片

8.5 可视化监控系统

https://github.com/codecentric/spring-boot-admin

使用手册:https://codecentric.github.io/spring-boot-admin/2.5.1/#getting-started

9. 原理解析

9.1 Profile 功能

为了方便多环境适配,Spring Boot 简化了 profile 功能

在整个应用系统开发的时候可能会有一套数据库的配置信息,上线的时候又有另一套配置信息,如果每次开发上线都去修改配置文件会非常的麻烦,所以可以配置两套配置文件,一个上线的时候使用,一个平常开发测试的时候使用。

9.1.1 application-profile 功能
  • 默认配置文件 application.yml 任何时候都会被加载
  • 在默认配置文件中指定环境配置文件 application-{env}.yaml
  • 激活指定环境
    • 配置文件激活
    • 命令行激活
  • 默认配置与环境配置同时生效
  • 同名配置项,profile.active 激活的 配置优先

使用

创建两个配置文件:application-test.yml(测试环境),application-prod.yml(生产环境)

在配置文件中写入相同的配置

person:
  name: test-张三

SpringBoot2 核心知识点_第152张图片

@RestController
public class HelloController {

    @Value("${person.name:李四}")	// 如果person.name 为空给默认值李四
    private String name;

    @GetMapping("/")
    public String hello() {
        return "Hello  " + name;
    }
}

如果没有指定配置环境,则默认的配置文件生效

如果想要指定是测试环境还是生成环境,则在默认的配置文件中进行指定

# 指定使用那个环境的配置文件,默认配置文件和指定环境的配置文件都会生效(如果默认配置文件和指定的环境配置中有相同的配置属性,则指定的会覆盖默认的)
spring.profiles.active=test

项目都是需要打包然后进行部署的,如果我们打包好了又需要切换环境重新打包非常的麻烦,所以我们可以在执行 jar 包的时候切换环境配置文件,命令行方式可以修改配置文件同时也可以修改配置文件中的内容信息

java -jar 打包好的项目 --spring.profile.active=prod --person.name=王五
9.1.2 @Profile 条件装配功能

官网:https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.profiles

模拟使用

有两个类,一个 Boss,一个Worker类,分别都包含属性 name、age,实现一个 Person 接口,配置文件 test 中配置了老板的信息,prod 配置了工人的信息,如果我们在 Controller 使用某一个对象的时候,自动注入 Person 接口,返回 person 的信息,不能确定生效的老板的类还是员工的类。所以在Boss 类和 Worker 类中分别进行指定配置文件的装配,@Profile 注解

// 激活配置文件,当 prod 文件的时候生效
@Profile("prod")
@Data
@Component
@ConfigurationProperties("person")
public class Worker implements Person{

    private String name;
    private Integer age;

}
// 激活配置文件,当 test 文件的时候生效// 激活配置文件,当 test 文件的时候生效
@Profile("test")
@Data
@Component
@ConfigurationProperties("person")
public class Boss implements Person{

    private String name;
    private Integer age;

}
#配置文件

# 默认配置文件 application.properties 进行激活使用的环境配置信息
spring.profiles.active=prod

# 测试环境配置文件 application-test.yml
person:
  name: boss-张三
  age: 30
  
# 生产环境配置文件 application-prod.yml
person:
  name: worker-张三
  age: 10

然后启动测试运行结果

启动后可以看到当前使用的生产 prod 环境,当然上面默认配置文件中是指定好的

SpringBoot2 核心知识点_第153张图片

返回值信息是 prod中配置好的内容

SpringBoot2 核心知识点_第154张图片

9.1.3 Profile 分组

官网:https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.profiles.groups

# 激活一个组
spring.profiles.active=myprod

spring.profiles.group.myprod[0]=prod
spring.profiles.group.myprod[1]=dev

SpringBoot2 核心知识点_第155张图片

9.2外部化配置

简单的说外部化配置就是抽取一些文件,放在外边集中管理

官网:https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.external-config

Spring Boot 可以让我们使用外部化配置,外部化设置可以在不修改代码的情况下可以适配多种环境,外部化来源包括:Java properties files, YAML files, environment variables,command-line arguments.

9.2.1 配置文件查找位置
  1. classpath 根路径
  2. classpath 根路径下 config 目录

SpringBoot2 核心知识点_第156张图片

  1. jar 包当前目录

  2. jar 包当前目录的config 目录

SpringBoot2 核心知识点_第157张图片

  1. /config 子目录的直接子目录(这里的 / 指的是 Linux 系统的 / 目录)

优先级:5 > 4 > 3 > 2 > 1

9.2.2 配置文件加载顺序
  1. 当前 jar 包内部的 application.properties 和 application.yml
  2. 当前 jar 包内部的 application-{profile}.properties 和 application-{profile}.yml
  3. 引用的外部 jar 包的 application.properties 和 application.yml
  4. 应用的外部 jar 包的 application-{profile}.properties 和 application-{profile}.yml

后面的可以覆盖前面的同名配置项,指定环境优先,外部优先

9.3 自定义 starter

9.3.1 starter 启动原理
  • starter-pom 引入 autoconfigure 包

SpringBoot2 核心知识点_第158张图片

  • autoconfigure 包中配置使用 META-INF/spring.factoriesEnableAutoConfigurtion 的值,使得项目启动加载指定自动配置类
  • 编写自动配置类 xxxxAutoConfiguration -> xxxxProperties
    • @Configuration
    • Conditional
    • @EnableConfigurationProperties
    • @Bean

引入 starter — xxxxAutoConfiguration — 容器中放入组件 — 绑定 xxxxProperties — 配置项

9.3.2 自定义一个 starter
  1. 打开 Idea ,创建一个空的项目 customer-starter

SpringBoot2 核心知识点_第159张图片

  1. 创建一个空的 Maven 项目,hello-spring-boot-starter 作为我们的启动器

SpringBoot2 核心知识点_第160张图片

  1. 创建一个 Spring Boot 初始化项目 hello-spring-boot-starter-autoconfigure 自动配置包

SpringBoot2 核心知识点_第161张图片

创建好之后

SpringBoot2 核心知识点_第162张图片

  1. hello-spring-boot-starter 的 pom.xml 文件中配置好自动配置类
// 这个内容是自动配置类中pom.xml中的信息
<dependencies>
    <dependency>
        <groupId>com.examplegroupId>
        <artifactId>hello-spring-boot-starter-autoconfigureartifactId>
        <version>0.0.1-SNAPSHOTversion>
    dependency>
dependencies>

至此 hello-spring-boot-starter 配置好了,这个启动器只需要配置一个自动配置的信息

  1. 配置 hello-spring-boot-starter-autoconfigure 自动配置类
  • 创建一个 HelloProperties 用于保存配置类的配置属性信息
// 指定前缀信息,在配置文件中使用这个 example.hello 就是配置我们自定义的starter 的属性
@ConfigurationProperties("example.hello")
public class HelloProperties {

    private String prefix;
    private String suffix;

// 省略 get/set 方法
    
}

  • 创建一个服务类 HelloService,真正执行的代码
/**
 *  默认不放到容器中
 */
public class HelloService {

    @Autowired
    HelloProperties helloProperties;

    public String sayHello(String name) {
        return helloProperties.getPrefix() + "你好" + name + helloProperties.getSuffix();
    }

}
  • 创建一个自动配置类 HelloServiceAutoConfiguration
@Configuration
@ConditionalOnMissingBean(HelloService.class)
@EnableConfigurationProperties(HelloProperties.class)// 默认把 HelloProperties 放到容器中
public class HelloServiceAutoConfiguration {

    @Bean
    public HelloService helloService() {
        HelloService helloService = new HelloService();
        return helloService;
    }

}
  • 在 resources 文件夹下创建文件 /META-INF/spring.factories,指定自动配置的类
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.auto.HelloServiceAutoConfiguration

SpringBoot2 核心知识点_第163张图片

至此启动器和自动配置类创建完成,然后将我们自己创建好了项目安装到 maven 中

  1. 安装到 maven 中

SpringBoot2 核心知识点_第164张图片

  1. 在别的项目就可以用这个自定义的 starter 了

使用

  • 引入启动器
<dependency>
    <groupId>org.examplegroupId>
    <artifactId>hello-spring-boot-starterartifactId>
    <version>1.0-SNAPSHOTversion>
dependency>
	- 配置属性信息
example.hello.suffix="111"
example.hello.prefix="222"
  • Controller 中注入 HelloService 类,执行方法
@RestController
public class HelloController {
    @Autowired
    HelloService helloService;
    @RequestMapping("/hello")
    public String sayHello() {
        String name = helloService.sayHello("张三三");
        return name;
    }
}
  • 测试运行

SpringBoot2 核心知识点_第165张图片

9.4 Spring Boot 原理

9.4.1 Spring Boot 启动过程

主启动类 debug 运行

SpringBoot2 核心知识点_第166张图片

// 这块是创建的流程  new SpringApplication(primarySources)

return run(new Class[]{primarySource}, args);	// new 一个 Class 类

进去,1. 创建一个 Spring 应用,2. 然后调用 run 方法启动 
return (new SpringApplication(primarySources)).run(args);

进去进去
@SuppressWarnings({ "unchecked", "rawtypes" })
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
    // 资源加载器
	this.resourceLoader = resourceLoader;
   	// 断言程序中有主配置类,如果没有失败
	Assert.notNull(primarySources, "PrimarySources must not be null");
	this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
    // 判断当前应用的类型
	this.webApplicationType = WebApplicationType.deduceFromClasspath();
    // 初始启动引导器,去 spring.factories 文件中找 Bootstrap类型的
	this.bootstrapRegistryInitializers = getBootstrapRegistryInitializersFromSpringFactories();
    //   找 ApplicationContextInitializer  也是去 spring.factories 文件中找
	setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
    // 找应用 监听器   ApplicationListener
	setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
    // 决定哪一个是主程序类
	this.mainApplicationClass = deduceMainApplicationClass();
}


private Class<?> deduceMainApplicationClass() {
	try {
        // 进到 堆栈中,找到有 main 方法的类就是主启动类
		StackTraceElement[] stackTrace = new RuntimeException().getStackTrace();
		for (StackTraceElement stackTraceElement : stackTrace) {
			if ("main".equals(stackTraceElement.getMethodName())) {
				return Class.forName(stackTraceElement.getClassName());
			}
		}
	}
	catch (ClassNotFoundException ex) {
		// Swallow and continue
	}
	return null;
}
// 这块是 run 的流程  run(args)

// String... args 就是 main 方法中的参数 main(String[] args)
public ConfigurableApplicationContext run(String... args) {
    // 停止的监听器,监控应用的启动停止
	StopWatch stopWatch = new StopWatch();
    // 记录的启动时间
	stopWatch.start();
    // 创建 引导上下文,并且获取到之前的 BootstrapRegistryInitializer 并且挨个执行 initializer() 方法 来完成对引导启动器上下文环境设置
	DefaultBootstrapContext bootstrapContext = createBootstrapContext();
	ConfigurableApplicationContext context = null;
    // 让当前应用进入 headless 模式(自力更生模式)
	configureHeadlessProperty();
    // 获取所有的 运行时监听器 并进行保存
	SpringApplicationRunListeners listeners = getRunListeners(args);
    // 遍历所有的 RunListener,调用 starting 方法,相当于通知所有感兴趣系统正在启动过程的正在starting,为了方便所有 Listener 进行事件感知
	listeners.starting(bootstrapContext, this.mainApplicationClass);
	try {
        // 保存命令行参数
		ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
        // 准备基础环境(保存环境变量等等),调用方法,如果有就返回,没有就创建基础一个,无论如何得有一个环境信息,配置环境环境变量信息,加载全系统的配置源的属性信息,绑定环境信息,监听器调用 environmentPrepard,通知所有的监听器当前环境准备完成
//        prepareEnvironment 结束后所有环境信息准备完毕	
		ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
   		// 配置一些忽略的 bean 信息     
		configureIgnoreBeanInfo(environment);
        // 打印 Banner
		Banner printedBanner = printBanner(environment);
        // 创建 IOC 容器 就是创建 ApplicationContext,根据当前项目类型,servlet,AnnotationConfigServletWebServerApplicationContex,容器创建对象new 出来了
		context = createApplicationContext();
        // 记录当前应用的 startup事件
		context.setApplicationStartup(this.applicationStartup);
        // 准备 IOC 容器的信息 ,保存环境信息,后置处理流程,应用初始化器(遍历所有的 ApplicationContextInitlalizer,调用initialize 来对 IOC 容器进行初始化扩展功能),遍历所有的 listener 调用 contextPrepared,EvenPublishRunListener 通知所有的监听器  contextPrepared 完成
		prepareContext(bootstrapContext, context, en vironment, listeners, applicationArguments, printedBanner);
        // 刷新 IOC 容器,(实例化容器中的所有组件)
		refreshContext(context);
        // 刷新完成后工作,
		afterRefresh(context, applicationArguments);
		stopWatch.stop();
		if (this.logStartupInfo) {
			new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
		}
        // 所有监听器调用started 方法,通知所有监听器 started
		listeners.started(context);
        // 调用所有的 Runners,获取容器中 ApplicationRunner,CommandLineRunner,合并所有 Runner 按照 @Order 进行排序,遍历所有的 Runner,调用 run 方法,如果以上有异常调用 Listener 的faild 方法
		callRunners(context, applicationArguments);
	}
	catch (Throwable ex) {
		handleRunFailure(context, ex, listeners);
		throw new IllegalStateException(ex);
	}
	try {
        // 如果以上都准备好并且没有异常,调用所有监听器的 running 方法,通知所有监听器进入 running 状态了,running 如果有错误继续通知 failed,通知监听器当前失败
		listeners.running(context);
	}
	catch (Throwable ex) {
		handleRunFailure(context, ex, null);
		throw new IllegalStateException(ex);
	}
	return context;
}


// 如果一开始找到了 BootstrapRegistryInitializer 引导启动器就会调用这个方法,把每一个启动器遍历调用 initialize 方法
private DefaultBootstrapContext createBootstrapContext() {
	DefaultBootstrapContext bootstrapContext = new DefaultBootstrapContext();
	this.bootstrapRegistryInitializers.forEach((initializer) -> initializer.initialize(bootstrapContext));
	return bootstrapContext;
}

@FunctionalInterface
public interface BootstrapRegistryInitializer {

	/**
	 * Initialize the given {@link BootstrapRegistry} with any required registrations.
	 * @param registry the registry to initialize
	 */
	void initialize(BootstrapRegistry registry);

}

private SpringApplicationRunListeners getRunListeners(String[] args) {
    // 拿到上下文 SpringApplication
	Class<?>[] types = new Class<?>[] { SpringApplication.class, String[].class };
	return new SpringApplicationRunListeners(logger,
            // 去 spring.factories 中找  SpringApplicationRunListener.class
     
			getSpringFactoriesInstances(SpringApplicationRunListener.class, types, this, args),
			this.applicationStartup);
}

SpringBoot2 核心知识点_第167张图片

9.4.2 自定义事件监听组件

几个重要组件的自定义

  1. ApplicationContextInitializer
public class MyApplicationContextInitializer implements ApplicationContextInitializer {

    @Override
    public void initialize(ConfigurableApplicationContext applicationContext) {
        System.out.println("MyApplicationContextInitializer...initialize");
    }
}
  1. ApplicationLineRunner
public class MyCommandLineRunner implements CommandLineRunner {
    @Override
    public void run(String... args) throws Exception {
        System.out.println("MyCommandLineRunner...run...");
    }
}
  1. SpringApplicationRunListener
public class MySpringApplicationRunListener implements SpringApplicationRunListener {

    public SpringApplication application;

    public MySpringApplicationRunListener(SpringApplication application,String[] args) {
        this.application = application;
    }

    // 调用时机:应用刚一开始运行,刚创建好容器的基本信息的时候就调用 starting,相当于应用开始启动了
    @Override
    public void starting(ConfigurableBootstrapContext bootstrapContext) {
        System.out.println("MySpringApplicationRunListener...starting...");
    }

    // 环境准备完成的时候调用
    @Override
    public void environmentPrepared(ConfigurableBootstrapContext bootstrapContext, ConfigurableEnvironment environment) {
        System.out.println("MySpringApplicationRunListener...environmentPrepared...");

    }

    // IOC 容器准备完成
    @Override
    public void contextPrepared(ConfigurableApplicationContext context) {
        System.out.println("MySpringApplicationRunListener...contextPrepared...");

    }

    // IOC 容器加载完成
    @Override
    public void contextLoaded(ConfigurableApplicationContext context) {
        System.out.println("MySpringApplicationRunListener...contextLoaded...");

    }

    // IOC 容器启动,调用 reflash 方法后
    @Override
    public void started(ConfigurableApplicationContext context) {
        System.out.println("MySpringApplicationRunListener...started...");

    }

    // 整个容器创建完成全部都实例之后,整个容器没有异常都启动起来的时候调用
    @Override
    public void running(ConfigurableApplicationContext context) {
        System.out.println("MySpringApplicationRunListener...running...");

    }

    @Override
    public void failed(ConfigurableApplicationContext context, Throwable exception) {
        System.out.println("MySpringApplicationRunListener...failed...");

    }
}
  1. ApplicationListener
@Component
public class MyApplicationListener implements ApplicationListener {
    @Override
    public void onApplicationEvent(ApplicationEvent event) {
        System.out.println("MyApplicationListener...onApplicationEvent");
    }
}
  1. ApplicationRunner
@Component
public class MyApplicationRunner implements ApplicationRunner {
    @Override
    public void run(ApplicationArguments args) throws Exception {
        System.out.println("MyApplicationRunner...run...");
    }
}

然后再resources 文件夹下创建 /META-INF/spring.factories

org.springframework.context.ApplicationContextInitializer=\
  com.lss.listener.MyApplicationContextInitializer

org.springframework.context.ApplicationListener=\
  com.lss.listener.MyApplicationListener

org.springframework.boot.SpringApplicationRunListener=\
  com.lss.listener.MySpringApplicationRunListener

执行查看输出结果就可以直到那个事件先执行了

SpringBoot2 核心知识点_第168张图片

SpringBoot2 核心知识点_第169张图片

**学习参考视频:**https://www.bilibili.com/video/BV19K4y1L7MT?p=1

你可能感兴趣的:(笔记,spring,spring,boot,java)