自定义starter为什么要加上spring.factories,这个问题要从@SpringApplication注解的实现开始
@SpringApplication注解的实现
@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 {
排除元注解,先来看看比较熟悉的@ComponentScan注解
@ComponentScan的作用是,扫描指定的basePackages包下的,用@Component及其派生注解标记的类,加入到Spring容器中管理。
注:@Service,@Configuration,@Controller等等都是@Component的派生注解
自定义starter里面的类可以由这个注解扫描吗?
思考一个问题,如果我自定义了一个封装的starter,里面包含一个@Service标记的服务类,它也可以由这个注解进行扫描并注册到Spring容器中吗?
可以做两个测试接口来验证上面的问题
1)自定义一个starter,包名分别是com.zby和com.ttt,下面各有一个Service服务类,并且使用@Service注解标记
service方法一:
package com.zby.service;
import org.springframework.stereotype.Service;
/**
* @author zhengbingyuan
* @date 2022/5/25
*/
@Service
public class TestImportService {
public String test01(){
return "TestImportService.test01";
}
}
service方法二:
package com.ttt.service;
import org.springframework.stereotype.Service;
/**
* @author zhengbingyuan
* @date 2022/5/25
*/
@Service
public class TestNotImportService {
public String test01(){
return "TestNotImportService.test01";
}
}
位置:
在另外的工作项目,Application所在包名为com.zby,引入这个自定义的starter,并新建两个测试接口:
注:这里我本身想使用构造器注入的方式的,但是这样启动就会报错,因为构造器注入要求依赖不能为空,所以为了更好的测试,我把注入方式改为了@Autowired注入
/**
* 其他测试接口
*
* @author XBird
* @date 2022/5/8
**/
@Api(tags = "测试接口集合")
@RestController
//@AllArgsConstructor
public class TestController {
@Autowired
private TestImportService testImportService;
@Autowired(required = false)
private TestNotImportService testNotImportService;
@ApiOperation(value = "测试starter中跟当前项目父包相同的service是否会被加载", notes = "\n开发者:郑炳元")
@PostMapping(value = "/testStarterImportService")
public JsonResult<String> testStarterImportService() {
return JsonResult.success(testImportService.test01());
}
@ApiOperation(value = "测试starter中跟当前项目父包不同的service是否会被加载", notes = "\n开发者:郑炳元")
@PostMapping(value = "/testStarterNotImportService")
public JsonResult testStarterNotImportService() {
return JsonResult.success(testNotImportService.test01());
}
}
结果:
com.zby下的服务类可以引入成功,但是com.ttt下的服务类引入失败
接口一:
接口二:
结论:
如果starter里面的包名是跟工作项目(引入这个starter的项目)的SpringApplication主运行类的所在包是相同的,那么可以成功引入,否则引入失败。
怎么样可以引入依赖jar包中在项目包外的类?
那SpringBoot有办法引入这些不是跟当前工作项目统一包名的封装功能吗?
工作环境下,没办法保证引入的starter的功能和当前工作项目是相同的包,例如我封装了包为com.xxx的starter,其他小组觉得挺好用的,想直接拿过来用。
这就要说到@EnableAutoConfiguration 这个注解了。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
特别注意到两个注解:
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
先来分析@Import(AutoConfigurationImportSelector.class)
AutoConfigurationImportSelector 实现了DeferredImportSelector 接口,DeferredImportSelector 又是继承自ImportSelector接口。
因此,首先知道一点,AutoConfigurationImportSelector 会存在一个selectImports 方法,该方法返回的数组中的类名会注册到Spring容器当中。
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
//从这句代码可以知道,通过spring.boot.enableautoconfiguration =false
//可以禁用自动配置功能
if (!isEnabled(annotationMetadata)) {
return NO_IMPORTS;
}
AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata);
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
其中,getAutoConfigurationEntry(annotationMetadata) 方法就是获取项目中需要自动配置的配置类
protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
}
AnnotationAttributes attributes = getAttributes(annotationMetadata);
//从META-INF/spring.factories获取候选的配置类
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(annotationMetadata, attributes); 这一步,实际上是获取候选的配置类,这些候选的配置类来源就是从类路径下的META-INF/spring.factories,寻找key=EnableAutoConfiguration.class 对应的value(一个数组),这些数组都是配置类的类名
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
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;
}
loadSpringFactories 方法不仅仅是getCandidateConfigurations方法调用的,其他方法也会调用到,例如springboot启动时加载ApplicationContextInitializer和SpringApplicationRunListener的时候也会访问这个方法,所以这个方法做了缓存
private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
//这个应该是第二次执行这个方法的时候,从缓存里面拿的意思
//因为spring.factories是静态文件不会改,下面有cache.put(classLoader, result)的操作
MultiValueMap<String, String> result = cache.get(classLoader);
if (result != null) {
return result;
}
try {
//这里FACTORIES_RESOURCE_LOCATION就是META-INF/spring.factories
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);
}
}
spring.factories文件示例:是以K-V形式获取的
结论
回到最初的问题,“为什么要写spring.factories文件?”,
spring.factories是为了能够将SpringBoot工作项目启动类所在包之外的bean(即在pom文件中添加依赖中的bean)注册到spring-boot项目的spring容器中。因为启动类中的@ComponentScan只能扫描启动类所在的包及其子包的bean。因此需要@EnableAutoConfiguration注解来注册项目包外的bean。
但是,如果自定义starter的配置类所在包跟项目包相同,就没必要写spring.factories了
这个,其实也就是回答了@EnableAutoConfiguration注解的原理。
总结一下使用场景:
(1)starter中有配置类
(2)其他项目需要引用到starter,且启动类所在的包级别不同
所以对于上面的问题,com.ttt.TestNotImportService 的情况,可以通过写一个配置类,返回它的bean,然后在META-INF/spring.factories中写一个EnableAutoConfiguration的键值对,就可以成功加载了。
@Configuration
public class TestNotImportConfiguration {
@Bean
public TestNotImportService testNotImportService(){
return new TestNotImportService();
}
}
然后配置META-INF/spring.factories,注意!注意!注意!千万别漏了META-INF,血的教训(T_T)
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.ttt.config.TestNotImportConfiguration
放的位置
结果:
可以成功访问了项目包以外的类了