目录
代理模式简介
JDK动态代理
代理模式的应用
动态代理在Mybatis中的应用
我最认同的网络上对于代理模式的解释是代理模式提供了一种对于目标对象的另一种访问形式,意思是通过代理对象访问目标对象。从这句话中我们能够得到这样几个信息,要想实现代理模式,我们需要一个目标对象,一个代理对象,并且代理对象可以访问目标对象。
举个很简单的例子:我想剥一个橘子,但是我不自己剥,我找我的好朋友小明帮我剥,在这件事情上,我就是一个目标对象,我要做的事情是剥橘子,我的朋友小明就是代理对象,代理我剥橘子,而且他在剥橘子的前后可能做其他事情,比如先洗手,后把剥好的橘子递给我。
所以从上面的描述中不难看出代理对象包含了目标对象,是目标对象的扩展。java的总共有三种代理模式:静态代理、JDK动态代理、CJLIB代理。本篇文章以JDK动态代理为例,因为Mybatis的执行核心就是使用的JDK动态代理,我们从JDK动态代理的实现到Mybatis中代理模式的运用来深入的了解它。
实现一个简单的JDK动态代理需要以下元素:
对象接口:
/**
* @Description: 抽象接口举例
* @Auther:
* @Date: 2020/7/23 11:09
*/
public interface SimpleExample {
/**
* do some thing
*/
void doSomeThing();
}
接口实现类:
/**
* @Description: SimpleExample接口实现类
* @Auther:
* @Date: 2020/7/23 11:11
*/
public class SimpleExampleImpl implements SimpleExample {
@Override
public void doSomeThing() {
System.out.println("do some thing!");
}
}
JDK动态代理类:
/**
* @Description:
* @Auther: able.liu
* @Date: 2020/7/23 11:13
*/
public class JdkDynamicProxy implements InvocationHandler {
private Object target;
public JdkDynamicProxy(Object target){
this.target = target;
}
public T getProxy(){
return (T) Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("before do other thing");
Object o = method.invoke(target, args);
System.out.println("after do some thing");
return o;
}
}
区分是否是JDK动态代理的标志就是代理类重写了InvocationHandler,实现代理的关键就是调用了Proxy.newProxyInstance方法,这个方法public static Object newProxyInstance(ClassLoader loader, Class>[] interfaces, InvocationHandler h)的三个参数定义如下:
ClassLoader loader
:指定当前目标对象使用类加载器,获取加载器的方法是固定的Class>[] interfaces
:目标对象实现的接口的类型,使用泛型方式确认类型InvocationHandler h
:事件处理,执行目标对象的方法时,会触发事件处理器的方法,会把当前执行目标对象的方法作为参数传入
至于这个方法的底层实现,感兴趣的同学可以扒一下源码。上面的代理的示例测试方法如下:
public class JdkDynamicProxyTest {
public static void main(String[] args) {
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
SimpleExample example = new JdkDynamicProxy(new SimpleExampleImpl()).getProxy();
example.doSomeThing();
}
}
例子很简单,我们来看下代理模式的UML类图:
通过上面的描述,我们得出结论,JDK动态代理有以下特点:
通过以上对于JDK动态代理的简单介绍加上实例的实现,我们已经认识了动态代理,那么代理模式在实际的开发工作中有哪些应用呢?
- 远程代理:可以理解为远程对象的本地代表,所谓“远程对象”就是一种对象,活在不同的java虚拟机(JVM)堆中。更一般的说法是在不同的地址空间运行的远程对象。所谓本地代表,就是一种可以由本地方法调用的对象,其行为会转发到远程对象中。所以当客户对象调用代理对象,就像是在做远程方法调用,其实只是调用本地堆中的”代理“对象上的方法,再由代理处理所有网络通信的低层细节。Java已经有内置远程调用的功能了,可以帮助我们实现远程代理。变量只能引用和当前代码语句在同一堆空间中的对象。
虚拟代理:帮助我们控制访问创建开销大的资源。虚拟代理作为创建开销大的对象的代表,经常会直到我们真正需要一个对象的时候才创建它。当对象在创建前和创建中时,由虚拟代理地来扮演对象的替身。对象创建后,代理就会将请求直接委托给对象。
保护代理:基于权限控制对资源的访问。
说起Mybatis我们就不得不提一下Mybatis的sql的执行流程。大家都知道Mybatis执行一个sql是通过SqlSessionFactoryBuilder的build方法读取配置文件中的配置生成SqlSessionFactory,然后SqlSessionFactory调用openSession得到sqlSession,通过sqlSession 获取到 Mapper的代理对象MapperProxy,然后通过MapperProxy进行数据库的相关操作。
Mybatis之Sql的执行流程如下:
我想你也发现了MapperProxy,这是mybatis执行可以通过配置执行我们dao层接口的关键。我们从源码更能直观的看到Mybatis对于JDK动态代理的应用。
我们从代理模式必须的几个要素看起,首先我们需要有目标对象,及目标接口,在我们的项目中这些目标接口就是我们在配置文件中配置的DAO接口,配置举例如下:
......
......
接下来我们需要一个代理对象(代理类),在mybatis中这个代理类就是MapperProxy,我们看下源码中代理类的实现:
首先是在MapperProxyFactory构造我们的代理对象。
public class MapperProxyFactory {
...
...
//jdk动态代理我们写的dao接口
@SuppressWarnings("unchecked")
protected T newInstance(MapperProxy mapperProxy) {
//返回代理对象MapperProxy
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[]{mapperInterface}, mapperProxy);
}
public T newInstance(SqlSession sqlSession) {
final MapperProxy mapperProxy = new MapperProxy(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
}
我们看下MapperProxy的源码:
public class MapperProxy implements InvocationHandler, Serializable {
...
...
...
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 t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
final MapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(sqlSession, args);
}
...
...
}
到这就很明显了,该类重写了InvocationHandler,这可以说是JDK动态代理的标志了。我们看这个invoke接口的实现,调用了mapperMethod.execute, 找到类MapperMethod类的execute方法,发现execute中通过调用本类中的其他方法获取并封装返回结果。我们来看下这个方法的完整代码:
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
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;
}
MapperMethod类是整个代理机制的核心类。上面方法中的command和method分别初始化自MapperMethod类的两个内部类SqlCommand和MethodSignature。SqlCommand用来封装CRUD操作,也就是我们在xml中配置的操作的节点。每个节点都会生成一个MappedStatement类。MethodSignature用来封装方法的参数以及返回类型,在execute的方法中我们发现在这里又回到了SqlSession中的接口调用,和我们自己实现UerDao接口的方式中直接用SqlSession对象调用DefaultSqlSession的实现类的方法是一样的,经过一大圈的代理又回到了原地,这就是整个动态代理的实现过程了。
你如果还想自己看sql是如何被执行的,可以追一下sqlSession.insert(command.getName(), param))的代码,追下去可以发现最终执行的PreparedStatement.execute()方法。