笔记-Java源码阅读-SpringAop

1. 简介

1.1 定义

Spring提供了两种AOP(Aspect Oriented Program 面向切面编程)的实现,基于注解式配置和基于XML配置;AOP编程主要有两种:

  1. Spring AOP :基于运行时的动态代理实现的功能增强;编译快,运行慢;
  2. AspectJ :基于编译时的静态代理实现的功能增强;编译慢,运行快;

当切面较少时,二者性能相差并不大,如果切面太多的话,AspectJ要快得多;

1.2 目的

功能区分:核心业务与周边功能;

核心业务:最为核心的业务逻辑,关键数据的增删改查;

周边功能:辅助性质的业务逻辑,比如性能统计、日志记录、事务管理等;

AOP的目的:将周边功能封装起来,减少重复代码,降低模块耦合度,增强可扩展性;在不改变原始设计的前提下进行功能的增强;

1.3 概念

  1. 连接点(join point):核心业务逻辑的方法;
  2. 切入点(Pointcut):被选中的连接点,被增强的业务方法;
  3. 通知(Advice):伴随切入点执行的操作,内部由周围功能的代码逻辑组成;
    1. 前置通知(Before Advice):在目标方法被调用前调用通知功能;
    2. 后置通知(After Advice):在目标方法被调用后调用通知功能;
    3. 返回通知(After-returning):在目标方法成功执行(未抛异常,有返回值)后调用通知;
    4. 异常通知(After-throwing):在目标方法抛出异常后调用通知;
    5. 环绕通知(Around):在方法执行前和执行后都调用一次通知;
  4. 切面(Aspect):切面是特殊的类,由通知和切入点组成,作用是定义切点所执行的通知;
  5. 织入(Weaving):根据切面创建出代理类的过程;
  6. 切入点表达式:指明被增强的方法的位置;

1.4 原理

Spring Aop 是通过动态代理的方式实现的,同时采用了jdk动态代理和cglib动态代理这两种代理模式:

  1. jdk动态代理:采用反射实现,只能对实现接口的类生成代理,加载速度快、执行效率低;
  2. cglib动态代理:采用asm实现,直接操作字节生成代理类,加载速度慢,执行效率高;

AspectJ 是通过静态代理的方式实现的:

  1. 在程序编译阶段生成代理类,并加载进内存中;
  2. 相对于SpringAop 而言,AspectJ生成的静态代理类在编译阶段会耗费较长的时间,但是在运行阶段会更快;
  3. AspectJ的运用场景相对于SpringAop是有局限性的;

1.5 场景

AOP主要用于,在无代码入侵的场景下实现一些扩展功能,比如:

  1. 日志打印;
  2. 消息发送;
  3. 自定义注解的捕获及处理;

2. 源码解读

在aop编程中,我们常用的 @Aspect、@Pointcut、@Before之类的注解,其实并不在spring-aop包内,而是在aspectjweaver包内,这些注解的包位置为:org.aspectj.lang.annotation ;但我们并不需要专门引入这个包,在maven中引入spring-boot-starter-aop 这个包即可,因为它整合Spring AOP 和 AspectJ。

2.1 AspectJ框架

AspectJ是一个基于Java语言的AOP框架,Spring2.0 之后,Spring AOP引入了对AspectJ的支持,也就是现在SpringAop中,基于注解形式的实现方式。AspectJ支持两种实现AOP的方式:XML声明和注解。

2.1.1 简单例子

一个简单切面编程例子如下:

定义一个业务服务接口:

/**
* 业务服务接口
*/
public interface BusinessService {
    String functionA(String arg);
    String functionB(String arg);
    String functionC(String arg);
    String functionD(String arg);
    String functionE(String arg);
}

顶一个上述业务服务接口的实现类:

@Component
public class BusinessServiceImpl implements BusinessService {
    @Override
    public String functionA(String arg) {
        System.out.println("run function A");
        return "A";
    }

    @Override
    public String functionB(String arg) {
        System.out.println("run function B");
        return "B";
    }

    @Override
    public String functionC(String arg) {
        throw new RuntimeException();
    }

    @Override
    public String functionD(String arg) {
        System.out.println("run function B");
        return "D";
    }

    @Override
    public String functionE(String arg) {
        System.out.println("run function B");
        return "E";
    }
}

定义个切面:

@Aspect
@Component
public class MyAspect {
    // 切点定义
    @Pointcut("execution(* example.yang.yu.app.service.impl.BusinessServiceImpl.*(..))")
    private void pointCut() {}

    // 前置通知
    @Before("pointCut()")
    public void before() {
        System.out.println("执行:Before 通知");
    }

    @After("pointCut()")
    public void after() {
        System.out.println("执行:After 通知");
    }

    @Around("pointCut()")
    public Object around(ProceedingJoinPoint proceedingJoinPoint) {
        System.out.println("执行:Around 通知开始");
        try {
            Object proceed = proceedingJoinPoint.proceed();
            System.out.println("执行:Around 通知结束");
            return proceed;
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
    }

    @AfterReturning("pointCut()")
    public void afterReturning() {
        System.out.println("执行:afterReturning 通知");
    }

    @AfterThrowing("pointCut()")
    public void afterThrowing() {
        System.out.println("执行:afterThrowing 通知");
    }
}

需要注意的是:

  1. @Around 注解修饰的环绕通知是特殊的:

    1. 必须接收一个ProceedingJoinPoint类型参数,表示被代理的目标方法;
    2. 必须返回一个Object对象;
    3. 若存在其他通知,则其他通知会伴随着ProceedingJoinPointer的proceed() 方法执行,环绕通知代码里若不执行该方法,则其他通知也不会被执行;
  2. @AfterReturning注解的通知和@AfterThrowing注解的通知互斥,同时只能有一个被触发;

  3. @Aspect 注解自身并不提供容器注入支持,但切面生效的前提就是被注入容器中,因此需要添加例如 @Component 之类的注解,使切面被容器收集;

2.1.2 切点表达式

切点表达式既可以直接内置在通知注解中,也可以写在独立注解 @PointCut 中,效果相同,使用如下:

@Aspect
@Component
public class MyAspect {
    
    // 1. 切点表达式内置于通知注解
    @Before("excution(public void indi.example.target.BusinessServiceImpl.run())")
    public void doBefore() {
        
    }
    
    // 2. 切点表达式独立于 @PointCut 注解
    @PointCut("excution(public void indi.example.target.BusinessServiceImpl.run())")
    private void pt(){}
    
    @After("pt()")
    public void doAfter() {
        
    }
}

需要注意的是,访问控制为public的@PointCut注解的方法,是一个公共的切点,也就是说,可以在其他切面调用,也就是切点的复用。

表达式格式:

excution([修饰符] [返回值类型] 包名.类名.方法名(参数列表))
/**
* 访问修饰符可以省略
* 返回值类型、类名、包名可以用 * 表示通配符
* 参数列表可以用两个英文点号 .. 表示通配符
* 包名和类名之间的一个点当前包下的类,两个代表当前包及其子包下的类
*/
execution(void indi.example.target.BusinessServiceImpl.run())
execution(* indi.example.*.BusinessServiceImpl.*())
// 匹配任意个、任意类型的参数
execution(* indi.example.*.BusinessServiceImpl.*(..))
// 匹配一个、任意类型的参数
execution(* indi.example.*.BusinessServiceImpl.*(*))
execution(* indi.example.*.BusinessServiceImpl.*(*, String args))
// 包名和类名之间的两个点代表当前包及其子包下的类
execution(* indi.example..*.BusinessServiceImpl.*(*, String args))

切点表达式规则可以总结为:

  1. 返回值类型、类名、包名可以用 * 表示通配符;
  2. 参数列表可以用两个英文点号 … 表示通配符;
  3. 包名和类名之间的一个点代表当前包下的类,两个代表当前包及其子包下的类;
  4. 访问修饰符可以省略;

2.2 SpringAop框架

不同于AspectJ 框架,spring-aop 框架采用的是动态代理方式,实现的切面编程,底层采用CGLIB和JDK动态代理实现。由于AspectJ中用于定义AOP的API非常好,直观易用,所以Spring引入了AspectJ中的部分注解,帮助自身在获取Advisor阶段生产Advisor,后面的代理生成与代理增强与AspectJ无关。

2.2.2 核心接口

  1. Advisor: 一个接口,代表被拦截方法需要增强的逻辑,即切面,Advisor有许多子接口:

    1. PointcutAdvisor: 切点模式的切面,最常用的Advisor;
  2. MethodBeforeAdvice: 前置通知;

  3. AfterReturningAdvice : 后置通知;

  4. MethodInterceptor :环绕通知;

  5. ThrowsAdvice : 异常通知;

  • 17
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值