MybatisPlus 是一款在 Mybatis 基础上进行的增强 orm 框架,可以实现不写 sql 就完成数据库相关的操作。普通的 mapper 接口通过继承 BaseMapper 接口,即可获得增强,如下所示:
public interface UserMapper extends BaseMapper<User> {
}
复制代码
接下来就对其源码一探究竟,看看他到底是如何实现的
环境搭建
1、使用 h2 数据库,方便测试,导入相关依赖
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web:2.7.1'
implementation 'com.baomidou:mybatis-plus-boot-starter:3.5.3.1'
implementation 'org.projectlombok:lombok:1.18.24'
implementation 'com.h2database:h2:1.4.200'
}
复制代码
2、springboot 配置文件
spring:
datasource:
driver-class-name: org.h2.Driver
username: root
password: test
sql:
init:
schema-locations: classpath:db/schema-h2.sql
data-locations: classpath:db/data-h2.sql
复制代码
3、resources 目录下新建 db 目录,创建 sql 文件
schema-h2.sql
DROP TABLE IF EXISTS demo_user;
CREATE TABLE demo_user
(
id int primary key,
name varchar,
age int,
email varchar
);
复制代码
data-h2.sql
DELETE
FROM demo_user;
INSERT INTO demo_user (id, name, age, email)
VALUES (1, 'Jone', 18, '[email protected]'),
(2, 'Jack', 20, '[email protected]'),
(3, 'Tom', 28, '[email protected]'),
(4, 'Sandy', 21, '[email protected]'),
(5, 'Billie', 24, '[email protected]');
复制代码
4、编写 mapper 文件
public interface UserMapper extends BaseMapper<User> {
}
复制代码
5、启动测试
@MapperScan("org.example.mapper")
@SpringBootApplication
public class Main {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(Main.class, args);
UserMapper userMapper = context.getBean(UserMapper.class);
System.out.println(userMapper.selectList(null));
}
}
复制代码
结果如下
[User(id=1, name=Jone, age=18, [email protected]), User(id=2, name=Jack, age=20, [email protected]), User(id=3, name=Tom, age=28, [email protected]), User(id=4, name=Sandy, age=21, [email protected]), User(id=5, name=Billie, age=24, [email protected])]
复制代码
从 @MapperScan 入手
@MapperScan 注解的作用是扫描指定 mapper 接口所在的包,并生成接口的代理对象,注入到 ioc 容器中,接口定义如下
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(MapperScannerRegistrar.class)
@Repeatable(MapperScans.class)
public @interface MapperScan {
}
复制代码
可以看到 Import 了个 MapperScannerRegistrar,点进去看看这个类做了什么
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
AnnotationAttributes mapperScanAttrs = AnnotationAttributes
.fromMap(importingClassMetadata.getAnnotationAttributes(MapperScan.class.getName()));
if (mapperScanAttrs != null) {
// 注册一个 beanDefinition
registerBeanDefinitions(importingClassMetadata, mapperScanAttrs, registry,
generateBaseBeanName(importingClassMetadata, 0));
}
}
void registerBeanDefinitions(AnnotationMetadata annoMeta, AnnotationAttributes annoAttrs,
BeanDefinitionRegistry registry, String beanName) {
// 注册MapperScannerConfigurer的BeanDefinition
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class);
// ......
registry.registerBeanDefinition(beanName, builder.getBeanDefinition());
}
复制代码
这个 importRegister 注册了一个 MapperScannerConfigurer,这个类是个 BeanDefinitionRegistryPostProcessor,核心逻辑就是在这个类中,即扫描指定 mapper 接口所在的包,并生成接口的代理对象,注入到 ioc 容器中,查看该类的 postProcessBeanDefinitionRegistry() 方法
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
if (this.processPropertyPlaceHolders) {
processPropertyPlaceHolders();
}
ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
// 设置一些scanner参数
scanner.setSqlSessionTemplate(this.sqlSessionTemplate);
// ......
// 扫描mapper接口
scanner.scan(
StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
}
复制代码
进入父类 scan 方法,发现核心方法是子类的 doScan(), 来到 MapperScannerConfigurer.doScan()
@Override
public Set<BeanDefinitionHolder> doScan(String... basePackages) {
// 拿到扫描到的 beanDefinition
Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);
if (beanDefinitions.isEmpty()) {
LOGGER.warn(() -> "No MyBatis mapper was found in '" + Arrays.toString(basePackages)
+ "' package. Please check your configuration.");
} else {
// 处理 mapper beanDefinition
processBeanDefinitions(beanDefinitions);
}
return beanDefinitions;
}
复制代码
核心在 processBeanDefinitions(beanDefinitions) 中
private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
AbstractBeanDefinition definition;
BeanDefinitionRegistry registry = getRegistry();
for (BeanDefinitionHolder holder : beanDefinitions) {
definition = (AbstractBeanDefinition) holder.getBeanDefinition();
// 设置该BeanDefinition的beanClass是 MapperFactoryBean
definition.setBeanClass(this.mapperFactoryBeanClass);
// ......
// 设置该MapperFactoryBean 中的 sqlSessionTemplateBeanName
if (StringUtils.hasText(this.sqlSessionTemplateBeanName)) {
definition.getPropertyValues().add("sqlSessionTemplate",
new RuntimeBeanReference(this.sqlSessionTemplateBeanName));
explicitFactoryUsed = true;
}
// ......
}
}
复制代码
通过这一系列源码,可以知道,@MapperScan 指定的包在 MapperScannerConfigurer 被扫描成 BeanDefinition, 并且修改了 BeanDefinition 的 beanClass 属性为 MapperFactory,这样 spring 实例化 UserMapper 单例 bean 时,会生成对应的 MapperFactory
看看这个 MapperFactory 是什么鬼
public class MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T> {
@Override
public T getObject() throws Exception {
return getSqlSession().getMapper(this.mapperInterface);
}
public SqlSession getSqlSession() {
return this.sqlSessionTemplate;
}
}
复制代码
这个类是个 FactoryBean,那么它的 getObject() 方法就是调用 sqlSessionTemplate 的 getMapper() 方法获取代理对象,关于这个 getMapper() 方法的解析,可以参考我之前写的《Mybatis 通过接口实现 sql 执行原理解析 - 掘金 (juejin.cn)》
到这里,MapperFactory 生成的 bean 被放到了 ioc 容器中,结束了吗?我们忽略了 MapperFactory 的父类 SqlSessionDaoSupport,下面一节来看看这个父类 SqlSessionDaoSupport 做了什么
SqlSessionDaoSupport
这个类看名字是给 Dao 做支持的,Dao 指的就是那个 mapper 接口,做什么支持?其实给就是给 BaseMapper 里定义的方法生成对应的 Statemnet,注册到 MybatisMapperRegistry 中,这样调用 BaseMapper 方法时,代理类就会从 MybatisMapperRegistry 中找到 Statemnet,这样可以取出 sql 执行了,来看源码,其他都是抽象方法,只有一个初始化方法
@Override
public final void afterPropertiesSet() throws IllegalArgumentException, BeanInitializationException {
// 让子类处理
checkDaoConfig();
// Let concrete implementations initialize themselves.
try {
initDao();
}
catch (Exception ex) {
throw new BeanInitializationException("Initialization of DAO failed", ex);
}
}
复制代码
调用了抽象方法,子类实现了 checkDaoConfig(),来看下 MapperFactoryBean.checkDaoConfig()
protected void checkDaoConfig() {
super.checkDaoConfig();
Assert.notNull(this.mapperInterface, "Property 'mapperInterface' is required");
Configuration configuration = this.getSqlSession().getConfiguration();
if (this.addToConfig && !configuration.hasMapper(this.mapperInterface)) {
try {
// 解析这个 mapper 方法
configuration.addMapper(this.mapperInterface);
} catch (Exception var6) {
this.logger.error("Error while adding the mapper '" + this.mapperInterface + "' to configuration.", var6);
throw new IllegalArgumentException(var6);
} finally {
ErrorContext.instance().reset();
}
}
}
复制代码
看到 configuration.addMapper(this.mapperInterface) 方法,相信看过 mybatis 源码的小伙伴们已经知道要干什么了吧。就是解析这个 mapper 类方法,找到对应的 sql,并封装成 statemnet,下面看看这个 configuration.addMapper(this.mapperInterface) 的实现逻辑吧
MybatisConfiguration.addMapper()
因为是 MybatisPlus,所以源码内部的 Configuration 类是 MybatisConfiguration,查看他的 addMapper() 方法源码
@Override
public <T> void addMapper(Class<T> type) {
mybatisMapperRegistry.addMapper(type);
}
复制代码
再进入 mybatisMapperRegistry.addMapper(type) 源码
@Override
public <T> void addMapper(Class<T> type) {
if (type.isInterface()) {
if (hasMapper(type)) {
// TODO 如果之前注入 直接返回
return;
}
boolean loadCompleted = false;
try {
// TODO 注册mapper类对应的代理工厂类,用于生成代理对象
knownMappers.put(type, new MybatisMapperProxyFactory<>(type));
MybatisMapperAnnotationBuilder parser = new MybatisMapperAnnotationBuilder(config, type);
// 解析mapper类,生成 statement
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
复制代码
进入 parse() 方法查看
@Override
public void parse() {
String resource = type.toString();
if (!configuration.isResourceLoaded(resource)) {
// 解析mapper.xml
loadXmlResource();
configuration.addLoadedResource(resource);
String mapperName = type.getName();
assistant.setCurrentNamespace(mapperName);
// 解析缓存
parseCache();
parseCacheRef();
IgnoreStrategy ignoreStrategy = InterceptorIgnoreHelper.initSqlParserInfoCache(type);
for (Method method : type.getMethods()) {
if (!canHaveStatement(method)) {
continue;
}
if (getAnnotationWrapper(method, false, Select.class, SelectProvider.class).isPresent()
&& method.getAnnotation(ResultMap.class) == null) {
parseResultMap(method);
}
try {
// TODO 加入 注解过滤缓存
InterceptorIgnoreHelper.initSqlParserInfoCache(ignoreStrategy, mapperName, method);
parseStatement(method);
} catch (IncompleteElementException e) {
// TODO 使用 MybatisMethodResolver 而不是 MethodResolver
configuration.addIncompleteMethod(new MybatisMethodResolver(this, method));
}
}
// TODO 注入 CURD 动态 SQL , 放在在最后, because 可能会有人会用注解重写sql
try {
// https://github.com/baomidou/mybatis-plus/issues/3038
if (GlobalConfigUtils.isSupperMapperChildren(configuration, type)) {
parserInjector();
}
} catch (IncompleteElementException e) {
configuration.addIncompleteMethod(new InjectorResolver(this));
}
}
parsePendingMethods();
}
复制代码
关注最后注释,注入 CRUD 动态 SQL,其实就是给 BaseMapper 里的方法创建对应的 Statement,查看内部逻辑:
void parserInjector() {
// DefaultSqlInjector.inspectInject();
GlobalConfigUtils.getSqlInjector(configuration).inspectInject(assistant, type);
}
复制代码
这里先获取到默认的 Sql 注入器 DefaultSqlInjector,再调用其 inspectInject() 方法注入 sql
@Override
public void inspectInject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass) {
Class<?> modelClass = ReflectionKit.getSuperClassGenericType(mapperClass, Mapper.class, 0);
if (modelClass != null) {
String className = mapperClass.toString();
Set<String> mapperRegistryCache = GlobalConfigUtils.getMapperRegistryCache(builderAssistant.getConfiguration());
if (!mapperRegistryCache.contains(className)) {
// 根据实体类,根据注解解析出表的信息
TableInfo tableInfo = TableInfoHelper.initTableInfo(builderAssistant, modelClass);
// 拿到所有的AbstractMethod实现类
List<AbstractMethod> methodList = this.getMethodList(mapperClass, tableInfo);
if (CollectionUtils.isNotEmpty(methodList)) {
// 循环注入自定义方法
methodList.forEach(m -> m.inject(builderAssistant, mapperClass, modelClass, tableInfo));
} else {
logger.debug(mapperClass.toString() + ", No effective injection method was found.");
}
mapperRegistryCache.add(className);
}
}
}
复制代码
这里面的 AbstractMethod 的实现类有很多,如下
可以说,BaseMapper 中每个方法都有一个对应的 AbstractMethod 实现类,以 selectList() 为例,可以找到 SelectList 类
在下面循环注入的地方:methodList.forEach(m -> m.inject(builderAssistant, mapperClass, modelClass, tableInfo))
, 进入 AbstractMethod.inject() 方法
public void inject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
this.configuration = builderAssistant.getConfiguration();
this.builderAssistant = builderAssistant;
this.languageDriver = configuration.getDefaultScriptingLanguageInstance();
/* 注入自定义方法 */
injectMappedStatement(mapperClass, modelClass, tableInfo);
}
复制代码
子类实现了 injectMappedStatement 方法,还是以 SelectList 为例
@Override
public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
// selectList sql 模版
SqlMethod sqlMethod = SqlMethod.SELECT_LIST;
// 格式化sql
String sql = String.format(sqlMethod.getSql(), sqlFirst(), sqlSelectColumns(tableInfo, true), tableInfo.getTableName(),
sqlWhereEntityWrapper(true, tableInfo), sqlOrderBy(tableInfo), sqlComment());
// 封装成 sqlSource
SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, modelClass);
// 注册 mapperStatement
return this.addSelectMappedStatementForTable(mapperClass, methodName, sqlSource, tableInfo);
}
复制代码
其中 sqlSelectColumns(tableInfo, true) 方法是构造出 select 的所有列名,并加上动态sql标签
<choose>
<when test="ew != null and ew.sqlSelect != null">
${ew.sqlSelect}
</when>
<otherwise>id,name,age,email</otherwise>
</choose>
复制代码
其中 sqlWhereEntityWrapper(true, tableInfo) 方法是构造出 where 后面的条件语句,并加上动态sql标签
<if test="ew != null">
<where>
<if test="ew.entity != null">
<if test="ew.entity.id != null">id=#{ew.entity.id}</if>
<if test="ew.entity['name'] != null"> AND name=#{ew.entity.name}</if>
<if test="ew.entity['age'] != null"> AND age=#{ew.entity.age}</if>
<if test="ew.entity['email'] != null"> AND email=#{ew.entity.email}</if>
</if>
<if test="ew.sqlSegment != null and ew.sqlSegment != '' and ew.nonEmptyOfWhere">
<if test="ew.nonEmptyOfEntity and ew.nonEmptyOfNormal"> AND</if> ${ew.sqlSegment}
</if>
</where>
<if test="ew.sqlSegment != null and ew.sqlSegment != '' and ew.emptyOfWhere">
${ew.sqlSegment}
</if>
</if>
复制代码
最后 format 后的 sql 语句是
<script>
<if test="ew != null and ew.sqlFirst != null">
${ew.sqlFirst}
</if> SELECT <choose>
<when test="ew != null and ew.sqlSelect != null">
${ew.sqlSelect}
</when>
<otherwise>id,name,age,email</otherwise>
</choose> FROM demo_user
<if test="ew != null">
<where>
<if test="ew.entity != null">
<if test="ew.entity.id != null">id=#{ew.entity.id}</if>
<if test="ew.entity['name'] != null"> AND name=#{ew.entity.name}</if>
<if test="ew.entity['age'] != null"> AND age=#{ew.entity.age}</if>
<if test="ew.entity['email'] != null"> AND email=#{ew.entity.email}</if>
</if>
<if test="ew.sqlSegment != null and ew.sqlSegment != '' and ew.nonEmptyOfWhere">
<if test="ew.nonEmptyOfEntity and ew.nonEmptyOfNormal"> AND</if> ${ew.sqlSegment}
</if>
</where>
<if test="ew.sqlSegment != null and ew.sqlSegment != '' and ew.emptyOfWhere">
${ew.sqlSegment}
</if>
</if> <if test="ew != null and ew.sqlComment != null">
${ew.sqlComment}
</if>
</script>
复制代码
最后是把 sql 封装成了 SqlSource,并构造 MapperStatement 存入 configuration.mappedStatements 中,后面 mapper 调用 selectList 方法时,会从 mappedStatements 中找到对应的 statement,并取出 sql 语句执行,就能拿到数据了
小结
到此,MybatisPlus BaseMapper 实现对数据库增删改查源码解析完毕,相信通过源码的阅读能对 mybatisPlus 有更深的了解