今天小编来介绍下SpringAOP。

AOP:

Aspect Oriented Programming(⾯向切⾯编程)

面向切面编程它是一种编程范式,旨在通过分离横切关注点来提高软件模块性。

这里的切面代表的是一类特定的问题。

举例:

比如在Controller层某个类中,我想对此类下的方法进行一些功能实现,比如日志记录、方法耗时、权限认证等

那么此时我又不想对该类上的方法进行代码侵入,此时呢,很适合用到AOP思想来做这样的事。

所以简单来说,AOP它就是一种思想,他就是对某一类事情的集中处理。

对于SprinAOP来说,,它是实现了AOP了,所以SpringAOP是一种实现方式,

除了SpringAOP,那么还有AspectJ,CGLIB等,它们也是实现了AOP。

AOP核心概念:

切面(Aspect)、连接点(Join Point)、通知 (Advice)、切入点(Pointcut)

引入(Introduction)、目标对象(Target Object)、AOP代理(Proxy)

这些概念会在代码引入中,进行详细解释。

快速上手:

一:项目准备:

1.创建一个Springboot的项目,创建时,引入相关依赖,比如spring-boot-starter-web、lombok等,按需引入即可

2.pom.xml添加配置:

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

3.创建controller包、aspect包,然后再创建以下这几个类在controller包中

Test1Controller:

import com.nanxi.springaop.aspect.TimeRecord;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/test")
public class Test1Controller {
    @RequestMapping("/t1")
    public String t1(){
        System.out.println("执行t1方法……");
        return "t1返回";
    }
    @RequestMapping("/t2")
    public String t2(){
        System.out.println("执行t2方法……");
        return "t2返回";
    }
    @RequestMapping("/t3")
    public int t3(){
        System.out.println("执行t3方法……");
        int ret=10 / 0;
        return ret;
    }
}

Test2Controller:

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/user")
public class Test2Controller {
    @RequestMapping("/u1")
    public String u1(){
        System.out.println("执行u1方法");
        return "u1返回";
    }
    @RequestMapping("/u2")
    public String u2(){
        System.out.println("执行u2方法");
        return "u2返回";
    }
}

二:使用AOP

1.在aspect包中创建TestRecordAspect类

@Slf4j
@Component
@Aspect
public class TestRecordAspect {
    //方法写好后,要明确告诉给谁用
    @Around("execution(* com.nanxi.springaop.controller.*.*(..))")
    public Object timeRecord(ProceedingJoinPoint point) throws Throwable {
        /**
         * 实现一个计算耗时功能,但对代码不进行侵入,适应AOP切面编程
         * 1.记录开始时间
         * 2.执行目标代码
         * 3.记录结束时间
         * 4.返回结果
         */
        long start=System.currentTimeMillis();
        Object o=null;
        o=point.proceed();
        log.info(point.getSignature()+"耗时:"+(System.currentTimeMillis()-start)+"ms");
        return o;
    }

}

通过访问URL:http://127.0.0.1:8080/user/u1

获取结果如下:

代码详解:

@Aspect:标识这是一个切面类

@Around:环绕通知,在目标方法前后都会执行,里面是一个切面表达式,表达了这个切面类对哪些方法进行增强。

o=point.proceed();:这个是让被增强的方法去执行。

切面:整个TestRecordAspect方法就是切面

切面、连接点、切入点、通知

切点表达式:

execution(* com.nanxi.springaop.controller.*.*(..))

com.nanxi.springaop.controller..*(..)这一坨东西就是切点表达式

常见的有这以下两种:

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

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

先介绍execution类型的表达式: 该表达式是用来匹配方法的,具体语法如下:

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

*与..解释:

*:匹配任意字符,只匹配一个元素(返回类型、包、类名,方法活着方法参数)

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

类名使用*表示任意类

返回值使用*表示任意返回类型

方法名使用*表示任意方法

参数使用*表示一个任意的参数

.. :

使用该符号,代表匹配多个连续的任意符号,可以统配任意层级的包,或任意类型,任意个数的参数

使用..配置包名,标识此包下及其所有子包

使用..配置参数,表示任意个类型的参数

值得注意的是,切点表达式中返回参数是指,可以指定该方法的返回类型是什么,从而达到控制范围。

对于execution表达式而言,它更适合有规则的,那么对于无规则,比如,在Test1Controller下的u1方法

以及UserController下的c1方法下,且这两个方法写上了注解(比如RequestMapping),那么此时使用@annotation表达式去做。

举个例子,我们去增强只带有RequestMapping注解的方法

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

@annotation的内容可以填写自定义注解。

除了这个注解表示式,还有其他注解表达式:

表格 还在加载中,请等待加载完成后再尝试复制

在介绍自定义注解之前,先来介绍Poincut注解,该注解是为了复用里面的切点表达式。

Pointcut:

@Pointcut("execution(* com.nanxi.springaop.controller.*.*(..))")
public void p1(){}
//调用该表达式
@Around("p1()")
public int test(){
……………………
}

自定义注解:

首先定义一个注解类:

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

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface TimeRecord {

}

@Retention(RetentionPolicy.RUNTIME)

这个是说明该注解会在运行时起效 @Target({ElementType.METHOD})

这个是说明该注解对方法起效

实现注解:

注解方法任是实现方法耗时:

@Slf4j
@Component
@Aspect
public class ACTimeRecord {
    //方法写好后,要明确告诉给谁用
    @Around("@annotation(com.nanxi.springaop.aspect.TimeRecord)")
    public Object timeRecord(ProceedingJoinPoint point) throws Throwable {
        /**
         * 实现一个计算耗时功能,但对代码不进行侵入,适应AOP切面编程
         * 1.记录开始时间
         * 2.执行目标代码
         * 3.记录结束时间
         * 4.返回结果
         */
        long start=System.currentTimeMillis();
        Object o=null;
        o=point.proceed();
        log.info(point.getSignature()+"耗时:"+(System.currentTimeMillis()-start)+"ms");
        return o;
    }

}

如何调用:

在需要统计方法上加上@TimeRecord即可

@RestController
@RequestMapping("/test")
public class Test1Controller {
    @TimeRecord
    @RequestMapping("/t1")
    public String t1(){
        System.out.println("执行t1方法……");
        return "t1返回";
    }
    @TimeRecord
    @RequestMapping("/t2")
    public String t2(){
        System.out.println("执行t2方法……");
        return "t2返回";
    }
    @RequestMapping("/t3")
    public int t3(){
        System.out.println("执行t3方法……");
        int ret=10 / 0;
        return ret;
    }
}

通知(Advice)

通知类型如下:

表格 还在加载中,请等待加载完成后再尝试复制

代码演示:

新建一个AspectDemo1在aspect包下

@Component
@Slf4j
@Aspect
public class AspectDemo1 {
    @Pointcut("execution(* com.nanxi.springaop.controller.*.*(..))")
    public void p1(){}

    @Before("p1()")
    public void doBefore(){
        log.info("AspectDemo1 DoBefore……");
    }

    @After("p1()")
    public void doAfter(){
        log.info("AspectDemo1 doAfter……");
    }

    @AfterReturning("p1()")
    public void doAfterReturning(){
        log.info("AspectDemo1 doAfterReturning……");
    }
    @AfterThrowing("p1()")
    public void doAfterThrowing(){
        log.info("AspectDemo1 doAfterThrowing……");
    }
    @Around("p1()")
    public Object doAround(ProceedingJoinPoint point)   {
        log.info("Around前处理");
        Object ret=null;
        try {
            ret=point.proceed();
        } catch (Throwable e) {
            log.error("Around异常处理");
        }
        log.info("Around后处理");
        return ret;
    }
}

访问Test1Controller下的t1方法:http://127.0.0.1:8081/test/t1,访问结果如下:

显然是没有看到doThrowing返回的,这是因为,t1方法中没有出现异常,同时也可以观察到,Around通知级别是最高的。

接下来访问http://127.0.0.1:8081/test/t3(已把端口号修改),该方法会出现报错

此时就会看到这个AspectDemo1 doAfterThrowing……

切面优先级:

准备以下这几个类来测试切面优先级:

@Component
@Slf4j
@Aspect
public class AspectDemo2 {
    @Pointcut("execution(* com.nanxi.springaop.controller.*.*(..))")
    public void p1(){}

    @Before("p1()")
    public void doBefore(){
        log.info("AspectDemo2 DoBefore……");
    }

    @After("p1()")
    public void doAfter(){
        log.info("AspectDemo2 doAfter……");
    }
}
@Component
@Slf4j
@Aspect
public class AspectDemo3 {
    @Pointcut("execution(* com.nanxi.springaop.controller.*.*(..))")
    public void p1(){}

    @Before("p1()")
    public void doBefore(){
        log.info("AspectDemo3 DoBefore……");
    }

    @After("p1()")
    public void doAfter(){
        log.info("AspectDemo3 doAfter……");
    }
}
@Component
@Slf4j
@Aspect
public class AspectDemo4 {
    @Pointcut("execution(* com.nanxi.springaop.controller.*.*(..))")
    public void p1(){}

    @Before("p1()")
    public void doBefore(){
        log.info("AspectDemo4 DoBefore……");
    }

    @After("p1()")
    public void doAfter(){
        log.info("AspectDemo4 doAfter……");
    }
}

访问:http://127.0.0.1:8081/test/t1

由此可以观察出先进来的后出去。

默认优先级是按照包中,类的首字母按字典顺序排列。

但我们也可以设置它的优先级。

使用@Order注解

在AspectDemo4、AspectDemo3、AspectDemo2分别加上@Order(1)、@Order(2)、@Order(3)注解

再次访问URL,得到结果如下:

此时我们发现顺序颠倒过来,由此得出结论,@Order注解中的值,数字越大,级别越低,反之亦然。

上面分享到了SpringAOP的一些应用,那么它是如何实现这个SpringAOP的呢?

SpringAOP原理:

它的实现本质是基于代理模式。而且还是基于动态代理实现的。

代理模式:

它是一种结构性设计模式,它允许你提供一个代替对象(即代理对象),来控制对某个其他对象(即目标对象)的访问,代理模式可以用来执行额外的操作,比如延迟初始化,访问控制,日志记录,而不需要改变原始对象的行为或接口。

代理也分为静态代理和动态代理:

表格 还在加载中,请等待加载完成后再尝试复制

JDK 动态代理

  • 只能对接口进行代理。

  • 优点:JVM 原生支持,性能较好。

  • 缺点:不能代理没有实现接口的类。

CGLIB 代理

  • 可以对类进行代理(不需要接口)。

  • 原理:通过继承目标类并重写方法的方式实现代理。

  • 优点:适用于所有类,包括没有接口的类。

  • 缺点:生成代理类需要更多内存和初始化时间

使用代理前:

使用代理后:

代理有角色划分,

Subject:业务接口类,可以是抽象类

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

Proxy:RealSubject的代理

举个例子:

现实生活中大多数去工作,那么避免不了租房。

那么这个租房就是业务(Subject),那么谁来实现这个业务呢,显然就是房东(RealSubject),毕竟是房东才有房子出租

那么房东此时觉得不想自己来做这些出租工作了,此时就会交给中介(Proxy)来做,此时中介就是代理

静态代理演示:

我们把例子转换成代码:

新建一个proxy包,新建HouseSubject接口、RealSubject类、HouseProxy类、TestProxy类

HouseSubject:

public interface HouseSubject {
    void rent();
    void sale();
}

RealSubject:

public class RealHouseSubject implements HouseSubject{
    @Override
    public void rent() {
        System.out.println("我是房东,有房屋出租");
    }

    @Override
    public void sale() {
        System.out.println("我是房东,有房屋出售");
    }
}

HouseProxy类:

public class HouseProxy implements HouseSubject{
    private RealHouseSubject subject;
    public HouseProxy(RealHouseSubject subject){
        this.subject=subject;
    }

    @Override
    public void sale() {
        System.out.println("我是中介,开始代理房屋出售");
        subject.sale();
        System.out.println("我是中介,结束代理房屋出售");
    }

    @Override
    public void rent() {
        System.out.println("我是中介,开始代理房屋租聘");
        subject.rent();
        System.out.println("我是中介,结束代理房屋出租聘");
    }
}

TestProxy:

public class TestProxy {
    public static void main(String[] args) {
//        静态代理
        HouseProxy proxy=new HouseProxy(new RealHouseSubject());
        proxy.rent();
        proxy.sale();
  }

结果展示:

以上是展示静态代理。

那么接下来演示下动态代理

JDK动态代理:

//使用静态代理,要实现下InvocationHanlder接口
public class JDKProxy implements InvocationHandler {
    //创建一个要代理的对象
    private Object target;
    public JDKProxy(HouseSubject target){
        this.target=target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //before
        System.out.println("JDK动态代理:开始代理");
        //通过反射去执行目标方法
        Object ret = method.invoke(target, args);
        //after
        System.out.println("JDK动态代理:结束代理");
        return ret;
    }
}

invoke方法参数解释:

object proxy:当前被代理的对象(即通过 Proxy.newProxyInstance(...) 创建出来的代理对象本身)。

Metho method:你要调用的目标方法(即客户端调用的那个方法)的 Method 对象。

object[] args:调用目标方法时传入的参数数组

TestProxy:

//方式一:
HouseSubject target=new RealHouseSubject();
HouseSubject proxy= (HouseSubject)Proxy.newProxyInstance(target.getClass().getClassLoader(),
        new Class[]{HouseSubject.class},new JDKProxy(target));
System.out.println(proxy.getClass());
proxy.rent();
proxy.sale();

方式二:
//        HouseSubject target=new RealHouseSubject();
//        HouseSubject proxy= (HouseSubject) Proxy.newProxyInstance(target.getClass().getClassLoader(),
//                target.getClass().getInterfaces(),new JDKProxy(target));
//        System.out.println(proxy.getClass());
//        proxy.rent();
//        proxy.sale();

结果:

代码解释:

Proxy.newProxyInstance:

这是 Java 提供的一个静态方法,用于在运行时动态创建一个代理对象。这个代理对象实现了指定的接口,并将所有方法调用转发给一个 InvocationHandler 对象(也就是你写的 JDKProxy 类)。

1. target.getClass().getClassLoader()

  • 获取目标对象的类加载器;

  • 这是为了让生成的代理类能被正确加载进 JVM。

2. new Class[]{HouseSubject.class}

  • 表示你要为 HouseSubject 接口生成代理;

  • 如果有多个接口,可以传入多个接口类。

3. new JDKProxy(target)

  • 创建了一个 InvocationHandler 实现类的实例;

  • 当代理对象的方法被调用时,就会进入你自定义的 invoke() 方法中。

调用链条:

客户端调用 rent()
     ↓
$Proxy0.rent()
     ↓
invoke(proxy, Method("rent"), null)
     ↓
前置增强:开始代理
     ↓
method.invoke(target, null) → RealHouseSubject.rent()
     ↓
后置增强:结束代理
     ↓
返回结果

CGLIB动态代理

CGLIB(Code Generation Library):

CGLIB(Code Generation Library)是一个强大的、高性能的代码生成库,它广泛应用于Java编程中。与JDK动态代理不同,CGLIB通过继承的方式实现代理对象,即它会生成目标类的子类来覆盖目标类的方法,以此达到增强目的方法的功能。这意味着即使目标对象没有实现任何接口,CGLIB也能为其创建代理对象。

若是使用该库需要引入依赖

引入依赖:

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

新建一个CGLIBProxy类(注意导入的包是来自cglib的)

public class CGLIBProxy implements MethodInterceptor {
    //新建一个对象
    private Object  target;
    public CGLIBProxy(Object target){
        this.target=target;
    }

    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        //before
        System.out.println("CGLIB动态代理:开始代理");
        Object ret = method.invoke(target, objects);
        //after
        System.out.println("CGLIB动态代理:结束代理!");
        return ret;
    }
}

新建一个HouseSubject2:

public class HouseSubject2 {
    public void rent(){
        System.out.println("我是房东,有房屋出租");
    }
    public void sale(){
        System.out.println("我是房东,有房屋出售");
    }
}

TestProxy

HouseSubject2 target=new HouseSubject2();
//运行时创建代理对象
//运行CGLIB要添加vm option
HouseSubject2 proxy=(HouseSubject2) Enhancer.create(target.getClass(), new CGLIBProxy(target));
System.out.println(proxy.getClass());
proxy.rent();

值得注意的是,允许该方法前,需要配置vm option

点击modify options

然后在出现的框上写入:--add-opens java.base/java.lang=ALL-UNNAMED

为什么要加上该参数:

从 Java 9 之后引入了 Jigsaw 模块系统,它默认禁止某些模块内部使用反射访问其他模块的内部类或方法

参数意义拆解:

表格 还在加载中,请等待加载完成后再尝试复制

结果:

调用链图:

客户端调用 rent()
     ↓
HouseSubject2$$EnhancerByCGLIB...rent()
     ↓
intercept(o, Method("rent"), args, methodProxy)
     ↓
前置增强:开始代理
     ↓
method.invoke(target, null) → HouseSubject2.rent()
     ↓
后置增强:结束代理
     ↓
返回结果

那么除了原生带的cglib库,spring它也提供自身的cglib库,其演示效果是一模一样的

那么它们有什么区别呢?

原生cglib与Springcglib相比

表格 还在加载中,请等待加载完成后再尝试复制

那么对于Spring framework以及Spring boot而言,它们是使用cglib还是JDK呢?

那么这里就涉及到一个重要的熟悉,来自代理工厂的proxyTargetClass,通过程序设置其值,是的代理方式变样

Spring framework:

proxyTargetClass

目标对象

代理方式

false

实现了接口

jdk代理

false

未实现接口(只有实现类)

cglib代理

变更设置,该变更设置适合于非Spring boot项目

@Configuration
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class AppConfig {
}

意义:设置为true时,强制对类进行代理(即使目标类实现了接口),使用CGLIB 代理

Springboot2.x:

proxyTargetClass

目标对象

代理方式

true

实现了接口

cglib代理

true

未实现接口(只有实现类)

cglib代理

变更设置:

在application.yml中写入即可

spring:
  aop:
    proxy-target-class: false

文章作者: 南汐
版权声明: 本站所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 www.phblog.cloud
JavaFrame JavaFrame
喜欢就支持一下吧