SpringBoot入门及核心源码分析

SpringBoot

一、学习SpringBoot的知识要求及其环境配置要求

1、知识要求

  • 熟悉基本的Java代码

  • 熟悉Spring、SpringMVC、Mybatis

  • 熟悉Maven

2、环境要求

  • Java8及以上
  • Maven3.3及以上

3、本文的环境

  • Maven3.8.1
  • Java1.8
  • SpringBoot2.6.2

二、SpringBoot和Spring的关系

1、Spring能干什么

img

2、Spring生态

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

3、Spring5的升级以及Java8的新特性

  1. spring5增加了响应式编程

    img

  2. Java8增加了一些新特性

    如:接口默认实现。

    所以需要重新设计源码架构

4、为什么要用SpringBoot

通俗的说:Spring的生态已经十分完整了,但是有一个问题,Spring本身是一个机箱,其他Spring组件是电脑配件,而把电脑配件安装到机箱里是相当麻烦的事情,尤其是配件数量众多的情况下,需要保证插口匹配(版本匹配),还要进行繁琐的安装(spring的xml可是配置炼狱),所以我们需要有个“机器”来帮我们自动装配好主机,把插口匹配和安装都完成,而我只需要使用主机即可,SpringBoot就是起到这样一个作用,它帮助我们管理Spring繁琐的配置,也做了严格的版本控制,使得我们只需要下载相关依赖就可以达到和原来配置老半天一样的效果,甚至效果胜过原来的Spring

  1. SpringBoot的优点

    • Create stand-alone Spring applications

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

      • 内嵌web服务器 (SSM框架时期是需要外搭Tomcat的,先把项目打成war包在部署到Tomcat上,而现在SpringBoot自带轻量级服务器,只需要打成jar包就可以直接部署了)
    • Provide opinionated ‘starter’ dependencies to simplify your build configuration

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

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

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

      • 无代码生成、无需编写XML
  2. SpringBoot的缺点

    • 人称版本帝,迭代快,需要时刻关注变化
    • 封装太深,内部原理复杂,不容易精通
  3. 符合时代要求(微服务、分布式、云原生)

    3.1、微服务

    James Lewis and Martin Fowler (2014) 提出微服务完整概念。https://martinfowler.com/microservices/

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

    • 微服务是一种架构风格

    • 一个应用拆分为一组小型服务

    • 每个服务运行在自己的进程内,也就是可独立部署和升级

    • 服务之间使用轻量级HTTP交互

    • 服务围绕业务功能拆分

    • 可以由全自动部署机制独立部署

    • 去中心化,服务自治。服务可以使用不同的语言、不同的存储技术

    3.2、分布式

    img

    分布式的困难

    • 远程调用

    • 服务发现

    • 负载均衡

    • 服务容错

    • 配置管理

    • 服务监控

    • 链路追踪

    • 日志管理

    • 任务调度

    分布式的解决

    • SpringBoot + SpringCloud

    img

    3.3、云原生

    原生应用如何上云。 Cloud Native

    上云的困难

    • 服务自愈

    • 弹性伸缩

    • 服务隔离

    • 自动化部署

    • 灰度发布

    • 流量治理

三、SpringBoot入门

这里以我们经典的helloworld程序作为入门,而且我们不使用idea自带的SpringBoot一键生成项目,我们只创建Maven项目,然后一步一步增加需要的东西

1、Maven的配置

这里需要配置Maven的镜像和jdk的版本

先找到Maven的setting.xml配置文件,路径为:你的Maven文件夹所在地下的conf下的setting.xml

随机找一个地方插入以下配置即可:

    <mirrors>
      <mirror>
        <id>nexus-aliyun</id>
        <mirrorOf>central</mirrorOf>
        <name>Nexus aliyun</name>
        <url>http://maven.aliyun.com/nexus/content/groups/public</url>
      </mirror>
  </mirrors>
 
  <profiles>
         <profile>
              <id>jdk-1.8</id>
              <activation>
                <activeByDefault>true</activeByDefault>
                <jdk>1.8</jdk>
              </activation>
              <properties>
                <maven.compiler.source>1.8</maven.compiler.source>
                <maven.compiler.target>1.8</maven.compiler.target>
                <maven.compiler.compilerVersion>1.8</maven.compiler.compilerVersion>
              </properties>
         </profile>
  </profiles>

2、创建一个空的Maven项目即可,不作演示

3、引入SpringBoot所需的依赖

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.6.2</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>

4、引入SpringBoot的打包插件,方便我们把项目打成jar包

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

5、开始编写主程序类,关键是@SpringBootApplication注解

/**
 * 主程序类
 * @SpringBootApplication标记这是springboot应用
 */
@SpringBootApplication
public class boot {
    public static void main(String[] args) {
        //固定写法
        SpringApplication.run(boot.class,args);
    }
}

6、编写controller测试类

@RestController
public class HelloWorld {
    @RequestMapping("/")
    public String hello(){
        return "helloworld";
    }
}

值得一提的是:@RestController相当于@ResponseBody和@Controller两个注解一起使用

7、运行后在浏览器上测试

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pobFtSR1-1644079490001)(C:\Users\利姆鲁\AppData\Roaming\Typora\typora-user-images\image-20220121153106742.png)]

8、尝试把项目打成jar包并运行测试

1.在Maven构建中先执行clean清除原来的target项目,再用package打包

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-F7rd7XDO-1644079490002)(C:\Users\利姆鲁\AppData\Roaming\Typora\typora-user-images\image-20220121152651295.png)]

此时在target下会得到一个jar包,到cmd中运行改jar包即可

2.到jar包目录下启动cmd,执行java -jar +jar包文件的命令,即可启动项目,如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AONp2w4n-1644079490003)(C:\Users\利姆鲁\AppData\Roaming\Typora\typora-user-images\image-20220121153042819.png)]

3.浏览器测试

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GH51aczW-1644079490004)(C:\Users\利姆鲁\AppData\Roaming\Typora\typora-user-images\image-20220121153109890.png)]

四、SpringBoot的依赖管理、场景启动器和自动装配

1、依赖管理

注意到,我们在入门中,pom文件需要引入一个父项目,即:

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.6.2</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

进入该父项目后,发现它还有一个父项目,我们这里防止混淆,叫它“爷项目”

  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-dependencies</artifactId>
    <version>2.6.2</version>
  </parent>

进入爷项目,发现,爷项目中配置了依赖管理dependencyManagement属性,并规定了各依赖的版本号,使得我们导入它涉及到的依赖的时候,是不需要配置版本号的,它会对应给我们匹配默认的版本号,我们也称这种机制为自动版本仲裁机制,当然,如果我们的依赖不存在依赖管理中,还是需要配置版本号的,以下展示部分配置:

版本号配置:

<properties>
    <activemq.version>5.16.3</activemq.version>
    <antlr2.version>2.7.7</antlr2.version>
    <appengine-sdk.version>1.9.93</appengine-sdk.version>
    <artemis.version>2.19.0</artemis.version>
    <aspectj.version>1.9.7</aspectj.version>

依赖管理:

<dependencyManagement>    <dependencies>      <dependency>        <groupId>org.apache.activemq</groupId>        <artifactId>activemq-amqp</artifactId>        <version>${activemq.version}</version>      </dependency>      <dependency>        <groupId>org.apache.activemq</groupId>        <artifactId>activemq-blueprint</artifactId>        <version>${activemq.version}</version>      </dependency>

这里有一个问题,如果我们需要跟默认版本不一样的版本怎么办,我们可以自行配置,以MySQL为例,SpringBoot2.6.2指定的MySQL版本默认为8.0.47[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ae3KLSgk-1644079490005)(C:\Users\利姆鲁\AppData\Roaming\Typora\typora-user-images\image-20220121162052097.png)]

如果我们需要别的版本,则需要自己配置,比如我要5.0.14版本,则在项目的pom文件中如下配置即可:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Xpj0pVKz-1644079490006)(C:\Users\利姆鲁\AppData\Roaming\Typora\typora-user-images\image-20220121162152613.png)]

2、场景启动器

我们会经常见到形如spring-boot-starter-*的依赖,这些依赖就代表某种场景,这些是官方提供的场景启动器

如,web场景:

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

只要引入了场景的依赖,场景所需要的依赖也会一并自动引入

我们也会经常见到形如 *-spring-boot-starter的依赖,这些依赖则是第三方提供的简化开发的场景启动器

无论是官方提供的还是第三方提供的,场景启动器的底层依赖都是(2.6.2版本):

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

3、自动装配

小技巧,主程序的返回参数是我们的IOC容器,我们可以查看IOC容器中注入了哪些组件

@SpringBootApplicationpublic class boot {    public static void main(String[] args) {        //返回IOC容器        ConfigurableApplicationContext run = SpringApplication.run(boot.class, args);        //查看容器中的组件的名字        String[] beanDefinitionNames = run.getBeanDefinitionNames();        for (String beanDefinitionName : beanDefinitionNames) {            System.out.println(beanDefinitionNames);        }    }}

可以发现:

  • 自动配好Tomcat

    • 引入Tomcat依赖。
    • 配置Tomcat
  • 自动配好SpringMVC

    • 引入SpringMVC全套组件
    • 自动配好SpringMVC常用组件(功能)
  • 自动配好Web常见功能,如:字符编码问题

    • SpringBoot帮我们配置好了所有web开发的常见场景
  • SpringBoot是怎么自动识别我们所写的类的呢?

    • SpringBoot默认识别和主程序统计的类和同级包下的类。如果我们需要修改默认识别的路径,有多种方式,以下介绍两种:
      • 1.修改主程序注解,修改成形如:@SpringBootApplication(scanBasePackages = “想要的路径”)的形式
      • 2.把主程序注解替换为等价的三个注解
@SpringBootApplication等同于@SpringBootConfiguration@EnableAutoConfiguration@ComponentScan("想要的路径")
  • 各种配置拥有默认值

    • 默认配置最终都是映射到某个类上,如:MultipartProperties
    • 配置文件的值最终会绑定每个类上,这个类会在容器中创建对象
  • 按需加载所有自动配置项

    • 引入了哪些场景这个场景的自动配置才会开启
    • SpringBoot所有的自动配置功能都在 spring-boot-autoconfigure 包里面

五、SpringBoot的底层注解

1、@Configuration和@Bean 组件和实体对象

回忆一下,Spring中,我们如何注入一个对象,首先创建一个bean.xml,然后再配置注入对象,如下:

<bean id="user" class="User">	<property>...</property>    ...</bean>

而SpringBoot引入了@Configuration,使得使用这个注解配置的类的作用,就相当于bean.xml,类中的带有注解@Bean的方法的返回值是一个对象,而这个对象会被注入到这个组件类中,方法名相当于bean.xml的id,方法返回类型相当于bean.xml中的class,如下:

@Configurationpublic class MyConfig {    @Bean    public User user01(){        return new User("xiafan");    }}

如果想要更换更换id,只需要配置bean注解的参数即可:

如:@Bean(“user1”)

注意:带@Configuration注解的类会被注入到容器中

组件依赖:

就是方法中的一个方法依赖另外一个方法(一个类的属性包含别的类)

比如人可以拥有宠物,person类包含pet类

Configuration通过参数proxyBeanMethods来实现两种不同的模式,该参数默认为true。

  1. 当proxyBeanMethods为true的时候,启动代理模式,此时该组件类下的对象方法返回的对象会被代理,如果需要调用到,需要去内存中找是否有这个对象,比较耗费时间资源,启动较慢,但可以使不同情况下(比如存在依赖),调用同一个注入对象是同一个对象,我们也称这种情况为FULL()模式。

    使用场景:配置类组件之间有依赖关系,方法会被调用得到之前单实例组件,用Full模式

  2. 当proxyBeanMethods为false的时候,不启动代理模式,当需要调用到该组件类下的对象方法返回的对象时,会直接new一个新的该对象,而不需要去代理中找是否有这么对象已经存在,启动更快,我们也称这种情况为LITE()模式。

    使用场景:配置 类组件之间无依赖关系用Lite模式加速容器启动过程,减少判断

  3. 总的来说:存在组件依赖必须使用Full模式默认,否则默认使用Lite模式

2、SpringMVC中使用的注解依旧可以使用

@Component:相当于@Bean,但不太一样,Component需要将内容写固定且必须自己掌控源码,而@Bean较为灵活,即使是第三方的类也可以直接使用,不需要知道类内部的具体情况

@Controller:作用于表现层(controller层)

@Service:作用于业务逻辑层(service层)

@Resository:作用于持久层(dao层)

这四个注解本质上是一样的,名字不同只是方便区分层次类

3、@ComponentScan 组件扫描

重新定义SpringBoot的扫描包路径,在第四章已经说过了,不再说明

3、@Import 导入

导入我们不方便以本章第一点和第二点的方法的类,比如第三方的类,我们就没办法给它加一个如@Service之类的注解,所以需要@Import来导入,导入也很简单,只需要在某个类的前面导入即可,比如:

@Import({SQLData.class, RedisProperties.class})

参数是一个可以无限追加的Class类数组

4、@Conditional 条件装配

条件装配:满足条件才会装配,比如是否已存在某个组件,是否存在某个类,是否为web项目等等

img

用在对象方法上:只对该对象方法有限制

用在组件类上:对整个类下的方法都有限制

5、@ImportResource 导入资源

这个注解使得原来Spring的xml配置形式得以延续下来,但其实也极少使用,比如我们有一个Spring的xml文件叫bean.xml,里面注入了多个对象,然后我们想要把这个xml里面对象注入到SpringBoot项目中,要怎么做呢,只需要用这个注解导入就行了。

比如:@ImportResource(“classpath:beans.xml”)

6、@ConfigurationProperties 配置绑定

配置绑定有两种方式:

  1. @Component + @ConfigurationProperties

    这种方式只能用于类是由自己编写的情况下才能使用

    如下:

    @Component@ConfigurationProperties(prefix = "myuser")public class User {    public String name;    ...}
    

    此时在配置文件中就可以进行配置了,这里用yaml配置文件进行配置:

    myuser:	name:		"user01"
    
  2. @EnableConfigurationProperties + @ConfigurationProperties

    这种方式即可以用于类是由自己编写的情况也可以用于第三方提供的类的情况,但一般用于第三方提供的类的情况

    如下:

    实体类:

    @ConfigurationProperties(prefix = "myuser")public class User {    public String name;    ...}
    

    组件类:

    @Configuration@EnableConfigurationProperties(User.class)
    

    此时就可以配置绑定了,这里**@EnableConfigurationProperties**有两个作用,第一是开启了User类的配置绑定功能,第二是把User类注入到了容器里面。

    注意:prefix后面所带的字符串必须是全小写,不要用大写或者驼峰

六、从源码分析SpringBoot的自动配置原理

也就是我们的主程序注解@SpringBootApplication,我们进入这个注解进行一步一步分析。

主要关注三个注解:

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

1、@SpringBootConfiguration

进入该注解源码,如下:

@Target({ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)@Documented@Configuration@Indexedpublic @interface SpringBootConfiguration {    @AliasFor(        annotation = Configuration.class    )    boolean proxyBeanMethods() default true;}

注意到,它有@Configuration注解,说明它的主要作用就是把主程序标记为一个组件。

2、@EnableAutoConfiguratio(重点)

进入该注解源码,如下:

@Target({ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)@Documented@Inherited@AutoConfigurationPackage@Import({AutoConfigurationImportSelector.class})public @interface EnableAutoConfiguration{    ...}

这里我们重点关注@AutoConfigurationPackage,进入该注解源码,如下:

@Target({ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)@Documented@Inherited@Import({Registrar.class})public @interface AutoConfigurationPackage {    ...}

可以看到,该注解重点导入了一个Registrar.class的组件,我们再看一下这个组件干了什么,该类源码如下:

static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {        Registrar() {        }        public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {            AutoConfigurationPackages.register(registry, (String[])(new AutoConfigurationPackages.PackageImports(metadata)).getPackageNames().toArray(new String[0]));        }        public Set<Object> determineImports(AnnotationMetadata metadata) {            return Collections.singleton(new AutoConfigurationPackages.PackageImports(metadata));        }}

发现注册功能主要是由第二个方法完成的,我们研究一下第二个方法干了什么。首先,参数有一个AnnotationMetadata metadata,也就是注解的元信息,这个参数指的是什么呢,指的是使用了这个注解的类的元信息以及这个注解被使用在了哪里等,层层追踪也就是我们SpringBoot的主程序的信息。

然后有一个很重要的参数临时对象变量:new AutoConfigurationPackages.PackageImports(metadata),看名字就可以知道,它是根据metadata被使用的类,得到这个类的包名,到这里我们终于知道了,为什么SpringBoot扫描组件默认只扫描主程序类所在的包下的所有组件了。

好,回到我们@EnableAutoConfiguration的源码,除了@AutoConfigurationPackage外还有@Import({AutoConfigurationImportSelector.class}),下面我们研究一下这个导入的组件。

进入这个组件类,我们会发现,它的第一个方法就是选择导入的组件:

    public String[] selectImports(AnnotationMetadata annotationMetadata) {        if (!this.isEnabled(annotationMetadata)) {            return NO_IMPORTS;        } else {            AutoConfigurationImportSelector.AutoConfigurationEntry autoConfigurationEntry = this.getAutoConfigurationEntry(annotationMetadata);            return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());        }    }

其中主要依靠的是getAutoConfigurationEntry方法来完成的,我们再看一下这个方法干了什么:

    protected AutoConfigurationImportSelector.AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {        if (!this.isEnabled(annotationMetadata)) {            return EMPTY_ENTRY;        } else {            AnnotationAttributes attributes = this.getAttributes(annotationMetadata);            List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);            configurations = this.removeDuplicates(configurations);            Set<String> exclusions = this.getExclusions(annotationMetadata, attributes);            this.checkExcludedClasses(configurations, exclusions);            configurations.removeAll(exclusions);            configurations = this.getConfigurationClassFilter().filter(configurations);            this.fireAutoConfigurationImportEvents(configurations, exclusions);            return new AutoConfigurationImportSelector.AutoConfigurationEntry(configurations, exclusions);        }    }

我们发现这个方法主要是对getCandidateConfigurations方法产生的字符串进行操作,我们看一下getCandidateConfigurations方法干了什么:

    protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {        List<String> configurations = SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.getBeanClassLoader());        Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you are using a custom packaging, make sure that file is correct.");        return configurations;    }

发现,这个方法也是返回一个字符串,而这个字符串是由SpringFactoriesLoader类的loadFactoryNames方法生成的,我们看一下这个类的该方法在干什么:

    public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {        ClassLoader classLoaderToUse = classLoader;        if (classLoader == null) {            classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();        }        String factoryTypeName = factoryType.getName();        return (List)loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList());    }

这个方法依赖该类的loadSpringFactories方法,我们再看一下这个方法在干什么:

    private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {        Map<String, List<String>> result = (Map)cache.get(classLoader);        if (result != null) {            return result;        } else {            HashMap result = new HashMap();            try {                Enumeration urls = classLoader.getResources("META-INF/spring.factories");				...    }

这里省略了很多对urls的操作,我们只关注Enumeration urls = classLoader.getResources(“META-INF/spring.factories”);语句,这个语句说明,我们启动时自动导入的组件的资源文件路径是META-INF/spring.factories。

我们再进入自动配置注解的依赖项,发现,确实有这个资源文件,如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HlEWeIVF-1644079490007)(C:\Users\利姆鲁\AppData\Roaming\Typora\typora-user-images\image-20220122114236551.png)]

至此,我们知道了,自动配置导入非自己编写的组件是从哪里来的了,再进入这个文件看一下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8NHdsOYm-1644079490008)(C:\Users\利姆鲁\AppData\Roaming\Typora\typora-user-images\image-20220122114344219.png)]

第一行是我们的注解,意思是使用了第一行的注解,后续的组件会在启动时自动配置进SpringBoot。

好,接下来继续探讨,是不是我们导入的组件,都可以直接使用?答案是不一定,我们进入导入的组件看一下,以kafka为例分析,进入kafka自动配置类发现,它是拥有条件装配注解的(实际上所有的导入的组件的自动装配类都有),如下:

@Configuration(    proxyBeanMethods = false)@ConditionalOnClass({KafkaTemplate.class})@EnableConfigurationProperties({KafkaProperties.class})@Import({KafkaAnnotationDrivenConfiguration.class, KafkaStreamsAnnotationDrivenConfiguration.class})public class KafkaAutoConfiguration {    private final KafkaProperties properties;    public KafkaAutoConfiguration(KafkaProperties properties) {        this.properties = properties;    }    @Bean    @ConditionalOnMissingBean({KafkaTemplate.class})    public KafkaTemplate<?, ?> kafkaTemplate(ProducerFactory<Object, Object> kafkaProducerFactory, ProducerListener<Object, Object> kafkaProducerListener, ObjectProvider<RecordMessageConverter> messageConverter) {        KafkaTemplate<Object, Object> kafkaTemplate = new KafkaTemplate(kafkaProducerFactory);        messageConverter.ifUnique(kafkaTemplate::setMessageConverter);        kafkaTemplate.setProducerListener(kafkaProducerListener);        kafkaTemplate.setDefaultTopic(this.properties.getTemplate().getDefaultTopic());        return kafkaTemplate;    }    ...}

可以看到它的装载都有某些要求,比如要有某个类啊@ConditionalOnClass,要不存在某个bean啊@ConditionalOnMissingBean之类的,所以自动配置不等于可以直接使用,当我们需要使用的时候,还可能需要自己下载某些依赖才可以正常使用

3、@ComponentScan

扫描组件包,可自动可手动,上面已经使用过了,不再赘述

4、修改默认配置

就是我不要SpringBoot给我自动装配的组件了,我要自己写一个覆盖它。比如字符编码组件CharacterEncodingFilter,SpringBoot会给我们自动装配一个CharacterEncodingFilter,如果我们想要自己配一个则可以自己写一个,覆盖(原理是@ConditionalOnMissingBean注解)。

如下:

@Bean	@ConditionalOnMissingBean	public CharacterEncodingFilter characterEncodingFilter() {    }

七、总结自动装配和修改配置

  • SpringBoot先加载所有的自动配置类 ,一般都形如xxxxxAutoConfiguration

  • 每个自动配置类按照条件进行生效,默认都会绑定配置文件指定的值。xxxxxAutoConfiguration的值从xxxxProperties里面拿,而xxxProperties则跟配置文件application.yaml/application.properties进行了绑定

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

  • 只要容器中有这些组件,相当于这些功能就有了

  • 定制化配置

    • 用户直接自己@Bean替换底层的组件
    • 用户去看这个组件是获取的配置文件application.yaml/application.properties什么值就去修改。

自动装配的流程:

先通过xxxxxAutoConfiguration注入组件,然后组件从xxxxProperties中得到值,而xxxxProperties是和application.yaml/application.properties绑定的,xxxxProperties中的值可以从配置文件中进行赋值来修改默认值

八、SpringBoot正式实践中需要考虑的事情

1、选择所需的场景

可以根据官网参考选择:https://docs.spring.io/spring-boot/docs/current/reference/html/using.html#using.build-systems.starters

2、查看自动配置的内容

  1. 自己分析,查看引入场景对应的自动配置,一般都会生效了,也可能存在不生效的
  2. 在配置文件中配置:debug=true,查看配置报告,可以看到配置为:Positive(生效)或Negative(不生效)

3、是否需要修改SpringBoot提供的配置

  1. 关于配置

    • 查看文档,根据文档从配置文件中修改配置

      文档:https://docs.spring.io/spring-boot/docs/current/reference/html/application-properties.html#application-properties

    • 自己分析xxxxProperties类中绑定了什么配置属性,根据xxxxProperties类从配置文件中修改配置

  2. 自定义添加组件或更换SpringBoot提供的组件

    • 使用@Bean、@Component等注解,查看原来组件的条件装配机制决定如何替换
  3. 自定义器

    • XXXXXCustomizer

4、项目结构说明

这个东西其实很多教学都不会说的,可能因为是应该看得懂的,但是可能有些新手看不懂,这里画图说一下,如下图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PWtE0iNj-1644079490009)(C:\Users\利姆鲁\AppData\Roaming\Typora\typora-user-images\image-20220122165635175.png)]

target是我们编译生成的文件

5、如何创建项目

  1. 创建Maven项目然后自己导入依赖和建立各种包,这样搭建项目比较麻烦
  2. 直接使用IDEA提供的脚手架,IDEA可以直接配置创建一个SpringBoot项目,比较推荐这样搭建项目

九、配置文件

配置文件的文件类型可以是yaml也可以是properties,但我们更推荐用yaml来编写配置文件,学过SSM或JavaWeb的同学对properties应该都很熟悉了,所以以下主要介绍yaml配置文件。

注意:yaml和yml是一样的

1、简介

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

yaml非常适合用来做以数据为中心的配置文件

2、基本语法

  • key: value;kv之间有空格
  • 大小写敏感
  • 使用缩进表示层级关系
  • 缩进不允许使用tab,只允许空格
  • 缩进的空格数不重要,只要相同层级的元素左对齐即可
  • '#'表示注释
  • 字符串一般无需加引号,如果要加,''与""表示字符串内容 会被 转义/不转义
    • 这里单独举例说明:比如我们需要换行操作,那么如果是“hello \n world”则会换行成功,也就是不转义;如果是‘hello \n world’则会当成字符串hello \n world处理,也就是被转义了

3、数据类型

  • 字面量:单个的、不可再分的值。date、boolean、string、number、null
k: v
  • 对象:键值对的集合。map、hash、set、object
行内写法:  k: {k1:v1,k2:v2,k3:v3}#或k:   k1: v1  k2: v2  k3: v3
  • 数组:一组按次序排列的值。array、list、queue
行内写法:  k: [v1,v2,v3]#或者k: - v1 - v2 - v3

4、自定义配置类的配置提示

我们在第五章的配置绑定的时候说,SpringBoot允许我们自定义编写一个类并把它作为一个配置类,怎么操作我们不再说明,但是如果我们按照第五章所说的做了,会发现,自动装配的配置在yaml配置的时候会自动提示,但我们自定义编写的时候却不会,这样使得我们配置的时候比较麻烦,SpringBoot考虑到了这一点,只需要向pom.xml导入一个依赖即可解决这个问题:

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

导入之后配置自定义配置类的时候也会有提示了,但还会有一个问题,这个依赖对于我们项目来说是毫无作用的,只是方便编码人员而已,所以需要再向pom.xml增加一个配置,使得打jar包的时候不会把这个依赖一起打包进去,否则运行是会浪费不必要的资源,配置如下:

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

十、SpringBoot在Web开发场景的使用

1、SpringMVC自动配置概览

官方文档说明:

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

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

  • Inclusion of ContentNegotiatingViewResolver and BeanNameViewResolver beans.

    • 内容协商视图解析器和BeanName视图解析器
  • Support for serving static resources, including support for WebJars (covered later in this document)).

    • 静态资源(包括webjars)
  • Automatic registration of Converter, GenericConverter, and Formatter beans.

    • 自动注册 Converter,GenericConverter,Formatter
  • Support for HttpMessageConverters (covered later in this document).

    • 支持 HttpMessageConverters (后来我们配合内容协商理解原理)
  • Automatic registration of MessageCodesResolver (covered later in this document).

    • 自动注册 MessageCodesResolver (国际化用)
  • Static index.html support.

    • 静态index.html 页支持
  • Custom Favicon support (covered later in this document).

    • 自定义 Favicon
  • Automatic use of a ConfigurableWebBindingInitializer bean (covered later in this document).

    • 自动使用 ConfigurableWebBindingInitializer ,(DataBinder负责将请求数据绑定到JavaBean上)

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

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

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

声明 **WebMvcRegistrations** 改变默认底层组件

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

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

2、静态资源和欢迎页

2.1、静态资源访问

根据官方文档:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IZu75IpU-1644079490009)(C:\Users\利姆鲁\AppData\Roaming\Typora\typora-user-images\image-20220122220245781.png)]

可以知道,只要静态资源放在类路径下: called /static (or /public or /resources or /META-INF/resources,就可以直接通过当前项目根路径/ + 静态资源名来访问

比如有一个文件在这四个文件夹的任意一个下面,名为1.jpg

那么访问的时候,我们输入http://localhost:8080/1.png即可访问,注意到,静态资源访问默认是没有前缀的,那我们可不可以配置一个前缀,答案是可以的

根据官方文档:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JkeUOQne-1644079490011)(C:\Users\利姆鲁\AppData\Roaming\Typora\typora-user-images\image-20220122221150675.png)]

官方文档就说了,静态资源访问默认是没有前缀了,如果需要前缀可以配置文件中进行配置,比如我想要我的资源访问路径前面加上/xiafan那么我可以这么配置:

spring:  mvc:    static-path-pattern: /xiafan/**

这里补充说明一下==//*的区别,/是扫描该路径下所有的文件包括子目录里的文件,而/*==是只扫描该路径下的文件,如果包括子目录也不会扫描子目录下的文件

那么,可不可以不使用它默认的四个文件夹来使得我们的文件也可以被扫描作为静态资源呢,也是可以的。

根据官方文档:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Fb3pRW5w-1644079490012)(C:\Users\利姆鲁\AppData\Roaming\Typora\typora-user-images\image-20220122222058126.png)]

我们可以使用spring.web.resources.static-locations这个属性来配置,比如我要一个类路径下/xf的包作为静态资源的存放包,我可以这么写:

spring:  web:    resources:      static-locations: [classpath:/xf/]

我们进入这个类的源码也可以发现我们默认的四个文件夹是它的默认值,配置自定义路径后,原来的默认路径依然可以使用,默认路径源码如下:

private static final String[] CLASSPATH_RESOURCE_LOCATIONS = new String[]{"classpath:/META-INF/resources/", "classpath:/resources/", "classpath:/static/", "classpath:/public/"};
2.2、欢迎页配置

欢迎页就是我们访问站点的时候所直接显示的页面

根据官方文档:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Mm841gRY-1644079490013)(C:\Users\利姆鲁\AppData\Roaming\Typora\typora-user-images\image-20220122224023197.png)]

翻译一下就是:

Spring Boot支持静态和模板化的欢迎页面。它首先在配置的静态内容位置中查找index.html文件。如果没有找到,它就会寻找一个索引模板。如果找到其中任何一个,则自动将其用作应用程序的欢迎页面。

再说大白话就是:

  1. 首先在静态配置下寻找有没有叫index.html的文件,有的话就将它设置为欢迎页面,否则就到第二步
  2. 到controller层中寻找是否有路径为/index的controller,有的话就将它指向的页面作为欢迎页

这里还有一个可能是SpringBoot需要改进的一个机制,比如我们配置了静态资源访问前缀,然后再配置欢迎页,就会发现必须要加上前缀才可以访问欢迎页了,这明显是不符合业务的,用户不可能一上来就知道我们的前缀是什么,所以一般我们也不配置这个前缀。

2.3、静态资源访问和欢迎页配置的源码分析

我们知道,启动默认加载配置一般都名为xxxAutoConfiguration,而SpringMVC的大部分自动配置都在WebMvcAutoConfiguration类中。

@Configuration(    proxyBeanMethods = false)@ConditionalOnWebApplication(    type = Type.SERVLET)@ConditionalOnClass({Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class})@ConditionalOnMissingBean({WebMvcConfigurationSupport.class})@AutoConfigureOrder(-2147483638)@AutoConfigureAfter({DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class, ValidationAutoConfiguration.class})public class WebMvcAutoConfiguration {    ...}

查找内容,寻找到自动装配适配器WebMvcAutoConfigurationAdapter静态内部类

    @Configuration(        proxyBeanMethods = false    )    @Import({WebMvcAutoConfiguration.EnableWebMvcConfiguration.class})    @EnableConfigurationProperties({WebMvcProperties.class, WebProperties.class})    @Order(0)    public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer, ServletContextAware {        ...    }

可以看到它绑定了WebMvcProperties.class和WebProperties.class两个配置类,WebMvcProperties对应的配置前缀为:spring.mvc,WebProperties对应的配置前缀为:spring.web。

配置类一般都只有一个有参构造器,这个类的有参构造器如下:

	//有参构造器所有参数的值都会从容器中确定//ResourceProperties resourceProperties;获取和spring.resources绑定的所有的值的对象//WebMvcProperties mvcProperties 获取和spring.mvc绑定的所有的值的对象//ListableBeanFactory beanFactory Spring的beanFactory//HttpMessageConverters 找到所有的HttpMessageConverters//ResourceHandlerRegistrationCustomizer 找到 资源处理器的自定义器。=========//DispatcherServletPath  //ServletRegistrationBean   给应用注册Servlet、Filter....	public WebMvcAutoConfigurationAdapter(ResourceProperties resourceProperties, WebMvcProperties mvcProperties,				ListableBeanFactory beanFactory, ObjectProvider<HttpMessageConverters> messageConvertersProvider,				ObjectProvider<ResourceHandlerRegistrationCustomizer> resourceHandlerRegistrationCustomizerProvider,				ObjectProvider<DispatcherServletPath> dispatcherServletPath,				ObjectProvider<ServletRegistrationBean<?>> servletRegistrations) {			this.resourceProperties = resourceProperties;			this.mvcProperties = mvcProperties;			this.beanFactory = beanFactory;			this.messageConvertersProvider = messageConvertersProvider;			this.resourceHandlerRegistrationCustomizer = resourceHandlerRegistrationCustomizerProvider.getIfAvailable();			this.dispatcherServletPath = dispatcherServletPath;			this.servletRegistrations = servletRegistrations;		}

我们找到addResourceHandlers这个方法:

        public void addResourceHandlers(ResourceHandlerRegistry registry) {            if (!this.resourceProperties.isAddMappings()) {                logger.debug("Default resource handling disabled");            } else {                this.addResourceHandler(registry, "/webjars/**", "classpath:/META-INF/resources/webjars/");                this.addResourceHandler(registry, this.mvcProperties.getStaticPathPattern(), (registration) -> {                    registration.addResourceLocations(this.resourceProperties.getStaticLocations());                    if (this.servletContext != null) {                        ServletContextResource resource = new ServletContextResource(this.servletContext, "/");                        registration.addResourceLocations(new Resource[]{resource});                    }                });            }        }

这里就是我们静态资源注册的机制了,第一个addResourceHandler扫描webjars等前端静态资源,第二个addResourceHandler扫描我们配置文件下的静态资源。

接下来看欢迎页配置,发现welcomePageHandlerMapping方法:

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

可以看到这个方法都是对WelcomePageHandlerMapping类进行操作,我们进入这个类,发现有这样一个构造方法,HandlerMapping:处理器映射,保存了每一个Handler能处理哪些请求:

    WelcomePageHandlerMapping(TemplateAvailabilityProviders templateAvailabilityProviders, ApplicationContext applicationContext, Resource welcomePage, String staticPathPattern) {        if (welcomePage != null && "/**".equals(staticPathPattern)) {            logger.info("Adding welcome page: " + welcomePage);            this.setRootViewName("forward:index.html");        } else if (this.welcomeTemplateExists(templateAvailabilityProviders, applicationContext)) {            logger.info("Adding welcome page template: index");            this.setRootViewName("index");        }    }

到这里为什么之前配置了资源访问前缀后就不能自动把index.html页面作为欢迎页的问题找到了,因为welcomePage != null && “/**”.equals(staticPathPattern)已经把staticPathPattern的路径固定为默认路径了,所以如果我们修改了,就无法执行第一个if判断了。

至此,我们的源码分析完毕。

3、请求参数处理

3.1、Rest风格的使用和源码原理分析

在controller中,我们需要设置很多的Mapping,这些Mapping就是我们访问资源的时候的请求映射“向导”。

  1. 什么是Rest风格以及我们的探究扩展

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

      • 以前:/getUser 获取用户 /deleteUser 删除用户 /editUser 修改用户 /saveUser 保存用户

        以前每个操作需要一个请求映射

      • 现在: /user GET-获取用户 DELETE-删除用户 PUT-修改用户 POST-保存用户

        现在使用Rest风格,一个请求映射可以通过访问方式的不同而被多个操作共同使用

      • 核心Filter:HiddenHttpMethodFilter
        • 用法: 先在表单设置method=post,然后在input隐藏域中设置 _method=put
        • 需要在SpringBoot中手动开启这个组件
      • 扩展:如何把_method 这个名字换成我们自己喜欢的
  2. 我们尝试一下如何使得一个请求映射的不同访问方式是如何实现的

    先在欢迎页放四个表单,并设置访问方式:

    <form action="/user" method="get">    <input value="get" type="submit"/></form><form action="/user" method="post">    <input value="post" type="submit"/></form><form action="/user" method="delete">    <input value="delete" type="submit"/></form><form action="/user" method="put">    <input value="put" type="submit"/></form>
    

    然后创建一个controller来访问,也设置相应的访问方式:

    @RestControllerpublic class UserController {    @RequestMapping(value = "/user",method = RequestMethod.GET)    public String user1(){        return "GET";    }    @RequestMapping(value = "/user",method = RequestMethod.POST)    public String user2(){        return "POST";    }    @RequestMapping(value = "/user",method = RequestMethod.DELETE)    public String user3(){        return "DELETE";    }    @RequestMapping(value = "/user",method = RequestMethod.PUT)    public String user4(){        return "PUT";    }}
    

    然后运行测试,发现:

    只有get和post访问成功了,而delete和put都默认变成了get访问,这是为什么呢,我们知道html本身没有提供delete和put方式访问,但SpringBoot又支持这样的方式,我们进行源码的探究,看一下怎么解决。

  3. 源码探究

    在WebMvcAutoConfiguration类中我们找到了与我们方式访问有关系的OrderedHiddenHttpMethodFilter隐藏方式拦截器组件,如下:

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

    进入ConditionalOnProperty注解查看它的默认值情况,如下:

    @Retention(RetentionPolicy.RUNTIME)@Target({ElementType.TYPE, ElementType.METHOD})@Documented@Conditional({OnPropertyCondition.class})public @interface ConditionalOnProperty {    String[] value() default {};    String prefix() default "";    String[] name() default {};    String havingValue() default "";    boolean matchIfMissing() default false;}
    

    发现它的默认值matchIfMissing是false,也就是默认是不开启的,那么首先我们肯定是要在配置文件中把它设置为true。

    开启后,我们的请求都会被这个拦截器所拦截,并进行一些操作,我们继续分析,进入HiddenHttpMethodFilter源码发现这样一段拦截器代码:

        private String methodParam = "_method";    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {        HttpServletRequest requestToUse = request;        if ("POST".equals(request.getMethod()) && request.getAttribute("javax.servlet.error.exception") == null) {            String paramValue = request.getParameter(this.methodParam);            if (StringUtils.hasLength(paramValue)) {                String method = paramValue.toUpperCase(Locale.ENGLISH);                if (ALLOWED_METHODS.contains(method)) {                    requestToUse = new HiddenHttpMethodFilter.HttpMethodRequestWrapper(request, method);                }            }        }        filterChain.doFilter((ServletRequest)requestToUse, response);    }
    

    读这段代码,我们可以知道,拦截器只过滤出form表单上请求是POST的请求,然后再把input中名为_method的参数的值取出来,并强制转化为大写,然后判断方法是不是允许的值ALLOWED_METHODS.contains(method),我们看一下允许什么值干了什么:

        static {        ALLOWED_METHODS = Collections.unmodifiableList(Arrays.asList(HttpMethod.PUT.name(), HttpMethod.DELETE.name(), HttpMethod.PATCH.name()));    }
    

    可以看到,允许PUT、DELETE、PATCH三种方式,所以是可以的。

    我们再看一下HttpMethodRequestWrapper是什么,看名字可以知道是一个包装类,那我们看一下它包装了什么,它的源码:

        private static class HttpMethodRequestWrapper extends HttpServletRequestWrapper {        private final String method;        public HttpMethodRequestWrapper(HttpServletRequest request, String method) {            super(request);            this.method = method;        }        public String getMethod() {            return this.method;        }    }
    

    它继承了一个servelet的包装类,我们再看有一下这个包装类是什么:

    public class HttpServletRequestWrapper extends ServletRequestWrapper implements HttpServletRequest
    

    我们发现它继承了原生的HttpServletRequest,所以其实就是把原生的HttpServletRequest给包装了,此时的getMethod方法其实已经被重写了已经和原来的原生代码不一样了,获取的是我们拓展访问方式之后的method了。

    那么现在,我们给DELETE和PUT请求加上一个参数为_method的隐藏域,如下:

    <form action="/user" method="post">    <input name="_method" value="delete" type="hidden">    <input value="delete" type="submit"/></form><form action="/user" method="post">    <input name="_method" value="put" type="hidden">    <input value="put" type="submit"/>
    

    此时打开这个拦截器,我们再尝试一下访问可以发现,DELETE和PUT方式已经可以正常访问了。

    注意:如果是客户端的请求,则不需要开启这个拦截器,比如postman或者安卓直接访问,所以这个功能并不会默认开启

3.2、如何改变默认的_method参数

注意到HiddenHttpMethodFilter类中提供了set方法来设置这个属性:

private String methodParam = "_method";    public void setMethodParam(String methodParam) {        Assert.hasText(methodParam, "'methodParam' must not be empty");        this.methodParam = methodParam;    }

所以我们自己写一个配置类,然后通过set方法修改这个值,再注入到组件中就好了,如:

@Beanpublic HiddenHttpMethodFilter hiddenHttpMethodFilter(){    HiddenHttpMethodFilter mf = new HiddenHttpMethodFilter();    mf.setMethodParam("你喜欢的参数名");    return mf;}
3.3、请求映射的原理

我们知道SpringMVC是帮我们处理请求映射的,其中DispatcherServlet是我们的视图解析类,我们来看一下SpringBoot是怎么:

查看DispatcherServlet的继承树情况为:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-B2OVBCis-1644079490014)(C:\Users\利姆鲁\AppData\Roaming\Typora\typora-user-images\image-20220124153534623.png)]

我们看到DispatcherServlet也是继承的HttpServlet,所以它必定有继承或重写doGet和doPost方法,而Servlet的功能都是写在这两个方法中的,我们通过阅读源码发现,doGet和doPost方法的重写在FrameworkServlet中,但它的实现是依赖另外一个方法processRequest的:

    protected final void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {        this.processRequest(request, response);    }    protected final void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {        this.processRequest(request, response);    }

那我们看一下processRequest方法做了什么:

    protected final void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {        long startTime = System.currentTimeMillis();        Throwable failureCause = null;        LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext();        LocaleContext localeContext = this.buildLocaleContext(request);        RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();        ServletRequestAttributes requestAttributes = this.buildRequestAttributes(request, response, previousAttributes);        WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);        asyncManager.registerCallableInterceptor(FrameworkServlet.class.getName(), new FrameworkServlet.RequestBindingInterceptor());        this.initContextHolders(request, localeContext, requestAttributes);        try {            this.doService(request, response);        } catch (IOException | ServletException var16) {            failureCause = var16;            throw var16;        } catch (Throwable var17) {            failureCause = var17;            throw new NestedServletException("Request processing failed", var17);        } finally {            this.resetContextHolders(request, previousLocaleContext, previousAttributes);            if (requestAttributes != null) {                requestAttributes.requestCompleted();            }            this.logResult(request, response, (Throwable)failureCause, asyncManager);            this.publishRequestHandledEvent(request, response, startTime, (Throwable)failureCause);        }    }

我们发现上面一大段都是对参数或者程序时间,程序异步等操作,而最主要是使用了一个叫doService的方法来作为方法的主体,所以我们再看一下doService方法做了什么:

    protected abstract void doService(HttpServletRequest request, HttpServletResponse response) throws Exception;

我们发现在FrameworkServlet中并没有定义这个方法的实现,而FrameworkServlet是DispatcherServlet的直接父类,所以在DispatcherServlet中必定有它的实现才对,我们查看DispatcherServlet的源码发现,确实有这样一个方法:

    protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {        this.logRequest(request);        Map<String, Object> attributesSnapshot = null;        if (WebUtils.isIncludeRequest(request)) {            attributesSnapshot = new HashMap();            Enumeration attrNames = request.getAttributeNames();            label116:            while(true) {                String attrName;                do {                    if (!attrNames.hasMoreElements()) {                        break label116;                    }                    attrName = (String)attrNames.nextElement();                } while(!this.cleanupAfterInclude && !attrName.startsWith("org.springframework.web.servlet"));                attributesSnapshot.put(attrName, request.getAttribute(attrName));            }        }        request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.getWebApplicationContext());        request.setAttribute(LOCALE_RESOLVER_ATTRIBUTE, this.localeResolver);        request.setAttribute(THEME_RESOLVER_ATTRIBUTE, this.themeResolver);        request.setAttribute(THEME_SOURCE_ATTRIBUTE, this.getThemeSource());        if (this.flashMapManager != null) {            FlashMap inputFlashMap = this.flashMapManager.retrieveAndUpdate(request, response);            if (inputFlashMap != null) {                request.setAttribute(INPUT_FLASH_MAP_ATTRIBUTE, Collections.unmodifiableMap(inputFlashMap));            }            request.setAttribute(OUTPUT_FLASH_MAP_ATTRIBUTE, new FlashMap());            request.setAttribute(FLASH_MAP_MANAGER_ATTRIBUTE, this.flashMapManager);        }        RequestPath previousRequestPath = null;        if (this.parseRequestPath) {            previousRequestPath = (RequestPath)request.getAttribute(ServletRequestPathUtils.PATH_ATTRIBUTE);            ServletRequestPathUtils.parseAndCache(request);        }        try {            this.doDispatch(request, response);        } finally {            if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted() && attributesSnapshot != null) {                this.restoreAttributesAfterInclude(request, attributesSnapshot);            }            if (this.parseRequestPath) {                ServletRequestPathUtils.setParsedRequestPath(previousRequestPath, request);            }        }    }

我们发现这个方法仍然是先由一大堆代码对参数等情况进行处理,然后会执行一个doDispatch方法作为主体,源码为:

    protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {        HttpServletRequest processedRequest = request;        HandlerExecutionChain mappedHandler = null;        boolean multipartRequestParsed = false;        WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);        try {            try {                ModelAndView mv = null;                Object dispatchException = null;                try {                    processedRequest = this.checkMultipart(request);                    multipartRequestParsed = processedRequest != request;                    mappedHandler = this.getHandler(processedRequest);                    if (mappedHandler == null) {                        this.noHandlerFound(processedRequest, response);                        return;                    }                    HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());                    String method = request.getMethod();                    boolean isGet = HttpMethod.GET.matches(method);                    if (isGet || HttpMethod.HEAD.matches(method)) {                        long lastModified = ha.getLastModified(request, mappedHandler.getHandler());                        if ((new ServletWebRequest(request, response)).checkNotModified(lastModified) && isGet) {                            return;                        }                    }                    if (!mappedHandler.applyPreHandle(processedRequest, response)) {                        return;                    }                    mv = ha.handle(processedRequest, response, mappedHandler.getHandler());                    if (asyncManager.isConcurrentHandlingStarted()) {                        return;                    }                    this.applyDefaultViewName(processedRequest, mv);                    mappedHandler.applyPostHandle(processedRequest, response, mv);                } catch (Exception var20) {                    dispatchException = var20;                } catch (Throwable var21) {                    dispatchException = new NestedServletException("Handler dispatch failed", var21);                }                this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException);            } catch (Exception var22) {                this.triggerAfterCompletion(processedRequest, response, mappedHandler, var22);            } catch (Throwable var23) {                this.triggerAfterCompletion(processedRequest, response, mappedHandler, new NestedServletException("Handler processing failed", var23));            }        } finally {            if (asyncManager.isConcurrentHandlingStarted()) {                if (mappedHandler != null) {                    mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);                }            } else if (multipartRequestParsed) {                this.cleanupMultipart(processedRequest);            }        }    }

所以我们主要分析doDispatch方法即可。

我们重点分析这段代码:

                try {                    processedRequest = this.checkMultipart(request);                    multipartRequestParsed = processedRequest != request;                    mappedHandler = this.getHandler(processedRequest);                    if (mappedHandler == null) {                        this.noHandlerFound(processedRequest, response);                        return;                    }                    HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());                    String method = request.getMethod();                    boolean isGet = HttpMethod.GET.matches(method);                    if (isGet || HttpMethod.HEAD.matches(method)) {                        long lastModified = ha.getLastModified(request, mappedHandler.getHandler());                        if ((new ServletWebRequest(request, response)).checkNotModified(lastModified) && isGet) {                            return;                        }                    }                    if (!mappedHandler.applyPreHandle(processedRequest, response)) {                        return;                    }                    mv = ha.handle(processedRequest, response, mappedHandler.getHandler());                    if (asyncManager.isConcurrentHandlingStarted()) {                        return;                    }                    this.applyDefaultViewName(processedRequest, mv);                    mappedHandler.applyPostHandle(processedRequest, response, mv);                }

从第一行开始debug:

processedRequest = this.checkMultipart(request);是判断请求是不是上传文件的请求。

mappedHandler = this.getHandler(processedRequest);if (mappedHandler == null) {    this.noHandlerFound(processedRequest, response);    return;}

判断是否存在映射,如果一个映射都没有,当然也没办法请求成功,就调出到请求找不到方法处理了,再往下看:

这里要看一下getHandler干了什么,步入这个方法发现,它可以获取handlerMappings,这个参数存储着请求和控制器映射的规则:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BauVQQUA-1644079490015)(C:\Users\利姆鲁\AppData\Roaming\Typora\typora-user-images\image-20220124161850954.png)]

非常重要,调用了getHandler方法:

    @Nullable    protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {        if (this.handlerMappings != null) {            Iterator var2 = this.handlerMappings.iterator();            while(var2.hasNext()) {                HandlerMapping mapping = (HandlerMapping)var2.next();                HandlerExecutionChain handler = mapping.getHandler(request);                if (handler != null) {                    return handler;                }            }        }        return null;    }

总结:

所有的请求映射都在HandlerMapping中

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

  • SpringBoot自动配置了默认 的 RequestMappingHandlerMapping

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

    • 如果有就找到这个请求对应的handler
    • 如果没有就是下一个 HandlerMapping
  • 我们需要一些自定义的映射处理,我们也可以自己给容器中放HandlerMapping。自定义 HandlerMapping

3.4、Controller中常见参数和注解的使用

一般常见的注解如下:

@PathVariable 路径携带的变量@RequestHeader 获取请求头@RequestParam 获取请求参数(指问号后的参数,url?a=1&b=2@CookieValue 获取Cookie@RequestAttribute 获取request域属性@RequestBody 获取请求体[POST]@MatrixVariable 矩阵变量,一般用不上,这里也不讲了,形式形如:/user;id=1,2,3;age=18@ModelAttribute 

先不讲注解,先说一下请求参数:

  1. 什么是请求参数:

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

    比如上面这个映射方法,user1方法的形参就是请求参数

  2. 请求参数支持三种方式传入

    • 基本类型:包括基本类型(一般使用封装的基本类型,比如用Integer而不用int)和 String 类型
    • POJO实体类:包括实体类,以及关联的实体类
    • 数组和集合类型参数:包括 List 结构和 Map 结构的集合(包括数组)
  3. 使用要求

    • 如果是基本类型或者 String 类型:

      要求我们的参数名称必须和控制器中方法的形参名称保持一致。(严格区分大小写)

    • 如果是 POJO 类型,或者它的关联对象:

      要求表单中参数名称和 POJO 类的属性名称保持一致。并且控制器方法的参数类型是 POJO 类型。

    • 如果是集合类型,有两种方式:

      • 第一种:

        要求集合类型的请求参数必须在 POJO 中。在表单中请求参数名称要和 POJO 中集合属性名称相同。
        给 List 集合中的元素赋值,使用下标。
        给 Map 集合中的元素赋值,使用键值对。

      • 第二种:

        接收的请求参数是 json 格式数据。需要借助一个注解实现。

先说一下我们原生Javaweb是怎么传参的,我们一般是借助HttpServletRequest对象来操作的,实例操作如下:

前端:

<a href="userr">点我</a>

controller:

    @GetMapping("/userr")    public Map<String,Object> getAttr1(            HttpServletRequest request){        String _id = request.getParameter("id");        String _age = request.getParameter("age");        Map<String,Object> map = new HashMap<>();        map.put("id",_id);        map.put("age",_age);        return map;    }

url输入:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8DTmu4dP-1644079490015)(C:\Users\利姆鲁\AppData\Roaming\Typora\typora-user-images\image-20220124225617763.png)]

运行结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IvqNrbKT-1644079490016)(C:\Users\利姆鲁\AppData\Roaming\Typora\typora-user-images\image-20220124225627325.png)]

那么到SpringBoot,我们不再在路径中携带参数名和参数值了,我们使用rest风格就可以完成,操作演示如下,包括对注解的使用:

前端:

<a href="rest">点我</a>

controller:

    @GetMapping("/rest/{id}/{age}/{name}")    public Map<String,Object> getAttr2(            @PathVariable("id") Integer id,            @PathVariable("age") Integer age,            @PathVariable("name") String name,            @PathVariable Map<String,String> user,            @RequestHeader("User-Agent") String user_agent,            @RequestHeader Map<String,String> header,            @RequestParam("hobby") String hobby,            @RequestParam("sex") String sex                ){        //查看路径传参        Map<String,Object> map = new HashMap<>();        map.put("id",id);        map.put("age",age);        map.put("name",name);        map.put("user",user);        //查看头部信息        map.put("user_agent",user_agent);        map.put("header",header);        //传统路径带参数        map.put("hobby",hobby);        map.put("sex",sex);        return map;    }

不作测试,有兴趣直接拿代码跑一下就好。

4、

5、

6、拦截器

拦截器在原生javaweb我们用filter来做,在SpringBoot中我们用接口HandlerInterceptor来实现即可。

6.1、编写自己的HandlerInterceptor实现类
public class LoginInterceptor implements HandlerInterceptor {    //在目标方法执行前    @Override    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {        //获得请求的uri路径        String requestURI = request.getRequestURI();        //获得session对象        HttpSession session = request.getSession();        //获得已登录的用户名        Object loginUserName = session.getAttribute("loginUserName");        //如果用户存在,则进入请求的页面,如果用户不存在,则返回原来的页面        if(loginUserName!=null){            return true;        }else{            request.getRequestDispatcher("/").forward(request,response);            return false;        }    }    //在目标方法执行后    @Override    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {        HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);    }    //页面渲染之后    @Override    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {        HandlerInterceptor.super.afterCompletion(request, response, handler, ex);    }}
6.2、注册为WebMvcConfigurer实现的组件

注意拦截规则的编写

@Configurationpublic class LoginWebConfig implements WebMvcConfigurer {    @Override    public void addInterceptors(InterceptorRegistry registry) {        registry.addInterceptor(new LoginInterceptor())        .addPathPatterns("/**")//拦截所有的请求路径        .excludePathPatterns("/","/login","/css/**","/js/**","/images/**","")//拦截排除这里面的路径,一般不拦截登录页和静态资源        ;    }}
6.3、拦截器的原理

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

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

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

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

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

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

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

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

img

7、文件上传

7.1、前端表单上传

必须用post方式,且要设置enctype=“multipart/form-data”,否则无法接受文件上传。

<form action="/fileupload" method="post" enctype="multipart/form-data">    <label>昵称:</label>    <input type="text" name="username" placeholder="name"/>    <input type="file" name="head" placeholder="name"/>    <input type="file" name="photos" placeholder="name" multiple/>    <input type="submit" name="submit"></form>
7.2、后端实现

主要实现文件的转存,判断文件后缀决定是否转存

    @RequestMapping("/fileupload")    public String upload(@RequestParam("username") String username,                         @RequestPart("head") MultipartFile head,                         @RequestPart("photos") MultipartFile[] photos        ) throws IOException {        System.out.println(username);        if(!head.isEmpty()){            String filename1 = head.getOriginalFilename();//获取文件名            System.out.println(filename1);            String filename = filename1.substring(filename1.lastIndexOf(".") + 1);//获取后缀            System.out.println(filename);            if("jpg".equals(filename)){                head.transferTo(new File("D:\\chickenVideo\\"+head.getOriginalFilename()));//转存            }        }        System.out.println("=========");        for (MultipartFile photo : photos) {            if(!photo.isEmpty()){                String filename1 = photo.getOriginalFilename();                System.out.println(filename1);                String filename = filename1.substring(filename1.lastIndexOf(".") + 1);                System.out.println(filename);                if("jpg".equals(filename)){                    photo.transferTo(new File("D:\\chickenVideo\\"+photo.getOriginalFilename()));                }            }        }        return "upload";    }
7.3、文件上传配置

进入MultipartAutoConfiguration文件上传配置类,发现有一个文件上传解析器,但这里不需要讲到,我们直接进入MultipartProperties配置类,发现配置前缀为:spring.servlet.multipart,且默认配置有:

    private boolean enabled = true;    private String location;    private DataSize maxFileSize = DataSize.ofMegabytes(1L);    private DataSize maxRequestSize = DataSize.ofMegabytes(10L);    private DataSize fileSizeThreshold = DataSize.ofBytes(0L);    private boolean resolveLazily = false;

可以看到,文件上传是默认开启的enabled,最大单文件大小是1MB,最大请求文件(多文件)大小是10MB,fileSizeThreshold是缓存的临界点,超过则保存在临时文件,location是临时文件的路径。

所以我们可以配置它的最大文件,来对文件进行限制:

spring:  servlet:    multipart:      max-file-size: 10      max-request-size: 100
7.4、文件上传原理分析

8、错误处理

8.1、错误处理的规则
  • 默认情况下,Spring Boot提供/error处理所有错误的映射

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

    机器客户端如下:

img

web页面端如下:

img

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

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

  • error/目录下的4xx,5xx页面会被自动解析

    如下:

  • img

8.2、自定义错误处理
8.3、错误处理自动配置原理

9、如何注入Web原生组件(servlet、filter、listener)

有两种方式

9.1、使用Servlet API

只要正常编写相应继承类即可,然后需要添加对应注解:

如:

在主程序中:

@ServletComponentScan(basePackages = “com.xiafan”) 😕/指定原生Servlet组件都放在那里

在我们编写的程序中:

@WebServlet(urlPatterns = “/xf”)://效果:直接响应,没有经过Spring的拦截器?

@WebFilter(urlPatterns={“/css/*”,“/images/*”})

@WebListener

9.2、使用RegistrationBean

其实就是bean注入的方式,如下:

@Configurationpublic class MyRegistConfig {    @Bean    public ServletRegistrationBean myServlet(){        MyServlet myServlet = new MyServlet();        return new ServletRegistrationBean(myServlet,"/my","/my02");    }    @Bean    public FilterRegistrationBean myFilter(){        MyFilter myFilter = new MyFilter();//        return new FilterRegistrationBean(myFilter,myServlet());        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(myFilter);        filterRegistrationBean.setUrlPatterns(Arrays.asList("/my","/css/*"));        return filterRegistrationBean;    }    @Bean    public ServletListenerRegistrationBean myListener(){        MySwervletContextListener mySwervletContextListener = new MySwervletContextListener();        return new ServletListenerRegistrationBean(mySwervletContextListener);    }}
9.3、DispatchServlet是如何注册进来的

我们进入DispatcherServletAutoConfiguration自动装配类来看一下。

我们发现DispatchServlet被注入到了容器中:

        @Bean(            name = {"dispatcherServlet"}        )        public DispatcherServlet dispatcherServlet(WebMvcProperties webMvcProperties) {            DispatcherServlet dispatcherServlet = new DispatcherServlet();            dispatcherServlet.setDispatchOptionsRequest(webMvcProperties.isDispatchOptionsRequest());            dispatcherServlet.setDispatchTraceRequest(webMvcProperties.isDispatchTraceRequest());            dispatcherServlet.setThrowExceptionIfNoHandlerFound(webMvcProperties.isThrowExceptionIfNoHandlerFound());            dispatcherServlet.setPublishEvents(webMvcProperties.isPublishRequestHandledEvents());            dispatcherServlet.setEnableLoggingRequestDetails(webMvcProperties.isLogRequestDetails());            return dispatcherServlet;        }

而且它还被作为servlet被注册到了tomcat中:

        @Bean(            name = {"dispatcherServletRegistration"}        )        @ConditionalOnBean(            value = {DispatcherServlet.class},            name = {"dispatcherServlet"}        )        public DispatcherServletRegistrationBean dispatcherServletRegistration(DispatcherServlet dispatcherServlet, WebMvcProperties webMvcProperties, ObjectProvider<MultipartConfigElement> multipartConfig) {            DispatcherServletRegistrationBean registration = new DispatcherServletRegistrationBean(dispatcherServlet, webMvcProperties.getServlet().getPath());            registration.setName("dispatcherServlet");            registration.setLoadOnStartup(webMvcProperties.getServlet().getLoadOnStartup());            multipartConfig.ifAvailable(registration::setMultipartConfig);            return registration;        }
9.4、servlet的处理原则

多个Servlet都能处理到同一层路径,精确优选原则

什么是精确优选原则?

比如:

Aservlet能处理”/“

Bservlet能处理“/login”

Cservlet能处理“/login/error”

那么当“/login”路径请求发起的时候,经过的是Bservlet而不是另外两个servlet,因为B的处理路径“/login”是最精确路径,同理,当“/login/error”路径请求发起的时候,经过的是Cservlet而不是另外两个servlet

10、嵌入servlet服务器

11、定制化原理

11.1、定制化方法
  1. 直接在配置文件中修改默认配置

  2. 利用xxxxCustomizer定制化器,来改变某个自动配置类的组件属性,作用类似与使用配置文件,如:

    @Componentpublic class CustomizationBean implements WebServerFactoryCustomizer<ConfigurableServletWebServerFactory> {    @Override    public void customize(ConfigurableServletWebServerFactory server) {        server.setPort(9000);    }}
    
  3. 很重要的一种,也是我们经常用的:编写一个配置类实现 WebMvcConfigurer 即可定制化web功能;+ @Bean给容器中再扩展一些组件,比如我们要添加一个自定义的SpringBoot拦截器就使用过这样的方式,这种方式可以在SpringBoot已有的组件基础上再添加上我们自己想要的组件,实现一个拓展的效果

  4. 这种方式可以全面接管SpringMVC:@EnableWebMvc(全面接管) + WebMvcConfigurer(继承MVC组件类)+@Bean(向容器中放入组件),除了最底层最基本的功能还是由SpringBoot自己编写外,其余的组件都可以由我们的实际情况来定制和扩展,相当于SpringBoot给你一个壳,我们自己编写里面的肉

    • 原理:我们知道,MVC的所有组件都是由WebMvcAutoConfiguration自动装配的,包括静态资源、欢迎页等规则,但如果有一个类使用了注解@EnableWebMvc,由于@EnableWebMvc本身导入了@Import(DelegatingWebMvcConfiguration.class),而DelegatingWebMvcConfiguration类是继承WebMvcConfigurationSupport的(public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport),我们进入WebMvcAutoConfiguration发现,WebMvcAutoConfiguration的条件装配为@ConditionalOnMissingBean(WebMvcConfigurationSupport.class),所以一旦出现一个WebMvcConfigurationSupport的子类,WebMvcAutoConfiguration就会失效而没有自动装配,从而使得IOC中只有我们自己写的WebMvcConfigurer子类而已,所以MVC中原本有的组件都需要我们自己重新编写(如果用不上也可以不编写)
11.2、SpringBoot的原理分析套路
  1. 引入场景starter
  2. 某个功能就会对应它的xxxxAutoConfiguration自动装配类
  3. 自动装配类里面就会导入xxxx组件
  4. 组件中的属性都会绑定xxxxProperties文件
  5. xxxxProperties文件会绑定到配置文件,我们只要对应修改配置文件中的项即可修改默认配置

我们在深究原理的时候可以按上面从第一步到第五步来探讨,而如果不需要探究原理的时候,我们只需要引入相关的场景,然后查看可以配置什么样的项或者查看我们想修改的属性如何修改即可使用了

十一、SpringBoot在数据访问场景的使用

SQL场景:

使用的框架和数据库版本使用SpringBoot自带的版本仲裁中的即可

1、JDBC和MySQL

1.1、场景引入

可以在IDEA创建项目的时候直接引入,也可以在之后依赖引入:

		<dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter-data-jdbc</artifactId>        </dependency>		<dependency>            <groupId>mysql</groupId>            <artifactId>mysql-connector-java</artifactId>        </dependency>
1.2、分析JDBC场景包中的自动配置类
1.2.1、DataSourceAutoConfiguration 数据源自动配置类

可以看到它绑定的配置文件为:@EnableConfigurationProperties({DataSourceProperties.class}),进入该配置类发现,配置的前缀为:prefix = “spring.datasource”。

可以看到它自带的连接池组件:

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

我们随便进入一个导入类,可以看到它的导入类都来自一个叫DataSourceConfiguration的类,然后我们发现,这里面只有一个组件是开启了的:Hikari,而其他的组件是需要引入特定的类的,所以Hikari就是我们SpringBoot默认的数据源。接下来我们分析默认连接池:

    @Configuration(        proxyBeanMethods = false    )    @ConditionalOnClass({HikariDataSource.class})    @ConditionalOnMissingBean({DataSource.class})    @ConditionalOnProperty(        name = {"spring.datasource.type"},        havingValue = "com.zaxxer.hikari.HikariDataSource",        matchIfMissing = true    )    static class Hikari {        Hikari() {        }        @Bean        @ConfigurationProperties(            prefix = "spring.datasource.hikari"        )        HikariDataSource dataSource(DataSourceProperties properties) {            HikariDataSource dataSource = (HikariDataSource)DataSourceConfiguration.createDataSource(properties, HikariDataSource.class);            if (StringUtils.hasText(properties.getName())) {                dataSource.setPoolName(properties.getName());            }            return dataSource;        }    }

@ConditionalOnClass({HikariDataSource.class})
@ConditionalOnMissingBean({DataSource.class})

这两个条件装配组件就可以看出来,只有当存在HikariDataSource且不存在DataSource的时候才会注入这个组件。

接下来看属性条件装配:

    @ConditionalOnProperty(        name = {"spring.datasource.type"},        havingValue = "com.zaxxer.hikari.HikariDataSource",        matchIfMissing = true    )

可以看出,如果我们没有引入多个值的话,spring.datasource.type属性的值是不需要自己配置的,它默认就是com.zaxxer.hikari.HikariDataSource了,当然我们配置了也无所谓,可以使得配置文件更健壮。

创建数据源的方法是一个泛型方法:

    protected static <T> T createDataSource(DataSourceProperties properties, Class<? extends DataSource> type) {        return properties.initializeDataSourceBuilder().type(type).build();    }//对应HikariDataSource dataSource = (HikariDataSource)DataSourceConfiguration.createDataSource(properties, HikariDataSource.class);

进入DataSourceProperties可以看到可以配置的属性:

    private ClassLoader classLoader;    private boolean generateUniqueName = true;    private String name;    private Class<? extends DataSource> type;    private String driverClassName;    private String url;    private String username;    private String password;    private String jndiName;

然后就可以修改配置文件的相关配置了:

正常的四项配置:username、password、url、driverClassName,也可以选择数据源类型:type。

1.2.2、DataSourceTransactionManagerAutoConfiguration 事务管理器自动配置
1.2.3、JdbcTemplateAutoConfiguration 管理JdbcTemplate的自动配置

JdbcTemplate就是用来操作数据库CRUD的。

这个类非常清爽,只是作为一个壳而已,重点是导入类和配置绑定类:

@Configuration(    proxyBeanMethods = false)@ConditionalOnClass({DataSource.class, JdbcTemplate.class})@ConditionalOnSingleCandidate(DataSource.class)@AutoConfigureAfter({DataSourceAutoConfiguration.class})@EnableConfigurationProperties({JdbcProperties.class})@Import({DatabaseInitializationDependencyConfigurer.class, JdbcTemplateConfiguration.class, NamedParameterJdbcTemplateConfiguration.class})public class JdbcTemplateAutoConfiguration {    public JdbcTemplateAutoConfiguration() {    }}

进入配置类,可以看到配置文件的配置前缀:

@ConfigurationProperties(    prefix = "spring.jdbc")

还可以看到它可以配置的三个属性:

        private int fetchSize = -1;        private int maxRows = -1;        @DurationUnit(ChronoUnit.SECONDS)        private Duration queryTimeout;//超时属性,单位为秒,超过配置的时间就认为查询超时

我们进入JdbcTemplate类看一下,是怎么封装查询的,个人比较好奇,想看一下:

比如我们很常使用的查询成列表的方法,我们进来发现一共有三个多态方法:

    public <T> List<T> queryForList(String sql, Class<T> elementType, @Nullable Object... args) throws DataAccessException {        return this.query(sql, args, this.getSingleColumnRowMapper(elementType));    }    public List<Map<String, Object>> queryForList(String sql, Object[] args, int[] argTypes) throws DataAccessException {        return this.query(sql, args, argTypes, this.getColumnMapRowMapper());    }    public List<Map<String, Object>> queryForList(String sql, @Nullable Object... args) throws DataAccessException {        return this.query(sql, args, this.getColumnMapRowMapper());    }

我们发现他们都调用了query方法,且发现query方法也是多态的,我们研究一下,选取第二个方法来研究它对应的query方法:

    public <T> List<T> query(String sql, Object[] args, int[] argTypes, RowMapper<T> rowMapper) throws DataAccessException {        return (List)result(this.query(sql, args, argTypes, (ResultSetExtractor)(new RowMapperResultSetExtractor(rowMapper))));    }

但是发现它又嵌套了一个query方法,并用result方法封装了结果(…,那我们看一下再下一层的query方法):

    @Nullable    public <T> T query(String sql, Object[] args, int[] argTypes, ResultSetExtractor<T> rse) throws DataAccessException {        return this.query(sql, this.newArgTypePreparedStatementSetter(args, argTypes), rse);    }

…又调用了下一层query方法,那我们继续…,这次我们不再嵌套了,一次点到最底层的query方法:

    @Nullable    public <T> T query(PreparedStatementCreator psc, @Nullable final PreparedStatementSetter pss, final ResultSetExtractor<T> rse) throws DataAccessException {        Assert.notNull(rse, "ResultSetExtractor must not be null");        this.logger.debug("Executing prepared SQL query");        return this.execute(psc, new PreparedStatementCallback<T>() {            @Nullable            public T doInPreparedStatement(PreparedStatement ps) throws SQLException {                ResultSet rs = null;                Object var3;                try {                    if (pss != null) {                        pss.setValues(ps);                    }                    rs = ps.executeQuery();                    var3 = rse.extractData(rs);                } finally {                    JdbcUtils.closeResultSet(rs);                    if (pss instanceof ParameterDisposer) {                        ((ParameterDisposer)pss).cleanupParameters();                    }                }                return var3;            }        }, true);    }

可以看到这里的主要是一个返回值,返回值的参数中第一个参数是一个PreparedStatementCreator对象,就当它创建了一个预编译Statement对象,第二个参数是函数式接口,第三个参数是true,主要关注这个:

                    if (pss != null) {                        pss.setValues(ps);                    }                    rs = ps.executeQuery();                    var3 = rse.extractData(rs);

可以看到它实现的是,像一个指定的PreparedStatement对象注入值,然后执行这个sql,最后得到resultset进行提取封装,我们再进去execute方法看一下:

    @Nullable    private <T> T execute(PreparedStatementCreator psc, PreparedStatementCallback<T> action, boolean closeResources) throws DataAccessException {        Assert.notNull(psc, "PreparedStatementCreator must not be null");        Assert.notNull(action, "Callback object must not be null");        if (this.logger.isDebugEnabled()) {            String sql = getSql(psc);            this.logger.debug("Executing prepared SQL statement" + (sql != null ? " [" + sql + "]" : ""));        }        Connection con = DataSourceUtils.getConnection(this.obtainDataSource());        PreparedStatement ps = null;        Object var14;        try {            ps = psc.createPreparedStatement(con);            this.applyStatementSettings(ps);            T result = action.doInPreparedStatement(ps);            this.handleWarnings((Statement)ps);            var14 = result;        } catch (SQLException var11) {            if (psc instanceof ParameterDisposer) {                ((ParameterDisposer)psc).cleanupParameters();            }            String sql = getSql(psc);            psc = null;            JdbcUtils.closeStatement(ps);            ps = null;            DataSourceUtils.releaseConnection(con, this.getDataSource());            con = null;            throw this.translateException("PreparedStatementCallback", sql, var11);        } finally {            if (closeResources) {                if (psc instanceof ParameterDisposer) {                    ((ParameterDisposer)psc).cleanupParameters();                }                JdbcUtils.closeStatement(ps);                DataSourceUtils.releaseConnection(con, this.getDataSource());            }        }        return var14;    }

重点关注这里:

            ps = psc.createPreparedStatement(con);            this.applyStatementSettings(ps);            T result = action.doInPreparedStatement(ps);            this.handleWarnings((Statement)ps);            var14 = result;

可以看到我们的得到结果的关键是doInPreparedStatement(ps);方法,再进入这个方法的实现中发现:

            this.setValues(ps, lobCreator);            var4 = ps.executeUpdate();

主要就是把值注入预编译对象,然后执行并返回数据即可,在深入到JDBC的封装。

实操:

  1. 配置配置文件:

    spring:  datasource:    username: root    password: 123456    #?serverTimezone=UTC解决时区的报错    url: jdbc:mysql://localhost:3306/springboot?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8    driver-class-name: com.mysql.cj.jdbc.Driver  jdbc:    template:      query-timeout: 3
    
  2. 测试类:

        @Autowired    JdbcTemplate jdbcTemplate;    @Test    void contextLoads() {        Integer userNum = jdbcTemplate.queryForObject("select count(*) from employee", Integer.class);        System.out.println(userNum);    }
    
1.2.4、JndiDataSourceAutoConfiguration jndi的自动配置
1.2.5、XADataSourceAutoConfiguration 分布式事务相关的自动动配置

2、整合Druid数据源(也就是我们经常说的德鲁伊)

Druid的github官方地址,包含文档

https://github.com/alibaba/druid

2.1、第一种场景引入:导入依赖 + 组件配置注入
  1. 导入依赖

            <dependency>            <groupId>com.alibaba</groupId>            <artifactId>druid</artifactId>            <version>1.1.17</version>        </dependency>
    
  2. 组件配置

    @Configurationpublic class DruidConfig {    @ConfigurationProperties(            prefix = "spring.datasource"    )//绑定配置文件的相关配置    @Bean//注入为bean    public DataSource druidDataSource(){        return new DruidDataSource();    }}
    
  3. 测试,看一下现在的数据源是什么

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2PQq7zhg-1644079490019)(C:\Users\利姆鲁\AppData\Roaming\Typora\typora-user-images\image-20220130165337240.png)]

    为什么我们什么都没做,只是注入bean就自动换了数据源了,我们回头看一下,所有自带的数据源都有一个注解:@ConditionalOnMissingBean({DataSource.class}),而此时我们注入的bean就是DataSource的子类,所以自带的数据源都不会自动装配注入,只注入了我们自己引入的数据源

2.2、第二种场景引入:starter引入

引入场景依赖:

        <dependency>            <groupId>com.alibaba</groupId>            <artifactId>druid-spring-boot-starter</artifactId>            <version>1.1.17</version>        </dependency>
2.3、开启监控功能

回忆一下SSM中我们是如何引入的:

	<servlet>		<servlet-name>DruidStatView</servlet-name>		<servlet-class>com.alibaba.druid.support.http.StatViewServlet</servlet-class>	</servlet>	<servlet-mapping>		<servlet-name>DruidStatView</servlet-name>		<url-pattern>/druid/*</url-pattern>	</servlet-mapping>

说白了,我们就是引入了一个叫StatViewServlet的servlet,在上面我们已经学过了,如何注入一个我们自己编写的servlet并注入SpringBoot中了,所以接下来说一下怎么引入:

还是这个配置类,我们增加一个bean,这个bean的返回值是ServletRegistrationBean类,翻译一下就是servlet的注册bean类了,看一下如何注入,它有一个构造方法:

    public ServletRegistrationBean(T servlet, String... urlMappings) {        this(servlet, true, urlMappings);    }

这里不关心它另一个实现功能的构造方法了,我们知道需要放入一个servlet和一个路径映射即可,那么我们就这么做就好了,最后的组件为:

    @Bean    public ServletRegistrationBean statViewServlet(){        StatViewServlet statViewServlet = new StatViewServlet();        ServletRegistrationBean<StatViewServlet> servletRegistrationBean = new ServletRegistrationBean(statViewServlet,"/druid/*");        return servletRegistrationBean;    }

此时访问http://localhost:8080/druid就可以进入如下页面了

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4B2zHGZq-1644079490019)(C:\Users\利姆鲁\AppData\Roaming\Typora\typora-user-images\image-20220130172244934.png)]

2.4、添加SQL监控(stat)和防火墙功能(wall)
    @ConfigurationProperties(            prefix = "spring.datasource"    )//绑定配置文件的相关配置    @Bean//注入为bean    public DataSource druidDataSource() throws SQLException {        DruidDataSource dataSource = new DruidDataSource();        dataSource.setFilters("stat,wall");        return dataSource;    }

也就是添加dataSource.setFilters(“stat”);即可

2.5、添加WEB监控

就是增加一个叫WebStatFilter的过滤器即可

    @Bean    public FilterRegistrationBean webStatFilter(){        WebStatFilter webStatFilter = new WebStatFilter();        FilterRegistrationBean<WebStatFilter> filterFilterRegistrationBean = new FilterRegistrationBean<>(webStatFilter);        //设置过滤路径,设置过滤所有路径        filterFilterRegistrationBean.setUrlPatterns(Arrays.asList("/*"));        //排除静态资源路径和某些不需要过滤的路径        filterFilterRegistrationBean.addInitParameter("exclusion","*.js,*.css,*.jpg");        return filterFilterRegistrationBean;    }

3、整合mybatis

3.1、导入场景
        <dependency>            <groupId>org.mybatis.spring.boot</groupId>            <artifactId>mybatis-spring-boot-starter</artifactId>            <version>2.2.1</version>        </dependency>
3.2、自动配置情况

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TXIxsWM4-1644079490020)(C:\Users\利姆鲁\AppData\Roaming\Typora\typora-user-images\image-20220131210825099.png)]

我们看一下自动配置工厂,看到如下配置:

# Auto Configureorg.springframework.boot.autoconfigure.EnableAutoConfiguration=\org.mybatis.spring.boot.autoconfigure.MybatisLanguageDriverAutoConfiguration,\org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration

所以MybatisAutoConfiguration和MybatisLanguageDriverAutoConfiguration被自动装配了。

我们看一下MybatisAutoConfiguration类的注解

@Configuration@ConditionalOnClass({SqlSessionFactory.class, SqlSessionFactoryBean.class})@ConditionalOnSingleCandidate(DataSource.class)@EnableConfigurationProperties({MybatisProperties.class})@AutoConfigureAfter({DataSourceAutoConfiguration.class, MybatisLanguageDriverAutoConfiguration.class})public class MybatisAutoConfiguration implements InitializingBean{    ...}

条件装配需要SqlSessionFactory和SqlSessionFactoryBean存在,我们发现这两个类在导入的mybatis中已经有了,所以没有问题。

还需要有且仅有一个DataSource,这个自己配置好即可。

看一下MybatisProperties配置类:

    public static final String MYBATIS_PREFIX = "mybatis";    private static final ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();    private String configLocation;    private String[] mapperLocations;    private String typeAliasesPackage;    private Class<?> typeAliasesSuperType;    private String typeHandlersPackage;    private boolean checkConfigLocation = false;    private ExecutorType executorType;    private Class<? extends LanguageDriver> defaultScriptingLanguageDriver;    private Properties configurationProperties;    @NestedConfigurationProperty    private Configuration configuration;

比如mapperLocations配置路径等。

3.3、使用前的准备

回忆一下SSM整合的时候mybatis需要做什么,并反应到SpringBoot来做:

  1. 要配DataSource

    SpringBoot:这里是使用我们已经配好的DataSource所以可以省略

  2. 要向xml文件注入SqlSessionFactory和SqlSession

    SpringBoot:导入场景的时候已经自带了SqlSessionFactory,而且还自带了SqlSessionTemplate,而SqlSessionTemplate就封装了SqlSession了,如下:

    public class SqlSessionTemplate implements SqlSession, DisposableBean {    private final SqlSessionFactory sqlSessionFactory;    private final ExecutorType executorType;    private final SqlSession sqlSessionProxy;    ...
    
  3. 配置mapper

    SpringBoot:只要我们需要操作mybatis的类上添加了*@Mapper*注解即可。这里可以在MybatisAutoConfiguration中找到

        @Configuration    @Import({MybatisAutoConfiguration.AutoConfiguredMapperScannerRegistrar.class})    @ConditionalOnMissingBean({MapperFactoryBean.class, MapperScannerConfigurer.class})    public static class MapperScannerRegistrarNotFoundConfiguration implements InitializingBean {        public MapperScannerRegistrarNotFoundConfiguration() {        }        public void afterPropertiesSet() {            MybatisAutoConfiguration.logger.debug("Not found configuration for registering mapper bean using @MapperScan, MapperFactoryBean and MapperScannerConfigurer.");        }    }    	public static class AutoConfiguredMapperScannerRegistrar implements BeanFactoryAware, ImportBeanDefinitionRegistrar {        private BeanFactory beanFactory;        public AutoConfiguredMapperScannerRegistrar() {        }        public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {            if (!AutoConfigurationPackages.has(this.beanFactory)) {                MybatisAutoConfiguration.logger.debug("Could not determine auto-configuration package, automatic mapper scanning disabled.");            } else {                MybatisAutoConfiguration.logger.debug("Searching for mappers annotated with @Mapper");                List<String> packages = AutoConfigurationPackages.get(this.beanFactory);                if (MybatisAutoConfiguration.logger.isDebugEnabled()) {                    packages.forEach((pkg) -> {                        MybatisAutoConfiguration.logger.debug("Using auto-configuration base package '{}'", pkg);                    });                }                BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class);                builder.addPropertyValue("processPropertyPlaceHolders", true);                builder.addPropertyValue("annotationClass", Mapper.class);                builder.addPropertyValue("basePackage", StringUtils.collectionToCommaDelimitedString(packages));                BeanWrapper beanWrapper = new BeanWrapperImpl(MapperScannerConfigurer.class);                Set<String> propertyNames = (Set)Stream.of(beanWrapper.getPropertyDescriptors()).map(FeatureDescriptor::getName).collect(Collectors.toSet());                if (propertyNames.contains("lazyInitialization")) {                    builder.addPropertyValue("lazyInitialization", "${mybatis.lazy-initialization:false}");                }                if (propertyNames.contains("defaultScope")) {                    builder.addPropertyValue("defaultScope", "${mybatis.mapper-default-scope:}");                }                registry.registerBeanDefinition(MapperScannerConfigurer.class.getName(), builder.getBeanDefinition());            }        }        public void setBeanFactory(BeanFactory beanFactory) {            this.beanFactory = beanFactory;        }    }
    
3.4、第一种mybatis的整合:使用配置文件
3.5、第二种mybatis的整合:使用注解

4、整合mybatis-plus

这里主要说一下自动配置的东西,具体怎么引入和使用直接看我mybatis-plus的文章即可。

这里也不去找源码了,现在应该很容易就能找到了,直接找MybatisPlusAutoConfiguration 配置类和MybatisPlusProperties 配置项绑定,然后查看相应代码即可。

总结:

  • 配置前缀为:mybatis-plus

  • SqlSessionFactory 自动配置好。底层是容器中默认的数据源

  • **mapperLocations 自动配置好的。有默认值。**classpath*:/mapper/**/*.xml;任意包的类路径下的所有mapper文件夹下任意路径下的所有xml都是sql映射文件。 建议以后sql映射文件,放在 mapper下

  • 容器中也自动配置了SqlSessionTemplate

  • @Mapper 标注的接口也会被自动扫描;建议直接 @MapperScan(“com.atguigu.admin.mapper”) 批量扫描就行

NOSQL场景:

5、整合Redis

十一、单元测试

1、简介JUnit5

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

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

JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage

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

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

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

img

注意:

SpringBoot 2.4 以上版本移除了默认对 Vintage 的依赖。如果需要兼容junit4需要自行引入(不能使用junit4的功能 @Test**)**

我们Spring时期使用的是JUnit4

2、JUnit5的使用

  1. 编写测试方法:@Test标注(注意需要使用junit5版本的注解)
  2. Junit类具有Spring的功能,@Autowired、比如 @Transactional 标注测试方法,测试完成后自动回滚

3、JUnit5常用注解

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

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

  • **@Test 😗*表示方法是测试方法。但是与JUnit4的@Test不同,他的职责非常单一不能声明任何属性,拓展的测试将会由Jupiter提供额外测试

    添加该注解即可使用测试功能:

    代码:

        @Test    void test1(){        System.out.println("first test");    }
    

    运行:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xHwCndsy-1644079490021)(C:\Users\利姆鲁\AppData\Roaming\Typora\typora-user-images\image-20220201234935940.png)]

  • **@ParameterizedTest 😗*表示方法是参数化测试,下方会有详细介绍

  • **@RepeatedTest 😗*表示方法可重复执行,下方会有详细介绍

  • **@DisplayName 😗*为测试类或者测试方法设置展示名称

    添加该注解可给测试类或测试方法添加一个名字,用于展示:

    代码:

        @DisplayName("夏帆的第一个测试")    @Test    void test1(){        System.out.println("first test");    }
    

    运行:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ECSaeRHW-1644079490021)(C:\Users\利姆鲁\AppData\Roaming\Typora\typora-user-images\image-20220201235049894.png)]

  • **@BeforeEach 😗*表示在每个单元测试之前执行

  • **@AfterEach 😗*表示在每个单元测试之后执行

    BeforeEach 和AfterEach 一起测试,重点是每个,也就是每个测试方法前后都会执行:

    代码:

        @BeforeEach    void before1(){        System.out.println("我是每次运行测试前都要运行的方法啦");    }    @AfterEach    void after1(){        System.out.println("我是每次运行测试后都要运行的方法啦");    }    @DisplayName("夏帆的第一个测试")    @Test    void test1(){        System.out.println("first test");    }
    

    运行:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Pwrva6IJ-1644079490022)(C:\Users\利姆鲁\AppData\Roaming\Typora\typora-user-images\image-20220202000700734.png)]

  • **@BeforeAll 😗*表示在所有单元测试之前执行

  • **@AfterAll 😗*表示在所有单元测试之后执行

    BeforeAll 和AfterAll 一起测试,重点是所有,这是我们启动整个测试类的时候只在一开始和整个测试类结束才会执行的:

    代码:

        @BeforeEach    void before1(){        System.out.println("我是每次运行测试前都要运行的方法啦");    }    @AfterEach    void after1(){        System.out.println("我是每次运行测试后都要运行的方法啦");    }    @BeforeAll    static void before2(){        System.out.println("我是所有测试前都要运行的方法啦");    }    @AfterAll    static void after2(){        System.out.println("我是所有测试后都要运行的方法啦");    }    @DisplayName("夏帆的第一个测试")    @Test    void test1(){        System.out.println("first test");    }    @DisplayName("夏帆的第二个测试")    @Test    void test2(){        System.out.println("second test");    }
    

    运行:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1D3npZLy-1644079490022)(C:\Users\利姆鲁\AppData\Roaming\Typora\typora-user-images\image-20220202001756544.png)]

  • **@Tag 😗*表示单元测试类别,类似于JUnit4中的@Categories

  • **@Disabled 😗*表示测试类或测试方法不执行,类似于JUnit4中的@Ignore

    忽略这个测试方法,也就是不会执行它:

        @Disabled    @DisplayName("夏帆的第一个测试")    @Test    void test1(){        System.out.println("first test");    }
    
  • **@Timeout 😗*表示测试方法运行如果超过了指定时间将会返回错误

    设定一个时间,测试方法运行时间超过设定的时间就返回错误:

    代码:

        @Timeout(value = 300,unit = MILLISECONDS)    @Test    void test3() throws InterruptedException {       Thread.sleep(500);    }
    

    运行:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3gMuSTYf-1644079490023)(C:\Users\利姆鲁\AppData\Roaming\Typora\typora-user-images\image-20220202002143471.png)]

  • **@ExtendWith 😗*为测试类或测试方法提供扩展类引用

    相当于JUnit4的RunWith,SpringBootTest注解里面就自带引入了扩展@ExtendWith({SpringExtension.class})

    @ExtendWith({SpringExtension.class})public @interface SpringBootTest
    

4、断言机制Assertions

断言听起来很不像人话,压根不知道是干嘛的,其实就是帮助我们判断测试结果是否符合我们预期的东西,像以前我们可能把结果打印出来看一下是怎么样的,现在我们可以使用断言来帮助我们直接判断,而不需要输出出来看了,仅此而已。

4.1、简单断言

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

方法说明
assertEquals判断两个对象或两个原始类型是否相等
assertNotEquals判断两个对象或两个原始类型是否不相等
assertSame判断两个对象引用是否指向同一个对象
assertNotSame判断两个对象引用是否指向不同的对象
assertTrue判断给定的布尔值是否为 true
assertFalse判断给定的布尔值是否为 false
assertNull判断给定的对象引用是否为 null
assertNotNull判断给定的对象引用是否不为 null

注意:一个方法运行中有多个断言的时候,当有一个断言出现错误之后,后面的所有代码都不会再运行了,有点像编译错误

这里就拿一个断言演示就好了,都很简单易懂的,这里拿assertEquals举例:

@Testpublic void equal(){	assertEquals(3, 1 + 2, "计算错误");	assertNotEquals(3, 1 + 1);}

可以看到有两个参数,第一个参数是预期结果,第二个参数是实际结果,第三个参数是返回错误的文本提示(可选参数)

4.2、数组断言

判断两个数组的内容是否相同

@Testpublic void array() { assertArrayEquals(new int[]{1, 2}, new int[] {1, 2});}
4.3、组合断言

多个简单断言组合形成一个大的断言,这个大断言会运行其下所有简单断言,同理,要所有简单断言都成功才会继续往下运行。

@Testpublic void all() { assertAll("我是一个组合断言", //组合断言的名字title    () -> assertEquals(2, 1 + 1),    () -> assertTrue(1 > 0) );}
4.4、异常断言

就是假定这个业务是一定会抛出这个异常的,如果不抛出说明有问题,会返回错误。

@Testpublic void exceptionTest() {	assertThrows(ArithmeticException.class,                  () -> System.out.println(1 % 0));}
4.5、超时断言

如果业务运行超过设定的时间,就会返回错误

@Testpublic void timeoutTest() {    //如果测试方法时间超过1s将会异常    Assertions.assertTimeout(Duration.ofMillis(1000), () -> Thread.sleep(500));}
4.6、快速失败

就是运行到这一行语句,无论如何都会返回失败

@Testpublic void shouldFail() {    //返回失败 	fail("This should fail");}

5、前置条件Assumptions

和断言差不多,但是断言会返回错误,前置条件不会,前置条件运行失败的时候会选择跳过这个方法,而不是返回错误。

 @Test @DisplayName("Assumptions") public void AssumptionTest() {    assumeTrue(Objects.equals(this.environment, "Assumptions"));    assumeFalse(() -> Objects.equals(this.environment, "Assumptions")); }

6、嵌套测试

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

这个要实操才可以讲的清楚,直接放代码比较麻烦,可以自行找一段代码理解

7、参数化测试

我们有一个注解还没讲过,那就是@ParameterizedTest,这个注解就是用来标注测试方法是参数化测试的注解。

参数化测试还需要增加其他方法注解来协同使用,如下:

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

8、JUnit4到JUnit5的变化

在进行迁移的时候需要注意如下的变化:

  • 注解在 org.junit.jupiter.api 包中,断言在 org.junit.jupiter.api.Assertions 类中,前置条件在 org.junit.jupiter.api.Assumptions 类中。

  • 把@Before 和@After 替换成@BeforeEach 和@AfterEach。

  • 把@BeforeClass 和@AfterClass 替换成@BeforeAll 和@AfterAll。

  • 把@Ignore 替换成@Disabled。

  • 把@Category 替换成@Tag。

  • 把@RunWith、@Rule 和@ClassRule 替换成@ExtendWith。

十二、指标监控

1、SpringBoot Actuator

1.1、简介

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

而SpringBoot Actuator就是SpringBoot整合的监控场景。

1.2、依赖
        <dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter-actuator</artifactId>        </dependency>
1.3、SpringBoot Actuator 1.x和2.x的区别

img

1.4、如何使用
  1. 引入SpringBoot Actuator场景

  2. 访问 http://localhost:8080/actuator/** 链接可以访问响应的监控内容

  3. 配置文件的如何配置

    management:  endpoints:    enabled-by-default: true #暴露所有端点信息    web:      exposure:        include: '*'  #以web方式暴露
    

2、Actuator Endpoint

监控端点,也就是我们想查看的监控内容

2.1、常用端点
ID描述
auditevents暴露当前应用程序的审核事件信息。需要一个AuditEventRepository组件
beans显示应用程序中所有Spring Bean的完整列表。
caches暴露可用的缓存。
conditions显示自动配置的所有条件信息,包括匹配或不匹配的原因。
configprops显示所有@ConfigurationProperties
env暴露Spring的属性ConfigurableEnvironment
flyway显示已应用的所有Flyway数据库迁移。 需要一个或多个Flyway组件。
health显示应用程序运行状况信息。
httptrace显示HTTP跟踪信息(默认情况下,最近100个HTTP请求-响应)。需要一个HttpTraceRepository组件。
info显示应用程序信息。
integrationgraph显示Spring integrationgraph 。需要依赖spring-integration-core
loggers显示和修改应用程序中日志的配置。
liquibase显示已应用的所有Liquibase数据库迁移。需要一个或多个Liquibase组件。
metrics显示当前应用程序的“指标”信息。
mappings显示所有@RequestMapping路径列表。
scheduledtasks显示应用程序中的计划任务。
sessions允许从Spring Session支持的会话存储中检索和删除用户会话。需要使用Spring Session的基于Servlet的Web应用程序。
shutdown使应用程序正常关闭。默认禁用。
startup显示由ApplicationStartup收集的启动步骤数据。需要使用SpringApplication进行配置BufferingApplicationStartup
threaddump执行线程转储。

如果您的应用程序是Web应用程序(Spring MVC,Spring WebFlux或Jersey),则可以使用以下附加端点:

ID描述
heapdump返回hprof堆转储文件。
jolokia通过HTTP暴露JMX bean(需要引入Jolokia,不适用于WebFlux)。需要引入依赖jolokia-core
logfile返回日志文件的内容(如果已设置logging.file.namelogging.file.path属性)。支持使用HTTPRange标头来检索部分日志文件的内容。
prometheus以Prometheus服务器可以抓取的格式公开指标。需要依赖micrometer-registry-prometheus

其中,最常用的三个监控内容是:

  • Health:监控状况

  • Metrics:运行时指标

  • Loggers:日志记录

2.2、默认暴露端点

支持的暴露方式

  • HTTP:默认只暴露healthinfo Endpoint

  • JMX:默认暴露所有Endpoint

  • 除过health和info,剩下的Endpoint都应该进行保护访问。如果引入SpringSecurity,则会默认配置安全访问规则

IDJMXWeb
auditeventsYesNo
beansYesNo
cachesYesNo
conditionsYesNo
configpropsYesNo
envYesNo
flywayYesNo
healthYesYes
heapdumpN/ANo
httptraceYesNo
infoYesYes
integrationgraphYesNo
jolokiaN/ANo
logfileN/ANo
loggersYesNo
liquibaseYesNo
metricsYesNo
mappingsYesNo
prometheusN/ANo
scheduledtasksYesNo
sessionsYesNo
shutdownYesNo
startupYesNo
threaddumpYesNo

3、定制化端点

比如定制health:

@Componentpublic class MyComHealthIndicator extends AbstractHealthIndicator {    /**     * 真实的检查方法     * @param builder     * @throws Exception     */    @Override    protected void doHealthCheck(Health.Builder builder) throws Exception {        //mongodb。  获取连接进行测试        Map<String,Object> map = new HashMap<>();        // 检查完成        if(1 == 2){//            builder.up(); //健康            builder.status(Status.UP);            map.put("count",1);            map.put("ms",100);        }else {//            builder.down();            builder.status(Status.OUT_OF_SERVICE);            map.put("err","连接超时");            map.put("ms",3000);        }        builder.withDetail("code",100)                .withDetails(map);    }}

4、监控服务器

了解即可,可以搭建一个监控服务器,引入一个依赖,然后将别的微服务运行的情况传到这个监控服务器上,这里就不演示了,很少会用到,需要用到再来看。

十三、Profile功能

1、什么是Profile功能

又是不说人话的名字,只看这个名字是不知道什么意思的,其实就是方便我们多环境配置的,比如我们有测试环境,有生产环境,和其他一些什么什么环境,但每个环境的配置文件是不一样的,如果没有Profile功能,我们每次换环境都要改配置文件,或者需要频繁的ctrl cv,非常麻烦,有Profile功能就可以直接切换配置文件了。

2、如何使用Profile功能

  1. 默认配置文件为application.yaml,任何时候都会加载

  2. 指定环境配置文件application-{随便一个名字,自己标识}.yaml

  3. 激活指定环境

    1. 在application.yaml配置文件激活

      比如:spring.profiles.active=text

    2. 在命令行激活

      比如:java -jar xxx.jar –spring.profiles.active=test

      启动jar包的时候,后面双杠–,可以指定yaml的配置项的值

    这两种激活方式以命令行激活的优先级更高

  4. 默认配置与环境配置同时生效

  5. 如果默认配置与环境配置拥有相同的项,且项的值不同,那么以环境配置的项值优先

3、条件配置

@Profile注解可以起到条件配置的作用,也就是放入指定配置文件的参数。

比如:

@Configuration@Profile("你的配置文件标识名")public class ProductionConfiguration {	...}

4、Profile分组

这个分组指的是,比如我们有一个要上线的环境,需要用到多个配置文件一起激活,此时就需要用到分组来组织多个配置文件一起生效。

以下配置都是在默认配置文件中编写的:

#配置分组#对应application-test1.yaml文件spring.profiles.group.mygroup[0]=test1#对应application-test2.yaml文件spring.profiles.group.mygroup[1]=test2#激活,group后面的就是分组的名字,在这里我命名为mygroupspring.profiles.active=mygroup

十四、外部化配置

1、外部配置源

常用:Java属性文件YAML文件环境变量命令行参数

2、配置文件查找位置

(1) classpath 根路径

(2) classpath 根路径下config目录

(3) jar包当前目录

(4) jar包当前目录的config目录

(5) /config子目录的直接子目录

从上到下优先级越来越高

3、配置文件加载顺序:

  1. 当前jar包内部的application.properties和application.yml

  2. 当前jar包内部的application-{profile}.properties 和 application-{profile}.yml

  3. 引用的外部jar包的application.properties和application.yml

  4. 引用的外部jar包的application-{profile}.properties 和 application-{profile}.yml

4、指定环境优先,外部优先,后面的可以覆盖前面的同名配置项

十五、自定义starter

我们之前都是用别人写好的starter,那如果要我们自己定义一个starter给别人用怎么做呢

1、starter的结构

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MaKMjNWt-1644079490024)(C:\Users\利姆鲁\AppData\Roaming\Typora\typora-user-images\image-20220205095305826.png)]

starter本身是不携带代码的,它只是作为一个父项目

而真正的代码在autoconfigure子项目中,这个子项目必须是springboot项目或者有spring-boot-starter依赖,这样才可以使用springboot注解

2、自定义starter

  1. 创建一个空项目

  2. 在空项目中创建一个maven项目和一个springboot项目

    • maven项目作为父项目,也就是starter,这里我命名为:xiafan-hello-spring-boot-starter
    • springboot项目作为子项目,也就是autoconfigure,这里我命名为:xiafan-hello-spring-boot-starter-autoconfigure
      • springboot项目中只需要功能代码即可,可以去掉测试包和启动类,因为我们根本不需要它们
  3. 在父项目中导入子项目,把子项目作为依赖

        <dependencies>        <groupId>com.xiafan</groupId>        <artifactId>xiafan-hello-spring-boot-starter-autoconfigure</artifactId>        <version>0.0.1-SNAPSHOT</version>    </dependencies>
    
  4. 编写自定义功能,这里我们以一个hello程序为例子

    • 编写service服务类

      //无需作为组件装配,还需要个性化配置呢public class HelloService {    @Autowired    HelloProperties helloProperties;    public String toSayHello(String username){        return helloProperties.getPrefix()+username+helloProperties.getSuffix();    }}
      
    • 编写properties配置类

      @ConfigurationProperties(prefix = "xiafan.hello")public class HelloProperties {    private String prefix;    private String suffix;    public String getPrefix() {        return prefix;    }    public void setPrefix(String prefix) {        this.prefix = prefix;    }    public String getSuffix() {        return suffix;    }    public void setSuffix(String suffix) {        this.suffix = suffix;    }}
      
    • 编写autoconfiguration自动装配类

      @Configuration//绑定配置文件@EnableConfigurationProperties(HelloProperties.class)public class HelloAutocinfiguration {    //不存在这个类型的bean才会自动装配这个bean,要放在这里才对,如果放在类上面,自定义组件的时候properties会失效	@ConditionalOnMissingBean(HelloService.class)    @Bean    public HelloService helloService(){        return new HelloService();    }}
      
    • 在类文件夹下,创建一个META-INF/spring.factories文件(先创建META-INF目录,再在这个目录下创建spring.factories文件)

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aaxRWkho-1644079490024)(C:\Users\利姆鲁\AppData\Roaming\Typora\typora-user-images\image-20220205103404374.png)]

      在文件中书写如下配置:

      # Auto Configureorg.springframework.boot.autoconfigure.EnableAutoConfiguration=\com.xiafan.xiafanhellospringbootstarterautoconfigure.autoconfigure.HelloAutocinfiguration
      
    • 打包到本地仓库,使用maven的clean+install功能,注意:先打包springboot项目再打包maven项目,因为maven项目是依赖于springboot项目的

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HAeWQfZK-1644079490026)(C:\Users\利姆鲁\AppData\Roaming\Typora\typora-user-images\image-20220205103730889.png)]

  5. 在我们需要引用自定义的项目中导入自定义starter

            <!--引入自定义starter-->        <dependency>            <groupId>org.example</groupId>            <artifactId>xiafan-hello-spring-boot-starter</artifactId>            <version>1.0-SNAPSHOT</version>        </dependency>
    
  6. 配置、编码、调试运行

    • 配置文件配置

      xiafan:  hello:    prefix: hello    suffix: java is yyds
      
    • controller

      @RestControllerpublic class HelloController {    @Autowired    HelloService helloService;    @RequestMapping("/hello")    public String hello(){        return helloService.toSayHello("夏帆");    }}
      
    • 运行启动

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9f41fPOW-1644079490026)(C:\Users\利姆鲁\AppData\Roaming\Typora\typora-user-images\image-20220205105214536.png)]

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值