Spring包掃描機制詳解


聲明:源碼基於4.3.18

目標

此篇文章會主要介紹Spring中兩個非常重要的關於包掃描的基礎類,由於Spring代碼太龐大,因此本文不會細致地說明每一行代碼地作用,只會講清楚關鍵的地方有什么作用,以及一些子類可以重寫的方法,用來覆蓋默認掃描行為。最后會基於Spring提供的包掃描設施來寫一個簡單的例子來模仿MyBatis-Spring掃描Mapper接口,生成代理注冊到容器中。我們主要關注ClassPathScanningCandidateComponentProvider以及ClassPathBeanDefinitionScanner這兩個類,講清楚這兩個類的作用以及開發者需要關注的方法。

ClassPathScanningCandidateComponentProvider

此類是Spring中包掃描機制最底層的類,用於掃描指定包下面的類文件,並且會根據用戶提供的includeFilters以及excludeFilters來過濾掉不想注冊的類,最后生成一個基本的BeanDefinition

先看下兩個比較重要的屬性吧

/**
 * 包含集合,如果類文件匹配includeFilters集合中任意一個TypeFilter條件,那么就通過篩選。
 * 其中最常見的TypeFilter有AnnotationTypeFilter、AssignableTypeFilter
 * AnnotationTypeFilter: 代表類是否被指定注解標注
 * AssignableTypeFilter: 代表類是否繼承(實現)自指定的超類
 */
private final List<TypeFilter> includeFilters = new LinkedList<TypeFilter>();

/**
 * 排除集合, 如果類文件匹配excludeFilters集合中任何一個TypeFilter條件,那么就不會通過篩選
 * 並且excludeFilters優先級高於includeFilters
 */
private final List<TypeFilter> excludeFilters = new LinkedList<TypeFilter>();

初始化,只看參數最長的那個構造方法

public ClassPathScanningCandidateComponentProvider(boolean useDefaultFilters, 
                                                   Environment environment) {
    // 主要關注useDefaultFilters這個參數, 如果為true, 會注冊一個默認的
    // includeFilter
    if (useDefaultFilters) {
        registerDefaultFilters();
    }
    setEnvironment(environment);
    setResourceLoader(null);
}

protected void registerDefaultFilters() {
    // 注冊一個注解的TypeFilter,意思是如果類定義時有被@Component注解標注
    // 那么就會通過篩選,需要注意的是衍生注解也是會通過篩選的
    // 比如@Service、@Controller、@Repository,它們有一個共同點,那就是這三個
    // 注解本身就是被@Component注解標注的
    this.includeFilters.add(new AnnotationTypeFilter(Component.class));
    // 下面是注冊JAVA EE里的一些注解,一般開發也不用, 就省略了
}

接下來就是最重要的方法了,也就是掃描包文件,並且將符合篩選條件的類生成BeanDefinition。下面代碼將日志打印剔除了。

/**
 * 此方法會掃描指定包以及子包
 * @param basePackage 需要掃描的包,形如com.wangtao.dao
 * @return 返回一個符合篩選條件后的BeanDefinition集合
 */
public Set<BeanDefinition> findCandidateComponents(String basePackage) {
    Set<BeanDefinition> candidates = new LinkedHashSet<BeanDefinition>();
    try {
        // 將包名解析成路徑
        String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
            resolveBasePackage(basePackage) + '/' + this.resourcePattern;
        // 獲取此包以及子包下的所有.class文件資源
        Resource[] resources = this.resourcePatternResolver.
            getResources(packageSearchPath);
        for (Resource resource : resources) {
            if (resource.isReadable()) {
                try {
                    // 得到類文件的元數據,是基於ASM字節碼技術實現的,此時還沒有加載類文件
                    // 包括類名、注解信息、父類、接口等等一系列信息
                    MetadataReader metadataReader = this.metadataReaderFactory.
                        getMetadataReader(resource);
                    // 匹配篩選條件,也就是上述includeFilters、excludeFilters這兩個集合
                    if (isCandidateComponent(metadataReader)) {
                        // 創建一個BeanDefinition
                        // 只是簡單的設置了beanClassName屬性為類的完全限定名
                        ScannedGenericBeanDefinition sbd = new ScannedGenericBean
                            Definition(metadataReader);
                        sbd.setResource(resource);
                        sbd.setSource(resource);
                        // 這里會再有一個篩選條件,一般是根據類文件的元數據篩選
                        // 比如是不是具體類,是不是頂層類,是不是抽象類等
                        // 默認情況下只添加頂層的具體類,頂層的意思是可以獨立實例化而不會依賴外部類
                        // 成員內部類需要外部類對象才能實例化,就不會通過。
                        if (isCandidateComponent(sbd)) {
                            candidates.add(sbd);
                        }
                    }
                }
                catch (Throwable ex) {
                    throw new BeanDefinitionStoreException(
                        "Failed to read candidate component class: " + resource, ex);
                }
            }
        }
    }
    catch (IOException ex) {
        throw new BeanDefinitionStoreException("I/O failure during classpath 
                                               scanning", ex);
    }
    return candidates;
}

再看看兩個isCandidateComponent方法的默認實現,一般來說我們可能需要重寫這兩個方法來改變默認的篩選條件。

// 根據excludeFilters、excludeFilters初步篩選
// 一目了然,基本不用再需要解釋
protected boolean isCandidateComponent(MetadataReader metadataReader) throws IOException {
    for (TypeFilter tf : this.excludeFilters) {
        if (tf.match(metadataReader, this.metadataReaderFactory)) {
            return false;
        }
    }
    for (TypeFilter tf : this.excludeFilters) {
        if (tf.match(metadataReader, this.metadataReaderFactory)) {
            return isConditionMatch(metadataReader);
        }
    }
    return false;
}

// 根據類文件元數據篩選
// 只掃描頂層具體類或者雖然是抽象類但是存在@Lookup標記的方法
// 后面那個是用於方法注入,我從來沒用過。
protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
		AnnotationMetadata metadata = beanDefinition.getMetadata();
		return (metadata.isIndependent() && (metadata.isConcrete() ||
				(metadata.isAbstract() && metadata.hasAnnotatedMethods(
                    Lookup.class.getName()))));
}

總結:此類在默認情況下會將指定包以及子包下中被@Component(以及衍生注解)標記的頂層類創建一個BeanDefinition

說到這插下Spring開啟注解掃描的配置,有時我們可能只想在SpringMVC的配置文件中掃描@Controller標記的類,其它層掃描@Service、@Component、@Repository標記的類,就可以像下面這樣分層配置。

springmvc.xml

<context:component-scan base-package="com.wangtao.controller" 
                        use-default-filters="false">
    <context:include-filter 
        type="annotation" 
        expression="org.springframework.stereotype.Controller"/>
</context:component-scan>

applicationContext.xml

<context:component-scan base-package="com.wangtao.service,com.wangtao.dao">
    <context:exclude-filter 
        type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>

也就是說SpringMVC中禁掉默認的includeFilters,添加了一個使用@Controller標記的條件,而applicationContext.xml使用默認的includeFilters,但是排除對@Controller標記的類。

ClassPathBeanDefinitionScanner

此類繼承自ClassPathScanningCandidateComponentProvider,除了擁有父類掃描包的功能外,還會對掃描后的BeanDefinition加工並注冊到Spring容器中,所謂的加工就是指會設置一些類文件中用注解標記的一些屬性值,如@Lazy@Scope@Primary等。

先看一些重要屬性

/** 用於注冊bean **/
private final BeanDefinitionRegistry registry;

/** 
 * 此類存儲了BeanDefinition一些默認屬性值
 * lazyInit: false
 * autowireMode: AbstractBeanDefinition.AUTOWIRE_NO
 * initMethodName: null
 * destroyMethodName: null
**/
private BeanDefinitionDefaults beanDefinitionDefaults = new BeanDefinitionDefaults();

/** bean name 生成器 **/
private BeanNameGenerator beanNameGenerator = new AnnotationBeanNameGenerator();

/**
 * 默認值:true
 * 相當於開啟<context:annotation-config>
 * 意味着我們可以使用@Autowired、@Resource、@PostConstruct、@PreDestroy注解
 * 會自動幫我們注冊解析這幾個注解的BeanPostProcessor
 */
private boolean includeAnnotationConfig = true;

構造方法

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

    Assert.notNull(registry, "BeanDefinitionRegistry must not be null");
    this.registry = registry;
    // 同ClassPathScanningCandidateComponentProvider
    if (useDefaultFilters) {
        registerDefaultFilters();
    }
    setEnvironment(environment);
    setResourceLoader(resourceLoader);
}

接下來看最重要的掃描方法

/**
 * 返回掃描真正注冊bean的數量
 */
public int scan(String... basePackages) {
    int beanCountAtScanStart = this.registry.getBeanDefinitionCount();
    doScan(basePackages);
	// 注冊幾個BeanPostProcessor用來解析@Autowired、@Reource等幾個注解
    if (this.includeAnnotationConfig) {
        AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry);
    }
    return (this.registry.getBeanDefinitionCount() - beanCountAtScanStart);
}

/**
 * 將BeanDefinition集合返回,子類若有必要可以繼續對BeanDefinition做修改
 */
protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
    Assert.notEmpty(basePackages, "At least one base package must be specified");
    Set<BeanDefinitionHolder> beanDefinitions = new 
        LinkedHashSet<BeanDefinitionHolder>();
    for (String basePackage : basePackages) {
        // 得到所有符合掃描條件的BeanDefinition集合,接下來會對這些BeanDefinition加工
        Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
        for (BeanDefinition candidate : candidates) {
            // 主要包括bean的scope屬性(默認單例)以及代理策略(不需要代理,JDK動態代理、CGLIB)
            // 來適配AOP
            ScopeMetadata scopeMetadata = this.scopeMetadataResolver
                .resolveScopeMetadata(candidate);
            // 設置scope屬性
            candidate.setScope(scopeMetadata.getScopeName());
            // 生成bean name
            String beanName = this.beanNameGenerator
                .generateBeanName(candidate, this.registry);
            if (candidate instanceof AbstractBeanDefinition) {
                // 根據beanDefinitionDefaults設置一些默認值
                postProcessBeanDefinition((AbstractBeanDefinition) candidate, beanName);
            }
            // 如果是注解定義的Bean, findCandidateComponents默認實現返回的BeanDefinition
            // 是一個ScannedGenericBeanDefinition,其實現了AnnotatedBeanDefinition接口
            if (candidate instanceof AnnotatedBeanDefinition) {
                // 解析@Scope、@Primary、@Lazy等屬性並設置到BeanDefinition中
                AnnotationConfigUtils.processCommonDefinitionAnnotations(
                    (AnnotatedBeanDefinition) candidate);
            }
            // 檢查BeanDefinition
            // 主要檢查這個容器中是否已經存在此BeanDefinition
            if (checkCandidate(beanName, candidate)) {
                BeanDefinitionHolder definitionHolder = 
                    new BeanDefinitionHolder(candidate, beanName);
                // 設置代理策略
                definitionHolder =AnnotationConfigUtils.applyScopedProxyMode(
                    scopeMetadata, definitionHolder, this.registry);
                beanDefinitions.add(definitionHolder);
                // 注冊到Spring容器中
                registerBeanDefinition(definitionHolder, this.registry);
            }
        }
    }
    return beanDefinitions;
}

因此如果我們需要自定義掃描實現bean的注冊,基本上就是要繼承ClassPathBeanDefinitionScanner並且重寫doScan方法了。大致框架就是

public class MyScanner extends ClassPathBeanDefinitionScanner {
    
    public MyScanner(BeanDefinitionRegistry registry) {
        super(registry)
    }
    
    @Overide
    protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
        // 父類已經將這些bean注冊了
        Set<BeanDefinitionHolder> holders = super.doScan(basePackages);
        for(BeanDefinitionHolder holder : holders) {
            // 在這里修改BeanDefinition引用的對象即可
        }
        return holders;
    }
}

例子

本小節將會實現一個將指定包下的帶有@Mapper標記的接口生成代理注冊到Spring容器中,命名將與MyBatis-Spring一致。

MapperFactoryBean

此類作用用來生成接口的代理實現

public class MapperFactoryBean<T> implements FactoryBean<T>, InitializingBean {

    /**
     * 代理的接口
     */
    private Class<T> mapperInterface;

    /**
     * 代理對象
     */
    private T mapperObject;

    public MapperFactoryBean(Class<T> mapperInterface) {
        if(mapperInterface == null || !mapperInterface.isInterface()) {
            throw new IllegalArgumentException(mapperInterface + " 
                                               must be a interface.");
        }
        this.mapperInterface = mapperInterface;
    }

    @SuppressWarnings("unchecked")
    private T createProxy() {
        return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(),
                                          new Class<?>[]{mapperInterface}, 
                                          new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) {
                System.out.println(method.getName() + " is called.");
                // 注意代理方法的返回值不能是基本類型, 否則返回null會發生空指針異常.
                return null;
            }                                     
        });
    }
    /**
     * 此方法會在Spring創建對象后, 並且調用了所有配置文件中配置屬性的setter方法后執行
     * 在這里我們檢查下參數以及創建接口代理對象
     */
    @Override
    public void afterPropertiesSet() {
        // 檢查參數
        if(mapperInterface == null || !mapperInterface.isInterface()) {
            throw new IllegalArgumentException(mapperInterface + " 
                                               must be a interface.");
        }
        if(mapperObject == null) {
            mapperObject = createProxy();
        }
    }

    @Override
    public T getObject() {
        if(mapperObject == null) {
            mapperObject = createProxy();
        }
        return mapperObject;
    }

    @Override
    public Class<?> getObjectType() {
        return mapperInterface;
    }

    @Override
    public boolean isSingleton() {
        return true;
    }
}

@Mapper

@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Mapper {
    String value() default "";
}

MapperAnnotationBeanNameGenerator

此類用來生成bean的名字

/**
 * bean name生成器
 * 繼承自AnnotationBeanNameGenerator
 * AnnotationBeanNameGenerator會解析Componnet注解或者其衍生注解(@Service、@Repository等)的
 * value屬性
 * 獲取名字, 如果沒有顯示指定value屬性, 那么會生成一個默認名字, 即類名首字母小寫后的名字
 * 現在想要實現解析指定注解的value屬性而不是Component注解
 * 如果value屬性沒有指定, 那么采用默認策略
 */
public class MapperAnnotationBeanNameGenerator extends AnnotationBeanNameGenerator {

    /**
     * 以這個注解指定的名字來生成bean name
     */
    private Class<? extends Annotation> annotation;

    public MapperAnnotationBeanNameGenerator(Class<? extends Annotation> annotation) {
        this.annotation = annotation;
    }

    /*
     * 所有注解信息都是通過.class文件解析出來的, 基於ASM實現, 也意味着這個類還被有被加載
     */
    @Override
    public String generateBeanName(BeanDefinition definition, 
                                   BeanDefinitionRegistry registry) {
        return super.generateBeanName(definition, registry);
    }

    /**
     * 重寫此方法, 使得適配@Mapper注解指定的bean name.
     * 決定generateBeanName方法是否解析此annotationType代表的注解
     * 舉例:
     * 若 annotationType = org.springframework.stereotype.Service(@Service)
     * 那么 metaAnnotationTypes = [@Target, @Retention, @Documented, @Component]
     * 即標記annotationType這個注解的注解集合
     * 那么 attributes = {value: ''}
     * @param annotationType 想要解析的注解
     * @param metaAnnotationTypes 注解annotationType的注解集合
     * @param attributes annotationType注解的屬性集合
     */
    @Override
    protected boolean isStereotypeWithNameValue(String annotationType, 
                                                Set<String> metaAnnotationTypes, 
                                                Map<String, Object> attributes) {
        boolean isStereotype = (Objects.equals(annotationType, annotation.getName()))
                || (metaAnnotationTypes != null 
                    && metaAnnotationTypes.contains(annotation.getName()));
        return isStereotype && attributes.containsKey("value");
    }
}

MapperScannerConfigurer

提供可配置的掃描參數

public class MapperScannerConfigurer implements BeanDefinitionRegistryPostProcessor, InitializingBean {

    private static final Logger LOG = LoggerFactory.getLogger(
        MapperScannerConfigurer.class);

    private String basePackage;

    /**
     * 默認為Mapper注解
     */
    private Class<? extends Annotation> markedAnnotation = Mapper.class;

    /**
     * bean name生成器
     */
    private BeanNameGenerator beanNameGenerator;

    public void setBasePackage(String basePackage) {
        this.basePackage = basePackage;
    }

    public void setMarkedAnnotation(Class<? extends Annotation> markedAnnotation) {
        this.markedAnnotation = markedAnnotation;
    }

    public void setBeanNameGenerator(BeanNameGenerator beanNameGenerator) {
        this.beanNameGenerator = beanNameGenerator;
    }

    @Override
    public void afterPropertiesSet() {
        if(basePackage == null) {
            throw new IllegalArgumentException("the property 'basePackage' 
                                               can not be null!");
        }
    }

    /**
     * 掃描指定包, 並且將滿足掃描條件的的接口生成代理注冊
     */
    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) 
                                               throws BeansException {
        MapperScanner mapperScanner = new MapperScanner(registry);
        // 添加掃描條件, 默認只掃描@Mapper標記的類文件
        mapperScanner.addIncludeFilter(new AnnotationTypeFilter(markedAnnotation));
        if(beanNameGenerator == null) {
            beanNameGenerator = new MapperAnnotationBeanNameGenerator(markedAnnotation);
        }
        mapperScanner.setBeanNameGenerator(beanNameGenerator);
        // 開始掃描, 並注冊
        int beanCount = mapperScanner.scan(StringUtils.tokenizeToStringArray(
            basePackage, ",");
        LOG.debug("The count of registered bean is " + beanCount);
    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) 
                                           throws BeansException {
        // just do nothing.
    }
}

MapperScanner

public class MapperScanner extends ClassPathBeanDefinitionScanner {

    public MapperScanner(BeanDefinitionRegistry registry) {
        // 禁用掉默認的掃描選擇條件
        // 默認只掃描被@Component標記的類,
        // 當然了衍生注解(@Controller、@Service、@Repository)也會被掃描
        super(registry, false);
    }

    /**
     * 默認情況下只有頂層具體類才會通過
     * 只返回是接口的beanDefinition
     * @param beanDefinition bean
     * @return true / false
     */
    @Override
    protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
        return beanDefinition.getMetadata().isInterface() 
            && beanDefinition.getMetadata().isIndependent();
    }

    /**
     * 注冊掃描的類
     * @param basePackages 包數組, 形如{"com.wangtao"}
     * @return 返回實際注冊的bean數量
     */
    @Override
    public int scan(String... basePackages) {
        return super.scan(basePackages);
    }

    @Override
    protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
        // 父類方法已經向Spring容器中注冊過這些BeanDefinition了, 只需修改此引用
        // 不需要再注冊
        Set<BeanDefinitionHolder> holders = super.doScan(basePackages);
        for (BeanDefinitionHolder holder : holders) {
            convertToMapperFactoryBean(holder.getBeanDefinition());
        }
        return holders;
    }

    /**
     * 修改原有Mapper接口的beanDefinition, 將其轉化為MapperFactoryBean的beanDefinition
     * @param beanDefinition Mapper接口的beanDefinition
     */
    private void convertToMapperFactoryBean(BeanDefinition beanDefinition) {
        // 強轉
        GenericBeanDefinition mapperFactoryDefinition = 
            (GenericBeanDefinition) beanDefinition;
        // 只能拿到接口名, 不能拿到Class對象, 因為此時還沒有被類加載器加載
        String mapperInterfaceName = beanDefinition.getBeanClassName();
        mapperFactoryDefinition.setBeanClass(MapperFactoryBean.class);

        // 使用構造函數注入
        // 這里給的只是接口的完全限定名, 而不是Class對象, 因為Spring初始化的時候
        // 會自動將字符串轉化成對應的類型, 而對應這里將會使用的是ClassEditor轉化功能.
        // 之所以不用Class, 是因為對應Class文件此時還沒有被類加載器加載
        mapperFactoryDefinition.getConstructorArgumentValues()
            .addIndexedArgumentValue(0, mapperInterfaceName);
    }
}

測試

applicationContext.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd">

  <bean id="mapperScannerConfigurer"      class="com.wangtao.spring.scan.MapperScannerConfigurer">
    <property name="basePackage" value="com.wangtao.spring.scan.mapper" />
  </bean>
</beans>

com.wangtao.spring.scan.mapper包下的mapper接口

/** 頂層具體類 **/
public class AddressMapper {
}

/** 頂層具體類,使用@Mapper標記 **/
@Mapper
public class StudentMapper {
}

/** 接口,將會被掃描,bean name = userMapper **/
@Mapper
public interface UserMapper {
    void insert();
}

/** 接口,將會被掃描,bean name = orderMapperProxy **/
@Mapper("orderMapperProxy")
public interface OrderMapper {
    void update();
}
public class ScannerTest {
    @Test
    public void scan() {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext(
            "classpath:spring-scan.xml");
        String[] beanNames = applicationContext.getBeanDefinitionNames();
        UserMapper userMapper = applicationContext
            .getBean("userMapper", UserMapper.class);
        Assert.assertNotNull(userMapper);
        userMapper.insert();
        OrderMapper orderMapper = applicationContext
            .getBean("orderMapperProxy", OrderMapper.class);
        Assert.assertNotNull(orderMapper);
        orderMapper.update();
    }
}

總結

文本主要介紹了Spring中兩個非常重要的有關包掃描的類,最后給出一個可運行的例子來講解如何使用這兩個類來實現自定義的掃描器。從這個例子其實已經道出了MyBatis-Spring掃描Mapper接口的工作原理。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM