Mybatis-Mapper实现类
摘要
说到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动态代理技术实践
概述
mybatis针对如何实现动态代理根据自身的需求又进行了封装,封装的模块包为__org.apache.ibatis.bingding__包,让我们来看看这个包中有些类,然后这些类具体是干啥的
BindingException.java
MapperMethod.java Mapper方法的封装类
MapperProxy.java Mapper接口的代理类
MapperProxyFactory.java Mapper代理类工厂
MapperRegistry.java Mapper工厂注册
核心类解读
Mapper的实现类-MapperProxy
首先就来介绍让很多刚工作的同学常问的问题,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的生产者
Mapper实现类工厂----MapperProxyFactory
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 - MapperRegistry
上一章我们说到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,读下来还是蛮有收获的
Mapper接口方法的实际执行者-MapperMethod
这里我们不深究该类的实现方式,该类的复杂程度,三言两语是说不清的,我们可以简单的看看,他的核心方法
核心方法
//执行
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的实现。
好好学习,天天向上