AOP篇

1.基本概念

AOP:Aspect Oriented Programming(面向切面编程)

OOP:Object Oriented Programming(面向对象编程)

2.静态代理

基本概念:

定义:代理对象,是目标对象的接口的子类型,代理对象本身并不是目标对象,而是将目标对象作为自己的属性

优点:同一种类型的所有对象都能代理

缺点:范围太小,只能负责部分接口的代理

实现:

首先定义一个包含方法的接口

public interface MathCalculator {
       public int add(int a, int b);
}

然后实现这个接口

public class MathCalculatorImpl implements MathCalculator {
     @Override
    public int add(int a, int b) {
        return a + b;
    }
}

现在定义一个静态代理类,同样实现接口

静态代理类接收目标对象,在重写接口方法时直接调用该对象的方法

public class CalculatorStaticProxy implements MathCalculator {
     private MathCalculator target;
    public CalculatorStaticProxy(MathCalculator target) {
        this.target = target;
    }
​
    @Override
    public int add(int a, int b) {
        int add = this.target.add(a, b);
        System.out.println("[日志]add结果: " + add);
        return add;
    }
}

这样就可以在测试的同时打印日志

3.动态代理

基本概念:

定义:目标对象在执行期间会被动态拦截,插入指定逻辑

优点:可以代理世间万物

缺点:不好写(雾)
动态代理:JDK动态代理强制要求:目标必须有接口

实现:

void test() {
     // 创建真实对象
    MathCalculator mathCalculator = new MathCalculatorImpl();
​
    // 创建动态代理处理器
    /**
     * 当代理对象的方法被调用时,会自动调用此方法。
     *
     * @param proxy  代理对象
     * @param method 被调用的方法
     * @param args   方法的参数
     * @return 方法的返回值
     * @throws Throwable 如果调用方法时发生异常
     */
    InvocationHandler handler = new InvocationHandler() {
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            System.out.println("Before method: " + method.getName());
            // 调用真实对象的方法
            Object result = method.invoke(mathCalculator, args);
            // 打印日志
            System.out.println("Result: " + result);
            System.out.println("After method: " + method.getName());
            return result;
        }
    };
​
    // 创建代理对象
    MathCalculator proxyCalculator = (MathCalculator) Proxy.newProxyInstance(
            mathCalculator.getClass().getClassLoader(),
            mathCalculator.getClass().getInterfaces(),
            handler
    );
​
    // 调用代理对象的方法
    int add = proxyCalculator.add(1, 2);
}

可以使用lamda表达式简化方法,下面是一个工具类,返回proxy

public class DynamicProxy {
    public static Object getProxyInstance(Object target){
        return Proxy.newProxyInstance(
                target.getClass().getClassLoader(),
                target.getClass().getInterfaces(),
                (Proxy, method, args) -> {
                    String name = method.getName();
                    System.out.println("【日志】,【"+name+"】开始 :参数:"+ Arrays.toString(args)+"动态代理执行了");
                    Object result = method.invoke(target, args);
                    return result;
                }
        );

    }


}

4.日志工具类

可以写一个日志工具类,统一日志功能,简化开发

public class LogUtils {
       private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
​
    /**
     * 输出信息级别的日志
     * @param message 日志信息
     */
    public static void info(String message) {
        log("INFO", message);
    }
​
    /**
     * 输出警告级别的日志
     * @param message 日志信息
     */
    public static void warn(String message) {
        log("WARN", message);
    }
​
    /**
     * 输出错误级别的日志
     * @param message 日志信息
     */
    public static void error(String message) {
        log("ERROR", message);
    }
​
    private static void log(String level, String message) {
        String timestamp = LocalDateTime.now().format(FORMATTER);
        System.out.printf("[%s] [%s] %s%n", timestamp, level, message);
    }
}

5.AOP初步实现

5.1.专业术语:

  1. 切面(Aspect):封装横切逻辑的模块,包含通知和切入点。
  2. 连接点(Join Point):程序执行中可插入切面的点,如方法调用、异常抛出。
  3. 切入点(Pointcut):一组连接点的表达式,指定哪些连接点会被增强。
  4. 通知(Advice):切面在连接点执行的代码,有前置、后置、环绕、异常、最终通知。
  5. 目标对象(Target Object):被切面增强的对象。
  6. 代理(Proxy):AOP框架为目标对象生成的包含增强逻辑的对象。
  7. 织入(Weaving):将切面应用到目标对象创建代理的过程,分编译、类加载、运行时织入。

5.2.导入依赖:

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

5.3.切入方法的实现

  1. 创建单独的切面类,@Component,@Aspect
  2. 为四个切面(开始,结束,返回,抛错)定义方法,并加上注释
  3. 匹配切入方法:”execution(返回值类型 方法全签名(参数类型))”
  4. execution中支持使用通配符(*代表省略一个,..代表省略多个)
  5. 方法被切后目标对象就会被包装为代理对象
  6. 流程:前置>>>目标方法>>>返回/异常>>>后置
@Component
@Aspect
public class LogAspect {
​

    @Before("execution(int com.example.demo.calculator.Impl.MathCalculatorImpl.*(int ,int))")
    public void logStart(){
        System.out.println("start");
    }
​
    @After("execution(int com.example.demo.calculator.Impl.MathCalculatorImpl.*(int,int))")
    public void logEnd(){
        System.out.println("end");
    }
​
    @AfterReturning("execution(int com.example.demo.calculator.Impl.MathCalculatorImpl.*(int,int))")
    public void logReturn(){
        System.out.println("return");
    }
​
    @AfterThrowing("execution(int com.example.demo.calculator.Impl.MathCalculatorImpl.*(int,int))")
    public void logThrow(){
        System.out.println("throw");
    }
}

全写法:@注解("execution [public] int [com.lin.spring.aop.calclator.....].add(int,int) [throws ArithmeticException]")
省略写法:@注解("execution int add(int i,int j)")

5.4.匹配方法总结

  1. execution:基于方法签名匹配 语法:execution(修饰符? 返回类型 类路径?方法名(参数) throws 异常?) 示例: execution(public * com.example.service..(..)):匹配com.example.service包下所有类的所有公共方法。 execution(* com.example.dao..(int, int)):匹配com.example.dao包下所有类接收两个int类型参数的方法。
  2. @annotation:基于注解匹配 语法:@annotation(注解全限定名) 示例:@annotation(com.example.annotation.MyAnnotation):匹配带有MyAnnotation注解的方法。
  3. within:基于类或包匹配 语法:within(类或包路径) 示例:within(com.example.service.*):匹配com.example.service包下所有类的所有方法。
  4. this 和 target:基于代理对象和目标对象类型匹配 语法:this(类型全限定名) 或 target(类型全限定名) 示例: this(com.example.service.MyService):匹配代理对象是MyService类型的方法。 target(com.example.service.MyService):匹配目标对象是MyService类型的方法。
  5. args:基于运行时参数类型匹配 语法:args(参数类型列表) 示例: args(int, int):匹配运行时传入两个int类型参数的方法。 execution(* com.example.service..(..)) && args(String, int):匹配com.example.service包下所有类的方法,且运行时传入参数为一个String类型和一个int类型。
  6. 组合表达式 使用&&(与)、||(或)、!(非)组合多个切入点表达式。 示例:execution(* com.example.service..(..)) && @annotation(com.example.annotation.MyAnnotation) && args(int):匹配com.example.service包下所有带有MyAnnotation注解且运行时传入一个int类型参数的方法。

AOP的底层原理
1、Spring会为每个被切面切入的组件创建代理对象(Spring CGLIB 创建的代理对象,无视接口)。
2、代理对象中保存了切面类里面所有通知方法构成的增强器链。
3、目标方法执行时,会先去执行增强器链中拿到需要提前执行的通知方法去执行。

5.5.切入方法参数:

JoinPoint获取连接点

JoinPoint:包装了当前目标方法的所有信息

@Before("execution(..)")
     public void logStart(JoinPoint jp){
    System.out.println("start");
}

通过JoinPoint可以获得方法的各种信息

获得目标方法传来的返回值

 @Before("pointCut()")
    public void logStart(JoinPoint joinPoint){
        //1、拿到方法全签名
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();

        //方法名
        String name = signature.getName();
        //目标方法传来的参数值
        Object[] args = joinPoint.getArgs();

        System.out.println("【切面 - 日志】【"+name+"】开始:参数列表:【"+ Arrays.toString(args) +"】");
    }

Returning指定返回值接收

 @AfterReturning(value = "pointCut()", returning = "result") //returning="result" 获取目标方法返回值
    public void logReturn(JoinPoint joinPoint,Object result){
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();

        String name = signature.getName();

        System.out.println("【切面 - 日志】【"+name+"】返回:值:"+result);
    }

可以指定目标对象接收方法的返回值

throwing指定异常接收

@AfterThrowing(
            value = "pointCut()",
            throwing = "e" //throwing="e" 获取目标方法抛出的异常
    )
    public void logException(JoinPoint joinPoint,Throwable e){
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        String name = signature.getName();

        System.out.println("【切面 - 日志】【"+name+"】异常:错误信息:【"+e.getMessage()+"】");
    }

5.6.@PointCut封装匹配方法

@Pointcut("execution(..)")
public void pppcut() {}

封装好后切入注解可以直接引用

@Before("pppcut()")

5.7.多切面执行顺序

代理对象封装目标方法(前置>>>目标方法>>>返回/异常>>>后置)

代理对象又作为新的目标方法被封装(前置>>>原代理对象>>>返回/异常>>>后置)

@Order(数字)可以指定优先级(不指定为类名首字母,A最前Z最后)

数字越小越先执行,越处于外层

6.环绕通知

作用:上述方法只能感知对象,不能修改对象,环绕通知则可以控制目标方法是否执行,修改目标对象的参数,执行结果

public class aroundAspect {
​
     @Around("execution(..)")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
​
        Object[] args = pjp.getArgs();//获取参数
​
        System.out.println("环绕-前置通知");
        Object result = null;
​
        try {
            result = pjp.proceed(args);//执行目标方法
            System.out.println("环绕-返回通知");
        }
        catch (Throwable e) {
            System.out.println("环绕-异常通知");
            throw e;
        }
        finally {
            System.out.println("环绕-后置通知");
        }
        return result;
    }
}

7.AOP常见应用场景:

  1. 日志记录:在方法前后记录操作信息,便于调试与监控。
  2. 事务管理:统一管理事务的开启、提交和回滚。
  3. 权限验证:执行方法前检查用户权限,保障系统安全。
  4. 性能监控:统计方法执行时间,定位性能瓶颈。
  5. 缓存管理:先查缓存,无结果则执行方法并缓存结果。

PS:Spring也提供了许多工具类
比如:

Spring的三级缓存机制

——————-AOP篇结束——————–

天下繁华,唯有一心
最后更新于 2025-03-17