前言
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
裡: