说起mybatis,大伙应该都用过,有些人甚至底层源码都看过了。在mybatis中,mapper接口是没有实现类的,取而代之的是一个xml文件。也就是说我们调用mapper接口,其实是使用了mapper.xml中定义sql完成数据操作。
大家有没想过,为什么mapper没有实现类,它是如何和xml关联起来的?
一个简单的例子
ok,别急,现在我们已经抛出问题,现在我们从demo开始,再结合我们所拥有的知识点出发,一一剖析整个过程。
先来搞个简单的查询:
UserMapper一个接口:
User findById(Long id);
userMapper.xml的sql语句
<mapper namespace="com.lfq.UserMapper"> <select id="findById" resultType="com.lfq.User"> select * * from user where id = #{id} </select></mapper>
猜想
我们知道,接口是不直接被初始化的,但是可以被实现,所以new对象的时候是初始化实现类,然后接口再引用该对象。那么调用接口的方法实际上就是调用被引用对象的方法,也就是实现类的方法。
那么,UserMapper.findById被调用时候,不禁有这两个疑问?
我们先来回答第二个问题,既然找不到实现类,UserMapper有没可能被代理起来呢,findById方法调用时候,我们找到代理对象来执行就行了。
代理有两种方式:
而静态代理基本是不可能的了,静态代理需要对UserMapper所有的方法进行重写。那么只能是动态代理,动态代理接口的所有方法,每次接口被调用,就会进入动态代理对象的invoke方法,然后加载xml中的sql完成操作数据库,再返回结果。
再然后说到动态代理,常见的方式有以下2种方式:
所以,动态代理代理还是对象类,那么我们只有接口,不能new,哪来的对象呢?别忘了,我们还有反射机制,我们是不是可以通过反射给接口生成对象,还记得Class.*forName*吗。
综合上面的猜想:
第一步:通过反射机制给接口生成对象
第二步:动态代理反射对象,这样接口被调用,就会触发动态代理
嗯,好像有点道理,我果然是个天才!
知识点:动态代理
动态代理有几种实现方式,这里我们就先讲JDK动态代理,使用步骤如下:
接口我们就用UserMapper,我们来写个代理对象。
public class Test { // 接口 static interface Subject{ void sayHi(); void sayHello(); }
// 默认实现类(我们可以反射生成) static class SubjectImpl implements Subject{ @Override public void sayHi() { System.out.println("hi"); } @Override public void sayHello() { System.out.println("hello"); } }
// jkd动态代理 // 原创:公众号:java思维导图 static class ProxyInvocationHandler implements InvocationHandler{ private Subject target; public ProxyInvocationHandler(Subject target) { this.target=target; }
@Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.print("say:"); return method.invoke(target, args); } }
public static void main(String[] args) { Subject subject=new SubjectImpl();
Subject subjectProxy=(Subject) Proxy.newProxyInstance(subject.getClass().getClassLoader(), subject.getClass().getInterfaces(), new ProxyInvocationHandler(subject));
subjectProxy.sayHi(); subjectProxy.sayHello(); }}
ok,一个简单的动态代理例子送给你们,上面代码中关键生成动态代理对象的关键代码是:
Proxy.newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h);
源码分析
好啦,上面该做的准备已经都准备好了,我们对mybatis的这个mapper接口大概都有些思路了,下面我们去正式验证一下,那么肯定就要去看源码了。我们只是去验证上面的mapper接口问题,所以不需要去看全部的代码,当然如果你看整个流程下来的话,会更加清晰。
论证猜想,我们可以采用结果导向的方式去看源码,从获取mapper那里开始看,也就是
SqlSession sqlSession = sqlSessionFactory.openSession();UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
主要从sqlSession.getMapper(UserMapper.class);这里开始,先看整个UserMapper是不是被动态代理的。ok,我们进入代码中:
@Overridepublic <T> T getMapper(Class<T> type) { return getConfiguration().getMapper(type, this);}
继续走到Configuration方法里,Configuration是mybatis所有配置相关的地方,mybatis-cfg.xml、UserMapper.xml等文件都会被预先加载到Configuration里。
public <T> T getMapper(Class<T> type, SqlSession sqlSession) { return mapperRegistry.getMapper(type, sqlSession);}
这时候,我们发现Configuration里面出现了一个mapperRegistry,翻译过来可以理解为mapper的注册器,其实在加载UserMapper.xml的时候,我们就需要在mapperRegistry里面进行注册,所有,我们可以从这里面进行获取。继续走~
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
//获取mapper代理工厂 // 原创:公众号:java思维导图 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); }}
ok,这里分为了两步:
我们一步步分析,别急,knownMappers其实是个map,根据userMapper.class获取MapperProxyFactory:
Map<Class<?>, MapperProxyFactory<?>> knownMappers
所以knownMappers必然是源码前面的步骤中set进去的。我们先找找,到底是哪里set进去的。找呀找,找到这里:
private void bindMapperForNamespace() { String namespace = builderAssistant.getCurrentNamespace(); if (namespace != null) { Class<?> boundType = null; try { //反射生成namespace的对象 // 原创:公众号:java思维导图 boundType = Resources.classForName(namespace); } catch (ClassNotFoundException e) { //ignore, bound type is not required } if (boundType != null) { if (!configuration.hasMapper(boundType)) { // Spring may not know the real resource name so we set a flag // to prevent loading again this resource from the mapper interface // look at MapperAnnotationBuilder#loadXmlResource configuration.addLoadedResource("namespace:" + namespace);// namespace:com.lfq.UserMapper configuration.addMapper(boundType); } } }}
我们看这个boundType,是通过Resources.classForName(namespace);生成的class,Resources.classForName底层其实就是调用Class.forName生成的反射对象,而参数是namespace,namespacne不正是com.lfq.UserMapper嘛:
<mapper namespace="com.lfq.UserMapper">
完美!!Class.forName(com.lfq.UserMapper)生成反射对象。论证了我们第一点猜想。生成的boundType在被configuration.addMapper(boundType);所以就有了:
public <T> void addMapper(Class<T> type) { mapperRegistry.addMapper(type);}
public <T> void addMapper(Class<T> type) { if (type.isInterface()) { if (hasMapper(type)) { throw new BindingException("Type " + type + " is already known to the MapperRegistry."); } boolean loadCompleted = false; try {
/** * TODO 将mapper接口包装成mapper代理 * 原创:公众号:java思维导图 */ knownMappers.put(type, new MapperProxyFactory<>(type));
//解析接口上的注解或者加载mapper配置文件生成mappedStatement(com/lfq/UserMapper.java) MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type); // 开始解析 parser.parse();
// 加载完成标记 loadCompleted = true; } finally { if (!loadCompleted) { knownMappers.remove(type); } } }}
上面的代码,就给我论证了这个MapperProxyFactory是哪里来的,MapperProxyFactory里面其实就一个参数mapperInterface,就是反射生成的这个对象。ok,第一个猜想已经论证完毕,接着我们看刚才说到的第二点:动态代理。
回到mapperProxyFactory.newInstance(sqlSession); 这个MapperProxyFactory就是我们刚刚new出来的,我们打开newInstance方法看看:
public T newInstance(SqlSession sqlSession) {
//MapperProxy为InvocationHandler的实现类 // 原创:公众号:java思维导图 final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
//真实生成代理 return newInstance(mapperProxy);}protected T newInstance(MapperProxy<T> mapperProxy) { //采用JDK自带的Proxy代理模式生成 return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);}
终于在里面看到Proxy.newProxyInstance了,好激动呀。又论证了第二点的动态代理猜想 上面代码中,首先把sqlSession, mapperInterface, methodCache三个参数封装到MapperProxy中,而MapperProxy是实现了InvocationHandler接口的方法,因此动态代理被调用的时候,会进入到MapperProxy的invoke方法中。
sqlSession是必须的,因为操作数据库需要用到sqlsession。具体invoke里面的内容,我们不做多分析啦,刚兴趣的同学自己去看下源码哈。可以猜想:找到对应的sql,然后执行sql操作,哈哈哈。
总结
好啦,今天的内容就到这里啦~
如果你喜欢我的文章,欢迎关注我的公众号:java思维导图,给我点个在看或者转发一下,万分感谢哈!
互动环节:
最后不做小调查了,我们来个留言互动吧:以你所学知识,请大胆猜想一下,spring data jpa为什么只需要声明符合命名规范的接口即可,而不需要写实现类?他的底层实现可能用了什么技术?
(完)
精选文章
springboot+redis+拦截器,实现接口幂等性校验