Java 動態代理,看這篇就夠了

Wizey發表於2019-03-02

這篇文章需要用到 Java 的反射知識,如果對反射還不清楚的小夥伴,可以先移步到這裡 《Java進階-反射》

程式設計思想都是來自於生活的,“代理” 在生活中很常見。比如我們買一個東西時,一般都不會是直接從工廠裡買的,而是去商店或者其他的商家買,這些商家做的事情就是代理。再比如,做微商的朋友經常在朋友圈裡推廣商品,它們也是代理商。Java 程式中的代理是用介面實現的,使用者買某件商品的需求就可以看做是一種介面,而真正賣給使用者商品的是代理商,代理商和廠家都和這種需求有直接關係,代理商又和廠家有關係(代理與被代理),也即是下面這幅圖。

代理模式.png

在這裡,圖中的使用者可以看做買東西的人,被代理的介面可以看做是某種商品,代理看做是中間代理商,真正被代理的物件看做是生產商品的工廠或者說是廠家吧。下面我們就以這個為例,實現代理模式。

注:這裡將所有的類放在了一個資料夾內,將內部類用 static 修飾,是為了方便使用。

public class StaticProxy {
    // 商品介面
    static interface Goods {
        public void trade();
    }

    // 商品產地類
    static class ChangJia implements Goods {
        @Override
        public void trade() {
            System.out.println("廠家生產商品");
        }
    }

    // 商品代理類
    static class JingXiaoShang implements Goods {
        private ChangJia changJia;

        public JingXiaoShang(ChangJia changJia) {
            this.changJia = changJia;
        }

        @Override
        public void trade() {
            System.out.println("廠家生產產品,成本為1000元");
            changJia.trade();
            System.out.println("經銷商賣出商品,利潤為100元");
        }
    }

    // 使用者購買東西
    public static void main(String[] args) {
        Goods proxy = new JingXiaoShang(new ChangJia());
        proxy.trade(); // 商品被交易
    }
}
複製程式碼

輸出結果為

廠家生產產品,成本為1000元
廠家生產商品
經銷商賣出商品,利潤為100複製程式碼

上面的程式碼便是代理模式的實現,這是一種 “靜態代理”,也即是代理類在程式編譯期就已經確定了,而在 Java 還可以實現 “動態代理”,代理類是在程式執行時生成的。

Java 實現動態代理有兩種方式,一種是 Java 自帶的 JDK 動態代理,還有一種是使用位元組碼增強技術實現的 CGLIB 庫動態代理,下面就詳細介紹並用這兩種方式實現上面程式碼的功能。

1、JDK 動態代理

JDK 動態代理本質上使用的是 Java 中的反射。方法定義在介面(這裡是 Goods 介面)中,被代理的類(這裡是ChangJia類)要實現介面,進而實現介面中的方法。當呼叫介面中的方法時,攔截要執行的方法,新增其他的操作,還需要一個處理攔截的處理類(這裡是 GoodsHander 類),這個處理類實現自 InvocationHandler 介面,其中 invoke() 方法會在呼叫介面中的方法時先執行,這樣就起到了代理的作用。和上面代理模式中的實現不同,JDK動態代理並沒有直接定義代理類,而是新增了介面中方法的處理類,當程式執行中動態生成一個代理類或者直接生成一個代理物件來執行方法,詳情可以仔細閱讀下面的程式碼及註釋。

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class JDKDynamicProxy {
    // 商品介面
    static interface Goods {
        public void trade();
    }

    // 商品產地類
    static class ChangJia implements Goods {
        @Override
        public void trade() {
            System.out.println("廠家生產商品");
        }
    }

    // 商品處理類
    static class GoodsHander implements InvocationHandler {
        private Object object; // 要代理的物件,這裡為商品

        public GoodsHander(Object object) {
            this.object = object;
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            // proxy 為生成的代理物件
            System.out.println("廠家生產產品,成本為1000元");
            Object result = method.invoke(object, args);
            System.out.println("經銷商賣出商品,利潤為100元");
            return result;
        }
    }

    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        GoodsHander goodsHander = new GoodsHander(new ChangJia()); // GoodHander類和ChangJia打交道
        // 先動態生成代理類,再生成代理類的物件
//        Class proxyClass = Proxy.getProxyClass(Goods.class.getClassLoader(),Goods.class.getInterfaces());
//        Goods proxy = (Goods) proxyClass.getConstructor(GoodsHander.class).newInstance(goodsHander);
        // 動態生成的代理物件,一步到位
        Goods proxy = (Goods) Proxy.newProxyInstance(Goods.class.getClassLoader(), Goods.class.getInterfaces(), goodsHander);
        proxy.trade(); // 執行的是GoodsHander中的 invoke() 方法,然後是ChangJia中的 trade() 方法
    }
}
複製程式碼

2、CGLIB 動態代理

上面的 JDK 動態代理需要定義一個介面,實現類實現介面中的方法,如果實現類不能實現介面時,我們就無法使用上面這種方式代理了,而可以使用下面這種 CGLIB 動態代理。CGLIB 動態代理使用的位元組碼增強技術,也即是對編譯好的 class(位元組碼)檔案生成該類的子類。利用多型性,呼叫父類中的方法實際上是呼叫的子類中對應的方法。所以由於是繼承,被代理的類或者方法不要用 final 關鍵字修飾。這個子類是在程式中執行的生成的,所以使用 CGLIB 庫也是動態代理。使用 CGLIB 要引入 cglib 的 jar 包以及依賴 jar 包 asm.jar,這個 asm.jar 是位元組碼增強技術的核心。使用 CGLIB 庫就不需要介面了,增加了一個方法增強類,通過 CGLIB 庫中的 Enhance 增強類來生成被代理類的子類,詳情可以仔細閱讀下面的程式碼及註釋。

注:使用 CGLIB 動態代理之前需要匯入相關 jar 包,可以單獨匯入 cglib-*.jar 包和 asm-.jar 包(注意兩者的版本搭配),也可以只匯入一個 cglib-nodep-*.jar 包(包含了 asm)。下載地址:github.com/cglib/cglib…

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

public class CGLIBDynamicProxy {
    // 商品產地類
    static class ChangJia{
        public void trade() {
            System.out.println("廠家生產商品");
        }
    }

    // 方法增強類
    static class GoodsTrade implements MethodInterceptor {
        @Override
        // o 代表要增強的物件,method代表要攔截的方法,objects 代表方法中的引數,methodProxy 代表對方法的代理
        public Object intercept(Object object, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
            System.out.println("廠家生產產品,成本為1000元");
            methodProxy.invokeSuper(object, objects);
            System.out.println("經銷商賣出商品,利潤為100元");
            return object;
        }

        public static void main(String[] args) {
            Enhancer enhancer = new Enhancer(); // 增強類物件
            enhancer.setSuperclass(ChangJia.class); // 設定要增強的類,這裡為 ChangJia
            GoodsTrade goodsTrade = new GoodsTrade();
            enhancer.setCallback(goodsTrade); // 設定要增強的方法,這裡為 GoodsTrade
            ChangJia changJia = (ChangJia) enhancer.create(); // 生成增強過的子類物件
            changJia.trade(); // 呼叫方法實際為增強過的方法
        }
    }
}
複製程式碼

兩種動態代理方式的區別:

  • JDK 動態代理使用的是反射技術,被代理的類要實現方法介面。
  • CGLIB 動態代理使用的是位元組碼增強技術,被代理的類不用實現方法介面。

在大名鼎鼎的 Spring 框架中,這兩種代理方式都有使用,Spring 會根據被代理的類有沒有實現介面,在 JDK 動態代理和 CGLIB 之間動態切換。另外面向切面程式設計的 AOP 也是動態代理思想的體現,通過在方法執行前後織入新的方法,來達到增強方法的效果,動態代理在框架中應用廣泛。

程式設計心路

相關文章