Spring Boot是Spring家族中的新宠,它不仅继承了Spring框架原有的优秀特性,还通过简化配置来进一步简化Spring应用程序的创建和开发过程。SpringBoot框架中有两个最主要的策略:开箱即用和约定优于配置。
- 开箱即用:在开发过程中,通过引入maven依赖包,然后使用注解来代替繁琐的XML配置文件来管理对象的生命周期,这让开发人员摆脱了复杂的配置和包依赖管理的工作,更加专注于业务逻辑。
- 约定优于配置:按约定编程是一种软件设计范式,系统、类库、框架应该假定合理的默认值,而非要求提供不必要的配置,从而既能获得配置简单的好处,而又不失灵活性。
有关SpringBoot的概念就不说太多了,可以查看一下官方文档。
自动配置
SpringBoot的自动配置乍一看很神奇,其实原理非常简单,实现自动配置的核心就是@Conditional注解。
一、@Condition是什么
@Condition是Spring4的一个新特性,注解的注释第一句写到“表明仅当所有组件都符合注册条件时,该组件才具有注册资格”,所以我们可以根据这个注解动态的决定需要加载的Bean。
例如我们想要根据不同的环境加载不同的类,我们可以通过spring.profiles.active=dev
指定当前环境,创建类的代码如下:
1 |
|
我们这里用到了@Profile注解,这个注解是spring3.1之后版本中提供的,从注解的定义上我们可以看到它也是一个Conditional,可以通过ConfigurableEnvironment#setActiveProfiles
方法和spring.profiles.active
配置完成设置,当然还有其他方法,这里就不一一写出了,详情可以查看类org.springframework.core.env.AbstractEnvironment
。
在业务复杂的情况下,可以使用@Conditional注解来提供更加灵活的条件判断,在SpringBoot中的的很多CccConfiguration类上都设置了很多的Conditional,整理后发现大致有以下几种:
@ConditionalOnClass
:当classpath下存在指定的类时,加载被注解的类,使用方法@ConditionalOnClass({A.class, B.class, C.class})
@ConditionalOnBean
:当Spring容器中存在指定的Bean实例时,加载被注解的类,使用方法@ConditionalOnBean({A.class, B.class})
@ConditionalOnMissingBean
:当Spring容器中不存在指定的Bean实例时,加载被注解的类,使用方法@ConditionalOnMissingBean({A.class, B.class, C.class})
@ConditionalOnMissingClass
:当classpath下不存在指定的类时,加载被注解的类,使用方法@ConditionalOnMissingClass({"cc.lu.A", "cc.lu.B"})
@ConditionalOnProperty
:控制某个configuration是否生效。具体操作是通过其两个属性name以及havingValue来实现的,其中name用来从application.properties中读取某个属性值,如果该值为空,则返回false;如果值不为空,则将该值与havingValue指定的值进行比较,如果一样则返回true;否则返回false。如果返回值为false,则该configuration不生效;为true则生效,使用方法@ConditionalOnProperty(prefix="cc.lu.config", name="enable", havingValue="true")
,上面的@Profile("dev")
对等于@ConditionalOnProperty(name="spring.profiles.active", havingValue="dev")
还有很多常用到的注解,可以到org.springframework.autoconfigure.condition
包内了解一下,每一个注解单独拿出来都可以讨论半天。下面我们写一个简单的例子:当classpath路径中存在cc.lu.A
类、容器中不存在cc.lu.B
类且存在配置cc.lu.config.auto=true
时加载cc.lu.Cc
类
1 | package cc.lu; |
二、CccAutoConfiguration分析
上面了解了@Conditional
注解的机制,也写了一个简单的例子,灵的同学应该已经能猜到SpringBoot是如何来实现自动配置的了,我们现在基于2.2.6版本的源码来粗略的看一下。
我们创建一个SpringBoot工程,idea可以通过Spring Initializr来快速的创建一个项目,然后我们看整个工程的入口,也就是Application.java(新创建的SpringBoot应用一般只有一个启动类)
1 |
|
既然过程只有这么一个类,那么关键点就是@SpringBootApplication
和SpringApplication#run
了,我们先来看下注解@SpringBootApplication
,这玩意儿放在启动类上是想要干啥。
1 | (ElementType.TYPE) |
注意到这个注解上有一个@EnableAutoConfiguration
,这个注解的目的是启用Spring应用程序上下文的自动配置,尝试猜测和配置可能需要的bean。再来瞜一眼这个注解的定义
1 | (ElementType.TYPE) |
哎,这个@Import
注解把AutoConfigurationImportSelector
这个类导入了进来,也就是说我们使用@EnableAutoConfiguration
的时候,AutoConfigurationImportSelector
类会自动被加载,那么是不是核心代码就是在这个类中了?(这样写貌似有点尬!)
AutoConfigurationImportSelector
实现于ImportSelector
,关键方法就是ImportSelector#selectImports
1 |
|
从selectImports
的源码可以看到它只做了两件事:加载默认的配置属性和返回所有的AutoConfiguration的类信息,通过方法调用链找到最终加载的方法是SpringFactoriesLoader#loadSpringFactories
1 | private static Map<String, List<String>> loadSpringFactories( ClassLoader classLoader) { |
SpringBoot为我们提供的配置类有一二百个,但是我们不可能每个工程都把它们全部引入。所以在自动装配的时候,会去classpath下面寻找,是否有对应的配置类。如果有配置类,则按条件注解 @Conditional或者@ConditionalOnProperty等相关注解进行判断,决定是否需要装配。如果classpath下面没有对应的字节码,则不进行任何处理。
我们到spring.factories文件中随便找一个AutoConfiguration类,比如org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration
1 | false) (proxyBeanMethods = |
这个类被@ConditionalOnClass
和@EnableConfigurationProperties
两个注解修饰。
@ConditionalOnClass(RedisOperations.class)
的意思是当classpath路径下存在RedisOperations
这个类的时候加载RedisAutoConfiguration
,类RedisOperations
在spring-data-redis.jar包中,这个包通过spring-boot-starter-data-redis
的starter引入,所以在我们引入这个starter的时候就自动去加载了RedisAutoConfiguration,然后再类中又创建了两个Bean,创建的前提是容器中不存在这两个类的实例,如果我们自定义一个RedisTemplate的实例,RedisAutoConfiguration#redisTemplate方法就会失效。@EnableConfigurationProperties(RedisProperties.class)
:在加载RedisAutoConfiguration的时候同步加载RedisProperties,RedisProperties中通过注解@ConfigurationProperties(prefix = "spring.redis")
指定关联的配置信息,若没有配置则使用类中属性的默认值。
至此,容器中有了RedisTemplate的实例和StringRedisTemplate的实例,并且还使用了配置文件中我们设置的Redis相关配置。
总结
整个SpringBoot中,都是通过@Conditional注解的各种扩展来实现自动配置的,我们也可以完全利用这些注解去实现我们自己的starter。