一、什麼是動態代理
代理模式是 Java 中的常用設計模式,代理類通過調用被代理類的相關方法,提供預處理、過濾、事後處理等服務,動態代理及通過反射機制動態實現代理機制。如Spring中使用動態代理完成AOP的操作,Mybatis中使用動態代理完成對Mapper Interface到可用的Mapper Class的生成。
簡單的說我們現在有一個計算的方法,我們在寫代碼時沒有加入時間統計的這個業務,我們如果需要對業務提供計算業務時,我們需要修改原有的這些代碼,並重新侵入一些代碼。這樣如果有很多計算方法我們需要都給它們加上統計時間的業務,我們就得找到所有這些有關於計算的方法,並手動添加時間統計。這樣的做法很油膩。
二、JDK動態代理
無論JDK動態代理還是Cglib動態代理都是用反射去實現的。JDK動態代理是基於接口實現的。其大概的原理就是根據當前對象,獲取對象的class文件,獲取到其接口的類加載器 和 原生對象所擁有的接口 還有一個我們自定義的處理器。再通過實現處理器中的invoke方法,封裝原生對象的方法並得到一個代理後的屬於該接口的實現類。並代碼如下:
首先:我們需要擁有一個接口:
public interface CalculationInterface {
/**
* 做計算類
* */
public void doCalculation();
}
其次:我們需要一個具體的實現類:這個實現類繼承了上面的接口,並實現了計算的方法。
public class MakeCalculation implements CalculationInterface{
/**
* 做計算
* */
@Override
public void doCalculation(){
System.out.println("做點計算");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("計算了兩秒鐘....");
}
}
第三步:我們需要一個處理器,這個處理器的作用是負責創建一個代理對象。我們去創建代理對象時,JDK已經提供給我們一個接口(InvocationHandler),我們只需要自定義一個處理類實現InvocationHandler接口並實現其中的invoke方法,在invoke方法中選擇我們要去包裝的方法, 對其進行包裝。
第四步:我們需要去獲取代理對象的方法,這個時候就需要有一個原生對象。因爲我們的處理器只是通過反射拿到原生對象的方法,再對原生對象的方法增強或重寫。所以我們在處理器中需要保有一個原生對象(你可能在別的博客中看到處理類中並沒有保有原生對象,其實都是一個道理。我將獲得代理對象的方法封裝到處理類中,以便於用戶獲取代理對象時,可以透明無感。)、
第三步、第四步中代碼如下,除了基礎的JDK代理,我還提供了guava reflect包對JDK動態代理封裝後的調用方法, 以簡化書寫
public class ProxyHandler implements InvocationHandler{
private MakeCalculation target;
private void setTarget(MakeCalculation target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if("doCalculation".equals(method.getName())){
long start = System.currentTimeMillis();
method.invoke(target,args);
long end = System.currentTimeMillis();
System.out.println(end - start);
}else{
method.invoke(proxy,args);
}
return null;
}
/**
*用戶提供一個子類去創建一個子類的代理類
*/
public CalculationInterface getProxy(MakeCalculation target){
setTarget(target);
return (CalculationInterface)Proxy.newProxyInstance(CalculationInterface.class.getClassLoader(),target.getClass().getInterfaces(),this);
}
/**
* 用戶直接獲取代理對象,我們內部獲取要被代理的對象,並對其代理
* */
public CalculationInterface getProxyWithOutTarget(){
setTarget(new MakeCalculation());
return (CalculationInterface)Proxy.newProxyInstance(CalculationInterface.class.getClassLoader(),target.getClass().getInterfaces(),this);
}
/**
* 用guava簡化原生JDK代理語法
* 需要導入com.google.common.reflect.Reflection;
* */
public CalculationInterface getProxyWithGuava(){
setTarget(new MakeCalculation());
return Reflection.newProxy(CalculationInterface.class,this);
}
}
第五步:測試類
/**
* JDK動態代理測試類
*/
public class ProxyTest {
@Test
public void proxy(){
ProxyHandler proxyHandler = new ProxyHandler();
MakeCalculation calculation = new MakeCalculation();
System.out.println("代理前的原對象輸出的結果----");
calculation.doCalculation();
// 用戶傳入子類獲取代理
System.out.println();
System.out.println("用戶自傳原生類生成代理 -- 代理對象輸出的結果----");
proxyHandler.getProxy(calculation).doCalculation();
// 用戶直接獲取代理後的對象
System.out.println();
System.out.println("封裝原生類 -- 用戶透明版 -- 代理對象輸出的結果----");
proxyHandler.getProxyWithOutTarget().doCalculation();
// 使用guava簡化JDK動態代理語法
System.out.println();
System.out.println("使用guava簡化JDK動態代理語法 -- 代理對象輸出的結果----");
proxyHandler.getProxyWithGuava().doCalculation();
}
}
輸出結果
代理前的原對象輸出的結果----
做點計算
計算了兩秒鐘....
用戶自傳原生類生成代理 -- 代理對象輸出的結果----
做點計算
計算了兩秒鐘....
2000
封裝原生類 -- 用戶透明版 -- 代理對象輸出的結果----
做點計算
計算了兩秒鐘....
2000
使用guava簡化JDK動態代理語法 -- 代理對象輸出的結果----
做點計算
計算了兩秒鐘....
2001
Process finished with exit code 0
三、cglib動態代理
Cglib動態代理與JDK動態代理完成的目標一樣,只不過Cglib動態代理不需要一個接口,只需要一個實現類,就可以完成對此對象的代理。具體代碼如下:
首先一個計算類: 因爲Cglib不需要接口,所以我們直接實現一個計算類
public class CglibTarget {
/**
* 做計算
* */
public void doCalculation(){
System.out.println("做點計算");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("計算了兩秒鐘....");
}
}
動態代理處理類以及獲取代理對象:
/**
* Cglib 動態代理的處理類
* */
public class TargetInterceptor implements MethodInterceptor {
/**
* Cglib 動態代理的處理方法
* */
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
// 方法調用返回值
Object result = null;
if("doCalculation".equals(method.getName())){
System.out.println("計算開始");
result = methodProxy.invokeSuper(o,objects);
System.out.println("計算結束");
}else{
result = methodProxy.invokeSuper(o,objects);
}
return result;
}
public CglibTarget getProxy(){
// Enhancer 爲 Cglib中的字節碼增強器,將目標類和動態處理類傳入此增強器,調用增強器的create方法返回代理對象
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(CglibTarget.class);
enhancer.setCallback(this);
return (CglibTarget)enhancer.create();
}
測試類:public class CglibProxyTest {
@Test
public void testProxy(){
new TargetInterceptor().getProxy().doCalculation();
}
}
四、JDK動態代理及Cglib的效率比較
在面試現在工作的這家公司時,面試官問了我一個問題。就是JDK動態代理和Cglib誰更快。當時我聽到這個問題就尷尬了,因爲的確沒有關注過這類問題。既然我們上面講述了JDK動態代理和Cglib動態代理,那我們就做一個小測試,看看他們的效率誰更快。
改寫JDK動態代理測試類:
@Test
public void speed(){
Long startTime = System.currentTimeMillis();
new ProxyHandler().getProxyWithOutTarget().doCalculation();
Long endTime = System.currentTimeMillis();
System.out.println(endTime - startTime);
}
運行五次 結果分別爲:
2004 、2003、2004、2004、2003
可以得知,JDK動態代理完成整個代理過程大概4毫秒左右
再來測試Cglib:
首先改寫測試類
@Test
public void speed(){
Long startTime = System.currentTimeMillis();
new TargetInterceptor().getProxy().doCalculation();
Long endTime = System.currentTimeMillis();
System.out.println("cglib -- :"+ (endTime - startTime));
}
運行五次結果如下:
2172、2208、2154、2135、2169
可以看出,Cglib完成整個代理大概要 150毫秒左右。明顯JDK的速度要快很多。本次測試JDK動態代理要比Cglib快近40倍,這個性能差距也是蠻大的。
本次測試Cglib使用版本 2.2.2
JDK使用版本 1.8
感興趣的同學可以使用別的版本試一試他們的效率差距,留言給我呦!
五、MyBatis中使用JDK動態代理動態生成Mapper實現類
在MyBatis的使用中,我們只定義了Mapper接口,在Mapper接口中去定義了一些CRUD的方法,但是我們卻從來沒有去實現過Mapper。問題來了,誰去幫我們做了這件事呢?那必須就是MyBatis啊。在MyBatis中有一個MapperProxy類去完成這件事。
讓我們看一下源碼:
首先去獲取代理類:
//org.apache.ibatis.binding.MapperProxyFactory
protected T newInstance(MapperProxy<T> mapperProxy) {
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
讓我們看看MapperProxy
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;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else if (isDefaultMethod(method)) {
return invokeDefaultMethod(proxy, method, args);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
final MapperMethod mapperMethod = cachedMapperMethod(method);
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;
}
@UsesJava7
private Object invokeDefaultMethod(Object proxy, Method method, Object[] args)
throws Throwable {
final Constructor<MethodHandles.Lookup> constructor = MethodHandles.Lookup.class
.getDeclaredConstructor(Class.class, int.class);
if (!constructor.isAccessible()) {
constructor.setAccessible(true);
}
final Class<?> declaringClass = method.getDeclaringClass();
return constructor.newInstance(declaringClass, MethodHandles.Lookup.PRIVATE)
.unreflectSpecial(method, declaringClass).bindTo(proxy).invokeWithArguments(args);
}
/**
* Backport of java.lang.reflect.Method#isDefault()
*/
private boolean isDefaultMethod(Method method) {
return ((method.getModifiers()
& (Modifier.ABSTRACT | Modifier.PUBLIC | Modifier.STATIC)) == Modifier.PUBLIC)
&& method.getDeclaringClass().isInterface();
}
}
看源碼需要定焦,你看這端代碼主要想從代碼中獲得什麼,只要想知道代碼中的哪部分是如何實現的。從上面的源碼中,我們可以到看到,MyBatis也是使用了JDK的動態代理。
MyBatis中判斷是否是一個類,如果是一個類,那麼就直接傳遞方法和參數調用即可。但我們知道此時是一個接口(也可以自己實現接口,舊版本通常這樣做)。如果不是一個類的話,就會創建一個MapperMethod方法。見名思意:好像就是這個類在執行我們所調用的每一個接口方法。最後返回的是MapperMethod.execute方法。暫時不予理會MapperProxy類中的cachedMapperMethod方法。
具體MyBatis源碼解讀請參見http://www.cnblogs.com/yulinfeng/p/6063974.html
這個系列他寫的很好,我就不重複造輪子了。第五部分只想告訴大家在MyBatis中對於Dao層的Mapper接口是使用JDK的動態代理實現的。學習設計模式,也是想讓自己可以更快速的看懂源碼,並應用在自己的項目中。