springboot自动配置以及原理分析 - Go语言中文社区

springboot自动配置以及原理分析


本文所用的springboot版本为2.0.5.RELEASE

一、springboot的自动配置

​ 1、从一个数据库依赖说起

​ 很多人刚接触springboot的时候都踩过这样一个坑:在pom文件中不小心加了db相关的依赖,比如

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

​ 然后项目就启动不了,报错如下:
在这里插入图片描述

​ 看报错中框出来的部分——数据源没有指定url。what?这个pom依赖只是我不小心加的啊,它怎么就自动给我配了一个数据源,还由于没配url这些数据源相关的配置导致报错了。

​ 再回想起我们之前搭ssm框架的时候是如何配置datasource的呢?

  <!-- - - - -数据源配置 - - - - - -->
    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"
          destroy-method="close">
        <property name="driverClass" value="${driverClass}"/>
        <property name="jdbcUrl" value="${jdbcUrl}"/>
        <property name="user" value="${user}"/>
        <property name="password" value="${password}"/>
    </bean>

​ 在spring配置xml中配置一个datasource的bean,注册进ioc容器。那么,我们是否可以猜想,springboot由于我们加了一开始说的spring-boot-starter-jdbc依赖,所以它自动帮我们往ioc容器里注入了一个datasource的bean?

tips:这个报错可以在springboot启动类中排除datasource自动配置类来解决,原因后文会讲到。

@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)

​ 2、自动配置

​ springboot的自动配置,指的是springboot,会自动将一些配置类的bean注册进ioc容器,我们可以需要的地方使用@autowired或者@resource等注解来使用它。

​ “自动”的表现形式就是我们只需要引我们想用功能的包,相关的配置我们完全不用管,springboot会自动注入这些配置bean,我们直接使用这些bean即可。

二、springboot配置bean以及注册bean的几种方式

​ 在讲springboot自动配置如何实现之前,我们先复习一下springboot的配置类和把配置类注册进ioc容器的几种方式。

​ springboot的优点之一就是近乎零配置,我们可以抛弃繁杂的xml,使用代码来配置bean。例如:

配置bean

有一个简单的闹钟服务类,功能是打印当前时间

/**
 * Created by Sun on 2018/9/19
 */
public class ClockService {

    public void showTime() {
        System.out.println("today is "+ new Date());
    }
}

注册bean到ioc容器的几种方式

  1. 在上面的配置类上加@Service或者@Component等注解,springboot会扫描启动类所在的包下面所有带有这些注解的类,实例化bean加到ioc容器。
/**
 * Created by Sun on 2018/9/19
 */
@Service
public class ClockService {

    public void showTime() {
        System.out.println("today is "+ new Date());
    }
}
  1. 使用@Configuration@Bean注解来配置bean到ioc容器,这个类也需要在springboot启动类所在的包或者子包下面,否则无法扫到。

    删除上面的@Service注解,新加一个配置类

/**
 * Created by Sun on 2018/9/19
 */
@Configuration
public class BeanConfig {

    @Bean
    public ClockService clockService() {
        return new ClockService();
    }
}
  1. 使用@Import注解

    相信很多人对@EnableScheduling@EnableCaching等@Enablexxxx系列的注解都不陌生,它们就是使用的是@Import注解来实现开启xx功能的。比如说我们熟悉的@EnableScheduling 注解:
    在这里插入图片描述

    注释掉1,2中的所有内容。在springboot启动类上加一行代码:

    @Import(ClockService.class)

    @SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
    @Import(ClockService.class)
    public class AutoConfDemoApplication {
    
    	public static void main(String[] args) {
    		SpringApplication.run(AutoConfDemoApplication.class, args);
    	}
    }
    

    这样,我们依然可以把我们自定义的ClockService实例化一个bean加到ioc容器

    注意 :1.2点其实是可以给bean设置名称的,比如说@Bean(name=“xx”),这样在一个类型多个实例bean的时候可以按名称注入(@Autowire是默认按类型注入,但可以用Qualifier注解来指定bean的名称; @Resource是按名称注入),第3点只适用于一个类只有一个实例bean的情况下。

    ps:@Import注解其实并不是只能导入@Configuration修饰的配置类,还有很多更强大的功能,这里暂时不详细讨论。

  2. 第4点就是这篇文章要讲的,springboot的自动配置。下面讲springboot是如何实现它的。具体实现请看四。

验证注入成功

​ 写一个随着springboot项目启动而启动的runner,注入这个闹钟服务类。

/**
 * Created by Sun on 2018/9/19
 */
@Component
public class Runner implements ApplicationRunner {

    @Autowired
    private ClockService service;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        service.showTime();
    }
}

无论以上面4点的哪种方式,都能注入成功。启动工程,打印日志如下:

在这里插入图片描述

三、结合源码分析自动配置是如何实现的

源码分析

​ 我们都知道springboot最核心的注解 @SpringBootApplication等于@SpringBootConfiguration@EnableAutoConfiguration@ComponentScan等注解的作用,顾名思义,@EnableAutoConfiguration 显然就是springboot实现自动配置的核心所在。
在这里插入图片描述

​ 我们来看看AutoConfigurationImportSelector类,重点是selectImports方法
在这里插入图片描述
看方法名——getCandidateConfigurations,获取候选配置,由此我们可以猜想这一步就是springboot获取所有用@Configuration注解修饰的配置类的名称,那么为什么叫做“候选”配置呢?往下看,根据方法名,我们就能知道方法做了什么,接下来就是从这里获取的候选配置的list里,剔除重复部分,再剔除一开始我们@SpringbootApplication 注解里exclude掉的配置,(一、1.tipis的原因。) 才得到最终的配置类名集合。

​ 接下来细看getCandidateConfigurations方法是如何拿到这些配置类名称的

在这里插入图片描述

​ 再进一步看SpringFactoriesLoader.loadFactoryNames方法,又调用了loadSpringFactories来获取,来看

loadSpringFactories方法。

​ 方法里用到的全局变量:
在这里插入图片描述

一个Map类型的cache,key为类加载器,value是一个MultiValueMap,MultiValueMap 类定义如下:

public interface MultiValueMap<K, V> extends Map<K, List<V>> 

是一个key可以对应多个value的map类型。

​ 方法里用到的常量:
public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";

指定了classloader去load的路径。

方法的详细注释:

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是appclassloader,getResources能获取所有依赖jar里面的META-INF/spring.factories的完整路径
			ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
			result = new LinkedMultiValueMap<>();
			while (urls.hasMoreElements()) { // 遍历上述返回的url集合
				URL url = urls.nextElement(); // URL类可以获取来自流,web,甚至jar包里面的资源
				UrlResource resource = new UrlResource(url);
				Properties properties = PropertiesLoaderUtils.loadProperties(resource);
				for (Map.Entry<?, ?> entry : properties.entrySet()) { // 解析spring.factories
					List<String> factoryClassNames = Arrays.asList(
					StringUtils.commaDelimitedListToStringArray((String) entry.getValue()));
					// spring.facories中配置的不仅仅有自动配置相关的内容,还有其他比如 								// ApplicationContextInitializer等等各种springboot启动的时候,初始化spring环					  // 境需要的配置,自动配置只是其中一项。这个cache也是在springboot启动阶段就赋值的
                      result.addAll((String) entry.getKey(), factoryClassNames);
				}
			}
			cache.put(classLoader, result);
			return result;
		}
		catch (IOException ex) {
			throw new IllegalArgumentException("Unable to load factories from location [" +
					FACTORIES_RESOURCE_LOCATION + "]", ex);
		}
	}

spring.provides和spring.factories和@ConditionOnxxx注解

​ 结合上面的内容,再结合一、1的例子,我们猜想 spring-boot-starter-jdbc的jar里面,有个META-INF目录,目录里有spring.factories文件,文件里配置了springboot的自动配置类的名字,所以我们加了这个依赖,springboot就会自动加载spring.factories里配置的自动配置类,于是我们去打开看看里面到底长什么样子吧。

​ 去maven仓里找到这个jar,右键解压

在这里插入图片描述

在这里插入图片描述

和想象中的不太一样啊,spring.factories文件去哪了?spring.provides文件又是什么鬼?

​ 别慌,点开看看,在这里插入图片描述

只有这一行内容。看这个名字,provide,提供者,难道是这个文件又给我们自动加了这三个依赖,于是我们可以不用手动再去pom文件里再引这三个包?

​ ok,按照这个猜想,我们再去maven仓里找到这三个jar包,分别解压看看。
在这里插入图片描述

也没有期待中的spring.factories文件,另外两个也没有。

​ 这又是什么情况,之前的猜想错了吗,那为什么加了spring-boot-starter-jdbc依赖后,就非得我们配置数据源信息,除非我们手动exclude这个配置类,不然就报错呢。难道这个配置类不是这个依赖提供的吗?是的话,为何spring.factories文件找不到呢?

@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)

我们点进去DataSourceAutoConfiguration.class,看看代码先:
在这里插入图片描述

发现这么一个注解:有xx条件则生效,那么这一条的意思是当DataSource.class, EmbeddedDatabaseType.class被classloader加载,则这个配置类生效。

​ ok,那是否是这个配置类是springboot本身的配置类,只不过本身不生效,刚好我们配置了spring-boot-starter-jdbc的依赖后,给了它这个生效的条件呢?我们来验证一下:

  • 既然DataSourceAutoConfiguration.class 是springboot自带的配置类,那么肯定可以在spring.factories里找到自动配置相关的内容,我们打开springboot框架自身的META-INF/spring.factories文件
    在这里插入图片描述

自动配置相关的配置里,果然找到了DataSourceAutoConfiguration.class 这一项:
在这里插入图片描述

  • 既然spring-boot-starter-jdbc依赖给DataSourceAutoConfiguration这个配置类带来了生效条件:
    在这里插入图片描述

    那么这个依赖中必定有DataSource.class或者EmbeddedDatabaseType.class,而DataSource类又是javax.sql包中的类,是必带的,因此spring-boot-starter-jdbc依赖包中肯定带有EmbeddedDatabaseType.class,

    又由于之前说的,spring.provides里的内容springboot也会自动引用,我们最终在spring-boot-starter-jdbc依赖包中spring.provides文件中引用的spring-jdbc依赖中,找到了EmbeddedDatabaseType.class。

在这里插入图片描述

总结

​ springboot自身的autoconfigure包里有大量的java配置类,我们也可以在自己的工程中写这些配置类,这些配置类需要在相应的META-INF/spring.facotries文件中配置好,如下:
在这里插入图片描述

​ 这样就会因为在@EnableAutoConfiguration注解的存在,这些配置类里面的bean被注册进ioc容器,不过也是有条件的,条件注解ConditionOnxxx。下面列一些常用的Condition注解:

@ConditionalOnBean(仅仅在当前上下文中存在某个对象时,才会实例化一个Bean)
@ConditionalOnExpression(当表达式为true的时候,才会实例化一个Bean)
@ConditionalOnMissingBean(仅仅在当前上下文中不存在某个对象时,才会实例化一个Bean)
@ConditionalOnMissingClass(某个class类路径上不存在的时候,才会实例化一个Bean)
@ConditionalOnNotWebApplication(不是web应用)

@ConditionalOnClass(当注解在方法上,某个class位于类路径上,才会实例化一个Bean)
@ConditionalOnClass (当注解于类上, 某个class位于类路径上,否则不解析该注解修饰的配置类)

四、代码验证

​ 我们前三点已经写过部分代码了,我们接下来完善它,来验证自动配置。

这个工程叫auto-conf-demo,目录结构如下:
在这里插入图片描述

首先屏蔽@Service注解
在这里插入图片描述

由于要拿这个工程当做一个jar包来验证自动配置,这里屏蔽该工程自己的runner
在这里插入图片描述

resource目录下新建META-INF/springfactories文件,声明自己的自动配置类

org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.sunxy.config.BeanConfig

然后

mvn clean install -Dmaven.test.skip=true

打包进maven仓
在这里插入图片描述

再新建一个springboot工程,叫auto-use-demo,引用这个上一步打好的auto-conf-demo。
在这里插入图片描述

新工程只建了一个runner类,来测试能否注入成功,由于ClockService类和这个runner,并不在一个包内,只能通过自动配置来注册进ioc容器。
在这里插入图片描述

运行结果:
在这里插入图片描述

至此,验证成功。

版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/sunxy24/article/details/82801508
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢