【SpringBoot3】核心特性(入门)

1 SpringBoot3-快速入门

1.1 简介

1.1.1 前置知识

  • Java17
  • Spring、SpringMVC、MyBatis
  • Maven、IDEA

1.1.2 环境要求

环境&工具

版本(or later)

SpringBoot

3.0.5+

IDEA

2021.2.1+

Java

17+

Maven

3.5+

Tomcat

10.0+

Servlet

5.0+

GraalVM Community

22.3+

Native Build Tools

0.9.19+

1.1.3 SpringBoot是什么

SpringBoot 帮我们简单、快速地创建一个独立的、生产级别的 Spring 应用(说明:SpringBoot底层是Spring)

大多数 SpringBoot 应用只需要编写少量配置即可快速整合 Spring 平台以及第三方技术

特性:

  • 快速创建独立 Spring 应用

        ○ SSM:导包、写配置、启动运行

  • 直接嵌入Tomcat、Jetty or Undertow(无需部署 war 包)【Servlet容器】

        ○ linux java tomcat mysql: war 放到 tomcat 的 webapps下

        ○ jar: java环境; java -jar

  • 重点:提供可选的starter,简化应用整合

        ○ 场景启动器(starter):web、json、邮件、oss(对象存储)、异步、定时任务、缓存...

        ○ 导包一堆,控制好版本。

        ○ 为每一种场景准备了一个依赖; web-starter。mybatis-starter

  • 重点:按需自动配置 Spring 以及 第三方库

        ○ 如果这些场景我要使用(生效)。这个场景的所有配置都会自动配置好。

        ○  约定大于配置:每个场景都有很多默认配置。

        ○  自定义:配置文件中修改几项就可以

  • 提供生产级特性:如 监控指标、健康检查、外部化配置等

        ○  监控指标、健康检查(k8s)、外部化配置

  • 无代码生成、无xml

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

1.2 快速体验

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

1.2.1 开发流程

1. 创建项目

maven 项目

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

2. 导入场景

场景启动器

<dependencies>
<!--web开发的场景启动器-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>

3. 主程序

@SpringBootApplication //这是一个SpringBoot应用
public class MainApplication {

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

4. 业务

//@ResponseBody
//@Controller
@RestController
public class HelloController {

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

        return "Hello,Spring Boot 3!";
    }

}

5. 测试

默认启动访问: localhost:8080

6. 打包

<!--SpringBoot应用打包插件-->
<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

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

java -jar demo.jar 启动项目

1.2.2 特性小结

1. 简化整合

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

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

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

场景一导入,万物皆就绪

2. 简化开发

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

3. 简化配置

application.properties

4. 简化部署

打包为可执行的jar包。

只需linux服务器上有java环境。

5. 简化运维

修改配置(外部放一个application.properties文件,外部有,项目内部也有,以外部优先)、监控、健康检查。

.....

1.2.3 Spring Initializr 创建向导

一键创建好整个项目结构

1.3 应用分析

1.3.1 依赖管理机制

思考:

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

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

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

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

3、自定义版本号

  • 利用maven的就近原则

        ○ 第一种:直接在当前项目properties标签中声明父项目用的版本属性的key

<properties>
    <mysql.version>8.0.31</mysql.version>
</properties>

        ○ 第二种:直接在导入依赖的时候声明版本

<dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <version>8.0.31</version>
</dependency>

4、第三方的jar包

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

image.png

1.3.2 自动配置机制

1.3.2.1 初步理解
  • 自动配置的 Tomcat、SpringMVC 等

        ○ 导入场景,容器中就会自动配置好这个场景的核心组件。

        ○ 以前:DispatcherServlet、ViewResolver、CharacterEncodingFilter....

        ○ 现在:自动配置好的这些组件

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

@SpringBootApplication //这是一个SpringBoot应用
public class MainApplication {

    public static void main(String[] args) {

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

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

    }
}
  • 默认的包扫描规则
    • @SpringBootApplication 标注的类就是主程序类
    • SpringBoot只会扫描主程序所在的包及其下面的子包,自动的component-scan功能
    • 自定义扫描路径(两种方式)
      • @SpringBootApplication(scanBasePackages = "com.atguigu")
      • @ComponentScan("com.atguigu") 直接指定扫描的路径
//主程序:com.atguigu.boot
//@SpringBootApplication(scanBasePackages = "com.atguigu")
//@SpringBootConfiguration
//@EnableAutoConfiguration
//@ComponentScan("com.atguigu")
@SpringBootApplication //这是一个SpringBoot应用
public class MainApplication {

    public static void main(String[] args) {
    }
}
  • 配置默认值
    • 配置文件的所有配置项是和某个类的对象值进行一一绑定的。
    • 绑定了配置文件中每一项值的类: 属性类
    • 比如:
      • ServerProperties 绑定了所有Tomcat服务器有关的配置
      • MultipartProperties 绑定了所有文件上传相关的配置
      • ....参照官方文档:或者参照 绑定的 属性类
  • 按需加载自动配置
    • 导入场景spring-boot-starter-web
    • 场景启动器除了会导入相关功能依赖,还会导入一个spring-boot-starter(也是一个依赖),是所有starter的starter,基础核心starter
    • spring-boot-starter导入了一个包 spring-boot-autoconfigure。包里面都是各种场景的AutoConfiguration 自动配置类
    • 虽然全场景的自动配置都在 spring-boot-autoconfigure这个包,但是不是全都开启的。
      • 导入哪个场景就开启哪个自动配置

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

1.3.2.2 完整流程

思考:

1、SpringBoot怎么实现导一个starter、写一些简单配置,应用就能跑起来,我们无需关心整合

2、为什么Tomcat的端口号可以配置在application.properties中,并且Tomcat能启动成功?

3、导入场景后哪些自动配置能生效

以下原理图要完全理解★★★★★

自动配置流程细节梳理:

1、导入场景启动器spring-boot-starter-web:导入了web开发场景

  • 1、场景启动器导入了相关场景的所有依赖:spring-boot-starter-json、spring-boot-starter-tomcat、springmvc
  • 2、每个场景启动器都引入了一个spring-boot-starter,核心场景启动器。
  • 3、核心场景启动器引入了spring-boot-autoconfigure包。
  • 4、spring-boot-autoconfigure里面囊括了所有场景的所有配置。
  • 5、只要这个包下的所有类都能生效,那么相当于SpringBoot官方写好的整合功能就生效了。
  • 6、SpringBoot默认却扫描不到 spring-boot-autoconfigure下写好的所有配置类。(这些配置类给我们做了整合操作),默认只扫描主程序所在的包。(却利用主程序的@EnableAutoConfiguration注解,批量地将142个配置类全部导入进来

2主程序:@SpringBootApplication

  • 1、@SpringBootApplication由三个注解组成@SpringBootConfiguration、@EnableAutoConfiguration、@ComponentScan
  • 2、SpringBoot默认只能扫描自己主程序所在的包及其下面的子包,扫描不到 spring-boot-autoconfigure包中官方写好的配置类
  • 3、@EnableAutoConfiguration:SpringBoot 开启自动配置的核心
    • 1. 是由@Import(AutoConfigurationImportSelector.class)提供功能:批量给容器中导入组件。
    • 2. SpringBoot启动会默认加载 142个配置类。
    • 3. 这142个配置类来自于spring-boot-autoconfigure下 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件指定的
    • 项目启动的时候利用 @Import 批量导入组件机制把 spring-boot-autoconfigure 包下的142 xxxxAutoConfiguration类导入进来(自动配置类
    • 虽然导入了142个自动配置类
  • 4、按需生效:
    • 并不是这142个自动配置类都能生效
    • 每一个自动配置类,都有条件注解@ConditionalOnxxx,只有条件成立,才能生效

3xxxxAutoConfiguration自动配置类

  • 1、给容器中使用@Bean 放一堆组件。
  • 2、每个自动配置类都可能有这个注解@EnableConfigurationProperties(ServerProperties.class),用来把配置文件中配的指定前缀的属性值封装(绑定)到 xxxProperties属性类中,并且将xxxProperties属性类注入容器
  • 3、以Tomcat为例:把服务器的所有配置都是以server开头的。配置都封装到了属性类中。给容器中放Tomcat自定义工厂组件的时候,要求传入ServerProperties。ServerProperties在上一步中已经完成属性绑定注入容器;@Bean标注的方法上有参数,而参数在容器中有,则自动从容器中拿)
  • 4、给容器中放的所有组件的一些核心参数(如端口号port),都来自于xxxPropertiesxxxProperties都是和配置文件绑定。
  • 最终实现一个效果:只需要改配置文件的值,核心组件的底层参数都能修改

4写业务,全程无需关心各种整合(底层这些整合写好了,而且也生效了)

核心流程总结:

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

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

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

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

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

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

以上原理图要完全理解★★★★★ 

1.3.2.3 如何学好SpringBoot

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

摄影:

  • 傻瓜:自动配置好。
  • 单反:焦距、光圈、快门、感光度....
  • 傻瓜+单反:SpringBoot就类似这种

1 理解自动配置原理

导入starter --> 生效xxxxAutoConfiguration --> 组件 --> xxxProperties --> 配置文件

2 理解其他框架底层

拦截器

3 可以随时定制化任何组件

a.配置文件

b.自定义组件

普通开发:导入starter,Controller、Service、Mapper、偶尔修改配置文件

高级开发:自定义组件、自定义配置、自定义starter

核心:

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

最佳实战

  • 选场景,导入到项目
    • 官方:starter
    • 第三方:去仓库搜
  • 写配置,改配置文件关键项
    • 数据库参数(连接地址、账号密码...)
  • 分析这个场景给我们导入了哪些能用的组件
    • 自动装配这些组件进行后续使用
    • 不满意boot提供的自动配好的默认组件
      • 定制化
        • 改配置
        • 自定义组件

整合redis:

  • 选场景:spring-boot-starter-data-redis
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    • 场景名+AutoConfiguration 拼一下,一般就是这个场景的自动配置类
  • 写配置:
    • 分析到这个场景的自动配置类开启了哪些属性绑定关系
    • @EnableConfigurationProperties(RedisProperties.class)
    • 修改redis相关的配置
  • 分析组件:
    • 分析到 RedisAutoConfiguration 给容器中放了 StringRedisTemplate
    • 给业务代码中自动装配 StringRedisTemplate
  • 定制化
    • 修改配置文件
    • 自定义组件,自己给容器中放一个 StringRedisTemplate(因为StringRedisTemplate被@ConditionalOnMissingBean注解标注,表示当容器中没有这个组件时,SpringBoot才给我们配置;如果容器中有这个组件,SpringBoot就不配了)

1.4 核心技能

1.4.1 常用注解

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

1.4.1.1 组件注册

@Configuration@SpringBootConfiguration

@Bean@Scope

@Controller、 @Service、@Repository、@Component

@Import

@ComponentScan

步骤:

1、@Configuration 编写一个配置类

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

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

//@Import(FastsqlException.class) //给容器中放指定类型的组件,组件的名字默认是全类名

/**
 * 1、开启Sheep组件的属性绑定
 * 2、默认会把这个组件自己放到容器中
 */
@EnableConfigurationProperties(Sheep.class) //导入第三方写好的组件进行属性绑定
//SpringBoot默认只扫描自己主程序所在的包。如果导入第三方包,即使组件上标注了 @Component、@ConfigurationProperties 注解,也没用。因为组件都扫描不进来
@SpringBootConfiguration //这是一个配置类,替代以前的配置文件。配置类本身也是容器中的组件
//@Configuration
public class AppConfig {

    @Bean
    @ConfigurationProperties(prefix = "pig")
    public Pig pig(){
        return new Pig(); //我们自己new新pig
    }
    /**
     * 1、组件默认是单实例的
     * @return
     */
    @Scope("prototype")
    @Bean("userHaha") //替代以前的Bean标签。 组件在容器中的名字默认是方法名,可以直接修改注解的值
    public User user01(){
        var user = new User();
        user.setId(1L);
        user.setName("张三");
        return user;
    }

//    @Bean
//    public FastsqlException fastsqlException(){
//
//        return new FastsqlException();
//    }
}
1.4.1.2 条件注解

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

@ConditionalOnXxx

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

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

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

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

场景:

  • 如果存在 FastsqlException 这个类,给容器中放一个 Cat 组件,名cat01,
  • 否则,就给容器中放一个 Dog组件,名dog01
  • 如果系统中有 dog01这个组件,就给容器中放一个 User组件,名zhangsan
  • 否则,就放一个User,名叫lisi
@ConditionalOnMissingClass(value="com.alibaba.druid.FastsqlException") //放在类级别,如果注解判断生效,则整个配置类才生效
@SpringBootConfiguration
public class AppConfig2 {

    @ConditionalOnClass(name="com.alibaba.druid.FastsqlException") //放在方法级别,单独对这个方法进行注解判断。
    @Bean
    public Cat cat01(){
        return new Cat();
    }


    @Bean
    public Dog dog01(){
        return new Dog();
    }

    @ConditionalOnBean(value = Dog.class)
    @Bean
    public User zhangsan(){
        return new User();
    }


    @ConditionalOnMissingBean(value = Dog.class)
    @Bean
    public User lisi(){
        return new User();
    }
}

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

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

1.4.1.3 属性绑定

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

@EnableConfigurationProperties:快速注册注解:

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

@EnableConfigurationProperties(Sheep.class)有两个功能:1、把配置文件绑定到属性类中;2、把类添加到容器中

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

  • 1、给容器中注册组件(@Component、@Bean)
  • 2、使用@ConfigurationProperties 声明组件和配置文件的哪些配置项进行绑定
pig.id=1
pig.name=佩奇
pig.age=5
@ConfigurationProperties(prefix = "pig")
@Component
public class Pig {
    private Long id;
    private String name;
    private Integer age;

    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;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

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

属性配置文件解决中文乱码问题

第二种属性绑定方式:@EnableConfigurationProperties

sheep.id=1
sheep.name=苏西
sheep.age=5
@ConfigurationProperties(prefix = "sheep")
public class Sheep {

    private Long id;
    private String name;
    private Integer age;

    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;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Sheep{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}
/**
 * 1、开启Sheep组件的属性绑定
 * 2、默认会把这个组件自己放到容器中
 */
@EnableConfigurationProperties(Sheep.class) //导入第三方写好的组件,并进行属性绑定。
//SpringBoot默认只扫描自己主程序所在的包。如果导入第三方包,即使组件上标注了 @Component、@ConfigurationProperties 注解,也没用。因为组件都扫描不进来
@SpringBootConfiguration //这是一个配置类,替代以前的配置文件。配置类本身也是容器中的组件
//@Configuration
public class AppConfig {

}

1.4.2 YAML配置文件

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

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

YAML 是 "YAML Ain't a Markup Language"(YAML 不是一种标记语言)。在开发的这种语言时,YAML 的意思其实是:"Yet Another Markup Language"(是另一种标记语言)。

  • 设计目标,就是方便人类读写
  • 层次分明,更适合做配置文件
  • 使用.yaml.yml作为文件后缀
1.4.2.1 基本语法
  • 大小写敏感
  • 使用缩进表示层级关系,k: v,使用空格分割k,v
  • 缩进时不允许使用Tab键,只允许使用空格。换行
  • 缩进的空格数目不重要,只要相同层级的元素左侧对齐即可
  • # 表示注释,从这个字符一直到行尾,都会被解析器忽略。
# 1、k: v # k v 之间 是空格区分 k:
# 2、属性有层级关系,使用下一行,空两个空格
# 3、左侧对齐的代表同一层级的属性
---
server:
  port: 9999


spring:
  servlet:
    multipart:
      max-file-size: 10MB
  data:
    redis:
      host: localhost
      port: 6379

支持的写法:

  • 对象键值对的集合,如:映射(map)/ 哈希(hash) / 字典(dictionary)
  • 数组:一组按次序排列的值,如:序列(sequence) / 列表(list)
  • 纯量:单个的、不可再分的值,如:字符串、数字、bool、日期
1.4.2.2 示例
@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:
      name: 小蓝
      age: 3
    c2: {name: 小绿,age: 2} #对象也可用{}表示
1.4.2.3 细节
  • birthDay 推荐写为 birth-day
  • 文本
    • 单引号不会转义【\n 则为普通字符串显示】
    • 双引号会转义【\n会显示为换行符
  • 大文本
    • |开头,大文本写在下层,保留文本格式换行符正确显示
    • >开头,大文本写在下层,折叠换行符
  • 多文档合并
    • 使用---可以把多个yaml文档合并在一个文档中,每个文档区依然认为内容独立
1.4.2.4 小技巧:lombok

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

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

 使用@Data等注解

1.4.3 日志配置

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

日志门面是日志接口,相当于数据库中JDBC接口的概念

日志实现是日志接口的具体实现类,相当于数据库中导入mysql、oracle等驱动,真正实现CRUD操作数据库

SpringBoot默认采用 SLF4j + Logback(可以选择切换成其他组合)

感兴趣日志框架关系与起源可参考尚硅谷SpringBoot顶尖教程(springboot之idea版spring boot)_哔哩哔哩_bilibili 视频 21~27集

1.4.3.1 简介

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

        spring5及以后 commons-logging被spring直接自己写了。

2. 支持 jullog4j2,logback。SpringBoot 提供了默认的控制台输出配置,也可以配置输出为文件。

3. logback是默认使用的。

4. 虽然日志框架很多,但是我们不用担心,使用 SpringBoot 的默认配置就能工作的很好

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

1、每个starter场景,都会导入一个核心场景spring-boot-starter

2、核心场景引入了日志的所用功能spring-boot-starter-logging

3、默认使用了logback + slf4j 组合作为默认底层日志

4、日志是系统一启动就要用,xxxAutoConfiguration是系统启动好了以后放好的组件,后来用的,时机不同。日志的时机更早,系统一启动,系统的一些核心行为都要被日志记录

5、日志是利用监听器机制配置好的。ApplicationListener。

6、日志所有的配置都可以通过修改配置文件实现。以logging开始的所有配置。

1.4.3.2 日志格式
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-boot包additional-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}

%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'

1.4.3.3 记录日志
Logger logger = LoggerFactory.getLogger(getClass());

或者使用Lombok的@Slf4j注解
@Slf4j
@RestController
public class HelloController {
    Logger logger = LoggerFactory.getLogger(getClass());

    @GetMapping("/h")
    public String hello(){
        
        logger.info("LoggerFactory.getLogger(getClass())打印日志")
        
        log.info("@Slf4j打印日志更方便")
        
        return "hello";
    }
}
1.4.3.4 日志级别
  • 由低到高:ALL,TRACE, DEBUG, INFO, WARN, ERROR,FATAL,OFF;
    • 只会打印指定级别及以上级别的日志
    • ALL:打印所有日志
    • TRACE:追踪框架详细流程日志,一般不使用
    • DEBUG:开发调试细节日志
    • INFO:关键、感兴趣信息日志
    • WARN:警告但不是错误的信息日志,比如:版本过时
    • ERROR业务错误日志,比如出现各种异常
    • FATAL:致命错误日志,比如jvm系统崩溃
    • OFF:关闭所有日志记录
  • 不指定级别的所有类,都使用root指定的级别作为默认级别
  • SpringBoot日志默认级别是 INFO
  1. application.properties/yaml中配置logging.level.<logger-name>=<level>指定日志级别
  2. level可取值范围:TRACE, DEBUG, INFO, WARN, ERROR, FATAL, or OFF定义在 LogLevel类中
  3. root logger-nameroot,可以配置logging.level.root=warn,代表所有未指定日志级别都使用 root 的 warn 级别
#默认所有日志没有精确指定级别就使用root的默认级别
logging.level.root=info

#精确调整某个包下的日志级别
logging.level.com.atguigu.logging.controller=debug
logging.level.com.atguigu.logging.service=debug
logging.level.com.aaa=debug
logging.level.com.bbb=debug
1.4.3.5 日志分组

比较有用的技巧是:

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

logging.group.tomcat=org.apache.catalina,org.apache.coyote,org.apache.tomcat
logging.level.tomcat=trace
logging.group.abc=com.atguigu.logging.controller,com.atguigu.logging.service,com.aaa,com.bbb
logging.level.abc=debug

logging.level.sql=debug

SpringBoot 预定义两个组

Name

Loggers

web

org.springframework.core.codec, org.springframework.http, org.springframework.web, org.springframework.boot.actuate.endpoint.web, org.springframework.boot.web.servlet.ServletContextInitializerBeans

sql

org.springframework.jdbc.core, org.hibernate.SQL, org.jooq.tools.LoggerListener

1.4.3.6 文件输出

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

logging.file.name

logging.file.path

示例

效果

未指定

未指定

仅控制台输出

指定

未指定

my.log

写入指定文件。可以加路径

未指定

指定

/var/log

写入指定目录,文件名为spring.log

指定

指定

logging.file.name为准

#指定日志文件的路径, 日志文件默认名叫 spring.log
#logging.file.path=D:\\
#指定日志文件的名: filename 和 path的配置同时存在只看filename
# 1、只写名字: 就生成到当前项目同位置的 my.log
# 2、写名字+路径:生成到指定位置的指定文件
logging.file.name=my.log

1.4.3.7 文件归档与滚动切割

归档:每天的日志单独存到一个文档中。

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

1 每天的日志应该独立分割出来存档。如果使用logback(SpringBoot 默认整合),可以通过application.properties/yaml文件指定日志滚动规则。

2 如果是其他日志系统,需要自行配置(添加log4j2.xmllog4j2-spring.xml

3 支持的滚动规则设置如下

配置项

描述

logging.logback.rollingpolicy.file-name-pattern

日志存档的文件名格式(默认值:${LOG_FILE}.%d{yyyy-MM-dd}.%i.gz

logging.logback.rollingpolicy.clean-history-on-start

应用启动时是否清除以前存档(默认值:false

logging.logback.rollingpolicy.max-file-size

存档前,每个日志文件的最大大小(默认值:10MB

logging.logback.rollingpolicy.total-size-cap

日志文件被删除之前,可以容纳的最大大小(默认值:0B)。设置1GB则磁盘存储超过 1GB 日志后就会删除旧日志文件

logging.logback.rollingpolicy.max-history

日志文件保存的最大天数(默认值:7).

#归档、切割
logging.logback.rollingpolicy.file-name-pattern=${LOG_FILE}.%d{yyyy-MM-dd}.%i.gz
logging.logback.rollingpolicy.max-file-size=1MB

1.4.3.8 自定义配置

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

日志系统

自定义

Logback

logback-spring.xml, logback-spring.groovy,

logback.xml, or logback.groovy

Log4j2

log4j2-spring.xml or log4j2.xml

JDK (Java Util Logging)

logging.properties

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

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

1.4.3.9 切换日志组合
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!--排除默认日志框架,切换日志系统 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
    <exclusions>
        <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
        </exclusion>
    </exclusions>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>

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

格式

依赖

文件名

YAML

com.fasterxml.jackson.core:jackson-databind + com.fasterxml.jackson.dataformat:jackson-dataformat-yaml

log4j2.yaml + log4j2.yml

JSON

com.fasterxml.jackson.core:jackson-databind

log4j2.json + log4j2.jsn

1.4.3.10 最佳实战

1 导入任何第三方框架,先排除它的日志包,因为Boot底层控制好了日志

2 修改 application.properties 配置文件,就可以调整日志的所有行为。如果不够,可以编写日志框架自己的配置文件放在类路径下就行,比如logback-spring.xmllog4j2-spring.xml

3 如需对接专业日志系统,也只需要把 logback 记录的日志灌倒 kafka之类的中间件,这和SpringBoot没关系,都是日志框架自己的配置,修改配置文件即可

4 业务中使用slf4j-api记录日志。不要再 sout

2 SpringBoot3-Web开发

SpringBootWeb开发能力,由SpringMVC提供。

2.0 WebMvcAutoConfiguration原理

2.0.1 生效条件

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

2.0.2 效果

a)放了两个Filter:

  1. HiddenHttpMethodFilter;页面表单提交Rest请求(GET、POST、PUT、DELETE)
  2. FormContentFilter: 表单内容Filter,GET(数据放URL后面)、POST(数据放请求体)请求可以携带数据,PUT、DELETE 的请求体数据会被忽略
@Bean
@ConditionalOnMissingBean({HiddenHttpMethodFilter.class})
@ConditionalOnProperty(
    prefix = "spring.mvc.hiddenmethod.filter",
    name = {"enabled"}
)
public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {
    return new OrderedHiddenHttpMethodFilter();
}

@Bean
@ConditionalOnMissingBean({FormContentFilter.class})
@ConditionalOnProperty(
    prefix = "spring.mvc.formcontent.filter",
    name = {"enabled"},
    matchIfMissing = true
)
public OrderedFormContentFilter formContentFilter() {
    return new OrderedFormContentFilter();
}

b)给容器中放了WebMvcConfigurer组件;给SpringMVC添加各种定制功能

  1. 所有的功能最终会和配置文件进行绑定
  2. WebMvcProperties: spring.mvc配置文件
  3. WebProperties: spring.web配置文件
	@Configuration(proxyBeanMethods = false)
	@Import(EnableWebMvcConfiguration.class) //额外导入了其他配置
	@EnableConfigurationProperties({ WebMvcProperties.class, WebProperties.class })
	@Order(0)
	public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer, ServletContextAware{
        
    }

2.0.3 WebMvcConfigurer接口

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

2.0.4 静态资源规则源码

@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
    if (!this.resourceProperties.isAddMappings()) {
        logger.debug("Default resource handling disabled");
        return;
    }
    //1、
    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);
        }
    });
}

a)规则一:访问: /webjars/**路径就去 classpath:/META-INF/resources/webjars/下找资源.

  1. maven 导入依赖

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

  1. classpath:/META-INF/resources/
  2. classpath:/resources/
  3. classpath:/static/
  4. classpath:/public/

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

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

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

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

2.0.5 EnableWebMvcConfiguration 源码

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

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

        WelcomePageHandlerMapping:

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

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

2.0.6 为什么容器中放一个WebMvcConfigurer就能配置底层行为

1. WebMvcAutoConfiguration 是一个自动配置类,它里面有一个 EnableWebMvcConfiguration

2. EnableWebMvcConfiguration继承于 DelegatingWebMvcConfiguration,这两个都生效

3. DelegatingWebMvcConfiguration利用 DI 把容器中 所有 WebMvcConfigurer 注入进来,保存在configures属性中

4. 别人调用 DelegatingWebMvcConfiguration 的方法配置底层规则,而它调用所有 WebMvcConfigurer的配置底层方法。

2.0.7 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);

2.1 Web场景

2.1.1 自动配置

1、整合web场景

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

2、引入了 autoconfigure功能

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

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

5、所有web自动配置类如下

org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration
org.springframework.boot.autoconfigure.web.embedded.EmbeddedWebServerFactoryCustomizerAutoConfiguration
====以下是响应式web场景和现在的没关系======
org.springframework.boot.autoconfigure.web.reactive.HttpHandlerAutoConfiguration
org.springframework.boot.autoconfigure.web.reactive.ReactiveMultipartAutoConfiguration
org.springframework.boot.autoconfigure.web.reactive.ReactiveWebServerFactoryAutoConfiguration
org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration
org.springframework.boot.autoconfigure.web.reactive.WebSessionIdResolverAutoConfiguration
org.springframework.boot.autoconfigure.web.reactive.error.ErrorWebFluxAutoConfiguration
org.springframework.boot.autoconfigure.web.reactive.function.client.ClientHttpConnectorAutoConfiguration
org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration
================以上没关系=================
org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration
org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration
org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration
org.springframework.boot.autoconfigure.web.servlet.HttpEncodingAutoConfiguration
org.springframework.boot.autoconfigure.web.servlet.MultipartAutoConfiguration
org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration

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

        a)SpringMVC的所有配置以 spring.mvc 开头

        b)Web场景通用配置以 spring.web 开头

        c)文件上传配置以 spring.servlet.multipart 开头

        d)服务器的配置以 server:  开头,比如:编码方式

2.1.2 默认效果

SpringBoot整合SpringMVC

默认配置:

1 包含了 ContentNegotiatingViewResolverBeanNameViewResolver 组件,方便视图解析

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

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

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

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

6 支持 静态 index.html

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

重要:

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

2.2 静态资源

2.2.1 默认规则

2.2.1.1 静态资源映射

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

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

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

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

  1. period: 缓存间隔。 默认 0S;
  2. cacheControl:缓存控制。 默认无;
  3. useLastModified:是否使用lastModified头。 默认 false;
2.2.1.2 静态资源缓存

如前面所述

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

  1. period: 缓存间隔。 默认 0S;
  2. cacheControl:缓存控制。 默认无;
  3. useLastModified:是否使用lastModified头。 默认 false;
2.2.1.3 欢迎页

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

  1. 静态资源目录下找 index.html
  2. 没有就在 templates下找index模板页
2.2.1.4 Favicon
  1. 在静态资源目录下找 favicon.ico
2.2.1.5 缓存实验
server.port=9000

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

#开启静态资源映射规则(默认为true)
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

2.2.2 自定义静态资源规则

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

2.2.2.1 配置方式

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

spring.web:

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

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

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

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

#2、 spring.mvc
## 2.1. 自定义webjars路径前缀
spring.mvc.webjars-path-pattern=/wj/**
## 2.2. 静态资源访问路径前缀
spring.mvc.static-path-pattern=/static/**
2.2.2.2 代码方式
  • 容器中只要有一个 WebMvcConfigurer 组件。配置的底层行为都会生效
  • @EnableWebMvc //禁用boot的默认配置,全手动
@Configuration //这是一个配置类
public class MyConfig implements WebMvcConfigurer {

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        //保留以前规则
        //自己写新的规则。
        registry.addResourceHandler("/static/**")
                .addResourceLocations("classpath:/a/","classpath:/b/")
                .setCacheControl(CacheControl.maxAge(1180, TimeUnit.SECONDS));
    }
}
//@EnableWebMvc //禁用boot的默认配置
@Configuration //这是一个配置类,给容器中放一个 WebMvcConfigurer 组件,就能自定义底层
public class MyConfig  /*implements WebMvcConfigurer*/ {

    @Bean
    public WebMvcConfigurer webMvcConfigurer(){
        return new WebMvcConfigurer() {
            @Override //配置静态资源
            public void addResourceHandlers(ResourceHandlerRegistry registry) {
                registry.addResourceHandler("/static/**")
                        .addResourceLocations("classpath:/a/", "classpath:/b/")
                        .setCacheControl(CacheControl.maxAge(1180, TimeUnit.SECONDS));
            }

            @Override //配置拦截器
            public void addInterceptors(InterceptorRegistry registry) {

            }
}

2.3 路径匹配

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

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

2.3.1 Ant风格路径用法

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

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

例如:

  • *.html 匹配任意名称,扩展名为.html的文件。
  • /folder1/*/*.java 匹配在folder1目录下的任意两级目录下的.java文件。
  • /folder2/**/*.jsp 匹配在folder2目录下任意目录深度的.jsp文件。
  • /{type}/{id}.html 匹配任意文件名为{id}.html,在任意命名的{type}目录下的文件。

注意:Ant 风格的路径模式语法中的特殊字符需要转义,如:

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

2.3.2 模式切换

AntPathMatcher 与 PathPatternParser

  • PathPatternParser 在 jmh 基准测试下,有 6~8 倍吞吐量提升,降低 30%~40%空间分配率
  • PathPatternParser 兼容 AntPathMatcher语法,并支持更多类型的路径模式
  • PathPatternParser "**" 多段匹配的支持仅允许在模式末尾使用,不能匹配 ** 在中间的情况,剩下的和 antPathMatcher语法兼容
@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

2.4 内容协商

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

2.4.1 多端内容适配

2.4.1.1 默认规则

SpringBoot 多端内容适配:

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

客户端向服务端发送请求,携带HTTP标准的Accept请求头

  1. Accept: application/jsontext/xmltext/yaml
  2. 服务端根据客户端请求头期望的数据类型进行动态返回

b)基于请求参数内容协商:(需要手动开启)

  1. 发送请求 GET /projects/spring-boot?format=json
  2. 匹配到 @GetMapping("/projects/spring-boot")
  3. 根据参数协商,优先返回 json 类型数据【需要开启参数匹配设置
  4. 发送请求 GET /projects/spring-boot?format=xml,优先返回 xml 类型数据
2.4.1.2 效果演示

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

1、引入支持写出xml内容依赖

<dependency>
    <groupId>com.fasterxml.jackson.dataformat</groupId>
    <artifactId>jackson-dataformat-xml</artifactId>
</dependency>

2、标注注解

@JacksonXmlRootElement  // 可以写出为xml文档
@Data
public class Person {
    private Long id;
    private String userName;
    private String email;
    private Integer age;
}

3、开启基于请求参数的内容协商

# 开启基于请求参数的内容协商功能。 默认参数名:format。 默认此功能不开启
spring.mvc.contentnegotiation.favor-parameter=true
# 指定内容协商时使用的参数名。默认是 format
spring.mvc.contentnegotiation.parameter-name=type

4、效果

2.4.1.3 配置协商规则与支持类型

修改内容协商方式

#开启基于请求参数的内容协商功能。 默认参数名:format 默认此功能不开启
spring.mvc.contentnegotiation.favor-parameter=true  
#自定义参数名,默认为format
spring.mvc.contentnegotiation.parameter-name=format

大多数 MediaType 都是开箱即用的。也可以自定义内容类型,如:

# 增加一种新的内容类型
spring.mvc.contentnegotiation.media-types.yaml=text/yaml

2.4.2 自定义内容返回

2.4.2.1 增加yaml返回支持

导入依赖

<!--支持返回YAML格式数据-->
<dependency>
    <groupId>com.fasterxml.jackson.dataformat</groupId>
    <artifactId>jackson-dataformat-yaml</artifactId>
</dependency>

把对象写出成YAML

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

编写配置

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

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

//@EnableWebMvc //禁用boot的默认配置
@Configuration //这是一个配置类,给容器中放一个 WebMvcConfigurer 组件,就能自定义底层
public class MyConfig  /*implements WebMvcConfigurer*/ {

    @Bean
    public WebMvcConfigurer webMvcConfigurer(){
        return new WebMvcConfigurer() {

            @Override //配置一个能把对象转为yaml的messageConverter
            public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
                converters.add(new MyYamlHttpMessageConverter());
            }
        };
    }
}
2.4.2.2 思考:如何增加其他

配置媒体类型支持:

  • spring.mvc.contentnegotiation.media-types.yaml=text/yaml

编写对应的HttpMessageConverter,要告诉Boot这个支持的媒体类型

  • 按照3的示例

把MessageConverter组件加入到底层

  • 容器中放一个WebMvcConfigurer 组件,并配置底层的MessageConverter
2.4.2.3 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);
        }

    }
}

2.4.3 内容协商原理-HttpMessageConverter

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

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

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

a)请求进来先来到 DispatcherServlet的doDispatch() 进行处理

b)找到一个 HandlerAdapter 适配器。利用适配器执行目标方法

c)RequestMappingHandlerAdapter来执行,调用invokeHandlerMethod()来执行目标方法

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

  1. HandlerMethodArgumentResolver:参数解析器,确定目标方法每个参数值
  2. HandlerMethodReturnValueHandler:返回值处理器,确定目标方法的返回值改怎么处理

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

f)目标方法执行完成,会返回返回值对象

g)找到一个合适的返回值处理器 HandlerMethodReturnValueHandler

h)最终找到 RequestResponseBodyMethodProcessor能处理 标注了 @ResponseBody注解的方法

i)RequestResponseBodyMethodProcessor 调用writeWithMessageConverters ,利用MessageConverter把返回值写出去

上面步骤解释:@ResponseBody由HttpMessageConverter处理

2 HttpMessageConverter 会先进行内容协商

a)遍历所有的MessageConverter看谁支持这种内容类型的数据

b)默认MessageConverter有以下

c)最终因为要json所以MappingJackson2HttpMessageConverter支持写出json

d)jackson用ObjectMapper把对象写出去

2.4.3.2 WebMvcAutoConfiguration提供几种默认HttpMessageConverters

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

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

默认8个:

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

2.5 模板引擎

由于 SpringBoot 使用了嵌入式 Servlet 容器。所以 JSP 默认是不能使用的。

如果需要服务端页面渲染,优先考虑使用 模板引擎

image.png

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

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

  • FreeMarker
  • Groovy
  • Thymeleaf
  • Mustache

Thymeleaf官网Thymeleaf

<!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>

2.5.1 Thymeleaf整合

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

自动配置原理:

开启了 org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration 自动配置

属性绑定在 ThymeleafProperties 中,对应配置文件 spring.thymeleaf 内容

所有的模板页面默认在 classpath:/templates文件夹下

默认效果

  1. 所有的模板页面在 classpath:/templates/下面找
  2. 找后缀名为.html的页面

Thymeleaf初体验 

@Controller //适配 服务端渲染   前后不分离模式开始
public class WelcomeController {

    @Autowired
    AService aService;

    /**
     * 利用模板引擎跳转到指定页面
     * @return
     */
    @GetMapping("/well")
    public String hello(@RequestParam("name") String name,
                        Model model){

        //模板的逻辑视图名
        //物理视图 =  前缀 + 逻辑视图名 + 后缀
        //真实地址 = classpath:/templates/welcome.html

        //把需要给页面共享的数据放到model中
        model.addAttribute("msg",name);
        return "welcome";
    }

}
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>你好:<span th:text="${msg}"></span></h1>
</body>
</html>
# 项目的根路径
#server.servlet.context-path=/demo

# 配置thymeleaf场景
#spring.thymeleaf
#thymeleaf的前缀,默认位置 classpath:/templates/
spring.thymeleaf.prefix=classpath:/templates/
#thymeleaf的后缀,默认 .html
spring.thymeleaf.suffix=.html
#缓存,开发期间关闭,上线以后开启
spring.thymeleaf.cache=false

#模板渲染之前检查模板是否存在,默认为true。改为false忽略检查,整体速度更快
spring.thymeleaf.check-template=false

2.5.2 基础语法

2.5.2.1 核心用法

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

  • th:text:标签体内文本值渲染
    • th:utext:不会转义,显示为html原本的样子。
  • th:属性:标签指定属性渲染
  • th:attr:标签任意属性渲染
  • th:if  th:each ...:其他th指令
  • 例如:
<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}" />

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>你好:<span th:text="${msg}"></span></h1>

<hr/>
th:text: 替换标签体的内容; 会转义
th:utext: 替换标签体的内容; 不会转义html标签,真正显示为html该有的样式
<h1 th:text="${msg}">哈哈</h1>
<h1 th:utext="${msg}">呵呵</h1>
<hr/>
转大写
<h1 th:text="${#strings.toUpperCase(name)}"></h1>
<h1 th:text="${'前缀:'+name+'后缀'}"></h1>
拼串
<h1 th:text="|前缀哈哈 ${name} 后缀呵呵|"></h1>

<hr/>
th:任意html属性; 动态替换任意属性的值
<img th:src="@{${imgUrl}}" src="1.jpg" style="width:300px;"/>
<br/>
th:attr:任意属性指定
<img src="1.jpg" style="width:300px;" th:attr="src=${imgUrl},style=${style}"/>
<br/>
th:其他指令
<img th:src="@{${imgUrl}}" th:style="${style}" th:if="${show}">


<br/>
2.jpg  @{} 专门用来取各种路径
<img src="/static/2.jpg" style="width:300px;" th:src="@{/static/2.jpg}">

</body>
</html>

表达式:用来动态取值

  • ${}:变量取值;使用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生成工具
2.5.2.2 语法示例

表达式:

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

常见:

  • 文本: 'one text''another one!',...
  • 数字: 0,34,3.0,12.3,...
  • 布尔:truefalse
  • null: null
  • 变量名: one,sometext,main...

文本操作:

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

布尔操作:

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

比较运算:

  • 比较:><<=>=gtltge,le
  • 等值运算:==,!=eqne

条件运算:

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

特殊语法:

  • 无操作:_

所有以上都可以嵌套组合

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

2.5.3 属性设置

1. th:href="@{/product/list}"

2. th:attr="class=${active}"

3. th:attr="src=@{/images/gtvglogo.png},title=${logo},alt=#{logo}"

4. th:checked="${user.active}"

<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}" />

2.5.4 遍历

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

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

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

iterStat 有以下属性:

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

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>列表页</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/css/bootstrap.min.css" rel="stylesheet"
          integrity="sha384-KK94CHFLLe+nY2dmCWGMq91rCGa5gtU4mk92HdvYe+M/SXH301p5ILy+dN9+nJOZ" crossorigin="anonymous">

</head>
<body>
<!--导航区-->
<div th:replace="~{common :: myheader}"></div>


<div class="container">
    <table class="table">
        <thead>
        <tr>
            <th scope="col">#</th>
            <th scope="col">名字</th>
            <th scope="col">邮箱</th>
            <th scope="col">年龄</th>
            <th scope="col">角色</th>
            <th scope="col">状态信息</th>
        </tr>
        </thead>
        <tbody>
        <tr th:each="person,stats : ${persons}" th:if="${person.age > 10}" th:object="${person}">
            <th scope="row" th:text="${person.id}">1</th>
            <!--        <td th:text="${person.userName}">Mark</td>-->
            <td th:text="*{userName}">Mark</td>
            <td th:if="${#strings.isEmpty(person.email)}" th:text="'联系不上'">  </td>
            <td th:if="${not #strings.isEmpty(person.email)}" th:text="${person.email}">  </td>
            <td th:text="| ${person.age} / ${person.age >= 18?'成年':'未成年' }|">  </td>
            <td th:switch="${person.role}">
                <button th:case="'admin'" type="button" class="btn btn-danger">管理员</button>
                <button th:case="'pm'" type="button" class="btn btn-primary">项目经理</button>
                <button th:case="'hr'" type="button" class="btn btn-default">人事</button>
            </td>

            <td>
                index: [[${stats.index}]] <br/>
                count: [[${stats.count}]] <br/>
                size(总数量): [[${stats.size}]] <br/>
                current(当前对象): [[${stats.current}]] <br/>
                even(true)/odd(false): [[${stats.even}]] <br/>
                first: [[${stats.first}]] <br/>
                last: [[${stats.last}]] <br/>
            </td>
        </tr>
        </tbody>
    </table>

</div>


<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/js/bootstrap.bundle.min.js"
        integrity="sha384-ENjdO4Dr2bkBIFxQpeoTz1HIcje39Wm4jDKdf19U8gI4ddQ3GYNS7NTKfAdVQSZe"
        crossorigin="anonymous"></script>
</body>
</html>

2.5.5 判断

2.5.5.1 th:if
<a
  href="comments.html"
  th:href="@{/product/comments(prodId=${prod.id})}"
  th:if="${not #lists.isEmpty(prod.comments)}"
  >view</a
2.5.5.2 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>

2.5.6 属性优先级

  • 片段
  • 遍历
  • 判断
<ul>
  <li th:each="item : ${items}" th:text="${item.description}">Item description here...</li>
</ul>

Order

Feature

Attributes

1

片段包含

th:insert th:replace

2

遍历

th:each

3

判断

th:if th:unless th:switch th:case

4

定义本地变量

th:object th:with

5

通用方式属性修改

th:attr th:attrprepend th:attrappend

6

指定属性修改

th:value th:href th:src ...

7

文本值

th:text th:utext

8

片段指定

th:fragment

9

片段移除

th:remove

2.5.7 行内写法

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

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

2.5.8 变量选择

<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

2.5.9 模板布局

  • 定义模板: th:fragment
  • 引用模板:~{templatename::selector}
  • 插入模板:th:insert、th: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>

common.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>通用的东西</title>
</head>
<body>
<!--抽取的判断,名字叫 myheader-->
<header th:fragment="myheader" class="p-3 text-bg-dark">
</header>

</body>
</html>

index.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>首页</title>
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/css/bootstrap.min.css" rel="stylesheet"
        integrity="sha384-KK94CHFLLe+nY2dmCWGMq91rCGa5gtU4mk92HdvYe+M/SXH301p5ILy+dN9+nJOZ" crossorigin="anonymous">

</head>
<body>
<!--导航  使用公共部分进行替换-->
<!--  ~{ 模板名 :: 片段名} -->
<div th:replace="~{common :: myheader}"></div>

<div class="container">

  <a th:href="@{/list}">列表展示</a>

</div>

<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/js/bootstrap.bundle.min.js"
        integrity="sha384-ENjdO4Dr2bkBIFxQpeoTz1HIcje39Wm4jDKdf19U8gI4ddQ3GYNS7NTKfAdVQSZe"
        crossorigin="anonymous"></script>
</body>
</html>

2.5.10 devtools

热启动依赖

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

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

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

建议:页面修改直接ctrl+F9;java代码的修改,重启项目


常用配置项

# 项目的根路径
#server.servlet.context-path=/demo

# 配置thymeleaf场景
#spring.thymeleaf
#thymeleaf的前缀,默认位置 classpath:/templates/
spring.thymeleaf.prefix=classpath:/templates/
#thymeleaf的后缀,默认 .html
spring.thymeleaf.suffix=.html
#缓存,开发期间关闭,上线以后开启
spring.thymeleaf.cache=false

#模板渲染之前检查模板是否存在,默认为true。改为false忽略检查,整体速度更快
spring.thymeleaf.check-template=false

2.6 国际化

国际化的自动配置参照MessageSourceAutoConfiguration

实现步骤

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

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

        a)messages.properties:默认

        b)messages_zh_CN.properties:中文环境

        c)messages_en_US.properties:英语环境

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

4 在页面中可以使用表达式 #{}获取国际化的配置项值

    @Autowired  //国际化取消息用的组件
    MessageSource messageSource;
    @GetMapping("/haha")
    public String haha(HttpServletRequest request){

        //区域信息,获取语言环境
        Locale locale = request.getLocale();
        //利用代码的方式获取国际化配置文件中指定的配置项的值
        String login = messageSource.getMessage("login", null, locale);
        return login;
    }
#国际化配置
spring.messages.basename=messages #国际化配置文件名,默认messages
spring.messages.encoding=UTF-8 #编码格式

common.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>通用的东西</title>
</head>
<body>
<!--抽取的判断,名字叫 myheader-->
<header th:fragment="myheader" class="p-3 text-bg-dark">
  <div class="container">
    <div class="d-flex flex-wrap align-items-center justify-content-center justify-content-lg-start">
      <a href="/" class="d-flex align-items-center mb-2 mb-lg-0 text-white text-decoration-none">
        <svg class="bi me-2" width="40" height="32" role="img" aria-label="Bootstrap"><use xlink:href="#bootstrap"></use></svg>
      </a>

      <ul class="nav col-12 col-lg-auto me-lg-auto mb-2 justify-content-center mb-md-0">
        <li><a th:href="@{/}" class="nav-link px-2 text-secondary" th:text="#{home}">Home</a></li>
        <li><a th:href="@{/list}" class="nav-link px-2 text-white" th:text="#{list}">List</a></li>
        <li><a href="#" class="nav-link px-2 text-white">Pricing</a></li>
        <li><a href="#" class="nav-link px-2 text-white">FAQs</a></li>
        <li><a href="#" class="nav-link px-2 text-white">About</a></li>
      </ul>

      <form class="col-12 col-lg-auto mb-3 mb-lg-0 me-lg-3" role="search">
        <input type="search" class="form-control form-control-dark text-bg-dark" placeholder="Search..." aria-label="Search">
      </form>

      <div class="text-end">
<!--        配置页面的值去配置文件哪里取哪个key-->
        <button type="button" class="btn btn-outline-light me-2" th:text="#{login}"></button>
        <button type="button" class="btn btn-warning" th:text="#{sign}">Sign-up</button>
      </div>
    </div>
  </div>
</header>


</body>
</html>

messages.properties

login=Login
sign=Sign-Up
home=Home
list=List

2.7 错误处理

2.7.1 默认机制

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

  • 1. SpringBoot 会自适应处理错误响应页面JSON数据
  • 2. SpringMVC的错误处理机制依然保留,MVC处理不了,才会交给boot进行处理
//SpringMVC处理错误
@ControllerAdvice //这个类是集中处理所有 @Controller 发生的错误
public class GlobalExceptionHandler {

    /**
     * 1、@ExceptionHandler 标识一个方法处理错误,默认只能处理这个类发生的指定错误
     * 2、@ControllerAdvice 统一处理所有错误
     * @param e
     * @return
     */
    @ResponseBody //对象写出为json
//    @ExceptionHandler(Exception.class)
    public String handleException(Exception e){

        return "Ohho~~~统一处理,原因:"+e.getMessage();
    }
}

  • 发生错误以后,转发给/error路径,SpringBoot在底层写好一个 BasicErrorController的组件,专门处理这个请求
@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、解析错误的自定义视图地址
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
//2、如果解析不到错误页面的地址,默认的错误页就是 error
return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);

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

@Bean
@ConditionalOnBean(DispatcherServlet.class)
@ConditionalOnMissingBean(ErrorViewResolver.class)
DefaultErrorViewResolver conventionErrorViewResolver() {
    return new DefaultErrorViewResolver(this.applicationContext, this.resources);
}

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

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

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

@Bean(name = "error")
@ConditionalOnMissingBean(name = "error")
public View defaultErrorView() {
    return this.defaultErrorView;
}

封装了JSON格式的错误信息

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

规则:

解析一个错误页

a)如果发生了500、404、503、403 这些错误

  1. 如果有模板引擎,默认在 classpath:/templates/error/精确码.html
  2. 如果没有模板引擎,在静态资源文件夹下找 精确码.html

b)如果匹配不到精确码.html这些精确的错误页,就去找5xx.html,4xx.html模糊匹配

  1. 如果有模板引擎,默认在 classpath:/templates/error/5xx.html
  2. 如果没有模板引擎,在静态资源文件夹下找 5xx.html

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

2.7.2 自定义错误响应

2.7.2.1 自定义json响应

使用@ControllerAdvice + @ExceptionHandler 进行统一异常处理

2.7.2.2 自定义页面响应

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

2.7.3 最佳实战

前后分离

后台发生的所有错误,@ControllerAdvice + @ExceptionHandler进行统一异常处理。

服务端页面渲染

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

给classpath:/templates/error/下面,放常用精确的错误码页面。500.html,404.html

给classpath:/templates/error/下面,放通用模糊匹配的错误码页面。 5xx.html,4xx.html

发生业务错误

核心业务,每一种错误,都应该代码控制,跳转到自己定制的错误页

通用业务,classpath:/templates/error.html页面,显示错误信息

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

2.8 嵌入式容器

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

2.8.1 自动配置原理

SpringBoot 默认嵌入Tomcat作为Servlet容器。

自动配置类是 ServletWebServerFactoryAutoConfigurationEmbeddedWebServerFactoryCustomizerAutoConfiguration

自动配置类开始分析功能。`xxxxAutoConfiguration`

@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 导入了 嵌入式的三大服务器 Tomcat、Jetty、Undertow

    a)导入 Tomcat、Jetty、Undertow 都有条件注解。系统中有这个类才行(也就是导了包)

    b)默认 Tomcat配置生效。给容器中放 TomcatServletWebServerFactory

  c)都给容器中 放了一个ServletWebServerFactory(接口) web服务器工厂(造web服务器的)——xxxServletWebServerFactory(具体实现类)

    d)web服务器工厂 都有一个功能,getWebServer获取web服务器

    e)TomcatServletWebServerFactory 创建了 tomcat。

4 ServletWebServerFactory 什么时候会创建 webServer出来。

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

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

7 refresh() 容器刷新 十二大步的刷新子容器会调用 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。

2.8.2 自定义

切换服务器;

<properties>
    <servlet-api.version>3.1.0</servlet-api.version>
</properties>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <exclusions>
        <!-- Exclude the Tomcat dependency -->
        <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<!-- Use Jetty instead -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jetty</artifactId>
</dependency>

2.8.3 最佳实战

用法:

  • 修改server下的相关配置就可以修改服务器参数

  • 通过给容器中放一个ServletWebServerFactory(接口,放入的是xxxServletWebServerFactory实现类),来禁用掉SpringBoot默认放的服务器工厂,实现自定义嵌入任意服务器

以Tomcat服务器为例,当容器中没有ServletWebServerFactory时,系统创建new TomcatServletWebServerFactory();所以当我们自己配置了ServletWebServerFactory放入容器,则系统就不创建服务器工厂了

2.9 全面接管SpringMVC

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

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

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

1 WebMvcAutoConfigurationweb场景的自动配置类

        1.1 支持RESTful的filter:HiddenHttpMethodFilter

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

        1.3 导入EnableWebMvcConfiguration:

                1.3.1 RequestMappingHandlerAdapter

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

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

                1.3.4 ExceptionHandlerExceptionResolver:默认的异常解析器

                1.3.5 LocaleResolver:国际化解析器

                1.3.6 ThemeResolver:主题解析器

                1.3.7 FlashMapManager:临时数据共享

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

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

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

                1.3.11 ContentNegotiationManager:内容协商管理器

        1.4 WebMvcAutoConfigurationAdapter配置生效,它是一个WebMvcConfigurer,定义mvc底层组件

                1.4.1 定义好 WebMvcConfigurer 底层组件默认功能;所有功能详见列表

                1.4.2 视图解析器:InternalResourceViewResolver

                1.4.3 视图解析器:BeanNameViewResolver,视图名(controller方法的返回值字符串)就是组件名

                1.4.4 内容协商解析器:ContentNegotiatingViewResolver

                1.4.5 请求上下文过滤器:RequestContextFilter: 任意位置直接获取当前请求

                1.4.6 静态资源链规则

                1.4.7 ProblemDetailsExceptionHandler:错误详情

                1.4.7.1 SpringMVC内部场景异常被它捕获:

        1.5 定义了MVC默认的底层行为: WebMvcConfigurer

2.9.2 @EnableWebMvc 禁用默认行为

  1. @EnableWebMvc给容器中导入 DelegatingWebMvcConfiguration组件,他是 WebMvcConfigurationSupport

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

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

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

2.9.3 WebMvcConfigurer 功能

定义扩展SpringMVC底层功能

2.10 最佳实践

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

2.10.1 三种方式

方式

用法

效果

全自动

直接编写控制器逻辑

全部使用自动配置默认效果

手自一体

@Configuration +
配置WebMvcConfigurer +
配置 WebMvcRegistrations

不要标注
@EnableWebMvc

保留自动配置效果
手动设置部分功能
定义MVC底层组件

全手动

@Configuration +
配置WebMvcConfigurer

标注
@EnableWebMvc

禁用自动配置效果
全手动设置

总结:

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

2.10.2 两种模式

1、前后分离模式: @RestController 响应JSON数据

2、前后不分离模式:@Controller + Thymeleaf模板引擎

2.11 Web新特性

2.11.1 Problemdetails

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

错误信息返回新格式

原理 

@Configuration(proxyBeanMethods = false)
//配置过一个属性 spring.mvc.problemdetails.enabled=true,默认是false
@ConditionalOnProperty(prefix = "spring.mvc.problemdetails", name = "enabled", havingValue = "true")
static class ProblemDetailsErrorHandlingConfiguration {

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

}

1 ProblemDetailsExceptionHandler 是一个 @ControllerAdvice集中处理系统异常

2 处理以下异常。如果系统出现以下异常,会被SpringBoot支持以 RFC 7807规范方式返回错误数据

@ExceptionHandler({
    HttpRequestMethodNotSupportedException.class, //请求方式不支持
    HttpMediaTypeNotSupportedException.class,
	HttpMediaTypeNotAcceptableException.class,
	MissingPathVariableException.class,
	MissingServletRequestParameterException.class,
	MissingServletRequestPartException.class,
	ServletRequestBindingException.class,
	MethodArgumentNotValidException.class,
	NoHandlerFoundException.class,
	AsyncRequestTimeoutException.class,
	ErrorResponseException.class,
	ConversionNotSupportedException.class,
	TypeMismatchException.class,
	HttpMessageNotReadableException.class,
	HttpMessageNotWritableException.class,
	BindException.class
})

 效果:

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

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

开启ProblemDetails返回, 使用新的MediaType

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

{
    "type": "about:blank",
    "title": "Method Not Allowed",
    "status": 405,
    "detail": "Method 'POST' is not supported.",
    "instance": "/list"
}

2.11.2 函数式Web

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

函数式接口

Web请求处理的方式:

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

场景:User RESTful - CRUD

  • GET /user/1 获取1号用户
  • GET /users 获取所有用户
  • POST /user 请求体携带JSON,新增一个用户
  • PUT /user/1 请求体携带JSON,修改1号用户
  • DELETE /user/1 删除1号用户
2.11.2.2 核心类
  • RouterFunction
  • RequestPredicate
  • ServerRequest
  • ServerResponse
2.11.2.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()
                .GET("/{user}", ACCEPT_JSON, userHandler::getUser)
                .GET("/{user}/customers", ACCEPT_JSON, userHandler::getUserCustomers)
                .DELETE("/{user}", ACCEPT_JSON, userHandler::deleteUser)
                .build();
    }

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

}

package com.atguigu.web.config;

import com.atguigu.web.bean.Person;
import com.atguigu.web.biz.UserBizHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.web.servlet.function.RequestPredicates;
import org.springframework.web.servlet.function.RouterFunction;
import org.springframework.web.servlet.function.RouterFunctions;
import org.springframework.web.servlet.function.ServerResponse;

/**
 * @author lfy
 * @Description
 * @create 2023-04-18 21:46
 */

/**
 * 场景:User RESTful - CRUD
 * ● GET /user/1  获取1号用户
 * ● GET /users   获取所有用户
 * ● POST /user  请求体携带JSON,新增一个用户
 * ● PUT /user/1 请求体携带JSON,修改1号用户
 * ● DELETE /user/1 删除1号用户
 */
@Configuration
public class WebFunctionConfig {

    /**
     * 函数式Web:
     * 1、给容器中放一个Bean:类型是 RouterFunction<ServerResponse>,集中所有路由信息
     * 2、每个业务准备一个自己的Handler
     *
     *
     * 核心四大对象
     * 1、RouterFunction: 定义路由信息。发什么请求,谁来处理
     * 2、RequestPredicate:定义请求规则:请求谓语。请求方式(GET、POST)、请求参数
     * 3、ServerRequest:  封装请求完整数据
     * 4、ServerResponse: 封装响应完整数据
     */
    @Bean
    public RouterFunction<ServerResponse> userRoute(UserBizHandler userBizHandler/*这个会被自动注入进来*/){

        return RouterFunctions.route() //开始定义路由信息
                .GET("/user/{id}", RequestPredicates.accept(MediaType.ALL), userBizHandler::getUser)
                .GET("/users", userBizHandler::getUsers)
                .POST("/user", RequestPredicates.accept(MediaType.APPLICATION_JSON), userBizHandler::saveUser)
                .PUT("/user/{id}", RequestPredicates.accept(MediaType.APPLICATION_JSON), userBizHandler::updateUser)
                .DELETE("/user/{id}", userBizHandler::deleteUser)
                .build();
    }


//    @Bean
//    public RouterFunction<ServerResponse> groupRoute(UserBizHandler userBizHandler/*这个会被自动注入进来*/){
//
//        return RouterFunctions.route() //开始定义路由信息
//                .GET("/user/{id}", RequestPredicates.accept(MediaType.ALL), userBizHandler::getUser)
//                .GET("/users", userBizHandler::getUsers)
//                .POST("/user", RequestPredicates.accept(MediaType.APPLICATION_JSON), userBizHandler::saveUser)
//                .PUT("/user/{id}", RequestPredicates.accept(MediaType.APPLICATION_JSON), userBizHandler::updateUser)
//                .DELETE("/user/{id}", userBizHandler::deleteUser)
//                .build();
//    }
}
package com.atguigu.web.biz;

import com.atguigu.web.bean.Person;
import jakarta.servlet.ServletException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import org.springframework.web.servlet.function.ServerRequest;
import org.springframework.web.servlet.function.ServerResponse;

import java.io.IOException;
import java.util.Arrays;
import java.util.List;

/**
 * @author lfy
 * @Description 专门处理User有关的业务
 * @create 2023-04-18 21:55
 */
@Slf4j
@Service
public class UserBizHandler {

    /**
     * 查询指定id的用户
     * @param request
     * @return
     */
    public ServerResponse getUser(ServerRequest request) throws Exception{
        String id = request.pathVariable("id");
        log.info("查询 【{}】 用户信息,数据库正在检索",id);
        //业务处理
        Person person = new Person(1L,"哈哈","aa@qq.com",18,"admin");
        //构造响应
        return ServerResponse
                .ok()
                .body(person);
    }


    /**
     * 获取所有用户
     * @param request
     * @return
     * @throws Exception
     */
    public ServerResponse getUsers(ServerRequest request) throws Exception{
        log.info("查询所有用户信息完成");
        //业务处理
        List<Person> list = Arrays.asList(new Person(1L, "哈哈", "aa@qq.com", 18, "admin"),
                new Person(2L, "哈哈2", "aa2@qq.com", 12, "admin2"));

        //构造响应
        return ServerResponse
                .ok()
                .body(list); //凡是body中的对象,就是以前@ResponseBody原理。利用HttpMessageConverter 写出为json
    }


    /**
     * 保存用户
     * @param request
     * @return
     */
    public ServerResponse saveUser(ServerRequest request) throws ServletException, IOException {
        //提取请求体
        Person body = request.body(Person.class);
        log.info("保存用户信息:{}",body);
        return ServerResponse.ok().build();
    }

    /**
     * 更新用户
     * @param request
     * @return
     */
    public ServerResponse updateUser(ServerRequest request) throws ServletException, IOException {
        Person body = request.body(Person.class);
        log.info("保存用户信息更新: {}",body);
        return ServerResponse.ok().build();
    }

    /**
     * 删除用户
     * @param request
     * @return
     */
    public ServerResponse deleteUser(ServerRequest request) {
        String id = request.pathVariable("id");
        log.info("删除【{}】用户信息",id);
        return ServerResponse.ok().build();
    }
}

3 SpringBoot3-数据访问

整合SSM场景

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

3.1 创建SSM整合项目

<dependencies>
    <!--web场景-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!--mybatis场景-->
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>3.0.0</version>
    </dependency>

    <dependency>
        <groupId>com.mysql</groupId>
        <artifactId>mysql-connector-j</artifactId>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>com.atguigu</groupId>
        <artifactId>boot3-08-robot-starter</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </dependency>
</dependencies>

3.2 配置数据源

# 1、先配置数据源信息
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.type=com.zaxxer.hikari.HikariDataSource
spring.datasource.url=jdbc:mysql://localhost:3306/test
spring.datasource.username=root
spring.datasource.password=123456

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

UserMapper接口

public interface UserMapper {

    /**
     * 1、每个方法都在Mapper文件中有一个sql标签对应。
     * 2、所有参数都应该用@Param进行签名,以后使用指定的名字在SQL中取值
     * @param id
     * @return
     */
    TUser getUserById(@Param("id") Long id);
}

UserMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.atguigu.boot3.ssm.mapper.UserMapper">
    <!--    接口的全类名和namespace的值是一一对应的
            select id,login_name loginName,nick_name nickName,passwd from t_user where id=#{id}
    -->

    <select id="getUserById" resultType="com.atguigu.boot3.ssm.bean.TUser">
        select *
        from t_user
        where id = #{id}
    </select>
</mapper>

告诉SpringBoot,扫描Mapper接口的位置 

package com.atguigu.boot3.ssm;

import com.atguigu.boot3.starter.robot.annotation.EnableRobot;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * 1、@MapperScan【批量扫描注解】; 告诉MyBatis,扫描哪个包下面的所有接口
 * 2、使用mybatis.mapper-locations,告诉MyBatis,每个接口的xml文件都在哪里
 * 3、MyBatis自动关联绑定。
 */
@EnableRobot
@MapperScan(basePackages = "com.atguigu.boot3.ssm.mapper")
@SpringBootApplication
public class Boot305SsmApplication {

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

}

3.3 配置MyBatis

# 2、配置整合MyBatis
mybatis.mapper-locations=classpath:/mapper/*.xml
# 打开驼峰命名规则
mybatis.configuration.map-underscore-to-camel-case=true

3.4 CRUD编写

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

UserController

package com.atguigu.boot3.ssm.controller;

import com.atguigu.boot3.ssm.bean.TUser;
import com.atguigu.boot3.ssm.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author lfy
 * @Description
 * @create 2023-04-20 17:07
 */
@RestController
public class UserController {

    @Autowired
    UserMapper userMapper;


    /**
     * 返回User的json数据
     * @param id
     * @return
     */
    @GetMapping("/user/{id}")
    public TUser getUser(@PathVariable("id") Long id){
        TUser user = userMapper.getUserById(id);
        return user;
    }
}

3.5 自动配置原理

SSM整合总结:

  1. 导入 mybatis-spring-boot-starter
  2. 配置数据源信息
  3. 配置mybatis的mapper接口扫描xml映射文件扫描
  4. 编写bean,mapper,生成xml,编写sql 进行crud。事务等操作依然和Spring中用法一样
  5. 效果:

        a)所有sql写在xml中

        b)所有mybatis配置写在application.properties下面

如何分析哪个场景导入以后,开启了哪些自动配置类。

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

3.6 快速定位生效的配置

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

3.7 扩展:整合其他数据源

3.7.1 Druid 数据源

暂不支持 SpringBoot3

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

Druid官网:

GitHub - alibaba/druid: 阿里云计算平台DataWorks(https://help.aliyun.com/document_detail/137663.html) 团队出品,为监控而生的数据库连接池

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

3.8 附录:示例数据库

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

4 SpringBoot3-基础特性

4.1 SpringApplication

4.1.1 自定义 banner

类路径添加banner.txt或设置spring.banner.location就可以定制 banner

推荐网站:Spring Boot banner 在线生成工具,制作下载英文 banner.txt,修改替换 banner.txt 文字实现自定义,个性化启动 banner-bootschool.net

spring.banner.location=classpath:banner.txt
#spring.main.banner-mode=off    //关闭banner

4.1.2 自定义 SpringApplication

@SpringBootApplication //主程序类
public class Boot306FeaturesApplication {

    public static void main(String[] args) {
        //1、SpringApplication: Boot应用的核心API入口
//        SpringApplication.run(Boot306FeaturesApplication.class, args);

        //===============1、自定义 SpringApplication 的底层设置
        SpringApplication application = new SpringApplication(Boot306FeaturesApplication.class);

        //程序化调整【SpringApplication的参数】
        application.setDefaultProperties();
        //这个配置不优先
        application.setBannerMode(Banner.Mode.OFF);

        //【配置文件优先级高于程序化调整的优先级】

        //2、SpringApplication 运行起来
        application.run(args);
    }
}

4.1.3 FluentBuilder API

流式写法

//================2、Builder方式构建 SpringApplication; 通过FluentAPI进行设置
new SpringApplicationBuilder()
    .sources(Parent.class)
    .child(Application.class)
    .bannerMode(Banner.Mode.OFF)
    .run(args);

4.2 Profiles

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

步骤:

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

Boot306FeaturesApplication 

package com.atguigu.boot3.features;

import com.atguigu.boot3.features.bean.Cat;
import com.atguigu.boot3.features.bean.Dog;
import com.atguigu.boot3.features.bean.Pig;
import com.atguigu.boot3.features.bean.Sheep;
import com.atguigu.boot3.starter.robot.RobotAutoConfiguration;
import com.atguigu.boot3.starter.robot.annotation.EnableRobot;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.Banner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Import;

/**
 * 环境隔离:
 * 1、标识环境
 *    1)、区分出几个环境: dev(开发环境)、test(测试环境)、prod(生产环境)
 *    2)、指定每个组件在哪个环境下生效; default环境:默认环境
 *          通过: @Profile({"test"})标注
 *          组件没有标注@Profile代表任意时候都生效
 *    3)、默认只有激活指定的环境,这些组件才会生效。
 * 2、激活环境
 *    配置文件激活:spring.profiles.active=dev;
 *    命令行激活: java -jar xxx.jar  --spring.profiles.active=dev
 *
 * 3、配置文件怎么使用Profile功能
 *    1)、application.properties: 主配置文件。任何情况下都生效
 *    2)、其他Profile环境下命名规范:  application-{profile标识}.properties:
 *          比如:application-dev.properties
 *    3)、激活指定环境即可:  配置文件激活、命令行激活
 *    4)、效果:
 *        项目的所有生效配置项 = 激活环境配置文件的所有项 + 主配置文件和激活文件不冲突的所有项
 *        如果发生了配置冲突,以激活的环境配置文件为准。
 *        application-{profile标识}.properties 优先级高于 application.properties
 *
 *        主配置和激活的配置都生效,优先以激活的配置为准
 */

//SpringBoot的默认扫描规则,只扫描自己主程序所在的包以及子包

/**
 * 自定义starter所有组件包名:com.atguigu.boot3.starter.robot
 * 当前项目的主程序所在包:    com.atguigu.boot3.features
 */
@EnableRobot
@Slf4j
@SpringBootApplication //主程序类
public class Boot306FeaturesApplication {

    public static void main(String[] args) {
        //1、SpringApplication: Boot应用的核心API入口
//        SpringApplication.run(Boot306FeaturesApplication.class, args);

        //===============1、自定义 SpringApplication 的底层设置
//        SpringApplication application = new SpringApplication(Boot306FeaturesApplication.class);
//
//        //程序化调整【SpringApplication的参数】
        application.setDefaultProperties();
//        //这个配置不优先
//        application.setBannerMode(Banner.Mode.OFF);
//
//
//        //【配置文件优先级高于程序化调整的优先级】
//
//        //2、SpringApplication 运行起来
//        application.run(args);


        //================2、Builder方式构建 SpringApplication; 通过FluentAPI进行设置
        ConfigurableApplicationContext context = new SpringApplicationBuilder()
                .main(Boot306FeaturesApplication.class) //各种参数均可设置
                .sources(Boot306FeaturesApplication.class)
                .bannerMode(Banner.Mode.CONSOLE)
                .properties("server.port=8888","aaa=bbb") //springboot所有配置项都可以在这里定义
//                .environment(null)
//                .listeners(null)
                .run(args);

        try {
            Cat cat = context.getBean(Cat.class);
            log.info("组件cat:{}",cat);
        }catch (Exception e){

        }

        try {
            Dog dog = context.getBean(Dog.class);
            log.info("组件dog:{}",dog);
        }catch (Exception e){

        }

        try {
            Pig pig = context.getBean(Pig.class);
            log.info("组件pig:{}",pig);
        }catch (Exception e){

        }

        try {
            Sheep sheep = context.getBean(Sheep.class);
            log.info("组件sheep:{}",sheep);
        }catch (Exception e){

        }

    }
}

application.properties 

server.port=8000

spring.banner.location=classpath:banner.txt
#spring.main.banner-mode=off

# 激活指定的一个或多个环境
spring.profiles.active=dev

# 导入指定的配置
spring.config.import=classpath:/aaaa.properties
# 导入配置优先级低于配置文件的优先级
aaaaa=ccccc
# 指定默认环境
#spring.profiles.default=default

haha=我的端口是:${server.port} 我的名字是:${useraname:雷丰阳}
# 包含指定环境,不管你激活哪个环境,这个都要有。 总是要生效的环境
#spring.profiles.include=dev,test

# 环境分组
#spring.profiles.group.haha[0]=dev
#spring.profiles.group.haha[1]=test
#spring.profiles.group.hehe=prod,abc
robot.name=哈哈
robot.age=18
robot.email=haha@qq.com

4.2.1 使用

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

1 配置激活指定环境; 配置文件

spring.profiles.active=production,hsqldb

2 也可以使用命令行激活。--spring.profiles.active=dev,hsqldb

3 还可以配置默认环境; 不标注@Profile 的组件永远都存在。

       a)以前默认环境叫default

       b)spring.profiles.default=test

4 推荐使用激活方式激活指定环境

4.2.1.3 环境包含

注意:

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

最佳实战:

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

4.2.2 Profile 分组

创建prod组,指定包含dbmq配置

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

使用--spring.profiles.active=prod ,就会激活proddbmq配置文件

4.2.3 Profile 配置文件

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

profile优先级 > application

4.3 外部化配置

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

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

4.3.1 配置优先级

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

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

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

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

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

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

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

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

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

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

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

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

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

演示场景:

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

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

4.3.2 外部配置

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

1 类路径: 内部

        a)类根路径

        b)类下/config

2 当前路径(项目所在的位置)

        a)当前路径

        b)当前下/config子目录

        c)/config目录的直接子目录

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

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

规律:最外层的最优先。

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

配置不同就都生效(互补),配置相同高优先级覆盖低优先级

4.3.3 导入配置

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

# 导入指定的配置
spring.config.import=classpath:/aaaa.properties
# 导入配置优先级低于配置文件的优先级
aaaaa=ccccc

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

4.3.4 属性占位符

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

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

 ${name:default}取name的时候如果配置文件中没有配置过,则可以使用默认值

haha=我的端口是:${server.port} 我的名字是:${useraname:雷丰阳}

4.4 单元测试-JUnit5

4.4.1 整合

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

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

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

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

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

4.4.2 测试

4.4.2.0 组件测试

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

4.4.2.1 注解

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

JUnit 5 User Guide

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

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

class StandardTests {

    @BeforeAll
    static void initAll() {
    }

    @BeforeEach
    void init() {
    }

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

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

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

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

    @AfterEach
    void tearDown() {
    }

    @AfterAll
    static void tearDownAll() {
    }

}

package com.atguigu.boot3.features;


import com.atguigu.boot3.features.service.HelloService;
import org.junit.jupiter.api.*;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;
import org.junit.platform.commons.util.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.stream.Stream;


//测试类也必须在主程序所在的包及其子包
@SpringBootTest //具备测试SpringBoot应用容器中所有组件的功能
class Boot306FeaturesApplicationTests {


    @Autowired //自动注入任意组件即可测试
    HelloService helloService;

    @DisplayName("测试1")
    @Test
    void contextLoads() {
        int sum = helloService.sum(1, 2);
        Assertions.assertEquals(4,sum);

    }

    @ParameterizedTest //参数化测试
    @ValueSource(strings = {"one", "two", "three"})
    @DisplayName("参数化测试1")
    public void parameterizedTest1(String string) {
        System.out.println(string);
        Assertions.assertTrue(StringUtils.isNotBlank(string));
    }

    @DisplayName("😱")
    @Test
    void test01(){
        System.out.println("aaaa");
    }

    @BeforeAll  //所有测试方法运行之前先运行这个 : 只打印一次
    static void initAll() {
        System.out.println("hello");
    }

    @BeforeEach //每个测试方法运行之前先运行这个 : 每个方法运行打印一次
    void init() {
        System.out.println("world");
    }


    @ParameterizedTest
    @MethodSource("method")    //指定方法名,返回值就是测试用的参数
    @DisplayName("方法来源参数")
    public void testWithExplicitLocalMethodSource(String name) {
        System.out.println(name);
        Assertions.assertNotNull(name);
    }

    //返回Stream即可
    static Stream<String> method() {
        return Stream.of("apple", "banana");
    }

}
 4.4.2.2 断言

方法

说明

assertEquals

判断两个对象或两个原始类型是否相等

assertNotEquals

判断两个对象或两个原始类型是否不相等

assertSame

判断两个对象引用是否指向同一个对象

assertNotSame

判断两个对象引用是否指向不同的对象

assertTrue

判断给定的布尔值是否为 true

assertFalse

判断给定的布尔值是否为 false

assertNull

判断给定的对象引用是否为 null

assertNotNull

判断给定的对象引用是否不为 null

assertArrayEquals

数组断言

assertAll

组合断言

assertThrows

异常断言

assertTimeout

超时断言

fail

快速失败

4.4.2.3 嵌套测试

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());
            }
        }
    }
}
4.4.2.4 参数化测试(了解)

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

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

@ValueSource: 为参数化测试指定入参来源,支持八大基础类以及String类型,Class类型

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

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

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

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

@ParameterizedTest
@ValueSource(strings = {"one", "two", "three"})
@DisplayName("参数化测试1")
public void parameterizedTest1(String string) {
    System.out.println(string);
    Assertions.assertTrue(StringUtils.isNotBlank(string));
}


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

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

5 SpringBoot3-核心原理

5.1 事件和监听器

5.1.1 生命周期监听

场景:监听应用生命周期

5.1.1.1 监听器-SpringApplicationRunListene

自定义SpringApplicationRunListener监听事件:

1 编写SpringApplicationRunListener 实现类

2 在 META-INF/spring.factories 中配置 org.springframework.boot.SpringApplicationRunListener=自己的Listener,还可以指定一个有参构造器,接受两个参数(SpringApplication application, String[] args)

3 springboot spring-boot.jar中配置了默认的 Listener,如下

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

5.1.2 事件触发时机

5.1.2.1 各种回调监听器
  • BootstrapRegistryInitializer: 感知特定阶段:感知引导初始化
    • META-INF/spring.factories
    • 创建引导上下文bootstrapContext的时候触发。
    • application.addBootstrapRegistryInitializer();
    • 场景:进行密钥校对授权。
  • ApplicationContextInitializer: 感知特定阶段: 感知ioc容器初始化
    • META-INF/spring.factories
    • application.addInitializers();
  • ApplicationListener: 感知全阶段:基于事件机制,感知事件。 一旦到了哪个阶段可以做别的事
    • @Bean@EventListener事件驱动
    • SpringApplication.addListeners(…)SpringApplicationBuilder.listeners(…)
    • META-INF/spring.factories
  • SpringApplicationRunListener: 感知全阶段生命周期 + 各种阶段都能自定义操作; 功能更完善。
    • META-INF/spring.factories
  • ApplicationRunner: 感知特定阶段:感知应用就绪Ready。卡死应用,就不会就绪
    • @Bean
  • CommandLineRunner: 感知特定阶段:感知应用就绪Ready。卡死应用,就不会就绪
    • @Bean

最佳实战:

  • 如果项目启动前做事: BootstrapRegistryInitializer 和 ApplicationContextInitializer
  • 如果想要在项目启动完成后做事:ApplicationRunnerCommandLineRunner
  • 如果要干涉生命周期做事:SpringApplicationRunListener
  • 如果想要用事件机制:ApplicationListener
5.1.2.2 完整触发流程

9大事件触发顺序&时机

  1. ApplicationStartingEvent:应用启动但未做任何事情, 除过注册listeners and initializers.
  2. ApplicationEnvironmentPreparedEventEnvironment 准备好,但context 未创建.
  3. ApplicationContextInitializedEvent: ApplicationContext 准备好,ApplicationContextInitializers 调用,但是任何bean未加载
  4. ApplicationPreparedEvent: 容器刷新之前,bean定义信息加载
  5. ApplicationStartedEvent: 容器刷新完成, runner未调用

=========以下就开始插入了探针机制============

  1. AvailabilityChangeEventLivenessState.CORRECT应用存活; 存活探针
  2. ApplicationReadyEvent: 任何runner被调用
  3. AvailabilityChangeEvent:ReadinessState.ACCEPTING_TRAFFIC就绪探针,可以接请求
  4. ApplicationFailedEvent :启动出错

应用事件发送顺序如下:

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

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

5.1.2.3 SpringBoot 事件驱动开发

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

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


举个例子

LoginController

package com.atguigu.boot3.core.controller;

import com.atguigu.boot3.core.entity.UserEntity;
import com.atguigu.boot3.core.event.EventPublisher;
import com.atguigu.boot3.core.event.LoginSuccessEvent;
import com.atguigu.boot3.core.service.AccountService;
import com.atguigu.boot3.core.service.CouponService;
import com.atguigu.boot3.core.service.SysService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author lfy
 * @Description
 * @create 2023-04-24 18:41
 */
@RestController
public class LoginController {

    @Autowired
    AccountService accountService;

    @Autowired
    CouponService couponService;

    @Autowired
    SysService sysService;

    @Autowired
    EventPublisher eventPublisher;


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

        //TODO 发送事件.
        //1、创建事件信息
        LoginSuccessEvent event = new LoginSuccessEvent(new UserEntity(username, passwd));
        //2、发送事件
        eventPublisher.sendEvent(event);


        //1、账户服务自动签到加积分
//        accountService.addAccountScore(username);
//        //2、优惠服务随机发放优惠券
//        couponService.sendCoupon(username);
//        //3、系统服务登记用户登录的信息
//        sysService.recordLog(username);

        //设计模式:对新增开放,对修改关闭
        //xxx
        //xxx
        //xxx
        return username+"登录成功";
    }
}

事件发布者

@Service
public class EventPublisher implements ApplicationEventPublisherAware {

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

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

    /**
     * 会被自动调用,把真正发事件的底层组组件给我们注入进来
     * @param applicationEventPublisher event publisher to be used by this object
     */
    @Override
    public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
        this.applicationEventPublisher = applicationEventPublisher;
    }
}

事件订阅者 

@Service
public class CouponService {

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

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

5.2 自动配置原理

5.2.1 入门理解

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


总结:场景导入自动配置类,自动配置类注入组件;组件绑定属性类,属性类绑定配置文件。

所以:修改配置文件,就能修改底层参数


5.2.1.1 自动配置流程

1 导入starter

2 依赖导入autoconfigure

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

4 启动,加载所有 自动配置类 xxxAutoConfiguration

        a)给容器中配置功能组件

        b)组件参数绑定到 属性类中。xxxProperties

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

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

5 效果:

        a)修改配置文件,修改底层参数

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

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

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

以上回答来自ChatGPT-3.5

在SpringBoot中,

META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 

5.2.1.3 功能开关
  • 自动配置:全部都配置好,什么都不用管。 自动批量导入
    • 项目一启动,spi文件中指定的所有都加载。
  • @EnableXxxx:手动控制哪些功能的开启; 手动导入。
    • 开启xxx功能
    • 都是利用 @Import 把此功能要用的组件导入进去

5.2.2 进阶理解

5.2.2.1 @SpringBootApplication

@SpringBootConfiguration

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

@EnableAutoConfiguration:开启自动配置

开启自动配置

@AutoConfigurationPackage:扫描主程序包:加载自己的组件

  • 利用 @Import(AutoConfigurationPackages.Registrar.class) 想要给容器中导入组件。
  • 把主程序所在的的所有组件导入进来。
  • 为什么SpringBoot默认只扫描主程序所在的包及其子包

@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) })
5.2.2.2 完整启动加载流程

生命周期启动加载流程

5.3 自定义starter

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

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

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

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

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

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

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

5.3.1 业务代码

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

@ConfigurationProperties(prefix = "robot")  //此属性类和配置文件指定前缀绑定
@Component
@Data
public class RobotProperties {

    private String name;
    private String age;
    private String email;
}
<!--导入配置处理器,配置文件自定义的properties配置都会有提示-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
</dependency>

#robot.name=李四
#robot.age=18
#robot.email=aaa@qq.com

robot.name=王五
robot.age=19
robot.email=haha@qq.com

5.3.2 基本抽取

  • 创建starter项目,把公共代码需要的所有依赖导入
  • 把公共代码复制进来
  • 自己写一个 RobotAutoConfiguration,给容器中导入这个场景需要的所有组件
    • 为什么这些组件默认不会扫描进去?
    • starter所在的包和 引入它的项目的主程序所在的包不是父子层级
  • 别人引用这个starter,直接导入这个 RobotAutoConfiguration,就能把这个场景的组件导入进来
  • 功能生效。
  • 测试编写配置文件

RobotAutoConfiguration

package com.atguigu.boot3.starter.robot;

import com.atguigu.boot3.starter.robot.controller.RobotController;
import com.atguigu.boot3.starter.robot.properties.RobotProperties;
import com.atguigu.boot3.starter.robot.service.RobotService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

/**
 * @author lfy
 * @Description
 * @create 2023-04-27 20:15
 */
//给容器中导入Robot功能要用的所有组件
@Import({RobotProperties.class, RobotService.class})
@Configuration
public class RobotAutoConfiguration {

    @Bean //把组件导入到容器中
    public RobotController robotController(){
        return new RobotController();
    }

}

5.3.3 使用@EnableXxx机制

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Import(RobotAutoConfiguration.class)
public @interface EnableRobot {

}

别人引入starter需要使用 @EnableRobot开启功能(使用@EnableRobot,相当于使用了@Import(RobotAutoConfiguration.class))

5.3.4 完全自动配置

依赖SpringBoot的SPI机制

META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件中编写好我们自动配置类的全类名即可(这样@EnableXxx都可以不用标了,导入依赖就可以使用

项目启动,自动加载我们的自动配置类

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

1、自动配置包位置变化【参照视频:07、11】

META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

2、jakata api迁移

  • druid有问题

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

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

5、响应式编程全套

  • 4
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

开五档的蒙奇

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值