SpringBoot自动装配特性的深入浅出

作者

樊远航

推荐理由

spring boot核心能力之自动装配的原理分析,讲解了自己实现自动装配的两种方式,并提供流程图让读者更好理解,排版规整。

前言

SpringBoot作为敏捷开发的常用封装框架,对于企业开发速度的提升、配置的简化、业务的专注三个方面进行了相对于SpringMVC等架构的升级。

那么,作为使用过SpringMVC的开发同学来说,初次使用SpringBoot时会感觉原本繁杂的xml配置工作一下子没有了。项目创建完成后,基本可以直接开始进行业务代码的编写(引入依赖除外)。那么,原本繁杂的配置工作是怎么消失掉的呢?这就不得不说下SpringBoot的核心特性之一的自动装配。

约定大于配置

SpringBoot能够顺利支持自动装配的原因,主要就是所有基于SpringBoot的开发者都遵循“约定大于配置”这一规则。

在SpringBoot中约定大于配置的具体体现在:

1、SpringBoot项目的目录结构下的resource目录统一用于存放配置文件

2、默认打包方式为jar

3、spring-boot-stater-web中包含springMVC相关的依赖以及内置的Tomcat容器,使得构建一个web应用更加方便

4、默认提供application.properties/yml文件用于项目配置

5、通过key(spring.profiles.active)属性来决定运行环境的分配和选择

6、EnableAutoConfiguration默认对于依赖的stater进行加载

自动装配

定义

什么是自动装配?回想我们使用SpringMVC时,我们需要在xml文件中写入很多以标签包装的信息,将这些Bean交予Spring 的IOC容器进行管理。而自动装配顾名思义,由框架本身帮我们自动去生成这些Bean并交予IOC容器管理,省去我们的配置工作。

前置准备

那么,SpringBoot中是如何做到自动装配,其原理是什么?下面,我将分享我的理解和心得。

IDEA中SpringBoot的创建

区别于传统通过maven创建项目的方式,我们按下面的流程进行SpringBoot的项目创建:
在这里插入图片描述设置我们的项目坐标:
在这里插入图片描述这里,我们可以选择一些配置:
在这里插入图片描述之后一路next,我们就可以创建一个SpringBoot的项目了。创建好的项目结构如下:
在这里插入图片描述java目录:我们的代码存放位置

resource目录:我们项目的配置文件的存放位置

test目录:测试代码的存放位置

target目录:项目的class文件的存放位置

看到这里,我们就能明白了为什么SpringBoot的核心是“约定大于配置”。只有统一了项目的结构规范,那么才能实现后续自动配置的相关操作(扫描注定结构路劲下的指定文件)。而传统的MVC项目中,我们可以随意的建立项目的目录结构,这就导致项目结构无法统一。所以老话说的好:”无规矩,不成方圆“。

SpringBootApplication注解

项目创建好后,会帮我们创建一个启动类:


/**

 * @author fanyuanhang

 */

@SpringBootApplication

public class SpringBootFirstApplication {

    public static void main(String[] args) {

    SpringApplication.run(SpringBootFirstApplication.class, args);

   }

}

此时,我们不需要任何配置,就可以直接启动我们的SpringBoot项目。那么, 相比较于传统MVC项目的繁琐配置,SpringBoot是如何实现自动配置的?那么, 我们想要知道自动装配的原理,那么就得从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 {

其中,核心注解为以下三个:

1、Configuration(SpringBootConfiguration)

2、EnableAutoConfiguration

3、ComponentScan

下面,我们由简到难来分别说明下这三个注解的作用是什么。

Configuration注解

这个注解熟悉JavaConfig的同学应该不陌生,其本质就是Spring容器IOC的一种基于Bean的配置注解(相当于MVC中的beans.xml)。通过这个注解,我们可以将被标记的类视为IOC容器的配置类,类中的每一个通过@Bean注解声明的对象全部交予IOC容器进行管理。

举例:


/**

 * 配置类

 * @author fanyuanhang 

 * */

@Configuration

public class SpringConfig {

  // Bean名称默认以方法名,可以通过name属性定义

    @Bean

    // @Scope IOC容器管理Bean,默认单例,可通过@Scop注解修改。

    public DefaultBean defaultBean(){

        return new DefaultBean();

   }

}

通过上面的方式,我们将传统基于xml的配置方式替换成为了JavaConfig形式的配置类来向IOC容器中添加Bean对象。Bean的名字默认以方法名为主,同样可以在@Bean注解中通过name属性来声明当前Bean对象的名称。

所以,不难理解,在Spring项目启动时,通过Configuration注解将配置类中的Bean初始化创建好后统一交予IOC容器进行管理。

ComponentScan注解

这个注解大家应该不陌生,主要是进行包扫描的注解。将当前声明路径下的包中所有符合加载条件的Bean或者组件注册进IOC容器中。

在SpringBoot中主要是如下注解:

@Component、 @Repository、@Service、@Controller

有以上注解声明的类或者对象,都会被交予IOC进行统一管理。

举例:

创建一个controller类,放到java根目录下:


@RestController

public class HelloController {

    @GetMapping("/hello")

    public String sayHello(){

        return "Hello Mic";

   }

}

修改启动类:


public class TestMain {

    public static void main(String[] args) {

        AnnotationConfigApplicationContext applicationContext = new

                AnnotationConfigApplicationContext(SpringConfig.class);

 

 

        String[] defNames = applicationContext.getBeanDefinitionNames();

        for (int i = 0; i < defNames.length; i++) {

            System.out.println(defNames[i]);

       }

   }

}

执行后,我们可以看到此时IOC容器中已经有了helloController这个Bean对象:
在这里插入图片描述
Import注解

在Spring中,从3.1版本开始提供了一系列Enable开头的注解:@EnableWebMVC、@EnableScheduling…

我们进入这些以Enable开头的注解中,都可以看到另外一个注解的存在:


@Target(ElementType.TYPE)

@Retention(RetentionPolicy.RUNTIME)

@Import(SchedulingConfiguration.class)

@Documented

public @interface EnableScheduling {


@Target(ElementType.TYPE)

@Retention(RetentionPolicy.RUNTIME)

@Documented

@Inherited

@AutoConfigurationPackage

@Import(AutoConfigurationImportSelector.class)

public @interface EnableAutoConfiguration {


@Retention(RetentionPolicy.RUNTIME)

@Target(ElementType.TYPE)

@Documented

@Import(DelegatingWebMvcConfiguration.class)

public @interface EnableWebMvc

从上面的源码中,我们可以看到:每一个Enable开头的注解内部都有@Import注解的存在。

对比xml式的配置方式,我们可以知道这个注解的作用:容器的合并。

举例:


/**

 * 另一Bean

 *

 * @author fanyuanhang

 */

public class OtherBean {

 

 

}

@Configuration

public class OtherConfig {

    @Bean

    public OtherBean otherBean() {

        return new OtherBean();

   }

}

/**

 * 配置类

 *

 * @author fanyuanhang

 */

@Import(OtherConfig.class)

@Configuration

public class SpringConfig {

 

 

    @Bean

    @Scope

    public DefaultBean defaultBean() {

        return new DefaultBean();

   }

}

我们在开始的SpringConfig配置类中通过@Import注解导入OtherConfig配置类,此时我们可以看到,IOC容器中同时存在了这两个Bean对象:

defaultBean

otherBean
动态加载Bean

不知道大家有没有思考这么一个问题:我上面的代码示例中都是在配置类中声明了一个Bean对象之后,再进行的其他操作。

可以,像Spring中不可能每一个Bean都在IOC容器中进行加载。比如,数据库驱动的Bean。数据库有很多种,我们不知道到底使用的是哪一个数据库。那么,此时将所有的数据库驱动都加载到IOC容器中无疑是一种浪费系统资源的操作。那么,Spring中是如何进行Bean对象的动态加载的呢?

其实,Spring给我们提供了关于Bean的三种配置方式:

1、通过@Configuration注解标识的配置类

2、ImportSelector接口的实现

3、AutoConfigurationPackages.Registrar接口的实现

后面,我会逐一说明用法以及在自动装配中的应用。

SPI机制

我先提出一个问题大家可以思考下。当我们有很多很多的Bean对象需要被放入IOC容器中管理,并且这些Bean对象存在替换策略(比如数据库驱动)。那么,我们除了在xml或者JavaConfig中进行配置外没有别的方法可以进行快速、可扩展、可替换的方式了吗?答案是肯定的。

SPI全称(Service Provider Interface),是Java提供的一套用来被第三方实现或者扩展的API,它可以用来启用框架扩展和替换组件。(图来自网上)
在这里插入图片描述
Java SPI实际上是"基于接口的编程+策略模式+配置文件"组合实现的动态加载机制。

系统设计的各个抽象,往往有很多不同的实现方案,在面向的对象的设计里,一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。为了实现在模块装配的时候能不在程序里动态指明,这就需要一种服务发现机制。 Java SPI就是提供这样的一个机制:为某个接口寻找服务实现的机制。有点类似IOC的思想,就是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要。所以SPI的核心思想就是解耦。

SpringFactoriesLoader工具类

那么,我们在了解了关于动态加载的实现方式以及理论基础(SPI)。下面我来介绍下在SpringBoot的自动装配中是如何做到Bean的动态加载的。

这里,我先说明下SpringFactoriesLoader这个工具类。它是SPI机制中为我们从配置类中转换出我们需要的类的一个工具类。这里,我简单说明下它的作用和原理。

SpringFactoriesLoader的主要作用就是依据SPI规则从META-INF/spring.factories中的文件中加载对应的Bean实例类。其中,在spring.factories中主要分为以下的key:


# Initializers

org.springframework.context.ApplicationContextInitializer=\

...

# Application Listeners

org.springframework.context.ApplicationListener=\

...

# Auto Configuration Import Listeners

org.springframework.boot.autoconfigure.AutoConfigurationImportListener=\

...

# Auto Configuration Import Filters

org.springframework.boot.autoconfigure.AutoConfigurationImportFilter=\

...

# Auto Configure

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\

...

# Failure analyzers

org.springframework.boot.diagnostics.FailureAnalyzer=\

...

# Template availability providers

org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider=\

通过key进行分类,value为对应配置的需要初始化的Bean的实例的全路径。

而SpringFactoriesLoader为我们提供了loadFactories、loadFactoryNames三个静态方式来从spring.factories中通过反射机制获取相对应的Bean实例。


private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {

   MultiValueMap<String, String> result = cache.get(classLoader);

   if (result != null) {

      return result;

   }

 

 

   try {

      Enumeration<URL> urls = (classLoader != null ?

            classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :

            ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));

      result = new LinkedMultiValueMap<>();

      while (urls.hasMoreElements()) {

         URL url = urls.nextElement();

         UrlResource resource = new UrlResource(url);

         Properties properties = PropertiesLoaderUtils.loadProperties(resource);

         for (Map.Entry<?, ?> entry : properties.entrySet()) {

            String factoryClassName = ((String) entry.getKey()).trim();

            for (String factoryName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {

               result.add(factoryClassName, factoryName.trim());

           }

         }

     }

      cache.put(classLoader, result);

      return result;

   }

   catch (IOException ex) {

      throw new IllegalArgumentException("Unable to load factories from location [" +

            FACTORIES_RESOURCE_LOCATION + "]", ex);

   }

}

举例:

我通过EnableAutoConfiguration来获取自动装配时需要的Bean


public static void main(String[] args) {

        List<String> configurations = SpringFactoriesLoader.loadFactoryNames(EnableAutoConfiguration.class,

                new ClassLoader() {

               });

        configurations.forEach(configuration -> {

            System.out.println(configuration);

       });

   }

结果:


org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration

org.springframework.boot.autoconfigure.aop.AopAutoConfiguration

org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration

...

Process finished with exit code 0

总结:

其实,这就是一种工厂+策略模式的结合体现。我们通过传入对应的策略参数后,由工厂类统一返回我们需要的数据。所以是SPI思想的最佳实践。

原理探究

至此,在说明自动装配原理的前置知识已经补充差不多了,我们下面正式开始探究SpringBoot自动装配原理的路程。

AutoConfigurationImportSelector类

进入EnableAutoConfiguration注解中,我们会发现这里的@Import注解不走寻常路:


@AutoConfigurationPackage

@Import(AutoConfigurationImportSelector.class)

public @interface EnableAutoConfiguration {

这里导入了AutoConfigurationImportSelector这个类。结合上面说的,Spring为我们提供的三种Bean的配置方式中其中之一就有实现ImportSelector接口。那么,我们一起研究下这个类。

这个类实现自ImportSelector,主要方法就是selectImports方法。


public interface ImportSelector {

   /**

    * Select and return the names of which class(es) should be imported based on

    * the {@link AnnotationMetadata} of the importing @{@link Configuration} class.

    */

   String[] selectImports(AnnotationMetadata importingClassMetadata);

}

这个方法通过@Import注解触发,然后我们可以在里面做一些逻辑处理,去动态生成我们需要的Bean对象。而AutoConfigurationImportSelector就是一个最佳的例子。

AutoConfigurationImportSelector中,提供的selectImports方法主要为我们返回IOC管理的自动装配时需要初始化Bean的名称。


@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());

}

其中,getAutoConfigurationEntry方法主要是获取的方法:


protected AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata,

      AnnotationMetadata annotationMetadata) {

   if (!isEnabled(annotationMetadata)) {

      return EMPTY_ENTRY;

   }

   AnnotationAttributes attributes = getAttributes(annotationMetadata);

  // 1、读取spring.factory文件加载Bean

   List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);

  // 2、去重

   configurations = removeDuplicates(configurations);

  // 3、获取过滤的Bean -- 不加载的Bean

   Set<String> exclusions = getExclusions(annotationMetadata, attributes);

  // 4、再次从配置文件中过滤不需要的Bean

   checkExcludedClasses(configurations, exclusions);

  // 5、移除不加载的Bean

   configurations.removeAll(exclusions);

   configurations = filter(configurations, autoConfigurationMetadata);

   fireAutoConfigurationImportEvents(configurations, exclusions);

   return new AutoConfigurationEntry(configurations, exclusions);

}

如上面源码中的我的注释,这里主要分为四大步骤:

1、getCandidateConfigurations方法中通过SpringFactoriesLoader从spring.factories中加载自动装配需要的Bean。


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;

}

当然,这里可以仿照加载我们自己希望的Bean。只要传入你自己的策略类就可以改变加载的key。(参考我上面的实例代码)

2、对于加载出的Bean名称进行去重,主要就是转换set操作。


protected final <T> List<T> removeDuplicates(List<T> list) {

   return new ArrayList<>(new LinkedHashSet<>(list));

}

3、获取启动时不希望加载的Bean。(条件加载)


protected Set<String> getExclusions(AnnotationMetadata metadata, AnnotationAttributes attributes) {

   Set<String> excluded = new LinkedHashSet<>();

   excluded.addAll(asList(attributes, "exclude"));

   excluded.addAll(Arrays.asList(attributes.getStringArray("excludeName")));

   excluded.addAll(getExcludeAutoConfigurationsProperty());

   return excluded;

}

4、移除过滤的Bean后,Spring就可以对于这些Bean进行IOC容器的注入了。最后在SpringBoot项目启动时,通过EnableAutoConfiguration注解将所有的Bean合并。

AutoConfigurationPackages.Registrar

同样的,AutoConfigurationPackages.Registrar方法也可以帮助我们实现动态的Bean管理。同样在EnableAutoConfiguration注解中,还有一个注解@AutoConfigurationPackage:


@Target(ElementType.TYPE)

@Retention(RetentionPolicy.RUNTIME)

@Documented

@Inherited

@Import(AutoConfigurationPackages.Registrar.class)

public @interface AutoConfigurationPackage {


}

这个注解内部调用了Registrar这个方法:


static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {

 

 

   @Override

   public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {

      register(registry, new PackageImport(metadata).getPackageName());

   }

 

 

   @Override

   public Set<Object> determineImports(AnnotationMetadata metadata) {

      return Collections.singleton(new PackageImport(metadata));

   }

 

 

}

这里提供了Bean的注册和销毁的两个方法。实现自ImportBeanDefinitionRegistrar这个接口:


public interface ImportBeanDefinitionRegistrar {

   void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry);

 

 

}

接口主要为我们提供了registerBeanDefinitions方法,这个方法用于Bean实例向IOC容器中的注册。所以,我们实现这个接口后,也可以实现自定义的Bean的动态管理。

举例:


/**

 * 实现Bean的动态管理

 *

 * @author fanyuanhang

 */

public class LoggerDefinitionRegistrar implements ImportBeanDefinitionRegistrar {

 

 

    @Override

    public void registerBeanDefinitions(AnnotationMetadata annotationMetadata, BeanDefinitionRegistry beanDefinitionRegistry) {

        Class beanClass = LoggerService.class;

        RootBeanDefinition beanDefinition = new RootBeanDefinition(beanClass);

        // 首字母小写转化

        String beanName = StringUtils.uncapitalize(beanClass.getSimpleName());

        beanDefinitionRegistry.registerBeanDefinition(beanName, beanDefinition);

   }

}

上面是我写的一个日志的Bean管理方法。我们可以在这里进行自己的逻辑判断和处理。

同理,这里操作的话,也需要通过EnableAutoConfiguration注解将Bean合并起来,所以在EnableAutoConfiguration注解内部增加了AutoConfigurationPackage注解。

条件过滤

AutoConfigurationMetadataLoader类

在AutoConfigurationImportSelector#selectImports方法中一开始就调用了这个类中的loadMetadata方法用来过滤一些根据其他Bean或者配置的条件来判断是否创建的Bean。

在分析 AutoConfigurationImportSelector 的源码时,会 先扫描 spring-autoconfiguration-metadata.properties 文件,最后在扫描 spring.factories 对应的类时,会结合 前面的元数据进行过滤,为什么要过滤呢? 原因是很多 的@Configuration 其实是依托于其他的框架来加载的, 如果当前的 classpath 环境下没有相关联的依赖,则意味 着这些类没必要进行加载,所以,通过这种条件过滤可以 有效的减少@configuration 类的数量从而降低 SpringBoot 的启动时间。

这些Bean主要维护在:

protected static final String PATH = “META-INF/” + “spring-autoconfigure-metadata.properties”;

这里主要分为如下的几种情况:(对照注解形式)


@ConditionalOnBean 在存在某个 bean 的时候

 

 

@ConditionalOnMissingBean 不存在某个 bean 的时候

 

 

@ConditionalOnClass 当前 classpath 可以找到某个类型的类时 

 

 

@ConditionalOnMissingClass 当前 classpath 不可以找到某个类型的类 时

 

 

@ConditionalOnResource 当前 classpath 是否存在某个资源文件

 

 

@ConditionalOnProperty  当前 jvm 是否包含某个系统属性为某个值

 

 

@ConditionalOnWebApplication 当前 spring context 是否是 web 应用程序

举例:


@SpringBootApplication

public class SpringBootFirstApplication {

    public static void main(String[] args) {

        ConfigurableApplicationContext ca =

                SpringApplication.run(SpringBootFirstApplication.class, args);

        Object condition = ca.getBean("TestClass");

        System.out.println(condition);

   }

}

@Configuration

public class TestClassConfig {

    @Bean(name = "TestClass")

    @ConditionalOnBean(name = "condition")

    public TestClass getTestClass() {

        return new TestClass();

   }

}

@Configuration

public class ConditionConfig {

    @Bean("condition")

    public Condition getCondition() {

        return new Condition();

   }

}

此时,若没有condition这个Bean,TestClass这个Bean就会获取失败;有的话就会成功。
流程图
在这里插入图片描述
总结

SpringBoot提供的自动装配功能,简单来说就是可以通过SPI思想来动态的选择加载项目启动时需要的Bean,从而达到不许用开发者去配置Bean,对于常用的外部依赖,添加相关的配置类,并提供多种配置策略来创建Bean。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值