基本使用
引入pom:
如果是 springboot3 中必须手动指定mybatis-spring的版本 3.X, 否则无法启动。(默认引入的mybatis-spring为2.X版本),
xml
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.14</version>
<exclusions>
<exclusion>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>3.0.5</version>
</dependency>
创建实体类:
less
@Data
@TableName
public class Student {
@TableId(type = IdType.AUTO)
private Integer id;
private String stu_code;
private Integer age;
}
创建Mapper接口:
java
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface StudentMapper extends BaseMapper<Student> {
}
Springboot启动类添加:
@MapperScan(basePackageClasses = StudentMapper.class)
配置文件添加SQL日志打印:输出到控制台
yaml
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
controller 注入Mapper接口即可:
kotlin
@Autowired
private StudentMapper studentMapper;
@GetMapping("/test")
public List<Student> test() {
List<com.demo.boot2.mapper.Student> students =
studentMapper.selectList(null);
System.out.println(students);
return null;
}
MybatisPlusAutoConfiguration:
mybatisplus 核心自动装配类。 主要构造SqlSessionFactory、SqlSessionTemplate
SqlSessionFactory
MybatisSqlSessionFactory 替换mybatis中的SqlSessionFactoryBean。
主要进行解析一些核心配置:类型转换器、拦截器、SQL注入器、ID生成器等。


factory.getObject方法会调用 MybatisSqlSessionFactoryBean#afterPropertiesSet。 解析XML、Mapper接口,最终构建SqlSessionFactoryBean。
解析XML
XMLMapperBuilder#parse: 解析mybatis的Mapper 配置文件。如果没有配置则不会走这里的逻辑
一般配置路径为:*classpath:/mapper/**/*.xml**


-
configurationElement:
这里会解析XML 文件 生成MappedStatement, 记录到MybatisConfiguration#mappedStatements, 同一个id 记录两条entry。
XML 文件跟对应的Mapper接口不能同时定义二级缓存(没人这样干吧),因为最终保存到Configuration中,会发生覆盖冲突报错。

解析XML 创建SqlSource:

org.apache.ibatis.builder.xml.XMLStatementBuilder#parseStatementNode 最终生成MappedStatement:

bash

ini
最终生成MappedStatement:
-
bindMapperForNamespace:
解析当前XML的namespace 接口, 处理生成接口对应的Mapper对象


记录到MybatisMapperRegistry#knownMappers :

-
继续执行内部parse:
-
-
首先检查Mapper接口缓存相关定义:@CacheNamespace,@CacheNamespaceRef。

如果定义了相关注解,创建缓存对象。默认PerpetualCache, 添加到Configuration

-
-
-
然后检查接口方法是否有mybtis注解:进行替换BaseMapper的CRUD方法。 MybatisMapperAnnotationBuilder#statementAnnotationTypes

-
parseInjector:解析Entity为TableInfo对象, 同时注入Mybatis-plus 提供的默认CRUD 方法
以Insert 为例: 会构建SQL 执行脚本,生成sqlSource对象, 创建MappedStatement 对象
最后生成MappedStatement:
-
创建DefaultSqlSessionFactory
通过configuration创建DefaultSqlSessionFactory

构建SqlSessionTemplate
这里构建的ibatis中的SqlSessionTemplate


提供了一些常用的查询方法,都是通过sqlSessionProxy实现, 最终交给SqlSessionInterceptor


@MapperScan处理
import --> MapperScannerRegistrar:
注册beanDefinition: MapperScannerConfigurer
MapperScannerConfigurer 实现了BeanDefinitionRegistryPostProcessor, postProcessBeanDefinitionRegistry方法中会生成ClassPathMapperScanner对象进行扫描basePackage目录的接口类。

把接口类扫描出来, 生成 MapperFactoryBean beanDefinition

这里的beanDefinition只设置了两个PropertyValue

Mapper代理对象属性注入
MapperFactoryBean继承了 SqlSessionDaoSupport, 里面包含几个set方法

Spring在填充属性的时候会尝试将带set方法、非简单参数类型的方法 尝试执行其setXXX。 会从IOC中获取参数类型的对象。

SqlSessionDaoSupport:
由于SqlSessionFactory、SqlSessionTemplate 在MybatisPlusAutoConfiguration中都已经定义了Bean,因此下面的setSqlSessionFactory、setSqlSessionTemplate都会执行一遍。
注意到setSqlSessionFactory中会重新创建sqlSessionTemplate。 这里是否多于了,为什么没有定义属性SqlSessionFactory
kotlin
public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
if (this.sqlSessionTemplate == null || sqlSessionFactory != this.sqlSessionTemplate.getSqlSessionFactory()) {
this.sqlSessionTemplate = createSqlSessionTemplate(sqlSessionFactory);
}
}
public final SqlSessionFactory getSqlSessionFactory() {
return (this.sqlSessionTemplate != null ? this.sqlSessionTemplate.getSqlSessionFactory() : null);
}
public void setSqlSessionTemplate(SqlSessionTemplate sqlSessionTemplate) {
this.sqlSessionTemplate = sqlSessionTemplate;
}
public SqlSession getSqlSession() {
return this.sqlSessionTemplate;
}
afterPropertiesSet
当Bean对象初始化完成后,最后会调用afterPropertiesSet方法,完成最后的一些参数初始化。

这里会判断Configuration中是否已经有该接口对应的Mapper对象(当定义过相关XML配置的时候,这里不会继续处理), 生成Mapper接口对应的MappedStatement对象



这里的逻辑实际上跟前面解析XML部分后期解析Mapper接口对应的逻辑是一样的。最终生成MappedStatement到MybatisConfiguration中
getObject
当bean实例化完成后,执行 #getObject方法, 构建Mapper代理对象 MybatisMapperProxy 作为最终的Bean

从SqlSessionTemplate中获取Mapper代理对象


创建JDK代理对象:

Mapper方法执行流程
前面提到注入的DAO方法实际上是MybatisMapperProxy对象,在执行目标的时候会转到MybatisMapperMethod#execute,通过接口方法,配置判断属于哪类操作,进而分配到合适的执行方法

sqlSession 即SqlSessionTemplate, 最终委派给SqlSessionInterceptor

不管是mybatis-plus 内置的方法,还是mybatis xml定义的方法,最终都会在这里进行开启SqlSession
1. 开启SqlSession
org.mybatis.spring.SqlSessionTemplate.SqlSessionInterceptor:


根据执行类型选择合适的执行器(默认为SimpleExecutor)

2. Executor 执行目标
DefaultSqlSession:
查询方法都会进入select:
通过statement 从Configuration中得到MappedStatement(包含了缓存对象信息) , 交给Executor 查询

BoundSQL: 包含最终的SQL,以及参数信息

先查询二级缓存:
MappedStatement中记录了缓存对象,默认namespace 独有。(@CacheNamespaceRef 可以改变为非namespace)

二级缓存中没有数据,查询一级缓存:


执行底层查询逻辑:

prepareStatement: 获取Connection,生成PrepareStatement,填充参数

从SpringManagedTransaction对象 获取JDBC连接对象, 如果没有初始化链接,进行初始化。


DataSourceUtils.getConnection: 会先尝试从TransactionSynchronizationManager中获取链接,如果获取不到则从Datasource中新建链接对象。
autoCommit:会检查该链接是否是autoCommit。如果是则执行完成后不需要提交事务。

handler#query:
这里的ps 实际上是mybatis的代理对象,最终会调用底层JDBC API 进行查询

3. 提交事务


将记录存入二级缓存。

Interceptor
类似AOP机制来拦截执行过程,可以进行执行耗时统计,日志记录,SQL修改等。
如下面对SQL添加注释前缀,用于记录一些SQL执行信息:
less
@Intercepts({
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})
})
public class LogQueryAndUpdateSqlHandler implements Interceptor {
// 拦截时调用方法
@Override
public Object intercept(Invocation invocation) throws Throwable {
return LogSqlHelper.intercept(invocation, this.slowSqlThreshold, this.isOptimizeSql);
}
// 指定Executor 目标才开始拦截
@Override
public Object plugin(Object target) {
return target instanceof Executor ? Plugin.wrap(target, this) : target;
}
@Override
public void setProperties(Properties properties) {
}
}
// 拦截prepare
@Intercepts({
@Signature(
type = StatementHandler.class,
method = "prepare",
args = {Connection.class, Integer.class}
)
})
public class SqlCommentInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 1. 获取原始 SQL
StatementHandler handler = (StatementHandler) invocation.getTarget();
String originalSql = handler.getBoundSql().getSql();
// 2. 添加自定义注释前缀
String commentedSql = "/* 业务标记 */ " + originalSql;
// 3. 通过反射修改 SQL
Field boundSqlField = handler.getBoundSql().getClass().getDeclaredField("sql");
boundSqlField.setAccessible(true);
boundSqlField.set(handler.getBoundSql(), commentedSql);
// 4. 继续执行原逻辑
return invocation.proceed();
}
}
在创建Executor的时候会织入拦截器


除了Executor外,下面方法也都可以进行拦截处理

分页
- 使用MybatisPlus 自带的分页拦截器
ini
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor();
paginationInnerInterceptor.setDbType(DbType.MYSQL);
paginationInnerInterceptor.setOverflow(true);
interceptor.addInnerInterceptor(paginationInnerInterceptor);
return interceptor;
}
- 使用pagehelper
引入依赖
xml
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.4.6</version>
</dependency>
PageHelperAutoConfiguration会自动注入PageInterceptor:

查询前使用:PageHelper.startPage(1, 2);
会将Page参数信息存入ThreadLocal中,在执行查询过程中,pageHelper的拦截器会自动处理相关分页信息。
执行结束后pageHelper会自动remove ThreadLocal的信息。
SQL 记录拦截器
mybatis 默认打印的SQL日志都是带参数的预编译SQL,不便于日志分析, 也没有记录执行时间。 demo来自:blog.csdn.net/weixin_4550...
typescript
// 只拦截Executor的这两个方法, save 也是执行的update
@Intercepts({
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})
})
public class LogQueryAndUpdateSqlHandler implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
return LogSqlHelper.intercept(invocation, this.slowSqlThreshold, this.isOptimizeSql);
}
@Override
public Object plugin(Object target) {
// 这里不判断也可以, 内部会判断 是否符合@Signature的type
return target instanceof Executor ? Plugin.wrap(target, this) : target;
}
}
java
package com.example.springbootadmin.mybatis;
public class LogSqlHelper {
private static final Logger log = LoggerFactory.getLogger(LogSqlHelper.class);
private static final String SELECT = "select";
private static final String FROM = "from";
private static final String SIMPLE_SELECT = "select * ";
private static final int MAX_SQL_LENGTH = 120;
private static final String PATTERN = "yyyy-MM-dd HH:mm:ss";
public LogSqlHelper() {
}
public static Object intercept(Invocation invocation, int slowSqlThreshold, boolean optimizeSql) throws Throwable {
long startTime = System.currentTimeMillis();
Object returnValue = invocation.proceed();
long cost = System.currentTimeMillis() - startTime;
if (cost >= (long) slowSqlThreshold) {
log.info("cost = {} ms, affected rows = {}, SQL: {}",
cost, formatResult(returnValue), formatSql(invocation, optimizeSql));
}
return returnValue;
}
private static Object formatResult(Object obj) {
if (obj == null) {
return "NULL";
} else if (obj instanceof List) {
return ((List) obj).size();
} else if (!(obj instanceof Number) && !(obj instanceof Boolean) && !(obj instanceof Date)
&& !(obj instanceof String)) {
return obj instanceof Map ? ((Map) obj).size() : 1;
} else {
return obj;
}
}
private static String formatSql(Invocation invocation, boolean isOptimizeSql) {
MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
Object parameter = null;
if (invocation.getArgs().length > 1) { // 拦截的方法query、update 都是有多个参数的。
parameter = invocation.getArgs()[1]; // SQL的参数对象
}
BoundSql boundSql = mappedStatement.getBoundSql(parameter); // 通过参数对象重新构建BoundSql,这里会包含数据库生成的主键信息
Configuration configuration = mappedStatement.getConfiguration();
Object parameterObject = boundSql.getParameterObject();
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
String sql = boundSql.getSql().replaceAll("[\s]+", " ");
String formatSql = sql.toLowerCase();
if (isOptimizeSql && formatSql.startsWith(SELECT) && formatSql.length() > MAX_SQL_LENGTH) {
sql = SIMPLE_SELECT + sql.substring(formatSql.indexOf(FROM));
}
// 通过参数对象填充SQL占位符
if (parameterMappings.size() > 0 && parameterObject != null) {
TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
sql = sql.replaceFirst("\?", formatParameterValue(parameterObject));
} else {
MetaObject metaObject = configuration.newMetaObject(parameterObject);
for (ParameterMapping parameterMapping : parameterMappings) {
String propertyName = parameterMapping.getProperty();
Object obj;
if (metaObject.hasGetter(propertyName)) {
obj = metaObject.getValue(propertyName);
sql = sql.replaceFirst("\?", formatParameterValue(obj));
} else if (boundSql.hasAdditionalParameter(propertyName)) {
obj = boundSql.getAdditionalParameter(propertyName);
sql = sql.replaceFirst("\?", formatParameterValue(obj));
}
}
}
}
return sql;
}
private static String formatParameterValue(Object obj) {
if (obj == null) {
return "NULL";
} else {
String value = obj.toString();
if (obj instanceof Date) {
DateFormat dateFormat = new SimpleDateFormat(PATTERN);
value = dateFormat.format((Date) obj);
}
if (!(obj instanceof Number) && !(obj instanceof Boolean)) {
value = "'" + value + "'";
}
return value;
}
}
}
打印的结果如下:

当执行批量save 逻辑的时候,这里会分开打印多条insert SQL。
如果有多个拦截器的时候,需要注意定义顺序。可以像pageHelper中的那样在afterPropertiesSet中手动添加Interceptor
其他内容
Executor:
可以通过配置指定executor-type: simple(默认), 都只在sqlSession会话有效。
BaseExecutor : 抽象类,实现了一级缓存(无法关闭) 。 执行update会清理所有缓存对象。
具体实现:
CachingExecutor: 会处理二级缓存 ,默认关闭(query方法),当update后执行查询会清理缓存。 namespace独享。默认都会使用CachingExecutor进行包装下面的执行器。
- SimpleExecutor:默认执行器,同一个事务中,重复执行会有缓存
- ReuseExecutor:会缓存SQL 对应的PreparedStatement。 由于默认有一级缓存的存在,这里貌似一直用不上。
- BatchExecutor: 用于批量操作优化性能。mybatis-plus 中的saveBatch相关方法会使用该执行器。具体可以查看SqlHelper
缓存:
- 先查二级缓存(默认关闭,@CacheNamespace: 默认namespace 独有。 @CacheNamespaceRef),PerpetualCache。 MappedStatement#cache
- 再查一级缓存: sqlSession级别,在创建Executor时生成 PerpetualCache, BaseExecutor#localCache。
ISqlInjector
定义了各种默认CRUD通用的方法,可以继承默认的进行扩展方法
mybatis-plus中内置的DefaultSqlInjector:实现ISqlInjector。

自定义实现 insert duplicate Update
当key不存在时 执行insert、 存在时执行update。
insert into test (id,b, c) value (1,2,2) on duplicate key update b = 2;
参考Insert来实现:
ini
public class InsertDuplicateUpdate extends AbstractMethod {
private static final String name = "insertDuplicateUpdate";
private static final String sqlScript = "<script>\nINSERT INTO %s %s VALUES %s on duplicate key update %s\n</script>";
public InsertDuplicateUpdate() {
super(name);
}
@Override
public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
KeyGenerator keyGenerator = NoKeyGenerator.INSTANCE;
SqlMethod sqlMethod = SqlMethod.INSERT_ONE;
String columnScript = SqlScriptUtils.convertTrim(tableInfo.getAllInsertSqlColumnMaybeIf(null),
LEFT_BRACKET, RIGHT_BRACKET, null, COMMA);
String valuesScript = SqlScriptUtils.convertTrim(tableInfo.getAllInsertSqlPropertyMaybeIf(null),
LEFT_BRACKET, RIGHT_BRACKET, null, COMMA);
String keyProperty = null;
String keyColumn = null;
// 表包含主键处理逻辑,如果不包含主键当普通字段处理
if (StringUtils.isNotBlank(tableInfo.getKeyProperty())) {
if (tableInfo.getIdType() == IdType.AUTO) {
/* 自增主键 */
keyGenerator = Jdbc3KeyGenerator.INSTANCE;
keyProperty = tableInfo.getKeyProperty();
keyColumn = tableInfo.getKeyColumn();
} else if (null != tableInfo.getKeySequence()) {
keyGenerator = TableInfoHelper.genKeyGenerator(this.methodName, tableInfo, builderAssistant);
keyProperty = tableInfo.getKeyProperty();
keyColumn = tableInfo.getKeyColumn();
}
}
// 前面都是Insert copy来的逻辑
String sqlSet = SqlScriptUtils.convertTrim(tableInfo.getAllSqlSet(tableInfo.isWithLogicDelete(), null), null, null, null, COMMA);
String sql = String.format(sqlScript, tableInfo.getTableName(), columnScript, valuesScript, sqlSet);
SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, modelClass);
return this.addInsertMappedStatement(mapperClass, modelClass, getMethod(sqlMethod), sqlSource, keyGenerator, keyProperty, keyColumn);
}
}
创建自定义SQLInjector:
继承DefaultSqlInjector,在原有基础上加入InsertDuplicateUpdate。
scala
@Component // 会自动替换默认的DefaultSqlInjector
public class CustSqlInjector extends DefaultSqlInjector {
@Override
public List<AbstractMethod> getMethodList(Class<?> mapperClass, TableInfo tableInfo) {
List<AbstractMethod> methodList = super.getMethodList(mapperClass, tableInfo);
methodList.add(new InsertDuplicateUpdate());
return methodList;
}
}
Mapper接口:
参考insert 添加方法即可使用
arduino
int insertDuplicateUpdate(User entity);
mybatis 核心类:
MybatisMapperProxy:Mapper接口代理对象
SqlSessionTemplate: Mapper接口执行目标方法后会委托到SqlSessionTemplate, 提供了selectXXX方法。selectXXX 最后委托给代理对象执行逻辑。
SqlSessionInterceptor:拦截上面selectXXX执行逻辑。 从DefaultSqlSessionFactory或ThreadLocal获取DefaultSqlSession类。
SpringManagedTransaction:在开启SqlSession 的时候,从TransactionFactory中获取的Transaction(此时并没有获取真正的连接,在执行PrepareStatement的时候才会获取底层连接)。
DefaultSqlSession:包含了Executor(包含Transaction),Configuration
MappedStatement: 对应一个mapper方法的相关定义信息
SystemMetaObject.forObject(): 方便set,get的工具类。 支持属性.属性 进行操作
Reflector: 会缓存类的所有get,set方法
解析生成SQL Script
RawSqlSource: 不需要动态判断的, 没有If 相关的标签,SQL 始终都是一个。
DynamicSqlSource: 每次执行都需要通过条件动态生成最终SQL。
参考文章:
- 核心流程分析 blog.csdn.net/weixin_4550...