自定义starter为什么要加上spring.factories


自定义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

放的位置
在这里插入图片描述

结果:
在这里插入图片描述
可以成功访问了项目包以外的类了

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值