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)。