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

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

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

//委托類
class Hello implements IHello {
  public void sayHello() {
      System.out.println("Hello world!!");
  }
}
    1. 通過(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;
  }
}
    1. 生成代理對(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地址

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容