1. 程式人生 > >Spring AOP(動態代理\動態位元組碼)精華一頁紙

Spring AOP(動態代理\動態位元組碼)精華一頁紙

1、AOP

AOP作為一種設計理念, 攔截方法執行前後, 提供一些輔助功能。實際上, 在AOP火爆起來之前, 已經存在了很多AOP實現的理念

比如一些設計模式就體現了AOP的思想
Decorator(裝飾者)
Observer(觀察者)
Chain of Responsibility(責任鏈)
...

一些現有的使用場景, 比如 Servlet 攔截器;比如 Java 集合中 Collections 提供了 併發 | 只讀 等等攔截的功能。
除了常說的 鑑權、日誌等用途, 實際使用過程中, 事務和快取的應用意義非常大, 在框架設計上, 把快取和事務的功能從應用功能中獨立出來。從而實現 職責單一的思想。還能對呼叫堆疊、效能分析。比如, 在不破壞現有程式碼的情況下,統計方法的執行時間。

2、動態代理

針對每一種型別,都可以採用 Collections的做法, 實現多個靜態代理的類. 這麼做的優點是效能較好, 而且程式碼結構相對清晰;缺點是程式碼層次多, 冗餘程式碼多。java提供了另一種機制 - 動態代理。

介面
public interface AOP {
public String aop();
}

目標類
public class AOPObject implements AOP{
@Override
public String aop() {
return this.getClass().getName();
}
}

代理類
public class AOPProxy implements InvocationHandler{
private AOP target;

public AOPProxy(AOP target){
this.target = target;
}

@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
System.out.println("aop---");
return method.invoke(target, args);
}
}
使用

AOP aop = (AOP)Proxy.newProxyInstance(AOP.class.getClassLoader(), new Class[]{AOP.class}, new AOPProxy(new AOPObject()));
aop.aop();

核心在於Proxy類通過反射把所有的方法呼叫都轉換成 InvocationHandler 的invoke呼叫。
使用動態代理的注意點:
只能攔截有介面的物件
和介面要在同一個類載入器下

如果沒有的物件, 如何攔截?

3、動態位元組碼

因為Class的設計特點, 可以動態載入Class;所以產生了很多, 動態生成位元組碼-動態載入的技術,實現攔截。

I、Instrumentation 指令改寫
Instrumentation(指令改寫), Java提供的這個功能開口, 可以向java類檔案新增位元組碼資訊。可以用來輔助日誌資訊,記錄方法執行的次數,或者建立程式碼覆蓋測試的工具。通過該方式,不會對現有應用功能產生更改。
a、靜態方式 PreMain
manifest檔案 指定 Premain-Class 實現類
實現 ClassFileTransformer 介面的transform 方法

b、動態方式 AgentMain
manifest檔案 指定Agent-Class 實現類
Attach Tools API 把程式碼繫結到具體JVM上 (需要指定繫結的程序號)

-- 這個功能正常的用途是JVM管理的一個功能,可以監控系統執行的;原理上也可以用在系統增強上面,不過premain和agentmain是針對整個虛擬機器的鉤子(hook),每個類裝載進來都會執行,所以很少使用在功能增強上

II、asm 
原理是 讀入 現有Class 的位元組碼, 生成結構樹, 通過二次開發介面, 提供增強功能, 然後再把位元組碼寫入
a、繼承 ClassAdapter 獲取Class內域和方法的訪問器 ClassVisitor
b、繼承 MethodAdapter 實現方法的覆蓋
c、通過 ClassReader 和 ClassWriter 修改原先的實現類原始碼

III、CGLib (Code Generation Library)
因為 asm可讀性較差, 產生了更易用的框架 cglib,其底層使用的是 asm 介面;這個技術被很多AOP框架使用,例如
Spring AOP 和dynaop ,為她們提供方法的 interception(攔截)
hibernate使用 cglib 來代理單端 single-ended(多對一和一對一)關聯
EasyMoc和jMock通過使用moke 物件來測試java程式碼包

a、Enhancer 類似於 動態代理的 Proxy 提供代理的物件
b、MethodInterceptor 類似於 動態代理的 InvocationHandler 提供攔截邏輯

對應動態管理的例子, 就是類似如下程式碼
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(AOPObject.class);
enhancer.setCallback(new AOPProxy());
AOPObject aop = (AOPObject)enhancer.create();
aop.aop();

真實的介面和架構要比者複雜很多,最基本呼叫就是如此

IV、javasist
這是另外一種位元組碼生成技術, 通過java 文字方式, 來實現位元組碼的替換;不像 asm 是位元組層面的替換, javasist 封裝了底層細節, 直接把java 程式碼文字翻譯替換。

-- 一個經典的列印執行時間的例子
CtClass clas = ClassPool.getDefault().get("StringBuilder");

CtMethod method = cls.getDeclaredMethod(methodName);
String replaceMethodName = methodName + "$impl";
method.setName(replaceMethodName);
CtMethod oldMethod = CtNewMethod.copy(method, methodName, cls, null);

String type = method.getReturnType().getName();
StringBuffer body = new StringBuffer();
body.append("{\nlong start = System.currentTimeMillis();\n");
if (!"void".equals(type)) {
body.append(type + " result = ");
}
body.append(replaceMethodName + "($$);\n"); // 第一個$符號是關鍵字,第二個是引數

body.append("System.out.println(\"Call to method " + methodName
+ " took \" +\n (System.currentTimeMillis()-start) + "
+ "\" ms.\");\n");
if (!"void".equals(type)) {
body.append("return result;\n");
}
body.append("}");

oldMethod.setBody(body.toString());
cls.addMethod(oldMethod);

System.out.println("Interceptor method body:");
System.out.println(body.toString());

V、Bcel/Serl
還有很多動態位元組碼技術, 不做介紹, 有需要的可以去了解一下

4、Spring AOP

有了動態代理 + 動態位元組碼增強 技術, AOP 攔截的實現技術已經完備。
是時候介紹一下 AOP的一些概念了。

I、aopalliance -- AOP聯盟的API介面規範
通知(Advice): 何時(Before,After,Around,After還有幾個變種) 做什麼
連線點(JoinPoint): 應用物件提供可以切入的所有功能(一般是方法,有時也是引數)
切點(PointCut): 通過指定,比如指定名稱,正則表示式過濾, 指定某個/些連線點, 切點描繪了 在何地 做
切面(Aspect): 通知 + 切點 何時何地做什麼
引入(Introduction):向現有類新增新的屬性或方法
織入(Weaving): 就是將切面應用到目標物件的過程

-- aopalliance 包含了一些常用的介面, 實現 AOP 功能的框架, 一般都會實現這些介面

II、Spring AOP

需要有依賴的, 繼承自 Spring AOP 體系的用法

擴充套件以下介面
org.aopalliance.intercept.MethodInterceptor -- 標準AOP介面,可以對方法做任意增強
org.springframework.aop.MethodBeforeAdvice -- 繼承自標準介面Advice,對方法做前增強
org.springframework.aop.AfterReturningAdvice -- 繼承自標準介面Advice,對方法做後增強
org.springframework.aop.ThrowsAdvice -- 繼承自標準介面Advice,對方法做異常增強,該介面是一個空方法介面,約定方法是 afterThrowing ,引數是任意定義的異常。

可以自己編寫程式碼, 編碼式來實現 AOP 的呼叫過程。

a、手工代理 - FactoryBean 適配 代理 方式
org.springframework.aop.framework.ProxyFactoryBean
<bean id="businessObject" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="target">
<ref local="xxx"/>
</property>
<property name="proxyInterfaces">
<value>xxx</value>
</property>
<property name="interceptorNames">
<list>
<value>beforeAdive</value>
<value>afterAdive</value>
</list>
</property>
</bean>

b、自動代理
<bean id="autoProxy" class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"/>
<bean id="pointcutAdvisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
<property name="advice">
<ref local="pointcutBeforeAdive"/>
</property>
<property name="patterns">
<list>
<value>.*add.*</value> -- 只匹配名稱中含有add方法的
</list>
</property>
</bean>
Spring自動去搜索攔截

利用Aspect , 不需要繼承 Spring AOP 體系, 普通類POJO 能實現的攔截, 非註解方式下, 實現完全解耦的用法(AspectJ + Spring AOP)

c、基於Aspect 註解
配置 <aop:aspectj-autoproxy />,目的是啟用Spring的 AnnotationAwareAspectJAutoProxyCreator 類,由該類來掃描註解,把代理的通知和切入點等等載入,(代替上下文載入載入),實現代理的過程
每個通知的 實現類,通過 Aspecj 的標註,實現 切入點定義 和 方法
@Aspect
public class POJOAdvisor {
// 定義切入點,此處 doActionPoint 即切點的名字,是一個空方法 主要目的給Pointcut註解使用,不然Poingcut註解無法使用
@Pointcut("execution(* com.aop.annotation.AopInterface.doAction(..)) ")
public void doActionPoint(){

}

@Before("doActionPoint()")
public void beforePointCut(){
System.out.println(getClass().getName() + ":beforePointCut() by annotation");
}

@After("doActionPoint()")
public void afterPointCut(){
System.out.println(getClass().getName() + ":afterPointCut() by annotation");
}

d、基於Aspect 配置 - 此方式為最常用方式
<bean id="pointcutBeforeAdive" class="com.aop.POJOAdvisor"/>
<aop:config>
<aop:aspect ref="pointcutBeforeAdive">
<!-- within 可以指定包下的類,bean 可以指定具體類 -->
<aop:before pointcut="execution(* com.aop.AOP.aop(..))" method="beforePointCut"/>
</aop:aspect>
</aop:config >

III、ApsectJ 增強功能
Aspect 不僅能攔截方法, 還能擴充方法和功能

<!-- 依賴 asectj.jar aspectjwear.jar -->
<aop:config>
<aop:aspect ref="pojoadvisor">
<!-- within 可以指定包下的類,bean 可以指定具體類 -->
<aop:before pointcut="execution(* com.aop.AopInterface.doAction(..)) and within(com.aop.*)" method="beforePointCut"/>
<!-- after-returning/after-throwing -->
<aop:after pointcut="execution(* com.aop.AopInterface.doAction(..)) and bean(targetaop)" method="afterPointCut"/>
</aop:aspect>
<!-- 如果pointcut 是一樣的,可以抽取出來單獨定義 -->

<aop:aspect ref="pojoadvisor">
<aop:pointcut id="doActionPoint" expression="execution(* com.aop.AopInterface.doAction(..))"/>
<aop:before pointcut-ref="doActionPoint" method="beforePointCut"/>
<aop:after pointcut-ref="doActionPoint" method="afterPointCut"/>
</aop:aspect>
<!-- 環繞相對於普通 JAVA物件而言,多了一個 ProceedingJoinPoint 的傳入引數,依賴了AspectJ 功能 -->

<aop:aspect ref="aroundAdvisor">
<aop:around pointcut-ref="doActionPoint" method="around"/>
</aop:aspect>

<!-- 給通知 傳遞引數, 主要用在 可以檢查傳入的引數,被攔截的方法呼叫中,傳入的引數是否合法 -->
<aop:aspect ref="intercept">
<aop:pointcut id="action" expression="execution(* com.aop.AopParaInterface.doAction(..)) and args(msg)"/>
<aop:before pointcut-ref="action" method="intercept" arg-names="msg"/>
</aop:aspect>

<!-- 利用攔截功能,為物件增加新的功能,其實就是動態代理,在invoke時,發現是註冊的新方法,改呼叫新方法 -->
<aop:aspect>
<aop:declare-parents
types-matching="舊的物件+"
implement-interface="新增介面"
default-impl="委託物件"
/>

</aop:aspect>

</aop:config>

Aspect 攔截 語法
execution(* 方法路徑(..))
within( 指定方法)
args 傳入引數

AOP的概念和知識點很多, 只要能理解其用途, 無非就是兩個部分
需要攔截哪些模組 -- 定位 (從包-類-方法, Spring最小粒度就到方法)
增加哪些功能 -- 增加 (普通POJO類、或者繼承擴充套件Spring AOP攔截器)