Spring源码系列(一):<context:component-scan />
1. 基本使用
我们在创建spring项目时会在xml配置文件中配置<context:component-scan base-package="com.test"/>后,spring就会自动扫描com.test包下的java类,如果java类被@Controller、@Service、@Repository、@Component注解修饰,那么就会把这些类注册进容器中。
2. 实现原理
2.1 @Controller、@Service、@Repository注解解析
要想明白<context:component-scan base-package="com.test"/>的实现原理,首先要明白这几个注解是干嘛的。以@Controller为例。
-
先来看看官方的解释
This annotation serves as a specialization of @Component, allowing for implementation classes to be autodetected through classpath scanning. It is typically used in combination with annotated handler methods based on the RequestMapping annotation. 翻译: 此注释用作@Component的专用化,允许通过类路径扫描自动检测实现类。它通常与基于 RequestMapping 注释的带批注的处理程序方法结合使用。 -
@Controller是@Component注解的一个专用化,我们点开@Controller注解(windows下 ctrl + 鼠标左键进入该注解)。会发现@Controller注解里面套了一个@Component注解。实际上@Controller、@Service、@Repository、@Component注解,打开以后都是@Component。我们再扫描过程中,也是扫描该类是不是被@Component注解修饰,如果被修饰则进入到容器中。
@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Component public @interface Controller { /** * The value may indicate a suggestion for a logical component name, * to be turned into a Spring bean in case of an autodetected component. * @return the suggested component name, if any (or empty String otherwise) */ @AliasFor(annotation = Component.class) String value() default ""; }2.2 源码剖析
xml文件中的默认标签和自定义标签
在spring解析xml文件的过程中,默认的标签有四个:
import、alias、bean、beans,例如我们自定义一个bean时常写的。这些xml中标签都会调用parseDefaultElement()方法来解析。<bean id="person" class="com.test.entity.Person"> <property name="id" value="1"/> <property name="name" value="zhangsan"/> </bean>其余的spring自带的标签,例如
<context:component-scan />、< annotation-driven/>都是属于自定义命名空间。解析它们的时候会调用parseCustomElement()方法。所以这里有一个spring的拓展点:自定义标签解析。标签有了,我们需要一个类解析它。spring中解析
<context:component-scan />的类是ComponentScanBeanDefinitionParser。我把主要的代码放到下面。目录结构
我们有两个包:
com.ming.shouldSkip, com.ming.config,下面主要以config包为例子。config包的类:
- PersonService
- PersonService2
/** * 被@Service注解修饰,可被识别到 */ @Service public class PersonService { } /** * 没有被@Service注解(@Component)修饰,不可被识别到 */ @Service public class PersonService2 { }进入源码
当我们进入一个类,不知道该类是干什么用的时候,先看注解,比如当前类,我们通过注解可以了解到。这个类是__解析
<context:component-scan />元素的__。applicationContext.xml中的配置: <context:component-scan base-package="com.ming.shouldSkip, com.ming.config"/> Spring源码: /** * Parser for the {@code <context:component-scan/>} element. * @since 2.5 */ public class ComponentScanBeanDefinitionParser implements BeanDefinitionParser { public BeanDefinition parse(Element element, ParserContext parserContext) { // 获取<context:component-scan />节点的base-package属性值 // 根据我们的配置,basePackage的值为 com.ming.shouldSKip,com.ming.config String basePackage = element.getAttribute(BASE_PACKAGE_ATTRIBUTE); // 解析占位符 basePackage = parserContext.getReaderContext().getEnvironment(). .resolvePlaceholders(basePackage); // 解析base-package(允许通过,;\t\n中的任一符号填写多个) // 因为我们写了两个包路径,所有这里的basePackages的size为2。 String[] basePackages = StringUtils.tokenizeToStringArray(basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS); // 构建和配置ClassPathBeanDefinitionScanner ClassPathBeanDefinitionScanner scanner = configureScanner(parserContext, element); // 使用scanner在执行的basePackages包中执行扫描,返回已注册的bean定义 // 重点,doScan方法是扫描包下的java类有没有被@Component注解修饰的。我们ctrl + 鼠标左键,进入。 Set<BeanDefinitionHolder> beanDefinitions = scanner.doScan(basePackages); // 组件注册(包括注册一些内部的注解后置处理器,触发注册事件) registerComponents(parserContext.getReaderContext(), beanDefinitions, element); return null; } }doScan方法
前置知识:What is BeanDefiniton?
spring初始化容器时,会加载配置文件(applicationContext.xml),然后把配置文件转化成一个document文档对象,然后解析里面的标签(自定义标签,默认标签)。然后生成一个BeanDefinition对象来保存bean的信息,被注解修饰的类比如@Component,也会先转换成一个BeanDefinition对象。
之后spring在根据BeanDefinition的信息来生成这个bean,并放入到容器中,提供给我们使用。
这里比较复杂,我们只看加注释的代码即可。
/** * Perform a scan within the specified base packages, * returning the registered bean definitions. * 翻译:在指定的基包内执行扫描,返回已注册的 Bean 定义。 */ protected Set<BeanDefinitionHolder> doScan(String... basePackages) { // 创建一个集合,用来储存bean定义信息。 Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<>(); // 遍历basePackages for (String basePackage : basePackages) { // 扫描basePackage,将符合要求的bean定义全部找出来 // findCandidateComponents()方法是检查这个包下的类,是不是有被@Component修饰的类。下面会看源码 Set<BeanDefinition> candidates = findCandidateComponents(basePackage); // 遍历所有候选的bean定义 for (BeanDefinition candidate : candidates) { ...... // 注册beanDefinition registerBeanDefinition(definitionHolder, this.registry); } } // 返回beanDefinitions集合 return beanDefinitions; }findCandidateComponents()方法
主要逻辑是里面的scanCandidateComponents(basePackage)方法,我们来看它的源码。
private Set<BeanDefinition> scanCandidateComponents(String basePackage) { Set<BeanDefinition> candidates = new LinkedHashSet<>(); // 1. 获取包的classpath路径,因为我们的上一步是循环包路径,所有得一个个的来。 // 这里先处理com.ming.config,并以这个为例子。 // 经过拼接等操作,packageSearchPath的值为 classpath*:com/ming/config/**/*.class // 这里的com/ming/config/**/*.class,意思是config包下的类以后其中的内部类。都会被扫描到。 String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + resolveBasePackage(basePackage) + '/' + this.resourcePattern; // 2. 把包下的所有class解析成resource资源 Resource[] resources = getResourcePatternResolver().getResources(packageSearchPath); // 3. 遍历所有resource for (Resource resource : resources) { // 4. 获取类的元信息 MetadataReader metadataReader = getMetadataReaderFactory() .getMetadataReader(resource); // 5. 判断是否是候选component // 核心步骤,我们已经获取到类的元信息了,判断是否@Component注解修饰 // 也就是isCandidateComponent()方法,如果为真,则说明这个类需要被注入到容器中 if (isCandidateComponent(metadataReader)) { ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader); sbd.setSource(resource); // 6. 判断该bean是否能实例化 if (isCandidateComponent(sbd)) { // 7. 加入候选类列表 candidates.add(sbd); } } } // 8. 返回候选components列表,这些类会被注入到ApplicationContext容器中,供我们使用。 return candidates; }执行结果
在
com.ming.config包下的PersonService类因为加了@Service注解,所有最终会被加载到ApplicationContext容器中,我们可以这样使用:ApplicationContext context = new MyClassPathXmlApplicationContext( "applicationContext.xml"); PersonService personService = (PersonService) context.getBean("personService");