代理模式是一種通過中間代理訪問目標物件,以達到增強或擴充原物件功能目的的設計模式,舉個例子來說,我們在購買飛機票時往往會通過一些第三方平臺來購買,在這裡第三方平臺就可看成代理物件,目標物件則是各大航空公司,常見的代理方式有靜態代理、動態代理以及Cglib代理。
靜態代理
靜態代理屬於比較典型的代理模式,它的類圖如下所示,從圖中可以看到客戶端是通過代理類的介面來訪問目標物件的介面,也就是目標物件和代理類是一一對應的,如果有多個目標介面需要代理則產生多個代理類,實現方式比較冗餘,另外如果擴充介面,對應的目標物件和代理類也需修改,不易維護。
動態代理
動態代理通過Java反射機制或者ASM位元組碼技術,動態地在記憶體中構建代理物件,從而實現對目標物件的代理功能。它與靜態代理的主要區別在與動態代理的代理類是在執行期才會生成的,也就是說不會在編譯期代理類的Class檔案。常見的動態代理有JDK動態代理和Cglib動態代理。
JDK動態代理
JDK動態代理又稱介面代理,它要求目標物件必須實現介面,否則不能代理。動態代理是基於java.lang.reflect.Proxy
類和java.lang.reflect.InvocationHandler
類來實現的,其中Proxy
是攔截髮生的地方,而InvocationHandler
則是發生呼叫地方,newProxyInstance
方法返回一個指定介面的代理類例項。
newProxyInstance方法
public static Object newProxyInstance(ClassLoader loader, //目標物件的類載入器
Class<?>[] interfaces, // 目標物件所實現的介面
InvocationHandler h) // 事件處理器
複製程式碼
InvocationHandler的Invoke方法
public Object invoke(Object obj, Object... args) // 該方法會呼叫目標物件對應的方法
複製程式碼
在這裡丟擲一個問題,JDK動態代理為什麼必須實現介面才能代理?要弄明白這個問題,我們需要拿到生成的代理類,下面是通過技術手段拿到的執行期的代理類,可以看到$Proxy0
代理類已經繼承Proxy
類,由於Java是單繼承的,所以只能通過實現介面的方式來實現。
public final class $Proxy0 extends Proxy implements IUserDao {
private static Method m1;
private static Method m2;
private static Method m0;
private static Method m3;
public $Proxy0(InvocationHandler var1) throws {
super(var1);
}
...
public final void register() throws {
try {
super.h.invoke(this, m3, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
...
}
複製程式碼
CGLIB代理
CGLib相對於JDK動態代理更加靈活,它是通過生成子類來擴充目標物件的功能,使用cglib代理的物件無需實現介面,可以做到代理類無侵入,另外因CGLib具備很好的效能,所以被很多AOP框架所引用,比如Spring、Hibernate。
Cglib代理方式是通過繼承來實現,其中代理物件是由Enhancer建立(Enhancer是Cglib位元組碼增強器,可以很方便對類進行擴充),另外,可以通過實現MethodInterceptor
介面來定義方法攔截器。
public Object getProxyInstance() {
Enhancer en = new Enhancer();
// 繼承被代理類
en.setSuperclass(target.getClass());
// 設定回撥函式
en.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println("開啟事務");
// 執行目標物件的方法
Object returnValue = method.invoke(target, objects);
System.out.println("關閉事務");
return null;
}
});
return en.create();
}
複製程式碼
UserDao$$EnhancerByCGLIB$$b0e8b18d
是獲取到的UserDao的Cglib代理,可以看到它繼承了UserDao方法,併為UserDao的每個方法生成了2個代理方法(這裡只保留了register方法),第一個代理方法CGLIB$register$0()
是直接呼叫父類的方法,第二個方法register()
是代理類真正呼叫的方法,它會判斷是否實現了MethodInterceptor
介面,如果實現就會呼叫intercept
方法,MethodInterceptor
即為setCallback
時注入的MethodInterceptor
的實現類。
public class UserDao$$EnhancerByCGLIB$$b0e8b18d extends UserDao implements Factory {
...
final void CGLIB$register$0() {
super.register();
}
public final void register() {
MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
if (this.CGLIB$CALLBACK_0 == null) {
CGLIB$BIND_CALLBACKS(this);
var10000 = this.CGLIB$CALLBACK_0;
}
// 判斷是否實現了MethodInterceptor介面
if (var10000 != null) {
var10000.intercept(this, CGLIB$register$0$Method, CGLIB$emptyArgs, CGLIB$register$0$Proxy);
} else {
super.register();
}
}
...
}
複製程式碼
Spring AOP
Spring AOP是基於動態代理實現的對程式碼無侵入的程式碼增強方式,它從本質上來說,是將Spring生成代理類物件放入IOC容器中,每次獲取目標物件bean時都是通過getBean()
方法,如果一個類被代理,那麼實際通過getBean
方法獲取的就是代理類的物件,這也是Spring AOP為什麼只能作用於IOC容器中的物件。
Spring AOP預設使用的JDK動態代理,如果目標物件沒有實現介面,才會使用CGLib來代理,當然也可以強制使用CGLib代理,只需加上@EnableAspectJAutoProxy(proxyTargetClass = true)
註解,@EnableAspectJAutoProxy
一般用來開啟Aspect
註解配置,如果是基於xml配置的,在配置檔案新增<aop:aspectj-autoproxy/>
即可。
在org.aopalliance
包下有兩個核心介面,分別是MethodInvocation
和MethodInterceptor
,這兩個介面也是Spring AOP中的核心類
- MethodInvocation: AOP對需要增強方法的封裝,它是真正執行AOP攔截的,該介面只包含
getMethod()
方法。 - MethodInterceptor:AOP方法攔截器,AOP的相關操作一般在其內部完成
下面程式碼是
JdkDynamicAopProxy
類,它是Spring AOP中JDK動態代理的具體實現,其中invoke()
方法作為代理物件的回撥函式被觸發,通過invoke
方法具體實現來完成對目標物件方法呼叫攔截或者功能增強,在invoke()
方法中會建立一個ReflectiveMethodInvocation
物件,該物件的proceed()
方法會呼叫下一個攔截器,直至攔截器鏈被呼叫結束。
final class JdkDynamicAopProxy implements AopProxy, InvocationHandler, Serializable {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
MethodInvocation invocation;
Object oldProxy = null;
boolean setProxyContext = false;
TargetSource targetSource = this.advised.targetSource;
Object target = null;
try {
...
//獲得定義好的攔截器鏈(增強處理)
List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
//如果攔截器鏈為空,執行原方法
if (chain.isEmpty()) {
Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);
retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse);
}
else {
// ReflectiveMethodInvocation實現了ProxyMethodInvocation介面
// ProxyMethodInvocation繼承自MethodInvocation
invocation = new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain);
// 執行proceed方法,呼叫下一個攔截器,直至攔截器鏈被呼叫結束,拿到返回值
retVal = invocation.proceed();
}
Class<?> returnType = method.getReturnType();
if (retVal != null && retVal == target &&
returnType != Object.class && returnType.isInstance(proxy) &&
!RawTargetAccess.class.isAssignableFrom(method.getDeclaringClass())) {
retVal = proxy;
}
else if (retVal == null && returnType != Void.TYPE && returnType.isPrimitive()) {
throw new AopInvocationException(
"Null return value from advice does not match primitive return type for: " + method);
}
return retVal;
}
finally {
if (target != null && !targetSource.isStatic()) {
// Must have come from TargetSource.
targetSource.releaseTarget(target);
}
if (setProxyContext) {
// Restore old proxy.
AopContext.setCurrentProxy(oldProxy);
}
}
}
}
複製程式碼
解決自我呼叫時無法增強的問題
當TestProxyImpl
被Spring Aop增強時,testA()
方法內部呼叫tesB()
方法,那麼testB()
也會被增強嗎?實際是不會的,從下面的輸出結果可以看到testB()
方法未被增強,可以很容易想到testB()
未被增強的根本原因是this指的目標物件而非代理類物件
@Component
public class TestProxyImpl implements ITestProxy {
@Override
public void testA() {
System.out.println("testA() execute ...");
this.testB();
}
@Override
public void testB() {
System.out.println("testB() execute ...");
}
}
// 輸出
[AOP] Before ...
testA() execute ...
testB() execute ...
複製程式碼
如果想在testA()
方法呼叫testB()
方法時增強testB()
方法,即實際呼叫代理物件的testB()
方法,下面有兩種方法可以做到。
1.設定expose-proxy
屬性為true
如果是Spring Boot專案可以直接使用@EnableAspectJAutoProxy(exposeProxy = true)
來暴露代理物件,如果是使用XML配置的,則用<aop:config expose-proxy="true">
配置即可。該方法的原理就是使用ThreadLocal暫存代理物件,然後通過AopContext.currentProxy()
方法重新拿到代理物件。
// JdkDynamicAopProxy類invoke方法中的程式碼片段
// 判斷expose-proxy屬性是否true
if (this.advised.exposeProxy) {
// 暫存到ThreadLocal中,可點入setCurrentProxy方法檢視
oldProxy = AopContext.setCurrentProxy(proxy);
setProxyContext = true;
}
複製程式碼
為了能拿到代理物件,可以testA()
方法做如下修改
public void testA() {
System.out.println("testA() execute ...");
//從ThreadLocal中取出代理物件,前提已設定expose-proxy屬性為true,暴露了代理物件
ITestProxy proxy = (ITestProxy) AopContext.currentProxy();
proxy.testB();
}
複製程式碼
2.獲取代理物件的Bean
還有一種方式和上面方法的原理差不多,都是獲取的代理物件再呼叫testB()
方法,不過該方法直接從Spring容器中獲取,下面直接貼程式碼了~
@Component(value = "testProxy")
public class TestProxyImpl implements ITestProxy,ApplicationContextAware {
private ApplicationContext applicationContext;
@Override
public void testA() {
System.out.println("testA() execute ...");
applicationContext.getBean("testProxy", ITestProxy.class).testB();
}
@Override
public void testB() {
System.out.println("testB() execute ...");
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}
複製程式碼
本文相關程式碼地址:github.com/LJWLgl/java…