前言
Mybatis-Plus是一個 MyBatis增強工具包,簡化 CRUD 操作,在 MyBatis 的基礎上只做增強不做改變,為簡化開發、提高效率而生,號稱無侵入,現在開發中比較常用,包括我自己現在的項目中ORM框架除使用JPA就是他了。
我好奇的是他是如何實現單表的CRUD操作的?
不看源碼之前,其實我大致能猜一猜:因為他號稱零入侵,只做增強,那我們就能簡單的理解為他只是在上面做了一層封裝類似於裝飾器模式,簡化了許多繁重的操作。
但是萬變不離其宗,他最后應該還是執行MyBatis里Mapper注冊MappedStatement這一套,所以他應該是內置了一套CRUD的SQL模板,根據不同的entity來生成對應的語句,然后注冊到Mapper中用來執行。
帶着猜想,我們具體跟下他的注冊流程。
1.MybatisPlusAutoConfiguration
Mybatis-Plus依托於spring,一切都是用的ioc這一套。創建SqlSessionFactory
從之前的SqlSessionFactoryBuilder
主動創建改成ioc來控制創建。具體我們看一代碼:
@Bean
@ConditionalOnMissingBean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
// TODO 使用 MybatisSqlSessionFactoryBean 而不是 SqlSessionFactoryBean
MybatisSqlSessionFactoryBean factory = new MybatisSqlSessionFactoryBean();
factory.setDataSource(dataSource);
factory.setVfs(SpringBootVFS.class);
if (StringUtils.hasText(this.properties.getConfigLocation())) {
factory.setConfigLocation(this.resourceLoader.getResource(this.properties.getConfigLocation()));
}
//初始化configuration
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);
}
//獲得mapper文件
Resource[] mapperLocations = this.properties.resolveMapperLocations();
if (!ObjectUtils.isEmpty(mapperLocations)) {
factory.setMapperLocations(mapperLocations);
}
// TODO 對源碼做了一定的修改(因為源碼適配了老舊的mybatis版本,但我們不需要適配)
Class<? extends LanguageDriver> defaultLanguageDriver = this.properties.getDefaultScriptingLanguageDriver();
if (!ObjectUtils.isEmpty(this.languageDrivers)) {
factory.setScriptingLanguageDrivers(this.languageDrivers);
}
Optional.ofNullable(defaultLanguageDriver).ifPresent(factory::setDefaultScriptingLanguageDriver);
// TODO 自定義枚舉包
if (StringUtils.hasLength(this.properties.getTypeEnumsPackage())) {
factory.setTypeEnumsPackage(this.properties.getTypeEnumsPackage());
}
// TODO 此處必為非 NULL
GlobalConfig globalConfig = this.properties.getGlobalConfig();
// TODO 注入填充器
this.getBeanThen(MetaObjectHandler.class, globalConfig::setMetaObjectHandler);
// TODO 注入主鍵生成器
this.getBeanThen(IKeyGenerator.class, i -> globalConfig.getDbConfig().setKeyGenerator(i));
// TODO 注入sql注入器
this.getBeanThen(ISqlInjector.class, globalConfig::setSqlInjector);
// TODO 注入ID生成器
this.getBeanThen(IdentifierGenerator.class, globalConfig::setIdentifierGenerator);
// TODO 設置 GlobalConfig 到 MybatisSqlSessionFactoryBean
factory.setGlobalConfig(globalConfig);
return factory.getObject();
}
代碼比較簡單,再加上是國人開發的框架,在關鍵節點上有一定的注釋,所以看上去還算是輕松加愉快。這個方法基本上就是MybatisSqlSessionFactoryBean的初始化操作。
我們主要是看Mapper的生成,所以其它的放一旁,所以我們基本最在意的應該是注入sql注入器this.getBeanThen(ISqlInjector.class, globalConfig::setSqlInjector)
。
2.ISqlInjector(SQL自動注入器接口)
public interface ISqlInjector {
/**
* 檢查SQL是否注入(已經注入過不再注入)
*
* @param builderAssistant mapper 信息
* @param mapperClass mapper 接口的 class 對象
*/
void inspectInject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass);
}
public abstract class AbstractSqlInjector implements ISqlInjector {
private static final Log logger = LogFactory.getLog(AbstractSqlInjector.class);
@Override
public void inspectInject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass) {
Class<?> modelClass = extractModelClass(mapperClass);
if (modelClass != null) {
String className = mapperClass.toString();
Set<String> mapperRegistryCache = GlobalConfigUtils.getMapperRegistryCache(builderAssistant.getConfiguration());
if (!mapperRegistryCache.contains(className)) {
//獲得CRUD一系列的操作方法
List<AbstractMethod> methodList = this.getMethodList(mapperClass);
if (CollectionUtils.isNotEmpty(methodList)) {
//取得對應TableEntity
TableInfo tableInfo = TableInfoHelper.initTableInfo(builderAssistant, modelClass);
// 循環注入自定義方法
methodList.forEach(m -> m.inject(builderAssistant, mapperClass, modelClass, tableInfo));
} else {
logger.debug(mapperClass.toString() + ", No effective injection method was found.");
}
mapperRegistryCache.add(className);
}
}
}
/**
* SQL 默認注入器
*
* @author hubin
* @since 2018-04-10
*/
public class DefaultSqlInjector extends AbstractSqlInjector {
@Override
public List<AbstractMethod> getMethodList(Class<?> mapperClass) {
return Stream.of(
new Insert(),
new Delete(),
new DeleteByMap(),
new DeleteById(),
new DeleteBatchByIds(),
new Update(),
new UpdateById(),
new SelectById(),
new SelectBatchByIds(),
new SelectByMap(),
new SelectOne(),
new SelectCount(),
new SelectMaps(),
new SelectMapsPage(),
new SelectObjs(),
new SelectList(),
new SelectPage()
).collect(toList());
}
}
ISqlInjector
接口只有一個inspectInject方法來提供SQL注入的操作,在AbstractSqlInjector
抽象類來提供具體的操作,最終對外的默認實現類是DefaultSqlInjector。
看到這,通過上面的注釋,先是不是跟我們最開始的猜想已經有點眉目了?
我們簡單看下SelectOne
操作。
public class SelectOne extends AbstractMethod {
@Override
public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
SqlMethod sqlMethod = SqlMethod.SELECT_ONE;
SqlSource sqlSource = languageDriver.createSqlSource(configuration, String.format(sqlMethod.getSql(),
sqlFirst(), sqlSelectColumns(tableInfo, true), tableInfo.getTableName(),
sqlWhereEntityWrapper(true, tableInfo), sqlComment()), modelClass);
return this.addSelectMappedStatementForTable(mapperClass, getMethod(sqlMethod), sqlSource, tableInfo);
}
}
上面就是具體生成MappedStatement的地方,細節就不說了,其實追蹤到最后都是一跟之前篇章的分析是一樣的。
我們主要是看SqlMethod.SELECT_ONE
就是框架中自定義SQL的地方。我們打開SqlMethod就可以看到全部的SQL語句。
其實看到這,我們就大概了解了整個單表CRUD生成的方法,其實如果我們想要實現自己的類似的自定義SQL,就可以實現AbstractSqlInjector
抽象類。
生成自己的DefaultSqlInjector,然后在仿照框架的寫法,實現自己的injectMappedStatement
方法,這樣就可以了。
3.inspectInject的調用
分析完上面的重頭戲,我們正常還是要看下inspectInject
在哪被調用的,直接跟跟蹤下代碼,我們就能輕易的追蹤到代碼調用的地方,MybatisConfiguration
中addMapper
的時候會調用。
我們直接跳到調用的地方。
而調用addMapper
的地方,第一個我們很容易找到,就是在buildSqlSessionFactory
里解析mapperLocations的時候。這一塊的代碼基本上就是之前的xml解析這一套,跟之前的mybatis解析是差不多的,
所以就不累述了。
...
if (this.mapperLocations != null) {
if (this.mapperLocations.length == 0) {
LOGGER.warn(() -> "Property 'mapperLocations' was specified but matching resources are not found.");
} else {
for (Resource mapperLocation : this.mapperLocations) {
if (mapperLocation == null) {
continue;
}
try {
XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(),
targetConfiguration, mapperLocation.toString(), targetConfiguration.getSqlFragments());
xmlMapperBuilder.parse();
} catch (Exception e) {
throw new NestedIOException("Failed to parse mapping resource: '" + mapperLocation + "'", e);
} finally {
ErrorContext.instance().reset();
}
LOGGER.debug(() -> "Parsed mapper file: '" + mapperLocation + "'");
}
}
...
重頭戲到了,xml的解析這一套我們都找到了,那那些沒有配置xml的mapper接口呢?他是如何注冊的?
4.MybatisPlusAutoConfiguration
其實通過打斷點,我們是能找到調用addMapper的地方,就在MapperFactoryBean
中的checkDaoConfig
方法中。
當時就懵逼了,mapper接口是怎么變成MapperFactoryBean
,FactoryBean
用來spring里用來bean封裝這一套我們是理解的,關鍵是我們的mapper接口在哪進行轉換的呢?
首先分析下我們的Mapper接口是怎么被發現的?這么一想,我就立刻想到了在啟動類上的@MapperScan(basePackages = {"com.xx.dao"})
,這個@MapperScan
注解就是掃描對應包下面的mapper進行注冊的。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(MapperScannerRegistrar.class)
@Repeatable(MapperScans.class)
public @interface MapperScan {
...
打開我們就發現了MapperScannerRegistrar類,它實現了ImportBeanDefinitionRegistrar
接口,在registerBeanDefinitions
方法中進行手動注冊bean的操作
void registerBeanDefinitions(AnnotationMetadata annoMeta, AnnotationAttributes annoAttrs,
BeanDefinitionRegistry registry, String beanName) {
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class);
builder.addPropertyValue("processPropertyPlaceHolders", true);
...
...
builder.addPropertyValue("basePackage", StringUtils.collectionToCommaDelimitedString(basePackages));
registry.registerBeanDefinition(beanName, builder.getBeanDefinition());
}
終於找到了根源了: MapperScannerConfigurer。
我們看下這個類的注釋:BeanDefinitionRegistryPostProcessor從基包開始遞歸搜索接口,並將其注冊為MapperFactoryBean 。 請注意,只有具有至少一種方法的接口才會被注冊; 具體的類將被忽略。
從上面這段話,我們就大致知道他的作用,他實現了BeanDefinitionRegistryPostProcessor
接口,在postProcessBeanDefinitionRegistry
里對所有的package下掃描到的未實例但已注冊的bean進行封裝處理。具體我們看下代碼:
@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));
}
postProcessBeanDefinitionRegistry
方法里注冊了一個ClassPathBeanDefinitionScanner,一個掃描器。它通過basePackage, annotationClass或markerInterface注冊markerInterface 。 如果指定了annotationClass和/或markerInterface ,則僅搜索指定的類型(將禁用搜索所有接口)。作用很明顯了,在我們這的作用就是通過basePackage來掃描包內的所有mapperbeans。
最后一步的scan操作,我們來看下操作。
最終再說下所有mapper注入的地方,在ServiceImpl
里: