文档章节

Mybatis深入源码分析之Mapper与接口绑定原理源码分析

须臾之余
 须臾之余
发布于 07/26 14:02
字数 2565
阅读 52
收藏 0

紧接上篇文章:Mybatis深入源码分析之SqlSessionFactoryBuilder源码分析,这里再来分析下,Mapper与接口绑定原理。

本章疑问:

// 5.操作Mapper接口
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
public interface UserMapper {
    public UserEntity getUser(int id);
}

为什么UserMapper是接口,没用实现类,那么他是怎么初始化的?getMapper()方法为什么可以调用?

mapper接口是怎么初始化的?是反射?不是的,接口是不能反射初始化。揭秘:其实是代理设计模式【动态代理】,底层使用AOP实现。

另外MyBayis中最重要的是SqlSession:操纵SQL语句。

分析源码前,我们先回顾下动态代理技术,在我的这篇博客中详细介绍了:浅谈Java【代理设计模式】——看这篇文章就懂了

思考问题:动态代理分为:jdk动态代理和CGLIB动态代理,那么Mybatis使用了那种代理设计模式?

答案:MyBatis采用的jdk动态代理,因为代理的是接口。

回顾jdk动态代理

JDK动态代理的一般步骤如下:

1.创建被代理的接口和类;

2.实现InvocationHandler接口,对目标接口中声明的所有方法进行统一处理;

3.调用Proxy的静态方法,创建代理类并生成相应的代理对象;

代码实现jdk动态代理:

/**
 * 1.创建被代理的接口和类;
 */
public interface OrderService {
    public String add();
}
public class OrderServiceImpl implements OrderService {
    public String add() {
        System.out.println("OrderServiceImpl add。。。");
        return "success";
    }
}
/**
 * 2.实现InvocationHandler接口,对目标接口中声明的所有方法进行统一处理;
 */
public class JdkMapperProxy implements InvocationHandler {
    //目标对象,被代理对象
    private Object targect;
    public JdkMapperProxy(Object targect){
        this.targect=targect;
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("前置通知...在代理方法之前处理");
        //目标方法,目标方法参数
        Object result = method.invoke(targect, args);//被执行目标方法,被代理的方法
        System.out.println("后置通知...在代理方法之后处理");
        return null;
    }
}
/**
 * 3.调用Proxy的静态方法,创建代理类并生成相应的代理对象;
 */
public class TestMybatis02 {
    public static void main(String[] args) {

        System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
        OrderService orderService = (OrderService) Proxy.newProxyInstance(OrderServiceImpl.class.getClassLoader()
                , OrderServiceImpl.class.getInterfaces(), new JdkMapperProxy(new OrderServiceImpl()));
        orderService.add();
    }
}

运行TestMybatis02 结果如下:

前置通知...在代理方法之前处理
OrderServiceImpl add。。。
后置通知...在代理方法之后处理

生成的代理类

通过反编译工具查看生成的代理类,可知,代理类实现了OrderService被代理类接口,add()方法中,调用h.invoke()方法,其中this.h指的是InvocationHandler,本质就是调用下面的这个方法

回顾了下jdk动态代理,下面我们开始源码分析

思考问题:会不会把下面这段配置转为实体类

<select id="getUser" parameterType="int"
        resultType="com.mayikt.entity.UserEntity">
    select * from user where id=#{id}
</select>

答案是肯定的,在那里进行解析的呢?下面开始分析源码:下面就是解析的地方

private void configurationElement(XNode context) {
    try {
        String namespace = context.getStringAttribute("namespace");
        if (namespace != null && !namespace.equals("")) {
         ....
            //进入这里
            this.buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
        } else {
            throw new BuilderException("Mapper's namespace cannot be empty");
        }
    } catch (Exception var3) {
        throw new BuilderException("Error parsing Mapper XML. Cause: " + var3, var3);
    }
}

重点这段代码:

this.buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
private void buildStatementFromContext(List<XNode> list) {
    if (this.configuration.getDatabaseId() != null) {
        //会进入到这里
        this.buildStatementFromContext(list, this.configuration.getDatabaseId());
    }
    this.buildStatementFromContext(list, (String)null);
}
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
    Iterator i$ = list.iterator();
    while(i$.hasNext()) {
        XNode context = (XNode)i$.next();
        XMLStatementBuilder statementParser = new XMLStatementBuilder(this.configuration, this.builderAssistant, context, requiredDatabaseId);
        try {
            //进入到这里
            statementParser.parseStatementNode();
        } catch (IncompleteElementException var7) {
            this.configuration.addIncompleteStatement(statementParser);
        }
    }
}
public void parseStatementNode() {
    String id = this.context.getStringAttribute("id");
    String databaseId = this.context.getStringAttribute("databaseId");
    if (this.databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
         ....
        if (this.configuration.hasKeyGenerator(keyStatementId)) {
            keyGenerator = this.configuration.getKeyGenerator(keyStatementId);
        } else {
            keyGenerator = this.context.getBooleanAttribute("useGeneratedKeys", this.configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType)) ? new Jdbc3KeyGenerator() : new NoKeyGenerator();
        }
        //最终到这里了
        this.builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass, resultSetTypeEnum, flushCache, useCache, resultOrdered, (KeyGenerator)keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
    }
}
public MappedStatement addMappedStatement(String id, SqlSource sqlSource, StatementType statementType, SqlCommandType sqlCommandType, Integer fetchSize, Integer timeout, String parameterMap, Class<?> parameterType, String resultMap, Class<?> resultType, ResultSetType resultSetType, boolean flushCache, boolean useCache, boolean resultOrdered, KeyGenerator keyGenerator, String keyProperty, String keyColumn, String databaseId, LanguageDriver lang, String resultSets) {
    if (this.unresolvedCacheRef) {
        throw new IncompleteElementException("Cache-ref not yet resolved");
    } else {
      .....
        //进入这里
        this.configuration.addMappedStatement(statement);
        return statement;
    }
}
public void addMappedStatement(MappedStatement ms) {
    //最终结果
    this.mappedStatements.put(ms.getId(), ms);
}
protected final Map<String, MappedStatement> mappedStatements;
this.mappedStatements = new Configuration.StrictMap("Mapped Statements collection");
protected static class StrictMap<V> extends HashMap<String, V> {

通过上面的代码执行流程,最终我们知道,mapper.xml中的配置文件里的每条sql语句是如何转化为对象保存起来的。最终都是封装成一个MappedStatement对象,再通过一个HashMap集合保存起来。

通过源码可知:HadhMap被put了两次

后面我们来分析getMapper()方法:默认走的是DefaultSqlSession

// 5.操作Mapper接口
UserMapper mapper = sqlSession.getMapper(UserMapper.class);

public <T> T getMapper(Class<T> type) {
    return this.configuration.getMapper(type, this);
}
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    return this.mapperRegistry.getMapper(type, sqlSession);
}
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory)this.knownMappers.get(type);
    if (mapperProxyFactory == null) {
        throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
    } else {
        try {
            return mapperProxyFactory.newInstance(sqlSession);
        } catch (Exception var5) {
            throw new BindingException("Error getting mapper instance. Cause: " + var5, var5);
        }
    }
}

由上面代码可知:通过configuration.getMapper()去查下我们之前有没有注册过mapper接口,没有则会报:没用绑定接口错误。

再看看上篇文章中介绍的mapperRegistery里面的东西:存放的是mapper接口,key为:接口,value为:MapperProxyFactory

这里我们mapper接口注册过,会进入else分支的这段代码:使用mapperProxyFactory创建代理类:

return mapperProxyFactory.newInstance(sqlSession);
public T newInstance(SqlSession sqlSession) {
    MapperProxy<T> mapperProxy = new MapperProxy(sqlSession, this.mapperInterface, this.methodCache);
    return this.newInstance(mapperProxy);
}

对比:mybatis的jdk动态代理和我们自己实现的jdk动态代理:

public class MapperProxy<T> implements InvocationHandler, Serializable {//mybatis的实现
public class JdkMapperProxy implements InvocationHandler {//我们的实现
protected T newInstance(MapperProxy<T> mapperProxy) {//mybatis的实现
    return Proxy.newProxyInstance(this.mapperInterface.getClassLoader(), new Class[]{this.mapperInterface}, mapperProxy);
}
OrderService orderService = (OrderService) Proxy.newProxyInstance(OrderServiceImpl.class.getClassLoader()//我们的实现
        , OrderServiceImpl.class.getInterfaces(), new JdkMapperProxy(new OrderServiceImpl()));

最后返回mapper信息如下:mapper为:我们通过:mapperProxyFactory创建的代理类MapperProxy

所以当我们调用mapper的getUser()方法时候,就会执行MapperProxy代理类的invoke()方法

UserEntity user = mapper.getUser(2);
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    if (Object.class.equals(method.getDeclaringClass())) {    //判断mapper接口有没有实现类,显然我们mapper没用实现类
        try {
            return method.invoke(this, args);
        } catch (Throwable var5) {
            throw ExceptionUtil.unwrapThrowable(var5);
        }
    } else {    //会执行这个分支
        MapperMethod mapperMethod = this.cachedMapperMethod(method);    //缓存中获取method
        return mapperMethod.execute(this.sqlSession, args);    //执行sql语句
    }
}

思考问题:Mybatis里面,mapper接口中有多个方法,每次调用会走同一个invoke()方法吗?

答案:不会的,因为你的每个MapperRegistry里面的class为mapper接口,都有独立的MapperProxyFactory,因为MapperRegistry中key存放的是mapper接口,value为MapperProxyFactory。

我们使用MapperProxyFactory创建MapperProxy去创建的代理,所以每次调用getMapper()方法取到同一个mapper则会走同一个invoke()方法,反过来每次调用mapper时候,就会走不同invoke()方法。

一般我们把Mapper接口定义为全局,则会走同一个invoke()方法,除非设=设置为多例,就每次都会new 不同,走不同invoke()方法。

Mybatis是基于多个不同的mapper接口生产的代理类,不同的mapper接口走不同的invoke方法,如果是相同的mapper接口,不同的方法,肯定是走同一个invoke方法。

那么就有问题了,多个不同mapper接口会产生多个代理类( new MapperProxy()),占太多的内存,后面会详解。

MapperProxy<T> mapperProxy = new MapperProxy(sqlSession, this.mapperInterface, this.methodCache);

上面我们把mapper接口看完了,执行 mapper.getUser(2) 会走invoke(),下面看invoke()方法

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    if (Object.class.equals(method.getDeclaringClass())) {
        try {
            return method.invoke(this, args);
        } catch (Throwable var5) {
            throw ExceptionUtil.unwrapThrowable(var5);
        }
    } else {
        //进入这里
        MapperMethod mapperMethod = this.cachedMapperMethod(method);
        return mapperMethod.execute(this.sqlSession, args);
    }
}
private MapperMethod cachedMapperMethod(Method method) {
        MapperMethod mapperMethod = (MapperMethod)this.methodCache.get(method);    //去缓存中查看是否有method,我们这里是没用的
        if (mapperMethod == null) {
            mapperMethod = new MapperMethod(this.mapperInterface, method, this.sqlSession.getConfiguration());    //会走到这里
            this.methodCache.put(method, mapperMethod);
        }
        return mapperMethod;
    }
}
public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
    this.command = new MapperMethod.SqlCommand(config, mapperInterface, method);
    this.method = new MapperMethod.MethodSignature(config, method);
}

先看下这块

this.command = new MapperMethod.SqlCommand(config, mapperInterface, method);
public enum SqlCommandType {
    UNKNOWN,
    INSERT,
    UPDATE,
    DELETE,
    SELECT,
    FLUSH;

SqlCommandType 是和sql语句相关的

public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
    String statementName = mapperInterface.getName() + "." + method.getName();
    MappedStatement ms = null;
    if (configuration.hasStatement(statementName)) {//进入这里
 
        ms = configuration.getMappedStatement(statementName);
    } else if (!mapperInterface.equals(method.getDeclaringClass())) {
        String parentStatementName = method.getDeclaringClass().getName() + "." + method.getName();
        if (configuration.hasStatement(parentStatementName)) {
            ms = configuration.getMappedStatement(parentStatementName);
        }
    }
    if (ms == null) {
        if (method.getAnnotation(Flush.class) == null) {
            throw new BindingException("Invalid bound statement (not found): " + statementName);
        }
        this.name = null;
        this.type = SqlCommandType.FLUSH;
    } else {    //ms不为null,则执行到这里
        this.name = ms.getId();
        this.type = ms.getSqlCommandType();
        if (this.type == SqlCommandType.UNKNOWN) {
            throw new BindingException("Unknown execution method for: " + this.name);
        }
    }
}
configuration.hasStatement(statementName)
public boolean hasStatement(String statementName) {
    return this.hasStatement(statementName, true);
}

getId()为namespace+id

将mapper.xml里面配置的sql语句和对应的mapper接口方法进行关联并放入map缓存中,后期直接走缓存了。最后执行execute()方法

public Object execute(SqlSession sqlSession, Object[] args) {
    Object param;
    Object result;
    if (SqlCommandType.INSERT == this.command.getType()) {
        param = this.method.convertArgsToSqlCommandParam(args);
        result = this.rowCountResult(sqlSession.insert(this.command.getName(), param));
    } else if (SqlCommandType.UPDATE == this.command.getType()) {
        param = this.method.convertArgsToSqlCommandParam(args);
        result = this.rowCountResult(sqlSession.update(this.command.getName(), param));
    } else if (SqlCommandType.DELETE == this.command.getType()) {
        param = this.method.convertArgsToSqlCommandParam(args);
        result = this.rowCountResult(sqlSession.delete(this.command.getName(), param));
    } else if (SqlCommandType.SELECT == this.command.getType()) {    //select类型走这里
        if (this.method.returnsVoid() && this.method.hasResultHandler()) {    //判断方法是否没用返回结果的,不是
            this.executeWithResultHandler(sqlSession, args);
            result = null;
        } else if (this.method.returnsMany()) {    //判断返回结果是不是返回多个结果集,不是
            result = this.executeForMany(sqlSession, args);
        } else if (this.method.returnsMap()) {    //是否返回map集合?不是
            result = this.executeForMap(sqlSession, args);
        } else {                //所以走这里
            param = this.method.convertArgsToSqlCommandParam(args);    //转换参数
            result = sqlSession.selectOne(this.command.getName(), param); //重点在这:selectOne()
        }
    } else {
        if (SqlCommandType.FLUSH != this.command.getType()) {
            throw new BindingException("Unknown execution method for: " + this.command.getName());
        }

        result = sqlSession.flushStatements();
    }

    if (result == null && this.method.getReturnType().isPrimitive() && !this.method.returnsVoid()) {
        throw new BindingException("Mapper method '" + this.command.getName() + " attempted to return null from a method with a primitive return type (" + this.method.getReturnType() + ").");
    } else {
        return result;
    }
}
public <T> T selectOne(String statement, Object parameter) {
    List<T> list = this.selectList(statement, parameter);
    if (list.size() == 1) {
        return list.get(0);
    } else if (list.size() > 1) {
        throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
    } else {
        return null;
    }
}

通过源码我们可以改成下面这样:selectOne(),后面我们针对selectOne()进行源码分析

//UserEntity user = mapper.getUser(2);
sqlSession.selectOne("com.mayikt.mapper.UserMapper.getUser",2);

总结:

MybatisMapper接口绑定原理分析流程

1、mapper.xml中的配置文件里的每条sql语句,最终都是封装成一个MappedStatement对象,再通过一个HashMap集合保存起来。

2、执行getMapper()方法,判断是否注册过mapper接口,注册了就会使用mapperProxyFactory去生成代理类MapperProxy

3、执行目标方法时,会调用MapperProxy代理类的invoke()方法

4、将mapper.xml里面配置的sql语句和对应的mapper接口方法进行关联并放入map缓存中,后期直接走缓存了。最后执行execute()方法

5、执行execute()方法最终调用selectOne()方法,执行结果。

 

本文参考

蚂蚁课堂:http://www.mayikt.com/

 

© 著作权归作者所有

须臾之余
粉丝 125
博文 67
码字总数 178724
作品 0
吉安
程序员
私信 提问
早前学习Java记录

Spring 对 iBATIS 的支持】 Spring 通过 DAO 模式,提供了对 iBATIS 的良好支持。 SqlMapClient:是 iBATIS 中的主要接口,通过 xml 配置文件可以让 Spring 容器来管理 SqlMapClient 对象的创...

大风厂蔡成功
2016/07/10
43
0
Spring与Mybatis整合的MapperScannerConfigurer

本文将分析mybatis与spring整合的MapperScannerConfigurer的底层原理,之前已经分析过Java中实现动态,可以使用jdk自带api和cglib第三方库生成动态代理。本文分析的mybatis版本3.2.7,mybat...

tantexian
2016/04/07
130
0
深入浅出MyBatis_Index

深入浅出MyBatis系列 【深入浅出MyBatis系列一】MyBatis入门 【深入浅出MyBatis系列二】配置简介(MyBatis源码篇) 【深入浅出MyBatis系列三】Mapper映射文件配置 【深入浅出MyBatis系列四】...

陶邦仁
2015/12/22
1K
0
通过源码分析MyBatis的缓存

前方高能! 本文内容有点多,通过实际测试例子+源码分析的方式解剖MyBatis缓存的概念,对这方面有兴趣的小伙伴请继续看下去~ MyBatis缓存介绍 首先看一段wiki上关于MyBatis缓存的介绍: MyBa...

whthomas
2014/12/11
20
0
Mybatis3.3.x技术内幕(二):动态代理之投鞭断流(自动映射器Mapper的底层实现原理)

一日小区漫步,我问朋友:Mybatis中声明一个interface接口,没有编写任何实现类,Mybatis就能返回接口实例,并调用接口方法返回数据库数据,你知道为什么不?朋友很是诧异:是啊,我也很纳闷...

祖大俊
2016/04/26
6.7K
15

没有更多内容

加载失败,请刷新页面

加载更多

c语言实现Sqlite3的创建db和增删改查db操作

SQLite,是一款轻型的数据库,而且目前已经在很多嵌入式产品中使用了它,它占用资源非常的低,在嵌入式设备中使用广泛,现在准备学习一下sqlite3的使用方法并写一个测试demo,后面在项目智能...

jorin_zou
12分钟前
2
0
【2019年8月版本】OCP 071认证考试最新版本的考试原题-第2题

choose three Which three are true about the CREATE TABLE command? A) It can include the CREATE...INDEX statement for creating an index to enforce the primary key constraint. B) ......

oschina_5359
15分钟前
3
0
如何在二维码中循环批量插入图片

现在二维码种类比较多,为了突出二维码的个性及吸引客户,很多朋友都喜欢在二维码上插入图片。想要每个二维码都与众不同,但是有的时候需要批量插入图片数量有限,如果制作的二维码比较多的话...

中琅软件
16分钟前
4
0
LTR那点事—AUC及其与线上点击率的关联详解

LTR(Learning To Rank)学习排序是一种监督学习(SupervisedLearning)的排序方法,现已经广泛应用于信息索引,内容推荐,自然语言处理等多个领域。以推荐系统为例,推荐一般使用多个子策略...

达观数据
16分钟前
3
0
IntelliJ 如何显示代码的代码 docs

希望能够在 IntelliJ 代码上面显示方法的 docs。 如何进行显示? 你可以使用 Ctrl + Q 这个快捷键来查看方法的 Docs。 https://blog.ossez.com/archives/3061...

honeymoose
19分钟前
3
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部