MyBatis-接口实现数据库操作的原理

参考:

https://blog.csdn.net/luoposhushengyizhuce/article/details/83902433

https://www.cnblogs.com/williamjie/p/11188355.html

https://www.cnblogs.com/williamjie/p/11188346.html

 

 

 

 

mybatis如何通过只需要配置接口就能实现数据库的操作

在用mybatis的时候,我们只需要写一个接口,然后服务层就能调用,在我们的认知中,是不能直接调接口的方法的,这个其中的原理是什么呢?由于自己比较好奇,就取翻了一下mybatis的源码,一下是做的一些记录。
通过一个最简单的例子来揭开它的面目。

@Test
public void testDogSelect() throws IOException {
    String resource = "allconfig.xml";// ①
    InputStream inputStream = Resources.getResourceAsStream(resource);// ②
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);// ③
    SqlSession session = sqlSessionFactory.openSession();// ④
    DogMapper dogMapper = session.getMapper(DogMapper.class);// ⑤
    List<Dog> dogs = dogMapper.selectDog();//⑥
    System.out.println(dogs.size());// ⑦
}


首先就是①②两行是没什么要解释的,就从第三行开始,我们追踪代码,进入SqlSessionFactoryBuilder的build(InputStream)的方法
public SqlSessionFactory build(InputStream inputStream) {
    return build(inputStream, null, null);
}

public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    try {
        XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
        return build(parser.parse());
    } catch (Exception e) {
        throw ExceptionFactory.wrapException("Error building SqlSession.", e);
    } finally {
        ErrorContext.instance().reset();
        try {
            inputStream.close();
        } catch (IOException e) {
            // Intentionally ignore. Prefer previous error.
        }
    }
}


重要代码就两行,其中XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);是构建解析器用的,主要看下一行build(parser.parse()),其中parser.parse()主要就是把我们的mybatis的配置文件进行解析,并把解析的大部分内容保存到Configuration中。然后看build的代码

public SqlSessionFactory build(Configuration config) {
    return new DefaultSqlSessionFactory(config);
  }


返回了一个DefaultSqlSessionFactory
继续看④这一句,我们知道sqlSessionFactory实际上是DefaultSqlSessionFactory的一个实例,进入DefaultSqlSessionFactory的openSession()方法

@Override
public SqlSession openSession() {
    return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    try {
        final Environment environment = configuration.getEnvironment();
        final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
        tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
        final Executor executor = configuration.newExecutor(tx, execType);
        return new DefaultSqlSession(configuration, executor, autoCommit);
    } catch (Exception e) {
        closeTransaction(tx); // may have fetched a connection so lets call close()
        throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
    } finally {
        ErrorContext.instance().reset();
    }
}


其他我们都先不管,我们只要看到这个方法返回的是一个DefaultSqlSession的实例就好
我们继续看⑤这一行,session是DefaultSqlSession的一个实例。我们进入DefaultSqlSession的getMapper(Class type)方法,

@Override
public <T> T getMapper(Class<T> type) {
    return configuration.<T>getMapper(type, this);
}


其中configuration是Configuration的实例,在解析mybatis的配置文件的时候进行的初始化。继续追进去

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


继续进到MapperRegistry的getMapper(Class type, SqlSession sqlSession)方法

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


此方法根据传进来的type生成对应的代理,我们进入看看MapperProxyFactory

public T newInstance(SqlSession sqlSession) {
    final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
    return newInstance(mapperProxy);
}
protected T newInstance(MapperProxy<T> mapperProxy) {
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}


到这里已经看到已经完成代理的生成,MapperProxyFactory是个工厂。再继续看MapperProxy这个类
public class MapperProxy<T> implements InvocationHandler, Serializable {

    private static final long serialVersionUID = -6424540398559729838L;
    private final SqlSession sqlSession;
    private final Class<T> mapperInterface;
    private final Map<Method, MapperMethod> methodCache;

    public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethod> methodCache) {
        this.sqlSession = sqlSession;
        this.mapperInterface = mapperInterface;
        this.methodCache = methodCache;
    }

    @Override
    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 t) {
            throw ExceptionUtil.unwrapThrowable(t);
        }
        }
        final MapperMethod mapperMethod = cachedMapperMethod(method);
        return mapperMethod.execute(sqlSession, args);
    }

    private MapperMethod cachedMapperMethod(Method method) {
        MapperMethod mapperMethod = methodCache.get(method);
        if (mapperMethod == null) {
        mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration());
        methodCache.put(method, mapperMethod);
        }
        return mapperMethod;
    }

}

当我们通过生成的对象调用方法的时候,都会进入这个类的invoke方法方法,我看看到如果调用的是我们自定的方法,直接就是调用的mybatis的实现,通过接口找到配置信息,然后根据我们的配置去操作数据库。

最后总结一下,mybatis之所以配置接口以后就能执行是因为在生产mapper的时候实质上是生成的一个代理,然后通过mapper调用接口方法的时候直接被MapperProxy的invoke截断了,直接去调用了mybatis为我们制定的实现,而没有去回调。

 

 

 

 

 

 

 

 

MyBatis你只写了接口为啥就能执行SQL啊?

一、静态代理

又是一年秋招季,很多小伙伴开始去大城市打拼。来大城市第一件事就是租房,免不了和中介打交道,因为很多房东很忙,你根本找不到他。从这个场景中就可以抽象出来代理模式:

  • ISubject:被访问者资源的抽象

  • SubjectImpl:被访问者具体实现类(房东)

  • SubjectProxy:被访问者的代理实现类(中介)

UML图如下:

举个例子来理解一下这个设计模式:

老板让记录一下用户服务的响应时间,用代理模式来实现这个功能。

 

一切看起来都非常的美好,老板又发话了,把产品服务的响应时间也记录一下吧。又得写如下3个类:

  • IProductService

  • ProductServiceImpl

  • ProductServiceProxy

UserServiceProxy和ProductServiceProxy这两个代理类的逻辑都差不多,却还得写2次。其实这个还好,如果老板说,把现有系统的几十个服务的响应时间都记录一下吧,你是不是要疯了?这得写多少代理类啊?

二、动态代理

黑暗总是暂时的,终究会迎来黎明,在JDK1.3之后引入了一种称之为动态代理(Dynamic Proxy)的机制。使用该机制,我们可以为指定的接口在系统运行期间动态地生成代理对象,从而帮助我们走出最初使用静态代理实现AOP的窘境

动态代理的实现主要由一个类和一个接口组成,即java.lang.reflect.Proxy类和java.lang.reflect.InvocationHandler接口。

让我们用动态代理来改造一下上面记录系统响应时间的功能。虽然要为IUserService和IProductService两种服务提供代理对象,但因为代理对象中要添加的横切逻辑是一样的。所以我们只需要实现一个InvocationHandler就可以了。代码如下

UML图如下。恭喜你,你现在已经理解了Spring AOP是怎么回事了,就是这么简单,今天先不展开谈Spring

先简单谈谈动态代理在Mybatis中是如何被大佬玩的出神入化的

三、Mybatis核心设计思路

相信用过mybatis的小伙伴都能理解下面这段代码,通过roleMapper这个接口直接从数据库中拿到一个对象

Role role = roleMapper.getRole(3L);

直觉告诉我,一个接口是不能运行的啊,一定有接口的实现类,可是这个实现类我自己没写啊,难道mybatis帮我们生成了?你猜的没错,mybatis利用动态代理帮我们生成了接口的实现类,这个类就是:

org.apache.ibatis.binding.MapperProxy,

我先画一下UML图,MapperProxy就是下图中的SubjectProxy类

和上面的UML类图对比一下,发现不就少了一个SubjectImpl类吗?那应该就是SubjectProxy类把SubjectImple类要做的事情做了呗,猜对了。SubjectProxy通过SubjectImple和SubjectImple.xml之间的映射关系知道自己应该执行什么SQL。所以mybatis最核心的思路就是这么个意思,细节之类的可以看源码,理清最主要的思路,看源码就能把握住重点。

关于源码相关的内容,更进一步的解释动态代理在MyBatis中的使用,可以参考以前的一篇文章:《动态代理之投鞭断流!看一下MyBatis的底层实现原理!

 

附录:

https://mp.weixin.qq.com/s/ToMvAD0-QyUJgg_1eFvcaA

 
 
 
 
 
 
 
 
 

动态代理之投鞭断流!看一下MyBatis的底层实现原理

转:https://mp.weixin.qq.com/s?__biz=MzI1NDQ3MjQxNA==&mid=2247486856&idx=1&sn=d430be5d14d159fd36b733c83369d59a&chksm=e9c5f439deb27d2f60b69d7f09b240eb43a8b1de2d07f7511e1f1fecdf9d49df1cb7bc6e1ab5&scene=21#wechat_redirect

一日小区漫步,我问朋友:Mybatis中声明一个interface接口,没有编写任何实现类,Mybatis就能返回接口实例,并调用接口方法返回数据库数据,你知道为什么不?朋友很是诧异:是啊,我也很纳闷,我们领导告诉我们按照这个模式编写就好了,我同事也感觉很奇怪,虽然我不知道具体是怎么实现的,但我觉得肯定是……(此处略去若干的漫天猜想),但是也不对啊,难道是……(再次略去若干似懂非懂)。

这激发了我写本篇文章的冲动。

 


动态代理的功能:通过拦截器方法回调,对目标target方法进行增强。

言外之意就是为了增强目标target方法。上面这句话没错,但也不要认为它就是真理,殊不知,动态代理还有投鞭断流的霸权,连目标target都不要的科幻模式。

注:本文默认认为,读者对动态代理的原理是理解的,如果不明白target的含义,难以看懂本篇文章,建议先理解动态代理。

一、自定义JDK动态代理之投鞭断流实现自动映射器Mapper

首先定义一个pojo

再定义一个接口UserMapper.java

接下来我们看看如何使用动态代理之投鞭断流,实现实例化接口并调用接口方法返回数据的。

自定义一个InvocationHandler。

上面代码中的target,在执行Object.java内的方法时,target被指向了this,target已经变成了傀儡、象征、占位符。在投鞭断流式的拦截时,已经没有了target。

写一个测试代码:

output:

这便是Mybatis自动映射器Mapper的底层实现原理。

可能有读者不禁要问:你怎么把代码写的像初学者写的一样?没有结构,且缺乏美感。

必须声明,作为一名经验老道的高手,能把程序写的像初学者写的一样,那必定是高手中的高手。这样可以让初学者感觉到亲切,舒服,符合自己的Style,让他们或她们,感觉到大牛写的代码也不过如此,自己甚至写的比这些大牛写的还要好,从此自信满满,热情高涨,认为与大牛之间的差距,仅剩下三分钟。

二、Mybatis自动映射器Mapper的源码分析

首先编写一个测试类:

Mapper长这个样子:

org.apache.ibatis.binding.MapperProxy.java部分源码。

org.apache.ibatis.binding.MapperProxyFactory.java部分源码。

这便是Mybatis使用动态代理之投鞭断流。

三、接口Mapper内的方法能重载(overLoad)吗?(重要)

类似下面:

Answer:不能。

原因:在投鞭断流时,Mybatis使用package+Mapper+method全限名作为key,去xml内寻找唯一sql来执行的。类似:key=x.y.UserMapper.getUserById,那么,重载方法时将导致矛盾。对于Mapper接口,Mybatis禁止方法重载(overLoad)。

 
 
 
 
 
 
 
 

 

posted on 2020-11-17 11:05  秦羽的思考  阅读(1766)  评论(0编辑  收藏  举报