SpringBoot源码(1)-基础


一、依赖

依赖

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.3.4.RELEASE</version>
</parent>

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

简化部署

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

把项目打成jar包,直接在目标服务器执行即可。

注意点:

  • 取消掉cmd的快速编辑模式

1、依赖管理

  • 父项目做依赖管理
<!--依赖管理-->    
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.3.4.RELEASE</version>
</parent>

<!--他的父项目-->
<!--几乎声明了所有开发中常用的依赖的版本号,自动版本仲裁机制-->
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-dependencies</artifactId>
    <version>2.3.4.RELEASE</version>
</parent>
  • 开发导入starter场景启动器
<!--
1、见到很多 spring-boot-starter-* : *就某种场景
2、只要引入starter,这个场景的所有常规需要的依赖我们都自动引入
3、SpringBoot所有支持的场景
https://docs.spring.io/spring-boot/docs/current/reference/html/using-spring-boot.html#using-boot-starter
4、见到的  *-spring-boot-starter: 第三方为我们提供的简化开发的场景启动器。
5、所有场景启动器最底层的依赖
-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
    <version>2.3.4.RELEASE</version>
    <scope>compile</scope>
</dependency>
  • 无需关注版本号,自动版本仲裁
1、引入依赖默认都可以不写版本
2、引入非版本仲裁的jar,要写版本号。
  • 可以修改默认版本号
<!--
1、查看spring-boot-dependencies里面规定当前依赖的版本 用的 key。
2、在当前项目里面重写配置
-->
<properties>
    <mysql.version>5.1.43</mysql.version>
</properties>

2、完整的pom文件

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.rewind</groupId>
    <artifactId>test</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>test</name>
    <description>测试模块</description>
    <packaging>jar</packaging>  <!--添加这一行-->

    <properties>
        <java.version>1.8</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <spring-boot.version>2.3.7.RELEASE</spring-boot.version>
    </properties>

    <dependencies>

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

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>2.3.7.RELEASE</version>
                <configuration>
                    <mainClass>com.rewind.test.TestApplication</mainClass>
                </configuration>
                <executions>
                    <execution>
                        <id>repackage</id>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

</project>

二、自动配置原理

1、自动配置

  • 自动配好 Tomcat

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

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

    • SpringBoot 帮我们配置好了所有web开发的常见场景
  • 默认的包结构

    • 主程序所在包及其下面的所有子包里面的组件都会被默认扫描进来
    • 无需以前的包扫描配置
    • 想要改变扫描路径,@SpringBootApplication(scanBasePackages="com.rewind")
      • 或者@ComponentScan 指定扫描路径
@SpringBootApplication
//等同于
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan("com.rewind.boot")
  • 各种配置拥有默认值

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

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

三、组件添加注解

1、@Configuration

该注解将类标注为配置类 (等同于spring 项目的配置文件),同时也注入到容器中,该类可用来注入其他的Bean

添加了 @Configuration 注解的配置类只有一个有参构造时,该构造方法的参数均来源于容器

2、@Bean

单例Bean的注入

// 该注解标注了该类为配置类,即相当于Spring的配置文件的作用
@Configuration
public class MyConf {

    // 该注解用于加载bean,相当于Spring的配置文件的bean标签
    // 默认bean的名称为方法名,也可通过value属性指定bean的名称
    @Bean
    public Person person(){
        return new Person();
    }

    // 自定义beanName
    @Bean(value = "person2")
    public Person getPerson(){
        return new Person();
    }

}

测试代码

@SpringBootApplication
public class TestApplication {

    public static void main(String[] args) {
        // 1、获取ioc容器
        ConfigurableApplicationContext applicationContext = SpringApplication.run(TestApplication.class, args);

        // 2、获取容器中所有的bean的名称
        String[] beanDefinitionNames = applicationContext.getBeanDefinitionNames();
        for (String beanDefinitionName : beanDefinitionNames) {
            System.out.println(beanDefinitionName);
        }

        // 3、获取指定的bean
        Person person = applicationContext.getBean("person", Person.class);
        Person person1 = applicationContext.getBean("person", Person.class);
        System.out.println(person == person1);  //true,默认单例
    }

}

参数

@Bean标注的方法的参数,是从容器中找到相应的类型的bean进行赋值的

@Bean
@ConditionalOnBean(MultipartResolver.class)  //容器中有这个类型组件
@ConditionalOnMissingBean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME) //容器中没有这个名字 multipartResolver 的组件
public MultipartResolver multipartResolver(MultipartResolver resolver) {
    //给@Bean标注的方法传入了对象参数,这个参数的值就会从容器中找。
    //SpringMVC multipartResolver。防止有些用户配置的文件上传解析器不符合规范
    return resolver;
}

3、@Scope

@Scope 注解的目的是用来调节作用域

可用于方法和类上,一般和@Component@Bean等配合使用,用于调节bean的作用域。

//多实例,IOC容器启动创建的时候,并不会创建对象放在容器在容器当中,当你需要的时候,需要从容器当中取该对象的时候,就会创建。
@Scope("prototype")

//单实例 IOC容器启动的时候就会调用方法创建对象,以后每次获取都是从容器当中拿同一个对象(map当中)。
@Scope("singleton")

//同一个请求创建一个实例,也就是会为每个Http请求创建一个对象,请求结束后该对象生命周期结束。
@Scope("request")

//同一个session创建一个实例,对于每次HTTP Session(同一用户),使用session定义的Bean都将产生一个新实例。
@Scope("session")

//表示这个bean在全局http session中是单例的,也就是说在全局会话中的bean是同一个实例。这个值需要spring是在web应用中的。
@Scope("global session")

案例如下:获取多例bean的方式

4、获取多例bean方式

(1)@Scope(“prototype”)

通过Spring管理的类,默认是单例模式,但是如果有的类需要使用独立的属性,则需要配置为多例模式的. 但是多例模式不仅仅只是加一个声明,使用 @Autowired 进行注入,可能并不会是你想要的结果。因为多例模式的类是需要单独调用的。

需要多例的类上加上注解 @Scope("prototype")

@Configuration
public class MyConf {

    @Bean
    @Scope("prototype")     //注解标注bean为多例
    public Person person(){
        return new Person();
    }
}

此时在通过上面的测试代码测试时,发现多次获得的 person 不是同一个,即返回 false

错误调用方式

@RestController
public class TestController {

    @Autowired
    private Person person;

    @GetMapping("person")
    public int person(){
        return person.hashCode();
    }
}

不论调用多少次person方法,返回值都是一样的,为啥@Scope("prototype")没生效,

原因:controller 默认是单例的,所以无论多少个请求,只会 @Autowired 一次,所以其实也只是从容器中获取了一次bean而已。

正确调用方式应该每次都从容器中获取 bean,详见下面的使用多例bean。

(2)proxyBeanMethods

Full模式与Lite模式

属性proxyBeanMethods:代理bean的方法

  •  `Full(proxyBeanMethods = true)`【默认值】【保证每个@Bean方法被调用多少次返回的组件都是单实例的】
  •  `Lite(proxyBeanMethods = false)`【每个@Bean方法被调用多少次返回的组件都是新创建的】

最佳实战

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

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

/**
 * 1、配置类里面使用@Bean标注在方法上给容器注册组件,默认也是单实例的
 * 2、配置类本身也是组件
 * 3、proxyBeanMethods:代理bean的方法
 * 组件依赖必须使用Full模式默认。其他Lite模式
 */
@Configuration(proxyBeanMethods = false) 
public class MyConfig {

    /**
     * Full:外部无论对配置类中的这个组件注册方法调用多少次获取的都是之前注册容器中的单实例对象
     * @return
     */
    @Bean //给容器中添加组件。以方法名作为组件的id。返回类型就是组件类型。返回的值,就是组件在容器中的实例
    public User user01(){
        User zhangsan = new User("zhangsan", 18);
        //user组件依赖了Pet组件
        zhangsan.setPet(tomcatPet());
        return zhangsan;
    }

    @Bean("tom")
    public Pet tomcatPet(){
        return new Pet("tomcat");
    }
}


//########## @Configuration测试代码如下 #########
@SpringBootApplication
public class MainApplication {

    public static void main(String[] args) {
        //1、返回我们IOC容器
        ConfigurableApplicationContext run = SpringApplication.run(MainApplication.class, args);

        //2、查看容器里面的组件
        String[] names = run.getBeanDefinitionNames();
        for (String name : names) {
            System.out.println(name);
        }

        //3、从容器中获取组件
        Pet tom01 = run.getBean("tom", Pet.class);
        Pet tom02 = run.getBean("tom", Pet.class);
        System.out.println("组件:"+(tom01 == tom02));


        //4、
        MyConfig bean = run.getBean(MyConfig.class);
        System.out.println(bean);

        //如果@Configuration(proxyBeanMethods = true)代理对象调用方法。SpringBoot总会检查这个组件是否在容器中有。
        //保持组件单实例
        User user = bean.user01();
        User user1 = bean.user01();
        System.out.println(user == user1);


        User user01 = run.getBean("user01", User.class);
        Pet tom = run.getBean("tom", Pet.class);

        System.out.println("用户的宠物:"+(user01.getPet() == tom));
    }
}

(3)使用多例bean

在我们的Spring程序中,可以将bean的依赖关系简单分为四种:

  • 单例bean依赖单例bean;
  • 多例bean依赖多例bean;
  • 多例bean依赖单例bean;
  • 单例bean依赖多例bean;

前三种依赖关系都很好解决,Spring容器会帮我们正确地处理,唯独第四种——单例bean依赖多例bean,Spring容器无法帮我们得到想要的结果。

解决这个问题可以让spring容器放弃注入多例对象,由我们自己管理多例对象的注入,spring中提供了两种自己注入的方法

  • 每次工厂注入
  • 使用方法注入

每次工厂注入

@Component
public class TestComponent implements ApplicationContextAware {

    private ApplicationContext context;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        context = applicationContext;
    }

    public Person test(){
        return  (Person) context.getBean("person");
    }
}

上面的做法不太好。Spring的一个很大的优点就是,它侵入性很低,我们在自己编写的代码中,几乎看不到Spring的组件,一般只会有一些注解。但是上面的代码中,却直接耦合了Spring容器,将容器存储在类中,并显式地调用了容器的方法,这不仅增加了Spring的侵入性,也让我们的代码变得不那么容易管理,也变得不再优雅。

@Lazy

@Component
public class TestComponent implements ApplicationContextAware {

    @Lazy
    @Autowired
    private Person person;
}

在单例对象中注入多例对象时,因为单例对象只初始化了一次所以单例对象里面的多例对象一直时同一个对象;当加上 @Lazy 注解时,注入的是多例 Bean 的代理对象,虽然这个代理对象不会变,但是每次调用这个代理对象的方法时,这个代理对象都会创建一个目标对象

proxyMode

@Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS)
@Component

ObjectFactory 对象工厂


@Autowired
private ObjectFactory<Person> person;

...

Person p = person.getObject();    

5、@Import

在平时看源码或者很多配置类上面都会出现 @Import 注解,功能就是和Spring XML 里面的一样,@Import 注解是用来导入配置类或者一些需要前置加载的类。

// 给容器中自动创建出这两个类型的组件、默认组件的名字就是全类名
@Import({Person.class, DBHelper.class})
@Configuration
public class MyConfig {
}

如果 @Import 的bean已经被其他方式注入到容器中了,那么容器中会存在两个相同类型的bean,但是beanName不同

com.rewind.test.entity.Person    //@Import导入的
person                            //其他方式导入的

@Import有三种用法:

  • .class 数组方式(如上文的使用方式)
  • ImportSelector 方式(Spring Boot底层采用比较得多的方式);
  • ImportBeanDefinitionRegistrar 方式

(1).class 数组方式

// 给容器中自动创建出这两个类型的组件、默认组件的名字就是全类名
@Import({Person.class, DBHelper.class})
@Configuration
public class MyConfig {
}

Person 可以是普通类,也可以是配置类

(2)ImportSelector

这种方式要写个类实现 ImportSelector 接口,重写 selectImports() 方法,返回的是要导入到容器中的类的全类名数组。

假设有个要加入IOC容器的类TestC……

public class TestImportSelector implements ImportSelector {
    @Override
    public String[] selectImports(AnnotationMetadata annotationMetadata) {
        return new String[]{"com.rewind.model.TestC"};
    }
}

然后用 @Import 注解导入这个类就行了

@Import({TestImportSelector.class})
@Configuration
public class ImportConfig {
}

(3)ImportBeanDefinitionRegistrar

这种方式和前一种方式类似,也是写个类实现 ImportBeanDefinitionRegistrar 接口,重写 registerBeanDefinitions() 方法,不过这种方式可以自定义Bean在容器中的名称。

也是假设有个要加入IOC容器的类TestD

public class TestImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
    @Override
    public void registerBeanDefinitions(AnnotationMetadata annotationMetadata, BeanDefinitionRegistry registry) {
        RootBeanDefinition rootBeanDefinition = new RootBeanDefinition(TestD.class);
        //自定义注册bean
        registry.registerBeanDefinition("testD1111",rootBeanDefinition);
    }
}

再然后就像上面一样用 @Import 注解导入这个类就行了

@Import({TestImportBeanDefinitionRegistrar.class})
@Configuration
public class ImportConfig {}

(4)三种方法总结

三种用法方式都可在一个 @Import 中使用,需要注意的是第一种、第二种方式在IOC容器中bean名称是类的全类名,而第三种方式是自定义的名称。

@Import({TestA.class,
        TestB.class,
        TestImportSelector.class,
        TestImportBeanDefinitionRegistrar.class})
@Configuration
public class ImportConfig {}

第一种用于注入少量 bean

第二种用于注入比较多的bean

第三种用于注入需要自定义beanName的情况

6、@Conditional

条件装配:满足Conditional指定的条件,则进行组件注入

image-20220102170452812
  • @ConditionalOnProperty:当配置文件存在某个属性的值为指定值时
  • @ConditionalOnBean(Person.class):当容器中存在某个bean时
  • @ConditionalOnMissingBean(Person.class):当容器中不存在某个bean时
  • @ConditionalOnClass(Person.class):当某个类存在时
  • @ConditionalOnMissingClass(Person.class) :当某个类不存在时
  • @ConditionalOnExpression:判断表达式
  • @ConditionalOnJava(value = JavaVersion.EIGHT,range = ConditionalOnJava.Range.EQUAL_OR_NEWER):当 jdk 版本大于或等于 1.8 时
  • @ConditionalOnWebApplication:当项目为web项目时
  • @ConditionalOnNotWebApplication:当项目不为web项目时
  • @ConditionalOnSingleCandidate:当容器中存在某个bean,并且为单例时;或是多例,但是有一个 primary 候选项被指定(通常在类上使用 @Primary 注解)
  • @ConditionalOnResource(resources = "/lockback.xml"):指定资源(配置文件,静态资源等)是否存在于 classpath 中
  • @ConditionalOnJndi("java:comp/env/foo"):只有指定的资源通过 JNDI 加载后才加载 bean
  • @ConditionalOnCloudPlatform:只有运行在指定的云平台上才加载指定的 bean

使用方法

  • 都可以应用在 TYPE 上,也就是说,Spring 自动扫描的一切类 (@Configuration, @Component, @Service, @Repository, or @Controller) 都可以通过添加相应的 @ConditionalOnXxxx 来判断是否加载
  • 都可以应用在 METHOD 上,所以有 @Bean 标记的方法也可以应用这些注解
  • 都是用了 @Conditional 注解来标记,OnBeanCondition 等自定义 Condition 还是实现了 Condition 接口的

(1)@ConditionalOnProperty

该注解使用频率最高

@ConditionalOnProperty(value = "mybean.enable",havingValue = "true",matchIfMissing = true)

application.propertiesapplication.yml 文件中 mybean.enable 为 true 才会加载这个 Bean,如果没有匹配上也会加载,因为 matchIfMissing = true,默认值是 false。

  • havingValue:当 value 指定的属性等于该值时加载 bean
  • matchIfMissing:当属性值不存在时,是否加载 bean

(2)@ConditionalOnExpression

如果我们有更复杂的多个配置属性一起判断,那么我们就可以用这个表达式了:

@ConditionalOnExpression("${myBean.enable:true} and ${other.bean:true}")

只有当两个属性都为 true 的时候才加载

其中 :true 就是: 如果没有为该属性设置值,则为该属性设置默认值true, 其实这就是@Vaue 注解的规范

(3)组合条件

好多时候,一个条件不能满足我们的要求,我们想多个条件一起使用,这就要求我们将他们组合起来去

AND

如果我们想多个条件一起应用,并且条件的关系是 and,我们只需要在类上使用多个@ConditionalOnXxxx 就可以了 ,当然也可以继承 AllNestedConditions类封装我们多个条件

OR

如果我们希望组合的条件是 or 的关系,我们可以通过继承 AnyNestedCondition 来完成

NONE

有 and 和 or 就肯定有 non(非),我们可以通过继承 NoneNestedConditions 完成这一要求

组合条件的使用

定义组合条件类

public class BeanAndProperty extends AllNestedConditions {

    /*public BeanAndProperty(ConfigurationPhase configurationPhase) {
        super(configurationPhase);
    }*/

    public BeanAndProperty() {
        super(ConfigurationPhase.REGISTER_BEAN);
    }

    @ConditionalOnBean(Person.class)
    static class OnBean{}

    @ConditionalOnProperty(value = "myConf.person",havingValue = "true")
    static class OnPropery{}
}

使用方式一:直接使用

@Bean(value = "person2")
@Conditional(BeanAndProperty.class)
public Person getPerson(){
    return new Person();
}

使用方式二:通过自定义注解使用

自定义注解

@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(BeanAndProperty.class)
public @interface ConditionalOnBeanAndProperty {
}

使用

@Bean(value = "person2")
@ConditionalOnBeanAndProperty
public Person getPerson(){
    return new Person();
}

(4)自定义条件类

例子:

首先,两个Condition,判断当前系统是否是Windows或者Linux(True False)

ConditionConfig使用了Java配置与@Conditional注解,根据LinuxCondition,或者WindowsCondition作为判断条件

LinuxConditionWindowsCondition

import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;

public class LinuxCondition implements Condition {

    @Override
    public boolean matches(ConditionContext arg0, AnnotatedTypeMetadata arg1) {
        return arg0.getEnvironment().getProperty("os.name").contains("Linux");
    }

}
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;

public class WindowsCondition implements Condition {

    @Override
    public boolean matches(ConditionContext arg0, AnnotatedTypeMetadata arg1) {
        return arg0.getEnvironment().getProperty("os.name").contains("Windows");
    }

}

使用

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ConditionConfig {

    @Bean
    @Conditional(WindowsCondition.class)
    public ListService windowsListService() {
        return new WindowsListService();
    }

    @Bean
    @Conditional(LinuxCondition.class)
    public ListService linuxListService() {
        return new LinuxListService();
    }

}

四、原生配置文件引入

1、@ImportResource

允许在 SpringBoot 中以spring配置文件的方式对组件进行注册。

主要是为了兼容第三方,注入IOC

beans.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"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">

    <bean id="haha" class="com.atguigu.boot.bean.User">
        <property name="name" value="zhangsan"></property>
        <property name="age" value="18"></property>
    </bean>

    <bean id="hehe" class="com.atguigu.boot.bean.Pet">
        <property name="name" value="tomcat"></property>
    </bean>
</beans>

测试代码

@ImportResource("classpath:beans.xml")
public class MyConfig {}

======================测试=================
boolean haha = run.containsBean("haha");
boolean hehe = run.containsBean("hehe");
System.out.println("haha:"+haha);//true
System.out.println("hehe:"+hehe);//true

五、配置绑定

0、原生java读取配置文件

如何使用Java读取到properties文件中的内容,并且把它封装到JavaBean中,以供随时使用;

public class getProperties {
     public static void main(String[] args) throws FileNotFoundException, IOException {
         Properties pps = new Properties();
         pps.load(new FileInputStream("a.properties"));
         Enumeration enum1 = pps.propertyNames();//得到配置文件的名字
         while(enum1.hasMoreElements()) {
             String strKey = (String) enum1.nextElement();
             String strValue = pps.getProperty(strKey);
             System.out.println(strKey + "=" + strValue);
             //封装到JavaBean。
         }
     }
 }

过于麻烦,所以才会有以下的注解

1、@ConfigurationProperties

用于将配置文件的属性值绑定到指定的bean上

配置文件

car:
  name: 自行车
  price: 400

bean

@Data
@Component    // 未用@EnableConfigurationProperties引入时,需要添加该注解
@ConfigurationProperties(prefix = "car")
public class Car {
    private String name;
    private Integer price;
}

2、@EnableConfigurationProperties

当需要绑定配置的类在源码中时,有 @ConfigurationProperties 注解,但是没有@Component 注解时,可用 @EnableConfigurationProperties 实现配置绑定并注入。该注解必须在启动类或是配置类上。

  • 1、开启Car配置绑定功能
  • 2、把这个Car这个组件自动注册到容器中
@Configuration
@EnableConfigurationProperties(Car.class)
public class MyConfig {
}

3、yml配置提示

自定义的类和配置文件绑定一般没有提示。

userName 会转化为 user-name

要加上以下的依赖

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


<!-- 作用:不打包以上依赖 -->
<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>

六、自动配置原理

0、自动配置

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

实际上也是一个 @Configuration 而已;区别是它用于标识启动类。

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

2、@ComponentScan

指定包扫描

3、@EnableAutoConfiguration

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

    /**
     * 在启用自动配置时可用于覆盖的环境属性。
     */
    String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";

    /**
     * 排除特定的自动配置类,这样它们将永远不会被应用。
     * @return 要排除的类
     */
    Class<?>[] exclude() default {};

    /**
     * 排除特定的自动配置类名,这样它们将永远不会被应用。
     * @return 要排除的类名
     * @since 1.3.0
     */
    String[] excludeName() default {};

}

(1)@AutoConfigurationPackage

指定了默认的包规则

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

    String[] basePackages() default {};

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

}

AutoConfigurationPackages.Registrar

/**
* 利用Registrar给容器中导入一系列组件
* 将指定的一个包下的所有组件导入进来,MainApplication 所在包下。
*/
static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {

    /**
    * 注册bean
    * 参数一:AutoConfigurationPackage注解的源信息
    * 
    */
    @Override
    public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
        register(registry, new PackageImports(metadata).getPackageNames().toArray(new String[0]));
    }

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

}

new PackageImports(metadata).getPackageNames()即为启动类的包路径

(2)@Import(AutoConfigurationImportSelector.class)

/**
* 给容器中批量导入一些组件
*/
protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
    if (!isEnabled(annotationMetadata)) {
        return EMPTY_ENTRY;
    }
    AnnotationAttributes attributes = getAttributes(annotationMetadata);
    // 获取到所有需要导入到容器中的配置类 默认127个,底层见下一段代码
    List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
    configurations = removeDuplicates(configurations);
    Set<String> exclusions = getExclusions(annotationMetadata, attributes);
    checkExcludedClasses(configurations, exclusions);
    configurations.removeAll(exclusions);
    configurations = getConfigurationClassFilter().filter(configurations);
    fireAutoConfigurationImportEvents(configurations, exclusions);
    return new AutoConfigurationEntry(configurations, exclusions);
}

1、利用 getAutoConfigurationEntry(annotationMetadata); 给容器中批量导入一些组件

2、获取到所有需要导入到容器中的配置类

protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
    // 底层见下一段代码
    List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
                                                                         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;
}
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
    String factoryTypeName = factoryType.getName();
    return (List)loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList());
}
// 实际执行加载组件
private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
    MultiValueMap<String, String> result = (MultiValueMap)cache.get(classLoader);
    if (result != null) {
        return result;
    } else {
        try {
            // 实际上默认的 127 个bean都是在META-INF/spring.factories中写死的
            Enumeration<URL> urls = classLoader != null ? classLoader.getResources("META-INF/spring.factories") : ClassLoader.getSystemResources("META-INF/spring.factories");
            LinkedMultiValueMap result = new LinkedMultiValueMap();

            while(urls.hasMoreElements()) {
                URL url = (URL)urls.nextElement();
                UrlResource resource = new UrlResource(url);
                Properties properties = PropertiesLoaderUtils.loadProperties(resource);
                Iterator var6 = properties.entrySet().iterator();

                while(var6.hasNext()) {
                    Entry<?, ?> entry = (Entry)var6.next();
                    String factoryTypeName = ((String)entry.getKey()).trim();
                    String[] var9 = StringUtils.commaDelimitedListToStringArray((String)entry.getValue());
                    int var10 = var9.length;

                    for(int var11 = 0; var11 < var10; ++var11) {
                        String factoryImplementationName = var9[var11];
                        result.add(factoryTypeName, factoryImplementationName.trim());
                    }
                }
            }

            cache.put(classLoader, result);
            return result;
        } catch (IOException var13) {
            throw new IllegalArgumentException("Unable to load factories from location [META-INF/spring.factories]", var13);
        }
    }
}

3、利用工厂加载 Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader);得到所有的组件

4、从 META-INF/spring.factories 位置来加载一个文件。

默认扫描我们当前系统里面所有 META-INF/spring.factories 位置的文件

spring-boot-autoconfigure-2.3.4.RELEASE.jar 包里面也有META-INF/spring.factories

文件里面写死了 spring-boot 一启动就要给容器中加载的所有配置类,共 127 个

image-20220105211215381

4、按需加载配置

虽然我们127个场景的所有自动配置启动的时候默认全部加载。xxxxAutoConfiguration
按照条件装配规则(@Conditional),最终会按需配置。

所有以 AutoConfiguration 结尾的类,都是用来条件装配指定场景的 bean 的

以下以 AOP 为例

@Configuration(proxyBeanMethods = false)
// 当配置文件中的 spring.aop.auto 的值为 true 时,该配置类才生效
// 即使配置文件中没有改配置也默认生效
@ConditionalOnProperty(prefix = "spring.aop", name = "auto", havingValue = "true", matchIfMissing = true)
public class AopAutoConfiguration {

    // 指定该类内部生成的bean均为多例
    @Configuration(proxyBeanMethods = false)
    // 只有当项目中存在《指定包》下的 Advice 这个类时,配置类才生效,所以要导AOP的包
    @ConditionalOnClass(Advice.class)
    static class AspectJAutoProxyingConfiguration {

        @Configuration(proxyBeanMethods = false)
        @EnableAspectJAutoProxy(proxyTargetClass = false)
        @ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "false",
                matchIfMissing = false)
        static class JdkDynamicAutoProxyConfiguration {}

        @Configuration(proxyBeanMethods = false)
        @EnableAspectJAutoProxy(proxyTargetClass = true)
        @ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "true",
                matchIfMissing = true)
        static class CglibAutoProxyConfiguration {}

    }

    /**
    * 用于创建简单 AOP 功能
    */
    @Configuration(proxyBeanMethods = false)
    // 当不存在指定类时,该配置类生效
    @ConditionalOnMissingClass("org.aspectj.weaver.Advice")
    @ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "true",
            matchIfMissing = true)
    static class ClassProxyingConfiguration {

        ClassProxyingConfiguration(BeanFactory beanFactory) {
            if (beanFactory instanceof BeanDefinitionRegistry) {
                BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory;
                AopConfigUtils.registerAutoProxyCreatorIfNecessary(registry);
                AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry);
            }
        }

    }

}

6、自动配置顺序

@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass(DispatcherServlet.class)
@AutoConfigureAfter(ServletWebServerFactoryAutoConfiguration.class)
public class DispatcherServletAutoConfiguration {}

@AutoConfigureAfter 注解用于定义自动注入顺序,上述案例为 DispatcherServletAutoConfiguration 该类的注入是在 ServletWebServerFactoryAutoConfiguration 类之后

5、修改默认配置

// 给容器中加入了文件上传解析器;
@Bean
//容器中有这个类型组件
@ConditionalOnBean(MultipartResolver.class)  
//容器中没有这个名字 multipartResolver 的组件
@ConditionalOnMissingBean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME) 
// @Bean 标注的方法的参数,是从容器中找到相应的类型的bean进行赋值的
public MultipartResolver multipartResolver(MultipartResolver resolver) {
    //给@Bean标注的方法传入了对象参数,这个参数的值就会从容器中找。
    //SpringMVC multipartResolver。防止有些用户配置的文件上传解析器乱命名
    return resolver;
}

SpringBoot默认会在底层配好所有的组件。但是如果用户自己配置了以用户的优先:

@Bean
@ConditionalOnMissingBean
public CharacterEncodingFilter characterEncodingFilter() {
}

总结:

  • SpringBoot 先加载所有的自动配置类 xxxxxAutoConfiguration

  • 每个自动配置类按照条件进行生效,默认都会绑定配置文件指定的值。xxxxProperties里面拿。xxxProperties 和配置文件进行了绑定

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

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

  • 定制化配置

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

xxxxxAutoConfiguration —> 组件 —> xxxxProperties里面拿值 —-> application.properties

七、spring.factories

spring.factories 文件是帮助spring-boot项目包以外的bean(即在pom文件中添加依赖中的bean)注册到spring-boot项目的spring容器中。由于@ComponentScan 注解只能扫描spring-boot项目包内的bean并注册到spring容器中,因此需要 @EnableAutoConfiguration 注解来注册项目包外的bean。而spring.factories文件,则是用来记录项目包外需要注册的bean类名。

在springboot运行时,SpringFactoriesLoader 类会去寻找,resource/META-INF/spring.factories 下配置文件,并将里面的类注册到 容器中

public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
# Auto Configure
org.springframework.boot.env.EnvironmentPostProcessor=\
  com.baomidou.mybatisplus.autoconfigure.SafetyEncryptProcessor
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.baomidou.mybatisplus.autoconfigure.MybatisPlusInnerInterceptorAutoConfiguration,\
  com.baomidou.mybatisplus.autoconfigure.IdentifierGeneratorAutoConfiguration,\
  com.baomidou.mybatisplus.autoconfigure.MybatisPlusLanguageDriverAutoConfiguration,\
  com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration
# Depends On Database Initialization Detectors
org.springframework.boot.sql.init.dependency.DependsOnDatabaseInitializationDetector=\
com.baomidou.mybatisplus.autoconfigure.MybatisDependsOnDatabaseInitializationDetector

该文件的格式为键值对,键是自动配置类的全限定名,值是该自动配置类所对应的配置类的全限定名

  • Key:是一个完全限定的接口名、注解名或者Spring Boot定义的特定字符串,用于标识一组可以被Spring Boot自动发现的组件或配置。在上述示例中,org.springframework.boot.autoconfigure.EnableAutoConfigurationorg.springframework.context.ApplicationListenerorg.springframework.boot.ApplicationContextInitializer都是key。
  • Value:是实现了key所指定的接口或使用了该注解的类的完全限定名列表,这些类将被Spring Boot在启动时自动扫描并注册到Spring应用上下文中。如果有多个类,它们之间通过换行符(\后跟换行)或逗号(,)分隔。在上述示例中,com.example.autoconfig.MyAutoConfigurationcom.example.listeners.MyApplicationListenercom.example.initializers.MyApplicationContextInitializer都是对应的value。

这种方式允许开发者通过简单的配置文件扩展Spring Boot的功能,而无需修改Spring Boot自身的代码。

八、web请求处理

1、静态资源处理

WebMvcAutoConfiguration 自动配置类下的 WebMvcAutoConfigurationAdapter 静态内部类

@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
    // 判断配置文件中 spring.resources.add-mappings 的值
    // true: 默认值
    // false: 禁用所有静态资源规则
    if (!this.resourceProperties.isAddMappings()) {
        logger.debug("Default resource handling disabled");
        return;
    }
    // 可配置静态资源的缓存时间
    Duration cachePeriod = this.resourceProperties.getCache().getPeriod();
    CacheControl cacheControl = this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl();
    if (!registry.hasMappingForPattern("/webjars/**")) {
        customizeResourceHandlerRegistration(registry.addResourceHandler("/webjars/**")
                                             .addResourceLocations("classpath:/META-INF/resources/webjars/")
                                             .setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
    }
    String staticPathPattern = this.mvcProperties.getStaticPathPattern();
    if (!registry.hasMappingForPattern(staticPathPattern)) {
        customizeResourceHandlerRegistration(registry.addResourceHandler(staticPathPattern)
                                             .addResourceLocations(getResourceLocations(this.resourceProperties.getStaticLocations()))
                                             .setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
    }
}

  目录