0.前言
本文主要想闡述的問題如下:
- 什么動態(tài)代理(AOP)以及如何用JDK的Proxy和InvocationHandler實(shí)現(xiàn)自己的代理尾抑?
- 什么是Spring動態(tài)代理(AOP)斗搞?
- Spring AOP注解實(shí)現(xiàn)
1.動態(tài)代理(AOP)
1.1 AOP
- 什么是AOP辞色?
AOP(Aspect Oriented Programming)刑赶,即面向切面編程箕憾,可以說是OOP(Object Oriented Programming袍辞,面向?qū)ο缶幊蹋┑难a(bǔ)充和完善浆兰。- 為什么需要用AOP颓哮?
OOP允許開發(fā)者定義縱向的關(guān)系家妆,但并不適合定義橫向的關(guān)系,例如日志功能冕茅。日志代碼往往橫向地散布在所有對象層次中伤极,而與它對應(yīng)的對象的核心功能毫無關(guān)系,在OOP設(shè)計(jì)中嵌赠,它導(dǎo)致了大量代碼的重復(fù)塑荒,而不利于各個模塊的重用。
AOP技術(shù)利用一種稱為"橫切"的技術(shù)姜挺,剖解開封裝的對象內(nèi)部齿税,并將那些影響了多個類的公共行為封裝到一個可重用模塊,并將其命名為"Aspect"炊豪,即切面凌箕。- 什么是切面(Aspect)?
所謂"切面"词渤,簡單說就是那些與業(yè)務(wù)無關(guān)牵舱,卻為業(yè)務(wù)模塊所共同調(diào)用的邏輯或責(zé)任封裝起來,便于減少系統(tǒng)的重復(fù)代碼缺虐,降低模塊之間的耦合度芜壁,并有利于未來的可操作性和可維護(hù)性。- 使用切面(Aspect)技術(shù)有什么好處高氮?
使用"橫切"技術(shù)慧妄,AOP把軟件系統(tǒng)分為兩個部分:核心關(guān)注點(diǎn)和橫切關(guān)注點(diǎn)。業(yè)務(wù)處理的主要流程是核心關(guān)注點(diǎn)剪芍,與之關(guān)系不大的部分是橫切關(guān)注點(diǎn)塞淹。橫切關(guān)注點(diǎn)的一個特點(diǎn)是,他們經(jīng)常發(fā)生在核心關(guān)注點(diǎn)的多處罪裹,而各處基本相似饱普,比如權(quán)限認(rèn)證、日志状共、事物套耕。AOP的作用在于分離系統(tǒng)中的各種關(guān)注點(diǎn),將核心關(guān)注點(diǎn)和橫切關(guān)注點(diǎn)分離開來峡继。
1.2 代理模式
代理模式是AOP的基礎(chǔ)箍铲,也是常用的java設(shè)計(jì)模式,他的特征是代理類與委托類有同樣的接口鬓椭,代理類主要負(fù)責(zé)為委托類預(yù)處理消息颠猴、過濾消息关划、把消息轉(zhuǎn)發(fā)給委托類,以及事后處理消息等翘瓮。
使用代理模式必須要讓代理類和目標(biāo)類實(shí)現(xiàn)相同的接口贮折,客戶端通過代理類來調(diào)用目標(biāo)方法,代理類會將所有的方法調(diào)用分派到目標(biāo)對象上反射執(zhí)行资盅,還可以在分派過程中添加"前置通知"和后置處理(如在調(diào)用目標(biāo)方法前校驗(yàn)權(quán)限调榄,在調(diào)用完目標(biāo)方法后打印日志等)等功能。
如上圖所示:
1.委托對象和代理對象都共同實(shí)現(xiàn)的了同一個接口呵扛。
2.委托對象中存在的方法在代理對象中也同樣存在每庆。代理模式分為兩種:
- 靜態(tài)代理:代理類是在編譯時就實(shí)現(xiàn)好的。也就是說 Java 編譯完成后代理類是一個實(shí)際的 class 文件今穿。
- 動態(tài)代理:代理類是在運(yùn)行時生成的缤灵,也就是說 Java 編譯完之后并沒有實(shí)際的 class 文件,而是在運(yùn)行時動態(tài)生成的類字節(jié)碼蓝晒,并加載到JVM中腮出。
1.2 靜態(tài)代理實(shí)現(xiàn)
//客戶端 public class Client { public static void main(String args[]) { Target subject = new Target(); Proxy p = new Proxy(subject); p.request(); } } //委托對象和代理對象都共同實(shí)現(xiàn)的接口 interface Interface { void request(); } //委托類 class Target implements Interface { public void request() { System.out.println("request"); } } //代理類 class Proxy implements Interface { private Interface subject; public Proxy(Interface subject) { this.subject = subject; } public void request() { System.out.println("PreProcess"); subject.request(); System.out.println("PostProcess"); } }
1.3 Java 實(shí)現(xiàn)動態(tài)代理
Java實(shí)現(xiàn)動態(tài)代理的大致步驟如下:
- 定義一個委托類和公共接口
//公共接口 public interface IHello { void sayHello(); } //委托類 class Hello implements IHello { public void sayHello() { System.out.println("Hello world!!"); } }
- 通過實(shí)現(xiàn)InvocationHandler接口來自定義自己的InvocationHandler,指定運(yùn)行時將生成的代理類需要完成的具體任務(wù)
//自定義InvocationHandler public class HWInvocationHandler implements InvocationHandler { // 目標(biāo)對象 private Object target; public HWInvocationHandler(Object target) { this.target = target; } public Object invoke(Object proxy, Method method, Object[] args) >throws Throwable { System.out.println("------插入前置通知代碼-------------"); // 執(zhí)行相應(yīng)的目標(biāo)方法 Object rs = method.invoke(target, args); System.out.println("------插入后置處理代碼-------------"); return rs; } }
- 生成代理對象芝薇,這個可以分為四步:
(1)通過Proxy.getProxyClass獲得動態(tài)代理類
(2)通過反射機(jī)制獲得代理類的構(gòu)造方法胚嘲,方法簽名為getConstructor(InvocationHandler.class)
(3)通過構(gòu)造函數(shù)獲得代理對象并將自定義的InvocationHandler實(shí)例對象傳為參數(shù)傳入
(4)通過代理對象調(diào)用目標(biāo)方法public class Client { public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, >InvocationTargetException, InstantiationException { // 生成Proxy的class文件 System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true"); // 獲取動態(tài)代理類 Class<?> proxyClazz = Proxy.getProxyClass(IHello.class.getClassLoader(), IHello.class); // 獲得代理類的構(gòu)造函數(shù),并傳入?yún)?shù)類型InvocationHandler.class Constructor<?> constructor = proxyClazz.getConstructor(InvocationHandler.class); // 通過構(gòu)造函數(shù)來創(chuàng)建動態(tài)代理對象洛二,將自定義的InvocationHandler實(shí)例傳入 IHello iHello = (IHello) constructor.newInstance(new HWInvocationHandler(new Hello())); // 通過代理對象調(diào)用目標(biāo)方法 iHello.sayHello(); } }
Proxy類中還有個將2~4步驟封裝好的簡便方法來創(chuàng)建動態(tài)代理對象馋劈,其方法簽名為:newProxyInstance(ClassLoader loader,Class<?>[] instance, InvocationHandler h),如下例:
public class Client2 { public static void main(String[] args) throws NoSuchMethodException, >IllegalAccessException, InvocationTargetException, InstantiationException { //生成$Proxy0的class文件 System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true"); IHello ihello = (IHello) >Proxy.newProxyInstance(IHello.class.getClassLoader(), //加載接口的類加載器 new Class[]{IHello.class}, //一組接口 new HWInvocationHandler(new Hello())); //自定義的>InvocationHandler ihello.sayHello(); } }
這個靜態(tài)函數(shù)的第一個參數(shù)是類加載器對象(即哪個類加載器來加載這個代理類到 JVM 的方法區(qū))晾嘶,第二個參數(shù)是接口(表明你這個代理類需要實(shí)現(xiàn)哪些接口)侣滩,第三個參數(shù)是調(diào)用處理器類實(shí)例(指定代理類中具體要干什么)
以上就是對代理類如何生成,代理類方法如何被調(diào)用的分析变擒!在很多框架都使用了動態(tài)代理如Spring,HDFS的RPC調(diào)用等等。
2.Spring動態(tài)代理
2.1 Spring AOP實(shí)現(xiàn)的原理
Spring中AOP代理由Spring的IOC容器負(fù)責(zé)生成寝志、管理娇斑,其依賴關(guān)系也由IOC容器負(fù)責(zé)管理。因此材部,AOP代理可以直接使用容器中的其它bean實(shí)例作為目標(biāo)毫缆,這種關(guān)系可由IOC容器的依賴注入提供。Spirng的AOP的動態(tài)代理實(shí)現(xiàn)機(jī)制有兩種乐导,分別是:
- JDK動態(tài)代理:JDK動態(tài)代理是利用反射機(jī)制生成一個實(shí)現(xiàn)代理接口的匿名類苦丁,在調(diào)用具體方法前調(diào)用InvokeHandler來處理。這個在之前已經(jīng)介紹過了物臂。
- CGLib動態(tài)代理:cglib動態(tài)代理是利用asm開源包旺拉,對代理對象類的class文件加載進(jìn)來产上,通過修改其字節(jié)碼生成子類來處理。
2.2 如何選擇的使用代理機(jī)制
- 如果目標(biāo)對象實(shí)現(xiàn)了接口蛾狗,默認(rèn)情況下會采用JDK的動態(tài)代理實(shí)現(xiàn)AOP
- 如果目標(biāo)對象實(shí)現(xiàn)了接口晋涣,可以強(qiáng)制使用CGLIB實(shí)現(xiàn)AOP
- 如果目標(biāo)對象沒有實(shí)現(xiàn)了接口,必須采用CGLIB庫沉桌,spring會自動在JDK動態(tài)代理和CGLIB之間轉(zhuǎn)換
2.3 AOP基本概念
在寫Spring AOP之前先簡單介紹下幾個概念:
- 切面(Aspect) :通知和切入點(diǎn)共同組成了切面谢鹊,時間、地點(diǎn)和要發(fā)生的“故事”留凭。
- 連接點(diǎn)(Joinpoint) :程序能夠應(yīng)用通知的一個“時機(jī)”佃扼,這些“時機(jī)”就是連接點(diǎn),例如方法被調(diào)用時蔼夜、異常被拋出時等等兼耀。
- 通知(Advice) :通知定義了切面是什么以及何時使用。描述了切面要完成的工作和何時需要執(zhí)行這個工作挎扰。
- 切入點(diǎn)(Pointcut) :通知定義了切面要發(fā)生的“故事”和時間翠订,那么切入點(diǎn)就定義了“故事”發(fā)生的地點(diǎn),例如某個類或方法的名稱遵倦。
- 目標(biāo)對象(Target Object) :即被通知的對象尽超。
- 織入(Weaving):把切面應(yīng)用到目標(biāo)對象來創(chuàng)建新的代理對象的過程,織入一般發(fā)生在如下幾個時機(jī):
1)編譯時:當(dāng)一個類文件被編譯時進(jìn)行織入梧躺,這需要特殊的編譯器才能做到似谁,例如AspectJ的織入編譯器;
2)類加載時:使用特殊的ClassLoader在目標(biāo)類被加載到程序之前增強(qiáng)類的字節(jié)代碼掠哥;
3)運(yùn)行時:切面在運(yùn)行的某個時刻被織入巩踏,SpringAOP就是以這種方式織入切面的,原理是使用了JDK的動態(tài)代理续搀。AOP通知類型:
- @Before 前置通知(Before advice) :在某連接點(diǎn)(JoinPoint)之前執(zhí)行的通知塞琼,但這個通知不能阻止連接點(diǎn)前的執(zhí)行。
- @After 后通知(After advice) :當(dāng)某連接點(diǎn)退出的時候執(zhí)行的通知(不論是正常返回還是異常退出)禁舷。
- @AfterReturning 返回后通知(After return advice) :在某連接點(diǎn)正常完成后執(zhí)行的通知彪杉,不包括拋出異常的情況。
- @Around 環(huán)繞通知(Around advice) :包圍一個連接點(diǎn)的通知牵咙,類似Web中Servlet規(guī)范中的Filter的doFilter方法派近。可以在方法的調(diào)用前后完成自定義的行為洁桌,也可以選擇不執(zhí)行渴丸。
- @AfterThrowing 拋出異常后通知(After throwing advice) : 在方法拋出異常退出時執(zhí)行的通知。
3.Spring AOP注解實(shí)現(xiàn)
對于AOP編程,我們只需要做三件事:
- 定義普通業(yè)務(wù)組件
- 定義切入點(diǎn)谱轨,一個切入點(diǎn)可能橫切多個業(yè)務(wù)組件
- 定義增強(qiáng)處理戒幔,增強(qiáng)處理就是在AOP框架為普通業(yè)務(wù)組件織入的處理動作
首先我們定義一個接口:它只完成增加用戶的功能。
public interface UserDao { public void add(User user); }
其次碟嘴,我們定義一個接口實(shí)現(xiàn)類:它實(shí)現(xiàn)了用戶的添加功能溪食。
@Component("u") public class UserDaoImpl implements UserDao { @Override public void add(User user) { System.out.println("add user!"); } }
然后,定義一個service類娜扇,他會調(diào)用UserDao的add方法
@Component public class UserService { private UserDao userDao; public void add(User user) { userDao.add(user); } public UserDao getUserDao() { return userDao; } @Resource(name = "u") public void setUserDao(UserDao userDao) { this.userDao = userDao; } }
定義一下橫切關(guān)注點(diǎn)的類:我們這里列舉了各種情況错沃,在方法執(zhí)行之前,之后雀瓢,成功等等情況都有涉及
@Aspect @Component public class LogInterceptor { // @Pointcut("execution(public * com.syf.dao.impl..*.*(..))") @Pointcut("execution(public * com.syf.service..*.add(..))") public void myMethod() { }; // @Before("execution(public void // com.syf.dao.impl.UserDaoImpl.add(com.syf.model.User))") // @Before("execution(public * com.syf.dao.impl..*.*(..))") @Before("myMethod()") public void before() { System.out.println("method start"); } // @After("execution(public * com.syf.dao.impl..*.*(..))") @After("myMethod()") public void after() { System.out.println("method end"); } // @AfterReturning("execution(public * com.syf.dao.impl..*.*(..))") @AfterReturning("myMethod()") public void afterReturning() { System.out.println("method after returning"); } @Around("myMethod()") public void aroundMethod(ProceedingJoinPoint pjp) throws Throwable { System.out.println("around start method"); pjp.proceed(); System.out.println("around end method"); } }
Spring 的配置文件如下枢析。通過aop命名空間的<aop:aspectj-autoproxy />聲明自動為spring容器中那些配置@aspectJ切面的bean創(chuàng)建代理,織入切面
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:context="http://www.springframework.org/schema/context" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd"> <context:annotation-config></context:annotation-config> <context:component-scan base-package="com.syf">></context:component-scan> <aop:aspectj-autoproxy /> </beans>
編寫測試類對其進(jìn)行測試:
public class UserServiceTest { @Test public void testAdd() throws Exception{ @SuppressWarnings("resource") ApplicationContext applicationContext = new ClassPathXmlApplicationContext("beans.xml"); UserService svc = (UserService) applicationContext.getBean("userService"); User u = new User(); u.setId(1); u.setName("name"); svc.add(u); } }
打印出的log證明刃麸,在add方法執(zhí)行前后等情況下醒叁,切面均有被織入,Spring
AOP代理實(shí)現(xiàn)成功:around start method method start add user! //add 方法實(shí)現(xiàn)的內(nèi)容 around end method method end method after returning
所以進(jìn)行AOP編程的關(guān)鍵就是定義切入點(diǎn)和定義增強(qiáng)處理泊业,一旦定義了合適的切入點(diǎn)和增強(qiáng)處理把沼,AOP框架將自動生成AOP代理,即:代理對象的方法=增強(qiáng)處理+被代理對象的方法吁伺。
4.代碼
本文中所涉及的代碼在github上都有饮睬,可以點(diǎn)擊以下鏈接:
GIthub地址