【Spring boot高级特性】玩一玩Spring boot的自动装配

676 阅读6分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第5天
Spring Bean生命周期,从入门到进阶:
【Spring Bean生命周期】小白也能看懂的入门篇
【Spring Bean生命周期】高手如何解读源码(一) - 资源的扫描和注册
【Spring Bean生命周期】高手如何解读源码(二) - Bean的生命周期
【Spring Bean生命周期】聊透扩展点的流程及应用

  在前面的文章中,我们了解到了Spring是怎样扫描文件,以及注册为Bean的,但是本身xml的配置也是一个很大的工作量,让程序员来维护它仍然是一份很繁杂的工作。低效的人默默接受这种现状,而聪明的人已经在思考如何避免重复而低效的工作。

  我们发现,在XML维护中有几个工作是重复再做而收益又很小的,比如,当你在引用数据库组件时,驱动和ORM框架等在大多数情况下都是必须的,那么我们可以将这两个打一个包,定一个新的包,暂且叫'db-starter',当你在XML配置db-starter,Spring将默认把驱动和ORM框架都注册到IOC容器中,这样就少了一部分配置量。

  Spring boot的设计初衷就是帮助开发者减少工作量的。我们来看下,Spring boot在这一部分是如何对我们工作简化的

一  Spring boot 组件自动装配

  在Spring boot框架中,我们可以不必配置xml文件,就能完成Bean注入IOC的动作。这在之前的Spring是通过xml配置来说明要装配的组件,Spring boot为了简化开发人员的配置工作,遵循"约定大于配置,配置大于编码"原则,

  通过约定来实现IOC的注入,通过默认配置来代替常规的配置,从而简化了开发人员的配置和开发过程,提升研发效率。

二 Spring boot 如何实现自动装配

  整体流程是@EnableAutoConfiguration中的实现类

  1. 扫描所有包含/META-INF/spring.factories的jar包
  2. 根据spring.factories中xxxAutoConfiguration和/META-INF/spring-autoconfigure-metadata.properties中的条件判断来决定是否进行自动装配。

主类设置如下

@SpringBootApplication(exclude={DataSourceAutoConfiguration.class, HibernateJpaAutoConfiguration.class})
@EnableScheduling
@EnableConfigurationProperties({GlobalConfig.class})
public class ProxyApplication {
    public static void main(String[] args) {
        SpringApplication.run(ProxyApplication.class, args);
    }
}

我们知道,注解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 {}

而根据@Enable模块驱动设计模式===>即被@Configuration注解或实现ImportSelector接口,可实现注入)

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

//2. 实现了DeferredImportSelector接口,并重写selectImports方法
public class AutoConfigurationImportSelector
      implements DeferredImportSelector, BeanClassLoaderAware, ResourceLoaderAware,
      BeanFactoryAware, EnvironmentAware, Ordered {
}


//3. 继承ImportSelector接口
public interface DeferredImportSelector extends ImportSelector {}

基于@Enable和selectImports装配的方式我们知道,通过selectImports方法返回需要装配的对象数组,整个搜索流程如下:

   @Override
   public String[] selectImports(AnnotationMetadata annotationMetadata) {
      if (!isEnabled(annotationMetadata)) {
         return NO_IMPORTS;
      }
      AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
            .loadMetadata(this.beanClassLoader);//1. 获取classpath下自动加载配置的元数据
      AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(
            autoConfigurationMetadata, annotationMetadata);
      return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
   }

    protected AutoConfigurationEntry getAutoConfigurationEntry(
         AutoConfigurationMetadata autoConfigurationMetadata,
         AnnotationMetadata annotationMetadata) {
      if (!isEnabled(annotationMetadata)) {
         return EMPTY_ENTRY;
      }
      AnnotationAttributes attributes = getAttributes(annotationMetadata);//2.获取注解的属性信息
      List<String> configurations = getCandidateConfigurations(annotationMetadata,
            attributes);//3. 获取classpath下所有enable的候装配组件
      configurations = removeDuplicates(configurations); //4. 组件去重
      Set<String> exclusions = getExclusions(annotationMetadata, attributes);//5. exclude去重
      checkExcludedClasses(configurations, exclusions);//6. 组件去exclude
      configurations.removeAll(exclusions);//6. 组件去exclude
      configurations = filter(configurations, autoConfigurationMetadata);//7. 组件过滤
      fireAutoConfigurationImportEvents(configurations, exclusions);//8. 触发自动装配监听
      return new AutoConfigurationEntry(configurations, exclusions);
   }

整体流程:

  1. 获取自动加载配置的元数据:主要是获取classpath下所有META-INF/spring-autoconfigure-metadata.properties中的配置信息,该文件里面配置的作用等同于在xxxAutoConfiguration上面的@ConditionalOnClass等注解,这么做的好处就是参考官网说明(Spring Boot uses an annotation processor to collect the conditions on auto-configurations in a metadata file (META-INF/spring-autoconfigure-metadata.properties). If that file is present, it is used to eagerly filter auto-configurations that do not match, which will improve startup time.)总而言之,加速启动时间。

2.获取注解的属性信息:这里的注解是指在主类上的注解,所以获取出来的属性信息如图所示:

  1. 获取classpath下所有enable的候装配组件:获取的是META-INF/spring.factories中key为EnableAutoConfiguration的Value值的集合,此处META-INF/spring.factories可以是多个,EnableAutoConfiguration也可以是多个,此处原始数据DEBUG为118个(在spring.factories中定义的是)

4.5.6去重(此处无重复),去exclude(共2个)后,剩余116个

7.组件过滤:过滤对象是configurations,过滤条件是1中获取的autoConfigurationMetadata,过滤后,可被自动装配对象变为35个,比对删除的configurations,此处可以猜到autoConfigurationMetadata作用是配合configurations去除不符合条件的(比如当前classloader下不存在的class)configurations,同时也表明META-INF/spring-autoconfigure-metadata.properties中的Key-Value是对META-INF/spring.factories定义自动配置条件的提取。

  1. 触发自动装配监听

至此,selectImports流程结束,返回需要加载对象,@Enable驱动模块完成bean的装配

三 如何自定义starter?

完成自定义starter需要包含下面三个因素:

  • 主类激活@EnableAutoConfiguration注解
  • 组件配置/META-INF/spring.factories(约定,是否将组件下某个class加入自动装配管理)
  • 实现xxxAutoConfiguration组件配置类(符合什么条件才加入自动装配)

 那么,如何自定义一个自动装配组件呢?首先,我们先要创建个普通的maven工程,并且采用{name}-spring-boot-starter 的命名风格[自定义第三方均采取此风格],也就是我们平常常说的starter启动器,引入maven依赖

<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.zyq</groupId>
    <artifactId>arthorn-spring-boot-starter</artifactId>
    <version>0.1.2.Release</version>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-autoconfigure</artifactId>
            <version>2.1.4.RELEASE</version>
        </dependency>

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

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

</project>

项目的工程目录如图所示: ​

  1. 在resources/META-INF/spring.factories下面添加配置类ArthornAutoconfiguration

org.springframework.boot.autoconfigure.EnableAutoConfiguration= com.zyq.ArthornAutoconfiguration

  1. ArthornAutoconfiguration来决定组件装配的条件;

   在此处决定是否装载ArthornService.class,比如,此处只有当classpath下存在ArthornService.class,且不存在该Bean,且存在属性arthorn.enabled=true三个条件时,才装配ArthornService.class组件.

@Configuration
@EnableConfigurationProperties(ArthornProperties.class)
@ConditionalOnClass(ArthornService.class)
public class ArthornAutoconfiguration {

    @Autowired
    private ArthornProperties arthornProperties;

    @Bean
    @ConditionalOnMissingBean(ArthornService.class)
    @ConditionalOnProperty(name = "enabled",prefix = "arthorn", havingValue = "true")
    public ArthornService arthornService(){
        ArthornService arthornService = new ArthornService(arthornProperties);
        return arthornService;
    }
    
}

  ArthornService是我们要装配的业务组件,主要是组件的业务逻辑

public class ArthornService {

    private ArthornProperties arthornProperties;

    public ArthornService(ArthornProperties arthornProperties){
        this.arthornProperties = arthornProperties;
    }

    public String kill(String blood){
        if  ("first".equals(blood))
            return "Death is like the wind; always by my side";
        else if ("second".equals(blood))
            return "A sword's poor company for a long road.";
        else
            return "My honor left a long time ago.";
    }

}

ArthornProperties用于接收在application.properties配置的属性

/**
 * @author zyq
 * @description 亚索属性配置
 */
@ConfigurationProperties(prefix = "arthorn")
public class ArthornProperties {
    private String killNum;

    public String getKillNum() {
        return killNum;
    }

    public void setKillNum(String killNum) {
        this.killNum = killNum;
    }
}

执行maven install安装该组件到本地repository,完成starter启动器的制作,IDEA操作如图

image.png

接下来,就是在我们的Spring Boot项目中引入该依赖,并完成自动装配,新建Spring Boot项目testdemo,并引入我们的arthorn-spring-boot-starter依赖

<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.4.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>testdemo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>testdemo</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

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

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

        <dependency>
            <groupId>com.zyq</groupId>
            <artifactId>arthorn-spring-boot-starter</artifactId>
            <version>0.1.2.Release</version>
        </dependency>

    </dependencies>

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

</project>

在application.properties文件中添加属性配置

arthorn.blood=first

任意写一个接口,调用自动配置的类

@RestController
@RequestMapping("/test")
public class testController {

    @Autowired
    ArthornService arthornService;

    @GetMapping("/{blood}")
    public String test(@PathVariable String blood){
        return arthornService.kill(blood);
    }
}

启动报错,什么鬼?

原来是我们之前配置的条件注解,调用该组件需要在配置属性中添加arthorn.enabled=true,修改后启动成功,调用接口

image.png

SUCCESS!面对疾风吧

四 进阶:JPA的starter实现原理

  接下来,我们实例化一下通用组件JPA是如何在Spring Boot中完成自动装配的。我们不讨论Jpa的源码,只对实现自动装配的几个元素进行研究

  • spring-boot-starter-data-jpa的starter启动器结构
  • spring-boot-starter-data-jpa的xxxAutoConfiguration 随便创建一个Spring Boot项目,并引入jpa依赖 image.png

首先,来看下整个项目的maven依赖和Jpa的依赖,可以看到spring-boot-starter-data-jpa本身依赖于spring-data-jpa(注意区别,真正的服务实体在这个jar包里面,spring-boot-starter-data-jpa只是作为一个启动依赖),hibernate依赖,aop依赖等。

image.png

到maven仓库解压这个jar包,会发现里面有效信息只有一个pom,我们这里可以把spring-boot-starter-data-jpa看成是依赖多个jar包的空组件,它的作用主要是提供spring boot jpa自动注册需要的依赖。我们看到所有的官方自动配置组件的starter启动器都在该目录下

image.png

接下来,我们看下在IDEA双击shift,输入spring.factories来查看类路径下面所有的spring.factories文件,发现只有spring-boot-autoconfigure下面的spring.factories文件存在XXXEnableAutoConfiguration类,找到其中Jpa相关的XXXEnableAutoConfiguration,

该类的路径存在于spring-boot-starter-data-jpa的路径下,所有官方配置组件的XXXEnableAutoConfiguration类都在该路径下 image.png

其中,spring-boot-starter-data-jpa的路径下只存在对组件的自动注册类实现等,看一下具体的JpaRepositoriesAutoConfiguration


/*
 * Copyright 2012-2019 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.boot.autoconfigure.data.jpa;

import java.util.Map;

import javax.sql.DataSource;

import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.AnyNestedCondition;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.orm.jpa.EntityManagerFactoryBuilderCustomizer;
import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration;
import org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.data.jpa.repository.config.JpaRepositoryConfigExtension;
import org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBean;

/**
 * {@link EnableAutoConfiguration Auto-configuration} for Spring Data's JPA Repositories.
 * <p>
 * Activates when there is a bean of type {@link javax.sql.DataSource} configured in the
 * context, the Spring Data JPA
 * {@link org.springframework.data.jpa.repository.JpaRepository} type is on the classpath,
 * and there is no other, existing
 * {@link org.springframework.data.jpa.repository.JpaRepository} configured.
 * <p>
 * Once in effect, the auto-configuration is the equivalent of enabling JPA repositories
 * using the {@link org.springframework.data.jpa.repository.config.EnableJpaRepositories}
 * annotation.
 * <p>
 * This configuration class will activate <em>after</em> the Hibernate auto-configuration.
 *
 * @author Phillip Webb
 * @author Josh Long
 * @see EnableJpaRepositories
 */
@Configuration
@ConditionalOnBean(DataSource.class)
@ConditionalOnClass(JpaRepository.class)
@ConditionalOnMissingBean({ JpaRepositoryFactoryBean.class,
      JpaRepositoryConfigExtension.class })
@ConditionalOnProperty(prefix = "spring.data.jpa.repositories", name = "enabled",
      havingValue = "true", matchIfMissing = true)
@Import(JpaRepositoriesAutoConfigureRegistrar.class)
@AutoConfigureAfter({ HibernateJpaAutoConfiguration.class,
      TaskExecutionAutoConfiguration.class })
public class JpaRepositoriesAutoConfiguration {

   @Bean
   @Conditional(BootstrapExecutorCondition.class)
   public EntityManagerFactoryBuilderCustomizer entityManagerFactoryBootstrapExecutorCustomizer(
         Map<String, AsyncTaskExecutor> taskExecutors) {
      return (builder) -> {
         AsyncTaskExecutor bootstrapExecutor = determineBootstrapExecutor(
               taskExecutors);
         if (bootstrapExecutor != null) {
            builder.setBootstrapExecutor(bootstrapExecutor);
         }
      };
   }

   //省略
}

可以看到,只有满足上面定义的条件(@ConditionalOnBean等注解的含义请自行百度)才会自动装配,装配的时候采取调用原始的spring-data-jpa的jar包。这样设计既不侵入spring-data-jpa等原生组件的逻辑,又完成了spring-boot的自动装配。所以,这符合哪种设计模式呢?~~~~~