先了解下代理模式的概念:為其他對(duì)象提供一種代理,以控制對(duì)這個(gè)對(duì)象的訪問。也就是通過一個(gè)新的對(duì)象去代表目標(biāo)對(duì)象,再通過新對(duì)象間接去訪問目標(biāo)對(duì)象的功能,主要的作用有:
- 增強(qiáng)目標(biāo)對(duì)象的功能
- 保護(hù)目標(biāo)對(duì)象,屏蔽掉一些敏感的功能
具體的實(shí)現(xiàn)可以采用靜態(tài)代理或者動(dòng)態(tài)代理兩種方案,我們?cè)鰪?qiáng)目標(biāo)對(duì)象的功能為例來學(xué)習(xí)動(dòng)態(tài)代理。
注意,有些說法是“代理模式一般是內(nèi)部創(chuàng)建被代理的對(duì)象” ,你這例子是裝飾者模式,但我認(rèn)為這并不是必須,更多的時(shí)候需要根據(jù)業(yè)務(wù)去權(quán)衡,比如在某些場(chǎng)景上要求代理類有一定通用性,目標(biāo)對(duì)象是可以依賴注入的,就需要變通了。
一、靜態(tài)代理
首先定義一個(gè)Shop接口,作用就是售賣物品:
public interface Shop {
void sale(String name);
}
再定義一個(gè)CoffeeShop類,實(shí)現(xiàn)Shop接口,專門售賣咖啡:
public class CoffeeShop implements Shop {
public void sale(String name) {
System.out.println("開始制作" + name + "......制作完成!");
}
}
最后定義代理類,需要通過構(gòu)造函數(shù)注入目標(biāo)對(duì)象,調(diào)用目標(biāo)對(duì)象的售賣方法,并在售賣的前后添加問候語:
public class StaticProxy implements Shop {
private Shop shop;
public StaticProxy(Shop shop) {
this.shop = shop;
}
public void sale(String name) {
System.out.println("歡迎!");
shop.sale(name);
System.out.println("再見!");
}
}
public class ProxyTest {
@Test
public void staticProxyTest() {
Shop shop = new CoffeeShop();
StaticProxy staticProxy = new StaticProxy(shop);
staticProxy.sale("拿鐵");
}
}
測(cè)試結(jié)果如下:

動(dòng)態(tài)代理我們主要學(xué)習(xí)兩種:JDK動(dòng)態(tài)代理、CGLIB動(dòng)態(tài)代理,在上邊的例子基礎(chǔ)上進(jìn)行改進(jìn)。
二、JDK動(dòng)態(tài)代理
JDK動(dòng)態(tài)代理主要依賴java.lang.reflect包下的相關(guān)類實(shí)現(xiàn),但要求被代理類必須最少實(shí)現(xiàn)一個(gè)接口,來規(guī)定類要實(shí)現(xiàn)哪些方法,否則無法創(chuàng)建代理對(duì)象,所以可以繼續(xù)使用上邊的CoffeeShop類。
public class JdkProxy {
public Shop createProxy(final Shop shop) {
ClassLoader loader = shop.getClass().getClassLoader();
Class[] interfaces = shop.getClass().getInterfaces();
InvocationHandler h = new InvocationHandler() {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("歡迎!");
Object result = method.invoke(shop, args);
System.out.println("再見!");
return result;
}
};
return (Shop) Proxy.newProxyInstance(loader, interfaces, h);
}
}
可以看到,上邊的Proxy.newProxyInstance()方法就是用來創(chuàng)建代理對(duì)象的,需要三個(gè)參數(shù):
- ClassLoader :一般我們使用目標(biāo)對(duì)象的類加載器。
- Class[]:被代理的類實(shí)現(xiàn)接口的字節(jié)碼數(shù)組,保證代理對(duì)象和目標(biāo)對(duì)象有相同的方法,要不然還怎么代理。這也是為什么要求被代理的類必須最少實(shí)現(xiàn)一個(gè)接口了。
- InvocationHandler :一般可以用匿名內(nèi)部類,在其
invoke()回調(diào)中完成目標(biāo)對(duì)象的功能增強(qiáng)。
invoke()方法三個(gè)參數(shù)的作用:
- Object proxy:創(chuàng)建的代理對(duì)象。
- Method method:當(dāng)前調(diào)用的代理對(duì)象的方法。
- Object[] args:當(dāng)前調(diào)用的代理對(duì)象方法傳遞的參數(shù)。
這樣就可以通過反射間接調(diào)用目標(biāo)對(duì)象的對(duì)應(yīng)方法了:Object result = method.invoke(shop, args)
接下來就是測(cè)試代碼了:
public class ProxyTest {
@Test
public void jdkProxyTest() {
Shop shop = new CoffeeShop();
JdkProxy jdkProxy = new JdkProxy();
Shop shopProxy = jdkProxy.createProxy(shop);
shopProxy.sale("拿鐵");
}
}
三、CGLIB動(dòng)態(tài)代理
之前提到過,JDK動(dòng)態(tài)代理要求被代理類必須最少實(shí)現(xiàn)一個(gè)接口,否則無法創(chuàng)建代理對(duì)象。但我們并不能要求用到的類都實(shí)現(xiàn)了接口,所以就需要第三方的 CGLIB 庫來實(shí)現(xiàn)動(dòng)態(tài)代理了,jar自行導(dǎo)入即可。下面用 CGLIB 來改造代碼:
public class CglibProxy {
public Object createProxy(final Object shop) {
Class clazz = shop.getClass();
Callback callback = new MethodInterceptor() {
public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
System.out.println("歡迎!");
Object result = method.invoke(shop, args);
System.out.println("再見!");
return result;
}
};
return Enhancer.create(clazz, callback);
}
}
其實(shí)和 JDK動(dòng)態(tài)代理的使用步驟類似。
這里用Enhancer.create()方法來創(chuàng)建代理對(duì)象,需要兩個(gè)參數(shù):
- Class :被代理對(duì)象的字節(jié)碼。
- Callback :這里用到了它的
MethodInterceptor子類,并在intercept()回調(diào)中完成目標(biāo)對(duì)象的功能增強(qiáng),這點(diǎn)和JDK動(dòng)態(tài)代理類似。
通過如下測(cè)試代碼可以實(shí)現(xiàn)相同的效果:
public class ProxyTest {
@Test
public void cglibProxyTest() {
CoffeeShop2 shop = new CoffeeShop2();
CglibProxy cglibProxy = new CglibProxy();
CoffeeShop2 shopProxy = (CoffeeShop2) cglibProxy.createProxy(shop);
shopProxy.sale("拿鐵");
}
}
其實(shí) CGLIB 庫也可以用來實(shí)現(xiàn)JDK動(dòng)態(tài)代理例子中的功能,因?yàn)?code>Enhancer.create()還有其他的重載方法:
