只有掌握了這三種代理模式,才能進軍Spring AOP哦!

llldddbbb發表於2019-04-02

代理模式定義

首先我們來看看代理模式:

image.png
所謂代理模式,是指客戶端(Client)並不直接呼叫實際的物件(下圖右下角的RealSubject),而是通過呼叫代理(ProxySubject),來間接的呼叫實際的物件。

代理模式的使用場合,一般是由於客戶端不想直接訪問實際物件,或者訪問實際的物件存在技術上的障礙,因而通過代理物件作為橋樑,來完成間接訪問。

業務場景

首先有個UserService介面,介面裡有一個新增使用者的方法

public interface UserService {
    void addUser();
}
複製程式碼

這是它的實現類

public class UserServiceImpl implements UserService {
    @Override
    public void addUser() {
        System.out.println("新增一個使用者");
    }
}
複製程式碼

現在需要在新增使用者的時候記錄一下日誌。當然,你可以直接在addUser裡面直接寫新增日誌的程式碼,

    public void addUser() {
        System.out.println("新增一個使用者");
	System.out.println("拿個小本本記一下");
    }
複製程式碼

但是Java推崇單一職責原則,如果這樣寫就違背了這個原則,我們需要將新增日誌的程式碼解耦出來,讓addUser()方法專注寫自己的業務邏輯。

靜態代理

根據類圖,建立一個靜態代理類

public class UserStaticProxy implements UserService{
    private UserService userService;
    public UserStaticProxy(UserService userService) {
        this.userService = userService;
    }

    @Override
    public void addUser() {
        userService.addUser();
        System.out.println("拿個小本本記錄一下");
    }
}
複製程式碼

我們建立一個測試類來測試靜態代理:

public class Test {

    public static void main(String[] args) {
        UserStaticProxy userStaticProxy = new UserStaticProxy(new UserServiceImpl());
        userStaticProxy.addUser();
    }
}
複製程式碼

執行結果:

image.png
如此,一個靜態代理類就建立好了,我們可以專注在Service寫業務邏輯,新增日誌等非業務邏輯交給這個靜態代理類來完成。

靜態代理的缺點

缺點一:介面增加方法,代理類需要同步維護

隨著業務擴大,UserService類裡不知有addUser方法,還有updateUser、deleteUser、batchUpdateUser、batchDeleteUser等方法,這些方法都需要記錄日誌。

UserServiceImpl類如下:

public class UserServiceImpl implements UserService {
    @Override
    public void addUser() {
        System.out.println("新增一個使用者");
    }

    @Override
    public void updateUser() {
        System.out.println("更新一個使用者");
    }

    @Override
    public void deleteUser() {
        System.out.println("刪除一個使用者");
    }

    @Override
    public void batchUpdateUser() {
        System.out.println("批量更新使用者");
    }

    @Override
    public void batchDeleteUser() {
        System.out.println("批量刪除使用者");
    }
}
複製程式碼

那麼對應的靜態代理類如下:

public class UserStaticProxy implements UserService{
    private UserService userService;
    public UserStaticProxy(UserService userService) {
        this.userService = userService;
    }

    @Override
    public void addUser() {
        userService.addUser();
        System.out.println("拿個小本本記錄一下");
    }

    @Override
    public void updateUser() {
        userService.updateUser();
        System.out.println("拿個小本本記錄一下");
    }

    @Override
    public void deleteUser() {
        userService.deleteUser();
        System.out.println("拿個小本本記錄一下");
    }

    @Override
    public void batchUpdateUser() {
        userService.batchUpdateUser();
        System.out.println("拿個小本本記錄一下");
    }

    @Override
    public void batchDeleteUser() {
        userService.batchDeleteUser();
        System.out.println("拿個小本本記錄一下");
    }
}
複製程式碼

從上面我們可以看到,代理類裡有很多重複的日誌程式碼。因為代理類和目標物件實現同一個介面,一旦介面增加方法,代理類也得同步增加方法並且得同步增加重複的額外功能程式碼,增大了程式碼量

缺點二:介面越多,導致代理類繁多

如果需要增加業務類,如StudentService,TeacherService等等,這些類裡的方法也都需要實現增加日誌的方法,那麼就需要同步建立對應的代理類。此外靜態代理類不是自動生成的,需要在編譯之前就編寫好的,如果業務越來越龐大,那麼建立的代理類越來越多,這樣又增大了程式碼量

如何解決這些缺點呢?這時候就需要動態代理方法了

JDK動態代理

其實動態代理和靜態代理的本質是一樣的,最終程式執行時都需要生成一個代理物件例項,通過它來完成相關增強以及業務邏輯,只不過靜態代理需要硬編碼的方式指定,而動態代理支援執行時動態生成這種實現方式。

JDK本身幫我們實現了動態代理,只需要使用newProxyInstance方法:

 public static Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)
複製程式碼

注意該方法是在Proxy類中是靜態方法,且接收的三個引數依次為:

  • ClassLoader loader,:指定當前目標物件使用類載入器
  • Class<?>[] interfaces,:代理類需要實現的介面列表
  • InvocationHandler h:呼叫處理程式,將目標物件的方法分派到該呼叫處理程式

程式碼示例:

public class DynamicProxy implements InvocationHandler {

    private Object target; // 目標物件

    public Object bind(Object target) {
        this.target = target;
        return Proxy.newProxyInstance(target.getClass().getClassLoader(),target.getClass().getInterfaces(),this);
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Object result = method.invoke(target, args);
        System.out.println("拿個小本本記錄一下");
        return result;
    }

}
複製程式碼

上文的invoke方法,負責增強目標物件的方法,介面類的所有方法都會走這個invoke方法。另外bind方法簡單封裝了JDK的代理方法newProxyInstance,負責返回介面類。

測試類:

 public static void main(String[] args) {
        DynamicProxy dynamicProxy = new DynamicProxy();
        UserService userService = (UserService)dynamicProxy.bind(new UserServiceImpl());
        userService.addUser();
        userService.updateUser();
    }
複製程式碼

執行結果如下:

image.png

如圖UserService介面裡的所有方法都已經加上了日誌邏輯了,此外,我們看一下UserDynamicProxy這個類裡的target屬性是Object型別的。所以,這個動態代理的方法同樣可以給其他Service複用。可以這樣呼叫:

DynamicProxy dynamicProxy = new DynamicProxy();
TeacherService teacherService = (TeacherService)dynamicProxy.bind(new TeacherServiceImpl());
複製程式碼

綜上,動態代理解決了靜態代理的缺點

用arthas檢視JDK動態代理生成的類

動態代理是執行時候動態生成代理類的,這個類放在記憶體中,我們要怎麼才能看到這個類呢?

artias是阿里開源的一個牛逼閃閃的Java診斷工具,不懂的可以看看這篇文章http://www.dblearn.cn/article/5,用它就可以線上反編譯程式碼。

這裡我們新增一個斷點:

public static void main(String[] args) throws IOException {
        DynamicProxy dynamicProxy = new DynamicProxy();
        UserService userService = (UserService)dynamicProxy.bind(new UserServiceImpl());
        userService.addUser();
        userService.updateUser();
        System.in.read();
    }
複製程式碼

執行 arthas

image.png

jad命令反編譯,java生成的代理類都在com.sun.proxy目錄下。因此反編譯命令如下

jad com.sun.proxy.$Proxy0

package com.sun.proxy;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;
import proxy.UserService;

public final class $Proxy0
extends Proxy
implements UserService {
    private static Method m1;
    private static Method m6;
    private static Method m2;
    private static Method m7;
    private static Method m0;
    private static Method m3;
    private static Method m4;
    private static Method m5;

    public final void addUser() {
        try {
            this.h.invoke(this, m3, null);
            return;
        }
        catch (Error | RuntimeException throwable) {
            throw throwable;
        }
        catch (Throwable throwable) {
            throw new UndeclaredThrowableException(throwable);
        }
    }

    public final void updateUser() {
        try {
            this.h.invoke(this, m4, null);
            return;
        }
        catch (Error | RuntimeException throwable) {
            throw throwable;
        }
        catch (Throwable throwable) {
            throw new UndeclaredThrowableException(throwable);
        }
    }

    public final void deleteUser() {
        try {
            this.h.invoke(this, m5, null);
            return;
        }
        catch (Error | RuntimeException throwable) {
            throw throwable;
        }
        catch (Throwable throwable) {
            throw new UndeclaredThrowableException(throwable);
        }
    }

    public final void batchUpdateUser() {
        try {
            this.h.invoke(this, m6, null);
            return;
        }
        catch (Error | RuntimeException throwable) {
            throw throwable;
        }
        catch (Throwable throwable) {
            throw new UndeclaredThrowableException(throwable);
        }
    }

    public final void batchDeleteUser() {
        try {
            this.h.invoke(this, m7, null);
            return;
        }
        catch (Error | RuntimeException throwable) {
            throw throwable;
        }
        catch (Throwable throwable) {
            throw new UndeclaredThrowableException(throwable);
        }
    }

    public $Proxy0(InvocationHandler invocationHandler) {
        super(invocationHandler);
    }

    static {
        try {
            m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
            m6 = Class.forName("proxy.UserService").getMethod("batchUpdateUser", new Class[0]);
            m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
            m7 = Class.forName("proxy.UserService").getMethod("batchDeleteUser", new Class[0]);
            m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
            m3 = Class.forName("proxy.UserService").getMethod("addUser", new Class[0]);
            m4 = Class.forName("proxy.UserService").getMethod("updateUser", new Class[0]);
            m5 = Class.forName("proxy.UserService").getMethod("deleteUser", new Class[0]);
            return;
        }
        catch (NoSuchMethodException noSuchMethodException) {
            throw new NoSuchMethodError(noSuchMethodException.getMessage());
        }
        catch (ClassNotFoundException classNotFoundException) {
            throw new NoClassDefFoundError(classNotFoundException.getMessage());
        }
    }

    public final boolean equals(Object object) {
        try {
            return (Boolean)this.h.invoke(this, m1, new Object[]{object});
        }
        catch (Error | RuntimeException throwable) {
            throw throwable;
        }
        catch (Throwable throwable) {
            throw new UndeclaredThrowableException(throwable);
        }
    }

    public final String toString() {
        try {
            return (String)this.h.invoke(this, m2, null);
        }
        catch (Error | RuntimeException throwable) {
            throw throwable;
        }
        catch (Throwable throwable) {
            throw new UndeclaredThrowableException(throwable);
        }
    }

    public final int hashCode() {
        try {
            return (Integer)this.h.invoke(this, m0, null);
        }
        catch (Error | RuntimeException throwable) {
            throw throwable;
        }
        catch (Throwable throwable) {
            throw new UndeclaredThrowableException(throwable);
        }
    }
}

複製程式碼

由上面的程式碼可以看到我們的代理類已經生成好了,沒當我們呼叫方法如 addUser(),實際分派到h變數的invoke方法上執行:

this.h.invoke(this, m3, null);

h變數是什麼呢?其實就是我們實現了InvocationHandler的DynamicProxy類。

cglib動態代理

通過觀察上面的靜態代理和JDK動態代理模式,發現要求目標物件實現一個介面,但是有時候目標物件只是一個單獨的物件,並沒有實現任何的介面。這時候要怎麼處理呢?下面引出大名鼎鼎的CGlib動態代理

cglib代理,也叫作子類代理,它是在記憶體中構建一個子類物件從而實現對目標物件功能的擴充套件。

要用cglib需要引入它的jar包,因為spring已經整合了它,因此引入spring包即可

編寫代理類:

public class CGLibProxy implements MethodInterceptor {
    private Object target; // 目標物件
    public Object bind(Object target) {
        this.target = target;
        Enhancer enhancer = new Enhancer();
        //設定父類
        enhancer.setSuperclass(this.target.getClass());
        //設定回撥函式
        enhancer.setCallback(this);
        //建立子類(代理物件)
        return enhancer.create();
    }
    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        Object result = methodProxy.invokeSuper(obj, args);
        System.out.println("拿個小本本記錄一下");
        return result;
    }
}
複製程式碼

其中,Enhancer需要設定目標物件為父類(因為生成的代理類需要繼承目標物件)

測試類:

 public static void main(String[] args) throws IOException {
        CGLibProxy cgLibProxy = new CGLibProxy();
        UserServiceImpl userService = (UserServiceImpl)cgLibProxy.bind(new UserServiceImpl());
        userService.addUser();
        userService.updateUser();
        System.in.read();
    }
複製程式碼

執行結果:

image.png

我們看到已經成功代理了。但是結果有亂碼出現,此處設定一個// TODO,我猜測是Spring對CGlib再封裝導致的,也請知道的大大回答一下。

用arthas檢視cglib動態代理生成的類

步驟和JDK代理類雷同,只不過cglib的代理類生成在和測試類同一個包下,由於程式碼太多,只上部分程式碼

package com.example.demo.proxy;

import com.example.demo.proxy.UserServiceImpl;
import java.lang.reflect.Method;
import org.springframework.cglib.core.ReflectUtils;
import org.springframework.cglib.core.Signature;
import org.springframework.cglib.proxy.Callback;
import org.springframework.cglib.proxy.Factory;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;

public class UserServiceImpl$$EnhancerByCGLIB$$3ca8cfc3
extends UserServiceImpl
implements Factory {
    private boolean CGLIB$BOUND;
    public static Object CGLIB$FACTORY_DATA;
    private static final ThreadLocal CGLIB$THREAD_CALLBACKS;
    private static final Callback[] CGLIB$STATIC_CALLBACKS;
    private MethodInterceptor CGLIB$CALLBACK_0;
    private static Object CGLIB$CALLBACK_FILTER;
    private static final Method CGLIB$deleteUser$0$Method;
    private static final MethodProxy CGLIB$deleteUser$0$Proxy;
    private static final Object[] CGLIB$emptyArgs;
    private static final Method CGLIB$addUser$1$Method;
    private static final MethodProxy CGLIB$addUser$1$Proxy;
    private static final Method CGLIB$updateUser$2$Method;
    private static final MethodProxy CGLIB$updateUser$2$Proxy;
    private static final Method CGLIB$batchUpdateUser$3$Method;
    private static final MethodProxy CGLIB$batchUpdateUser$3$Proxy;
    private static final Method CGLIB$batchDeleteUser$4$Method;
    private static final MethodProxy CGLIB$batchDeleteUser$4$Proxy;
    private static final Method CGLIB$equals$5$Method;
    private static final MethodProxy CGLIB$equals$5$Proxy;
    private static final Method CGLIB$toString$6$Method;
    private static final MethodProxy CGLIB$toString$6$Proxy;
    private static final Method CGLIB$hashCode$7$Method;
    private static final MethodProxy CGLIB$hashCode$7$Proxy;
    private static final Method CGLIB$clone$8$Method;
    private static final MethodProxy CGLIB$clone$8$Proxy;

    public final void deleteUser() {
        MethodInterceptor methodInterceptor = this.CGLIB$CALLBACK_0;
        if (methodInterceptor == null) {
            UserServiceImpl$$EnhancerByCGLIB$$3ca8cfc3.CGLIB$BIND_CALLBACKS(this);
            methodInterceptor = this.CGLIB$CALLBACK_0;
        }
        if (methodInterceptor != null) {
            Object object = methodInterceptor.intercept(this, CGLIB$deleteUser$0$Method, CGLIB$emptyArgs, CGLIB$deleteUser$0$Proxy);
            return;
        }
        super.deleteUser();
    }

    public final void addUser() {
        MethodInterceptor methodInterceptor = this.CGLIB$CALLBACK_0;
        if (methodInterceptor == null) {
            UserServiceImpl$$EnhancerByCGLIB$$3ca8cfc3.CGLIB$BIND_CALLBACKS(this);
            methodInterceptor = this.CGLIB$CALLBACK_0;
        }
        if (methodInterceptor != null) {
            Object object = methodInterceptor.intercept(this, CGLIB$addUser$1$Method, CGLIB$emptyArgs, CGLIB$addUser$1$Proxy);
            return;
        }
        super.addUser();
    }

    public final void updateUser() {
        MethodInterceptor methodInterceptor = this.CGLIB$CALLBACK_0;
        if (methodInterceptor == null) {
            UserServiceImpl$$EnhancerByCGLIB$$3ca8cfc3.CGLIB$BIND_CALLBACKS(this);
            methodInterceptor = this.CGLIB$CALLBACK_0;
        }
        if (methodInterceptor != null) {
            Object object = methodInterceptor.intercept(this, CGLIB$updateUser$2$Method, CGLIB$emptyArgs, CGLIB$updateUser$2$Proxy);
            return;
        }
        super.updateUser();
    }

複製程式碼

其中

public class UserServiceImpl$$EnhancerByCGLIB$$3ca8cfc3 extends UserServiceImpl

可以看到生成的代理類繼承了目標物件,因此有兩個注意點:

  1. 目標物件不能處理被final關鍵字修飾,因為被final修飾的物件是不可繼承的。
  2. 目標物件的方法如果為final/static,那麼就不會被攔截,即不會執行目標物件額外的業務方法.

相關文章