SpringBoot高级特性-自动装配&自定义starter

这是笔者的学习笔记 在这里与大家分享一下,不墨迹,不卖关子。

SpringBoot自动装配

SpringBoot自动装配用到了以下几个注解

  • @Conditional
  • @Enable
  • @Import
  • @EnableAutoConfigure
    我们以注解为驱动 展开内容

Condition 条件判断

Condition是Spring在4.0引入的条件判断功能,Spring根据这个功能选择性的创建Bean。
下面我们使用SpringDataRedis 来演示一下

案例

  • 创建SpringBoot工程,引入springboot基础设施依赖
<!-- springboot依赖版本控制-->
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.2.4.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>
<!--starter依赖-->
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>
</dependencies>
  • main方法
@SpringBootApplication
public class springbootApp {
    public static void main(String[] args) {
 // run方法返回的是IOC容器的上下文对象,后续我们会使用他来获取IOC容器内的Bean
        ConfigurableApplicationContext context =
         SpringApplication.run(springbootApp.class, args);
    }
}
  • 工程结构图
    在这里插入图片描述
  • 获取redisTemplate
@SpringBootApplication
public class springbootApp {
    public static void main(String[] args) {
       ConfigurableApplicationContext context =
         	SpringApplication.run(springbootApp.class, args);
       // 获取redisTemplate 并打印输出
       Object redisTemplate = context.getBean("redisTemplate");
       System.out.println(redisTemplate);
    }
}
  • 启动springboot 这里包括下文的启动步骤,我会直接贴出运行结果。
    在这里插入图片描述
    报错,没有名为“redisTemplate”的bean可用,这里大家都知道,因为我们没有引入相应的依赖。
  • MAVEN中加入SpringdataRedis 依赖
<!--使用了<Parent> 标签锁定版本 这里无需在指定版本-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
  • 再次启动
    在这里插入图片描述问题:为什么我们引入了相应坐标,就能在IOC容器中获取与之对应的Bean。
  • 我们自定义一个实体类RedisTest,注入IOC容器,提出一个条件,当容器中存在jedis的时候加载该实体,否者不加载。
  • 我们在POM中加入Jedis依赖
<!-- 这里贴出当前工程的所有坐标-->
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>
    <!--<dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>-->
    <dependency>
        <groupId>redis.clients</groupId>
        <artifactId>jedis</artifactId>
    </dependency>
</dependencies>
  • 定义实体RedisTest
public class RedisTest {
}
  • 定义配置类 将实体对象注入IOC容器 (ps:直接注入很简单 这里是为了后续演示@Conditional注解)
@Configuration
public class RedisConfigurationTest {

    @Bean
    public RedisTest redisTest(){
        return new RedisTest();
    }
}
  • main方法中获取RedisTest 并打印输出
@SpringBootApplication
public class SpringbootApp {
    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(SpringbootApp.class, args);
//        Object redisTemplate = context.getBean("redisTemplate");
        Object redisTest = context.getBean("redisTest");
        System.out.println(redisTest);
    }
}
  • 获取成功
    在这里插入图片描述
    依据条件当前容器中存在jedis时,我们才加载RedisTest。
  • 修改配置类,增加@Conditional注解
@Configuration
public class RedisConfigurationTest {

    @Bean
    @Conditional()
    public RedisTest redisTest(){
        return new RedisTest();
    }
}

这里@Conditional 是报错的 他需要传入一个参数。我们进入注解内部看一下他需要的参数是什么。

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Conditional {
    Class<? extends Condition>[] value();
}

需要一个类的class对象,这个类需要继承Condition,我们再次点击Condition,进入他的内部

import org.springframework.core.type.AnnotatedTypeMetadata;

@FunctionalInterface
public interface Condition {
    boolean matches(ConditionContext var1, AnnotatedTypeMetadata var2);
}

Condition是一个接口 只有一个返回值是布尔类型的 mathces方法

  • 那么我们就定义Condition配置类实现Condition接口,顺带说明一下两个参数的作用
public class ConditionalConfig implements Condition {

    /**
     *
     * @param conditionContext  作用: 获取 Bean工厂  ClassLoad  Class 环境变量等
     * @param annotatedTypeMetadata 作用: 注解元对象
     * @return
     */
    @Override
    public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
        return false;
    }
}
  • 这个mathces 就是决定我们是否加载某个类的方法,返回false不加载,true则加载,那么依据刚才的条件我们可以对配置类做一些条件的判断。
public class ConditionalConfig implements Condition {

    /**
     *
     * @param conditionContext  作用: 获取 Bean工厂  ClassLoad  Class 环境变量等
     * @param annotatedTypeMetadata 作用: 注解元对象
     * @return
     */
    @Override
    public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
        boolean flag;
        try {
            Class<?> jedis = Class.forName("redis.clients.jedis.Jedis");
            System.out.println("jedis加载成功: "+jedis);
            flag = true;
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
            flag = false;
        }
        return flag;
    }
}

将配置类加载到@Conditional注解中

@Configuration
public class RedisConfigurationTest {

    @Bean
    @Conditional(ConditionalConfig.class)
    public RedisTest redisTest(){
        return new RedisTest();
    }
}
  • 工程结构
    在这里插入图片描述
  • 启动main方法
    在这里插入图片描述
    jedis、RedisTest均加载成功,接下来我们把jedis的坐标注释,再来观察一下运行结果。
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>
    <!--<dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>-->
    <!--<dependency>
        <groupId>redis.clients</groupId>
        <artifactId>jedis</artifactId>
    </dependency>-->
</dependencies>
  • 启动 是报错的 加载不到RedisTest
2020-02-18 13:11:55.803  INFO 10936 --- [           main] com.ips.springbootapp.SpringbootApp      : No active profile set, falling back to default profiles: default
java.lang.ClassNotFoundException: redis.clients.jedis.Jedis
 at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
 at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
 at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:349)
 at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
 at java.lang.Class.forName0(Native Method)
 at java.lang.Class.forName(Class.java:264)
 at com.ips.springbootapp.config.ConditionalConfig.matches(ConditionalConfig.java:19)
 at org.springframework.context.annotation.ConditionEvaluator.shouldSkip(ConditionEvaluator.java:108)
 at org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader.loadBeanDefinitionsForBeanMethod(ConfigurationClassBeanDefinitionReader.java:184)
 at org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader.loadBeanDefinitionsForConfigurationClass(ConfigurationClassBeanDefinitionReader.java:144)
 at org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader.loadBeanDefinitions(ConfigurationClassBeanDefinitionReader.java:120)
 at org.springframework.context.annotation.ConfigurationClassPostProcessor.processConfigBeanDefinitions(ConfigurationClassPostProcessor.java:331)
 at org.springframework.context.annotation.ConfigurationClassPostProcessor.postProcessBeanDefinitionRegistry(ConfigurationClassPostProcessor.java:236)
 at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanDefinitionRegistryPostProcessors(PostProcessorRegistrationDelegate.java:275)
 at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(PostProcessorRegistrationDelegate.java:95)
 at org.springframework.context.support.AbstractApplicationContext.invokeBeanFactoryPostProcessors(AbstractApplicationContext.java:706)
 at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:532)
 at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:747)
 at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:397)
 at org.springframework.boot.SpringApplication.run(SpringApplication.java:315)
 at org.springframework.boot.SpringApplication.run(SpringApplication.java:1226)
 at org.springframework.boot.SpringApplication.run(SpringApplication.java:1215)
 at com.ips.springbootapp.SpringbootApp.main(SpringbootApp.java:10)
2020-02-18 13:11:56.212  INFO 10936 --- [           main] com.ips.springbootapp.SpringbootApp      : Started SpringbootApp in 0.614 seconds (JVM running for 1.187)
Exception in thread "main" org.springframework.beans.factory.NoSuchBeanDefinitionException: No bean named 'redisTest' available
 at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBeanDefinition(DefaultListableBeanFactory.java:805)
 at org.springframework.beans.factory.support.AbstractBeanFactory.getMergedLocalBeanDefinition(AbstractBeanFactory.java:1278)
 at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:297)
 at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202)
 at org.springframework.context.support.AbstractApplicationContext.getBean(AbstractApplicationContext.java:1108)
 at com.ips.springbootapp.SpringbootApp.main(SpringbootApp.java:12)
  • 刚才说了matches方法中的两个参数的作用,其中一个是注解的元对象,我们可以根据这个将案例进行一个升级,自定义注解

  • 定义RedisConditional注解


@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(ConditionalConfig.class)
public @interface RedisConditional {
    // 在自定义注解上加入 @Conditional(ConditionalConfig.class) 让此类具有同样的功能 
    String[] value(); // 定义注解参数 这里我们使用数组。
}
  • 修改实现的matches方法
public class ConditionalConfig implements Condition {

    /**
     *
     * @param conditionContext  作用: 获取 Bean工厂  ClassLoad  Class 环境变量等
     * @param annotatedTypeMetadata 作用: 注解元对象
     * @return
     */
    @Override
    public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
        boolean flag;
        /**
         * annotatedTypeMetadata.getAnnotationAttributes 方法可以获取注解的属性值
         * 参数 为注解的全限定名 这里我们用类的Class属性直接获取 
         * 返回的map 结构为 {value=[]} vaule为固定key 我们直接通过value获取属性即可
         */
        Map<String, Object> conditional =
                        annotatedTypeMetadata.getAnnotationAttributes(RedisConditional.class.getName());
        //我们定义的属性值为数组 返回值为Object 强转为数组
        String[] value = (String[]) conditional.get("value");
        try {
            for (String s : value) {
                Class<?> aClass = Class.forName(s);
		System.out.println(aClass + " 加载成功");
            }
            flag = true;
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
            flag = false;
        }
        return flag;
    }
}
  • 修改RedisConfigurationTest的注解
@Configuration
public class RedisConfigurationTest {

    @Bean
    @RedisConditional({"redis.clients.jedis.Jedis","redis.clients.jedis.BinaryClient"})
    public RedisTest redisTest(){
        return new RedisTest();
    }
}

这里传入了两个参数 ,将刚才的注释的jedis坐标在加上

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>
    <!--<dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>-->
    <dependency>
        <groupId>redis.clients</groupId>
        <artifactId>jedis</artifactId>
    </dependency>
</dependencies>
  • 启动
2020-02-18 13:44:44.379  INFO 19856 --- [           main] com.ips.springbootapp.SpringbootApp      : Starting SpringbootApp on DESKTOP-KT61VF7 with PID 19856 (B:\designMode\springbootapp\target\classes started by 92862 in B:\designMode\Decorator)
2020-02-18 13:44:44.381  INFO 19856 --- [           main] com.ips.springbootapp.SpringbootApp      : No active profile set, falling back to default profiles: default
class redis.clients.jedis.Jedis 加载成功
class redis.clients.jedis.BinaryClient 加载成功
2020-02-18 13:44:44.733  INFO 19856 --- [           main] com.ips.springbootapp.SpringbootApp      : Started SpringbootApp in 0.555 seconds (JVM running for 1.158)
com.ips.springbootapp.doamin.RedisTest@1b45c0e
  • 传入的两个类都加载出来了 这就是@Conditional 注解
  • 这里我们使用了四个类分别是
RedisTest实体类
RedisConfigurationTest 配置类 将RedisTest注入IOC容器
ConditionConfig 注解配置类 实现Condition接口 自定义matches规则
RedisConditional 自定义注解类 定义注解参数为String 数组

我们要把RedisTest注入IOC容器,根据规则当容器中存在jedis时,我才进行注入操作,接着我自定义了注解RedisConditional,并引入了 @Condition注解让他拥有与 @Condition注解相同的行为,而引入的 @Condition注解使用了ConditionConfig注解配置类作为判断依据,ConditionConfig类则是实现了Condition接口 重写了matches方法 ,在matches方法中我们使用了AnnotatedTypeMetadata 注解元对象的getAnnotationAttributes方法,通过这个方法可以获取指定注解的属性值,我们选取了自己自定义的注解RedisConditional,并获取他的属性值,来作为matches返回参数的规则。

  • 接下来我们去看一下SpringBoot是怎么使用@Conditional注解的
    在这里插入图片描述- 我们关注 这么一个类

在这里插入图片描述

这个与我们定义的RedisConfigurationTest配置类是不是很像 作用是将一些对象注入到IOC容器中,只不过他注入的是redisTemplate 指定了Bean的名字,而且它不仅在方法上加入了@ConditionalOnMissingBean 注解还在类上加入了ConditionalOnClass 做判断。

ps @ConditionalOnClass 是指在当前的classpath下如果有指定的某个类 就加载当前类
   @ConditionalOnMissingBean(name = "redisTemplate") 是如果不存在redisTemplate这个bean 就注入

接着我们在去当前jar报下的 另一个包中condition包中
在这里插入图片描述

  • 他定义了非常多的@Conditional 注解 我们就选择一个刚才看到的@ConditionalOnClass 看一下,其他的都与之类似
    在这里插入图片描述

  • spring自定义的注解里面 也是使用了@Conditional注解,是不是跟我们的自定义注解类一样
    在这里插入图片描述

  • 他使用的注解配置类,大家感兴趣可以自己进入看一下,他 的调用链比较长,不过最后也是实现了Condition接口,(嘿嘿,spring跟我写的一样!!!)

  • 所以在SpringApplication一启动的时候 我只要引入了坐标,他就去读取是否有这个类 读取到了 就加载相应的bean

  • 我们在举个例子,大家都知道springboot内置了四个服务器 但是为什么springboot引入sping-boot-starter-web的时候就默认加载了tomcat呢

  • 我们继续在当前包下找到web包
    在这里插入图片描述

  • we包下有个embedded 看包名我们也能猜到,内置.

  • 下面有五个类,其中四个看名字我们也就知道了他是干嘛的,我们关注第一个。
    在这里插入图片描述

  • 里面有四段注入bean的方法 篇幅原因,我将里面tomcat单独贴出来(

/**
 * Nested configuration if Tomcat is being used.
 * 如果有tomcat.class 就注入
 */
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ Tomcat.class, UpgradeProtocol.class })
public static class TomcatWebServerFactoryCustomizerConfiguration {

   @Bean
   public TomcatWebServerFactoryCustomizer tomcatWebServerFactoryCustomizer(Environment environment,
         ServerProperties serverProperties) {
      return new TomcatWebServerFactoryCustomizer(environment, serverProperties);
   }

}
  • 其他的几个方法 分别对应 jetty netty undertow
  • 我们引入spring-boot-starter-web依赖,进入内部查看一下,不出所料应该是加入了tomcat的依赖,所以在@Conditional 条件判断的时候选择加载了tomcat
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

果然如此
在这里插入图片描述

  • 那么根据@Conditional 条件判断 我们只要剔除tomcat引入别的容器依赖 即可完成内置服务器的切换。 实践一下,剔除tomcat依赖,引入jetty
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <dependency>
        <groupId>redis.clients</groupId>
        <artifactId>jedis</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <exclusions>
            <exclusion>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-tomcat</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-jetty</artifactId>
    </dependency>
</dependencies>
  • 启动
    在这里插入图片描述
  • 我们了解了Condition条件之后 知道了Springboot选择性的加载bean,那他到底是如何加载的呢,这里要看@Enable注解了

@Enable 注解

  • 我们在使用SpringBoot技术栈进行开发的时候,肯定会遇到很多以Enable开头的注解。例如
@EnableAsync
@EnableDiscoveryClient
@EnableFeignClients 
 ...
  • Enable是动态的加载某些功能,其在内部使用的是@Import注解,是依据@Import的注解动态的导入配置类,实现bean的动态创建。
  • 这个注解我们也用一个案例来演示。

案例

  1. 创建enable工程,enable-t工程
    在这里插入图片描述
  2. enable-t工程中定义实体EnableTest,配置类EnableConfiguration
  • EnableTest
public class EnableTest {
}
  • EnableConfiguration
@Configuration
public class EnableConfiguration {

    @Bean
    public EnableTest enableTest() {
        return new EnableTest();
    }
}
  • pom
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>
</dependencies>
  1. 工程enable 引入 工程enable-t 坐标
  • pom
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>
    <dependency>
        <groupId>com.ips</groupId>
        <artifactId>enable-t</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </dependency>
</dependencies>
  • 在main方法中获取 enable-t工程 注入的bean,直接拿肯定报错,不再演示了,一般我们需要加入包扫描
@SpringBootApplication
@ComponentScan("com.ips.enablet.config") // 扫描指定类当前包及其子包
public class EnableApplication {
    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(EnableApplication.class, args);
        Object enableTest = context.getBean("enableTest");
        System.out.println(enableTest);
    }
}
  1. 启动 enable工程,加载成功
    在这里插入图片描述
  • 除了包扫描 还能使用@Import注解
@SpringBootApplication
//@ComponentScan("com.ips.enablet.config")
@Import(EnableConfiguration.class)
public class EnableApplication {
    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(EnableApplication.class, args);
        Object enableTest = context.getBean("enableTest");
        System.out.println(enableTest);
    }
}

运行结果
在这里插入图片描述

  • 这样做的前提是我们要知道目标类名或是包名才行,但是像redis这些框架他是不会把自己的这些信息给我们的。
  1. enable 工程 自定义Enable注解
  • 注解类 EnableTConfiguration
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(EnableConfiguration.class) 
public @interface EnableTConfiguration {

}
  • main方法改用自定义Enable注解
@SpringBootApplication
//@ComponentScan("com.ips.enablet.config")
//@Import(EnableConfiguration.class)
@EnableTConfiguration
public class EnableApplication {
    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(EnableApplication.class, args);
        Object enableTest = context.getBean("enableTest");
        System.out.println(enableTest);
    }
 }

启动
在这里插入图片描述
这是不是跟我们使用@Enable* 这些注解很像。

@Import注解

  • 在@SpringBootApplication注解中,他也有一个@Enable注解,而我们刚才说了Enable注解内部其实是使用了@Import注解来动态导入配置。下面我们进入@SpringBootApplication的注解内部一探究竟,看看Spring的@Enable是如何使用@Import的

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  • 他在这里使用的AutoConfigurationImportSelector 与我们 自己定义的EnableConfiguration 作用都是引入目标配置类

在这里插入图片描述

  • 只不过他的配置类内容会更丰富一些,我们进去看看

  • 在这里插入图片描述

  • AutoConfigurationImportSelector 实现了一堆接口 ,重写了很多方法,其中selectImports()是SpringBoot动态加载配置最关键的方法,这个方法来自DeferredImportSelector接口,而DeferredImportSelector接口则继承了ImportSelector。

  • 关于ImportSelector。接口我们就不进去看了。我们来看一下 selectImports()是走了哪些方法返回了什么内容。

@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
   if (!isEnabled(annotationMetadata)) {
      return NO_IMPORTS;
   }
   AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
         .loadMetadata(this.beanClassLoader);
   AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(autoConfigurationMetadata,
         annotationMetadata);
   return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
  • selectImports 方法 内 调用getAutoConfigurationEntry() 得到了AutoConfigurationMetadataLoader对象 最后将AutoConfigurationMetadataLoader的getConfigurations() 转换为数组返回
AutoConfigurationMetadataLoader 是AutoConfigurationImportSelector 的一个静态内部类
他内置了两个数据类型的变量分别为:
List<String> configurations;
Set<String> exclusions;
而selectImports()内则是将AutoConfigurationImportSelector 的configurations变量转换为数组返回,而AutoConfigurationImportSelector 来自于getAutoConfigurationEntry方法 
  • getAutoConfigurationEntry()
protected AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata,
      AnnotationMetadata annotationMetadata) {
   if (!isEnabled(annotationMetadata)) {
      return EMPTY_ENTRY;
   }
   AnnotationAttributes attributes = getAttributes(annotationMetadata);
   List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
   configurations = removeDuplicates(configurations);
   Set<String> exclusions = getExclusions(annotationMetadata, attributes);
   checkExcludedClasses(configurations, exclusions);
   configurations.removeAll(exclusions);
   configurations = filter(configurations, autoConfigurationMetadata);
   fireAutoConfigurationImportEvents(configurations, exclusions);
   return new AutoConfigurationEntry(configurations, exclusions);
}
getAutoConfigurationEntry 接收的两个参数
@param1  AutoConfigurationMetadata  注解配置类元数据对象 获取注解配置属性
@param2  AnnotationMetadata  注解元数据对象 获取注解属性
返回参数 AutoConfigurationEntry对象  其内部configurations属性 包含了需要自动导入的配置类
而AutoConfigurationEntry的configurations属性 来源于  getCandidateConfigurations方法
  • getCandidateConfigurations()
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;
}
  • getCandidateConfigurations获取的参数与getAutoConfigurationEntry一致,抛开这些,我们关注自己需要的部分
AutoConfigurationEntry的configurations属性 来自于 SpringFactoriesLoader.loadFactoryNames 方法
在这个方法中,他首先拿到了SpringFactoriesLoaderFactoryClass的class对象,在由class对象的Resource方法 读取 META-INF/spring.factories 下的属性配置类。
如果该对象为null,会直接使用ClassLoad.getSystemResources 方法读取 META-INF/spring.factories 下的属性配置类。 并返回一个list集合
Assert.notEmpty 如果判断的参数不为空则直接返回,否者返回错误信息,根据信息我们也能了解到他是去读取了 META-INF/spring.factories 下的内容
  • 综上来看selectImports() 方法返回的配置类资源是读取自 META-INF/spring.factories。这中间经历了很多去重,筛选需要的配置类等操作,感兴趣的可以自己去研究一下。
  • META-INF/spring.factories 中 这个META-INF是不是很熟悉,前面我们在翻看springboot的jar包时,是有见过的。我们再回去翻看一下jar包下的META-INF下是不是有个spring.factories ,他里面是些什么内容。

在这里插入图片描述

  • 加载的原来是这些内容 ,找几个熟悉的看一下
    在这里插入图片描述- redisAutoConfiguration,Mongolian,jdbc是不是很熟悉,我们再看一下data包下的redisAutoConfiguration

在这里插入图片描述
他就是一开始我们看到的,用来动态加载redisTemplate的配置类,我们来过一下整体的步骤

  1. 在AutoConfigurationImportSelector使用getCandidateConfigurations方法 获取META-INF/spring.factories 中的配置信息然后返回给
    getAutoConfigurationEntry方法 接着返回给selectImports并将其通过StringUtils.toStringArray 转化成数组返回。
  2. 通过@Import注解导入AutoConfigurationImportSelector.class得到返回的数组内容。
  3. 而@Import注解又作用在@EnableAutoConfiguration注解类
  4. @EnableAutoConfiguration注解又作用在@SpringBootApplication
  5. 所以在我们启动的时候通过@SpringBootApplication 将内容都加载了进来。

看了SpringBoot的@Import注解的玩法,我们可以把刚才的demo升级一下

  • enable工程定义@Import配置类
  • ImportConfigure (springboot是读取配置信息,这里我直接写死)
public class ImportConfigure implements ImportSelector {

    @Override
    public String[] selectImports(AnnotationMetadata annotationMetadata) {
        return new String[]{"com.ips.enablet.config.EnableConfiguration"};
    }
}
  • 修改EnableTConfiguration 注解类
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(ImportConfigure.class) //改为Import注解类
public @interface EnableTConfiguration {

}
  • main 方法
@SpringBootApplication
//@ComponentScan("com.ips.enablet.config")
//@Import(EnableConfiguration.class)
@EnableTConfiguration
public class EnableApplication {
    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(EnableApplication.class, args);
        Object enableTest = context.getBean("enableTest");
        System.out.println(enableTest);
    }
 }

启动
在这里插入图片描述
读取成功

自定义starter

springboot的自动装配仅限于与springboot整合的内容,通常我们单独引入某个框架的坐标时,需要自己手动注入bean,这里不在给大家演示。

  • 我们不难发现,与springboot整合的框架,他们的坐标都均以spring-boot-starter开头,但也有例外,比如mybatis。这是因为以spring-boot-starter开头的坐标都是spring自己整合的,而mybatis是他自己自定义的starter。我们来看一下mybatis自己的starter的如何处理的。

  • 引入mybatis的坐标 查看mybatis源码包

<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.1</version>
</dependency>
  • 它定义了两个包
mybatis-spring-boot-starter
mybatis-spring-boot-autoconfigure
并在starter的pom中依赖了 autoconfigure

在这里插入图片描述

  • 我们再来看一下mybatis-spring-boot-autoconfigure
    在这里插入图片描述

  • MEAT-INF下有个spring.factories,@SpringBootApplication会从这里获取配置类,这两个AutoConfiguration我们选择一个进入
    在这里插入图片描述

  • 我将部分源码 贴在下面

@Configuration
@ConditionalOnClass({SqlSessionFactory.class, SqlSessionFactoryBean.class})
@ConditionalOnSingleCandidate(DataSource.class)
@EnableConfigurationProperties({MybatisProperties.class})
@AutoConfigureAfter({DataSourceAutoConfiguration.class, MybatisLanguageDriverAutoConfiguration.class})
public class MybatisAutoConfiguration implements InitializingBean {
    private static final Logger logger = LoggerFactory.getLogger(MybatisAutoConfiguration.class);
    private final MybatisProperties properties;
    private final Interceptor[] interceptors;
    ...
    
@Bean
@ConditionalOnMissingBean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
    SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
    factory.setDataSource(dataSource);
    factory.setVfs(SpringBootVFS.class);
    if (StringUtils.hasText(this.properties.getConfigLocation())) {
    ...
    
@Bean
@ConditionalOnMissingBean
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
    ExecutorType executorType = this.properties.getExecutorType();
    return executorType != null ? new SqlSessionTemplate(sqlSessionFactory, executorType) : new SqlSessionTemplate(sqlSessionFactory);
}    
  • 这个配置类就是用来动态的创建bean的,与我们先前看到的RedisAutoConfiguration是一样的作用。
  • 在这类上他也有一个Enable注解 @EnableConfigurationProperties({MybatisProperties.class})
  • 我们进入MybatisProperties.class看一下
@ConfigurationProperties(
    prefix = "mybatis"
)
public class MybatisProperties {
    public static final String MYBATIS_PREFIX = "mybatis";
    private static final ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();
    private String configLocation;
    private String[] mapperLocations;
    private String typeAliasesPackage;
  • 哦 他是用来加载以mybatis开头的属性的,我们在使用yml的时候,设置属性会以特定的格式开头,比如端口是 server,redis的是spring.redis。spring他也是同样在这里设置了prefix 前缀,我将server,redis配置源码贴在下面。
  • server属性配置
@ConfigurationProperties(prefix = "server", ignoreUnknownFields = true)
public class ServerProperties {

   /**
    * Server HTTP port.
    */
   private Integer port;

   /**
    * Network address to which the server should bind.
    */
   private InetAddress address;

   @NestedConfigurationProperty
   private final ErrorProperties error = new ErrorProperties();
  • spring.redis
@ConfigurationProperties(prefix = "spring.redis")
public class RedisProperties {

   /**
    * Database index used by the connection factory.
    */
   private int database = 0;

   /**
    * Connection URL. Overrides host, port, and password. User is ignored. Example:
    * redis://user:[email protected]:6379
    */
   private String url;

   /**
    * Redis server host.
    */
   private String host = "localhost";
  • 接下来我们就照着mybatis的starter, 自定义一个starter,为了演示方便,这里还是选择redis来操作。
  1. 创建工程
redisips-spring-boot-starter
redisips-spring-boot-autoconfigure

在这里插入图片描述

  • redisips-spring-boot-autoconfigure的pom
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>
    <dependency>
        <groupId>redis.clients</groupId>
        <artifactId>jedis</artifactId>
    </dependency>
</dependencies>
  • redisips-spring-boot-starter的pom
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>
    <dependency>
        <groupId>com.ips</groupId>
        <artifactId>redisips-spring-boot-autoconfigure</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </dependency>
</dependencies>
  • 按照mybatis的starter ,redisips-spring-boot-autoconfigure工程需要一个驱动配置类,和属性配置类,再加上spring的@Conditional注解
  1. redisips-spring-boot-autoconfigure
  • 属性配置类RedisIpsProperties
// 我们选择加载 以redisips 开头的内容
@ConfigurationProperties(prefix = "redisips")
public class RedisIpsProperties {
   // 设置默认地址和端口
    private String host="localhost";
    private int port=6379;

    public String getHost() {
        return host;
    }

    public void setHost(String host) {
        this.host = host;
    }

    public int getPort() {
        return port;
    }

    public void setPort(int port) {
        this.port = port;
    }
}
  • 驱动配置类 RedisIpsConfiguration 这里为了验证生效我们输出一段文字
@Configuration
@ConditionalOnClass(Jedis.class)
@EnableConfigurationProperties(RedisIpsProperties.class)
public class RedisIpsConfiguration {

    @Bean(name="jedisIps")
    @ConditionalOnMissingBean(name = "jedisIps")
    public Jedis jedis(RedisIpsProperties redisIpsProperties){
        System.out.println("jedis 正在注入 IOC 。。。");
        return new Jedis(redisIpsProperties.getHost(),redisIpsProperties.getPort());
    }
}
  • 我们还需要META-INF/spring.factories 来加载我们的驱动配置类
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.ips.redisipsspringbootautoconfigure.config.RedisIpsConfiguration

在这里插入图片描述

  1. redisips-spring-boot-starter工程
  • 只需要依赖autoconfiguration
  1. 创建测试 工程 引用自定义的starter。这里不在打jar包演示,直接依赖
  • 测试工程我也是直接用了以前创建的的springbootapp

  • pom 注意引入的依赖是我们自己的 redisips-spring-boot-starter

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>
    <dependency>
        <groupId>com.ips</groupId>
        <artifactId>redisips-spring-boot-starter</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </dependency>
</dependencies>
  • 工程结构 ,之前的测试类全部需要干掉
    在这里插入图片描述
  1. 启动本地redis测试
    在这里插入图片描述
  • 测试工程的main方法
@SpringBootApplication
public class SpringbootApp {
    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(SpringbootApp.class, args);
        Jedis jedisIps = (Jedis) context.getBean("jedisIps");
        jedisIps.set("key","jedisIps");
        String value = jedisIps.get("key");
        System.out.println("jedi获取成功 :"+value);
    }
}
  • 测试结果

在这里插入图片描述

成功,确实走了我们的RedisIpsConfiguration ,我们再RedisIpsConfiguration 上面加入了@ConditionalOnMissingBean ,接下来我们再测试类启动时注入bean,看@ConditionalOnMissingBean 注解是否生效

  • main方法 注入bean
@SpringBootApplication
public class SpringbootApp {
    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(SpringbootApp.class, args);
        Jedis jedisIps = (Jedis) context.getBean("jedisIps");
        jedisIps.set("key","jedisIps");
        String value = jedisIps.get("key");
        System.out.println("jedi获取成功 :"+value);
    }
    @Bean(name = "jedisIps")
    public Jedis jedis(){
        return new Jedis();
    }
}
  • 启动
    在这里插入图片描述
    验证成功

  • 我们还设置了读取以redisips开头的配置,去除测试类中的bean注入,下面来测试一下配置类读取

在这里插入图片描述

  • 启动测试

在这里插入图片描述

读取了配置没有问题。

以上的案例均是直接照着SpringBoot框架抄的,嘿嘿。感谢springboot的大力支持 ^ _ ^

一般我们再使用的时候都是直接引入依赖,却不知道他里面是如何操作的,用的时候都是心慌慌啊,现在知道了SpringBoot是如何通过依赖创建bean的,心里有底多了。

发布了10 篇原创文章 · 获赞 3 · 访问量 2376

猜你喜欢

转载自blog.csdn.net/qq_44647212/article/details/104371446