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

Stella981
• 阅读 750

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

紧接上篇文章: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。。。
后置通知...在代理方法之后处理

生成的代理类

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

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

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

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

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

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

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 list) { if (this.configuration.getDatabaseId() != null) {         //会进入到这里 this.buildStatementFromContext(list, this.configuration.getDatabaseId()); } this.buildStatementFromContext(list, (String)null); }

private void buildStatementFromContext(List 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 extends HashMap<String, V> {

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

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

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

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

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

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

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

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

public T getMapper(Class type) { return this.configuration.getMapper(type, this); }

public T getMapper(Class type, SqlSession sqlSession) { return this.mapperRegistry.getMapper(type, sqlSession); }

public T getMapper(Class type, SqlSession sqlSession) { MapperProxyFactory 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

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

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

return mapperProxyFactory.newInstance(sqlSession);

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

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

public class MapperProxy implements InvocationHandler, Serializable {//mybatis的实现

public class JdkMapperProxy implements InvocationHandler {//我们的实现

protected T newInstance(MapperProxy 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

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

所以当我们调用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()方法。

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

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

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

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

MapperProxy 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); }

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

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

getId()为namespace+id

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

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

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

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 selectOne(String statement, Object parameter) { List 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/

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
5个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Stella981 Stella981
3年前
Mybatis深入源码分析之SQLSession一级缓存原理分析
!(https://www.w3cschool.cn/attachments/image/20170807/1502093784622523.png)通过前面几篇文章,Mybatis深入源码分析之SqlSessionFactoryBuilder源码分析(https://my.oschina.net/u/3995125/blog/3079296),
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
11个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这