Mybatis源码分析(四)mapper接口方法是怎样被调用到的

一、问题

在Mybatis架构的最上层就是接口层,它定义的是与数据库交互的方式。还记不记得我们在前面章节说的那两种方式?不记得没关系,我们回忆一下。

  • Mybatis提供的API

使用Mybatis提供的API进行操作,通过获取SqlSession对象,然后根据Statement Id 和参数来操作数据库。

String statement = "com.viewscenes.netsupervisor.dao.UserMapper.getUserList";
List<User> result = sqlsession.selectList(statement);
  • mapper接口

定义Mapper接口,里面定义一系列业务数据操作方法。在Service层通过注入mapper属性,调用其方法就可以执行数据库操作。就像下面这样

public interface UserMapper {   
    List<User> getUserList();
}

@Service
public class UserServiceImpl implements UserService{
    @Autowired
    UserMapper userDao;
    @Override
    public List<User> getUserList() {
        return userDao.getUserList();
    }
}

那么,问题就来了。UserMapper 只是个接口,并没有任何实现类。那么,我们在调用它的时候,它是怎样最终执行到我们的SQL语句的呢

二、扫描

1、配置信息

说到这,我们就要看配置文件中的另外一个Bean。通过指定基本包的路径,Mybatis可以通过Spring扫描下面的类,将其注册为BeanDefinition对象。

<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
    <property name="basePackage" value="com.viewscenes.netsupervisor.dao" />
    <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"></property>
</bean>

或者有的朋友项目里还有个annotationClass的属性,即

<property name="annotationClass" value="org.springframework.stereotype.Repository" />

它的作用就是在扫描的包的时候,会过滤定义的annotationClass。如果有这个注解才会被扫描,通常会在类上以@Repository来标识。不过它的作用也仅是为了过滤而已,我们也完全可以自定义这个注解。比如:

@MyDao
public interface UserMapper {}
<property name="annotationClass" value="com.viewscenes.netsupervisor.util.MyDao" />

当然了,如果你确定基本包路径下的所有类都要被注册,那就不必配置annotationClass。

2、扫描基本包

我们来到org.mybatis.spring.mapper.MapperScannerConfigurer这个类,可以看到它实现了几个接口。其中的重点是BeanDefinitionRegistryPostProcessor。它可以 动态的注册Bean信息,方法为postProcessBeanDefinitionRegistry()

public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
    //创建ClassPath扫描器,设置属性,然后调用扫描方法
    ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
    scanner.setAnnotationClass(this.annotationClass);
    scanner.setSqlSessionFactoryBeanName(this.sqlSessionFactoryBeanName);
    //如果配置了annotationClass,就将其添加到includeFilters
    scanner.registerFilters();
    scanner.scan(this.basePackage);
}

ClassPathMapperScanner继承自Spring中的类ClassPathBeanDefinitionScanner,所以scan方法会调用到父类的scan方法,而在父类的scan方法中又调用到子类的doScan方法。

public class ClassPathMapperScanner extends ClassPathBeanDefinitionScanner {
    public Set<BeanDefinitionHolder> doScan(String... basePackages) {
        //调用Spring的scan方法。就是将基本包下的类注册为BeanDefinition
        Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);
        processBeanDefinitions(beanDefinitions);
        return beanDefinitions;
    }
}

super.doScan(basePackages)是Spring中的方法。我们在Spring系列文章中已经详细分析了,在这里就不细究。主要看它返回的是BeanDefinition的集合。

3、配置BeanDefinition

上面已经扫描到了所有的Mapper接口,并将其注册为BeanDefinition对象。接下来调用processBeanDefinitions()要配置这些BeanDefinition对象。

public class ClassPathMapperScanner extends ClassPathBeanDefinitionScanner {

    private MapperFactoryBean<?> mapperFactoryBean = new MapperFactoryBean<Object>();
    
    private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
        GenericBeanDefinition definition;
        for (BeanDefinitionHolder holder : beanDefinitions) {
            definition = (GenericBeanDefinition) holder.getBeanDefinition();
            
            //将mapper接口的名称添加到构造参数
            definition.getConstructorArgumentValues().addGenericArgumentValue(definition.getBeanClassName());
            //设置BeanDefinition的class
            definition.setBeanClass(this.mapperFactoryBean.getClass());
            //添加属性addToConfig
            definition.getPropertyValues().add("addToConfig", this.addToConfig);
            //添加属性sqlSessionFactory
            definition.getPropertyValues().add("sqlSessionFactory", new RuntimeBeanReference(this.sqlSessionFactoryBeanName));
            ......
    }
}

处理的过程很简单,就是往BeanDefinition对象中设置了一些属性。我们重点关注两个。

  • 设置beanClass

设置BeanDefinition对象的BeanClass为MapperFactoryBean<?>。这意味着什么呢?以UserMapper为例,意味着当前的mapper接口在Spring容器中,beanName是userMapper,beanClass是MapperFactoryBean.class。那么在IOC初始化的时候,实例化的对象就是MapperFactoryBean对象。

  • 设置sqlSessionFactory属性

为BeanDefinition对象添加属性sqlSessionFactory,这就意味着,在为BeanDefinition对象设置PropertyValue的时候,会调用到setSqlSessionFactory()

三、创建SqlSession的代理

上面我们说在为BeanDefinition对象设置PropertyValue的时候,会调用它的setSqlSessionFactory,我们来看这个方法。

首先,这里说的BeanDefinition对象就是beanClass为MapperFactoryBean.class的MapperFactoryBean对象。定位到这个类,我们发现它继承自org.mybatis.spring.support.SqlSessionDaoSupport

public abstract class SqlSessionDaoSupport extends DaoSupport {
    
    private SqlSession sqlSession;
    
    public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
        if (!this.externalSqlSession) {
            this.sqlSession = new SqlSessionTemplate(sqlSessionFactory);
        }
    }
}

在它的setSqlSessionFactory方法里,最终调用的是new SqlSessionTemplate()。所以sqlSession的对象其实是一个SqlSessionTemplate的实例。我们来看它的构造函数。

public class SqlSessionTemplate implements SqlSession, DisposableBean {
    public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType,
                        PersistenceExceptionTranslator exceptionTranslator) {
        
        //设置sqlSessionFactory
        this.sqlSessionFactory = sqlSessionFactory;
        //设置执行器的类型
        this.executorType = executorType;
        //异常相关处理类
        this.exceptionTranslator = exceptionTranslator;
        //sqlSession的代理
        this.sqlSessionProxy = (SqlSession) newProxyInstance(
        SqlSessionFactory.class.getClassLoader(),
        new Class[] { SqlSession.class },
        new SqlSessionInterceptor());
    }
}

对JDK动态代理熟悉的朋友,一定会先看到newProxyInstance。它是给sqlSession接口创建了一个代理类,这个代理类的处理器程序就是SqlSessionInterceptor()。不用多说,SqlSessionInterceptor肯定实现了InvocationHandler接口。
这就意味着,当调用到sqlSession的时候,实际执行的它的代理类,代理类又会调用到处理器程序的invoke()方法。

private class SqlSessionInterceptor implements InvocationHandler {
    public Object invoke(Object proxy, Method method, Object[] args){
        //内容先略过不看
    }
}

最终在setSqlSessionFactory这个方法里,sqlSession获取到的是SqlSessionTemplate实例。而在SqlSessionTemplate对象中,主要包含sqlSessionFactory和sqlSessionProxy,而sqlSessionProxy实际上是SqlSession接口的代理对象。

sqlSession对象实例

四、创建Mapper接口的代理

上面我们说到MapperFactoryBean继承自SqlSessionDaoSupport,还有一点没说的是,它同时实现了FactoryBean接口。

这就说明,MapperFactoryBean不是一个纯粹的人。啊不对,不是一个纯粹的Bean,而是一个工厂Bean。如果要声明一个Bean为工厂Bean,它要实现FactoryBean接口,这个接口就三个方法。

public interface FactoryBean<T> {
    //返回对象的实例
    T getObject() throws Exception;
    //返回对象实例的类型
    Class<?> getObjectType();
    //是否为单例
    boolean isSingleton();
}

MapperFactoryBean既然是一个工厂Bean,那么它返回就不是这个对象的本身,而是这个对象getObjectType方法返回的实例。为什么会这样呢?
在Spring中执行getBean的时候,在创建完Bean对象且完成依赖注入之后,用调用到
bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
这个方法会判断当前Bean是否为FactoryBean,如果不是就不再执行,如果是最终就会调用到它的getObject()方法。

protected Object getObjectForBeanInstance(
            Object beanInstance, String name, String beanName, RootBeanDefinition mbd) {

    if (!(beanInstance instanceof FactoryBean) || BeanFactoryUtils.isFactoryDereference(name)) {
        return beanInstance;
    }
    //getObjectFromFactoryBean最终调用的是getObject
    Object object = getObjectFromFactoryBean(factory, beanName, !synthetic);
    return object;
}

说了这么多,就是怕有的朋友对工厂Bean不了解,看这块内容的时候会比较迷惑。那么,getObject究竟会返回什么对象呢?

1、getObject()

public class MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T> {
    public T getObject() throws Exception { 
        //mapperInterface是mapper接口的对象
        return getSqlSession().getMapper(this.mapperInterface);
    }
}

getSqlSession()我们已经分析完了,它返回的就是SqlSessionTemplate对象的实例。所以,我们主要看getMapper()。

2、getMapper

public class MapperRegistry {
    public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
        final MapperProxyFactory<T> mapperProxyFactory = knownMappers.get(type);
        return mapperProxyFactory.newInstance(sqlSession);   
    }
}

我们看到,它实现比较简单。不过,有个问题是knownMappers是从哪儿来的呢?它为什么可以根据type接口就能获取到MapperProxyFactory实例呢?

是否还记得,在扫描注解式SQL声明的时候,它调用到addMapper方法,其实就是这个类。

public class MapperRegistry {
    public <T> void addMapper(Class<T> type) {
        if (type.isInterface()) {
            try {
                /注入type接口的映射
                knownMappers.put(type, new MapperProxyFactory<T>(type));
                //扫描注解
                MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
                parser.parse();
            }
        }
    }
}

这也就解释了为什么knownMappers.get(type)就能获取到MapperProxyFactory的实例,下面来看它内部到底创建了什么对象并返回的。

3、newInstance

在创建过程中,实际返回的是一个代理类,即mapper接口的代理类。

public class MapperProxyFactory<T> {

    public T newInstance(SqlSession sqlSession) {
        //mapperProxy就是一个调用程序处理器,显然它要实现InvocationHandler接口
        final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
        
        //JDK动态代理,生成的就是mapperInterface接口的代理类
        //mapperInterface就是我们的mapper接口
        //比如com.viewscenes.netsupervisor.dao.UserMapper
        return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), 
                new Class[] { mapperInterface }, mapperProxy);
    }
}
public class MapperProxy<T> implements InvocationHandler {

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //具体流程先略过....
        return method.invoke(this, args);
    }
}

看到这里,我们都已经明白了。getObject方法返回的就是mapper接口的代理类。换言之,每一个mapper接口对应的都是自身的接口代理。那么,在实际调用到mapper方法的时候,就会调用到调用程序处理器的MapperProxy.invoke(Object proxy, Method method, Object[] args)

五、总结

本章节重要阐述了Mapper接口的代理创建过程。我们简单梳理下流程:

  • 扫描mapper接口基本包,将为注册为BeanDefinition对象。

  • 设置BeanDefinition的对象的beanClass和sqlSessionFactory属性。

  • 设置sqlSessionFactory属性的时候,调用SqlSessionTemplate的构造方法,创建SqlSession接口的代理类。

  • 获取BeanDefinition对象的时候,调用其工厂方法getObject,返回mapper接口的代理类。

最后我们在Service层,通过@Autowired UserMapper userDao注入属性的时候,返回的就是代理类。执行userDao的方法的时候,实际调用的是代理类的invoke方法。
最后的最后,我们看一下这个代理类长什么样子。

mapper接口的代理类实例

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 201,468评论 5 473
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,620评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,427评论 0 334
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,160评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,197评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,334评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,775评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,444评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,628评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,459评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,508评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,210评论 3 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,767评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,850评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,076评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,627评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,196评论 2 341

推荐阅读更多精彩内容