Spring自定義類掃描器 ClassPathScanningCandidateComponentProvider Spring生命周期 Constructor > @PostConstruct > InitializingBean > init-method


Spring生命周期 Constructor > @PostConstruct > InitializingBean > init-method

Spring 容器中的 Bean 是有生命周期的,Spring 允許在 Bean 在初始化完成后以及 Bean 銷毀前執行特定的操作,常用的設定方式有以下三種:

(1) 通過實現 InitializingBean/DisposableBean 接口來定制初始化之后/銷毀之前的操作方法;
(2) 通過 <bean> 元素的 init-method/destroy-method屬性指定初始化之后 /銷毀之前調用的操作方法;
(3) 在指定方法上加上@PostConstruct 或@PreDestroy注解來制定該方法是在初始化之后還是銷毀之前調用。 

具體參考:Spring生命周期 Constructor > @PostConstruct > InitializingBean > init-method

項目中有個需求 讀取xml文件,然后 對xml文件進行解析,比如如果是 Gender=0/1的話,分別代表男女。

所以需要在構造函數之后,初始化bean之前進行過濾解析

xml文件:

 <interface name="網約車乘客基本信息(CKJB)" message="baseInfoPassenger">
        <field name="interfaceType" valExpr="BASIC" desc="接口類型標識" serialize="false"/>
        <field name="command" valExpr="CKJB" serialize="false" desc="接口操作命令"/>
        <field name="symbol" valExpr="PassengerPhone"
               desc="唯一標識 公司標識(見2.6)加平台自編號(將根據唯一標識執行操作標識對應操作)"/>
        <field name="companyId" valExpr="MEITUANDACHE" desc="公司標識,與交通部一致。 見2.6"/>
        <field name="registerDate" valExpr="${RegisterDate}" must="false" dataType="long"
               desc="注冊時間 乘客在平台的注冊日期YYYYMMDD"/>
        <field name="passengerPhone" valExpr="${PassengerPhone}" desc="乘客電話 "/>
        <field name="passengerSex" valExpr="M_genderConvertToNumber(PassengerGender)" dataType="int"
               desc="乘客性別 見JT/T 697.7-2014中,與平台發送交通部一致。"/>
        <field name="state" valExpr="${State}" dataType="int" desc="狀態 0:有效 1:失效"/>
        <field name="flag" valExpr="${Flag}" dataType="int" desc="操作標識 1:新增 2:更新 3:刪除"/>
        <field name="updateTime" valExpr="${UpdateTime}" dataType="long"
               desc="更新時間 網約車平台完成數據更新的時間,格式YYYYMMDDHHMMSS"/>
    </interface>

注意里面的方法:valExpr="M_genderConvertToNumber(PassengerGender)"

 

可以繼承 InitializingBean 這個接口,然后重寫方法:

 
         
@Component
public class CityRepositoryImpl implements CityRepository, InitializingBean {

/**
* 模板方法的掃描路徑
*/
private static final String TEMPLATE_METHOD_SCAN_LOCATION = "com.sankuai";

/**
* 模板方法
*/
private static final Map<String, TemplateMethod> TEMPLATE_METHOD_MAP = new HashMap<>();

@Override
public void afterPropertiesSet() { synchronized (CityRepositoryImpl.class) { if (TEMPLATE_METHOD_MAP.size() == 0) { loadTemplateMethod(); } } }
}

其實方法:afterPropertiesSet 就是設置屬性,initializingBean 具體參考:Spring生命周期 Constructor > @PostConstruct > InitializingBean > init-method

然后在這方法里面實現了一個自己定義的類掃描類,只要是繼承TempleMethod.class 就 掃描出來

  /**
     * 加載模板方法(基於spring的掃描器)
     */
    private void loadTemplateMethod() {
        ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false);
        scanner.addIncludeFilter(new AssignableTypeFilter(TemplateMethod.class));
        for (BeanDefinition beanDefinition : scanner.findCandidateComponents(TEMPLATE_METHOD_SCAN_LOCATION)) {
            try {
                TemplateMethod templateMethod = (TemplateMethod) Class.forName(beanDefinition.getBeanClassName()).newInstance();

                String name = templateMethod.getMethodName();
                if (name == null || name.length() == 0) {
                    throw QcsFactErrorEnum.TEMPLATE_METHOD_ERROR.formException(String.format("模板方法:%s 名稱不能為空!", beanDefinition.getBeanClassName()));
                }

                /* 模板方法追加前綴標識 */
                String realName = "M_" + name;
                if (TEMPLATE_METHOD_MAP.containsKey(realName)) {
                    throw QcsFactErrorEnum.TEMPLATE_METHOD_ERROR.formException(String.format("模板方法:%s重復定義", name));
                }
                TEMPLATE_METHOD_MAP.put(realName, templateMethod);
            } catch (QcsFactException e) {
                throw e;
            } catch (Exception e) {
                throw QcsFactErrorEnum.TEMPLATE_METHOD_ERROR.formException(e, String.format("模板方法:%s加載失敗", beanDefinition.getBeanClassName()));
            }
        }

    }

 

然后方法:

public class GenderTempleMethod {
    /**
     * 字符型轉數字
     * 例如
     * 男 convert to 1
     * 女 convert to 2
     */
    public static final class GenderConvertNumber implements TemplateMethod {
        @Override
        public String getMethodName() {
            return "genderConvertToNumber";
        }

        @Override
        public Object exec(List list) {
            String val = list.get(0).toString();
            Integer result = Gender.UNKNOWN.getValue();

            for (Gender gender : Gender.values()) {
                if (gender.getName().equals(val)) {
                    result = gender.getValue();
                    break;
                }
            }
            return result;
        }
    }

 

Gender類:

public enum Gender {
    /**
     * 男性
     */
    MALE("", 1),
    /**
     * 女性
     */
    FEMALE("", 2),
    /**
     * 未說明
     */
    UNKNOWN("未知", 0),
    /**
     * 未解釋
     */
    OTHER("其他", 9);

    private String name;
    private Integer value;

    Gender(String name, Integer value) {
        this.name = name;
        this.value = value;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getValue() {
        return value;
    }

    public void setValue(Integer value) {
        this.value = value;
    }
}

 

具體的spring 自定義掃描器的實現參考:

在我們剛開始接觸Spring的時候,要定義bean的話需要在xml中編寫,比如:

<bean id="myBean" class="your.pkg.YourClass"/>

 


后來發現如果bean比較多,會需要寫很多的bean標簽,太麻煩了。於是出現了一個component-scan注解。這個注解直接指定包名就可以,它會去掃描這個包下所有的class,然后判斷是否解析:

<context:component-scan base-package="your.pkg"/>

 


再后來,由於注解Annotation的流行,出現了@ComponentScan注解,作用跟component-scan標簽一樣,跟@Configuration注解配合使用:

@ComponentScan(basePackages = {"your.pkg", "other.pkg"})
public class Application { ... }

 


不論是component-scan標簽,還是@ComponentScan注解。它們掃描或解析的bean只能是Spring內部所定義的,比如@Component、@Service、@Controller或@Repository。如果有一些自定義的注解,比如@Consumer、這個注解修飾的類是不會被掃描到的。這個時候我們就得自定義掃描器完成這個操作。
 
Spring內置的掃描器
 
component-scan標簽底層使用ClassPathBeanDefinitionScanner這個類完成掃描工作的。@ComponentScan注解配合@Configuration注解使用,底層使用ComponentScanAnnotationParser解析器完成解析工作。

ComponentScanAnnotationParser解析器內部使用了ClassPathBeanDefinitionScanner掃描器,ClassPathBeanDefinitionScanner掃描器內部的處理過程整理如下:

1. 遍歷basePackages,根據每個basePackage找出這個包下的所有的class。比如basePackage為your/pkg,會找出your.pkg包下所有的class。找出之后封裝成Resource接口集合,這個Resource接口是Spring對資源的封裝,有FileSystemResource、ClassPathResource、UrlResource實現等
2. 遍歷找到的Resource集合,通過includeFilters和excludeFilters判斷是否解析。這里的includeFilters和excludeFilters是TypeFilter接口類型的集合,是ClassPathBeanDefinitionScanner內部的屬性。TypeFilter接口是一個用於判斷類型是否滿足要求的類型過濾器。excludeFilters中只要有一個TypeFilter滿足條件,這個Resource就會被過濾。includeFilters中只要有一個TypeFilter滿足條件,這個Resource就不會被過濾
3. 如果沒有被過濾。把Resource封裝成ScannedGenericBeanDefinition添加到BeanDefinition結果集中
4. 返回最后的BeanDefinition結果集
 
TypeFilter接口的定義:
 

public interface TypeFilter {
boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory)
throws IOException;
}

 


TypeFilter接口目前有AnnotationTypeFilter實現類(類是否有注解修飾)、RegexPatternTypeFilter(類名是否滿足正則表達式)等。

ClassPathBeanDefinitionScanner繼承ClassPathScanningCandidateComponentProvider類。

ClassPathScanningCandidateComponentProvider內部的構造函數提供了一個useDefaultFilters參數:
 

public ClassPathScanningCandidateComponentProvider(boolean useDefaultFilters) {
this(useDefaultFilters, new StandardEnvironment());
}

 


useDefaultFilters這個參數表示是否使用默認的TypeFilter,如果設置為true,會添加默認的TypeFilter:

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));
logger.debug("JSR-250 'javax.annotation.ManagedBean' found and supported for component scanning");
}
catch (ClassNotFoundException ex) {
// JSR-250 1.1 API (as included in Java EE 6) not available - simply skip.
}
try {
this.includeFilters.add(new AnnotationTypeFilter(
((Class<? extends Annotation>) ClassUtils.forName("javax.inject.Named", cl)), false));
logger.debug("JSR-330 'javax.inject.Named' annotation found and supported for component scanning");
}
catch (ClassNotFoundException ex) {
// JSR-330 API not available - simply skip.
}
}

 


我們看到這里includeFilters加上了AnnotationTypeFilter,並且對應的注解是@Component。@Service、@Controller或@Repository注解它們內部都是被@Component注解所修飾的,所以它們也會被識別。
 
自定義掃描功能
 
一般情況下,我們要自定義掃描功能的話,可以直接使用ClassPathScanningCandidateComponentProvider完成,加上一些自定義的TypeFilter即可。或者寫個自定義掃描器繼承ClassPathScanningCandidateComponentProvider,並在內部添加自定義的TypeFilter。后者相當於對前者的封裝。

我們就以一個簡單的例子說明一下自定義掃描的實現,直接使用ClassPathScanningCandidateComponentProvider。

項目結構如下:
./
└── spring
└── study
└── componentprovider
├── annotation
│   └── Consumer.java
├── bean
│   ├── ConsumerWithComponentAnnotation.java
│   ├── ConsumerWithConsumerAnnotation.java
│   ├── ConsumerWithInterface.java
│   ├── ConsumerWithNothing.java
│   └── ProducerWithInterface.java
└── interfaze
   ├── IConsumer.java
   └── IProducer.java
我們直接使用ClassPathScanningCandidateComponentProvider掃描spring.study.componentprovider.bean包下的class:

ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false); // 不使用默認的TypeFilter
provider.addIncludeFilter(new AnnotationTypeFilter(Consumer.class));
provider.addIncludeFilter(new AssignableTypeFilter(IConsumer.class));
Set<BeanDefinition> beanDefinitionSet = provider.findCandidateComponents("spring.study.componentprovider.bean");

 


這里掃描出來的類只有2個,分別是ConsumerWithConsumerAnnotation(被@Consumer注解修飾)和ConsumerWithInterface(實現了IConsumer接口)。ConsumerWithComponentAnnotation使用@Component注解,ConsumerWithNothing沒實現任何借口,沒使用任何注解,ProducerWithInterface實現了IProducer接口;所以這3個類不會被識別。

如果我們要自定義ComponentProvider,繼承ClassPathScanningCandidateComponentProvider類即可。

RepositoryComponentProvider這個類是SpringData模塊提供的,繼承自ClassPathScanningCandidateComponentProvider,主要是為了識別SpringData相關的類。

它內部定義了一些自定義TypeFilter,比如InterfaceTypeFilter(識別接口的TypeFilter,目標比較是個接口,而不是實現類)、AllTypeFilter(保存存儲TypeList集合,這個集合內部所有的TypeFilter必須全部滿足條件才能被識別)等。

參考:Spring自定義類掃描器


免責聲明!

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



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