Mybatis中Mapper与接口绑定原理的源码分析

今天就跟大家聊聊有关Mybatis中Mapper与接口绑定原理的源码分析,可能很多人都不太了解,为了让大家更加了解,小编给大家总结了以下内容,希望大家根据这篇文章可以有所收获。

10年积累的做网站、网站制作经验,可以快速应对客户对网站的新想法和需求。提供各种问题对应的解决方案。让选择我们的客户得到更好、更有力的网络服务。我虽然不认识你,你也不认识我。但先网站设计后付款的网站建设流程,更有南明免费网站建设让你可以放心的选择与我们合作。

这里再来分析下,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与接口绑定原理的源码分析

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

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


    select * from user where id=#{id}

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

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 mappedStatements;
this.mappedStatements = new Configuration.StrictMap("Mapped Statements collection");
protected static class StrictMap extends HashMap {

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()方法,执行结果。

看完上述内容,你们对Mybatis中Mapper与接口绑定原理的源码分析有进一步的了解吗?如果还想了解更多知识或者相关内容,请关注创新互联行业资讯频道,感谢大家的支持。


文章标题:Mybatis中Mapper与接口绑定原理的源码分析
分享网址:http://ybzwz.com/article/ijiiij.html