1. 程式人生 > 實用技巧 >Spring Boot 開發 WebService 服務

Spring Boot 開發 WebService 服務

Java動態代理

在介紹動態代理之前,我們先來說說靜態代理。

靜態代理

假設,現在有這麼一個需求場景:專案依賴了一個三方庫,現在想要在專案呼叫三方庫時記錄呼叫日誌。那麼我們如何能夠在無法修改三方庫程式碼的前提下,完成這個需求呢?

相信大家能夠想到很多種方法來實現,其中最簡單粗暴的就是靜態代理了。大概的做法是:

  1. 為每一個被呼叫的三方類(委託類)都編寫一個對應的代理類,並實現相同的介面,通過代理類創建出代理物件。
  2. 在建立代理物件時,通過構造器塞入原委託物件,在代理物件的方法內部呼叫原委託物件的同名方法,並在呼叫前後列印日誌。

也就是說,代理物件=原委託物件+增強程式碼,有了代理物件後,就不用原委託物件了。

大概的程式碼是這樣的:

/**
 * 代理類與被代理類(委託類)都實現了UserService介面
 */
public interface UserService {
    public void select();
}

/**
 * 實現了UserService介面的委託類
 */
public class UserServiceImpl implements UserService {  
    public void select() {  
        System.out.println("invoke UserServiceImpl::select");
    }
}

/**
 * 實現了UserService介面的代理類
 */
public class UserServiceProxy implements UserService {
  // 被代理的原委託物件
  private UserService target;
  
  public UserServiceProxy(UserService target) {
    this.target = target;
  }
  
  public void select() {
    before();
    // 呼叫原委託物件的原方法,也是真正呼叫三方介面的程式碼
    target.select();
    after();
  }
  
  private void before() {
    System.out.println("invoe UserServiceProxy::before");
  }
  
  private void after() {
    System.out.println("invoe UserServiceProxy::after");
  }
}

/**
 * 在其他的物件中使用代理類
 */
public class Client {
  public void selectUser() {
    UserService proxy = new UserServiceProxy(new UserServiceImpl());
    proxy.select();
  }
}

像這樣的通過對應的代理物件來代理原委託物件的方法,我們稱之為靜態代理。

靜態代理的優點就是簡單粗暴,分別為原委託物件實現各自的代理物件即可;但缺點也很明顯,靜態代理需要編寫大量的代理程式碼,實現起來非常的繁瑣,此外一旦原委託物件需要增加新的方法或修改已有的方法時,代理物件都需要進行相應的修改,在維護性上較差。

動態代理

靜態代理在程式碼冗餘、維護性上都存在問題,回到文章開篇的場景下,如果專案中大量呼叫了三方庫,那靜態代理就不是最優解了。所以,我們的解決方向應該是如何少寫或者不寫代理類但也能夠完成代理功能

現在的方向是少寫或不寫代理類也能完成代理,那麼我們可不可以想辦法自動生成代理類不需要手動編寫呢,讓我們從基礎的物件建立開始。

我們知道JVM建立物件的過程如下:

其中的Class物件,是Class類的例項,而Class類是Java中用來描述所有類的類,所以要建立一個物件,首先是得到對應的Class物件,既然如此,那麼是不是可以想辦法得到委託物件的Class物件,然後根據Class物件建立代理例項。

在Java中,JDK已經為我們提供了java.lang.reflect.Proxyjava.lang.reflect.InvocationHandler,這兩個API相互配合,Proxy是整體的入口,而InvocateionHandler是中介類,連線委託類和代理類,主要用來增強程式碼的實現。

Proxy有個靜態方法:getProxyClass(ClassLoader loader, Class<?>... interfaces),這個方法會根據傳入的類載入器Class介面列表返回代理物件的Class類物件,同時這個Class類物件會繼承實現所傳入的介面Class。

簡單點的理解就是這樣:

到這裡我們已經能夠通過委託類的Class創建出代理類的Class,那麼接下來就是生成代理例項,同時插入增強程式碼。

這裡的增強程式碼就需要藉助中介類java.lang.reflect.InvocationHandler了,中介類主要是作為呼叫處理器攔截對委託類方法的呼叫。

一個簡單的中介類大概是這樣的:

public class LogHandler implements InvocationHandler { 
    // obj為委託物件; 
    private Object obj; 
 
    public LogHandler(Object obj) {
        this.obj = obj;
    } 
 
    @Override 
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { 
        System.out.println("invoke before"); 
        // 呼叫委託物件的方法
        Object result = method.invoke(obj, args); 
        System.out.println("invoke after"); 
        return result;
    }
} 

其內部的呼叫邏輯是這樣的:

也可以理解成這樣的:

這樣的話我們能夠得到全部的程式碼是這樣的:

public class ProxyTest {
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {

        // 拿到委託類的代理物件的Class物件
        Class userServiceClass = Proxy.getProxyClass(UserService.class.getClassLoader(), UserService.class);
        // 得到Class物件的構造器$Proxy0(java.lang.reflect.InvocationHandler)
        Constructor constructor = userServiceClass.getConstructor(InvocationHandler.class);

        // 增強程式碼中介類物件
        InvocationHandler handler = new LogHandler(new UserServiceImpl());
        // 反射建立代理物件
        UserService impl = (UserService) constructor.newInstance(handler);
        impl.select();
    }

}

class LogHandler implements InvocationHandler {
    // obj為委託物件
    private Object obj;

    public LogHandler(Object obj) {
        this.obj = obj;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("invoke before");
        Object result = method.invoke(obj, args);
        System.out.println("invoke after");
        return result;
    }
}

最終的輸出結果:

invoke before
invoke UserServiceImpl::select
invoke after

到了這裡動態代理的基本內容是差不多了,但回頭看看我們的程式碼,會發現在程式碼存在多處的硬編碼,如Proxy.getProxyClass(UserService.class.getClassLoader(), UserService.class);等等,這種寫法並不優雅,如果後面想代理其他委託物件就很是麻煩,所以接下來,對程式碼進行優化下:

public class ProxyTest {
    public static void main(String[] args) throws Exception {
        // 根據委託物件獲取代理物件
        UserService impl = (UserService) getProxy(new UserServiceImpl());
        impl.select();
    }

    private static Object getProxy(Object obj) throws Exception {
        // 拿到委託類的代理物件的Class物件
        // 引數1:委託物件的類載入器 引數2:委託物件的介面列表,這樣代理物件能夠繼承實現相同的介面
        Class objClass = Proxy.getProxyClass(obj.getClass().getClassLoader(), obj.getClass().getInterfaces());
        // 得到Class物件的構造器$Proxy0(java.lang.reflect.InvocationHandler)
        Constructor constructor = objClass.getConstructor(InvocationHandler.class);

        // 反射建立代理物件,傳入中介類物件
        return constructor.newInstance(new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                System.out.println("invoke before");
                Object result = method.invoke(obj, args);
                System.out.println("invoke after");
                return result;
            }
        });
    }
}

改進後的程式碼就好多了,無論現有系統有多少類,只需要將委託物件傳進去就可以了。

實際上,Proxy還提供了一步到位的靜態方法newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h),方法可以直接返回代理物件,省去了獲取代理物件的Class物件的過程:

public class ProxyTest {

    public static void main(String[] args) throws Exception {
        // 根據委託物件獲取代理物件
        UserService impl = (UserService) getProxy(new UserServiceImpl());
        impl.select();
    }

    private static Object getProxy(Object obj) throws Exception {
        // 直接一步到位
        return Proxy.newProxyInstance(
                obj.getClass().getClassLoader(),
                obj.getClass().getInterfaces(),
                // lambda改進
                (proxy, method, args) -> {
                    System.out.println("invoke before");
                    Object result = method.invoke(obj, args);
                    System.out.println("invoke after");
                    return result;
                });
    }
}

動態代理實際上有很多應用,比如Spring AOP的實現,RPC框架的實現,一些第三方工具庫的內部使用等等。這裡就不扯了這些了。