0.前言
本文主要想闡述的問(wèn)題如下:
- 什么動(dòng)態(tài)代理(AOP)以及如何用JDK的Proxy和InvocationHandler實(shí)現(xiàn)自己的代理?
- 什么是Spring動(dòng)態(tài)代理(AOP)?
- Spring AOP注解實(shí)現(xiàn)
1.動(dòng)態(tài)代理(AOP)
1.1 AOP
- 什么是AOP?
AOP(Aspect Oriented Programming),即面向切面編程,可以說(shuō)是OOP(Object Oriented Programming,面向?qū)ο缶幊蹋┑难a(bǔ)充和完善。- 為什么需要用AOP?
OOP允許開發(fā)者定義縱向的關(guān)系,但并不適合定義橫向的關(guān)系,例如日志功能。日志代碼往往橫向地散布在所有對(duì)象層次中,而與它對(duì)應(yīng)的對(duì)象的核心功能毫無(wú)關(guān)系,在OOP設(shè)計(jì)中,它導(dǎo)致了大量代碼的重復(fù),而不利于各個(gè)模塊的重用。
AOP技術(shù)利用一種稱為"橫切"的技術(shù),剖解開封裝的對(duì)象內(nèi)部,并將那些影響了多個(gè)類的公共行為封裝到一個(gè)可重用模塊,并將其命名為"Aspect",即切面。- 什么是切面(Aspect)?
所謂"切面",簡(jiǎn)單說(shuō)就是那些與業(yè)務(wù)無(wú)關(guān),卻為業(yè)務(wù)模塊所共同調(diào)用的邏輯或責(zé)任封裝起來(lái),便于減少系統(tǒng)的重復(fù)代碼,降低模塊之間的耦合度,并有利于未來(lái)的可操作性和可維護(hù)性。- 使用切面(Aspect)技術(shù)有什么好處?
使用"橫切"技術(shù),AOP把軟件系統(tǒng)分為兩個(gè)部分:核心關(guān)注點(diǎn)和橫切關(guān)注點(diǎn)。業(yè)務(wù)處理的主要流程是核心關(guān)注點(diǎn),與之關(guān)系不大的部分是橫切關(guān)注點(diǎn)。橫切關(guān)注點(diǎn)的一個(gè)特點(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)分離開來(lái)。
1.2 代理模式
代理模式是AOP的基礎(chǔ),也是常用的java設(shè)計(jì)模式,他的特征是代理類與委托類有同樣的接口,代理類主要負(fù)責(zé)為委托類預(yù)處理消息、過(guò)濾消息、把消息轉(zhuǎn)發(fā)給委托類,以及事后處理消息等。
使用代理模式必須要讓代理類和目標(biāo)類實(shí)現(xiàn)相同的接口,客戶端通過(guò)代理類來(lái)調(diào)用目標(biāo)方法,代理類會(huì)將所有的方法調(diào)用分派到目標(biāo)對(duì)象上反射執(zhí)行,還可以在分派過(guò)程中添加"前置通知"和后置處理(如在調(diào)用目標(biāo)方法前校驗(yàn)權(quán)限,在調(diào)用完目標(biāo)方法后打印日志等)等功能。
代理模式
如上圖所示:
1.委托對(duì)象和代理對(duì)象都共同實(shí)現(xiàn)的了同一個(gè)接口。
2.委托對(duì)象中存在的方法在代理對(duì)象中也同樣存在。代理模式分為兩種:
- 靜態(tài)代理:代理類是在編譯時(shí)就實(shí)現(xiàn)好的。也就是說(shuō) Java 編譯完成后代理類是一個(gè)實(shí)際的 class 文件。
- 動(dòng)態(tài)代理:代理類是在運(yùn)行時(shí)生成的,也就是說(shuō) Java 編譯完之后并沒有實(shí)際的 class 文件,而是在運(yùn)行時(shí)動(dòng)態(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(); } } //委托對(duì)象和代理對(duì)象都共同實(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)動(dòng)態(tài)代理
Java實(shí)現(xiàn)動(dòng)態(tài)代理的大致步驟如下:
- 定義一個(gè)委托類和公共接口
//公共接口 public interface IHello { void sayHello(); } //委托類 class Hello implements IHello { public void sayHello() { System.out.println("Hello world!!"); } }
- 通過(guò)實(shí)現(xiàn)InvocationHandler接口來(lái)自定義自己的InvocationHandler,指定運(yùn)行時(shí)將生成的代理類需要完成的具體任務(wù)
//自定義InvocationHandler public class HWInvocationHandler implements InvocationHandler { // 目標(biāo)對(duì)象 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; } }
- 生成代理對(duì)象,這個(gè)可以分為四步:
(1)通過(guò)Proxy.getProxyClass獲得動(dòng)態(tài)代理類
(2)通過(guò)反射機(jī)制獲得代理類的構(gòu)造方法,方法簽名為getConstructor(InvocationHandler.class)
(3)通過(guò)構(gòu)造函數(shù)獲得代理對(duì)象并將自定義的InvocationHandler實(shí)例對(duì)象傳為參數(shù)傳入
(4)通過(guò)代理對(duì)象調(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"); // 獲取動(dòng)態(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); // 通過(guò)構(gòu)造函數(shù)來(lái)創(chuàng)建動(dòng)態(tài)代理對(duì)象,將自定義的InvocationHandler實(shí)例傳入 IHello iHello = (IHello) constructor.newInstance(new HWInvocationHandler(new Hello())); // 通過(guò)代理對(duì)象調(diào)用目標(biāo)方法 iHello.sayHello(); } }Proxy類中還有個(gè)將2~4步驟封裝好的簡(jiǎn)便方法來(lái)創(chuàng)建動(dòng)態(tài)代理對(duì)象,其方法簽名為: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(); } }這個(gè)靜態(tài)函數(shù)的第一個(gè)參數(shù)是類加載器對(duì)象(即哪個(gè)類加載器來(lái)加載這個(gè)代理類到 JVM 的方法區(qū)),第二個(gè)參數(shù)是接口(表明你這個(gè)代理類需要實(shí)現(xiàn)哪些接口),第三個(gè)參數(shù)是調(diào)用處理器類實(shí)例(指定代理類中具體要干什么)
以上就是對(duì)代理類如何生成,代理類方法如何被調(diào)用的分析!在很多框架都使用了動(dòng)態(tài)代理如Spring,HDFS的RPC調(diào)用等等。
2.Spring動(dòng)態(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的動(dòng)態(tài)代理實(shí)現(xiàn)機(jī)制有兩種,分別是:
- JDK動(dòng)態(tài)代理:JDK動(dòng)態(tài)代理是利用反射機(jī)制生成一個(gè)實(shí)現(xiàn)代理接口的匿名類,在調(diào)用具體方法前調(diào)用InvokeHandler來(lái)處理。這個(gè)在之前已經(jīng)介紹過(guò)了。
- CGLib動(dòng)態(tài)代理:cglib動(dòng)態(tài)代理是利用asm開源包,對(duì)代理對(duì)象類的class文件加載進(jìn)來(lái),通過(guò)修改其字節(jié)碼生成子類來(lái)處理。
2.2 如何選擇的使用代理機(jī)制
- 如果目標(biāo)對(duì)象實(shí)現(xiàn)了接口,默認(rèn)情況下會(huì)采用JDK的動(dòng)態(tài)代理實(shí)現(xiàn)AOP
- 如果目標(biāo)對(duì)象實(shí)現(xiàn)了接口,可以強(qiáng)制使用CGLIB實(shí)現(xiàn)AOP
- 如果目標(biāo)對(duì)象沒有實(shí)現(xiàn)了接口,必須采用CGLIB庫(kù),spring會(huì)自動(dòng)在JDK動(dòng)態(tài)代理和CGLIB之間轉(zhuǎn)換
2.3 AOP基本概念
在寫Spring AOP之前先簡(jiǎn)單介紹下幾個(gè)概念:
- 切面(Aspect) :通知和切入點(diǎn)共同組成了切面,時(shí)間、地點(diǎn)和要發(fā)生的“故事”。
- 連接點(diǎn)(Joinpoint) :程序能夠應(yīng)用通知的一個(gè)“時(shí)機(jī)”,這些“時(shí)機(jī)”就是連接點(diǎn),例如方法被調(diào)用時(shí)、異常被拋出時(shí)等等。
- 通知(Advice) :通知定義了切面是什么以及何時(shí)使用。描述了切面要完成的工作和何時(shí)需要執(zhí)行這個(gè)工作。
- 切入點(diǎn)(Pointcut) :通知定義了切面要發(fā)生的“故事”和時(shí)間,那么切入點(diǎn)就定義了“故事”發(fā)生的地點(diǎn),例如某個(gè)類或方法的名稱。
- 目標(biāo)對(duì)象(Target Object) :即被通知的對(duì)象。
- 織入(Weaving):把切面應(yīng)用到目標(biāo)對(duì)象來(lái)創(chuàng)建新的代理對(duì)象的過(guò)程,織入一般發(fā)生在如下幾個(gè)時(shí)機(jī):
1)編譯時(shí):當(dāng)一個(gè)類文件被編譯時(shí)進(jìn)行織入,這需要特殊的編譯器才能做到,例如AspectJ的織入編譯器;
2)類加載時(shí):使用特殊的ClassLoader在目標(biāo)類被加載到程序之前增強(qiáng)類的字節(jié)代碼;
3)運(yùn)行時(shí):切面在運(yùn)行的某個(gè)時(shí)刻被織入,SpringAOP就是以這種方式織入切面的,原理是使用了JDK的動(dòng)態(tài)代理。AOP通知類型:
- @Before 前置通知(Before advice) :在某連接點(diǎn)(JoinPoint)之前執(zhí)行的通知,但這個(gè)通知不能阻止連接點(diǎn)前的執(zhí)行。
- @After 后通知(After advice) :當(dāng)某連接點(diǎn)退出的時(shí)候執(zhí)行的通知(不論是正常返回還是異常退出)。
- @AfterReturning 返回后通知(After return advice) :在某連接點(diǎn)正常完成后執(zhí)行的通知,不包括拋出異常的情況。
- @Around 環(huán)繞通知(Around advice) :包圍一個(gè)連接點(diǎn)的通知,類似Web中Servlet規(guī)范中的Filter的doFilter方法??梢栽诜椒ǖ恼{(diào)用前后完成自定義的行為,也可以選擇不執(zhí)行。
- @AfterThrowing 拋出異常后通知(After throwing advice) : 在方法拋出異常退出時(shí)執(zhí)行的通知。
3.Spring AOP注解實(shí)現(xiàn)
對(duì)于AOP編程,我們只需要做三件事:
- 定義普通業(yè)務(wù)組件
- 定義切入點(diǎn),一個(gè)切入點(diǎn)可能橫切多個(gè)業(yè)務(wù)組件
- 定義增強(qiáng)處理,增強(qiáng)處理就是在AOP框架為普通業(yè)務(wù)組件織入的處理動(dòng)作
首先我們定義一個(gè)接口:它只完成增加用戶的功能。
public interface UserDao { public void add(User user); }其次,我們定義一個(gè)接口實(shí)現(xiàn)類:它實(shí)現(xiàn)了用戶的添加功能。
@Component("u") public class UserDaoImpl implements UserDao { @Override public void add(User user) { System.out.println("add user!"); } }然后,定義一個(gè)service類,他會(huì)調(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 的配置文件如下。通過(guò)aop命名空間的<aop:aspectj-autoproxy />聲明自動(dòng)為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>編寫測(cè)試類對(duì)其進(jìn)行測(cè)試:
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框架將自動(dòng)生成AOP代理,即:代理對(duì)象的方法=增強(qiáng)處理+被代理對(duì)象的方法。
4.代碼
本文中所涉及的代碼在github上都有,可以點(diǎn)擊以下鏈接:
GIthub地址
