理解springAop的正確姿勢

Spring有兩大核心,IOC和AOP。IOC在spring項目中基本無處不在,而AOP則用的基本比較少。 AOP(Aspect Oriented Programming),即面向切面編程。

AOP有一些復(fù)雜的概念:

  • 切面(aspect):用來切插業(yè)務(wù)方法的類。
  • 連接點(joinpoint):是切面類和業(yè)務(wù)類的連接點,其實就是封裝了業(yè)務(wù)方法的一些基本屬性,作為通知的參數(shù)來解析。
  • 通知(advice):在切面類中,聲明對業(yè)務(wù)方法做額外處理的方法。
  • 切入點(pointcut):業(yè)務(wù)類中指定的方法,作為切面切入的點。其實就是指定某個方法作為切面切的地方。
  • 目標對象(target object):被代理對象。
  • AOP代理(aop proxy):代理對象。
    通知:
  • 前置通知(before advice):在切入點之前執(zhí)行。
  • 后置通知(after returning advice):在切入點執(zhí)行完成后,執(zhí)行通知。
  • 環(huán)繞通知(around advice):包圍切入點,調(diào)用方法前后完成自定義行為。
  • 異常通知(after throwing advice):在切入點拋出異常后,執(zhí)行通知。

初學的時候這些概念很難理解,可以先把它理解成一個高級的代理模式:

那么我們首先看一下什么是代理模式

代理模式即它可以在不改變原始類(或叫被代理類)代碼的情況下,通過引入代理類來給原始類附加功能。

代理模式常用在業(yè)務(wù)系統(tǒng)中開發(fā)一些非功能性需求,比如:監(jiān)控、統(tǒng)計、鑒權(quán)、限流、事務(wù)、冪等、日志。我們將這些附加功能與業(yè)務(wù)功能解耦,放到>代理類統(tǒng)一處理,讓程序員只需要關(guān)注業(yè)務(wù)方面的開發(fā)。除此之外,代理模式還可以用在 RPC、緩存等應(yīng)用場景中。

比如有一個類,我們想不改變這個類代碼的情況下統(tǒng)計這個類下面的所有方法執(zhí)行時間。

public class UserController {

    public String login(String username, String password) {
        //do something
        try {
            Thread.sleep(50);
        }catch (Exception e){
            e.printStackTrace();
        }
        System.out.println("執(zhí)行用戶密碼登錄");
        return "登錄成功";
    }

    public String register(String username, String password) {
        //do something
        try {
            Thread.sleep(50);
        }catch (Exception e){
            e.printStackTrace();
        }
        System.out.println("執(zhí)行用戶密碼注冊");
        return "注冊成功";
    }
}
先看一下靜態(tài)代理實現(xiàn)

很簡單,就是創(chuàng)建一個代理類來繼承原始類(還有一種方法:可以通過實現(xiàn)相同的接口來創(chuàng)建代理類)

public class UserControllerProxy extends UserController {


    public String login(String username, String password) {
        long startTimestamp = System.currentTimeMillis();
        //執(zhí)行原始方法
        String res = super.login(username, password);
        long endTimeStamp = System.currentTimeMillis();
        long executeTime = endTimeStamp - startTimestamp;
        System.out.println("登錄方法執(zhí)行時間:" + executeTime);
        return res;
    }


    public String register(String username, String password) {
        long startTimestamp = System.currentTimeMillis();
        //執(zhí)行原始方法
        String res = super.register(username, password);
        long endTimeStamp = System.currentTimeMillis();
        long executeTime = endTimeStamp - startTimestamp;
        System.out.println("注冊方法執(zhí)行時間:" + executeTime);
        return res;
    }

}

執(zhí)行直接用代理類來執(zhí)行

    @Test
    public void test(){
        UserController userControllerProxy = new UserControllerProxy();
        userControllerProxy.login("hello","123456");
    }
//輸出:
//執(zhí)行用戶密碼登錄
//登錄方法執(zhí)行時間:50

這樣的代碼有點問題。一方面,我們需要在代理類中,將原始類中的所有的方法,都重新實現(xiàn)一遍,并且為每個方法都附加相似的代碼邏輯。另一方面,如果要添加的附加功能的類有不止一個,我們需要針對每個類都創(chuàng)建一個代理類

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

動態(tài)代理不需要給每一類創(chuàng)建代理類,只需要一個攔截方法類就可以。java有兩種常用的代理實現(xiàn),一種是基于反射的JDK動態(tài)代理實現(xiàn),一種是基于字節(jié)碼的CGLIB實現(xiàn)。這點主要介紹一些CGLIB的實現(xiàn)。
先引入cglib包

     <dependency>
            <groupId>cglib</groupId>
            <artifactId>cglib</artifactId>
            <version>3.3.0</version>
       </dependency>
       <dependency>
            <groupId>org.ow2.asm</groupId>
            <artifactId>asm</artifactId>
            <version>9.1</version>
       </dependency>

創(chuàng)建CGLIB代理類

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;


public class CGLibProxy implements MethodInterceptor {


    public <T> T getProxy(Class<T> cls) {
        return (T) Enhancer.create(cls, this);

    }

    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        long startTimestamp = System.currentTimeMillis();
        //代理類中的所有方法
        Object result = proxy.invokeSuper(obj, args);
        long endTimeStamp = System.currentTimeMillis();
        long executeTime = endTimeStamp - startTimestamp;
        System.out.println("方法執(zhí)行時間是:" + executeTime);
        return result;
    }
}
    @Test
    public void test1(){
        CGLibProxy cgLibProxy = new CGLibProxy();
        UserController userControllerProxy = cgLibProxy.getProxy(UserController.class);
        userControllerProxy.login("hello","123456");
      //輸出:
      //執(zhí)行用戶密碼登錄
      //方法執(zhí)行時間是:62
    }
再回頭看一下springAOP

aop其實就像代理模式一樣可以幫助我們增強方法,但它比動態(tài)代理方法更為細致??梢詳r截到方法執(zhí)行之前,方法執(zhí)行過程,以及方法執(zhí)行之后,通過這一些面對方法進行增強。
首先我們可以先定義一個注解,用于標識那些方法需要增強
假如我們要做一個業(yè)務(wù)監(jiān)控的功能,這個功能需要和業(yè)務(wù)代碼解耦,用AOP來做就非常適合了。

@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Monitor {

    String value();

}

首先我們先定義一個AOP的切面
切面的作用就是攔截方法的,通過切面可以對方法進行增強。

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

@Component
@Aspect
public class AopAspect {
    
    
    //定義切入點,指定那些方法可以被切面到,這里定義的是 被@Monitor注解定義的方法會被切面
    @Pointcut("@annotation(com.dofun.aopdemo.aop.Monitor)")
    public void pointcut() {
        System.out.println("定義 pointcut");
    }

    
    //定義方法前置攔截,方法在執(zhí)行之前可以做一些事情
    @Before("pointcut()")
    public void before(JoinPoint joinPoint) {
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();
        //獲取方法上的注解
        Monitor monitor = method.getDeclaredAnnotation(Monitor.class);
        if (monitor != null) {
            Object[] paramValues = joinPoint.getArgs();
            String[] paramNames = methodSignature.getParameterNames();
            System.out.print(monitor.value()+"參數(shù):");
            for (int i = 0; i < paramNames.length; i++) {
                System.out.print(paramNames[i] + ":" + paramValues[i]+";");
            }
            System.out.println("");
        }
    }
    //定義方法后置攔截,方法在執(zhí)行之后可以做一些什么事情
    @After("pointcut()")
    public void after(JoinPoint joinPoint) {
        System.out.println(""+joinPoint.getSignature().getName()+"方法結(jié)束。。。發(fā)送事件");
    }

    //定義方法環(huán)繞攔截,可以在方法執(zhí)行過程中做一些什么事情
    @Around("pointcut()")
    public void around(ProceedingJoinPoint joinPoint) {
        long startTimestamp = System.currentTimeMillis();
        try {
            joinPoint.proceed();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        long endTimeStamp = System.currentTimeMillis();
        long executeTime = endTimeStamp - startTimestamp;
        System.out.println("方法執(zhí)行時間是:" + executeTime);
    }
}

添加一個測試方法

@SpringBootTest
public class ProxyTest {

    @Autowired
    private AdminService adminService;

    @Test
    public void test2(){
        adminService.updatePassword("1","張三");
    }

  //修改管理員密碼參數(shù):adminId:1;adminName:張三;
  //執(zhí)行方法本身,do something
  //updatePassword方法結(jié)束。。。發(fā)送事件
  //方法執(zhí)行時間是:13
}

整個AOP實現(xiàn)就完成了。然后我們可以打個斷點看一下,adminService其實已經(jīng)不是我們當初定義那個AdminService類型了,而是一個代理類的對象。


image.png

這其實就是spring AOP的原理了,spring容器在掃描這個類的過程中發(fā)現(xiàn)這個類有被切面“切入”到,就會往容器里面放入代理類而不是原始類。

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

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

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