SpringBoot2原理篇-黑马

发布时间 2023-05-15 09:15:02作者: 爵岚

 

原理篇 1 自动配置 1.1 bean 的加载方式【一】

1 自动配置

1.1 bean 的加载方式【一】

1.1.1 环境准备

创建一个新的工程模块【Maven 的,不是SpringBoot 的】

 直接创建

 一个全新的Maven 工程

【添加坐标】

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.3.23</version>
    </dependency>
</dependencies>

 记得刷一下

创建新的bean 类

 再整一套业务层的东西

业务层接口

package com.dingjiaxiong.service;

public interface BookService {
    
    void check();
}

实现类

package com.dingjiaxiong.service.impl;

import com.dingjiaxiong.service.BookService;

public class BookServiceImpl1 implements BookService {


    @Override
    public void check() {
        System.out.println("book service 1..");
    }
}

复制出三个差不多的实现类

package com.dingjiaxiong.service.impl;

import com.dingjiaxiong.service.BookService;


public class BookServiceImpl2 implements BookService {


    @Override
    public void check() {
        System.out.println("book service 2....");
    }
}
package com.dingjiaxiong.service.impl;

import com.dingjiaxiong.service.BookService;

public class BookServiceImpl3 implements BookService {


    @Override
    public void check() {
        System.out.println("book service 3......");
    }
}
package com.dingjiaxiong.service.impl;

import com.dingjiaxiong.service.BookService;

public class BookServiceImpl4 implements BookService {


    @Override
    public void check() {
        System.out.println("book service 4........");
    }
}

 这样儿差不多准备工作就完成了

1.1.2 第一种方式

Spring 刚出现的时候,它提供的最早的bean 的声明方式就是通过xml 的方式进行声明

来一个配置文件

 

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!--  通过xml的方式声明自己开发的bean  -->
    <bean id="cat" class="com.dingjiaxiong.bean.Cat"/>
</beans>

编写程序运行它

package com.dingjiaxiong.app;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class App1 {

    public static void main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext1.xml");
        Object cat = ctx.getBean("cat");
        System.out.println(cat);
    }

}

运行结果

 这就可以说明cat 这个bean 已经初始化成功了

当然定义时其实也可以不用写 id

<!--  不写id ,通过class获取 -->
<bean class="com.dingjiaxiong.bean.Dog"/>

通过 class直接拿一下

// 通过class,获取bean
        Dog dog = ctx.getBean(Dog.class);
        System.out.println(dog);

运行结果

 没毛病

还有一个小操作,可以一次性到位

String[] names = ctx.getBeanDefinitionNames();
for (String name : names) {
    System.out.println(name);
}

执行结果

 cat 是配置文件中bean的id,下面的狗是一个全路径的类名,后面还跟着一个 #0 ,意思就是编号,如果我有四个

 就是这样

1.1.3 第三方bean

添加一个依赖坐标

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.2.11</version>
</dependency>
<!--  xml方式声明第三方开发的bean  -->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"/>

同样直接运行

 xml 方式声明bean 差不多就是这样,

回顾一下

  • XML方式声明bean

 在创建bean的时候可以带上scope属性,scope有下面几种类型

Singleton

这也是Spring默认的scope,表示Spring容器只创建一个bean的实例,Spring在创建第一次后会缓存起来,之后不再创建,就是设计模式中的单例模式。

Prototype

代表线程

Request

表示每个request作用域内的请求只创建一个实例

Session

表示每个session作用域内的请求只创建一个实例

GlobalSession

这个只在porlet的web应用程序中才有意义,它映射到pt的global范围的session,如果普通的web应用使用了这个scope,容器会把它作为普通的session作用域的scope创建。

注解方式

@Component
@Scope("prototype")
public class Student{

}
 
package com.tongda.app;

import com.tongda.domain.Dog;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class App1 {
    public static void main(String[] args) {
        // 初始化上下文对象
        ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext1.xml");
        // 通过id,获取bean
        Object cat = ctx.getBean("cat");
        System.out.println(cat);

        // 通过class,获取bean
        Dog dog = ctx.getBean(Dog.class);
        System.out.println(dog);
        String[] names = ctx.getBeanDefinitionNames();
        for (String name : names) {
            System.out.println(name);
        }
    }
}

这种方式的优点:

  • 全在一个文件中,一目了然

缺点:

  • 写起来麻烦

 

原理篇 1 自动配置 1.2 bean 的加载方式【二】

1 自动配置

1.2 bean 的加载方式【二】

1.2.1 第二种方式

上一次我们已经回顾了一下通过xml 配置文件的形式去定义bean

 其他没啥, 就是特别繁琐【能不能简化?】 【答案是肯定的】

于是Spring 就提供了一种实用注解的方式来定义bean

就我们想把哪个类配置成受Spring 管控的bean,在类上面加注解就行了

package com.tongda.bean;
import org.springframework.stereotype.Component; // @Component代表<bean标签 id="juelan" 省略class=Cat类/> @Component("tom") public class Cat { }
package com.tongda.bean;

import org.springframework.stereotype.Service;

@Service("jerry")
public class Mouse {
}

但是这样又有了个新的问题,就这样写就能加载的话,那计算机上这么多类库,岂不是Spring 都要去扫一遍,这样工作量太大

为了降低Spring 的工作强度,还是要配置一下【就你告诉Spring ,去哪儿找】

applicationContext2.xml:注意修改

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="
       http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd
    ">

    <!--  指定加载bean的位置,component组件扫描  -->
    <context:component-scan base-package="com.dingjiaxiong.bean"/>

</beans>

来一个新的运行程序 

package com.tongda.app;

import org.springframework.context.support.ClassPathXmlApplicationContext;

public class App2 {
    public static void main(String[] args) {
        ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext2.xml");
        String[] names = ctx.getBeanDefinitionNames();
        for (String name : names) {
            System.out.println(name);
        }
    }
}

直接运行

 可以看到, 东西还有点多

但是tom、jerry 已经加载上了

这就是第二种bean 的加载方式,就是使用组件扫描 + 类上面写注解 来配置

那现在第三方的bean 怎么定义呢?总不能去改人家的源代码加注解吧

是可以做的,先来一个配置类

package com.tongda.config;

import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;

// 配置第三方bean组件
// @Component
// 或者使用@Configuration配置类
@Configuration
public class DbConfig {

    @Bean
    public DruidDataSource dataSource(){
        DruidDataSource ds = new DruidDataSource();
        return ds;
    }
}

修改一下扫描

 再次运行

 可以看到已经上来了,而且dbConfig 也上来了,这倒不奇怪

如果我换个注解

 再次运行

 效果没变,这意味着这两个有关系

 这大致就是使用注解 + 扫描配置 去加载 bean的介绍

回顾一下

XML+注解方式声明bean

 使用@Component及其衍生注解@Controller 、@Service、@Repository定义bean

 

原理篇 1 自动配置 1.3 bean 的加载方式【三】 

1.3.1 第三种方式

之前我们又使用了 xml + 注解的方式加载bean

 这样问题就又来了,如果我那个范围指定的够大

 

 那岂不是这个文件就定死了…【能不能这个文件都不需要呢?】

【答案是肯定的】【配置类取代配置文件

package com.tongda.config;

import org.springframework.context.annotation.ComponentScan;

// 配置类代替xml
@ComponentScan({"com.tongda.bean","com.tongda.config"})
public class SpringConfig3 {
}

就这样就行了,

来一个全新的应用程序

package com.tongda.app;

import com.tongda.config.SpringConfig3;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;


public class App3 {
    public static void main(String[] args) {
        // 加载类配置:AnnotationConfigApplicationContext(类名.class)
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig3.class);
        String[] names = ctx.getBeanDefinitionNames();
        for (String name : names) {
            System.out.println(name);
        }
    }
}

直接运行

 好像和之前又有些不一样,刚刚写的配置类也上来了

其实是只要咱们用了AnnotationConfigApplicationContext 这玩意儿加载一个类,那个类也会自动变成一个bean

到这儿其实第三种方式就说完了,感觉上其实和第二种没有太大的区别

回顾一下

 @Configuration配置项如果不用于被扫描可以省略

原理篇 1 自动配置 1.4 FactoryBean

1.4.1 FactoryBean

之前我们又完成了使用一个配置类去加载bean

 现在又有人提出疑问了

在我们用Spring 整合Mybatis 或者 Mybatis-Plus 的时候

里面有一种很特殊的bean的声明

 我定义了一个bean ,返回类型是 AA ,但是最后造出来的bean ,不是AA类型的对象【这是怎么回事?】

【这不是一种完整的创建bean 的方式,只能说一种特殊的应用】

举个例子

package com.dingjiaxiong.bean;

import org.springframework.beans.factory.FactoryBean;


public class DogFactoryBean implements FactoryBean<Dog> {

    //第一个方法意思是确定这个工厂造出来的bean是什么
    @Override
    public Dog getObject() throws Exception {
        return new Dog();
    }

    //第二个方法要你告诉它造出来这个东西是什么类型
    @Override
    public Class<?> getObjectType() {
        return Dog.class;
    }

    @Override
    public boolean isSingleton() {
        return FactoryBean.super.isSingleton();
    }
}

这就是一个生产bean 的工厂

修改一下我们之前的配置类 SpringConfig3

package com.dingjiaxiong.config;

import com.dingjiaxiong.bean.Dog;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;

// 配置类代替xml
@ComponentScan({"com.dingjiaxiong.bean","com.dingjiaxiong.config"})
public class SpringConfig3 {

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

}

其实这样也能直接出一个Dog 对象,直接运行看看

 没毛病,改下方法名

 OK, 方法名就是bean 的名字

现在改一下,换成我们写的工厂

package com.dingjiaxiong.config;

import com.dingjiaxiong.bean.Dog;
import com.dingjiaxiong.bean.DogFactoryBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;

@ComponentScan({"com.dingjiaxiong.bean","com.dingjiaxiong.config"})
public class SpringConfig3 {

    @Bean
    public DogFactoryBean dog(){
        return new DogFactoryBean();
    }

}

直接运行

 很明显, dog 对象还是出来了,但是它是啥类型?

 打印出来看看

 可以看到,它并不是工厂,而直接是我们的Dog 类,虽然我们类型写的 工厂,但是返回的确实是Dog 类型

原因就是我们实现了那个接口

 现在这个工厂类一旦被spring 加载到之后,由它造出来的对象不是工厂类型自身,而是后面的泛型类型【妙啊】

类型就在下面

 为什么会有这样子的需求的呢?

原因就在于咱们在返回对象之前,可以在工厂中做一系列的初始化工作

 

 这样就和我们直接new 出来就不一样了【当然不止于此,包括环境监测…】

这就相当于给我们提供了一个造这个对象的工厂,和他的名字也很搭,在这里面可以进行好一系列的出厂前操作【各种前置初始化操作】,做好了再return 出去

而且就DogFactoryBean 本身而言

 它是造不出来自己 的

然后最后那个方法,其实之前学Spring 的时候老师也讲过,就是否单例模式

 

 意思就是现在是单例的,如果我改成false

 效果很明显,现在就不是单例的了

OK,大概就是这样

回顾一下

初始化实现FactoryBean接口的类,实现对bean加载到容器之前的批处理操作

原理篇 1 自动配置 1.5 proxyBeanMethod

1.5.1 @ImportResource

之前我们又完成了使用“工厂” 去创建bean 对象

现在有个新问题又来了

 

 现在的加载方式,有xml 配置文件,也有配置类,如果说我现在有一个十年前写的程序,它是用的配置文件加载的bean, 但是现在要上新功能,用注解写的,如果不能修改配置文件,那咋办?【典型的系统迁移问题:如何在一个新的注解开发的项目中使用老的配置文件的方式?可以实现吗?】

【答案是当然的】

来一个新的配置类

package com.dingjiaxiong.config;

import com.dingjiaxiong.bean.DogFactoryBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;

public class SpringConfig32 {
    
}

其实啥也没有

来一个新的运行类去加载32 这个配置类

package com.dingjiaxiong.app;

import com.dingjiaxiong.config.SpringConfig3;
import com.dingjiaxiong.config.SpringConfig32;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class App32 {

    public static void main(String[] args) {

        ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig32.class);

        String[] names = ctx.getBeanDefinitionNames();
        for (String name : names) {
            System.out.println(name);
        }        
    }
}

直接运行看看效果

 OK,没啥问题

现在就来修改32 这个配置类,让它可以去加载到我们使用 xml 配置文件的方式做的bean

其实特简单,一个注解

package com.dingjiaxiong.config;

import com.dingjiaxiong.bean.DogFactoryBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.ImportResource;

@ImportResource("applicationContext1.xml")
public class SpringConfig32 {

}

效果很明显,都上来了

这就是在现在的配置类中导入原始的配置文件的做法

回顾一下

  • 加载配置类并加载配置文件(系统迁移) 

 

1.5.2 proxyBeanMethod

之前我们提了一下,@Configuration 这个注解和 @Component 这个注解很像,但是是假的

看看两个注解的源码:

@Component :

 @Configuration:

 注意看这最后一个【研究一下】

再来一个新的配置类

package com.dingjiaxiong.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportResource;

@Configuration
public class SpringConfig33 {

}

还是一个空壳

再来一个新的运行类

package com.dingjiaxiong.app;

import com.dingjiaxiong.config.SpringConfig32;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class App33 {

    public static void main(String[] args) {

        ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig33.class);

        String[] names = ctx.getBeanDefinitionNames();
        for (String name : names) {
            System.out.println(name);
        }
    }
}

直接运行看看效果

 OK, 没啥问题

看看这个bean 的类型

System.out.println("=========================");
System.out.println(ctx.getBean("springConfig33"));

直接运行看看

 这一串什么玩意儿

意思就是SpringConfig33 其实是一个代理对象

现在修改一下33 配置类

 再次运行

 效果很明显,那一串东西没了,就说明现在这个对象,已经不是一个代理对象了,就成了一个原始对象

【所以这玩意儿的作用是什么?】

我们在33 配置类中定义一个 bean

package com.tongda.config;

import com.tongda.bean.Cat;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportResource;

// 默认为true:调用方法获取容器中的Bean复用,无论几次都是同一个Bean
// 修改为false:则每次调用方法获取的都是new的出新Bean.
@Configuration(proxyBeanMethods = false)
public class SpringConfig33 {

    @Bean
    public Cat cat(){
        return new Cat();
    }
}

直接再次运行

 OK, 这没啥,上来了一个bean 对象

现在我们直接用33 这个配置类,去调用这个方法

package com.dingjiaxiong.app;

import com.dingjiaxiong.config.SpringConfig32;
import com.dingjiaxiong.config.SpringConfig33;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class App33 {

    public static void main(String[] args) {

        ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig33.class);

        String[] names = ctx.getBeanDefinitionNames();
        for (String name : names) {
            System.out.println(name);
        }
        System.out.println("=========================");
        SpringConfig33 springConfig33 = ctx.getBean("springConfig33", SpringConfig33.class);
        System.out.println(springConfig33.cat());
        System.out.println(springConfig33.cat());
        System.out.println(springConfig33.cat());
    }
}

调了三次,而且把proxyBeanMethods 属性值改为了 true

直接运行看看

 可以看到,就是普通的bean对象,而且单例的,即同一个对象

如果改成false

 

很明显,现在就不是单例的了【就成了完全不同的三个对象】

意思就是如果proxyBeanMethods 属性为true,我们在运行对应的方法时,这个方法如果曾经在Spring 容器中加载过bean,那么我们再调,就是直接从容器中去拿了,而不是重新创建

如果属性值为false【不创建代理对象】,就是使用当前类对象去执行它里面的方法,所以才有了三个不同的cat 对象【这就是true 和 false 的区别

【默认是true,即创建代理对象】

看懂了这个,就可以解释我们前面遇到过的一个事情,学mq 的时候

 我把这个注解一关,下面的东西就不能用了,但是我一开 

 因为proxyBeanMethods 属性默认为true,所以这就保证了我们每次都操作的 是同一个队列

如果我明着写成配成false

 这样就绑不对了,压根儿都不是同一个了

OK,差不多就是这样

回顾一下

使用proxyBeanMethods=true可以保障调用此方法得到的对象是从容器中获取的而不是重新创建的

 

原理篇 1 自动配置 1.6 bean 的加载方式【四】

1.6.1 @Import

OK,前面咱们已经学习了三种初始化bean 的方式了,中间还学习了关于 proxyBeanMethods 属性的知识

 接下来就来看看第四种

先来一个新的配置类

package com.dingjiaxiong.config;

public class SpringConfig4 {
}

非常干净的配置类

再来一个新的运行类

package com.dingjiaxiong.app;

import com.dingjiaxiong.config.SpringConfig33;
import com.dingjiaxiong.config.SpringConfig4;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class App4 {

    public static void main(String[] args) {

        ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig4.class);

        String[] names = ctx.getBeanDefinitionNames();
        for (String name : names) {
            System.out.println(name);
        }
    }
}

直接运行看看

 没啥问题

第四种方式是使用 @Import

package com.dingjiaxiong.config;

import com.dingjiaxiong.bean.Dog;
import org.springframework.context.annotation.Import;

@Import(Dog.class)
public class SpringConfig4 {
}

再次运行

 可以看到,这个东西确实上来了

看看是不是真的加载到了

 OK,没问题,确实加载到了Dog

而且这种方式加载出来的类是一个非常标准的全路径类名,而且是大写开头

【所以这种方式的作用体现在哪里?】

之前我们加注解、写配置,现在使用直接导入,原本的类咱们压根儿没动过

 可以进行有效的解耦,开发中实践

Spring 倡导无侵入式 编程或者叫无入侵式编程

说加就加、看不到动过的痕迹

就现在这种直接导入的方式会是一种非常好用的最佳实践,以后咱们有了一个外部类,可以完全不做改变的把搞成一个bean放入我们的容器

OK, 回顾一下

使用@Import注解导入要注入的bean对应的字节码

【@Import 的扩展】

我现在给它导入一个配置类 

 

package com.dingjiaxiong.config;

import com.dingjiaxiong.bean.Dog;
import org.springframework.context.annotation.Import;


@Import({Dog.class, DbConfig.class})
public class SpringConfig4 {
}

直接运行

 可以看到直接就上来了俩,而且是全路径类名,而且它里面的bean 也被加载了

如果把配置类上的注解拿掉

 这就变成Dog 了

直接运行看看效果

 DbConfig 上来是应该的,和Dog 原理一样,而且可以看到,它里面定义的bean 还是上来了

就是这样

原理篇 1 自动配置 1.7 bean 的加载方式【五】

1.7.1 register

之前我们又说了一种加载bean 的方式,使用@Import 注解可以无侵入的将一个普通类变成一个bean 放到容器中进行管理。

 现在来说第五种:

这种方式平时开发的时候不常用,但是如果是要做一些框架的话就可能会用到…【啊这】

直接开干,先来一个运行程序

package com.dingjiaxiong.app;

import com.dingjiaxiong.config.SpringConfig33;
import com.dingjiaxiong.config.SpringConfig4;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class App5 {

    public static void main(String[] args) {

        ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig4.class);

        String[] names = ctx.getBeanDefinitionNames();
        for (String name : names) {
            System.out.println(name);
        }
        System.out.println("=========================");        
    }
}

直接运行一下看看

 OK,出来了这些东西无所谓

第五种方式就是在上下文对象已经初始化完毕之后,手工加载bean

ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig4.class);

 不要配置类、也不要配置文件【直接编程干】

【怎么做?】

package com.dingjiaxiong.app;

import com.dingjiaxiong.bean.Cat;
import com.dingjiaxiong.config.SpringConfig33;
import com.dingjiaxiong.config.SpringConfig4;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class App5 {

    public static void main(String[] args) {

        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig4.class);
        //在上下文对象已经初始化完毕之后,手工加载bean

        ctx.registerBean("tom", Cat.class);
        
        String[] names = ctx.getBeanDefinitionNames();
        for (String name : names) {
            System.out.println(name);
        }
        System.out.println("=========================");

    }

}

现在就手工加载了一个tom

直接运行,看看效果

 没啥毛病,上来了

如果多复制几个, 看看会不会有冲突

 运行结果

 可以看到,出来了, 说明没有冲突

那究竟是哪个留下来了,验证一下,修改一下实体类

package com.dingjiaxiong.bean;

import org.springframework.stereotype.Component;

//这个注解就代表了<bean> 这个标签
@Component("tom")
public class Cat {
    public Cat(){
    }

    int age;
    public Cat(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Cat{" +
                "age=" + age +
                '}';
    }
}

再修改一下运行类

package com.dingjiaxiong.app;

import com.dingjiaxiong.bean.Cat;
import com.dingjiaxiong.config.SpringConfig33;
import com.dingjiaxiong.config.SpringConfig4;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class App5 {

    public static void main(String[] args) {

        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig4.class);
        //在上下文对象已经初始化完毕之后,手工加载bean

        ctx.registerBean("tom", Cat.class,0);
        ctx.registerBean("tom", Cat.class,1);
        ctx.registerBean("tom", Cat.class,2);

        String[] names = ctx.getBeanDefinitionNames();
        for (String name : names) {
            System.out.println(name);
        }
        System.out.println("=========================");
        System.out.println(ctx.getBean(Cat.class));

    }

}

直接运行,看看是哪只猫

 OK,效果很明显,最后一个留下来了【就像map,因为key 一样,最终只留了最后那个

这个很有用,假如说现在系统里面有个bean,现在再来一个,把原来那个覆盖了,这就相当于把现有东西给隐藏掉了,只有新的生效了

即一个新的值替换老的值【这个场景就很多了,不做配置用老的值,但凡我一做,就用我们自己的值】

最后再提一个,如果我们仅仅是想注册进去一个bean,还可更简单

并不是全路径

OK, 回顾一下

  • 使用上下文对象在容器初始化完毕后注入bean

原理篇 1 自动配置 1.8 bean 的加载方式【六】

1.8.1 ImportSelector

OK,上一节又说完了第五种

使用上下文对象在容器初始化完毕后注入bean

 下面就来说说第六种

第六种方式平时咱们自己用的少,但是在框架内部大量使用

先来一个全新的类

package com.dingjiaxiong.bean;

import org.springframework.context.annotation.ImportSelector;
import org.springframework.core.type.AnnotationMetadata;

public class MyImportSelector implements ImportSelector {

    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        return new String[]{"com.dingjiaxiong.bean.Dog"};
    }
}

 数组中是要加载bean 的全路径名

再来一个新的配置类

package com.dingjiaxiong.config;

import com.dingjiaxiong.bean.Dog;
import com.dingjiaxiong.bean.MyImportSelector;
import org.springframework.context.annotation.Import;

@Import(MyImportSelector.class)
public class SpringConfig6 {
}

最后来个运行类

package com.dingjiaxiong.app;

import com.dingjiaxiong.bean.Cat;
import com.dingjiaxiong.bean.Mouse;
import com.dingjiaxiong.config.SpringConfig4;
import com.dingjiaxiong.config.SpringConfig6;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class App6 {

    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig6.class);
        String[] names = ctx.getBeanDefinitionNames();
        for (String name : names) {
            System.out.println(name);
        }
        System.out.println("=========================");
    }
}

直接运行

 效果很明显,已经上来了

还可以写多个

 加载就是这样了,难免会有人想问,为嘛要折腾这?

这种方式的关键

 

在于这个东西

Metadata 元数据

举个例子,我们造了一个数据库、表,表里面有数据,那么这个数据表的信息在哪儿描述?

还得有另外一张表来描述这张表的结构,那这另外这张表就可以称为被描述表的元数据

还可以有元元数据、元元元数据…【数据库中有四层】

在我们这儿,元数据就是往上追溯一下

【这个东西有什么用?】

修改一下

package com.dingjiaxiong.bean;

import org.springframework.context.annotation.ImportSelector;
import org.springframework.core.type.AnnotationMetadata;

public class MyImportSelector implements ImportSelector {

    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {

        System.out.println(importingClassMetadata.getClassName());

        return new String[]{"com.dingjiaxiong.bean.Dog"};
    }
}

先拿个classname 看看

 意思就是 importingClassMetadata 这个形参描述的是 SpringConfig6

简单的说,

 加载的是谁,描述的就是谁

再来

System.out.println(importingClassMetadata.hasAnnotation("org.springframework.context.annotation.Configuration"));

这个意思就是描述的那个东西上面有没有Configuration 注解

直接看看

 如果我注掉

 这样就false了

所以现在就可以明确了 importingClassMetadata 这个指的就是它那个类出现在谁上面

 意思就是SpringConfig6 就是它对应的元数据

再来一个

 现在我想知道,它有没有加basePackages 这个属性

package com.dingjiaxiong.bean;

import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.ImportSelector;
import org.springframework.core.type.AnnotationMetadata;

import java.util.Map;

public class MyImportSelector implements ImportSelector {

    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {

        System.out.println("===========================");

        System.out.println(importingClassMetadata.getClassName());
        System.out.println(importingClassMetadata.hasAnnotation("org.springframework.context.annotation.Configuration"));
        Map<String, Object> attributes = importingClassMetadata.getAnnotationAttributes("org.springframework.context.annotation.ComponentScan");

        System.out.println(attributes);
        
        System.out.println("===========================");
        return new String[]{"com.dingjiaxiong.bean.Dog"};
    }
}

直接运行

 没啥毛病

到现在大概就知道了,我们可以利用这个东西去做一系列的判定,ImportSelector 的意义就在于我们可以进行各种条件的判定,判定完毕后,决定是否装载指定的bean,动态加载bean

举个栗子

package com.dingjiaxiong.bean;

import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.ImportSelector;
import org.springframework.core.type.AnnotationMetadata;

import java.util.Map;

public class MyImportSelector implements ImportSelector {

    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {

        boolean flag = importingClassMetadata.hasAnnotation("org.springframework.context.annotation.Configuration");
        if (flag){
            return new String[]{"com.dingjiaxiong.bean.Dog"};
        }
        return new String[]{"com.dingjiaxiong.bean.Cat"};
    }
}

判定运行结果

 就是这样,意思就是这个东西不仅能够加载bean,它还可以进行条件判定、然后控制加载谁,动态加载bean

package com.tongda.bean;

import org.springframework.context.annotation.ImportSelector;
import org.springframework.core.type.AnnotationMetadata;

import java.util.Map;

public class MyImportSelector implements ImportSelector {

    @Override
    // 牢记:Metadata,meta元data数据,元数据又称中介数据、中继数据
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        System.out.println("=============");
        System.out.println("提示:" + importingClassMetadata.getClassName());
        // 注解全类名;@Configuration
        importingClassMetadata.hasAnnotation("org.springframework.context.annotation.Configuration");
        // 获取注解的属性集合
        Map<String, Object> attributes = importingClassMetadata.getAnnotationAttributes("org.springframework.context.annotation.ComponentScan");
        System.out.println(attributes);
        System.out.println("=============");
        // 各种条件的判定,判定完毕后,决定是否装在指定的bean,动态记载bean
        boolean flag = importingClassMetadata.hasAnnotation("org.springframework.context.annotation.Configuration");
        if (flag) {
            return new String[]{"com.tongda.bean.Dog",};
        }
        return new String[]{"com.tongda.bean.Cat"};
        // return new String[0];
        // 全路径类名
        // return new String[]{"com.tongda.bean.Dog","com.tongda.bean.Cat",};
    }

    /*@Override
    public Predicate<String> getExclusionFilter() {
        return ImportSelector.super.getExclusionFilter();
    }*/
}

 

OK,回顾一下

导入实现了ImportSelector接口的类,实现对导入源的编程式处理

 

原理篇 1 自动配置 1.9 bean 的加载方式【七】

1.9.1 ImportBeanDefinitionRegistrar

之前我们又讲了第六种 ImportSelector ,通过实现这个接口去查某一个东西的“户口”

 

 

 现在来说说第七种,这种方式又要复杂一些,高端一些

【直接开干】

来一个新的类

package com.dingjiaxiong.bean;

import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.type.AnnotationMetadata;

public class MyRegistrar implements ImportBeanDefinitionRegistrar {


    //第一个参数就是我们之前看的元数据,而且他还有一个参数,也就是这种方式比第六种更强大
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {

    }
}

以前我们第六种的时候

返回

 是用的全路径类名

第七种方式就不用了,靠的就是registry 这个对象来帮我们初始化bean

package com.tongda.bean;

import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.type.AnnotationMetadata;

public class MyRegistrar implements ImportBeanDefinitionRegistrar {

    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        // ImportBeanDefinitionRegistrar.super.registerBeanDefinitions(importingClassMetadata, registry);
        // 使用元数据去做判定
        // 1. 创建BeanDefinition对象,并获得BeanDefinitionRegistry对象
        BeanDefinition beanDefinition = BeanDefinitionBuilder.rootBeanDefinition(Dog.class).getBeanDefinition();

        // 2. 使用registry来初始化对象,将bean注册进入(bean名,beanDefinition)
        registry.registerBeanDefinition("yellow",beanDefinition);
    }
}

再来一个配置类

package com.dingjiaxiong.config;

import com.dingjiaxiong.bean.MyImportSelector;
import com.dingjiaxiong.bean.MyRegistrar;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

@Import(MyRegistrar.class)
public class SpringConfig7 {
}

来一个运行类

package com.dingjiaxiong.app;

import com.dingjiaxiong.config.SpringConfig6;
import com.dingjiaxiong.config.SpringConfig7;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class App7 {

    public static void main(String[] args) {

        ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig7.class);

        String[] names = ctx.getBeanDefinitionNames();
        for (String name : names) {
            System.out.println(name);
        }
        System.out.println("=========================");
    }
}

直接运行

 出来了【这就又实现了对bean 的管理,更高端牛逼了】

OK,回顾一下

导入实现了ImportBeanDefinitionRegistrar接口的类,通过BeanDefinition的注册器注册实名bean,实现对
容器中bean的裁定,例如对现有bean的覆盖,进而达成不修改源代码的情况下更换实现的效果

原理篇 1 自动配置 1.10 bean 的加载方式【八】

1.10.1 BeanDefinitionRegistryPostProcessor

之前我们又使用 ImportBeanDefinitionRegistrar 接口来实现了bean 的加载

 比ImportSelector 接口更高端了些,【还有吗?】【还有第八种】

来个新的类

package com.dingjiaxiong.bean;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;

public class MyPostProcessor implements BeanDefinitionRegistryPostProcessor {

    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry beanDefinitionRegistry) throws BeansException {
        //后处理bean 定义注册,参数和第七种那个注册一样
        BeanDefinition beanDefinition = BeanDefinitionBuilder.rootBeanDefinition(Dog.class).getBeanDefinition();
        beanDefinitionRegistry.registerBeanDefinition("yellow",beanDefinition);

    }
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {

    }
}

可以看到,它和第七种特别像,直接把第七种贴过来都能用了,问题就有了, 为啥叫什么后处理?

一点一点来

先来一个配置类

package com.dingjiaxiong.config;

import com.dingjiaxiong.bean.MyRegistrar;
import com.dingjiaxiong.service.impl.BookServiceImpl1;
import org.springframework.context.annotation.Import;

@Import(BookServiceImpl1.class)
public class SpringConfig8 {
}

这个图书实现类,已经是好久之前的了,笔者这里再贴一下

package com.dingjiaxiong.service.impl;

import com.dingjiaxiong.service.BookService;

public class BookServiceImpl1 implements BookService {
    
    @Override
    public void check() {
        System.out.println("book service 1...");
    }
}

再来个运行类

package com.dingjiaxiong.app;

import com.dingjiaxiong.config.SpringConfig7;
import com.dingjiaxiong.config.SpringConfig8;
import com.dingjiaxiong.service.BookService;
import com.dingjiaxiong.service.impl.BookServiceImpl1;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class App8 {

    public static void main(String[] args) {

        ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig8.class);
        BookService bookService = ctx.getBean("bookService", BookService.class);

        bookService.check();
    }

}

修改一下实现类,把它定义成一个bean

package com.dingjiaxiong.service.impl;

import com.dingjiaxiong.service.BookService;
import org.springframework.stereotype.Service;


@Service("bookService")
public class BookServiceImpl1 implements BookService {

    @Override
    public void check() {
        System.out.println("book service 1...");
    }
}

 现在结构就很清晰了,所有都在针对实现类1

OK,直接运行

 调用成功【这和第八种方法暂时没关系,就只是一个简单的bean 调用】

接下来加上我们写的类

 并且修改一下这个类

package com.dingjiaxiong.bean;

import com.dingjiaxiong.service.impl.BookServiceImpl2;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.type.AnnotationMetadata;

public class MyRegistrar implements ImportBeanDefinitionRegistrar {

    //第一个参数就是我们之前看的元数据,而且他还有一个参数,也就是这种方式比第六种更强大
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {

        //1. 使用元数据进行判定【这里就不判了】
        //初始化BeanDefinition对象
        BeanDefinition beanDefinition = BeanDefinitionBuilder.rootBeanDefinition(BookServiceImpl2.class).getBeanDefinition();
        registry.registerBeanDefinition("bookService",beanDefinition);
    }
}

也就是说,我现在本身导了个 1 ,但是又用ImportBeanDefinitionRegistrar 导了个2

直接运行看看结果

 结果很明显,它既没报错, 而且调用了实现类2的方法

实现类2 BookServiceImpl2

现在的意思就是1 被覆盖了,好家伙

 这样子的意义是什么?前面的1 就是默认技术【比如内嵌的Tomcat、内嵌的数据源…】

而后面就是我们自己定义了一个【那万一项目组人多】

举个栗子

package com.dingjiaxiong.bean;

import com.dingjiaxiong.service.impl.BookServiceImpl2;
import com.dingjiaxiong.service.impl.BookServiceImpl3;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.type.AnnotationMetadata;


public class MyRegistrar2 implements ImportBeanDefinitionRegistrar {


    //第一个参数就是我们之前看的元数据,而且他还有一个参数,也就是这种方式比第六种更强大
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {

        //1. 使用元数据进行判定【这里就不判了】
        //初始化BeanDefinition对象
        BeanDefinition beanDefinition = BeanDefinitionBuilder.rootBeanDefinition(BookServiceImpl3.class).getBeanDefinition();

        registry.registerBeanDefinition("bookService",beanDefinition);
    }
}

现在的情况就是,有两个开发者,都对同一个技术进行了实现,然后!很巧的是

 它俩都摆在了这儿,现在会是谁生效,直接运行

 妙了,3 生效了

挪下位置

 OK,效果很明显,和加载顺序有关【现在真正的问题来了,现在是配置的顺序说了算,没有东西能够管一下它俩?万一以后更多,那不乱死?】

OK,第八种加载方式就是干这事儿的

把它也加上

 这个加载的4 ,OK,终局之战

直接运行

 MyPostProcessor说了算

这就是“后处理” 的意义,前面的全整完之后,我一来,前面的全部没用 了

【这种后处理的机制有什么用?】

保障性工作

这就是它最牛的地方

OK,回顾一下

导入实现了BeanDefinitionRegistryPostProcessor接口的类,通过BeanDefinition的注册器注册实名bean
实现对容器中bean的最终裁定

 

其实还有加载方式,但是这些够用了

1.10.2 小结

原理篇 1 自动配置 1.11 bean 的加载控制【编程式】

1.11.1 bean 的加载控制

前面我们已经看完了8 种+ 的bean的加载方式【收获颇丰】,这些都可以往Spring 的容器中去加载bean

但是很多情况下,都需要根据情况去加载对应的bean,而不是每次都加载固定的那些bean【就是咱们可以对其进行控制】

bean的加载控制:bean的加载控制指根据特定情况对bean进行选择性加载以达到适用于项目的目标。

前面我们讲的各种方式中,哪些可以实现控制呢?

一、XML方式声明bean

 二、XML+注解方式声明bean

  二、使用@Component及其衍生注解@Controller 、@Service、@Repository定义bean;使用@Bean定义第三方bean,并将所在类定义为配置类或Bean

三、注解方式声明配置类

  • 初始化实现FactoryBean接口的类,实现对bean加载到容器之前的批处理操作

  • 加载配置类并加载配置文件(系统迁移)

  • 使用proxyBeanMethods=true可以保障调用此方法得到的对象是从容器中获取的而不是重新创建的

 四、使用@Import注解导入要注入的bean对应的字节码

  • 使用@Import注解导入配置类

 五、使用上下文对象在容器初始化完毕后注入bean

 六、导入实现了ImportSelector接口的类,实现对导入源的编程式处理

 七、导入实现了ImportBeanDefinitionRegistrar接口的类,通过BeanDefinition的注册器注册实名bean,实现对
容器中bean的裁定,例如对现有bean的覆盖,进而达成不修改源代码的情况下更换实现的效果

 八、导入实现了BeanDefinitionRegistryPostProcessor接口的类,通过BeanDefinition的注册器注册实名bean,
实现对容器中bean的最终裁定

 所以,通过下面的 四种加载方式,我们就可以通过编程的方式去控制bean 的加载。

 

1.11.2 环境准备

新建一个普通的Maven 工程模块

 OK,直接创建

 一个全新的Maven 工程

【添加坐标】

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.3.23</version>
    </dependency>
</dependencies>

 【创建类】

package com.dingjiaxiong.bean;

public class Cat {
}

狗和老鼠同理

 【配置类】

package com.dingjiaxiong.config;

public class SpringConfig {
}

【运行类】

package com.dingjiaxiong;

import com.dingjiaxiong.config.SpringConfig;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;


public class App {
    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
        String[] beans = ctx.getBeanDefinitionNames();

        for (String bean : beans) {
            System.out.println(bean);
        }
    }
}

直接运行,效果可以猜到了,几个长的,一个SpringConfig

 OK,没问题

1.11.3 加载控制

先来个importselector 类

package com.dingjiaxiong.bean;

import org.springframework.context.annotation.ImportSelector;
import org.springframework.core.type.AnnotationMetadata;

public class MyImportSelector implements ImportSelector {
    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        return new String[]{"com.dingjiaxiong.bean.Cat"};
    }
}

修改配置类

package com.dingjiaxiong.config;

import com.dingjiaxiong.bean.MyImportSelector;
import org.springframework.context.annotation.Import;

@Import(MyImportSelector.class)
public class SpringConfig {
}

OK,再次运行

 OK, 上来了,给Cat 加个id名

package com.dingjiaxiong.bean;

import org.springframework.stereotype.Component;

@Component("tom")
public class Cat {
}

再次运行

 假设一个场景,如果我的环境中有Mouse,那我就加载Cat

package com.dingjiaxiong.bean;

import org.springframework.context.annotation.ImportSelector;
import org.springframework.core.type.AnnotationMetadata;

public class MyImportSelector implements ImportSelector {
    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
      
        try {
            Class<?> clazz = Class.forName("com.dingjiaxiong.bean.Mouse");

            if (clazz != null){ //说明加载上了老鼠
                return new String[]{"com.dingjiaxiong.bean.Cat"}; //有老鼠就加载猫
            }
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        return null;
    }
}

直接运行

 所以猫出来了

如果 现在有狼,猫就不要加载了

package com.dingjiaxiong.bean;

import org.springframework.context.annotation.ImportSelector;
import org.springframework.core.type.AnnotationMetadata;

/**
 * ClassName: MyImportSelector
 * date: 2022/10/25 10:38
 *
 * @author DingJiaxiong
 */

public class MyImportSelector implements ImportSelector {
    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {

        try {
            Class<?> clazz = Class.forName("com.dingjiaxiong.bean.Wolf");

            if (clazz != null){ 
                return new String[]{"com.dingjiaxiong.bean.Cat"}; 
            }
        } catch (ClassNotFoundException e) {
//            e.printStackTrace();
            return new String[0];
        }
        return null;
    }
}

 可以看到这次就没有加载猫

又改成老鼠

 这样又出来了,现在我们就可以根据自己的需要来控制某个bean 的加载与否了

package com.tongda.bean;

import org.springframework.context.annotation.ImportSelector;
import org.springframework.core.type.AnnotationMetadata;

public class MyImportSelector implements ImportSelector {
    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        // 开发:控制
        // 获取当前环境中有什么,Class.forName生成对象
        try {
            // 如果环境中有Mouse就加载,没有return new String[0]
            Class<?> clazz = Class.forName("com.tongda.bean.Mouse");
            // 环境中没有Wolf,return new String[0]
            // Class<?> clazz = Class.forName("com.tongda.bean.Wolf");
            if (clazz == null){
                return new String[]{"com.tongda.bean.Cat"};
            }
        } catch (ClassNotFoundException e) {
            // e.printStackTrace();
            return new String[0];
        }
        return null;
    }
}

回顾一下

根据任意条件确认是否加载bean

原理篇 1 自动配置 1.12 bean 的加载控制【注解式】

1.12.1 问题引入

前面我们通过硬编码的形式完成了根据需求来控制某个bean 的加载,

 但是这样就很容易想到问题,这样子代码量就太大了,而且每出现一种情况,就要整一个if 判断

而且还会出现,判断语句压根儿写不出来的情况

【有简单点儿的办法吗?当然】

1.12.2 bean的加载控制【注解式】

使用@Conditional注解的派生注解设置各种组合条件控制bean的加载

直接开干

先把原先的做法注掉 

 现在在配置中通过@Bean 注解主动加载一个bean

package com.dingjiaxiong.config;

import com.dingjiaxiong.bean.Cat;
import com.dingjiaxiong.bean.MyImportSelector;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;

public class SpringConfig {
    
    @Bean
    public Cat tom(){
        return new Cat();
    }
    
}

直接运行

 现在来控制它

先换下坐标 

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

 

package com.dingjiaxiong.config;

import com.dingjiaxiong.bean.Cat;
import com.dingjiaxiong.bean.Mouse;
import com.dingjiaxiong.bean.MyImportSelector;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Import;

public class SpringConfig {

    @Bean
    @ConditionalOnClass(Mouse.class) //现在的意思就是只要发现了有老鼠,就创建猫
    public Cat tom(){
        return new Cat();
    }

}

直接运行

如果我现在想换成狼,就不能用这种形式了,因为我们压根儿没有这个类

 换个注解 @ConditionalOnMissingClass

package com.dingjiaxiong.config;

import com.dingjiaxiong.bean.Cat;
import com.dingjiaxiong.bean.Mouse;
import com.dingjiaxiong.bean.MyImportSelector;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Import;

public class SpringConfig {

    @Bean
//    @ConditionalOnClass(Wolf.class)
    @ConditionalOnMissingClass("com.dingjiaxiong.bean.Wolf")
    public Cat tom(){
        return new Cat();
    }

}

现在的意思就是如果我没有加载到Wolf,那我就加载猫

直接运行

 因为压根儿没有Wolf,所以Cat 可以被加载

如果改成Mouse 

 其实之前那个注解也可以,毕竟都能直接导入类了,那还有啥判断的必要? 

 改成Wolf

 这是通过类,还可以根据容器中是否有bean 来进行判断

package com.dingjiaxiong.config;

import com.dingjiaxiong.bean.Cat;
import com.dingjiaxiong.bean.Mouse;
import com.dingjiaxiong.bean.MyImportSelector;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Import;

@Import(Mouse.class)
public class SpringConfig {

    @Bean
//    @ConditionalOnClass(name = "com.dingjiaxiong.bean.Wolf")
    @ConditionalOnBean(name = "com.dingjiaxiong.bean.Mouse")
    public Cat tom(){
        return new Cat();
    }

}

直接运行

 有老鼠,有猫

如果tom 只想“等” Jerry

package com.dingjiaxiong.config;

import com.dingjiaxiong.bean.Cat;
import com.dingjiaxiong.bean.Mouse;
import com.dingjiaxiong.bean.MyImportSelector;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Import;

@Import(Mouse.class)
public class SpringConfig {

    @Bean
    @ConditionalOnBean(name = "jerry")
    public Cat tom(){
        return new Cat();
    }

}

运行

 如果给Mouse 加上名

 现在的老鼠是Jerry 了,tom 就出来了

再来个场景,就算Jerry 在了,但是如果狗在,tom 还是不出来

package com.dingjiaxiong.config;

import com.dingjiaxiong.bean.Cat;
import com.dingjiaxiong.bean.Mouse;
import com.dingjiaxiong.bean.MyImportSelector;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Import;

@Import(Mouse.class)
public class SpringConfig {

    @Bean
    @ConditionalOnBean(name = "jerry")
    @ConditionalOnMissingClass("com.dingjiaxiong.bean.Dog")
    public Cat tom(){
        return new Cat();
    }

}

运行结果

 有狗,tom 不会出来

当然其实还有特别多可以进行判定的注解

 试试判断非web 项目@ConditionalOnNotWebApplication

 当然现在的配置都是写在配置类中使用@bean 注解

以后不一定要用这种格式,写类上

package com.dingjiaxiong.bean;

import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnNotWebApplication;
import org.springframework.stereotype.Component;


@Component("tom")
@ConditionalOnNotWebApplication
@ConditionalOnBean(name = "jerry")
public class Cat {
}

直接在类上也可以

配置类上扫一下@ComponentScan("类路径")

 运行看看

 有Jerry、非web,条件都满足,tom 就出现了

OK,回顾一下

 匹配指定类

 

 

 

 

 所以这个东西有什么用?

举个栗子,假如我现在要用数据库

先导入一个坐标

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.2.11</version>
</dependency>

 【以往的写法】

@Bean
public DruidDataSource dataSource(){
    return new DruidDataSource();
}

运行

 没啥问题,已经上来了

假如我现在不用数据库, 这个东西就没必要加载了

现在就可以整一个判定

@Bean
@ConditionalOnClass(name = "com.mysql.jdbc.Driver")
public DruidDataSource dataSource(){
    return new DruidDataSource();
}

运行结果

 这样就不会加载了

如果我们现在又加上MySQL 的驱动

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.30</version>
</dependency>

 再次运行

 就是这样

匹配指定环境

 

 

原理篇 1 自动配置 1.13 bean 依赖属性配置

1.13.1 环境准备

创建一个全新的Maven 工程模块

【添加坐标】

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
        <version>2.7.8</version>
    </dependency>
</dependencies>

【实体类】

package com.dingjiaxiong.bean;

public class Cat {
}
package com.dingjiaxiong.bean;

public class Mouse {
}

【运行类】

package com.dingjiaxiong;

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

@SpringBootApplication
public class App {

    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(App.class);
    }
}

基本环境就准备好了

1.13.2 bean 依赖属性配置

来一个业务层【拍猫和老鼠的动画片】

package com.dingjiaxiong.bean;

import org.springframework.stereotype.Component;

@Component
public class CartoonCatAndMouse {

    private Cat cat;
    private Mouse mouse;

    public void play(){
        System.out.println("3岁的tom和4岁的jerry打起来了");
    }
}

在运行程序中获取这个bean

package com.dingjiaxiong;

import com.dingjiaxiong.bean.CartoonCatAndMouse;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

@SpringBootApplication
public class App {

    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(App.class);

        CartoonCatAndMouse bean = context.getBean(CartoonCatAndMouse.class);
        bean.play();
    }

}

直接运行

 没啥问题,现在就是一个调用打印

现在的问题就是这句打印的数据都是一个字符串写死的,所以要换

先导一个lombok

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.24</version>
</dependency>

 修改实体类

package com.dingjiaxiong.bean;

import lombok.Data;

@Data
public class Cat {

    private String name;
    private Integer age;

}
package com.dingjiaxiong.bean;

import lombok.Data;

@Data
public class Mouse {

    private String name;
    private Integer age;

}

现在属性都有了

修改业务代码

package com.dingjiaxiong.bean;

import org.springframework.stereotype.Component;

@Component
public class CartoonCatAndMouse {

    private Cat cat;
    private Mouse mouse;

    public CartoonCatAndMouse() {
        cat = new Cat();
        cat.setName("tom");
        cat.setAge(3);

        mouse = new Mouse();
        mouse.setName("jerry");
        mouse.setAge(4);
    }

    public void play(){
        System.out.println(cat.getAge() + "岁的" + cat.getName() + "和" + mouse.getAge() + "岁的" + mouse.getName() + "打起来了");
    }

}

直接运行

 这样就不是写死的数据了而且实现相同的效果

现在问题就是,当前这样子写,如果想正常运行

 这些数据必须得告诉程序,但是如果有一天猫或者老鼠出意外了,导致咱们必须从外部修改这些“演员”,怎么做?

先来一个配置文件

cartoon:
  cat:
    name: tom
    age: 3

  mouse:
    name: jerry
    age: 4

在业务中读取值

package com.dingjiaxiong.bean;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Component
@Data
@ConfigurationProperties(prefix = "cartoon")
public class CartoonCatAndMouse {

    private Cat cat;
    private Mouse mouse;

    public void play(){
        System.out.println(cat.getAge() + "岁的" + cat.getName() + "" + mouse.getAge() + "岁的" + mouse.getName() + "打起来了");
    }

}

直接运行

 可以看到, 这样子数据也上来了

修改一下配置文件

 数据也正常替换了

但是如果我压根儿不给老鼠的数据

 没给就导致业务中的mouse 没有初始化,然后又直接使用,就这样儿了

而且现在的问题是

 现在这个类已经绑死了 yml 中的数据,如果yml 不提供数据,这个类就没法儿用了【这好像也不太行,有解决办法吗?】

答案是当然的

先来一个全新的类

package com.dingjiaxiong.bean;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Component
@Data
@ConfigurationProperties(prefix = "cartoon")
public class CartoonProperties {

    private Cat cat;
    private Mouse mouse;

}

之前的去使用这个属性类

package com.dingjiaxiong.bean;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Component
@Data

public class CartoonCatAndMouse {

    private Cat cat;
    private Mouse mouse;

    private CartoonProperties cartoonProperties;

    public CartoonCatAndMouse(CartoonProperties cartoonProperties) {
        this.cartoonProperties = cartoonProperties;
        cat = new Cat();
        cat.setName("tom");
        cat.setAge(3);

        mouse = new Mouse();
        mouse.setName("jerry");
        mouse.setAge(4);
    }

    public void play(){
        System.out.println(cat.getAge() + "岁的" + cat.getName() + "和" + mouse.getAge() + "岁的" + mouse.getName() + "打起来了");
    }

}

先直接打印看看, 

 说明现在这些值拿的还是本类构造方法中设置的值

改一下:三元表达式

cat.setName(StringUtils.hasText(cartoonProperties.getCat().getName()) ? cartoonProperties.getCat().getName() : "tom");

意思是如果属性类中有值,就用属性类的值对tom 进行覆盖

直接运行

 如果配置文件中没配猫的name

 就还是tom

现在达到的效果 就是如果配置里面设置 了新的,就用新的,如果没有,就用老的

现在我把整个猫都拿掉

 这样会报错,因为属性类中的cat 为空了,再去拿空的name 就报错了,所以要再加个条件

cat.setName(cartoonProperties.getCat() != null && StringUtils.hasText(cartoonProperties.getCat().getName()) ? cartoonProperties.getCat().getName() : "tom");

再次运行

 这样就行了,现在我仅仅是把猫的名字注掉

 还是可以用

按照这种判断形式,完善一下业务代码

package com.dingjiaxiong.bean;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

@Component
@Data

public class CartoonCatAndMouse {

    private Cat cat;
    private Mouse mouse;

    private CartoonProperties cartoonProperties;

    public CartoonCatAndMouse(CartoonProperties cartoonProperties) {
        this.cartoonProperties = cartoonProperties;
        cat = new Cat();
        cat.setName(cartoonProperties.getCat() != null && StringUtils.hasText(cartoonProperties.getCat().getName()) ? cartoonProperties.getCat().getName() : "tom");
        cat.setAge(cartoonProperties.getCat() != null && cartoonProperties.getCat().getAge() != null ? cartoonProperties.getCat().getAge() : 3);

        mouse = new Mouse();

        mouse.setName(cartoonProperties.getMouse() != null && StringUtils.hasText(cartoonProperties.getMouse().getName()) ? cartoonProperties.getMouse().getName() : "jerry");
        mouse.setAge(cartoonProperties.getMouse() != null && cartoonProperties.getMouse().getAge() != null ? cartoonProperties.getMouse().getAge() : 4);
        
    }

    public void play(){
        System.out.println(cat.getAge() + "岁的" + cat.getName() + "和" + mouse.getAge() + "岁的" + mouse.getName() + "打起来了");
    }

}

现在的效果,就是配置文件的值和属性中的值融合

 去掉一个单一属性

 就是这样

整个猫都拿掉

 如果全部拿掉

 现在就没有在真正的动画片类中去强制和配置文件绑死了【通过一个属性类解耦了】

 OK, 差不多就是这样 了,但还有个小问题,在属性类中

 加上这个注解后,这个属性类最终肯定会被加载成 bean

如果现在我们不用动画片那个业务类的了的话,这个属性类也就没有必要加载了

如果直接删掉这个注解

 可以看到,如果不是一个bean了,就不能读取配置文件中的数据了

【解决办法】 

 这样就可以把它强制整个一个bean了

OK, 这个问题解决了,顺带的问题又来了

 这个类现在也一定会被加载成bean,好家伙,也拿掉?

 拿掉之后,到运行类中使用@Import 进行加载

 这样达到的效果就是

 这两个东西现在都没有强制声明成bean 了

【但是现在能用吗?试试】 

 现在的感觉就是在运行类中,导入这样一个功能,

 就能用了

做了这么多步骤:

核心:

现在我们可以“合理”的读取配置文件中的数据了,配了我就读,没配我就默认
而且对于业务类和属性类,我都无需声明成bean,只需要在用的地方写清楚就行了
OK,回顾一下

将业务功能bean运行需要的资源抽取成独立的属性类(******Properties),设置读取配置文件信息 

 

 定义业务功能bean,通常使用@Import导入,解耦强制加载bean

 使用@EnableConfigurationProperties注解设定使用属性类时加载bean

 

 

原理篇 1 自动配置 1.14 自动配置思想

1.14.1 自动配置原理

一个大大的原因:

 

配置搞成自动的,我们就解放了,而且人家自动搞的都是正确的,我们又省心了

对于整个自动配置的出现过程,感觉其实就是一个符合正常人思维习惯的过程,不过说在咱们老是想而不做,而SpringBoot 把他做出来了。

【SpringBoot 怎么做的?】

收集Spring开发者的编程习惯,整理开发过程使用的常用技术列表——>(技术集A)【小本本抄下来】

收集常用技术(技术集A)的使用参数,整理开发过程中每个技术的常用设置列表——>(设置集B)【大家使用这些技术有些区别,配置常用参数记下来,例如Redis 端口6379、MongoDB…MySQL…】

【1.2 步前期调研】

初始化SpringBoot基础环境,加载用户自定义的bean和导入的其他坐标,形成初始化环境【做程序先做一个SpringBoot 】

将技术集A包含的所有技术都定义出来,在Spring/SpringBoot启动时默认全部加载【开发者想用啥里面就有啥】

将技术集A中具有使用条件的技术约定出来,设置成按条件加载,由开发者决定是否使用该技术(与初始化环境比对)【设置“激活方式”,满足方式才进行加载】

【4.5 就是这些东西我SpringBoot 都给你了,你爱用哪个用哪个,默认全部都有】

将设置集B作为默认配置加载(约定大于配置),减少开发者配置工作量【默认配置,不一定是我们真正使用的】

开放设置集B的配置覆盖接口,由开发者根据自身需要决定是否覆盖默认配置【默认的东西和开发搭不上,改】

原理篇 1 自动配置 1.15 自动配置原理【1】

1.15.1 看源码了

依赖一个程序来看,

package com.dingjiaxiong;

import com.dingjiaxiong.bean.CartoonCatAndMouse;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Import;

@SpringBootApplication
@Import(CartoonCatAndMouse.class)
public class App {

    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(App.class);

        CartoonCatAndMouse bean = context.getBean(CartoonCatAndMouse.class);
        bean.play();
    }
}

运行结果

 OK, 结果不重要

整个程序的开始

@SpringBootApplication

这个注解,点击进去看看

 可以看到它是若干个注解的组合注解

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

看这三个

 意思就是上面那个是下面3个的合体

点进第一个 SpringBootConfiguration

 又看@Configuration 里面

 主要看这个

然后@Indexed

 再看@EnableAutoConfiguration 【自动配置的开关】

点进去

 主要就是这两个

 看看@ AutoConfigurationPackage

 再看@ComponentScan

 点进去

 基本到头

 经过老师的勾勾选选

 最后剩了俩

 1.15.2 @Import({AutoConfigurationPackages.Registrar.class})

点进Registrar

 这个类的功能就是能够按照BeanDefinition 的形式去定义bean ,并且分情况处理

这一节太难做笔记了…

 对于这个方法进行断点调试

 现在这个方法貌似是获取了我的包

 原因就在于现在我的程序现在在com.dingjiaxiong 包下,这个程序作为整个应用的入口,它需要扫描它所在的包和其子包【扫哪儿就是这样得到的】

所以,@Import({AutoConfigurationPackages.Registrar.class}) 这个东西设置当前配置所在的包作为扫描包,后续要针对当前的包进行扫描【确认包信息】

原理篇 1 自动配置 1.16 自动配置原理【2】

1.16.1 看源码了

之前我们讲了一下//@Import({AutoConfigurationPackages.Registrar.class}) 这个东西在干嘛

 OK,下面来看第二个 //@Import({AutoConfigurationImportSelector.class})

1.16.2 @Import({AutoConfigurationImportSelector.class})

直接点进去

 

public class AutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware, ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered {

可以看到它实现了一堆接口

这些接口可以分为三大类:

BeanClassLoaderAware, ResourceLoaderAware, BeanFactoryAware, EnvironmentAware【资源发现】

解释下:只要有bean 实现了这些接口之一,那就可以在那个bean 使用 applicationContext 这个对象了

 拿到之后,就可以进行一系列操作了,比如在自定义的方法中去打印所有的bean
  • DeferredImportSelector

 这个接口在 ImportSelector 的基础上进行了扩展,【翻译:推迟的导入选择器】

 

  所以这个东西加载得一定比别人晚

 

  • Ordered【排序】

    点进去看看

 两个常量【最小值、最大值】,一个方法

设置在容器中的加载顺序

默认的加载顺序值【】

 

 很明显就是最大值 - 1

1.16.3 spring.factories 

 一个一个的名儿、对应一组一组的值

 

说实话,没看懂 

原理篇 1 自动配置 1.17 自动配置原理【3】

1.17.1 看源码了

【笔者用的2.7.4 ,感觉和老师的2.5.4 都已经差别很大了】

换个版本

 

 OK, 前面我们已经说到

它在这个文件中默认加载了很多的自动配置的功能

 比如说这儿的第一个就是它默认要加载的一个项

找出来看看这个配置的代码 

 现在在我们猫和老鼠的案例中 ,加入redis

没加之前的运行效果 

package com.dingjiaxiong;

import com.dingjiaxiong.bean.CartoonCatAndMouse;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.autoconfigure.*;
import org.springframework.boot.context.TypeExcludeFilter;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.context.annotation.Import;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Indexed;


@SpringBootApplication

//@SpringBootConfiguration
//        @Configuration
//                @Component
//        @Indexed
//@EnableAutoConfiguration
//        @AutoConfigurationPackage
//                @Import({AutoConfigurationPackages.Registrar.class})
//        @Import({AutoConfigurationImportSelector.class})
//@ComponentScan(excludeFilters = {
//                @ComponentScan.Filter(type = FilterType.CUSTOM, classes = {TypeExcludeFilter.class}),
//                @ComponentScan.Filter(type = FilterType.CUSTOM, classes = {AutoConfigurationExcludeFilter.class}
//        )}

//@Import({AutoConfigurationPackages.Registrar.class})
//@Import({AutoConfigurationImportSelector.class})


@Import(CartoonCatAndMouse.class)
public class App {

    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(App.class);

        String[] names = context.getBeanDefinitionNames();
        for (String name : names) {
            System.out.println(name);
        }

        CartoonCatAndMouse bean = context.getBean(CartoonCatAndMouse.class);
        bean.play();
    }
}

运行结果

 这一堆bean ,是没有redis的,现在我加个依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <version>2.7.8</version>
</dependency>

 OK,现在再试一次

 效果超级明显,这就说明它已经加载了所有与redis 有关的bean 了

 就是这句话生效了。

 再看看这个,好家伙,和我们之前的案例有原理相像啊

点进去看看

 嗯,它也没说它是个bean

 我的这个也没说,看看它的默认值

 这也是我们不配就能用的原因

 接着后面它又导入了两组客户端的实现【这就是一个对象里面包另一个对象了,想想我们写配置的时候】

再接着看

 如果我没有找到redisTemplate 这样一个bean,我就给你一个,如果你自己定义了,那我就不加载了,因为它怕加载重了

太细了

下面的StringRedisTemplate 也是一样的道理

 OK,

 1.17.2 小结

  1. 先开发若干种技术的标准实现
  2. SpringBoot启动时加载所有的技术实现对应的自动配置类
  3. 检测每个配置类的加载条件是否满足并进行对应的初始化
  4. 切记是先加载所有的外部资源,然后根据外部资源进行条件比对

原理篇 1 自动配置 1.18 自动配置原理

1.18.1 变更自动配置

既然这个自动配置这么好用,自己也想搞这种自动配置的东西,可以实现吗?

【答案是当然的】

看看MP 的自动配置类

 

 点开META-INF

 可以看到它也有这个东西

做自动配置方法其实很简单,就是在spring.factories 做配置就行了

【直接开干】

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.dingjiaxiong.bean.CartoonCatAndMouse

 OK, 现在其实就已经完成了

修改一下运行程序

 我现在不导入了,让它自动配置

 再整点儿花活儿

 我现在给它加点条件,意思有redis ,我才被加载

@ConditionalOnClass({RedisOperations.class})

但是我在pom 文件中又把redis 依赖给注掉

 很明显, 都红了,就不会自动配置了

这样看不出来效果,因为编译通不过了已经,恢复成全路径名

@ConditionalOnClass(name = "org.springframework.data.redis.core.RedisOperations")

再次执行运行类

 这次就加载不到了,自动配置失败

如果把坐标放开

 这样就又加载到了

这是加,那么问题又来了,现有的我们能不能把它去了【当然】

跟老师一样,

 修改配置文件:去掉taskExecutorBuilder

 

spring:
  autoconfigure:
    exclude: org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration

OK,再次运行,看看它还可以加载不

 没毛病

这是在配置文件中写

也可以在启动类上写

例如,

 我现在还想把它 也排除掉

@SpringBootApplication(excludeName = "org.springframework.boot.autoconfigure.task.TaskSchedulingAutoConfiguration")

 OK, 直接启动

 肯定就没有加载了

OK,回顾一下

 

 

1.18.2 小结

  1. 通过配置文件exclude属性排除自动配置
  2. 通过注解@EnableAutoConfiguration属性排除自动配置项
  3. 启用自动配置只需要满足自动配置条件即可
  4. 可以根据需求开发自定义自动配置项

1.18.3 总结

  1. bean加载方式(8+
  2. bean加载控制(编程 & 注解
  3. bean依赖属性配置(Properties
  4. 自动配置原理
  5. 变更系统自动配置(配置文件、注解属性
  6. 添加自定义自动配置(META-INF/spring.factories

 

原理篇 2 自定义starter 2.1 记录系统访客独立IP访问次数案例介绍

2.1 记录系统访客独立IP访问次数案例介绍

2.1.1 介绍

原始的项目不管做成一个什么样的程度,现在只要加入我们的这个starter,就能完成下面的功能

案例:记录系统访客独立IP访问次数

  1. 每次访问网站行为均进行统计
  2. 后台每10秒输出一次监控信息(格式:IP+访问次数)

OK,这次的工作也是基于08 SSMP 整合案例进行的

  还可以正常运行【把日志关掉】

 

 现在是没有任何统计信息的

老师做好的效果

 就是这样

 而且还可以进行自定义配置,好啊好啊

 

 极简模式就只显示IP 了,不显示次数

2.1.2 需求分析

  1、数据记录位置:Map / Redis
  2、功能触发位置:每次web请求(拦截器)
    ① 步骤一:降低难度,主动调用,仅统计单一操作访问次数(例如查询)
    ② 步骤二:开发拦截器
  3、业务参数(配置项)
    ① 输出频度,默认10秒
    ② 数据特征:累计数据 / 阶段数据,默认累计数据
    ③ 输出格式:详细模式 / 极简模式
  4、校验环境,设置加载条件
妙啊,开始开始

2.1.3 小结

案例:记录系统访客独立IP访问次数

原理篇 2 自定义starter 2.2 IP计数业务功能开发【自定义starter】

2.2 IP计数业务功能开发【自定义starter】

2.2.1 大概看看别人的starter

 命名虽然可以随便,但是还是尽量和人家的像一点

而且,这些starter 是分成两部分的

 这里面好像没有功能,在上面的自动配置里面

 org → … → data → redis

 这里面就有它的自动配置类 了

先把坐标定义出来, 然后做了一个工程

看看MP 的

 好像也是这样哈

【咱们就一个模块搞定,像druid 那样】

 2.2.2 直接开干

创建一个全新的SpringBoot 工程模块 

 依赖都不勾

 直接创建,上来先把SpringBoot 的版本改掉

 一个全新的SpringBoot 工程

大概修改一下pom 文件

不要测试依赖、也不要maven 插件

 把测试直接拿掉了,已经无意义了

 

 结构的空壳就起来了

【业务类】

先导入web 包

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

 

package cn.dingjiaxiong.service;

import org.springframework.beans.factory.annotation.Autowired;

import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;

public class IpCountService {

    private Map<String, Integer> ipCountMap = new HashMap<String, Integer>();

    @Autowired
    private HttpServletRequest httpServletRequest; //当前的request 对象的注入工作由使用当前starter的工程提供自动装配

    //调用这个方法,就可以统计ip的访问次数
    public void count() {
        System.out.println("==============================================");
        //每次调用当前操作,就记录当前访问的IP,然后累加访问次数
        //1. 获取当前操作的IP地址
        String ip = httpServletRequest.getRemoteAddr();
        //2. 根据IP地址从Map取值,并递增
        Integer count = ipCountMap.get(ip);
        if (count == null){
            ipCountMap.put(ip,1);
        }else{
            ipCountMap.put(ip,ipCountMap.get(ip) + 1);
        }
    }
}

 做业务就是这样了

【自动配置类】

package cn.dingjiaxiong.autoconfig;

public class IpAutoConfiguration {
}

【创建META-INF 的 spring.factories,让它自动配置】

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  cn.dingjiaxiong.autoconfig.IpAutoConfiguration

 实现一下配置类

package cn.dingjiaxiong.autoconfig;

import cn.dingjiaxiong.service.IpCountService;
import org.springframework.context.annotation.Bean;


public class IpAutoConfiguration {

    @Bean
    public IpCountService ipCountService(){
        return new IpCountService();
    }

}

现在我们已经可以直接在08 SSMP 中去使用这个starter 了

不过要先安装到仓库中,【先clean 再 install】

 在08 中导入坐标

<dependency>
    <groupId>cn.dingjiaxiong</groupId>
    <artifactId>ip_spring_boot_starter</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>

 这样这个模块就进来了

调用一下

修改controller

@Autowired
private IpCountService ipCountService;

@GetMapping("/{currentPage}/{pageSize}")
public R getPage(@PathVariable int currentPage, @PathVariable int pageSize,Book book) {

    ipCountService.count();

    IPage<Book> page = bookService.getPage(currentPage, pageSize,book);

    //如果当前页码值大于了总页码值,那么重新执行查询操作,使用最大页码值作为当前页码值
    if (currentPage > page.getPages()){
        page = bookService.getPage((int) page.getPages(),pageSize);
    }
    return new R(null != page, page);
}

为了更好的查看到效果

package cn.dingjiaxiong.service;

import org.springframework.beans.factory.annotation.Autowired;

import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;

public class IpCountService {

    private Map<String, Integer> ipCountMap = new HashMap<String, Integer>();

    @Autowired
    private HttpServletRequest httpServletRequest; //当前的request 对象的注入工作由使用当前starter的工程提供自动装配

    //调用这个方法,就可以统计ip的访问次数
    public void count() {

        //每次调用当前操作,就记录当前访问的IP,然后累加访问次数
        //1. 获取当前操作的IP地址
        String ip = httpServletRequest.getRemoteAddr();

        System.out.println("==============================================" + ip);

        //2. 根据IP地址从Map取值,并递增
        Integer count = ipCountMap.get(ip);
        if (count == null){
            ipCountMap.put(ip,1);
        }else{
            ipCountMap.put(ip,ipCountMap.get(ip) + 1);
        }
    }
}

改了下打印

记得重新clean 、安装一下

 直接启动SSMP

 效果很明显,已经拿到了。OK,这就说明程序已经跑通了

OK回顾一下

业务功能开发

 自动配置类

 

 

2.2.3 小结

  1. 使用自动配置加载业务功能
  2. 切记使用之前先clean后install安装到maven仓库,确保资源更新

原理篇 2 自定义starter 2.3 定时任务报表开发

2.3 定时任务报表开发

2.3.1 直接开干

之前我们已经把程序跑通了,

 这个数据就会存到map 集合里面,现在要做的就是展示

【定义方法展示数据】

先开启定时任务

 定义打印方法

@Scheduled(cron = "0/5 * * * * ?") // 定时任务
public void print(){
    System.out.println("         IP访问监控");
    System.out.println("+-----ip-address-----+--num--+");

    for (Map.Entry<String, Integer> entry : ipCountMap.entrySet()) {
        String key = entry.getKey();
        Integer value = entry.getValue();
        System.out.println(String.format("|%18s  |%5d  |",key,value));
    }

    System.out.println("+--------------------+-------+");
}

OK,clean + install

 OK, 再次启动SSMP

 效果很明显

现在就可以把调用那句打印删掉了,功能已经达成

 没毛病

OK,回顾一下

开启定时任务功能

 

2.3.2 小结

  1. 完成业务功能定时显示报表
  2. String.format()

 原理篇 2 自定义starter 2.4 使用属性配置设置功能参数【1】

2.4 使用属性配置设置功能参数【1】

2.4.1 直接开干

上一次咱们把功能基本上做好了,每5秒打印一次访问情况

 问题来了,现在这一组控制太死板了,固定的样式…固定的…

能不能灵活点儿?

【当然】

先来一个配置属性类

package cn.dingjiaxiong.properties;

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

@ConfigurationProperties(prefix = "tools.ip")
public class IpProperties {

    /*
    * 日志显示周期
    * */
    private Long cycle = 5L;

    /*
    * 是否周期内重置数据
    * */
    private Boolean cycleReset = false;

    /*
    * 日志输出格式 detail:详细模式 simple:极简模式
    * */
    private String model = LogModel.DETAIL.value;

    public enum LogModel{
        DETAIL("detail"),
        SIMPLE("simple");
        private String value;

        LogModel(String value) {
            this.value = value;
        }

        public String getValue() {
            return value;
        }
    }

    public Long getCycle() {
        return cycle;
    }

    public void setCycle(Long cycle) {
        this.cycle = cycle;
    }

    public Boolean getCycleReset() {
        return cycleReset;
    }

    public void setCycleReset(Boolean cycleReset) {
        this.cycleReset = cycleReset;
    }

    public String getModel() {
        return model;
    }

    public void setModel(String model) {
        this.model = model;
    }


}

【修改自动配置类】

 强制让配置属性类成为一个bean

【业务方法】

package cn.dingjiaxiong.service;

import cn.dingjiaxiong.properties.IpProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;

import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;

public class IpCountService {

    private Map<String, Integer> ipCountMap = new HashMap<String, Integer>();

    @Autowired
    private HttpServletRequest httpServletRequest; //当前的request 对象的注入工作由使用当前starter的工程提供自动装配

    //调用这个方法,就可以统计ip的访问次数
    public void count() {

        //每次调用当前操作,就记录当前访问的IP,然后累加访问次数
        //1. 获取当前操作的IP地址
        String ip = httpServletRequest.getRemoteAddr();

//        System.out.println("==============================================" + ip);

        //2. 根据IP地址从Map取值,并递增
        Integer count = ipCountMap.get(ip);
        if (count == null){
            ipCountMap.put(ip,1);
        }else{
            ipCountMap.put(ip,ipCountMap.get(ip) + 1);
        }
    }

    @Autowired
    private IpProperties ipProperties;

    @Scheduled(cron = "0/5 * * * * ?")
    public void print(){

        //判断显示模式
        if (ipProperties.getModel().equals(IpProperties.LogModel.DETAIL.getValue())){

            System.out.println("         IP访问监控");
            System.out.println("+-----ip-address-----+--num--+");

            for (Map.Entry<String, Integer> entry : ipCountMap.entrySet()) {
                String key = entry.getKey();
                Integer value = entry.getValue();
                System.out.println(String.format("|%18s  |%5d  |",key,value));
            }

            System.out.println("+--------------------+-------+");

        }else if (ipProperties.getModel().equals(IpProperties.LogModel.SIMPLE.getValue())){
            System.out.println("     IP访问监控");
            System.out.println("+-----ip-address-----+");

            for (String key : ipCountMap.keySet()) {
                System.out.println(String.format("|%18s  |",key));
            }

            System.out.println("+--------------------+");
        }

        //判断是否清理数据
        if (ipProperties.getCycleReset()){
            ipCountMap.clear();
        }
    }

    public static void main(String[] args) {
        new IpCountService().print();
    }

}

OK,直接clean + install

 OK,直接重启SSMP

 OK, 原来做好的功能不受影响

现在给它来点配置,第一个,测试清除数据

 重启运行

效果很明显,会被清除

测试模式切换 

 重启运行

 这下就不显示次数了,没毛病

OK,回顾一下

  • 定义属性类,加载对应属性

 

2.4.2 小结

  1. 使用属性修改自动配置加载的设置值 

原理篇 2 自定义starter 2.5 使用属性配置设置功能参数【2】

2.5 使用属性配置设置功能参数【2】

2.5.1 直接开干

上一节我们把【模式切换】、【是否清数据】两个配置加上了

 还剩了一个

cycle,输出频率【日志显示周期】

这个还真和另外两个不太一样

这个配置值的使用位置

 在注解里面,好家伙,这下知道不一般了

这里就不能用${} 的方式读取,有值还好说

 运行效果

如果没给值

 再次运行

 直接就挂掉了【就是读取不到数据】

虽然也有解决办法

@Scheduled(cron = "0/${tools.ip.cycle:5} * * * * ?")

5就是默认值

再次安装运行

 这样表面上是解决了,不一样

 很明显,压根儿就没用咱们的属性值【读取bean 中的属性值才是真正的解决方案】

先来个测试

package cn.dingjiaxiong.properties;

import org.springframework.stereotype.Component;

@Component("abc")
public class TestValue {

    private Integer cycle = 1;

    public Integer getCycle() {
        return cycle;
    }

    public void setCycle(Integer cycle) {
        this.cycle = cycle;
    }
}

这就是一个bean, 里面有一个属性值cycle,默认为1

 OK,直接启动这个服务,看看效果

 可以看出是可以的【即这个值可以加载上】

OK, 测试成功,记得恢复一下,现在要来真的了

先给配置属性类来个bean 名称

 现在我们已经明着说了它是一个 bean 了

在自动配置类中,就不用EnableConfigurationProperties 了

 注掉

仅仅让它加载就行了

 业务层

@Scheduled(cron = "0/#{ipProperties.cycle} * * * * ?")

 OK,安装测试

 安装完成,运行SSMP

 现在没配置值,默认就是用的属性值 5

OK,现在配一下

 再次运行

成功了

回顾一下

  • 配置信息

 

 

  

2.5.2 小结

  1. 配置调整

 

原理篇 2 自定义starter 2.6 拦截器开发

2.6.1 拦截器开发

OK, 到咱们之前为止,基本上所有的功能都做完了,

 剩下的问题就是,现在只能是调用了分页查询接口才计算访问次数,咱们也不能去源码中给所有的方法都加上这玩意儿【太low了】,所以拦截器来了

AOP思想,在所有方法运行前执行一下计算访问次数操作

但是现在是Web 工程,那就用拦截器实现

拦截器类

package com.dingjiaxiong.controller.interceptor;

import cn.dingjiaxiong.service.IpCountService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class IpCountInterceptor implements HandlerInterceptor {

    @Autowired
    private IpCountService ipCountService;

    //运行拦截之前
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        ipCountService.count();
        return true; // 拦截器是否向下执行,false默认不执行,一定要ture开启!
    }
}

 

controller 里面就别加了

 配置类

package com.dingjiaxiong.controller.interceptor;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class SpringMvcConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {    
        registry.addInterceptor(ipCountInterceptor()).addPathPatterns("/**"); // addInterceptor添加拦截器().addPathpatterns拦截器路径()

    }

   // 拦截器对象 @Bean
public IpCountInterceptor ipCountInterceptor(){ return new IpCountInterceptor(); } }

OK,直接运行

 OK,现在只要发了请求,不管是什么,都会被计数了

【现在就完成了…吗?】

不,现在的问题是,一旦我把坐标注掉

 再次运行

 直接就红了

所以还是要修改一下

先恢复坐标【其实问题就是拦截器的位置不对】

直接复制粘贴

 SSMP 中的拦截器就不要了

 注意还要配置一下加载配置类

 OK,再次安装运行

 OK, 这样就可以了,现在把坐标注掉

 也不影响工程运行

OK,回顾一下

  • 自定义拦截器

 

2.6.2 小结

  1. 拦截器开发

原理篇 2 自定义starter 2.7 开启yml 提示功能

2.7.1 问题引入

之前我们又完成了拦截器的开发,现在的程序只要加入坐标,就可以使用IP计数,注掉坐标就可以恢复如初

 但是现在还有个问题

在我们导入坐标后

 去配置文件中书写配置时

 压根儿没提示,这也太不友好了

【有办法解决吗?当然】:用谁,就在谁那里进行设置

2.7.2 开启yml提示功能

先在starter 中加个坐标,配置处理器

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

 现在我们先clean 一下

 再安装

 注意看这个json 文件

把它复制到工程中

 现在看看在配置文件中书写配置

 出了两组的原因

 现在看看在配置文件中书写配置

 出了两组的原因

 所以现在咱们把starter 给别人用的时候

 再次看看配置文件

 OK, 现在就只有一组了

笔者这里文档注释写错了

 写成多行注释了

改一下

 OK, 重新生成一下那个json

 这次就是拿的文档注释在后面跟着提示了

OK,现在真正的安装,看看在SSMP 中的效果

 安装成功

 没毛病

现在就有个问题了

 这玩意儿本来是个,枚举,但是不给提示,不好填啊,

【再优化一下】

 这里面就可以写提示

  "hints": [
    {
      "name": "tools.ip.model",
      "values": [
        {
          "value": "detail",
          "description": "详细模式."
        },
        {
          "value": "simple",
          "description": "极简模式."
        }
      ]
    }
  ]

 OK,这样写好后,直接在starter 的yml 中看看

 没毛病

再重新安装,看看SSMP 中的效果

 OK, 没毛病!!!

回顾一下

  • 导入配置处理器坐标

 

 

 

 原理篇 3 核心原理 3.1 SpringBoot程序启动过程思想解析

3.1.1 SpringBoot 启动流程

Spring 程序的核心:容器【容器做出来,把需要管理的bean 告诉它就行了】

在Spring 程序的运行过程中,都是通过获取bean,然后让bean 去执行各种操作

  1. 初始化各种属性,加载成对象【准备工作】
  • 读取环境属性(Environment)
  • 系统配置(spring.factories)
  • 参数(Arguments、application.properties)
  1. 创建Spring容器对象ApplicationContext,加载各种配置
  2. 在容器创建前,通过监听器机制,应对不同阶段加载数据、更新数据的需求
  3. 容器初始化过程中追加各种功能,例如统计时间、输出日志等

3.1.2 小结

  1. 初始化数据
  2. 创建容器

 

原理篇 3 核心原理 3.2 启动流程【1】

3.2.1 环境准备

创建一个全新的模块工程 

 依赖都不勾,直接创建

 上来先把SpringBoot 的版本改到2.5.4 ,和李老师一样

 OK,一个全新的SpringBoot 工程【版本 2.5.4】

其实还可以再简单点儿, 测试依赖不要、maven 插件不要

OK, 干干净净

3.2.2 启动流程

看到启动类

SpringApplication.run(Springboot30StartupApplication.class, args);

这一行首先运行,通过SpringApplication 类调用它的一个静态方法

点击run 方法

 

public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) {
    return run(new Class[]{primarySource}, args);
}

注意这个 primarySource

 指的就是这个

现在意思就是运行了

return run(new Class[]{primarySource}, args);

这段代码

再点进这个run 方法

return (new SpringApplication(primarySources)).run(args);

可以看到它直接就下来了

这是两个纯调用

然后这个run 是new 了一个对象,然后让这个对象又去调了一个run 方法

SpringApplication(primarySources)

先看这个new 出来的东西【它的作用就是:加载各种配置信息,初始化各种配置对象】

(new SpringApplication(primarySources)).run(args);

后面这个run 【作用:初始化容器】

原理篇 3 核心原理 3.5 启动流程【4】【5】【6】

3.5.1 看源码咯 

 OK, 接着上次我们已经把

 这个东西了解得差不多了 

 就是前面这一段,它初始化了SpringBoot 程序的启动的所有相关配置

接下来就是看后面的run 方法了,点进去

 运行整个过程 → 初始化容器【但是过程没那么简单】

ConfigurableApplicationContext

这是run 方法的返回值

 一行一行的来看

StopWatch stopWatch = new StopWatch();

计时器

 

 就是这样来的【所以,之前的初始化操作都没有算到显示给我们看的那个时间中【假象】】

【设置了计时器】

stopWatch.start();

【计时器开始】

DefaultBootstrapContext bootstrapContext = this.createBootstrapContext();

【系统引导信息对应的上下文对象】

ConfigurableApplicationContext context = null;

【定义了一个对象】

this.configureHeadlessProperty();

【模拟输入输出信号,避免出现因缺少外设导致的信号传输失败,进而引发错误(模拟显示器、键盘、鼠标…)】

【java.awt.headless = true;】

SpringApplicationRunListeners listeners = this.getRunListeners(args);

【获取当前注册的可运行的监听器】

listeners.starting(bootstrapContext, this.mainApplicationClass);

【监听器执行了对应的操作步骤】

到这里其实还是一些准备性的工程,还没开始创建容器

ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
【获取参数】

ConfigurableEnvironment environment = this.prepareEnvironment(listeners, bootstrapContext, applicationArguments);

【将前期读取的数据加载成了一个环境对象,用来描述信息】

this.configureIgnoreBeanInfo(environment);

【做了一个配置,备用】

Banner printedBanner = this.printBanner(environment);

【初始化启动图标】

context = this.createApplicationContext();

【创建容器对象,根据前期配置的容器类型进行判定并创建】

context.setApplicationStartup(this.applicationStartup);

【设置启动模式】

this.prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);

【对容器进行设置,参数来源于前期的设定】

this.refreshContext(context);

【刷新容器环境】

this.afterRefresh(context, applicationArguments);

【刷新完毕后,做后处理】

stopWatch.stop();

【计时结束】

if (this.logStartupInfo) {
    (new StartupInfoLogger(this.mainApplicationClass)).logStarted(this.getApplicationLog(), stopWatch);
}

【判定是否记录启动时间的日志】【创建日志对应的对象,输出日志信息,包含启动时间】

listeners.started(context);

【监听器执行了对应的操作步骤】

listeners.running(context);

【监听器执行了对应的操作步骤】

 到这儿就结束了

OK, 回顾一下

初始化各种属性,加载成对象
读取环境属性(Environment)
系统配置(spring.factories)
参数(Arguments、application.properties)
创建Spring容器对象ApplicationContext,加载各种配置
在容器创建前,通过监听器机制,应对不同阶段加载数据、更新数据的需求
容器初始化过程中追加各种功能,例如统计时间、输出日志等
【两个阶段】

【监听器类型】

在应用运行但未进行任何处理时,将发送 ApplicationStartingEvent。
当Environment被使用,且上下文创建之前,将发送 ApplicationEnvironmentPreparedEvent。
在开始刷新之前,bean定义被加载之后发送 ApplicationPreparedEvent。
在上下文刷新之后且所有的应用和命令行运行器被调用之前发送 ApplicationStartedEvent。
在应用程序和命令行运行器被调用之后,将发出 ApplicationReadyEvent,用于通知应用已经准备处理请求。
启动时发生异常,将发送 ApplicationFailedEvent

3.5.2 总结

理解过程有助于思考