AOP與JAVA動態代理
AOP與JAVA動態代理
1、AOP的各種實現
AOP就是面向切面程式設計,我們可以從以下幾個層面來實現AOP
在編譯期修改原始碼
在執行期位元組碼載入前修改位元組碼
在執行期位元組碼載入後動態建立代理類的位元組碼
2、AOP各種實現機制的比較
以下是各種實現機制的比較:
3、AOP裡的公民
Joinpoint:攔截點,如某個業務方法
Pointcut:Joinpoint的表示式,表示攔截哪些方法。一個Pointcut對應多個Joinpoint
Advice:要切入的邏輯
Before Advice:在方法前切入
After Advice:在方法後切入,丟擲異常則不會切入
After Returning Advice:在方法返回後切入,丟擲異常則不會切入
After Throwing Advice:在方法丟擲異常時切入
Around Advice:在方法執行前後切入,可以中斷或忽略原有流程的執行
公民之間的關係
織入器通過在切面中定義pointcout來搜尋目標(被代理類)的JoinPoint(切入點),然後把要切入的邏輯(Advice)織入到目標物件裡,生成代理類
4、AOP的實現機制
動態代理
動態位元組碼生成
自定義類載入器
位元組碼轉換
4.1 動態代理
靜態代理:由程式設計師建立或特定工具自動生成原始碼,再對其編譯。在程式執行前,代理類的.class檔案就已經存在了
動態代理:即在執行期動態建立代理類,使用動態代理實現AOP需要4個角色:
被代理的類:即AOP裡所說的目標物件
被代理類的介面
織入器:使用介面反射機制生成一個代理類,在這個代理類中織入程式碼
InvocationHandler切面:切面,包含了Advice和Pointcut
4.1.1 動態代理的演示
例子演示的是在方法執行前織入一段記錄日誌的程式碼,其中
Business是代理類
LogInvocationHandler是記錄日誌的切面
IBusiness、IBusiness2是代理類的介面
Proxy.newProxyInstance是織入器
public interface IBusiness {
void doSomeThing();
}
public interface IBusiness2 {
void doSomeThing2();
}
public class Business implements IBusiness , IBusiness2 {
@Override
public void doSomeThing() {
System.out.println("執行業務邏輯");
}
@Override
public void doSomeThing2() {
System.out.println("執行業務邏輯2");
}
}
package aop;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
/**
* 列印日誌的切面
*/
public class LogInvocationHandler implements InvocationHandler {
private Object target;//目標物件
public LogInvocationHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//執行織入的日誌,你可以控制哪些方法執行切入邏輯
if (method.getName().equals("doSomeThing2")) {
System.out.println("記錄日誌");
}
//執行原有邏輯
Object recv = method.invoke(target, args);
return recv;
}
}
package aop;
import java.lang.reflect.Proxy;
public class Main {
public static void main(String[] args) {
//需要代理的類介面,被代理類實現的多個介面都必須在這這裡定義
Class[] proxyInterface = new Class[] {IBusiness.class, IBusiness2.class};
//構建AOP的Advice,這裡需要傳入業務類的例項
LogInvocationHandler handler = new LogInvocationHandler(new Business());
//生成代理類的位元組碼載入器
ClassLoader classLoader = Business.class.getClassLoader();
//織入器,織入程式碼並生成代理類
IBusiness2 proxyBusiness = (IBusiness2) Proxy.newProxyInstance(classLoader, proxyInterface, handler);
proxyBusiness.doSomeThing2();
((IBusiness)proxyBusiness).doSomeThing();
}
}
執行結果:
記錄日誌
執行業務邏輯2
執行業務邏輯
4.1.2 動態代理的原理
本節將結合動態代理的原始碼講解其實現原理
動態代理的核心其實就是代理物件的生成,即Proxy.newProxyInstance(classLoader, proxyInterface, handler)
讓我們進入newProxyInstance方法觀摩下,核心程式碼就三行:
//獲取代理類
Class cl = getProxyClass(loader, interfaces);
//獲取帶有InvocationHandler引數的構造方法
Constructor cons = cl.getConstructor(constructorParams);
//把handler傳入構造方法生成例項
return (Object) cons.newInstance(new Object[] { h });
getProxyClass(loader, interfaces)方法用於獲取代理類,它主要做了三件事情:
在當前類載入器的快取裡搜尋是否有代理類
沒有則生成代理
並快取在本地JVM裡
查詢代理類getProxyClass(loader, interfaces)方法:
1 // 快取的key使用介面名稱生成的List
2 Object key = Arrays.asList(interfaceNames);
3 synchronized (cache) {
4 do {
5 Object value = cache.get(key);
6 // 快取裡儲存了代理類的引用
7 if (value instanceof Reference) {
8 proxyClass = (Class) ((Reference) value).get();
9 }
10 if (proxyClass != null) {
11 // 代理類已經存在則返回
12 return proxyClass;
13 } else if (value == pendingGenerationMarker) {
14 // 如果代理類正在產生,則等待
15 try {
16 cache.wait();
17 } catch (InterruptedException e) {
18 }
19 continue;
20 } else {
21 //沒有代理類,則標記代理準備生成
22 cache.put(key, pendingGenerationMarker);
23 break;
24 }
25 } while (true);
26 }
生成載入代理類:
//生成代理類的位元組碼檔案並儲存到硬碟中(預設不儲存到硬碟)
proxyClassFile = ProxyGenerator.generateProxyClass(proxyName, interfaces);
//使用類載入器將位元組碼載入到記憶體中
proxyClass = defineClass0(loader, proxyName,proxyClassFile, 0, proxyClassFile.length);
代理類生成過程ProxyGenerator.generateProxyClass()方法的核心程式碼分析:
//新增介面中定義的方法,此時方法體為空
for (int i = 0; i < this.interfaces.length; i++) {
localObject1 = this.interfaces[i].getMethods();
for (int k = 0; k < localObject1.length; k++) {
addProxyMethod(localObject1[k], this.interfaces[i]);
}
}
//新增一個帶有InvocationHandler的構造方法
MethodInfo localMethodInfo = new MethodInfo("<init>", "(Ljava/lang/reflect/InvocationHandler;)V", 1);
//迴圈生成方法體程式碼(省略)
//方法體裡生成呼叫InvocationHandler的invoke方法程式碼。(此處有所省略)
this.cp.getInterfaceMethodRef("InvocationHandler", "invoke", "Object; Method; Object;")
//將生成的位元組碼,寫入硬碟,前面有個if判斷,預設情況下不儲存到硬碟。
localFileOutputStream = new FileOutputStream(ProxyGenerator.access$000(this.val$name) + ".class");
localFileOutputStream.write(this.val$classFile);
通過以上分析,我們可以推出動態代理為我們生產了一個這樣的代理類。把方法soSomeThing的方法體修改為呼叫LogInvocationHandler的invoke方法
程式碼如下:
public class ProxyBusiness implements IBusiness, IBusiness2 {
private LogInvocationHandler h;
@Override
public void doSomeThing2() {
try {
Method m = (h.target).getClass().getMethod("doSomeThing", null);
h.invoke(this, m, null);
} catch (Throwable e) {
// 異常處理(略)
}
}
@Override
public boolean doSomeThing() {
try {
Method m = (h.target).getClass().getMethod("doSomeThing2", null);
return (Boolean) h.invoke(this, m, null);
} catch (Throwable e) {
// 異常處理(略)
}
return false;
}
public ProxyBusiness(LogInvocationHandler h) {
this.h = h;
}
//測試用
public static void main(String[] args) {
//構建AOP的Advice
LogInvocationHandler handler = new LogInvocationHandler(new Business());
new ProxyBusiness(handler).doSomeThing();
new ProxyBusiness(handler).doSomeThing2();
}
}
4.1.3 小結
從前兩節的分析我們可以看出,動態代理在執行期通過介面動態生成代理類,這為其帶來了一定的靈活性,但這個靈活性卻帶來了兩個問題:
第一,代理類必須實現一個介面,如果沒實現介面會丟擲一個異常
第二,效能影響,因為動態代理是使用反射機制實現的,首先反射肯定比直接呼叫要慢,其次使用反射大量生成類檔案可能引起full gc,因為位元組碼檔案載入後會存放在JVM執行時方法區(或者叫永久代、元空間)中,當方法區滿時會引起full gc,所以當你大量使用動態代理時,可以將永久代設定大一些,減少full gc的次數
4.2 CGLIB動態位元組碼生成
使用動態位元組碼生成技術實現AOP原理是在執行期間目標位元組碼載入後,生成目標類的子類,將切面邏輯加入到子類中,所以cglib實現AOP不需要基於介面
本節介紹如何使用cglib來實現動態位元組碼技術。
cglib是一個強大的、高效能的Code生成類庫,它可以在執行期間擴充套件Java類和實現Java介面,它封裝了Asm,所以使用cglib前需要引入Asm的jar
4.2.1 使用cglib實現AOP
1 package cglib;
2
3 /**
4 * 這個是沒有實現介面的實現類
5 */
6 public class BookFacadeImpl {
7 public void addBook() {
8 System.out.println("增加圖書的普通方法。。。");
9 }
10
11 public void deleteBook() {
12 System.out.println("刪除圖書的普通方法。。。");
13 }
14 }
1 package cglib;
2
3 import net.sf.cglib.proxy.Enhancer;
4 import net.sf.cglib.proxy.MethodInterceptor;
5 import net.sf.cglib.proxy.MethodProxy;
6
7 import java.lang.reflect.Method;
8
9 /**
10 * 使用cglib動態代理
11 */
12 public class BookFacadeCglib implements MethodInterceptor {
13
14 private Object target;
15
16 /**
17 * 建立代理物件
18 *
19 * @param target
20 * @return
21 */
22 public Object getInstance(Object target) {
23 this.target = target;
24 Enhancer enhancer = new Enhancer();
25 enhancer.setSuperclass(this.target.getClass());
26 //回撥方法
27 enhancer.setCallback(this);
28 //建立代理
29 return enhancer.create();
30 }
31
32 //回撥方法
33 @Override
34 public Object intercept(Object obj, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
35 if (method.getName().equals("addBook")) {
36 System.out.println("記錄增加圖書的日誌");
37 }
38 methodProxy.invokeSuper(obj, args);
39 return null;
40 }
41 }
package cglib;
/**
* 測試cglib位元組碼代理
*/
public class TestCglib {
public static void main(String[] args) {
BookFacadeCglib cglib = new BookFacadeCglib();
BookFacadeImpl bookFacade = (BookFacadeImpl) cglib.getInstance(new BookFacadeImpl());
bookFacade.addBook();
bookFacade.deleteBook();
}
}
執行結果:
記錄增加圖書的日誌
增加圖書的普通方法。。。
刪除圖書的普通方法。。。
4.3 自定義類載入器
如果我們實現了一個自定義類載入器,在類載入到JVM之前直接修改某些類的方法,並將切入邏輯織入到這個方法裡,然後將修改後的位元組碼檔案交給虛擬機器執行,那豈不是更直接
Javassist是一個編輯位元組碼的框架,可以讓你很簡單地操作位元組碼。它可以在執行期定義或修改Class。使用Javassist實現AOP的原理是在位元組碼載入前直接修改需要切入的方法
這比使用cglib實現AOP更加高效,並且沒有太多限制,實現原理如下圖:
我們使用類載入器啟動我們自定義的類載入器,在這個類載入器里加一個類載入監聽器,監聽器發現目標類被載入時就織入切入邏輯
4.3.1 Javassist實現AOP的程式碼
清單1:啟動自定義的類載入器
//獲取存放CtClass的容器ClassPool
ClassPool cp = ClassPool.getDefault();
//建立一個類載入器
Loader cl = new Loader();
//增加一個轉換器
cl.addTranslator(cp, new MyTranslator());
//啟動MyTranslator的main函式
cl.run("jsvassist.JavassistAopDemo$MyTranslator", args);
清單2:類載入監聽器
public static class MyTranslator implements Translator {
public void start(ClassPool pool) throws NotFoundException, CannotCompileException {
}
/* *
* 類裝載到JVM前進行程式碼織入
*/
public void onLoad(ClassPool pool, String classname) {
if (!"model$Business".equals(classname)) {
return;
}
//通過獲取類檔案
try {
CtClass cc = pool.get(classname);
//獲得指定方法名的方法
CtMethod m = cc.getDeclaredMethod("doSomeThing");
//在方法執行前插入程式碼
m.insertBefore("{ System.out.println(\"記錄日誌\"); }");
} catch (NotFoundException e) {
} catch (CannotCompileException e) {
}
}
public static void main(String[] args) {
Business b = new Business();
b.doSomeThing2();
b.doSomeThing();
}
}
輸出:
執行業務邏輯2
記錄日誌
執行業務邏輯
4.3.2 小結
從本節中可知,使用自定義的類載入器實現AOP在效能上有優於動態代理和cglib,因為它不會產生新類,但是它仍人存在一個問題,就是如果其他的類載入器來載入類的話,這些類就不會被攔截
4.4 位元組碼轉換
自定義類載入器實現AOP只能攔截自己載入的位元組碼,那麼有一種方式能夠監控所有類載入器載入的位元組碼嗎?
有,使用Instrumentation,它是Java5的新特性,使用Instrument,開發者可以構建一個位元組碼轉換器,在位元組碼載入前進行轉換
本節使用Instrumentation和javassist來實現AOP
4.4.1 構建位元組碼轉換器
首先需要建立位元組碼轉換器,該轉換器負責攔截Business類,並在Business類的doSomeThing方法前使用javassist加入記錄日誌的程式碼
1 public class MyClassFileTransformer implements ClassFileTransformer {
2
3 /**
4 * 位元組碼載入到虛擬機器前會進入這個方法
5 */
6 @Override
7 public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
8 ProtectionDomain protectionDomain, byte[] classfileBuffer)
9 throws IllegalClassFormatException {
10 System.out.println(className);
11 //如果載入Business類才攔截
12 if (!"model/Business".equals(className)) {
13 return null;
14 }
15
16 //javassist的包名是用點分割的,需要轉換下
17 if (className.indexOf("/") != -1) {
18 className = className.replaceAll("/", ".");
19 }
20 try {
21 //通過包名獲取類檔案
22 CtClass cc = ClassPool.getDefault().get(className);
23 //獲得指定方法名的方法
24 CtMethod m = cc.getDeclaredMethod("doSomeThing");
25 //在方法執行前插入程式碼
26 m.insertBefore("{ System.out.println(\"記錄日誌\"); }");
27 return cc.toBytecode();
28 } catch (NotFoundException e) {
29 } catch (CannotCompileException e) {
30 } catch (IOException e) {
31 //忽略異常處理
32 }
33 return null;
34 }
4.4.2 註冊轉換器
使用premain函式註冊位元組碼轉換器,該方法在main函式之前執行
public class MyClassFileTransformer implements ClassFileTransformer {
public static void premain(String options, Instrumentation ins) {
//註冊我自己的位元組碼轉換器
ins.addTransformer(new MyClassFileTransformer());
}
}
4.4.3 配置和執行
需要告訴JVM在啟動main函式之前,需要先執行premain函式。
首先,需要將premain函式所在的類打成jar包,並修改jar包裡的META-INF\MANIFEST.MF檔案
1 Manifest-Version: 1.0
2 Premain-Class: bci. MyClassFileTransformer
其次,在JVM的啟動引數里加上-javaagent:D:\java\projects\opencometProject\Aop\lib\aop.jar
4.4.4 輸出
執行main函式,你會發現切入的程式碼無侵入性的織入進去了
1 public static void main(String[] args) {
2 new Business().