窥探Mybatis配置到执行源码剖析
mybatis自动配置过程
首先我们项目中使用mybatis如果是mybatis的话会引入依赖
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
引入这个starer,想要窥探,先从starer入手,SpringBoot自动配置会寻找spring.factories这个文件,但是这是springboot 2.7.0之前的事情了,2.7.0之后引入了org.springframework.boot.autoconfigure.AutoConfiguration.imports来代替spring.factories自动配置注册方式, 一个位于META-INF/spring目录下,作为starer这个功能是向后兼容的
首先从MybatisAutoConfiguration看起
首先明确这个一个配置类,并且实现了InitializingBean,并且加载了MybatisProperties这个主要的配置类,
1、其次开始调用的是有参构造方法,其中ObjectProvider
2、调用完属性值set开始执行InitializingBean的afterPropertiesSet方法了,
可以发现如果checkConfigLocation:true的话判断是否存在configLocation属性的配置文件,实际中没什么卵用,
3、因为没有别的依赖关系,接下来开始注入的是SqlSessionFactory这个bean,但是这个bean是通过FactoryBean来完成的,并且是单例模式, 构造方法是简单粗暴的new SqlSessionFactoryBean()方法,并且依赖与刚才的属性MybatisProperties,以及Interceptor[],
但是在SqlSessionFactoryBean类里面由于实现了InitializingBean以及FactoryBean
4、根据加载顺序,下一个是创建SqlSessionTemplate这个bean
SqlSessionTemplate这个类里面主要的可能就是创建了一个SqlSession的代理类,这个代理是jdk代理类,
5、下面就是开始扫描注册所有的mapper类了
我们知道注入spring bean的时候可以使用@Bean或者@Component等方法一一进行显式的注入,但是当有大量相同类似的bean时可以借助@Import进行注入,这里通过@Import + ImportBeanDefinitionRegistrar 通过BeanDefinition可以更加灵活的注入,以及自定义配置,也是常用的手段,像Feign也是采用这种方式进行扫描注入
但是这个地方看名字MapperScannerRegistrar,通过
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class);可以看到其实首先注入的是MapperScannerConfigurer这个类,它才是真实扫描basePackage来进行注入mapper的
这里需要注意的是,以mybatis开头的这几个属性都是配置文件中的内容,区别在于lazyInitialization和defaultScope使用了占位符,而inject-sql-session-on-mapper-scan在后续的逻辑中需要用这个属性的值,所以使用environment直接获取属性的值
仔细看defaultScope的值为\({mybatis.mapper-default-scope:},他跟\){mybatis.mapper-default-scope}又有何区别呢,
这里举个例子,custom.prop和custom.defaultProp这两个属性值在配置文件中都不存在,最后启动项目会发现根本起不来,如果将\({custom.prop}换成\){custom.prop:}在PropertyPlaceholderHelper中发现会将:后面的设置为默认值,所以这里custom.prop默认值为"",
注册完MapperScannerConfigurer内部是通过实现BeanDefinitionRegistryPostProcessor通过annotationClass也就是Mapper.class来扫描
在ClassPathMapperScanner中,根据父类ClassPathBeanDefinitionScanner进行doScan进行扫描Mapper.class的类,这里bean的定义信息还是处于BeanDefinition没有实例化状态,这个中间态我们可以大做文章,动态的修改bean的元数据信息,当调用父类进行扫描完之后,我们开始执行此类中的processBeanDefinitions来进行其他属性的赋值,当然BeanDefinition最重要的方法还是setBeanClass,如果是提前知道类型可以使用BeanDefinitionBuilder.genericBeanDefinition’
,beanClass将是即将实例化的类型,这里beanClass为MapperFactoryBean
MapperBeanFactory实现了InitializingBean,所以每个mapper后续的逻辑从这里进行
addToConfig默认值为true。这里面主要的逻辑就是将mapper交给Configurationl里面的mapperRegistry进行维护处理
内部是一个Map<Class, MapperProxyFactory>的一个map,
其中在addMapper的时候,会new一个MapperProxyFactory放入map,通过名字也可以看出来这是mapper类的一个代理工厂,主要方法为创建代理类newinstance
同时在addMapper中有一个非常重要的东西,看名字是解析注解类的,但是其实parse方法里面也包含了parseCache、parseCacheRef、parseResultMap、parseStatement,因为sql结果返回的类型,执行sql的类型,这些都是固定的,而且只需要执行一次,所以在这个时候提前执行可以避免不必要的错,只有sql的参数是在后续逻辑执行sql之前进行解析的
但是可以看到parse.parse()并没有返回结果,所以里面肯定是有猫腻,大概是借助第三个类将解析完的结果存了起来,最后发现有一个助理类MapperBuilderAssistant来帮助完成这些功能,(Mybatis plus里面的baseMapper也是借助这个)当然最后的存储都是放到了Configuration里面,parse方法里面比较重要的应该就是parseStatement这个了,如果有解析失败的,会放进Configurations的incompleteMethods里面,等待所有的方法都执行完了,会进行重试,这里有的项目可能用的多,有的可能用的不是很多,这个方法主要解析带有这些注解的方法Select.class, Update.class, Insert.class, Delete.class, SelectProvider.class, UpdateProvider.class,InsertProvider.class, DeleteProvider.class,
有的喜欢在@SelectProvier里面写大篇sql的这个地方会执行耗时长一些,有的喜欢xml写的 loadXmlResource 这方法会耗时长一些
解析xml文件的逻辑也比较复杂,这里先不看了,
解析的时候,不管是注解形式的还是xml形式的,最后都是通过助理类MapperBuilderAssistant执行addMappedStatement。往Configuration里的Map<String, MappedStatement>里面放,最后在执行接口方法的时候,从这里取
MapperStatement含有很多的属性,囊括了CRUD所能用到的
至此。才执行完了addMapper方法,也执行完了父类的InitializingBean里面的checkDaoConfig方法,由于实现了FactoryBean,需要返回被实例化的对象,因为上一步addMapper时将mapper都放到了Configuration里面,所以这里取的时候也是从Configuration里面取
同时在MapperRegistry这个类里面,看名字以为只是负责mapper注册,不要被这个名字给骗了,其实他有个getMapper方法,而且这里面也是自己维护了一个mapper的map
Map<Class, MapperProxyFactory> knownMappers = new HashMap<>();
在调用getMapper的时候,会执行代理工厂类的newInstance方法,所以其实注入spring的mapper bean是代理类,并且在调用mapper里面方法的时候会执行MapperProxy里面的拦截器方法
最终的结果是,所有的mapper类已经即将交由spring进行实例话,并且mapper类统一放在Configuration里面的MapperRegistry,每一个mapper里面的每一个方法,包括sql,返回类型,statement放在mappedStatements缓存里面,
mybatis代理拦截实现
以上如果mapper没有其他的错误,一切顺利的话项目会启动成功,至此,其他类里面的mapper就可以正常依赖,可以开始执行sql了
从这里也可以再次验证到,往spring注入的确实是代理类MapperProxy,调用mapper方法的时候,首先会执行MapperProxy的invoke,static里面的方法不要理会,只是为了兼容jdk的版本兼容
这里又引入了MapperMethod这个类,主要用于执行mapper方法的执行,包括参数解析以及返回结果的处理,算的上是框架主要执行的逻辑,从newInstace的时候这个sqlSession一直 作为参数传了进来,后续执行sql的时候也是通过sqlSession
值得主要的是SqlComment这个内部类,可以看作是MapperStatement的精简版,因为里面所有的值通过statement取出,其中type也是在构建beandefinition的时候提前解析完成的
换句话说,从调用这个mapper的方法之前,我们就已经知道了这个方法的sql类型,以及返回的结果类型,相比于select, insert、update、delete的步骤就简单很多了,显示解析参数,然后执行sql,select的话由于返回的类型居多,所以需要进一步判断,比如Mybatis plus的返回分页类型是IPage,也需要在这个时候进行判断,
这里以查询多条结果executeForMany为例,这里sqlSessioin是SqlSessionTemplate
这里首先进行参数解析,涉及到ParamNameResolver,里面是一个TreeMap用来存放变量,解析方法的参数以及注解@Param,可以看到@Param注解是可有可无,不是必须的,实际解析的时候是根据参数顺序来的,
aMethod(@Param("M") int a, @Param("N") int b) -> {{0, "M"}, {1, "N"}}
aMethod(int a, int b) -> {{0, "0"}, {1, "1"}}
aMethod(int a, RowBounds rb, int b) -> {{0, "0"}, {2, "1"}}
但是getNamedParams方法又将names做了一下操作,
参数解析完,就要准备执行sql之前的工作了,调用了sqlsession里面的方法,
像以上两种方法,效果是一模一样的
SqlSessionTemplate与SqlSession的最大的区别就是SqlSessionTemplate内部是sqlSession的代理类,并且是线程安全的,并通过sqlSessionUtils可以按需创建和销毁sqlSession,并且SqlSessionTemplate与spring的事务相集成,
这里可以看到最后的sqlSession其实是DefaultSqlSession
进入的DefaultSqlSession,会发现里面有个sql执行器Executor,并且selectOne方法是selectList方法之后get(0),并不是 limit 1来查询
从Configuration获取到MappedStatement就开始调用执行器里面的方法
这个执行器又是从哪来的呢,其实是刚才SaleSessionTemplate代理方法里面新建DefaultSession的时候根据executorType创建的,也就是在配置文件当中可以指定的,默认值为SIMPLE
如果cacheEnabled为true,实际上默认值也是true,会创建一个CachingExecutor执行器
最终执行的query方法
如果是特殊类型的话会在Configuration中有一个resultMapId,这个时候sql语句,以及stmt需要的参数都已经准备好了
下一步就是判断是否查询缓存,这里我们没有开启缓存,直接往下走,这里的delegate是SimpleExecutor,属于套娃类型的装饰器模式
其中queryFromDatabase的doQuery才是真正开始查询的地方,但是还没有到jdbc,其他的地方都是在维护缓存,紧接着,调用configutaion创建了RoutingStatementHandler,StatementHandler主要的作用是处理Statement
RoutingStatementHadnler相当于一个策略模式,根据statement返回对应的Handler,这里执行的sql语句为 select id, name, state, country from city where state = ?,
所以采用PREPARED,
接下来大致的流程就是属于jdbc的操作了,创建连接,创建Statement,以及statement.execute,结果处理,关闭statement,关闭conn
至此,Mybatis注入以及执行sql的逻辑大致就是这样,当然还有好多细节没时间看的,有时间再慢慢研究
Mybatis的Cache
Mybatis与Spring事务结合
Mybatis, xml文件以及LanguageDriver
Mybatis的插件机制