Spring Boot (七) 自动配置原理及自定义starter

Spring Boot作为一个快速框架备受人们喜欢的原因是它简化了编码、简化了配置,而之所以简化则是因为它的自动配置,通过引入对应的依赖starter就完成了相应功能的整合,本章我们学习自动配置的原理,以及自定义一个starter。

1 自动配置

本次我们以mybatis为例来讲述自动配置。
回顾Spring整合mybatis的步骤如下:
1.导入jar包依赖:mysql-connector-java、mybatis、mybatis-spring、spring-jdbc。
2.配置bean:数据源参数、DataSource、SqlSessionFactory、@MapperScan或MapperScannerConfigurer
3.注册Mapper、*Mapper.xml。

可见需要导入大量的jar,需要配置大量的bean。

现在使用Spring Boot 整合mybatis

1.导入mybatis的starter和数据库jar

<dependency>
   <groupId>org.mybatis.spring.boot</groupId>
   <artifactId>mybatis-spring-boot-starter</artifactId>
   <version>2.1.3</version>
</dependency>
<dependency>
   <groupId>mysql</groupId>
   <artifactId>mysql-connector-java</artifactId>
   <version>5.1.47</version>
</dependency>

starter 里就为我们导入了mybatis、mybatis-spring、jdbc的jar包。
2. 配置数据源参数

spring:
  datasource:
    url: jdbc:mysql://121.36.168.192:3306/demotest
    driver-class-name: com.mysql.jdbc.Driver
    username: root
    password: root

3. 创建mapper和实体类

UserMapper

@Mapper
public interface UserMapper {

    @Select("select * from user")
    public List<User> queryList();

}

User

public class User {
    private Integer id;
    private String name;
    private Integer age;
    private String email;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", age=" + age +
                ", email='" + email + '\'' +
                '}';
    }
}

测试:

@SpringBootTest
class SpringbootLearnDemo04MybatisApplicationTests {

    @Autowired
    private UserMapper userMapper;
    @Test
    void contextLoads() {
        List<User> users = userMapper.queryList();
        users.stream().forEach(user -> {
            System.out.println(user.getName()+":"+user.getAge()+":"+user.getEmail());
        });
    }

}

以上就是Spring Boot对Mybatis的整合,没有配置dataSource,没有配置SqlSessionFactory,也没有对Mapper进行扫描,这些都是Spring Boot帮我们去做的。

2 自动配置原理分析

2.1 starter 的作用

查看依赖mybatis-spring-boot-starter
在这里插入图片描述
可以看到只有pom文件

<?xml version="1.0" encoding="UTF-8"?>
<!--

       Copyright 2015-2020 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

          http://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.

-->
<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.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot</artifactId>
    <version>2.1.3</version>
  </parent>
  <artifactId>mybatis-spring-boot-starter</artifactId>
  <name>mybatis-spring-boot-starter</name>
  <properties>
    <module.name>org.mybatis.spring.boot.starter</module.name>
  </properties>
  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-jdbc</artifactId>
    </dependency>
    <dependency>
      <groupId>org.mybatis.spring.boot</groupId>
      <artifactId>mybatis-spring-boot-autoconfigure</artifactId>
    </dependency>
    <dependency>
      <groupId>org.mybatis</groupId>
      <artifactId>mybatis</artifactId>
    </dependency>
    <dependency>
      <groupId>org.mybatis</groupId>
      <artifactId>mybatis-spring</artifactId>
    </dependency>
  </dependencies>
</project>

pom里帮我们引入了mybatis、jdbc的依赖,其中还有一个mybatis-spring-boot-autoconfigure,进入依赖可以发现有一个MybatisAutoConfiguration类,见名知意,Mybatis的自动配置。
部分代码如下:

@org.springframework.context.annotation.Configuration
@ConditionalOnClass({ SqlSessionFactory.class, SqlSessionFactoryBean.class })
@ConditionalOnSingleCandidate(DataSource.class)
@EnableConfigurationProperties(MybatisProperties.class)
@AutoConfigureAfter({ DataSourceAutoConfiguration.class, MybatisLanguageDriverAutoConfiguration.class })
public class MybatisAutoConfiguration implements InitializingBean {

  
  @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())) {
      factory.setConfigLocation(this.resourceLoader.getResource(this.properties.getConfigLocation()));
    }
    applyConfiguration(factory);
    if (this.properties.getConfigurationProperties() != null) {
      factory.setConfigurationProperties(this.properties.getConfigurationProperties());
    }
    if (!ObjectUtils.isEmpty(this.interceptors)) {
      factory.setPlugins(this.interceptors);
    }
    if (this.databaseIdProvider != null) {
      factory.setDatabaseIdProvider(this.databaseIdProvider);
    }
    if (StringUtils.hasLength(this.properties.getTypeAliasesPackage())) {
      factory.setTypeAliasesPackage(this.properties.getTypeAliasesPackage());
    }
    if (this.properties.getTypeAliasesSuperType() != null) {
      factory.setTypeAliasesSuperType(this.properties.getTypeAliasesSuperType());
    }
    if (StringUtils.hasLength(this.properties.getTypeHandlersPackage())) {
      factory.setTypeHandlersPackage(this.properties.getTypeHandlersPackage());
    }
    if (!ObjectUtils.isEmpty(this.typeHandlers)) {
      factory.setTypeHandlers(this.typeHandlers);
    }
    if (!ObjectUtils.isEmpty(this.properties.resolveMapperLocations())) {
      factory.setMapperLocations(this.properties.resolveMapperLocations());
    }
    Set<String> factoryPropertyNames = Stream
        .of(new BeanWrapperImpl(SqlSessionFactoryBean.class).getPropertyDescriptors()).map(PropertyDescriptor::getName)
        .collect(Collectors.toSet());
    Class<? extends LanguageDriver> defaultLanguageDriver = this.properties.getDefaultScriptingLanguageDriver();
    if (factoryPropertyNames.contains("scriptingLanguageDrivers") && !ObjectUtils.isEmpty(this.languageDrivers)) {
      // Need to mybatis-spring 2.0.2+
      factory.setScriptingLanguageDrivers(this.languageDrivers);
      if (defaultLanguageDriver == null && this.languageDrivers.length == 1) {
        defaultLanguageDriver = this.languageDrivers[0].getClass();
      }
    }
    if (factoryPropertyNames.contains("defaultScriptingLanguageDriver")) {
      // Need to mybatis-spring 2.0.2+
      factory.setDefaultScriptingLanguageDriver(defaultLanguageDriver);
    }

    return factory.getObject();
  }

  @Bean
  @ConditionalOnMissingBean
  public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
    ExecutorType executorType = this.properties.getExecutorType();
    if (executorType != null) {
      return new SqlSessionTemplate(sqlSessionFactory, executorType);
    } else {
      return new SqlSessionTemplate(sqlSessionFactory);
    }
  }

  /**
   * This will just scan the same base package as Spring Boot does. If you want more power, you can explicitly use
   * {@link org.mybatis.spring.annotation.MapperScan} but this will get typed mappers working correctly, out-of-the-box,
   * similar to using Spring Data JPA repositories.
   */
  public static class AutoConfiguredMapperScannerRegistrar implements BeanFactoryAware, ImportBeanDefinitionRegistrar {

    private BeanFactory beanFactory;

    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {

      if (!AutoConfigurationPackages.has(this.beanFactory)) {
        logger.debug("Could not determine auto-configuration package, automatic mapper scanning disabled.");
        return;
      }

      logger.debug("Searching for mappers annotated with @Mapper");

      List<String> packages = AutoConfigurationPackages.get(this.beanFactory);
      if (logger.isDebugEnabled()) {
        packages.forEach(pkg -> logger.debug("Using auto-configuration base package '{}'", pkg));
      }

      BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class);
      builder.addPropertyValue("processPropertyPlaceHolders", true);
      builder.addPropertyValue("annotationClass", Mapper.class);
      builder.addPropertyValue("basePackage", StringUtils.collectionToCommaDelimitedString(packages));
      BeanWrapper beanWrapper = new BeanWrapperImpl(MapperScannerConfigurer.class);
      Stream.of(beanWrapper.getPropertyDescriptors())
          // Need to mybatis-spring 2.0.2+
          .filter(x -> x.getName().equals("lazyInitialization")).findAny()
          .ifPresent(x -> builder.addPropertyValue("lazyInitialization", "${mybatis.lazy-initialization:false}"));
      registry.registerBeanDefinition(MapperScannerConfigurer.class.getName(), builder.getBeanDefinition());
    }

    @Override
    public void setBeanFactory(BeanFactory beanFactory) {
      this.beanFactory = beanFactory;
    }

  }

  /**
   * If mapper registering configuration or mapper scanning configuration not present, this configuration allow to scan
   * mappers based on the same component-scanning path as Spring Boot itself.
   */
  @org.springframework.context.annotation.Configuration
  @Import(AutoConfiguredMapperScannerRegistrar.class)
  @ConditionalOnMissingBean({ MapperFactoryBean.class, MapperScannerConfigurer.class })
  public static class MapperScannerRegistrarNotFoundConfiguration implements InitializingBean {

    @Override
    public void afterPropertiesSet() {
      logger.debug(
          "Not found configuration for registering mapper bean using @MapperScan, MapperFactoryBean and MapperScannerConfigurer.");
    }

  }

}

可以看到该类里面配置了方法bean装配了SqlSessionFactory、SqlSessionTemplate并且加了条件@ConditionalOnMissingBean,即如果Spring中不存在这个bean就会生效,以及扫描@Mapper注解的静态内部类AutoConfiguredMapperScannerRegistrar。

在看自动配置类上的注解:

@Configuration: 表明这是一个配置类,生成cglib的代理对象。
@ConditionalOnClass({ SqlSessionFactory.class, SqlSessionFactoryBean.class }): 当存在这两个class时生效。
@ConditionalOnSingleCandidate(DataSource.class): 存在一个单例的DataSource的bean时生效。
@EnableConfigurationProperties(MybatisProperties.class): 加载Mybatis的配置为spring的以bean
@AutoConfigureAfter({ DataSourceAutoConfiguration.class, MybatisLanguageDriverAutoConfiguration.class }): 在这两个自动配置类之后生效

可以看出最后一个注解中的DataSourceAutoConfiguration这又是DataSource的自动配置。这样就完成对Mybatis所需的bean的配置。

2.2 自动配置原理

由上节我们知道通过自动配置类MybatisAutoConfiguration配置mybatis所需要的bean,那么自动配置类是什么时候加载的呢?这就需要看我们Spring Boot的启动类,我们知道每个启动类上都有@SpringBootApplication,它的作用是什么呢?

@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 {

}

可以看到该注解上面又有许多注解:
前四个不用说是元注解
@SpringBootConfiguration 表明这是一个Spring Boot的配置类
@ComponentScan 这是扫描注解的配置

重点@EnableAutoConfiguration,由名字可以看出这就是我们要找的目标开启自动配置,进入该注解

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

}

@AutoConfigurationPackage:将启动类所在包注册为一个BasePackages类型的Bean,作为基本包。
@Import(AutoConfigurationImportSelector.class) 读取并解析自动配置类。

AutoConfigurationImportSelector部分代码如下:

public class AutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware,
		ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered {
/**
 * 省略
 */

protected AutoConfigurationEntry getAutoConfigurationEntry(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 = getConfigurationClassFilter().filter(configurations);
		fireAutoConfigurationImportEvents(configurations, exclusions);
		return new AutoConfigurationEntry(configurations, exclusions);
	}

/**
 * 省略
 */

}

AutoConfigurationImportSelector 实现的DeferredImportSelector接口,在bean工厂后置处理器ConfigurationClassPostProcessor中会调用其getAutoConfigurationEntry方法,而该方法中如上图代码中会调用getCandidateConfigurations方法。

protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
		// SPI 加载自动配置类
		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;
	}

第一行代码就是前面几篇文章中我们多次提到过的spring的SPI机制,通过SpringFactoriesLoader的loadFactoryNames方法加载META-INF/spring.factories文件中指定配置,而方法中传入的类型是通过getSpringFactoriesLoaderFactoryClass方法获取的,该方法返回EnableAutoConfiguration,所以此处是在加载自动配置类。找到spring.factories文件,如下图果然配置了
MybatisAutoConfiguration

在这里插入图片描述
这样就找到了MybatisAutoConfiguration,这就是整合mybatis整个自动配置的过程,当然其它整合的自动配置也是这个原理。

3 自定义starter

通过上文自动配置的原理分析我们可以总结出starter解决了以下几个问题:

  1. 自动创建哪些bean
  2. 在什么条件下创建
  3. 被创建bean的属性配置

由mybatis的starter我们可以分析出,starter大概分为三个部分:

  1. 第三方jar包工程
  2. 自动配置jar包工程
  3. starter工程

下面我们来自定义一个starter。

3.1 创建第三方jar包demo

创建一个普通maven工程springbootdemo作为第三方jar包:

在这里插入图片描述
该工程不添加任何依赖,只有一个类ConnectionService

public class ConnectionService {
    public String connect() {

        return "Spring Boot Starter Connection Success";
    }
}

3.2 创建自动配置工程

创建maven工程springbootdemo-spring-boot-autoconfigura作为自动配置jar包。在pom文件中引入自动配置的依赖,以及第三方jar包,即上面创建的工程springbootdemo,并配置optional属性为true,表示依赖不会传递。如下:

<?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.jt.learn</groupId>
    <artifactId>springbootdemo-spring-boot-autoconfigura</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <dependency>
            <groupId>com.jt.learn</groupId>
            <artifactId>springbootdemo</artifactId>
            <version>1.0-SNAPSHOT</version>
            <!-- 可选属性 依赖不会传递 -->
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-autoconfigure</artifactId>
            <version>2.3.3.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <version>2.3.3.RELEASE</version>
        </dependency>
    </dependencies>

</project>

创建自动配置类:

@Configuration
@ConditionalOnClass(ConnectionService.class)
@EnableConfigurationProperties(SpringbootdemoProperties.class)
public class SpringbootdemoAutoConfiguration {

    @Autowired
    private SpringbootdemoProperties springbootdemoProperties;

    @Bean
    @ConditionalOnProperty(prefix = "jt.demo", name = "enable", havingValue = "true")
    public ConnectionService connectionService() {
        ConnectionService connectionService = new ConnectionService();
        connectionService.setName(springbootdemoProperties.getName());
        return connectionService;
    }

}

创建自动配置属性类:

@ConfigurationProperties(prefix = "jt.demo")
public class SpringbootdemoProperties {
    private String name;
    private boolean enabled;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public boolean isEnabled() {
        return enabled;
    }

    public void setEnabled(boolean enabled) {
        this.enabled = enabled;
    }
}

创建SPI配置文件/META-INF/spring.factories,配置内容如下:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.jt.learn.autoconfigura.SpringbootdemoAutoConfiguration

项目结构如下:
在这里插入图片描述

3.3 创建starter工程

starter工程只需要引入自动配置的jar和第三方的jar就可以了

<?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.jt.learn</groupId>
    <artifactId>springbootdemo-spring-boot-starter</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <dependency>
            <groupId>com.jt.learn</groupId>
            <artifactId>springbootdemo-spring-boot-autoconfigura</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>com.jt.learn</groupId>
            <artifactId>springbootdemo</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>

</project>

项目结构如下:
在这里插入图片描述

3.4 测试

创建一个Spring Boot 工程测试。
pom中加入上面创建的starter依赖

<dependency>
            <groupId>com.jt.learn</groupId>
            <artifactId>springbootdemo-spring-boot-starter</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>

配置application.yaml文件

jt:
  demo:
    enabled: true
    name: tom

测试类直接注入ConnectionService

@SpringBootTest
class SpringBootCustomerstarterApplicationTests {

    @Autowired
    private ConnectionService connectionService;

    @Test
    void contextLoads() {
        System.out.println(connectionService.connect());
    }

}

测试通过:

在这里插入图片描述
修改application.yaml的配置:jt.demo.enabled改为false

jt:
  demo:
    enabled: false
    name: tom

在此运行测试类报错

org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'com.jt.learn.springbootcustomerstarter.SpringBootCustomerstarterApplicationTests': Unsatisfied dependency expressed through field 'connectionService'; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.jt.learn.service.ConnectionService' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}
	at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:643) ~[spring-beans-5.2.9.RELEASE.jar:5.2.9.RELEASE]
	at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:130) ~[spring-beans-5.2.9.RELEASE.jar:5.2.9.RELEASE]
	at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessProperties(AutowiredAnnotationBeanPostProcessor.java:399) ~[spring-beans-5.2.9.RELEASE.jar:5.2.9.RELEASE]
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1420) ~[spring-beans-5.2.9.RELEASE.jar:5.2.9.RELEASE]

可以发现错误原因是注入失败,找不到connectionService这个bean。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值