基于springboot 2.6.0分析
Spring对@Configuration
@Component
配置类中 内部类的特殊处理
整合SpringBoot和Mybatis的一个Starter时发现:
- Starter在
spring.factories
中配置了一个自动装配的配置类MapperAutoConfiguration
并标记了@Configuration
, - 而
MapperAutoConfiguration
中有个静态内部类MapperScannerRegistrarNotFoundConfiguration
。 - 该静态内部类虽然使用了
@Configuration
注解,但并没有配置在spring.factories
中,启动时spring也不会扫描到这个包下。
SpringBoot启动后MapperAutoConfiguration
能正常实例化成一个Bean,这个能理解。
但是 静态内部类MapperScannerRegistrarNotFoundConfiguration
竟然也被创建成了一个Bean,为什么? ? ?
示例代码如下:
简单的一个启动类
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Demo1Application {
public static void main(String[] args) {
SpringApplication.run(Demo1Application.class, args);
}
}
MapperAutoConfiguration及静态内部类,注意: MapperAutoConfiguration
并不在工程默认的扫描包下。
package com.out.scan;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.BeanNameAware;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
// Created by bruce on 2021/12/20 15:07
@Slf4j
@Configuration
public class MapperAutoConfiguration {
public MapperAutoConfiguration() {
log.info("MapperAutoConfiguration init...");
}
//@Import(value = ImportedBean1.class)
@Configuration
public static class MapperScannerRegistrarNotFoundConfiguration implements BeanNameAware {
@PostConstruct
public void afterPropertiesSet() {
log.info("MapperScannerRegistrarNotFoundConfiguration init...");
}
@Override
public void setBeanName(String s) {
System.out.println("内部类的Bean名称:" + s);
}
}
}
spring.factories中的配置及整体工程结构如下:
针对上面的疑问,来跟源码一探究竟:
Spring在启动过程中会将扫描的类,(通过类路径扫描得到,或是在spring.factories配置的类),包装成ConfigurationClass
对其进行再次解析,以为MapperAutoConfiguration
例,ConfigurationClass
中存放信息如下:
同时MapperAutoConfiguration
还会包装成ConfigurationClassParser.SourceClass
ConfigurationClassParser.SourceClass
中有个方法#getMemberClasses
则是用于获取一个类中的内部类。
当解析到MapperAutoConfiguration
时,会先调用#getMemberClasses
获取所有的内部类,遍历这些内部类并通过ConfigurationClassUtils.isConfigurationCandidate(memberClass.getMetadata())
判断是否是一个符合要求的配置类,如果是的话同样会被包装成ConfigurationClass
调用ConfigurationClassParser#processConfigurationClass
解析。
所以默认情况下,内部类中的Bean将会优先被创建。
源码如下:
最后这些ConfigurationClass
就会在ConfigurationClassPostProcessor#processConfigBeanDefinitions
方法中调用this.reader.loadBeanDefinitions(configClasses);
注册所有的BeanDefinition在spring容器中。
再看下ConfigurationClassUtils#isConfigurationCandidate
方法中是怎么定义哪些是符合要求的类,
private static final Set<String> candidateIndicators = new HashSet<>(8);
static {
candidateIndicators.add(Component.class.getName());
candidateIndicators.add(ComponentScan.class.getName());
candidateIndicators.add(Import.class.getName());
candidateIndicators.add(ImportResource.class.getName());
}
public static boolean isConfigurationCandidate(AnnotationMetadata metadata) {
// Do not consider an interface or an annotation...
if (metadata.isInterface()) {
return false;
}
// Any of the typical annotations found?
for (String indicator : candidateIndicators) {
if (metadata.isAnnotated(indicator)) {
return true;
}
}
// Finally, let's look for @Bean methods...
return hasBeanMethods(metadata);
}
static boolean hasBeanMethods(AnnotationMetadata metadata) {
try {
return metadata.hasAnnotatedMethods(Bean.class.getName());
} catch (Throwable ex) {
if (logger.isDebugEnabled()) {
logger.debug("Failed to introspect @Bean methods on class [" + metadata.getClassName() + "]: " + ex);
}
return false;
}
}
即类上标记@Component
,@ComponentScan
,@Import
,@ImportResource
这几个注解的其中一个的,或者有标记@Bean
注解方法的都可以.
所以文章例子中的内部类,如下两种写法,实际上也会被注册成Bean
写法 1
@Configuration
public class MapperAutoConfiguration {
public MapperAutoConfiguration() { log.info("MapperAutoConfiguration init..."); }
//写法 1 在内部类上使用@Component, @ComponentScan, @Import, @ImportResource中的任意一个注解
@Import(value = ImportedBean1.class)
public static class MapperScannerRegistrarNotFoundConfiguration {
public MapperScannerRegistrarNotFoundConfiguration() {
log.info("MapperScannerRegistrarNotFoundConfiguration init...");
}
@Bean
public ImportedBean1 importedBean1() {
return new ImportedBean1();
}
}
}
写法 2
@Configuration
public class MapperAutoConfiguration {
public MapperAutoConfiguration() { log.info("MapperAutoConfiguration init..."); }
//写法 2 内部类上不使用任何注解,但是内部类中有个@Bean方法
public static class MapperScannerRegistrarNotFoundConfiguration {
public MapperScannerRegistrarNotFoundConfiguration() {
log.info("MapperScannerRegistrarNotFoundConfiguration init...");
}
@Bean
public ImportedBean1 importedBean1() {
return new ImportedBean1();
}
}
}
如果spring.factories中的配置不变,但是去掉MapperAutoConfiguration上的@Configuration注
解,会如何呢?
MapperAutoConfiguration仍然会被注册到Spring中,但是内部类 MapperScannerRegistrarNotFoundConfiguration 不会再被注册.
原因在于调用ConfigurationClassParser#processMemberClasses
之前会判断外部类是否有@Component
Spring是如何获取一个类中的内部类的呢?
JDK11之前版本并没有提供api来获取一个类中有哪些内部类,jdk11中提供了如下的方式来获取
// @since 11
Class<?>[] nestMembers = MapperAutoConfiguration.class.getNestMembers();
而Spring其实是通过org.ow2.asm:asm
包读取class文件中字节码的方法获取的,但Spring并没有直接使用asm包,而是将asm包中源码完全的复制了一份到spring-core包中。源码见:
org.springframework.core.type.classreading.SimpleAnnotationMetadataReadingVisitor#visitInnerClass