【瞭解】Spring 的代理模式

代理模式是什麼?

代理模式是常見的設計模式之一,顧名思義,代理模式就是代理對象具備真實對象的功能,並代替真實對象完成相應操作,並能夠在操作執行的前後,對操作進行增強處理。(爲真實對象提供代理,然後供其他對象通過代理訪問真實對象)。

之前還了解過 spring 的觀察者模式

spring 的觀察者模式

三豐,公衆號:soft 張三丰【瞭解】Spring 的觀察者模式

靜態代理

以租房爲例,租客找房東租房,然後中間經過房屋中介,以此爲背景,它的 UML 圖如下:

動態代理

從靜態代理的代碼中可以發現,靜態代理的缺點顯而易見,那就是當真實類的方法越來越多的時候,這樣構建的代理類的代碼量是非常大的,所以就引進動態代理.

動態代理允許使用一種方法的單個類(代理類)爲具有任意數量方法的任意類(真實類)的多個方法調用提供服務,看到這句話,可以容易的聯想到動態代理的實現與反射密不可分。

JAVA 反射機制是在運行狀態中,對於任意一個類,都能夠知道這個類的所有屬性和方法;對於任意一個對象,都能夠調用它的任意一個方法和屬性;這種動態獲取的信息以及動態調用對象的方法的功能稱爲 java 語言的反射機制。

JDK 動態代理

JDK 動態代理有兩大核心類,它們都在 Java 的反射包下(java.lang.reflect),分別爲 InvocationHandler 接口和 Proxy 類。

InvocationHandler 接口

代理實例的調用處理器需要實現 InvocationHandler 接口,並且每個代理實例都有一個關聯的調用處理器。當一個方法在代理實例上被調用時,這個方法調用將被編碼並分派到其調用處理器的 invoke 方法上。

也就是說,我們創建的每一個代理實例都要有一個關聯的 InvocationHandler,並且在調用代理實例的方法時,會被轉到 InvocationHandler 的 invoke 方法上。

publicObject invoke(Object proxy, Method method, Object[] args) throws Throwable;

該 invoke 方法的作用是:處理代理實例上的方法調用並返回結果。

其有三個參數,分別爲:

proxy:是調用該方法的代理實例。method:是在代理實例上調用的接口方法對應的 Method 實例。args:一個 Object 數組,是在代理實例上的方法調用中傳遞的參數值。如果接口方法爲無參,則該值爲 null。其返回值爲:調用代理實例上的方法的返回值。

Proxy 類

Proxy 類提供了創建動態代理類及其實例的靜態方法,該類也是動態代理類的超類。

代理類具有以下屬性:

代理類的名稱以 “$Proxy” 開頭,後面跟着一個數字序號。代理類繼承了 Proxy 類。代理類實現了創建時指定的接口(JDK 動態代理是面向接口的)。每個代理類都有一個公共構造函數,它接受一個參數,即接口 InvocationHandler 的實現,用於設置代理實例的調用處理器。Proxy 提供了兩個靜態方法,用於獲取代理對象。

getProxyClass

用於獲取代理類的 Class 對象,再通過調用構造函數創建代理實例。

該方法有兩個參數:

loader:爲類加載器。intefaces:爲接口的 Class 對象數組。返回值爲動態代理類的 Class 對象。

newProxyInstance

用於創建一個代理實例。

該方法有三個參數:

loader:爲類加載器。interfaces:爲接口的 Class 對象數組。h:指定的調用處理器。返回值爲指定接口的代理類的實例。

cglib 動態代理

JDK 的動態代理機制只能代理實現了接口的類。而不能實現接口的類就不能使用 JDK 的動態代理,CGLIB 是針對類來實現代理的,它的原理是對指定目標類生成一個子類,並覆蓋其中的方法實現增強但因爲採用的是繼承,所以不能對 final 修飾的類進行代理。

 CGLIB 像是一個攔截器,在調用我們的代理類方法時,代理類 (子類) 會去找到目標類(父類), 此時它會被一個方法攔截器所攔截,在攔截器中才會去實現方法的調用。並且還會對方法進行行爲增強。

Java 動態代理和 cglib 比較

生成代理類技術不同

生成代理類的方式不同

生成類數量不同

調用方式不同

在 jdk6 之前比使用 Java 反射效率要高,在 jdk6、jdk7、jdk8 逐步對 JDK 動態代理優化之後,在調用次數較少的情況下,JDK 代理效率 高於 CGLIB 代理效率。只有當進行大量調用的時候,jdk6 和 jdk7 比 CGLIB 代理效率低一點,但是到 jdk8 的時候,jdk 代理效率高於 CGLIB 代理,總之,每一次 jdk 版本升級,JDK 代理效率 都得到提升,而 CGLIB 代理效率 確有點跟不上步伐。

Spring 創建動態代理的類在哪裏?

Spring 框架運行時,會通過動態字節碼技術,在 JVM 中創建的動態代理對象,運行在 JVM 內部,等程序結束後,會和 jvm 一起消失。

這也是爲什麼叫做動態代理的原因,就是因爲這個對象是動態生成出來的。不像靜態代理我們必須自己創建。

動態代理編程簡化了代理的開發

切入點的表達式是配置的所有方法都執行額外功能,如果想要指定部分方法執行,可以通過修改切入點表達式的方式實現。

在額外功能不變的前提下,創建其他目標類的代理對象時,只需要執行目標對象即可。

spring 中也爲我們提供了動態代理的實現。可以幫助我們爲目標類添加額外功能。

Spring Boot 自定義註解,AOP 切面統一打印出入參請求日誌

先看看切面日誌輸出效果

從上圖中可以看到,每個對於每個請求,開始與結束一目瞭然,並且打印了以下參數:

項目 pom.xml 文件中添加依賴

<!-- aop 依賴 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- 用於日誌切面中,以 json 格式打印出入參 -->
<dependency>
    <groupId>com.google.code.gson</groupId>
    <artifactId>gson</artifactId>
    <version>2.8.5</version>
</dependency>

自定義日誌註解

源代碼如下:

import java.lang.annotation.*;
/**
 * @author 三豐 (微信號:soft張三丰)
 * @site https://mp.weixin.qq.com/s?__biz=MzI3MTQyNDc5MA==&mid=2247488556&idx=1&sn=f633fea17f4405d1c9064f6a7596cd74&chksm=eac35855ddb4d143d2cced71dbb5c5449ad63f1d4495eb5677190bf269c50efa80f3a0e4599a&token=1994772128&lang=zh_CN#rd
 * @date 2012/12
 * @time 下午9:19
 * @discription
 **/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
public @interface WebLog {
    /**
     * 日誌描述信息
     *
     * @return
     */
    String description() default "";
}

配置 AOP 切面

在配置 AOP 切面之前,我們需要了解下 aspectj 相關注解的作用:

切點定義好後,就是圍繞這個切點做文章了:

接下來,定義一個 WebLogAspect.java 切面類,聲明一個切點:

然後,定義 @Around 環繞,用於何時執行切點:

再來看看 @Before 方法:

代碼

import com.google.gson.Gson;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
/**
 * @author 三豐 (微信號:soft張三丰)
 * @site https://mp.weixin.qq.com/s?__biz=MzI3MTQyNDc5MA==&mid=2247488556&idx=1&sn=f633fea17f4405d1c9064f6a7596cd74&chksm=eac35855ddb4d143d2cced71dbb5c5449ad63f1d4495eb5677190bf269c50efa80f3a0e4599a&token=1994772128&lang=zh_CN#rd
 * @date 2012/12
 * @date 2019/2/12
 * @time 下午9:19
 * @discription
 **/
@Aspect
@Component
@Profile({"dev", "test"})
public class WebLogAspect {
    private final static Logger logger         = LoggerFactory.getLogger(WebLogAspect.class);
    /** 換行符 */
    private static final String LINE_SEPARATOR = System.lineSeparator();
    /** 以自定義 @WebLog 註解爲切點 */
    @Pointcut("@annotation(site.exception.springbootaopwebrequest.aspect.WebLog)")
    public void webLog() {}
    /**
     * 在切點之前織入
     * @param joinPoint
     * @throws Throwable
     */
    @Before("webLog()")
    public void doBefore(JoinPoint joinPoint) throws Throwable {
        // 開始打印請求日誌
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        // 獲取 @WebLog 註解的描述信息
        String methodDescription = getAspectLogDescription(joinPoint);
        // 打印請求相關參數
        logger.info("========================================== Start ==========================================");
        // 打印請求 url
        logger.info("URL            : {}", request.getRequestURL().toString());
        // 打印描述信息
        logger.info("Description    : {}", methodDescription);
        // 打印 Http method
        logger.info("HTTP Method    : {}", request.getMethod());
        // 打印調用 controller 的全路徑以及執行方法
        logger.info("Class Method   : {}.{}", joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName());
        // 打印請求的 IP
        logger.info("IP             : {}", request.getRemoteAddr());
        // 打印請求入參
        logger.info("Request Args   : {}", new Gson().toJson(joinPoint.getArgs()));
    }
    /**
     * 在切點之後織入
     * @throws Throwable
     */
    @After("webLog()")
    public void doAfter() throws Throwable {
        // 接口結束後換行,方便分割查看
        logger.info("=========================================== End ===========================================" + LINE_SEPARATOR);
    }
    /**
     * 環繞
     * @param proceedingJoinPoint
     * @return
     * @throws Throwable
     */
    @Around("webLog()")
    public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        long startTime = System.currentTimeMillis();
        Object result = proceedingJoinPoint.proceed();
        // 打印出參
        logger.info("Response Args  : {}", new Gson().toJson(result));
        // 執行耗時
        logger.info("Time-Consuming : {} ms", System.currentTimeMillis() - startTime);
        return result;
    }
    /**
     * 獲取切面註解的描述
     *
     * @param joinPoint 切點
     * @return 描述信息
     * @throws Exception
     */
    public String getAspectLogDescription(JoinPoint joinPoint)
            throws Exception {
        String targetName = joinPoint.getTarget().getClass().getName();
        String methodName = joinPoint.getSignature().getName();
        Object[] arguments = joinPoint.getArgs();
        Class targetClass = Class.forName(targetName);
        Method[] methods = targetClass.getMethods();
        StringBuilder description = new StringBuilder("");
        for (Method method : methods) {
            if (method.getName().equals(methodName)) {
                Class[] clazzs = method.getParameterTypes();
                if (clazzs.length == arguments.length) {
                    description.append(method.getAnnotation(WebLog.class).description());
                    break;
                }
            }
        }
        return description.toString();
    }
}

源碼地址

https://github.com/weiwosuoai/spring-boot-tutorial/tree/master/spring-boot-aop-web-request
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/_a9KOm6JHhwvTm_EMkNTCw