在当今软件开发的广袤领域中,MyBatis 作为一款备受青睐的持久层框架,以其强大的功能和灵活的特性发挥着重要作用。当我们深入探究 MyBatis 的源码时,就如同开启一扇通往技术奥秘的大门。
在这里,每一行代码都蕴含着智慧与巧思,每一个模块都承载着独特的设计理念。我们将逐步揭开 MyBatis 源码那神秘的面纱,去追寻它高效数据处理、灵活映射机制以及出色性能表现背后的根源。通过对其源码的仔细剖析,我们不仅能更深刻地理解 MyBatis 是如何工作的,更能汲取其中的精髓,为我们自身的技术成长和项目实践提供宝贵的经验和启示。让我们怀揣着对技术的好奇与探索之心,正式踏上 MyBatis 源码解析的精彩旅程……
详解Mybatis的功能架构与核心技术
按照工作层次划分可以分为三层:
- 接口层:也就是我们用户用到的这一层,提供各种对数据的CRUD以及配置信息维护的API调用。
- 数据处理层:这层是框架为上层提供的关键,这一层实现参数映射,SQL解析,SQL执行,结果处理。
- 基础支撑层:负责连接管理、配置加载,事务管理、缓存机制等。
详解Mybatis执行过程
本质上mybatis执行过程大体是:
- 参数映射
- sql解析
- sql执行
- 结果和处理映射
我们以下面这段查询代码为例,针对该流程进行深入讲解:
Tb1Example tb1Example = new Tb1Example();
tb1Example.createCriteria().andBirthdayIsNull();
List<Tb1> tb1List = SpringUtil.getBean(Tb1Mapper.class).selectByExample(tb1Example);
log.info("tb1List: {}", tb1List);
本质上我们所使用的Tb1Mapper是基于我们的xml配置动态代理生成的一个MapperProxy,在执行查询请求时被本质上就调用这个生成代理对象,以我们的selectByExample为例,在初始配置的时候我们指明了select标签在进行代理创建时该方法就会被标准为SELECT命令请求,执行时就会按照代理的查询逻辑执行。
随后代理的MapperProxy会调用MapperMethod进行参数解析,将参数转换为后续可拼接到xml中所配置sql语句中的参数。
然后SqlSessionTemplate通过内部sqlSessionProxy的selectList着手进行实际查询工作,其内部会拿到当前sql连接的session和xml中配置的sql还有我们上述步骤的参数生成jdbc的Statement然后通过SimpleExecutor执行sql查询,然后通过resultSetHandler将结果解析并返回:
对此我们也给出相应的源码,首先从MapperProxy开始调用mapperMethod进行参数解析。
//MapperProxy的invoke方法调用mapperMethod
@Override
public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
return mapperMethod.execute(sqlSession, args);
}
//MapperMethod解析参数并基于指令匹配SQL操作
```java
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
switch (command.getType()) {
case INSERT: {
//......
}
case UPDATE: {
//......
}
case DELETE: {
//......
}
case SELECT:
if (method.returnsVoid() && method.hasResultHandler()) {
//......
} else if (method.returnsMany()) {
//内部进行参数解析和查询调用
result = executeForMany(sqlSession, args);
} else if (method.returnsMap()) {
//......
} else {
//......
}
break;
case FLUSH:
result = sqlSession.flushStatements();
break;
default:
throw new BindingException("Unknown execution method for: " + command.getName());
}
//......
return result;
}
随后步入MapperMethod进行通过convertArgsToSqlCommandParam参数解析,底层在基于sqlSession着手查询和结果转换:
private <E> Object executeForMany(SqlSession sqlSession, Object[] args) {
List<E> result;
//参数解析
Object param = method.convertArgsToSqlCommandParam(args);
if (method.hasRowBounds()) {
RowBounds rowBounds = method.extractRowBounds(args);
//
result = sqlSession.selectList(command.getName(), param, rowBounds);
} else {
result = sqlSession.selectList(command.getName(), param);
}
//......
return result;
}
最终来到SimpleExecutor的doQuery方法,通过xml配置所得的各种信息生成StatementHandler创建出Statement ,再通过resultSetHandler处理结果并返回:
@Override
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Statement stmt = null;
try {
//通过配置信息生成StatementHandler
Configuration configuration = ms.getConfiguration();
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
//基于StatementHandler 生成Statement
stmt = prepareStatement(handler, ms.getStatementLog());
//内部通过Statement 执行sql并将结果交由resultSetHandler转换并返回
return handler.query(stmt, resultHandler);
} finally {
//......
}
}
为什么Mybatis不需要实现类
是通过代理生成的,我们不妨通过源码来看看究竟,以下面这段代码作为入口讲解原生mapper创建思路:
SqlSession sqlSession = SpringUtil.getBean(SqlSessionFactory.class).openSession();
Tb1Mapper mapper = sqlSession.getMapper(Tb1Mapper.class);
本质上getMapper会基于接口和sqlSession信息通过mapper创建工厂mapperProxyFactory ,然后mapperProxyFactory 底层通过反射的方式创建JDK动态代理mapper对象:
对此我们给出getMapper的入口,逻辑和笔者说的一样mapperProxyFactory 传递元信息进行动态代理创建:
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
//......
try {
//通过mapperProxyFactory创建动态代理
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
}
}
查看newInstance方法即可看到我们所说的基于类加载器、接口信息和methodCache内部的MapperMethodInvoker完成动态代理对象的创建:
protected T newInstance(MapperProxy<T> mapperProxy) {
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
如何实现Mybatis插件
Mybatis支持对ParameterHandler、ResultSetHandler、StatementHandler、Executor进行拦截,例如我们想对mybatis查询的SQL结果解析阶段进行拦截,我们可以编写下面这样一段代码:
import java.sql.Statement;
//@Intercepts({@Signature(
// type = Executor.class, //确定要拦截的对象
// method = "query", //确定要拦截的方法
// args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class} //拦截方法的参数
//)})
@Intercepts({@Signature(
type = ResultSetHandler.class, //确定要拦截的对象
method = "handleResultSets", //确定要拦截的方法
args = {Statement.class} //拦截方法的参数
)})
public class MyInterceptor implements Interceptor {
private static Logger logger = LoggerFactory.getLogger(MyInterceptor.class);
@Override
public Object intercept(Invocation invocation) throws Throwable {
logger.info("请求被拦截,拦截类:[{}],请求方法:[{}]请求参数[{}]", invocation.getTarget().getClass().getName(),
invocation.getMethod().getName(),
invocation.getArgs());
//如果当前代理的是一个非代理对象,那么就会调用真实拦截对象的方法
// 如果不是它就会调用下个插件代理对象的invoke方法
Object obj = invocation.proceed();
logger.info("请求被拦截结果:[{}]", obj);
return obj;
}
}
然后配置文件,增加对这个拦截类的配置:
<plugins>
<plugin interceptor="com.sharkchili.mapper.MyInterceptor">
<property name="dbType" value="mysql"/>
</plugin>
</plugins>
执行我们的请求:
// 可以从配置或者直接编码来创建SqlSessionFactory
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
//2)通过SqlSessionFactory创建SqlSession
SqlSession sqlSession = sqlSessionFactory.openSession();
//3)通过sqlsession执行数据库操作
User1Mapper user1Mapper = sqlSession.getMapper(User1Mapper.class);
User1 user = user1Mapper.select("1");
logger.info("查询结果:[{}]", user.toString());
if (sqlSession != null) {
sqlSession.close();
}
从输出结果就可以看出,我们的方法拦截到了结果处理的逻辑了。
2022-11-30 10:11:55,389 [main] DEBUG [com.sharkchili.mapper.User1Mapper.select] - ==> Preparing: select * from user1 where id = ?
2022-11-30 10:11:55,526 [main] DEBUG [com.sharkchili.mapper.User1Mapper.select] - ==> Parameters: 1(String)
[main] INFO com.sharkchili.mapper.MyInterceptor - 请求被拦截,拦截类:[org.apache.ibatis.executor.resultset.DefaultResultSetHandler],请求方法:[handleResultSets]请求参数[[org.apache.ibatis.logging.jdbc.PreparedStatementLogger@12d2ce03]]
2022-11-30 10:12:06,928 [main] DEBUG [com.sharkchili.mapper.User1Mapper.select] - <== Total: 1
[main] INFO com.sharkchili.mapper.MyInterceptor - 请求被拦截结果:[[User1{id='1', name='小明', user2=null}]]
[main] INFO com.sharkchili.mapper.MyBatisTest - 查询结果:[User1{id='1', name='小明', user2=null}]
2022-11-30 10:12:06,938 [main] DEBUG [org.apache.ibatis.transaction.jdbc.JdbcTransaction] - Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@24959ca4]
Mybatis插件的工作原理
Mybatis如何引入自定义插件?
我们的业务代码如下,创建SqlSessionFactory:
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
步入build逻辑会看到xml解析相关代码有一行,如下所示,可以看出他就是对xml文件中plugins标签进行解析:
//获取标签内容并反射生成拦截器存到某个list中
this.pluginElement(root.evalNode("plugins"));
其内部做的就是解析xml配置,生成拦截器对象MyInterceptor,并存放到interceptorChain中的一个名为interceptors的list中。
private void pluginElement(XNode parent) throws Exception {
if (parent != null) {
Iterator var2 = parent.getChildren().iterator();
while(var2.hasNext()) {
//解析配置生成Interceptor
XNode child = (XNode)var2.next();
String interceptor = child.getStringAttribute("interceptor");
Properties properties = child.getChildrenAsProperties();
Interceptor interceptorInstance = (Interceptor)this.resolveClass(interceptor).getDeclaredConstructor().newInstance();
interceptorInstance.setProperties(properties);
//存到拦截器链中
this.configuration.addInterceptor(interceptorInstance);
}
}
}
我们看看addInterceptor,逻辑非常简单,说白了就是存到一个名为interceptors的list集合中,然后进行链式调用:
public void addInterceptor(Interceptor interceptor) {
this.interceptorChain.addInterceptor(interceptor);
}
执行真正逻辑,调用插件:
//3)通过sqlsession执行数据库操作
User1Mapper user1Mapper = sqlSession.getMapper(User1Mapper.class);
User1 user = user1Mapper.select("1");
注意在Plugin的signatureMap插个断点,如下所示:
这时候进行debug,我们可以看到堆栈中停在这样一段代码上。由于我们编写了一个结果解析的拦截插件MyInterceptor,所以在newResultSetHandler时会从上文注册的interceptorChain中取出对应处理器,给我们的resultSetHandler ,这过程中piugin类会通过Plugin.wrap(target, this);对我们的结果处理类进行包装。
public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler, ResultHandler resultHandler, BoundSql boundSql) {
//获取我们的结果解析器
ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);
//调用pluginAll将所有插件都引入
ResultSetHandler resultSetHandler = (ResultSetHandler)this.interceptorChain.pluginAll(resultSetHandler);
return resultSetHandler;
}
所以我们步入看看wrap的逻辑,可以看到interceptor即我们自己编写的插件,他会通过getSignatureMap获取这个我们编写插件MyInterceptor注解上的信息,通过反射生成一个新的代理对象,这个对象存放着signatureMap。
public static Object wrap(Object target, Interceptor interceptor) {
//获取自定义插件信息存到signatureMap 中
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
Class<?> type = target.getClass();
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
// 使用插件包装我们的目标类
return interfaces.length > 0 ? Proxy.newProxyInstance(type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap)) : target;
}
执行SQL逻辑:
最终我们的结果处理插件会在handleResultSets阶段发现signatureMap里面有值,当前处理器有拦截,执行this.interceptor.intercept(new Invocation(this.target, method, args))拦截相关处理器执行我们的MyInterceptor逻辑。
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
//发现signatureMap有值
Set<Method> methods = (Set)this.signatureMap.get(method.getDeclaringClass());
//methods 中包含我们的这个阶段的方法handleResultSets,故调用this.interceptor.intercept(new Invocation(this.target, method, args))
return methods != null && methods.contains(method) ? this.interceptor.intercept(new Invocation(this.target, method, args)) : method.invoke(this.target, args);
} catch (Exception var5) {
throw ExceptionUtil.unwrapThrowable(var5);
}
}
Mybatis的分页插件的实现原理
通过上面工作原理的介绍我们就知道原理了,分页插件就是通过mybatis提供的接口,拦截Executor的query方法,重写执行的sql,例如select * from student,拦截sql后重写为:select t.* from (select * from student) t limit 0, 10;即可。