Spring AOP

目录

1.AOP概述

2.核心概念详解

2.1 切点(Pointcut)

2.2 连接点(Join Point)

2.3 通知(Advice)

2.4 切面

2.5 通知类型

2.6 @PointCut

2.7 切面优先级

2.8 切点表达式

2.8.1 execution表达式

2.8.2 @annotation

2.8.2.1 自定义注解@MyAspect

2.8.2.2 切面类

2.8.2.3 添加自定义注解

3.Spring AOP原理

3.1 代理模式

3.2 静态代理

3.3 动态代理

3.3.1 JDK动态代理

3.3.2 CGLIB动态代理


1.AOP概述

AOP是Spring框架的第二大核心(第一大核心是 IoC)

Aspect Oriented Programming(面向切面编程)

切面就是指某一类特定问题,所以AOP也可以理解为面向特定方法编程.

  • 登录校验拦截器,就 是对"登录校验"这类问题的统一处理.所以,拦截器也是AOP的一种应用.AOP是一种思想,拦截器是AOP思想的一种实现.Spring框架实现了这种思想,提供了拦截器技术的相关接口.

  • 统一数据返回格式和统一异常处理,也是AOP思想的一种实现.

AOP是一种思想,是对某一类事情的集中处理

什么是Spring AOP?

AOP是一种思想,它的实现方法有很多,有SpringAOP,也有AspectJ、CGLIB等.

SpringAOP是其中的一种实现方式.

package com.example.aop.aspect;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Slf4j
@Aspect
@Component
public class TimeRecordAspect {
    @Around("execution(* com.example.aop.aspect.*.*(..))")
    public Object recordTime(ProceedingJoinPoint pjp) throws Throwable {
        //1.记录方法执行开始时间
        long start = System.currentTimeMillis();
        //2.执行原始方法
        Object result = pjp.proceed();
        //3.记录方法执行结束时间
        long end = System.currentTimeMillis();
        //4.记录方法执行耗时
        log.info(pjp.getSignature() + "执行耗时:{}ms", end - start);
        return result;
    }
}
  1. @Aspect:标识这是一个切面类

  2. @Around:环绕通知,在目标方法的前后都会被执行.后面的表达式表示对哪些方法进行增强.

  3. ProceedingJoinPoint.proceed() 让原始方法执行

2.核心概念详解

2.1 切点(Pointcut)

切点(Pointcut),也称之为"切入点"

Pointcut的作用就是提供一组规则(使用AspectJpointcut expression language来描述),告诉程序对哪些方法来进行功能增强.

execution(* com.example.aop.aspect.*.*(..))

就是切点表达式

2.2 连接点(Join Point)

满足切点表达式规则的方法,就是连接点,也就是可以被AOP控制的方法

所有com.example.aop.aspect 路径下的方法,都是连接点.

切点和连接点的关系:连接点是满足切点表达式的元素,切点可以看做是保存了众多连接点的一个集合。

2.3 通知(Advice)

通知就是具体要做的工作,指哪些重复的逻辑,也就是共性功能(最终体现为一个方法)

比如上述程序中记录业务方法的耗时时间,就是通知。

在AOP面向切面编程当中,我们把这部分重复的代码逻辑抽取出来单独定义,这部分代码就是通知的内容.

2.4 切面

切面(Aspect) =切点(Pointcut) +通知(Advice)

通过切面就能够描述当前AOP程序需要针对于哪些方法,在什么时候执行什么样的操作。

切面既包含了通知逻辑的定义,也包括了连接点的定义.

切面所在的类,我们一般称为切面类(被@Aspect注解标识的类)

2.5 通知类型

Spring中AOP的通知类型有以下几种:

  • @Around:环绕通知,此注解标注的通知方法在目标方法前,后都被执行

  • @Before:前置通知,此注解标注的通知方法在目标方法前被执行

  • @After:后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行

  • @AfterReturning:返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行

  • @AfterThrowing:异常后通知,此注解标注的通知方法发生异常后执行

package com.example.aop.aspect;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Slf4j
@Aspect
@Component
public class AspectDemo {
    //前置通知
    @Before("execution(* com.example.aop.controller.*.*(..))")
    public void before() {
        log.info("执行before方法");
    }

    //后置通知
    @After("execution(* com.example.aop.controller.*.*(..))")
    public void after() {
        log.info("执行after方法");
    }
    //返回后通知
    @AfterReturning("execution(* com.example.aop.controller.*.*(..))")
    public void afterReturning() {
        log.info("执行afterReturning方法");
    }

    //抛出异常后通知
    @AfterThrowing("execution(* com.example.aop.controller.*.*(..))")
    public void afterThrowing() {
        log.info("执行afterThrowing方法");
    }

    //添加环绕通知
    @Around("execution(* com.example.aop.controller.*.*(..))")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        log.info("around方法开始执行");
        Object result = pjp.proceed();
        log.info("around方法结束执行");
        return result;
    }
}
package com.example.aop.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/test")
public class TestController {
    @RequestMapping("t1")
    public String t1() {
        return "t1";
    }

    @RequestMapping("/t2")
    public Boolean t2() {
        int a = 10/0;
        return true;
    }
}

程序正常运行的情况下,@AfterThrowing标识的通知方法不会执行

从上图也可以看出来,@Around标识的通知方法包含两部分,一个"前置逻辑"一个"后置逻辑".其

中"前置逻辑"会先于@Before标识的通知方法执行,"后置逻辑"会晚于@After标识的通知方法执行

程序发生异常的情况下:

  • @AfterReturning标识的通知方法不会执行,@AfterThrowing标识的通知方法执行了

  • @Around环绕通知中原始方法调用时有异常,通知中的环绕后的代码逻辑也不会在执行了(因为原始方法调用出异常了)

注意事项:

  • @Around环绕通知需要调用ProceedingJoinPoint.proceed()来让原始方法执行,其他通知不需要考虑目标方法执行.

  • @Around环绕通知方法的返回值,必须指定为Object,来接收原始方法的返回值,否则原始方法执行完毕,是获取不到返回值的.

  • 一个切面类可以有多个切点.

2.6 @PointCut

Spring提供了 @PointCut 注解,把公共的切点 表达式提取出来,需要用到时引用该切入点表达式即可.

@Pointcut("execution(* com.example.aop.controller.*.*(..))")
public void pointCut() {}
//前置通知
@Before("pointCut()")
public void before() {
    log.info("执行before方法");
}

当切点定义使用private修饰时,仅能在当前切面类中使用,当其他切面类也要使用当前切点定义时,就需要把private改为public.引用方式为:全限定类名.方法名()

@Slf4j
@Aspect
@Component
public class AspectDemo2 {
    @Before("com.example.aop.aspect.AspectDemo.pointCut()")
    public void before() {
        log.info("执⾏ AspectDemo2 -> Before ⽅法");
    }
}

2.7 切面优先级

@Aspect
@Component
@Order(2)
public class AspectDemo2 {
    //...代码省略 
}

存在多个切面类时,默认按照切面类的类名字母排序:

@Before通知:字母排名靠前的先执行

@After通知:字母排名靠前的后执行

Spring给我们提供了一个新的注解,来控制这些切面通知的执行顺序:@Order

@Order注解标识的切面类,执行顺序如下:

  • @Before通知:数字越小先执行

  • @After通知:数字越大先执行

@Order控制切面的优先级,先执行优先级较高的切面,再执行优先级较低的切面,最终执行目标方法.

2.8 切点表达式

切点表达式常见有两种表达方式

1.execution(.....):根据方法的签名来匹配

2.@annotation(....):根据注解匹配

2.8.1 execution表达式

execution()是最常用的切点表达式,用来匹配方法,语法为:

execution(<访问修饰符> <返回类型> <包名.类名.方法(方法参数)> <异常>)

其中:访问修饰符和异常可以省略

切点表达式支持通配符表达:

  1. *:匹配任意字符,只匹配一个元素(返回类型,包,类名,方法或者方法参数)

    1. 包名使用*表示任意包(一层包使用一个*)

    2. 类名使用*表示任意类

    3. 返回值使用表示任意返回值类型

    4. 方法名使用表示任意方法

    5. 参数使用表示一个任意类型的参数

  2. ..:匹配多个连续的任意符号,可以通配任意层级的包,或任意类型,任意个数的参数

    1. 使用··配置包名,标识此包以及此包下的所有子包

    2. 可以使用·.配置参数,任意个任意类型的参数

切点表达式实例

TestController下的public修饰,返回类型为String方法名为t1,无参方法

execution(public String com.example.demo.controller.TestController.t1()) 

省略访问修饰符

execution(String com.example.demo.controller.TestController.t1())

匹配所有返回类型

execution(* com.example.demo.controller.TestController.t1())

匹配TestController下的所有无参方法

execution(* com.example.demo.controller.TestController.*())

匹配TestController下的所有方法

execution(* com.example.demo.controller.TestController.*(..))

匹配controller包下所有的类的所有方法

execution(* com.example.demo.controller.*.*(..))

匹配所有包下面的TestController

execution(* com..TestController.*(..))

匹配com.example.demo包下,子孙包下的所有类的所有⽅法

execution(* com.example.demo..*(..))

2.8.2 @annotation

2.8.2.1 自定义注解@MyAspect

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)

public @interface MyAspect {
}

2.8.2.2 切面类

使用@annotation切点表达式定义切点,只对@MyAspect生效

@Aspect
@Component
@Slf4j
public class MyAspectDemo {
    @Before("@annotation(com.example.aop.aspect.MyAspect)")
    public void before() {
        log.info("MyAspect -> before ...");
    }
    @After("@annotation(com.example.aop.aspect.MyAspect)")
    public void after(){
        log.info("MyAspect -> after ...");
    }

}
2.8.2.3 添加自定义注解
@MyAspect
@RequestMapping("t1")
public String t1() {
    return "t1";
}

@MyAspect
@RequestMapping("u1")
public String u1() {
    return "u1";
}

@Around("@annotation(org.springframework.web.bind.annotation.RequestMapping)")

执行所有标注为@RequestMapping注解的方法,执行当前环绕通知逻辑

3.Spring AOP原理

SpringAOP是基于动态代理来实现AOP的,咱们学习内容主要分以下两部分

  1. 代理模式

  2. SpringAOP源码剖析

3.1 代理模式

代理模式,也叫委托模式

定义:为其他对象提供一种代理以控制对这个对象的访问.它的作用就是通过提供一个代理类,让我们在调用目标方法的时候,不再是直接对目标方法进行调用,而是通过代理类间接调用

在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用。

使用代理前:

使用代理后:

房屋中介:房屋进行租赁时,卖方会把房屋授权给中介,由中介来代理看房,房屋咨询等服务

代理模式的主要角色

  1. Subject:业务接口类.可以是抽象类或者接口(不一定有)

  2. RealSubject:业务实现类.具体的业务执行,也就是被代理对象.

  3. Proxy:代理类. RealSubject的代理.

Subject就是提前定义了房东做的事情,交给中介代理,也是中介要做的事情

RealSubject:房东

Proxy:中介

UML类图:

代理模式可以在不修改被代理对象的基础上,通过扩展代理类,进行一些功能的附加与增强。

根据代理的创建时期,代理模式分为静态代理和动态代理.

  • 静态代理:由程序员创建代理类或特定工具自动生成源代码再对其编译,在程序运行前代理类的 .class文件就已经存在了.

  • 动态代理:在程序运行时,运用反射机制动态创建而成

3.2 静态代理

静态代理:在程序运行前,代理类的.class文件就已经存在了.(在出租房子之前,中介已经做好了相关的

工作,就等租户来租房子了)

我们通过代码来加深理解,以房租租赁为例

  1. 定义接口(定义房东要做的事情,也是中介需要做的事情)

    //房东
    public interface HouseSubject {
        void rent();
        void sale();
    }

  2. 实现接口(房屋出租房子)

    //房东要做的事
    public class RealHouseSubject implements HouseSubject {
        @Override
        public void rent() {
            System.out.println("我是房东.我出租房子");
        }
    
        @Override
        public void sale() {
            System.out.println("我是房东,我出售房子");
        }
    }

  3. 代理(中介,帮房东出租房子)

    //中介代理房东
    public class HouseProxy implements HouseSubject{
        private RealHouseSubject subject;
    
        public HouseProxy(RealHouseSubject subject) {
            this.subject = subject;
        }
    
        @Override
        public void rent() {
            System.out.println("我是中介, 我帮房东开始代理");
            subject.rent();
            System.out.println("我是中介, 我帮房东结束代理");
        }
    
        @Override
        public void sale() {
            System.out.println("我是中介, 我帮房东开始代理");
            subject.sale();
            System.out.println("我是中介, 我帮房东结束代理");
        }
    }

  4. 使用

    public class Main {
        public static void main(String[] args) {
            //通过代理访问目标方法
            HouseProxy proxy = new HouseProxy(new RealHouseSubject());
            proxy.rent();
        }
    }

    从上述代码可以看出,我们修改接口(Subject)和业务实现类(RealSubject)时,还需要修改代理类(Proxy).

    同样的,如果有新增接口(Subject)和业务实现类(RealSubject),也需要对每一个业务实现类新增代理类(Proxy).

3.3 动态代理

相比于静态代理来说,动态代理更加灵活

我们不需要针对每个目标对象都单独创建一个代理对象,而是把这个创建代理对象的工作推迟到程序运

行时由JVM来实现,也就是说动态代理在程序运行时,根据需要动态创建生成。

比如房屋中介,我不需要提前预测都有哪些业务,而是业务来了我再根据情况创建。

Java也对动态代理进行了实现,并给我们提供了一些API,常见的实现方式有两种:

  1. JDK动态代理

  2. CGLIB动态代理

3.3.1 JDK动态代理

JDK动态代理类实现步骤

  1. 定义一个接口及其实现类(静态代理中的HouseSubjectRealHouseSubject

  2. 自定义InvocationHandler并重写invoke方法,在invoke方法中我们会调用目标方法(被代理类的方法)并自定义一些处理逻辑

  3. 通过Proxy.newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)方法创建代理对象

实现JDK动态代理类

实现InvocationHandler接口

invoke 方法是代理逻辑的执行入口,当代理对象调用任何方法(如 rent()sale())时,JVM 会自动将调用转发到该方法

public class JDKInvocation implements InvocationHandler {

    //目标对象 / 被代理对象
    private Object target;
    public JDKInvocation(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //before
        System.out.println("开始代理");
        //通过反射执行目标对象的目标方法
        Object result = method.invoke(target, args);
        //after
        System.out.println("结束代理");
        return result;
    }
}
public class Main {
    public static void main(String[] args) {
        //JDK动态代理
        //中介代理房东
        HouseSubject target = new RealHouseSubject();
        //运行时创建代理对象
        HouseSubject proxy = (HouseSubject) Proxy.newProxyInstance(target.getClass().getClassLoader(),
                new Class[]{HouseSubject.class},new JDKInvocation(target));
        /*HouseSubject proxy = (HouseSubject) Proxy.newProxyInstance(target.getClass().getClassLoader(),
                target.getClass().getInterfaces(), new JDKInvocation(target));*/
        proxy.rent();
        proxy.sale();
    }
}
HouseSubject proxy = (HouseSubject) Proxy.newProxyInstance(
    target.getClass().getClassLoader(),  // 类加载器:通常使用目标对象的类加载器
    new Class[]{HouseSubject.class},     // 目标接口数组:代理对象需要实现的接口(JDK动态代理必须基于接口)
    new JDKInvocation(target)            // 代理逻辑处理器:封装代理增强逻辑
);

用同一个包

  1. InvocationHandler InvocationHandler接口是Java动态代理的关键接口之一,它定义了一个单一方法invoke() ,用于处理被代理对象的方法调用.

    public interface InvocationHandler {
        /**
         * 参数说明 
         * proxy:代理对象 
         * method:代理对象需要实现的方法,即其中需要重写的方法 
         * args:method所对应方法的参数 
         */
        public Object invoke(Object proxy, Method method, Object[] args)
                throws Throwable;
    }

    通过实现InvocationHandler接口,可以对被代理对象的方法进行功能增强.

  2. Proxy
    Proxy类中使用频率最高的方法是:newProxyInstance(),这个方法主要用来生成一个代理对象

    public static Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces, InvocationHandler h)
            throws IllegalArgumentException
    {
        //...代码省略
    }

这个方法一共有3个参数:

Loader:类加载器,用于加载代理对象.

interfaces:被代理类实现的一些接口(这个参数的定义,也决定了JDK动态代理只能代理实现了接口的一些类)

h:实现了InvocationHandler接口的对象

3.3.2 CGLIB动态代理

JDK动态代理有一个最致命的问题是其只能代理实现了接口的类

有些场景下,我们的业务代码是直接实现的,并没有接口定义,为了解决这个问题,我们可以用CGLIB动态代理机制来解决.

CGLIB(CodeGenerationLibrary)是一个基于ASM的字节码生成库,它允许我们在运行时对字节码进行修改和动态生成.CGLIB通过继承方式实现代理,很多知名的开源框架都使用到了CGLIB.例如Spring中的AOP模块中:如果目标对象实现了接口,则默认采用JDK动态代理,否则采用CGLIB动态代理

CGLIB动态代理类实现步骤

  1. 定义一个类(被代理类)

  2. 自定义MethodInterceptor并重写intercept方法,intercept用于增强目标方法,和JDK动态代理中的invoke方法类似

  3. 通过Enhancer类的 create()创建代理类

添加依赖:

和JDK动态代理不同,CGLIB(Code GenerationLibrary)实际是属于一个开源项目,如果你要使用它的话,需要手动添加相关依赖

<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>3.3.0</version>
</dependency>

自定义MethodInterceptor(方法拦截器)

实现MethodInterceptor接口

public class CGLibMethodInterceptor implements MethodInterceptor {
    private Object target;
    public CGLibMethodInterceptor(Object target) {
        this.target = target;
    }
    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        //before
        System.out.println("开始代理");
        //通过反射执行目标对象的目标方法
        Object result = method.invoke(target, objects);
        //after
        System.out.println("结束代理");
        return result;
    }
}
//运行时, 动态创建代理对象
//如果使用CGlib运行的话, 需要添加vm option
//--add-opens java.base/java.lang=ALL-UNNAMED
HouseSubject target = new RealHouseSubject();
HouseSubject proxy = (HouseSubject) Enhancer.create(target.getClass(), new CGLibMethodInterceptor(target));
proxy.rent();

###Spring AOP 的概念 AOP(Aspect-Oriented Programming)即面向切面编程,是一种编程范式,旨在通过分离横切关注点来提高模块化程度。在 Spring 框架中,AOP 被广泛用于实现诸如日志记录、事务管理、安全性等通用功能,这些功能通常与业务逻辑无关但又需要在多个地方重复使用。 Spring AOP 主要是基于 AspectJ 实现的,尽管 AspectJ 是一个独立的 AOP 框架,并不是 Spring 的组成部分,但它通常与 Spring 一起使用以提供更强大的 AOP 功能[^1]。Spring AOP 支持两种方式来定义切面:基于 XML 配置文件的方式和基于注解的方式。 ###Spring AOP 的原理 Spring AOP 使用运行时代理来实现 AOP 功能,这意味着它会在运行时动态生成代理对象。对于实现了接口的类,Spring AOP 默认使用 JDK 动态代理;而对于没有实现接口的类,则会使用 CGLIB 代理[^4]。这种方式允许在不修改原始代码的情况下向程序中添加新的行为。 织入(Weaving)是将增强(advice)应用到目标对象的过程,Spring AOP 在运行时进行织入操作[^3]。当创建了代理对象后,所有对目标对象方法的调用都会被拦截,并且可以插入额外的操作,比如在方法执行前后做一些处理。 ###Spring AOP 的使用教程 要开始使用 Spring AOP,首先需要确保项目中包含了必要的依赖。如果使用 Maven 构建工具,可以在 `pom.xml` 文件中加入如下依赖: ```xml <!-- 引入aop依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> ``` 一旦添加了依赖并刷新了 Maven 项目,就可以开始编写切面了。下面是一个简单的例子,展示如何使用注解来定义一个切面: ```java import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.springframework.stereotype.Component; @Aspect @Component public class LoggingAspect { @Before("execution(* com.example.service.*.*(..))") public void logBefore(JoinPoint joinPoint) { System.out.println("Method " + joinPoint.getSignature().getName() + " is called."); } } ``` 在这个示例中,`LoggingAspect` 类被标记为 `@Aspect` 和 `@Component` 注解,这样 Spring 就能识别这是一个切面组件。`@Before` 注解指定了在哪些方法上应用前置通知(before advice),这里的表达式表示匹配 `com.example.service` 包下所有的方法。 ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值