窥探系列之Mybatis-plus BaseMapper实现

news/2024/11/23 9:52:03/

我们知道,mybatisplus的BaseMapper接口中提供了一些如updateById的方法,框架本身已经实现了这些CRUD功能,基本的CRUD我们就没必要写sql,直接使用java语法就能对数据进行操控,很方便。那么这些功能是如何被实现的呢?这是我研读源码的动机
在这里插入图片描述

关键类

  1. AbstractSqlInjector及其子类DefaultSqlInjector
  2. AbstractMethod及其子类
  3. SqlMethod
  4. mapper代理:MybatisMapperProxy
  5. Configuration

MybatisMapperProxy 以代理类作为切入点

BaseMapper是一个接口,里面的方法都是抽象方法,所以很明显框架是通过代理的方式去实现这些抽象方法。

public class MybatisMapperProxy<T> implements InvocationHandler, Serializable {public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {try {if (Object.class.equals(method.getDeclaringClass())) {return method.invoke(this, args);} else if (method.isDefault()) {return invokeDefaultMethod(proxy, method, args);}} catch (Throwable t) {throw ExceptionUtil.unwrapThrowable(t);}final MybatisMapperMethod mapperMethod = cachedMapperMethod(method);return mapperMethod.execute(sqlSession, args);}......省略
}

这段代码是来自 MyBatis 库中的 MapperProxy 类,用于为 Mapper 接口创建动态代理实例,通过该实例调用实际的 SQL 语句。

invoke 方法会在动态代理实例上调用,当 Mapper 接口中的方法被调用时,该方法将被执行。其中,proxy 参数是动态代理实例本身,method 参数是被调用的方法,args 参数包含传递给方法的参数。

第一个 if 语句检查方法是否属于 Object 类。如果是,则在 MapperProxy 实例本身上调用该方法,并将 args 数组作为参数传递。

第二个 else if 语句检查方法是否为默认方法,这是在 Java 8 中引入的。如果是默认方法,则使用 invokeDefaultMethod 方法调用它,传递 proxy、method 和 args 参数。

如果方法既不属于 Object 类也不是默认方法,则使用 method 参数从缓存中获取一个 MybatisMapperMethod 实例,并在其上调用 execute 方法,传递 sqlSession 和 args 参数。execute 方法负责使用 SqlSession 对象执行实际的 SQL 语句,并返回结果。

可以看到,BaseMapper的抽象方法的实现是在这两步:
final MybatisMapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(sqlSession, args);


MybatisMapperMethod#execute主要是一下逻辑
  1. 判断command类型,可以看到类型包括INSERT、UPDATE等;
  2. 转化参数;
  3. command.getName()作为入参执行sqlSession对应方法;
public class MybatisMapperMethod {private final MapperMethod.SqlCommand command;private final MapperMethod.MethodSignature method;public MybatisMapperMethod(Class<?> mapperInterface, Method method, Configuration config) {this.command = new MapperMethod.SqlCommand(config, mapperInterface, method);this.method = new MapperMethod.MethodSignature(config, mapperInterface, method);}public Object execute(SqlSession sqlSession, Object[] args) {Object result;switch (command.getType()) {case INSERT: {Object param = method.convertArgsToSqlCommandParam(args);result = rowCountResult(sqlSession.insert(command.getName(), param));break;}case UPDATE: {Object param = method.convertArgsToSqlCommandParam(args);result = rowCountResult(sqlSession.update(command.getName(), param));break;}case DELETE: {Object param = method.convertArgsToSqlCommandParam(args);result = rowCountResult(sqlSession.delete(command.getName(), param));break;}case SELECT:
......}
}

接下来,要看一下command的type和name具体是什么?

可以看到,将mapper的接口类和方法作为入参,调用resolveMappedStatement方法得到一个
MappedStatement 对象,取出id作为command的name,sqlCommandType作为command的type;

在resolveMappedStatement中,String statementId = mapperInterface.getName() + "." + methodName;将mapper的完全限定名和调用方法名拼接起来,作为statementId;

configuration.getMappedStatement(statementId);然后根据statementId从configuration对象中获取到对应的MappedStatement对象;

另外,这里引申出了一个关键的类com.baomidou.mybatisplus.core.MybatisConfiguration,很显然MybatisConfiguration对象维护了MappedStatement对象集合,所以要想弄明白对应sqlCommandType,需要研究MappedStatement对象如何被加载到MybatisConfiguration中;

  public static class SqlCommand {private final String name;private final SqlCommandType type;public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {final String methodName = method.getName();final Class<?> declaringClass = method.getDeclaringClass();MappedStatement ms = resolveMappedStatement(mapperInterface, methodName, declaringClass,configuration);if (ms == null) {if (method.getAnnotation(Flush.class) != null) {name = null;type = SqlCommandType.FLUSH;} else {throw new BindingException("Invalid bound statement (not found): "+ mapperInterface.getName() + "." + methodName);}} else {name = ms.getId();type = ms.getSqlCommandType();if (type == SqlCommandType.UNKNOWN) {throw new BindingException("Unknown execution method for: " + name);}}}
# 类:org.apache.ibatis.binding.MapperMethodprivate MappedStatement resolveMappedStatement(Class<?> mapperInterface, String methodName,Class<?> declaringClass, Configuration configuration) {String statementId = mapperInterface.getName() + "." + methodName;if (configuration.hasStatement(statementId)) {return configuration.getMappedStatement(statementId);} else if (mapperInterface.equals(declaringClass)) {return null;}for (Class<?> superInterface : mapperInterface.getInterfaces()) {if (declaringClass.isAssignableFrom(superInterface)) {MappedStatement ms = resolveMappedStatement(superInterface, methodName,declaringClass, configuration);if (ms != null) {return ms;}}}return null;}

以insert为例,继续往下看sqlSession.insert做了哪些操作?

  1. 根据statementId,从configuration对象中获取到对应的MappedStatement,然后调用执行器执行MappedStatement对象;
  2. MybatisSimpleExecutor#doUpdate 中调用configuration对象的newStatementHandler方法创建一个StatementHandler 对象;
  3. prepareStatement(handler, ms.getStatementLog(), false) 生成PreparedStatement对象;
  4. ps.execute()执行sql;
# org.apache.ibatis.session.defaults.DefaultSqlSessionpublic int insert(String statement, Object parameter) {return update(statement, parameter);}public int update(String statement, Object parameter) {try {dirty = true;MappedStatement ms = configuration.getMappedStatement(statement);return executor.update(ms, wrapCollection(parameter));} catch (Exception e) {throw ExceptionFactory.wrapException("Error updating database.  Cause: " + e, e);} finally {ErrorContext.instance().reset();}}
public class MybatisSimpleExecutor extends AbstractBaseExecutor {public MybatisSimpleExecutor(Configuration configuration, Transaction transaction) {super(configuration, transaction);}@Overridepublic int doUpdate(MappedStatement ms, Object parameter) throws SQLException {Statement stmt = null;try {Configuration configuration = ms.getConfiguration();StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null);stmt = prepareStatement(handler, ms.getStatementLog(), false);return stmt == null ? 0 : handler.update(stmt);} finally {closeStatement(stmt);}}private Statement prepareStatement(StatementHandler handler, Log statementLog, boolean isCursor) throws SQLException {Statement stmt;Connection connection = getConnection(statementLog);stmt = handler.prepare(connection, transaction.getTimeout());//游标不支持返回null.if (stmt == null && !isCursor) {return null;} else {handler.parameterize(stmt);return stmt;}}......
public class PreparedStatementHandler extends BaseStatementHandler {@Overridepublic int update(Statement statement) throws SQLException {PreparedStatement ps = (PreparedStatement) statement;ps.execute();int rows = ps.getUpdateCount();Object parameterObject = boundSql.getParameterObject();KeyGenerator keyGenerator = mappedStatement.getKeyGenerator();keyGenerator.processAfter(executor, mappedStatement, ps, parameterObject);return rows;}
......

PreparedStatement对象的创建

  1. boundSql.getSql()获取sql模版;
  2. connection.prepareStatement创建PreparedStatement对象;
  3. BaseStatementHandler#BaseStatementHandler中,获取BoundSql对象;
# org.apache.ibatis.executor.statement.PreparedStatementHandlerprotected Statement instantiateStatement(Connection connection) throws SQLException {String sql = boundSql.getSql();if (mappedStatement.getKeyGenerator() instanceof Jdbc3KeyGenerator) {String[] keyColumnNames = mappedStatement.getKeyColumns();if (keyColumnNames == null) {return connection.prepareStatement(sql, PreparedStatement.RETURN_GENERATED_KEYS);} else {return connection.prepareStatement(sql, keyColumnNames);}} else if (mappedStatement.getResultSetType() == ResultSetType.DEFAULT) {return connection.prepareStatement(sql);} else {return connection.prepareStatement(sql, mappedStatement.getResultSetType().getValue(), ResultSet.CONCUR_READ_ONLY);}}
# org.apache.ibatis.executor.statement.BaseStatementHandlerprotected BaseStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {this.configuration = mappedStatement.getConfiguration();this.executor = executor;this.mappedStatement = mappedStatement;this.rowBounds = rowBounds;this.typeHandlerRegistry = configuration.getTypeHandlerRegistry();this.objectFactory = configuration.getObjectFactory();if (boundSql == null) { // issue #435, get the key before calculating the statementgenerateKeys(parameterObject);boundSql = mappedStatement.getBoundSql(parameterObject);}this.boundSql = boundSql;this.parameterHandler = configuration.newParameterHandler(mappedStatement, parameterObject, boundSql);this.resultSetHandler = configuration.newResultSetHandler(executor, mappedStatement, rowBounds, parameterHandler, resultHandler, boundSql);}

注入抽象方法

前提:
mybatis的mapper类不能有重载方法Mapper类中存在名称相同的方法重载报错,所以生成的MappedStatement的id只由完全类限定名 + 方法名构成。

  1. com.baomidou.mybatisplus.core.injector.AbstractSqlInjector#inspectInject
  2. com.baomidou.mybatisplus.core.injector.AbstractMethod#injectMappedStatement

以deleteById为例,

public class DeleteById extends AbstractMethod {@Overridepublic MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {String sql;SqlMethod sqlMethod = SqlMethod.LOGIC_DELETE_BY_ID;if (tableInfo.isLogicDelete()) {sql = String.format(sqlMethod.getSql(), tableInfo.getTableName(), sqlLogicSet(tableInfo),tableInfo.getKeyColumn(), tableInfo.getKeyProperty(),tableInfo.getLogicDeleteSql(true, false));SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, Object.class);return addUpdateMappedStatement(mapperClass, modelClass, sqlMethod.getMethod(), sqlSource);} else {sqlMethod = SqlMethod.DELETE_BY_ID;sql = String.format(sqlMethod.getSql(), tableInfo.getTableName(), tableInfo.getKeyColumn(),tableInfo.getKeyProperty());SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, Object.class);return this.addDeleteMappedStatement(mapperClass, sqlMethod.getMethod(), sqlSource);}}
}

http://www.ppmy.cn/news/503155.html

相关文章

video标签播放本地视频只有声音没有图像的解决办法-视频转码格式工厂或FFmpeg

目前video标签只支持MP4&#xff0c;WebMail&#xff0c;Ogg格式的视频。 MP4MPEG4文件使用H264视频编解码器和AAC音频编解码器 WebMWebM文件使用VP8视频编解码器和Vorbis音频编解码器 OggOgg文件使用Theora视频编解码器和Vorbis音频编解码器 MP4有3种编码&#xff0c;MPEG…

解决在页面中播放视频时只有声音却不显示图像的问题

视频上传后&#xff1a;网页播放只有声音不显示图像。 左&#xff1a;正常视频播放。 右&#xff1a;只有声音没有图像。 原因&#xff1a; 视频不光区分格式&#xff0c;相同的格式的情况下还区分编码。就拿mp4格式的视频来说吧&#xff0c;mp4有3种编码&#xff0c;mpg4(…

54 KVM工具使用指南-vmtop使用指南

文章目录 54 KVM工具使用指南-vmtop使用指南54.1 概述54.1.1 多架构支持54.1.2 显示项说明54.1.2.1 AArch64和x86_64架构公共显示项54.1.2.2 仅AArch64架构的显示项54.1.2.3 仅x86_64架构的显示项 54.2 使用方法54.2.1 语法格式54.2.2 选项说明54.2.3 快捷键 54.3 示例 54 KVM工…

理想吹响城市NOA号角:激光雷达车型又火了

作者 | 德新编辑 | 王博 2023下半年&#xff0c;以华蔚小理为代表的智能驾驶头部厂商&#xff0c;其高阶辅助驾驶全面进城。 在过去短短一周时间里&#xff0c;蔚来、华为、理想、小鹏紧锣密鼓悉数公布了新进展。此外据HiEV了解&#xff0c;比亚迪、智己、飞凡等品牌的智驾团队…

RPA×IDP×AIGC,实在智能打造全新“超进化”文档审阅超自动化解决方案

企业商业活动频繁&#xff0c;每日都有大量文档被创建、书写、传递&#xff0c;需要人工审阅核查&#xff0c;以确保其准确性和合法性。这是对企业文档管理的一个巨大挑战&#xff0c;尤其对于金融机构、审计机构等文本相关岗位的工作人员来说更是如此。传统的文档审核通常需要…

用Photoshop脚本批量为照片增加拍摄时间水印

效果 感谢 本脚本参考了&#xff1a;laozeng, https://github.com/laozeng1024 核心功能 如果照片为横幅&#xff0c;则顺时针旋转90后&#xff0c;再增加水印&#xff1b;如果exif中没有拍摄时间&#xff0c;从xmp信息里取&#xff1b;增加 JPEG 保存时存储质量等设置&…

PS教程!手把手教你打造一个配色小清新的地图图标

P大点S微博 &#xff1a;今天的教程是一个小清新风格的定位图标&#xff0c;操作不复杂&#xff0c;重点是图标样式的应用。源文件已打包&#xff0c;来收&#xff01; PSD源文件 打包下载&#xff1a;http://pan.baidu.com/s/1qWYXqlm 效果图&#xff1a; 新建文档&#xf…

史上最全的前端资源汇总(上)

1.综合类 综合类地址前端知识体系http://www.cnblogs.com/sb19871023/p/3894452.html前端知识结构https://github.com/JacksonTian/fksWeb前端开发大系概览https://github.com/unruledboy/WebFrontEndStackWeb前端开发大系概览-中文版http://www.cnblogs.com/unruledboy/p/Web…