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

0.前言


本文主要想闡述的問題如下:

  • 什么動態(tài)代理(AOP)以及如何用JDK的Proxy和InvocationHandler實現(xiàn)自己的代理?
  • 什么是Spring動態(tài)代理(AOP)?
  • Spring AOP注解實現(xiàn)

1.動態(tài)代理(AOP)

1.1 AOP

  • 什么是AOP?
    AOP(Aspect Oriented Programming),即面向切面編程,可以說是OOP(Object Oriented Programming,面向?qū)ο缶幊蹋┑难a充和完善。
  • 為什么需要用AOP?
    OOP允許開發(fā)者定義縱向的關(guān)系,但并不適合定義橫向的關(guān)系,例如日志功能。日志代碼往往橫向地散布在所有對象層次中,而與它對應(yīng)的對象的核心功能毫無關(guān)系,在OOP設(shè)計中,它導(dǎo)致了大量代碼的重復(fù),而不利于各個模塊的重用。
    AOP技術(shù)利用一種稱為"橫切"的技術(shù),剖解開封裝的對象內(nèi)部,并將那些影響了多個類的公共行為封裝到一個可重用模塊,并將其命名為"Aspect",即切面。
  • 什么是切面(Aspect)?
    所謂"切面",簡單說就是那些與業(yè)務(wù)無關(guān),卻為業(yè)務(wù)模塊所共同調(diào)用的邏輯或責任封裝起來,便于減少系統(tǒng)的重復(fù)代碼,降低模塊之間的耦合度,并有利于未來的可操作性和可維護性。
  • 使用切面(Aspect)技術(shù)有什么好處?
    使用"橫切"技術(shù),AOP把軟件系統(tǒng)分為兩個部分:核心關(guān)注點和橫切關(guān)注點。業(yè)務(wù)處理的主要流程是核心關(guān)注點,與之關(guān)系不大的部分是橫切關(guān)注點。橫切關(guān)注點的一個特點是,他們經(jīng)常發(fā)生在核心關(guān)注點的多處,而各處基本相似,比如權(quán)限認證、日志、事物。AOP的作用在于分離系統(tǒng)中的各種關(guān)注點,將核心關(guān)注點和橫切關(guān)注點分離開來。

1.2 代理模式

代理模式是AOP的基礎(chǔ),也是常用的java設(shè)計模式,他的特征是代理類與委托類有同樣的接口,代理類主要負責為委托類預(yù)處理消息、過濾消息、把消息轉(zhuǎn)發(fā)給委托類,以及事后處理消息等。
使用代理模式必須要讓代理類和目標類實現(xiàn)相同的接口,客戶端通過代理類來調(diào)用目標方法,代理類會將所有的方法調(diào)用分派到目標對象上反射執(zhí)行,還可以在分派過程中添加"前置通知"和后置處理(如在調(diào)用目標方法前校驗權(quán)限,在調(diào)用完目標方法后打印日志等)等功能。


代理模式

如上圖所示:
1.委托對象和代理對象都共同實現(xiàn)的了同一個接口。
2.委托對象中存在的方法在代理對象中也同樣存在。

代理模式分為兩種:

  • 靜態(tài)代理:代理類是在編譯時就實現(xiàn)好的。也就是說 Java 編譯完成后代理類是一個實際的 class 文件。
  • 動態(tài)代理:代理類是在運行時生成的,也就是說 Java 編譯完之后并沒有實際的 class 文件,而是在運行時動態(tài)生成的類字節(jié)碼,并加載到JVM中。

1.2 靜態(tài)代理實現(xiàn)

//客戶端
public class Client {
  public static void main(String args[]) {
      Target subject = new Target();
      Proxy p = new Proxy(subject);
      p.request();
  }
}
//委托對象和代理對象都共同實現(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 實現(xiàn)動態(tài)代理

Java實現(xiàn)動態(tài)代理的大致步驟如下:

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

//委托類
class Hello implements IHello {
  public void sayHello() {
      System.out.println("Hello world!!");
  }
}
    1. 通過實現(xiàn)InvocationHandler接口來自定義自己的InvocationHandler,指定運行時將生成的代理類需要完成的具體任務(wù)
//自定義InvocationHandler
public class HWInvocationHandler implements InvocationHandler {
  // 目標對象
  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)的目標方法
      Object rs = method.invoke(target, args);
      System.out.println("------插入后置處理代碼-------------");
      return rs;
  }
}
    1. 生成代理對象,這個可以分為四步:
      (1)通過Proxy.getProxyClass獲得動態(tài)代理類
      (2)通過反射機制獲得代理類的構(gòu)造方法,方法簽名為getConstructor(InvocationHandler.class)
      (3)通過構(gòu)造函數(shù)獲得代理對象并將自定義的InvocationHandler實例對象傳為參數(shù)傳入
      (4)通過代理對象調(dià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實例傳入
      IHello iHello = (IHello) constructor.newInstance(new  HWInvocationHandler(new Hello()));
      // 通過代理對象調(dià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ù)是接口(表明你這個代理類需要實現(xiàn)哪些接口),第三個參數(shù)是調(diào)用處理器類實例(指定代理類中具體要干什么)

以上就是對代理類如何生成,代理類方法如何被調(diào)用的分析!在很多框架都使用了動態(tài)代理如Spring,HDFS的RPC調(diào)用等等。

2.Spring動態(tài)代理

2.1 Spring AOP實現(xiàn)的原理

Spring中AOP代理由Spring的IOC容器負責生成、管理,其依賴關(guān)系也由IOC容器負責管理。因此,AOP代理可以直接使用容器中的其它bean實例作為目標,這種關(guān)系可由IOC容器的依賴注入提供。Spirng的AOP的動態(tài)代理實現(xiàn)機制有兩種,分別是:

  • JDK動態(tài)代理:JDK動態(tài)代理是利用反射機制生成一個實現(xiàn)代理接口的匿名類,在調(diào)用具體方法前調(diào)用InvokeHandler來處理。這個在之前已經(jīng)介紹過了。
  • CGLib動態(tài)代理:cglib動態(tài)代理是利用asm開源包,對代理對象類的class文件加載進來,通過修改其字節(jié)碼生成子類來處理。

2.2 如何選擇的使用代理機制

  • 如果目標對象實現(xiàn)了接口,默認情況下會采用JDK的動態(tài)代理實現(xiàn)AOP
  • 如果目標對象實現(xiàn)了接口,可以強制使用CGLIB實現(xiàn)AOP
  • 如果目標對象沒有實現(xiàn)了接口,必須采用CGLIB庫,spring會自動在JDK動態(tài)代理和CGLIB之間轉(zhuǎn)換

2.3 AOP基本概念

在寫Spring AOP之前先簡單介紹下幾個概念:

  • 切面(Aspect) :通知和切入點共同組成了切面,時間、地點和要發(fā)生的“故事”。
  • 連接點(Joinpoint) :程序能夠應(yīng)用通知的一個“時機”,這些“時機”就是連接點,例如方法被調(diào)用時、異常被拋出時等等。
  • 通知(Advice) :通知定義了切面是什么以及何時使用。描述了切面要完成的工作和何時需要執(zhí)行這個工作。
  • 切入點(Pointcut) :通知定義了切面要發(fā)生的“故事”和時間,那么切入點就定義了“故事”發(fā)生的地點,例如某個類或方法的名稱。
  • 目標對象(Target Object) :即被通知的對象。
  • 織入(Weaving):把切面應(yīng)用到目標對象來創(chuàng)建新的代理對象的過程,織入一般發(fā)生在如下幾個時機:
    1)編譯時:當一個類文件被編譯時進行織入,這需要特殊的編譯器才能做到,例如AspectJ的織入編譯器;
    2)類加載時:使用特殊的ClassLoader在目標類被加載到程序之前增強類的字節(jié)代碼;
    3)運行時:切面在運行的某個時刻被織入,SpringAOP就是以這種方式織入切面的,原理是使用了JDK的動態(tài)代理。

AOP通知類型:

  • @Before 前置通知(Before advice) :在某連接點(JoinPoint)之前執(zhí)行的通知,但這個通知不能阻止連接點前的執(zhí)行。
  • @After 后通知(After advice) :當某連接點退出的時候執(zhí)行的通知(不論是正常返回還是異常退出)。
  • @AfterReturning 返回后通知(After return advice) :在某連接點正常完成后執(zhí)行的通知,不包括拋出異常的情況。
  • @Around 環(huán)繞通知(Around advice) :包圍一個連接點的通知,類似Web中Servlet規(guī)范中的Filter的doFilter方法。可以在方法的調(diào)用前后完成自定義的行為,也可以選擇不執(zhí)行。
  • @AfterThrowing 拋出異常后通知(After throwing advice) : 在方法拋出異常退出時執(zhí)行的通知。

3.Spring AOP注解實現(xiàn)

對于AOP編程,我們只需要做三件事:

  • 定義普通業(yè)務(wù)組件
  • 定義切入點,一個切入點可能橫切多個業(yè)務(wù)組件
  • 定義增強處理,增強處理就是在AOP框架為普通業(yè)務(wù)組件織入的處理動作

首先我們定義一個接口:它只完成增加用戶的功能。

public interface UserDao {
  public void add(User user);
}

其次,我們定義一個接口實現(xiàn)類:它實現(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)注點的類:我們這里列舉了各種情況,在方法執(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>

編寫測試類對其進行測試:

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代理實現(xiàn)成功:

around start method
method start
add user!   //add 方法實現(xiàn)的內(nèi)容
around end method
method end
method after returning

所以進行AOP編程的關(guān)鍵就是定義切入點和定義增強處理,一旦定義了合適的切入點和增強處理,AOP框架將自動生成AOP代理,即:代理對象的方法=增強處理+被代理對象的方法。

4.代碼

本文中所涉及的代碼在github上都有,可以點擊以下鏈接:
GIthub地址

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,967評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,273評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,870評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,742評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,527評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,010評論 1 322
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,108評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,250評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,769評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,656評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,853評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,371評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,103評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,472評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,717評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,487評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,815評論 2 372

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