SpringBoot自动配置

一、SpringBoot自动装配原理

从Spring Boot的核心注解@SpringBootApplication开始入手。观察首启动类

@SpringBootApplication
 public class SpringbootApplication {
     public static void main(String[] args) {
        SpringApplication.run(SpringbootApplication.class, args);
     }
 }

@SpringBootApplication 这个注解是标注一个Spring Boot应用,标注这个类是主启动类,SpringApplication.run()方法会启动Spring应用上下文,进行自动配置和组件扫描。

进入这个注解的内部

@SpringBootConfiguration : 标注在某个类上,表示这是一个Spring Boot的配置类;
@EnableAutoConfiguration : 开启自动装配功能

@ComponentScan : 配置扫描路径,用于加载使用注解格式定义的bean

1、进入@SpringBootConfiguration注解内部

可以发现除了几个元注解外,该注解被@Configuration标注,那么可以理解为@SpringBootConfiguration标注的类为一个配置类,配置类主要负责定义应用程序的配置信息,包括创建和配置 Bean 对象、设置数据源、事务管理器等。

1.1、再观察@Configuration的内部

@Component说明该注解被注入到Spring IOC容器中,所以在SpringBoot项目启动时可以自动找到配置。

2.@EnableAutoConfiguration开启自动配置

@AutoConfigurationPackage 指定了默认的包规则将主程序类所在包及所有子包下的组件扫描到Spring容器中;
@Import(AutoConfigurationImportSelector.class) : 通过 @Import 注解导入 AutoConfigurationImportSelector类,然后通过该类的selectimport()方法去读取MATE-INF/spring.factories文件中配置的组件的全类名,并按照一定的规则过滤掉不符合要求的组件的全类名,将剩余读取到的各个组件的全类名集合返回给IOC容器并将这些组件注册为bean

源代码解读如下: 

@Import({Registrar.class})
public @interface AutoConfigurationPackage {
    String[] basePackages() default {};

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

2.1、@AutoConfigurationPackage 自动配置包

进入这个注解内部,可看出被@AutoConfigurationPackage标注,该注解的作用是将 添加该注解的类所在的package 作为 自动配置package 进行管理。进入这个注解内部,可以看出该注解导入了Registrar.class类,进入这个类中

static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {
        Registrar() {
        }

        public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
            AutoConfigurationPackages.register(registry, (String[])(new AutoConfigurationPackages.PackageImports(metadata)).getPackageNames().toArray(new String[0]));
        }

        public Set<Object> determineImports(AnnotationMetadata metadata) {
            return Collections.singleton(new AutoConfigurationPackages.PackageImports(metadata));
        }
    }
2.1.1、registerBeanDefinitions()方法

在类中的registerBeanDefinitions()方法中,传入两个参数,AnnotationMetadata metadata为注解元数据,用于检索注解的属性、判断是否存在特定的注解等。第二个参数BeanDefinitionRegistry registry为Bean的信息对象,分析register()方法中的第二个参数

new PackageImports(metadata).getPackageNames().toArray(new String[0])),进入  PackageImports () 方法

PackageImports(AnnotationMetadata metadata) {
            AnnotationAttributes attributes = AnnotationAttributes.fromMap(metadata.getAnnotationAttributes(AutoConfigurationPackage.class.getName(), false));
            List<String> packageNames = new ArrayList(Arrays.asList(attributes.getStringArray("basePackages")));
            Class[] var4 = attributes.getClassArray("basePackageClasses");
            int var5 = var4.length;

            for(int var6 = 0; var6 < var5; ++var6) {
                Class<?> basePackageClass = var4[var6];
                packageNames.add(basePackageClass.getPackage().getName());
            }

            if (packageNames.isEmpty()) {
                packageNames.add(ClassUtils.getPackageName(metadata.getClassName()));
            }

            this.packageNames = Collections.unmodifiableList(packageNames);
        }
2.1.2、PackageImports()方法

在这个构造方法中将元数据即启动类AnnotationMetadata metadata经过处理获取标签注解信息,

basePackages、 basePackageClasses为注解@AutoConfigurationPackage中的属性。

根据注解信息里面的basePackages和basePackageClasses是否有数据判断,如果packageNames为空,则获取注解所在的类的名字目录,放在List集合中

如此获得packageNames属性也就是启动类所在的包。回到Registrar中的registerBeanDefinitions()方法中register()方法的第二个参数即为启动类所在的包的名称,并且使用数组来进行表示。

2.1.3、再进入到register()方法内部,
public static void register(BeanDefinitionRegistry registry, String... packageNames) {
        if (registry.containsBeanDefinition(BEAN)) {
            AutoConfigurationPackages.BasePackagesBeanDefinition beanDefinition = (AutoConfigurationPackages.BasePackagesBeanDefinition)registry.getBeanDefinition(BEAN);
            beanDefinition.addBasePackages(packageNames);
        } else {
            registry.registerBeanDefinition(BEAN, new AutoConfigurationPackages.BasePackagesBeanDefinition(packageNames));
        }

    }

这个方法的if语句为判断registry这个参数中是否已经注册了AutoConfigurationPackages的类路径所对应的bean(AutoConfigurationPackages)。如若已经被注册,则把上面分析的第二个参数所获取的包(启动类所在的包的名称)添加到这个bean的定义中。如若没有,则注册这个bean并且把包名设置到该bean的定义中。

2.2、回到EnableAutoConfiguration中再看该注解使用@Import({AutoConfigurationImportSelector.class})导入一个类,进入该类的内部

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

在这个用法中,所返回的字符串数组为所有的将要被导入的类的全类名。分析return可知该方法的返回值在getAutoConfigurationEntry()方法中获得,所以分析这个方法。 

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

可以从return大体看出,返回的是一个使用属性configurations所生成的自动配置实体,而这个实体是通过getCandidateConfigurations方法获得,进入这个方法

protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
        List<String> configurations = new ArrayList(SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.getBeanClassLoader()));
        ImportCandidates.load(AutoConfiguration.class, this.getBeanClassLoader()).forEach(configurations::add);
        Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories nor in META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports. If you are using a custom packaging, make sure that file is correct.");
        return configurations;
    }

 return configurations,那我们又去找该返回值的获取方法,loadFactoryNames()方法

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

        String factoryTypeName = factoryType.getName();
        return (List)loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList());
    }

classLoader翻译过了就是类加载器,就是负责把磁盘上的.class文件加载到JVM内存中

getClassLoader()方法,得到是谁把我从.class文件加载到内存中变成Class对象的

该方法中通过判断若是classLoader为null,则直接获取SpringFactoriesLoader的classLoader,若不为null则调用loadSpringFactories()方法,进入这个方法:

 2.2.3、loadSpringFactories()方法
    //这个方法返回的为Map<String, List<String>> 变量result。
    private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
       //从全局变量cache中以classLoader为key获取所对应的value
        Map<String, List<String>> result = cache.get(classLoader);
       //判断在cache中是否存在相对应的value,如果已经存在则直接返回对应的result
        if (result != null) {
          return result;
       }
        //至此,已经判断得出result为空,所以实例化一个新的HashMap
       result = new HashMap<>();
       try {
           //从传入的类加载器中获取资源,路径为"META-INF/spring.factories"
          Enumeration<URL> urls = classLoader.getResources(FACTORIES_RESOURCE_LOCATION);
           //将获取到的urls进行遍历
          while (urls.hasMoreElements()) {
             URL url = urls.nextElement();
              //获取资源的url在当前项目中的位置等
             UrlResource resource = new UrlResource(url);
              //通过配置类加载工具类加载配置类,获取所有配置
             Properties properties = PropertiesLoaderUtils.loadProperties(resource);
              //遍历所有配置实体
             for (Map.Entry<?, ?> entry : properties.entrySet()) {
                String factoryTypeName = ((String) entry.getKey()).trim();
                 String[] factoryImplementationNames =
                      StringUtils.commaDelimitedListToStringArray((String) entry.getValue());
                //遍历当前配置实体中的所有value,如果不存在当前key,则将当前key以及下面遍历所得到的value一起添加到result中,如果存在则将该value添加到所对应的key下面。
                 for (String factoryImplementationName : factoryImplementationNames) {
                   result.computeIfAbsent(factoryTypeName, key -> new ArrayList<>())
                         .add(factoryImplementationName.trim());
                }
             }
          }

          // Replace all lists with unmodifiable lists containing unique elements(译文:用包含唯一元素的不可修改列表替换所有列表)
          result.replaceAll((factoryType, implementations) -> implementations.stream().distinct()
                .collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList)));
          //将该classLoader以及对应的result添加到cache中
           cache.put(classLoader, result);
       }
       catch (IOException ex) {
          throw new IllegalArgumentException("Unable to load factories from location [" +
                FACTORIES_RESOURCE_LOCATION + "]", ex);
       }
        //至此,返回了META-INF/spring.factories中的所有的配置
       return result;
    }

小结 

1、利用getAutoConfigurationEntry(annotationMetadata);给容器中批量导入一些组件
2、调用List configurations = getCandidateConfigurations(annotationMetadata, attributes)获取到所有需要导入到容器中的配置类
3、利用工厂加载 Map<String, List> loadSpringFactories(@Nullable ClassLoader classLoader);得到所有的组件
4、从META-INF/spring.factories位置来加载一个文件。默认扫描我们当前系统里面所有META-INF/spring.factories位置的文件**,按照条件装配,@Conditional最终会按需配置

3、回到@ComponentScan注解

这个注解在Spring中很重要 ,它对应XML配置中的元素。

作用:自动扫描并加载符合条件的组件或者bean , 将这个bean定义加载到IOz

总结:

@EnableAutoConfiguration 注解内部使用 @Import(AutoConfigurationImportSelector.class) 来加载配置类。
配置文件位置:META-INF/spring.factories,该配置文件中定义了大量的配置类,当 SpringBoot应用启动时,会自动加载这些配置类,初始化Bean。但并不是所有的Bean都会被初始化,在配置类中使用Condition来加载满足条件的Bean

二、Condition

Condition 是在Spring 4.0 增加的条件判断功能,通过这个可以功能可以实现选择性的创建 Bean 操 作。

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

@Conditional 注解可以放在类或者方法上。当你放置在配置类上时,只有当所有指定的条件都满足时,配置类中的Bean才会被创建。放置在Bean声明的方法上时,只有当条件满足时,对应的Bean才会被注册

内置条件注解

Spring Boot提供了一系列内置的条件注解,可直接使用:

  • @ConditionalOnBean: 当指定的Bean存在时。
  • @ConditionalOnMissingBean: 当指定的Bean不存在时。
  • @ConditionalOnClass: 当指定的字节码文件存在时。
  • @ConditionalOnMissingClass: 当指定的字节码文件不存在时。
  • @ConditionalOnProperty: 当指定的属性有指定的值时。
  • @ConditionalOnResource: 当指定的资源存在时。
  • @ConditionalOnWebApplication: 当项目是一个Web应用程序时。
  • @ConditionalOnNotWebApplication: 当项目不是一个Web应用程序时。

Spring Boot中条件匹配具体实现:

4.1、若在 Spring 的 IOC 容器中有一个 User 的 Bean, 导入Jedis坐标后,加载该Bean,没导入,则不加载。

1、创建实体类

public class User {
    //
}

2、创建配置类,使用@Bean将User注入容器,标注@Conditional注解,value值为ClassCondition类

@Configuration
public class UserConfig {

    //@Conditional中的ClassCondition.class的matches方法,返回true执行以下代码,否则反之
    @Bean
    @Conditional(value= ClassCondition.class)
    public User user(){
        return new User();
    }

 @param context 上下文对象。用于获取环境,IOC容器,ClassLoader对象
 @param metadata 注解元对象。 可以用于获取注解定义的属性值

3、创建条件类,判断坐标是否导入

public class ClassCondition implements Condition {
 
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        //1.需求: 导入Jedis坐标后创建Bean
        //思路:判断redis.clients.jedis.Jedis.class文件是否存在

        boolean flag=true;
        try {
            Class<?> aClass = Class.forName("redis.clients.jedis.Jedis");
            return true;
        } catch (Exception e) {
            flag=false;
        }
        return flag;
    }
}

 4.2、在 Spring 的 IOC 容器中有一个 User 的 Bean,现要求: 将类的判断定义为动态的。判断哪个字节码文件存在可以动态指定

1、创建实体类

public class User {
    //
}

2、创建注解

@Target({ElementType.TYPE, ElementType.METHOD})//可以修饰在类与方法上
@Retention(RetentionPolicy.RUNTIME)//注解生效节点runtime
@Documented//生成文档
@Conditional(value = ClassCondition.class)
public @interface conditiononmy {
    String[] value();//设置此注解的属性redis.clients.jedis.Jedis
}

3、创建一个条件类继承Condition重写matches方法

public class ClassCondition implements Condition {

        //2.需求: 导入通过注解属性值value指定坐标后创建Bean
        //获取注解属性值  value

        Map<String, Object> map = metadata.getAnnotationAttributes(conditiononmy.class.getName());
        System.out.println(map);
        String[] value = (String[]) map.get("value");
        boolean flag = true;

        try {
            for (String s : value) {
                Class<?> cls = Class.forName(s);
            }
            flag = true;
        } catch (Exception e) {
            flag = false;
        }
        return flag;
    }
}

 3.创建配置类加载User对象

@Configuration
public class Userconfig {
    //@Conditional中的ClassCondition.class的matches方法,返回true执行以下代码,否则反之
    @Bean
    @conditiononmy(value = {"com.alibaba.fastjson.JSON","redis.clients.jedis.Jedis"})
    public User user(){
        return new User();
    }
}

最后在启动类测试


@SpringBootApplication
public class SpringbootCondition01Application {
    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(SpringbootCondition01Application.class, args);
        //固定坐标在condition输入,使用原有注解实现
        Object user = context.getBean("user");
        System.out.println(user);
        //不存在坐标:NoSuchBeanDefinitionException: No bean named 'user' available
        //存在坐标:com.example.springboot_condition_01.domain.User@726a6b94
 
        //动态输入坐标,在config配置类使用自定义注解@ConditiononClass(坐标数组)输入,使用自定义注解实现
       Object user1 = context.getBean("user1");
       System.out.println(user1);
        Object user2 = context.getBean("user2");
        System.out.println(user2);
    }
 
}

三. @Enable注解

SpringBoot中提供了很多Enable开头的注解,都用于启动某些功能。其底层原理是使用@Import注解导入一些配置类,实现Bean对象的加载。使用@Import导入的类会被Spring加载到IOC容器中。

例如:

@EnableAutoConfiguration开启自动配置,具体详解件上

@Import提供4中用法:

1. 导入Bean

方法一:

  • 创建enable模块 

创建一个User的Bean对象

public class User {
    //
}

创建一个配置类,加载User对象

@Configuration
public class Userconfig {

    @Bean
    public User user(){
        return new User();
    }
}

测试模块

  • 依赖enable的模块

先将enable模块坐标导入依赖enable的模块

<dependency>
  <groupId>com.apesource</groupId>
  <artifactId>enable</artifactId>
  <version>0.0.1SNAPSHOT</version>
</dependency>

在启动类测试获取User

@SpringBootApplication
@Import(User.class)
public class SpringbootEnable01Application {
 
    public static void main(String[] args) {
        ConfigurableApplicationContext run = SpringApplication.run(SpringbootEnable01Application.class, args);
        User user = run.getBean(User.class);
        System.out.println(user);
    }
 
}


方法二:在enable模块写一个@Enable注解,用@Import注解导入注入User的配置类

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(Userconfig.class)
public @interface EnableUser {

}

 在测试模块的启动类上方通过@EnableUser注解导入Bean对象

@EnableUser
@Import(myimportselector.class)
@SpringBootApplication
public class SpringbootEnable03Application {

    public static void main(String[] args) {
        ConfigurableApplicationContext run = SpringApplication.run(SpringbootEnable03Application.class, args);
        Object user = run.getBean("user");
        System.out.println(user);
    }

}

2. 导入配置类

也可以在启动类上扫描enable模块的config配置类

@Import(Userconfig.class)
@SpringBootApplication
public class SpringbootEnable03Application {

    public static void main(String[] args) {
        ConfigurableApplicationContext run = SpringApplication.run(SpringbootEnable03Application.class, args);
        Object user = run.getBean("user");
        System.out.println(user);
    }

}

3. 导入 ImportSelector 实现类

  • enble模块

创建Student对象

public class Student{
    //
}

 创建MyImportSelector类,实现了ImportSelector接口,重写接口中的selectImports方法,往String[]中传入两个Bean对象

public class MyImportSelector implements ImportSelector {
    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        return new String[]{"com.example.springboot_enable_02_other.domain.User","com.example.springboot_enable_02_other.domain.Student"};
    }
}

 测试

@Import(myimportselector.class)
@SpringBootApplication
public class SpringbootEnable03Application {

    public static void main(String[] args) {
        ConfigurableApplicationContext run = SpringApplication.run(SpringbootEnable03Application.class, args);
        Object user = run.getBean("user");
        System.out.println(user);
    }

}

4. 导入 ImportBeanDefinitionRegistrar 实现类

  • enable模块

MyImportBeanDefinitionRegister类,实现了ImportBeanDefinitionRegistrar接口,重写接口中的registerBeanDefinitions方法,往String[]中传入两个Bean对象

public class MyImportBeanDefinitionRegister implements ImportBeanDefinitionRegistrar {
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        //AnnotationMetadata注解
        //BeanDefinitionRegistry向spring容器中注入
 
        //1.获取user的definition对象
        AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.rootBeanDefinition(User.class).getBeanDefinition();
        //2.通过beanDefinition属性信息,向spring容器中注册id为user的对象
        registry.registerBeanDefinition("user",beanDefinition);
    }
}

测试模块

@SpringBootApplication
 
//ImportBeanDefinitionRegister实现类
@Import(MyImportBeanDefinitionRegister.class)
public class SpringbootEnable01Application {
 
    public static void main(String[] args) {
        ConfigurableApplicationContext run = SpringApplication.run(SpringbootEnable01Application.class, args);
        //ImportBeanDefinitionRegister实现类
       User user = run.getBean(User.class);
       System.out.println(user);
    }
 
}

 


四、自定义启动器


自定义redisstarter,要求当导入redis坐标时,SpringBoot自动创建Jedis的Bean

直接设置port、host

  •  创建redisspringbootautoconfigure模块

1.1、导入Jedis坐标

<dependency>
  <groupId>redis.clients</groupId>
  <artifactId>jedis</artifactId>
</dependency>

2、创建配置类,注入jedisBean对象

@Configuration
public class redisautoconfigure {

    @Bean
    public Jedis jedis(){
        return new Jedis("localhost",6379);
    }
}
  • 在resources中定义METAINF/spring.factories文件,

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.apesource.redisspringbootautoconfigure.RedisAutoconfiguration

1.2 创建redisspringbootstarter模块

  • 依赖redisspringbootautoconfigure的模块

导入坐标

<dependency>
  <groupId>com.apesource</groupId>
  <artifactId>redisspringbootautoconfigure</artifactId>
  <version>0.0.1SNAPSHOT</version>
</dependency>


1.3 测试模块

引入自定义的redisstarter依赖

<dependency>
  <groupId>com.apesource</groupId>
  <artifactId>redisspringbootstarter</artifactId>
  <version>0.0.1SNAPSHOT</version>
</dependency>


测试获取Jedis的Bean,操作redis。

@SpringBootApplication
public class SpringbootStarter01Application {
    public static void main(String[] args) {
 
        ConfigurableApplicationContext run = SpringApplication.run(SpringbootStarter01Application.class, args);
        Jedis bean = run.getBean(Jedis.class);
        System.out.println(bean);
    }
 
}

将host,port隐藏实现方法、

在redisspringbootautoconfigure模块创建properties文件

设置host,port

spring.redis.host=121.0.0
spring.redis.port=6379

创建一个properties配置类,将host,port设置为私有属性

@ConfigurationProperties
public class redisproperties {
    private int port=6379;
    private String host="localhost";

    public redisproperties(int port, String host) {
        this.port = port;
        this.host = host;
    }

    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;
    }

    public redisproperties() {
    }
}

在配置类中使用@EnableConfigurationProperties扫描properties配置文件启动

@Configuration
@EnableConfigurationProperties(redisproperties.class)
public class redisautoconfigure {

    @Bean
    public Jedis jedis(redisproperties redisproperties){
        return new Jedis(redisproperties.getHost(),redisproperties.getPort());
    }
}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值