Spring AOP 入门教程

Spring AOP 入门教程

2021-6-17·devcxl
devcxl

Spring AOP(面向切面编程)是 Spring 框架的一个核心模块,用于实现面向切面编程。

它通过在运行时动态地将额外的功能(如日志、事务管理等)织入到应用程序的代码中,以提供更好的模块化、更清晰的代码和更好的可维护性。

核心概念

  1. 切面(Aspect): 切面是一个模块化的单元,用于横切关注点(cross-cutting concerns)的实现。例如,日志、事务管理等功能可以作为切面。

  2. 连接点(Join Point): 连接点是在应用程序执行过程中可以插入切面的点,比如方法的调用或异常的处理。

  3. 通知(Advice): 通知是在连接点上执行的动作。Spring AOP 提供了多种类型的通知,包括前置通知(Before advice)、后置通知(After advice)、环绕通知(Around advice)、异常通知(After-throwing advice)和返回通知(After-returning advice)。

  4. 切点(Pointcut): 切点是指在应用程序中定义的连接点的集合。通知会在切点上执行。

  5. 引入(Introduction): 引入允许向现有的类添加新方法或属性。在 Spring AOP 中,引入允许为现有的类添加新接口。

  6. 目标对象(Target Object): 目标对象是应用程序中的真实对象,在其上应用通知。

  7. 代理(Proxy): 代理是创建的对象,它包含了通知应用到的目标对象。

  8. 织入(Weaving): 织入是将切面与目标对象结合起来创建新的代理对象的过程。它可以在编译时、类加载时或运行时进行。

Spring AOP 通过将这些概念结合起来,允许开发者更方便地管理和实现横切关注点,提高了代码的模块化程度和可维护性。

使用指南

首先,你得在pom.xml中添加 AOP 依赖。

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

然后使用@Aspect注解创建一个切面类

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

@Aspect
@Component
public class LoggingAspect {
    // todo
}

切面定义好后,我们需要在代码中定义一个切入点,切入点的定义可以通过@Pointcut注解定义

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

@Aspect
@Component
public class LoggingAspect {
    /**
     * `@Pointcut`注解定义切入点 该切点定义在LoginVerification子类中的verify方法被调用时
     */
    @Pointcut("execution(* com.example.demo.service.LoginVerification+.verify(..))")
    private void pointcut() {
    }
}

也可以利用自定义注解来标记方法或类,并在切面中使用这些注解作为切入点。

import java.lang.annotation.*;
// 定义一个自定义注解`@Loggable`
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Loggable {
    
}

@Service
public class MyService {
    //在要想应用切面的方法上使用 `@Loggable` 注解
    @Loggable
    public void performTask() {
        // Method logic
    }
}

@Aspect
@Component
public class LoggingAspect {
    //在切面中使用这个自定义注解作为切入点
    @Pointcut("@annotation(com.example.Loggable)")
    private void pointcut() {
    }
}

根据业务需要定义通知

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class LoggingAspect {

    /**
     * `@Pointcut`注解定义切入点 该切点定义在LoginVerification子类中的verify方法被调用时
     */
    @Pointcut("execution(* com.example.demo.service.LoginVerification+.verify(..))")
    private void pointcut() {
    }

    /**
     * 前置通知
     *
     * @param joinPoint
     */
    @Before("pointcut()")
    public void beforeAdvice(JoinPoint joinPoint) {
        System.out.println("Before method execution")
    }

    /**
     * 环绕通知
     */
    @Around("pointcut()")
    public void around() {
        System.out.println("Around method execution")
    }

    /**
     * 后置通知
     */
    @After("pointcut()")
    public void afterMethodExecution() {
        System.out.println("After method execution")
    }

    /**
     * 返回通知
     *
     * @param result
     */
    @AfterReturning(pointcut = "pointcut()", returning = "result")
    public void afterMethodExecutionReturn(Object result) {
        System.out.println("After method execution: result = " + result);
    }

    /**
     * 异常通知
     *
     * @param ex
     */
    @AfterThrowing(pointcut = "pointcut()", throwing = "ex")
    public void afterMethodExecution(Exception ex) {
        System.out.println("After method execution: Exception caught: " + ex.getMessage());
    }
}

为了使 AOP 起作用,需要在 Spring Boot 应用程序的主类中添加@EnableAspectJAutoProxy注解。

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

@SpringBootApplication
@EnableAspectJAutoProxy
public class MyApplication {

    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }
}

现在,你已经完成了 AOP 的设置。可以在你的业务代码中调用com.example.demo.service.LoginVerification子类中的verify方法,然后观察控制台中的输出。查看AOP的效果

SpringAOP中代理对象创建方式

在 Spring AOP 中,代理对象的创建可以使用两种主要方式:JDK 动态代理和 CGLIB(Code Generation Library)代理。它们之间有一些区别:

  1. JDK 动态代理:

    • 基于接口: JDK 动态代理要求目标类实现至少一个接口。它利用 Java 标准库中的 java.lang.reflect.Proxy 类来创建代理对象。
    • 运行时创建: 在运行时,通过反射机制动态地创建代理对象。代理对象实现了目标类所实现的接口,并重定向方法调用到通知逻辑(Advice)。
    • 适用范围: 适用于那些实现了接口的类,对于没有实现接口的类无法进行代理。
    • 优势: JDK 动态代理是 Java 标准库提供的功能,遵循标准,更易于使用,同时不会生成过多的代理类。
  2. CGLIB 代理:

    • 基于继承: CGLIB 通过继承目标类来创建代理对象,不要求目标类实现接口。
    • 运行时生成字节码: 在运行时,CGLIB 利用字节码生成库在内存中动态生成目标类的子类作为代理对象。这个子类覆盖了目标类中的非 final 方法,将通知逻辑织入其中。
    • 适用范围: 对于没有实现接口的类,也能够进行代理。
    • 优势: 能够代理没有实现接口的类,更加灵活,但因为涉及字节码生成,可能对性能有轻微影响。

在 Spring AOP 中,默认情况下:

  • 当目标对象实现了至少一个接口时,Spring AOP 会使用 JDK 动态代理。
  • 当目标对象没有实现接口时,Spring AOP 会使用 CGLIB 代理。

JDK动态代理实现代码

package com.example.demo;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

// 接口
interface UserService {
    void addUser(String name);

    void delUser(String name);
}
// 实现类
class UserServiceImpl implements UserService {
    @Override
    public void addUser(String name) {
        System.out.printf("添加用户:%s", name);
    }

    @Override
    public void delUser(String name) {
        System.out.printf("删除用户:%s", name);
    }
}
// 切面类
class UserLogger {
    public void before() {
        System.out.println("---------------操作前");
    }

    public void after() {
        System.out.println("---------------操作完成");
    }

    public void afterThrowing() {
        System.out.println("---------------操作异常");
    }

    public void afterReturning() {
        System.out.println("---------------操作结束");
    }
}

// JDK动态代理的实现
class JDKProxy implements InvocationHandler {
    private Object targetObject; //目标对象

    public Object createProxyInstance(Object targetObject) {
        this.targetObject = targetObject;
        return Proxy.newProxyInstance(targetObject.getClass().getClassLoader(),
                targetObject.getClass().getInterfaces(),
                this);
    }

    public Object invoke(Object target, Method method, Object[] arg) throws Throwable {
        UserLogger userLogger = new UserLogger();//切面类
        try {
            //前置通知
            userLogger.before();
            // 调用目标方法
            Object obj = method.invoke(targetObject, arg);// 切入点
            userLogger.after();//后置通知
            return obj;
        } catch (RuntimeException e) {
            e.printStackTrace();
            userLogger.afterThrowing();//异常通知
        } finally {
            userLogger.afterReturning();//最终通知
        }
        return null;
    }
}

// 示例
public class Main {
    public static void main(String[] args) {
        JDKProxy proxy = new JDKProxy();
        UserService userService = (UserService) proxy.createProxyInstance(new UserServiceImpl());
        userService.addUser("a");
        userService.delUser("a");
    }
}

参考文档