Spring AOP

目录

1.AOP 组成

1.1 切面(Aspect)

1.2 切点(Pointcut)

1.3 通知(Advice)

1.4 连接点(JoinPoit) 

2.Spring AOP 实现步骤

2.1 添加 Spring AOP 依赖

2.2 定义切面和切面

2.3 执行通知

2.3.1 前置通知

2.3.2 前置+后置通知

2.3.3 环绕通知

3.Spring AOP 实现原理——动态代理

3.1 JDK 动态代理

3.2 CGLIB 动态代理

3.3 JDK Proxy VS CGLIB 


AOP(Aspect Oriented Programming):面向切面编程,它是⼀种思想,它是对某⼀类事情的集中处理;

AOP 是⼀种思想,而 Spring AOP 是⼀个框架,提供了⼀种对 AOP 思想的实现,它们的关系和 IoC 与 DI 类似

AOP 实现功能:

  • 用户登陆判断
  • 统一方法执行时间统计
  • 统一日志记录
  • 统一返回格式设置
  • 统一异常处理
  • 事务的开启和提交等等

也就是说使用 AOP 可以扩充多个对象的某个能力,所以 AOP 可以说是 OOP(Object Oriented Programming,⾯向对象编程)的补充和完善。

1.AOP 组成

1.1 切面(Aspect)

定义的是事件(AOP 是做啥的)

切面(Aspect)由切点(Pointcut)和通知(Advice)组成,它既包含了横切逻辑的定义,也包括了连接点的定义;切面是包含了:通知、切点和切面的类,相当于 AOP 实现的某个功能的集合

1.2 切点(Pointcut)

定义具体规则

Pointcut 是匹配 Join Point 的谓词;Pointcut 的作用就是提供⼀组规则(使⽤ AspectJ pointcut expression language 来描述)来匹配 Join Point,给满足规则的 Join Point 添加 Advice

1.3 通知(Advice)

AOP 执行的具体方法

通知:定义了切面是什么,何时使用,其描述了切面要完成的工作,还解决何时执行这个工作的问题。

Spring 切面类中,可以在⽅法上使⽤以下注解,会设置⽅法为通知方法,在满足条件后会通知本方法进行调用:

  • 前置通知 @Before:通知方法会在目标调用之前执行。
  • 后置通知 @After:通知方法会在目标方法返回或者抛出异常后调用
  • 返回之后通知 @AfterReturning:通知方法会目标方法返回之后调用
  • 抛出异常通知 @AfterThrowing:通知方法会在目标方法抛出异常后调用
  • 环绕通知 @Around:通知包裹了被通知的方法,在被通知的方法通知之前和调用之后执行自定义的行为

1.4 连接点(JoinPoit) 

有可能触发切点的所有点

应用执行过程中能够插⼊切面的⼀个点,这个点可以是方法调用时,抛出异常时,甚⾄修改字段 

例如:切面相当于一个用户登录校验(也相当于老板定义公司方向);切点相当于定义用户登录拦截规则,哪些接口判断用户登录权限?哪些不判断(也相当于中层指定具体的方案);通知相当于获取用户登录信息,如果获取到说明已经登陆,否则未登录(也相当于底层具体业务执行者);连接点相当于所有接口(也相当于招生)

1.5 织入(Weaving)

将切面应用到目标对象中的过程,可以在编译时、加载时或运行时进行。

2.Spring AOP 实现步骤

2.1 添加 Spring AOP 依赖

在 pom.xml 添加依赖:

<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-aop -->
<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

2.2 定义切面和切面

package com.example.demo.common;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

@Aspect //定义切面
@Component
public class UserAspect {

    //定义切点
    @Pointcut("Execution(* com.example.demo.controller.UserController.*(..))") //拦截规则
    public void pointcut() { }
}

 拦截代码:

package com.example.demo.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/user")
public class UserController {

    @RequestMapping("/getuser")
    public String getUser() {
        System.out.println("do getUser");
        return "get user";
    }

    @RequestMapping("/deluser")
    public String delUser() {
        System.out.println("do delUser");
        return "del user";
    }
}

其中:pointcut 方法为空方法,它不需要有方法体,此方法名就是起到⼀个“标识”的作⽤,标识下⾯的通知方法具体指的是哪个切点(因为切点可能有很多个

切点表达式(拦截规则):

  • Execution:执行
  • *:表示返回值,匹配任意字符,只匹配一个元素(包、类或方法、方法参数),后面跟空格
  • com.example.demo.controller.UserController:包名+类名
  • .*:表示这个类下边的所有方法
  • ..:参数,匹配任意字符,可以匹配多个元素 ,在表示类时,必须和 * 联合使用。
  • Execution(* com.example.demo.controller.UserController.*(..)):拦截 UserController 里边的所有方法中的所有参数,并且是任意返回值的切点

2.3 执行通知

2.3.1 前置通知

package com.example.demo.common;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
@Aspect //定义切面
@Component
public class UserAspect {

    //定义切点
    @Pointcut("execution(* com.example.demo.controller.UserController.*(..))") //拦截规则
    public void pointcut() { }

    //前置通知
    @Before("pointcut()")
    public void doBefore() {
        System.out.println("执行了前置通知");
    }
}

作为对比,创建一个 ArticleController 类,不做任何拦截:

@RestController
@RequestMapping("/art")
public class ArticleController {
    @RequestMapping("/getart")
    public String getArticle() {
        System.out.println("do getArticle");
        return "getArticle";
    }
}

运行启动类,访问 localhost:8080/art/getart

只是执行了方法本身,通知没有执行

访问 localhost:8080/user/getuser

2.3.2 前置+后置通知

package com.example.demo.common;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
@Aspect //定义切面
@Component
public class UserAspect {

    //定义切点
    @Pointcut("execution(* com.example.demo.controller.UserController.*(..))") //拦截规则
    public void pointcut() { }

    //前置通知
    @Before("pointcut()")
    public void doBefore() {
        System.out.println("执行了前置通知");
    }

    //后置通知
    @After("pointcut()")
    public void doAfter() {
        System.out.println("执行了后置通知");
    }
}

访问 localhost:8080/user/getuser

2.3.3 环绕通知

环绕通知需要传一个固定参数:ProceedingJoinPoint ,并且返回值是 Object 

@Aspect //定义切面
@Component
public class UserAspect {

    //定义切点
    @Pointcut("execution(* com.example.demo.controller.UserController.*(..))") //拦截规则
    public void pointcut() { }

    //前置通知
    @Before("pointcut()")
    public void doBefore() {
        System.out.println("执行了前置通知");
    }

    //后置通知
    @After("pointcut()")
    public void doAfter() {
        System.out.println("执行了后置通知");
    }

    //环绕通知
    @Around("pointcut()")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("环绕通知执行之前");
        //执行目标方法
        Object result = joinPoint.proceed();
        System.out.println("环绕通知执行之后");
        return result;
    }
}

访问 localhost:8080/user/getuser

环绕通知:把整个执行过程放在一个方法中,进行原子性操作

例如统计目标执行的时间,前置和后置通知很难去写,单个统计是ok的,如果是多线程,前置方法和后置方法是非原子性的会出现混乱;但是环绕通知是在一个方法中,通过加锁进行目标执行的时间统计

3.Spring AOP 实现原理——动态代理

Spring AOP 是构建在 动态代理 基础上,因此 Spring 对 AOP 的支持局限于方法级别的拦截

AOP 常见实现技术有以下两种:

  1. 静态代理:静态代理是一种在编译时就已经确定代理关系的代理方式。在静态代理中,代理类和被代理类都要实现同一个接口或继承同一个父类,代理类中包含了被代理类的实例,并在调用被代理类的方法前后执行相应的操作。静态代理的优点是实现简单,易于理解和掌握,但是它的缺点是需要为每个被代理类编写一个代理类,当被代理类的数量增多时,代码量会变得很大。
  2. 动态代理:动态代理是一种在运行时动态生成代理类的代理方式。在动态代理中,代理类不需要实现同一个接口或继承同一个父类,而是通过 Java 反射机制动态生成代理类,并在调用被代理类的方法前后执行相应的操作。动态代理的优点是可以为多个被代理类生成同一个代理类,从而减少了代码量,但是它的缺点是实现相对复杂,需要了解 Java 反射机制和动态生成字节码的技术。

例如:假设一个宿舍有三个人(张三李四王五)同一时间点了同一家外卖,配送到达之后;在没有动态代理,那就是需要自己去取(三个人各自取各自的,需要每人去跑一趟),如果有动态代理,这个时候就可以让赵六(代理)帮忙去取三个人的外卖只需要跑一趟。

在统一的动态代理中,写一个用户拦截方法(例如添加文章、修改方法和删除方法中每个方法都需要写一个用户判断操作),如果有动态代理只需要写一个操作即可

调用者首先来到代理对象中,代理对象会经过一个判定,去代理目标对象实现 AOP


动态代理 使用 JDK ProxyCGLIB 实现

实现了接口的类,使用 AOP 会基于 JDK 生成代理类;没有实现接口的类,会基于 CGLIB 生成代理类(通过实现代理类的子类实现动态代理,被 final 修饰的类是不能被代理)

3.1 JDK 动态代理

JDK 动态代理是一种使用 Java 标准库中的 java.lang.reflect.Proxy 类来实现动态代理的技术。在 JDK 动态代理中,被代理类必须实现一个或多个接口,并通过 InvocationHandler 接口来实现代理类的具体逻辑。

具体来说,当使用 JDK 动态代理时,需要定义一个实现 InvocationHandler 接口的类,并在该类中实现代理类的具体逻辑。然后,通过 Proxy.newProxyInstance() 方法来创建代理类的实例。该方法接受三个参数:类加载器、代理类要实现的接口列表和 InvocationHandler 对象,如下代码所示:

import org.example.demo.service.AliPayService;
import org.example.demo.service.PayService;

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

//动态代理:使用JDK提供的api(InvocationHandler、Proxy实现),此种方式实现,要求被代理类必须实现接口
public class PayServiceJDKInvocationHandler implements InvocationHandler {
    
    //目标对象即就是被代理对象
    private Object target;
    
    public PayServiceJDKInvocationHandler( Object target) {
        this.target = target;
    }
    
    //proxy代理对象
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //1.安全检查
        System.out.println("安全检查");
        //2.记录日志
        System.out.println("记录日志");
        //3.时间统计开始
        System.out.println("记录开始时间");

        //通过反射调用被代理类的方法
        Object retVal = method.invoke(target, args);

        //4.时间统计结束
        System.out.println("记录结束时间");
        return retVal;
    }

    public static void main(String[] args) {

        PayService target=  new AliPayService();
        //方法调用处理器
        InvocationHandler handler = 
            new PayServiceJDKInvocationHandler(target);
        //创建一个代理类:通过被代理类、被代理实现的接口、方法调用处理器来创建
        PayService proxy = (PayService) Proxy.newProxyInstance(
                target.getClass().getClassLoader(),
                new Class[]{PayService.class},
                handler
        );
        proxy.pay();
    }
}

JDK 动态代理的优点是实现简单,易于理解和掌握,但是它的缺点是只能代理实现了接口的类,无法代理没有实现接口的类。

3.2 CGLIB 动态代理

GLIB 动态代理是一种使用 CGLIB 库来实现动态代理的技术。在 CGLIB 动态代理中,代理类不需要实现接口,而是通过继承被代理类来实现代理。 具体来说,当使用 CGLIB 动态代理时,需要定义一个继承被代理类的子类,并在该子类中实现代理类的具体逻辑。然后,通过 Enhancer.create() 方法来创建代理类的实例。该方法接受一个类作为参数,表示要代理的类,如下代码所示:

import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
import org.example.demo.service.AliPayService;
import org.example.demo.service.PayService;

import java.lang.reflect.Method;

public class PayServiceCGLIBInterceptor implements MethodInterceptor {

    //被代理对象
    private Object target;
    
    public PayServiceCGLIBInterceptor(Object target){
        this.target = target;
    }
    
    @Override
    public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        //1.安全检查
        System.out.println("安全检查");
        //2.记录日志
        System.out.println("记录日志");
        //3.时间统计开始
        System.out.println("记录开始时间");

        //通过cglib的代理方法调用
        Object retVal = methodProxy.invoke(target, args);

        //4.时间统计结束
        System.out.println("记录结束时间");
        return retVal;
    }
    
    public static void main(String[] args) {
        PayService target=  new AliPayService();
        PayService proxy= (PayService) Enhancer.create(target.getClass(),new PayServiceCGLIBInterceptor(target));
        proxy.pay();
    }
}

CGLIB 动态代理的优点是可以代理没有实现接口的类,但是它的缺点是实现相对复杂,需要了解 CGLIB 库的使用方法。

3.3 JDK Proxy VS CGLIB 

  • JDK Proxy 来自于 Java;CGLIB 属于三方
  • JDK 动态代理基于接口,要求目标对象实现接口;CGLIB 动态代理基于类,可以代理没有实现接口的目标对象。
  • JDK 动态代理使用 java.lang.reflect.Proxy 和 java.lang.reflect.InvocationHandler 来生成代理对象;CGLIB 动态代理使用 CGLIB 库来生成代理对象。
  • JDK 动态代理生成的代理对象是目标对象的接口实现;CGLIB 动态代理生成的代理对象是目标对象的子类。
  • CGLIB 动态代理无法代理 final 类和 final 方法;JDK 动态代理可以代理任意类
  • 性能不同:JDK 7后 JDK Proxy 性能是略高于 CGLIB;JDK 7 之前 CGLIB 性能高于 JDK Proxy
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

奋斗小温

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值