前言
平时我们使用springboot可以做到去除xml配置,并且使用外部组件时可以做到开箱即用,使用非常方便,本文就详细介绍下springboot自动装配的原理,以便理解springboot的核心
首先我们先以RedisTemplate这个类来做案例讲解,如果我们在springboot项目中需要使用redis那么流程应该是
1、依赖官方提供的redis包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2、在项目中需要使用的地方直接注入RedisTemplate即可使用
@Autowired
private RedisTemplate redisTemplate;
public void get() {
redisTemplate.opsForValue().get("redisKey");
}
依照spring的约定RedisTemplate应该并没有被我们注册到spring的ioc容器中,那么我们为什么可以ioc容器中拿到RedisTemplate呢?
在springboot注解式编程中,如果我们需要把一个类注册到spring ioc容器中,那么应该有几种方式
1、直接在RedisTemplate这个类中使用注解@Service,@Component等标记,然后让spring扫描
2、使用@Configurable加@Bean标签装配bean,并且让spring扫描
@Configurable
public class RedisConfiguration {
@Bean
private RedisTemplate redisTemplate() {
return new RedisTemplate();
}
}
但是以上两种方法都需要让spring扫描对应的包才能扫描到对应的bean,这样对于我们的使用非常不方便,因为我们必须要@ComponentScan很多个包路径,而且我们必须知道我们要注入的类的包路径才可以,所以针对以上缺点,spring提供了一种规定,只要满足spring的规定则spring就会去扫描对应的类
ImportSelector核心原理
接下来我们就引入spring自动装配的核心接口ImportSelector
public interface ImportSelector {
String[] selectImports(AnnotationMetadata var1);
@Nullable
default Predicate<String> getExclusionFilter() {
return null;
}
}
该接口核心方法selectImports,主要作用就是当我们使用@Import注解注入一个ImportSelector的实现类时,selectImports返回的数组列表中的所有文件路径spring都会去扫描,如下所示:
1、自定义配置类,默认情况下spring不会把TestBean加入到ioc容器,因为TestBeanConfiguration所在的包路径不在spring扫描路径之内
@Configurable
public class TestBeanConfiguration {
@Bean
private TestBean testBean() {
// 随意创建的一个类
return new TestBean();
}
}
2、自定义ImportSelector实现类
public class MyImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
return new String[]{
RedisConfiguration.class.getName()
};
}
}
3、注解引入MyImportSelector
@SpringBootApplication
@Import({MyImportSelector.class}) // 导入MyImportSelector类
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
配置完上述代码之后spring即会去扫描RedisConfiguration,然后注册TestBean到ioc容器中
springboot中的自动装配原理
按照上述规定的理解,接下来我们就很容易理解springboot中自动装配的原理了
我们直接查看springboot中自动装配的核心源码
1、先查看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
2、核心注解@EnableAutoConfiguration
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {
String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
Class<?>[] exclude() default {};
String[] excludeName() default {};
}
我们已经可以看到我们的@Import注解了
3、核心类AutoConfigurationImportSelector
AutoConfigurationImportSelector实现于接口DeferredImportSelector,DeferredImportSelector继承于ImportSelector接口,那么我们就可以直接看selectImports方法了,即selectImports方法返回的数组都是spring会去帮我们扫描的
public class AutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware, ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered {
private static final AutoConfigurationImportSelector.AutoConfigurationEntry EMPTY_ENTRY = new AutoConfigurationImportSelector.AutoConfigurationEntry();
private static final String[] NO_IMPORTS = new String[0];
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 AutoConfigurationImportSelector.ConfigurationClassFilter configurationClassFilter;
public AutoConfigurationImportSelector() {
}
// 核心方法
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!this.isEnabled(annotationMetadata)) {
return NO_IMPORTS;
} else {
// 核心代码
AutoConfigurationImportSelector.AutoConfigurationEntry autoConfigurationEntry = this.getAutoConfigurationEntry(annotationMetadata);
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
}
}
4、核心方法getAutoConfigurationEntry
protected AutoConfigurationImportSelector.AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
if (!this.isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
} else {
AnnotationAttributes attributes = this.getAttributes(annotationMetadata);
// 核心代码
List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);
configurations = this.removeDuplicates(configurations);
Set<String> exclusions = this.getExclusions(annotationMetadata, attributes);
this.checkExcludedClasses(configurations, exclusions);
configurations.removeAll(exclusions);
configurations = this.getConfigurationClassFilter().filter(configurations);
this.fireAutoConfigurationImportEvents(configurations, exclusions);
return new AutoConfigurationImportSelector.AutoConfigurationEntry(configurations, exclusions);
}
}
5、核心方法getCandidateConfigurations
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
// 核心方法
List<String> configurations = SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.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;
}
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
String factoryTypeName = factoryType.getName();
return loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList());
}
6、核心方法loadSpringFactories
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.getSystemResources(FACTORIES_RESOURCE_LOCATION));
result = new LinkedMultiValueMap<>();
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
UrlResource resource = new UrlResource(url);
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
for (Map.Entry<?, ?> entry : properties.entrySet()) {
String factoryTypeName = ((String) entry.getKey()).trim();
for (String factoryImplementationName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
result.add(factoryTypeName, factoryImplementationName.trim());
}
}
}
cache.put(classLoader, result);
return result;
}
catch (IOException ex) {
throw new IllegalArgumentException("Unable to load factories from location [" +
FACTORIES_RESOURCE_LOCATION + "]", ex);
}
}
我们看核心代码classLoader.getResources(FACTORIES_RESOURCE_LOCATION),FACTORIES_RESOURCE_LOCATION = “META-INF/spring.factories”
即扫描classpath下面类路径为META-INF/spring.factories的所有文件(包括jar包的)
然后轮询这些文件,返回这些文件中所有的key-value值,再提取出里面key为
org.springframework.boot.autoconfigure.EnableAutoConfiguration的所有数据,即最后返回的String列表是spring会帮我们扫描的配置类
根据上述源码流程,我们可以得到结果,只要是classpath下有文件夹META-INF/spring.factories的,里面定义的org.springframework.boot.autoconfigure.EnableAutoConfiguration=xxx列表,xxx列表都是spring会帮我们扫描的
如上我们的代码,我们只要定义META-INF/spring.factories,在里面定义org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.example.demo.config.TestBeanConfiguration, spring就会扫描到TestBeanConfiguration类,以上就是spring对应第三方扩展组件想加入到springboot自动装配提供的约定
spring官方组件VS第三方组件自动装配
还有一点我们可以介绍下,spring官方的组件和第三方组件是有些不同的
- 第三方组件都是依据以上约定实现的自动装配,如mybatis-spring-boot-starter-2.0.1.jar
- 官方组件的classpath下不一定会有META-INF/spring.factories文件,其实现方式是基于spring-boot-autoconfigure组件来实现的
spring-boot-autoconfigure组件中有定义META-INF/spring.factories文件,我们可以看到里面定义了非常多的配置类,包含我们上述讲到的redis的配置类RedisAutoConfiguration
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\
......
org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration,\
org.springframework.boot.autoconfigure.data.redis.RedisReactiveAutoConfiguration,\
org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration
......
我们看下RedisAutoConfiguration
@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;
}
}
可以看到一个注解@ConditionalOnClass(RedisOperations.class),ConditionalOnClass标记的类的作用是当ConditionalOnClass标记的类存在时才会被spring装配,即RedisOperations类在classpath下存在RedisAutoConfiguration才会被装配,RedisOperations存在于官方组件spring-boot-starter-data-redis中,即当项目依赖于spring-boot-starter-data-redis组件时RedisAutoConfiguration会被装配。
即springboot官方组件的装配过程是在spring-boot-autoconfigure组件定义要装配的配置类,并且使用ConditionalOnClass注解标记,只有当对应的官方组件被依赖时才会被加载,这就是官网组件和第三方组件的差别
扩展
如何区分是spring的组件还是第三方组件
- spring官方自动装配组件的格式如:spring-boot-starter-data-xxx
- 第三方自动装配组件的格式如:xxx-spring-boot-starter
以上就是springboot自动装配的原理,依据此约定,我们也可以实现我们自己的开箱即用的starter组件了