从零开始SpringCloud Alibaba实战(72)——springboot核心之自动配置

前言

SpringBoot中提供了很多Enable开头的注解,这些注解都是用于动态启用某些功能的,而其底层原理是使用@Import注解导入一些配置类,比如实现Bean的动态加载。这句话听起来稀里糊涂,那么我们来思考一个问题:SpringBoot工程是否可以直接获取jar包中定义的Bean?带着问题我们来一起刨析下SpringBoot自动配置之@Enable*注解的源码。

验证

1、首先简单看一下启动注解@SpringBootApplication
点进去源码如下:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
    excludeFilters = {@Filter(
    type = FilterType.CUSTOM,
    classes = {TypeExcludeFilter.class}
), @Filter(
    type = FilterType.CUSTOM,
    classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication {
    ...
}

解释:

@Target({ElementType.TYPE}) ----------声明注解作用范围是类;
@Retention(RetentionPolicy.RUNTIME)—声明注解作用时机是运行时;
@Documented---------------------------声明生成javaDoc文档;
@SpringBootConfiguration--------------点进去,源码如下,说明@SpringBootConfiguration是一个组合注解,本质其实就是一个@Configuration,也就是一个配置类的标记,这也是为什么SpringBoot主配置类中可以创建Bean的根本原因:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration    //这个注解的核心
@Indexed
public @interface SpringBootConfiguration {
    @AliasFor(
        annotation = Configuration.class
    )
    boolean proxyBeanMethods() default true;
}

@EnableAutoConfiguration--------------点进去看源码,发现它也是一个组合注解,有一个@Import注解

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})    //这个注解是核心
public @interface EnableAutoConfiguration {
    String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
    Class<?>[] exclude() default {};
    String[] excludeName() default {};
}

问题 SpringBoot工程是否可以直接获取jar包中定义的Bean?

1、创建两个模块演示
本文分别创建springboot-enable、springboot-embedded两个模块,前者代表我们的springBoot项目,后者模拟三方jar包

编写springboot-embedded

1、定义Bean类User.java


 
public class User {
}

2、定义配置类UserConfig.java


 

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
 
@Configuration
public class UserConfig {
 
    @Bean
    public User user(){
        return new User();
    }
}

3、编写springboot-enable
1、pom.xml中引入springboot-embedded的坐标依赖

<dependency>
    <groupId>com.test</groupId>
    <artifactId>springboot-embedded</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>

2、启动类


 

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
 
@SpringBootApplication
public class SpringbootEnableApplication {
 
    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(SpringbootEnableApplication.class, args);
        User user = (User)context.getBean("user");
        System.out.println(user);
    }
}

启动后,报错
nosuch bean

说明,SpringBoot工程是不可以直接获取jar包中定义的Bean,那么问题又来了,为什么我们在pom.xml中引入Redis的坐标以后,却可以直接使用名为redisTemplate这样一个对象呢?根本原因就是@Import这个注解

SpringBoot实现使用第三方jar包中定义的Bean

上面已经验证SpringBoot工程是不可以直接获取jar包中定义的Bean,原因就是启动类上的@SpringBootApplication注解内部组合之一的@ComponentScan是个扫包范围注解,默认扫包范围是扫描被@SpringBootApplication注解的启动类的同级包及其子包,我们发现启动类的包为com.itlean,而UserConfig.java的包是com.embedded.config,并没有同级或者包含关系,想要启动时候能扫描到UserConfig.java,有3中解决方式,具体如下:

1,第一种方式(扫包范围)

修改启动类,添加扫包范围注解,将UserConfig.java所在包进行手动扫描:


import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.ComponentScan;
 
@SpringBootApplication
@ComponentScan("com.test.config") //UserConfig.java手动扫描加入IOC容器
public class SpringbootEnableApplication {
 
    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(SpringbootEnableApplication.class, args);
        User user = (User)context.getBean("user");
        System.out.println(user);
    }
}

但是这种方式是不是太粗放了,我用你一个对象,完了我还得知道这个对象所在的包,那我要用到Redis的redisTemplate对象,我还得一顿找这玩意在哪个包,然后在启动类的@ComponentScan注解中添加扫包,肯定是不现实的

2、第二种方式@Import注解

    先看一下源码,发现入参是Class<?>[] value();也就是一堆类的数组,多一嘴哈,在SpringBoot中使用@Import注解引入的这些类都会被加载到IOC容器中。
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Import {
    Class<?>[] value();
}

既然如上所说,那我们把启动类注解再一次修改如下:


import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;
 
@SpringBootApplication
@Import(UserConfig.class)//直接将User的配置类引入
public class SpringbootEnableApplication {
 
    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(SpringbootEnableApplication.class, args);
        User user = (User)context.getBean("user");
        System.out.println(user);
    }
}

这种方式要比方式一间接很多了,不需要知道被使用Bean的包路径,但是还是要记住配置类的名字,我用一个对象,还需要知道这个对象对应的配置类叫什么,显然还是不够友好,接下来方式三就是@Import用法的终极优化。

3、第三种方式@Import注解封装

    在第三方jar中新建一个注解类,对@Import注解进行封装:

import org.springframework.context.annotation.Import;
import java.lang.annotation.*;
 
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(UserConfig.class)// 在第三方中直接将UserConfig.class引入,调用方就不需要知道包路径和配置类名称了
public @interface EnableUser {
}

调用方启动类修改如下:


import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
 
@SpringBootApplication
@EnableUser //这种使用起来已经非常简洁了
public class SpringbootEnableApplication {
 
    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(SpringbootEnableApplication.class, args);
        User user = (User)context.getBean("user");
        System.out.println(user);
    }
}

实际开发中,都是使用最后这种方式。

SpringBoot中@Enable*注解

    以上第三种方式就是SpringBoot底层自动装配的原理,所以说@Enable*这类注解是开启某些功能的注解,底层是使用@Import方式来实现Bean的动态加载,不使用@EnableUser这个注解,我就不能使用User这个对象。再回过头来看启动类的注解@SpringBootAppliction注解,内部注解组合中有一个@EnableAutoConfiguration注解,如下:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
    excludeFilters = {@Filter(
    type = FilterType.CUSTOM,
    classes = {TypeExcludeFilter.class}
), @Filter(
    type = FilterType.CUSTOM,
    classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication {
    ...
}

@EnableAutoConfiguration注解内部呢又使用@Import注解,如下:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {
    String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
    Class<?>[] exclude() default {};
    String[] excludeName() default {};
}

其中的关键功能由@Import提供,其导入的AutoConfigurationImportSelector的selectImports()方法通过SpringFactoriesLoader.loadFactoryNames()扫描所有具有META-INF/spring.factories的jar包。spring-boot-autoconfigure-x.x.x.x.jar里就有一个这样的spring.factories文件。

这个spring.factories文件也是一组一组的key=value的形式,其中一个key是EnableAutoConfiguration类的全类名,而它的value是一个xxxxAutoConfiguration的类名的列表,这些类名以逗号分隔
@EnableAutoConfiguration注解通过@SpringBootApplication被间接的标记在了Spring Boot的启动类上。在SpringApplication.run(…)的内部就会执行selectImports()方法,找到所有JavaConfig自动配置类的全限定名对应的class,然后将所有自动配置类加载到Spring容器中。
自动配置生效
每一个XxxxAutoConfiguration自动配置类都是在某些条件之下才会生效的,这些条件的限制在Spring Boot中以注解的形式体现,常见的条件注解有如下几项:

@ConditionalOnBean:当容器里有指定的bean的条件下。

@ConditionalOnMissingBean:当容器里不存在指定bean的条件下。

@ConditionalOnClass:当类路径下有指定类的条件下。

@ConditionalOnMissingClass:当类路径下不存在指定类的条件下。

@ConditionalOnProperty:指定的属性是否有指定的值,比如@ConditionalOnProperties(prefix=”xxx.xxx”, value=”enable”, matchIfMissing=true),代表当xxx.xxx为enable时条件的布尔值为true,如果没有设置的情况下也为true。

以ServletWebServerFactoryAutoConfiguration配置类为例,解释一下全局配置文件中的属性如何生效,比如:server.port=8081,是如何生效的(当然不配置也会有默认值,这个默认值来自于org.apache.catalina.startup.Tomcat)。
在ServletWebServerFactoryAutoConfiguration类上,有一个@EnableConfigurationProperties注解:开启配置属性,而它后面的参数是一个ServerProperties类,这就是习惯优于配置的最终落地点。

@ConfigurationProperties,它的作用就是从配置文件中绑定属性到对应的bean上,而@EnableConfigurationProperties负责导入这个已经绑定了属性的bean到spring容器中。那么所有其他的和这个类相关的属性都可以在全局配置文件中定义,也就是说,真正“限制”我们可以在全局配置文件中配置哪些属性的类就是这些XxxxProperties类,它与配置文件中定义的prefix关键字开头的一组属性是唯一对应的。

至此,我们大致可以了解。在全局配置的属性如:server.port等,通过@ConfigurationProperties注解,绑定到对应的XxxxProperties配置实体类上封装为一个bean,然后再通过@EnableConfigurationProperties注解导入到Spring容器中。

而诸多的XxxxAutoConfiguration自动配置类,就是Spring容器的JavaConfig形式,作用就是为Spring 容器导入bean,而所有导入的bean所需要的属性都通过xxxxProperties的bean来获得。

可能到目前为止还是有所疑惑,但面试的时候,其实远远不需要回答的这么具体,你只需要这样回答:

Spring Boot启动的时候会通过@EnableAutoConfiguration注解找到META-INF/spring.factories配置文件中的所有自动配置类,并对其进行加载,而这些自动配置类都是以AutoConfiguration结尾来命名的,它实际上就是一个JavaConfig形式的Spring容器配置类,它能通过以Properties结尾命名的类中取得在全局配置文件中配置的属性如:server.port,而XxxxProperties类是通过@ConfigurationProperties注解与全局配置文件中对应的属性进行绑定的。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值