0.前言
这段时间自己练习了一个springboot项目云E办,项目架构如下图所示,集成MybatisPlus后,项目不需要进行mapper层接口实现,如下图第一个方框中的mapper包是mapper接口,第二个mapper包是xml文件。对于MP底层如何实现很感兴趣,通过多方查询及debug测试,稍稍弄懂了一些,现将自己的学习过程记录一下,欢迎各位大佬指正。
1.mapper接口的扫描
在本项目的启动类中,存在 @MapperScan注解,从此处发,开始进行讲解。@MapperScan注解上存在注解 @Import(MapperScannerRegistrar.class)。此处,@Import存在三种导入方式,分别是:
- 带有 @Configuration注解的配置类(4.2 版本之前只可以导入配置类,4.2版本之后 也可以导入 普通类)
- 实现
ImportSelector
- 实现
ImportBeanDefinitionRegistrar
这里属于第三种,即在MapperScannnerRegistrar
中实现ImportBeanDefinitionRegistrar
接口,从而注册MapperScannnerConfigurer
。
在MapperScannerConfigurer
类中,有一个方法postProcessBeanDefinitionRegistry()
,在这个方法中,new一个ClassPathMapperScanner
类的对象(父类是ClassPathBeanDefinitionScanner
),通过该对象实现对mapper包的扫描。该过程首先调用父类中的scan()
方法,再调用自己类中的doScan()
方法,在子类中的doScan()中又super.doScan()...(debug给我看懵了,还能这么跳的?)
-------------------------------------ClassPathMapperScanner------------------------------------------
public Set<BeanDefinitionHolder> doScan(String... basePackages) {
Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);
...
processBeanDefinitions(beanDefinitions);
...
return beanDefinitions;
其中,在子类doScan()方法中,调用processBeanDefinitions()
方法,该方法生成mapper接口的工厂beanMapperFactoryBean
,这样每次获取mapper 实例实际是通过MapperFactoryBean
的实例去获取(具体的mapper实例又涉及到动态代理)。而在源码中,存在如下注解,证明mapper接口的实例是通过MapperFactoryBean
得到。
-----------------------------ClassPathMapperScanner------------------------------------
private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
...
definition.setBeanClass(this.mapperFactoryBeanClass);
....
// the mapper interface is the original class of the bean
// but, the actual class of the bean is MapperFactoryBean
2.mapper接口的调用
在实现mapper接口的扫描后,我们现在手中拥有mapper接口的工厂,当serviceImpl类中进行 @Autowired时,我们需要进行“加工生产”,即MapperFactoryBean.getObject()
。
-------------------------MapperFactoryBean-------------------------
public T getObject() throws Exception {
return this.getSqlSession().getMapper(this.mapperInterface);
}
------------------------SqlSessionDaoSupport--------------------------
public SqlSession getSqlSession() {
return this.sqlSessionTemplate;
}
------------------------SqlSessionTemplate--------------------------
public <T> T getMapper(Class<T> type) {
return getConfiguration().getMapper(type, this);
}
对了,此处有一个小问题,在其他的一些博客中,我看到博主并不使用类.方法名,而是类#方法名进行表述,希望有大神在评论区解释一下。
由源码可知,getObject()方法进行一系列调用最终来到SqlSessionTemplate
类中的getMapper()
方法。
SqlSessionTemplate是Mybatis为了接入Spring提供的Bean,
SqlSessionManager是Mybatis不接入Spring时用于管理SqlSession的Bean
两者均是SqlSession的实现类
接着,调用两个方法分别是getConfiguration()
得到Configuration
(在MP中是MybatisConfiguration
) 在getConfiguration方法中,用到一个属性sqlSessionFactory
,该属性的获取涉及到另一篇博文,其实,建议在此处停止阅读,前往另一篇博文将其中涉及到的类、属性、方法弄明白,再继续阅读,会更加清楚、容易。MybatisConfiguration.getMapper()
方法调用重要属性mybatisMapperRegistry
的getMapper()
方法。(记住它,就是它开启了动态代理!!!)
------------------------SqlSessionTemplate---------------------------
public Configuration getConfiguration() {
return this.sqlSessionFactory.getConfiguration();
}
-----------------------MybatisConfiguration--------------------------
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
return mybatisMapperRegistry.getMapper(type, sqlSession);
}
3.MybatisPlus中的动态代理
终于,终于,我们来到了动态代理部分,我一直没弄明白动态代理和AOP的关系,所以也不敢贸然写“MP中的AOP”。对于此部分,简单绘制动态代理的关系图,便于理解。
进入MybatisMapperRegistry
,找到对应的getMapper()方法,此处,全部CV过来,一步步分析。
- 第一句从
knownMappers
属性中获取mapper工厂类,并进行强转为MybatisMapperProxyFactory
。 - 接着,通过该工厂类newInstance,获取泛型T(即我们所需要的接口实例)。
进入MybatisMapperProxyFactory
中,再进行newInstance()
分析。通过源码可以看到,这里运用Prox
类获取泛型T的动态代理对象。对newProxyInstance()
中三个参数进行简要分析:
- mapperInsterface.getClassLoader() :被代理接口的类加载器,即泛型T的类加载器。
- new Class[]{mapperInterface} : 被代理接口。
- mapperProxy :InvocationHandler的实现类,重写了invoke方法(手动加星,重点中的重点)。
进入
MyBatisMapperProxy
,找到invoke()
方法,如源码所示,变量method
可以通俗理解为mapper接口调用的方法,如selectOne、selectList等等。通过一定量代码让该方法执行起来,从而完成数据库查询工作。
动态代理这一章节对mapper接口的代理实现进行简要叙述,至于一定量代码如何实现method,在第四章进行介绍,再次强调一下,建议对MybatisPlusAutoConfiguration
有一定的了解再进行第四章的阅读!
------------------------------------------MybatisMapperRegistry---------------------------------------------
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
// TODO 这里换成 MybatisMapperProxyFactory 而不是 MapperProxyFactory
final MybatisMapperProxyFactory<T> mapperProxyFactory = (MybatisMapperProxyFactory<T>) knownMappers.get(type);
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the MybatisPlusMapperRegistry.");
}
try {
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
}
}
-----------------------------------------MybatisMapperProxyFactory-----------------------------------------
public T newInstance(SqlSession sqlSession) {
final MybatisMapperProxy<T> mapperProxy = new MybatisMapperProxy<>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
protected T newInstance(MybatisMapperProxy<T> mapperProxy) {
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[]{mapperInterface}, mapperProxy);
}
-----------------------------------------MybatisMapperProxy------------------------------------------------
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
...
final MybatisMapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(sqlSession, args);
}
4.MybatisMapperProxy的invoke方法
未完待续...(2021.12.20更新)
接一下来的一章主要讲解MP与数据库的交互,也就是selectOne究竟是怎么查询一个的。
首先,还是回顾一下我们现在手中拥有的主要资源,1.mapper接口的代理对象;2.SqlSessionTemplate对象;3.MybatisConfiguration和4.MybatisMapperRegistry。接着,简要分析它们的关系,“4”在进行工厂bean的工厂bean(名词解释点击这里)建立时通过对mapperXML文件进行解析得到,并存储在“3”中。通过将“3”传入MybatisSqlSessionFactoryBuilder().build()
方法建立SqlSessionFactory
,即SqlSession
的工厂bean,从而生产“2”。根据上文对MybatisMapperRegistry
的源码分析可知,在MybatisMapperRegistry
中有一个hashmap存储着类型为MybatisMapperProxyFactory
的工厂bean,在进行 @Autowired时,通过工厂bean生产“1”。
当MP与数据库发生交互时,意味着程序开始调用mapper接口的方法。其实,也可以通过debug进行阶段区分,上述所讲内容如果进行断点调试会影响项目的启动,停滞在以下界面。而进入invoke方法进行断点调试,意味着我们来到方法实现阶段,单纯的项目启动不会调用mapper接口的方法,因此项目可以顺利来到swagger的API调试页面。
讲了这么多,接下来让我们进入invoke方法,开始从数据库获取数据吧。通过invoke进入mapperMethod.execute()
方法(这里作简单说明:由于项目配置springsecurity,在进行mapper方法调用时,会进行权限过滤,查询当前用户的权限信息,因此以下示例以AdminMapper的selectOne方法进行讲解)。 command
对象的Name
属性是selectOne的命名空间+sql语句id,通过源码也可以看出来mapper方法的调用通过SqlSession(此处是SqlSessionTemplate)进行实现。进入SqlSessionTemplate后,发现其selectOne
方法是通过代理对象进行实现,而进一步执行代码时,来到SqlSessionTemplate
中实现的一个invoke方法,不过不要紧张,这里仅仅是对method方法(这里的method已经更换为SqlSession下的selectOne)的一个实现,没有增强功能,也不存在其它复杂的逻辑(留个坑吧,感觉没这么简单)。继续debug调试,果然来到了DefaultSqlSession
(我就说怎么SqlSessionTemplate
怎么代码这么少,原来还是通过Default实现)在Default中几番跳跃,来到selectList
方法(其实selectOne都是通过selectList进行查询,然后增加两个if判断,将0个查询结果和多个查询结果的情况进行特殊处理),其实到达这里就差不多了,源码中,这个query函数代表对数据库的进一步查询操作,内部的封装太过复杂(哎,要不也不会这么流行了~),仅展示最熟悉的jdbc操作ps.execute()
。ps中的sql语句如下图所示。
----------------------------------------MybatisMapperMethod-------------------------------------
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
...
result = sqlSession.selectOne(command.getName(), param);
...
}
--------------------------------------SqlSessionTemplate--------------------------------
public <T> T selectOne(String statement, Object parameter) {
return this.sqlSessionProxy.selectOne(statement, parameter);
}
private class SqlSessionInterceptor implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
SqlSession sqlSession = getSqlSession(SqlSessionTemplate.this.sqlSessionFactory,
SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);
try {
Object result = method.invoke(sqlSession, args);
if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
// force commit even on non-dirty sessions because some databases require
// a commit/rollback before calling close()
sqlSession.commit(true);
}
return result;
...
-------------------------------------DefaultSqlSession----------------------------------
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
MappedStatement ms = configuration.getMappedStatement(statement);
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
...
-----------------------------------PreparedStatementHandler----------------------------------
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
ps.execute();
return resultSetHandler.handleResultSets(ps);
}
再顺便回顾一下通过jdbc进行数据库查询的操作过程:
- 建立数据库连接
- 预编译sql语句,返回PreparedStatement的实例
- 填充占位符
- 执行sql语句
- 资源关闭
总结
兜兜转转,终于来到结尾,这几天对源码的跟踪很让人抓狂,循环往复,无限套娃,恐怖如斯。不过,通过学习也可以发现,MP中的源码主要是对方法的封装太多,导致debug跳来跳去,而且各个类之间的属性相互包含,初学者很难提纲挈领,抓住首尾。这里,最后再将我对MP运行顺序进行分析,抛砖引玉,希望得到大佬的指点。
首先,分为三个部分,第一个部分是mapperXML文件的解析与存储;第二个部分是mapper接口代理对象的实现;第三个部分是mapper方法的实现。
未完待续...