手写spring-boot-starter-data-redis
springboot的自动装配是starter的基础,简单来说,就是将Bean装配到Ioc。
本文我们先学习redis的starter如何实现自动装配,然后手写一个redis的starter的,来学习spring如何通过starter实现自动装配。
一、学习spring-boot-starter-data-redis如何实现自动装配
首先,新建一个springboot项目,添加starter依赖
compile("org.springframework.boot:spring-boot-starter-data-redis")
在yml中添加redis数据源:
redis:
database: 8
host: 127.0.0.1
# password:
port: 6379
timeout: 1000ms
lettuce:
pool:
max-active: 20
min-idle: 1
max-idle: 8
max-wait: 10000
编写一个controller,测试
/**
* @author cgg
* @version 1.0
* @date 2021/4/1
*/
@RestController
@Slf4j
@Api(tags = "测试")
@RequestMapping("test")
public class TestController {
@Resource
private RedisTemplate redisTemplate;
@GetMapping("/")
@ApiOperation("测试")
public void helloWorld() {
System.out.println("hello world");
}
}
可以看到,在项目中,我们并没有使用注解或者xml将redisTemplate注入到Ioc容器中就可以使用,说明容器中已经存在了,其实这就是springBoot的自动装配。
其实springboot 通过一个starter依赖就能实现自动装配,是starter遵守了约定规范,才实现了自动装配,下面我们就学习一下原理,并学习starter的规范,为我们手写自己的starter做准备。
springboot实现自动装配是通过 @SpringBootApplication 注解中的 @EnableAutoConfiguration实现的。
@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 {
.......
}
接下来,再看 @EnableAutoConfiguration的源码
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
/**
* Exclude specific auto-configuration classes such that they will never be applied.
* @return the classes to exclude
*/
Class<?>[] exclude() default {};
/**
* Exclude specific auto-configuration class names such that they will never be
* applied.
* @return the class names to exclude
* @since 1.3.0
*/
String[] excludeName() default {};
}
其中使用了@import 导入了 AutoConfigurationImportSelector类,那我们我们继续往下看 AutoConfigurationImportSelector 实现了 DeferredImportSelector,而DeferredImportSelector实现了ImportSelector
其中的重写了selectImports ,返回了一个String【】数组,spring把返回的数组中的类名全部装配到容器中。继续看AutoConfigurationImportSelector代码。
public class AutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware,
ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered {
private static final AutoConfigurationEntry EMPTY_ENTRY = new AutoConfigurationEntry();
private static final String[] NO_IMPORTS = {};
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 ConfigurationClassFilter configurationClassFilter;
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return NO_IMPORTS;
}
AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata);
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations()); //可以看到 autoConfigurationEntry.getConfigurations()才是需要装配的类名称数组。那么就需要查看 getAutoConfigurationEntry(annotationMetadata)
} ......
继续跟到getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) 方法中
protected AutoConfigurationEntry getAutoConfigurationEntry(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 = getConfigurationClassFilter().filter(configurations);
fireAutoConfigurationImportEvents(configurations, exclusions);
return new AutoConfigurationEntry(configurations, exclusions);
}
由于调用栈过深,我说下源码的调用链:getCandidateConfigurations -> loadFactoryNames -> loadSpringFactories ->
在loadSpringFactories 可以发现下面的代码
Enumeration<URL> urls = (classLoader != null ?
classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
而 public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories"; 接下来我们再看一下项目启动后,getCandidateConfigurations返回值。
这里看到第一个返回值是 MybatisPlusAutoConfiguration 那么他是怎么来的呢,再看下一张图
相信看到这里,大多数同学已经明白了,其实就是获取了 每一个 starter 的 "META-INF/spring.factories" 中声明类全名。根据org.springframework.boot.autoconfigure.EnableAutoConfiguration 作为Key获取value作为返回值。
然后再说一下getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) 其他方法的含义:
getAttributes获得@EnableAutoConfiguration注解中的exclude 、 excludeName等
getCandidateConfigurations获取自动装配的配置类,重点。
removeDuplicates去除重复的配置项。
getExclusions根据 @EnableAutoConfiguration配置的exclude将不需要的配置类移除。
最后对自动装配的核心流程做一个总结:
1、通过@Import(AutoConfigurationImportSelector) 实现配置类的导入,
2、AutoConfigurationImportSelector实现了ImportSelector,重写了selectImport用来实现批量导入
3、通过Spring的SpringFactoriesLoader机制,扫描"META-INF/spring.factories" 路径下的配置类,实现自动装配。
4、通过条件筛选,把不符合条件的配置类移除,最终完成自动装配。
整体来看,springboot的自动装配完美的体现了:约定由于配置。
另外,还有一个文件需要注意:
上述文件的作用和@condition作用是一样的,只是将条件配置类放置了文件中,原理和上面的自动装配配置类一样,好处在于降低了springboot得启动时间,因为这个文件的装配发生在配置类装载之前。同样,这也是“约定由于配置”的体现。
二、手写一个spring-boot-starter-cgg-redis
接下来,我们手写一个starter。通过上述分析,stater主要具有三点功能:
1、实现相关组件的Jar依赖
2、自动装配Bean
3、自动声明并且加载application.properties中的配置
我们实现的是一个基于redis简化版的stater,主要是想通过动手操作学习自动装配的原理。
1、新建一个项目,命名为 spring-boot-starter-cgg-redis,添加Jar包依赖,我们使用Redisson来实现。添加redisson依赖。
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.16.0</version>
</dependency>
2、定义属性类,获取application.properties的属性
@Data
@ConfigurationProperties(prefix = "cgg.redisson")
public class RedissonProperties {
private String host = "localhost";
private String password;
private int port = 6379;
private int timeout;
private boolean ssl;
}
3、编写自动装配的配置类,会将RedissonClient装配到Ioc容器中
@Configuration
@ConditionalOnClass(Redisson.class)
@EnableConfigurationProperties(RedissonProperties.class)
public class RedissonAutoConfiguration {
@Bean
public RedissonClient redissonClient(RedissonProperties redissonProperties) {
Config config = new Config();
String prefix = "redis://";
if (redissonProperties.isSsl()) {
prefix = "rediss://";
}
SingleServerConfig singleServerConfig = config.useSingleServer().setAddress(prefix + redissonProperties.getHost() + ":" + redissonProperties.getPort())
.setConnectTimeout(redissonProperties.getTimeout());
if (ObjectUtils.isEmpty(redissonProperties.getPassword())) {
singleServerConfig.setPassword(redissonProperties.getPassword());
}
return Redisson.create(config);
}
}
4、在项目resources下META-INF/spring.factories文件,文件内容为:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
cgg.redisson.propieties.RedissonAutoConfiguration
5、最后已经将我们的项目打成Jar,然后别的项目就可以使用我们的spring-boot-starter-cgg-redis,需要添加依赖
<dependency>
<groupId>cgg.redisson</groupId>
<artifactId>spring-boot-stater-cgg-redis</artifactId>
<version>1.0.0</version>
</dependency>
最后再项目的依赖文件application.properties中添加配置:
cgg.redisson.host=127.0.0.1
cgg.redisson.port=6379
启动项目,发现无需配置redissonClient也可以直接使用,说明通过我们的stater自动装配 Ioc容器中已经有了RedissonClient.
至此,手写一个stater就完成了,相信看到这里,对SpringBoot的自动装配又有了一定的学习。