9、Spring之代理模式

来源:https://www.cnblogs.com/Javaer1995/archive/2023/08/08/17610379.html
-Advertisement-
Play Games

## 9.1、環境搭建 ### 9.1.1、創建module ![image](https://img2023.cnblogs.com/blog/2052479/202308/2052479-20230806234218377-617105837.png) ### 9.1.2、選擇maven ![i ...


9.1、環境搭建

9.1.1、創建module

image

9.1.2、選擇maven

image

9.1.3、設置module名稱和路徑

image

image

9.1.4、module初始狀態

image

9.1.5、配置打包方式和依賴

image

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.rain</groupId>
    <artifactId>spring_proxy</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <dependencies>
        <!-- junit測試 -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
    </dependencies>


</project>

9.2、場景模擬

9.2.1、創建Calculator介面及實現類

image

package org.rain.spring.proxy;

/**
 * @author liaojy
 * @date 2023/8/6 - 23:53
 */
public interface Calculator {

    int add(int i, int j);
    int sub(int i, int j);
    int mul(int i, int j);
    int div(int i, int j);

}

image

package org.rain.spring.proxy;

/**
 * @author liaojy
 * @date 2023/8/6 - 23:55
 */
public class CalculatorImpl implements Calculator {
    public int add(int i, int j) {

        int result = i + j;
        System.out.println("方法內部 result = " + result);
        return result;

    }

    public int sub(int i, int j) {

        int result = i - j;
        System.out.println("方法內部 result = " + result);
        return result;

    }

    public int mul(int i, int j) {

        int result = i * j;
        System.out.println("方法內部 result = " + result);
        return result;

    }

    public int div(int i, int j) {

        int result = i / j;
        System.out.println("方法內部 result = " + result);
        return result;

    }
}

9.2.2、為Calculator實現類增加日誌功能

image

package org.rain.spring.proxy;

/**
 * @author liaojy
 * @date 2023/8/6 - 23:55
 */
public class CalculatorImpl implements Calculator {
    public int add(int i, int j) {

        System.out.println("[日誌] add 方法開始了,參數是:" + i + "," + j);
        int result = i + j;
        System.out.println("方法內部 result = " + result);
        System.out.println("[日誌] add 方法結束了,結果是:" + result);
        return result;

    }

    public int sub(int i, int j) {

        System.out.println("[日誌] sub 方法開始了,參數是:" + i + "," + j);
        int result = i - j;
        System.out.println("方法內部 result = " + result);
        System.out.println("[日誌] sub 方法結束了,結果是:" + result);
        return result;

    }

    public int mul(int i, int j) {

        System.out.println("[日誌] mul 方法開始了,參數是:" + i + "," + j);
        int result = i * j;
        System.out.println("方法內部 result = " + result);
        System.out.println("[日誌] mul 方法結束了,結果是:" + result);
        return result;

    }

    public int div(int i, int j) {

        System.out.println("[日誌] div 方法開始了,參數是:" + i + "," + j);
        int result = i / j;
        System.out.println("方法內部 result = " + result);
        System.out.println("[日誌] div 方法結束了,結果是:" + result);
        return result;

    }
}

9.3、場景分析

9.3.1、代碼缺陷

關於帶日誌功能的實現類,有如下缺陷:

  • 附加功能對核心業務功能有干擾,降低了開發效率

  • 附加功能分散在各個業務功能方法中,不利於統一維護

9.3.2、解決思路

解決這兩個問題,核心方式就是:解耦;把附加功能從業務功能代碼中抽取出來

9.3.3、技術難點

要抽取的代碼在方法內部,靠以前把子類中的重覆代碼抽取到父類的方式沒法解決,因此需要引入新的技術(代理模式)。

9.4、代理模式的概述

9.4.1、概念

  • 代理模式是二十三種設計模式中的一種,屬於結構型模式

  • 它的思想就是在不改動目標方法代碼的基礎上,增強目標方法的功能

  • 它的實現就是通過提供一個代理類,讓我們在調用目標方法的時候,不再是直接對目標方法進行調用,而是通過代理類間接調用目標方法

  • 它的作用就是把不屬於目標方法核心邏輯的代碼從目標方法中剝離出來,從而實現解耦和統一維護

9.4.2、術語

  • 目標:封裝了核心功能代碼,的類、對象、方法

  • 代理:封裝了增強功能代碼、且能調用目標,的類、對象、方法

9.4.3、生活中的目標和代理

  • 廣告商找大明星(目標)拍廣告,需要經過經紀人(代理)

  • 買房者找賣房者(目標)購房,需要經過房產中介(代理)

9.5、靜態代理

先將實現類CalculatorImpl還原為沒有增加日誌功能的狀態,即9.2.1小節的狀態

9.5.1、創建靜態代理類CalculatorStaticProxy

image

註意:代理類和目標類要實現相同的介面,這樣能保證它們有相同的方法列表

package org.rain.spring.proxy;

/**
 * @author liaojy
 * @date 2023/8/7 - 12:56
 */
public class CalculatorStaticProxy implements Calculator {

    // 將被代理的目標對象聲明為成員變數
    private Calculator target;

    public CalculatorStaticProxy(Calculator target) {
        this.target = target;
    }

    public int add(int i, int j) {
        // 附加功能由代理類中的代理方法來實現
        System.out.println("[日誌] add 方法開始了,參數是:" + i + "," + j);
        // 通過目標對象來實現核心業務邏輯
        int addResult = target.add(i, j);
        System.out.println("[日誌] add 方法結束了,結果是:" + addResult);
        return addResult;
    }

    public int sub(int i, int j) {
        System.out.println("[日誌] sub 方法開始了,參數是:" + i + "," + j);
        int subResult = target.sub(i, j);
        System.out.println("[日誌] sub 方法結束了,結果是:" + subResult);
        return subResult;
    }

    public int mul(int i, int j) {
        System.out.println("[日誌] mul 方法開始了,參數是:" + i + "," + j);
        int mulResult = target.mul(i, j);
        System.out.println("[日誌] mul 方法結束了,結果是:" + mulResult);
        return mulResult;
    }

    public int div(int i, int j) {
        System.out.println("[日誌] div 方法開始了,參數是:" + i + "," + j);
        int divResult = target.div(i, j);
        System.out.println("[日誌] div 方法結束了,結果是:" + divResult);
        return divResult;
    }
}

9.5.2、測試

image

package org.rain.spring.test;

import org.junit.Test;
import org.rain.spring.proxy.CalculatorImpl;
import org.rain.spring.proxy.CalculatorStaticProxy;

/**
 * @author liaojy
 * @date 2023/8/7 - 14:12
 */
public class ProxyTest {

    @Test
    public void testStaticProxy(){
        CalculatorStaticProxy calculatorStaticProxy = new CalculatorStaticProxy(new CalculatorImpl());
        int addResult = calculatorStaticProxy.add(1, 2);
        System.out.println(addResult);
    }

}

9.5.3、靜態代理的缺點

  • 靜態代理確實實現瞭解耦,但是由於代碼都寫死了,完全不具備任何的靈活性

  • 當其他目標類也需要附加日誌,就得創建更多靜態代理類,還是產生了大量重覆的代碼;而且日誌功能還是分散的,沒有統一管理

9.6、動態代理

動態代理的意思是,在代碼運行的過程中動態地生成目標類的代理類

9.6.1、創建生成代理對象的工廠類ProxyFactory

image

比起實現固定介面方法的靜態代理,動態代理的關鍵是能動態獲取並實現目標的介面方法;
因此動態代理能對任意目標對象的核心業務方法(介面方法)進行增強

package org.rain.spring.proxy;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;

/**
 * @author liaojy
 * @date 2023/8/7 - 23:07
 */
//這個類不是一個代理類而是一個工具(工廠)類,用於動態生成目標對象的代理對象
public class ProxyFactory {

    //因為被代理的目標對象是任意的,所以目標對象變數的類型設為Object
    private Object target;

    //通過工廠類的有參構造方法,對目標對象變數進行賦值
    public ProxyFactory(Object target) {
        this.target = target;
    }

    //生成任意目標對象所對應的代理對象;因為不確定動態生成的代理對象的類型,所以返回值設為Object
    public Object getPoxy(){

        //通過目標對象獲取應用類載入器
        ClassLoader classLoader = target.getClass().getClassLoader();

        //獲取目標對象實現的所有介面的class對象所組成的數組
        Class<?>[] interfaces = target.getClass().getInterfaces();

        //通過InvocationHandler的匿名內部類,來設置代理類中如何重寫介面中的抽象方法
        InvocationHandler invocationHandler = new InvocationHandler() {

            //通過invoke方法來統一管理代理類中的方法該如何執行,該方法有三個參數
            /**
             * @param proxy:表示代理對象
             * @param method:表示要執行的方法
             * @param args:表示要執行的方法的參數列表
             */
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                //在調用目標對象執行功能之前,加入額外的操作(這裡是附加日誌功能)
                System.out.println("[日誌] "+method.getName()+" 方法開始了,參數是:" + Arrays.toString(args));

                //固定寫法:調用目標對象實現的核心邏輯(最重要的步驟)
                Object result = method.invoke(target, args);

                //在調用目標對象執行功能之後,加入額外的操作(這裡是附加日誌功能)
                System.out.println("[日誌] "+method.getName()+" 方法結束了,結果是:" + result);

                //固定寫法:保證代理對象和目標對象的返回值一致
                return result;
            }

        };

        //返回(java.lang.reflect包下的)Proxy類的newProxyInstance方法所生產的代理對象
        /**
         * newProxyInstance方法有三個參數:
         *
         * 1、ClassLoader classLoader:指定載入(動態生成的)代理類的類載入器
         *    類只有被載入後才能使用,(動態生成的)代理類需要用應用類載入器來載入
         *    類載入器有四種:
         *      跟類載入器(用於載入核心類庫)
         *      擴展類載入器(用於載入擴展類庫)
         *      應用類載入器(用於載入自己寫的類或第三方jar包中的類)
         *      自定義類載入器
         *
         * 2、Class<?>[] interfaces:指定代理對象要實現的介面
         *    這個參數用於保證代理對象和目標對象有相同的方法列表
         *
         * 3、InvocationHandler invocationHandle:指定調用處理器
         *    該處理器設置了代理對象實現的介面的方法被調用時,該如何執行
         */
        return Proxy.newProxyInstance(classLoader,interfaces,invocationHandler);

    }
}

9.6.2、測試

image

    @Test
    public void testDynamicProxy(){

        //根據目標對象來創建(動態)代理對象的工廠
        ProxyFactory proxyFactory = new ProxyFactory(new CalculatorImpl());

        //通過(動態)代理對象的工廠,生成目標對象所對應的(動態)代理對象
        //因為代理類是動態生成的,所以不確定代理類的類型,因此用其所實現的介面類型
        Calculator poxy = (Calculator) proxyFactory.getPoxy();

        //調用動態代理對象的方法,該方法是目標對象核心業務方法的增強方法
        int addResult = poxy.add(1, 2);
        System.out.println(addResult);

    }

9.6.3、增強的位置

除了可以在調用目標對象執行功能之前或之後,加入額外的操作之外;

還可以在調用目標對象執行功能發生異常時(catch位置)或在調用目標對象執行功能完畢時(finally位置),加入額外的操作

也就是說,(靜態或動態)代理能增強的位置一共有四個

            //通過invoke方法來統一管理代理類中的方法該如何執行,該方法有三個參數
            /**
             * @param proxy:表示代理對象
             * @param method:表示要執行的方法
             * @param args:表示要執行的方法的參數列表
             */
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                Object result = null;
                try {
                    //第1個增強位置:在調用目標對象執行功能之前,加入額外的操作(這裡是附加日誌功能)
                    System.out.println("[日誌] "+method.getName()+" 方法開始了,參數是:" + Arrays.toString(args));

                    //固定寫法:調用目標對象實現的核心邏輯(最重要的步驟)
                    result = method.invoke(target, args);

                    //第2個增強位置:在調用目標對象執行功能之後,加入額外的操作(這裡是附加日誌功能)
                    System.out.println("[日誌] "+method.getName()+" 方法結束了,結果是:" + result);
                } catch (Exception e) {
                    //第3個增強位置:在調用目標對象執行功能發生異常時,加入額外的操作(這裡是附加日誌功能)
                    System.out.println("[日誌] "+method.getName()+",異常:"+e.getMessage());
                }  finally {
                    //第4個增強位置:在調用目標對象執行功能完畢時,加入額外的操作(這裡是附加日誌功能)
                    System.out.println("[日誌] "+method.getName()+",方法執行完畢");
                }

                //固定寫法:保證代理對象和目標對象的返回值一致
                return result;
            }

9.6.4、擴展知識

  • 動態代理有兩種方式:jdk動態代理(本示例)和cglib動態代理

  • jdk動態代理,要求目標必須實現介面,而且只能對目標所實現的介面方法進行增強

  • jdk動態代理,生成的代理類在com.sun.proxy包下,類名為:$proxy+數字

  • cglib動態代理,不要求目標必須實現介面,生成的代理類會繼承目標類,並且和目標類在相同的包下

  • 雖然在實際中很少寫動態代理的代碼,但瞭解動態代理的思想,對學習Spring的AOP知識很有幫助


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • > 本文首發於[掘金](https://juejin.cn/post/7264128388288708664),未經許可禁止轉載 Vuex4 是 Vue 的狀態管理工具,Vuex 和單純的全局對象有以下兩點不同: 1. Vuex 的狀態存儲是響應式的 2. 不能直接改變 store 中的狀態。改變 ...
  • 這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 我們經常會遇到一個場景,比如在一個列表中批量獲取用戶的信息。 如果我們一次性往後端發送幾十條請求是非常愚蠢的事情。此時我們就要學會如何使用批量獲取的邏輯。 但是批量獲取有一個問題就是,我需要在用戶列表項的上層去獲取,然後再把結果分發給下層 ...
  • 分享的 WebStorm 2023.2 最新激活註冊碼,可免費永久激活,親測有效,下麵是詳細文檔哦~ 申明:本教程 WebStorm 激活碼收集於網路,請勿商用,僅供個人學習使用,如有侵權,請聯繫作者刪除。若條件允許,希望大家購買正版 ! PS: 本教程最新更新時間: 2023年08月08日~ 前言 ...
  • 最近,群友分享了一個很有意思的效果: ![](https://img2023.cnblogs.com/blog/608782/202308/608782-20230808101320920-311621134.gif) 原效果的網址:[frosted-glass](https://frosted-g ...
  • 說到 Hybrid App(混合應用)大家都不陌生,因為這種開發模式大行其道發展的這些年取代了很多原生和 Web 應用,為什麼大家對這種「Native + HTML5」的開發模式額外偏愛呢? 因為一方面在一定程度上兼顧了原生應用的優質體驗,另一方面又兼顧到了 HTML5 靈活的開發模式。 這種模式的 ...
  • 為了降低啟動時間,quarkus下的常規作用域bean遵循懶載入規則,但有時我們希望bean可以更早實例化,本篇,咱們一起來瞭解懶載入規則和改變規則的方法 ...
  • 由於垃圾收集演算法的實現涉及大量的程式細節,而且各個平臺的虛擬機操作記憶體的方法又各不相同,因此本節不打算過多地討論演算法的實現,只是介紹幾種演算法的思想及其發展過程。 ...
  • ##### 該切麵功能適用場景 - 下單請求多次提交,導致生成多個相同的訂單 ##### 解決方案 - 前端解決:限制點擊下單按鈕為1次後失效。不足:用戶體驗下降,能繞過前端 - 後端解決:防重提交切麵解決,自定義註釋實現該功能(如下) - 步驟: - 自定義註釋類RepeatSubmit - 創建 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...