尚硅谷SpringBoot3零基础教程

本笔记来源于:尚硅谷SpringBoot3零基础教程,springboot入门到实战
b站视频

文章来自:
https://gitee.com/satan1996/springboot3-learning-notes
https://www.yuque.com/leifengyang/springboot3

一、Springboot3-快速入门

1、Springboot简介

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

1.Springboot是什么

Spring Boot是一个基于Spring框架的快速应用开发框架,它简化了Spring应用程序的初始搭建和开发过程。Spring Boot通过提供默认配置、自动配置和启动器等机制,使得开发者可以更加高效地构建Spring应用程序。

与传统的Spring框架不同,Spring Boot可以在不需要大量XML配置文件的情况下轻松创建应用程序,而且可以通过内嵌的Web容器(如Tomcat、Jetty或Undertow)快速部署和运行应用程序。此外,Spring Boot还支持许多其他常用框架和库,例如Spring Data JPA、Spring Security、Thymeleaf和MyBatis等。

2.Springboot的特点

  • 自动配置:Spring Boot会根据classpath中的jar包、类名和注解等信息自动配置应用程序。
  • 独立运行:Spring Boot应用程序可以打包成一个可执行的jar包,并直接使用java -jar命令运行。
  • 内嵌服务器:Spring Boot应用程序可以使用嵌入式的Tomcat、Jetty或Undertow等内置Web服务器。
  • 无需XML配置:Spring Boot大量使用Java配置和注解,避免了繁琐的XML配置。
  • 微服务支持:Spring Boot支持构建微服务应用程序,并提供了一些有用的功能,例如服务发现、断路器和负载均衡等。

特性:

  • 快速创建独立 Spring 应用

    • SSM:导包、写配置、启动运行
  • 直接嵌入Tomcat、Jetty or Undertow(无需部署 war 包)【Servlet容器】

    • linux java tomcat mysql: war 放到 tomcat 的 webapps下
    • jar: java环境; java -jar
  • 重点:提供可选的starter,简化应用整合

    • 场景启动器(starter):web、json、邮件、oss(对象存储)、异步、定时任务、缓存…
    • 导包一堆,控制好版本。
    • 为每一种场景准备了一个依赖; web-starter。mybatis-starter
  • 重点:按需自动配置 Spring 以及 第三方库

    • 如果这些场景我要使用(生效)。这个场景的所有配置都会自动配置好。
    • 约定大于配置:每个场景都有很多默认配置。
    • 自定义:配置文件中修改几项就可以
  • 提供生产级特性:如 监控指标、健康检查、外部化配置等

    • 监控指标、健康检查(k8s)、外部化配置
  • 无代码生成、无xml

总结:简化开发,简化配置,简化整合,简化部署,简化监控,简化运维。

2、快速体验

场景:浏览器发送**/hello**请求,返回”Hello,Spring Boot 3!

1. 开发流程

1. 创建项目

maven 项目

1
2
3
4
5
6
<!--    所有springboot项目都必须继承自 spring-boot-starter-parent -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.0.5</version>
</parent>

2. 导入场景

场景启动器

1
2
3
4
5
6
7
8
    <dependencies>
<!-- web开发的场景启动器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>

3. 主程序

1
2
3
4
5
6
7
@SpringBootApplication //这是一个SpringBoot应用
public class MainApplication {

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

4. 业务

1
2
3
4
5
6
7
8
9
@RestController
public class HelloController {

@GetMapping("/hello")
public String hello(){

return "Hello,Spring Boot 3!";
}
}

5. 测试

默认启动访问: localhost:8080

6. 打包

1
2
3
4
5
6
7
8
9
<!--    SpringBoot应用打包插件-->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

mvn clean package把项目打成可执行的jar包
java -jar demo.jar启动项目

2. 特性小结

1. 简化整合

导入相关的场景,拥有相关的功能。场景启动器

默认支持的所有场景:
https://docs.spring.io/spring-boot/docs/current/reference/html/using.html#using.build-systems.starters

  • 官方提供的场景:命名为:spring-boot-starter-*
  • 第三方提供场景:命名为:*-spring-boot-starter

场景一导入,万物皆就绪

2. 简化开发

无需编写任何配置,直接开发业务

3. 简化配置

application.properties

4. 简化部署

打包为可执行的jar包。

linux服务器上有java环境。

5. 简化运维

修改配置(外部放一个application.properties文件)、监控、健康检查。

…..

3. Spring Initializr 创建向导

一键创建好整个项目结构

3、应用分析

1. 依赖管理机制

思考:

1、为什么导入starter-web所有相关依赖都导入进来?

  • 开发什么场景,导入什么场景启动器。
  • maven依赖传递原则。A-B-C: A就拥有B和C
  • 导入 场景启动器。 场景启动器 自动把这个场景的所有核心依赖全部导入进来

2、为什么版本号都不用写?

  • 每个boot项目都有一个父项目spring-boot-starter-parent
  • parent的父项目是spring-boot-dependencies
  • 父项目 版本仲裁中心,把所有常见的jar的依赖版本都声明好了。
  • 比如:mysql-connector-j

3、自定义版本号

  • 利用maven的就近原则
    • 直接在当前项目properties标签中声明父项目用的版本属性的key
    • 直接在导入依赖的时候声明版本

4、第三方的jar包

  • boot父项目没有管理的需要自行声明好
1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/com.alibaba/druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.16</version>
</dependency>

2. 自动配置机制

1. 初步理解

  • 自动配置的 Tomcat、SpringMVC 等
    • 导入场景,容器中就会自动配置好这个场景的核心组件。
    • 以前:DispatcherServlet、ViewResolver、CharacterEncodingFilter….
    • 现在:自动配置好的这些组件
    • 验证:容器中有了什么组件,就具有什么功能
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void main(String[] args) {

//java10: 局部变量类型的自动推断
var ioc = SpringApplication.run(MainApplication.class, args);

//1、获取容器中所有组件的名字
String[] names = ioc.getBeanDefinitionNames();
//2、挨个遍历:
// dispatcherServlet、beanNameViewResolver、characterEncodingFilter、multipartResolver
// SpringBoot把以前配置的核心组件现在都给我们自动配置好了。
for (String name : names) {
System.out.println(name);
}
}
  • 默认的包扫描规则

    • @SpringBootApplication 标注的类就是主程序类
    • SpringBoot只会扫描主程序所在的包及其下面的子包,自动的component-scan功能
    • 自定义扫描路径
      • @SpringBootApplication(scanBasePackages = “com.atguigu”)
      • @ComponentScan("com.atguigu") 直接指定扫描的路径
  • 配置默认值

    • 配置文件的所有配置项是和某个类的对象值进行一一绑定的。
    • 绑定了配置文件中每一项值的类: 属性类
    • 比如:
      • ServerProperties绑定了所有Tomcat服务器有关的配置
      • MultipartProperties绑定了所有文件上传相关的配置
      • ….参照官方文档:或者参照 绑定的 属性类
  • 按需加载自动配置

    • 导入场景spring-boot-starter-web
    • 场景启动器除了会导入相关功能依赖,导入一个spring-boot-starter,是所有starterstarter,基础核心starter
    • spring-boot-starter导入了一个包 spring-boot-autoconfigure。包里面都是各种场景的AutoConfiguration自动配置类
    • 虽然全场景的自动配置都在 spring-boot-autoconfigure这个包,但是不是全都开启的。
      • 导入哪个场景就开启哪个自动配置

总结: 导入场景启动器、触发 spring-boot-autoconfigure这个包的自动配置生效、容器中就会具有相关场景的功能

2. 完整流程

思考:

1、SpringBoot怎么实现导一个starter、写一些简单配置,应用就能跑起来,我们无需关心整合
2、为什么Tomcat的端口号可以配置在application.properties中,并且Tomcat能启动成功?
3、导入场景后哪些自动配置能生效

自动配置流程细节梳理:

1、导入starter-web:导入了web开发场景
  • 1、场景启动器导入了相关场景的所有依赖:starter-jsonstarter-tomcatspringmvc
  • 2、每个场景启动器都引入了一个spring-boot-starter,核心场景启动器。
  • 3、核心场景启动器引入了spring-boot-autoconfigure包。
  • 4、spring-boot-autoconfigure里面囊括了所有场景的所有配置。
  • 5、只要这个包下的所有类都能生效,那么相当于SpringBoot官方写好的整合功能就生效了。
  • 6、SpringBoot默认却扫描不到 spring-boot-autoconfigure下写好的所有配置类。(这些配置类给我们做了整合操作),默认只扫描主程序所在的包
2、主程序@SpringBootApplication

-1、@SpringBootApplication由三个注解组成@SpringBootConfiguration@EnableAutoConfiguratio@ComponentScan

  • 2、SpringBoot默认只能扫描自己主程序所在的包及其下面的子包,扫描不到 spring-boot-autoconfigure包中官方写好的配置类

  • 3、@EnableAutoConfiguration:SpringBoot 开启自动配置的核心

      1. 是由@Import(AutoConfigurationImportSelector.class)提供功能:批量给容器中导入组件。
      1. SpringBoot启动会默认加载 142个配置类。
      1. 142个配置类来自于spring-boot-autoconfigureMETA-INF/spring/**org.springframework.boot.autoconfigure.AutoConfiguration**.imports文件指定的
    • 项目启动的时候利用 @Import 批量导入组件机制把 autoconfigure 包下的142 xxxxAutoConfiguration类导入进来(自动配置类
    • 虽然导入了142个自动配置类,但是不是全部生效,见下行4中介绍。
  • 4、按需生效:

    • 并不是这142个自动配置类都能生效
    • 每一个自动配置类,都有条件注解@ConditionalOnxxx,只有条件成立,才能生效
3、xxxxAutoConfiguration自动配置类
  • 1、给容器中使用@Bean 放一堆组件。
  • 2、每个自动配置类都可能有这个注解@EnableConfigurationProperties(ServerProperties.class),用来把配置文件中配的指定前缀的属性值封装到 xxxProperties属性类
  • 3、以Tomcat为例:把服务器的所有配置都是以server开头的。配置都封装到了属性类中。
  • 4、给容器中放的所有组件的一些核心参数,都来自于xxxPropertiesxxxProperties都是和配置文件绑定。
  • 只需要改配置文件的值,核心组件的底层参数都能修改
4、写业务,全程无需关心各种整合(底层这些整合写好了,而且也生效了)

核心流程总结:

1、导入starter,就会导入autoconfigure包。

2、autoconfigure 包里面 有一个文件 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports,里面指定的所有启动要加载的自动配置类

3、@EnableAutoConfiguration 会自动的把上面文件里面写的所有自动配置类都导入进来。xxxAutoConfiguration 是有条件注解进行按需加载

4、xxxAutoConfiguration给容器中导入一堆组件,组件都是从 xxxProperties中提取属性值

5、xxxProperties又是和配置文件进行了绑定

效果:导入starter、修改配置文件,就能修改底层行为。

3、自动配置原理

1.@SpringBootApplication

一切的来自起源SpringBoot(3.1.0)的启动类,我们发现main方法上面有个注解:@SpringBootApplication

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.satan.boot;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Boot3TestDemo4Application {

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

}

@SpringBootApplication 标注在某个类上说明这个类是 SpringBoot 的主配置类, SpringBoot 就应该运行这个类的main方法来启动 SpringBoot 应用;它的本质是一个组合注解,我们点进去查看该类的元信息主要包含3个注解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
excludeFilters = {@Filter(
type = FilterType.CUSTOM,
classes = {TypeExcludeFilter.class}
), @Filter(
type = FilterType.CUSTOM,
classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication {
//...
}
  • @SpringBootConfiguration(里面就是@Configuration,标注当前类为配置类,其实只是做了一层封装改了个名字而已)
  • @EnableAutoConfiguration(开启自动配置)
  • @ComponentScan(包扫描)

注:**@Inherited是一个标识,用来修饰注解,如果一个类用上了@Inherited修饰的注解,那么其子类也会继承这个注解**

我们下面逐一分析这3个注解作用

1.1.@SpringBootConfiguration
  • 我们继续点@SpringBootConfiguration进去查看源码如下:
1
2
3
4
5
6
7
8
9
10
11
12
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
@Indexed
public @interface SpringBootConfiguration {
@AliasFor(
annotation = Configuration.class
)
boolean proxyBeanMethods() default true;
}

@Configuration标注在某个类上,表示这是一个 springboot的配置类。可以向容器中注入组件。

1.2 @ComponentScan
  • @ComponentScan:配置用于 Configuration 类的组件扫描指令。
  • 提供与 Spring XML<context:component-scan> 元素并行的支持。
  • 可以 basePackageClassesbasePackages 来定义要扫描的特定包。 如果没有定义特定的包,将从声明该注解的类的包开始扫描
1.3 @EnableAutoConfiguration
  • @EnableAutoConfiguration顾名思义就是:开启自动导入配置
  • 这个注解是SpringBoot的重点,我们下面详细讲解

2.@EnableAutoConfiguration

我们点进去看看该注解有什么内容

1
2
3
4
5
6
7
8
9
10
11
12
13
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage //自动导包
@Import({AutoConfigurationImportSelector.class}) //自动配置导入选择
public @interface EnableAutoConfiguration {
String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";

Class<?>[] exclude() default {};

String[] excludeName() default {};
}
2.1.@AutoConfigurationPackage
  • 自动导入配置包
  • 点进去查看代码:
1
2
3
4
5
6
7
8
9
10
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import({AutoConfigurationPackages.Registrar.class})
public @interface AutoConfigurationPackage {
String[] basePackages() default {};

Class<?>[] basePackageClasses() default {};
}
  • 它的作用是让 Spring Boot 自动扫描指定的包或类所在的包,以便自动配置相关的 Bean 或组件。

  • Import 为spring的注解,导入一个配置文件,在springboot中为给容器导入一个组件,而导入的组件由 AutoConfigurationPackages.class的内部类Registrar.class 执行逻辑来决定是如何导入的。

2.1.1 @Import({Registrar.class})

点Registrar.class进去查看源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {
Registrar() {
}

public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
AutoConfigurationPackages.register(registry, (String[])(new PackageImports(metadata)).getPackageNames().toArray(new String[0]));
}

public Set<Object> determineImports(AnnotationMetadata metadata) {
return Collections.singleton(new PackageImports(metadata));
}
}

注:Registrar实现了ImportBeanDefinitionRegistrar类,就可以被注解@Import导入到spring容器里。

这个地方打断点

image.png

运行可以查看到AutoConfigurationPackages.register(registry, (String[])(new PackageImports(metadata)).getPackageNames().toArray(new String[0]));的值为当前启动类所在的包名

结论:**@AutoConfigurationPackage 就是将主配置类(@SpringBootApplication 标注的类)所在的包下面所有的组件都扫描注冊到 spring 容器中。**

2.2.@Import({AutoConfigurationImportSelector.class})

作用:AutoConfigurationImportSelector开启自动配置类的导包的选择器,即是带入哪些类,有选择性的导入。

点AutoConfigurationImportSelector.class进入查看源码,这个类中有两个方法见名知意:

  1. selectImports:用于根据注解元数据获取自动配置类并返回其全限定名数组。

    具体来说,selectImports() 方法会先检查当前环境中是否启用了自动配置,如果没有启用,则会直接返回空数组。否则,它会调用 getAutoConfigurationEntry() 方法获取自动配置条目,并通过 getConfigurations() 方法获取候选的自动配置类。最后,通过 StringUtils.toStringArray() 方法将自动配置类列表转换为字符串数组并返回。

    总之,该方法主要用于在应用程序启动时加载自动配置类,进而实现对各种功能的统一、标准化和协作式管理。

    1
    2
    3
    4
    5
    6
    7
    8
    public String[] selectImports(AnnotationMetadata annotationMetadata) {
    if (!this.isEnabled(annotationMetadata)) {
    return NO_IMPORTS;
    } else {
    AutoConfigurationEntry autoConfigurationEntry = this.getAutoConfigurationEntry(annotationMetadata);
    return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
    }
    }
  2. getAutoConfigurationEntry:具体来说,getAutoConfigurationEntry() 方法根据当前环境中是否启用了自动配置来决定是否返回空对象。如果启用了自动配置,则从注解元数据 annotationMetadata 和注解属性 attributes 中获取候选的自动配置类。然后,通过 removeDuplicates() 方法删除重复项,再通过 getExclusions() 方法获取需要排除的依赖,并将其从候选的自动配置类中移除。接着,通过 filter() 方法筛选出需要保留的自动配置类。最后,调用 fireAutoConfigurationImportEvents() 方法触发自动配置导入事件,以便其他组件也能对自动配置进行监听和处理。

    总之,该方法是 Spring Boot 自动配置机制的核心,通过自动扫描、检测和筛选等步骤,实现了自动配置的灵活、高效和可靠。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    protected AutoConfigurationImportSelector.AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
    if (!this.isEnabled(annotationMetadata)) {
    return EMPTY_ENTRY;
    } else {
    AnnotationAttributes attributes = this.getAttributes(annotationMetadata);
    // 这打个断点,看看 返回的数据
    List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);
    //删除重复项
    configurations = this.removeDuplicates(configurations);
    Set<String> exclusions = this.getExclusions(annotationMetadata, attributes);
    //检查
    this.checkExcludedClasses(configurations, exclusions);
    //删除需要排除的依赖
    configurations.removeAll(exclusions);
    configurations = this.getConfigurationClassFilter().filter(configurations);
    this.fireAutoConfigurationImportEvents(configurations, exclusions);
    return new AutoConfigurationImportSelector.AutoConfigurationEntry(configurations, exclusions);
    }
    }

image-20230605153115062

configurations数组长度为150,并且文件后缀名都为 **AutoConfiguration

结论: 这些都是候选的配置类,经过去重,去除需要的排除的依赖,最终的组件才是这个环境需要的所有组件。有了自动配置,就不需要我们自己手写配置的值了,配置类有默认值的。

我们继续往下看看是如何返回需要配置的组件的

2.2.1.getCandidateConfigurations(annotationMetadata, attributes)

方法如下:

1
2
3
4
5
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
List<String> configurations = ImportCandidates.load(AutoConfiguration.class, this.getBeanClassLoader()).getCandidates();
Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports. If you are using a custom packaging, make sure that file is correct.");
return configurations;
}

这是 Spring Boot 自动配置的核心代码之一,用于获取自动配置类的候选列表。

具体来说,getCandidateConfigurations() 方法会通过 ImportCandidates.load() 方法,从指定路径 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 中加载所有 AutoConfiguration 类型的候选配置。然后,它会检查是否加载到了自动配置类,并在没有找到时抛出异常。最后,将所有候选配置(即自动配置类)返回。

总之,该方法是 Spring Boot 自动配置机制的重要部分,通过自动扫描和加载,实现了对各种功能的快速、便捷和精准的自动配置。

org.springframework.boot.autoconfigure.AutoConfiguration.imports文件位置如下:

image-20230605160726517

image-20230605160745380

  • 由图可知文件内包含146个自动配置类的全类名
  • 并不是这146个自动配置类都能生效
  • 每一个自动配置类,都有条件注解@ConditionalOnxxx,只有条件成立,才能生效

3.流程总结图

4.常用的Conditional注解

  • 在加载自动配置类的时候,并不是将spring.factories的配置全部加载进来,而是通过@Conditional等注解的判断进行动态加载
  • @Conditional其实是spring底层注解,意思就是根据不同的条件,来进行自己不同的条件判断,如果满足指定的条件,那么配置类里边的配置才会生效。
  • 常用的Conditional注解:
    • @ConditionalOnClass : classpath中存在该类时起效
    • @ConditionalOnMissingClass : classpath中不存在该类时起效
    • @ConditionalOnBean : DI容器中存在该类型Bean时起效
    • @ConditionalOnMissingBean : DI容器中不存在该类型Bean时起效
    • @ConditionalOnSingleCandidate : DI容器中该类型Bean只有一个或@Primary的只有一个时起效
    • @ConditionalOnExpression : SpEL表达式结果为true时
    • @ConditionalOnProperty : 参数设置或者值一致时起效
    • @ConditionalOnResource : 指定的文件存在时起效
    • @ConditionalOnJndi : 指定的JNDI存在时起效
    • @ConditionalOnJava : 指定的Java版本存在时起效
    • @ConditionalOnWebApplication : Web应用环境下起效
    • @ConditionalOnNotWebApplication : 非Web应用环境下起效

5.@Import支持导入的三种方式

  • 带有@Configuration的配置类
  • ImportSelector 的实现
  • ImportBeanDefinitionRegistrar 的实现

4. 如何学好SpringBoot

框架的框架、底层基于Spring。能调整每一个场景的底层行为。100%项目一定会用到底层自定义

摄影:

  • 傻瓜:自动配置好。
  • 单反:焦距、光圈、快门、感光度….
  • 傻瓜+单反
  1. 理解自动配置原理
  • 导入starter –> 生效xxxxAutoConfiguration –> 组件 –> xxxProperties –> 配置文件
  1. 理解其他框架底层
  • 拦截器
  1. 可以随时定制化任何组件
  • 配置文件
  • 自定义组件

普通开发:导入starter,Controller、Service、Mapper、偶尔修改配置文件
高级开发:自定义组件、自定义配置、自定义starter
核心:

  • 这个场景自动配置导入了哪些组件,我们能不能Autowired进来使用
  • 能不能通过修改配置改变组件的一些默认参数
  • 需不需要自己完全定义这个组件
  • 场景定制化

最佳实战

  • 选场景,导入到项目

    • 官方:starter
    • 第三方:去仓库搜
  • 写配置,改配置文件关键项

    • 数据库参数(连接地址、账号密码…)
  • 分析这个场景给我们导入了哪些能用的组件

    • 自动装配这些组件进行后续使用
    • 不满意boot提供的自动配好的默认组件
      • 定制化
        • 改配置
        • 自定义组件

整合redis:

  • 选场景spring-boot-starter-data-redis

    • 场景AutoConfiguration 就是这个场景的自动配置类
  • 写配置:

    • 分析到这个场景的自动配置类开启了哪些属性绑定关系
    • @EnableConfigurationProperties(RedisProperties.class)
    • 修改redis相关的配置
  • 分析组件:

    • 分析到 RedisAutoConfiguration 给容器中放了 StringRedisTemplate
    • 给业务代码中自动装配 StringRedisTemplate
  • 定制化

    • 修改配置文件
    • 自定义组件,自己给容器中放一个 StringRedisTemplate

4、常用注解

SpringBoot摒弃XML配置方式,改为全注解驱动

1.组件注册

@Configuration、**@SpringBootConfiguration**

@Bean、**@Scope**

@Controller、 @Service、@Repository、@Component

@Import

@ComponentScan

步骤:
1、@Configuration 编写一个配置类
2、在配置类中,自定义方法给容器中注册组件。配合@Bean
3、或使用@Import 导入第三方的组件

1.1.@Configuration@SpringBootConfiguration

1
2
3
4
5
6
7
@Configuration // 这是一个配置类,替代以前的配置文件。配置类本身也是容器中的组件
public class AppConfig {
@Bean("myself") // 替代以前的Bean标签。组件在容器中的名字默认是方法名,可以直接修改注解的值
public MyService myService() {
return new MyServiceImpl();
}
}

在上面的示例中,我们使用 @Configuration 注解来声明一个 Spring 配置类,并在其中定义了一个名为 myService() 的 Bean。这个 Bean 交由 Spring 容器进行托管管理,可以通过容器获取它并调用它的方法。

1.2.@Bean@Scope

1
2
3
4
5
6
7
8
9
10
11
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AppConfig {
@Bean("myService")
@Scope("prototype")
public MyService myService() {
return new MyServiceImpl();
}
}

在上面的示例中,我们使用 @Bean 注解来创建名为 myService 的 Bean 对象,并指定它的作用域为 prototype,表示每次获取该 Bean 时都会创建一个新的实例对象。这个 Bean 会被 Spring 容器托管管理,可以在其它组件中使用。

1.3.@Controller@Service@Repository@Component

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao;

@Override
public User getUserById(Long id) {
return userDao.getUserById(id);
}
}

@Repository
public class UserDaoImpl implements UserDao {
@Override
public User getUserById(Long id) {
// ...
}
}

在上面的示例中,我们分别使用 @Service@Repository 注解来声明服务和数据访问组件,这些组件会被 Spring 容器进行托管管理,并可以在其它组件中自动注入。另外,@Controller 注解也可以用于声明控制器组件,@Component 则是一个通用的组件注解。

1.4.@Import

1
2
3
4
5
@Configuration
@Import({DaoConfig.class, ServiceConfig.class})
public class AppConfig {
// ...
}

在上面的示例中,我们使用 @Import 注解来导入另外两个配置类 DaoConfigServiceConfig,以便在当前应用程序上下文中注册相关的 Bean 或配置。

1.5.@ComponentScan

1
2
3
4
5
@Configuration
@ComponentScan(basePackages = {"com.example.project.service", "com.example.project.dao"})
public class AppConfig {
// ...
}

在上面的示例中,我们使用 @ComponentScan 注解配置 Spring 组件的扫描范围,指定了 com.example.project.servicecom.example.project.dao 两个包及其子包中的组件都会被扫描到并注册为 Bean。

1.6.@component

@Component 是 Spring 框架中的一个注解,它表示当前标注的类是一个组件类,需要被 Spring 管理。通常情况下,我们使用 @Component 注解来声明一个普通的 bean,这个 bean 可以在整个应用程序中被其他组件引用和使用。

具体来说,使用 @Component 注解需要按照以下步骤进行:

  1. 在需要被 Spring 管理的 Java 类上添加 @Component 注解。
  2. 在需要使用这个 bean 的地方使用 @Autowired 注解进行自动注入。

例如,下面是一个简单的例子:

1
2
3
4
5
6
7
@Component
public class MyService {

public void sayHello() {
System.out.println("Hello, world!");
}
}

在上述代码中,我们定义了一个名为 MyService 的 bean,它使用了 @Component 注解,表示这是一个 Spring 管理的组件,并且可以被其他组件引用和使用。该 bean 中有一个名为 sayHello() 的方法,用于输出一段简单的问候语。

接着,我们可以在需要使用 MyService 的组件中使用 @Autowired 注解来自动注入该 bean:

1
2
3
4
5
6
7
8
9
10
11
@Service
public class MyServiceImpl implements MyService {

@Autowired
private MyService myService;

@Override
public void hello() {
myService.sayHello();
}
}

在上述代码中,我们声明了一个名为 MyServiceImpl 的服务实现类,并使用 @Service 注解表示这是一个服务组件。在该组件中,我们使用 @Autowired 注解自动注入了 MyService bean,并在 hello() 方法中调用了该 bean 的 sayHello() 方法。

总之,@Component 是 Spring 中最基本的注解之一,它让我们可以轻松地声明和管理 bean,并且利用 Spring 框架提供的依赖注入机制来进行组件之间的协作。

2.条件注解

如果注解指定的条件成立,则触发指定行为

@ConditionalOnXxx

@ConditionalOnClass:如果类路径中存在这个类,则触发指定行为

@ConditionalOnMissingClass:如果类路径中不存在这个类,则触发指定行为

@ConditionalOnBean:如果容器中存在这个Bean(组件),则触发指定行为

@ConditionalOnMissingBean:如果容器中不存在这个Bean(组件),则触发指定行为

场景:

  • 如果存在FastsqlException这个类,给容器中放一个Cat组件,名cat01,
  • 否则,就给容器中放一个Dog组件,名dog01
  • 如果系统中有dog01这个组件,就给容器中放一个 User组件,名zhangsan
  • 否则,就放一个User,名叫lisi

@ConditionalOnBean(value=组件类型,name=组件名字):判断容器中是否有这个类型的组件,并且名字是指定的值

@ConditionalOnRepositoryType (org.springframework.boot.autoconfigure.data)
@ConditionalOnDefaultWebSecurity (org.springframework.boot.autoconfigure.security)
@ConditionalOnSingleCandidate (org.springframework.boot.autoconfigure.condition)
@ConditionalOnWebApplication (org.springframework.boot.autoconfigure.condition)
@ConditionalOnWarDeployment (org.springframework.boot.autoconfigure.condition)
@ConditionalOnJndi (org.springframework.boot.autoconfigure.condition)
@ConditionalOnResource (org.springframework.boot.autoconfigure.condition)
@ConditionalOnExpression (org.springframework.boot.autoconfigure.condition)
@ConditionalOnClass (org.springframework.boot.autoconfigure.condition)
@ConditionalOnEnabledResourceChain (org.springframework.boot.autoconfigure.web)
@ConditionalOnMissingClass (org.springframework.boot.autoconfigure.condition)
@ConditionalOnNotWebApplication (org.springframework.boot.autoconfigure.condition)
@ConditionalOnProperty (org.springframework.boot.autoconfigure.condition)
@ConditionalOnCloudPlatform (org.springframework.boot.autoconfigure.condition)
@ConditionalOnBean (org.springframework.boot.autoconfigure.condition)
@ConditionalOnMissingBean (org.springframework.boot.autoconfigure.condition)
@ConditionalOnMissingFilterBean (org.springframework.boot.autoconfigure.web.servlet)
@Profile (org.springframework.context.annotation)
@ConditionalOnInitializedRestarter (org.springframework.boot.devtools.restart)
@ConditionalOnGraphQlSchema (org.springframework.boot.autoconfigure.graphql)
@ConditionalOnJava (org.springframework.boot.autoconfigure.condition)

Spring Boot 中的条件注解是一种特殊的注解,当满足特定条件时才会创建或注册一个 bean 或配置类。这种方式可以让我们根据应用程序当前的环境、配置、状态等情况来动态地决定 bean 和配置类是否需要被创建或注册,以及创建或注册的方式和内容。

2.1.@Conditional

在 Spring Boot 中,条件注解通常都是以 @Conditional 开头的,它们都实现了 Condition 接口。使用条件注解一般有以下几个步骤:

  1. 创建一个 Java 类,实现 Condition 接口,实现 matches() 方法来判断是否满足特定条件。如下面的例子:

    1
    2
    3
    4
    5
    6
    7
    8
    public class LinuxCondition implements Condition {

    @Override
    public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
    String os = conditionContext.getEnvironment().getProperty("os.name");
    return os != null && os.toLowerCase().contains("linux");
    }
    }
  2. 在需要进行条件化配置的 bean 或配置类上添加条件注解,配合上面实现的 Condition 类进行动态配置。如下面的例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @Configuration
    @Conditional(LinuxCondition.class)
    public class LinuxServiceConfig {

    @Bean
    public MyService myService() {
    return new LinuxService();
    }
    }

在上述代码中,我们定义了一个名为 LinuxServiceConfig 的配置类,使用 @Conditional(LinuxCondition.class) 注解来根据当前系统是 Linux 系统还是其它操作系统来决定是否创建 LinuxService bean。如果当前系统是 Linux 系统,那么就会创建 LinuxService bean,并且将其注册为 MyService 类型的 bean。

值得注意的是,条件注解并不是只有 @Conditional 一种,Spring Boot 还提供了很多实现了 Condition 接口的注解,如 @ConditionalOnBean@ConditionalOnMissingBean@ConditionalOnClass@ConditionalOnWebApplication 等。

2.2.@ConditionalOnBean

@ConditionalOnBean 注解会检查指定的 Bean 是否存在于 Spring 容器中,如果存在,则自动配置当前 Bean。

例如:

1
2
3
4
5
6
7
8
9
@Configuration
public class MyConfig {

@Bean
@ConditionalOnBean(DataSource.class)
public JdbcTemplate jdbcTemplate(DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
}

从上面的例子中,我们可以看出当 DataSource 这个 Bean 存在时,才会进行 jdbcTemplate 的自动配置。

2.3.@ConditionalOnMissingBean

@ConditionalOnMissingBean 注解会先检查指定的名称的 Bean 在 Spring 容器中是否存在,如果不存在,则自动配置当前 Bean。

例如:

1
2
3
4
5
6
7
8
9
@Configuration
public class MyConfig {

@Bean
@ConditionalOnMissingBean(JdbcTemplate.class)
public JdbcTemplate jdbcTemplate(DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
}

从上面的例子中,我们可以看出,只有当 JdbcTemplate 这个 Bean 不存在时,才会进行 jdbcTemplate 的自动配置。

2.4.@ConditionalOnClass

@ConditionalOnClass 注解会检查指定的类是否存在于项目的 ClassPath 中,如果存在,则自动配置当前 Bean。

例如:

1
2
3
4
5
6
@Configuration
@ConditionalOnClass(name="com.alibaba.dubbo.config.RegistryConfig")
public class MyConfig {

// ...
}

从上面的例子中,我们可以看出当 com.alibaba.dubbo.config.RegistryConfig 这个类存在时,才会自动配置 MyConfig

2.5.@ConditionalOnWebApplication

@ConditionalOnWebApplication 注解会检查当前项目是否为 Web 应用程序,并且支持过滤 Servlet 和 Reactive 应用程序。

例如:

1
2
3
4
5
6
@Configuration
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
public class MyConfig {

// ...
}

从上面的例子中,我们可以看出当当前项目为 Servlet 应用程序时,才会自动配置 MyConfig

综上所述,Spring Boot 中的条件注解提供了一种优雅、灵活的使用方式以进行自动化配置,减少了冗余的代码,并大大增强了系统的可扩展性。

3.属性绑定

@ConfigurationProperties: 声明组件的属性和配置文件哪些前缀开始项进行绑定

@EnableConfigurationProperties:快速注册注解:

  • 场景:SpringBoot默认只扫描自己主程序所在的包。如果导入第三方包,即使组件上标注了 @Component、@ConfigurationProperties 注解,也没用。因为组件都扫描不进来,此时使用这个注解就可以快速进行属性绑定并把组件注册进容器

将容器中任意组件(Bean)的属性值配置文件的配置项的值进行绑定

  • 1、给容器中注册组件(@Component、@Bean)
  • 2、使用@ConfigurationProperties 声明组件和配置文件的哪些配置项进行绑定

更多注解参照:Spring注解驱动开发【1-26集】

3.1.@Value

@Value可以用于注入单个属性值,支持SpEL表达式,可以直接将配置文件中的值注入到Java对象中

例如:

  1. 创建实体类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class Dog {
    @Value("${dog.name}")
    private String name;
    @Value("${dog.age}")
    private Integer age;

    public Dog() {

    }
    //省略toString()、Get、Set方法、构造方法
    }
  2. 配置文件

1
2
dog.name=wangcai
dog.age=44
  1. 配置类

    1
    2
    3
    4
    5
    6
    7
    @Configuration
    public class AppConfig4 {
    @Bean
    public Dog dog() {
    return new Dog();
    }
    }
  2. 使用

    1
    2
    3
    4
    5
    public static void main(String[] args) {
    var applicationContext=SpringApplication.run(Boot303DemoApplication.class, args);
    Dog dog=applicationContext.getBean(Dog.class);
    System.out.println("dog = " + dog);
    }

3.2.@ConfigurationProperties

@ConfigurationProperties可以将一个或多个配置文件中的属性映射到Java对象中,也可以使用前缀来匹配属性。

例如:

  1. 创建实体类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    package com.satan.boot.bean;

    import org.springframework.boot.context.properties.ConfigurationProperties;

    @ConfigurationProperties(prefix = "user")
    public class User {

    private String name;

    private int age;

    //省略toString()、Get、Set方法、构造方法
    }
  2. 配置文件

1
2
user.name=zhangsan
user.age=12
  1. 配置类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    package com.satan.boot.config;

    import com.satan.boot.bean.Student;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.PropertySource;

    /**
    * @ClassName AppConfig3
    * @Description TODO
    * @date 2023/6/3 18:29
    * @Version 1.0
    */
    @Configuration
    public class AppConfig {
    @Bean
    public User user(){
    return new User();
    }

    //或者
    @Bean
    @ConfigurationProperties(prefix = "user")//将此注解从实体类移动到此处
    public User user(){
    return new User();
    }

    }

  2. 使用

    1
    2
    3
    4
    5
    public static void main(String[] args) {
    var applicationContext = SpringApplication.run(Boot303DemoApplication.class, args);
    User user = applicationContext.getBean(User.class);
    System.out.println("user = " + user);
    }

3.3.@EnableConfigurationProperties

@EnableConfigurationProperties用于开启@ConfigurationProperties注解的自动配置功能。

例如:

  1. 创建实体类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    package com.satan.boot.bean;

    import org.springframework.boot.context.properties.ConfigurationProperties;

    @ConfigurationProperties(prefix = "person")
    public class Person {

    private String name;

    private int age;
    //省略toString()、Get、Set方法、构造方法

    }
  2. 配置文件

1
2
person.name=zhangsan
person.age=12
  1. 配置类
1
2
3
4
5
@Configuration
@EnableConfigurationProperties(Person.class)
public class AppConfig2 {

}
  1. 使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.satan.boot;

import com.satan.boot.bean.Dog;
import com.satan.boot.bean.Person;
import com.satan.boot.bean.Student;
import com.satan.boot.bean.User;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Boot303DemoApplication {

public static void main(String[] args) {
var applicationContext = SpringApplication.run(Boot303DemoApplication.class, args);
Person person = applicationContext.getBean(Person.class);
System.out.println("person = " + person);
}

}

3.4.@PropertySource

@PropertySource用于指定加载的外部属性文件,可以使用${}表达式引用外部文件中的属性值。

例如:

  1. 创建实体类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    package com.satan.boot.bean;

    import org.springframework.boot.context.properties.ConfigurationProperties;

    /**
    * @ClassName Student
    * @Description TODO
    * @date 2023/6/3 18:13
    * @Version 1.0
    */
    @ConfigurationProperties(value = "student")
    public class Student {
    private String name;
    private int age;
    //省略toString()、Get、Set方法、构造方法

    }

  2. 配置文件

1
2
student.name=studyA
student.age=22
  1. 配置类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.satan.boot.config;

import com.satan.boot.bean.Student;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;

@Configuration
@PropertySource("classpath:hello.properties")
public class AppConfig3 {
@Bean
public Student student(){
return new Student();
}


}

  1. 使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.satan.boot;

import com.satan.boot.bean.Dog;
import com.satan.boot.bean.Person;
import com.satan.boot.bean.Student;
import com.satan.boot.bean.User;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Boot303DemoApplication {

public static void main(String[] args) {
var applicationContext = SpringApplication.run(Boot303DemoApplication.class, args);
Student student = applicationContext.getBean(Student.class);
System.out.println("student = " + student);
}

}

5、YAML配置文件

1. yaml 简介

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

  • YAML 的语法和其他高级语言类似,并且可以简单表达清单、散列表,标量等数据形态。它使用空白符号缩进和大量依赖外观的特色,特别适合用来表达或编辑数据结构、各种配置文件、倾印调试内容、文件大纲(例如:许多电子邮件标题格式和 YAML 非常接近)。

  • YAML 的配置文件后缀为 .yml.yaml

2.基本语法

  • 大小写敏感
  • 使用缩进表示层级关系
  • 缩进不允许使用 Tab,只允许空格(IDEA 除外,因为 IDEA 会自动将 Tab 转换为空格)
  • 缩进的空格数不重要,只要相同层级的元素左对齐即可
  • #:表示注释
  • 字符串无需加引号,' '" " 表示的字符串内容分别被转义不转义

3.数据类型

YAML 支持以下几种数据类型:

  • 字面量:单个、不可再分的值【date、boolean、string、number、null】
  • 对象:键值对的集合【map、object】
  • 数组:一组按次序排列的值【array、listset、queue】

3.1 .字面量

1
null: ~		# ~表示null

在 YAML 文件中,可以使用 |> 表示多行文本,如下所示:

1
2
3
message: |
Hello,
World!

在上述示例中,message 属性的值为一个多行文本,使用 | 表示。如果要忽略换行符,可以使用 >

3.2.对象

1
2
3
key:
child-key1: value1
child-key2: value2

较为复杂的对象格式,可以使用问号加一个空格代表一个复杂的 key,配合一个冒号加一个空格代表一个 value:

1
2
3
4
5
6
? 
- complexkey1
- complexkey2
:
- complexvalue1
- complexvalue2

意思即对象的属性是一个数组 [complexkey1, complexkey2],对应的值也是一个数组 [complexvalue1, complexvalue2]

3.3.数组

- 开头的行表示构成一个数组:

1
2
3
- A
- B
- C

YAML 支持多维数组,可以使用行内表示

1
key: [value1, value2, ...]

一个相对复杂的例子 List<Person> companies

1
2
3
4
5
6
7
8
9
10
companies:
-
id: 1
name: company1
price: 200W
-
id: 2
name: company2
price: 500W
- {id: 3,name: company3,price: 600W}

意思是 companies 属性是一个数组,每一个数组元素又是由 idnameprice 三个属性构成。

数组也可以使用 {流式} 的方式表示:

1
companies: [{id: 1,name: company1,price: 200W},{id: 2,name: company2,price: 500W}]

3.4.引用

&:锚点

*:别名

<<:合并到当前数据

1
2
3
4
5
6
7
defaults: &defaults
adapter: postgres
host: localhost

development:
database: myapp_development
<<: *defaults

相当于:

1
2
3
4
5
6
7
8
defaults:
adapter: postgres
host: localhost

development:
database: myapp_development
adapter: postgres
host: localhost

下面是另一个例子:

1
2
3
4
5
- &showell Steve 
- Clark
- Brian
- Oren
- *showell

转为 JavaScript 代码如下:

1
[ 'Steve', 'Clark', 'Brian', 'Oren', 'Steve' ]

3.5.例子

举一个比较全面的例子,弄清楚就好了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Component
@ConfigurationProperties(prefix = "yaml.test")
public class Yaml {
private String userName;
private Boolean boss;
private Date birth;
private Integer age;
private Other other;
private String[] interests;
private List<String> animal;
private Map<String, Object> score;
private Set<Double> salary;
private Map<String, List<Other>> allOthers;
// 省略 setter、getter

public static class Other {
private String name;
private int count;
// 省略 setter、getter
}

}

对照 application.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# yaml.test(.other)
yaml:
test:
user-name: Kun
boss: false
birth: 2021/07/26
age: 19
other:
name: hello
count: 1
interests:
- pingpang
- football
- basketball
animal: [list1,list2,list3]
score:
map1:
- object1
- object2
map2: {name: 1, count: 1}
salary: [3.14, 15926]
all-others:
string1: [{name: name1, count: 1}, {name: name2, count: 2}]
string2:
-
name: name11
count: 11
-
name: name22
count: 22
- {name: name33, count: 33}

结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
{
"userName": "Kun",
"boss": false,
"birth": "2021-07-25T16:00:00.000+00:00",
"age": 19,
"other": {
"name": "hello",
"count": 1
},
"interests": [
"pingpang",
"football",
"basketball"
],
"animal": [
"list1",
"list2",
"list3"
],
"score": {
"map1": {
"0": "object1",
"1": "object2"
},
"map2": {
"name": 1,
"count": 1
}
},
"salary": [
3.14,
15926
],
"allOthers": {
"string1": [
{
"name": "name1",
"count": 1
},
{
"name": "name2",
"count": 2
}
],
"string2": [
{
"name": "name11",
"count": 11
},
{
"name": "name22",
"count": 22
},
{
"name": "name33",
"count": 33
}
]
}
}

3.6.配置提示

自定义的类和配置文件绑定没有提示,所以我们需要手动设置。

pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<!--maven plugin:当运行 “mvn package” 进行打包时,会打包成一个可以直接运行的 JAR 文件,使用 “Java -jar” 命令就可以直接运行 -->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>

4. 细节

  • birthDay 推荐写为 birth-day

  • 文本

    • 单引号不会转义【\n 则为普通字符串显示】
    • 双引号会转义【\n会显示为换行符
  • 大文本

    • |开头,大文本写在下层,保留文本格式换行符正确显示
    • >开头,大文本写在下层,折叠换行符
  • 多文档合并

    • 使用---可以把多个yaml文档合并在一个文档中,每个文档区依然认为内容独立

5. 小技巧:lombok

简化JavaBean 开发。自动生成构造器、getter/setter、自动生成Builder模式等

1
2
3
4
5
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>compile</scope>
</dependency>

使用@Data等注解

6、日志配置

1.简介

  • Spring使用commons-logging作为内部日志,但底层日志实现是开放的。可对接其他日志框架。
    • spring5及以后 commons-logging被spring直接自己写了。
  • 支持 jul,log4j2,logback。SpringBoot 提供了默认的控制台输出配置,也可以配置输出为文件。
  • logback是默认使用的。
  • 虽然日志框架很多,但是我们不用担心,使用 SpringBoot 的默认配置就能工作的很好

SpringBoot怎么把日志默认配置好的
1、每个starter场景,都会导入一个核心场景spring-boot-starter
2、核心场景引入了日志的所用功能spring-boot-starter-logging
3、默认使用了logback + slf4j 组合作为默认底层日志
4、日志是系统一启动就要用xxxAutoConfiguration是系统启动好了以后放好的组件,后来用的。
5、日志是利用监听器机制配置好的。ApplicationListener
6、日志所有的配置都可以通过修改配置文件实现。以logging开始的所有配置。

2.日志格式

Springboot的默认日志输出类似于以下示例:

1
2
2023-06-06T13:02:33.782+08:00  INFO 7580 --- [  restartedMain] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2023-06-06T13:02:33.790+08:00 INFO 7580 --- [ restartedMain] com.satan.boot.Boot3LogTestApplication : Started Boot3LogTestApplication in 1.871 seconds (process running for 2.518)

默认输出格式:

  • 时间和日期:毫秒级精度
  • 日志级别:ERROR, WARN, INFO, DEBUG, or TRACE.
  • 进程 ID
  • —: 消息分割符
  • 线程名: 使用[]包含
  • Logger 名: 通常是产生日志的类名
  • 消息: 日志记录的内容

注意: logback 没有FATAL级别,对应的是ERROR

默认值:参照:spring-bootadditional-spring-configuration-metadata.json文件

默认输出格式值:%clr(%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd'T'HH:mm:ss.SSSXXX}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}

可修改为:

1
2
3
4
logging:
pattern:
console: '%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] %logger{15} ===> %msg%n'
file: '%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] %logger{15} ===> %msg%n'

3.记录日志

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.satan.boot.controller;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class LogController {
Logger logger= LoggerFactory.getLogger(LogController.class);

@GetMapping("/log")
public String log(){
logger.info("info log");
logger.warn("warn log");
logger.error("severe log");
return "success";
}
}

或者使用Lombok的@Slf4j注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.satan.boot.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@Slf4j
public class LogController {

@GetMapping("/log")
public String log(){
log.info("info log");
log.warn("warn log");
log.error("severe log");
return "success";
}
}

4.日志级别

  • 由低到高:ALL,TRACE, DEBUG, INFO, WARN, ERROR,FATAL,OFF

    • 只会打印指定级别及以上级别的日志
    • ALL:打印所有日志
    • TRACE:追踪框架详细流程日志,一般不使用
    • DEBUG:开发调试细节日志
    • INFO:关键、感兴趣信息日志
    • WARN:警告但不是错误的信息日志,比如:版本过时
    • ERROR业务错误日志,比如出现各种异常
    • FATAL:致命错误日志,比如jvm系统崩溃
    • OFF:关闭所有日志记录
  • 不指定级别的所有类,都使用root指定的级别作为默认级别

  • SpringBoot日志默认级别是 INFO

  1. 在application.properties/yaml中配置logging.level.<logger-name>=<level>指定日志级别

    1
    2
    3
    logging:
    level:
    com.satan.boot.controller: info

    此时控制台只能打印info级别及高于info级别以上的日志

  2. level可取值范围:TRACE, DEBUG, INFO, WARN, ERROR, FATAL, or OFF,定义在 LogLevel类中

  3. root 的logger-nameroot,可以配置logging.level.root=warn,代表所有未指定日志级别都使用 root 的 warn 级别

5.日志分组

比较有用的技巧是:

将相关的logger分组在一起,统一配置。SpringBoot 也支持。比如:Tomcat 相关的日志统一设置

1
2
logging.group.tomcat=org.apache.catalina,org.apache.coyote,org.apache.tomcat
logging.level.tomcat=trace

SpringBoot 预定义两个组

Name Loggers
web org.springframework.core.codec, org.springframework.http, org.springframework.web, org.springframework.boot.actuate.endpoint.web, org.springframework.boot.web.servlet.ServletContextInitializerBeans
sql org.springframework.jdbc.core, org.hibernate.SQL, org.jooq.tools.LoggerListener

日志分组机制允许您对不同的模块或组件生成的日志信息进行分类,并且可以为每个分类设置不同的日志级别和输出目标。这样,您就可以更加灵活地控制和管理您的应用程序日志信息。

下面是一个使用日志分组机制的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
logging:
group:
myapp:
- com.example.package1
- com.example.package2
mylib:
- com.example.lib1
- com.example.lib2
level:
myapp: DEBUG
mylib: INFO
file:
myapp: /var/log/myapp.log
mylib: /var/log/mylib.log

上面的示例定义了两个日志分组,分别是myapp和mylib。myapp分组包括了两个包:com.example.package1和com.example.package2;mylib分组包括了两个包:com.example.lib1和com.example.lib2。同时,为每个分组分别设置了不同的日志级别和输出目标。对于myapp分组,设置了DEBUG级别并将日志输出到/var/log/myapp.log文件;对于mylib分组,设置了INFO级别并将日志输出到/var/log/mylib.log文件。

6.文件输出

SpringBoot 默认只把日志写在控制台,如果想额外记录到文件,可以在application.properties中添加logging.file.name or logging.file.path配置项。

logging.file.name logging.file.path 示例 效果
未指定 未指定 仅控制台输出
指定 未指定 my.log 写入指定文件。可以加路径
未指定 指定 /var/log 写入指定目录,文件名为spring.log
指定 指定 以logging.file.name为准

logging.file.name:设置日志文件的名称,该文件位于应用程序的工作目录中。

logging.file.path:指定存储日志文件的完整路径,包括文件名。如果同时指定了 logging.file.pathlogging.file.name,则优先使用 logging.file.path

以下是使用这些配置项的示例:

1
2
3
logging.file.name=myapp.log # 在工作目录下创建名为 myapp.log 的日志文件

logging.file.path=/var/log/myapp/myapp.log # 将日志文件保存到 /var/log/myapp 目录下,并将其命名为 myapp.log

7.文件归档与滚动切割

归档:每天的日志单独存到一个文档中。
切割:每个文件10MB,超过大小切割成另外一个文件。

  1. 每天的日志应该独立分割出来存档。如果使用logback(SpringBoot 默认整合),可以通过application.properties/yaml文件指定日志滚动规则。
  2. 如果是其他日志系统,需要自行配置(添加log4j2.xml或log4j2-spring.xml)
  3. 支持的滚动规则设置如下
配置项 描述
logging.logback.rollingpolicy.file-name-pattern 日志存档的文件名格式(默认值:${LOG_FILE}.%d{yyyy-MM-dd}.%i.gz)
logging.logback.rollingpolicy.clean-history-on-start 应用启动时是否清除以前存档(默认值:false)
logging.logback.rollingpolicy.max-file-size 存档前,每个日志文件的最大大小(默认值:10MB)
logging.logback.rollingpolicy.total-size-cap 日志文件被删除之前,可以容纳的最大大小(默认值:0B)。设置1GB则磁盘存储超过 1GB 日志后就会删除旧日志文件
logging.logback.rollingpolicy.max-history 日志文件保存的最大天数(默认值:7).

8.自定义配置

通常我们配置 application.properties 就够了。当然也可以自定义。比如:

日志系统 自定义
Logback logback-spring.xml, logback-spring.groovy, logback.xml, or logback.groovy
Log4j2 log4j2-spring.xml or log4j2.xml
JDK (Java Util Logging) logging.properties

如果可能,我们建议您在日志配置中使用-spring 变量(例如,logback-spring.xml 而不是 logback.xml)。如果您使用标准配置文件,spring 无法完全控制日志初始化。

最佳实战:自己要写配置,配置文件名加上 xx-spring.xml

  • 创建 Logback 配置文件

在项目的 src/main/resources 目录下创建一个 logback-spring.xml 文件(并编写自定义的 Logback 配置。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 引入spirng boot默认的logback配置文件 -->
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>

<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- 采用Spring boot中默认的控制台彩色日志输出模板 -->
<encoder>
<pattern>${CONSOLE_LOG_PATTERN}</pattern>
</encoder>
</appender>

<root level="INFO">
<appender-ref ref="STDOUT" />
</root>
</configuration>

上述配置定义了一个名为 CONSOLE 的 Appender,将日志输出到控制台。同时定义了一个名为 ROOT 的 Logger,将日志级别设为 INFO,输出到 CONSOLE Appender 中。具体的日志格式可以根据自己的需求进行调整。

  • 配置 Spring Boot 应用程序

在 application.properties 或 application.yml 配置文件中指定使用自定义的 Logback 配置文件:

1
logging.config=classpath:logback-spring.xml
  • 自定义配置常用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
<?xml version="1.0" encoding="UTF-8"?>
<!-- 日志级别从低到高分为TRACE < DEBUG < INFO < WARN < ERROR < FATAL,比如: 如果设置为WARN,则低于WARN的信息都不会输出 -->
<!-- configuration标签下的三个属性 -->
<!-- scan:当此属性设置为true时,配置文档如果发生改变,将会被重新加载,默认值为true -->
<!-- scanPeriod:设置监测配置文档是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒。当scan为true时,此属性生效。默认的时间间隔为1分钟 -->
<!-- debug:当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false -->
<configuration scan="true" scanPeriod="10 seconds">
<contextName>logback-test</contextName>

<!-- 1.property标签用来定义变量值-->
<!-- name的值是变量的名称,value的值时变量定义的值。通过定义的值会被插入到logger上下文中。定义后,可以使“${}”来使用变量 -->
<property name="log.path" value="/opt/test/log"/>

<!-- 2.日志格式和颜色渲染 -->
<!-- 彩色日志依赖的渲染类 -->
<conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter" />
<conversionRule conversionWord="wex" converterClass="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter" />
<conversionRule conversionWord="wEx" converterClass="org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter" />
<!-- 彩色日志格式 -->
<property name="CONSOLE_LOG_PATTERN" value="${CONSOLE_LOG_PATTERN:-%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/>

<!-- 3.appender标签用于写日志的组件 -->
<!-- 把日志输出到控制台 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<!--此日志appender是为开发使用,只配置最底级别,控制台输出的日志级别是大于或等于此级别的日志信息-->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>debug</level>
</filter>
<!-- 日志格式化 -->
<encoder>
<Pattern>${CONSOLE_LOG_PATTERN}</Pattern>
<!-- 设置字符集 -->
<charset>UTF-8</charset>
</encoder>
</appender>

<!-- 滚动记录文件-->
<!-- level为 DEBUG 日志,时间滚动输出 -->
<appender name="DEBUG_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 正在记录的日志文档的路径及文档名 -->
<file>${log.path}/debug.log</file>
<!--日志文档输出格式-->
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
<charset>UTF-8</charset> <!-- 设置字符集 -->
</encoder>
<!-- 日志记录器的滚动策略,按日期,按大小记录 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 日志归档 -->
<fileNamePattern>${log.path}/debug-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>100MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<!-- 日志文档保留天数 -->
<maxHistory>15</maxHistory>
<!-- 限制日志文件总容量 -->
<totalSizeCap>10GB</totalSizeCap>
</rollingPolicy>
<!-- 此日志文档只记录debug级别的 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>debug</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>

<!-- level为 INFO 日志,时间滚动输出 -->
<appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 正在记录的日志文档的路径及文档名 -->
<file>${log.path}/info.log</file>
<!--日志文档输出格式-->
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
<!-- 日志记录器的滚动策略,按日期,按大小记录 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 每天日志归档路径以及格式 -->
<fileNamePattern>${log.path}/info-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>100MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<!--日志文档保留天数-->
<maxHistory>15</maxHistory>
</rollingPolicy>
<!-- 此日志文档只记录info级别的 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>info</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>

<!-- level为 WARN 日志,时间滚动输出 -->
<appender name="WARN_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 正在记录的日志文档的路径及文档名 -->
<file>${log.path}/warn.log</file>
<!--日志文档输出格式-->
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
<charset>UTF-8</charset> <!-- 此处设置字符集 -->
</encoder>
<!-- 日志记录器的滚动策略,按日期,按大小记录 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${log.path}/warn-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>100MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<!--日志文档保留天数-->
<maxHistory>15</maxHistory>
</rollingPolicy>
<!-- 此日志文档只记录warn级别的 -->
<!-- 级别拦截器如果事件的级别等于配置的级别,则过滤器接受或拒绝该事件,具体取决于onMatch和onMismatch属性的配置。-->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>warn</level>
<!-- 上面的级别放行-->
<onMatch>ACCEPT</onMatch>
<!-- 没抓到上面级别的就拦截-->
<onMismatch>DENY</onMismatch>
</filter>
</appender>

<!-- level为 ERROR 日志,时间滚动输出 -->
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 正在记录的日志文档的路径及文档名 -->
<file>${log.path}/error.log</file>
<!--日志文档输出格式-->
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
<charset>UTF-8</charset> <!-- 此处设置字符集 -->
</encoder>
<!-- 日志记录器的滚动策略,按日期,按大小记录 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${log.path}/error-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>100MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<!--日志文档保留天数-->
<maxHistory>15</maxHistory>
</rollingPolicy>
<!-- 此日志文档只记录ERROR级别的 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>

<!-- 所有 除了DEBUG级别的其它高于DEBUG的 日志,记录到一个文件 -->
<appender name="ALL_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 正在记录的日志文档的路径及文档名 -->
<file>${log.path}/all.log</file>
<!--日志文档输出格式-->
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
<charset>UTF-8</charset> <!-- 此处设置字符集 -->
</encoder>
<!-- 日志记录器的滚动策略,按日期,按大小记录 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${log.path}/all-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>100MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<!--日志文档保留天数-->
<maxHistory>15</maxHistory>
</rollingPolicy>
<!-- 此日志文档记录除了DEBUG级别的其它高于DEBUG的 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>DEBUG</level>
<!--抓到该级别的就拦截-->
<onMatch>DENY</onMatch>
<!-- 上面没抓到的就放行-->
<onMismatch>ACCEPT</onMismatch>
</filter>
</appender>

<!--
<logger>用来设置某一个包或者具体的某一个类的日志打印级别、以及指定<appender>。<logger>仅有一个name属性,一个可选的level和一个可选的addtivity属性。
name:用来指定受此logger约束的某一个包或者具体的某一个类。
level:用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF,
还有一个特殊值INHERITED或者同义词NULL,代表强制执行上级的级别。
如果未设置此属性,那么当前logger将会继承上级的级别。
addtivity:是否向上级logger传递打印信息。默认是true。
<logger name="org.springframework.web" level="info"/>
<logger name="org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor" level="INFO"/>
-->

<!--
root配置必须在appender下边
root节点是必选节点,用来指定最基础的日志输出级别,只有一个level属性
level:用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF,
不能设置为INHERITED或者同义词NULL。默认是DEBUG
可以包含零个或多个元素,标识这个appender将会添加到这个logger。
-->

<!-- 最终的策略:基本策略(root级) + 根据profile在启动时, logger标签中定制化package日志级别(优先级高于上面的root级)-->
<springProfile name="dev">
<!-- 大于等于info级别的才会输出 -->
<root level="info">
<appender-ref ref="CONSOLE" />
<appender-ref ref="DEBUG_FILE" />
<appender-ref ref="INFO_FILE" />
<appender-ref ref="WARN_FILE" />
<appender-ref ref="ERROR_FILE" />
<appender-ref ref="ALL_FILE" />
</root>
<logger name="com.shen.test" level="debug"/> <!-- 开发环境, 指定某包日志为debug级 -->
</springProfile>

<springProfile name="test">
<root level="info">
<appender-ref ref="CONSOLE" />
<appender-ref ref="DEBUG_FILE" />
<appender-ref ref="INFO_FILE" />
<appender-ref ref="WARN_FILE" />
<appender-ref ref="ERROR_FILE" />
<appender-ref ref="ALL_FILE" />
</root>
<logger name="com.shen.test" level="info"/> <!-- 测试环境, 指定某包日志为info级 -->
</springProfile>

<springProfile name="pro">
<root level="info">
<!-- 生产环境最好不配置console写文件 -->
<appender-ref ref="DEBUG_FILE" />
<appender-ref ref="INFO_FILE" />
<appender-ref ref="WARN_FILE" />
<appender-ref ref="ERROR_FILE" />
<appender-ref ref="ALL_FILE" />
</root>
<logger name="com.shen.test" level="warn"/> <!-- 生产环境, 指定某包日志为warn级 -->
<logger name="com.shen.test.MyApplication" level="info"/> <!-- 特定某个类打印info日志, 比如application启动成功后的提示语 -->
</springProfile>

</configuration>

9. 切换日志组合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>

log4j2支持yaml和json格式的配置文件

格式 依赖 文件名
YAML com.fasterxml.jackson.core:jackson-databind + com.fasterxml.jackson.dataformat:jackson-dataformat-yaml log4j2.yaml + log4j2.yml
JSON com.fasterxml.jackson.core:jackson-databind log4j2.json + log4j2.jsn

10. 最佳实战

  1. 导入任何第三方框架,先排除它的日志包,因为Boot底层控制好了日志
  2. 修改 application.properties 配置文件,就可以调整日志的所有行为。如果不够,可以编写日志框架自己的配置文件放在类路径下就行,比如logback-spring.xmllog4j2-spring.xml
  3. 如需对接专业日志系统,也只需要把 logback 记录的日志灌倒 kafka之类的中间件,这和SpringBoot没关系,都是日志框架自己的配置,修改配置文件即可
  4. 业务中使用slf4j-api记录日志。不要再 sout 了

二、web开发

1.web场景

1.1.自动配置

1、整合web场景

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

2、引入了 autoconfigure功能

1
spring-boot-starter-web->spring-boot-starter->spring-boot-autoconfigure

3、@EnableAutoConfiguration注解使用@Import(AutoConfigurationImportSelector.class)批量导入组件

4、加载 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件中配置的所有组件

5、所有自动配置类如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration
org.springframework.boot.autoconfigure.web.embedded.EmbeddedWebServerFactoryCustomizerAutoConfiguration
====以下是响应式web场景和现在的没关系======
org.springframework.boot.autoconfigure.web.reactive.HttpHandlerAutoConfiguration
org.springframework.boot.autoconfigure.web.reactive.ReactiveMultipartAutoConfiguration
org.springframework.boot.autoconfigure.web.reactive.ReactiveWebServerFactoryAutoConfiguration
org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration
org.springframework.boot.autoconfigure.web.reactive.WebSessionIdResolverAutoConfiguration
org.springframework.boot.autoconfigure.web.reactive.error.ErrorWebFluxAutoConfiguration
org.springframework.boot.autoconfigure.web.reactive.function.client.ClientHttpConnectorAutoConfiguration
org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration
================以上没关系=================
org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration
org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration
org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration
org.springframework.boot.autoconfigure.web.servlet.HttpEncodingAutoConfiguration
org.springframework.boot.autoconfigure.web.servlet.MultipartAutoConfiguration
org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration

6、绑定了配置文件的一堆配置项

  • 1、SpringMVC的所有配置 spring.mvc
  • 2、Web场景通用配置 spring.web
  • 3、文件上传配置 spring.servlet.multipart
  • 4、服务器的配置 server: 比如:编码方式

1.2.默认效果

默认配置:

  1. 包含了 ContentNegotiatingViewResolver 和 BeanNameViewResolver 组件,方便视图解析
  2. 默认的静态资源处理机制: 静态资源放在 static 文件夹下即可直接访问
  3. 自动注册Converter,GenericConverter,Formatter组件,适配常见数据类型转换格式化需求
  4. 支持 HttpMessageConverters,可以方便返回json等数据类型
  5. 注册 MessageCodesResolver,方便国际化及错误消息处理
  6. 支持 静态 index.html
  7. 自动使用ConfigurableWebBindingInitializer,实现消息处理、数据绑定、类型转化、数据校验等功能

重要:

  • 如果想保持 boot mvc 的默认配置*,并且自定义更多的 mvc 配置,如:interceptors*, formatters*,* view controllers** *等。可以使用@Configuration*注解添加一个 WebMvcConfigurer 类型的配置类,并不要标注 @EnableWebMvc
  • 如果想保持 boot mvc 的默认配置,但要自定义核心组件实例,比如:*RequestMappingHandlerMapping,* RequestMappingHandlerAdapter, 或ExceptionHandlerExceptionResolver,给容器中放一个 WebMvcRegistrations 组件即可
  • 如果想全面接管 Spring MVC,@Configuration 标注一个配置类,并加上 @EnableWebMvc**注解,实现 WebMvcConfigurer 接口

2.WebMvcAutoConfiguration原理

2.1.生效条件

1
2
3
4
5
6
7
8
9
10
@AutoConfiguration(
after = {DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class, ValidationAutoConfiguration.class}
)
@ConditionalOnWebApplication(
type = Type.SERVLET
)
@ConditionalOnClass({Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class})
@ConditionalOnMissingBean({WebMvcConfigurationSupport.class})
@AutoConfigureOrder(-2147483638)
public class WebMvcAutoConfiguration {

这段代码是 Spring Boot 中自动配置 Web MVC 的核心类

  • @AutoConfiguration(after={...}):表明这个自动配置类在另外三个自动配置类之后启用。具体来说,它会在使用 DispatcherServletAutoConfigurationTaskExecutionAutoConfigurationValidationAutoConfiguration 进行自动配置之后再进行自动配置。
  • @ConditionalOnWebApplication(type=Type.SERVLET):表明这个自动配置类只会在 Servlet 容器中运行时才生效。
  • @ConditionalOnClass({Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class}):表明这个自动配置类只有在 Servlet、DispatcherServlet 和 WebMvcConfigurer 这些类都被加载时才会生效。
  • @ConditionalOnMissingBean({WebMvcConfigurationSupport.class}):表明这个自动配置类只有在没有 WebMvcConfigurationSupport 类型的 Bean 被注册时才会生效。
  • @AutoConfigureOrder(-2147483638):表明这个自动配置类的执行顺序(优先级)为 -2147483638,即比大多数自动配置类优先级都高。

2.2.效果

1.WebMvcAutoConfiguration.javaz中放了两个Filter:

a.HiddenHttpMethodFilter;页面表单提交Rest请求(GET、POST、PUT、DELETE)

1
2
3
4
5
6
7
8
9
@Bean
@ConditionalOnMissingBean({HiddenHttpMethodFilter.class})
@ConditionalOnProperty(
prefix = "spring.mvc.hiddenmethod.filter",
name = {"enabled"}
)
public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {
return new OrderedHiddenHttpMethodFilter();
}

这个方法旨在自动配置隐藏 HTTP 方法过滤器,它会检查 Spring 上下文中是否已经存在一个 HiddenHttpMethodFilter 类型的 Bean,如果不存在,则基于条件创建一个 OrderedHiddenHttpMethodFilter Bean 对象。其中,OrderedHiddenHttpMethodFilter 是 Spring 提供的用于将 PUT、DELETE 等 HTTP 方法伪装成正常的 HttpMethod 的过滤器,可以在 Spring Web MVC 应用中使用。

b.FormContentFilter: 表单内容Filter,GET(数据放URL后面)、POST(数据放请求体)请求可以携带数据,PUT、DELETE 的请求体数据会被忽略

1
2
3
4
5
6
7
8
9
10
@Bean
@ConditionalOnMissingBean({FormContentFilter.class})
@ConditionalOnProperty(
prefix = "spring.mvc.formcontent.filter",
name = {"enabled"},
matchIfMissing = true
)
public OrderedFormContentFilter formContentFilter() {
return new OrderedFormContentFilter();
}

这个方法旨在自动配置表单内容过滤器,它会检查 Spring 上下文中是否已经存在一个 FormContentFilter 类型的 Bean,如果不存在,则基于条件创建一个 OrderedFormContentFilter Bean 对象。其中,OrderedFormContentFilter 是一个过滤器,用于将请求体数据转换成参数映射,方便处理表单数据。如果你没有自定义过滤器,可以直接使用此自动配置功能。

2.给容器中放了WebMvcConfigurer组件;给SpringMVC添加各种定制功能
  1. 所有的功能最终会和配置文件进行绑定
  2. WebMvcProperties: spring.mvc配置文件
  3. WebProperties: spring.web配置文件
1
2
3
4
5
6
7
@Configuration(proxyBeanMethods = false)
@Import(EnableWebMvcConfiguration.class) //额外导入了其他配置
@EnableConfigurationProperties({ WebMvcProperties.class, WebProperties.class })
@Order(0)
public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer, ServletContextAware{

}

该类的作用是实现 Spring MVC 的自动配置,并提供一些默认的 MVC 配置。具体来说,该类包括以下几个方面的功能:

  1. 引入 EnableWebMvcConfiguration 类,以提供基本的 Spring MVC 配置;
  2. 启用 WebMvcPropertiesWebProperties 配置属性类,以自动配置 Spring MVC 的一些属性;
  3. 实现 WebMvcConfigurer 接口,以提供更多的 MVC 配置选项和定制化;
  4. 实现 ServletContextAware 接口,以与 Servlet 上下文进行交互;
  5. 使用 @Order(0) 注解指定此自动配置类的执行顺序为 0,即在其他自动配置类之前。

2.3. WebMvcConfigurer接口

提供了配置SpringMVC底层的所有组件入口

image-20230607154122714

以下是这些回调方法的具体说明:

  • configurePathMatch(PathMatchConfigurer configurer):配置路径匹配选项;
  • configureContentNegotiation(ContentNegotiationConfigurer configurer):配置内容协商选项;
  • configureAsyncSupport(AsyncSupportConfigurer configurer):配置异步请求支持;
  • configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer):配置 DefaultServlet 处理;
  • addFormatters(FormatterRegistry registry):添加格式化器;
  • addInterceptors(InterceptorRegistry registry):添加拦截器;
  • addResourceHandlers(ResourceHandlerRegistry registry):添加静态资源处理器;
  • addCorsMappings(CorsRegistry registry):添加 CORS 映射;
  • addViewControllers(ViewControllerRegistry registry):添加视图控制器;
  • configureViewResolvers(ViewResolverRegistry registry):配置视图解析器;
  • addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers):添加自定义的参数解析器;
  • addReturnValueHandlers(List<HandlerMethodReturnValueHandler> handlers):添加自定义的返回值处理器;
  • configureMessageConverters(List<HttpMessageConverter<?>> converters):配置消息转换器;
  • extendMessageConverters(List<HttpMessageConverter<?>> converters):扩展消息转换器;
  • configureHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers):配置异常解析器;
  • extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers):扩展异常解析器;
  • getValidator():获取 Validator 实例;
  • getMessageCodesResolver():获取 MessageCodesResolver 实例。

2.4.静态资源规则源码

位置:WebMvcAutoConfiguration.class->WebMvcAutoConfigurationAdapter静态内部类->addResourceHandlers方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void addResourceHandlers(ResourceHandlerRegistry registry) {
if (!this.resourceProperties.isAddMappings()) {
logger.debug("Default resource handling disabled");
} else {
this.addResourceHandler(registry, "/webjars/**", "classpath:/META-INF/resources/webjars/");
this.addResourceHandler(registry, this.mvcProperties.getStaticPathPattern(), (registration) -> {
registration.addResourceLocations(this.resourceProperties.getStaticLocations());
if (this.servletContext != null) {
ServletContextResource resource = new ServletContextResource(this.servletContext, "/");
registration.addResourceLocations(new Resource[]{resource});
}

});
}
}
  • 规则一:访问: /webjars/**路径就去 classpath:/META-INF/resources/webjars/下找资源.
    1. maven 导入依赖
  • 规则二:访问: /**路径就去 静态资源默认的四个位置找资源

    1. `classpath:/META-INF/resources/``
    2. ```classpath:/resources/`
    3. classpath:/static/
    4. classpath:/public/
  • 规则三:静态资源默认都有缓存规则的设置

    1. 所有缓存的设置,直接通过配置文件spring.web
    2. cachePeriod: 缓存周期; 多久不用找服务器要新的。 默认没有,以s为单位
    3. cacheControl: HTTP缓存控制;https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Caching
    4. useLastModified:是否使用最后一次修改。配合HTTP Cache规则

如果浏览器访问了一个静态资源 index.js,如果服务这个资源没有发生变化,下次访问的时候就可以直接让浏览器用自己缓存中的东西,而不用给服务器发请求。

位置:addResourceHandlers方法->this.addResourceHandler方法

1
2
3
4
5
6
7
8
9
10
private void addResourceHandler(ResourceHandlerRegistry registry, String pattern, Consumer<ResourceHandlerRegistration> customizer) {
if (!registry.hasMappingForPattern(pattern)) {
ResourceHandlerRegistration registration = registry.addResourceHandler(new String[]{pattern});
customizer.accept(registration);
registration.setCachePeriod(this.getSeconds(this.resourceProperties.getCache().getPeriod()));
registration.setCacheControl(this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl());
registration.setUseLastModified(this.resourceProperties.getCache().isUseLastModified());
this.customizeResourceHandlerRegistration(registration);
}
}

2.5.EnableWebMvcConfiguration 源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//SpringBoot 给容器中放 WebMvcConfigurationSupport 组件。
//我们如果自己放了 WebMvcConfigurationSupport 组件,Boot的WebMvcAutoConfiguration都会失效。
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(WebProperties.class)
public static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration implements ResourceLoaderAware
{
@Bean
public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext applicationContext, FormattingConversionService mvcConversionService, ResourceUrlProvider mvcResourceUrlProvider) {
WelcomePageHandlerMapping welcomePageHandlerMapping = new WelcomePageHandlerMapping(new TemplateAvailabilityProviders(applicationContext), applicationContext, this.getWelcomePage(), this.mvcProperties.getStaticPathPattern());
welcomePageHandlerMapping.setInterceptors(this.getInterceptors(mvcConversionService, mvcResourceUrlProvider));
welcomePageHandlerMapping.setCorsConfigurations(this.getCorsConfigurations());
return welcomePageHandlerMapping;
}
}
  1. 如果自己手动放置了一个 WebMvcConfigurationSupport 组件,在 Spring Boot 应用程序中,将会失去 Spring Boot 自动配置的功能,因为 Spring Boot 自动配置都是依赖于 WebMvcAutoConfiguration 内部的 WebMvcConfigurationSupport 来实现的。因此,如果需要在 Spring Boot 中自定义 Web MVC 配置,建议是通过继承 WebMvcConfigurerAdapter 或者 DelegatingWebMvcConfiguration 来实现。而 DelegatingWebMvcConfiguration 则更加灵活,因为它既可以使用基于 JavaConfig 的方式来配置 Spring MVC,也可以使用自动配置的方式来配置 Spring MVC。

  2. HandlerMapping: 根据请求路径 /a 找那个handler能处理请求

  3. WelcomePageHandlerMapping

    • 访问 /**路径下的所有请求,都在以前 四个静态资源路径下找,欢迎页也一样
    • index.html:只要静态资源的位置有一个 index.html页面,项目启动默认访问

2.6. 为什们容器中放一个WebMvcConfigurer就能配置底层行为

  1. WebMvcAutoConfiguration 是一个自动配置类,它里面有一个 EnableWebMvcConfiguration
1
2
@EnableConfigurationProperties({WebProperties.class})
public static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration implements ResourceLoaderAware {
  1. EnableWebMvcConfiguration继承与 DelegatingWebMvcConfiguration,这两个都生效
1
2
3
4
@Configuration(
proxyBeanMethods = false
)
public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {
  1. DelegatingWebMvcConfiguration利用 DI 把容器中 所有 WebMvcConfigurer 注入进来
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {
private final WebMvcConfigurerComposite configurers = new WebMvcConfigurerComposite();

public DelegatingWebMvcConfiguration() {
}

@Autowired(
required = false
)
public void setConfigurers(List<WebMvcConfigurer> configurers) {
if (!CollectionUtils.isEmpty(configurers)) {
this.configurers.addWebMvcConfigurers(configurers);
}

}
  1. 别人调用 DelegatingWebMvcConfiguration 的方法配置底层规则,而它调用所有 WebMvcConfigurer的配置底层方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
protected void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
this.configurers.configureDefaultServletHandling(configurer);
}

protected void addFormatters(FormatterRegistry registry) {
this.configurers.addFormatters(registry);
}

protected void addInterceptors(InterceptorRegistry registry) {
this.configurers.addInterceptors(registry);
}

protected void addResourceHandlers(ResourceHandlerRegistry registry) {
this.configurers.addResourceHandlers(registry);
}

2.7.WebMvcConfigurationSupport

提供了很多的默认设置。

判断系统中是否有相应的类:如果有,就加入相应的HttpMessageConverter

1
2
3
4
jackson2Present = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", classLoader) &&
ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", classLoader);
jackson2XmlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", classLoader);
jackson2SmilePresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.smile.SmileFactory", classLoader);

3.静态资源

3.1.默认规则

3.1.1.静态资源映射

静态资源映射规则在 WebMvcAutoConfiguration 中进行了定义:

  1. /webjars/** 的所有路径 资源都在 classpath:/META-INF/resources/webjars/

  2. /** 的所有路径 资源都在 classpath:/META-INF/resources/classpath:/resources/classpath:/static/classpath:/public/

  3. 所有静态资源都定义了缓存规则。【浏览器访问过一次,就会缓存一段时间】,但此功能参数无默认值

    • period: 缓存间隔。 默认 0S;
    • cacheControl:缓存控制。 默认无;
    • useLastModified:是否使用lastModified头。 默认 false;

3.1.2. 静态资源缓存

如前面所述

  1. 所有静态资源都定义了缓存规则。【浏览器访问过一次,就会缓存一段时间】,但此功能参数无默认值

    • period: 缓存间隔。 默认 0S;
    • cacheControl:缓存控制。 默认无;
    • useLastModified:是否使用lastModified头。 默认 false;

3.1.3. 欢迎页

欢迎页规则在 WebMvcAutoConfiguration 中进行了定义:

  • 静态资源目录下找 index.html
  • 没有就在 templates下找index模板页

3.1.4. Favicon

  • 在静态资源目录下找 favicon.ico

3.1.5.缓存实验

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
server.port=9000

#1、spring.web:
# 1.配置国际化的区域信息
# 2.静态资源策略(开启、处理链、缓存)

#开启静态资源映射规则
spring.web.resources.add-mappings=true

#设置缓存
#spring.web.resources.cache.period=3600
##缓存详细合并项控制,覆盖period配置:
## 浏览器第一次请求服务器,服务器告诉浏览器此资源缓存7200秒,7200秒以内的所有此资源访问不用发给服务器请求,7200秒以后发请求给服务器
spring.web.resources.cache.cachecontrol.max-age=7200
#使用资源 last-modified 时间,来对比服务器和浏览器的资源是否相同没有变化。相同返回 304
spring.web.resources.cache.use-last-modified=true

3.2.自定义静态规则

自定义静态资源路径、自定义缓存规则

3.2.1.配置方式

spring.mvc: 静态资源访问前缀路径

spring.web

  • 静态资源目录
  • 静态资源缓存策略
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#1、spring.web:
# 1.配置国际化的区域信息
# 2.静态资源策略(开启、处理链、缓存)

#开启静态资源映射规则
spring.web.resources.add-mappings=true

#设置缓存
spring.web.resources.cache.period=3600
##缓存详细合并项控制,覆盖period配置:
## 浏览器第一次请求服务器,服务器告诉浏览器此资源缓存7200秒,7200秒以内的所有此资源访问不用发给服务器请求,7200秒以后发请求给服务器
spring.web.resources.cache.cachecontrol.max-age=7200
## 共享缓存
spring.web.resources.cache.cachecontrol.cache-public=true
#使用资源 last-modified 时间,来对比服务器和浏览器的资源是否相同没有变化。相同返回 304
spring.web.resources.cache.use-last-modified=true

#自定义静态资源文件夹位置
spring.web.resources.static-locations=classpath:/a/,classpath:/b/,classpath:/static/

#2、 spring.mvc
## 2.1. 自定义webjars路径前缀
spring.mvc.webjars-path-pattern=/wj/**
## 2.2. 静态资源访问路径前缀
spring.mvc.static-path-pattern=/static/**

3.2.2.代码方式

容器中只要有一个 WebMvcConfigurer 组件。配置的底层行为都会生效
@EnableWebMvc //禁用boot的默认配置

方式一:
1
2
3
4
5
6
7
8
9
10
11
12
13
//@EnableWebMvc//禁用springboot默认配置
@Configuration
public class MyConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
//保留以前规则
WebMvcConfigurer.super.addResourceHandlers(registry);
registry.addResourceHandler("/static/**")
.addResourceLocations("classpath:/static/")
.setCacheControl(CacheControl.maxAge(1180, TimeUnit.SECONDS));
;
}
}

这段代码是定义一个Spring MVC配置类 MyConfig,实现了 WebMvcConfigurer 接口,并在其中重写了接口中的 addResourceHandlers 方法。在该方法中,先调用了 WebMvcConfigurer 接口的默认实现,然后添加了新的静态资源处理规则:

  • /static开头的 URL 映射到 classpath:/static/ 目录下

  • 且设置缓存时间为 1180 秒

  • 在使用 @EnableWebMvc 注解时,Spring Boot 会自动导入 WebMvcAutoConfiguration 类中的默认配置。同时,如果需要自定义配置,只需要创建一个带有 @Configuration 注解的类,实现 WebMvcConfigurer 接口,并重写其中的方法即可。

  • 需要注意的是,在使用 @EnableWebMvc 注解开启 Spring MVC 后,默认配置将失效,因此需要手动配置所有相关的特性和组件,包括拦截器、异常处理、视图解析等等。所以,建议在不需要进行自定义配置的情况下,尽量避免使用该注解。

方式二:
1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
public class MyConfig2 {
@Bean
public WebMvcConfigurer webMvcConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
WebMvcConfigurer.super.addResourceHandlers(registry);
registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static/");
}
};
}
}

需要注意的是,在使用自定义的 WebMvcConfigurer Bean 时,不需要使用 @EnableWebMvc 注解开启 Spring MVC 的相关配置。

4.路径匹配

pring5.3 之后加入了更多的请求路径匹配的实现策略;

以前只支持AntPathMatcher策略, 现在提供了 PathPatternParser 策略。并且可以让我们指定到底使用那种策略。

4.1.Ant风格路径用法

Ant 风格的路径模式语法具有以下规则:

  • :表示任意数量的字符。
  • ?:表示任意一个字符
  • :表示任意数量的目录
  • {}:表示一个命名的模式占位符
  • []:表示字符集合,例如[a-z]表示小写字母。
  • {spring:[a-z]+}:将路径中满足 [a-z]+ 正则表达式的部分赋值给名为 spring 的路径变量

例如:

  • */user/*/detail 可以匹配 /user/123/detail 或者 /user/user_456/detail
  • ?/user/??/detail 可以匹配 /user/12/detail 或者 /user/ab/detail
  • **/user/**/detail 可以匹配 /user/123/detail/user/user_123/detail/user/x/y/z/detail 等多层级目录结构。
  • {}/user/{id}/detail 可以匹配 /user/123/detail,并且可以通过提取 id 来获取实际传入的值。
  • []/user/[a-z]/detail 可以匹配 /user/a/detail 或者 /user/g/detail,但不能匹配 /user/1/detail
  • {spring:[a-z]+}:/user/{spring:[a-z]+}/detail 可以匹配 /user/abc/detail 并将 abc 赋值给 spring 变量。注意这里必须是完全匹配才行,在 Spring MVC 中只有完全匹配才会进入 Controller 层的方法。

4.2.模式切换

AntPathMatcher 与 PathPatternParser

  • PathPatternParser 在 jmh 基准测试下,有 68 倍吞吐量提升,降低 30%40%空间分配率
  • PathPatternParser 兼容 AntPathMatcher语法,并支持更多类型的路径模式
  • PathPatternParser “**“ 多段匹配的支持仅允许在模式末尾使用**
1
2
3
4
5
6
7
 @GetMapping("/a*/b?/{p1:[a-f]+}")
public String hello(HttpServletRequest request, @PathVariable("p1") String path) {
log.info("路径变量p1: {}", path);
//获取请求路径
String uri = request.getRequestURI();
return uri;
}

总结:

  • 使用默认的路径匹配规则,是由 PathPatternParser 提供的
  • 如果路径中间需要有 **,替换成ant风格路径
1
2
3
4
# 改变路径匹配策略:
# ant_path_matcher 老版策略;
# path_pattern_parser 新版策略;
spring.mvc.pathmatch.matching-strategy=ant_path_matcher

5.内容协商

一套系统适配多端数据返回

image.png

5.1. 默认规则

  1. 基于请求头内容协商:(默认开启)

    • 客户端向服务端发送请求,携带HTTP标准的Accept请求头
    • Accept: application/jsontext/xmltext/yaml
    • 服务端根据客户端请求头期望的数据类型进行动态返回
  2. 基于请求参数内容协商:(需要开启)

    • 发送请求 GET /projects/spring-boot?format=json
    • 匹配到 @GetMapping("/projects/spring-boot")
    • 根据参数协商,优先返回 json 类型数据【需要开启参数匹配设置
    • 发送请求 GET /projects/spring-boot?format=xml,优先返回 xml 类型数据

5.2. 效果演示

请求同一个接口,可以返回json和xml不同格式数据

  1. 引入支持写出xml内容依赖
1
2
3
4
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>
  1. 标注注解
1
2
3
4
5
6
7
8
@JacksonXmlRootElement  // 可以写出为xml文档
@Data
public class Person {
private Long id;
private String userName;
private String email;
private Integer age;
}
  1. 开启基于请求参数的内容协商
1
2
3
4
# 开启基于请求参数的内容协商功能。 默认参数名:format。 默认此功能不开启
spring.mvc.contentnegotiation.favor-parameter=true
# 指定内容协商时使用的参数名。默认是 format
spring.mvc.contentnegotiation.parameter-name=type
  1. 效果

image.png

image.png

5.3.配置协商规则与支持类型

  1. 修改内容协商方式
1
2
3
4
#使用参数进行内容协商
spring.mvc.contentnegotiation.favor-parameter=true
#自定义参数名,默认为format
spring.mvc.contentnegotiation.parameter-name=myparam
  1. 大多数 MediaType 都是开箱即用的。也可以自定义内容类型,如:
1
spring.mvc.contentnegotiation.media-types.yaml=text/yaml

5.4.自定义内容返回

5.4.1.增加yaml返回支持

导入依赖

1
2
3
4
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId>
</dependency>

把对象写出成YAML

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void main(String[] args) throws JsonProcessingException {
Person person = new Person();
person.setId(1L);
person.setUserName("张三");
person.setEmail("aaa@qq.com");
person.setAge(18);

YAMLFactory factory = new YAMLFactory().disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER);
ObjectMapper mapper = new ObjectMapper(factory);

String s = mapper.writeValueAsString(person);
System.out.println(s);
}

编写配置

1
2
#新增一种媒体类型
spring.mvc.contentnegotiation.media-types.yaml=text/yaml

增加HttpMessageConverter组件,专门负责把对象写出为yaml格式

1
2
3
4
5
6
7
8
9
@Bean
public WebMvcConfigurer webMvcConfigurer(){
return new WebMvcConfigurer() {
@Override //配置一个能把对象转为yaml的messageConverter
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(new MyYamlHttpMessageConverter());
}
};
}

5.4.2. 思考:如何增加其他

  • 配置媒体类型支持:

    • spring.mvc.contentnegotiation.media-types.yaml=text/yaml
  • 编写对应的HttpMessageConverter,要告诉Boot这个支持的媒体类型

    • 按照3的示例
  • 把MessageConverter组件加入到底层

    • 容器中放一个WebMvcConfigurer 组件,并配置底层的MessageConverter

5.4.3.HttpMessageConverter的示例写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class MyYamlHttpMessageConverter extends AbstractHttpMessageConverter<Object> {

private ObjectMapper objectMapper = null; //把对象转成yaml

public MyYamlHttpMessageConverter(){
//告诉SpringBoot这个MessageConverter支持哪种媒体类型 //媒体类型
super(new MediaType("text", "yaml", Charset.forName("UTF-8")));
YAMLFactory factory = new YAMLFactory()
.disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER);
this.objectMapper = new ObjectMapper(factory);
}

@Override
protected boolean supports(Class<?> clazz) {
//只要是对象类型,不是基本类型
return true;
}

@Override //@RequestBody
protected Object readInternal(Class<?> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
return null;
}

@Override //@ResponseBody 把对象怎么写出去
protected void writeInternal(Object methodReturnValue, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {

//try-with写法,自动关流
try(OutputStream os = outputMessage.getBody()){
this.objectMapper.writeValue(os,methodReturnValue);
}

}
}

5.5. 内容协商原理-HttpMessageConverter

  • HttpMessageConverter 怎么工作?合适工作?
  • 定制 HttpMessageConverter 来实现多端内容协商
  • 编写WebMvcConfigurer提供的configureMessageConverters底层,修改底层的MessageConverter

5.5.1.@ResponseBodyHttpMessageConverter处理

标注了@ResponseBody的返回值 将会由支持它的 HttpMessageConverter写给浏览器

如果controller方法的返回值标注了 @ResponseBody 注解

1.1. 请求进来先来到DispatcherServletdoDispatch()进行处理
1.2. 找到一个 HandlerAdapter 适配器。利用适配器执行目标方法
1.3. RequestMappingHandlerAdapter来执行,调用invokeHandlerMethod()来执行目标方法
1.4. 目标方法执行之前,准备好两个东西
1.4.1. HandlerMethodArgumentResolver:参数解析器,确定目标方法每个参数值
1.4.2. HandlerMethodReturnValueHandler:返回值处理器,确定目标方法的返回值改怎么处理

1.5. RequestMappingHandlerAdapter 里面的invokeAndHandle()真正执行目标方法
1.6. 目标方法执行完成,会返回返回值对象
1.7. 找到一个合适的返回值处理器 HandlerMethodReturnValueHandler
1.8. 最终找到 RequestResponseBodyMethodProcessor能处理 标注了 @ResponseBody注解的方法
1.9. RequestResponseBodyMethodProcessor 调用writeWithMessageConverters ,利用MessageConverter把返回值写出去

上面解释:@ResponseBodyHttpMessageConverter处理

5.5.2.HttpMessageConverter先进行内容协商

  1. 遍历所有的MessageConverter看谁支持这种内容类型的数据
  2. 默认MessageConverter有以下

img
3. 最终因为要json所以MappingJackson2HttpMessageConverter支持写出json
4. jackson用ObjectMapper把对象写出去

5.5.3. WebMvcAutoConfiguration提供几种默认HttpMessageConverters

EnableWebMvcConfiguration通过 addDefaultHttpMessageConverters添加了默认的MessageConverter;如下:

  • ByteArrayHttpMessageConverter: 支持字节数据读写
  • StringHttpMessageConverter: 支持字符串读写
  • ResourceHttpMessageConverter:支持资源读写
  • ResourceRegionHttpMessageConverter: 支持分区资源写出
  • AllEncompassingFormHttpMessageConverter:支持表单xml/json读写
  • MappingJackson2HttpMessageConverter: 支持请求响应体Json读写

默认8个:

img

系统提供默认的MessageConverter 功能有限,仅用于json或者普通返回数据。额外增加新的内容协商功能,必须增加新的HttpMessageConverter

6. 模板引擎

  • 由于 SpringBoot 使用了嵌入式 Servlet 容器。所以 JSP 默认是不能使用的。
  • 如果需要服务端页面渲染,优先考虑使用 模板引擎。

image.png

模板引擎页面默认放在 src/main/resources/templates

SpringBoot 包含以下模板引擎的自动配置

  • FreeMarker
  • Groovy
  • Thymeleaf
  • Mustache

Thymeleaf官网https://www.thymeleaf.org/

1
2
3
4
5
6
7
8
9
10
11
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Good Thymes Virtual Grocery</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<link rel="stylesheet" type="text/css" media="all" th:href="@{/css/gtvg.css}" />
</head>
<body>
<p th:text="#{home.welcome}">Welcome to our grocery store!</p>
</body
</html>

6.1. Thymeleaf整合

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

自动配置原理

  1. 开启了 org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration 自动配置
  2. 属性绑定在 ThymeleafProperties 中,对应配置文件 spring.thymeleaf 内容
  3. 所有的模板页面默认在 classpath:/templates文件夹下
  4. 默认效果
    • 所有的模板页面在 classpath:/templates/下面找
    • 找后缀名为.html的页面

6.2.基础语法

6.2.1.核心用法

th:xxx:动态渲染指定的 html 标签属性值、或者th指令(遍历、判断等)

  • th:text:标签体内文本值渲染
    • th:utext:不会转义,显示为html原本的样子。
  • th:属性:标签指定属性渲染
  • th:attr:标签任意属性渲染
  • th:if``th:each``...:其他th指令
  • 例如:
1
2
3
4
<p th:text="${content}">原内容</p>
<a th:href="${url}">登录</a>
<img src="../../images/gtvglogo.png"
th:attr="src=@{/images/gtvglogo.png},title=#{logo},alt=#{logo}" />

**表达式**:用来动态取值

  • ${}:变量取值;使用model共享给页面的值都直接用${}
  • @{}:url路径;
  • #{}:国际化消息
  • ~{}:片段引用
  • *{}:变量选择:需要配合th:object绑定对象

系统工具&内置对象:详细文档

  • param:请求参数对象
  • session:session对象
  • application:application对象
  • #execInfo:模板执行信息
  • #messages:国际化消息
  • #uris:uri/url工具
  • #conversions:类型转换工具
  • #dates:日期工具,是java.util.Date对象的工具类
  • #calendars:类似#dates,只不过是java.util.Calendar对象的工具类
  • #temporals: JDK8+ **java.time** API 工具类
  • #numbers:数字操作工具
  • #strings:字符串操作
  • #objects:对象操作
  • #bools:bool操作
  • #arrays:array工具
  • #lists:list工具
  • #sets:set工具
  • #maps:map工具
  • #aggregates:集合聚合工具(sum、avg)
  • #ids:id生成工具

6.2.2. 语法示例

表达式:

  • 变量取值:${…}
  • url 取值:@{…}
  • 国际化消息:#{…}
  • 变量选择:*{…}
  • 片段引用: ~{…}

常见:

  • 文本: ‘one text’,’another one!’,…
  • 数字: 0,34,3.0,12.3,…
  • 布尔:true、false
  • null: null
  • 变量名: one,sometext,main…

文本操作:

  • 拼串: +
  • 文本替换:| The name is ${name} |

布尔操作:

  • 二进制运算: and,or
  • 取反:!,not

比较运算:

  • 比较:>,<,<=,>=(gt,lt,ge,le)
  • 等值运算:==,!=(eq,ne)

条件运算:

  • if-then: (if)?(then)
  • if-then-else: (if)?(then):(else)
  • default: (value)?:(defaultValue)

特殊语法:

  • 无操作:_

所有以上都可以嵌套组合

1
'User is of type ' + (${user.isAdmin()} ? 'Administrator' : (${user.type} ?: 'Unknown'))

6.3.属性设置

  1. th:href=”@{/product/list}”
  2. th:attr=”class=${active}”
  3. th:attr=”src=@{/images/gtvglogo.png},title=${logo},alt=#{logo}”
  4. th:checked=”${user.active}”
1
2
3
<p th:text="${content}">原内容</p>
<a th:href="${url}">登录</a>
<img src="../../images/gtvglogo.png" th:attr="src=@{/images/gtvglogo.png},title=#{logo},alt=#{logo}" />

6.4.遍历

语法: th:each="元素名,迭代状态 : ${集合}"

1
2
3
4
5
6
7
8
9
10
11
<tr th:each="prod : ${prods}">
<td th:text="${prod.name}">Onions</td>
<td th:text="${prod.price}">2.41</td>
<td th:text="${prod.inStock}? #{true} : #{false}">yes</td>
</tr>

<tr th:each="prod,iterStat : ${prods}" th:class="${iterStat.odd}? 'odd'">
<td th:text="${prod.name}">Onions</td>
<td th:text="${prod.price}">2.41</td>
<td th:text="${prod.inStock}? #{true} : #{false}">yes</td>
</tr>

iterStat 有以下属性:

  • index:当前遍历元素的索引,从0开始
  • count:当前遍历元素的索引,从1开始
  • size:需要遍历元素的总数量
  • current:当前正在遍历的元素对象
  • even(true)/odd(false):是否偶数/奇数行
  • first:是否第一个元素
  • last:是否最后一个元素

6.5.判断

th:if

1
2
3
4
5
<a
href="comments.html"
th:href="@{/product/comments(prodId=${prod.id})}"
th:if="${not #lists.isEmpty(prod.comments)}"
>view</a>

th:switch

1
2
3
4
5
<div th:switch="${user.role}">
<p th:case="'admin'">User is an administrator</p>
<p th:case="#{roles.manager}">User is a manager</p>
<p th:case="*">User is some other thing</p>
</div>

6.6. 属性优先级

  • 片段
  • 遍历
  • 判断
1
2
3
<ul>
<li th:each="item : ${items}" th:text="${item.description}">Item description here...</li>
</ul>
Order Feature Attributes
1 片段包含 th:insert th:replace
2 遍历 th:each
3 判断 th:if th:unless th:switch th:case
4 定义本地变量 th:object th:with
5 通用方式属性修改 th:attr th:attrprepend th:attrappend
6 指定属性修改 th:value th:href th:src …
7 文本值 th:text th:utext
8 片段指定 th:fragment
9 片段移除 th:remove

6.7.行内写法

[[...]] or [(...)]

1
<p>Hello, [[${session.user.name}]]!</p>

6.8.变量选择

1
2
3
4
5
<div th:object="${session.user}">
<p>Name: <span th:text="*{firstName}">Sebastian</span>.</p>
<p>Surname: <span th:text="*{lastName}">Pepper</span>.</p>
<p>Nationality: <span th:text="*{nationality}">Saturn</span>.</p>
</div>

等同于

1
2
3
4
5
<div>
<p>Name: <span th:text="${session.user.firstName}">Sebastian</span>.</p>
<p>Surname: <span th:text="${session.user.lastName}">Pepper</span>.</p>
<p>Nationality: <span th:text="${session.user.nationality}">Saturn</span>.</p>
</div

6.9. 模板布局

  • 定义模板: th:fragment
  • 引用模板:~{templatename::selector}
  • 插入模板:th:insertth:replace
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<footer th:fragment="copy">&copy; 2011 The Good Thymes Virtual Grocery</footer>

<body>
<div th:insert="~{footer :: copy}"></div>
<div th:replace="~{footer :: copy}"></div>
</body>
<body>
结果:
<body>
<div>
<footer>&copy; 2011 The Good Thymes Virtual Grocery</footer>
</div>

<footer>&copy; 2011 The Good Thymes Virtual Grocery</footer>
</body>
</body>

6.10. devtools

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>

修改页面后;ctrl+F9刷新效果;

java代码的修改,如果devtools热启动了,可能会引起一些bug,难以排查

7.国际化

国际化的自动配置参照MessageSourceAutoConfiguration

实现步骤:

  1. Spring Boot 在类路径根下查找messages资源绑定文件。文件名为:messages.properties
  2. 多语言可以定义多个消息文件,命名为messages_区域代码.properties。如:
    messages.properties:默认
    messages_zh_CN.properties:中文环境
    messages_en_US.properties:英语环境
  3. 在程序中可以自动注入 MessageSource组件,获取国际化的配置项值
  4. 在页面中可以使用表达式 #{}获取国际化的配置项值
1
2
3
4
5
6
7
8
9
10
@Autowired  //国际化取消息用的组件
MessageSource messageSource;
@GetMapping("/haha")
public String haha(HttpServletRequest request){

Locale locale = request.getLocale();
//利用代码的方式获取国际化配置文件中指定的配置项的值
String login = messageSource.getMessage("login", null, locale);
return login;
}

8.错误处理

8.1.默认处理机制

错误处理的自动配置都在ErrorMvcAutoConfiguration中,两大核心机制:

    1. SpringBoot 会自适应处理错误响应页面JSON数据
    1. SpringMVC的错误处理机制依然保留,MVC处理不了,才会交给boot进行处理

未命名绘图

发生错误以后,转发给/error路径,SpringBoot在底层写好一个 BasicErrorController的组件,专门处理这个请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE) //返回HTML
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 //返回 ResponseEntity, JSON
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);
}

错误页面是这么解析到的

1
2
3
4
//1、解析错误的自定义视图地址
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
//2、如果解析不到错误页面的地址,默认的错误页就是 error
return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);

容器中专门有一个错误视图解析器

1
2
3
4
5
6
@Bean
@ConditionalOnBean(DispatcherServlet.class)
@ConditionalOnMissingBean(ErrorViewResolver.class)
DefaultErrorViewResolver conventionErrorViewResolver() {
return new DefaultErrorViewResolver(this.applicationContext, this.resources);
}

SpringBoot解析自定义错误页的默认规则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
ModelAndView modelAndView = this.resolve(String.valueOf(status.value()), model);
if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
modelAndView = this.resolve((String)SERIES_VIEWS.get(status.series()), model);
}

return modelAndView;
}

private ModelAndView resolve(String viewName, Map<String, Object> model) {
String errorViewName = "error/" + viewName;
TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName, this.applicationContext);
return provider != null ? new ModelAndView(errorViewName, model) : this.resolveResource(errorViewName, model);
}

private ModelAndView resolveResource(String viewName, Map<String, Object> model) {
String[] var3 = this.resources.getStaticLocations();
int var4 = var3.length;

for(int var5 = 0; var5 < var4; ++var5) {
String location = var3[var5];

try {
Resource resource = this.applicationContext.getResource(location);
resource = resource.createRelative(viewName + ".html");
if (resource.exists()) {
return new ModelAndView(new HtmlResourceView(resource), model);
}
} catch (Exception var8) {
}
}

return null;
}

容器中有一个默认的名为 error 的 view; 提供了默认白页功能

1
2
3
4
5
@Bean
@ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT)
public DefaultErrorAttributes errorAttributes() {
return new DefaultErrorAttributes();
}

规则:

  1. 解析一个错误页

    • 如果发生了500、404、503、403 这些错误
      ​ 如果有模板引擎,默认在 classpath:/templates/error/精确码.html
      ​ 如果没有模板引擎,在静态资源文件夹下找 精确码.html

    • 如果匹配不到精确码.html这些精确的错误页,就去找5xx.html4xx.html模糊匹配
      ​ 如果有模板引擎,默认在 classpath:/templates/error/5xx.html
      ​ 如果没有模板引擎,在静态资源文件夹下找 5xx.html

  2. 如果模板引擎路径templates下有 error.html页面,就直接渲染

Springboot 系列(七)Spring Boot web 开发之异常错误处理机制剖析 - 掘金 (juejin.cn)

8.2.自定义页面响应

8.2.1. 自定义json响应

根据上面的 SpringBoot 错误处理原理分析,得知最终返回的 JSON 信息是从一个 map 对象中转换出来的,那么,只要能自定义 map 中的值,就可以自定义错误信息的 json 格式了。直接重写 DefaultErrorAttributes类的 getErrorAttributes 方法即可。

1
2
3
4
5
6
7
8
9
10
11
@Component
public class ErrorAttributesCustom extends DefaultErrorAttributes {
@Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
Map<String, Object> errorAttributes = super.getErrorAttributes(webRequest, options);
errorAttributes.put("code", "400");
errorAttributes.put("msg", "请检查参数");
return errorAttributes;

}
}

测试返回结果:

1
2
3
4
5
6
7
8
9
{
"timestamp": "2023-06-09T09:42:55.879+00:00",
"status": 404,
"error": "Not Found",
"message": "No message available",
"path": "/111",
"code": "400",
"msg": "请检查参数"
}

8.2.2.自定义页面响应

根据boot的错误页面规则,自定义页面模板

8.3.实战

处理系统异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@ControllerAdvice
@ResponseBody
public class GlobalExceptionHandler {

private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
/**
* 系统异常 预期以外异常
* @param ex
* @return
*/
@ExceptionHandler(Exception.class)
@ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
public JsonResult handleUnexpectedServer(Exception ex) {
logger.error("系统异常:", ex);
return new JsonResult("500", "系统发生异常,请联系管理员");
}
}

拦截自定义异常

定义异常信息

由于在业务中,有很多异常,针对不同的业务,可能给出的提示信息不同,所以为了方便项目异常信息管理,我们一般会定义一个异常信息枚举类。比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
* 业务异常提示信息枚举类
* @author shengwu ni
*/
public enum BusinessMsgEnum {
/** 参数异常 */
PARMETER_EXCEPTION("102", "参数异常!"),
/** 等待超时 */
SERVICE_TIME_OUT("103", "服务调用超时!"),
/** 参数过大 */
PARMETER_BIG_EXCEPTION("102", "输入的图片数量不能超过50张!"),
/** 500 : 一劳永逸的提示也可以在这定义 */
UNEXPECTED_EXCEPTION("500", "系统发生异常,请联系管理员!");
// 还可以定义更多的业务异常

/**
* 消息码
*/
private String code;
/**
* 消息内容
*/
private String msg;

private BusinessMsgEnum(String code, String msg) {
this.code = code;
this.msg = msg;
}
// set get方法
}

拦截自定义异常

然后我们可以定义一个业务异常,当出现业务异常时,我们就抛这个自定义的业务异常即可。比如我们定义一个 BusinessErrorException 异常,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 自定义业务异常
* @author shengwu ni
*/
public class BusinessErrorException extends RuntimeException {

private static final long serialVersionUID = -7480022450501760611L;

/**
* 异常码
*/
private String code;
/**
* 异常提示信息
*/
private String message;

public BusinessErrorException(BusinessMsgEnum businessMsgEnum) {
this.code = businessMsgEnum.code();
this.message = businessMsgEnum.msg();
}
// get set方法
}

在构造方法中,传入我们上面自定义的异常枚举类,所以在项目中,如果有新的异常信息需要添加,我们直接在枚举类中添加即可,很方便,做到统一维护,然后再拦截该异常时获取即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@ControllerAdvice
@ResponseBody
public class GlobalExceptionHandler {

private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
/**
* 拦截业务异常,返回业务异常信息
* @param ex
* @return
*/
@ExceptionHandler(BusinessErrorException.class)
@ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
public JsonResult handleBusinessError(BusinessErrorException ex) {
String code = ex.getCode();
String message = ex.getMessage();
return new JsonResult(code, message);
}
}

在业务代码中,我们可以直接模拟一下抛出业务异常,测试一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@RestController
@RequestMapping("/exception")
public class ExceptionController {

private static final Logger logger = LoggerFactory.getLogger(ExceptionController.class);

@GetMapping("/business")
public JsonResult testException() {
try {
int i = 1 / 0;
} catch (Exception e) {
throw new BusinessErrorException(BusinessMsgEnum.UNEXPECTED_EXCEPTION);
}
return new JsonResult();
}
}

运行一下项目,测试一下,返回 json 如下,说明我们自定义的业务异常捕获成功:

1
{"code":"500","msg":"系统发生异常,请联系管理员!"}

6.8.4.总结

  • 前后分离

    • 后台发生的所有错误,@ControllerAdvice + @ExceptionHandler进行统一异常处理。
  • 服务端页面渲染

    • 不可预知的一些,HTTP码表示的服务器或客户端错误

      • classpath:/templates/error/下面,放常用精确的错误码页面。500.html404.html
      • classpath:/templates/error/下面,放通用模糊匹配的错误码页面。 5xx.html4xx.html
    • 发生业务错误

      • 核心业务,每一种错误,都应该代码控制,跳转到自己定制的错误页
      • 通用业务classpath:/templates/error.html页面,显示错误信息

页面,JSON,可用的Model数据如下

img

9.嵌入式容器

Servlet容器:管理、运行Servlet组件(Servlet、Filter、Listener)的环境,一般指服务器

9.1. 自动配置原理

  • SpringBoot 默认嵌入Tomcat作为Servlet容器。
  • 自动配置类ServletWebServerFactoryAutoConfigurationEmbeddedWebServerFactoryCustomizerAutoConfiguration
  • 自动配置类开始分析功能。xxxxAutoConfiguration
1
2
3
4
5
6
7
8
9
10
11
12
@AutoConfiguration
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@ConditionalOnClass(ServletRequest.class)
@ConditionalOnWebApplication(type = Type.SERVLET)
@EnableConfigurationProperties(ServerProperties.class)
@Import({ ServletWebServerFactoryAutoConfiguration.BeanPostProcessorsRegistrar.class,
ServletWebServerFactoryConfiguration.EmbeddedTomcat.class,
ServletWebServerFactoryConfiguration.EmbeddedJetty.class,
ServletWebServerFactoryConfiguration.EmbeddedUndertow.class })
public class ServletWebServerFactoryAutoConfiguration {

}
  1. ServletWebServerFactoryAutoConfiguration 自动配置了嵌入式容器场景

  2. 绑定了ServerProperties配置类,所有和服务器有关的配置 server

  3. ServletWebServerFactoryAutoConfiguration 导入了 嵌入式的三大服务器 TomcatJettyUndertow

    1. 导入 TomcatJettyUndertow 都有条件注解。系统中有这个类才行(也就是导了包)
    2. 默认 Tomcat配置生效。给容器中放 TomcatServletWebServerFactory
    3. 都给容器中 ServletWebServerFactory放了一个 web服务器工厂(造web服务器的)
    4. web服务器工厂 都有一个功能,getWebServer获取web服务器
    5. TomcatServletWebServerFactory 创建了 tomcat。
  4. ServletWebServerFactory 什么时候会创建 webServer出来。

  5. ServletWebServerApplicationContext ioc容器,启动的时候会调用创建web服务器

  6. Spring容器刷新(启动)的时候,会预留一个时机,刷新子容器。onRefresh()

  7. refresh() 容器刷新 十二大步的刷新子容器会调用 onRefresh()

1
2
3
4
5
6
7
8
9
10
@Override
protected void onRefresh() {
super.onRefresh();
try {
createWebServer();
}
catch (Throwable ex) {
throw new ApplicationContextException("Unable to start web server", ex);
}
}

Web场景的Spring容器启动,在onRefresh的时候,会调用创建web服务器的方法。

Web服务器的创建是通过WebServerFactory搞定的。容器中又会根据导了什么包条件注解,启动相关的 服务器配置,默认EmbeddedTomcat会给容器中放一个 TomcatServletWebServerFactory,导致项目启动,自动创建出Tomcat。

9.2. 自定义

img

切换服务器;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<properties>
<servlet-api.version>3.1.0</servlet-api.version>
</properties>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<!-- Exclude the Tomcat dependency -->
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Use Jetty instead -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jetty</artifactId>
</dependency>

9.3. 最佳实践

用法:

  • 修改server下的相关配置就可以修改服务器参数
  • 通过给容器中放一个ServletWebServerFactory,来禁用掉SpringBoot默认放的服务器工厂,实现自定义嵌入任意服务器

10.全面接管SpringMVC

SpringBoot 默认配置好了 SpringMVC 的所有常用特性。
如果我们需要全面接管SpringMVC的所有配置并禁用默认配置,仅需要编写一个WebMvcConfigurer配置类,并标注 @EnableWebMvc 即可。
全手动模式

  • @EnableWebMvc : 禁用默认配置
  • WebMvcConfigurer组件:定义MVC的底层行为

10.1. WebMvcAutoConfiguration 到底自动配置了哪些规则

SpringMVC自动配置场景给我们配置了如下所有默认行为

1.WebMvcAutoConfigurationweb场景的自动配置类

  • 1.1.支持RESTful的filter:HiddenHttpMethodFilter
  • 1.2.支持非POST请求,请求体携带数据:FormContentFilter
  • 1.3.导入EnableWebMvcConfiguration: - 1.3.1.RequestMappingHandlerAdapter - 1.3.2.WelcomePageHandlerMapping欢迎页功能支持(模板引擎目录、静态资源目录放index.html),项目访问/ (根目录)就默认展示这个页面. - 1.3.3.RequestMappingHandlerMapping:找每个请求由谁处理的映射关系 - 1.3.4.ExceptionHandlerExceptionResolver:默认的异常解析器 - 1.3.5.LocaleResolver:国际化解析器 - 1.3.6.ThemeResolver:主题解析器 - 1.3.7.FlashMapManager:临时数据共享 - 1.3.8.FormattingConversionService: 数据格式化 、类型转化 - 1.3.9.Validator: 数据校验JSR303提供的数据校验功能 - 1.3.10.WebBindingInitializer:请求参数的封装与绑定 - 1.3.11.ContentNegotiationManager:内容协商管理器
  • 1.4.WebMvcAutoConfigurationAdapter配置生效,它是一个WebMvcConfigurer,定义mvc底层组件 - 1.4.1.定义好 WebMvcConfigurer 底层组件默认功能;所有功能详见下列表 - 1.4.2.视图解析器:InternalResourceViewResolver - 1.4.3.视图解析器:BeanNameViewResolver,视图名(controller方法的返回值字符串)就是组件名 - 1.4.4.内容协商解析器:ContentNegotiatingViewResolver - 1.4.5.请求上下文过滤器:RequestContextFilter: 任意位置直接获取当前请求 - 1.4.6.静态资源链规则 - 1.4.7.ProblemDetailsExceptionHandler:错误详情
    -    1.4.7.1.`SpringMVC`内部场景异常被它捕获:
    
  • 1.5.定义了MVC默认的底层行为: WebMvcConfigurer

10.2. @EnableWebMvc 禁用默认行为

  1. @EnableWebMvc给容器中导入 DelegatingWebMvcConfiguration组件,他是 WebMvcConfigurationSupport
  2. WebMvcAutoConfiguration有一个核心的条件注解, @ConditionalOnMissingBean(WebMvcConfigurationSupport.class),容器中没有WebMvcConfigurationSupportWebMvcAutoConfiguration才生效.
  3. @EnableWebMvc 导入 WebMvcConfigurationSupport 导致 WebMvcAutoConfiguration 失效。导致禁用了默认行为
  • @EnableWebMVC 禁用了 MVC 的自动配置
  • WebMvcConfigurer 定义SpringMVC底层组件的功能类

10.3. WebMvcConfigurer 功能

定义扩展SpringMVC底层功能

提供方法 核心参数 功能 默认
addFormatters FormatterRegistry 格式化器:支持属性上@NumberFormat和@DatetimeFormat的数据类型转换 GenericConversionService
getValidator 数据校验:校验 Controller 上使用@Valid标注的参数合法性。需要导入starter-validator
addInterceptors InterceptorRegistry 拦截器:拦截收到的所有请求
configureContentNegotiation ContentNegotiationConfigurer 内容协商:支持多种数据格式返回。需要配合支持这种类型的HttpMessageConverter 支持 json
configureMessageConverters List<HttpMessageConverter<?>> 消息转换器:标注@ResponseBody的返回值会利用MessageConverter直接写出去 8 个,支持byte,string,multipart,resource,json
addViewControllers ViewControllerRegistry 视图映射:直接将请求路径与物理视图映射。用于无 java 业务逻辑的直接视图页渲染 无 <mvc:view-controller>
configureViewResolvers ViewResolverRegistry 视图解析器:逻辑视图转为物理视图 ViewResolverComposite
addResourceHandlers ResourceHandlerRegistry 静态资源处理:静态资源路径映射、缓存控制 ResourceHandlerRegistry
configureDefaultServletHandling DefaultServletHandlerConfigurer 默认 Servlet:可以覆盖 Tomcat 的DefaultServlet。让DispatcherServlet拦截/
configurePathMatch PathMatchConfigurer 路径匹配:自定义 URL 路径匹配。可以自动为所有路径加上指定前缀,比如 /api
configureAsyncSupport AsyncSupportConfigurer 异步支持 TaskExecutionAutoConfiguration
addCorsMappings CorsRegistry 跨域
addArgumentResolvers List<HandlerMethodArgumentResolver> 参数解析器 mvc 默认提供
addReturnValueHandlers List<HandlerMethodReturnValueHandler> 返回值解析器 mvc 默认提供
configureHandlerExceptionResolvers List<HandlerExceptionResolver> 异常处理器 默认 3 个 ExceptionHandlerExceptionResolver ResponseStatusExceptionResolver DefaultHandlerExceptionResolver
getMessageCodesResolver 消息码解析器:国际化使用

10.4. 最佳实践

SpringBoot 已经默认配置好了Web开发场景常用功能。我们直接使用即可。

三种方式

方式 用法 效果
全自动 直接编写控制器逻辑 全部使用自动配置默认效果
手自一体 @Configuration + 配置WebMvcConfigurer+ 配置 WebMvcRegistrations 不要标注 @EnableWebMvc 保留自动配置效果 手动设置部分功能 定义MVC底层组件
全手动 @Configuration + 配置WebMvcConfigurer 标注 @EnableWebMvc 禁用自动配置效果 全手动设置

总结:

给容器中写一个配置类@Configuration实现 WebMvcConfigurer但是不要标注 @EnableWebMvc注解,实现手自一体的效果。

两种模式

1、前后分离模式@RestController 响应JSON数据
2、前后不分离模式:@Controller + Thymeleaf模板引擎

11.web新特性

11.1. Problemdetails

RFC 7807: https://www.rfc-editor.org/rfc/rfc7807

错误信息返回新格式

原理

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration(proxyBeanMethods = false)
//配置过一个属性 spring.mvc.problemdetails.enabled=true
@ConditionalOnProperty(prefix = "spring.mvc.problemdetails", name = "enabled", havingValue = "true")
static class ProblemDetailsErrorHandlingConfiguration {

@Bean
@ConditionalOnMissingBean(ResponseEntityExceptionHandler.class)
ProblemDetailsExceptionHandler problemDetailsExceptionHandler() {
return new ProblemDetailsExceptionHandler();
}

}
  1. ProblemDetailsExceptionHandler 是一个 @ControllerAdvice集中处理系统异常
  2. 处理以下异常。如果系统出现以下异常,会被SpringBoot支持以 RFC 7807规范方式返回错误数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@ExceptionHandler({
HttpRequestMethodNotSupportedException.class, //请求方式不支持
HttpMediaTypeNotSupportedException.class,
HttpMediaTypeNotAcceptableException.class,
MissingPathVariableException.class,
MissingServletRequestParameterException.class,
MissingServletRequestPartException.class,
ServletRequestBindingException.class,
MethodArgumentNotValidException.class,
NoHandlerFoundException.class,
AsyncRequestTimeoutException.class,
ErrorResponseException.class,
ConversionNotSupportedException.class,
TypeMismatchException.class,
HttpMessageNotReadableException.class,
HttpMessageNotWritableException.class,
BindException.class
})

效果

默认响应错误的json。状态码 405

1
2
3
4
5
6
7
8
{
"timestamp": "2023-04-18T11:13:05.515+00:00",
"status": 405,
"error": "Method Not Allowed",
"trace": "org.springframework.web.HttpRequestMethodNotSupportedException: Request method 'POST' is not supported\r\n\tat org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping.handleNoMatch(RequestMappingInfoHandlerMapping.java:265)\r\n\tat org.springframework.web.servlet.handler.AbstractHandlerMethodMapping.lookupHandlerMethod(AbstractHandlerMethodMapping.java:441)\r\n\tat org.springframework.web.servlet.handler.AbstractHandlerMethodMapping.getHandlerInternal(AbstractHandlerMethodMapping.java:382)\r\n\tat org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping.getHandlerInternal(RequestMappingInfoHandlerMapping.java:126)\r\n\tat org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping.getHandlerInternal(RequestMappingInfoHandlerMapping.java:68)\r\n\tat org.springframework.web.servlet.handler.AbstractHandlerMapping.getHandler(AbstractHandlerMapping.java:505)\r\n\tat org.springframework.web.servlet.DispatcherServlet.getHandler(DispatcherServlet.java:1275)\r\n\tat org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1057)\r\n\tat org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:974)\r\n\tat org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1011)\r\n\tat org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:914)\r\n\tat jakarta.servlet.http.HttpServlet.service(HttpServlet.java:563)\r\n\tat org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:885)\r\n\tat jakarta.servlet.http.HttpServlet.service(HttpServlet.java:631)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:205)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149)\r\n\tat org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149)\r\n\tat org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)\r\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149)\r\n\tat org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)\r\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149)\r\n\tat org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)\r\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149)\r\n\tat org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:166)\r\n\tat org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90)\r\n\tat org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:493)\r\n\tat org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:115)\r\n\tat org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93)\r\n\tat org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74)\r\n\tat org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:341)\r\n\tat org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:390)\r\n\tat org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63)\r\n\tat org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:894)\r\n\tat org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1741)\r\n\tat org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52)\r\n\tat org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191)\r\n\tat org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)\r\n\tat org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)\r\n\tat java.base/java.lang.Thread.run(Thread.java:833)\r\n",
"message": "Method 'POST' is not supported.",
"path": "/list"
}

开启ProblemDetails返回, 使用新的MediaType

1
spring.mvc.problemdetails.enabled=true

Content-Type: application/problem+json + 额外扩展返回

img

1
2
3
4
5
6
7
{
"type": "about:blank",
"title": "Method Not Allowed",
"status": 405,
"detail": "Method 'POST' is not supported.",
"instance": "/list"
}

11.2.函数式Web

SpringMVC 5.2 以后 允许我们使用函数式的方式,定义Web的请求处理流程

函数式接口

Web请求处理的方式:

  1. @Controller + @RequestMapping耦合式路由业务耦合)
  2. 函数式Web:分离式(路由、业务分离)

11.2.1.场景

场景:User RESTful - CRUD

  • GET /user/1 获取1号用户
  • GET /users 获取所有用户
  • POST /user 请求体携带JSON,新增一个用户
  • PUT /user/1 请求体携带JSON,修改1号用户
  • DELETE /user/1 删除1号用户

11.2.2. 核心类

  • RouterFunction : 定义路由信息。发送什么请求,谁来处理
  • RequestPredicate : 定义请求谓语。请求方式(GET、POST)、请求参数
  • ServerRequest : 封装请求完整数据
  • ServerResponse : 封装响应完整数据

11.2.3. 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.web.servlet.function.RequestPredicate;
import org.springframework.web.servlet.function.RouterFunction;
import org.springframework.web.servlet.function.ServerResponse;

import static org.springframework.web.servlet.function.RequestPredicates.accept;
import static org.springframework.web.servlet.function.RouterFunctions.route;

@Configuration(proxyBeanMethods = false)
public class MyRoutingConfiguration {

private static final RequestPredicate ACCEPT_JSON = accept(MediaType.APPLICATION_JSON);

@Bean
public RouterFunction<ServerResponse> routerFunction(MyUserHandler userHandler) {
return route()
.GET("/{user}", ACCEPT_JSON, userHandler::getUser)
.GET("/{user}/customers", ACCEPT_JSON, userHandler::getUserCustomers)
.DELETE("/{user}", ACCEPT_JSON, userHandler::deleteUser)
.build();
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.function.ServerRequest;
import org.springframework.web.servlet.function.ServerResponse;

@Component
public class MyUserHandler {

public ServerResponse getUser(ServerRequest request) {
...
return ServerResponse.ok().build();
}

public ServerResponse getUserCustomers(ServerRequest request) {
...
return ServerResponse.ok().build();
}

public ServerResponse deleteUser(ServerRequest request) {
...
return ServerResponse.ok().build();
}
}

三、数据访问

1.整合SSM场景

SpringBoot 整合 SpringSpringMVCMyBatis 进行数据访问场景开发

1.1.创建SSM整合项目

1
2
3
4
5
6
7
8
9
10
11
<!-- https://mvnrepository.com/artifact/org.mybatis.spring.boot/mybatis-spring-boot-starter -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>

1.2.配置数据源

1
2
3
4
5
spring.datasource.url=jdbc:mysql://192.168.200.100:3306/demo
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.type=com.zaxxer.hikari.HikariDataSource

安装MyBatisX 插件,帮我们生成Mapper接口的xml文件即可

1.3. 配置MyBatis

1
2
3
4
#指定mapper映射文件位置
mybatis.mapper-locations=classpath:/mapper/*.xml
#参数项调整
mybatis.configuration.map-underscore-to-camel-case=true

1.4. CRUD编写

  • 编写Bean
  • 编写Mapper
  • 使用mybatisx插件,快速生成MapperXML
  • 测试CRUD

1.5. 自动配置原理

SSM整合总结:

  1. 导入 mybatis-spring-boot-starter

  2. 配置数据源信息

  3. 配置mybatis的**mapper接口扫描****xml映射文件扫描**

  4. 编写bean,mapper,生成xml,编写sql 进行crud。事务等操作依然和Spring中用法一样

  5. 效果:

    1. 所有sql写在xml中
    2. 所有mybatis配置写在application.properties下面

jdbc场景的自动配置

  • mybatis-spring-boot-starter导入 spring-boot-starter-jdbc,jdbc是操作数据库的场景
  • Jdbc场景的几个自动配置
    • org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
      • 数据源的自动配置
      • 所有和数据源有关的配置都绑定在DataSourceProperties
      • 默认使用 HikariDataSource
    • org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration
      • 给容器中放了JdbcTemplate操作数据库
    • org.springframework.boot.autoconfigure.jdbc.JndiDataSourceAutoConfiguration
    • org.springframework.boot.autoconfigure.jdbc.XADataSourceAutoConfiguration
      • 基于XA二阶提交协议的分布式事务数据源
    • org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration
      • 支持事务
  • 具有的底层能力:数据源、JdbcTemplate事务

MyBatisAutoConfiguration:配置了MyBatis的整合流程

  • mybatis-spring-boot-starter导入 mybatis-spring-boot-autoconfigure(mybatis的自动配置包)
  • 默认加载两个自动配置类:
    • org.mybatis.spring.boot.autoconfigure.MybatisLanguageDriverAutoConfiguration
    • org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration
      • 必须在数据源配置好之后才配置
      • 给容器中SqlSessionFactory组件。创建和数据库的一次会话
      • 给容器中SqlSessionTemplate组件。操作数据库
    • MyBatis的所有配置绑定在MybatisProperties
    • 每个Mapper接口代理对象是怎么创建放到容器中。详见@MapperScan原理:
      • 利用@Import(MapperScannerRegistrar.class)批量给容器中注册组件。解析指定的包路径里面的每一个类,为每一个Mapper接口类,创建Bean定义信息,注册到容器中。

如何分析哪个场景导入以后,开启了哪些自动配置类。
找:classpath:/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件中配置的所有值,就是要开启的自动配置类,但是每个类可能有条件注解,基于条件注解判断哪个自动配置类生效了。

1.6. 快速定位生效的配置

1
2
3
#开启调试模式,详细打印开启了哪些自动配置
debug=true
# Positive(生效的自动配置) Negative(不生效的自动配置)

1.7. 扩展:整合其他数据源

Druid 数据源:

暂不支持 SpringBoot3

  • 导入druid-starter
  • 写配置
  • 分析自动配置了哪些东西,怎么用

Druid官网:https://github.com/alibaba/druid

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#数据源基本配置
spring.datasource.url=jdbc:mysql://192.168.200.100:3306/demo
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource

# 配置StatFilter监控
spring.datasource.druid.filter.stat.enabled=true
spring.datasource.druid.filter.stat.db-type=mysql
spring.datasource.druid.filter.stat.log-slow-sql=true
spring.datasource.druid.filter.stat.slow-sql-millis=2000
# 配置WallFilter防火墙
spring.datasource.druid.filter.wall.enabled=true
spring.datasource.druid.filter.wall.db-type=mysql
spring.datasource.druid.filter.wall.config.delete-allow=false
spring.datasource.druid.filter.wall.config.drop-table-allow=false
# 配置监控页,内置监控页面的首页是 /druid/index.html
spring.datasource.druid.stat-view-servlet.enabled=true
spring.datasource.druid.stat-view-servlet.login-username=admin
spring.datasource.druid.stat-view-servlet.login-password=admin
spring.datasource.druid.stat-view-servlet.allow=*

# 其他 Filter 配置不再演示
# 目前为以下 Filter 提供了配置支持,请参考文档或者根据IDE提示(spring.datasource.druid.filter.*)进行配置。
# StatFilter
# WallFilter
# ConfigFilter
# EncodingConvertFilter
# Slf4jLogFilter
# Log4jFilter
# Log4j2Filter
# CommonsLogFilter

1.8.附录

1
2
3
4
5
6
7
8
9
CREATE TABLE `t_user`
(
`id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '编号',
`login_name` VARCHAR(200) NULL DEFAULT NULL COMMENT '用户名称' COLLATE 'utf8_general_ci',
`nick_name` VARCHAR(200) NULL DEFAULT NULL COMMENT '用户昵称' COLLATE 'utf8_general_ci',
`passwd` VARCHAR(200) NULL DEFAULT NULL COMMENT '用户密码' COLLATE 'utf8_general_ci',
PRIMARY KEY (`id`)
);
insert into t_user(login_name, nick_name, passwd) VALUES ('zhangsan','张三','123456');

四、SpringBoot3-基础特性

1.SpringApplication

1.1.自定义 banner

  1. 类路径添加banner.txt或设置spring.banner.location就可以定制 banner
  2. 推荐网站:Spring Boot banner 在线生成工具,制作下载英文 banner.txt,修改替换 banner.txt 文字实现自定义,个性化启动 banner-bootschool.net

1.2.自定义 SpringApplication

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import org.springframework.boot.Banner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class MyApplication {

public static void main(String[] args) {
SpringApplication application = new SpringApplication(MyApplication.class);
application.setBannerMode(Banner.Mode.OFF);
application.run(args);
}

}

1.3. FluentBuilder API

1
2
3
4
5
new SpringApplicationBuilder()
.sources(Application.class)
.child(Application.class)
.bannerMode(Banner.Mode.OFF)
.run(args);

2. Profiles

环境隔离能力;快速切换开发、测试、生产环境

步骤:

  1. 标识环境:指定哪些组件、配置在哪个环境生效
  2. 切换环境:这个环境对应的所有组件和配置就应该生效

2.1. 使用

2.1.1 指定环境

  • Spring Profiles 提供一种隔离配置的方式,使其仅在特定环境生效;
  • 任何@Component, @Configuration 或 @ConfigurationProperties 可以使用 @Profile 标记,来指定何时被加载。【容器中的组件都可以被 @Profile标记】

2.1.2 环境激活

  1. 配置激活指定环境; 配置文件
1
spring.profiles.active=production,hsqldb
  1. 也可以使用命令行激活。–spring.profiles.active=dev,hsqldb
  2. 还可以配置默认环境; 不标注@Profile 的组件永远都存在。
    1. 以前默认环境叫default
    2. spring.profiles.default=test
  3. 推荐使用激活方式激活指定环境

2.1.3 环境包含

不管激活那个环境,这个都得有。总是要生效的环境

注意:

  1. spring.profiles.active 和spring.profiles.default 只能用到 无 profile 的文件中,如果在application-dev.yaml中编写就是无效的
  2. 也可以额外添加生效文件,而不是激活替换。比如:
1
2
spring.profiles.include[0]=common
spring.profiles.include[1]=local

最佳实战:

  • 生效的环境 = 激活的环境/默认环境 + 包含的环境
  • 项目里面这么用
    • 基础的配置mybatislogxxx:写到包含环境中
    • 需要动态切换变化的 dbredis:写到激活的环境中

2.2. Profile 分组

创建prod组,指定包含db和mq配置

1
2
spring.profiles.group.prod[0]=db
spring.profiles.group.prod[1]=mq

使用–spring.profiles.active=prod ,就会激活prod,db,mq配置文件

2.3. Profile 配置文件

  • application-{profile}.properties可以作为指定环境的配置文件
  • 激活这个环境,配置就会生效。最终生效的所有配置
    • application.properties:主配置文件,任意时候都生效
    • application-{profile}.properties:指定环境配置文件,激活指定环境生效

profile优先级 > application

2.4.YML分段配置示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
spring:
profiles:
active: test
---
spring:
config:
activate:
on-profile: dev
server:
port: 8080

---
spring:
config:
activate:
on-profile: test
server:
port: 8081

3.外部化配置

场景:线上应用如何快速修改配置,并应用最新配置

  • SpringBoot 使用 配置优先级 + 外部配置 简化配置更新、简化运维。
  • 只需要给jar应用所在的文件夹放一个application.properties最新配置文件,重启项目就能自动应用最新配置

3.1. 配置优先级

Spring Boot 允许将配置外部化,以便可以在不同的环境中使用相同的应用程序代码。

我们可以使用各种外部配置源,包括Java Properties文件、YAML文件、环境变量和命令行参数。

@Value可以获取值,也可以用@ConfigurationProperties将所有属性绑定到java object中

以下是 SpringBoot 属性源加载顺序。后面的会覆盖前面的值**。由低到高,高优先级配置覆盖低优先级

  1. 默认属性(通过SpringApplication.setDefaultProperties指定的)
  2. @PropertySource指定加载的配置(需要写在@Configuration类上才可生效)
  3. 配置文件(application.properties/yml等)
  4. RandomValuePropertySource支持的random.*配置(如:@Value(“${random.int}”))
  5. OS 环境变量
  6. Java 系统属性(System.getProperties())
  7. JNDI 属性(来自java:comp/env)
  8. ServletContext 初始化参数
  9. ServletConfig 初始化参数
  10. SPRING_APPLICATION_JSON属性(内置在环境变量或系统属性中的 JSON)
  11. 命令行参数
  12. 测试属性。(@SpringBootTest进行测试时指定的属性)
  13. 测试类@TestPropertySource注解
  14. Devtools 设置的全局属性。($HOME/.config/spring-boot)

结论:配置可以写到很多位置,常见的优先级顺序:

  • 命令行> 配置文件> springapplication配置

配置文件优先级如下:(后面覆盖前面)

  1. jar 包内的application.properties/yml
  2. jar 包内的application-{profile}.properties/yml
  3. jar 包外的application.properties/yml
  4. jar 包外的application-{profile}.properties/yml

建议用一种格式的配置文件如果.properties和.yml同时存在,则.properties优先

结论:包外 > 包内; 同级情况:profile配置 > application配置

所有参数均可由命令行传入,使用--参数项=参数值,将会被添加到环境变量中,并优先于配置文件

比如java -jar app.jar --name="Spring",可以使用@Value("${name}")获取

演示场景:

  • 包内: application.properties server.port=8000
  • 包内: application-dev.properties server.port=9000
  • 包外: application.properties server.port=8001
  • 包外: application-dev.properties server.port=9001

启动端口?:命令行 > 9001 > 8001 > 9000 > 8000

3.2. 外部配置

SpringBoot 应用启动时会自动寻找application.properties和application.yaml位置,进行加载。顺序如下:(后面覆盖前面

  1. 类路径: 内部

    1. 类根路径
    2. 类下/config包
  2. 当前路径(项目所在的位置)

    1. 当前路径
    2. 当前下/config子目录
    3. /config目录的直接子目录

最终效果:优先级由高到低,前面覆盖后面

  • 命令行 > 包外config直接子目录 > 包外config目录 > 包外根目录 > 包内目录
  • 同级比较:
    • profile配置 > 默认配置
    • properties配置 > yaml配置

未命名绘图

规律:最外层的最优先。

  • 命令行 > 所有
  • 包外 > 包内
  • config目录 > 根目录
  • profile > application
    配置不同就都生效(互补),配置相同高优先级覆盖低优先级

3.3. 导入配置

使用spring.config.import可以导入额外配置

1
2
spring.config.import=my.properties
my.property=value

无论以上写法的先后顺序,my.properties的值总是优先于直接在文件中编写的my.property。

3.4. 属性占位符

配置文件中可以使用 ${name:default}形式取出之前配置过的值。

1
2
app.name=MyApp
app.description=${app.name} is a Spring Boot application written by ${username:Unknown}

4.单元测试-JUnit5

4.1. 整合

SpringBoot 提供一系列测试工具集及注解方便我们进行测试。

spring-boot-test提供核心测试能力,spring-boot-test-autoconfigure 提供测试的一些自动配置。

我们只需要导入spring-boot-starter-test 即可整合测试

1
2
3
4
5
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

spring-boot-starter-test 默认提供了以下库供我们测试使用

4.2. 测试

4.2.1 组件测试

直接@Autowired容器中的组件进行测试

4.2.2 注解

JUnit5的注解与JUnit4的注解有所变化

https://junit.org/junit5/docs/current/user-guide/#writing-tests-annotations

  • @Test :表示方法是测试方法。但是与JUnit4的@Test不同,他的职责非常单一不能声明任何属性,拓展的测试将会由Jupiter提供额外测试
  • @ParameterizedTest :表示方法是参数化测试,下方会有详细介绍
  • @RepeatedTest :表示方法可重复执行,下方会有详细介绍
  • @DisplayName :为测试类或者测试方法设置展示名称
  • @BeforeEach :表示在每个单元测试之前执行
  • @AfterEach :表示在每个单元测试之后执行
  • @BeforeAll :表示在所有单元测试之前执行
  • @AfterAll :表示在所有单元测试之后执行
  • @Tag :表示单元测试类别,类似于JUnit4中的@Categories
  • @Disabled :表示测试类或测试方法不执行,类似于JUnit4中的@Ignore
  • @Timeout :表示测试方法运行如果超过了指定时间将会返回错误
  • @ExtendWith :为测试类或测试方法提供扩展类引用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import static org.junit.jupiter.api.Assertions.fail;
import static org.junit.jupiter.api.Assumptions.assumeTrue;

import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;

class StandardTests {

@BeforeAll
static void initAll() {
}

@BeforeEach
void init() {
}

@DisplayName("😱")
@Test
void succeedingTest() {
}

@Test
void failingTest() {
fail("a failing test");
}

@Test
@Disabled("for demonstration purposes")
void skippedTest() {
// not executed
}

@Test
void abortedTest() {
assumeTrue("abc".contains("Z"));
fail("test should have been aborted");
}

@AfterEach
void tearDown() {
}

@AfterAll
static void tearDownAll() {
}

}

4.2.3.断言

方法 说明
assertEquals 判断两个对象或两个原始类型是否相等
assertNotEquals 判断两个对象或两个原始类型是否不相等
assertSame 判断两个对象引用是否指向同一个对象
assertNotSame 判断两个对象引用是否指向不同的对象
assertTrue 判断给定的布尔值是否为 true
assertFalse 判断给定的布尔值是否为 false
assertNull 判断给定的对象引用是否为 null
assertNotNull 判断给定的对象引用是否不为 null
assertArrayEquals 数组断言
assertAll 组合断言
assertThrows 异常断言
assertTimeout 超时断言
fail 快速失败

4.2.4.嵌套测试

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
@DisplayName("A stack")
class TestingAStackDemo {

Stack<Object> stack;

@Test
@DisplayName("is instantiated with new Stack()")
void isInstantiatedWithNew() {
new 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);
}

@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());
}
}
}
}

4.2.5.参数化测试

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

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

@ValueSource: 为参数化测试指定入参来源,支持八大基础类以及String类型,Class类型
@NullSource: 表示为参数化测试提供一个null的入参
@EnumSource: 表示为参数化测试提供一个枚举入参
@CsvFileSource:表示读取指定CSV文件内容作为参数化测试入参
@MethodSource:表示读取指定方法的返回值作为参数化测试入参(注意方法返回需要是一个流)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@ParameterizedTest
@ValueSource(strings = {"one", "two", "three"})
@DisplayName("参数化测试1")
public void parameterizedTest1(String string) {
System.out.println(string);
Assertions.assertTrue(StringUtils.isNotBlank(string));
}


@ParameterizedTest
@MethodSource("method") //指定方法名
@DisplayName("方法来源参数")
public void testWithExplicitLocalMethodSource(String name) {
System.out.println(name);
Assertions.assertNotNull(name);
}

static Stream<String> method() {
return Stream.of("apple", "banana");
}

五、SpringBoot3-核心原理

1.事件和监听器

1.1生命周期监听

场景:监听应用生命周期

1.1.1.监听器-SpringApplicationRunListener

自定义SpringApplicationRunListener监听事件

  1. 编写SpringApplicationRunListener 实现类
  2. META-INF/spring.factories 中配置 org.springframework.boot.SpringApplicationRunListener=自己的Listener,还可以指定一个有参构造器,接受两个参数(SpringApplication application, String[] args)
  3. springboot 在spring-boot.jar中配置了默认的 Listener,如下:

image.png

Listener先要从META-INF/spring.factories 读到

  • 1、引导: 利用 BootstrapContext 引导整个项目启动
    • starting:应用开始,SpringApplicationrun方法一调用,只要有了 BootstrapContext 就执行
    • environmentPrepared: 环境准备好(把启动参数等绑定到环境变量中),但是ioc还没有创建;【调一次】
  • 2、启动:
    • contextPrepared:ioc容器创建并准备好,但是sources(主配置类)没加载。并关闭引导上下文;组件都没创建 【调一次】
    • contextLoaded:ioc容器加载。主配置类加载进去了。但是ioc容器还没刷新(我们的bean没创建)。
    • =======截止以前,ioc容器里面还没造bean呢=======
    • started:ioc容器刷新了(所有bean造好了),但是 runner 没调用。
    • ready: ioc容器刷新了(所有bean造好了),所有 runner 调用完了。
  • 3、运行
    • 以前步骤都正确执行,代表容器running。

1.1.2.生命周期全流程

image.png

1.1.3.测试代码

  1. 编写java代码实现SpringApplicationRunListener接口并重写接口方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class MyApplicationRunListener implements SpringApplicationRunListener {
@Override
public void starting(ConfigurableBootstrapContext bootstrapContext) {

System.out.println("=====starting=====正在启动======");
}

@Override
public void environmentPrepared(ConfigurableBootstrapContext bootstrapContext, ConfigurableEnvironment environment) {
System.out.println("=====environmentPrepared=====环境准备完成======");
}

@Override
public void contextPrepared(ConfigurableApplicationContext context) {
System.out.println("=====contextPrepared=====ioc容器准备完成======");
}

@Override
public void contextLoaded(ConfigurableApplicationContext context) {
System.out.println("=====contextLoaded=====ioc容器加载完成======");
}

@Override
public void started(ConfigurableApplicationContext context, Duration timeTaken) {
System.out.println("=====started=====启动完成======");
}

@Override
public void ready(ConfigurableApplicationContext context, Duration timeTaken) {
System.out.println("=====ready=====准备就绪======");
}

@Override
public void failed(ConfigurableApplicationContext context, Throwable exception) {
System.out.println("=====failed=====应用启动失败======");
}
}
  1. 编写rsources/META-INF/spring.factories文件
1
org.springframework.boot.SpringApplicationRunListener=com.satan.boot.listener.MyApplicationRunListener

1.2.事件触发时机

1.2.1.各种回调监听器

BootstrapRegistryInitializer感知特定阶段:感知引导初始化**

    • META-INF/spring.factories
  • 创建引导上下文bootstrapContext的时候触发。
  • application.addBootstrapRegistryInitializer();
  • 场景:进行密钥校对授权。

ApplicationContextInitializer: **感知特定阶段: 感知ioc容器初始化

  • META-INF/spring.factories
  • application.addInitializers();

ApplicationListener感知全阶段:基于事件机制,感知事件。 一旦到了哪个阶段可以做别的事

  • @Bean@EventListener事件驱动
  • SpringApplication.addListeners(…)SpringApplicationBuilder.listeners(…)
  • META-INF/spring.factories

SpringApplicationRunListener感知全阶段生命周期 + 各种阶段都能自定义操作; 功能更完善。

  • META-INF/spring.factories

ApplicationRunner感知特定阶段:感知应用就绪Ready。卡死应用,就不会就绪

  • @Bean

CommandLineRunner感知特定阶段:感知应用就绪Ready。卡死应用,就不会就绪

  • @Bean

最佳实战:

  • 如果项目启动前做事BootstrapRegistryInitializerApplicationContextInitializer
  • 如果想要在项目启动完成后做事ApplicationRunner CommandLineRunner
  • 如果要干涉生命周期做事:SpringApplicationRunListener
  • 如果想要用事件机制:ApplicationListener

1.2.2.完整触发流程

9大事件触发顺序&时机

  1. ApplicationStartingEvent:应用启动但未做任何事情, 除过注册listeners and initializers.
  2. ApplicationEnvironmentPreparedEvent: Environment 准备好,但context 未创建.
  3. ApplicationContextInitializedEvent: ApplicationContext 准备好,ApplicationContextInitializers 调用,但是任何bean未加载
  4. ApplicationPreparedEvent: 容器刷新之前,bean定义信息加载
  5. ApplicationStartedEvent: 容器刷新完成, runner未调用
    =========以下就开始插入了探针机制============
  6. AvailabilityChangeEventLivenessState.CORRECT应用存活; 存活探针
  7. ApplicationReadyEvent: 任何runner被调用
  8. AvailabilityChangeEventReadinessState.ACCEPTING_TRAFFIC就绪探针,可以接请求
  9. ApplicationFailedEvent :启动出错

image.png

应用事件发送顺序如下:

img

感知应用是否存活了:可能植物状态,虽然活着但是不能处理请求。

应用是否就绪了:能响应请求,说明确实活的比较好。

1.2.3.SpringBoot 事件驱动开发

应用启动过程生命周期事件感知(9大事件)应用运行中事件感知(无数种)

  • 事件发布ApplicationEventPublisherAware注入:ApplicationEventMulticaster
  • 事件监听组件 + @EventListener

image.png

image.png

  1. 事件发布者
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Service
public class EventPublisher implements ApplicationEventPublisherAware {

/**
* 底层发送事件用的组件,SpringBoot会通过ApplicationEventPublisherAware接口自动注入给我们
* 事件是广播出去的。所有监听这个事件的监听器都可以收到
*/
ApplicationEventPublisher applicationEventPublisher;

/**
* 所有事件都可以发
* @param event
*/
public void sendEvent(ApplicationEvent event) {
//调用底层API发送事件
applicationEventPublisher.publishEvent(event);
}

/**
* 会被自动调用,把真正发事件的底层组组件给我们注入进来
* @param applicationEventPublisher event publisher to be used by this object
*/
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
this.applicationEventPublisher = applicationEventPublisher;
}
}
  1. 自定义事件
1
2
3
4
5
6
7
8
9
public class LoginSuccessEvent extends ApplicationEvent {
public LoginSuccessEvent(Object source) {
super(source);
}

public LoginSuccessEvent(Object source, Clock clock) {
super(source, clock);
}
}
  1. 事件订阅者
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Service
public class CouponService {

@Order(1)
@EventListener
public void onEvent(LoginSuccessEvent loginSuccessEvent){
System.out.println("===== CouponService ====感知到事件"+loginSuccessEvent);
UserEntity source = (UserEntity) loginSuccessEvent.getSource();
sendCoupon(source.getUsername());
}

public void sendCoupon(String username){
System.out.println(username + " 随机得到了一张优惠券");
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Order(2)
@Service
public class AccountService implements ApplicationListener<LoginSuccessEvent> {
public void addAccountScore(String username){
System.out.println(username +" 加了1分");
}

@Override
public void onApplicationEvent(LoginSuccessEvent event) {
System.out.println("===== AccountService 收到事件 =====");

User user = (User) event.getSource();
addAccountScore(user.getUsername());
}
}
  1. controller
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@RestController
public class LoginController {

@Autowired
EventPublisher eventPublisher;


/**
* 增加业务
* @param username
* @param passwd
* @return
*/
@GetMapping("/login")
public String login(@RequestParam("username") String username,
@RequestParam("passwd")String passwd){
//业务处理登录
System.out.println("业务处理登录完成....");

//TODO 发送事件.
//1、创建事件信息
LoginSuccessEvent event = new LoginSuccessEvent(new User(username, passwd));
//2、发送事件
eventPublisher.sendEvent(event);
return username+"登录成功";
}
}

2.自动配置原理

2.1.入门理解

应用关注的三大核心场景配置组件

2.1.1.自动配置流程

img

  1. 导入starter
  2. 依赖导入autoconfigure
  3. 寻找类路径下 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件
  4. 启动,加载所有 自动配置类 xxxAutoConfiguration
    1. 给容器中配置功能组件
    2. 组件参数绑定到 属性类中。xxxProperties
    3. 属性类配置文件前缀项绑定
    4. @Contional派生的条件注解进行判断是否组件生效
  5. 效果:
    1. 修改配置文件,修改底层参数
    2. 所有场景自动配置好直接使用
    3. 可以注入SpringBoot配置好的组件随时使用

2.1.2.SPI机制

  • Java中的SPI(Service Provider Interface)是一种软件设计模式,用于在应用程序中动态地发现和加载组件SPI的思想是,定义一个接口或抽象类,然后通过在classpath中定义实现该接口的类来实现对组件的动态发现和加载。
  • SPI的主要目的是解决在应用程序中使用可插拔组件的问题。例如,一个应用程序可能需要使用不同的日志框架或数据库连接池,但是这些组件的选择可能取决于运行时的条件。通过使用SPI,应用程序可以在运行时发现并加载适当的组件,而无需在代码中硬编码这些组件的实现类。
  • 在Java中,SPI的实现方式是通过在META-INF/services目录下创建一个以服务接口全限定名为名字的文件,文件中包含实现该服务接口的类的全限定名。当应用程序启动时,Java的SPI机制会自动扫描classpath中的这些文件,并根据文件中指定的类名来加载实现类。
  • 通过使用SPI,应用程序可以实现更灵活、可扩展的架构,同时也可以避免硬编码依赖关系和增加代码的可维护性。

以上回答来自ChatGPT-3.5

在SpringBoot中,META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

2.1.3.功能开关

  • 自动配置:全部都配置好,什么都不用管。 自动批量导入

    • 项目一启动,spi文件中指定的所有都加载。
  • @EnableXxxx:手动控制哪些功能的开启; 手动导入。

    • 开启xxx功能
    • 都是利用 @Import 把此功能要用的组件导入进去

2.2.进阶理解

2.2.1.@SpringBootApplication

@SpringBootConfiguration

就是: @Configuration ,容器中的组件,配置类。spring ioc启动就会加载创建这个类对象

@EnableAutoConfiguration:开启自动配置

开启自动配置

@AutoConfigurationPackage:扫描主程序包:加载自己的组件
  • 利用 @Import(AutoConfigurationPackages.Registrar.class) 想要给容器中导入组件。
  • 把主程序所在的的所有组件导入进来。
  • 为什么SpringBoot默认只扫描主程序所在的包及其子包
@Import(AutoConfigurationImportSelector.class):加载所有自动配置类:加载starter导入的组件
1
2
List<String> configurations = ImportCandidates.load(AutoConfiguration.class, getBeanClassLoader())			
.getCandidates();

扫描SPI文件:META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

@ComponentScan

组件扫描:排除一些组件(哪些不要)
排除前面已经扫描进来的配置类、和自动配置类

1
2
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })

2.2.2.完整启动加载流程

自动配置进阶原理

3.自定义starter

场景:抽取聊天机器人场景,它可以打招呼

效果:任何项目导入此starter都具有打招呼功能,并且问候语中的人名需要可以在配置文件中修改

  1. 创建自定义starter项目,引入spring-boot-starter基础依赖
  2. 编写模块功能,引入模块所有需要的依赖。
  3. 编写xxxAutoConfiguration自动配置类,帮其他项目导入这个模块需要的所有组件
  4. 编写配置文件META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports指定启动需要加载的自动配置
  5. 其他项目引入即可使用

3.1. 业务代码

自定义配置有提示。导入以下依赖重启项目,再写配置文件就有提示

1
2
3
4
5
6
7
8
9
10
11
12
@RestController
public class RobotController {

@Autowired
RobotService robotService;

@GetMapping("/robot/hello")
public String sayHello(){
String s = robotService.sayHello();
return s;
}
}
1
2
3
public interface RobotService {
public String sayHello();
}
1
2
3
4
5
6
7
8
@Service
public class RobotServiceImpl implements RobotService {
@Autowired
RobotProperties robotProperties;
public String sayHello(){
return "你好:名字:【"+robotProperties.getName()+"】;年龄:【"+robotProperties.getAge()+"】";
}
}
1
2
3
4
5
6
7
8
9
@ConfigurationProperties(prefix = "robot")  //此属性类和配置文件指定前缀绑定
@Component
@Data
public class RobotProperties {

private String name;
private String age;
private String email;
}
1
2
3
4
5
6
//给容器中导入Robot功能要用的所有组件
@Import({RobotProperties.class, RobotServiceImpl.class, RobotController.class})
@Configuration
public class RobotAutoConfiguration {

}
1
2
3
4
5
6
<!--        导入配置处理器,配置文件自定义的properties配置都会有提示-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>

3.2.基本抽取

  • 创建starter项目,把公共代码需要的所有依赖导入

  • 把公共代码复制进来

  • 自己写一个 RobotAutoConfiguration,给容器中导入这个场景需要的所有组件

    • 为什么这些组件默认不会扫描进去?
    • starter所在的包和 引入它的项目的主程序所在的包不是父子层级
  • 别人引用这个starter,直接导入这个 RobotAutoConfiguration,就能把这个场景的组件导入进来

  • 功能生效。

  • 测试编写配置文件

3.3. 使用@EnableXxx机制

1
2
3
4
5
6
7
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Import(RobotAutoConfiguration.class)
public @interface EnableRobot {

}

3.4. 完全自动配置

  • 依赖SpringBoot的SPI机制
  • META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件中编写好我们自动配置类的全类名即可
  • 项目启动,自动加载我们的自动配置类

org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件内容:

1
com.boot.starter.robot.RobotAutoConfiguration

六、附录:SpringBoot3改变 & 新特性 快速总结

1、自动配置包位置变化【参照视频:07、11】
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

2、jakata api迁移

  • druid有问题

3、新特性 - 函数式Web、ProblemDetails【参照视频:50、51】

4、GraalVM 与 AOT【参照视频:86~93】

5、响应式编程全套【第三季:预计7~8月份发布】

6、剩下变化都是版本升级,意义不大

七、springboot3-场景整合

1.环境准备

1.1. 云服务器

  • 阿里云腾讯云华为云 服务器开通; 按量付费,省钱省心
  • 安装以下组件
    • docker
    • redis
    • kafka
    • prometheus
    • grafana

1.2.docker安装

还不会docker的同学,参考【云原生实战(10~25集)】快速入门

https://www.bilibili.com/video/BV13Q4y1C7hS?p=10

1
2
3
4
5
6
7
8
9
10
11
12
13
14
sudo yum install -y yum-utils

sudo yum-config-manager \
--add-repo \
https://download.docker.com/linux/centos/docker-ce.repo

sudo yum install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

sudo systemctl enable docker --now

#测试工作
docker ps
# 批量安装所有软件
docker compose

创建 /prod 文件夹,准备以下文件

1.3.prometheus.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
global:
scrape_interval: 15s
evaluation_interval: 15s

scrape_configs:
- job_name: 'prometheus'
static_configs:
- targets: ['localhost:9090']

- job_name: 'redis'
static_configs:
- targets: ['redis:6379']

- job_name: 'kafka'
static_configs:
- targets: ['kafka:9092']

1.4.docker-compose.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
version: '3.9'

services:
redis:
image: redis:latest
container_name: redis
restart: always
ports:
- "6379:6379"
networks:
- backend

zookeeper:
image: zookeeper
container_name: zookeeper
restart: always
environment:
ZOOKEEPER_CLIENT_PORT: 2181
ZOOKEEPER_TICK_TIME: 2000
TZ: "Asia/Shanghai"
ports:
- "2181:2181"
networks:
- backend

kafka:
image: bitnami/kafka:3.4.0
container_name: kafka
restart: always
depends_on:
- zookeeper
ports:
- "9092:9092"
environment:
ALLOW_PLAINTEXT_LISTENER: yes
KAFKA_BROKER_ID: 1
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://192.168.120.130:9092
networks:
- backend

kafka-ui:
image: provectuslabs/kafka-ui:latest
container_name: kafka-ui
restart: always
depends_on:
- kafka
ports:
- "8080:8080"
environment:
KAFKA_CLUSTERS_0_NAME: dev
KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka:9092
networks:
- backend

prometheus:
image: prom/prometheus:latest
container_name: prometheus
restart: always
volumes:
- /home/docker/data/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
ports:
- "9091:9090"
networks:
- backend

grafana:
image: grafana/grafana:latest
container_name: grafana
restart: always
depends_on:
- prometheus
ports:
- "3000:3000"
networks:
- backend

networks:
backend:
name: backend

1.5.启动环境

1
docker compose -f docker-compose.yml up -d

1.6.验证

2.redis整合

Redis不会的同学:参照 阳哥-《Redis7》 https://www.bilibili.com/video/BV13R4y1v7sP?p=1

HashMap: key:value

2.1.场景整合

依赖导入

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

配置

1
2
spring.data.redis.host=192.168.200.100
spring.data.redis.password=Lfy123!@!

测试

1
2
3
4
5
6
7
8
@Autowired
StringRedisTemplate redisTemplate;

@Test
void redisTest(){
redisTemplate.opsForValue().set("a","1234");
Assertions.assertEquals("1234",redisTemplate.opsForValue().get("a"));
}

2.2.自动配置原理

  1. META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports中导入了RedisAutoConfigurationRedisReactiveAutoConfigurationRedisRepositoriesAutoConfiguration所有属性绑定在RedisProperties

  2. RedisReactiveAutoConfiguratio属于响应式编程,不用管。RedisRepositoriesAutoConfiguration属于 JPA 操作,也不用管

  3. RedisAutoConfiguration 配置了以下组件

    • LettuceConnectionConfiguration: 给容器中注入了连接工厂LettuceConnectionFactory,和操作 redis 的客户端DefaultClientResources
    • RedisTemplate<Object, Object>: 可给 redis 中存储任意对象,会使用 jdk 默认序列化方式。
    • StringRedisTemplate: 给 redis 中存储字符串,如果要存对象,需要开发人员自己进行序列化。key-value都是字符串进行操作··

2.3.定制化

2.3.1.序列化机制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Configuration
public class AppRedisConfiguration {


/**
* 允许Object类型的key-value,都可以被转为json进行存储。
* @param redisConnectionFactory 自动配置好了连接工厂
* @return
*/
@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
//把对象转为json字符串的序列化工具
template.setDefaultSerializer(new GenericJackson2JsonRedisSerializer());
return template;
}
}

2.3.2.redis客户端

RedisTemplate、StringRedisTemplate: 操作redis的工具类

  • 要从redis的连接工厂获取链接才能操作redis

  • Redis客户端

    • Lettuce: 默认
    • Jedis:可以使用以下切换
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>

<!-- 切换 jedis 作为操作redis的底层客户端-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>

2.3.3. 配置参考

1
2
3
4
5
6
7
8
9
10
11
spring.data.redis.host=8.130.74.183
spring.data.redis.port=6379
#spring.data.redis.client-type=lettuce

#设置lettuce的底层参数
#spring.data.redis.lettuce.pool.enabled=true
#spring.data.redis.lettuce.pool.max-active=8

spring.data.redis.client-type=jedis
spring.data.redis.jedis.pool.enabled=true
spring.data.redis.jedis.pool.max-active=8

3.接口文档

Swagger 可以快速生成实时接口文档,方便前后开发人员进行协调沟通。遵循 OpenAPI 规范。

文档: https://springdoc.org/v2/

3.1. OpenAPI 3 架构

image.png

3.2.整合

导入场景

1
2
3
4
5
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.1.0</version>
</dependency>

或者导入

1
2
3
4
5
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
<version>4.1.0</version>
</dependency>

配置

1
2
3
4
5
6
7
8
# /api-docs endpoint custom path 默认 /v3/api-docs
springdoc.api-docs.path=/api-docs

# swagger 相关配置在 springdoc.swagger-ui
# swagger-ui custom path
springdoc.swagger-ui.path=/swagger-ui.html

springdoc.show-actuator=true

3.3.使用

3.3.1.常用注解

注解 标注位置 作用
@Tag controller 类 标识 controller 作用
@Parameter 参数 标识参数作用
@Parameters 参数 参数多重说明
@Schema model 层的 JavaBean 描述模型作用及每个属性
@Operation 方法 描述方法作用
@ApiResponse 方法 描述响应状态码等

3.3.2.Docket配置

如果有多个Docket,配置如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Bean
public GroupedOpenApi publicApi() {
return GroupedOpenApi.builder()
.group("springshop-public")
.pathsToMatch("/public/**")
.build();
}
@Bean
public GroupedOpenApi adminApi() {
return GroupedOpenApi.builder()
.group("springshop-admin")
.pathsToMatch("/admin/**")
.addMethodFilter(method -> method.isAnnotationPresent(Admin.class))
.build();
}

如果只有一个Docket,可以配置如下

1
2
springdoc.packagesToScan=package1, package2
springdoc.pathsToMatch=/v1, /api/balance/**

3.3.3.OpenAPI配置

1
2
3
4
5
6
7
8
9
10
11
@Bean
public OpenAPI springShopOpenAPI() {
return new OpenAPI()
.info(new Info().title("SpringShop API")
.description("Spring shop sample application")
.version("v0.0.1")
.license(new License().name("Apache 2.0").url("http://springdoc.org")))
.externalDocs(new ExternalDocumentation()
.description("SpringShop Wiki Documentation")
.url("https://springshop.wiki.github.org/docs"));
}

3.4.Springfox 迁移

3.4.1.注解变化

原注解 现注解 作用
@Api @Tag 描述Controller
@ApiIgnore @Parameter(hidden = true) @Operation(hidden = true) @Hidden 描述忽略操作
@ApiImplicitParam @Parameter 描述参数
@ApiImplicitParams @Parameters 描述参数
@ApiModel @Schema 描述对象
@ApiModelProperty(hidden = true) @Schema(accessMode = READ_ONLY) 描述对象属性
@ApiModelProperty @Schema 描述对象属性
@ApiOperation(value = “foo”, notes = “bar”) @Operation(summary = “foo”, description = “bar”) 描述方法
@ApiParam @Parameter 描述参数
@ApiResponse(code = 404, message = “foo”) @ApiResponse(responseCode = “404”, description = “foo”) 描述响应

3.4.2.Docket配置

以前写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Bean
public Docket publicApi() {
return new Docket(DocumentationType.SWAGGER_2)
.select()
.apis(RequestHandlerSelectors.basePackage("org.github.springshop.web.public"))
.paths(PathSelectors.regex("/public.*"))
.build()
.groupName("springshop-public")
.apiInfo(apiInfo());
}

@Bean
public Docket adminApi() {
return new Docket(DocumentationType.SWAGGER_2)
.select()
.apis(RequestHandlerSelectors.basePackage("org.github.springshop.web.admin"))
.paths(PathSelectors.regex("/admin.*"))
.apis(RequestHandlerSelectors.withMethodAnnotation(Admin.class))
.build()
.groupName("springshop-admin")
.apiInfo(apiInfo());
}

新写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Bean
public GroupedOpenApi publicApi() {
return GroupedOpenApi.builder()
.group("springshop-public")
.pathsToMatch("/public/**")
.build();
}
@Bean
public GroupedOpenApi adminApi() {
return GroupedOpenApi.builder()
.group("springshop-admin")
.pathsToMatch("/admin/**")
.addOpenApiMethodFilter(method -> method.isAnnotationPresent(Admin.class))
.build();
}

添加OpenAPI组件

1
2
3
4
5
6
7
8
9
10
11
@Bean
public OpenAPI springShopOpenAPI() {
return new OpenAPI()
.info(new Info().title("SpringShop API")
.description("Spring shop sample application")
.version("v0.0.1")
.license(new License().name("Apache 2.0").url("http://springdoc.org")))
.externalDocs(new ExternalDocumentation()
.description("SpringShop Wiki Documentation")
.url("https://springshop.wiki.github.org/docs"));
}

4.远程调用

RPC(Remote Procedure Call):远程过程调用

image.png

本地过程调用: a(); b(); a() { b();}: 不同方法都在同一个JVM运行

远程过程调用

  • 服务提供者:
  • 服务消费者:
  • 通过连接对方服务器进行请求\响应交互,来实现调用效果

API/SDK的区别是什么?

  • api:接口(Application Programming Interface)
    • 远程提供功能;
  • sdk:工具包(Software Development Kit)
    • 导入jar包,直接调用功能即可

开发过程中,我们经常需要调用别人写的功能

  • 如果是内部微服务,可以通过依赖cloud注册中心、openfeign等进行调用
  • 如果是外部暴露的,可以发送 http 请求、或遵循外部协议进行调用

SpringBoot 整合提供了很多方式进行远程调用

  • 轻量级客户端方式
    • RestTemplate: 普通开发
    • WebClient: 响应式编程开发
    • Http Interface: 声明式编程
  • Spring Cloud分布式解决方案方式
    • Spring Cloud OpenFeign
  • 第三方框架
    • Dubbo
    • gRPC

4.1.WebClient

4.4.1.创建与配置

发请求:

  • 请求方式: GET\POST\DELETE\xxxx
  • 请求路径: /xxx
  • 请求参数:aa=bb&cc=dd&xxx
  • 请求头: aa=bb,cc=ddd
  • 请求体:

创建 WebClient 非常简单:

  • WebClient.create()
  • WebClient.create(String baseUrl)

还可以使用 WebClient.builder() 配置更多参数项:

  • uriBuilderFactory: 自定义UriBuilderFactory ,定义 baseurl.
  • defaultUriVariables: 默认 uri 变量.
  • defaultHeader: 每个请求默认头.
  • defaultCookie: 每个请求默认 cookie.
  • defaultRequest: Consumer 自定义每个请求.
  • filter: 过滤 client 发送的每个请求
  • exchangeStrategies: HTTP 消息 reader/writer 自定义.
  • clientConnector: HTTP client 库设置.
1
2
//获取响应完整信息
WebClient client = WebClient.create("https://example.org");

4.1.2 获取响应

retrieve()方法用来声明如何提取响应数据。比如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//获取响应完整信息
WebClient client = WebClient.create("https://example.org");

Mono<ResponseEntity<Person>> result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.retrieve()
.toEntity(Person.class);

//只获取body
WebClient client = WebClient.create("https://example.org");

Mono<Person> result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToMono(Person.class);

//stream数据
Flux<Quote> result = client.get()
.uri("/quotes").accept(MediaType.TEXT_EVENT_STREAM)
.retrieve()
.bodyToFlux(Quote.class);

//定义错误处理
Mono<Person> result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.retrieve()
.onStatus(HttpStatus::is4xxClientError, response -> ...)
.onStatus(HttpStatus::is5xxServerError, response -> ...)
.bodyToMono(Person.class);

4.1.3.定义请求体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//1、响应式-单个数据
Mono<Person> personMono = ... ;

Mono<Void> result = client.post()
.uri("/persons/{id}", id)
.contentType(MediaType.APPLICATION_JSON)
.body(personMono, Person.class)
.retrieve()
.bodyToMono(Void.class);

//2、响应式-多个数据
Flux<Person> personFlux = ... ;

Mono<Void> result = client.post()
.uri("/persons/{id}", id)
.contentType(MediaType.APPLICATION_STREAM_JSON)
.body(personFlux, Person.class)
.retrieve()
.bodyToMono(Void.class);

//3、普通对象
Person person = ... ;

Mono<Void> result = client.post()
.uri("/persons/{id}", id)
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(person)
.retrieve()
.bodyToMono(Void.class);

4.2.HTTP Interface

Spring 允许我们通过定义接口的方式,给任意位置发送 http 请求,实现远程调用,可以用来简化 HTTP 远程访问。需要webflux场景才可使用。

4.2.1 导入依赖

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>

4.2.2 定义接口

1
2
3
4
5
public interface BingService {

@GetExchange(url = "/search")
String search(@RequestParam("q") String keyword);
}

4.2.3 创建代理&测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@SpringBootTest
class Boot05TaskApplicationTests {

@Test
void contextLoads() throws InterruptedException {
//1、创建客户端
WebClient client = WebClient.builder()
.baseUrl("https://cn.bing.com")
.codecs(clientCodecConfigurer -> {
clientCodecConfigurer
.defaultCodecs()
.maxInMemorySize(256*1024*1024);
//响应数据量太大有可能会超出BufferSize,所以这里设置的大一点
})
.build();
//2、创建工厂
HttpServiceProxyFactory factory = HttpServiceProxyFactory
.builder(WebClientAdapter.forClient(client)).build();
//3、获取代理对象
BingService bingService = factory.createClient(BingService.class);


//4、测试调用
Mono<String> search = bingService.search("尚硅谷");
System.out.println("==========");
search.subscribe(str -> System.out.println(str));

Thread.sleep(100000);

}

}

5.消息服务

https://kafka.apache.org/documentation/

5.1.消息队列-场景

5.1.1. 异步

img

5.1.2. 解耦

image.png

5.1.3. 削峰

image.png

5.1.4.4. 缓冲

image.png

5.2消息队列-Kafka

5.2.1.消息模式

img

5.2.2.Kafka工作原理

image.png

5.2.3. SpringBoot整合

参照:https://docs.spring.io/spring-kafka/docs/current/reference/html/#preface

1
2
3
4
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>

配置

1
spring.kafka.bootstrap-servers=172.20.128.1:9092

修改C:\Windows\System32\drivers\etc\hosts文件,配置8.130.32.70 kafka

5.2.4.消息发送

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@SpringBootTest
class Boot07KafkaApplicationTests {

@Autowired
KafkaTemplate kafkaTemplate;
@Test
void contextLoads() throws ExecutionException, InterruptedException {
StopWatch watch = new StopWatch();
watch.start();
CompletableFuture[] futures = new CompletableFuture[10000];
for (int i = 0; i < 10000; i++) {
CompletableFuture send = kafkaTemplate.send("order", "order.create."+i, "订单创建了:"+i);
futures[i]=send;
}
CompletableFuture.allOf(futures).join();
watch.stop();
System.out.println("总耗时:"+watch.getTotalTimeMillis());
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Component;

@Component
public class MyBean {

private final KafkaTemplate<String, String> kafkaTemplate;

public MyBean(KafkaTemplate<String, String> kafkaTemplate) {
this.kafkaTemplate = kafkaTemplate;
}

public void someMethod() {
this.kafkaTemplate.send("someTopic", "Hello");
}

}

5.2.5.消息监听

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Component
public class OrderMsgListener {

@KafkaListener(topics = "order",groupId = "order-service")
public void listen(ConsumerRecord record){
System.out.println("收到消息:"+record); //可以监听到发给kafka的新消息,以前的拿不到
}

@KafkaListener(groupId = "order-service-2",topicPartitions = {
@TopicPartition(topic = "order",partitionOffsets = {
@PartitionOffset(partition = "0",initialOffset = "0")
})
})
public void listenAll(ConsumerRecord record){
System.out.println("收到partion-0消息:"+record);
}
}

5.2.6.参数配置

消费者

1
2
3
spring.kafka.consumer.value-deserializer=org.springframework.kafka.support.serializer.JsonDeserializer
spring.kafka.consumer.properties[spring.json.value.default.type]=com.example.Invoice
spring.kafka.consumer.properties[spring.json.trusted.packages]=com.example.main,com.example.another

生产者

1
2
spring.kafka.producer.value-serializer=org.springframework.kafka.support.serializer.JsonSerializer
spring.kafka.producer.properties[spring.json.add.type.headers]=false

5.2.7.自动配置原理

kafka 自动配置在KafkaAutoConfiguration

  1. 容器中放了 KafkaTemplate 可以进行消息收发
  2. 容器中放了KafkaAdmin 可以进行 Kafka 的管理,比如创建 topic 等
  3. kafka 的配置在KafkaProperties中
  4. @EnableKafka可以开启基于注解的模式

6.web安全

  • Apache Shiro
  • Spring Security
  • 自研:Filter

6.1.Spring Security架构

6.1.1.认证:Authentication

who are you?
登录系统,用户系统

6.1.2. 授权:Authorization

what are you allowed to do?
权限管理,用户授权

6.1.3.攻击防护

  • XSS(Cross-site scripting)
  • CSRF(Cross-site request forgery)
  • CORS(Cross-Origin Resource Sharing)
  • SQL注入

6.1.4.扩展. 权限模型

RBAC(Role Based Access Controll)

  • 用户(t_user)

    • id,username,password,xxx
    • 1,zhangsan
    • 2,lisi
  • 用户_角色(t_user_role)【N对N关系需要中间表】

    • zhangsan, admin
    • zhangsan,common_user
    • lisi, hr
    • lisi, common_user
  • 角色(t_role)

    • id,role_name
    • admin
    • hr
    • common_user
  • 角色_权限(t_role_perm)

    • admin, 文件r
    • admin, 文件w
    • admin, 文件执行
    • admin, 订单query,create,xxx
    • hr, 文件r
  • 权限(t_permission)

    • id,perm_id
    • 文件 r,w,x
    • 订单 query,create,xxx

ACL(Access Controll List)

直接用户和权限挂钩

  • 用户(t_user)

    • zhangsan
    • lisi
  • 用户_权限(t_user_perm)

    • zhangsan,文件 r
    • zhangsan,文件 x
    • zhangsan,订单 query
  • 权限(t_permission)

    • id,perm_id
    • 文件 r,w,x
    • 订单 query,create,xxx
1
2
3
4
@Secured("文件 r")
public void readFile(){
//读文件
}

6.2.Spring Security 原理

6.2.1.过滤器链架构

Spring Security利用 FilterChainProxy 封装一系列拦截器链,实现各种安全拦截功能

Servlet三大组件:Servlet、Filter、Listener

6.2.2.FilterChainProxy

6.2.3.SecurityFilterChain

6.3.使用

6.3.1.HttpSecurity

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
@Order(SecurityProperties.BASIC_AUTH_ORDER - 10)
public class ApplicationConfigurerAdapter extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher("/match1/**")
.authorizeRequests()
.antMatchers("/match1/user").hasRole("USER")
.antMatchers("/match1/spam").hasRole("SPAM")
.anyRequest().isAuthenticated();
}
}

6.3.2.MethodSecurity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@SpringBootApplication
@EnableGlobalMethodSecurity(securedEnabled = true)
public class SampleSecureApplication {
}

@Service
public class MyService {

@Secured("ROLE_USER")
public String secure() {
return "Hello Security";
}

}

核心

  • WebSecurityConfigurerAdapter
  • @EnableGlobalMethodSecurity: 开启全局方法安全配置
    • @Secured
    • @PreAuthorize
    • @PostAuthorize
  • UserDetailService: 去数据库查询用户详细信息的service(用户基本信息、用户角色、用户权限)

6.4.实战

6.4.1.引入依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.0</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity6</artifactId>
<!-- Temporary explicit version to fix Thymeleaf bug -->
<version>3.1.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>

6.4.2.页面

6.4.2.1.首页
1
<p>Click <a th:href="@{/hello}">here</a> to see a greeting.</p>
6.4.2.2.Hello页
1
<h1>Hello</h1>
6.4.2.3.登录页
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org">
<head>
<title>Spring Security Example</title>
</head>
<body>
<div th:if="${param.error}">Invalid username and password.</div>
<div th:if="${param.logout}">You have been logged out.</div>
<form th:action="@{/login}" method="post">
<div>
<label> User Name : <input type="text" name="username" /> </label>
</div>
<div>
<label> Password: <input type="password" name="password" /> </label>
</div>
<div><input type="submit" value="Sign In" /></div>
</form>
</body>
</html>

6.4.3.配置类

6.4.3.1.视图控制
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.example.securingweb;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class MvcConfig implements WebMvcConfigurer {

public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/home").setViewName("index");
registry.addViewController("/").setViewName("index");
registry.addViewController("/hello").setViewName("hello");
registry.addViewController("/login").setViewName("login");
}
}
6.4.3.2.Security配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package com.atguigu.security.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;

/**
* @author lfy
* @Description
* @create 2023-03-08 16:54
*/
@Configuration
@EnableWebSecurity
public class WebSecurityConfig {

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {


http
.authorizeHttpRequests((requests) -> requests
.requestMatchers("/", "/home").permitAll()
.anyRequest().authenticated()
)
.formLogin((form) -> form
.loginPage("/login")
.permitAll()
)
.logout((logout) -> logout.permitAll());

return http.build();
}

@Bean
public UserDetailsService userDetailsService() {
UserDetails user =
User.withDefaultPasswordEncoder()
.username("admin")
.password("admin")
.roles("USER")
.build();

return new InMemoryUserDetailsManager(user);
}
}
6.4.3.3.改造Hello页
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!DOCTYPE html>
<html
xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="https://www.thymeleaf.org"
xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity6"
>
<head>
<title>Hello World!</title>
</head>
<body>
<h1 th:inline="text">
Hello <span th:remove="tag" sec:authentication="name">thymeleaf</span>!
</h1>
<form th:action="@{/logout}" method="post">
<input type="submit" value="Sign Out" />
</form>
</body>
</html>

7.可观测性

可观测性 Observability

对线上应用进行观测、监控、预警…

  • 健康状况【组件状态、存活状态】Health
  • 运行指标【cpu、内存、垃圾回收、吞吐量、响应成功率…】Metrics
  • 链路追踪

7.1.SpringBoot Actuator

7.1.1. 实战

7.1.1.1. 场景引入
1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
7.1.1.2.暴露指标
1
2
3
4
5
6
management:
endpoints:
enabled-by-default: true #暴露所有端点信息
web:
exposure:
include: '*' #以web方式暴露
7.1.1.3.访问数据

7.1.2. Endpoint

7.1.2.1.常用端点
ID 描述
auditevents 暴露当前应用程序的审核事件信息。需要一个AuditEventRepository组件
beans 显示应用程序中所有Spring Bean的完整列表。
caches 暴露可用的缓存。
conditions 显示自动配置的所有条件信息,包括匹配或不匹配的原因。
configprops 显示所有@ConfigurationProperties
env 暴露Spring的属性ConfigurableEnvironment
flyway 显示已应用的所有Flyway数据库迁移。 需要一个或多个Flyway组件。
health 显示应用程序运行状况信息。
httptrace 显示HTTP跟踪信息(默认情况下,最近100个HTTP请求-响应)。需要一个HttpTraceRepository组件。
info 显示应用程序信息。
integrationgraph 显示Spring integrationgraph 。需要依赖spring-integration-core
loggers 显示和修改应用程序中日志的配置。
liquibase 显示已应用的所有Liquibase数据库迁移。需要一个或多个Liquibase组件。
metrics 显示当前应用程序的“指标”信息。
mappings 显示所有@RequestMapping路径列表。
scheduledtasks 显示应用程序中的计划任务。
sessions 允许从Spring Session支持的会话存储中检索和删除用户会话。需要使用Spring Session的基于Servlet的Web应用程序。
shutdown 使应用程序正常关闭。默认禁用。
startup 显示由ApplicationStartup收集的启动步骤数据。需要使用SpringApplication进行配置BufferingApplicationStartup
threaddump 执行线程转储。
heapdump 返回hprof堆转储文件。
jolokia 通过HTTP暴露JMX bean(需要引入Jolokia,不适用于WebFlux)。需要引入依赖jolokia-core
logfile 返回日志文件的内容(如果已设置logging.file.namelogging.file.path属性)。支持使用HTTPRange标头来检索部分日志文件的内容。
prometheus 以Prometheus服务器可以抓取的格式公开指标。需要依赖micrometer-registry-prometheus

threaddumpheapdumpmetrics

7.1.2.2.定制端点
  • 健康监控:返回存活、死亡
  • 指标监控:次数、率
1.HealthEndpoint
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.stereotype.Component;

@Component
public class MyHealthIndicator implements HealthIndicator {

@Override
public Health health() {
int errorCode = check(); // perform some specific health check
if (errorCode != 0) {
return Health.down().withDetail("Error Code", errorCode).build();
}
return Health.up().build();
}

}

// 构建Health
Health build = Health.down()
.withDetail("msg", "error service")
.withDetail("code", "500")
.withException(new RuntimeException())
.build();
1
2
3
4
management:
health:
enabled: true
show-details: always #总是显示详细信息。可显示每个模块的状态信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Component
public class MyComHealthIndicator extends AbstractHealthIndicator {

/**
* 真实的检查方法
* @param builder
* @throws Exception
*/
@Override
protected void doHealthCheck(Health.Builder builder) throws Exception {
//mongodb。 获取连接进行测试
Map<String,Object> map = new HashMap<>();
// 检查完成
if(1 == 2){
// builder.up(); //健康
builder.status(Status.UP);
map.put("count",1);
map.put("ms",100);
}else {
// builder.down();
builder.status(Status.OUT_OF_SERVICE);
map.put("err","连接超时");
map.put("ms",3000);
}


builder.withDetail("code",100)
.withDetails(map);

}
}
2.MetricsEndpoint
1
2
3
4
5
6
7
8
9
10
class MyService{
Counter counter;
public MyService(MeterRegistry meterRegistry){
counter = meterRegistry.counter("myservice.method.running.counter");
}

public void hello() {
counter.increment();
}
}

7.2.监控案例落地

基于:Prometheus + Grafana

image.png

7.2.1.安装 Prometheus + Grafana

1
2
3
4
5
6
7
#安装prometheus:时序数据库
docker run -p 9090:9090 -d \
-v pc:/etc/prometheus \
prom/prometheus

#安装grafana;默认账号密码 admin:admin
docker run -d --name=grafana -p 3000:3000 grafana/grafana

7.2.2. 导入依赖

1
2
3
4
5
6
7
8
9
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
<version>1.10.6</version>
</dependency>
1
2
3
4
5
management:
endpoints:
web:
exposure: #暴露所有监控的端点
include: '*'

访问: http://localhost:8001/actuator/prometheus 验证,返回 prometheus 格式的所有指标

部署Java应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#安装上传工具
yum install lrzsz

#安装openjdk
# 下载openjdk
wget https://download.oracle.com/java/17/latest/jdk-17_linux-x64_bin.tar.gz

mkdir -p /opt/java
tar -xzf jdk-17_linux-x64_bin.tar.gz -C /opt/java/
sudo vi /etc/profile
#加入以下内容
export JAVA_HOME=/opt/java/jdk-17.0.7
export PATH=$PATH:$JAVA_HOME/bin

#环境变量生效
source /etc/profile

# 后台启动java应用
nohup java -jar boot3-14-actuator-0.0.1-SNAPSHOT.jar > output.log 2>&1 &

确认可以访问到: http://8.130.32.70:9999/actuator/prometheus

docker部署Java应用

1、在idea项目目录的target文件夹下新建Dockerfile文件内容如下

1
2
3
4
5
6
7
FROM openjdk:17

WORKDIR /app

COPY boot3-15-actuator-0.0.1-SNAPSHOT.jar /app

CMD ["java", "-jar", "boot3-15-actuator-0.0.1-SNAPSHOT.jar"]

2、开启docker远程连接

编辑docker.server文件

1
vi /usr/lib/systemd/system/docker.service

找到 [Service] 节点,修改 ExecStart 属性,增加 -H tcp://0.0.0.0:2375

1
ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock -H tcp://0.0.0.0:2375

3、idea添加docker连接配置

image-20230616140535808

3、dockerfile配置如下

image-20230616140201573

4、项目文件pom.xml添加如下插件配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<docker>
<host>tcp://192.168.120.130:2375</host>
</docker>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>

5、在idea中直接执行Dockerfile文件build出镜像并启动即可

image-20230616141030660

image-20230616140945379

6、确认可以访问到: http://192.168.120.130:8888/actuator/prometheus

image-20230616141134612

7.2.3. 配置 Prometheus 拉取数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
## 修改 prometheus.yml 配置文件
global:
scrape_interval: 15s
evaluation_interval: 15s

scrape_configs:
- job_name: 'prometheus'
static_configs:
- targets: ['192.168.120.130:9091']

- job_name: 'redis'
static_configs:
- targets: ['192.168.120.130:6379']

- job_name: 'kafka'
static_configs:
- targets: ['192.168.120.130:9092']
- job_name: 'boot3-15-actuator'
metrics_path: '/actuator/prometheus'
static_configs:
- targets: ['192.168.120.130:8888']

7.2.4.配置 Grafana 监控面板

  • 添加数据源(Prometheus)
  • 添加面板。可去 dashboard 市场找一个自己喜欢的面板,也可以自己开发面板;Dashboards | Grafana Labs

7.2.5. 效果

image.png

8.AOT

8.1.AOT与JIT

AOT:Ahead-of-Time(提前编译):程序执行前,全部被编译成机器码
JIT:Just in Time(即时编译): 程序边编译,边运行;

编译:

  • 源代码(.c、.cpp、.go、.java。。。) ===编译=== 机器码

语言:

  • 编译型语言:编译器
  • 解释型语言:解释器

8.1.1.Complier 与 Interpreter

image.png

对比项 编译器 解释器
机器执行速度 ,因为源代码只需被转换一次 ,因为每行代码都需要被解释执行
开发效率 ,因为需要耗费大量时间编译 ,无需花费时间生成目标代码,更快的开发和测试
调试 难以调试编译器生成的目标代码 容易调试源代码,因为解释器一行一行地执行
可移植性(跨平台) 不同平台需要重新编译目标平台代码 同一份源码可以跨平台执行,因为每个平台会开发对应的解释器
学习难度 相对较高,需要了解源代码、编译器以及目标机器的知识 相对较低,无需了解机器的细节
错误检查 编译器可以在编译代码时检查错误 解释器只能在执行代码时检查错误
运行时增强 可以动态增强

8.1.2. AOT 与 JIT 对比

JIT AOT
优点 1.具备实时调整能力 2.生成最优机器指令 3.根据代码运行情况优化内存占用 1.速度快,优化了运行时编译时间和内存消耗 2.程序初期就能达最高性能 3.加快程序启动速度
缺点 1.运行期边编译速度慢 2.初始编译不能达到最高性能 1.程序第一次编译占用时间长 2.牺牲高级语言一些特性

在 OpenJDK 的官方 Wiki 上,介绍了HotSpot 虚拟机一个相对比较全面的、即时编译器(JIT)中采用的优化技术列表

img

img

可使用:-XX:+PrintCompilation 打印JIT编译信息

8.1.3. JVM架构

.java === .class === 机器码

JVM: 既有解释器,又有编辑器(JIT:即时编译)

8.1.4.Java的执行过程

建议阅读:

1. 流程概要

img

解释执行:
编译执行:

2. 详细流程

热点代码:调用次数非常多的代码

8.1.5. JVM编译器

JVM中集成了两种编译器,Client Compiler 和 Server Compiler;

  • Client Compiler注重启动速度和局部的优化
  • Server Compiler更加关注全局优化,性能更好,但由于会进行更多的全局分析,所以启动速度会慢。

Client Compiler:

  • HotSpot VM带有一个Client Compiler C1编译器
  • 这种编译器启动速度快,但是性能比较Server Compiler来说会差一些。
  • 编译后的机器码执行效率没有C2的高

Server Compiler:

  • Hotspot虚拟机中使用的Server Compiler有两种:C2Graal
  • 在Hotspot VM中,默认的Server Compiler是C2编译器。

8.1.6. 分层编译

Java 7开始引入了分层编译(Tiered Compiler)的概念,它结合了C1C2的优势,追求启动速度和峰值性能的一个平衡。分层编译将JVM的执行状态分为了五个层次。五个层级分别是:

  • 解释执行。
  • 执行不带profiling的C1代码。
  • 执行仅带方法调用次数以及循环回边执行次数profiling的C1代码。
  • 执行带所有profiling的C1代码。
  • 执行C2代码。

profiling就是收集能够反映程序执行状态的数据。其中最基本的统计数据就是方法的调用次数,以及循环回边的执行次数。

img

  • 图中第①条路径,代表编译的一般情况,热点方法从解释执行到被3层的C1编译,最后被4层的C2编译。
  • 如果方法比较小(比如Java服务中常见的getter/setter方法),3层的profiling没有收集到有价值的数据,JVM就会断定该方法对于C1代码和C2代码的执行效率相同,就会执行图中第②条路径。在这种情况下,JVM会在3层编译之后,放弃进入C2编译,直接选择用1层的C1编译运行
  • C1忙碌的情况下,执行图中第③条路径,在解释执行过程中对程序进行profiling ,根据信息直接由第4层的C2编译
  • 前文提到C1中的执行效率是1层>2层>3层第3层一般要比第2层慢35%以上,所以在C2忙碌的情况下,执行图中第④条路径。这时方法会被2层的C1编译,然后再被3层的C1编译,以减少方法在3层的执行时间。
  • 如果编译器做了一些比较激进的优化,比如分支预测,在实际运行时发现预测出错,这时就会进行反优化,重新进入解释执行,图中第⑤条执行路径代表的就是反优化

总的来说,C1的编译速度更快,C2的编译质量更高,分层编译的不同编译路径,也就是JVM根据当前服务的运行情况来寻找当前服务的最佳平衡点的一个过程。从JDK 8开始,JVM默认开启分层编译。

云原生:Cloud Native; Java小改版;

最好的效果:

存在的问题:

  • java应用如果用jar,解释执行,热点代码才编译成机器码;初始启动速度慢,初始处理请求数量少。
  • 大型云平台,要求每一种应用都必须秒级启动。每个应用都要求效率高。

希望的效果:

  • java应用也能提前被编译成机器码,随时极速启动,一启动就极速运行,最高性能
  • 编译成机器码的好处:
    • 另外的服务器还需要安装Java环境
    • 编译成机器码的,可以在这个平台 Windows X64 直接运行

原生镜像:native-image(机器码、本地镜像)

  • 把应用打包成能适配本机平台 的可执行文件(机器码、本地镜像)

8.2. GraalVM

https://www.graalvm.org/

GraalVM是一个高性能的JDK,旨在加速用Java和其他JVM语言编写的应用程序执行,同时还提供JavaScript、Python和许多其他流行语言的运行时。

GraalVM提供了两种运行Java应用程序的方式:

    1. 在HotSpot JVM上使用Graal即时(JIT)编译器
    1. 作为预先编译(AOT)的本机可执行文件运行(本地镜像)。

GraalVM的多语言能力使得在单个应用程序中混合多种编程语言成为可能,同时消除了外部语言调用的成本。

8.2.1. 架构

img

8.2.2. 安装

跨平台提供原生镜像原理:

img

1. VisualStudio

https://visualstudio.microsoft.com/zh-hans/free-developer-offers/

img

img

别选中文

img

img

记住你安装的地址;

2. GraalVM
1. 安装

下载 GraalVM + native-image

img

img

img

img

2. 配置

修改 JAVA_HOME 与 Path,指向新bin路径

img

img

验证JDK环境为GraalVM提供的即可:

img

3. 依赖

安装 native-image 依赖:

1.网络环境好:参考: https://www.graalvm.org/latest/reference-manual/native-image/#install-native-image

1
gu install native-image

2.网络不好,使用我们下载的离线jar;native-image-xxx.jar文件

1
gu install --file native-image-installable-svm-java17-windows-amd64-22.3.2.jar
4. 验证
1
native-image
3.测试
1. 创建项目

创建普通java项目。编写HelloWorld类;

  • 使用mvn clean package进行打包
  • 确认jar包是否可以执行java -jar xxx.jar
  • 可能需要给 MANIFEST.MF添加 Main-Class: 你的主类
2.编译镜像
  • 编译为原生镜像(native-image):使用native-tools终端

image.png

1
2
3
4
5
#从入口开始,编译整个jar
native-image -cp boot3-15-aot-common-1.0-SNAPSHOT.jar com.atguigu.MainApplication -o Haha

#编译某个类【必须有main入口方法,否则无法编译】
native-image -cp .\classes org.example.App
3. Linux平台测试

1.安装gcc等环境

1
2
yum install lrzsz
sudo yum install gcc glibc-devel zlib-devel

2.下载安装配置Linux下的GraalVM、native-image

1
2
3
4
5
6
7
8
tar -zxvf graalvm-ce-java17-linux-amd64-22.3.2.tar.gz -C /opt/java/

sudo vim /etc/profile
#修改以下内容
export JAVA_HOME=/opt/java/graalvm-ce-java17-22.3.2
export PATH=$PATH:$JAVA_HOME/bin

source /etc/profile

3.安装native-image

1
gu install --file native-image-installable-svm-java17-linux-amd64-22.3.2.jar

4.使用native-image编译jar为原生程序

1
native-image -cp xxx.jar org.example.App

8.3. SpringBoot整合

8.3.1.依赖导入

1
2
3
4
5
6
7
8
9
10
11
12
<build>
<plugins>
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

8.3.2.生成native-image

1、运行aot提前处理命令:mvn springboot:process-aot
2、运行native打包:mvn -Pnative native:build

1
2
# 推荐加上 -Pnative
mvn -Pnative native:build -f pom.xml

image.png

8.3.3. 常见问题

可能提示如下各种错误,无法构建原生镜像,需要配置环境变量;

  • 出现cl.exe找不到错误
  • 出现乱码
  • 提示no include path set
  • 提示fatal error LNK1104: cannot open file ‘LIBCMT.lib’
  • 提示 LINK : fatal error LNK1104: cannot open file ‘kernel32.lib’
  • 提示各种其他找不到

需要修改三个环境变量PathINCLUDElib

  • 1、 Path:添加如下值

    • C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.33.31629\bin\Hostx64\x64
  • 2、新建INCLUDE环境变量:值为

1
C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.33.31629\include;C:\Program Files (x86)\Windows Kits\10\Include\10.0.19041.0\shared;C:\Program Files (x86)\Windows Kits\10\Include\10.0.19041.0\ucrt;C:\Program Files (x86)\Windows Kits\10\Include\10.0.19041.0\um;C:\Program Files (x86)\Windows Kits\10\Include\10.0.19041.0\winrt

image.png

  • 3、新建lib环境变量:值为
1
C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.33.31629\lib\x64;C:\Program Files (x86)\Windows Kits\10\Lib\10.0.19041.0\um\x64;C:\Program Files (x86)\Windows Kits\10\Lib\10.0.19041.0\ucrt\x64

image.png


尚硅谷SpringBoot3零基础教程
http://yuanql.top/2023/06/17/12_SpringBoot/SpringBoot3/尚硅谷SpringBoot3零基础教程/
作者
Qingli Yuan
发布于
2023年6月17日
许可协议