springboot原理-2-自动装配原理

1. springboot集成redis

springboot集成第三方组件,是非常容易的事情,以redis为例,pom文件中添加redis配置,

<dependency>
			<groupId>org.springframework.data</groupId>
			<artifactId>spring-data-redis</artifactId>
			<version>2.3.1.RELEASE</version>
		</dependency>

在application.properties配置文件中配置redis的连接信息(默认配置)

spring.redis.host=192.168.216.128

启动类

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

在需要使用redis的地方,注入RedisTemplate 并使用,仅此而已:

package com.gupaoedu.example.springbootdemo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
    
    
    @Autowired 
    private RedisTemplate<String,String> redisTemplate;
    @GetMapping("/say")
    public String say(){
    
    
        return redisTemplate.opsForValue().get("name");
    }
}

2. spring如何动态装载Bean

在上面能够实现注入的前提是IOC存在RedisTemplate 的实例,上一篇文章中,可以知道,SpringIOC Bean的装载有三种方式:

xml
Configuration配置类
EnableXXX 模块驱动 -》 springboot自动装配完成bean的自动装载

我们可以猜想一下RedisTemplate注入的实现过程:
我们加入了spring-data-redis 的jar包依赖,RedisTemplate 必然是存在于这个jar包中的,那么这个jar包里面是否存在一个配置类,通过如下方式把RedisTemplate 加载到IOC容器中:

@Configuration
public class RedisConfiguration{
    
    
	@Bean
	public RedisTemplate redisTemplate(){
    
    
		return new RedisTemplate();
	}
}

那么问题又来了:

  1. spring如何知道上面这个配置类在哪里
    2.如果pom添加了很多组件的依赖,这些组件的配置是如何批量扫描的

这里需要了解到spring的动态Bean的装载方式(根据条件或者上下文动态装载一些配置)

扫描二维码关注公众号,回复: 12426048 查看本文章
package com.gupaoedu.example.demo02;
public class GpRedisTemplate {
    
    
}
package com.gupaoedu.example.demo02;
import org.springframework.context.annotation.Bean;
/**
 * 配置类声明
 * 通过分包模拟不同的三方组件的引入
 **/
@Configuration
public class RedisConfiguration {
    
    
    @Bean
    public GpRedisTemplate gpRedisTemplate(){
    
    
        return new GpRedisTemplate();
    }
}

第二个包下的配置类:

package com.gupaoedu.example.demo03;
public class GpSqlSessionFactory {
    
    
}
package com.gupaoedu.example.demo03;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * 配置类声明
 * 用 不同的包模拟不同的jar包中的配置如何导入
 **/
@Configuration
public class MybatisConfiguration {
    
    

    @Bean
    public GpSqlSessionFactory gpSqlSessionFactory(){
    
    
        return new GpSqlSessionFactory();
    }
}

自定义ImportSelector的具体实现

package com.gupaoedu.example.demo04;

import com.gupaoedu.example.demo02.RedisConfiguration;
import com.gupaoedu.example.demo03.GpSqlSessionFactory;
import com.gupaoedu.example.demo03.MybatisConfiguration;
import org.springframework.context.annotation.ImportSelector;
import org.springframework.core.type.AnnotationMetadata;

/**
 * 模拟jar包的批量扫描
 **/
public class GpDefineImportSelector implements ImportSelector{
    
    
    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
    
    
       
        // 通过某种机制去完成指定路径的配置类的扫描就行?—— 约定优于配置  -》
        //package.class.classname  把各种的配置类的路径告诉spring行了 ——》标准:classpath:META-INF/spring.fatcories文件中配置
        // springboot只需要扫描classpath*:/META-INF/spring.factories文件即可

        // 告诉spring当前的配置类在哪里  
         //在这里去加载所有的配置类(通过某种机制完成指定的配置类的扫描),然后传入这个数组,就可以进行自动装配了
        return new String[]{
    
    MybatisConfiguration.class.getName(), RedisConfiguration.class.getName()};
    }
}

自定义Enable注解

package com.gupaoedu.example.demo04;

import org.springframework.context.annotation.Import;

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

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(GpDefineImportSelector.class)
public @interface EnableConfiguration {
    
    

}

通过EnableConfiguration ,最终实现Bean的自动装载:

package com.gupaoedu.example.springbootdemo;
import com.gupaoedu.example.demo02.GpRedisTemplate;
import com.gupaoedu.example.demo03.GpSqlSessionFactory;
import com.gupaoedu.example.demo04.EnableConfiguration;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.ComponentScan;

@EnableConfiguration // 
@SpringBootApplication
public class SpringBootDemoApplication {
    
    

	public static void main(String[] args) {
    
    
		ConfigurableApplicationContext ca=SpringApplication.run(SpringBootDemoApplication.class, args);
		System.out.println(ca.getBean(GpRedisTemplate.class));
        System.out.println(ca.getBean(GpSqlSessionFactory.class));		
	}
}

不加EnableConfiguration注解,获取不到GpRedisTemplate这个bean, 因为它不在SpringBootDemoApplication这个启动类的同级目录下
加上该注解后,通过@Import(GpDefineImportSelector.class) 动态导入了GpRedisTemplate 和MybatisConfiguration这两个配置类,
从而加载了GpRedisTemplate和GpSqlSessionFactory 这两个bean ,这就是ImportSelector 动态装载bean的实现原理;

3. springboot自动装配原理

上面整个过程中,关键的一个步骤就是GpDefineImportSelector 中把配置类添加到数组里面返回,那么是否有一种机制,能够让spring在importSelector里面能够加载所有的配置类呢? 显然是有的,spring中,在classpath:META-INF/spring.fatcories文件中配置了各种配置类,再回到springboot的@SpringBootApplication注解,它是一个复合注解,包含了@EnableAutoConfiguration注解:

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

	String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";

	/**
	 * Exclude specific auto-configuration classes such that they will never be applied.
	 * @return the classes to exclude
	 */
	Class<?>[] exclude() default {
    
    };

	/**
	 * Exclude specific auto-configuration class names such that they will never be
	 * applied.
	 * @return the class names to exclude
	 * @since 1.3.0
	 */
	String[] excludeName() default {
    
    };

}

在@Import注解里面,我们看到了AutoConfigurationImportSelector 这个自动装载的实现:

public class AutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware,
		ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered {
    
    

	private static final AutoConfigurationEntry EMPTY_ENTRY = new AutoConfigurationEntry();

	private static final String[] NO_IMPORTS = {
    
    };

	private static final Log logger = LogFactory.getLog(AutoConfigurationImportSelector.class);

	private static final String PROPERTY_NAME_AUTOCONFIGURE_EXCLUDE = "spring.autoconfigure.exclude";

	private ConfigurableListableBeanFactory beanFactory;

	private Environment environment;

	private ClassLoader beanClassLoader;

	private ResourceLoader resourceLoader;

	private ConfigurationClassFilter configurationClassFilter;

	@Override
	public String[] selectImports(AnnotationMetadata annotationMetadata) {
    
    
		if (!isEnabled(annotationMetadata)) {
    
    
			return NO_IMPORTS;
		}
		AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata);
		return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
	}

	@Override
	public Predicate<String> getExclusionFilter() {
    
    
		return this::shouldExclude;
	}

	private boolean shouldExclude(String configurationClassName) {
    
    
		return getConfigurationClassFilter().filter(Collections.singletonList(configurationClassName)).isEmpty();
	}

	/**
	 * Return the {@link AutoConfigurationEntry} based on the {@link AnnotationMetadata}
	 * of the importing {@link Configuration @Configuration} class.
	 * @param annotationMetadata the annotation metadata of the configuration class
	 * @return the auto-configurations that should be imported
	 */
	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);
	}
		……   // 其他代码省略
}

上面selectImports(AnnotationMetadata annotationMetadata) 就是导入各种自动配置类,具体的实现在getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) 方法中,我们在List configurations = getCandidateConfigurations(annotationMetadata, attributes); 这一行代码处打上断点,然后启动项目,可以看到加载了很多的配置类名:
在这里插入图片描述
在这里插入图片描述
其中当然也包含org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration 这个配置类,查看这个配置类的源码,赫然发现,redisTemplate 这个Bean是在这个配置类里面定义的!!!

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class)
@Import({
    
     LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
public class RedisAutoConfiguration {
    
    

	@Bean
	@ConditionalOnMissingBean(name = "redisTemplate")
	public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory)
			throws UnknownHostException {
    
    
		RedisTemplate<Object, Object> template = new RedisTemplate<>();
		template.setConnectionFactory(redisConnectionFactory);
		return template;
	}

	@Bean
	@ConditionalOnMissingBean
	public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory)
			throws UnknownHostException {
    
    
		StringRedisTemplate template = new StringRedisTemplate();
		template.setConnectionFactory(redisConnectionFactory);
		return template;
	}
}

所以,springboot自动装配的原理就是:

  • classpath:META-INF/spring.fatcories文件中配置各种配置类的全路径名
  • springboot启动时,由@SpringBootApplication -》 @EnableAutoConfiguration 去导入外部配置类
  • @Import(AutoConfigurationImportSelector.class) : AutoConfigurationImportSelector中,去加载spring.fatcories中定义的配置类,然后返回所有扫描到的配置类
  • 按照条件加载配置类中定义的各种Bean 到IOC容器中

Spring官方自有的自动装配类是放在spring-boot-autoconfigure-2.3.1.RELEASE.jar 这个jar包里面的spring.factories中的:(忽略版本号差异)
在这里插入图片描述

第三方组件的自动装配jar包是放在 XXX-spring-boot-autoconfigure-xxx.jar包中的META-INF/spring.factories中的:
在这里插入图片描述

4. 条件装配的两种方式

  • @ConditionalOnClass注解
    从上文中我们知道spring的官方包中,所有配置类都是放在spring-boot-autoconfigure-XXX.jar中的,而三方包的自动配置类才是放在META-INF/spring.factories文件中,那么针对于spring官方包中不按套路来的配置类,是如何加载的?以RedisAutoConfiguration为例
    在这里插入图片描述
    它有一个@ConditionalOnClass(RedisOperations.class)注解,当RedisOperations这个类在classpath中存在时,才会加载RedisAutoConfiguration这个配置类,而RedisOperations这个类又是在 spring-data-redis-2.3.1.RELEASE.jar 这个jar包中的,在这里插入图片描述
    也就是说,如果没有加入spring-data-redis的依赖,ConditionalOnClass条件不满足,spring就不会去扫描加载RedisAutoConfiguration这个配置类;
    因此,spring官方包,配置类是预先配置在spring-boot-autoconfigure-XXX.jar中的,通过条件来触发是否加载这个配置类,只有第三方的包,才需要在META-INF/factories中配置自动配置类的信息,告诉spring从哪里去扫描自动配置类

  • 通过AutoConfigurationMetadataLoader实现条件装配
    它的实现是,在META-INF目录下创建一个名字为
    spring-autoconfigure-metadata.properties的配置文件,配置文件内容是,配置类全路径名.ConditionalOnClass = 触发条件装配的类的全路径名 ,比如:
    com.gupaoedu.autoconfiguration.demo.GupaoConfiguration.ConditionalOnClass=com.gupaoedu.DemoClass
    在这里插入图片描述
    下面通过源码来说明如何实现的:
    在这里插入图片描述
    在org.springframework.boot.autoconfigure.AutoConfigurationImportSelector#getAutoConfigurationEntry 方法中,可以看到这样一行代码
    configurations = getConfigurationClassFilter().filter(configurations);
    这个filter方法是AutoConfigurationImportSelector的静态内部类ConfigurationClassFilter中的,
    在这里插入图片描述
    这个内部类中有这样一个赋值:

this.autoConfigurationMetadata = AutoConfigurationMetadataLoader.loadMetadata(classLoader);

查看该代码,就可以发现它是去加载META-INF/spring-autoconfigure-metadata.properties文件中的内容:
在这里插入图片描述
所以,如果Springboot单单全部加载spring.factories里的内容,那会在启动的时候会报很多类找达不到的错误,因为spring.factories是一个自动装配配置的全集,在我们没有加入某些jar包的情况下肯定会报相关类找不到的错误,而autoconfiguremetadata就是根据环境和配置剔除掉一部分spring.factories里的组件,所以在加载的时候不会报错!
参考文档:Springboot自动装配之spring-autoconfigure-metadata.properties

5. spring SPI机制(Service Provider Interface)

在AutoConfigurationImportSelector 这个类中,

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

这行代码返回所有的配置类,它是使用了SpringFactoriesLoader 来扫描classpath下的spring.factories中定义的配置类:

/**
	 * Return the auto-configuration class names that should be considered. By default
	 * this method will load candidates using {@link SpringFactoriesLoader} with
	 * {@link #getSpringFactoriesLoaderFactoryClass()}.
	 * @param metadata the source metadata
	 * @param attributes the {@link #getAttributes(AnnotationMetadata) annotation
	 * attributes}
	 * @return a list of candidate configurations
	 */
	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;
	}

SpringFactoriesLoader 就是SPI机制的一种体现。什么是SPI?
以JDK的数据库驱动为例,JDK中定义了java.jdbc.Driver这个接口,针对不同的数据库厂商,每个厂商所实现的数据库驱动都不相同,但是它们都遵循相同的规范,这个规范就是java.jdbc.Driver接口,接口的实现由第三方来完成,如果想要使用不同的实现,只需要更换引入的jar包即可, 这就是SPI 扩展点;
代码示例:
(1)新建一个工程:
在这里插入图片描述
定义一个接口,然后将这个工程安装到本地maven仓(需要把本地maven仓中的_remote.repositories文件删掉,否则依赖这个jar包时会有问题)

public interface DataBaseDriver {
    
    
    String buildCOnnect(String host);
}

在这里插入图片描述
(2)新建另外一个工程,来实现上面这个接口
在这里插入图片描述

  • pom中加入刚才定义的接口的依赖
<dependency>
      <groupId>com.gupao.spi</groupId>
      <artifactId>DataBaseDriver</artifactId>
      <version>1.0-SNAPSHOT</version>
    </dependency>
  • 实现接口定义:
public class MysqlDriver implements DataBaseDriver {
    
    
    @Override
    public String buildCOnnect(String s) {
    
    
        return "Mysql的驱动实现:"+s;
    }
}
  • SPI 扩展点设置
    与spring的spi类似,在resources目录下的META-INF目录下,创建一个services 目录,
    然后在目录下创建一个名字为第一个工程创建的接口的全路径名的文件com.gupao.spi.DataBaseDriver,
    文件里的内容就是该接口的实现类的名字
    在这里插入图片描述
    将上述工程安装到本地maven仓,去掉本地仓中的_remote.repositories文件
    (3)使用该扩展点

新建一个项目,引入上述两个工程的依赖

 <dependency>
      <groupId>com.gupao.spi</groupId>
      <artifactId>DataBaseDriver</artifactId>
      <version>1.0-SNAPSHOT</version>
    </dependency>
    <dependency>
      <groupId>com.gupao.spi.mysql</groupId>
      <artifactId>mysql-driver</artifactId>
      <version>1.0-SNAPSHOT</version>
    </dependency>

通过ServiceLoader来加载扩展点:

public class App 
{
    
    
    public static void main( String[] args )
    {
    
    
        ServiceLoader<DataBaseDriver> serviceLoader=ServiceLoader.load(DataBaseDriver.class);
        for(DataBaseDriver dbd:serviceLoader){
    
    
            System.out.println(dbd.buildCOnnect("Test"));
        }
    }
}

在这里插入图片描述
JAVA SPI机制的使用条件:

  • 需要在classpath目录下创建一个META-INF/services目录
  • 在该目录下创建一个扩展点的全路径名
    文件中填写该扩展点的实现的全路径名
    文件编码格式为UTF-8
  • 使用ServcieLoader去加载扩展点

猜你喜欢

转载自blog.csdn.net/weixin_41300437/article/details/108989791