Java AOP的理解与使用

一、基本概念

AOP(Aspect-Oriented Programming):即面向切面编程,这是一种编程范式,旨在提高模块化程度,特别是处理那些跨越多个类的功能,比如日志记录、事务管理等。AOP允许程序员定义“切面”,这些切面可以看作是横切关注点(如安全、日志、事务)的模块化组件,它们可以在不改变业务逻辑代码的情况下,被应用到多个不同的业务场景中。

基础知识

OOP(Object-Oriented Programming):即面向对象编程,它也是一种编程范式,使用“对象”来设计应用程序和计算机程序,专注于将功能划分为对象和类,通过封装、继承和多态来管理复杂系统的行为。(OOP主要关注的是将数据和行为封装在一起以创建可重用的代码模块。而AOP则侧重于解决跨多个对象的横向关注点(如日志记录、事务管理等),这些关注点通常被称为“切面”。AOP可以被视为对OOP的一种补充,它通过提供一种机制来处理这些横切关注点,从而减少代码中的重复并提高模块化程度。)

POP(Process-Oriented Programming):即面向过程编程,它也是一种编程范式,基于过程调用的概念,将程序组织成一系列过程或函数,每个过程完成特定的任务。(面向过程编程关注的是顺序执行的过程或步骤,不像AOP那样专注于横切关注点的处理。不过AOP可以通过在过程调用之间插入额外的行为来增强面向过程的程序,例如自动添加日志记录或错误处理功能,而无需修改原有的过程代码。)

核心概念

横切关注点:程序中跨越多个模块或类的关注点,需要实现的功能逻辑。(横切关注点是AOP试图解决的根本问题。通过AOP的模块化和切面实现,这些横切逻辑(如日志、安全性、事务管理)从业务代码中剥离出来,降低耦合度。)

AOP代理:在面向切面编程的上下文中,“代理”通常指的是一个对象,这个对象作为其他对象的中间人,可以用来拦截对真实对象的方法调用,并在此过程中添加额外的行为。(AOP框架使用代理机制来实现在不修改原有代码的情况下,动态地为对象添加功能。在Java中,AOP代理是织入逻辑的技术实现方式,分为静态代理(如AspectJ,不仅支持动态代理,还支持静态织入)和动态代理(如Spring AOP的JDK动态代理和CGLIB代理)。JDK动态代理主要适用于接口,通过反射机制创建接口的代理实例;而CGLIB则适用于没有接口的情况,它通过子类化的方式来创建代理。)

织入:指将切面逻辑实际嵌入到目标代码中的过程(织入是AOP执行的关键步骤,将定义的切面和切点结合起来,使横切逻辑在运行时、编译时或类加载时生效。在Spring AOP中,织入主要通过代理实现。代理作为目标对象的包装器,拦截方法调用并在合适的时机插入切面逻辑。)
目标对象:需要增强的原始业务逻辑对象(目标对象是AOP的核心增强对象,AOP框架通过代理方式对其功能进行增强,但不改变目标对象的本身逻辑。)

连接点:程序执行过程中一个具体的点(AOP中,连接点表示切面可以应用的所有潜在位置。)

切面:指横切关注点的抽象实现,是AOP的基本模块,包含需要应用于程序的功能逻辑(如日志记录、事务管理等)(切面是AOP的核心组成部分,代表了AOP实现的具体功能。所有横切关注点都通过切面来实现。AOP的意义就是利用切面将这些逻辑模块化并独立于核心业务逻辑。)

切点:指选择的程序中可以切入横切逻辑的执行点(切点用于定义切面应该作用的位置,是AOP配置的重要部分。通过切点表达式(如Spring中的execution语法),开发者可以精准指定切面的应用范围。切点是从连接点中选取的子集。)

通知:切点处执行的具体操作(通知是切面的具体实现部分,通过在切点位置插入通知代码,实现增强功能。通知的类型通常包括前置通知@Before、后置通知@After、环绕通知@Around、异常通知@AfterThrowing、返回通知@AfterReturning)

二、操作步骤

1、引入Spring AOP依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

spring-boot-starter-aop 包括了实现 AOP 所需的核心库和配置,简化 AOP 的集成过程

2、启用AOP功能

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@SpringBootApplication
@EnableAspectJAutoProxy
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

确保项目中启用了 AOP 功能,这通常通过在主类上添加 @EnableAspectJAutoProxy 注解或切面类上添加@Component实现
(多数场景下不需要显式添加,如果需要启用 CGLIB 代理而不是默认的 JDK 动态代理,可以显式设置@EnableAspectJAutoProxy(proxyTargetClass = true))

3、定义切面类

import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Aspect
@Component
@Order(1)
public class LoggingAspect {
    // 在这里定义切点和通知
}

创建一个类,用 @Aspect 注解标识为切面,并用 @Component 将其交由 Spring 容器管理,在切面类中定义切点和通知以及切面逻辑。@Order注解用于指定切面(Aspect)的优先级。当你有多个切面作用于同一个连接点(例如方法调用)时,@Order注解接受一个整数值作为参数,这个值决定了切面的优先级。数字越小,优先级越高,意味着该切面会先于其他具有较大数字的切面执行。如果两个切面具有相同的@Order值,那么它们之间的执行顺序是不确定的

4、定义切点

import org.aspectj.lang.annotation.Pointcut;

@Aspect
@Component
@Order(1)
public class LoggingAspect {
    // 匹配某包下所有方法
    @Pointcut("execution(* com.example.service..*(..))")
    public void serviceMethods() {}

    // 匹配特定注解
    @Pointcut("@annotation(org.springframework.web.bind.annotation.GetMapping)")
    public void getMappingMethods() {}
}

使用 @Pointcut 注解定义切点,指定横切逻辑的应用范围。切点表达式可以匹配包、类、方法等。

5、定义通知

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;

@Aspect
@Component
@Order(1)
public class LoggingAspect {

    // 定义切点
    @Pointcut("execution(* com.example.service..*(..))")
    public void serviceMethods() {}

    // 前置通知
    @Before("serviceMethods()")
    public void beforeAdvice() {
        System.out.println("Before method execution");
    }

    // 后置通知
    @After("serviceMethods()")
    public void afterAdvice() {
        System.out.println("After method execution");
    }

    // 返回通知
    @AfterReturning(pointcut = "serviceMethods()", returning = "result")
    public void afterReturningAdvice(Object result) {
        System.out.println("Method returned value: " + result);
    }

    // 异常通知
    @AfterThrowing(pointcut = "serviceMethods()", throwing = "exception")
    public void afterThrowingAdvice(Exception exception) {
        System.out.println("Method threw exception: " + exception.getMessage());
    }

    // 环绕通知
    @Around("serviceMethods()")
    public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("Before proceeding the method");
        Object result = joinPoint.proceed(); // 执行目标方法
        System.out.println("After proceeding the method");
        return result;
    }
}

在切点的基础上添加通知逻辑,用 @Before@After@AfterReturning@AfterThrowing@Around 注解实现
(在切点表达式只需要使用一次的情况下,切点表达式也可以简化代码直接写在通知定义中,而不需要单独定义为一个 @Pointcut 方法)

总结

java AOP通过不同形式的代理,将共同的逻辑对指定规则的所有方法进行统一的处理,关键点在于逻辑处理的场景决定了配置规则

三、额外补充

1、静态代理的使用

JDK 动态代理只支持接口类型的代理,不支持对具体类的代理,而CGLIB 代理虽然支持类代理,但需要目标类具有无参构造器,且会有一定的性能开销,而静态代理相比于动态代理则灵活性较低,由于在编译期间就确定了代理行为,这意味着一旦程序编译完成,代理的行为也就固定下来了,无法在运行时动态改变,不过如果性能要求较高,也可以考虑使用 AspectJ 编译时织入。
使用方法:
1、确保已经安装了 AspectJ 编译器 (AJC)
Maven项目中则需要引入依赖

        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjrt</artifactId>
            <version>1.9.19</version>
        </dependency>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjtools</artifactId>
            <version>1.9.19</version>
        </dependency>

2、创建切面类(不需要使用@Component或@EnableAspectJAutoProxy 启用,其他内容相同)
3、POM文件中添加AspectJ Maven插件,来使得切面在编译时期静态织入(注意,如果使用lombok则会编译失败,因为ajc无法识别lombok的注解,需要修改为原始代码再进行编译)

<build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version> <!-- 使用最新版本 -->
                <configuration>
                    <source>1.8</source> <!-- 源代码的Java版本 -->
                    <target>1.8</target> <!-- 目标字节码的Java版本 -->
                    <encoding>UTF-8</encoding> <!-- 源文件编码 -->
                    <showWarnings>true</showWarnings> <!-- 显示警告信息 -->
                    <fork>true</fork> <!-- 在单独的进程中运行编译器 -->
                    <compilerArgs>
                        <arg>-Xlint:all</arg> <!-- 启用所有警告 -->
                    </compilerArgs>
                </configuration>
            </plugin>
            <!-- AspectJ Maven Plugin -->
            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>aspectj-maven-plugin</artifactId>
                <version>1.14.0</version>
                <executions>
                    <execution>
                        <id>main-aspectj-compile</id>
                        <goals>
                            <goal>compile</goal>
                        </goals>
                        <phase>process-classes</phase>
                    </execution>
                    <execution>
                        <id>test-aspectj-compile</id>
                        <goals>
                            <goal>test-compile</goal>
                        </goals>
                        <phase>process-classes</phase>
                    </execution>
                </executions>
                <configuration>
                    <complianceLevel>1.8</complianceLevel>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>UTF-8</encoding>
                    <showWeaveInfo>true</showWeaveInfo> <!-- 添加调试信息 -->
                    <verbose>true</verbose> <!-- 添加调试信息 -->
                </configuration>
            </plugin>
        </plugins>
    </build>

这里先使用本身默认的javac进行编译,确保Java源代码能够按照指定版本被正确编译,然后再使用ajc进行补充编译(选择process-classes阶段,该阶段主要处理编译后的类文件),将切面织入到目标类中
4、重新同步所有Maven项目,并clean install(因为install包含了process-classes阶段,如果单纯只执行compile,则无法织入)

2、基于AOP的Spring事务管理

Spring默认使用的是JDK动态代理或CGLIB代理(如果目标类实现了接口则Spring将使用JDK动态代理。如果配置开启@EnableAspectJAutoProxy(proxyTargetClass = true)则使用CGLIB代理。JDK动态代理是通过Spring创建一个实现了与目标类相同接口的新代理类对象,而CGLIB代理是通过创建目标类的子类作为代理对象来实现的。

问题:由于代理的存在,当你从外部代码调用一个被@Transactional注解的方法时,实际上是调用了代理对象上的方法,而代理对象会在适当的时候启动和结束事务。但是,如果你在一个带有@Transactional注解的方法内部调用另一个同样带有此注解的方法,这不会经过代理对象,而是直接调用了同一个实例中的方法,因此不会触发新的事务行为。
idea会报出警告

解决方式:因此假如从外部调用的方法入口为A,A调用的子方法为B,为了确保事务管理按预期工作,可以仅A加@Transactional,或者A和B都加@Transactional另一种选择是不依赖于@Transactional注解,而是使用TransactionTemplate进行程序化事务管理。这种方式可以让你更灵活地控制事务边界,但代价是代码会变得稍微复杂一些。

另外事务的事务传播行为是在不同的类对象方法调用时由Spring框架所定义的事务管理行为,并不影响上述问题的判断和解决

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值