MyBaits自動配置原理
- 2021 年 5 月 22 日
- 筆記
- 【框架】MyBatis, 【框架】SpringBoot, mybatis, spring, Spring Boot
前言
首先我們建立一個SpringBoot工程,導入mybatis-spring-boot-starter依賴。
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.4</version>
</dependency>
導入後發現這個依賴其實就是幫助我們導入了mybatis需要的依賴,其中和自動配置相關最重要的一個就是mybatis-spring-boot-autoconfigure
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
</dependency>
</dependencies>
MyBatis自動配置中是如何工作的
如上面分析自動配置的關鍵類我們就從mybatis-spring-boot-autoconfigure開始著手分析。
spring.factories
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.mybatis.spring.boot.autoconfigure.MybatisLanguageDriverAutoConfiguration,\
org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration
從spring.factories文件看到這裡通過SPI機制載入了兩個類
- MybatisAutoConfiguration
- MybatisLanguateDriverAutoConfiguration.
MybatisAutoConfiguration
//表示這是一個Spring配置類
@Configuration
//這個類需要在classpath中存在SqlSessionFactory和SqlSessionFactoryBean時才生效
@ConditionalOnClass({SqlSessionFactory.class, SqlSessionFactoryBean.class}) --
//這個類需要有一個DataSource的Canidate註冊到Spring容器中
@ConditionalOnSingleCandidate(DataSource.class)
//使MybatisProperties註解類生效
@EnableConfigurationProperties({MybatisProperties.class})
//需要在DataSourceAutoConfiguration和MybatisLanguageDriverAutoConfiguration自動配置之後執行
@AutoConfigureAfter({DataSourceAutoConfiguration.class, MybatisLanguageDriverAutoConfiguration.class})
public class MybatisAutoConfiguration implements InitializingBean {
}
MybatisAutoConfiguration#sqlSessionFactory
@Bean
@ConditionalOnMissingBean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
//創建一個SqlSessionFactoryBean, 在mybatis-spring項目下
SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
factory.setDataSource(dataSource);
factory.setVfs(SpringBootVFS.class);
if (StringUtils.hasText(this.properties.getConfigLocation())) {
factory.setConfigLocation(this.resourceLoader.getResource(this.properties.getConfigLocation()));
}
//應用Configuration對象
this.applyConfiguration(factory);
if (this.properties.getConfigurationProperties() != null) {
factory.setConfigurationProperties(this.properties.getConfigurationProperties());
}
if (!ObjectUtils.isEmpty(this.interceptors)) {
factory.setPlugins(this.interceptors);
}
if (this.databaseIdProvider != null) {
factory.setDatabaseIdProvider(this.databaseIdProvider);
}
if (StringUtils.hasLength(this.properties.getTypeAliasesPackage())) {
factory.setTypeAliasesPackage(this.properties.getTypeAliasesPackage());
}
if (this.properties.getTypeAliasesSuperType() != null) {
factory.setTypeAliasesSuperType(this.properties.getTypeAliasesSuperType());
}
if (StringUtils.hasLength(this.properties.getTypeHandlersPackage())) {
factory.setTypeHandlersPackage(this.properties.getTypeHandlersPackage());
}
if (!ObjectUtils.isEmpty(this.typeHandlers)) {
factory.setTypeHandlers(this.typeHandlers);
}
if (!ObjectUtils.isEmpty(this.properties.resolveMapperLocations())) {
factory.setMapperLocations(this.properties.resolveMapperLocations());
}
Set<String> factoryPropertyNames = (Set)Stream.of((new BeanWrapperImpl(SqlSessionFactoryBean.class)).getPropertyDescriptors()).map(FeatureDescriptor::getName).collect(Collectors.toSet());
Class<? extends LanguageDriver> defaultLanguageDriver = this.properties.getDefaultScriptingLanguageDriver();
if (factoryPropertyNames.contains("scriptingLanguageDrivers") && !ObjectUtils.isEmpty(this.languageDrivers)) {
factory.setScriptingLanguageDrivers(this.languageDrivers);
if (defaultLanguageDriver == null && this.languageDrivers.length == 1) {
defaultLanguageDriver = this.languageDrivers[0].getClass();
}
}
//設置默認的語言驅動類
if (factoryPropertyNames.contains("defaultScriptingLanguageDriver")) {
factory.setDefaultScriptingLanguageDriver(defaultLanguageDriver);
}
//這裡默認會返回一個DefaultSqlSessionFactory對象
return factory.getObject();
}
MybatisAutoConfiguration#sqlSessionTemplate
@Bean
@ConditionalOnMissingBean
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
ExecutorType executorType = this.properties.getExecutorType();
return executorType != null ? new SqlSessionTemplate(sqlSessionFactory, executorType) : new SqlSessionTemplate(sqlSessionFactory);
}
到這裡也就知道了MyBatis自動配置其實就是替我們完成了SqlSessionFactory和SqlSessionTempate的創建, 省去了自己導入相關依賴和配置相關Bean的麻煩.
MybatisLanguageDriverAutoConfiguration
這個類的配置是對各個語言的支援,比如Thymeleaf, Velocity,LegacyVelociy, FreeMarker等視圖組件的支援。
@Configuration
@ConditionalOnClass({LanguageDriver.class})
public class MybatisLanguageDriverAutoConfiguration {
private static final String CONFIGURATION_PROPERTY_PREFIX = "mybatis.scripting-language-driver";
public MybatisLanguageDriverAutoConfiguration() {
}
@Configuration
@ConditionalOnClass({ThymeleafLanguageDriver.class})
public static class ThymeleafConfiguration {
public ThymeleafConfiguration() {
}
}
@Configuration
@ConditionalOnClass({VelocityLanguageDriver.class, VelocityLanguageDriverConfig.class})
public static class VelocityConfiguration {
public VelocityConfiguration() {
}
}
@Configuration
@ConditionalOnClass({Driver.class})
@ConditionalOnMissingClass({"org.mybatis.scripting.velocity.VelocityLanguageDriverConfig"})
public static class LegacyVelocityConfiguration {
public LegacyVelocityConfiguration() {
}
}
@Configuration
@ConditionalOnClass({FreeMarkerLanguageDriver.class, FreeMarkerLanguageDriverConfig.class})
public static class FreeMarkerConfiguration {
public FreeMarkerConfiguration() {
}
}
@Configuration
@ConditionalOnClass({FreeMarkerLanguageDriver.class})
@ConditionalOnMissingClass({"org.mybatis.scripting.freemarker.FreeMarkerLanguageDriverConfig"})
public static class LegacyFreeMarkerConfiguration {
public LegacyFreeMarkerConfiguration() {
}
}
}
MybatisLanguageDriverAutoConfiguration類在org.mybatis.spring.boot.autoconfigure包下,我刪掉了內部靜態類下的程式碼,為了保持這個類看起來更直觀
自定義Mapper是如何被掃描的
業務開發中,我們是聲明介面(Mapper),那麼我們自定義的Mapper是如何被掃描的呢, 我們繼續順著MybatisAutoConfiguration程式碼分析,其內部包含了一個AutoConfiguredMapperScannerRegistrar的內部靜態類.
AutoConfiguredMapperScannerRegistrar
registerBeanDefinitions
public static class AutoConfiguredMapperScannerRegistrar
implements BeanFactoryAware, ImportBeanDefinitionRegistrar {
private BeanFactory beanFactory;
public AutoConfiguredMapperScannerRegistrar() {
}
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
if (!AutoConfigurationPackages.has(this.beanFactory)) {
} else {
//1.獲取到SpringBoot的基礎包路徑
List<String> packages = AutoConfigurationPackages.get(this.beanFactory);
//2.生成一個BeanDefinition的構造器,用於構建MapperScannerConfigurer的 //BeanDefinition
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class);
builder.addPropertyValue("processPropertyPlaceHolders", true);
//3.設置@Mapper註解的介面才會被當成Mapper介面
builder.addPropertyValue("annotationClass", Mapper.class);
builder.addPropertyValue("basePackage", StringUtils.collectionToCommaDelimitedString(packages));
BeanWrapper beanWrapper = new BeanWrapperImpl(MapperScannerConfigurer.class);
//4.獲取MapperScannerConfigurer的屬性名稱
Set<String> propertyNames = (Set)Stream.of(beanWrapper.getPropertyDescriptors()).map(FeatureDescriptor::getName).collect(Collectors.toSet());
if (propertyNames.contains("lazyInitialization")) {
builder.addPropertyValue("lazyInitialization", "${mybatis.lazy-initialization:false}");
}
if (propertyNames.contains("defaultScope")) {
builder.addPropertyValue("defaultScope", "${mybatis.mapper-default-scope:}");
}
//5.這裡添加一個MapperScannerConfigurer的BeanDefinition對象,也就是注入一個
//MapperScannerConfigurer對象
registry.registerBeanDefinition(MapperScannerConfigurer.class.getName(), builder.getBeanDefinition());
}
}
public void setBeanFactory(BeanFactory beanFactory) {
this.beanFactory = beanFactory;
}
}
AutoConfiguredMapperScannerRegistrar類是MybatisAutoConfiguration的內部靜態類,位於包org.mybatis.spring.boot.autoconfigure下。
可以看到這個類實現了ImportBeanDefinitionRegistrar介面,ImportBeanDefinitionRegistrar介面是Spring用來動態註冊Bean的,也就是會向Spring容器中注入一個BeanDefinition, 這個BeanDefinition就是MapperScannerConfigurer。
ImportBeanDefinitionRegistrar實現類只能通過其他類@Import的方式來載入,通常是配置類或者啟動類,所以MybatisAutoConfiguration類下還有一個內部類MapperScannerRegistrarNotFoundConfiguration如下。
@Configuration
@Import({MybatisAutoConfiguration.AutoConfiguredMapperScannerRegistrar.class})
@ConditionalOnMissingBean({MapperFactoryBean.class, MapperScannerConfigurer.class})
public static class MapperScannerRegistrarNotFoundConfiguration implements InitializingBean {
}
這個方法里最後會調用介面BeanDefinitionRegistry.registerBeanDefinition, beanName是”org.mybatis.spring.mapper.MapperScannerConfigurer”, registerBeanDefinition方法實際會調用DefaultListableBeanFactory.registerBeanDefinition。DefaultListableBeanFactory是BeanDefinitionRegistry介面的實現類。
public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFactory
implements ConfigurableListableBeanFactory,
BeanDefinitionRegistry,
Serializable {
}
AutoConfiguredMapperScannerRegistrar類和MapperScanner註解的作用是一樣的,如果你沒有通過以下三種配置方式掃描Mapper介面的包路徑
- 配置MapperScannerConfigurer掃描器類型的Spring Bean
- @Mapper註解
- <mybatis: scan/>標籤
那麼這裡就會通過AutoConfiguredMapperScannerRegistrar類添加一個MapperScannerConfigurer掃描器對象,去掃描SpringBoot包設置的基礎包路徑,也就是啟動類的同級目錄。 如果設置了@Mapper註解,則會當成Mapper介面解析,那麼這裡自動配置則不生效。
MapperScannerConfigurer
MapperScannerConfigurer
MapperScannerConfigurer實現了BeanDefinitionRegistryPostProcessor介面,BeanDefinitionRegistryPostProcessor 介面又繼承了BeanFactoryPostProcessor介面, 也就是說在MapperScannerConfigurer類里需要實現這兩個介面的方法。
public interface BeanDefinitionRegistryPostProcessor extends BeanFactoryPostProcessor {
void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry var1) throws BeansException;
}
@FunctionalInterface
public interface BeanFactoryPostProcessor {
void postProcessBeanFactory(ConfigurableListableBeanFactory var1) throws BeansException;
}
在MapperScannerConfigurer類里可以看到這裡只實現了postProcessBeanDefinitionRegistry。
BeanDefinitionRegistryPostProcessor
Spring里有兩個用來動態註冊Bean到容器中(BeanDefinitionRegistryPostProcessor和ImportBeanDefinitionRegistrar)。ImportBeanDefinitionRegistrar上文中有提到。
BeanDefinitionRegistryPostProcessor介面實現了BeanFactoryPostProcessor介面,是Spring框架的BeanDefinitionRegistry的後置處理器,用來註冊額外的BeanDefinition,postProcessBeanDefinitionRegistry方法會在所有的beanDefinition被載入了,但是所有的Bean還沒創建前調用。BeanDefinitionRegistryPostProcessor經常被用來註冊BeanFactoryPostProcessor的BeanDefinition。
postProcessBeanDefinitionRegistry
public class MapperScannerConfigurer implements BeanDefinitionRegistryPostProcessor, InitializingBean, ApplicationContextAware, BeanNameAware {
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
if (this.processPropertyPlaceHolders) {
this.processPropertyPlaceHolders();
}
ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
scanner.setAddToConfig(this.addToConfig);
scanner.setAnnotationClass(this.annotationClass);
scanner.setMarkerInterface(this.markerInterface);
scanner.setSqlSessionFactory(this.sqlSessionFactory);
scanner.setSqlSessionTemplate(this.sqlSessionTemplate);
scanner.setSqlSessionFactoryBeanName(this.sqlSessionFactoryBeanName);
scanner.setSqlSessionTemplateBeanName(this.sqlSessionTemplateBeanName);
scanner.setResourceLoader(this.applicationContext);
scanner.setBeanNameGenerator(this.nameGenerator);
scanner.setMapperFactoryBeanClass(this.mapperFactoryBeanClass);
if (StringUtils.hasText(this.lazyInitialization)) {
scanner.setLazyInitialization(Boolean.valueOf(this.lazyInitialization));
}
if (StringUtils.hasText(this.defaultScope)) {
scanner.setDefaultScope(this.defaultScope);
}
scanner.registerFilters();
scanner.scan(StringUtils.tokenizeToStringArray(this.basePackage, ",; \t\n"));
}
}
MapperScannerConfigurer在包org.mybatis.spring.mapper下.
這裡會調用ClassPathMapperScanner.scan(),而ClassPathMapperScanner又繼承了ClassPathBeanDefinitionScanner,所以這裡scan()會調用ClassPathBeanDefinitionScanner.scan(), 而ClassPathBeanDefinitionScanner.scan() 第二句程式碼又調用了this.doScan(basePackages), this.doScan()又調用了ClassPathMapperScanner.doScan(), 而這個方法第一句程式碼又調用了super.doScan(basePackages),父子類來回互相調用,有點暈頭轉向的。
org.mybatis.spring.mapper.ClassPathMapperScanner
org.springframework.context.annotation.ClassPathBeanDefinitionScanner這個類在spring-context.jar
ClassPathMapperScanner
ClassPathBeanDefinitionScanner#scan
public int scan(String... basePackages) {
int beanCountAtScanStart = this.registry.getBeanDefinitionCount();
this.doScan(basePackages);
if (this.includeAnnotationConfig) {
AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry);
}
return this.registry.getBeanDefinitionCount() - beanCountAtScanStart;
}
ClassPathMapperScanner#doScan()
這個方法里在mybatis自動配置算比較重要的一個方法,也就是幫助我們自動配置MapperFactoryBean, 會把根據basePackage註冊進Spring容器的BeanDefinition的beanClass設置成MapperFactoryBean。
public Set<BeanDefinitionHolder> doScan(String... basePackages) {
Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);
if (beanDefinitions.isEmpty()) {
LOGGER.warn(() -> {
return "No MyBatis mapper was found in '" + Arrays.toString(basePackages) + "' package. Please check your configuration.";
});
} else {
//這是一個關鍵方法,會處理已經註冊進Spring容器的beanDefinition,也就是會把
//已經註冊進Spring容器的beanDefinitiond的beanClass為MapperFactoryBean
this.processBeanDefinitions(beanDefinitions);
}
return beanDefinitions;
}
ClassPathBeanDefinitionScanner#doScan()
protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
Assert.notEmpty(basePackages, "At least one base package must be specified");
Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet();
String[] var3 = basePackages;
int var4 = basePackages.length;
for(int var5 = 0; var5 < var4; ++var5) {
String basePackage = var3[var5];
//這個方法會掃描指定basePackage下被@Mapper註解標註的介面
Set<BeanDefinition> candidates = this.findCandidateComponents(basePackage);
Iterator var8 = candidates.iterator();
while(var8.hasNext()) {
BeanDefinition candidate = (BeanDefinition)var8.next();
ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate);
candidate.setScope(scopeMetadata.getScopeName());
//這裡獲取beanName, 默認值是類名首字母小寫
String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry);
if (candidate instanceof AbstractBeanDefinition) {
this.postProcessBeanDefinition((AbstractBeanDefinition)candidate, beanName);
}
if (candidate instanceof AnnotatedBeanDefinition) {
AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition)candidate);
}
//檢查對應的Mapper介面是否被註冊進Spring容器中。
if (this.checkCandidate(beanName, candidate)) {
BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName);
definitionHolder = AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
beanDefinitions.add(definitionHolder);
this.registerBeanDefinition(definitionHolder, this.registry);
}
}
}
//這個集合返回以後 Spring容器會將裡面的所有內容註冊到容器中
return beanDefinitions;
}
ClassPathMapperScanner#processBeanDefinitions
private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
BeanDefinitionRegistry registry = this.getRegistry();
Iterator var4 = beanDefinitions.iterator();
while(var4.hasNext()) {
BeanDefinitionHolder holder = (BeanDefinitionHolder)var4.next();
AbstractBeanDefinition definition = (AbstractBeanDefinition)holder.getBeanDefinition();
boolean scopedProxy = false;
if (ScopedProxyFactoryBean.class.getName().equals(definition.getBeanClassName())) {
definition = (AbstractBeanDefinition)Optional.ofNullable(((RootBeanDefinition)definition).getDecoratedDefinition()).map(BeanDefinitionHolder::getBeanDefinition).orElseThrow(() -> {
return new IllegalStateException("The target bean definition of scoped proxy bean not found. Root bean definition[" + holder + "]");
});
scopedProxy = true;
}
String beanClassName = definition.getBeanClassName();
LOGGER.debug(() -> {
return "Creating MapperFactoryBean with name '" + holder.getBeanName() + "' and '" + beanClassName + "' mapperInterface";
});
definition.getConstructorArgumentValues().addGenericArgumentValue(beanClassName);
//1.這裡把普通介面設置成MapperFactoryBean
definition.setBeanClass(this.mapperFactoryBeanClass);
//2.是否把Mapper介面加入到Mybatis的Config當中去, 這裡設置為true
definition.getPropertyValues().add("addToConfig", this.addToConfig);
definition.setAttribute("factoryBeanObjectType", beanClassName);
boolean explicitFactoryUsed = false;
//2.從核心容器里獲取SqlSessionFactory賦值給MapperFactoryBean
if (StringUtils.hasText(this.sqlSessionFactoryBeanName)) {
definition.getPropertyValues().add("sqlSessionFactory", new RuntimeBeanReference(this.sqlSessionFactoryBeanName));
explicitFactoryUsed = true;
} else if (this.sqlSessionFactory != null) {
definition.getPropertyValues().add("sqlSessionFactory", this.sqlSessionFactory);
explicitFactoryUsed = true;
}
//3.從核心容器里獲取SqlSessionTemplate賦值給MapperFactoryBean
if (StringUtils.hasText(this.sqlSessionTemplateBeanName)) {
if (explicitFactoryUsed) {
LOGGER.warn(() -> {
return "Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored.";
});
}
definition.getPropertyValues().add("sqlSessionTemplate", new RuntimeBeanReference(this.sqlSessionTemplateBeanName));
explicitFactoryUsed = true;
} else if (this.sqlSessionTemplate != null) {
if (explicitFactoryUsed) {
LOGGER.warn(() -> {
return "Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored.";
});
}
definition.getPropertyValues().add("sqlSessionTemplate", this.sqlSessionTemplate);
explicitFactoryUsed = true;
}
if (!explicitFactoryUsed) {
LOGGER.debug(() -> {
return "Enabling autowire by type for MapperFactoryBean with name '" + holder.getBeanName() + "'.";
});
definition.setAutowireMode(2);
}
definition.setLazyInit(this.lazyInitialization);
if (!scopedProxy) {
if ("singleton".equals(definition.getScope()) && this.defaultScope != null) {
definition.setScope(this.defaultScope);
}
if (!definition.isSingleton()) {
BeanDefinitionHolder proxyHolder = ScopedProxyUtils.createScopedProxy(holder, registry, true);
if (registry.containsBeanDefinition(proxyHolder.getBeanName())) {
registry.removeBeanDefinition(proxyHolder.getBeanName());
}
registry.registerBeanDefinition(proxyHolder.getBeanName(), proxyHolder.getBeanDefinition());
}
}
}
}
再看spring.factories文件
上面只是完成了MyBatis 自動配置的工作,那麼這些自動配置是如何在SpringBoot啟動時執行呢? 還記得SpringBoot的EnableAutoConfiguration註解吧,它是利用SpringFactoriesLoader機制載入所有的AutoConfiguration類,所以我們需要把編寫好的自動配置類放在META-INF/spring.factories文件中,這也就是我們一開始分析的mybatis-spring-boot-starter-autoconfigure的文件結構的原因。
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.mybatis.spring.boot.autoconfigure.MybatisLanguageDriverAutoConfiguration,\
org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration
總結
- mybatis-spring-boot-starter將mybatis需要的依賴全部引入。
- starter通過SPI機制引入了一個配置的Class(MyBatisAutoConfiguration)。它負責註冊SqlSessionFactory和SqlSessionTemplate到Spring容器中,使用MyBatis開發時絕大部分功能要使用這兩個類來完成。
- 注入了AutoConfiguredMapperScannerRegistrar這個Bean到Spring容器,它負責將MapperScanner引入到Spring容器,然後MapperScanner會將工程中指定package下的Mapper轉化為BeanDefinition並且註冊到Spring容器中。
- 在開發中使用某個具體的Mapper時,Spring能夠從容器中找到這個Mapper對應的BeanDefinition,將其實例化並且注入,這樣開發者就可以使用了。
- SpringBoot+MyBatis自動配置涉及到Spring中兩個自動註冊Bean的關鍵介面(BeanDefinitionRegistryPostProcessor和ImportBeanDefinitionRegistrar),也是我們業務開發中需要重點關注的地方。
參考鏈接
//www.cnblogs.com/nullifier/p/11967659.html
//zhuanlan.zhihu.com/p/30123517
//www.cnblogs.com/daxin/p/3545040.html