設計模式——代理模式的思考

代理模式是一種經過中間代理訪問目標對象,以達到加強或拓展原對象功能目的的設計模式,舉個例子來講,咱們在購買飛機票時每每會經過一些第三方平臺來購買,在這裏第三方平臺就可當作代理對象,目標對象則是各大航空公司,常見的代理方式有靜態代理、動態代理以及Cglib代理。java

靜態代理

靜態代理屬於比較典型的代理模式,它的類圖以下所示,從圖中能夠看到客戶端是經過代理類的接口來訪問目標對象的接口,也就是目標對象和代理類是一一對應的,若是有多個目標接口須要代理則產生多個代理類,實現方式比較冗餘,另外若是拓展接口,對應的目標對象和代理類也需修改,不易維護。 git

動態代理

動態代理經過Java反射機制或者ASM字節碼技術,動態地在內存中構建代理對象,從而實現對目標對象的代理功能。它與靜態代理的主要區別在與動態代理的代理類是在運行期纔會生成的,也就是說不會在編譯期代理類的Class文件。常見的動態代理有JDK動態代理和Cglib動態代理。github

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方法bash

public Object invoke(Object obj, Object... args) // 該方法會調用目標對象對應的方法
複製代碼

在這裏拋出一個問題,JDK動態代理爲何必須實現接口才能代理?要弄明白這個問題,咱們須要拿到生成的代理類,下面是經過技術手段拿到的運行期的代理類,能夠看到$Proxy0代理類已經繼承Proxy類,因爲Java是單繼承的,因此只能經過實現接口的方式來實現。app

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的實現類。ide

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包下有兩個核心接口,分別是MethodInvocationMethodInterceptor,這兩個接口也是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…

原文連接:blog.ganzhiqiang.wang/2019/02/17/…

相關文章
相關標籤/搜索