说到Mybatis,我们都知道这是一个与数据库交互的持久层框架,它能提供可自定义的数据库查询接口,并且封装了查询细节,让我们专注于业务开发的优秀框架。
但说到动态代理,大部分刚出来同学可能就有点疑惑了,因为在工作中我不止一次被刚参加工作的同事问道:“Mapper接口的实现是放在那个包下?我怎么找不到呢?”。然后我会毫不犹豫的告诉他:“Mapper接口的实现类是由动态代理技术生成的,是放在内存中的,你是看不到的”,然后他们带着一脸问号回到了工位。
接下来让我们来看看Mybatis是如何通过动态代理技术来把Mapper实现类生成并放到内存中的,竟然不用写代码也能生成实现类,而且还能连接数据库。
关于动态代理技术,在网上有一大堆相关的解读,我们先来看看网上的大佬是咋说的
知乎什么是动态代理?
看的多不如敲的多,我们来看看如何的基于动态代理技术获取一个接口的实现类。
开始之前我们先来整理一下需求
1、定义一个接口,并且在该接口中编写一个sayHello()方法
2、基于动态代理技术获取接口的实现类
3、实现sayHello方法
好了,知道要干什么了,来就干活吧
interface CustomInterface { void sayHello(); }
请注意,这一步是最重要的一步,也是最难理解的一步,但是我们不用着急,我们一步一步来
编写实现类
虽然我们的接口可以不编写实现类,但是方法的实现的逻辑也需要我们指定。我们需要实现jdk动态代理的重要接口__InvocationHandler__ 接口,该接口只有一个方法需要我们实现,我们的方法实现逻辑就可以写在其中
class CustomInterfaceProxy implements InvocationHandler{ @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println(method.getName()); return null; } }
/** * 生成实现类,并且用接口去接收该实现类 * Proxy.newProxyInstance方法需要接收三个参数 * 第一个参数: 需要传递一个类加载器实例,在这里我们如果需要给那个生成实现类,就需要传递那个接口的 类加载器 * 第二个参数: 需要传递一个Class数组,在这里我们直接把需要代理的接口的类型信息传递进去 * 第三个参数: 需要传递一个 InvocationHandler的实现类 * * 通过解读该方法的三个参数,我们可以大概的了解到,该方法通过接口的类加载器,加载该接口的类型信息, * 然后与InvocationHandler的实现类进行绑定,之后就会得到一个指定接口的实现类 * */ CustomInterface customInterface = (CustomInterface) Proxy.newProxyInstance(CustomInterface.class.getClassLoader(), new Class[]{CustomInterface.class}, new CustomInterfaceProxy()); //调用方法 customInterface.sayHello();
调用方法后的执行结果
打印了sayHello,这个输出是在我们编写的InvocationHandler实现类中打印的,执行代码如下
@Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println(method.getName()); return null; }
在该实现类中我们可以看到打印的是方法名称,不是实现了sayHello方法。不急,我们再往接口中加个方法看看
interface CustomInterface { void sayHello(); //增加一个求和方法 Integer sum(Integer v1,Integer v2); }
我们调用该方法
//调用求和方法 Integer v1 = 1; Integer v2 = 2; Integer sum = customInterface.sum(v1, v2); System.out.println(String.format("调用求和方法: 求和参数%s,%s, 求和结果:%s ",v1,v2,sum));
然后我们改写一下InvocationHandler实现类中的invoke实现
@Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { String name = method.getName(); System.out.println(method.getName()); //如果为求和方法 if(name.equals("sum")) { Integer v1 = (Integer) args[0]; Integer v2 = (Integer) args[1]; return v1 + v2; } return null; } // 或者这样 @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { String name = method.getName(); System.out.println(method.getName()); //如果为CustomInterface的求和方法 if(CustomInterface.class.getMethod("sum",Integer.class,Integer.class).equals(method)) { Integer v1 = (Integer) args[0]; Integer v2 = (Integer) args[1]; return v1 + v2; } return null; }
输出结果
进行到这里,我们可以得出得结论是,对于InvocationHandler的__invoke__方法,我们可以把这个方法看作是所有接口的统一入口,我们要区分这个方法是通过Method实例进行区分,当我们知道是那个方法后,就可以知道其参数类型,然后编写具体实现,返回该方法所需的返回值。
可能有同学会问,你编写代码就是判断了一下方法名字,方法名字可以重复啊,或者是其他接口的方法名字。这里同学们可以试想一下,当我们调用了接口方法之后,就执行了invoke方法,就说明他们必定具有联系。接下来更深层次的可以通过Debug的方式,查看Method实例的信息,args参数信息就会发现答案
我们在上一章中学习了如何使用jdk动态代理技术,了解了其基本原理,我们就一起来使用该技术实现个小需求吧
需求如下:
1、在接口中编写一个方法
2、在调用方法的实际代码之前,校验方法参数是否为null
我们以 CustomInterface.sum(Interger v1,Integer v2)为例
实现思路:
要实现上面的需求,如果是编写实现类的话,我们可以在每个方法进入之前编写参数校验的逻辑,并且需要每个方法都编写类似于 if(parameter == null) 的逻辑,会产生很多冗余的代码,并且代码阅读性很差。
但是现在我们使用了动态代理技术,我们只需要实现InvocationHandler的__invoke__方法,所有的方法都会经过该方法,这样就有利于我们在该方法编写一个通用的参数校验的方法。说干就干,我们来试试吧,体会下动态代理技术的神奇
1、编写一个校验参数是否为空的方法
/** * 校验调用的方法参数是否为空 * @param method 方法实例 * @param args 传递的实参列表 */ private void checkParameterHaNull(Method method,Object[] args) { //获取该方法的参数数量 int parameterCount = method.getParameterCount(); //没有参数不做处理 if(parameterCount == 0) { return; } //获取该方法的参数封装数据 Parameter[] parameters = method.getParameters(); for (int i = 0; i < parameterCount; i++) { Parameter parameter = parameters[i]; //基础数据类型则不校验是否为空 if (checkBaseType(parameter)) { continue; } //如果对应的参数为空 if(args[i] == null) { String name = method.getName(); String msg = "方法" + name + "的第" + i + "个参数" + parameter.getName() + "不能为空"; throw new RuntimeException(msg); } } }
2、如果有基本数据类型,我们需要单独校验是否是基本数据类型
/** * 校验这个参数是否为基本数据类型的参数 * @param parameter 基于反射封装的参数信息 * @return boolean true-是 false-不是 */ private boolean checkBaseType(Parameter parameter) { Class<?> type = parameter.getType(); return Byte.TYPE.equals(type) || Short.TYPE.equals(type) || Integer.TYPE.equals(type) || Long.TYPE.equals(type) || Character.TYPE.equals(type) || Float.TYPE.equals(type) || Double.TYPE.equals(type) || Boolean.TYPE.equals(type); }
3、调用方法
4、调用测试
第一个参数我们设置为null
5、执行结果
我们在这一章简单并实践了一下基于Jdk的动态代理技术,但是需要注意的是使用Jdk的动态代理技术,只能代理接口,如果要代理非接口类,需要使用cglb动态代理技术,我们的SpringAOP就是基于它实现的。
接下来我们趁热打铁,马上来看看Mybatis是如何使用jdk动态代理技术来实现Mapper接口的
mybatis针对如何实现动态代理根据自身的需求又进行了封装,封装的模块包为__org.apache.ibatis.bingding__包,让我们来看看这个包中有些类,然后这些类具体是干啥的
BindingException.java MapperMethod.java Mapper方法的封装类 MapperProxy.java Mapper接口的代理类 MapperProxyFactory.java Mapper代理类工厂 MapperRegistry.java Mapper工厂注册
首先就来介绍让很多刚工作的同学常问的问题,Mapper的实现类。为了便于理解,我们先大概的了解下该类是干啥的。
该类是实现InvocationHandler接口并抽象出了Mapper接口中所有方法的执行过程的类,一个MapperProxy实例就代表一个Mapper接口的实现类,说白了就是Mapper接口的实现类。
这样说起来有点抽象,接下来让我们来看看核心属性和核心方法
public class MapperProxy<T> implements InvocationHandler, Serializable { private static final long serialVersionUID = -6424540398559729838L; //SqlSession对象,访问数据库 private final SqlSession sqlSession; //Class类型信息,即Mapper接口的Class实例 private final Class<T> mapperInterface; //Mapper方法与我们编写的XML的各种方法的对应关系 // 使用Map进行一一对应,这就是为什么Mapper接口方法名称要与对应的XML文件的sql标签的id相同 // 这里的Map其实是一个ConcurrentHashMap,一个Mapper接口维护一个methodCache private final Map<Method, MapperMethod> methodCache; }
构造方法
//methodCache是由MapperProxyFactory传递进来的 public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethod> methodCache) { this.sqlSession = sqlSession; this.mapperInterface = mapperInterface; this.methodCache = methodCache; }
调用Mapper方法
@Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { //代理以后,所有Mapper的方法调用时,都会调用这个invoke方法 //并不是任何一个方法都需要执行调用代理对象进行执行,如果这个方法是Object中通用的方法(toString、hashCode等)无需执行 if (Object.class.equals(method.getDeclaringClass())) { try { return method.invoke(this, args); } catch (Throwable t) { throw ExceptionUtil.unwrapThrowable(t); } } //这里优化了,去缓存中找MapperMethod final MapperMethod mapperMethod = cachedMapperMethod(method); //执行 return mapperMethod.execute(sqlSession, args); }
缓存MethodMapper
//去缓存中找MapperMethod private MapperMethod cachedMapperMethod(Method method) { MapperMethod mapperMethod = methodCache.get(method); if (mapperMethod == null) { //找不到才去new mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()); methodCache.put(method, mapperMethod); } return mapperMethod; }
怎么就完了,这就是Mapper的实现类?那么多Mapper方法是如何执行的,没看到什么逻辑呢?
没错,这就是Mapper的实现类,并且是基于动态代理技术实现的,它在invoke方法中其实就只是调用了MeperMethod的execute方法而已。
所以想要了解具体的逻辑我们还需要深究MapperMethod,但是具体的代码实现逻辑不是我们本次的重点,Mybatis的执行逻辑非常复杂,我们深究会碰到JDBC的底层,Mybatis的核心组件Cache、ResultSetHandler、Executor、ResultMap,这些组件,随便搞一个出来,都够同学们弄很久了,所以我们这里先告诉同学们,到底Mybatis是如何生成是实现类的
所以,我们接下来看看MapperProxyFactory,MapperProxy的生产者
MapperProxyFactory,首先根据类名我们知道这是一个使用工厂模式设计类的,它的职责是用于生成MapperProxy,而MapperProxy其实就是Mapper接口的实现类,并且是基于动态代理的实现类,我们要理解它其实很简单,当然它的代码也很简单,不信你看
/** * @author Lasse Voss */ /** * 映射器代理工厂 */ public class MapperProxyFactory<T> { private static Logger logger = LoggerFactory.getLogger(MapperProxyFactory.class); // Mapper接口类型信息,和MapperProxy中的 mapperInterface一样 private final Class<T> mapperInterface; // Mapper接口方法与Mabatis的XML select、delete、等定义的标签的对应关系 // 这里我们可以看到这里直接 new了一个ConsurrentHashMap // 它的作用是传递给MapperProxy,这里很高明的是,它是基于一个工厂维护一个引用,在初始化时容器为空, // 但是当对应的Mapper调用方法时就会往该容器中加入映射 // 即一个Mapper接口 对应 一个MapperProxyFactory 对应 一个methodCache 对应多个 MapperProxy // 简单来说就是 多个MapperProxy通过引用的方式共用一个 methodCache // 这样的好处是即可以实现懒加载,第一次加载后,第二次就不需要再加载 private Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<Method, MapperMethod>(); // 此处省略了方法 }
@SuppressWarnings("unchecked") protected T newInstance(MapperProxy<T> mapperProxy) { //用JDK自带的动态代理生成映射器 return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy); } //获取 Mapper实现类 public T newInstance(SqlSession sqlSession) { // 请注意,这里始终把 methodCache传递给MapperProxy就证实了,多个MapperProxy共享一个methodCache final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache); return newInstance(mapperProxy); }
就这,Mapper的实现类就是这样创造出来的?
哈哈,没错,就是这样创造出来的,只不过Mybatis是基于自己的实现方式进行封装
好了,到了这里是不是就知道Mapper的实现类在哪里了,当然如果各位同学需要真正的理解,需要去过一下关于Java反射的知识,这部分知识真的很重要。
上一章我们说到Mapper实现类工厂类是如何产生实现类的,并且还告知了同学了要过一下Java反射的知识,那这个类这次我们就用到了,是关于类加载的
/** * @author Clinton Begin * @author Eduardo Macarron * @author Lasse Voss */ /** * 映射器注册机 * */ public class MapperRegistry { // Mybatis 配置信息类,该类包含了所有的Mybatis配置信息 // 类似于 数据源配置、事务管理器、插件、别名注册信息等等,很多,关于该类,我们会在Mybatis配置中去深究 // 该类几乎贯穿了整个 Mybatis的生命周期 private Configuration config; //将已经添加的映射都放入HashMap // 这里我们可以看到,该类只维护了一个Class实例与MapperProxyFactory的映射关系 // 那很明显, knownMappers 的 key值就是Mapper接口的Class信息 // 再次强调,Class实例信息在一个虚拟机,即一个Java应用中始终只有一个 private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<Class<?>, MapperProxyFactory<?>>(); }
//通过 Class类型实例添加Mapper实现类工厂 public <T> void addMapper(Class<T> type) { //mapper必须是接口!才会添加 if (type.isInterface()) { if (hasMapper(type)) { //如果重复添加了,报错 throw new BindingException("Type " + type + " is already known to the MapperRegistry."); } boolean loadCompleted = false; try { // 直接创建一个MapperProxyFactory放入容器中 knownMappers.put(type, new MapperProxyFactory<T>(type)); // It's important that the type is added before the parser is run // otherwise the binding may automatically be attempted by the // mapper parser. If the type is already known, it won't try. MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type); parser.parse(); loadCompleted = true; } finally { //如果加载过程中出现异常需要再将这个mapper从mybatis中删除,这种方式比较丑陋吧,难道是不得已而为之? if (!loadCompleted) { knownMappers.remove(type); } } } } /** * 通过包名添加Mapper实现类工厂 * @since 3.2.2 */ public void addMappers(String packageName, Class<?> superType) { //查找包下所有是superType的类 ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<Class<?>>(); resolverUtil.find(new ResolverUtil.IsA(superType), packageName); Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses(); for (Class<?> mapperClass : mapperSet) { addMapper(mapperClass); } } @SuppressWarnings("unchecked") //返回Mapper实现类 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 { // 获取到Mapper实现类工厂后,直接创建一个并返回 return mapperProxyFactory.newInstance(sqlSession); } catch (Exception e) { throw new BindingException("Error getting mapper instance. Cause: " + e, e); } }
到了这里,其实整个Mapper接口的创建与获取方法我们已经了解得七七八八了,最主要的还是Java基础,基础意味着更高层次的抽象,更高层次的封装。
其实对Mapper的操作,Class类型实例贯穿到底,从添加一个Mapper实现类工厂,还是获取一个Mapper实现类,并且实现的方式很简单,就是我们最常用到的HashMap,读下来还是蛮有收获的
这里我们不深究该类的实现方式,该类的复杂程度,三言两语是说不清的,我们可以简单的看看,他的核心方法
//执行 public Object execute(SqlSession sqlSession, Object[] args) { Object result; //可以看到执行时就是4种情况,insert|update|delete|select,分别调用SqlSession的4大类方法 if (SqlCommandType.INSERT == command.getType()) { logger.info("执行Insert"); Object param = method.convertArgsToSqlCommandParam(args); result = rowCountResult(sqlSession.insert(command.getName(), param)); } else if (SqlCommandType.UPDATE == command.getType()) { logger.info("执行Update"); Object param = method.convertArgsToSqlCommandParam(args); result = rowCountResult(sqlSession.update(command.getName(), param)); } else if (SqlCommandType.DELETE == command.getType()) { logger.info("执行Delete"); Object param = method.convertArgsToSqlCommandParam(args); result = rowCountResult(sqlSession.delete(command.getName(), param)); } else if (SqlCommandType.SELECT == command.getType()) { logger.info("执行Select"); if (method.returnsVoid() && method.hasResultHandler()) { //如果有结果处理器 executeWithResultHandler(sqlSession, args); result = null; } else if (method.returnsMany()) { //如果结果有多条记录 result = executeForMany(sqlSession, args); } else if (method.returnsMap()) { //如果结果是map result = executeForMap(sqlSession, args); } else { //否则就是一条记录 Object param = method.convertArgsToSqlCommandParam(args); result = sqlSession.selectOne(command.getName(), param); } } else { 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; }
嘿嘿,是不是有点JDBC的味道了,没错,我们已经快要触碰到底层了,JDBC的知识需要重新拾起来才能理解接下来的内容
Mybatis的buiding模块是我们学习动态代理的技术的范例,他实现方式简单,容易理解,但最重要的还是要的我们动手写,多多Debug,是学习动态代理技术的起点,也能帮助我们理解SpringAOP的实现。
好好学习,天天向上