本文所用的springboot版本为2.0.5.RELEASE
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自动配置如何实现之前,我们先复习一下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容器的几种方式
@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());
}
}
使用@Configuration
和@Bean
注解来配置bean到ioc容器,这个类也需要在springboot启动类所在的包或者子包下面,否则无法扫到。
删除上面的@Service
注解,新加一个配置类
/**
* Created by Sun on 2018/9/19
*/
@Configuration
public class BeanConfig {
@Bean
public ClockService clockService() {
return new ClockService();
}
}
使用@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修饰的配置类,还有很多更强大的功能,这里暂时不详细讨论。
第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 这一项:
既然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位于类路径上,否则不解析该注解修饰的配置类)
我们前三点已经写过部分代码了,我们接下来完善它,来验证自动配置。
由于要拿这个工程当做一个jar包来验证自动配置,这里屏蔽该工程自己的runner
resource目录下新建META-INF/springfactories文件,声明自己的自动配置类
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.sunxy.config.BeanConfig
然后
mvn clean install -Dmaven.test.skip=true
再新建一个springboot工程,叫auto-use-demo,引用这个上一步打好的auto-conf-demo。
新工程只建了一个runner类,来测试能否注入成功,由于ClockService类和这个runner,并不在一个包内,只能通过自动配置来注册进ioc容器。
至此,验证成功。