相信大家在使用SpringBoot的過程中,經常會使用到mybatis,通過使用mybatis-spring-boot-starter依賴進行自動配置,省去了自己依賴配置和Bean配置的很多麻煩。
有這么方便的starter,使大家不禁好奇,它究竟是怎么讓我們能夠不要任何配置就可以使用mybatis的,背后的原理究竟是什么?
本文將以mybatis-spring-boot-starter作為例子,探究它背后的秘密。
首先我們建立一個SpringBoot工程,並且添加mybatis-spring-boot-starter依賴(在此我們假設讀者已經熟悉SpringBoot基本知識和操作):
如果我們此時啟動程序,Mybatis會隨程序自動啟動。
starter減少了我們依賴配置和代碼中對象關系映射配置,我們分成兩部分分析:
第一步我們分析starter的依賴,從pom文件檢查工程的依賴:
<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>
可以看到,starter將需要的依賴全部引入,免去了我們配置的麻煩。
綜上所述,spring的starter免去了我們配置的麻煩,是個很好用的系統,全文完,謝謝觀看。
哈哈哈,如果文章寫到這里就結束,大家肯定要打我了(讀者內心:你寫的這什么東西)
光靠引入依賴包並不足以說明為什么這些組件就能自動被裝載,並且已經配置好了,所以我們分析下這些被引用的依賴,找到其中起作用的包。
首先mybatis,mybatis-spring都可以被忽略掉,這些就是正常的mybatis組件。
那可疑的就是這個mybatis-spring-boot-autoconfigure了。
直接打開這個jar包看里面的內容:
可疑的就是這個MybatisAutoConfiguration和spring.factories文件,其中spring.factories的內容:
# Auto Configure org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ org.mybatis.spring.boot.autoconfigure.MybatisLanguageDriverAutoConfiguration,\ org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration
首先百度一下spring.factories,這個是Spring的SPI機制,這個文件的內容也已經說的很明顯了,自動配置,連注釋都給帶上了,哈哈。
代碼通過SPI機制加載了兩個類,其中最重要的是:
org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration
我們打開這個類,查看其中代碼:
@org.springframework.context.annotation.Configuration @ConditionalOnClass({ SqlSessionFactory.class, SqlSessionFactoryBean.class }) @ConditionalOnSingleCandidate(DataSource.class) @EnableConfigurationProperties(MybatisProperties.class) @AutoConfigureAfter({ DataSourceAutoConfiguration.class, MybatisLanguageDriverAutoConfiguration.class }) public class MybatisAutoConfiguration implements InitializingBean { //......
可以看到,這是一個Spring配置類(因為有Configuration注解)
這個類需要在classpath中有SqlSessionFactory和SqlSessionFactoryBean定義時才會起效,並且要有一個DataSource的candidate注冊到spring容器中
當這些條件都滿足時,這個Configuration會注冊一個SqlSessionFactorySqlSessionTemplate :
@Bean @ConditionalOnMissingBean public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception { ...... @Bean @ConditionalOnMissingBean public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) { ......
而MyBatis整個數據庫訪問,都是圍繞這兩個對象展開的。
至此已經解開Mybatis自動配置的第一個問題了,還有第二個:我們定義的那些Dao是如何被掃描的?
請繼續在這個類中往下看,類中間包裹了一個static subclass:
/** * This will just scan the same base package as Spring Boot does. If you want more power, you can explicitly use * {@link org.mybatis.spring.annotation.MapperScan} but this will get typed mappers working correctly, out-of-the-box, * similar to using Spring Data JPA repositories. */ public static class AutoConfiguredMapperScannerRegistrar implements BeanFactoryAware, ImportBeanDefinitionRegistrar { private BeanFactory beanFactory; @Override public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { //...... registry.registerBeanDefinition(MapperScannerConfigurer.class.getName(), builder.getBeanDefinition()); }
這個類實現了ImportBeanDefinitionRegistrar,也就是會向Spring容器中注入一個bean定義,而這個bean定義就是MapperScannerConfigurer。
我們繼續看MapperScannerConfigurer:
public class MapperScannerConfigurer implements BeanDefinitionRegistryPostProcessor, InitializingBean, ApplicationContextAware, BeanNameAware { //......
標紅的部分已經很能說明問題了:這個類是BeanDefinitionRegistry的后處理器,可能會修改Spring容器中的BeanDefinition,果然,在下面我們就能找到對應的方法:
/** * {@inheritDoc} * * @since 1.0.2 */ @Override public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) { if (this.processPropertyPlaceHolders) { 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(lazyInitialization)) { scanner.setLazyInitialization(Boolean.valueOf(lazyInitialization)); } scanner.registerFilters(); scanner.scan( StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS)); }
其中最重要的部分就是這個標紅的scan,我們跟進去看下(其實不跟進去大家應該也看出所以然來了,這里有個basePackage,是不是很熟悉?):
/** * Perform a scan within the specified base packages, * returning the registered bean definitions. * <p>This method does <i>not</i> register an annotation config processor * but rather leaves this up to the caller. * @param basePackages the packages to check for annotated classes * @return set of beans registered if any for tooling registration purposes (never {@code null}) */ protected Set<BeanDefinitionHolder> doScan(String... basePackages) { Assert.notEmpty(basePackages, "At least one base package must be specified"); Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<>(); for (String basePackage : basePackages) { Set<BeanDefinition> candidates = findCandidateComponents(basePackage); for (BeanDefinition candidate : candidates) { ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate); candidate.setScope(scopeMetadata.getScopeName()); String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry); if (candidate instanceof AbstractBeanDefinition) { postProcessBeanDefinition((AbstractBeanDefinition) candidate, beanName); } if (candidate instanceof AnnotatedBeanDefinition) { AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate); } if (checkCandidate(beanName, candidate)) { BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName); definitionHolder = AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry); beanDefinitions.add(definitionHolder); registerBeanDefinition(definitionHolder, this.registry); } } } return beanDefinitions; }
這里就是從basePackages中掃描到所有合適的類(准確的說應該是接口),並且將這些類(接口)生成對應的BeanDefinition,這是為了讓Spring在實例化你定義的Dao時,能夠成功,如果沒有這一步,啟動肯定會報“Cannot find candidate for autowired property xxx”這類錯誤,因為你在工程里面聲明的本來就是個接口嘛。
並且請注意到最后面標紅的applyScopedProxyMode,這會為你的Dao生成代理,這也是為何你調試代碼時,調用到Dao時你把鼠標放上去,顯示的Dao類型是一個MapperProxy<T>。
至此,Mybatis自動加載的原理基本上可以總結如下:
1. mybatis-spring-boot-starter將mybatis需要的依賴全部引入
2. starter同時通過SPI機制引入了一個配置Class:MybatisAutoConfiguration,它負責注冊SqlSessionFactory和SqlSessionTemplate到Spring容器中,我們使用Mybatis時絕大部分功能靠這兩個Bean實現
3. 引入了AutoConfiguredMapperScannerRegistrar這個bean到Spring容器,它負責將MapperScanner引入Spring容器,然后MapperScanner會將工程中所有的Dao掃描出來轉換為BeanDefinition並且注冊到Spring容器中
4. 當開發的工程中使用了某個Dao時,Spring能夠從容器中找到這個Dao對應的BeanDefinition,將其實例化並且注入,這樣開發者就可以使用了,這也是為何我們只定義了Dao的接口,但是工程運行時能夠有實例Bean的原因