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启动时扫描标签,注册到configuration对应那个的InterceptorChain中。Properties里面的参数,会调用setProperties()方法处理。

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让SqlSessionFactoryBean有能力监控应用发出的一些事件通知。

比如这里监听了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处理流程。

Mybatis调用的设计模式总结

posted @ 2020-07-09 18:45  snail灬  阅读(379)  评论(0编辑  收藏  举报