SpringBoot自动配置原理解析

前言

小伙伴们都知道,现在市面上最流行的web开发框架就是springboot了,在springboot开始流行之前,我们都用的是strust2或者是springmvc框架来开发web应用,但是这两个框架都有一个特点就是配置非常的繁琐,要写一大堆的配置文件,spring在支持了注解开发之后稍微有些改观但有的时候还是会觉得比较麻烦,这个时候springboot就体现出了它的优势,springboot只需要一个properties或者yml文件就可以简化springmvc中在xml中需要配置的一大堆的bean,这就是因为springboot有自动配置,那么springboot自动配置的原理是什么呢,今天我们就来通过源码分析一下springboot的自动配置原理

开始

我以springboot整合redis为例,来向大家分析springboot的自动配置原理

首先创建一个springboot工程用来测试,然后在pom文件中引入springboot-starter-redis的启动器依赖

<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>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
            <version>2.1.7.RELEASE</version>
        </dependency>
</dependencies>

然后,在application.properties中配置redis属性

spring.redis.port=6379
spring.redis.host=localhost
spring.redis.database=0

然后,在启动类中注入redisTemplate类,redisTemplate为spring官方提供的对redis底层开发包(例如jedis)进行了深度封装的组件,使用redisTemplate可以优雅的操作redis。我在启动类中写了一个测试方法,向redis写入一条数据

@RequestMapping("/redistest")
    public String test(){
        redisTemplate.opsForSet().add("aaaaa","123456");
        return "OK";
    }

运行这个方法,打开redis客户端可以看到值已经写入了

5C596525-5B0D-545E-4287-B8AC07EF6FFF.png

先抛开这里的键和值让人看不懂的问题,大家是不是觉得springboot整合redis要比普通的springmvc整合redis简单多了?我只配置了redis的连接地址,端口号,注入了redisTemplate,就能开始操作redis了,那么springboot底层到底做了些什么使得整合变得如此的简单了呢。

首先我们来看,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

可以看到SpringbootApplication这个注解是由一系列的注解组合而成,这其中最重要的是@EnableAutoConfiguration和@ComponentScan,@ComponentScan的意思就是组件扫描注解,这个注解会自动注入所有在主程序所在包下的组件。比@ComponentScan注解更重要的就是@EnableAutoConfiguration注解了,这个注解的含义就是开启自动装配,直接把bean装配到ioc容器中,@EnableAutoConfiguration也是一个组合注解,我们点进去看一下

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

这个地方我们主要看@AutoConfigurationPackage和@Import(AutoConfigurationImportSelector.class)两个注解,首先来看@AutoConfigurationPackage注解

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

}

这个注解主要是获取我们注解所在包下的组件去进行注册,大家看到这个@Import注解,那么这个注解是什么含义呢,

@Import注解用来导入@Configuration注解的配置类、声明@Bean注解的bean方法、导入ImportSelector的实现类或导入ImportBeanDefinitionRegistrar的实现类,这里这个AutoConfigurationPackages.Registrar.class就是ImportBeanDefinitionRegistrar的实现类,来看下源码

static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {

        //metadata是注解的元信息 registry是bean定义的注册器
		@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));
		}

	}




public static void register(BeanDefinitionRegistry registry, String... packageNames) {
		//首先判断这个bean有没有被注册
		if (registry.containsBeanDefinition(BEAN)) {
			//获取bean定义
			BeanDefinition beanDefinition = registry.getBeanDefinition(BEAN);
			//通过bean定义获取构造函数值
			ConstructorArgumentValues constructorArguments = beanDefinition.getConstructorArgumentValues();
			//给构造函数添加参数值
			constructorArguments.addIndexedArgumentValue(0, addBasePackages(constructorArguments, packageNames));
		}
		else {
			//一个新的bean定义
			GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
			//设置beanClass为beanPackages类型
			beanDefinition.setBeanClass(BasePackages.class);
			beanDefinition.getConstructorArgumentValues().addIndexedArgumentValue(0, packageNames);
			beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
			//bean注册
			registry.registerBeanDefinition(BEAN, beanDefinition);
		}
	}

接下来就是@Import(AutoConfigurationImportSelector.class)这个注解,我们来看看AutoConfigurationImportSelector这个类,这个类是我们自动装配的导入选择器,首先看这个类的第一个方法,其实也就是这个类的核心方法

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

在这部分中,核心方法是getCandidateConfigurations,我们来看下这个方法

protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
		//从工厂中获取自动配置类
		List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
				getBeanClassLoader());
		//这句断言很重要,告诉了我们工厂是去哪里找自动配置类的,这里显然META-INF/spring.factories是一个路径
		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;
	}

那我们就找一下这个路径,去哪里找呢,我们看到这个类的包是org.springframework.boot.autoconfigure;那我们就到这个包的位置去找这个spring.factories,果不其然,我们点开这个文件

68A60F28-F5D5-4A98-2F2C-5CCB0EC191F2.png

38C4B962-63D5-211A-08B5-821CCD83179C.png

我们看到文件中有一行注释这Auto configure,表示这些都是自动配置相关的类,这里我们不得不说spring框架真的是强大,这里面居然有100多个自动配置类,我们找到redis有关的自动配置类

org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration,\
org.springframework.boot.autoconfigure.data.redis.RedisReactiveAutoConfiguration,\
org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration,\

这里我们需要的肯定是第一个自动配置类,我们点进去看看

@Configuration
//条件注解,某个class位于类路径上,才会实例化一个Bean,这个类是redis操作的类
@ConditionalOnClass(RedisOperations.class)
//使得@ConfigurationProperties 注解的类生效,这个类是配置redis属性的类
@EnableConfigurationProperties(RedisProperties.class)
//导入一些配置
@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
public class RedisAutoConfiguration {

	@Bean
	//仅仅在当前上下文中不存在某个对象时,才会实例化一个Bean,这个就是spring默认的redisTemplate
	@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;
	}

}

我们在application.properties中配置的redis属性,其实就是设置到了这个类中

//前缀spring.redis
@ConfigurationProperties(prefix = "spring.redis")
public class RedisProperties {

	/**
	 * Database index used by the connection factory.
	 */
	private int database = 0;

	/**
	 * Connection URL. Overrides host, port, and password. User is ignored. Example:
	 * redis://user:password@example.com:6379
	 */
	private String url;

	/**
	 * Redis server host.
	 */
	private String host = "localhost";

	/**
	 * Login password of the redis server.
	 */
	private String password;

	/**
	 * Redis server port.
	 */
	private int port = 6379;

	/**
	 * Whether to enable SSL support.
	 */
	private boolean ssl;

	/**
	 * Connection timeout.
	 */
	private Duration timeout;

	private Sentinel sentinel;

	private Cluster cluster;

	private final Jedis jedis = new Jedis();

	private final Lettuce lettuce = new Lettuce();
}

我们前面说了,用了spring默认的redisTemplate操作redis的话,存到redis里的数据对我们的阅读不友好,我们看不懂,那是因为redisTemplate中默认用了jdk自带的序列化器

927AB35A-4183-2D86-C5D7-716B381B894F.png

要想让数据变成我们能看得懂的样子,我们需要替换掉redisTempalte默认的序列化器,现在我就来实操一下,写一个配置类

@Configuration
public class RedisConfig {
    //这里的上下文已经有了自定义的redisTemplate,所以默认的redisTemplate不会生效
    @Bean
    public RedisTemplate<Object,Object> redisTemplate(RedisConnectionFactory redisConnectionFactory)  {
        RedisTemplate<Object,Object> redisTemplate=new RedisTemplate<>();
        //设置自定义序列化器
        redisTemplate.setDefaultSerializer(new Jackson2JsonRedisSerializer<Object>(Object.class));
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        return redisTemplate;
    }
}

然后我改写一下测试方法,一起来看结果

public String test(){
        redisTemplate.opsForSet().add("ffffff","55555555");
        return "OK";
    }

DF879FA9-D693-AA0F-3C6C-035D716F6E6A.png

我们看到,序列化器已经生效了,键值对已经是我们能看得懂的了。

总结

通过springboot整合redis的过程,我带大家分析了一下springboot的自动配置原理,基本上市面上流行的组件可以和spring整合的spring官方都有starter,引入starter,配合springboot的自动配置,基本上可以做到只需要几行属性的配置加上类的注入,就可以使用了,spring框架博大精深,还有很多很多东西需要学习,有时间我再给大家分享,望大家多多支持,谢谢。

收藏 (0)
评论列表
正在载入评论列表...
我是有底线的