【瞭解】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 比較
生成代理類技術不同
-
java 動態代理:jdk 自帶類 ProxyGenerator 生成 class 字節碼
-
cglib:通過 ASM 框架生成 class 字節碼文件
生成代理類的方式不同
-
java 動態代理:代理類繼承 java.lang.reflect.Proxy,實現被代理類的接口
-
cglib:代理類繼承被代理類,實現 net.sf.cglib.proxy.Factory
生成類數量不同
-
java 動態代理:生成一個 proxy 類
-
cglib:生成一個 proxy,兩個 fastclass 類
調用方式不同
-
java 動態代理:代理類 ->InvocationHandler-> 反射調用被代理類方法
-
cglib:代理類 ->MethodInterceptor-> 調用索引類 invoke-> 直接調用被代理類方法
在 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 切面統一打印出入參請求日誌
先看看切面日誌輸出效果
從上圖中可以看到,每個對於每個請求,開始與結束一目瞭然,並且打印了以下參數:
-
URL: 請求接口地址;
-
Description: 接口的中文說明信息;
-
HTTP Method: 請求的方法,是
POST
,GET
, 還是DELETE
等; -
Class Method: 被請求的方法路徑 : 包名 + 方法名;
-
IP: 請求方的 IP 地址;
-
Request Args: 請求入參,以 JSON 格式輸出;
-
Response Args: 響應出參,以 JSON 格式輸出;
-
Time-Consuming: 請求耗時,以此估算每個接口的性能指數;
項目 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>
自定義日誌註解
-
①:什麼時候使用該註解,我們定義爲運行時;
-
②:註解用於什麼地方,我們定義爲作用於方法上;
-
③:註解是否將包含在 JavaDoc 中;
-
④:註解名爲
WebLog
; -
⑤:定義一個屬性,默認爲空字符串;
源代碼如下:
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
相關注解的作用:
-
@Aspect:聲明該類爲一個註解類;
-
@Pointcut:定義一個切點,後面跟隨一個表達式,表達式可以定義爲切某個註解,也可以切某個 package 下的方法;
切點定義好後,就是圍繞這個切點做文章了:
-
@Before: 在切點之前,織入相關代碼;
-
@After: 在切點之後,織入相關代碼;
-
@AfterReturning: 在切點返回內容後,織入相關代碼,一般用於對返回值做些加工處理的場景;
-
@AfterThrowing: 用來處理當織入的代碼拋出異常後的邏輯處理;
-
@Around: 環繞,可以在切入點前後織入代碼,並且可以自由的控制何時執行切點;
接下來,定義一個 WebLogAspect.java
切面類,聲明一個切點:
然後,定義 @Around
環繞,用於何時執行切點:
-
①:記錄一下調用接口的開始時間;
-
②:執行切點,執行切點後,會去依次調用 @Before -> 接口邏輯代碼 -> @After -> @AfterReturning;
-
③:打印出參;
-
④:打印接口處理耗時;
-
⑤:返回接口返參結果;
再來看看 @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