紧接上篇文章: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。。。
后置通知...在代理方法之后处理
生成的代理类
通过反编译工具查看生成的代理类,可知,代理类实现了OrderService被代理类接口,add()方法中,调用h.invoke()方法,其中this.h指的是InvocationHandler,本质就是调用下面的这个方法
回顾了下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
private void buildStatementFromContext(List
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
通过上面的代码执行流程,最终我们知道,mapper.xml中的配置文件里的每条sql语句是如何转化为对象保存起来的。最终都是封装成一个MappedStatement对象,再通过一个HashMap集合保存起来。
通过源码可知:HadhMap被put了两次
后面我们来分析getMapper()方法:默认走的是DefaultSqlSession
// 5.操作Mapper接口 UserMapper mapper = sqlSession.getMapper(UserMapper.class);
public
public
public
由上面代码可知:通过configuration.getMapper()去查下我们之前有没有注册过mapper接口,没有则会报:没用绑定接口错误。
再看看上篇文章中介绍的mapperRegistery里面的东西:存放的是mapper接口,key为:接口,value为:MapperProxyFactory
这里我们mapper接口注册过,会进入else分支的这段代码:使用mapperProxyFactory创建代理类:
return mapperProxyFactory.newInstance(sqlSession);
public T newInstance(SqlSession sqlSession) {
MapperProxy
对比:mybatis的jdk动态代理和我们自己实现的jdk动态代理:
public class MapperProxy
public class JdkMapperProxy implements InvocationHandler {//我们的实现
protected T newInstance(MapperProxy
OrderService orderService = (OrderService) Proxy.newProxyInstance(OrderServiceImpl.class.getClassLoader()//我们的实现 , OrderServiceImpl.class.getInterfaces(), new JdkMapperProxy(new OrderServiceImpl()));
最后返回mapper信息如下:mapper为:我们通过:mapperProxyFactory创建的代理类MapperProxy
所以当我们调用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()方法。
一般我们把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); }
getId()为namespace+id
将mapper.xml里面配置的sql语句和对应的mapper接口方法进行关联并放入map缓存中,后期直接走缓存了。最后执行execute()方法
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
通过源码我们可以改成下面这样: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()方法,执行结果。