Spring如何扫描第三方自定义注解,并注入成Bean?

2,728 阅读3分钟

1. Spring如何扫描自身注解并将其初始化为BeanDefinition。

可以看看我的这篇文章@ComponentScan注解原理

Spring扫描@Component注解的类并将其初始化为BeanDefinition

首先Spring有一个扫描器ClassPathBeanDefinitionScanner,来扫描@SpringBootApplication注解里面scanBasePackages()方法配置的扫描路径,如果没有配置,那就扫描启动类包路径下所有的类。

如何扫描呢?先看看ClassPathBeanDefinitionScanner扫描类,注释上说了要扫描@Component@Repository@Service@Controller以及ManagedBeanNamed

/**
 * A bean definition scanner that detects bean candidates on the classpath,
 * registering corresponding bean definitions with a given registry ({@code BeanFactory}
 * or {@code ApplicationContext}).
 *
 * <p>Candidate classes are detected through configurable type filters. The
 * default filters include classes that are annotated with Spring's
 * {@link org.springframework.stereotype.Component @Component},
 * {@link org.springframework.stereotype.Repository @Repository},
 * {@link org.springframework.stereotype.Service @Service}, or
 * {@link org.springframework.stereotype.Controller @Controller} stereotype.
 *
 * <p>Also supports Java EE 6's {@link javax.annotation.ManagedBean} and
 * JSR-330's {@link javax.inject.Named} annotations, if available.
 *
 * @author Mark Fisher
 * @author Juergen Hoeller
 * @author Chris Beams
 * @since 2.5
 * @see AnnotationConfigApplicationContext#scan
 * @see org.springframework.stereotype.Component
 * @see org.springframework.stereotype.Repository
 * @see org.springframework.stereotype.Service
 * @see org.springframework.stereotype.Controller
 */
public class ClassPathBeanDefinitionScanner extends ClassPathScanningCandidateComponentProvider {

	public ClassPathBeanDefinitionScanner(BeanDefinitionRegistry registry, boolean useDefaultFilters,
			Environment environment, @Nullable ResourceLoader resourceLoader) {

		Assert.notNull(registry, "BeanDefinitionRegistry must not be null");
		this.registry = registry;

		if (useDefaultFilters) {
			// 初始化的时候调用注册默认filter,添加@Component注解
			registerDefaultFilters();
		}
		setEnvironment(environment);
		setResourceLoader(resourceLoader);
	}
	
	protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
		Assert.notEmpty(basePackages, "At least one base package must be specified");
		Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<>();
		for (String basePackage : basePackages) {
			// 扫描得到候选BeanDefinition
			Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
			···
			
			}
}

后面两个很少用,这里说前面4个注解。你会发现有一个共同点@Repository@Service@Controller这三个注解里面都标注了@Component.

其实你自定义一个注解,跟上面三个注解一样,标注到类上,也会被spring扫描到,然后初始化为BeanDefinition。效果等同于@Repository@Service@Controller。再说一嘴,其实这三个注解本质上是一样的,可以相互替代,只是官方为了使注解有不同的含义,才弄了几个名字。具体看官网,位置我忘记在哪里了。

比如@NameRepository:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface StringRepository {

    String value() default "";
}

至于具体是怎么生效的,可以看我另一篇文章

@ComponentScan注解原理

这里只简单提一下如何扫描@Component.

org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider#scanCandidateComponents

	private Set<BeanDefinition> scanCandidateComponents(String basePackage) {
		Set<BeanDefinition> candidates = new LinkedHashSet<>();
		try {
			String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
					resolveBasePackage(basePackage) + '/' + this.resourcePattern;
			Resource[] resources = getResourcePatternResolver().getResources(packageSearchPath);
			for (Resource resource : resources) {
				if (resource.isReadable()) {
					try {
						MetadataReader metadataReader = getMetadataReaderFactory().getMetadataReader(resource);
						// 这里是关键
						if (isCandidateComponent(metadataReader)) {
							ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader);
							sbd.setSource(resource);
							if (isCandidateComponent(sbd)) {
								candidates.add(sbd);
							}
							···
		}
		return candidates;
	}
	
	protected boolean isCandidateComponent(MetadataReader metadataReader) throws IOException {
		for (TypeFilter tf : this.excludeFilters) {
			if (tf.match(metadataReader, getMetadataReaderFactory())) {
				return false;
			}
		}
		// 只要includeFilters包含@Component注解,就满足.
		// 而初始化ClassPathBeanDefinitionScanner的时候调用registerDefaultFilters()方法注入了@Component
		for (TypeFilter tf : this.includeFilters) {
			if (tf.match(metadataReader, getMetadataReaderFactory())) {
				return isConditionMatch(metadataReader);
			}
		}
		return false;
	}
	
    protected void registerDefaultFilters() {
        this.includeFilters.add(new AnnotationTypeFilter(Component.class));
        ClassLoader cl = ClassPathScanningCandidateComponentProvider.class.getClassLoader();
        try {
        this.includeFilters.add(new AnnotationTypeFilter(
        ((Class<? extends Annotation>) ClassUtils.forName("javax.annotation.ManagedBean", cl)), false));
		···
    }

总结:初始化为BeanDefinition共分为2步

  1. 扫描器扫描类
  2. 满足isCandidateComponent()方法

那要是初始化为bean呢?还需要一步,调用getBean(beanName)方法

org.springframework.beans.factory.support.DefaultListableBeanFactory#preInstantiateSingletons

	public void preInstantiateSingletons() throws BeansException {
			List<String> beanNames = new ArrayList<>(this.beanDefinitionNames);

		// Trigger initialization of all non-lazy singleton beans...
		for (String beanName : beanNames) {
			RootBeanDefinition bd = getMergedLocalBeanDefinition(beanName);
			// 初始化为bean是需要条件的 不能是抽象类或者接口,因为无法实例化
			if (!bd.isAbstract() && bd.isSingleton() && !bd.isLazyInit()) {
				if (isFactoryBean(beanName)) {
					Object bean = getBean(FACTORY_BEAN_PREFIX + beanName);
					if (bean instanceof FactoryBean) {
						FactoryBean<?> factory = (FactoryBean<?>) bean;
						boolean isEagerInit;
						if (System.getSecurityManager() != null && factory instanceof SmartFactoryBean) {
							isEagerInit = AccessController.doPrivileged(
									(PrivilegedAction<Boolean>) ((SmartFactoryBean<?>) factory)::isEagerInit,
									getAccessControlContext());
						}
						else {
							isEagerInit = (factory instanceof SmartFactoryBean &&
									((SmartFactoryBean<?>) factory).isEagerInit());
						}
						if (isEagerInit) {
							getBean(beanName);
						}
					}
				}
				else {
					getBean(beanName);
				}
			}
		}

所以还需要加一条初始化成bean的时候,该类不能为接口或者抽象类。

2. Spring如何初始化自定义注解?

open-Feign里面的@FeignClient使用。可以参考我的文章@FeignClient使用及原理

都是定义一个接口,然后加上自定义的注解,就可以初始化为bean的,这与之前分析的Spring扫描自身注解的原理相悖论。

  1. 扫描自定义注解,拿feign里面的@FeignClient来说,那就重写一个扫描器,让isCandidateComponent方法里面的includeFilters包含@FeignClient即可
org.springframework.cloud.openfeign.FeignClientsRegistrar#registerFeignClients
	public void registerFeignClients(AnnotationMetadata metadata,
			BeanDefinitionRegistry registry) {
		ClassPathScanningCandidateComponentProvider scanner = getScanner();
		scanner.setResourceLoader(this.resourceLoader);

		Set<String> basePackages;

		Map<String, Object> attrs = metadata
				.getAnnotationAttributes(EnableFeignClients.class.getName());
				// 这里添加了注解@FeignClient
		AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(
				FeignClient.class);
		final Class<?>[] clients = attrs == null ? null
				: (Class<?>[]) attrs.get("clients");
		if (clients == null || clients.length == 0) {
			scanner.addIncludeFilter(annotationTypeFilter);
			basePackages = getBasePackages(metadata);
		}
		else {
			final Set<String> clientClasses = new HashSet<>();
			basePackages = new HashSet<>();
			for (Class<?> clazz : clients) {
				basePackages.add(ClassUtils.getPackageName(clazz));
				clientClasses.add(clazz.getCanonicalName());
			}
			AbstractClassTestingTypeFilter filter = new AbstractClassTestingTypeFilter() {
				@Override
				protected boolean match(ClassMetadata metadata) {
					String cleaned = metadata.getClassName().replaceAll("\\$", ".");
					return clientClasses.contains(cleaned);
				}
			};
			// 添加到IncludeFilter
			scanner.addIncludeFilter(
					new AllTypeFilter(Arrays.asList(filter, annotationTypeFilter)));
		}

		for (String basePackage : basePackages) {
			Set<BeanDefinition> candidateComponents = scanner
					.findCandidateComponents(basePackage);
			for (BeanDefinition candidateComponent : candidateComponents) {
				if (candidateComponent instanceof AnnotatedBeanDefinition) {
					// verify annotated class is an interface
					AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) candidateComponent;
					AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();
					Assert.isTrue(annotationMetadata.isInterface(),
							"@FeignClient can only be specified on an interface");

					Map<String, Object> attributes = annotationMetadata
							.getAnnotationAttributes(
									FeignClient.class.getCanonicalName());

					String name = getClientName(attributes);
					registerClientConfiguration(registry, name,
							attributes.get("configuration"));

					registerFeignClient(registry, annotationMetadata, attributes);
				}
			}
		}
	}
  1. 接口不能注入成bean,这个可以使用动态代理解决。

给生成的BeanDefinition的beanClass属性设置一个FactoryBean.class,那么实例化的时候,就会通过FactoryBean.getObject()获取一个实例。

我们熟知的很多框架,比如MyBatis中的@Mapper,Dubbo里面的@Service@Reference以及JPA框架里面的Repository接口。

都是上面这种套路来扫描自定义的注解或者接口模式,有兴趣的自己跟着文章去debug一下。