JDK動態(tài)代理以及Spring AOP使用介紹

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)代理的大致步驟如下:

    1. 定義一個委托類和公共接口
//公共接口
public interface IHello {
  void sayHello();
}

//委托類
class Hello implements IHello {
  public void sayHello() {
      System.out.println("Hello world!!");
  }
}
    1. 通過實(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. 生成代理對象芝薇,這個可以分為四步:
      (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地址

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市篮奄,隨后出現(xiàn)的幾起案子捆愁,更是在濱河造成了極大的恐慌,老刑警劉巖窟却,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件昼丑,死亡現(xiàn)場離奇詭異,居然都是意外死亡夸赫,警方通過查閱死者的電腦和手機(jī)菩帝,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來茬腿,“玉大人呼奢,你說我怎么就攤上這事∽艺茫” “怎么了?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵州袒,是天一觀的道長揭绑。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么他匪? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任菇存,我火速辦了婚禮,結(jié)果婚禮上邦蜜,老公的妹妹穿的比我還像新娘依鸥。我一直安慰自己,他們只是感情好悼沈,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布贱迟。 她就那樣靜靜地躺著,像睡著了一般絮供。 火紅的嫁衣襯著肌膚如雪衣吠。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天壤靶,我揣著相機(jī)與錄音缚俏,去河邊找鬼。 笑死贮乳,一個胖子當(dāng)著我的面吹牛忧换,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播向拆,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼亚茬,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了亲铡?” 一聲冷哼從身側(cè)響起才写,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎奖蔓,沒想到半個月后赞草,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡吆鹤,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年厨疙,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片疑务。...
    茶點(diǎn)故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡沾凄,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出知允,到底是詐尸還是另有隱情撒蟀,我是刑警寧澤,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布温鸽,位于F島的核電站保屯,受9級特大地震影響手负,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜姑尺,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一竟终、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧切蟋,春花似錦统捶、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至镰惦,卻和暖如春迷守,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背旺入。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工兑凿, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人茵瘾。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓礼华,卻偏偏與公主長得像,于是被迫代替她去往敵國和親拗秘。 傳聞我的和親對象是個殘疾皇子圣絮,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評論 2 345

推薦閱讀更多精彩內(nèi)容