ComponentScan Annotation이 처리되는 과정
Component Scan 실질적으로 처리하는 클래스와 메소드
스프링은 @ComponentScan
애노테이션이 부착된 클래스를 기준으로 하여 하위 패키지들의 빈을 등록한다.
그러면 과연 @ComponentScan
애노테이션은 스프링이 어떻게 이 애노테이션을 처리하는 걸까?
ConfigurationClassParser
class ConfigurationClassParser {
...
...
private final ComponentScanAnnotationParser componentScanParser;
...
...
@Nullable
protected final SourceClass doProcessConfigurationClass(
ConfigurationClass configClass, SourceClass sourceClass, Predicate<String> filter)
throws IOException {
...
...
// Process any @ComponentScan annotations
Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable(
sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class);
if (!componentScans.isEmpty() &&
!this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) {
for (AnnotationAttributes componentScan : componentScans) {
// The config class is annotated with @ComponentScan -> perform the scan immediately
Set<BeanDefinitionHolder> scannedBeanDefinitions =
this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName()); // componentScanParser를 인스턴스로 갖고 있음 여기서 @ComponentScan 처리!
// Check the set of scanned definitions for any further config classes and parse recursively if needed
for (BeanDefinitionHolder holder : scannedBeanDefinitions) {
BeanDefinition bdCand = holder.getBeanDefinition().getOriginatingBeanDefinition();
if (bdCand == null) {
bdCand = holder.getBeanDefinition();
}
if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, this.metadataReaderFactory)) {
parse(bdCand.getBeanClassName(), holder.getBeanName());
}
}
}
}
...
...
}
...
...
}
ComponentScanAnnotationParser 클래스
class ComponentScanAnnotationParser {
...
...
public Set<BeanDefinitionHolder> parse(AnnotationAttributes componentScan, final String declaringClass) {
ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(this.registry,
componentScan.getBoolean("useDefaultFilters"), this.environment, this.resourceLoader);
...
// @ComponentScan에 설정된 옵션들(excludeFilters, scopeProxy, nameGenerator 등)을 읽어 처리할 수 있게
// ClassPathBeanDefinitionScanner의 인스턴스 scanner에 설정하는 코드가 있음 생략
...
Set<String> basePackages = new LinkedHashSet<>();
String[] basePackagesArray = componentScan.getStringArray("basePackages");
for (String pkg : basePackagesArray) {
String[] tokenized = StringUtils.tokenizeToStringArray(this.environment.resolvePlaceholders(pkg),
ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
Collections.addAll(basePackages, tokenized);
}
for (Class<?> clazz : componentScan.getClassArray("basePackageClasses")) {
basePackages.add(ClassUtils.getPackageName(clazz));
}
if (basePackages.isEmpty()) {
basePackages.add(ClassUtils.getPackageName(declaringClass));
}
// 얘 아래쪽으로 내려왔넹 원래 위에 있었던 것 같은데...
scanner.addExcludeFilter(new AbstractTypeHierarchyTraversingFilter(false, false) {
@Override
protected boolean matchClassName(String className) {
return declaringClass.equals(className);
}
});
return scanner.doScan(StringUtils.toStringArray(basePackages)); // 실질적인 scan이 수행되는 메소
}
...
...
}
ClassPathBeanDefinitionScanner 클래스
public class ClassPathBeanDefinitionScanner extends ClassPathScanningCandidateComponentProvider {
...
...
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) {
Set<BeanDefinition> candidates = findCandidateComponents(basePackage); // 여기서 특정 패키지에 속한 모든 리소스 파일들을 읽어온다.
for (BeanDefinition candidate : candidates) {
ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate);
candidate.setScope(scopeMetadata.getScopeName());
String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry);
if (candidate instanceof AbstractBeanDefinition) {
postProcessBeanDefinition((AbstractBeanDefinition) candidate, beanName);
}
if (candidate instanceof AnnotatedBeanDefinition) {
AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate);
}
if (checkCandidate(beanName, candidate)) {
BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName);
definitionHolder =
AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
beanDefinitions.add(definitionHolder);
registerBeanDefinition(definitionHolder, this.registry);
}
}
}
return beanDefinitions;
}
...
...
}
ClassPathScanningCandidateComponentProvider
public class ClassPathScanningCandidateComponentProvider implements EnvironmentCapable, ResourceLoaderAware {
@Nullable
private CandidateComponentsIndex componentsIndex;
...
...
public Set<BeanDefinition> findCandidateComponents(String basePackage) {
if (this.componentsIndex != null && indexSupportsIncludeFilters()) {
return addCandidateComponentsFromIndex(this.componentsIndex, basePackage);
}
else {
return scanCandidateComponents(basePackage);
}
}
...
...
}
findCandidateComponents 메소드를 잘보면 조건이 있는 것을 볼 수 있다.
이 componentIndex 변수는 CandidateComponentsIndex 클래스 타입인데 재밌는 것이 @Nullable
메소드를 갖고 있다.
즉 이 값은 널이 될 수 있음을 암시하고 있다는 것에서 대략 이 멤버변수가 아마도 어떤 메타 데이터가 있을 때 채워지는 변수라고 볼 수 있겠다.
CandidateComponentsIndex
/**
* Provide access to the candidates that are defined in {@code META-INF/spring.components}.
*
* <p>An arbitrary number of stereotypes can be registered (and queried) on the index: a
* typical example is the fully qualified name of an annotation that flags the class for
* a certain use case. The following call returns all the {@code @Component}
* <b>candidate</b> types for the {@code com.example} package (and its sub-packages):
* <pre class="code">
* Set<String> candidates = index.getCandidateTypes(
* "com.example", "org.springframework.stereotype.Component");
* </pre>
*
* <p>The {@code type} is usually the fully qualified name of a class, though this is
* not a rule. Similarly, the {@code stereotype} is usually the fully qualified name of
* a target type but it can be any marker really.
*
* @author Stephane Nicoll
* @since 5.0
*/
public class CandidateComponentsIndex {
...
생략
}
위의 주석 최 상단을 보면 spring.components에 정의된 후보들에 대한 접근을 제공하는 클래스인 것을 알 수 있다.
그 말은 즉슨, 이 파일에 클래스 패스를 명시할 수도 있다는 뜻이 된다.
그러나 500개 미만의 빈의 경우 클래스 패스를 스캐닝 하는 것과 파일을 읽어서 빈을 등록하는 것의 속도차이는 거의 없다고 한다.
직접 디버깅을 찍어보면 파일이 있는 경우 addCandidateComponentsFromIndex 메소드를 수행하는 것을 확인할 수 있다.
componentsIndex가 null이 아니게 되면서 이 기능을 수행하는 것을 확인할 수 있다.
이름만 보면 알겠지만 인덱스 파일로부터 빈을 등록하는 메소드이다.
그게 아니라면 일반적으로 클래스 패스(@ComponentScan이 위치한) 패키지 하위에 있는 클래스 중 @Component
애노테이션이 존재하는 클래스나
그 클래스를 메타 애노테이션으로 갖고 있는 애노테이션들을 조사해서 빈으로 등록하는 기능을 scanCandidateComponents 메소드가 수행한다.
이 기능은 스프링 5에 추가된 기능인데, 대규모 애플리케이션일 때 이 방법을 사용하면 스프링 애플리케이션이 구동될 때 조금 더 빠르게 시작할 수 있다는 장점이 있다. (단 빈이 500개 이상인 경우에만)
약간 샛길로 빠졌는데 다시 돌아와서 scanCandidateComponents 메소드와 addCandidateComponentsFromIndex 메소드에는 각각 isCandidateComponent 메소드가 존재한다.
protected boolean isCandidateComponent(MetadataReader metadataReader) throws IOException {
for (TypeFilter tf : this.excludeFilters) {
if (tf.match(metadataReader, getMetadataReaderFactory())) {
return false;
}
}
for (TypeFilter tf : this.includeFilters) {
if (tf.match(metadataReader, getMetadataReaderFactory())) {
return isConditionMatch(metadataReader);
}
}
return false;
}
이 isCandidateComponent 메소드는 @Component
애노테이션인 경우인지를 판별하는 함수이다.
여기서 @Component
애노테이션인 경우 필터를 통과해서 빈으로 등록되게 된다.
빈 등록 과정은 다음과 같다.
public ScannedGenericBeanDefinition(MetadataReader metadataReader) {
Assert.notNull(metadataReader, "MetadataReader must not be null");
this.metadata = metadataReader.getAnnotationMetadata();
setBeanClassName(this.metadata.getClassName());
setResource(metadataReader.getResource());
}
ScannedGenericBeanDefinition
클래스의 생성자에서 빈의 이름을 등록한다.
이렇게 생성된 빈 후보들을 LinkedHashSet에 담아서 doScan 메소드로 반환한다.
그 이후 빈의 Scope이 싱글톤인지 프로토타입인지 판별하고 definitionHolder로 만든 뒤 beanDefinitions로 만들고 이를
registerBeanDefinition 메소드를 통해 registry에 등록하게 한다.
이 때 registry는 최초에 매개변수로 넘겨준 beanFactory로써 DefaultListableBeanFactory 클래스의 인스턴스이다.
때문에 아래의 과정에서 빈이 등록되게 되는 것이다.
if (checkCandidate(beanName, candidate)) {
BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName);
definitionHolder =
AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
beanDefinitions.add(definitionHolder);
registerBeanDefinition(definitionHolder, this.registry); // 여기서 빈팩토리에 빈이 등록
}
참조
https://thswave.github.io/spring/2015/02/02/spring-mvc-annotaion.html
https://github.com/awslabs/aws-serverless-java-container/issues/131
https://stackoverflow.com/questions/47254907/how-can-i-create-a-spring-5-component-index