編織夢想:SpringBoot AOP 教程與自定義日志切面完整實戰(zhàn)

什么是 AOP

AOP 是指通過預編譯方式和運行期動態(tài)代理的方式,在不修改源代碼的情況下對程序進行功能增強的一種技術。AOP 不是面向?qū)ο缶幊蹋∣OP)的替代品,而是 OOP 的補充和擴展。它是一個新的維度,用來表達橫切問題,并提供一個聲明式的處理方案。AOP 是 Spring 框架中的一個重要特性。

AOP 的使用場景

AOP 的使用場景一般是在某些縱向邏輯和多個相對獨立的橫向邏輯中,將橫向邏輯進行抽象和封裝,使得橫向邏輯不再與縱向邏輯混雜在一起,使得應用程序更加易于維護和擴展。在實際開發(fā)中,AOP 的使用場景比較廣泛,例如:

  • 日志記錄:在應用程序中,可以通過 AOP 對方法調(diào)用進行攔截,在方法調(diào)用前后記錄日志信息。
  • 安全處理:通過 AOP 實現(xiàn)安全方案,例如在應用程序中對某些敏感方法添加權限驗證。
  • 性能監(jiān)控:對應用程序進行性能監(jiān)控,實現(xiàn)性能分析和調(diào)優(yōu)。
  • 事物管理:通過 AOP 對事物進行管理,例如實現(xiàn)事物的回滾和提交。
  • 緩存管理:對應用程序進行緩存管理,例如在讀寫操作中進行緩存。

AOP 的核心概念

在學習 AOP 的過程中,有一些核心概念是相當重要的。

  1. 連接點(JointPoint)

連接點是程序中可能被攔截的方法。在 AOP 中,連接點是指所有被攔截到的方法。連接點包含兩個信息:一個是方法的位置信息,另一個是方法的名稱。

  1. 切點(Pointcut)

切點是一組連接點的集合,是要被攔截的連接點。在 Spring AOP 中,切點采用 AspectJ 的切點表達式進行描述,格式如 @Pointcut("execution(public * com.example.demo.controller.*.*(..))")。

  1. 通知(Advice)

通知是指攔截到連接點后要執(zhí)行的代碼,包括 @Before、@AfterReturning@AfterThrowing、@After@Around 五種類型。

  1. 切面(Aspect)

切面是一個包含通知和切點的對象,主要用來維護切點和通知之間的關系。

  1. 織入(Weaving)

織入是將切面應用到目標對象來創(chuàng)建新的代理對象的過程。在 Spring AOP 中,織入可以在編譯時、類加載時和運行時進行。

AOP 的實現(xiàn)方式

在 SpringBoot 中,AOP 的實現(xiàn)方式主要有兩種:Java 代理(JDK Proxy)和字節(jié)碼增強(CGLIB)。

  1. Java 代理(JDK Proxy)

Java 代理是一種基于接口的代理,通過實現(xiàn) Java 動態(tài)代理接口 InvocationHandler 來實現(xiàn)對代理類方法的調(diào)用。Java 代理只能代理實現(xiàn)了接口的類,在運行時通過生成代理類的方式來實現(xiàn)。Java 代理的優(yōu)點是操作簡單,劣勢是只能代理接口。

  1. 字節(jié)碼增強(CGLIB)

字節(jié)碼增強是一種基于繼承的代理,通過生成代理類來完成對目標對象方法的調(diào)用。CGLIB 代理不需要實現(xiàn)接口,對目標對象進行繼承并重寫其中的方法,從而實現(xiàn)對方法的調(diào)用攔截。字節(jié)碼增強的優(yōu)點是能夠代理非接口的類,劣勢是需要引入 CGLIB 依賴包。

SpringBoot AOP 的例子

我們創(chuàng)建一個日志切面來記錄調(diào)用方法開始時間、結束時間、持續(xù)時間等(方法名、參數(shù)、返回值......)。

pom.xml引入以下依賴包

<properties>
        <java.version>1.8</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <spring-boot.version>2.3.7.RELEASE</spring-boot.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
            <version>${spring-boot.version}</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>${spring-boot.version}</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
            <version>${spring-boot.version}</version>
        </dependency>
    </dependencies>

定義SysLog日志注解

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface SysLog {
    String name() default "";
}

定義切面 LogAspect

@Aspect
public class LogAspect {

    // 定義需要被攔截的切點
    @Pointcut("execution(public * com.example.demo.controller.*.*(..))")
    public void pointcut() {
    }

    // 在方法執(zhí)行時進行通知
    @Around("pointcut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        SysLog sysLog = method.getAnnotation(SysLog.class);
        if (sysLog != null) {
            System.out.println("日志名稱:" + sysLog.name());
        }
        System.out.println("開始時間:" + LocalDateTime.now());
        System.out.println("執(zhí)行方法 " + joinPoint.getSignature().getName() + " 前置環(huán)繞通知");
        Object o = joinPoint.proceed();
        System.out.println("結束時間:" + LocalDateTime.now());
        System.out.println("執(zhí)行方法 " + joinPoint.getSignature().getName() + " 后置環(huán)繞通知");
        return o;
    }

    // 在方法執(zhí)行之前進行通知
    @Before("pointcut()")
    public void before(JoinPoint joinPoint) {
        System.out.println("執(zhí)行方法 " + joinPoint.getSignature().getName() + " 前置通知");
    }

    // 在方法執(zhí)行之后進行通知
    @After("pointcut()")
    public void after(JoinPoint joinPoint) {
        System.out.println("執(zhí)行方法 " + joinPoint.getSignature().getName() + " 后置通知");
    }

    // 在方法執(zhí)行之后返回結果時進行通知
    @AfterReturning(returning = "result", pointcut = "pointcut()")
    public void afterReturning(JoinPoint joinPoint, Object result) {
        System.out.println("執(zhí)行方法 " + joinPoint.getSignature().getName() + " 返回通知,返回值:" + result);
    }

    // 在方法拋出異常時進行通知
    @AfterThrowing(throwing = "ex", pointcut = "pointcut()")
    public void afterThrowing(JoinPoint joinPoint, Exception ex) {
        System.err.println("執(zhí)行方法 " + joinPoint.getSignature().getName() + " 異常通知,異常:" + ex.getMessage());
    }
}


注意:如下必須注入LogAspect Bean。

@Configuration
public class MyConfiguration {
    @Bean
    public LogAspect logAspect() {
        return new LogAspect();
    }
}

測試,在Controller方法上添加@SysLog(name = "這是一個日志名稱")注解。

@RestController()
@RequestMapping("user")
public class UserController {

    @SysLog(name = "這是一個日志名稱")
    @GetMapping("test")
    public String test() {
        return "hello world";
    }
}

測試:訪問 http://localhost:8080/user/test,打印如下結果:

日志名稱:這是一個日志名稱
開始時間:2023-07-17T16:33:11.935
執(zhí)行方法 test 前置環(huán)繞通知
執(zhí)行方法 test 前置通知
執(zhí)行方法 test 返回通知,返回值:hello world
執(zhí)行方法 test 后置通知
結束時間:2023-07-17T16:33:11.940
執(zhí)行方法 test 后置環(huán)繞通知

總結

以上就是 SpringBoot AOP 的基本用法,通過使用 SpringBoot AOP,我們可以在不修改源代碼的情況下對程序進行功能增強,實現(xiàn)對方法的攔截、日志記錄、權限驗證、性能監(jiān)控等功能。

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

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

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