博客 / 詳情

返回

spring6-代理模式和AOP

jdbcTemplate

jdbcTemplate是spring提供的一個jdbc模板類,是對jdbc的封裝。

當然你也可以使用其他框架融入MyBatis、Hibernate。

GoF之代理模式

代理模式的作用

  1. 當一個對象需要受到保護的時候,可以使用代理對象去完成某個行為。
  2. 需要給某個對象進行功能增強的時候,可以找一個代理進行增強。
  3. A對象和B對象無法直接交互時,也可以使用代理模式來完成。

代理模式中的三個角色:

  1. 目標對象
  2. 代理對象
  3. 目標對象和代理對象的公共接口

如果使用代理模式的話,客户端程序是無法察覺的,客户端在使用代理對象的時候就像在使用目標對象。

代理模式分為靜態代理和動態代理。

靜態代理

目標對象類:

// 目標對象
public class OrderServiceImpl implements OrderService{
    @Override
    public void generateOrder() {
        System.out.println("生成訂單");
    }

    @Override
    public void modifyOrder() {
        System.out.println("修改訂單");
    }

    @Override
    public void detailOrder() {
        System.out.println("查看訂單詳情");

    }
}

代理對象類:

// 代理對象
public class OrderServiceProxy implements OrderService{
    // 代理對象中含有目標對象的引用
    // 這裏使用OrderService類型,因為他耦合度低
    private OrderService orderService;

    // 構造方法傳入目標對象
    public OrderServiceProxy(OrderService orderService) {
        this.orderService = orderService;
    }

    @Override
    public void generateOrder() {
        // 功能增強:統計方法執行時間
        long begin = System.currentTimeMillis();
        orderService.generateOrder();
        long end = System.currentTimeMillis();
        System.out.println("生成訂單耗時:" + (end - begin) + "ms");
    }

    @Override
    public void modifyOrder() {
        long begin = System.currentTimeMillis();
        orderService.modifyOrder();
        long end = System.currentTimeMillis();
        System.out.println("修改訂單耗時:" + (end - begin) + "ms");
    }

    @Override
    public void detailOrder() {
        long begin = System.currentTimeMillis();
        orderService.detailOrder();
        long end = System.currentTimeMillis();
        System.out.println("查看訂單詳情耗時:" + (end - begin) + "ms");
    }
}

公共接口:

// 訂單服務接口
// 目標對象和代理對象的公共接口
public interface OrderService {

    // 生成訂單
    void generateOrder();

    // 修改訂單
    void modifyOrder();

    // 查看訂單詳情
    void detailOrder();
}

測試:

// 實現目標對象方法執行時間的統計
public static void main(String[] args) {
    // 創建目標對象
    OrderService orderService = new OrderServiceImpl();
    // 創建代理對象,同時將目標對象傳入代理對象中
    OrderServiceProxy orderServiceProxy = new OrderServiceProxy(orderService);
    // 通過代理對象調用目標對象的方法
    orderServiceProxy.generateOrder();
    orderServiceProxy.modifyOrder();
    orderServiceProxy.detailOrder();
}

靜態代理優點:1.解決了ocp問題 2.採用代理模式的has a。降低了耦合度。

靜態代理的缺點:假設系統中有上千個接口,每個接口都需要寫代理類,這樣類的數量會急劇膨脹,不好維護。

那怎麼解決類爆炸的問題呢?

採用動態代理。 動態代理還是代理模式,只不過是在內存中為我們動態的生成一個class字節碼,這個字節碼就是代理類。

動態代理

在程序運行階段,在內存中動態生成代理類,成為動態代理。目的是減少代理類的數量。

常見的動態代理技術有:JDK動態代理(只能代理接口)、CGLIB動態代理、Javassist動態代理。

JDK動態代理

公共接口 和目標對象類引用 上面的代碼。Jdk動態代理不需要寫代理類,jdk會在內存中自動生成代理類,因此直接在客户端代碼裏直接調用:

客户端代碼:

public class Client {
    public void main() {
        // 1. 創建目標對象
        OrderService orderService = new OrderServiceImpl();
        // 2. 創建InvocationHandler對象

        // 3. 創建代理對象
        //    1. Proxy.newProxyInstance 的作用是創建代理對象。其實做了兩件事:
        //     1). 在內存中動態的構建一個類,
        //     2). 這個類要實現接口, 這個接口就是目標對象的接口,並且new了一個對象
        //  2. 這個方法的三個參數:
        //    1). ClassLoader: 類加載器, 用於加載內存中的代理對象類. 和目標對象使用相同的類加載器
        //   2). Class[]: 字節碼數組, 代理對象和目標對象實現相同的接口. 用於讓代理對象和目標對象具有相同的方法
        //  3). InvocationHandler: 調用處理器對象,他是一個接口, 這個調用處理器用於編寫增強代碼
        OrderService o = (OrderService)Proxy.newProxyInstance(OrderService.class.getClassLoader(), 			                                                 orderService.getClass().getInterfaces(), 
                            new TimerInvocationHandler(orderService));
        // 4. 通過代理對象調用方法
        // 調用代理對象的代理方法時,如果代理方法的作用是功能增強,那目標對象的目標方法必須執行。
        o.generateOrder();
    }
}

調用處理器類:

/**
 * 負責計時的一個調用處理器類
 * 在這個調用處理器中編寫增強代碼
 * 這個調用處理器只需要一個就好
 */
public class TimerInvocationHandler implements InvocationHandler {

    // 目標對象,就是要被增強的對象
    private Object target;
	// 構造方法傳入目標對象
    public TimerInvocationHandler(Object target) {
            this.target = target;
    }

    // 這個方法必須是invoke()方法,因為jdk在底層會調用這個方法
    // 這個方法什麼時候調用?
    //    什麼時候通過代理對象調用方法的時候就會調用這個invoke()方法
    // 這個方法的參數:
    //    proxy: 代理對象,就是通過Proxy.newProxyInstance()方法創建的代理對象
    //    method: 目標對象的目標方法,這就是要執行的目標方法
    //    args: 目標方法上的參數
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 功能增強:
        Long begin = System.currentTimeMillis();
        Object invoke = method.invoke(target, args);
        Long end = System.currentTimeMillis();
        System.out.println("方法執行耗時:" + (end - begin) + "ms");
        return invoke;
    }
}

CGLIB動態代理

既可以代理接口,也可以代理類。底層採用繼承的方式實現,因此被代理的目標類不能被final修飾

目標類:

// 目標類
public class UserService {
    public boolean login(String username, String password) {
        System.out.println("用户登錄,用户名:" + username + ",密碼:" + password);
        return "admin".equals(username) && "123456".equals(password);
    }

    public void logout(String username) {
        System.out.println("用户退出登錄,用户名:" + username);
    }
}

客户端代碼:

public static void main(String[] args) {
    // 創建字節碼增強對象
    // 這個對象是cglib的核心對象,依靠它來生成代理類
    Enhancer enhancer = new Enhancer();
    // 設置父類,也就是目標類
    enhancer.setSuperclass(UserService.class);
    // 設置回調函數(等同於jdk動態代理的中的調用處理器)
    // 在cglib中是實現方法攔截器MethodInterceptor接口
    enhancer.setCallback(new TimerMethodInterceptor());

    UserService userServiceProxy = (UserService)enhancer.create();
    boolean login = userServiceProxy.login("admin", "123456");
    System.out.println(login?"登錄成功":"登錄失敗");
    userServiceProxy.logout("admin");
}

增強代碼寫在方法攔截器裏面

public class TimerMethodInterceptor implements MethodInterceptor {

    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        Long startTime = System.currentTimeMillis();
        Object o1 = methodProxy.invokeSuper(o, objects);
        Long endTime = System.currentTimeMillis();
        System.out.println("方法 " + method.getName() + " 執行耗時:" + (endTime - startTime) + "ms");
        return o1;
    }
}

注意:在jdk17環境下,cglib動態代理功能啓動時會報錯,需要添加啓動時參數設置:

vm 參數:--add-opens java.base/java.lang=ALL-UNNAMED

program 參數:--add-opens java.base/sun.net.util=ALL-UNNAMED

3-1

面向切面編程AOP

在一個系統中一般會有許多系統服務,如:日誌,事務管理、安全等。這些服務成為交叉業務。

這些交叉業務是通用的。

如果在每一個業務處理過程中,都摻雜這些交叉業務代碼會出現2個問題:

  1. 交叉業務代碼在多個業務中反覆出現。代碼沒有得到複用,修改這些代碼會非常困難。
  2. 開發人員無法專業核心業務代碼,在編寫核心業務代碼時還要處理這些交叉業務代碼。

這就需要使用AOP來解決以上問題。

3-2

總之,AOP就是將與核心業務無關的代碼獨立的抽取出來。形成一個獨立的組件,然後以橫向交叉的方式應用到業務流程當中的過程。

aop底層使用動態代理技術實現。

spring AOP 使用的時jdk動態代理+cglib動態代理。spring 在這2種動態代理種靈活切換。如果是代理接口,則使用jdk動態代理,如果代理某個類,則使用cglib。當然也可以手動配置來強制使用cglib。

AOP的七大術語

連接點Joinpoint

在程序執行流程中,可以織入切面的位置。方法的執行前後,異常拋出之後等位置。

連接點描述的是位置。

切點Pointcut

在程序執行流程中,真正織入切面的方法。(一個切點對應多個連接點)

切點描述的是方法。

通知Advice

通知又叫增強,就是具體你要織入的的代碼

通知包括:前置通知、後置通知、環繞通知、異常通知、最終通知。

通知描述的是代碼。

切面Aspect

切點+通知就是切面。

織入Weaving

把通知應用到目標對象上的過程。

代理對象Proxy

一個目標對象被織入通知後產生的新對象。

目標對象Target

被織入通知的對象。

public void main(String[] args) {
    try {
        // Joint point  連接點
        do1(); // Pointcut 切點
        // Joint point  連接點
        do2();// Pointcut 切點
        // Joint point  連接點
        do3();// Pointcut 切點
        // Joint point  連接點
        do4();// Pointcut 切點
        // Joint point  連接點
        do5();// Pointcut 切點
        // Joint point  連接點
    } catch (Exception e) {
        // Joint point  連接點
    }
}

切點表達式

切點表達式用來定義通知(Advice)往哪些方法上切入

使用Spring的AOP

Spring對AOP的實現包括三種方式:

  1. Spring結合AspectJ框架實現的AOP,基於註解方式
  2. Spring結合AspectJ框架實現的AOP,基於XML方式
  3. spring自己實現的AOP,基於xml配置方式

常用的是前2種方式。

準備工作

​ 引入依賴

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>6.0.4</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
    <version>6.0.4</version>
</dependency>

引入命名空間(context和aop)和配置xml文件

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       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" >

<!--    組件掃描-->
    <context:component-scan base-package="com.ali.service" />
<!--    開啓aspectj自動代理,spring容器在掃描類的時候,會查看類上是否有@Aspect註解
        如果有。就給這個類生成代理對象 。
        proxy-target-class="true" 表示強制使用cglib動態代理
        proxy-target-class="false" 默認值,表示接口使用jdk動態代理,反之使用cglib動態代理-->
    <aop:aspectj-autoproxy proxy-target-class="true"/>
</beans>

編寫目標類

// 目標類
@Service
public class UserService {
    // 目標方法
    public void login() {
        System.out.println("UserService login....");
    }
}

編寫切面類

// 切面類,需要@Aspect 標註
@Aspect
@Component("logAspect")
public class LogAspect {

    // 切面 = 通知+切點
    // 通知就是增強,就是具體要編寫的增強代碼
    // 這裏通知以方法的形式出現。
    // @Before 標註的方法就是一個前置通知
    @Before("execution(* com.ali.service.UserService.*(..))")
    public void advice( ) {
        System.out.println("這是一個前置通知,方法執行前執行....");
    }
    
    // 環繞通知是最大的通知,在前置通知之前,在後置通知之後
    @Around("execution(* com.ali.service.UserService.*(..))")
    public void around(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("around start");
        // 執行目標方法
        joinPoint.proceed();
        System.out.println("around end");
    }
}

測試代碼:

@Test
public void test() {
    ApplicationContext context = new ClassPathXmlApplicationContext("spring.xml");
    UserService userService = context.getBean("userService", UserService.class);
    userService.login();
}

注意:當有多個切面類時,可以使用@Order()註解進行優先級排序,數字越小,優先級越高,就先執行。比如:@Order(2) 比@Order(3) 先執行。

在每個方法上都寫一遍切點表達式很麻煩,可以定義一個通用的切點表達式,然後在方法上使用這個通用的表達式即可。

// 定義通用的切點表達式,後續通知直接使用方法名來引用切點表達式
// 切點就是一個表達式,定義了在哪些連接點上執行通知
@Pointcut("execution(* com.ali.service.UserService.*(..))")
public void commmonPointcut() {
    // 這個方法只是一個標識,方法體不需要編寫任何代碼
}

@AfterReturning("commmonPointcut()")
public void afterAdvice( ) {
    System.out.println("這是一個前置通知,方法執行前執行....");
}

JoinPoint的使用

通知方法可以加入參數JoinPoint,這個是spring容器自動傳入的。

@Before("execution(* com.ali.service.UserService.*(..))")
public void advice(JoinPoint joinPoint) {
    System.out.println("這是一個前置通知,方法執行前執行....");
    // 使用JoinPoint對象獲取連接點的信息
    // joinPoint.getSignature() 獲取連接點的方法簽名
    // 通過方法簽名可以獲取到這個方法的具體信息
    // 獲取方法名
    String methodName = joinPoint.getSignature().getName();
    System.out.println("正在執行的方法是:" + methodName);
}
user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.