这个问题:估计大家都知道是动态代理。可是除了动态代理这之间还有哪些设计模式呢?
//3、获取mapper
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
//4、执行数据库操作,并处理结果集
return userMapper.selectUser("10");
先看我们的getMapper方法,是如何返回一个代理对象的。
@Override
public T getMapper(Class type) {
return configuration.getMapper(type, this);
}
public T getMapper(Class type, SqlSession sqlSession) {
return mapperRegistry.getMapper(type, sqlSession);
}
public T getMapper(Class type, SqlSession sqlSession) {
final MapperProxyFactory mapperProxyFactory = (MapperProxyFactory) 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);
}
}
先从配置对象configuration中获取,最后委托给mapperRegistry对象。
在mapperRegistry中,有个缓存(knownMappers)对象Map,我们根据key(接口的全限定类名,比如:learn.UserMapper)去获取我们的MapperProxyFactory对象,MapperProxyFactory负责每次创建我们的代理对象的实例。
从这里,我们可以学到缓存和工厂的使用,这样做的好处,很明显,避免了资源的重复加载,可以重复利用我们的资源。
我们的MapperProxyFactory对象是在我们,创建configuration对象的过程中,解析xml生成并缓存起来的。
package org.apache.ibatis.binding;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.ibatis.binding.MapperProxy.MapperMethodInvoker;
import org.apache.ibatis.session.SqlSession;
/**
* @author Lasse Voss
*/
public class MapperProxyFactory {
//就是我们的接口 比如:learn.UserMapper
private final Class mapperInterface;
//用于缓存我们的方法调用,我们的同一个方法每次调用时,只是参数的不一样,其他一样,所以
//可以缓存起来,这样可以高效利用,和前面将我们的factory缓存起来是一样的道理
//此外,我们每次调用方法时,需要新new一个MapperProxy对象,而这个对象的最终的invoke方法中
//执行具体方法的逻辑的MapperMethodInvoker调用者对象是可以重复使用的,所以很有必要缓存起来
private final Map methodCache = new ConcurrentHashMap<>();
public MapperProxyFactory(Class mapperInterface) {
this.mapperInterface = mapperInterface;
}
public Class getMapperInterface() {
return mapperInterface;
}
public Map getMethodCache() {
return methodCache;
}
@SuppressWarnings("unchecked")
protected T newInstance(MapperProxy mapperProxy) {
//这里是JDK动态代理,返回代理类的对象
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
public T newInstance(SqlSession sqlSession) {
//mapperProxy实现了InvocationHandler接口,可以详细看我关于动态代理的文章。
//简单理解,实现这个接口的类,代表了我们的行为的抽象,里面的invoke方法就是最终的调用方法
final MapperProxy mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
}
上面的MapperProxy类中的MethodCache中,实际上缓存的就是我们的接口中定义的那些方法。
接下来我们分析:
userMapper.selectUser("10");
因为是动态代理,所以这里的方法,会执行到MapperProxy中的invoke方法:
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) {
//这里是类似 toString,equals这样的方法,就是我们未代理的方法
return method.invoke(this, args);
} else {//代理的方法,比如:selectUser
return cachedInvoker(proxy, method, args).invoke(proxy, method, args, sqlSession);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
如果是我们进行设计:
//如何从 selectUser()方法,让它直接执行下面的方法
result = sqlSession.selectOne(command.getName(), param);
//首先,我们要找入参,入参需要进行转换,从java类型 ---> sql类型
//然后,我们需要知道这个java方法,对应的是xml中的哪个sqlid,也就是我们需要有个映射表可以查。
//其次,我们sql中常规的有 增删改查 四种类型的方法
然后,我们看源码:
public MapperMethod(Class> mapperInterface, Method method, Configuration config) {
this.command = new SqlCommand(config, mapperInterface, method);
this.method = new MethodSignature(config, mapperInterface, method);
}
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
switch (command.getType()) {
case INSERT: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.insert(command.getName(), param));
break;
}
case UPDATE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.update(command.getName(), param));
break;
}
case DELETE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.delete(command.getName(), param));
break;
}
case SELECT:
if (method.returnsVoid() && method.hasResultHandler()) {
executeWithResultHandler(sqlSession, args);
result = null;
} else if (method.returnsMany()) {
result = executeForMany(sqlSession, args);
} else if (method.returnsMap()) {
result = executeForMap(sqlSession, args);
} else if (method.returnsCursor()) {
result = executeForCursor(sqlSession, args);
} else {
Object param = method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(command.getName(), param);
if (method.returnsOptional()
&& (result == null || !method.getReturnType().equals(result.getClass()))) {
result = Optional.ofNullable(result);
}
}
break;
case FLUSH:
result = sqlSession.flushStatements();
break;
default:
throw new BindingException("Unknown execution method for: " + command.getName());
}
if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
throw new BindingException("Mapper method '" + command.getName()
+ " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
}
return result;
}
MapperMetod这个类,解决了我们之前提到的问题。sql和java方法的映射,方法的执行时,几种不同类型的不同处理逻辑。
所以,我们在MapperProxy的invoke方法中可以直接调用下面的方法,就达到了从java方法到执行sql的转换。
mapperMethod.execute(sqlSession, args);
可是源码,并不是这样实现的,这是为甚么?
查看源码,我们发现,先从缓存中获取执行器,然后再用执行器去执行invoke方法。而执行器接口有俩个实现类。
主要的原因应该是,为了处理接口中的default方法。(经过查看旧版源码,发现以前只能支持JDK8的default方法,为了扩展性
,所以新版都是让default方法和我们的自定义方法都是实现同一个接口)
如果我们按常规逻辑MapperProxy的invoke方法的else中的代码类似下面:
if (m.isDefault()) {
try {
if (privateLookupInMethod == null) {// jdk 1.8
//调用 default方法
} else {//jdk 1.9
//调用default方法
}
}else {//我们的逻辑
mapperMethod.execute(sqlSession, args);
}
这样的代码,一看就后期扩展不好,以后jdk到15,16怎么办?难道一直修改这个方法体吗?
根据设计原则,我们可以把变化的部分抽取,我们可以将其抽象为接口(接口更多的是行为的抽象)。这样,我们的代码可以优雅的写作下面:
接口.invoke()
然后我们查看源代码发现确实,定义一个接口
MapperMethodInvoker
然后接口里定义了方法:invoke
接下来就是:我们的接口MapperMethodInvoker的invoke方法执行的时候,它实际执行的应该是我们的mapperMethod.execute(sqlSession, args)
我们知道有一种设计模式作用就是兼容原本接口不匹配的两个类,起到桥梁的作用,所以查看源码果然是使用了对象型的适配器模式。
private static class PlainMethodInvoker implements MapperMethodInvoker {
private final MapperMethod mapperMethod;
public PlainMethodInvoker(MapperMethod mapperMethod) {
super();
this.mapperMethod = mapperMethod;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
return mapperMethod.execute(sqlSession, args);
}
}
对于每一次调用 userMapper.selectUser("10"),都是同一个调用者,只是参数不一样而已,所以我们完全可以使用缓存,这样不用每次都新new一个调用者对象。于是,就有了
我们的invoke方法中的,先从缓存中获取调用者,然后再用调用者去执行invoke方法。
cachedInvoker(proxy, method, args).invoke(proxy, method, args, sqlSession);
回到一开始,为何我们不直接new一个MapperProxy对象,而是,通过工厂来new实例呢?
我们将我们的MapperMethodInvoker调用者缓存起来了,放到了methodCache这个map中,而我们每次新new一个MapperProxy对象的时候,对于同一个接口而言,它的methodCache(简单理解为,里面就是存放这个接口下的所有method)是不变的。
所以,我们完全可以把这个methodCache属性托付给第三方,等每次new我们的MapperProxy对象的时候,属性值直接去第三方拿。更进一步,我们第三方负责生产我们的实例,于是乎,就有了MapperProxyFactory这个类。
同一个接口,同一个MapperProxyFactory对象,同一个methodCache,每次只是生产不同的MapperProxy对象就行。
final MapperProxy mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
总结:
1.我们的java方法如何映射为一个sql命令,于是有了MapperMethod这个类,将二者映射起来。
2.如何兼容我们的default方法和正常的sql代理方法,同时易于以后的扩展,于是我们抽象了行为,定义了接口MapperMethodInvoker。
3.如何兼容我们接口MapperMethodInvoker中的invoke方法和MapperMethod类中execute方法,于是我们使用了对象型的适配器设计模式。
4.同一个方法的每一次调用,除了入参不一样其他都是一样的,为了节约成本,于是我们使用缓存的概念,将我们的调用者缓存起来。
Map methodCache
5.使用工厂模式,MapperProxyFactory进行我们的对象的创建,同时不变的属性值methodCache,交由这个MapperProxyFactory对象管理。像这样的方式,也可以用于build模式中,每次构建不同的对象,但是不变的属性托付给builder