Springboot自动装配原理及自定义starter开发
starter解决的问题
当我们定义一个公共的组件时,如果别人要引用它,可以通过导入一个jar包的方式,然后如果要使用其中的类的话,我们就需要使用@Bean,但是这样会造成代码强依赖于该jar包,一旦我们进行组件的更新,甚至更改了类名的时候,就会导致用户的@Bean也需要更改,这样的用户体验不好。
这就引入了starter,我们就可以直接使用@Autowired,因为在组件内部已经为我们托管到Spring了,我们只要引入依赖到pom.xml文件,然后注入就可以了。
思考的问题
我们先要思考的问题,我们知道SpringMVC需要配置web.xml文件,Spring也需要配置applicationContext.xml,但是问题来了,为什么基于Spring和SpringMVC的SpringBoot却没有这些配置文件。那SpringBoot是如何做到这些自动配置的呢?我们很容易地知道是@SpringBootApplication的原因,但底层是怎样实现的呢?
解析自动装配
加载yml、properties、yaml文件
首先,我们要知道为什么可以自动加载src/main/application.yml文件?其实是在springboot项目的父模块中定义了的。
<includes>
<include>**/application*.yml</include>
<include>**/application*.yaml</include>
<include>**/application*.properties</include>
</includes>
自动装配原理
先看@SpringBootApplication
元注解肯定不可能自动装配,接下来看@SpringBootConfiguration
@SpringBootConfiguration
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration//如果不改属性值proxyBeanMethods,可以保证绝对单例(这也是@Configuration和@Component的区别)
public @interface SpringBootConfiguration {
@AliasFor(
annotation = Configuration.class
)
boolean proxyBeanMethods() default true;//默认使用CGLIB代理该类
}
可以知道@SpringBootConfiguration相当于@Configuration
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Configuration {
@AliasFor(
annotation = Component.class
)
String value() default "";
boolean proxyBeanMethods() default true;//proxyBeanMethods中文是代理Bean的方法,默认参数是true
}
关于@Configuration 可参考https://blog.csdn.net/weixin_46055693/article/details/125656999
下一个看@EnableAutoConfiguration
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})//导入参数类到IOC
public @interface EnableAutoConfiguration {
String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
Class<?>[] exclude() default {};
String[] excludeName() default {};
}
接下来,看@AutoConfigurationPackage
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import({Registrar.class})//因为spring不会自动扫描@Entity等注解,spring自动扫描的是@Controller这些注解,所以需要保存扫描路径,提供给spring-data-jpa扫描@Entity等注解,默认当前类所在的包(@Import的第二种使用情况,可点进去看一下源码)
public @interface AutoConfigurationPackage {
String[] basePackages() default {};
Class<?>[] basePackageClasses() default {};
}
Springboot自动装配的核心注解@Import
- 参数如果是普通类,将该类实例化交给IOC容器管理。(除开下面两种都是普通类,4.2之前的版本只能加载有@Configuration的配置类,但后面版本没有该注解也可以加载)
- 参数如果是ImportBeanDefinitionRegistrar的实现类,支持手工注册bean。
- 参数如果是ImportSelector的实现类,注册selectImports返回的数组(类全路径)则IOC容器批量注册。
然后从@Import({AutoConfigurationImportSelector.class})
点进去,找到selectImports方法,再从this.getAutoConfigurationEntry(annotationMetadata)点进去
protected AutoConfigurationImportSelector.AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
if (!this.isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
} else {
AnnotationAttributes attributes = this.getAttributes(annotationMetadata);//根据注解获取注解属性
//从META-INF/spring.factories加载EnableAutoConfiguration类
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);//把配置信息返回
}
}
按顺序点击this.getCandidateConfigurations(annotationMetadata, attributes);---->this.getSpringFactoriesLoaderFactoryClass()可以知道最后要加载的是EnableAutoConfiguration类,接着返回到上一个函数,然后点击SpringFactoriesLoader.loadFactoryNames—>(List)loadSpringFactories(classLoaderToUse)—>
Enumeration urls = classLoader.getResources(“META-INF/spring.factories”);
private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
Map<String, List<String>> result = (Map)cache.get(classLoader);
if (result != null) {
return result;
} else {
HashMap result = new HashMap();
try {
Enumeration urls = classLoader.getResources("META-INF/spring.factories");
while(urls.hasMoreElements()) {
URL url = (URL)urls.nextElement();
UrlResource resource = new UrlResource(url);
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
Iterator var6 = properties.entrySet().iterator();
while(var6.hasNext()) {
Entry<?, ?> entry = (Entry)var6.next();
String factoryTypeName = ((String)entry.getKey()).trim();
String[] factoryImplementationNames = StringUtils.commaDelimitedListToStringArray((String)entry.getValue());
String[] var10 = factoryImplementationNames;
int var11 = factoryImplementationNames.length;
for(int var12 = 0; var12 < var11; ++var12) {
String factoryImplementationName = var10[var12];
((List)result.computeIfAbsent(factoryTypeName, (key) -> {
return new ArrayList();
})).add(factoryImplementationName.trim());
}
}
}
result.replaceAll((factoryType, implementations) -> {
return (List)implementations.stream().distinct().collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList));
});
cache.put(classLoader, result);
return result;
} catch (IOException var14) {
throw new IllegalArgumentException("Unable to load factories from location [META-INF/spring.factories]", var14);
}
}
}
大概扫描一下上面的代码,我们可以知道,其实是加载了META-INF/spring.factories文件然后存到Map中,我们再看一下调用该方法的代码
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
ClassLoader classLoaderToUse = classLoader;
if (classLoader == null) {
classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
}
String factoryTypeName = factoryType.getName();
return (List)loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList());
}
再看上段代码的最后一行,就可以知道原来springboot在获取到Map之后是根据类名从键值对中去取值的。
然后找到Maven: org.springframework.boot:spring-boot-autoconfigure:2.4.5这个包,再进入 到META-INF/spring.factories。
# Auto Configure
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.batch.BatchAutoConfiguration,\
org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,\
上述的org.springframework.boot.autoconfigure.EnableAutoConfiguration,其实就是我们根据取值的键名,这里我们可以先猜测springboot就是通过它加载类到IOC容器的。以DataSourceAutoConfiguration为例,
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnClass({DataSource.class, EmbeddedDatabaseType.class})
@ConditionalOnMissingBean(
type = {"io.r2dbc.spi.ConnectionFactory"}
)
@EnableConfigurationProperties({DataSourceProperties.class})
@Import({DataSourcePoolMetadataProvidersConfiguration.class, DataSourceInitializationConfiguration.class})
public class DataSourceAutoConfiguration {
public DataSourceAutoConfiguration() {
}
DataSourceAutoConfiguration类上面有一个@Configuration注解,相当于一个xml配置,它通过使用@Bean或者@Import注解将需要托管的类托管的IOC容器中,而这些与sprinboot无关,你想要什么托管就可以加@bean或者@Import注解
自定义Starter
- 创建一个springboot项目
- 引入依赖
<!-- 自定义starter时,必须引入的依赖(SpringBoot自动引入了)-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
3.建立一个Properties类
@ConfigurationProperties(prefix = "yc.connection")
public class ConnectionProperties {
private String driverClassName;
private String url;
private String username;
private String password;
//省略了set和get方法
通常情况下,我们使用@Value来从application.yml文件中读取属性到类中。但是@Value只能单个单个注入,并且是根据字符串匹配的,所以这里使用@ConfigurationProperties(prefix = “yc.connection”)来批量注入,通过前缀匹配的方式,换句话说@ConfigurationProperties是批量的@Value,但是它需要配合@EnableConfigurationProperties(ConnectionProperties.class)
4.建立一个功能代码类
public class DBHelper implements IDBHelper{
@Autowired
private ConnectionProperties cp;
@Override
public Connection getConnection() {
Connection conn=null;
try {
Class.forName(cp.getDriverClassName());
conn= DriverManager.getConnection(cp.getUrl(),cp.getUsername(),cp.getPassword());
} catch (ClassNotFoundException |SQLException e) {
e.printStackTrace();
}
return conn;
}
}
- 自动配置类
@Configuration
@EnableConfigurationProperties(ConnectionProperties.class)
@ConditionalOnClass(IDBHelper.class)
public class ConnectionAutoConfiguration {
@Bean
public IDBHelper idbHelper(){
return new DBHelper();
}
}
其中@EnableConfigurationProperties(ConnectionProperties.class)可以将ConnectionProperties.class加载到IOC容器。
- 创建src/main/resources/META-INF/spring.factories 类上需要加@Component这种注解
https://blog.csdn.net/weixin_44923168/article/details/119903295
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.yc.ConnectionAutoConfiguration
- 把项目打包成jar包
- 再创建一个springboot项目,导入自定义starter的jar包
@SpringBootApplication
@RestController
public class TestmystarterMysqlstarterApplication {
public static void main(String[] args) {
SpringApplication.run(TestmystarterMysqlstarterApplication.class, args);
}
@Autowired
private IDBHelper idbHelper;
@GetMapping("/conn")
public String testConn(){
Connection conn=idbHelper.getConnection();
String constr=conn.toString();
return constr;
}
}
不需要@bean,直接使用@Autowired。因为只是说扫描META-INF/spring.factories但没有说扫描哪个包下的,所以都会扫描。
第二种:使用META-INF/spring.factories加载配置类是一种方法,但是我们也可以在启动类使用@Import加载配置类
@SpringBootApplication
@RestController
@Import(ConnectionAutoConfiguration.class)
public class TestmystarterMysqlstarterApplication {
public static void main(String[] args) {
SpringApplication.run(TestmystarterMysqlstarterApplication.class, args);
}
@Autowired
private IDBHelper idbHelper;
@GetMapping("/conn")
public String testConn(){
Connection conn=idbHelper.getConnection();
String constr=conn.toString();
return constr;
}
}
第三种:还可以自定义注解
写一个类实现ImportSelector ,而springboot底层会调用selectImports,所以我们只需要实现这个方法,然后将其用spring托管即可
public class MyImport implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
return new String[]{ConnectionAutoConfiguration.class.getName()};//直接传全路径名
}
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(MyImport.class)
public @interface EnableUtil {
}
最后在启动类上添加@EnableUtil
@SpringBootApplication
@RestController
@EnableUtil
public class TestmystarterMysqlstarterApplication {
public static void main(String[] args) {
SpringApplication.run(TestmystarterMysqlstarterApplication.class, args);
}
@Autowired
private IDBHelper idbHelper;
@GetMapping("/conn")
public String testConn(){
Connection conn=idbHelper.getConnection();
String constr=conn.toString();
return constr;
}
}
其实我们也可以使用@ComponentScan,但是会造成强依赖,以上三种方案,最好的就是第一种,因为后面的两种,如果托管多个配置类,就需要写多个@Import,同时也需要修改类名,这样就失去了starter弱依赖的优势。