Mybatis插件原理和整合Spring
插件编写要求(分页插件PageHelper)
自定义插件需要做到三点
1)实现Interceptor接口
public class PageInterceptor implements Interceptor{}
2)实现对应的方法。最关键的是intercept()方法里面是拦截的逻辑,需要增强的代码写在此处。
@Override
public Object intercept(Invocation invocation) throws Throwable {
return null;
}
@Override
public Object plugin(Object o) {
return null;
}
@Override
public void setProperties(Properties properties) {
}
3)在拦截器类上加上注解。注解签名制定了需要拦截的对象、拦截的方法、参数(因为方法有不同的重载,所以要指定具体的参数)。
@Intercepts({
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class,
RowBounds.class, ResultHandler.class}),
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class,
RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class})
})
插件配置
mybatis-config.xml中中注册插件,配置属性。
<plugin interceptor="com.github.pagehelper.PageInterceptor">
<property name="offsetAsPageNum" value="true"/>
<property name="rowBoundsWithCount" value="true"/>
<property name="pageSizeZero" value="true"/>
<property name="reasonable" value="true"/>
<property name="params" value="pageNum=start;pageSize=limit;"/>
<property name="supportMethodsArguments" value="true"/>
<property name="returnPageInfo" value="check"/>
</plugin>
插件解析注册
Mybatis启动时扫描
XMLConfigBuilder.pluginElement();
private void pluginElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
String interceptor = child.getStringAttribute("interceptor");
Properties properties = child.getChildrenAsProperties();
Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance();
interceptorInstance.setProperties(properties);
configuration.addInterceptor(interceptorInstance);
}
}
}
启动解析的时候,把所有的插件全部存到Configuration的InterceptorChain中,它是一个List。
QA:不修改代码怎么增强功能?多插件怎么拦截?
1.采用的是代理模式,这个也是MyBatis插件的实现原理。
2.插件是层层拦截,我们用到另一种设计模式--责任链模式。
QA:什么对象可以被拦截?那些方法可以被拦截?
这里注意的是,因为Executor有可能被二级缓存装饰,那么是先代理还是装饰,还是先装饰后代理呢?
Executor会被拦截到CachingExecutor或者BaseExecutor。
DefaultSqlSessionFactory.openSessionFromDataSource():
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
} else {
// 默认 SimpleExecutor
executor = new SimpleExecutor(this, transaction);
}
// 二级缓存开关,settings 中的 cacheEnabled 默认是 true
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
// 植入插件的逻辑,至此,四大对象已经全部拦截完毕
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
先创建基本类型,在创建二级缓存装饰,最后插件拦截。所以这里拦截的是CachingExecutor。
插件实现原理
代理类什么时候创建?
对Executor拦截的代理类是openSession()的时候创建的。
Executor executor = configuration.newExecutor(tx, execType);
StatementHandler是SimpleExecutor.doQuery()创建的;里面包含了ParameterHandler和ResultSetHandler的创建和代理。
代理怎么创建?
调用interceptorChain的pluginAll()方法。
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
遍历interceptorChain,使用Interceptor实现类的plugin()方法,对目标核心对象进行代理。
default Object plugin(Object target) {
//实现代理对象
}
这个plugin返回一个代理对象。JDK动态代理,我们需要写一个实现InvocationHandler接口的触发管理类。然后使用Proxy.newProxyInstance()创建一个代理对象。
这里Mybatis的插件机制提供一个触发管理类Plugin,实现了InvocationHandler。
创建代理对象的newProxyInstance()在这个类进行封装,就是wrap()方法。
public static Object wrap(Object target, Interceptor interceptor) {
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
Class<?> type = target.getClass();
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
if (interfaces.length > 0) {
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
new Plugin(target, interceptor, signatureMap));
}
return target;
}
在wrap的时候创建了一个Plugin对象,Plugin是被代理对象、Interceptor的一个封装对象:
new Plugin(target, interceptor, signatureMap)
持有了被代理对象和interceptor的实例:
private Plugin(Object target, Interceptor interceptor, Map<Class<?>, Set<Method>> signatureMap) {
this.target = target;
this.interceptor = interceptor;
this.signatureMap = signatureMap;
}
因为这里是for循环代理,所以某个核心对象有多个插件,会返回被代理多次的代理对象。
被代理之后,调用的流程?
在四大核心对象的一次执行过程中(可能被多次代理),因为已经被代理了,所以会触发管理类Plugin的invoke()方法。
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
Set<Method> methods = signatureMap.get(method.getDeclaringClass());
if (methods != null && methods.contains(method)) {
return interceptor.intercept(new Invocation(target, method, args));
}
return method.invoke(target, args);
} catch (Exception e) {
throw ExceptionUtil.unwrapThrowable(e);
}
}
如果被拦截的方法不为空,进入Plugin的invoke()方法,调用interceptor的intercept()方法:
return interceptor.intercept(new Invocation(target, method, args));
到了intercept()方法,也就走到了我们自己实现的拦截逻辑(例如PageInterceptor的intercept()方法)。
其中Invocation,它是对被拦截对象、方法、参数的一个封装。
当然,在执行逻辑完成后,继续执行被代理对象(四大核心对象)的原方法,需要使用method的invoke方法。
method.invoke(target, args);
拿到被代理的核心对象,继续执行它的方法(例如executor.query())。我们如何拿到被代理对象和参数呢?
这个采用了上面创建的Invocation对象,简化了参数的传递,直接提供了一个proceed()方法。原方法也可写成如下方法:
return invocation.proceed();
public Object proceed() throws InvocationTargetException, IllegalAccessException {
return method.invoke(target, args);
}
总结:
DefaultSqlSession类select()方法流程
如果对象被代理多次,这里会继续调用下一个插件的逻辑,再走一次Plugin的invoke()方法。这里需要注意多个插件的运行顺序。
配置的顺序和执行的顺序?
配置的顺序和执行的顺序是相反的。interceptorChain的List是按照插件从上往下的顺序解析、添加的。
创建的时候按照list的顺序代理。执行的时候也需要从最后代理的对象开始。
总结:
PageHelper原理
引入pageHelper的依赖,配置插件,如果需要分页,需要用到相关的工具类:
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>5.0.0</version>
</dependency>
PageHelper.startPage(pn, 10); //pageNumber, pageSize,第几页,每页几条
List<T> lists = Service.getAll();
PageInfo page = new PageInfo(lists, 10);
return page;
插件的优点就是不用修改Mybatis本身的代码。
QA:SQL改写的实现?PageHelper实现分页的原理?
首先看一下拦截器,PageInterceptor类。
首先判断是否需要count获取总数,默认是true。获得count之后,判断是否需要分页,如果pageSize > 0,就分页。
这里通过getPageSql()方法生成了一个新的BoundSql:
String pageSql = this.dialect.getPageSql(ms, boundSql, parameter, rowBounds, cacheKey);
BoundSql pageBoundSql = new BoundSql(ms.getConfiguration(), pageSql, boundSql.getParameterMappings(), parameter);
getPageSql()对于不同的数据库有不同的实现:
以MYSQL为例,实际上是添加了LIMIT语句,加上起始位置和结束位置。
public String getPageSql(String sql, Page page, CacheKey pageKey) {
StringBuilder sqlBuilder = new StringBuilder(sql.length() + 14);
sqlBuilder.append(sql);
if (page.getStartRow() == 0) {
sqlBuilder.append(" LIMIT ");
sqlBuilder.append(page.getPageSize());
} else {
sqlBuilder.append(" LIMIT ");
sqlBuilder.append(page.getStartRow());
sqlBuilder.append(",");
sqlBuilder.append(page.getPageSize());
pageKey.update(page.getStartRow());
}
pageKey.update(page.getPageSize());
return sqlBuilder.toString();
}
那么插件是怎么获取到页码和每页数量的,是怎么传递给插件的?
这个在PageHelper.startPage()方法可以找到答案。startPage()调用了PageMethod的setLocalPage()方法,包装了一个Page对象,并把这个对象放到ThreadLocal变量中。
protected static void setLocalPage(Page page) {
LOCAL_PAGE.set(page);
}
而在AbstractHelperDialect中,Page对象中的翻页信息使用过getLocalPage()取出来的:
public String getPageSql(MappedStatement ms, BoundSql boundSql, Object parameterObject, RowBounds rowBounds, CacheKey pageKey) {
String sql = boundSql.getSql();
Page page = this.getLocalPage();
return this.getPageSql(sql, page, pageKey);
}
它调用的正式PageHelper的getLocalPage(),从ThreadLocal中获取到了翻页信息。
public static <T> Page<T> getLocalPage() {
return (Page)LOCAL_PAGE.get();
}
所以每次查询都会有一个线程私有的Page对象,它里面有页码和每页数量。
关键类:
使用场景
作用 | 描述 | 实现方式 |
---|---|---|
水平分表 | 可以进行水平分表的查询 | 对query update进行拦截,在接口上添加注解,通过反射获取接口注解,根据主键上的配置进行分表,修改原SQL |
数据脱敏 | 手机号和身份证在数据库完整存储,屏蔽手机号的中间四位。 | query--对结果集脱敏 |
菜单权限控制 | 不同的用户登录,查询菜单权限表时获得不同的结果,在签单展示不同的菜单 | 对query方法进行拦截,在方法上添加注解,根据权限配置,以及用户登录信息,在SQL上加上权限过滤条件 |
整合Spring
关键配置
pom依赖
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>2.0.4</version>
</dependency>
<!-- mybatis -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.1</version>
</dependency>
SqlSessionFactoryBean
MapperScannerConfigurer
第一种配置一个MapperSacnnerConfigurer.
<bean id="mapperScanner" class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.xx.dao"/>
</bean>
第二种配置一个
<mybatis-spring:scan #base-package="com.xx.dao"/>
采用注解的方式@MapperScan注解,比如在Spring Boot的启动类上加上一个注解:
@MapperScan("com.xx.dao")
这三种效果都是一样的。
经过这两步(SqlSessionFactoryBean + MapperScannerConfigurer)配置以后,Mapper就可以注入到Service层了,Mybatis其他的代码和配置不需要进行任何的改动。
它是如何实现的呢?
只要我们理解了SqlSessionFactory、sqlSession、MapperProxy这三个对象怎么创建的,就理解了Spring继承Mybatis的原因。
1)SqlSessionFactory在哪里创建的。
2)SqlSession在哪里创建的。
3)代理类在哪里创建的。
创建会话工厂SqlSessionFactory
在springboot需要自己实现:
@Configuration
public class SqlSessionConfig {
private Logger logger = LoggerFactory.getLogger(SqlSessionConfig.class);
@Value("${spring.datasource.jndi-name}")
private String dataSourceJndiName;
@Value("${mybatis.mapper-locations}")
private String mapperLocations;
@Bean
public SqlSessionFactoryBean createSqlSessionFactory() {
SqlSessionFactoryBean sqlSessionFactoryBean = null;
try {
// 加载JNDI配置
Context context = new InitialContext();
DataSource dataSource = (DataSource)context.lookup(dataSourceJndiName);
// 实例SessionFactory
sqlSessionFactoryBean = new SqlSessionFactoryBean();
// 配置数据源
sqlSessionFactoryBean.setDataSource(dataSource);
// 加载MyBatis配置文件
PathMatchingResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
// 能加载多个,所以可以配置通配符(如:classpath*:mapper/**/*.xml)
sqlSessionFactoryBean.setMapperLocations(resourcePatternResolver.getResources(mapperLocations));
// 配置mybatis的config文件
sqlSessionFactoryBean.setConfigLocation("mybatis-config.xml");
} catch (Exception e) {
logger.error("创建SqlSession连接工厂错误:{}", e);
}
return sqlSessionFactoryBean;
}
}
spring:
# db
datasource:
jndi-name: 'java:comp/env/jdbc/spring_db'
# mybatis config
mybatis:
mapper-locations: classpath*:mapper/**/*.xml
sqlSessionFactoryBean的内容:
public class SqlSessionFactoryBean implements FactoryBean<SqlSessionFactory>, InitializingBean,
ApplicationListener<ApplicationEvent> {
}
他实现了三个接口:FactoryBean、InitializingBean、ApplicationListener
InitializingBean
实现了InitializingBean接口,所以要实现afterPRopertiesSet()方法,这个方法会在bean的属性值设置完的时候被调用。
public void afterPropertiesSet() throws Exception {
Assert.notNull(this.dataSource, "Property 'dataSource' is required");
Assert.notNull(this.sqlSessionFactoryBuilder, "Property 'sqlSessionFactoryBuilder' is required");
Assert.state(this.configuration == null && this.configLocation == null || this.configuration == null
|| this.configLocation == null, "Property 'configuration' and 'configLocation' can not specified with together");
this.sqlSessionFactory = this.buildSqlSessionFactory();
}
在afterPropertiesSet()方法里面,通过一些检查之后,调用buildSqlSessionFactory()方法。
这里创建了一个Configuration对象,叫做targetConfiguration。还创建了一个用来解析全局配置文件的XMLConfigBuilder。
XMLConfigBuilder xmlConfigBuilder = null;
Configuration targetConfiguration;
判断configuration对象是否已经存在,也就是判断是否解析过。如果已经有对象,就覆盖一下属性。
if (this.configuration != null) {
targetConfiguration = this.configuration;
if (targetConfiguration.getVariables() == null) {
targetConfiguration.setVariables(this.configurationProperties);
} else if (this.configurationProperties != null) {
targetConfiguration.getVariables().putAll(this.configurationProperties);
}
}
如果Configuration不存在,但配置了configLocation属性,就根据mybatis-config.xml的文件路径,构建了一个xmlConfigBuilder对象。
else if (this.configLocation != null) {
xmlConfigBuilder = new XMLConfigBuilder(this.configLocation.getInputStream(), (String)null, this.configurationProperties);
targetConfiguration = xmlConfigBuilder.getConfiguration();
}
如果Configuration不存在,configLocation路径也没有,只能使用默认属性去构建去给configurationProperties赋值。
else {
LOGGER.debug(
() -> "Property 'configuration' or 'configLocation' not specified, using default MyBatis Configuration");
targetConfiguration = new Configuration();
Optional.ofNullable(this.configurationProperties).ifPresent(targetConfiguration::setVariables);
}
创建一个用来解析Mapper.xml的XMLMapperBuilder,调用了它的parse()方法。这个步骤我们主要是做了两件事情,
一是把增删改查标签注册成MapperStatement对象。第二个是把接口和对应的MapperProxyFactoty工厂类注册到MapperRegistry中。
XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(),
targetConfiguration, mapperLocation.toString(),
targetConfiguration.getSqlFragments());
xmlMapperBuilder.parse();
最后返回一个DefaultSqlSessionFactory。
return this.sqlSessionFactoryBuilder.build(targetConfiguration);
总结
通过定义一个实现了InitializingBean接口的SqlSessionFactoryBean类,里面的有个afterPropertiesSet()方法会在bean的属性值设置完的时候被调用。Spring在启动初始化这个bean的时候,完成了解析和工厂类的创建工作。
FactoryBean
这个类作用是让用户可以自定义实例化bean的逻辑。如果从BeanFactory中根据Bean的ID获取一个bean,它获取的其实是FactoryBean的getObject()返回的对象。
@Override
public SqlSessionFactory getObject() throws Exception {
if (this.sqlSessionFactory == null) {
afterPropertiesSet();
}
return this.sqlSessionFactory;
}
ApplicationListener
比如这里监听了ContextRefreshListener(上下文刷新事件),会在Spring容器加载完之后执行。
这里是检查ms是否加载完毕。
public void onApplicationEvent(ApplicationEvent event) {
if (failFast && event instanceof ContextRefreshedEvent) {
// fail-fast -> check all statements are completed
this.sqlSessionFactory.getConfiguration().getMappedStatementNames();
}
}
SqlSessionFactoryBean用到的Spring扩展点总结:
创建会话SqlSession
为什么不直接使用DefaultSqlSession?
因为它是线程不安全的。
Note that this class is not Thread-Safe.
所以,在Spring里面,我们要保证SqlSession实例的线程安全,必须为每一次请求创建一个sqlSession。但是每一次请求用openSession()自己去创建,
又会比较麻烦。
在mybatis-spring的包中,提供了一个线程安全的SqlSession的包装类,用来代替SqlSession,这个类就是SqlSessionTemplate。
因为它是线程安全的,所以可以在所有的Dao层共享一个实例(默认是单例的)。
Thread safe, Spring managed,
SqlSessionTemplate虽然和DefaultSqlSession一样定义了数据操作的接口,但是没有自己的实现,全部调用了一个代理对象的方法。
public <E> List<E> selectList(String statement, Object parameter) {
return this.sqlSessionProxy.selectList(statement, parameter);
}
那么,这个代理独享怎么来的?在构造方法里面通过JDK动态代理创建:
this.sqlSessionProxy = (SqlSession) newProxyInstance(SqlSessionFactory.class.getClassLoader(),new Class[] { SqlSession.class },
new SqlSessionInterceptor());
它是对SqlSession实现类DefaultSqlSession的代理。既然是JDK动态代理,那对代理类任意方法的调用都会走到(第三个参数)实现了InvocationHandler接口的触发管理类SqlSessionInterceptor的invoke()方法。
SqlSessionInterceptor是一个内部类
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);
...
}
}
这里会用getSqlSession()方法创建一个SqlSession对象,把SqlSessionFactory、执行器类型、异常解析器传进去。
获取到sqlSession实例(DefaultSqlSession)后,在调用它的增删改查方法。
总结
因为DefaultSqlSession自己做不到每次请求调用产生一个新的实例,我们干脆创建一个代理类,也实现SqlSession,提供了跟DefaultSqlSession实例,
在调用被代理对象的相应方法。
和JdbcTemplate、RedisTemplate一样,SqlSessionTemplate可以简化Mybatis在Spring中的使用,也是Spring和Mybatis整合的最关键的一个类。
怎么拿到一个SqlSessionTemplate是线程安全的,可以替换DefaultSqlSession,那么在Dao层是怎么拿到SqlSessionTemplate呢?
可以使用new一个创建的方式,但它有三个重载的构造函数。而且这个单例的SqlSessionTemplate必须存起来放在一个地方,可以在任何需要代替DefaultSqlSession的地方都可以拿到,不能重复创建,否则就不是单例了。
因为需要存在一个地方,所以,我们是不是可以提供一个工具类来获取单例的SqlSessionTemplate呢?
Mybatis里面和Hibernate也是一样的,它提供了一个抽象的支持类SqlSessionDaoSupport(这里Hibernate使用HibernateDaoSupport)。
SqlSessionDaoSupport类中持有一个SqlSessionTemplate对象,并且提供了一个getSqlSession()方法,让我们获得一个SqlSessionTemplate。
public abstract class SqlSessionDaoSupport extends DaoSupport {
private SqlSessionTemplate sqlSessionTemplate;
public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
if (this.sqlSessionTemplate == null || sqlSessionFactory != this.sqlSessionTemplate.getSqlSessionFactory()) {
this.sqlSessionTemplate = createSqlSessionTemplate(sqlSessionFactory);
}
}
//其他代码省略
}
也就是说让我们Dao层继承抽象类SqlSessionDaoSupport,就自动拥有了getSqlSession()方法。调用getSqlSession()就能拿到共享的SqlSessionTemplate。
但Dao执行SQL格式还是不够简洁。
getSqlSession.selectOne(statement, parameter);
所以我们需要先创建一个BaseDao继承SqlSessionDaoSupport。在BaseDao里面封装对数据库的操作,包括selectOne()、
selectList()、insert()、delete()这些方法,子类就可以直接调用。
public class BaseDao extends SqlSessionDaoSupport {
//使用sqlSessionFactory
@Autowired
private SqlSessionFactory sqlSessionFactory;
@Autowired
public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
super.setSqlSessionFactory(sqlSessionFactory);
}
/**
* 获取Object对象
*
* @param statement
* @return
*/
public Object selectOne(String statement) {
return getSqlSession().selectOne(statement);
}
public Object selectOne(String statement, Object parameter) {
return getSqlSession().selectOne(statement, parameter);
}
//其他代码省略
}
然后让我们Dao层继承BaseDao实现Mapper接口。在实现类加上@Repository注解就可以了。但这样还是比较麻烦的还需要实现DaoImpl。
那有没有更好的方式呢?
我们通过上面的方式操作数据库,繁琐,而且还会出现Statement ID的硬编码问题。没有使用到JDK动态代理。那么如何解决呢?
当我们使用Spring来调用Mybatis的时候。只需要注入一个Mapper就可以使用,那么是怎么实现的?
接口的扫描注册
首先Spring可以通过配置或者是注解来扫描Mapper的接口。
其中当使用xml的时候,需要配置:
<bean id="mapperScanner" class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.xx.dao"/>
</bean>
其中MapperScannerConfigurer是来做mapper的扫描的,由上面类图可以看出:
MapperScannerConfigurer实现了BeanDefinitionRegistryPostProcessor接口。
BeanDefinitionRegistryPostProcessor是BeanFactoryPostProcessor的子类,里面有一个postProcessBeanDefinitionRegistry()方法。
实现了这个接口,就可以在Spring创建Bean之前,修改某些Bean在容器中的定义。Spring创建Bean之前会调用这个方法。
MapperScannerConfigurer重写了postProcessBeanDefinitionRegistry(),那他实现了什么功能呢?
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
if (this.processPropertyPlaceHolders) {
processPropertyPlaceHolders();
}
ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
scanner.setAddToConfig(this.addToConfig);
scanner.setAnnotationClass(this.annotationClass);
scanner.setMarkerInterface(this.markerInterface);
scanner.setSqlSessionFactory(this.sqlSessionFactory);
scanner.setSqlSessionTemplate(this.sqlSessionTemplate);
scanner.setSqlSessionFactoryBeanName(this.sqlSessionFactoryBeanName);
scanner.setSqlSessionTemplateBeanName(this.sqlSessionTemplateBeanName);
scanner.setResourceLoader(this.applicationContext);
scanner.setBeanNameGenerator(this.nameGenerator);
scanner.setMapperFactoryBeanClass(this.mapperFactoryBeanClass);
if (StringUtils.hasText(lazyInitialization)) {
scanner.setLazyInitialization(Boolean.valueOf(lazyInitialization));
}
scanner.registerFilters();
scanner.scan(
StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
}
在这个方法里面:创建了一个scanner对象,然后设置属性。
ClassPathBeanDefinitionScanner的scan();
public int scan(String... basePackages) {
int beanCountAtScanStart = this.registry.getBeanDefinitionCount();
doScan(basePackages);
// Register annotation config processors, if necessary.
if (this.includeAnnotationConfig) {
AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry);
}
return (this.registry.getBeanDefinitionCount() - beanCountAtScanStart);
}
这里会调用它的子类方法ClassPathMapperScanner的doScan()方法:
public Set<BeanDefinitionHolder> doScan(String... basePackages) {
Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);
if (beanDefinitions.isEmpty()) {
LOGGER.warn(() -> "No MyBatis mapper was found in '" + Arrays.toString(basePackages)
+ "' package. Please check your configuration.");
} else {
processBeanDefinitions(beanDefinitions);
}
return beanDefinitions;
}
子类ClassPathMapperScanner又调用了父类ClassPathBeanDefinitionScanner的doScan()扫描所有的接口,把接口全部添加到beanDefinitions中。
processBeanDefinitions()方法里面,在注册beanDefinitions的时候。BeanClass被改为MapperFactoryBean。(这里有注释讲解)
private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
GenericBeanDefinition definition;
for (BeanDefinitionHolder holder : beanDefinitions) {
definition = (GenericBeanDefinition) holder.getBeanDefinition();
String beanClassName = definition.getBeanClassName();
LOGGER.debug(() -> "Creating MapperFactoryBean with name '" + holder.getBeanName() + "' and '" + beanClassName
+ "' mapperInterface");
// the mapper interface is the original class of the bean
// but, the actual class of the bean is MapperFactoryBean
definition.getConstructorArgumentValues().addGenericArgumentValue(beanClassName); // issue #59
definition.setBeanClass(this.mapperFactoryBeanClass);
//其他代码省略
}
}
这就是说,所有的Mapper接口,在容器里面都被注册成一个支持泛型的MapperFactoryBean了。
为什么要注册成它呢?那注入使用的时候,也是这个对象,这个对象有什么作用呢?
MapperFactoryBean这个类:
public class MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T> {}
这个类继承了抽象类SqlSessionDaoSupport,这就解决了我们的第一个问题,现在每一个注入Mapper的地方,都可以拿到SqlSessionTemplate。
那有没有使用到MapperProxy呢?如果注册的是MapperFactoryBean,难道注入使用的也是MapperFactoryBean吗?但这个类并不是代理类。
接口注入使用
所以注入的是一个什么对象呢?这里MapperFactoryBean也实现了FactoryBean。它可以在getObject()中获取Bean实例的行为。
public T getObject() throws Exception {
return getSqlSession().getMapper(this.mapperInterface);
}
它并没有直接返回一个MapperFactoryBean。而是调用了SqlSessionTemplate的getMapper()方法。SqlSessionTemplate的本质是一个代理,所以它最终会调用DefaultSqlSession的getMapper()方法,最后返回的还是一个JDK的动态代理。
总结
1.提供了SqlSession的代替品SqlSessionTemplate,里面有一个实现了InvocationHandler的内部SqlSessionIntercepter,本质是对SqlSession的代理。
2.提供了获取SqlSessionTempldate的抽象类SqlSessionDaoSupport。
3.扫描Mapper接口,注册到容器中的是MapperFactoryBean,它继承了SqlSessionDaoSupport,可以获得SqlSessionTempldate。
4.把Mapper注入使用的时候,调用的是getObject()方法,它实际上是调用了SqlSessionTemplate的getMapper()方法,注入了一个人JDK动态代理对象。
5.执行Mapper接口的任意操作,会走到触发管理类MapperProxy,进入SQL处理流程。