Spring Boot 3核心特性【优化排版】

前言

博客须知

  • 本文来源于尚硅谷雷神的32.Web开发-【代码式】-WebMvcConfigurer使用_哔哩哔哩_bilibili以及他的语雀笔记2、SpringBoot3-Web开发 (yuque.com)
  • 作者对雷神视频和语雀中的笔记进行了提炼和整理,由于本文的图片使用的是本地路径,所以上传到博客时图片无法正常显示,有图片需求的伙伴可以下载上方的pdf
  • 有一说一,相比于springboot2,雷神的笔记稍微没有那么乱了**,自动配置原理那一块讲的很清晰**
  • 但是
    • 视频课程很多细节都没有讲到,比如日志那一块就是带着了解了一下怎么用而已
    • 同时语雀的部分笔记以及雷神讲解得有点乱
      • 静态资源那一块规则和原理是笔记是分开的,但讲课时又是合在一起讲的,就导致这一块的笔记有点乱
      • 笔记中Web开发-》WebMvcAutoConfiguration原理-》为什么容器中放一个WebMvcConfigurer就能配置底层行为,这一块笔记很差劲呀,表达得非常随意【我已经尽可能加上我的理解了,奈何雷神这一块讲得跟坨屎一样,我没法整理清晰】
  • 另外说一下,本篇博客只包含视频中核心特性的内容【大概占视频内容的70%,对应视频p1-》p66】,这一板块在该视频中是最重要的,后续板块等我看完再继续发布

总体感受

  • 原理部分除了自动装配那里讲的挺好的,其它原理就有些小乱【尤其是为什么容器中放一个WebMvcConfigurer就能配置底层行为
  • 其次,视频大多数知识点只是带着了解和使用了一下,细节方面相对较少
  • 还有,笔记结构有点小乱
    • WebMvcAutoConfiguration原理和静态资源是分开的
    • 内容协商和内容协商原理又放在了一起【不过内容协商原理讲的还行】
    • 这一块为了尊重雷神,我还是参考了他的笔记结构【晕大头,有点小强迫症了】
  • 雷神能力很强,但是由于他的语言表达能力实在一般,导致部分内容表述得非常抽象【晕大头】

学习建议

  • 关于springboot核心,个人感觉是自动装配原理,雷神将很多内容都是围绕自动装配原理展开的,所以这一块需要重点学习【在本文快速入门-》应用分析
  • 原理部分
    • 自动装配讲的不错,必学
    • 内容协商和错误处理的原理讲的也可以
    • WebMvcAutoConfiguration原理包含的内容有点多,但是鱼龙混杂讲的不好的部分也不要深究,毕竟雷神的表达能力有限
  • 本文标识了解的内容,就真的只用了解一下就行了,什么Thymeleaf和国际化过一遍就行了,感觉没有学习的必要
    • 如果以后看视频学项目有提到Thymeleaf,直接复制粘贴就行了,不必深究
  • 本文标识**!!!的内容是需要重点学习的**,尤其是核心原理中自定义starter,一定要跟着敲一遍,会加深你对自动配置原理的理解,而且还会提高你使用好自动配置的能力
  • 如果没有学习过springboot,那就跟着雷神的spring boot3的视频学,没必要去看spring boot2的视频了【很多内容都是和springboot2重合的,而且spring boot3至少需要jdk17的版本,你总有一天要换新的jdk版本
  • 这一个板块花费了我10天时间【一天学习2~4小时,压力不算太大,而且有不少收获】

快速入门

基础入门【了解】

SpringBoot是什么

  • 简介

    • SpringBoot 帮我们简单、快速地创建一个独立的、生产级别的 Spring 应用(说明:SpringBoot底层是Spring)
    • 大多数 SpringBoot 应用只需要编写少量配置即可快速整合 Spring 平台以及第三方技术
  • 特性

    • 通过导包、写配置、启动运行就能快速创建独立的Spring应用

    • 直接嵌入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

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

开发流程

  1. 导入springboot场景启动依赖,所有springboot项目都必须继承自 spring-boot-starter-parent
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.0.5</version>
</parent>
  1. 导入web场景依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
  1. 主程序
@SpringBootApplication
public class MainApplication {
    public static void main(String[] args) {
        SpringApplication.run(MainApplication.class,args);
    }
}
  1. 业务测试,springboot默认扫描主程序所在包下的组件
@RestController
public class HelloController {
    @ResponseBody
    @GetMapping("/hello")
    public String hello(){
        return "hello";
    }
}
  1. 导入SpringBoot应用打包插件

    • mvn clean package把项目打成可执行的jar包

    • java -jar demo.jar启动项目

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

image-20240318160720053==》image-20240318161037604

特性小结

  • 简化整合

    • 导入相关的场景,拥有相关的功能,官方默认支持的所有场景
    • 官方提供的场景:命名为:spring-boot-starter-*
    • 第三方提供场景:命名为:*-spring-boot-starter
  • 简化开发:无需编写任何配置,直接开发业务

  • 简化配置

  • 简化部署

    • springboot项目都可以打包为可执行的jar包
    • 只要linux服务器上有java环境,就直接允许jar包即可,无需配置tomcat服务器
  • 简化运维:修改配置(外部放一个application.properties文件)、监控、健康检查

Spring Initializr 创建向导

  1. 填写基本信息

image-20240318162051704.

  1. 选择需要用到的场景

image-20240318162152340.

  1. 一键创建好项目结构

image-20240318162749850.

!!!应用分析

依赖管理机制

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

    • 导入场景启动器就会自动把这个场景的所有核心依赖全部导入
    • maven依赖传递原则:A->B->C,A就拥有B和C
  • 为什么版本号都不用写?

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

image-20240318172143208.

  • 自定义版本号可以利用maven的就近原则【就是覆盖父项目的依赖版本】

    • 直接在当前项目properties标签中声明父项目版本属性绑定的key
    <properties>
        <java.version>17</java.version>
        <mysql.version>8.0.31</mysql.version>
    </properties>
    
    • 直接在导入依赖的时候声明版本
    <dependency>
        <groupId>com.mysql</groupId>
        <artifactId>mysql-connector-j</artifactId>
        <version>8.0.31</version>
    </dependency>
    
  • 第三方的jar包需要声明版本,即boot父项目没有管理的依赖需要自行声明好版本

    <!-- https://mvnrepository.com/artifact/com.alibaba/druid -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.2.16</version>
    </dependency>
    
    • 不指定版本会报如下错误

    image-20240318172519897.

!!!自动配置机制

初步理解
  • 导入场景,容器中就会自动配置好这个场景的核心组件
    • 以前需要自己配置和导入DispatcherServlet、ViewResolver、CharacterEncodingFilter…

    • 现在导入场景就会自动配置好的这些组件

    • 验证:容器中有了什么组件,就具有什么功能

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

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

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

image-20240319143514906.

  • 总结: 导入场景启动器–》触发 spring-boot-autoconfigure这个包的自动配置生效–》容器中就会具有相关场景的功能
!!!完整流程【面试重点】
1.导入starter-web为例:导入了web开发场景
  1. 场景启动器导入了相关场景的所有依赖:starter-jsonstarter-tomcatspringmvc

  2. 每个场景启动器都引入了一个spring-boot-starter【核心场景启动器】

    image-20240319155656933.

  3. 核心场景启动器引入了spring-boot-autoconfigure

    image-20240319155740445.

  4. spring-boot-autoconfigure里面囊括了所有场景的所有配置(这些配置类做了整合操作)

    • 只要这个包下的所有类都能生效,那么相当于SpringBoot官方写好的整合功能就生效了

    • SpringBoot默认只扫描主程序所在的包,所以扫描不到 spring-boot-autoconfigure下写好的所有配置类

    • 这一步只是把spring-boot-autoconfigure中的配置类先引入到项目中,此时还不能生效

2.主程序:@SpringBootApplication
  1. @SpringBootApplication由三个注解组成@SpringBootConfiguration@EnableAutoConfiguratio@ComponentScan

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

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

    @AutoConfigurationPackage
    @Import({AutoConfigurationImportSelector.class})
    public @interface EnableAutoConfiguration 
    
    • @Import(AutoConfigurationImportSelector.class)批量给容器中导入组件

    • SpringBoot启动会默认加载以下的配置类【关注AutoConfigurationImportSelector类中的getCandidateConfigurations方法】

      image-20240319160942555.

    • 这些配置类来自于spring-boot-autoconfigureMETA-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件

      image-20240319161306345.

    • 项目启动的时候利用@Import批量导入组件机制把 autoconfigure 包下的自动配置类导入进来

  4. 虽然批量导入了多个自动配置类,但是这些配置类是按需生效,每一个自动配置类,都有条件注解**@ConditionalOnxxx**,只有条件成立,才能生效

3.xxxxAutoConfiguration自动配置类
  1. 使用**@Bean**往容器中放一堆组件

  2. 每个自动配置类都可能有这个注解@EnableConfigurationProperties(ServerProperties.class),用来把配置文件中配的指定前缀的属性值封装到 xxxProperties属性类中

    • 以Tomcat为例:服务器的所有配置都是以server开头的,将这些配置都封装到了属性类中
  3. 给容器中放的所有组件的一些核心参数,都来自于xxxPropertiesxxxProperties都绑定了配置文件,只需要改配置文件的值,核心组件的底层参数都能修改

核心流程总结
  1. 导入starter,就会导入autoconfigure

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

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

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

  5. xxxProperties配置文件进行了绑定

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

如何学好SpringBoot【了解】

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

    • 普通开发:导入starter,Controller、Service、Mapper、偶尔修改配置文件
    • 高级开发:自定义组件、自定义配置、自定义starter
  • 理解自动配置原理导入starter --> 生效xxxxAutoConfiguration --> 组件 --> xxxProperties --> 配置文件

  • 理解其他框架底层,如MyBatis拦截器等

  • 可以随时定制化任何组件:配置文件、自定义组件

  • 核心

    • 这个场景自动配置导入了哪些组件,能不能Autowired进来使用
    • 能不能通过修改配置改变组件的一些默认参数
    • 需不需要自己完全定义这个组件
    • 场景定制化
最佳实战
  1. 选场景,导入到项目

    • 官方:starter

    • 第三方:去仓库搜

  2. 写配置,改配置文件关键项,例如数据库参数(连接地址、账号密码…)

  3. 分析这个场景导入了哪些能用的组件

    • 自动装配这些组件进行后续使用

    • 不满意boot提供的自动配好的默认组件,就定制化(改配置、自定义组件)

整合redis为例
  1. 选场景spring-boot-starter-data-redis ,场景AutoConfiguration就是这个场景的自动配置类

  2. 写配置

    • 分析这个场景的自动配置类开启了哪些属性绑定关系==》@EnableConfigurationProperties(RedisProperties.class)

    • 修改redis相关的配置

  3. 分析组件

    • RedisAutoConfiguration 给容器中放了 StringRedisTemplate

    • 所以可以给业务代码中自动装配 StringRedisTemplate来进行reids相关操作

  4. 定制化

    • 修改配置文件

    • 自定义组件,自己给容器中放一个 StringRedisTemplate

核心技能

常用注解

  • SpringBoot摒弃XML配置方式,改为全注解驱动
组件注册
注解
  • @Configuration、@SpringBootConfiguration:标识当前类是一个配置类,替代之前的配置文件,配置类本身也是容器中的组件

  • @Bean:替代以前的bean标签,标识在方法上,向容器注入组件,默认以方法名作为容器中的名称

  • @Scope:定制组件的范围,组件默认为单例

@Configuration
public class TestConfiguration {
    @Scope("prototype")//设置为多例
    @Bean
    public User user(){
        var user=new User();
        return user;
    }
}
  • @Import:可以导入第三方的组件

    • 也可以使用@Bean导入,但是无法使用@Component标识在第三方类上【因为那是别人写好的类,怎么可能修改人家源码】
    • 注意:@Import导入的组件默认名称为全类名
  • @Controller、 @Service、@Repository、@Component:标识在类上交给容器管理

  • @ComponentScan:扫描指定包的组件

步骤
  1. @Configuration 编写一个配置类

  2. 在配置类中,自定义方法给容器中注册组件,配合@Bean使用

  3. 或使用@Import 导入第三方的组件

条件注解
基础概念
  • 条件注解的作用:如果注解指定的条件成立,则触发指定行为

  • 常见的条件注解

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

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

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

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

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

场景
  • 如果存在FastsqlException这个类,给容器中放一个名为cat01的Cat组件
  • 否则,就给容器中放一个名dog01的Dog组件
  • 如果系统中有dog01这个组件,就给容器中放一个 User组件,名zhangsan
  • 否则,就放一个User,名叫lisi
@ConditionalOnClass(name = "com.alibaba.druid.FastsqlException")
@Bean
public Cat cat01(){
    return new Cat();
}
@ConditionalOnMissingClass("com.alibaba.druid.FastsqlException")
@Bean
public Dog dog01(){
    return new Dog();
}
=======测试代码=========
var ioc = SpringApplication.run(Boot301DemoApplication.class, args);
for (String s : ioc.getBeanNamesForType(Cat.class)) {
    System.out.println("cat:"+s);
}
for (String s : ioc.getBeanNamesForType(Dog.class)) {
    System.out.println("dog:"+s);
}
=======输出结果=========
cat:cat01
user:lisi
=======移除druid的依赖后的输出结果=========
dog:dog01
user:zhangsan
!!!属性绑定
基础知识
  • @ConfigurationProperties: 将容器中任意组件(Bean)的属性值和配置文件的配置项的值进行绑定

    1. 给容器中注册组件(@Component、@Bean)
    2. 使用@ConfigurationProperties声明组件的属性和配置文件进行绑定的前缀
  • @EnableConfigurationProperties:开启属性类的属性绑定功能并且将属性类注入到容器中

    • SpringBoot默认只扫描自己主程序所在的包,即使导入的第三方包中的组件上标注了@Component、@ConfigurationProperties注解也无法注入容器
    • 因为组件都扫描不进来,此时使用**@EnableConfigurationProperties**注解就可以快速进行属性绑定并把组件注册进容器
  • 更多注解参照:Spring注解驱动开发【1-26集】

测试@ConfigurationProperties
@ConfigurationProperties + @Component
@ConfigurationProperties(prefix = "pig")
@Component
public class Pig {
    private Long id;
    private String name;
    public Long getId() {
        return id;
    }
    public void setId(Long id) {
        this.id = id;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
}
  • 配置文件如下
pig.id=12036
pig.name=大猪头
  • 测试代码以及测试结果
var ioc = SpringApplication.run(Boot301DemoApplication.class, args);
Pig bean = ioc.getBean(Pig.class);
System.out.println(bean.getId()+" "+bean.getName());
======输出========
12036 ???
  • 乱码问题:不仅上述出现乱码,而且配置文件的内容也变为乱码

image-20240319152953197.

  • 乱码解决:在setting中设置文件编码【File Encoding】默认为utf-8

image-20240319152747085=乱码问题解决=》image-20240319153020395

@Bean
  • 也可以使用@Bean的方式,注入的组件也会自动绑定配置项
@ConfigurationProperties(prefix = "pig")
//@Component
public class Pig {
    private Long id;
    private String name;
    public Long getId() {
        return id;
    }
    public void setId(Long id) {
        this.id = id;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
}
@Bean
public Pig pig(){
    return new Pig();
}
======测试结果=========
12036 大猪头
  • @ConfigurationProperties()也可以标识在方法上
@ConfigurationProperties(prefix = "pig")
@Bean
public Pig pig(){
    return  new Pig();
}
测试@EnableConfigurationProperties
@ConfigurationProperties(prefix = "pig")
public class Pig {
    private Long id;
    private String name;
    public Long getId() {
        return id;
    }
    public void setId(Long id) {
        this.id = id;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
}
  • @EnableConfigurationProperties会开启指定的属性类的配置绑定功能,并将其放入容器
@Configuration
@EnableConfigurationProperties(Pig.class)
public class ConditionConfiguration {

}
========输出=======
12036 大猪头

YAML配置文件【了解,之前spring boot2有学习过】

设计目标

痛点:SpringBoot 集中化管理配置,application.properties

问题:配置多以后难阅读和修改,层级结构辨识度不高

  • 设计目标就是方便人类读写
  • 层次分明,更适合做配置文件
  • 使用.yaml.yml作为文件后缀
基本语法
  • 注意事项

    • 大小写敏感
    • 使用缩进表示层级关系,k: v,使用空格分割k,v
    • 缩进时不允许使用Tab键,只允许使用空格、换行
    • 缩进的空格数目不重要,只要相同层级的元素左侧对齐即可
    • #表示注释,从这个字符一直到行尾都会被解析器忽略
  • 支持的写法

    • 对象键值对的集合,如:映射(map)/ 哈希(hash) / 字典(dictionary)
    • 数组:一组按次序排列的值,如:序列(sequence) / 列表(list)
    • 纯量:单个的、不可再分的值,如:字符串、数字、bool、日期
示例【这里直接cv】
@Component
@ConfigurationProperties(prefix = "person") //和配置文件person前缀的所有配置进行绑定
@Data //自动生成JavaBean属性的getter/setter
//@NoArgsConstructor //自动生成无参构造器
//@AllArgsConstructor //自动生成全参构造器
public class Person {
    private String name;
    private Integer age;
    private Date birthDay;
    private Boolean like;
    private Child child; //嵌套对象
    private List<Dog> dogs; //数组(里面是对象)
    private Map<String,Cat> cats; //表示Map
}
@Data
public class Dog {
    private String name;
    private Integer age;
}
@Data
public class Child {
    private String name;
    private Integer age;
    private Date birthDay;
    private List<String> text; //数组
}
@Data
public class Cat {
    private String name;
    private Integer age;
}
  • properties表示法
person.name=张三
person.age=18
person.birthDay=2010/10/12 12:12:12
person.like=true
person.child.name=李四
person.child.age=12
person.child.birthDay=2018/10/12
person.child.text[0]=abc
person.child.text[1]=def
person.dogs[0].name=小黑
person.dogs[0].age=3
person.dogs[1].name=小白
person.dogs[1].age=2
person.cats.c1.name=小蓝
person.cats.c1.age=3
person.cats.c2.name=小灰
person.cats.c2.age=2
  • yaml表示法
person:
  name: 张三
  age: 18
  birthDay: 2010/10/10 12:12:12
  like: true
  child:
    name: 李四
    age: 20
    birthDay: 2018/10/10
    text: ["abc","def"] #数组/集合的第一种表示方式
  dogs:  #数组/集合的第二种表示方式
    - name: 小黑
      age: 3
    - name: 小白
      age: 2
  cats:
    c1: #key
      name: 小蓝 
      age: 3
    c2: {name: 小绿,age: 2} #对象也可用{}表示
细节
  • birthDay推荐写为birth-day【yaml中驼峰命名可以转成短横线的写法】

  • 文本

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

    • |开头,大文本写在下层,保留文本格式换行符正确显示

    image-20240319170905862.

    • >开头,大文本写在下层,换行符都会变成空格显示【如果文本存在缩进就不会压缩

      • 压缩换行image-20240319171311943文本有缩进image-20240319171340587
  • 多文档合并:使用---可以把多个yaml文档合并在一个文档中,每个文档区依然认为内容独立

image-20240319171644631.

小技巧:lombok【这玩意确实好用,之前用过,推荐】

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

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <scope>compile</scope>
</dependency>
  • 使用@Data注解标识在类上就可以自动为属性添加get/set方法

日志配置

简介
  • 2024.3.20我的吐槽:关于日志这一块雷神只是简单的过了一下,没有涉及太多细节

规范:项目开发不要编写System.out.println(),应该用日志记录信息

image-20240320125855037.

  • 日志门面就相当于日志的接口,springboot默认使用slf4j+logback

    • Spring使用commons-logging作为内部日志,但底层日志实现是开放的。可对接其他日志框架

      • spring5及以后自己整合 commons-logging,不需要导入第三方包
    • 支持 jul、log4j2、logback。SpringBoot提供了默认的控制台输出配置,也可以配置输出为文件

    • 虽然日志框架很多,但是使用SpringBoot 的默认配置就足够用了

  • SpringBoot怎么把日志默认配置好的

    1. 每个starter场景,都会导入一个核心场景spring-boot-starter
      • 核心场景引入了日志的所有功能spring-boot-starter-logging
    2. 默认使用了logback + slf4j 组合作为默认底层日志
    3. 日志是系统一启动就要使用的组件【启动时机更早】,xxxAutoConfiguration是系统启动好了以后才放好的组件
    4. 日志是利用监听器机制配置好的。ApplicationListener
    5. 日志所有的配置都可以通过修改配置文件实现。所有和日志相关的配置都以logging为前缀
日志格式
2023-03-31T13:56:17.511+08:00  INFO 4944 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2023-03-31T13:56:17.511+08:00  INFO 4944 --- [           main] o.apache.catalina.core.StandardEngine    : Starting Servlet engine: [Apache Tomcat/10.1.7]
  • 默认输出格式

    • 时间和日期:毫秒级精度
    • 日志级别: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}
      
    • 可修改为'%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] %logger{15} ===> %msg%n'

记录日志
Logger logger = LoggerFactory.getLogger(getClass());
  • 或者使用Lombok的@Slf4j注解
@Slf4j
@RestController
public class TestLogging {
    @RequestMapping("hello")
    public String helloController(){
        log.info("info");
        log.debug("debug");
        log.warn("warn");
        log.error("error");
        return "hello";
    }
}
日志级别
  • 由低到高:ALL,TRACE, DEBUG, INFO, WARN, ERROR,FATAL,OFF【只会打印指定级别及以上级别的日志】

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

    • 不指定级别的所有类,都使用root指定的级别作为默认级别,SpringBoot日志默认级别是INFO

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

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

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

      • 可以指定某个具体的包的日志级别【也可以精确到类】

      logging.level.com.study.liao.boot3logging.component=warn
      
开发技巧:日志分组
  • SpringBoot 也支持将相关的logger分组在一起,统一配置。比如:Tomcat 相关的日志统一设置
logging.group.tomcat=org.apache.catalina,org.apache.coyote,org.apache.tomcat
logging.level.tomcat=trace
  • SpringBoot 预定义两个组
NameLoggers
weborg.springframework.core.codec, org.springframework.http, org.springframework.web, org.springframework.boot.actuate.endpoint.web, org.springframework.boot.web.servlet.ServletContextInitializerBeans
sqlorg.springframework.jdbc.core, org.hibernate.SQL, org.jooq.tools.LoggerListener
文件输出
  • SpringBoot 默认把日志写在控制台,如果想额外记录到文件,可以在application.properties中添加logging.file.name 或者是logging.file.path配置项
    • logging.file.name
      • 如果只声明名字,日志文件生成到当前项目路径image-20240320161300034
      • 也可以指定路径logging.file.name=D://Study//FileTest//myLog.logimage-20240320161321378
    • logging.file.path可以生成到指定路径,文件名为spring.logimage-20240320161409713
    • 如果都指定就只有logging.file.name 会生效
logging.file.namelogging.file.path示例效果
未指定未指定仅控制台输出
指定未指定my.log写入指定文件。可以加路径
未指定指定/var/log写入指定目录,文件名为spring.log
指定指定以logging.file.name为准
文件归档与滚动切割
  • 归档:每天的日志单独存到一个文档中

  • 切割:每个文件10MB,超过大小切割成另外一个文件

  • 每天的日志应该独立分割出来存档

    • 如果使用logback(SpringBoot 默认整合),可以通过application.properties/yaml文件指定日志滚动规则
    logging.logback.rollingpolicy.file-name-pattern=${LONG_FILE}.%d{yyyy-MM-dd}.%i.gz
    logging.logback.rollingpolicy.max-file-size=100KB
    
    • 如果是其他日志系统,需要自行配置(添加log4j2.xml或log4j2-spring.xml)
  • logback支持的滚动规则设置如下

配置项描述
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)
自定义配置
命名
日志系统自定义
Logbacklogback-spring.xml, logback-spring.groovy, logback.xml, or logback.groovy
Log4j2log4j2-spring.xml or log4j2.xml
JDK (Java Util Logging)logging.properties
  • Spring建议在日志配置中使用-spring 命名(例如,logback-spring.xml 而不是 logback.xml

    • -spring就可以交给spring管理,如果不加就是由logback框架底层在控制
  • 最佳实战:如果自己要写配置,配置文件名需要加上 xx-spring.xml

切换日志组合
  • 导入自己的日志场景,并且排除掉springboot默认的日志场景
<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格式的配置文件
格式依赖文件名
YAMLcom.fasterxml.jackson.core:jackson-databind +
com.fasterxml.jackson.dataformat:jackson-dataformat-yaml
log4j2.yaml + log4j2.yml
JSONcom.fasterxml.jackson.core:jackson-databindlog4j2.json + log4j2.jsn
自定义配置【网上cv的log4j2的配置模板】
<?xml version="1.0" encoding="UTF-8"?>
<!--Configuration后面的status,这个用于设置log4j2自身内部的信息输出,可以不设置,当设置成trace时,你会看到log4j2内部各种详细输出-->
<!--monitorInterval:Log4j能够自动检测修改配置 文件和重新配置本身,设置间隔秒数(最小是5秒钟)-->
<configuration monitorInterval="5" status="warn">
    <!--日志级别以及优先级排序: OFF > FATAL > ERROR > WARN > INFO > DEBUG > TRACE > ALL -->
    <!--变量配置-->
    <Properties>
        <!-- 格式化输出:%date表示日期(可缩写成%d,后同),%thread表示线程名,%-5level:级别从左显示5个字符宽度 %msg:日志消息,%n是换行符-->
        <!-- %logger{36} 表示 Logger 名字最长36个字符 -->
        <property name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss,SSS} %highlight{%-5level} [%t] %highlight{%c{1.}.%M(%L)}: %msg%n" />
        <!-- 定义日志存储的路径 -->
        <property name="FILE_PATH" value="log" />
        <!--<property name="FILE_NAME" value="myProject" />-->
    </Properties>
    <!--此节点有三种常见的子节点:Console,RollingFile,File-->
    <appenders>
        <!--target:SYSTEM_OUT或SYSTEM_ERR,一般只设置默认:SYSTEM_OUT-->
        <console name="Console" target="SYSTEM_OUT">
            <!--输出日志的格式,默认为:%m%n,即只输出日志和换行-->
            <PatternLayout pattern="${LOG_PATTERN}"/>
            <!--阈值过滤器,控制台只输出level及其以上级别的信息(onMatch),其他的直接拒绝(onMismatch)-->
            <ThresholdFilter level="info" onMatch="ACCEPT" onMismatch="DENY"/>
        </console>
        <!--        &lt;!&ndash;文件会打印出所有信息,这个log每次运行程序会自动清空,由append属性决定,适合临时测试用&ndash;&gt;-->
        <!--        <File name="Filelog" fileName="${FILE_PATH}/test.log" append="false">-->
        <!--            <PatternLayout pattern="${LOG_PATTERN}"/>-->
        <!--        </File>-->
        <!-- 这个会打印出所有的debug及以下级别的信息,每次大小超过size,则这size大小的日志会自动存入按年份-月份建立的文件夹下面并进行压缩,作为存档-->
        <RollingFile name="RollingFileDebug" fileName="${FILE_PATH}/debug.log" filePattern="${FILE_PATH}/debug/DEBUG-%d{yyyy-MM-dd}_%i.log.gz">
            <!--阈值过滤器,控制台只输出level及其以上级别的信息(onMatch),其他的直接拒绝(onMismatch)-->
            <ThresholdFilter level="debug" onMatch="ACCEPT" onMismatch="DENY"/>
            <!--如果配置的是“%d{yyyy-MM}”,滚动时间单位就是月。“%d{yyyy-MM-dd}”,滚动时间单位就是天-->
            <PatternLayout pattern="${LOG_PATTERN}"/>
            <!--指定滚动日志的策略,就是指定新建日志文件的时机-->
            <Policies>
                <!--interval属性用来指定多久滚动一次,时间单位取决于<PatternLayout pattern>,modulate属性调整时间,true:0点为基准滚动,false:服务器启动时间开始滚动-->
                <TimeBasedTriggeringPolicy interval="1" modulate="true" />
                <SizeBasedTriggeringPolicy size="100MB"/>
            </Policies>
            <!-- DefaultRolloverStrategy属性如不设置,则默认为最多同一文件夹下7个文件开始覆盖-->
            <DefaultRolloverStrategy max="15">
                <!--删除15天之前的日志-->
                <Delete basePath="${FILE_PATH}" maxDepth="2">
                    <IfFileName glob="*/*.log.gz" />
                    <IfLastModified age="360H" />
                </Delete>
            </DefaultRolloverStrategy>
        </RollingFile>
        <!-- 这个会打印出所有的warn及以下级别的信息,每次大小超过size,则这size大小的日志会自动存入按年份-月份建立的文件夹下面并进行压缩,作为存档-->
        <RollingFile name="RollingFileInfo" fileName="${FILE_PATH}/info.log" filePattern="${FILE_PATH}/info/INFO-%d{yyyy-MM-dd}_%i.log.gz">
            <!--阈值过滤器,控制台只输出level及其以上级别的信息(onMatch),其他的直接拒绝(onMismatch)-->
            <ThresholdFilter level="info" onMatch="ACCEPT" onMismatch="DENY"/>
            <PatternLayout pattern="${LOG_PATTERN}"/>
            <Policies>
                <!--interval属性用来指定多久滚动一次,时间单位取决于<PatternLayout pattern>,modulate属性调整时间,true:0点为基准滚动,false:服务器启动时间开始滚动-->
                <TimeBasedTriggeringPolicy interval="1" modulate="true" />
                <SizeBasedTriggeringPolicy size="100MB"/>
            </Policies>
            <!-- DefaultRolloverStrategy属性如不设置,则默认为最多同一文件夹下7个文件开始覆盖-->
            <DefaultRolloverStrategy max="15"/>
        </RollingFile>
        <!-- 这个会打印出所有的error及以下级别的信息,每次大小超过size,则这size大小的日志会自动存入按年份-月份建立的文件夹下面并进行压缩,作为存档-->
        <RollingFile name="RollingFileError" fileName="${FILE_PATH}/error.log" filePattern="${FILE_PATH}/error/ERROR-%d{yyyy-MM-dd}_%i.log.gz">
            <!--阈值过滤器,控制台只输出level及其以上级别的信息(onMatch),其他的直接拒绝(onMismatch)-->
            <ThresholdFilter level="error" onMatch="ACCEPT" onMismatch="DENY"/>
            <PatternLayout pattern="${LOG_PATTERN}"/>
            <Policies>
                <!--interval属性用来指定多久滚动一次,时间单位取决于<PatternLayout pattern>,modulate属性调整时间,true:0点为基准滚动,false:服务器启动时间开始滚动-->
                <TimeBasedTriggeringPolicy interval="1" modulate="true" />
                <SizeBasedTriggeringPolicy size="100MB"/>
            </Policies>
            <!-- DefaultRolloverStrategy属性如不设置,则默认为最多同一文件夹下7个文件开始覆盖-->
            <DefaultRolloverStrategy max="15"/>
        </RollingFile>
        <!--启用异步日志,阻塞队列最大容量为20000,超出队列容量时是否等待日志输出,不等待将直接将日志丢弃-->
        <Async name="Async" bufferSize="20000" blocking="true">
            <AppenderRef ref="Console"/>
            <AppenderRef ref="RollingFileDebug"/>
            <AppenderRef ref="RollingFileInfo"/>
            <AppenderRef ref="RollingFileError"/>
        </Async>
    </appenders>
    <!--Logger节点用来单独指定日志的形式,比如要为指定包下的class指定不同的日志级别等。-->
    <!--然后定义loggers,只有定义了logger并引入的appender,appender才会生效-->
    <loggers>
        <!--过滤掉spring和mybatis的一些无用的DEBUG信息-->
        <logger name="org.mybatis" level="info" additivity="false">
            <AppenderRef ref="Async"/>
        </logger>
        <!--监控系统信息-->
        <!--若是additivity设为false,则 子Logger 只会在自己的appender里输出,而不会在 父Logger 的appender里输出。-->
        <Logger name="org.springframework" level="info" additivity="false">
            <AppenderRef ref="Async"/>
        </Logger>
        <!--root 节点用来指定项目的根日志,level:日志输出级别,共有8个级别,按照从低到高为:All < Trace < Debug < Info < Warn < Error < Fatal < OFF.-->
        <root level="debug">
            <AppenderRef ref="Async" />
        </root>
    </loggers>
</configuration>
最佳实战
  1. 导入任何第三方框架,先排除它的日志包,因为springBoot底层控制好了日志
  2. 修改 application.properties 配置文件,就可以调整日志的所有行为。如果不够,可以将自己编写日志框架的配置文件放在类路径下就行,比如logback-spring.xmllog4j2-spring.xml
  3. 如需对接专业日志系统,也只需要把 logback 记录的日志灌倒 kafka之类的中间件,这和SpringBoot没关系,都是日志框架自己的配置,修改配置文件即可
  4. 业务中使用slf4j-api记录日志。不要再 sout 了

Web开发

Web场景

自动配置

  1. 整合web场景
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
  1. 引入了 autoconfigure功能

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

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

  4. 所有自动配置类如下

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
  1. 绑定了配置文件的一堆配置项

    • SpringMVC的所有配置 spring.mvc

    • Web场景通用配置 spring.web

    • 文件上传配置 spring.servlet.multipart

    • 服务器的配置 server,比如编码方式

默认效果

  • 包含了 ContentNegotiatingViewResolver 和 BeanNameViewResolver 组件,方便视图解析

  • 默认的静态资源处理机制: 静态资源放在 static 文件夹下即可直接访问

  • 自动注册了Converter、GenericConverter、Formatter组件,适配常见数据类型转换和格式化需求

  • 支持HttpMessageConverters,可以方便返回json等数据类型

  • 注册MessageCodesResolver,方便国际化及错误消息处理

  • 支持静态index.html

  • 自动使用ConfigurableWebBindingInitializer,实现消息处理、数据绑定、类型转化、数据校验等功能

  • 重点

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

最佳实践

springBoot 已经默认配置好了Web开发场景常用功能,直接使用即可

三种方式
方式用法效果
全自动直接编写控制器逻辑全部使用自动配置默认效果
手自一体@Configuration + 配置WebMvcConfigurer+ 配置 WebMvcRegistrations不要标注 @EnableWebMvc保留自动配置效果,手动设置部分功能,定义MVC底层组件
全手动@Configuration + 配置WebMvcConfigurer标注 @EnableWebMvc禁用自动配置效果,全手动设置
  • 总结:给容器中写一个配置类@Configuration实现WebMvcConfigurer但是不要标注@EnableWebMvc注解,实现手自一体的效果
两种模式
  • 前后端分离模式: @RestController 响应JSON数据

  • 前后端不分离模式:@Controller + Thymeleaf模板引擎

WebMvcAutoConfiguration原理

生效条件

@AutoConfiguration(after = { DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,
		ValidationAutoConfiguration.class }) //after表示在括号中这些自动配置完成之后执行
@ConditionalOnWebApplication(type = Type.SERVLET) //如果是web应用且类型是SERVLET就生效
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class) //容器中没有这个Bean,才生效。默认就是没有
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)//优先级
@ImportRuntimeHints(WebResourcesRuntimeHints.class)
public class WebMvcAutoConfiguration { 
}
  • 拓展:web类型包括SERVLET、REACTIVE响应式web

效果

  • 放了两个Filter

    • HiddenHttpMethodFilter:页面表单提交Rest请求(GET、POST、PUT、DELETE)

    • FormContentFilter:由于GET(数据放URL后面)、POST(数据放请求体)请求可以携带数据,PUT、DELETE 的请求体数据会被忽略,因此需要加上该过滤器,防止服务器忽略掉PUT、DELETE 的请求体数据

  • 给容器中放了WebMvcConfigurer组件【详情看下方WebMvcConfigurer接口】:给SpringMVC添加各种定制功能,所有的功能最终会和配置文件进行绑定

    • WebMvcProperties绑定spring.mvc配置前缀

    • WebProperties绑定 spring.web配置前缀

@Configuration(proxyBeanMethods = false)
@Import(EnableWebMvcConfiguration.class) //额外导入了其他配置
@EnableConfigurationProperties({ WebMvcProperties.class, WebProperties.class })
@Order(0)
public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer, ServletContextAware{}

WebMvcConfigurer接口

  • 提供了配置SpringMVC底层的所有组件入口,add是添加功能,configure是配置功能,extend是扩展功能

image-20240321132945620.

默认静态资源规则

@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
    if (!this.resourceProperties.isAddMappings()) {
        logger.debug("Default resource handling disabled");
        return;
    }
    addResourceHandler(registry, this.mvcProperties.getWebjarsPathPattern(),
            "classpath:/META-INF/resources/webjars/");
    addResourceHandler(registry, this.mvcProperties.getStaticPathPattern(), (registration) -> {
        registration.addResourceLocations(this.resourceProperties.getStaticLocations());
        if (this.servletContext != null) {
            ServletContextResource resource = new ServletContextResource(this.servletContext, SERVLET_LOCATION);
            registration.addResourceLocations(resource);
        }
    });
}
  • 规则一:访问 /webjars/**路径就去 classpath:/META-INF/resources/webjars/下找资源【不常用】

    • 使用maven导入依赖,相关的js就导入到classpath:/META-INF/resources/webjars/

    • 然后使用 /webjars/资源名就可以访问相关的资源

    image-20240321134711443.

  • 规则二:访问/**路径就去静态资源默认的四个位置找资源

    image-20240321134134618.

    • classpath:/META-INF/resources/

    • classpath:/resources/

    • classpath:/static/

    • classpath:/public/

  • 规则三:静态资源默认都有缓存规则的设置

    • 所有缓存的设置,都可以直接通过配置文件中的 spring.web前缀进行配置

    • cachePeriod: 缓存周期,缓存的过期时间,以s为单位,默认没有过期时间

    • cacheControl: HTTP缓存控制,https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Caching

    • useLastModified:是否使用最后一次修改。配合HTTP Cache规则

      • 如果为true就用最后一次修改的时间来对比服务器和浏览器的资源是否相同,如果相同就返回304【表示资源没有修改】,不同就重新请求资源

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

registration.setCachePeriod(getSeconds(this.resourceProperties.getCache().getPeriod()));
registration.setCacheControl(this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl());
registration.setUseLastModified(this.resourceProperties.getCache().isUseLastModified());

EnableWebMvcConfiguration

//SpringBoot 给容器中放 WebMvcConfigurationSupport 组件
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(WebProperties.class)
public static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration implements ResourceLoaderAware { }
  • 如果自己放了WebMvcConfigurationSupport组件,那SpringBoot的WebMvcAutoConfiguration都会失效

  • HandlerMapping: 根据请求路径找到可以处理该请求的handler

  • WelcomePageHandlerMapping

    • 访问 /**路径下的所有请求,都在之前提到的四个静态资源路径下找,欢迎页也一样

    • 只要静态资源的位置有一个 index.html页面,项目启动默认访问

为什么容器中放一个WebMvcConfigurer就能配置底层行为【这一块讲的云里雾里的】

  1. WebMvcAutoConfiguration是一个自动配置类,它里面有一个 EnableWebMvcConfiguration
  2. EnableWebMvcConfiguration继承于DelegatingWebMvcConfiguration,这两个都生效【??这是什么勾吧表述??
  3. DelegatingWebMvcConfiguration利用依赖注入(DI)把容器中所有 WebMvcConfigurer 注入进来
  4. 调用 DelegatingWebMvcConfiguration 的方法配置底层规则实际上是调用所有 WebMvcConfigurer的配置底层方法
    • 来源于语雀simplegq的评论:DelegatingWebMvcConfiguration是代理,别人调用它,它就调用容器中所有WebMvcConfigurer的底层配置方法

WebMvcConfigurationSupport

  • 提供了很多的默认设置,判断系统中是否有相应的类,如果有就加入相应的HttpMessageConverter
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);

静态资源

前言

  • 这一块需要结合前面WebMvcAutoConfiguration原理的内容,雷神将这一部分笔记有点跳,一会讲静态资源一会讲原理,又把笔记分成两个位置,小乱

默认规则

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

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

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

    • 所有静态资源都定义了缓存规则。详情请看下方

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

    • period: 缓存间隔,默认 0S【说人话就是没有缓存】

    • cacheControl:缓存控制,默认不开启

    • useLastModified:是否使用lastModified头

      • 默认 false
      • 如果为true就用最后一次修改的时间来对比服务器和浏览器的资源是否相同
        • 如果相同就返回304【表示资源没有修改】
        • 不同就重新请求资源
欢迎页
  • 欢迎页规则在 WebMvcAutoConfiguration 中进行了定义
    • 静态资源目录下找 index.html
    • 没有就在 templates下找index模板页
缓存实验
  • spring.web为前缀用于配置国际化的区域信息和静态资源策略(开启、处理链、缓存)
#开启静态资源映射规则
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

自定义静态资源规则

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

配置方式
  • spring.mvc可以配置静态资源访问的前缀路径

    ## 自定义webjars路径前缀
    spring.mvc.webjars-path-pattern=/wj/**
    ## 静态资源访问路径前缀
    spring.mvc.static-path-pattern=/static/**
    
    • 例如配置spring.mvc.static-path-pattern=/static/**,那访问所以静态资源都得带上/static的前缀

    image-20240321164615580.

  • spring.web可以配置静态资源目录和静态资源缓存策略【默认的静态资源目录是之前提到的四个目录】

#开启静态资源映射规则【默认开启】
spring.web.resources.add-mappings=true
#设置缓存【笼统配置】
spring.web.resources.cache.period=3600
# 精确配置,会覆盖笼统配置
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/
代码方式
  • 方式一:实现WebMvcConfigurer方法重写addResourceHandlers()方法
    • 容器中只要有一个 WebMvcConfigurer 组件,配置的默认底层行为都会生效,不管重写方法有没有调用WebMvcConfigurer.super.addResourceHandlers(registry)都会生效
    • @EnableWebMvc注解会禁用boot的默认配置
@Configuration
public class MyConfig implements WebMvcConfigurer {
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        //WebMvcConfigurer.super.addResourceHandlers(registry);//super就是调用父类的配置,即保留以前的规则
        //配置静态资源规则
        registry.addResourceHandler("/static/**").//配置访问前缀
                addResourceLocations("classpath:/a/","classpath:/b/").//配置静态资源目录
                setCacheControl(CacheControl.maxAge(1000, TimeUnit.SECONDS));//配置缓存规则
    }
}

image-20240321170200166image-20240321170132857image-20240321170343994

  • 方式二:@Bean注入WebMvcConfigurer组件
@Configuration
public class MyConfig {
    @Bean
    public WebMvcConfigurer webMvcConfigurer() {
        return new WebMvcConfigurer() {
            @Override
            public void addResourceHandlers(ResourceHandlerRegistry registry) {
                registry.addResourceHandler("/static/**").//配置访问前缀
                        addResourceLocations("classpath:/a/","classpath:/b/").//配置静态资源目录
                        setCacheControl(CacheControl.maxAge(1000, TimeUnit.SECONDS));//配置缓存规则
            }
        };
    }
}

路径匹配

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

以前只支持 AntPathMatcher 策略, 现在提供了PathPatternParser 策略。并且可以指定具体使用哪种策略

Ant风格路径用法

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

    • *:表示任意数量的字符
    • ?:表示任意一个字符
    • :表示任意数量的目录
    • {}:表示一个命名的模式占位符
    • []:表示字符集合,例如[a-z]表示小写字母
  • 例如

    • *.html 匹配任意名称,扩展名为.html的文件
    • /folder1/*/*.java 匹配在folder1目录下的任意两级目录下的.java文件
    • /folder2/**/*.jsp匹配在folder2目录下任意目录深度的.jsp文件
    • /{type}/{id}.html 匹配任意文件名为{id}.html,在任意命名的{type}目录下的文件
  • 注意:Ant 风格的路径模式语法中的特殊字符需要转义

    • 要匹配文件路径中的星号,则需要转义为\*
    • 要匹配文件路径中的问号,则需要转义为\?

模式切换

  • AntPathMatcher 与 PathPatternParser【新版默认使用】
    • PathPatternParser 在 jmh 基准测试下,有 6~8 倍吞吐量提升,降低 30%~40%空间分配率
    • PathPatternParser 兼容 AntPathMatcher语法,并支持更多类型的路径模式
    • PathPatternParser **多段匹配的支持仅允许在模式末尾使用
@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风格路径
# 改变路径匹配策略:
# ant_path_matcher 老版策略;
# path_pattern_parser 新版策略;
spring.mvc.pathmatch.matching-strategy=ant_path_matcher

内容协商

  • 实现一套系统适配多端不同类型的数据返回

image-20240322161322266.

多端内容适配

默认规则
  • 基于请求头内容协商(默认开启):客户端向服务端发送请求,携带HTTP标准的Accept请求头

    • Accept:application/jsontext/xmltext/yaml
    • 服务端根据客户端请求头期望的数据类型进行动态返回
  • 基于请求参数内容协商(默认禁用)

    1. 发送请求GET /projects/spring-boot?format=json

    2. 匹配到@GetMapping("/projects/spring-boot")

    3. 根据参数协商,优先返回json 类型数据【需要开启参数匹配设置

      • web场景默认导入了jackson【处理json数据相关依赖】,因此默认把对象转成json的格式返回
      • jackson也支持响应xml格式的数据,前提是需要导入相关的依赖【具体实践看下方的效果演示】
    4. 发送请求GET /projects/spring-boot?format=xml,优先返回 xml 类型数据

效果演示
  • 需求:请求同一个接口,可以返回json和xml不同格式数据
  1. 导入返回xml格式数据的依赖
<dependency>
    <groupId>com.fasterxml.jackson.dataformat</groupId>
    <artifactId>jackson-dataformat-xml</artifactId>
</dependency>
  1. 标识注解
@JacksonXmlRootElement  // 可以响应为xml的数据格式
@Data
public class Person {
    private Long id;
    private String userName;
    private String email;
    private Integer age;
}
  1. 开启基于请求参数的内容协商
# 开启基于请求参数的内容协商功能。 默认参数名:format。 默认此功能不开启
spring.mvc.contentnegotiation.favor-parameter=true
# 指定内容协商时使用的参数名。默认是 format
spring.mvc.contentnegotiation.parameter-name=type
  1. 效果

image-20240322163045348.

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

内容协商原理HttpMessageConverter

前言
  • HttpMessageConverter 怎么工作?合适工作?
    • 定制 HttpMessageConverter 来实现多端内容协商
    • 编写WebMvcConfigurer提供的configureMessageConverters底层,修改底层的MessageConverter
@ResponseBody标识的所有方法的返回值由HttpMessageConverter处理
  • 标注了@ResponseBody的返回值将会由支持它的 HttpMessageConverter写给浏览器@ResponseBodyHttpMessageConverter处理】

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

    1. 请求先进入DispatcherServletdoDispatch()进行处理

    2. 找到一个 HandlerAdapter 适配器,利用适配器执行目标方法【标识了RequestMapping相关注解就由RequestMappingHandlerAdapter来执行】

      1. 目标方法执行之前,准备好两个东西

        • HandlerMethodArgumentResolver:参数解析器,确定目标方法每个参数值

        • HandlerMethodReturnValueHandler:返回值处理器,确定目标方法的返回值该怎么处理

      2. 最终会调用invokeHandlerMethod()来执行目标方法

    3. RequestMappingHandlerAdapter 里面的invokeAndHandle()真正执行目标方法

    4. 目标方法执行完成,会返回返回值对象

    5. 然后找到一个合适的返回值处理器HandlerMethodReturnValueHandler

      1. 遍历所有的返回值处理器

      2. 最终找到 RequestResponseBodyMethodProcessor,即可以处理标注了 @ResponseBody注解的方法的处理器

    6. RequestResponseBodyMethodProcessor 调用writeWithMessageConverters 利用MessageConverter把返回值写出去

  • HttpMessageConverter先进行内容协商

    1. 遍历所有的MessageConverter,看谁支持这种内容类型的数据

    2. 默认MessageConverter如下

    image-20240322170056166.

    1. 因为最终要json类型的数据,所以选用支持响应json格式的MappingJackson2HttpMessageConverter

    2. jackson用ObjectMapper把对象写出去

WebMvcAutoConfiguration默认的HttpMessageConverters
  • EnableWebMvcConfiguration通过 addDefaultHttpMessageConverters添加了默认的MessageConverter

    • ByteArrayHttpMessageConverter:支持字节数据读写
    • StringHttpMessageConverter:支持字符串读写
    • ResourceHttpMessageConverter:支持资源读写
    • ResourceRegionHttpMessageConverter:支持分区资源写出
    • AllEncompassingFormHttpMessageConverter:支持表单xml/json读写
    • MappingJackson2HttpMessageConverter:支持请求响应体Json读写
  • 系统提供默认的MessageConverter 功能有限,仅用于json或者普通返回数据。额外增加新的内容协商功能,必须增加新的HttpMessageConverter

自定义内容返回【以yaml为例】

步骤
  1. 导入支持返回yaml数据的依赖
<dependency>
    <groupId>com.fasterxml.jackson.dataformat</groupId>
    <artifactId>jackson-dataformat-yaml</artifactId>
</dependency>
  1. 测试输出yaml格式的数据:使用YAMLFactory配置yaml的返回格式,并用ObjectMapper的writeValueAsString方法将其转成字符串
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. 编写配置:新增一种内容/媒体类型
spring.mvc.contentnegotiation.media-types.yaml=text/yaml
spring.mvc.contentnegotiation.media-types.新的格式类型名=对应的媒体类型
  1. 声明自定义的内容协商类【HttpMessageConverter】
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);
        }
    }
}
  1. 增加HttpMessageConverter组件,专门负责把对象响应为yaml格式
@Bean
public WebMvcConfigurer webMvcConfigurer(){
    return new WebMvcConfigurer() {
        @Override //配置一个能把对象转为yaml的messageConverter
        public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
            converters.add(new MyYamlHttpMessageConverter());
        }
    };
}
测试和结论
  • 使用Accept请求头

image-20240322184346346.

  • 使用请求参数的内容协商,需要配置以下信息【奇怪,如果用浏览器测试就会下载一个文件】
#使用参数进行内容协商
spring.mvc.contentnegotiation.favor-parameter=true  
#自定义参数名,默认为format
spring.mvc.contentnegotiation.parameter-name=type

image-20240322185903455image-20240322190511098

  • 总结自定义内容返回类型的步骤

    1. 配置媒体类型支持:spring.mvc.contentnegotiation.media-types.yaml=text/yaml

    2. 编写对应的HttpMessageConverter,要告诉Boot这个支持的媒体类型【见上面的MyYamlHttpMessageConverter

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

模板引擎【了解】

前言

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

image-20240323133210296.

  • 模板引擎页面默认放在 src/main/resources/templates
  • SpringBoot 包含以下模板引擎的自动配置
    • FreeMarker
    • Groovy
    • Thymeleaf
    • Mustache
  • Thymeleaf官网:https://www.thymeleaf.org/
<!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>

Thymeleaf整合

  • 导入依赖
<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的模板页面
  • 控制器需要标注@Controller注解,会将返回的逻辑视图拼接上模板前后缀组成物理视图

    • 即classpath:/templates/逻辑视图.html

基础语法

核心用法
th:xxx
  • 动态渲染指定的 html 标签属性值、或者th指令(遍历、判断等)
    • th:text:标签体内文本值渲染

    • th:utext:不会转义,显示为html原本的样子

    • th:属性:标签指定属性渲染

    • th:attr:标签任意属性渲染

    • 其他th指令:th:ifth:each

<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生成工具
语法示例
表达式
  • 变量取值:${…}
  • 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)
特殊语法
  • 无操作:_
所有以上都可以嵌套组合
'User is of type ' + (${user.isAdmin()} ? 'Administrator' : (${user.type} ?: 'Unknown'))
属性设置
  • th:href=“@{/product/list}”

  • th:attr=“class=${active}”

  • th:attr=“src=@{/images/gtvglogo.png},title=${logo},alt=#{logo}”

  • th:checked=“${user.active}”

<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}" />
遍历
  • 语法: th:each="元素名,迭代状态 : ${集合}"
  • iterStat 有以下属性
    • index:当前遍历元素的索引,从0开始
    • count:当前遍历元素的索引,从1开始
    • size:需要遍历元素的总数量
    • current:当前正在遍历的元素对象
    • even/odd:是否偶数/奇数行
    • first:是否第一个元素
    • last:是否最后一个元素
<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>
判断
  • th:if
<a
  href="comments.html"
  th:href="@{/product/comments(prodId=${prod.id})}"
  th:if="${not #lists.isEmpty(prod.comments)}"
  >view</a>
  • th:switch
<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>
属性优先级
OrderFeatureAttributes
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
行内写法
  • [[...]] or [(...)]
  • <p>Hello, [[${session.user.name}]]!</p>
变量选择
<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>
  • 等同于
<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
模板布局
  • 定义片段: th:fragment
  • 引用片段:~{templatename【模板名】::selector【片段名】}
  • 插入片段:th:insertth:replace
<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>

devtools

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
</dependency>
  • 页面修改可以使用ctrl+F9可以刷新页面
  • java代码的修改还是使用重启,使用devtools热启动可能会引起一些bug,难以排查

国际化【了解】

  • 国际化的自动配置参照MessageSourceAutoConfiguration

  • 实现步骤

    1. Spring Boot在类路径根下查找messages资源绑定文件,文件名为messages.properties

    2. 多语言可以定义多个消息文件,命名为messages_区域代码.properties

      • messages.properties:默认

      • messages_zh_CN.properties:中文环境

      • messages_en_US.properties:英语环境

    3. 在程序中可以自动注入 MessageSource组件,获取国际化的配置项值

      @Autowired 
      MessageSource messageSource;//获取国际化消息的组件
      @GetMapping("/haha")
      public String haha(HttpServletRequest request){
          //利用代码的方式获取国际化配置文件中指定的配置项的值		
      	Locale locale = request.getLocale();//获取请求中的区域信息
          String login = messageSource.getMessage("login", null, locale);
          return login;
      }
      
    4. 在页面中可以使用表达式 #{}获取国际化的配置项值

  • 2024.3.23我的吐槽:这玩意有点鸡肋呀,一套语言就要定制一套配置项

错误处理

默认机制

基础概念
  • 错误处理的自动配置都在ErrorMvcAutoConfiguration中,有两大核心机制
    • SpringBoot会自适应处理错误,根据内容协商响应页面或JSON数据【2024.3.23我的理解,就是可以响应不同类型的错误页面】
    • SpringMVC的错误处理机制依然保留,MVC处理不了才会交给boot进行处理

image-20240323144140730.

SpringMVC错误处理注解
  • @ExceptionHandler表示的方法可以处理错误,默认只能处理本类的指定错误
@ExceptionHandler(Exception.class)
public String handleExceptin(Exception e){
    return "发生异常的原因"+e.getMessage();
}
  • @ControllerAdvice统一处理所以异常信息
@ControllerAdvice//集中处理所有控制器发生的错误
public class GlobalExceptinHander {
    @ResponseBody
    @ExceptionHandler(Exception.class)
    public String handleExceptin(Exception e){
        return "统一处理异常:"+e.getMessage();
    }
}
原理
流程
  1. SpringBoot在底层写好一个BasicErrorController的组件专门处理错误请求【前提是SpringMVC的规则都无法处理】
@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. 解析错误请求需要跳转的错误页
//1、解析自定义的错误视图地址
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
//2、如果解析不到错误页面的地址,默认的错误页就是 error
return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
  1. 容器中专门有一个错误视图解析器
@Bean
@ConditionalOnBean(DispatcherServlet.class)
@ConditionalOnMissingBean(ErrorViewResolver.class)
DefaultErrorViewResolver conventionErrorViewResolver() {
    return new DefaultErrorViewResolver(this.applicationContext, this.resources);
}
  1. 错误视图解析器的默认解析规则

    1. 发生错误时先获取到错误状态码,获取精确的错误状态码处理页

      • 有模板引擎就去templates/error目录下找

      • 如果没有模板引擎,在静态资源文件夹下找

    2. 如果没有该状态码的精确页面,那就模糊匹配4xx、5xx的处理页modelAndView == null && SERIES_VIEWS.containsKey(status.series())【有无模板引擎的规则同上】

      image-20240323151423758.

    3. 如果模糊匹配都找不到,那就跳转到/error【容器中默认有一个名为error的view,提供了默认白页功能】

      @Bean(name = "error")
      @ConditionalOnMissingBean(name = "error")
      public View defaultErrorView() {
          return this.defaultErrorView;
      }
      
    @Override
    public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
        ModelAndView modelAndView = resolve(String.valueOf(status.value()), model);
        if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
            modelAndView = resolve(SERIES_VIEWS.get(status.series()), model);
        }
        return modelAndView;
    }
    private ModelAndView resolve(String viewName, Map<String, Object> model) {
        String errorViewName = "error/" + viewName;
        TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName,
                this.applicationContext);
        if (provider != null) {
            return new ModelAndView(errorViewName, model);
        }
        return resolveResource(errorViewName, model);
    }
    private ModelAndView resolveResource(String viewName, Map<String, Object> model) {
        for (String location : this.resources.getStaticLocations()) {
            try {
                Resource resource = this.applicationContext.getResource(location);
                resource = resource.createRelative(viewName + ".html");
                if (resource.exists()) {
                    return new ModelAndView(new HtmlResourceView(resource), model);
                }
            }
            catch (Exception ex) {
            }
        }
        return null;
    }
    
解析错误页的规则总结
  • 如果发生了500、404、503、403 这些错误

    1. 先精确匹配具体的错误码对应的错误页

      • 如果有模板引擎,默认在 classpath:/templates/error/错误状态码.html

      • 如果没有模板引擎,在静态资源文件夹下找 错误状态码.html

    2. 如果匹配不到错误状态码.html这些精确的错误页,就去找5xx.html4xx.html模糊匹配

      • 如果有模板引擎,默认在 classpath:/templates/error/5xx.html

      • 如果没有模板引擎,在静态资源文件夹下找 5xx.html

    3. 如果都没有就转发到error视图

      • 如果使用模板引擎且templates目录下有 error.html页面,就直接渲染
      • 否则直接使用默认的白页
  • 拓展:以下组件封装了JSON格式的错误信息

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

最佳实战

  • 前后分离

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

    • 不可预知的服务器或客户端错误,根据错误状态码进行相应处理

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

      • 核心业务,每一种错误都应该由代码控制,跳转到自己定制的错误页,然后直接${错误信息字段}【详情看下方的错误信息】
      • [[${trace}]]输出错误堆栈信息
    • 通用业务,定制classpath:/templates/error.html页面,显示错误信息

  • 不论是响应页面还是JSON,可用错误信息如下

image-20240323154340151.

嵌入式容器

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

自动配置原理

  • SpringBoot默认嵌入Tomcat作为Servlet容器
  • 根据自动配置类分析功能,嵌入式容器的自动配置类是ServletWebServerFactoryAutoConfigurationEmbeddedWebServerFactoryCustomizerAutoConfiguration
@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. springboot给容器中放了TomcatServletWebServerFactory,默认 Tomcat配置生效

      3. 三大服务器都给容器中放了一个xxxxServletWebServerFactory【xxxweb服务器工厂(造web服务器的)】

        • web服务器工厂都有一个功能:getWebServer()获取web服务器

        • TomcatServletWebServerFactory创建了tomcat

  • ServletWebServerFactory创建webServer的时机

    1. ServletWebServerApplicationContext【ioc容器】启动的时候会调用创建web服务器

    2. Spring容器刷新【onRefresh()】时候,会预留一个时机,刷新子容器【容器刷新就是容器启动的时候】

    3. refresh() 容器刷新十二大步【这一块得去看Spring相关知识】的刷新子容器会调用 onRefresh()

    @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

最佳实战

  • 修改server为前缀的相关配置就可以修改服务器参数
  • 通过给容器中放一个ServletWebServerFactory,来禁用掉SpringBoot默认放的服务器工厂,实现自定义嵌入任意服务器
<properties>
    <servlet-api.version>3.1.0</servlet-api.version>
</properties>
<!-- 切换服务器 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <exclusions>
	    <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>

全面接管SpringMVC【知识串联】

基础知识

  • SpringBoot默认配置好了 SpringMVC 的所有常用特性

  • 如果需要全面接管SpringMVC的所有配置并禁用默认配置,需要编写一个WebMvcConfigurer配置类,并标注 @EnableWebMvc 即可

  • 全手动模式

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

WebMvcAutoConfiguration自动配置了哪些规则【了解】

  • SpringMVC自动配置场景配置了如下的所有默认行为

image-20240323170715607.

  • 支持RESTful的filter:HiddenHttpMethodFilter

  • 支持非POST请求,请求体携带数据:FormContentFilter

  • WebMvcAutoConfigurationAdapter是一个WebMvcConfigurerWebMvcConfigurer定义了MVC默认的底层行为

    • 视图解析器

      • InternalResourceViewResolver,根据返回的逻辑视图跳转到对应页面
      • BeanNameViewResolver,根据逻辑视图找到对应的组件【用视图名去匹配组件名】
    • 内容协商解析器:ContentNegotiatingViewResolver

    • 请求上下文过滤器:RequestContextFilter任意位置都可以直接获取当前请求【这个好像谷粒商城有用到过】

    • 静态资源链规则

    • ProblemDetailsExceptionHandler:捕获SpringMVC内部场景异常的错误详情

  • WebMvcAutoConfigurationAdapter导入了EnableWebMvcConfiguration,它配置了如下的组件

    • RequestMappingHandlerAdapter

    • WelcomePageHandlerMapping欢迎页功能支持(模板引擎/静态资源目录放index.html),项目访问/就展示这个页面

    • RequestMappingHandlerMapping:找每个请求由谁处理的映射关系

    • ExceptionHandlerExceptionResolver:默认的异常解析器

    • FormattingConversionService: 数据格式化 、类型转化

    • Validator: 数据校验JSR303提供的数据校验功能

    • WebBindingInitializer:请求参数的封装与绑定

    • ContentNegotiationManager:内容协商管理器

    • LocaleResolver:国际化解析器

    • ThemeResolver:主题解析器

    • FlashMapManager:临时数据共享

WebMvcConfigurer功能【了解】

  • WebMvcConfigurer定义和扩展SpringMVC底层功能
提供方法核心参数功能默认
addFormattersFormatterRegistry格式化器:支持属性上@NumberFormat和@DatetimeFormat的数据类型转换GenericConversionService
getValidator数据校验:校验 Controller 上使用@Valid标注的参数合法性。需要导入starter-validator
addInterceptorsInterceptorRegistry拦截器:拦截收到的所有请求
configureContentNegotiationContentNegotiationConfigurer内容协商:支持多种数据格式返回。需要配合支持这种类型的HttpMessageConverter支持 json
configureMessageConvertersList<HttpMessageConverter<?>>消息转换器:标注@ResponseBody的返回值会利用MessageConverter直接写出去8 个,支持byte,string,multipart,resource,json
addViewControllersViewControllerRegistry视图映射:直接将请求路径与物理视图映射。用于无 java 业务逻辑的直接视图页渲染无 mvc:view-controller
configureViewResolversViewResolverRegistry视图解析器:逻辑视图转为物理视图ViewResolverComposite
addResourceHandlersResourceHandlerRegistry静态资源处理:静态资源路径映射、缓存控制ResourceHandlerRegistry
configureDefaultServletHandlingDefaultServletHandlerConfigurer默认 Servlet:可以覆盖 Tomcat 的DefaultServlet。让DispatcherServlet拦截/
configurePathMatchPathMatchConfigurer路径匹配:自定义 URL 路径匹配。可以自动为所有路径加上指定前缀,比如 /api
configureAsyncSupportAsyncSupportConfigurer异步支持TaskExecutionAutoConfiguration
addCorsMappingsCorsRegistry跨域
addArgumentResolversList参数解析器mvc 默认提供
addReturnValueHandlersList返回值解析器mvc 默认提供
configureHandlerExceptionResolversList异常处理器默认 3 个 ExceptionHandlerExceptionResolver ResponseStatusExceptionResolver DefaultHandlerExceptionResolver
getMessageCodesResolver消息码解析器:国际化使用

@EnableWebMvc禁用默认行为

  1. @EnableWebMvc给容器中导入 DelegatingWebMvcConfiguration组件,该组件继承于WebMvcConfigurationSupport

  2. WebMvcAutoConfiguration有一个核心的条件注解,@ConditionalOnMissingBean(WebMvcConfigurationSupport.class),容器中没有WebMvcConfigurationSupportWebMvcAutoConfiguration才生效

  3. @EnableWebMvc 导入 WebMvcConfigurationSupport 导致 WebMvcAutoConfiguration 失效。导致禁用了默认行为

  • 总结
    • @EnableWebMVC 禁用了Mvc的自动配置
    • WebMvcConfigurer定义SpringMVC底层组件的功能类

Web新特性【了解】

Problemdetails

基础知识
  • 文档: https://www.rfc-editor.org/rfc/rfc7807

  • ProblemDetailsExceptionHandler 是一个 @ControllerAdvice,用于集中处理系统异常

@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();
    }
}
  • 可以处理以下异常,如果系统出现以下异常,会被SpringBoot支持RFC 7807规范方式返回错误数据,不是默认开启的
@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
})
使用
  • 没有开启ProblemDetails功能,状态码为405的json类型的错误信息响应格式如下
{
    "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 ...
    ,
    "message": "Method 'POST' is not supported.",
    "path": "/list"
}
  • 开启ProblemDetails功能,使用新的MediaTypeContent-Type: application/problem+json+,还可以扩展额外的错误信息
{
    "type": "about:blank",
    "title": "Method Not Allowed",
    "status": 405,
    "detail": "Method 'POST' is not supported.",
    "instance": "/list"
}

函数式Web【可以跳过】

基础知识
  • SpringMVC 5.2 以后允许使用函数式的方式,定义Web的请求处理流程【函数式接口】

  • Web请求处理的方式

    • @Controller + @RequestMapping:耦合式 (路由、业务耦合)

    • 函数式Web:分离式(路由、业务分离)

  • 核心类

    • RouterFunction:定义路由信息
    • RequestPredicate:定义请求规则,即请求方式、接收的请求参数等
    • ServerRequest:封装请求数据
    • ServerResponse:封装响应数据
  • 场景:User RESTful - CRUD

    • GET /user/1 获取1号用户
    • GET /users 获取所有用户
    • POST /user 请求体携带JSON,新增一个用户
    • PUT /user/1 请求体携带JSON,修改1号用户
    • DELETE /user/1 删除1号用户
示例
  • 使用步骤
    1. 给容器中放一个RouterFunction类型的组件【集中定义路由信息】
    2. router()方法用于定义路由信息
    3. 使用对应的业务处理器处理请求
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()
            		//请求路径	  接收参数类型   交给谁处理,用什么方法处理【每个业务都准备自己的handler】
                .GET("/{user}", ACCEPT_JSON, userHandler::getUser)
                .GET("/{user}/customers", ACCEPT_JSON, userHandler::getUserCustomers)
                .DELETE("/{user}", ACCEPT_JSON, userHandler::deleteUser)
                .build();//构建出上述规则的RouterFunction
    }
}
  • 业务处理器如下
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();
    }
}

数据访问–整合SSM场景

整合步骤

  1. 创建项目image-20240324150518826,springboot初始化向导自动导入了以下场景
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>3.0.3</version>
</dependency>
<dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <scope>runtime</scope>
</dependency>
  1. 配置数据库连接信息
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/study
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=1212go12
  1. 配置MyBatis相关信息
    • 小技巧:安装MyBatisX插件,可以帮忙生成Mapper接口的xml文件
#指定mapper映射文件位置
mybatis.mapper-locations=classpath:/mapper/*.xml
#开启驼峰命名规则,会被数据库中的带有_的字段转成驼峰命名
mybatis.configuration.map-underscore-to-camel-case=true
  1. 在主启动类中,标识@MapperScan注解,扫描mapper接口所在包,之后就可以自动注入mapper接口对象了
@MapperScan("com.study.liao.boot3ssmtest.mapper")
@SpringBootApplication
public class Boot3SsmTestApplication {
    public static void main(String[] args) {
        SpringApplication.run(Boot3SsmTestApplication.class, args);
    }
}
  1. 测试
@RestController
public class UserController {
    @Autowired
    UserMapper userMapper;
    @RequestMapping("/user/{id}")
    public TUser getUser(@PathVariable("id") Long id){
        return userMapper.getUserName(id);
    }
}

image-20240324163047259.

自动配置

jdbc场景的自动配置

  • mybatis-spring-boot-starter导入spring-boot-starter-jdbc【jdbc是操作数据库的场景】

  • Jdbc场景的几个自动配置

    • 数据源的自动配置:org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration

      • 所有和数据源有关的配置都绑定在DataSourceProperties
      • 默认使用 HikariDataSource
    • 给容器中放了JdbcTemplate用于操作数据库【其实这玩意没卵用】:org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration

    • org.springframework.boot.autoconfigure.jdbc.JndiDataSourceAutoConfiguration

    • 基于XA二阶提交协议的分布式事务数据源:org.springframework.boot.autoconfigure.jdbc.XADataSourceAutoConfiguration

    • 事务管理器:org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration

  • jdbc场景具有的底层能力:数据源、JdbcTemplate、事务

MyBatis场景的自动配置

  • mybatis-spring-boot-starter导入mybatis-spring-boot-autoconfigure(mybatis的自动配置包)

    • 默认加载两个自动配置类

      • org.mybatis.spring.boot.autoconfigure.MybatisLanguageDriverAutoConfiguration

      • org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration

        image-20240324164217607.

        • 必须在数据源配置好之后才生效
        • MyBatis的所有配置绑定在MybatisProperties
    • MybatisAutoConfiguration的作用

      • 给容器中放入SqlSessionFactory组件,用于创建和数据库的一次会话
      • 给容器中放入SqlSessionTemplate组件,用于操作数据库
    • @MapperScan注解创建每个Mapper接口的代理对象并放到容器中

      • 利用@Import(MapperScannerRegistrar.class)批量给容器中注册组件
      • 解析指定的包路径里面的每一个类,为每一个Mapper接口类,创建Bean定义信息,注册到容器中
  • 总结:MyBatisAutoConfiguration配置了MyBatis的整合流程

分析哪些自动配置类生效

  • classpath:/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件中配置的所有值,就是要开启的自动配置类,但是每个类可能有条件注解,基于条件注解判断哪个自动配置类生效了
  • 配置debug=true开启调试模式,就可以详细打印出开启的自动配置【快速定位生效的配置】
    • Positive是生效的自动配置
    • Negative是未生效的自动配置

扩展:整合其他数据源

  • 以Druid 数据源为例

    1. 导入druid-starter

    2. 写配置

    3. 分析自动配置了哪些东西,怎么用

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

#数据源基本配置
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

基础特性

SpringApplication【了解】

自定义banner

  • banner就这玩意

image-20240324170323024.

image-20240324170903307.

  • 配置spring.main.banner-mode=off可以关闭banner

自定义SpringApplication

  • 个人感觉不是很重要,都有配置文件了,用代码修改底层配置有点本末倒置了,了解一下就行

  • 拆分写法

    • 将运行SpringApplication方法拆分成两步,中间可以修改底层配置
    • 配置文件的优先级高于程序化调整的优先级
@SpringBootApplication
public class MyApplication {
    public static void main(String[] args) {
        SpringApplication application = new SpringApplication(MyApplication.class);
        //1.自定义spring应用的底层设置【但是优先级比配置文件低】
        application.setBannerMode(Banner.Mode.OFF);//关闭banner功能
        //2.运行spring应用
        application.run(args);
    }
}
  • FluentBuilder API(流式写法),注意:必须要用sources(应用名.class)指定源程序,不指定会抛异常image-20240324172135558
public static void main(String[] args) {
    new SpringApplicationBuilder()
            .main(Boot3FeaturesApplication.class)
            .sources(Boot3FeaturesApplication.class)
            .bannerMode(Banner.Mode.OFF)
            .run(args);
}

Profiles

基础知识

  • 作用:具有环境隔离能力,快速切换开发、测试、生产环境

  • 步骤

    1. 标识环境:指定哪些组件、配置在哪个环境生效

    2. 切换环境:这个环境对应的所有组件和配置就应该生效

使用

指定环境
  • Spring Profiles 提供一种隔离配置的方式,使其仅在特定环境生效
  • 容器中的组件(包含配置类)都可以被@Profile标记,来指定在什么环境下被加载
@Profile({"text","dev"})
@Component
public class Cat {
}
  • 内部嵌套的组件需要激活内外部环境才能生效spring.profiles.active=test,dev,其实很好理解
    • 如果只有test环境只保证Dog生效,并不保证dev环境下的pig生效
    • 只有dev环境,配置类Dog都不生效,怎么将该配置中的组件Pig注入容器
@Profile({"test"})
@Configuration
public class Dog {
    @Profile({"dev"})
    @Bean
    public Pig pig(){
        return new Pig();
    }
}
环境激活
  1. 配置文件激活指定环境【推荐使用】
spring.profiles.active=production,hsqldb
  1. 也可以使用命令行激活--spring.profiles.active=dev,hsqldb

    image-20240324183423070.

  2. 还可以配置默认环境【默认环境是始终生效的】

    • springboot默认环境为default
    • 配置默认环境名称spring.profiles.default=test
  • 注意:不标注@Profile的组件永远生效
环境包含
  • 也可以额外添加生效文件,而不是激活替换【不管激活了哪个环境,都额外包含include配置的环境,即总是生效的环境】
spring.profiles.include[0]=common
spring.profiles.include[1]=local
  • 最佳实战

    • 生效的环境 = 激活的环境/默认环境 + 包含的环境

    • 项目里面这么用

      • 基础的配置【mybatislog】写到包含环境中
      • 需要动态切换的配置【 dbredis】写到激活的环境中

Profile分组

  • 创建prod组,指定包含db和mq配置
#写法一
spring.profiles.group.prod[0]=db
spring.profiles.group.prod[1]=mq
#写法二
spring.profiles.group.prod=db,mq
  • 指定--spring.profiles.active=prod,就会激活prod,db,mq有关配置

Profile配置文件

  • application.properties是主配置文件,任何情况下都生效

  • application-{环境名}.properties可以作为指定环境的配置文件

  • profile优先级>application优先级 ,如果application和激活的环境配置文件都有同一项配置,最终生效的是环境配置文件的配置项

  • 激活当前环境对应的配置就会生效,最终生效的所有配置包括

    • application-{当前环境}.properties:当前环境的配置文件的所有配置

    • application.properties:主配置文件中和激活文件中不冲突的所有配置项【优先级的原因】

  • 激活有关的配置项如spring.profiles.activespring.profiles.default只能写在主配置类中,在指定环境中配置是无效的

19:07:23.658 [main] ERROR org.springframework.boot.SpringApplication -- Application run failed
org.springframework.boot.context.config.InvalidConfigDataPropertyException: Property 'spring.profiles.active' imported from location 'class path resource [application-dev.properties]' is invalid in a profile specific resource [origin: class path resource [application-dev.properties] - 3:24]
	at org.springframework.boot.context.config.InvalidConfigDataPropertyException.lambda$throwIfPropertyFound$1(InvalidConfigDataPropertyException.java:121)

外部化配置

场景引入

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

配置优先级

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

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

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

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

    • 默认属性(通过SpringApplication.setDefaultProperties指定的)

      image-20240325134846873.

    • @PropertySource加载指定的配置文件(需要写在@Configuration类上才可生效)

      image-20240325134947359.

    • 配置文件(application.properties/yml等)

    • RandomValuePropertySource支持的random.*配置(如:@Value(“${random.int}”))

    • OS 环境变量

    • Java 系统属性(System.getProperties())

    • JNDI 属性(来自java:comp/env)

    • ServletContext 初始化参数

    • ServletConfig 初始化参数

    • SPRING_APPLICATION_JSON属性(内置在环境变量或系统属性中的 JSON)

    • 命令行参数

      • 如下图所示,也可以使用控制台命令java -jar xxx.jar --server.port=9999

      image-20240325135050947.

    • 测试属性(@SpringBootTest进行测试时指定的属性)

    • 测试类@TestPropertySource注解

    • Devtools 设置的全局属性($HOME/.config/spring-boot)

  • 结论

    • 配置可以写到很多位置
    • 常见的优先级顺序:命令行> 配置文件> springapplication配置
配置文件优先级
  • 从低到高,后面覆盖前面

    • jar 包内的application.properties/yml

    • jar 包内的application-{profile}.properties/yml

    • jar 包的application.properties/yml

    • jar 包的application-{profile}.properties/yml

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

  • 结论

    • 包外 > 包内
    • 同级情况下:profile配置 > application配置
  • 演示场景

    • 包内: application.properties server.port=8000

    • 包内: application-dev.properties server.port=9000

    • 包外: application.properties server.port=8001

    • 包外: application-dev.properties server.port=9001

    • 所有参数均可由命令行传入

      • 使用--参数项=参数值将会被添加到环境变量中,并优先于配置文件
      • 比如java -jar app.jar --name="Spring",可以使用@Value(“${name}”)获取
    • 启动端口:命令行 > 9001 > 8001 > 9000 > 8000

外部配置

顺序
  • SpringBoot 应用启动时会自动寻找application.properties和application.yaml位置进行加载,顺序如下【由低到高】
    • 类路径:内部

      • 类根路径
      • 类下/config包
    • 当前路径(项目所在的位置):外部

      • 当前路径

      • 当前下/config子目录

      • /config目录的直接子目录

结论
  • 命令行 > 包外config直接子目录 > 包外config目录 > 包外根目录 > 包内目录

    • 命令行 > 所有
    • 包外 > 包内
    • config目录 > 根目录
    • profile > application
  • 同级比较

    • profile配置 > 默认配置
    • properties配置 > yaml配置
  • 配置不同就都生效(互补),配置相同高优先级覆盖低优先级

!!!一图流总结【很详细】

image-20240325140958110.

导入配置

  • 使用spring.config.import可以导入额外配置,类似于@PropertySource
spring.config.import=my.properties
my.property=value
  • 无论以上写法的先后顺序,my.properties的值总是优先于直接在文件中编写的my.property【导入文件的优先级低于配置文件的优先级,因为外部大于内部,而且该方式的优先级和@PropertySource一样】

属性占位符

  • 可以使用 ${name:default}形式取出之前配置过的值【:default是为了指定默认值】
@Value("${name:666}")
private String name;
  • 配置文件中也可以使用
app.name=MyApp
app.description=${app.name} is a Spring Boot application written by ${username:Unknown}

单元测试-JUnit5【了解】

整合

  • SpringBoot 提供一系列测试工具集及注解方便测试
  • spring-boot-test提供核心测试能力,spring-boot-test-autoconfigure 提供测试的一些自动配置
  • 只需要导入spring-boot-starter-test 即可整合测试,测试类必须在主启动类所在包或者子包,且需要标识@SpringBootTest
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

测试

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

  • 注解

    • @Test:标识的方法是测试方法。但是与JUnit4的@Test不同,他的职责非常单一不能声明任何属性,拓展的测试将会由Jupiter提供额外测试
    • @ParameterizedTest:表示方法是参数化测试,下方会有详细介绍
    • @RepeatedTest:表示方法可重复执行,下方会有详细介绍
    • @DisplayName:为测试类或者测试方法设置展示名称
    • @BeforeEach:表示在每个单元测试之前执行
    • @AfterEach:表示在每个单元测试之后执行
    • @BeforeAll:表示在所有单元测试之前执行
    • @AfterAll:表示在所有单元测试之后执行
    • @Tag:表示单元测试类别,类似于JUnit4中的@Categories
    • @Disabled:表示测试类或测试方法不执行,类似于JUnit4中的@Ignore
    • @Timeout:表示测试方法运行如果超过了指定时间将会返回错误
    • @ExtendWith:为测试类或测试方法提供扩展类引用

断言

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

嵌套测试

  • JUnit 5 可以通过 Java 中的内部类和@Nested注解实现嵌套测试,从而可以更好的把相关的测试方法组织在一起

  • 在内部类中可以使用@BeforeEach 和@AfterEach 注解,而且嵌套的层次没有限制

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

参数化测试

  • 参数化测试是JUnit5很重要的一个新特性,它使得用不同的参数多次运行测试成为了可能,也为单元测试带来许多便利
  • 利用**@ValueSource**等注解,将可以使用不同的参数进行多次单元测试,而不需要每新增一个参数就新增一个单元测试,省去了很多冗余代码
    • @ValueSource:为参数化测试指定入参来源,支持八大基础类以及String类型,Class类型
    • @NullSource:表示为参数化测试提供一个null的入参
    • @EnumSource:表示为参数化测试提供一个枚举入参
    • @CsvFileSource:表示读取指定CSV文件内容作为参数化测试入参
    • @MethodSource:表示读取指定方法的返回值作为参数化测试入参(注意方法返回需要是一个流)参数化测试

!!!核心原理

事件和监听器

生命周期监听

  • 场景:监听应用的生命周期,要在应用启动的某一刻执行某些操作
监听器
  • 自定义SpringApplicationRunListener监听事件

    1. 编写SpringApplicationRunListener 实现类

      public class MySpringBootListener implements SpringApplicationRunListener {
          @Override
          public void starting(ConfigurableBootstrapContext bootstrapContext) {
              System.out.println("正在启动");
          }
          @Override
          public void environmentPrepared(ConfigurableBootstrapContext bootstrapContext, ConfigurableEnvironment environment) {
              System.out.println("环境准备完成");
          }
          @Override
          public void contextPrepared(ConfigurableApplicationContext context) {
              System.out.println("上下文【ioc容器】准备完成");
          }
          @Override
          public void contextLoaded(ConfigurableApplicationContext context) {
              System.out.println("上下文【ioc容器】加载完成");
          }
          @Override
          public void started(ConfigurableApplicationContext context, Duration timeTaken) {
              System.out.println("启动完成");
          }
          @Override
          public void ready(ConfigurableApplicationContext context, Duration timeTaken) {
              System.out.println("应用准备就绪");
          }
          @Override
          public void failed(ConfigurableApplicationContext context, Throwable exception) {
              System.out.println("启动失败才生效");
          }
      }
      
    2. META-INF/spring.factories 中配置接口的全类名=实现类的全类名

      • org.springframework.boot.SpringApplicationRunListener=自己的Listener
      • 还可以指定一个有参构造器,接受两个参数(SpringApplication application, String[] args)
      org.springframework.boot.SpringApplicationRunListener=com.study.liao.boot3features.listener.MySpringBootListener
      

      image-20240325151403673.

    3. 测试

      image-20240325151444589.

  • springboot在spring-boot.jar中默认配置的Listener

    image-20240325150607463.

生命周期全流程
  1. 在项目启动之前,先去META-INF/spring.factories读取Listener

image-20240325152131672.

  1. 引导:利用BootstrapContext引导整个项目启动

    1. starting:应用开始,SpringApplication的run方法一调用,只要有了BootstrapContext就执行

    2. environmentPrepared:环境准备好(把启动参数等绑定到环境变量中),但是ioc还没有创建【调一次】

  2. 启动

    1. contextPrepared:ioc容器创建并准备好,并关闭引导上下文,但是sources(主配置类)没加载,组件都没创建【调一次】

    2. contextLoaded:ioc容器加载,主配置类加载进去了,但是ioc容器还没刷新(即bean还没创建)【调一次】

      =截止到这里,ioc容器里面还没创建bean=

    3. started:ioc容器刷新了(所有bean造好了),但是runner没调用

    4. ready:ioc容器刷新了(所有bean造好了),所有runner调用完了

  3. 运行:以上步骤都正确执行,代表容器running

image-20240325151619291.

事件触发时机

各种回调监听器
  • BootstrapRegistryInitializer:感知引导初始化【感知特定阶段】

    • 可以在META-INF/spring.factories配置,也可以调用application.addBootstrapRegistryInitializer()
    • 创建引导上下文bootstrapContext的时候触发
    • 场景:进行密钥校对授权
  • ApplicationContextInitializer: 感知ioc容器初始化【感知特定阶段】

    • 可以在META-INF/spring.factories配置,也可以调用application.addInitializers()
  • ApplicationListener基于事件机制感知事件,具体的阶段可以做别的事【感知全阶段的事件

    • 可以在META-INF/spring.factories配置,也可以调用SpringApplication.addListeners(…)SpringApplicationBuilder.listeners(…)
    • 也可以通过@Bean@EventListener注入IOC容器
    • 不仅能感知生命周期的事件,还可以感知运行时发生的事件,可以实现基于事件驱动模式的应用开发
  • SpringApplicationRunListener:各种阶段都能自定义操作,功能比ApplicationListener更完善【感知全阶段生命周期

    • 需要在META-INF/spring.factories配置
  • ApplicationRunner:感知应用就绪Ready,应用卡死就不会就绪【感知特定阶段】

    • 只要是容器中的组件就可以被获取到,可以使用【不限于】@Bean注入容器
    @Bean
    public ApplicationRunner applicationRunner(){
        return args -> {
            System.out.println("ApplicationRunner运行了,参数为"+args);
        };
    }
    
  • CommandLineRunner:感知应用就绪Ready,应用卡死就不会就绪【感知特定阶段】

    • 注入方式和ApplicationRunner一模一样
自定义ApplicationListener
  1. 实现接口
public class MyListener implements ApplicationListener<ApplicationEvent> {
    @Override
    public void onApplicationEvent(ApplicationEvent event) {
        System.out.println("事件到达"+event);
    }
}
  1. 配置信息org.springframework.context.ApplicationListener=com.study.liao.boot3features.listener.MyListener

  2. 测试【生命周期每一个阶段都对应一个事件】

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

事件完整触发流程【九大事件】
  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-20240325160144883.

  • 应用事件发送顺序如下

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • 感知应用是否存活了:虽然活着但是不能处理请求【植物人】
  • 应用是否就绪了:能响应请求,说明确实活的比较好
SpringBoot 事件驱动开发
  • 应用启动过程生命周期事件感知(9大事件)、应用运行中事件感知(无数种)
    • 事件发布:事件发布类实现ApplicationEventPublisherAware接口或自动注入ApplicationEventMulticaster调用底层的事件发布方法,事件是广播发布的
    • 事件监听
      • 实现ApplicationListener接口或者是组件 + @EventListener
      • 默认的监听顺序是按照全类名的字母序排列的,可以通过@Order注解修改优先级
  • 场景模拟

image-20240325162127192.

image-20240325162459313.

  1. 创建事件实体【消息】

    @AllArgsConstructor
    @Data
    public class UserEntity  {
        private String userName;
        private String password;
    }
    
  2. 实现ApplicationEvent,封装事件消息

    public class LoginEvent extends ApplicationEvent {
        public LoginEvent(UserEntity source) {
            super(source);
        }
    }
    
  3. 实现ApplicationEventPublisherAware接口,通过底层的事件发布者发布事件

    @Service
    public class EventPublisher implements ApplicationEventPublisherAware {
        ApplicationEventPublisher applicationEventPublisher;
        public void sendEvent(ApplicationEvent event){
            //调用底层的api发送事件
            applicationEventPublisher.publishEvent(event);
        }
        @Override
        public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
            //可以获取到springboot的事件发布组件
            this.applicationEventPublisher=applicationEventPublisher;
        }
    }
    
  4. 测试

    @RestController
    public class TestPublisher {
        @Autowired
        EventPublisher eventPublisher;
        @RequestMapping("/test")
        public String testEvent(){
            UserEntity userEntity = new UserEntity("3220999", "1212go12");
            eventPublisher.sendEvent(new LoginEvent(userEntity));
            return "ok";
        }
    }
    ==========底层的事件监听器可以感知到========
    事件到达com.study.liao.boot3features.event.LoginEvent[source=UserEntity(userName=3220999, password=1212go12)]
    
  5. 使用@EventListener监听事件

    @Service
    public class LoginEventListener {
        @EventListener
        public void getLoginMessage(LoginEvent event){
            System.out.println("感知到事件"+event);
        }
    }
    ========感知结果===========
    感知到事件com.study.liao.boot3features.event.LoginEvent[source=UserEntity(userName=3220999, password=1212go12)]
    

最佳实战

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

自动配置原理

入门理解

自动配置流程

image-20240326161321331.

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

  1. 导入starter场景之后会导入autoconfigure

  2. 寻找类路径下 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件

  3. 应用启动会加载所有自动配置类 xxxAutoConfiguration

  4. 给容器中配置功能组件

    • 组件参数绑定到属性类xxxProperties

    • 属性类和配置文件前缀项绑定

    • @Contional派生的条件注解进行判断是否组件生效

  5. 效果

    • 修改配置文件,修改底层参数

    • 所有场景自动配置好直接使用

    • 可以注入SpringBoot配置好的组件随时使用

SPI机制【了解,回答来自ChatGPT-3.5】
  • 勾吧,能不能给点官方回复

  • Java中的SPI(Service Provider Interface)是一种软件设计模式,用于在应用程序中动态地发现和加载组件

  • SPI的思想是,定义一个接口或抽象类,然后通过在classpath中定义实现该接口的类来实现对组件的动态发现和加载

  • SPI的主要目的是解决在应用程序中使用可插拔组件的问题

    • 例如,一个应用程序可能需要使用不同的日志框架或数据库连接池,但是这些组件的选择可能取决于运行时的条件
    • 通过使用SPI,应用程序可以在运行时发现并加载适当的组件,而无需在代码中硬编码这些组件的实现类
    • 通过使用SPI,应用程序可以实现更灵活、可扩展的架构,同时也可以避免硬编码依赖关系和增加代码的可维护性
  • 在Java中,SPI的实现方式是通过在META-INF/services目录下创建一个以服务接口全限定名为名字的文件,文件中包含实现该服务接口的类的全限定名

    • SpringBoot的SPI机制是找META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
    • 当应用程序启动时,Java的SPI机制会自动扫描classpath中的这些文件,并根据文件中指定的类名来加载实现类
功能开关
  • 自动配置:全部都配置好,什么都不用管【自动批量导入】

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

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

进阶理解

@SpringBootApplication
  • 包含以下三个重要注解

    • @SpringBootConfiguration:就是@Configuration配置类,spring ioc启动就会加载创建这个类对象

    • @EnableAutoConfiguration:开启自动配置,包含以下两个重要注解

      • @AutoConfigurationPackage:扫描主程序包,加载自己的组件
    • 利用 @Import(AutoConfigurationPackages.Registrar.class) 把主程序所在的包的所有组件导入进来

    • @Import(AutoConfigurationImportSelector.class):加载所有自动配置类,即加载starter导入的组件

      List<String> configurations = ImportCandidates.load(AutoConfiguration.class, getBeanClassLoader())
          .getCandidates();
      
      • 扫描SPI文件:META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
  • @ComponentScan:组件扫描,排除前面已经扫描进来的配置类和自动配置类

    @ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
          @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
    
完整启动加载流程【得配合前面的生命周期】
image-20240326163205953

!!!自定义starter【看得很爽,建议跟着敲】

需求

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

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

  1. 创建自定义starter项目,引入spring-boot-starter基础依赖

  2. 编写模块功能,引入模块所有需要的依赖。

  3. 编写xxxAutoConfiguration自动配置类,帮其他项目导入这个模块需要的所有组件

  4. 编写配置文件META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports指定启动需要加载的自动配置

  5. 其他项目引入即可使用

通用业务代码

  1. 导入配置处理器的依赖,配置文件中自定义的properties配置都会有提示

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

    image-20240326165718487.

  2. 配置绑定

    @Component
    @Data
    @ConfigurationProperties(prefix = "robot")//和配置文件的robot前缀进行绑定
    public class RobotProperties {
        private String name;
        private Integer age;
    }
    
  3. 业务方法

    @Service
    public class RobotService {
        @Autowired
        RobotProperties robotProperties;
        public String sayHello(){
            return "你好"+robotProperties.getName()+"\n年龄"+robotProperties.getAge();
        }
    }
    
  4. 测试

    @RestController
    public class RobotController {
        @Autowired
        RobotService robotService;
        @RequestMapping("/robot")
        public String sayHello(){
            return robotService.sayHello();
        }
    }
    

    image-20240326165853088.

基本抽取

  1. 创建starter项目,导入公共代码所有需要用到的依赖,然后把公共代码复制进来

    image-20240326185834837.

  2. 此时导入该自定义场景,发现组件无法生效,因为starter所在的包和引入它的项目的主程序所在的包不是父子层级

  3. 在自定义starter中配置 RobotAutoConfiguration,给容器中导入这个场景需要的所有组件【为了方便其它项目导入所需组件】

    • 使用@Import导入
    @Import({RobotController.class, RobotService.class, RobotProperties.class})//导入robot功能所需要的组件
    @Configuration
    public class RobotAutoConfiguration {
    }
    
    • 使用@Bean导入
    @Configuration
    public class RobotAutoConfiguration {
        @Bean
        public RobotProperties robotProperties(){
            return new RobotProperties();
        }
        //省略其它组件的注入
    }
    
  4. 其它项目引用这个starter,直接导入这个 RobotAutoConfiguration就能把这个场景的组件导入进来

    @Import(RobotAutoConfiguration.class)
    @SpringBootApplication
    public class Boot3FeaturesApplication {
        public static void main(String[] args) {
           SpringApplication.run(Boot3FeaturesApplication.class, args);
        }
    }
    
  5. 在引入的项目中编写配置文件,测试功能是否生效

    robot.name=里奥
    robot.age=21
    

    image-20240326190348677.

使用@EnableXxx机制

  1. 自定义starter中定义一个注解@EnableRoot,在注解中导入RobotAutoConfiguration

    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.TYPE})
    @Documented
    @Import({RobotAutoConfiguration.class})
    public @interface EnableRoot {
    }
    
  2. 其它项目导入所需组件时直接标识@EnableRoot注解即可开启功能

    @EnableRoot
    @SpringBootApplication
    public class Boot3FeaturesApplication {
        public static void main(String[] args) {
           SpringApplication.run(Boot3FeaturesApplication.class, args);
        }
    }
    

完全自动配置

  • 需求:导入starter时自动生效,连注解都不用标识

  • 原理:依赖SpringBoot的SPI机制

  1. 给类资源文件夹下放一个META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

    image-20240326191843282.

  2. 在文件中编写好自定义的自动配置类的全类名

    image-20240326192011248.

  3. 项目启动,会自动加载自定义的自动配置类,连注解都不用写,只需要在配置文件中配置信息即可

    image-20240326192157606.

  • 40
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值