1. 程式人生 > 程式設計 >Proxy實現AOP切面程式設計案例

Proxy實現AOP切面程式設計案例

通過JDK的Proxy代理實現對業務類做簡單的AOP實現

介面:UserService 包含的方法為切入點,會被代理攔截

類:UserServiceImpl 實現UserService介面

類:UserServiceFactory 工廠模式生成動態代理

類:MyAspect 切面類,實現對切入點的操作

UserService

public interface UserService {
  //切面: 需要被攔截的方法
  public void addUser();
  public void updateUser();
  public int deleteUser(int id);
}

UserServiceImpl

public class UserServiceImpl implements UserService {
  public void add() {
    System.out.println("UserServiceImpl.add()");
  }
 
  public void add(User user) {
    System.out.println("UserServiceImpl.add(" + user + ")");
  }
 
  //下面繼承自UserService介面的方法會被攔截
  @Override
  public void addUser() {
    System.out.println("UserServiceImpl.addUser()");
  }
 
  @Override
  public void updateUser() {
    System.out.println("UserServiceImpl.updateUser()");
  }
 
  @Override
  public int deleteUser(int id) {
    System.out.println("UserServiceImpl.deleteUser(" + id + ")");
    return 1;
  }
}

UserServiceFactory

public class UserServiceFactory {
  public static UserService createUserService() {
    //1、建立目標物件target
    final UserService userService = new UserServiceImpl();
    //2、宣告切面類物件
    final MyAspect myAspect = new MyAspect();
    //3、將切面類before()與after()方法應用到目標類
    //3.1、建立JDK代理(返回一個介面)
    /*
     newProxyInstance(
        ClassLoader loader,//類載入器,寫當前類
        Class<?>[] interfaces,//介面,介面中包含的方法執行時會被攔截
        InvocationHandler h)  //處理 呼叫切面類中的處理如:deforre()、after()
     */
    UserService serviceProxy = (UserService) Proxy.newProxyInstance(
        UserServiceFactory.class.getClassLoader(),userService.getClass().getInterfaces(),new InvocationHandler() {
          @Override
          public Object invoke(Object proxy,Method method,Object[] args) throws Throwable {
            //開啟事務
            myAspect.before();
            
            //返回值是呼叫的業務方法的返回值
            Object obj = method.invoke(userService,args);
 
            //提交事務
            myAspect.after();
            
            return obj;
          }
        });
    return serviceProxy;
  }
}

MyAspect :(就是一些具體操作,如記錄日誌等)

public class MyAspect {
  public void before() {
    System.out.println("MyAspect.before()開啟事務...");
  }
 
  public void after() {
    System.out.println("MyAspect.after()提交事務...");
  }
}

單元測試:

  @Test
  public void aop_test() {
    UserService userService = UserServiceFactory.createUserService();
    userService.addUser();
    userService.deleteUser(10);
    userService.updateUser();
  }

輸出:

MyAspect.before()開啟事務...

UserServiceImpl.addUser()

MyAspect.after()提交事務...

MyAspect.before()開啟事務...

UserServiceImpl.deleteUser(10)

MyAspect.after()提交事務...

MyAspect.before()開啟事務...

UserServiceImpl.updateUser()

MyAspect.after()提交事務...

補充知識:結合動態代理技術學習SpringAop實現切面程式設計

結合一個例子利用動態代理技術和SpringAop實現需求

需求:為我的UserService類中的每一個方法加上一個計時器

最初的實現是為每一個類新增一段程式碼,這樣看起來程式碼的冗餘度特別大

Proxy實現AOP切面程式設計案例

靜態代理實現

在使用JDK提供動態代理之前我們先利用靜態代理技術實現這個需求

Proxy實現AOP切面程式設計案例

靜態代理需要我們自己建立代理類具體程式碼如下:

建立UserService介面以及他的實現類及目標類UserServiceTarget

public interface UserService {
 
  public void insert();
  public void update();
  public void delete();
}
 
// 目標類
public class UserServiceTarget implements UserService {
 
  @Time
  public void insert() {
    System.out.println("插入使用者");
  }
 
  public void update() {
    System.out.println("修改使用者");
  }
 
  public void delete() {
    System.out.println("刪除使用者");
  }
}

建立TimeHandler類,將重複的計時器程式碼邏輯寫入TimeHandler類中

public class TimeHandler {
  private UserServiceTarget userService = new UserServiceTarget();
  //需要加計時器的方法對應的物件 -- method
  public void invoke(Method method) {
    long start = System.nanoTime();
    // 反射呼叫: 方法.invoke(物件,引數);
    try {
      method.invoke(userService);
    } catch (Exception e) {
      e.printStackTrace();
    }
    long end = System.nanoTime();
    Time time = method.getAnnotation(Time.class);
    if(time != null) {
      System.out.println("花費了: " + (end - start));
    }
  }
}

最後一步就是自己實現代理類UserServiceProxy,自己實現代理類被稱作靜態代理

public class UserServiceProxy implements UserService {
 
  public void insert() {
    try {
      TimeHandler timeHandler = new TimeHandler();
      Method a = UserServiceTarget.class.getMethod("insert");
      timeHandler.invoke(a);
    } catch (NoSuchMethodException e) {
      e.printStackTrace();
    }
  }
 
  public void update() {
    try {
      TimeHandler timeHandler = new TimeHandler();
      Method b = UserServiceTarget.class.getMethod("update");
      timeHandler.invoke(b);
    } catch (NoSuchMethodException e) {
      e.printStackTrace();
    }
  }
 
  public void delete() {
    try {
      TimeHandler timeHandler = new TimeHandler();
      Method c = UserServiceTarget.class.getMethod("delete");
      timeHandler.invoke(c);
    } catch (NoSuchMethodException e) {
      e.printStackTrace();
    }
  }
}

這樣在無需改變UserService類和其實現類的情況下增加了程式碼的擴充套件性,降低了程式碼間的耦合度。

動態代理實現

動態代理就是不需要我們自己建立代理類和代理物件,JDK會在程式執行中為我們自動生成代理物件

動態代理的三個步驟

1、生成代理類的位元組碼

2、執行類載入將位元組碼載入進入JVM

3、建立代理類的例項物件

方式一

自己寫程式碼生成需要代理類的位元組碼

1、獲取代理類的位元組碼

byte[] bytes = ProxyGenerator.generateProxyClass("UserServiceProxy",new Class[]{UserService.class});

//這裡第一個引數是自己為代理類起的類名,第二個引數是需要建立代理類的位元組碼陣列

2、執行類載入

 ClassLoader cl = new ClassLoader() {
      @Override
      protected Class<?> findClass(String name) throws ClassNotFoundException {
        return defineClass(name,bytes,bytes.length);
      }
    };
    Class c = cl.loadClass("UserServiceProxy"); // 進行類載入,獲得了 UserServiceProxy 類物件

3、 建立代理類例項物件--通過反射

 // 獲取代理類的構造方法
    Constructor constructor = c.getConstructor(InvocationHandler.class);
 
    UserServiceTarget target = new UserServiceTarget();
    // 建立例項物件,強制轉換為它的介面型別
    UserService proxy = (UserService)constructor.newInstance(new InvocationHandler() {
      @Override
      public Object invoke(Object proxy,Object[] args) throws Throwable {
        long start = System.nanoTime();
        method.invoke(target,args);
        long end = System.nanoTime();
        System.out.println("花費了:" + (end - start));
        return null;
      }
    });

這裡的InvocationHandler介面匿名實現類似於我們之前的TimeHandler類,只需要將重複程式碼邏輯寫入其中在通過方法物件反射呼叫該方法即可實現動態代理。

//使用代理物件

proxy.insert();

方式二

利用Proxy類的newProxyInstance()方法實現動態代理,具體程式碼如下

public static void main(String[] args) {
    // 直接建立代理類的例項
    // 1. 獲取類載入器
    ClassLoader cl = UserService.class.getClassLoader();
    // 2. 規定代理類要實現的介面
    Class[] interfaces = new Class[] {UserService.class};
    // 3. 給一個 InvocationHandler 物件,包含要執行的重複邏輯
    UserServiceTarget target = new UserServiceTarget();
    InvocationHandler h = new InvocationHandler() {
      @Override
      public Object invoke(Object proxy,Object[] args) throws Throwable {
        long start = System.nanoTime();
 
        // 方法.invoke(目標,引數);
        method.invoke(target,args);
 
        long end = System.nanoTime();
        System.out.println("花費了:" + (end - start));
        return null;
      }
    };
    UserService proxy = (UserService) Proxy.newProxyInstance(cl,interfaces,h);
    //4. 使用代理物件
    proxy.update();
  }
}

使用Spring框架AOP(面向切面程式設計)完成需求

Spring框架最最主要的兩大特性就是IOC(控制反轉)和AOP(面向切面程式設計)

IOC總結見我的部落格SpringIOC總結

AOP (aspect oriented programming ) 即面向切面程式設計

切面 aspect = 通知 adivce + 切點 pointcut

通知:是一個方法,其中包含了重複的邏輯(例如我們今天需要實現的計時器需求,以及Spring事務管理的底層實現)

切點:是一種匹配條件,與條件相符合的目標方法,才會應用通知方法,需要配合切點表示式

再來類比一下之前的圖

Proxy實現AOP切面程式設計案例

圖中的UserService就是SpringAOP技術中的代理,TimeHandler就是切面,UserServiceTarget在SpringAOP技術中被稱作目標。

SpringAOP實現

首先需要新增maven依賴

<!--spring核心依賴-->
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-context</artifactId>
  <version>4.3.22.RELEASE</version>
</dependency>
<!--切面相關依賴-->
<dependency>
  <groupId>org.aspectj</groupId>
  <artifactId>aspectjrt</artifactId>
  <version>1.8.13</version>
</dependency>
<dependency>
  <groupId>org.aspectj</groupId>
  <artifactId>aspectjweaver</artifactId>
  <version>1.8.13</version>
</dependency>

第二步,編寫切面類

@Component
//將切面交給spring容器管理
@Aspect
//@Aspect 註解表示該類是一個切面類
//切面 = 通知 + 切點
public class UserAspect {
  //配置切點 @Around註解和切點表示式
  @Around("within(service.impl.*)")
 
  //配置通知方法
  //ProceedingJoinPoint引數用來呼叫目標方法
  public Object time(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
    long start = System.nanoTime();
    Object proceed = proceedingJoinPoint.proceed();//呼叫目標方法返回結果
    long end = System.nanoTime();
    System.out.println("springaop 方法耗時" + (end - start) + "納秒");
    return proceed;
  }
}

UserService和UserServiceImpl程式碼如下

public interface UserService {
  void insert(); 
  void update(); 
  void delete();
}
 
@Service
public class UserServiceImpl implements UserService {
  @Override
  public void insert() {
    System.out.println("UserServiceImpl 增加使用者資訊");
}
 
  @Override
  public void update() {
    System.out.println("UserServiceImpl 修改使用者資訊");
  }
 
  @Override
  public void delete() {
    System.out.println("UserServiceImpl 刪除使用者資訊");
  }
}

最後一步,配置Spring配置檔案

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:aop="http://www.springframework.org/schema/aop"
    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.xsd
    http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
    http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"
>
  <!-- spring容器進行包掃描 配有@Componet @Service @Controller @Repository會交由spring容器管理-->
  <context:component-scan base-package="service,aspect"/>
 
  <!-- 啟用切面程式設計的相關注解,例如: @Aspect,@Around,還提供了自動產生代理類的功能-->
  <aop:aspectj-autoproxy/>
</beans>

編寫測試類

public class TestSpringAopProgramming {
  public static void main(String[] args) {
    ClassPathXmlApplicationContext context =
        new ClassPathXmlApplicationContext("spring.xml");
    UserService userService = context.getBean(UserService.class);
    userService.insert();
    userService.update();
    userService.delete();
  }
}

彩蛋

這個時候上面的需求又發生了變法,不是給UserService中所有的方法加計時器,而是給指定方法加計時器,又該如何實現?

如果我們給需要加計時器的方法加上一個註解,當反射呼叫該方法的時候判斷如果有該註解在通過動態代理的方式為其加計時器不就可以解決問題了。

自定義註解

Proxy實現AOP切面程式設計案例

自定義註解需要新增兩個註解 @Target @Retention

@Target 表示能夠加在哪些位置

ElementType.TYPE 表示能夠加在 類上

ElementType.METHOD 表示能夠加在 方法上

ElementType.FIELD 表示能夠加在 屬性上

@Retention 表示註解的作用範圍

Source 表示註解僅在 *.java 原始碼中有效

Class 表示註解在 *.java 原始碼 和 *.class 位元組碼中有效

Runtime 表示註解在 *.java 原始碼 和 *.class 位元組碼 和 執行期間都中有效

自定義註解類Time

@Target({ ElementType.METHOD } ) //該只需要載入方法上
@Retention(RetentionPolicy.RUNTIME)//需要在原始碼,位元組碼,以及執行中都有效
public @interface Time {
}

這個時候只需要在指定的方法上加@Time註解,然後在代理物件進行判斷即可,示例程式碼如下:

public void insert() {
    try {
      TimeHandler timeHandler = new TimeHandler();
      Method a = UserServiceTarget.class.getMethod("insert");
      //通過getAnnotation()方法判斷是否存在@Time註解
      if(method.getAnnotation(Time.class) !=null) {
        timeHandler.invoke(a);
      }
    } catch (NoSuchMethodException e) {
      e.printStackTrace();
    }
  }

以上這篇Proxy實現AOP切面程式設計案例就是小編分享給大家的全部內容了,希望能給大家一個參考,也希望大家多多支援我們。