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必須全部滿足條件才能被識別)等。