軟體設計模式白話文系列(六)代理模式

2022-11-11 18:02:27

1、描述

代理模式屬於結構型模式中的一種,通過對代理物件的呼叫來達到對原物件的增強、減弱作用。通過代理類的生成時機,我們將編譯期就生成代理類的情況稱之為靜態代理模式,而在 Java 執行期動態生成代理類的場景稱為動態代理模式。動態代理又基於介面繼承兩種實現方式分別分為 JDK 動態代理和 CGLib 動態代理兩種。

2、適用性

  • 當存取物件不方便直接參照時(如原物件授權太過寬泛、需要對不同使用者級別提供不同許可權)。
  • 原物件功能需要增加,可以通過代理模式在不影響原始類的基礎上實現目的。

3、實現邏輯

  1. 抽取真實主題類需要代理的介面獲取抽象主題類。
  2. 代理類和主題類共同實現抽象主題類。
  3. 代理類控制真實主題類生命週期。代理在完成⼀些任務後 應將⼯作委派給服真實主題類的物件。
  • 真實主題類:原始類、需要被代理的類。

  • 抽象主題類:定義真實主題類中需要被代理類代理的介面。保證代理物件和原始物件的可互動性。

  • 代理類:實現抽象主題類,其內部含有對真實主題的參照,它可以存取、控制或擴充套件真實主題的功能。

4、實戰程式碼

4.1 靜態代理

存在一個 Member 操作的業務層程式碼。現僅提供新增和查詢功能的代理類,並需記錄查詢紀錄檔。

/**
 * 使用者類
 *
 * @author Eajur.Wen
 * @version 1.0
 * @date 2022-11-11 06:00:46
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Member {
    private Long id;
    private String name;
}

/**
 * 原始主題類介面(模擬實際開發,代理模式中這個類沒有實際用途)
 *
 * @author Eajur.Wen
 * @version 1.0
 * @date 2022-11-11 06:17:12
 */
public interface MemberService {

    Member getMember(Long id);

    List<Member> listMember();

    void saveMember(Long id, String name);

    void updateMember(Long id, String name);

    void deleteMember(Long id);
}

/**
 * 抽象主題類
 *
 * @author Eajur.Wen
 * @version 1.0
 * @date 2022-11-11 06:00:14
 */
public class MemberServiceImpl implements MemberService, ProxyService {
    /**
     * 模擬 DB 初始化點資料
     */
    public static final Map<Long, Member> db = new HashMap<>();

    static {
        db.put(1L, new Member(1L, "張三"));
        db.put(2L, new Member(2L, "李四"));
    }

    @Override
    public Member getMember(Long id) {
        Member member = db.get(id);
        System.out.println(member);
        return member;
    }

    @Override
    public List<Member> listMember() {
        ArrayList<Member> members = new ArrayList<>(db.values());
        System.out.println(members);
        return members;
    }

    @Override
    public void saveMember(Long id, String name) {
        db.put(id, new Member(id, name));
    }

    @Override
    public void updateMember(Long id, String name) {
        db.put(id, new Member(id, name));
    }

    @Override
    public void deleteMember(Long id) {
        db.remove(id);
    }
}

/**
 * 抽象主題類
 *
 * @author Eajur.Wen
 * @version 1.0
 * @date 2022-11-11 06:17:12
 */
public interface ProxyService {

    Member getMember(Long id);

    List<Member> listMember();

    void saveMember(Long id, String name);

}

/**
 * 代理類
 *
 * @author Eajur.Wen
 * @version 1.0
 * @date 2022-11-11 06:16:26
 */
public class ProxyServerImpl implements ProxyService {

    private MemberService memberService = new MemberServiceImpl();

    @Override
    public Member getMember(Long id) {
        System.out.println("代理類查詢 member 詳情資訊");
        return memberService.getMember(id);
    }

    @Override
    public List<Member> listMember() {
        System.out.println("代理類查詢 member 列表資訊");
        return memberService.listMember();
    }

    @Override
    public void saveMember(Long id, String name) {
        System.out.println("代理類新增 member 資訊");
        memberService.saveMember(id, name);
    }
}

/**
 * 測試類
 *
 * @author Eajur.Wen
 * @version 1.0
 * @date 2022-11-11 06:28:01
 */
public class App {
    public static void main(String[] args) {
        ProxyService proxyServer = new ProxyServerImpl();
        proxyServer.saveMember(3L, "王五");
        System.out.println("--------------");
        proxyServer.getMember(1L);
        System.out.println("--------------");
        proxyServer.listMember();
    }
}

執行結果:

通過代理模式,我實現了對原始物件的存取控制,使用者端只能呼叫新增和查詢介面,同時也實現對原始物件介面的增強作用,新增了紀錄檔列印功能。

4.2 JDK 動態代理

Java 中提供了一個動態代理類 Proxy,Proxy 不是上述中的代理類,而是通過其提供的靜態方法(newProxyInstance 方法)來動態建立代理物件。

/**
 * 動態代理工廠
 *
 * @author Eajur.Wen
 * @version 1.0
 * @date 2022-11-11 13:47:40
 */
public class ProxyFactory {
    private static ProxyService proxyService = new MemberServiceImpl();

    public static ProxyService getProxyService() {
        return (ProxyService) Proxy.newProxyInstance(
                proxyService.getClass().getClassLoader(),
                proxyService.getClass().getInterfaces(),
                (proxy, method, args) -> {
                    System.out.println("代理類操作 member 功能:" + method.getName());
                    return method.invoke(proxyService, args);
                });
    }
}

/**
 * 測試類
 *
 * @author Eajur.Wen
 * @version 1.0
 * @date 2022-11-11 06:28:01
 */
public class App {
    public static void main(String[] args) {
        ProxyService proxyServer = ProxyFactory.getProxyService();
        proxyServer.saveMember(3L, "王五");
        System.out.println("--------------");
        proxyServer.getMember(1L);
        System.out.println("--------------");
        proxyServer.listMember();
    }
}

執行結果:

動態代理相對於靜態代理,可以把多個方法集中控制,在需代理的方法數量多或者會出現改動的時候,動態代理效率遠高於靜態代理。

4.3 CGLib 動態代理

JDK 動態代理和靜態代理的必要前提是代理類和原始類都需要建立⼀個接⼝來實現代理和服務物件的可交換性。 如果沒有現成的服務接⼝,從服務類中抽取接⼝並⾮總是可⾏的, 因為我們需要對服務的所有使用者端進⾏修改, 讓它們使⽤接⼝。所以我們的備選方案將代理作為服務類的⼦類, 這樣代理就能繼承服務的所有接⼝了。

這裡我們需要匯入 CGLib 依賴,CGLib 可以為沒有提供介面的類實現代理。

<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>2.2.2</version>
</dependency>
/**
 * 抽象主題類 不用實現介面
 *
 * @author Eajur.Wen
 * @version 1.0
 * @date 2022-11-11 06:00:14
 */
public class MemberServiceImpl {
    /**
     * 模擬 DB 初始化點資料
     */
    public static final Map<Long, Member> db = new HashMap<>();

    static {
        db.put(1L, new Member(1L, "張三"));
        db.put(2L, new Member(2L, "李四"));
    }

    public Member getMember(Long id) {
        Member member = db.get(id);
        System.out.println(member);
        return member;
    }

    public List<Member> listMember() {
        ArrayList<Member> members = new ArrayList<>(db.values());
        System.out.println(members);
        return members;
    }

    public void saveMember(Long id, String name) {
        db.put(id, new Member(id, name));
    }

    public void updateMember(Long id, String name) {
        db.put(id, new Member(id, name));
    }

    public void deleteMember(Long id) {
        db.remove(id);
    }
}

/**
 * 動態代理工廠
 *
 * @author Eajur.Wen
 * @version 1.0
 * @date 2022-11-11 13:47:40
 */
public class ProxyFactory implements MethodInterceptor {

    public MemberServiceImpl getProxyObject() {
        Enhancer enhancer = new Enhancer();
        //設定父類別的位元組碼物件
        enhancer.setSuperclass(MemberServiceImpl.class);
        //設定回撥函數
        enhancer.setCallback(this);
        //建立代理物件
        return (MemberServiceImpl) enhancer.create();
    }

    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        System.out.println("代理類操作 member 功能:" + method.getName());
        return methodProxy.invokeSuper(o, objects);
    }
}

/**
 * 測試類
 *
 * @author Eajur.Wen
 * @version 1.0
 * @date 2022-11-11 06:28:01
 */
public class App {
    public static void main(String[] args) {
        ProxyFactory proxyFactory = new ProxyFactory();
        MemberServiceImpl proxyObject = proxyFactory.getProxyObject();
        proxyObject.saveMember(3L, "王五");
        System.out.println("--------------");
        proxyObject.getMember(1L);
        System.out.println("--------------");
        proxyObject.listMember();
        System.out.println("--------------");
        proxyObject.deleteMember(1L);
    }
}

執行結果:

這裡需要注意,CGLib 生成代理類的本質是生成原始類的子類,因此無法代理被 final 修飾的類,或者被 final 修飾的方法,且會將原始類可被繼承的方法完全暴露給使用者端。

這裡簡單提一下,在 JDK 1.8 過後,JDK 動態代理效率高於 CGLib 動態代理,Spring AOP 中也是根據需代理類的方法有無介面來優先判斷是否使用 JDK 動態代理。