主要想搞清楚几个问题
本篇文章需要理解mybaties的源码为基础,否则看本篇文章会吃力。mybaties源码分析可以看我上一篇文章,如下 彻底看懂springboot mybaties源码流程
mybatis plus是基于mybatis实现的,下面来具体看看他们直接的关系,以及是怎样依赖的?
首先看mybatis plus自动配置类中的核心方法sqlSessionFactory,如下:
MybatisPlusAutoConfiguration
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
// mybaties plus使用 MybatisSqlSessionFactoryBean ,mybaties使用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()));
}
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);
}
if (!ObjectUtils.isEmpty(this.properties.resolveMapperLocations())) {
factory.setMapperLocations(this.properties.resolveMapperLocations());
}
this.getBeanThen(TransactionFactory.class, factory::setTransactionFactory);
Class<? extends LanguageDriver> defaultLanguageDriver = this.properties.getDefaultScriptingLanguageDriver();
if (!ObjectUtils.isEmpty(this.languageDrivers)) {
factory.setScriptingLanguageDrivers(this.languageDrivers);
}
Optional.ofNullable(defaultLanguageDriver).ifPresent(factory::setDefaultScriptingLanguageDriver);
// 提供了可定制MybatisSqlSessionFactoryBean的类,如果你想定制它,你可以自定定义SqlSessionFactoryBeanCustomizer类型的类。
applySqlSessionFactoryBeanCustomizers(factory);
// mybaties plus定义的全局的配置类,供后续方便使用。
GlobalConfig globalConfig = this.properties.getGlobalConfig();
// 从spring容器中获取定义的MetaObjectHandler类型的实例,并设置到globalConfig供后续使用,这个就是
// 字段自动填充功能的类。
this.getBeanThen(MetaObjectHandler.class, globalConfig::setMetaObjectHandler);
// 从spring容器中获取IKeyGenerator类型实例(主要是实现主键生成器),并设置到globalConfig供后续使用
this.getBeansThen(IKeyGenerator.class, i -> globalConfig.getDbConfig().setKeyGenerators(i));
// 从spring容器中获取ISqlInjector类型实例(Sql注入器),并设置到globalConfig供后续使用
this.getBeanThen(ISqlInjector.class, globalConfig::setSqlInjector);
// 从spring容器中获取IdentifierGenerator类型实例(主要是实现ID生成器),并设置到globalConfig供后续使用
this.getBeanThen(IdentifierGenerator.class, globalConfig::setIdentifierGenerator);
// 设置 GlobalConfig 到 MybatisSqlSessionFactoryBean
factory.setGlobalConfig(globalConfig);
return factory.getObject();
}
可以看到mybatis plus使用
MybatisSqlSessionFactoryBean (mybaties中使用的是SqlSessionFactoryBean),然后针对mybatis plus进行了一系列的初始化操作,并把相关的实例都设置到了GlobalConfig中,这块是mybatis plus扩展的初始化位置。
mybaties plus定制了mybaties很多核心类,总结如下:
mybaties plus |
mybaties |
功能描述 |
MybatisSqlSessionFactoryBean |
SqlSessionFactoryBean |
调用buildSqlSessionFactory创建SqlSessionFactory类 |
MybatisConfiguration |
Configuration |
用于描述 MyBatis 主配置文件信息,MyBatis 框架在启动时自动配置类中,会加载mapper配置文件,将配置信息转换为 Configuration 对象,然后把该对象传入给sqlSessionFactory供后续使用 |
MybatisMapperAnnotationBuilder |
MapperAnnotationBuilder |
解析Mapper方法中用注解方式定义的sql。 |
MybatisMapperRegistry |
MapperRegistry |
Mapper注册器,其实就是加入到一个内部数组中。 |
MybatisParameterHandler |
DefaultParameterHandler |
用于处理 SQL 中的参数占位符,为参数占位符设置值 |
上面是mybaties plus扩展mybaties的核心类。
答疑时刻
mybatis plus扩展了mybatis的很多功能,添加了很多实用功能。比如最主要的基于对象的增删改查(不需要写sql),基于雪花算法的ID生成器,字段自动填充功能,逻辑删除功能等。
字段自动填充功能可以干什么?
在开发过程中表中经常会建创建时间,创建人,更新时间,更新人字段,正常自己维护这几个字段的时候,插入数据的时候需要自己给创建时间创建人赋值。更新数据的时候需要自己给更新时间,更新人字段赋值。
mybaties plus “字段自动填充功能”就是来解决这个问题的,可以在插入数据的时候指定要更新哪些字段,更新数据的时候指定更新哪些字段。用起来还是很方便的。
下面是使用例子:
public class User {
// 注意!这里需要标记为填充字段
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT)
private String createUserName;
@TableField(fill = FieldFill.UPDATE)
private LocalDateTime updateTime;
@TableField(fill = FieldFill.UPDATE)
private String updateUserName;
}
@Slf4j
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
log.info("start insert fill ....");
this.strictInsertFill(metaObject, "createTime", () -> LocalDateTime.now(), LocalDateTime.class);
this.strictInsertFill(metaObject, "createUserName", () -> "wanglining", String.class);
}
@Override
public void updateFill(MetaObject metaObject) {
log.info("start update fill ....");
this.strictUpdateFill(metaObject, "updateTime", () -> LocalDateTime.now(), LocalDateTime.class);
this.strictInsertFill(metaObject, "updateUserName", () -> "wanglining", String.class)
}
}
主要两步:
经过上面两个步骤,自动填充功能就完成了,当你调用mybaties plus的BaseMapper中提供的insert方法的时候,会给createTime,createUserName两个字段填充你在handler中指定的值。相应的调用BaseMapper提供的update方法的时候,会给updateTime,updateUserName两个字段填充你在handler中指定的值。
注意:一定得是调用BaseMapper中提供的方法,如果你自己再xml中定义sql语句是不会有作用的。
实现原理
下面开始讲解它的实现原理。
在上面提到mybaties plus定制了参数处理器(ParameterHandler),mybaties plus中的实现为MybatisParameterHandler,它的作用是“用于处理 SQL 中的参数占位符,为参数占位符设置值”, mybaties plus就是用在jdbc真正执行sql前,通过MetaObjectHandler给对象相应字段赋值。进而mybaties把对象解析到了sql中进行执行。
MybatisParameterHandler
private void process(Object parameter) {
if (parameter != null) {
TableInfo tableInfo = null;
Object entity = parameter;
if (parameter instanceof Map) {
// 处理单参数使用注解标记的时候,尝试提取et来获取实体参数
Map<?, ?> map = (Map<?, ?>) parameter;
if (map.containsKey(Constants.ENTITY)) {
Object et = map.get(Constants.ENTITY);
if (et != null) {
entity = et;
tableInfo = TableInfoHelper.getTableInfo(entity.getClass());
}
}
} else {
tableInfo = TableInfoHelper.getTableInfo(parameter.getClass());
}
if (tableInfo != null) {
//到这里就应该转换到实体参数对象了,因为填充和ID处理都是针对实体对象处理的,不用传递原参数对象下去.
MetaObject metaObject = this.configuration.newMetaObject(entity);
if (SqlCommandType.INSERT == this.sqlCommandType) {
// 这个方法实现了给主键ID赋值的,根据你指定的策略生成id值,并赋值给主键ID。
populateKeys(tableInfo, metaObject, entity);
// 这里会进一步调用上面定义的handler里面的方法
insertFill(metaObject, tableInfo);
} else {
// 这里会进一步调用上面定义的handler里面的方法
updateFill(metaObject, tableInfo);
}
}
}
}
protected void insertFill(MetaObject metaObject, TableInfo tableInfo) {
// 这里会先获取到你上面定义的MyMetaObjectHandler。
GlobalConfigUtils.getMetaObjectHandler(this.configuration).ifPresent(metaObjectHandler -> {
// 判断openInsertFill是否为true,模式的就是返回true
// isWithInsertFill判断是否为true,这个方法返回的就是TableInfo类中的withInsertFill属性
// TableInfo就是dao层对象以及字段注解解析出来的一个对应数据库表的对象,其中withInsertFill属性
// 就是根据类的字段上是否有@TableField(fill = FieldFill.INSERT)注解,如果有那么withInsertFill
// 最终会是true
if (metaObjectHandler.openInsertFill() && tableInfo.isWithInsertFill()) {
// 调用MyMetaObjectHandler的insertFill方法
metaObjectHandler.insertFill(metaObject);
}
});
}
// 这个方法就不写注释了,跟上面一样。
protected void updateFill(MetaObject metaObject, TableInfo tableInfo) {
GlobalConfigUtils.getMetaObjectHandler(this.configuration).ifPresent(metaObjectHandler -> {
if (metaObjectHandler.openUpdateFill() && tableInfo.isWithUpdateFill()) {
metaObjectHandler.updateFill(metaObject);
}
});
}
metaObjectHandler.insertFill会调用上面自己定义的MyMetaObjectHandler的insertFill方法,然后会继续调用strictInsertFill方法。
分析下strictInsertFill方法如下:
MetaObjectHandler
default MetaObjectHandler strictInsertFill(TableInfo tableInfo, MetaObject metaObject, List<StrictFill<?, ?>> strictFills) {
return strictFill(true, tableInfo, metaObject, strictFills);
}
/**
* 严格填充,只针对非主键的字段,只有该表注解了fill 并且 字段名和字段属性 能匹配到才会进行填充(null 值不填充)
*
* @param insertFill 是否验证在 insert 时填充
* @param tableInfo cache 缓存
* @param metaObject metaObject meta object parameter
* @param strictFills 填充信息
* @return this
* @since 3.3.0
*/
default MetaObjectHandler strictFill(boolean insertFill, TableInfo tableInfo, MetaObject metaObject, List<StrictFill<?, ?>> strictFills) {
// 先过滤表上withInsertFill或withUpdateFill属性是否为true,其实就是看dao层对应的类属性上是否有
// @TableField(fill = FieldFill.INSERT)或@TableField(fill = FieldFill.UPDATE)注解
if ((insertFill && tableInfo.isWithInsertFill()) || (!insertFill && tableInfo.isWithUpdateFill())) {
strictFills.forEach(i -> {
final String fieldName = i.getFieldName();
final Class<?> fieldType = i.getFieldType();
tableInfo.getFieldList().stream()
// 过滤对象字段,把字段上有@TableField(fill = FieldFill.INSERT)或@TableField(fill = FieldFill.UPDATE)注解
// 的字段过滤出来。
.filter(j -> j.getProperty().equals(fieldName) && fieldType.equals(j.getPropertyType()) &&
((insertFill && j.isWithInsertFill()) || (!insertFill && j.isWithUpdateFill()))).findFirst()
// 针对过滤出来的字段赋值。
.ifPresent(j -> strictFillStrategy(metaObject, fieldName, i.getFieldVal()));
});
}
return this;
}
上面分析完了MybatisParameterHandler处理字段自动填充流程的核心逻辑,是从它的方法process说起的,那这个方法又是怎么被触发的?怎么和mybaties执行流程对接上的?
下面是mybaties 查询方法完成的调用时序图
上面重点关注下MybatisParameterHandler类的方法,主要两步:
经过上面两步ParameterHandler的任务就完成了。
mybatis plus通过定义sql注入器实现了此功能。核心类
DefaultSqlInjector
@Override
public List<AbstractMethod> getMethodList(Class<?> mapperClass, TableInfo tableInfo) {
Stream.Builder<AbstractMethod> builder = Stream.<AbstractMethod>builder()
// 注入BaseMapper的insert方法
.add(new Insert())
// 注入BaseMapper的delete方法
.add(new Delete())
.add(new DeleteByMap())
.add(new Update())
.add(new SelectByMap())
.add(new SelectCount())
.add(new SelectMaps())
.add(new SelectMapsPage())
.add(new SelectObjs())
// 注入BaseMapper的selectList方法
.add(new SelectList())
.add(new SelectPage());
if (tableInfo.havePK()) {
builder.add(new DeleteById())
.add(new DeleteBatchByIds())
.add(new UpdateById())
.add(new SelectById())
.add(new SelectBatchByIds());
} else {
logger.warn(String.format("%s ,Not found @TableId annotation, Cannot use Mybatis-Plus 'xxById' Method.",
tableInfo.getEntityType()));
}
return builder.build().collect(toList());
}
这里以insert举例看下Insert内部实现。
Insert
/**
* 插入一条数据(选择字段插入)
*
* @author hubin
* @since 2018-04-06
*/
public class Insert extends AbstractMethod {
public Insert() {
super(SqlMethod.INSERT_ONE.getMethod());
}
/**
* @param name 方法名
* @since 3.5.0
*/
public Insert(String name) {
super(name);
}
@Override
public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
KeyGenerator keyGenerator = NoKeyGenerator.INSTANCE;
// INSERT_ONE("insert", "插入一条数据(选择字段插入)", "<script>nINSERT INTO %s %s VALUES %sn</script>")
// INSERT_ONE枚举里面定义了sql脚本,以及BaseMapper中对应方法名
SqlMethod sqlMethod = SqlMethod.INSERT_ONE;
// 通过dao类对应表信息,获取表中的列并且拼“接插入sql”的列部分
String columnScript = SqlScriptUtils.convertTrim(tableInfo.getAllInsertSqlColumnMaybeIf(null),
LEFT_BRACKET, RIGHT_BRACKET, null, COMMA);
// 通过dao类对应表信息,获取表中的列并且拼接“插入sql”的值部分
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();
}
}
// 通过上面获取的插入sql脚本,以及列和值部分,拼接成最终的sql脚本字符串。
String sql = String.format(sqlMethod.getSql(), tableInfo.getTableName(), columnScript, valuesScript);
SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, modelClass);
// 通过上面的信息生成对应的MapperStatement,并且注册到mybaties容器中。供后续真正调用BaseMapper对应方法
// 的时候再取出来使用。
return this.addInsertMappedStatement(mapperClass, modelClass, getMethod(sqlMethod), sqlSource, keyGenerator, keyProperty, keyColumn);
}
通过上面的代码,可以想象到mybaties plus初始化过程中肯定会调用到Insert的injectMappedStatement方法,把它对应的MapperStatement注入到mybaties容器中,然后后续通过动态代理调用BaseMapper相关方法的时候就可以根据MapperStatement对应的信息去执行对应的sql了。
现在分析下injectMappedStatement方法是怎么被调用到的?
调用时序图如下:
这里关注下Insert的injectMappedStatement调用流程。
这里再重点分析MybatisMapperRegistry的addMapper代码,因为它的信息量比较大。如下:
MybatisMapperRegistry
public <T> void addMapper(Class<T> type) {
if (type.isInterface()) {
if (hasMapper(type)) {
// TODO 如果之前注入 直接返回
return;
// TODO 这里就不抛异常了
// throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}
boolean loadCompleted = false;
try {
// 这里也换成 MybatisMapperProxyFactory 而不是 MapperProxyFactory
// 对于mybaties plus用的MybatisMapperProxyFactory,mybaties用MapperProxyFactory
// 这句代码很重要,把动态代理工厂加入到了knownMappers中,这里的type是你定义的Mapper类。
// 下面的getMapper会通过MybatisMapperProxyFactory生成对应mapper的动态代理,进而执行mapper
// 的各个方法。
knownMappers.put(type, new MybatisMapperProxyFactory<>(type));
// It's important that the type is added before the parser is run
// otherwise the binding may automatically be attempted by the
// mapper parser. If the type is already known, it won't try.
// TODO 这里也换成 MybatisMapperAnnotationBuilder 而不是 MapperAnnotationBuilder
// 解析Mapper定义的方法中用注解方式定义的sql。这里最终会在你定义的Mapper类中添加mybaties plus
// 为你注入的Insert,Delete ,Update,Select等方法。其实就是把对应的MapperStatement注入到了Mybaties中。
MybatisMapperAnnotationBuilder parser = new MybatisMapperAnnotationBuilder(config, type);
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
@Override
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
// TODO 这里换成 MybatisMapperProxyFactory 而不是 MapperProxyFactory
// fix https://github.com/baomidou/mybatis-plus/issues/4247
MybatisMapperProxyFactory<T> mapperProxyFactory = (MybatisMapperProxyFactory<T>) knownMappers.get(type);
if (mapperProxyFactory == null) {
// 上面addMapper会在knownMappers添加对应的Factory
mapperProxyFactory = (MybatisMapperProxyFactory<T>) knownMappers.entrySet().stream()
.filter(t -> t.getKey().getName().equals(type.getName())).findFirst().map(Map.Entry::getValue)
.orElseThrow(() -> new BindingException("Type " + type + " is not known to the MybatisPlusMapperRegistry."));
}
try {
// 生成mapper对应的动态代理对象
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
}
}
答疑时刻
mybatis plus定义了增删改查对应的sql注入器,在解析注册mapper的时候,会把相应的方法作为MapperStatement注入到mybaties中。看起来没有定义对应的xml文件,其实是mybaties plus用sql注入器的方式,在代码中默认都提供了对应的sql了。
后面当你调用Mapper对应方法的时候,会通过对应mapper的动态代理获取到方法对应的MapperStatement,进而再通过执行器进行一系列的处理,最终执行该方法对应的sql。
通过上面的原理分析,也可以注入自己的sql,比如批量插入方法,mybaties plus默认没有提供该方法,你可以自己定义一个BaseMapper然后扩展出这个批量插入的方法,下一篇文章再讲解吧。