Mybatis
可以把
Mapper.xml
文件直接映射到对应的接口,调用接口方法会自动去Mapper.xml文件中找到对应的标签,这个功能就是利用java的动态代理在binding包中实现的。
动态代理
:动态代理是Java语言中非常经典的一种设计模式,也是所有设计模式中最难理解的一种。将通过一个简单的例子模拟JDK动态代理实现,让你彻底明白动态代理设计模式的本质。
什么是代理?
从字面意思来看,代理比较好理解,无非就是代为处理的意思。举个例子,你在上大学的时候,总是喜欢逃课。因此,你拜托你的同学帮你答到,而自己却窝在宿舍玩游戏… 你的这个同学恰好就充当了代理的作用,代替你去上课。
是的,你没有看错,代理就是这么简单!
理解了代理的意思,你脑海中恐怕还有两个巨大的疑问:
- 怎么实现代理模式
- 代理模式有什么实际用途
要理解这两个问题,看一个简单的例子:
public interface Flyable {
void fly();
}
public class Bird implements Flyable {
@Override
pubilc void fly(){
System.out.println("Bird is flying...");
try {
Thread.sleep(new Random().nextInt(1000));
}catch(InterruptedException e){
e.printStackTrace();
}
}
}
很简单的一个例子,用一个随机睡眠时间模拟小鸟在空中的飞行时间。接下来问题来了,如果我要知道小鸟在天空中飞行了多久,怎么办?
有人会说,很简单,在Bird->fly()方法的开头记录起始时间,在方法结束记录完成时间,两个时间相减就得到了飞行时间。
@Override
public void fly() {
long start = System.currentTimeMillis();
System.out.println("Bird is flying...");
try {
Thread.sleep(new Random().nextInt(1000));
}catch (InterruptedException e){
e.printStackTrace();
}
long end = System.currentTimeMillis();
System.out.println("Fly time = " + (end - start));
}
的确,这个方法没有任何问题,接下来加大问题的难度。如果Bird这个类来自于某个SDK(或者说Jar包)提供,你无法改动源码,怎么办?
一定会有人说,我可以在调用的地方这样写:
public static void main(String[] args) {
Bird bird = new Bird();
long start = System.currentTimeMillis();
bird.fly();
long end = System.currentTimeMillis();
System.out.println("Fly time = " + (end - start));
}
这个方案看起来似乎没有问题,但其实你忽略了准备这些方法所需要的时间,执行一个方法,需要开辟栈内存、压栈、出栈等操作,这部分时间也是不可以忽略的。因此,这个解决方案不可行。那么,还有什么方法可以做到呢?
a)使用继承
继承是最直观的解决方案,相信你已经想到了,至少我最开始想到的解决方案就是继承。
为此,我们重新创建一个类Bird2,在Bird2中我们只做一件事情,就是调用父类的fly方法,在前后记录时间,并打印时间差:
public class Bird2 extends Bird {
@Override
public void fly() {
long start = System.currentTimeMillis();
super.fly();
long end = System.currentTimeMillis();
System.out.println("Fly time = " + (end - start));
}
}
这是一种解决方案,还有一种解决方案叫做:
聚合
,其实也是比较容易想到的。
我们再次创建新类Bird3,在Bird3的构造方法中传入Bird实例。同时,让Bird3也实现Flyable接口,并在fly方法中调用传入的Bird实例的fly方法:
public class Bird3 implements Flyable {
private Bird bird;
public Bird3(Bird bird) {
this.bird = bird;
}
@Override
long start = System.currentTimeMillis();
bird.fly();
long end = System.currentTimeMillis();
System.out.println("Fly time = " + (end - start));
}
为了记录Bird->fly()方法的执行时间,我们在前后添加了记录时间的代码。同样地,通过这种方法我们也可以获得小鸟的飞行时间。那么,这两种方法孰优孰劣呢?咋一看,不好评判!
继续深入思考,用问题推导来解答这个问题:
问题一
:如果我还需要在fly方法前后打印日志,记录飞行开始和飞行结束,怎么办?
有人说,很简单!继承Bird2并在在前后添加打印语句即可。那么,问题来了,请看问题二。
问题二
:如果我需要调换执行顺序,先打印日志,再获取飞行时间,怎么办?
有人说,再新建一个类Bird4继承Bird,打印日志。再新建一个类Bird5继承Bird4,获取方法执行时间。
问题显而易见:使用继承将导致类无限制扩展,同时灵活性也无法获得保障。那么,使用 聚合 是否可以避免这个问题呢?
答案是:可以!但我们的类需要稍微改造一下。修改Bird3类,将聚合对象Bird类型修改为Flyable
public class Bird3 implements Flyable {
private Flyable flyable;
public Bird3(Flyable flyable) {
this.flyable = flyable;
}
@Override
public void fly() {
long start = System.currentTimeMillis();
flyable.fly();
long end = System.currentTimeMillis();
System.out.println("Fly time = " + (end - start));
}
}
为了让你看的更清楚,我将Bird3更名为BirdTimeProxy,即用于获取方法执行时间的代理的意思。同时我们新建BirdLogProxy代理类用于打印日志:
public class BirdLogProxy implements Flyable {
private Flyable flyable;
public BirdLogProxy(Flyable flyable) {
this.flyable = flyable;
}
@Override
public void fly(){
System.out.println("Bird fly start...");
flyable.fly();
System.out.println("Bird fly end...");
}
}
接下来神奇的事情发生了,如果我们需要先记录日志,再获取飞行时间,可以在调用的地方这么做:
public static void main(String[] args) {
Bird bird = new Bird();
BirdLogProxy p1 = new BirdLogProxy(bird);
BirdTimeProxy p2 = new BirdTimeProxy(p1);
p2.fly();
}
反过来,可以这么做:
public static void main(String[] args) {
Bird bird = new Bird();
BirdTimeProxy p2 = new BirdTimeProxy(bird);
BirdLogProxy p1 = new BirdLogProxy(p2);
p1.fly();
}
看到这里,有同学可能会有疑问了。虽然现象看起来,聚合可以灵活调换执行顺序。可是,为什么
聚合
可以做到,而继承不行呢。我们用一张图来解释一下:
静态代理
接下来,观察上面的类BirdTimeProxy,在它的fly方法中我们直接调用了flyable->fly()方法。换而言之,BirdTimeProxy其实代理了传入的Flyable对象,这就是典型的静态代理实现。
从表面上看,静态代理已经完美解决了我们的问题。可是,试想一下,如果我们需要计算SDK中100个方法的运行时间,同样的代码至少需要重复100次,并且创建至少100个代理类。往小了说,如果Bird类有多个方法,我们需要知道其他方法的运行时间,同样的代码也至少需要重复多次。因此,静态代理至少有以下两个局限性问题:
- 如果同时代理多个类,依然会导致类无限制扩展
- 如果类中有多个方法,同样的逻辑需要反复实现
那么,我们是否可以使用同一个代理类来代理任意对象呢?我们以获取方法运行时间为例,是否可以使用同一个类(例如:TimeProxy)来计算任意对象的任一方法的执行时间呢?甚至再大胆一点,代理的逻辑也可以自己指定。比如,获取方法的执行时间,打印日志,这类逻辑都可以自己指定。这就是最难理解的部分:
动态代理
。
动态代理
继续回到上面这个问题:是否可以使用同一个类(例如:TimeProxy)来计算任意对象的任一方法的执行时间呢。
这个部分需要一定的抽象思维,我想,你脑海中的第一个解决方案应该是使用反射。反射是用于获取已创建实例的方法或者属性,并对其进行调用或者赋值。很明显,在这里,反射解决不了问题。但是,再大胆一点,如果我们可以动态生成TimeProxy这个类,并且动态编译。然后,再通过反射创建对象并加载到内存中,不就实现了对任意对象进行代理了吗?那么用一张图来描述接下来要做什么:
动态生成Java源文件并且排版是一个非常繁琐的工作,为了简化操作,我们使用
JavaPoet
这个第三方库帮我们生成TimeProxy的源码。不理解
JavaPoet
没有关系,你只要把它当成一个Java源码生成工具使用即可。
第一步:生成TimeProxy源码
public class Proxy{
public static Object newProxyInstance() throws IOException {
TypeSpec.Builder typeSpecBuilder = TypeSpec.classBuilder("TimeProxy").addSuperinterface(Flyable.class);
FieldSpec fieldSpec = FieldSpec.builder(Flyable.class, "flyable", Modifier.PRIVATE).build();
typeSpecBuilder.addField(fieldSpec);
MethodSpec constructorMethodSpec = MethodSpec.constructorBuilder().addModifiers(Modifier.PUBLIC).addParameter(Flyable.class, "flyable").addStatement("this.flyable = flyable").build();
typeSpecBuilder.addMethod(constructorMethodSpec);
Method[] methods = Flyable.class.getDeclaredMethods();
for (Method method : methods) {
MethodSpec methodSpec = MethodSpec.methodBuilder(method.getName()).addModifiers(Modifier.PUBLIC).addAnnotation(Override.class).returns(method.getReturnType()).addStatement("long start = $T.currentTimeMillis()", System.class).addCode("\n").addStatement("this.flyable." + method.getName() + "()").addCode("\n").addStatement("long end = $T.currentTimeMillis()", System.class).addStatement("$T.out.println(\"Fly Time =\" + (end - start))", System.class).build();
typeSpecBuilder.addMethod(methodSpec);
}
JavaFile javaFile = JavaFile.builder("com.Slgod.proxy", typeSpecBuilder.build()).build();
// 为了看的更清楚,我将源码文件生成到桌面
javaFile.writeTo(new File("/Users/Slgod/Desktop/"));
return null;
}
}
在main方法中调用Proxy.newProxyInstance(),你将看到桌面已经生成了TimeProxy.java文件,生成的内容如下:
package com.Slgod.proxy;
import java.lang.Override;
import java.lang.System;
class TimeProxy implements Flyable {
private Flyable flyable;
public TimeProxy(Flyable flyable) {
this.flyable = flyable;
}
@Override
public void fly() {
long start = System.currentTimeMillis();
this.flyable.fly();
long end = System.currentTimeMillis();
System.out.println("Fly Time =" + (end - start));
}
}
第二步:编译TimeProxy源码
编译TimeProxy源码我们直接使用JDK提供的编译工具即可,为了使你看起来更清晰,我使用一个新的辅助类来完成编译操作:
public class JavaCompiler {
public static void compile(File javaFile) throws IOException {
javax.tools.JavaCompiler javaCompiler = ToolProvider.getSystemJavaCompiler();
StandardJavaFileManager fileManager = javaCompiler.getStandardFileManager(null, null, null);
Iterable iterable = fileManager.getJavaFileObjects(javaFile);
javax.tools.JavaCompiler.CompilationTask task = javaCompiler.getTask(null, fileManager, null, null, null, iterable);
task.call();
fileManager.close();
}
}
在Proxy->newProxyInstance()方法中调用该方法,编译顺利完成:
// 为了看的更清楚,我将源码文件生成到桌面
String sourcePath = "/Users/Slgod/Desktop/";
javaFile.writeTo(new File(sourcePath));
// 编译
JavaCompiler.compile(new File(sourcePath + "/com/youngfeng/proxy/TimeProxy.java"));
第三步:加载到内存中并创建对象
URL[] urls = new URL[] {new URL("file:/" + sourcePath)};
URLClassLoader classLoader = new URLClassLoader(urls);
Class clazz = classLoader.loadClass("com.Slgod.proxy.TimeProxy");
Constructor constructor = clazz.getConstructor(Flyable.class);
Flyable flyable = (Flyable) constructor.newInstance(new Bird());
flyable.fly();
通过以上三个步骤,我们至少解决了下面两个问题:
- 不再需要手动创建TimeProxy
- 可以代理任意实现了Flyable接口的类对象,并获取接口方法的执行时间
可是,说好的任意对象呢…
第四步:增加InvocationHandler接口
查看Proxy->newProxyInstance()的源码,代理类继承的接口我们是写死的,为了增加灵活性,我们将接口类型作为参数传入:
接口的灵活性问题解决了,TimeProxy的局限性依然存在,它只能用于获取方法的执行时间,而如果要在方法执行前后打印日志则需要重新创建一个代理类,显然这是不妥的!
为了增加控制的灵活性,我们考虑针将代理的处理逻辑也抽离出来(这里的处理就是打印方法的执行时间)。新增
InvocationHandler
接口,用于处理自定义逻辑:
public interface InvocationHandler {
void invoke(Object proxy, Method method, Object[] args);
}
想象一下,如果客户程序员需要对代理类进行自定义的处理,只要实现该接口,并在invoke方法中进行相应的处理即可。这里我们在接口中设置了三个参数(其实也是为了和JDK源码保持一致):
-
proxy => 这个参数指定动态生成的代理类,这里是
TimeProxy
- method => 这个参数表示传入接口中的所有Method对象
- args => 这个参数对应当前method方法中的参数
引入了InvocationHandler接口之后,我们的调用顺序应该变成了这样:
MyInvocationHandler handler = new MyInvocationHandler();
Flyable proxy = Proxy.newProxyInstance(Flyable.class, handler);
proxy.fly();
方法执行流:proxy.fly() => handler.invoke()
为此,需要在Proxy.newProxyInstance()方法中做如下改动:
- 在newProxyInstance方法中传入InvocationHandler
- 在生成的代理类中增加成员变量handler
- 在生成的代理类方法中,调用invoke方法
public static Object newProxyInstance(Class inf, InvocationHandler handler) throws Exception {
TypeSpec.Builder typeSpecBuilder = TypeSpec.classBuilder("TimeProxy")
.addModifiers(Modifier.PUBLIC)
.addSuperinterface(inf);
FieldSpec fieldSpec = FieldSpec.builder(InvocationHandler.class, "handler", Modifier.PRIVATE).build();
typeSpecBuilder.addField(fieldSpec);
MethodSpec constructorMethodSpec = MethodSpec.constructorBuilder()
.addModifiers(Modifier.PUBLIC)
.addParameter(InvocationHandler.class, "handler")
.addStatement("this.handler = handler")
.build();
typeSpecBuilder.addMethod(constructorMethodSpec);
Method[] methods = inf.getDeclaredMethods();
for (Method method : methods) {
MethodSpec methodSpec = MethodSpec.methodBuilder(method.getName())
.addModifiers(Modifier.PUBLIC)
.addAnnotation(Override.class)
.returns(method.getReturnType())
.addCode("try {\n")
.addStatement("\t$T method = " + inf.getName() + ".class.getMethod(\"" + method.getName() + "\")", Method.class)
// 为了简单起见,这里参数直接写死为空
.addStatement("\tthis.handler.invoke(this, method, null)")
.addCode("} catch(Exception e) {\n")
.addCode("\te.printStackTrace();\n")
.addCode("}\n")
.build();
typeSpecBuilder.addMethod(methodSpec);
}
JavaFile javaFile = JavaFile.builder("com.Slgod.proxy", typeSpecBuilder.build()).build();
// 为了看的更清楚,我将源码文件生成到桌面
String sourcePath = "/Users/Slgod/Desktop/";
javaFile.writeTo(new File(sourcePath));
// 编译
JavaCompiler.compile(new File(sourcePath + "/com/Slgod/proxy/TimeProxy.java"));
// 使用反射load到内存
URL[] urls = new URL[] {new URL("file:" + sourcePath)};
URLClassLoader classLoader = new URLClassLoader(urls);
Class clazz = classLoader.loadClass("com.Slgod.proxy.TimeProxy");
Constructor constructor = clazz.getConstructor(InvocationHandler.class);
Object obj = constructor.newInstance(handler);
return obj;
}
直接调用该方法,查看最后生成的源码。在main方法中测试newProxyInstance查看生成的TimeProxy源码:
测试代码
Proxy.newProxyInstance(Flyable.class, new MyInvocationHandler(new Bird()));
生成的TimeProxy.java源码
package com.Slgod.proxy;
import java.lang.Override;
import java.lang.reflect.Method;
public class TimeProxy implements Flyable {
private InvocationHandler handler;
public TimeProxy(InvocationHandler handler) {
this.handler = handler;
}
@Override
public void fly() {
try {
Method method = com.Slgod.proxy.Flyable.class.getMethod("fly");
this.handler.invoke(this, method, null);
} catch(Exception e) {
e.printStackTrace();
}
}
}
MyInvocationHandler.java
public class MyInvocationHandler implements InvocationHandler {
private Bird bird;
public MyInvocationHandler(Bird bird) {
this.bird = bird;
}
@Override
public void invoke(Object proxy, Method method, Object[] args) {
long start = System.currentTimeMillis();
try {
method.invoke(bird, new Object[] {});
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
long end = System.currentTimeMillis();
System.out.println("Fly time = " + (end - start));
}
}
至此,整个方法栈的调用栈变成了这样:
在静态代理部分,我们在代理类中传入了被代理对象。可是,使用newProxyInstance生成动态代理对象的时候,我们居然不再需要传入被代理对象了。我们传入了的实际对象是InvocationHandler实现类的实例,这看起来有点像生成了InvocationHandler的代理对象,在动态生成的代理类的任意方法中都会间接调用InvocationHandler->invoke(proxy, method, args)方法。
其实的确是这样。TimeProxy真正代理的对象就是InvocationHandler,不过这里设计的巧妙之处在于,InvocationHandler是一个接口,真正的实现由用户指定。另外,在每一个方法执行的时候,invoke方法都会被调用 ,这个时候如果你需要对某个方法进行自定义逻辑处理,可以根据method的特征信息进行判断分别处理。
再回到Mybatis,继续研究Mybatis的配置和接口映射原理
一、注册Mapper
在初始化时会把获取到的Mapper接口注册到MapperRegistry,注册的时候创建一个Mapper代理工厂,这个工厂通过JDK的代理创建一个执行对象,创建代理需要的InvocationHandler为MapperProxy
//接口注册
public class MapperRegistry {
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 {
//放到map中, value为创建代理的工厂
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.
//这里是解析Mapper接口里面的注解
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse();
loadCompleted = true;
}finally{
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
}
二、获取接口对象
从knownMappers中根据接口类型取出对应的代理创建工厂,用该工厂创建代理。
public class MapperRegistry {
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
//取出MapperProxyFactory
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);
}
}
}
//创建代理的工厂
public class MapperProxyFactory<T> {
//需要创建代理的接口
private final Class<T> mapperInterface;
//执行方法的缓存,不需要每次都创建MapperMethod
private Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<Method, MapperMethod>();
public MapperProxyFactory(Class<T> mapperInterface) {
this.mapperInterface = mapperInterface;
}
public Class<T> getMapperInterface() {
return mapperInterface;
}
public Map<Method, MapperMethod> getMethodCache() {
return methodCache;
}
@SuppressWarnings("unchecked")
protected T newInstance(MapperProxy<T> mapperProxy) {
//创建代理, InvocationHanderl是MapperProxy
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface },mapperProxy);
}
//传人sqlSession创建代理1
//@param sqlSession1
//@return
public T newInstance(SqlSession sqlSession) {
//把代理执行需要用到的对象传入
final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
}
三、调用接口方法
调用代理方法会进入到MapperProxy的public Object invoke(Object proxy, Method method, Object[] args)方法
public class MapperProxy<T> implements InvocationHandler, Serializable{
private static final long serialVersionUID = -6424540398559729838L;
private final SqlSession sqlSession;
private final Class<T> mapperInterface;
private final Map<Method, MapperMethod> methodCache;
public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethod> methodCache) {
this.sqlSession = sqlSession;
this.mapperInterface = mapperInterface;
this.methodCache = methodCache;
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//如果方法是Object里面的则直接调用方法
if (Object.class.equals(method.getDeclaringClass())){
try {
return method.invoke(this, args);
}catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
//获取执行方法的封装对象
final MapperMethod mapperMethod = cachedMapperMethod(method);
//里面就是找到对应的sql 执行sql语句
return mapperMethod.execute(sqlSession, args);
}
//缓存, 不需要每次都创建
private MapperMethod cachedMapperMethod(Method method) {
MapperMethod mapperMethod = methodCache.get(method);
if (mapperMethod == null) {
//传入配置参数
mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration());
methodCache.put(method, mapperMethod);
}
return mapperMethod;
}
}
最终执行sql会进入到MapperMethod中execute方法:
//具体的根据接口找到配置文件标签的类
public class MapperMethod {
private final SqlCommand command;
private final MethodSignature method;
public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
//SqlCommand封装该接口方法需要执行sql的相关属性,如:id(name), 类型
this.command = new SqlCommand(config, mapperInterface, method);
//执行方法特性进行封装,用于构造sql参数,判断执行sql逻辑走哪条分支
this.method = new MethodSignature(config, method);
}
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
//先找到对应的执行sql类型, sqlSession会调用不同方法
if (SqlCommandType.INSERT == command.getType()) {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.insert(command.getName(), param));
} else if (SqlCommandType.UPDATE == command.getType()) {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.update(command.getName(), param));
} else if (SqlCommandType.DELETE == command.getType()) {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.delete(command.getName(), param));
} else if (SqlCommandType.SELECT == command.getType()) {
//如果是查询, 需要对返回做判断处理
//根据方法的特性判断进入哪个执行分支
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 {
//只查一条数据
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;
}
}
上面就是根据接口、方法、配置参数找到对应的执行sql,并构造参数,解析执行结果,具体sql执行在sqlSession流程里面,后面再看。